spec-controller 0.1.0-alpha.9 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +18 -11
  2. package/dist/cli-allocate/cli.d.ts +2 -0
  3. package/dist/cli-allocate/cli.d.ts.map +1 -0
  4. package/dist/cli-allocate/cli.js +61 -0
  5. package/dist/cli-allocate/cli.js.map +1 -0
  6. package/dist/cli-allocate/readHistory.d.ts +14 -0
  7. package/dist/cli-allocate/readHistory.d.ts.map +1 -0
  8. package/dist/cli-allocate/readHistory.js +109 -0
  9. package/dist/cli-allocate/readHistory.js.map +1 -0
  10. package/dist/cli-allocate/readParts.d.ts +2 -0
  11. package/dist/cli-allocate/readParts.d.ts.map +1 -0
  12. package/dist/cli-allocate/readParts.js +23 -0
  13. package/dist/cli-allocate/readParts.js.map +1 -0
  14. package/dist/cli-allocate/readTree.d.ts +5 -0
  15. package/dist/cli-allocate/readTree.d.ts.map +1 -0
  16. package/dist/cli-allocate/readTree.js +74 -0
  17. package/dist/cli-allocate/readTree.js.map +1 -0
  18. package/dist/cli-allocate/refStore.d.ts +2 -0
  19. package/dist/cli-allocate/refStore.d.ts.map +1 -0
  20. package/dist/cli-allocate/refStore.js +35 -0
  21. package/dist/cli-allocate/refStore.js.map +1 -0
  22. package/dist/cli-allocate/request.d.ts +9 -0
  23. package/dist/cli-allocate/request.d.ts.map +1 -0
  24. package/dist/cli-allocate/request.js +23 -0
  25. package/dist/cli-allocate/request.js.map +1 -0
  26. package/dist/cli-allocate/stderr.d.ts +7 -0
  27. package/dist/cli-allocate/stderr.d.ts.map +1 -0
  28. package/dist/cli-allocate/stderr.js +32 -0
  29. package/dist/cli-allocate/stderr.js.map +1 -0
  30. package/dist/cli-allocate/unreadable.d.ts +13 -0
  31. package/dist/cli-allocate/unreadable.d.ts.map +1 -0
  32. package/dist/cli-allocate/unreadable.js +37 -0
  33. package/dist/cli-allocate/unreadable.js.map +1 -0
  34. package/dist/cli-args.d.ts +15 -70
  35. package/dist/cli-args.d.ts.map +1 -1
  36. package/dist/cli-args.js +167 -85
  37. package/dist/cli-args.js.map +1 -1
  38. package/dist/cli-balance/cli.d.ts +1 -82
  39. package/dist/cli-balance/cli.d.ts.map +1 -1
  40. package/dist/cli-balance/cli.js +86 -397
  41. package/dist/cli-balance/cli.js.map +1 -1
  42. package/dist/cli-balance/emit/format.d.ts +18 -39
  43. package/dist/cli-balance/emit/format.d.ts.map +1 -1
  44. package/dist/cli-balance/emit/format.js +32 -63
  45. package/dist/cli-balance/emit/format.js.map +1 -1
  46. package/dist/cli-balance/emit/writer.d.ts +2 -34
  47. package/dist/cli-balance/emit/writer.d.ts.map +1 -1
  48. package/dist/cli-balance/emit/writer.js +62 -37
  49. package/dist/cli-balance/emit/writer.js.map +1 -1
  50. package/dist/cli-balance/exitStatus.d.ts +2 -0
  51. package/dist/cli-balance/exitStatus.d.ts.map +1 -0
  52. package/dist/cli-balance/exitStatus.js +5 -0
  53. package/dist/cli-balance/exitStatus.js.map +1 -0
  54. package/dist/cli-balance/inputsRead.d.ts +11 -0
  55. package/dist/cli-balance/inputsRead.d.ts.map +1 -0
  56. package/dist/cli-balance/inputsRead.js +54 -0
  57. package/dist/cli-balance/inputsRead.js.map +1 -0
  58. package/dist/cli-balance/reRender.d.ts +8 -0
  59. package/dist/cli-balance/reRender.d.ts.map +1 -0
  60. package/dist/cli-balance/reRender.js +21 -0
  61. package/dist/cli-balance/reRender.js.map +1 -0
  62. package/dist/cli-balance/unreadableInputs.d.ts +15 -0
  63. package/dist/cli-balance/unreadableInputs.d.ts.map +1 -0
  64. package/dist/cli-balance/unreadableInputs.js +100 -0
  65. package/dist/cli-balance/unreadableInputs.js.map +1 -0
  66. package/dist/cli-registry.d.ts +16 -71
  67. package/dist/cli-registry.d.ts.map +1 -1
  68. package/dist/cli-registry.js +71 -136
  69. package/dist/cli-registry.js.map +1 -1
  70. package/dist/cli.d.ts +0 -21
  71. package/dist/cli.d.ts.map +1 -1
  72. package/dist/cli.js +1 -85
  73. package/dist/cli.js.map +1 -1
  74. package/dist/deferralTags.d.ts +0 -14
  75. package/dist/deferralTags.d.ts.map +1 -1
  76. package/dist/deferralTags.js +0 -14
  77. package/dist/deferralTags.js.map +1 -1
  78. package/dist/host.d.ts +10 -41
  79. package/dist/host.d.ts.map +1 -1
  80. package/dist/host.js +99 -50
  81. package/dist/host.js.map +1 -1
  82. package/dist/ingest/ciSourcePaths.d.ts +0 -39
  83. package/dist/ingest/ciSourcePaths.d.ts.map +1 -1
  84. package/dist/ingest/ciSourcePaths.js +1 -39
  85. package/dist/ingest/ciSourcePaths.js.map +1 -1
  86. package/dist/ingest/gherkinValidation.d.ts +0 -80
  87. package/dist/ingest/gherkinValidation.d.ts.map +1 -1
  88. package/dist/ingest/gherkinValidation.js +3 -82
  89. package/dist/ingest/gherkinValidation.js.map +1 -1
  90. package/dist/ingest/ingestQualityChecks.d.ts +8 -93
  91. package/dist/ingest/ingestQualityChecks.d.ts.map +1 -1
  92. package/dist/ingest/ingestQualityChecks.js +61 -224
  93. package/dist/ingest/ingestQualityChecks.js.map +1 -1
  94. package/dist/ingest/ingestScenarios.d.ts +0 -45
  95. package/dist/ingest/ingestScenarios.d.ts.map +1 -1
  96. package/dist/ingest/ingestScenarios.js +24 -110
  97. package/dist/ingest/ingestScenarios.js.map +1 -1
  98. package/dist/ingest/inputShapes.d.ts +16 -0
  99. package/dist/ingest/inputShapes.d.ts.map +1 -0
  100. package/dist/ingest/inputShapes.js +114 -0
  101. package/dist/ingest/inputShapes.js.map +1 -0
  102. package/dist/main.d.ts +2 -0
  103. package/dist/main.d.ts.map +1 -0
  104. package/dist/main.js +93 -0
  105. package/dist/main.js.map +1 -0
  106. package/dist/messages.d.ts +223 -0
  107. package/dist/messages.d.ts.map +1 -0
  108. package/dist/messages.js +319 -0
  109. package/dist/messages.js.map +1 -0
  110. package/dist/outputLocation.d.ts +23 -0
  111. package/dist/outputLocation.d.ts.map +1 -0
  112. package/dist/outputLocation.js +139 -0
  113. package/dist/outputLocation.js.map +1 -0
  114. package/dist/run-management/keptRun.d.ts +8 -109
  115. package/dist/run-management/keptRun.d.ts.map +1 -1
  116. package/dist/run-management/keptRun.js +49 -89
  117. package/dist/run-management/keptRun.js.map +1 -1
  118. package/dist/run-management/resolveRunInputs.d.ts +0 -67
  119. package/dist/run-management/resolveRunInputs.d.ts.map +1 -1
  120. package/dist/run-management/resolveRunInputs.js +13 -117
  121. package/dist/run-management/resolveRunInputs.js.map +1 -1
  122. package/dist/storedRun.d.ts +6 -0
  123. package/dist/storedRun.d.ts.map +1 -0
  124. package/dist/storedRun.js +27 -0
  125. package/dist/storedRun.js.map +1 -0
  126. package/package.json +2 -2
  127. package/dist/corpus/cli.d.ts +0 -34
  128. package/dist/corpus/cli.d.ts.map +0 -1
  129. package/dist/corpus/cli.js +0 -125
  130. package/dist/corpus/cli.js.map +0 -1
