packages feed

heni-cli (empty) → 0.1.0.0

raw patch · 21 files changed

+1076/−0 lines, 21 filesdep +aesondep +basedep +bytestring

Dependencies added: aeson, base, bytestring, containers, directory, filepath, heni, process, text, unix

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# 0.1.0.0 — 2026-10-24++## New Features++Strict JSON manifests and check/list/run/report over the shared Heni library.
+ LICENSE view
@@ -0,0 +1,27 @@+BSD 3-Clause License++Copyright (c) 2026, Nadeem Bitar+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this+   list of conditions and the following disclaimer.+2. Redistributions in binary form must reproduce the above copyright notice,+   this list of conditions and the following disclaimer in the documentation+   and/or other materials provided with the distribution.+3. Neither the name of the copyright holder nor the names of its contributors+   may be used to endorse or promote products derived from this software+   without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,10 @@+# heni-cli++The `heni` executable validates manifests, lists cases, runs mutation campaigns+and renders stored reports through the matching Heni library. Version 0.1.0.0 is+unreleased. Use `heni --help` and the [CLI reference](https://github.com/shinzui/heni/blob/master/docs/cli.md).++`run` exits 0 for a met trustworthy contract, 1 for a failed contract, 2 for+configuration/infrastructure failure, and 130 after SIGINT finalization. Generic+commands need exact diagnostic/exit evidence; optional reporters provide named+assertion evidence. Use fresh output directories; report artifacts move together.
+ app/Heni/Cli.hs view
@@ -0,0 +1,179 @@+-- | Command application: parsing and presentation only; the library owns behavior.+module Heni.Cli (mainCli, selectDefinitions, exitCodeFor) where++import Control.Exception+import Control.Monad (forM_, unless)+import Data.Aeson hiding (Options)+import Data.ByteString.Lazy.Char8 qualified as LBS+import Data.Map.Strict qualified as Map+import Data.Set qualified as Set+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Version (showVersion)+import Paths_heni_cli (version)+import System.Environment (getArgs)+import System.Exit+import System.FilePath ((</>))+import System.IO+import Test.Heni+import Text.Read (readMaybe)++data Options = Options !Bool !String ![Text] ![Text] !(Maybe FilePath) !Int !Bool++defaults :: Options+defaults = Options False "auto" [] [] Nothing 0 False++mainCli :: IO ()+mainCli = do+  args <- getArgs+  case args of+    ["--version"] -> putStrLn ("heni " ++ showVersion version)+    [] -> putStrLn help >> exitWith (ExitFailure 2)+    _ | "--help" `elem` args -> putStrLn help+    command : path : rest | command `elem` ["check", "list", "run", "report"] -> case parseOptions command defaults rest of+      Left why -> hPutStrLn stderr why >> exitWith (ExitFailure 2)+      Right options -> do+        attempted <- try (dispatch command path options)+        case attempted of+          Right code -> exitCode code+          Left (e :: SomeException) -> case fromException e :: Maybe AsyncException of+            Just UserInterrupt -> hPutStrLn stderr "Interrupted; inspect the partial report or recover the journal." >> exitWith (ExitFailure 130)+            Just ThreadKilled -> exitWith (ExitFailure 130)+            _ -> hPutStrLn stderr (diagnostic e) >> exitWith (ExitFailure 2)+    _ -> hPutStrLn stderr "Expected check, list, run, or report and a path. Use --help." >> exitWith (ExitFailure 2)++parseOptions :: String -> Options -> [String] -> Either String Options+parseOptions _ options [] = Right options+parseOptions command (Options _ color cases tags output jobs recover) ("--json" : rest) = parseOptions command (Options True color cases tags output jobs recover) rest+parseOptions command (Options json _ cases tags output jobs recover) ("--color" : color : rest)+  | color `elem` ["auto", "always", "never"] = parseOptions command (Options json color cases tags output jobs recover) rest+parseOptions "run" (Options json color cases tags output jobs recover) ("--case" : ident : rest) = parseOptions "run" (Options json color (cases ++ [Text.pack ident]) tags output jobs recover) rest+parseOptions "run" (Options json color cases tags output jobs recover) ("--tag" : tag : rest) = parseOptions "run" (Options json color cases (tags ++ [Text.pack tag]) output jobs recover) rest+parseOptions "run" (Options json color cases tags _ jobs recover) ("--output" : path : rest) = parseOptions "run" (Options json color cases tags (Just path) jobs recover) rest+parseOptions "run" (Options json color cases tags output _ recover) ("--jobs" : value : rest) = case readMaybe value of+  Just jobs | jobs > 0 && jobs <= 256 -> parseOptions "run" (Options json color cases tags output jobs recover) rest+  _ -> Left "--jobs must be between 1 and 256"+parseOptions "report" (Options json color cases tags output jobs _) ("--recover" : rest) = parseOptions "report" (Options json color cases tags output jobs True) rest+parseOptions _ _ (unknown : _) = Left ("Unknown, incomplete, or unsupported option: " ++ unknown)++dispatch :: String -> FilePath -> Options -> IO Int+dispatch "report" path (Options json color _ _ _ _ recover) = do+  stored <- (if recover then recoverReport path else readReport path) >>= either throwIO pure+  if json then LBS.putStrLn (encode (reportJSON stored)) else case stored of StoredReport {result} -> render color result+  pure 0+dispatch command path (Options json color cases tags output jobs _) = do+  manifest <- readManifest path >>= either (throwIO . ConfigurationFailure) pure+  let campaign = manifestCampaign manifest+      definitions = campaignCases campaign+  case command of+    "list" -> do+      if json+        then LBS.putStrLn (encode (object ["schemaVersion" .= (1 :: Int), "cases" .= map caseDefinitionJSON definitions]))+        else forM_ definitions $ \CaseDefinition {caseId, tags = caseTags, selector, expectation, description} ->+          putStrLn (Text.unpack (caseIdText caseId) ++ "  tags=" ++ show (Set.toAscList caseTags) ++ " selector=" ++ show selector ++ " expect=" ++ show expectation ++ "\n  " ++ Text.unpack description)+      pure 0+    "check" -> do+      checks <- checkManifest manifest >>= either (throwIO . ConfigurationFailure) pure+      if json+        then+          LBS.putStrLn+            ( encode+                ( object+                    [ "schemaVersion" .= (1 :: Int),+                      "checksPerformed" .= (["static-validation", "command-availability", "isolated-edit-applicability"] :: [Text]),+                      "testsExecuted" .= False,+                      "cases" .= [object ["id" .= caseIdText caseId, "applicable" .= applicable, "diagnostics" .= diagnostics] | Applicability {caseId, applicable, diagnostics} <- checks]+                    ]+                )+            )+        else do+          putStrLn "Checked structure, command availability and isolated edit applicability. Tests and discovery were not run."+          forM_ checks $ \Applicability {caseId, applicable, diagnostics} -> putStrLn (Text.unpack (caseIdText caseId) ++ if applicable then ": applicable" else ": " ++ show diagnostics)+      pure (if all (\Applicability {applicable} -> applicable) checks then 0 else 2)+    "run" -> do+      selected <- either (throwIO . ConfigurationFailure) pure (selectDefinitions cases tags definitions)+      let directory = maybe (manifestSourceRoot manifest </> "mutation-results") id output+          inheritedOptions = manifestRunOptions directory manifest+          options =+            inheritedOptions+              { selectedCases = Just selected,+                workers = if jobs == 0 then workers inheritedOptions else jobs,+                progress = \case CaseStarted i -> hPutStrLn stderr ("running " ++ Text.unpack (caseIdText i)); _ -> pure ()+              }+      result <- runCampaign options campaign+      if json+        then LBS.putStrLn (encode (object ["schemaVersion" .= (1 :: Int), "reportPath" .= (directory </> "report.json"), "selectionRule" .= String "union within filter kind, intersection across case/tag kinds", "result" .= runResultJSON result]))+        else render color result >> putStrLn ("Evidence: " ++ directory </> "report.json")+      pure (exitCodeFor result)+    _ -> error "validated command dispatch"++-- | Union repeated values within a filter kind; intersect the case and tag sets.+-- Unknown explicit IDs and empty intersections are configuration errors.+selectDefinitions :: [Text] -> [Text] -> [CaseDefinition] -> Either ConfigError SelectedCases+selectDefinitions requested tags definitions = do+  ids <- mapM mkCaseId requested+  let known = [caseId | CaseDefinition {caseId} <- definitions]+  unless (all (`elem` known) ids) (Left (ConfigError "case" "unknown requested case ID"))+  mkSelectedCases+    [ caseId+    | CaseDefinition {caseId, tags = caseTags} <- definitions,+      (null ids || caseId `elem` ids) && (null tags || not (Set.null (Set.intersection (Set.fromList tags) caseTags)))+    ]++-- | Infrastructure has precedence because RunStatus is library-owned.+exitCodeFor :: RunResult -> Int+exitCodeFor RunResult {status} = case status of RunPassed -> 0; RunFailed -> 1; RunUntrustworthy -> 2; RunInterrupted -> 130++exitCode :: Int -> IO ()+exitCode 0 = exitSuccess+exitCode n = exitWith (ExitFailure n)++render :: String -> RunResult -> IO ()+render color RunResult {results, status, score, failures} = do+  terminal <- hIsTerminalDevice stdout+  let colored = color == "always" || color == "auto" && terminal+      paint text = if colored then "\ESC[" ++ (if status == RunPassed then "32" else "31") ++ "m" ++ text ++ "\ESC[0m" else text+      counts = Map.fromListWith (+) [(show verdict, 1 :: Int) | CaseResult {verdict} <- results]+  putStrLn (paint (show status) ++ "  " ++ show (Map.toAscList counts))+  forM_ results $ \CaseResult {caseId, verdict, reason, evidence = CaseEvidence {stages}} -> do+    putStrLn (Text.unpack (caseIdText caseId) ++ ": " ++ show verdict ++ " — " ++ Text.unpack reason)+    forM_ stages $ \StageEvidence {transcript} -> unless (Text.null transcript) (putStrLn ("  witness: " ++ Text.unpack transcript))+  forM_ score $ \(SemanticScore caught total complete) -> putStrLn ("Semantic detections: " ++ show caught ++ "/" ++ show total ++ if complete then "" else " (incomplete)")+  forM_ failures (putStrLn . ("Run failure: " ++) . show)++help :: String+help =+  unlines+    [ "heni — deliberate mutation testing with attributable evidence",+      "",+      "Usage:",+      "  heni check MANIFEST [--json]",+      "  heni list MANIFEST [--json]",+      "  heni run MANIFEST [--case ID] [--tag TAG] [--output DIRECTORY] [--jobs N] [--json]",+      "  heni report REPORT.json [--json]",+      "  heni report DIRECTORY --recover [--json]",+      "",+      "Selection:",+      "  Repeated filters use union within each kind and intersection across case/tag kinds.",+      "  Unknown explicit IDs and empty selection fail. Manifest paths resolve from its location.",+      "  --output is an explicit caller-relative path; the default is SOURCE/mutation-results.",+      "",+      "Presentation:",+      "  --color auto|always|never  Applies to human output only; redirected auto output has no ANSI.",+      "  --json                   JSON on stdout, progress and errors on stderr.",+      "  --help                   Show this help.",+      "  --version                Show the source-package version without Git.",+      "",+      "Checks and exits:",+      "  check runs static/availability/applicability checks, never tests or discovery.",+      "  run: 0 met contract; 1 failed contract; 2 configuration/infrastructure; SIGINT 130.",+      "  report renders stored evidence only; recovery never resumes commands.",+      "  Runs require a new output directory. Inspect report.json or recover journal.jsonl after interruption."+    ]++diagnostic :: SomeException -> String+diagnostic e = case fromException e of+  Just (ConfigurationFailure (ConfigError field message)) -> Text.unpack field ++ ": " ++ Text.unpack message+  Nothing -> case fromException e of+    Just (ReportError message) -> Text.unpack message+    Nothing -> displayException e
+ app/Main.hs view
@@ -0,0 +1,6 @@+module Main (main) where++import Heni.Cli (mainCli)++main :: IO ()+main = mainCli
+ heni-cli.cabal view
@@ -0,0 +1,84 @@+cabal-version: 3.4+name: heni-cli+version: 0.1.0.0+synopsis: Command line interface to trustworthy deliberate mutation testing+description:+  Validate manifests, run checked mutations, and render portable+  Heni evidence through the shared library runner.++homepage: https://github.com/shinzui/heni+bug-reports: https://github.com/shinzui/heni/issues+license: BSD-3-Clause+license-file: LICENSE+author: Nadeem Bitar+maintainer: nadeem@gmail.com+copyright: 2026 Nadeem Bitar+category: Testing+build-type: Simple+tested-with: ghc ==9.12.4+extra-doc-files:+  CHANGELOG.md+  README.md++data-files: test/corpus/*.json++source-repository head+  type: git+  location: https://github.com/shinzui/heni.git+  subdir: heni-cli++common common+  default-language: GHC2024+  default-extensions:+    DeriveAnyClass+    DuplicateRecordFields+    OverloadedLabels+    OverloadedStrings++  ghc-options:+    -Wall+    -Wcompat++  build-depends:+    aeson >=2.3.2 && <2.4,+    base >=4.21.2 && <4.22,+    bytestring >=0.12.2 && <0.13,+    containers >=0.7 && <0.9,+    directory >=1.3.10.1 && <1.4,+    filepath >=1.5.5 && <1.6,+    heni ==0.1.0.0,+    text >=2.1.4 && <2.2,++executable heni+  import: common+  hs-source-dirs: app+  main-is: Main.hs+  other-modules:+    Heni.Cli+    Paths_heni_cli++  autogen-modules: Paths_heni_cli+  ghc-options: -threaded++test-suite heni-cli-test+  import: common+  type: exitcode-stdio-1.0+  hs-source-dirs:+    test+    app++  main-is: Main.hs+  other-modules:+    Heni.Cli+    Paths_heni_cli++  autogen-modules: Paths_heni_cli+  build-depends:+    process >=1.6.26.1 && <1.7,+    unix >=2.8.8 && <2.9,++  build-tool-depends:+    heni:heni-fixture,+    heni-cli:heni,++  ghc-options: -threaded
+ test/Main.hs view
@@ -0,0 +1,168 @@+module Main (main) where++import Control.Concurrent (threadDelay)+import Control.Exception hiding (handle)+import Control.Monad (forM_, unless, when)+import Data.Aeson+import Data.Aeson.KeyMap qualified as KM+import Data.ByteString qualified as BS+import Data.ByteString.Lazy qualified as LBS+import Data.Foldable (toList)+import Data.List (isInfixOf, isPrefixOf)+import Data.Text qualified as Text+import Data.Text.Encoding qualified as Text+import Heni.Cli (exitCodeFor, selectDefinitions)+import Paths_heni_cli (getDataFileName)+import System.Directory qualified as Dir+import System.Environment (lookupEnv)+import System.Exit (ExitCode (..))+import System.FilePath (takeDirectory, (</>))+import System.Posix.Signals (sigINT, sigKILL, signalProcess)+import System.Posix.Temp (mkdtemp)+import System.Process (readProcessWithExitCode)+import System.Process qualified as P+import System.Timeout (timeout)+import Test.Heni++assert' :: String -> Bool -> IO ()+assert' name good = unless good (error name)++checked :: (Show e) => Either e a -> a+checked = either (error . show) id++main :: IO ()+main = bracket (Dir.getTemporaryDirectory >>= \tmp -> mkdtemp (tmp </> "heni-cli-test-")) Dir.removePathForcibly $ \root -> do+  corpus <- getDataFileName "test/corpus"+  names <- Dir.listDirectory corpus+  forM_ names $ \name -> do+    content <- BS.readFile (corpus </> name)+    assert' ("shared schema/decoder corpus: " ++ name) ((case decodeManifest content of Right _ -> True; Left _ -> False) == ("valid-" `isPrefixOf` name))+  let definition ident tags old new = object ["id" .= (ident :: String), "description" .= String "Detect the intended fault", "tags" .= (tags :: [String]), "edits" .= [object ["kind" .= String "replace", "path" .= String "input", "old" .= (old :: String), "new" .= (new :: String)]], "build" .= String "build", "test" .= String "test", "expect" .= object ["kind" .= String "diagnostic", "stage" .= String "test", "exitCode" .= (1 :: Int), "outputContains" .= String "capacity rule violated"]]+      value =+        object+          [ "schemaVersion" .= (1 :: Int),+            "sourceRoot" .= String "source",+            "commands"+              .= object+                [ "build" .= object ["executable" .= String "heni-fixture", "arguments" .= (["exit", "0"] :: [String])],+                  "test" .= object ["executable" .= String "heni-fixture", "arguments" .= (["check", "input", "diagnostic"] :: [String])]+                ],+            "cases" .= [definition "caught" ["regression"] "anchor" "mutant", definition "survivor" ["example"] "anchor" "harmless"]+          ]+      bytes = LBS.toStrict (encode value)+      manifestPath = root </> "heni.json"+      cli args = readProcessWithExitCode "heni" args ""+  Dir.createDirectory (root </> "source")+  BS.writeFile (root </> "source/input") "anchor\n"+  BS.writeFile manifestPath bytes+  manifest <- readManifest manifestPath >>= either (error . show) pure+  let definitions = campaignCases (manifestCampaign manifest)+  assert' "case union and tag intersection" (fmap selectedCaseIds (selectDefinitions ["caught", "caught", "survivor"] ["regression"] definitions) == Right [checked (mkCaseId "caught")])+  assert' "unknown explicit case" (case selectDefinitions ["absent"] [] definitions of Left _ -> True; _ -> False)+  assert' "empty selection" (case selectDefinitions [] ["absent"] definitions of Left _ -> True; _ -> False)+  (versionExit, version, _) <- cli ["--version"]+  assert' "archive-safe version" (versionExit == ExitSuccess && "heni 0.1.0.0" `isInfixOf` version)+  (helpExit, help, _) <- cli ["--help"]+  assert' "help selection/exit contract" (helpExit == ExitSuccess && "intersection" `isInfixOf` help && "SIGINT 130" `isInfixOf` help)+  (checkExit, checkOutput, _) <- cli ["check", manifestPath, "--json"]+  assert' "check states it did not execute tests" (checkExit == ExitSuccess && case eitherDecodeStrict (Text.encodeUtf8 (Text.pack checkOutput)) :: Either String Value of Right (Object o) -> KM.lookup "testsExecuted" o == Just (Bool False); _ -> False)+  (listExit, listing, _) <- cli ["list", manifestPath, "--json"]+  assert' "list is JSON" (listExit == ExitSuccess && case eitherDecodeStrict (Text.encodeUtf8 (Text.pack listing)) :: Either String Value of Right _ -> True; _ -> False)+  library <- runCampaign ((manifestRunOptions (root </> "library") manifest) {selectedCases = Just (checked (selectDefinitions ["caught"] [] definitions))}) (manifestCampaign manifest)+  (caughtExit, runOutput, progress) <- cli ["run", manifestPath, "--case", "caught", "--output", root </> "cli", "--json", "--color", "always"]+  assert' "caught exit and clean machine stdout" (caughtExit == ExitSuccess && not ('\ESC' `elem` runOutput) && "running caught" `isInfixOf` progress)+  cliStored <- readReport (root </> "cli/report.json") >>= either (error . show) pure+  let signature RunResult {results} = [(caseIdText caseId, verdict, [(stage, outcome) | StageEvidence {stage, outcome} <- stages]) | CaseResult {caseId, verdict, evidence = CaseEvidence {stages}} <- results]+  assert' "library/CLI share stage decisions and verdicts" (case cliStored of StoredReport {result} -> signature result == signature library && exitCodeFor result == 0)+  let resourceValue = case value of+        Object o ->+          Object+            ( KM.insert+                "execution"+                (object ["workers" .= (2 :: Int), "baselineReuse" .= object ["toolchain" .= String "self-contained-fixture/v1"]])+                ( KM.insert+                    "resources"+                    (toJSON [object ["name" .= String "DB", "scope" .= String "case", "identity" .= String "local-fixture", "version" .= String "v1", "epoch" .= String "clean", "environment" .= object ["HENI_RESOURCE_DB_TOKEN" .= String "resource-private-secret"], "setup" .= String "build", "reset" .= String "build", "teardown" .= String "build"]])+                    (KM.insert "cases" (case KM.lookup "cases" o of Just (Array cases) -> toJSON [case c of Object fields -> Object (KM.insert "resources" (toJSON (["DB"] :: [String])) fields); other -> other | c <- toList cases]; _ -> Null) o)+                )+            )+        other -> other+      resourceManifest = root </> "resources.json"+  LBS.writeFile resourceManifest (encode resourceValue)+  configured <- readManifest resourceManifest >>= either (error . show) pure+  configuredLibrary <- runCampaign (manifestRunOptions (root </> "resource-library") configured) (manifestCampaign configured)+  (resourceExit, _, _) <- cli ["run", resourceManifest, "--jobs", "2", "--output", root </> "resource-cli", "--json"]+  resourceStored <- readReport (root </> "resource-cli/report.json") >>= either (error . show) pure+  assert' "resource commands/workers/reuse share library and CLI outcomes" (resourceExit == ExitFailure 1 && case resourceStored of StoredReport {result} -> signature result == signature configuredLibrary)+  reportWithResources <- BS.readFile (root </> "resource-cli/report.json")+  assert' "resource command configuration is redacted" (not ("resource-private-secret" `BS.isInfixOf` reportWithResources))+  fixtureOutput <- lookupEnv "HENI_SCHEMA_RESOURCE"+  forM_ fixtureOutput $ \path -> do+    Dir.createDirectoryIfMissing True (takeDirectory path)+    BS.writeFile path reportWithResources+  Dir.removeFile (root </> "resource-cli/resource-DB-case-caught-0.stdout.log")+  missingResourceLog <- readReport (root </> "resource-cli/report.json")+  assert' "missing setup command artifact invalidates portable report" (case missingResourceLog of Left _ -> True; _ -> False)+  (survivorExit, human, _) <- cli ["run", manifestPath, "--case", "survivor", "--output", root </> "survivor"]+  assert' "survivor exit and redirected output without ANSI" (survivorExit == ExitFailure 1 && not ('\ESC' `elem` human))+  (emptyExit, _, _) <- cli ["run", manifestPath, "--tag", "absent", "--output", root </> "empty"]+  assert' "empty run fails by default" (emptyExit == ExitFailure 2)+  Dir.renameDirectory (root </> "cli") (root </> "moved")+  (reportExit, storedOutput, _) <- cli ["report", root </> "moved/report.json", "--json"]+  assert' "moved evidence renders" (reportExit == ExitSuccess && not (null storedOutput))+  (recoveryExit, _, _) <- cli ["report", root </> "moved", "--recover"]+  assert' "explicit recovery renders without execution" (recoveryExit == ExitSuccess)+  let invalids =+        [ Text.replace "\"schemaVersion\":1" "\"schemaVersion\":999" (Text.decodeUtf8 bytes),+          Text.replace "\"schemaVersion\":1" "\"schemaVersion\":1,\"schemaVersion\":1" (Text.decodeUtf8 bytes),+          Text.replace "\"schemaVersion\":1" "\"schemaVersion\":1,\"\\u0073chemaVersion\":1" (Text.decodeUtf8 bytes),+          Text.replace "\"executable\":\"heni-fixture\"" "\"executable\":\"heni-fixture\",\"timeuotSeconds\":2" (Text.decodeUtf8 bytes),+          Text.replace "\"executable\":\"heni-fixture\"" "\"executable\":\"heni-fixture\",\"timeoutSeconds\":0" (Text.decodeUtf8 bytes),+          Text.replace "\"executable\":\"heni-fixture\"" "\"executable\":\"heni-fixture\",\"arguments\":null" (Text.decodeUtf8 bytes),+          Text.replace "\"path\":\"input\"" "\"path\":\"../escape\"" (Text.decodeUtf8 bytes),+          Text.replace "\"build\":\"build\"" "\"build\":\"absent\"" (Text.decodeUtf8 bytes)+        ]+  forM_ invalids $ \invalid -> assert' "invalid manifest rejected" (case decodeManifest (Text.encodeUtf8 invalid) of Left _ -> True; _ -> False)+  -- Applicability checks and listing do not execute a command with visible effects.+  let sentinel = root </> "must-not-execute"+      noExecute = object ["schemaVersion" .= (1 :: Int), "sourceRoot" .= String "source", "commands" .= object ["build" .= object ["executable" .= String "heni-fixture", "arguments" .= ["counter", sentinel]], "test" .= object ["executable" .= String "heni-fixture", "arguments" .= (["exit", "0"] :: [String])]], "cases" .= [definition "caught" [] "anchor" "mutant"]]+  LBS.writeFile (root </> "no-execute.json") (encode noExecute)+  _ <- cli ["check", root </> "no-execute.json"]+  _ <- cli ["list", root </> "no-execute.json"]+  Dir.doesFileExist sentinel >>= assert' "check/list never execute tests or builds" . not+  let interruptMarker = root </> "interrupted-child.pid"+      interruptManifest =+        object+          [ "schemaVersion" .= (1 :: Int),+            "sourceRoot" .= String "source",+            "commands"+              .= object+                [ "build" .= object ["executable" .= String "heni-fixture", "arguments" .= (["exit", "0"] :: [String])],+                  "test" .= object ["executable" .= String "heni-fixture", "arguments" .= ["check", "input", "timeout", interruptMarker]]+                ],+            "cases" .= [definition "caught" [] "anchor" "mutant"]+          ]+  LBS.writeFile (root </> "interrupt.json") (encode interruptManifest)+  let process = (P.proc "heni" ["run", root </> "interrupt.json", "--output", root </> "interrupted"]) {P.std_out = P.Inherit, P.std_err = P.Inherit}+  P.withCreateProcess process $ \_ _ _ handle -> do+    let waitReady = do+          exists <- Dir.doesFileExist interruptMarker+          unless exists (threadDelay 5000 >> waitReady)+        stopOwned = do+          markerExists <- Dir.doesFileExist interruptMarker+          when markerExists $ do+            pid <- read <$> readFile interruptMarker+            _ <- try (signalProcess sigKILL pid) :: IO (Either IOException ())+            pure ()+    ( do+        ready <- timeout 5000000 waitReady+        assert' "CLI interruption handshake" (ready == Just ())+        pid <- P.getPid handle >>= maybe (error "missing owned CLI PID") pure+        signalProcess sigINT pid+        exited <- timeout 5000000 (P.waitForProcess handle)+        assert' "SIGINT uses 130 after finalization" (exited == Just (ExitFailure 130))+        partial <- readReport (root </> "interrupted/report.json")+        assert' "SIGINT retains complete selected ledger" (case partial of Right StoredReport {result = RunResult {status = RunInterrupted, results = [CaseResult {verdict = NotRun}]}} -> True; _ -> False)+      )+      `finally` stopOwned+  putStrLn "Strict manifest boundary, CLI exits, selection and library parity passed"
+ test/corpus/invalid-adapter-version.json view
@@ -0,0 +1,38 @@+{+  "schemaVersion": 1,+  "sourceRoot": ".",+  "commands": {+    "build": {+      "executable": "heni-hspec-fixture",+      "arguments": [+        "--heni-build"+      ]+    },+    "test": {+      "executable": "heni-hspec-fixture"+    }+  },+  "cases": [+    {+      "id": "named",+      "description": "Detect the intended assertion",+      "adapter": "v999",+      "edits": [+        {+          "kind": "replace",+          "path": "input",+          "old": "anchor",+          "new": "intended"+        }+      ],+      "build": "build",+      "test": "test",+      "expect": {+        "kind": "named-tests",+        "failures": [+          "suite/intended"+        ]+      }+    }+  ]+}
+ test/corpus/invalid-budget.json view
@@ -0,0 +1,43 @@+{+  "schemaVersion": 1,+  "commands": {+    "build": {+      "executable": "heni-fixture",+      "arguments": [+        "exit",+        "0"+      ]+    },+    "test": {+      "executable": "heni-fixture",+      "arguments": [+        "check",+        "input",+        "diagnostic"+      ],+      "timeoutSeconds": 0+    }+  },+  "cases": [+    {+      "id": "caught",+      "description": "Detect the intended fault",+      "edits": [+        {+          "kind": "replace",+          "path": "input",+          "old": "anchor",+          "new": "mutant"+        }+      ],+      "build": "build",+      "test": "test",+      "expect": {+        "kind": "diagnostic",+        "stage": "test",+        "exitCode": 1,+        "outputContains": "capacity rule violated"+      }+    }+  ]+}
+ test/corpus/invalid-duplicate-key.json view
@@ -0,0 +1,1 @@+{"schemaVersion": 1, "schemaVersion": 1, "commands": {"build": {"executable": "heni-fixture", "arguments": ["exit", "0"]}, "test": {"executable": "heni-fixture", "arguments": ["check", "input", "diagnostic"]}}, "cases": [{"id": "caught", "description": "Detect the intended fault", "edits": [{"kind": "replace", "path": "input", "old": "anchor", "new": "mutant"}], "build": "build", "test": "test", "expect": {"kind": "diagnostic", "stage": "test", "exitCode": 1, "outputContains": "capacity rule violated"}}]}
+ test/corpus/invalid-named-without-adapter.json view
@@ -0,0 +1,37 @@+{+  "schemaVersion": 1,+  "sourceRoot": ".",+  "commands": {+    "build": {+      "executable": "heni-hspec-fixture",+      "arguments": [+        "--heni-build"+      ]+    },+    "test": {+      "executable": "heni-hspec-fixture"+    }+  },+  "cases": [+    {+      "id": "named",+      "description": "Detect the intended assertion",+      "edits": [+        {+          "kind": "replace",+          "path": "input",+          "old": "anchor",+          "new": "intended"+        }+      ],+      "build": "build",+      "test": "test",+      "expect": {+        "kind": "named-tests",+        "failures": [+          "suite/intended"+        ]+      }+    }+  ]+}
+ test/corpus/invalid-nested-key.json view
@@ -0,0 +1,43 @@+{+  "schemaVersion": 1,+  "commands": {+    "build": {+      "executable": "heni-fixture",+      "arguments": [+        "exit",+        "0"+      ]+    },+    "test": {+      "executable": "heni-fixture",+      "arguments": [+        "check",+        "input",+        "diagnostic"+      ]+    }+  },+  "cases": [+    {+      "id": "caught",+      "description": "Detect the intended fault",+      "edits": [+        {+          "kind": "replace",+          "path": "input",+          "old": "anchor",+          "new": "mutant"+        }+      ],+      "build": "build",+      "test": "test",+      "expect": {+        "kind": "diagnostic",+        "stage": "test",+        "exitCode": 1,+        "outputContains": "capacity rule violated",+        "witness": "typo"+      }+    }+  ]+}
+ test/corpus/invalid-null-arguments.json view
@@ -0,0 +1,39 @@+{+  "schemaVersion": 1,+  "commands": {+    "build": {+      "executable": "heni-fixture",+      "arguments": null+    },+    "test": {+      "executable": "heni-fixture",+      "arguments": [+        "check",+        "input",+        "diagnostic"+      ]+    }+  },+  "cases": [+    {+      "id": "caught",+      "description": "Detect the intended fault",+      "edits": [+        {+          "kind": "replace",+          "path": "input",+          "old": "anchor",+          "new": "mutant"+        }+      ],+      "build": "build",+      "test": "test",+      "expect": {+        "kind": "diagnostic",+        "stage": "test",+        "exitCode": 1,+        "outputContains": "capacity rule violated"+      }+    }+  ]+}
+ test/corpus/invalid-resource-unicode-name.json view
@@ -0,0 +1,63 @@+{+  "schemaVersion": 1,+  "commands": {+    "build": {+      "executable": "heni-fixture",+      "arguments": [+        "exit",+        "0"+      ]+    },+    "test": {+      "executable": "heni-fixture",+      "arguments": [+        "check",+        "input",+        "diagnostic"+      ]+    }+  },+  "cases": [+    {+      "id": "caught",+      "description": "Detect the intended fault",+      "edits": [+        {+          "kind": "replace",+          "path": "input",+          "old": "anchor",+          "new": "mutant"+        }+      ],+      "build": "build",+      "test": "test",+      "expect": {+        "kind": "diagnostic",+        "stage": "test",+        "exitCode": 1,+        "outputContains": "capacity rule violated"+      },+      "resources": [+        "DB١"+      ]+    }+  ],+  "resources": [+    {+      "name": "DB١",+      "scope": "case",+      "identity": "fixture",+      "version": "v1",+      "epoch": "clean",+      "setup": "build",+      "reset": "build",+      "teardown": "build"+    }+  ],+  "execution": {+    "workers": 2,+    "baselineReuse": {+      "toolchain": "fixture/v1"+    }+  }+}
+ test/corpus/invalid-unknown-key.json view
@@ -0,0 +1,43 @@+{+  "schemaVersion": 1,+  "commands": {+    "build": {+      "executable": "heni-fixture",+      "arguments": [+        "exit",+        "0"+      ]+    },+    "test": {+      "executable": "heni-fixture",+      "arguments": [+        "check",+        "input",+        "diagnostic"+      ]+    }+  },+  "cases": [+    {+      "id": "caught",+      "description": "Detect the intended fault",+      "edits": [+        {+          "kind": "replace",+          "path": "input",+          "old": "anchor",+          "new": "mutant"+        }+      ],+      "build": "build",+      "test": "test",+      "expect": {+        "kind": "diagnostic",+        "stage": "test",+        "exitCode": 1,+        "outputContains": "capacity rule violated"+      }+    }+  ],+  "surprise": true+}
+ test/corpus/invalid-unsafe-path.json view
@@ -0,0 +1,42 @@+{+  "schemaVersion": 1,+  "commands": {+    "build": {+      "executable": "heni-fixture",+      "arguments": [+        "exit",+        "0"+      ]+    },+    "test": {+      "executable": "heni-fixture",+      "arguments": [+        "check",+        "input",+        "diagnostic"+      ]+    }+  },+  "cases": [+    {+      "id": "caught",+      "description": "Detect the intended fault",+      "edits": [+        {+          "kind": "replace",+          "path": "../escape",+          "old": "anchor",+          "new": "mutant"+        }+      ],+      "build": "build",+      "test": "test",+      "expect": {+        "kind": "diagnostic",+        "stage": "test",+        "exitCode": 1,+        "outputContains": "capacity rule violated"+      }+    }+  ]+}
+ test/corpus/invalid-version.json view
@@ -0,0 +1,42 @@+{+  "schemaVersion": 99,+  "commands": {+    "build": {+      "executable": "heni-fixture",+      "arguments": [+        "exit",+        "0"+      ]+    },+    "test": {+      "executable": "heni-fixture",+      "arguments": [+        "check",+        "input",+        "diagnostic"+      ]+    }+  },+  "cases": [+    {+      "id": "caught",+      "description": "Detect the intended fault",+      "edits": [+        {+          "kind": "replace",+          "path": "input",+          "old": "anchor",+          "new": "mutant"+        }+      ],+      "build": "build",+      "test": "test",+      "expect": {+        "kind": "diagnostic",+        "stage": "test",+        "exitCode": 1,+        "outputContains": "capacity rule violated"+      }+    }+  ]+}
+ test/corpus/invalid-workers.json view
@@ -0,0 +1,63 @@+{+  "schemaVersion": 1,+  "commands": {+    "build": {+      "executable": "heni-fixture",+      "arguments": [+        "exit",+        "0"+      ]+    },+    "test": {+      "executable": "heni-fixture",+      "arguments": [+        "check",+        "input",+        "diagnostic"+      ]+    }+  },+  "cases": [+    {+      "id": "caught",+      "description": "Detect the intended fault",+      "edits": [+        {+          "kind": "replace",+          "path": "input",+          "old": "anchor",+          "new": "mutant"+        }+      ],+      "build": "build",+      "test": "test",+      "expect": {+        "kind": "diagnostic",+        "stage": "test",+        "exitCode": 1,+        "outputContains": "capacity rule violated"+      },+      "resources": [+        "DB"+      ]+    }+  ],+  "resources": [+    {+      "name": "DB",+      "scope": "case",+      "identity": "fixture",+      "version": "v1",+      "epoch": "clean",+      "setup": "build",+      "reset": "build",+      "teardown": "build"+    }+  ],+  "execution": {+    "workers": 0,+    "baselineReuse": {+      "toolchain": "fixture/v1"+    }+  }+}
+ test/corpus/valid-minimal.json view
@@ -0,0 +1,42 @@+{+  "schemaVersion": 1,+  "commands": {+    "build": {+      "executable": "heni-fixture",+      "arguments": [+        "exit",+        "0"+      ]+    },+    "test": {+      "executable": "heni-fixture",+      "arguments": [+        "check",+        "input",+        "diagnostic"+      ]+    }+  },+  "cases": [+    {+      "id": "caught",+      "description": "Detect the intended fault",+      "edits": [+        {+          "kind": "replace",+          "path": "input",+          "old": "anchor",+          "new": "mutant"+        }+      ],+      "build": "build",+      "test": "test",+      "expect": {+        "kind": "diagnostic",+        "stage": "test",+        "exitCode": 1,+        "outputContains": "capacity rule violated"+      }+    }+  ]+}
+ test/corpus/valid-named.json view
@@ -0,0 +1,38 @@+{+  "schemaVersion": 1,+  "sourceRoot": ".",+  "commands": {+    "build": {+      "executable": "heni-hspec-fixture",+      "arguments": [+        "--heni-build"+      ]+    },+    "test": {+      "executable": "heni-hspec-fixture"+    }+  },+  "cases": [+    {+      "id": "named",+      "description": "Detect the intended assertion",+      "adapter": "heni-v1",+      "edits": [+        {+          "kind": "replace",+          "path": "input",+          "old": "anchor",+          "new": "intended"+        }+      ],+      "build": "build",+      "test": "test",+      "expect": {+        "kind": "named-tests",+        "failures": [+          "suite/intended"+        ]+      }+    }+  ]+}
+ test/corpus/valid-resources.json view
@@ -0,0 +1,63 @@+{+  "schemaVersion": 1,+  "commands": {+    "build": {+      "executable": "heni-fixture",+      "arguments": [+        "exit",+        "0"+      ]+    },+    "test": {+      "executable": "heni-fixture",+      "arguments": [+        "check",+        "input",+        "diagnostic"+      ]+    }+  },+  "cases": [+    {+      "id": "caught",+      "description": "Detect the intended fault",+      "edits": [+        {+          "kind": "replace",+          "path": "input",+          "old": "anchor",+          "new": "mutant"+        }+      ],+      "build": "build",+      "test": "test",+      "expect": {+        "kind": "diagnostic",+        "stage": "test",+        "exitCode": 1,+        "outputContains": "capacity rule violated"+      },+      "resources": [+        "DB"+      ]+    }+  ],+  "resources": [+    {+      "name": "DB",+      "scope": "case",+      "identity": "fixture",+      "version": "v1",+      "epoch": "clean",+      "setup": "build",+      "reset": "build",+      "teardown": "build"+    }+  ],+  "execution": {+    "workers": 2,+    "baselineReuse": {+      "toolchain": "fixture/v1"+    }+  }+}