@@ -1,51 +1,6 @@
1
- /**
2
- * The Gherkin VALIDATOR — the feature corpus's integrity checkpoint (@SCN-CLI-016, 3F-1767).
3
- *
4
- * WHY THIS EXISTS. The feature corpus is the ledger's DEBIT side, and it was read by a
5
- * hand-rolled TAG-LINE SCAN (`parseScenarios`) that never looked for a `Scenario:` header at
6
- * all: a scenario EXISTED purely because an `@SCN-FFF-NNN` tag line did. So a
7
- * malformed tag line simply STOPPED BEING A TAG LINE, and its scenario ceased to exist —
8
- * silently. One stray character, in a file cucumber itself rejects, deleted a failing row and
9
- * turned an out-of-balance run (exit 1) into a clean BALANCED (exit 0). A dropped scenario
10
- * cannot even reconcile `missing`, because it was never raised to be missing: the run reads
11
- * CLEANER THAN REALITY. That is the false-green this tool exists to prevent, aimed at itself.
12
- *
13
- * THE FIX (this slice). VALIDATE every feature file with the REAL Gherkin parser — the same one
14
- * cucumber uses. A file the parser rejects is an input the tool CANNOT READ, and the CLI refuses
15
- * it: exit 2, halting before reconciling (@SCN-CLI-014/015's Rule — "an input the tool cannot read
16
- * is a usage error, never a verdict" — extended from the OBSERVATION side to the OBLIGATION side).
17
- *
18
- * VALIDATION IS HALF THE STORY. This decides whether a feature file can be READ at all; the corpus
19
- * is then EXTRACTED from the same real AST by `parseScenarios` (@SCN-LDG-020, 3F-1775), so the
20
- * scenarios spec-controller reconciles carry exactly the tags cucumber gives them — Feature/Rule
21
- * tag inheritance included. One reading of the file, not two.
22
- *
23
- * PURE: `string → parse errors`. No fs, no process — the file READ lives beside it at the ingest
24
- * edge, as it does for every other input.
25
- *
26
- * WHY IT LIVES IN THE APP, NOT CORE. Core's barrel is the v1 contract surface downstream consumers
27
- * build on. Since 3F-3449 nothing reds when a symbol is added there — the guard that reconciled it
28
- * both directions is gone — so keeping it narrow is a discipline rather than a gate. This validator
29
- * is an INGEST concern (it decides whether an input can be read at all), not part of the
30
- * reconciliation contract, so it belongs with the other readers at the app edge and the v1 surface
31
- * stays untouched.
32
- */
33
1
  import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
34
2
  import { IdGenerator } from "@cucumber/messages";
35
- /**
36
- * Validate one `.feature` source with the real Gherkin parser. Returns the parse errors it
37
- * reports — empty when the file is well-formed Gherkin.
38
- *
39
- * The parser is authoritative BY CONSTRUCTION: it is cucumber's own. A file it rejects is one
40
- * cucumber would refuse to RUN, so a scenario corpus built from it would be a FICTION — which is
41
- * precisely how a corrupt spec used to read balanced.
42
- *
43
- * A parse failure is reported, never thrown: the CLI edge routes it to stderr + exit 2 (the
44
- * shared typed-error surface), so it can never reach Node's default exit 1 and masquerade as an
45
- * out-of-balance verdict (@SCN-CLI-014's collision, on the obligation side).
46
- *
47
- * @SCN-CLI-016 (3F-1773).
48
- */
3
+ import { APP_MESSAGES, AppMessage } from "../messages.js";
49
4
  export function parseErrorsIn(file, source) {
50
5
  const parser = new Parser(new AstBuilder(IdGenerator.uuid()), new GherkinClassicTokenMatcher());
51
6
  try {
@@ -53,59 +8,25 @@ export function parseErrorsIn(file, source) {
53
8
  return [];
54
9
  }
55
10
  catch (err) {
56
- // The parser raises a CompositeParserException carrying every error it found; its message
57
- // is the operator's actionable payload (line, column, and what it expected). Reported
58
- // verbatim — the tool must not paraphrase the parser and risk describing a fault wrongly.
59
11
  const message = err instanceof Error ? err.message : String(err);
60
12
  return [{ file, message }];
61
13
  }
62
14
  }
63
- /**
64
- * The corpus-level REFUSAL (@SCN-GPG-001, 3F-2817). A parse failure is reported per file by
65
- * `parseErrorsIn` above; this is how a reader of the WHOLE corpus says no.
66
- *
67
- * WHY IT CARRIES EVERY FILE. The balance CLI already names every unparseable file it found
68
- * (@SCN-CLI-016), and a refusal raised at the first bad one would silently narrow that message —
69
- * a behaviour change dressed as an implementation detail. So the discovery collects the corpus
70
- * and refuses ONCE, carrying the lot in discovery order.
71
- *
72
- * The rendered message is the parser's words, per file, verbatim: a reader must be told WHAT
73
- * broke by the parser rather than by our paraphrase of it.
74
- */
75
15
  export class UnreadableFeatureCorpusError extends Error {
76
16
  errors;
77
17
  constructor(errors) {
78
18
  super([
79
- "The feature corpus contains a file that could not be parsed as Gherkin:",
19
+ APP_MESSAGES[AppMessage.FeatureCorpusUnparseable](),
80
20
  ...errors.map(({ file, message }) => `${file}\n${message}`),
81
21
  ].join("\n"));
82
22
  this.name = "UnreadableFeatureCorpusError";
83
23
  this.errors = errors;
84
24
  }
85
25
  }
86
- /**
87
- * The corpus-level ABSENCE refusal (@SCN-CLI-017, 3F-3345) — sibling to the parse refusal above,
88
- * and deliberately the same shape: the reader refuses ONCE, and each command decides what the
89
- * refusal means for it.
90
- *
91
- * WHY A TYPED ERROR RATHER THAN AN EMPTY CORPUS. The obvious fix is for `discoverFeatures` to hand
92
- * back `[]` when the directory is not there. Measured, that would ship a NEW false green in a
93
- * second command: `spec-controller tags` reaches the same reader through `readCorpusInputs`, whose
94
- * GENERIC catch is the only thing turning an absent corpus — and, since `tags` carries no
95
- * `checkInputsExist`, a supplied-but-absent `--features` too — into `The corpus could not be
96
- * read: …` at exit 2. Tolerance at the reader turns both into `tags: OK …` at exit 0, inside the
97
- * slice whose whole purpose is killing a vacuous certification. Typed, both survive untouched:
98
- * this IS an `Error`, so it still lands in that generic catch.
99
- *
100
- * IT CARRIES THE DIRECTORY because the report is owed the name of what it examined — "I found no
101
- * record" and "I found no regression" must never wear one word (3F-2947), and a consumer told
102
- * their corpus is empty deserves to know which directory was read to reach that.
103
- */
104
26
  export class MissingFeatureCorpusError extends Error {
105
- /** The directory that is not there — as resolved, so a reader can print the path examined. */
106
27
  directory;
107
28
  constructor(directory) {
108
- super(`The feature corpus directory does not exist: ${directory}`);
29
+ super(APP_MESSAGES[AppMessage.FeatureCorpusDirectoryMissing](directory));
109
30
  this.name = "MissingFeatureCorpusError";
110
31
  this.directory = directory;
111
32
  }
@@ -1 +1 @@
1
- {"version":3,"file":"gherkinValidation.js","sourceRoot":"","sources":["../../src/ingest/gherkinValidation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,UAAU,EAAE,0BAA0B,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACnF,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAUjD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,MAAc;IACxD,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,UAAU,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,0BAA0B,EAAE,CAAC,CAAC;IAEhG,IAAI,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACrB,OAAO,EAAE,CAAC;IACZ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,0FAA0F;QAC1F,sFAAsF;QACtF,0FAA0F;QAC1F,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IAC7B,CAAC;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,4BAA6B,SAAQ,KAAK;IAC5C,MAAM,CAA+B;IAE9C,YAAY,MAAoC;QAC9C,KAAK,CACH;YACE,yEAAyE;YACzE,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,KAAK,OAAO,EAAE,CAAC;SAC5D,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,8BAA8B,CAAC;QAC3C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IAClD,8FAA8F;IACrF,SAAS,CAAS;IAE3B,YAAY,SAAiB;QAC3B,KAAK,CAAC,gDAAgD,SAAS,EAAE,CAAC,CAAC;QACnE,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;QACxC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;CACF"}
1
+ {"version":3,"file":"gherkinValidation.js","sourceRoot":"","sources":["../../src/ingest/gherkinValidation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,0BAA0B,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACnF,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAO1D,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,MAAc;IACxD,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,UAAU,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,0BAA0B,EAAE,CAAC,CAAC;IAEhG,IAAI,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACrB,OAAO,EAAE,CAAC;IACZ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IAC7B,CAAC;AACH,CAAC;AAED,MAAM,OAAO,4BAA6B,SAAQ,KAAK;IAC5C,MAAM,CAA+B;IAE9C,YAAY,MAAoC;QAC9C,KAAK,CACH;YACE,YAAY,CAAC,UAAU,CAAC,wBAAwB,CAAC,EAAE;YACnD,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,KAAK,OAAO,EAAE,CAAC;SAC5D,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,8BAA8B,CAAC;QAC3C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IACzC,SAAS,CAAS;IAE3B,YAAY,SAAiB;QAC3B,KAAK,CAAC,YAAY,CAAC,UAAU,CAAC,6BAA6B,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;QACzE,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;QACxC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;CACF"}
@@ -1,109 +1,24 @@
1
- /**
2
- * Ingest the target's quality-check inputs — the IMPURE reads behind the balance
3
- * command (split out of the old cli.ts). Reads evidence
4
- * obligations from the repo's feature files, observed evidence from the Vitest +
5
- * Cucumber result JSON, and the static-check evidence from the CI config + package.json,
6
- * turning each into the engine's in-memory model. The orchestration (arg parsing, the
7
- * output plan/write, main) stays in cli-balance/cli.ts; these are the file reads it calls.
8
- *
9
- * Configurable input paths (no adapter registry in v1 — the readers are named, not
10
- * registered). Missing result files are tolerated: their evidence is simply absent
11
- * (the scenario reconciles as missing for that level). The static check kind reads
12
- * `--ci` (a workflow file or a directory of them, default `.github/workflows`) +
13
- * `package.json` to observe each fitness check the corpus cites.
14
- */
15
1
  import { type GherkinParseError } from "./gherkinValidation.js";
16
2
  import { type EvidenceObligations } from "@3f-consulting/spec-controller-core";
17
3
  import type { EvidenceReconciliation } from "@3f-consulting/spec-controller-core";
18
- /**
19
- * The reconciler's resolved input paths — the CLI options threaded to the readers.
20
- * Every path is optional; a missing result file is tolerated (its evidence is
21
- * simply absent). `packageJson` is the EXTERNALLY-SPECIFIED package.json the static
22
- * static-check reader reads `exists` from (the --package-json seam, @SCN-LNT-005):
23
- * default the tool's own cwd `package.json`; set it to reconcile ANOTHER repo's
24
- * static checks — required to render the historical watermelon correctly.
25
- */
26
4
  export interface ReconcileOptions {
27
5
  features?: string;
28
6
  vitest?: string;
29
7
  cucumber?: string;
30
8
  ci?: string;
31
9
  packageJson?: string;
32
- /**
33
- * The CI RUN'S OWN RECORD of what each of its steps did (@SCN-LNT-021, @SCN-LNT-028) — the credit
34
- * side of every CONVERTED fitness check, written by the consumer's own pipeline and by neither
35
- * party to this reconciliation.
36
- *
37
- * KEPT APART FROM `ci` BECAUSE THE TWO DOCUMENTS ANSWER DIFFERENT QUESTIONS. The workflow FILE
38
- * says a check is wired into this pipeline and which row its step cites — a fact about a
39
- * repository, true between runs. This is one RUN's record: did it run this time, and what did it
40
- * find. Optional like the other result files: a consumer whose pipeline dumps no record, or a
41
- * local run with no pipeline at all, hands none, and the converted rows it would have answered
42
- * read `missing`.
43
- */
44
10
  ciSteps?: string;
45
11
  }
46
- /**
47
- * The feature corpus's INTEGRITY CHECKPOINT (@SCN-CLI-016, 3F-1767) — validate every discovered
48
- * `.feature` with the REAL Gherkin parser and report the files it cannot read.
49
- *
50
- * The feature corpus is the ledger's DEBIT side, and it used to be read by a tag-line scan that
51
- * never consulted a parser — so a malformed tag line silently deleted its scenario, and a corrupt
52
- * spec could read BALANCED. A file the parser rejects is one cucumber would refuse to run: the
53
- * CLI halts on it (exit 2) rather than reconciling a corpus that is a fiction.
54
- *
55
- * THIS IS NOW THE CATCH SITE, NOT THE CALL SITE (@SCN-GPG-001, 3F-2817). The validation moved
56
- * into `discoverFeatures`, which REFUSES an unreadable corpus rather than returning one, so every
57
- * corpus reader inherits the refusal instead of each restating it. What is left here is the
58
- * translation back into the list this CLI edge already prints — same files, same order, same
59
- * verbatim parser messages, so `balance`'s behaviour is unchanged. Anything that is not a corpus
60
- * refusal is not ours to interpret and rethrows.
61
- */
12
+ export declare const DEFAULT_CI_PATH = ".github/workflows";
13
+ export declare const DEFAULT_PACKAGE_JSON = "package.json";
62
14
  export declare function featureCorpusParseErrors(featuresDir: string | undefined): GherkinParseError[];
63
- /**
64
- * The `@SCN` ids the feature corpus CARRIES MORE THAN ONCE (@SCN-LDG-020's guardrail, 3F-1775).
65
- *
66
- * An `@SCN` identifies exactly ONE scenario — that is the whole basis of attributing evidence to
67
- * it. If two scenarios carry the same id, a test citing it could be proving EITHER, and the tool
68
- * cannot know which: the corpus is ambiguous, and any verdict over it is a guess.
69
- *
70
- * WHY THIS EXISTS. Tag inheritance made this reachable: an `@SCN` hoisted to a `Feature:`/`Rule:`
71
- * is inherited by every scenario beneath it (cucumber does this, so we do — parity). Left
72
- * unhandled, ONE passing test then marked EVERY scenario sharing that id `balanced`, including
73
- * scenarios nothing proves — a BALANCED-WHEN-BROKEN run, exit 0. A new silent false-green,
74
- * introduced by the very fix that closed the last one.
75
- *
76
- * Refused at the edge (exit 2) rather than reconciled, under the Rule already shipped for the
77
- * observed side and the corrupt corpus (@SCN-CLI-014/015/016): an input the tool cannot read is a
78
- * usage error, never a verdict. Whether hoisting an `@SCN` is an authoring fault worth a richer
79
- * diagnostic is the deferred policy question (3F-1774's sibling); refusing to reconcile a corpus we
80
- * cannot read is not a policy — it is the floor.
81
- */
82
- export declare function featureCorpusDuplicateScnIds(featuresDir: string | undefined): string[];
83
- /**
84
- * Read the target's `feature-code → slug` map over the same `discoverFeatures` corpus
85
- * (@SCN-RPT-006) — each coded feature file's `@<FFF>` header (`featureCode`)
86
- * mapped to its filename `slug`, the full feature name. This is the mapping
87
- * `scenariosFromFeatures` discards (it keeps only the parsed scenarios). A sibling read to
88
- * the balance, threaded to `renderMarkdown` (via `writeOutputs`) as the 5th sibling arg
89
- * (markdown-only — the by-feature cut is markdown-only, so it is NOT serialised to JSON).
90
- * Uncoded files (`featureCode === null`) are omitted, so the by-feature emitter falls
91
- * back to the bare `FFF` for any id whose code no file registers. One-prefix-one-file holds
92
- * by invariant (`corpusFaults` raises `duplicate-feature-code`, @SCN-STG-010), so the map is
93
- * unambiguous. That invariant was `lintFeatureCodes`' until 3F-3313; it is the shipped command's
94
- * now, which is the first time a CONSUMER of this reader has it too.
95
- */
15
+ export interface InputReadFault {
16
+ readonly path: string;
17
+ readonly reason: string;
18
+ }
19
+ export declare function corpusReadFault(featuresDir: string | undefined): InputReadFault | null;
20
+ export declare function ciReadFault(ciPath: string | undefined): InputReadFault | null;
96
21
  export declare function readFeatureNames(featuresDir?: string): Map<string, string>;
97
- /**
98
- * Read the scenario-corpus census over the discovered feature files (the same
99
- * `discoverFeatures` corpus `scenariosFromFeatures` reads): sum the raw @SCN
100
- * occurrences (`countScnOccurrences`) and the scenario count
101
- * (`parseScenarios(...).length`) across every file, and build the `EvidenceObligations`
102
- * sibling (@SCN-RPT-008). Also collects the UNTAGGED scenarios
103
- * (@SCN-RPT-014) — `parseUntaggedScenarios` per file, each paired with its feature-file
104
- * locator, sorted by (feature, scenario). Read alongside the balance in `main()` and
105
- * threaded to the renderers via `writeOutputs`; `runReconcile` stays unchanged.
106
- */
107
22
  export declare function readEvidenceObligations(features?: string): EvidenceObligations;
108
23
  export declare function runReconcile(opts: ReconcileOptions): EvidenceReconciliation;
109
24
  //# sourceMappingURL=ingestQualityChecks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ingestQualityChecks.d.ts","sourceRoot":"","sources":["../../src/ingest/ingestQualityChecks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAYH,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,qCAAqC,CAAC;AAkB7C,OAAO,KAAK,EAIV,sBAAsB,EACvB,MAAM,qCAAqC,CAAC;AA6B7C;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA8ED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,iBAAiB,EAAE,CAQ7F;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,4BAA4B,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAStF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAM1E;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAqB9E;AA0CD,wBAAgB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,sBAAsB,CA2D3E"}
1
+ {"version":3,"file":"ingestQualityChecks.d.ts","sourceRoot":"","sources":["../../src/ingest/ingestQualityChecks.ts"],"names":[],"mappings":"AAWA,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,qCAAqC,CAAC;AAY7C,OAAO,KAAK,EAGV,sBAAsB,EACvB,MAAM,qCAAqC,CAAC;AAO7C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,eAAe,sBAAsB,CAAC;AAEnD,eAAO,MAAM,oBAAoB,iBAAiB,CAAC;AAuBnD,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,iBAAiB,EAAE,CAQ7F;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAQD,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,cAAc,GAAG,IAAI,CAUtF;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,cAAc,GAAG,IAAI,CAU7E;AAYD,wBAAgB,gBAAgB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAM1E;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAmB9E;AAmBD,wBAAgB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,sBAAsB,CA2B3E"}
@@ -1,94 +1,28 @@
1
- /**
2
- * Ingest the target's quality-check inputs — the IMPURE reads behind the balance
3
- * command (split out of the old cli.ts). Reads evidence
4
- * obligations from the repo's feature files, observed evidence from the Vitest +
5
- * Cucumber result JSON, and the static-check evidence from the CI config + package.json,
6
- * turning each into the engine's in-memory model. The orchestration (arg parsing, the
7
- * output plan/write, main) stays in cli-balance/cli.ts; these are the file reads it calls.
8
- *
9
- * Configurable input paths (no adapter registry in v1 — the readers are named, not
10
- * registered). Missing result files are tolerated: their evidence is simply absent
11
- * (the scenario reconciles as missing for that level). The static check kind reads
12
- * `--ci` (a workflow file or a directory of them, default `.github/workflows`) +
13
- * `package.json` to observe each fitness check the corpus cites.
14
- */
15
1
  import { readFileSync, existsSync } from "node:fs";
16
2
  import { basename } from "node:path";
17
3
  import { ciSourcePathsOf } from "./ciSourcePaths.js";
18
4
  import { discoverFeatures } from "./ingestScenarios.js";
19
- import { parseScenarios, countScnOccurrences, parseUntaggedScenarios, proseOnlyCodes, } from "@3f-consulting/spec-controller-core";
5
+ import { parseScenarios, countScnOccurrences, parseUntaggedScenarios, sharedScnCodesOf, untaggedScenarioId, } from "@3f-consulting/spec-controller-core";
20
6
  import { MissingFeatureCorpusError, UnreadableFeatureCorpusError, } from "./gherkinValidation.js";
21
7
  import { DEFAULT_DEFERRAL_TAGS } from "../deferralTags.js";
22
8
  import { evidenceObligations, } from "@3f-consulting/spec-controller-core";
23
9
  import { parseVitestEvidenceObservations, } from "@3f-consulting/spec-controller-core";
24
10
  import { parseCucumberEvidenceObservations, } from "@3f-consulting/spec-controller-core";
25
- // THE WHOLE COMPOSITION, THROUGH THE PUBLIC BARREL AND NOTHING ELSE (3F-3086). This module travels
26
- // in the tarball, and until 3F-3089 it reached nine core symbols through the UNPUBLISHED
27
- // `./internal` subpath to assemble the reconciliation itself — which is why an installed
28
- // `spec-controller balance --help` threw ERR_PACKAGE_PATH_NOT_EXPORTED while every gate in this
29
- // workspace stayed green. The nine are still internal (3F-2837 stands, unamended); what moved is
30
- // their CALLER. What is left here is the reads: this edge opens the files and hands the bytes over.
31
11
  import { evidenceReconciliationFromSources } from "@3f-consulting/spec-controller-core";
32
12
  import { resolveTargetRoot } from "../run-management/resolveRunInputs.js";
33
- /**
34
- * The per-input FALLBACK quality-check for each JS-ecosystem result source — the
35
- * source→quality-check mapping, SET at the CLI edge rather than hardcoded in the core
36
- * parsers / countRuntimeEvidenceObservationKinds. A second ecosystem (pytest / JUnit) sets its own here (or
37
- * via an adapter); the core never assumes a mapping. No CLI override flag yet — that
38
- * earns its place at the second ecosystem, not now.
39
- */
40
13
  const VITEST_QUALITY_CHECK = "unit";
41
14
  const CUCUMBER_QUALITY_CHECK = "integration";
42
- /**
43
- * THIS TOOL, RUN TO RECONCILE (@SCN-USG-014, 3F-3343) — the invocation whose step is the
44
- * reconciler rather than one of the reconciled rows, SET at the CLI edge and injected into the
45
- * core like the prefixes above and the workflow names below.
46
- *
47
- * HOW THIS PRODUCT IS INVOKED IS THIS LAYER'S FACT. `spec-controller` is the `bin` this package
48
- * publishes and `balance` is the command that reconciles; the engine holds neither, and a core
49
- * that assumed them would be asserting something about a command line it never sees.
50
- *
51
- * BOTH TOKENS, AND THE SECOND IS NOT DECORATION. This repository's own pipeline runs
52
- * `pnpm spec-controller tags` beside its balance step — the same binary, doing something that is
53
- * an ordinary step of the pipeline being reconciled — so an invocation naming the binary alone
54
- * would take that step off the books. The comparison is whole, adjacent shell tokens
55
- * (`parse/commandRuns`), so this names the reconciliation and nothing else the tool can do.
56
- */
57
15
  const RECONCILER_INVOCATION = "spec-controller balance";
58
- /** Read + JSON-parse a file if present, else return undefined. */
16
+ export const DEFAULT_CI_PATH = ".github/workflows";
17
+ export const DEFAULT_PACKAGE_JSON = "package.json";
59
18
  function readJsonIfPresent(path) {
60
19
  if (!path || !existsSync(path))
61
20
  return undefined;
62
21
  return JSON.parse(readFileSync(path, "utf8"));
63
22
  }
64
- /**
65
- * THE CORPUS `balance` READS — the shared discovery, with an ABSENT directory read as NO CORPUS
66
- * (@SCN-CLI-017, 3F-3345). The single place this command opts in.
67
- *
68
- * `new-client` is a supported adoption state, not a degraded one, and most targets have no
69
- * `features/` directory at all — so an absent DEFAULT corpus is the target's honest shape and
70
- * `balance` reports on it. Left to escape it was a raw `ENOENT` landing on Node's default exit 1,
71
- * the OUT-OF-BALANCE code, which is exactly the collision @SCN-CLI-014/015/016 closed on every
72
- * other input, alive on the one path each new consumer is guaranteed to take.
73
- *
74
- * ONE HELPER, NOT FIVE CATCHES, and that is the whole reason it exists: five readers each doing
75
- * this by hand is five chances for one to stay intolerant, and a run that reports on a corpus for
76
- * four of its readings and throws on the fifth is worse than one that throws. The translation is
77
- * also why `discoverFeatures` is not the place to be tolerant — see its own note, and
78
- * `MissingFeatureCorpusError`'s: `tags` reaches the same reader and keeps refusing.
79
- *
80
- * IT IS `[]`, NOT A VERDICT. Zero rows is what @SCN-CLI-019 reads as NOTHING TO RECONCILE, and a
81
- * corpus-less target that still has uncited credits keeps its suspense row and its exit 1
82
- * (@SCN-CLI-020) — this reader says what it found, never what the run means.
83
- *
84
- * A supplied-but-absent `--features` never arrives here: `checkInputsExist` refuses it at the CLI
85
- * edge, exit 2 (@SCN-CLI-002). The precedent is `ciSourcePathsOf`, which already answers an absent
86
- * DEFAULT `.github/workflows` with `[]` for the same reason — the default finding nothing is a fact
87
- * about the target, not a mistake by its author.
88
- */
89
23
  function balanceCorpus(featuresDir) {
90
24
  try {
91
- return featuresDir !== undefined ? discoverFeatures(featuresDir) : discoverFeatures();
25
+ return discoverFeatures(featuresDir);
92
26
  }
93
27
  catch (err) {
94
28
  if (err instanceof MissingFeatureCorpusError)
@@ -96,49 +30,9 @@ function balanceCorpus(featuresDir) {
96
30
  throw err;
97
31
  }
98
32
  }
99
- /**
100
- * The discovered feature corpus's SOURCES — read once so the two readings a citing run needs (the
101
- * obligations, and the corpus hop's `check tag → @SCN`) are taken from the same bytes. Two
102
- * discoveries could disagree about the corpus, and a citation resolved against a corpus the ledger
103
- * was not built from is exactly the phantom-row fault (@SCN-LNT-009).
104
- */
105
- function featureSources(featuresDir) {
106
- return balanceCorpus(featuresDir).map((f) => readFileSync(f.path, "utf8"));
107
- }
108
- function scenariosFromFeatures(featuresDir, deferralTags = DEFAULT_DEFERRAL_TAGS) {
109
- return featureSources(featuresDir).flatMap((source) => parseScenarios(source, deferralTags));
110
- }
111
- /**
112
- * The feature corpus's UNTAGGED half (@SCN-LDG-021, 3F-1774) — the scenarios `parseScenarios`
113
- * discards because they carry no `@SCN`. They are REAL scenarios: cucumber runs them, so each
114
- * owns a real Evidence Obligation — but the obligation has no code for an Evidence Observation to
115
- * link to, so it can never be discharged. `reconcile` gives each its own out-of-balance row
116
- * (`no-scn-code`); before this read they were invisible to the ledger, and a run whose only fault
117
- * was an untagged scenario printed "N scenario(s) carry no @SCN tag" and exited 0 BALANCED.
118
- *
119
- * Each is handled by its `<feature> : <scenario>` locator — the SAME locator the untagged warning
120
- * prints — because a bare scenario name is not unique across the corpus, and the row must say
121
- * where to go.
122
- */
123
33
  function untaggedScenariosFromFeatures(featuresDir) {
124
- return balanceCorpus(featuresDir).flatMap((f) => parseUntaggedScenarios(readFileSync(f.path, "utf8")).map((scenario) => `${f.slug}.feature : ${scenario}`));
34
+ return balanceCorpus(featuresDir).flatMap((f) => parseUntaggedScenarios(readFileSync(f.path, "utf8")).map((scenario) => untaggedScenarioId({ feature: `${f.slug}.feature`, scenario })));
125
35
  }
126
- /**
127
- * The feature corpus's INTEGRITY CHECKPOINT (@SCN-CLI-016, 3F-1767) — validate every discovered
128
- * `.feature` with the REAL Gherkin parser and report the files it cannot read.
129
- *
130
- * The feature corpus is the ledger's DEBIT side, and it used to be read by a tag-line scan that
131
- * never consulted a parser — so a malformed tag line silently deleted its scenario, and a corrupt
132
- * spec could read BALANCED. A file the parser rejects is one cucumber would refuse to run: the
133
- * CLI halts on it (exit 2) rather than reconciling a corpus that is a fiction.
134
- *
135
- * THIS IS NOW THE CATCH SITE, NOT THE CALL SITE (@SCN-GPG-001, 3F-2817). The validation moved
136
- * into `discoverFeatures`, which REFUSES an unreadable corpus rather than returning one, so every
137
- * corpus reader inherits the refusal instead of each restating it. What is left here is the
138
- * translation back into the list this CLI edge already prints — same files, same order, same
139
- * verbatim parser messages, so `balance`'s behaviour is unchanged. Anything that is not a corpus
140
- * refusal is not ours to interpret and rethrows.
141
- */
142
36
  export function featureCorpusParseErrors(featuresDir) {
143
37
  try {
144
38
  balanceCorpus(featuresDir);
@@ -150,48 +44,50 @@ export function featureCorpusParseErrors(featuresDir) {
150
44
  throw err;
151
45
  }
152
46
  }
153
- /**
154
- * The `@SCN` ids the feature corpus CARRIES MORE THAN ONCE (@SCN-LDG-020's guardrail, 3F-1775).
155
- *
156
- * An `@SCN` identifies exactly ONE scenario — that is the whole basis of attributing evidence to
157
- * it. If two scenarios carry the same id, a test citing it could be proving EITHER, and the tool
158
- * cannot know which: the corpus is ambiguous, and any verdict over it is a guess.
159
- *
160
- * WHY THIS EXISTS. Tag inheritance made this reachable: an `@SCN` hoisted to a `Feature:`/`Rule:`
161
- * is inherited by every scenario beneath it (cucumber does this, so we do — parity). Left
162
- * unhandled, ONE passing test then marked EVERY scenario sharing that id `balanced`, including
163
- * scenarios nothing proves — a BALANCED-WHEN-BROKEN run, exit 0. A new silent false-green,
164
- * introduced by the very fix that closed the last one.
165
- *
166
- * Refused at the edge (exit 2) rather than reconciled, under the Rule already shipped for the
167
- * observed side and the corrupt corpus (@SCN-CLI-014/015/016): an input the tool cannot read is a
168
- * usage error, never a verdict. Whether hoisting an `@SCN` is an authoring fault worth a richer
169
- * diagnostic is the deferred policy question (3F-1774's sibling); refusing to reconcile a corpus we
170
- * cannot read is not a policy — it is the floor.
171
- */
172
- export function featureCorpusDuplicateScnIds(featuresDir) {
173
- const seen = new Set();
174
- const duplicates = new Set();
175
- for (const scenario of scenariosFromFeatures(featuresDir)) {
176
- if (seen.has(scenario.scnId))
177
- duplicates.add(scenario.scnId);
178
- seen.add(scenario.scnId);
47
+ function readFaultOf(err, path) {
48
+ const errno = err;
49
+ if (!(err instanceof Error) || typeof errno.code !== "string")
50
+ return null;
51
+ return { path: errno.path ?? path, reason: err.message };
52
+ }
53
+ export function corpusReadFault(featuresDir) {
54
+ try {
55
+ balanceCorpus(featuresDir);
56
+ return null;
57
+ }
58
+ catch (err) {
59
+ if (err instanceof UnreadableFeatureCorpusError)
60
+ return null;
61
+ const fault = readFaultOf(err, featuresDir ?? "features");
62
+ if (fault === null)
63
+ throw err;
64
+ return fault;
65
+ }
66
+ }
67
+ export function ciReadFault(ciPath) {
68
+ const path = ciPath ?? DEFAULT_CI_PATH;
69
+ try {
70
+ for (const source of ciSourcePathsOf(path))
71
+ readCiSource(source);
72
+ return null;
73
+ }
74
+ catch (err) {
75
+ const fault = readFaultOf(err, path);
76
+ if (fault === null)
77
+ throw err;
78
+ return fault;
79
+ }
80
+ }
81
+ function readCiSource(source) {
82
+ try {
83
+ readFileSync(source);
84
+ }
85
+ catch (err) {
86
+ const errno = err;
87
+ errno.path ??= source;
88
+ throw err;
179
89
  }
180
- return [...duplicates].sort();
181
90
  }
182
- /**
183
- * Read the target's `feature-code → slug` map over the same `discoverFeatures` corpus
184
- * (@SCN-RPT-006) — each coded feature file's `@<FFF>` header (`featureCode`)
185
- * mapped to its filename `slug`, the full feature name. This is the mapping
186
- * `scenariosFromFeatures` discards (it keeps only the parsed scenarios). A sibling read to
187
- * the balance, threaded to `renderMarkdown` (via `writeOutputs`) as the 5th sibling arg
188
- * (markdown-only — the by-feature cut is markdown-only, so it is NOT serialised to JSON).
189
- * Uncoded files (`featureCode === null`) are omitted, so the by-feature emitter falls
190
- * back to the bare `FFF` for any id whose code no file registers. One-prefix-one-file holds
191
- * by invariant (`corpusFaults` raises `duplicate-feature-code`, @SCN-STG-010), so the map is
192
- * unambiguous. That invariant was `lintFeatureCodes`' until 3F-3313; it is the shipped command's
193
- * now, which is the first time a CONSUMER of this reader has it too.
194
- */
195
91
  export function readFeatureNames(featuresDir) {
196
92
  const map = new Map();
197
93
  for (const f of balanceCorpus(featuresDir)) {
@@ -200,25 +96,15 @@ export function readFeatureNames(featuresDir) {
200
96
  }
201
97
  return map;
202
98
  }
203
- /**
204
- * Read the scenario-corpus census over the discovered feature files (the same
205
- * `discoverFeatures` corpus `scenariosFromFeatures` reads): sum the raw @SCN
206
- * occurrences (`countScnOccurrences`) and the scenario count
207
- * (`parseScenarios(...).length`) across every file, and build the `EvidenceObligations`
208
- * sibling (@SCN-RPT-008). Also collects the UNTAGGED scenarios
209
- * (@SCN-RPT-014) — `parseUntaggedScenarios` per file, each paired with its feature-file
210
- * locator, sorted by (feature, scenario). Read alongside the balance in `main()` and
211
- * threaded to the renderers via `writeOutputs`; `runReconcile` stays unchanged.
212
- */
213
99
  export function readEvidenceObligations(features) {
214
100
  const featureFiles = balanceCorpus(features);
215
101
  let raw = 0;
216
102
  let scenarioCount = 0;
217
103
  const untagged = [];
218
- const sources = [];
104
+ const files = [];
219
105
  for (const f of featureFiles) {
220
106
  const source = readFileSync(f.path, "utf8");
221
- sources.push(source);
107
+ files.push({ feature: `${f.slug}.feature`, source });
222
108
  raw += countScnOccurrences(source);
223
109
  scenarioCount += parseScenarios(source).length;
224
110
  for (const scenario of parseUntaggedScenarios(source)) {
@@ -226,19 +112,9 @@ export function readEvidenceObligations(features) {
226
112
  }
227
113
  }
228
114
  untagged.sort((a, b) => a.feature.localeCompare(b.feature) || a.scenario.localeCompare(b.scenario));
229
- // The prose-only codes are a CROSS-CORPUS difference (a code tagged in any file isn't
230
- // dangling), so it reads all sources at once — not per-file like the others.
231
- return evidenceObligations(raw, scenarioCount, untagged, proseOnlyCodes(sources));
115
+ return evidenceObligations(raw, scenarioCount, untagged, sharedScnCodesOf(files));
232
116
  }
233
- /**
234
- * Parse the target's RUNTIME observations ONCE — the tag-derived, per-tier observation set
235
- * (@SCN-RPT-017 / 3F-1452 FULL UNIFY) fed to BOTH the reconciler and the runtime census, so the
236
- * two can never disagree. Each source's FALLBACK quality-check is set at the CLI edge
237
- * (vitest⇒unit, cucumber⇒integration); the parsers DERIVE the level from the cited scenario's tier
238
- * tag (@SCN-LDG-011 / 3F-1434), using the fallback only for an untagged scenario. A missing report
239
- * contributes nothing. Root-relative so a phantom-scenario row's where-to-fix stays portable.
240
- */
241
- function parseRuntimeObservations(opts, root) {
117
+ function parseBehaviouralObservations(opts, root) {
242
118
  const out = [];
243
119
  const vitestReport = readJsonIfPresent(opts.vitest);
244
120
  if (vitestReport)
@@ -249,66 +125,27 @@ function parseRuntimeObservations(opts, root) {
249
125
  return out;
250
126
  }
251
127
  export function runReconcile(opts) {
252
- // Read the target's package.json ONCE — its `specController.*` config supplies the deferral-tag
253
- // extension (@SCN-PND-021) and the configured fitness rows, and its `scripts` back the `exists`
254
- // read. The disk read stays at this impure edge; the engine takes the resolved values as
255
- // injected params, staying pure.
256
- const pkgPath = opts.packageJson ?? "package.json";
128
+ const pkgPath = opts.packageJson ?? DEFAULT_PACKAGE_JSON;
257
129
  const packageJson = readJsonIfPresent(pkgPath) ?? {};
258
- // The TARGET repo root — the credit locators are made relative to it, so a phantom-scenario row's
259
- // where-to-fix (ruling Q5) stays portable in a banked artefact. Resolved ONCE here and threaded,
260
- // which is also what keeps `execFileSync` and `process.cwd()` out of the engine.
261
130
  const root = resolveTargetRoot({ features: opts.features });
262
- // `--ci` names a workflow FILE or a DIRECTORY of them, and defaults to the directory
263
- // (@SCN-USG-006, 3F-2238): a pipeline split across files enforces its checks just as one
264
- // file does, and reading only ci.yml both invents watermelons for named checks enforced
265
- // elsewhere and drops uncited ones off the books entirely. Path semantics carry the
266
- // file-vs-directory distinction, so one flag stays one flag and an explicit file is still
267
- // a narrowing (@SCN-USG-008).
268
- const ciPath = opts.ci ?? ".github/workflows";
269
- // RESOLVED ONCE, READ TWICE (@SCN-USG-015). The engine takes anonymous source strings and holds
270
- // no filesystem, so the mapping from a step's source index back to the workflow file it came
271
- // from is made HERE, where the paths are already in hand — `ciSourcePaths.ts` is explicit that
272
- // keeping the core that way is the point. Resolving the list twice would be two chances for the
273
- // bytes and the names to fall out of the order they are paired by.
131
+ const ciPath = opts.ci ?? DEFAULT_CI_PATH;
274
132
  const ciPaths = ciSourcePathsOf(ciPath);
133
+ const corpusFiles = balanceCorpus(opts.features).map((f) => ({
134
+ slug: f.slug,
135
+ featureCode: f.featureCode,
136
+ source: readFileSync(f.path, "utf8"),
137
+ }));
275
138
  return evidenceReconciliationFromSources({
276
- // The corpus's bytes, read ONCE at this impure edge (3F-3088). The engine composes the three
277
- // readings a citing run needs from this one array, so they cannot be assembled from two
278
- // discoveries that disagree about the corpus (@SCN-LNT-009).
279
- featureSources: featureSources(opts.features),
280
- // The untagged half by its `<feature> : <scenario>` locator — parsed here because the locator
281
- // names the FILE, which the discovery knows and a source string does not.
139
+ featureSources: corpusFiles.map((f) => f.source),
140
+ featureFiles: corpusFiles.map(({ slug, featureCode }) => ({ slug, featureCode })),
282
141
  untaggedScenarios: untaggedScenariosFromFeatures(opts.features),
283
142
  packageJson,
284
143
  ciSources: ciPaths.map((path) => readFileSync(path, "utf8")),
285
- // THE FILE, NOT THE PATH. A `notChecks` entry opens with the workflow's own name — `ci.yml`,
286
- // `release.yml` — which is what a consumer reads at the head of their run log and the only
287
- // half of the entry that does not come out of the workflow's own text. The directory it sits
288
- // in is the caller's `--ci`, not the consumer's subject, so it is not part of what they write.
289
144
  ciSourceNames: ciPaths.map((path) => basename(path)),
290
- // The CONVERTED checks' credit stream, read at this edge like every other report. Absent it is
291
- // undefined rather than empty, and that distinction reaches the reader (@SCN-LNT-027): "no
292
- // record" is a run nobody recorded, "a record holding nothing" is a run that recorded none of
293
- // these steps, and those are opposite facts.
294
- // STRUCTURALLY, NOT BY NAME: this module travels in the tarball and reaches core through the PUBLIC barrel alone (3F-3086), and
295
- // `CiStepRecord` is not on it. The shape is the document's, which is the consumer's, so what is
296
- // written here is what the file holds rather than a contract owed to anyone.
297
145
  ciStepRecord: readJsonIfPresent(opts.ciSteps),
298
- runtimeObservations: parseRuntimeObservations(opts, root),
299
- // The two things the engine's resolvers cannot hold, and each for its own reason: the
300
- // starter deferral set is the application layer's own policy, and how this tool is invoked to
301
- // reconcile is a fact about this product's command line. THE PREFIX SET USED TO SIT HERE AND IS GONE (@SCN-USG-004, 3F-3343): the
302
- // engine no longer decides a check's membership by its name, so there is no convention left
303
- // for this edge to own.
146
+ behaviouralObservations: parseBehaviouralObservations(opts, root),
304
147
  deferralTags: DEFAULT_DEFERRAL_TAGS,
305
148
  reconcilerInvocation: RECONCILER_INVOCATION,
306
149
  });
307
150
  }
308
- // The static-check kind-counts are no longer read as a parallel census (@SCN-USG-001 / 3F-2118):
309
- // the former `readStaticEvidenceObservationCounts` re-built the static check observations and
310
- // re-derived the running / uncited split from a second read of the CI config. That counting now
311
- // FOLDS onto the Ledger the reconciler already produced (`staticObservationTally`, an internal
312
- // domain fold the renderers apply), so it cannot drift from the verdict — it counts the SAME posted
313
- // static-check credits `runReconcile` banked. The census read is deleted, not relocated.
314
151
  //# sourceMappingURL=ingestQualityChecks.js.map