spec-controller 0.1.0-alpha.3 → 0.1.0-alpha.31

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 (88) hide show
  1. package/README.md +27 -5
  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 +45 -0
  5. package/dist/cli-allocate/cli.js.map +1 -0
  6. package/dist/cli-allocate/readHistory.d.ts +21 -0
  7. package/dist/cli-allocate/readHistory.d.ts.map +1 -0
  8. package/dist/cli-allocate/readHistory.js +97 -0
  9. package/dist/cli-allocate/readHistory.js.map +1 -0
  10. package/dist/cli-allocate/readParts.d.ts +3 -0
  11. package/dist/cli-allocate/readParts.d.ts.map +1 -0
  12. package/dist/cli-allocate/readParts.js +25 -0
  13. package/dist/cli-allocate/readParts.js.map +1 -0
  14. package/dist/cli-allocate/readTree.d.ts +17 -0
  15. package/dist/cli-allocate/readTree.d.ts.map +1 -0
  16. package/dist/cli-allocate/readTree.js +63 -0
  17. package/dist/cli-allocate/readTree.js.map +1 -0
  18. package/dist/cli-allocate/request.d.ts +11 -0
  19. package/dist/cli-allocate/request.d.ts.map +1 -0
  20. package/dist/cli-allocate/request.js +29 -0
  21. package/dist/cli-allocate/request.js.map +1 -0
  22. package/dist/cli-allocate/stderr.d.ts +14 -0
  23. package/dist/cli-allocate/stderr.d.ts.map +1 -0
  24. package/dist/cli-allocate/stderr.js +33 -0
  25. package/dist/cli-allocate/stderr.js.map +1 -0
  26. package/dist/cli-args.d.ts +70 -14
  27. package/dist/cli-args.d.ts.map +1 -1
  28. package/dist/cli-args.js +239 -18
  29. package/dist/cli-args.js.map +1 -1
  30. package/dist/cli-balance/cli.d.ts +18 -1
  31. package/dist/cli-balance/cli.d.ts.map +1 -1
  32. package/dist/cli-balance/cli.js +185 -161
  33. package/dist/cli-balance/cli.js.map +1 -1
  34. package/dist/cli-balance/emit/writer.d.ts +8 -23
  35. package/dist/cli-balance/emit/writer.d.ts.map +1 -1
  36. package/dist/cli-balance/emit/writer.js +24 -32
  37. package/dist/cli-balance/emit/writer.js.map +1 -1
  38. package/dist/cli-registry.d.ts +28 -0
  39. package/dist/cli-registry.d.ts.map +1 -1
  40. package/dist/cli-registry.js +39 -34
  41. package/dist/cli-registry.js.map +1 -1
  42. package/dist/cli.d.ts +4 -5
  43. package/dist/cli.d.ts.map +1 -1
  44. package/dist/cli.js +26 -12
  45. package/dist/cli.js.map +1 -1
  46. package/dist/deferralTags.d.ts +2 -2
  47. package/dist/deferralTags.js +2 -2
  48. package/dist/host.d.ts +25 -1
  49. package/dist/host.d.ts.map +1 -1
  50. package/dist/host.js +119 -15
  51. package/dist/host.js.map +1 -1
  52. package/dist/ingest/gherkinValidation.d.ts +9 -12
  53. package/dist/ingest/gherkinValidation.d.ts.map +1 -1
  54. package/dist/ingest/gherkinValidation.js +9 -12
  55. package/dist/ingest/gherkinValidation.js.map +1 -1
  56. package/dist/ingest/ingestQualityChecks.d.ts +3 -49
  57. package/dist/ingest/ingestQualityChecks.d.ts.map +1 -1
  58. package/dist/ingest/ingestQualityChecks.js +27 -120
  59. package/dist/ingest/ingestQualityChecks.js.map +1 -1
  60. package/dist/ingest/ingestScenarios.d.ts +8 -14
  61. package/dist/ingest/ingestScenarios.d.ts.map +1 -1
  62. package/dist/ingest/ingestScenarios.js +32 -55
  63. package/dist/ingest/ingestScenarios.js.map +1 -1
  64. package/dist/outputLocation.d.ts +15 -0
  65. package/dist/outputLocation.d.ts.map +1 -0
  66. package/dist/outputLocation.js +39 -0
  67. package/dist/outputLocation.js.map +1 -0
  68. package/dist/run-management/keptRun.d.ts +14 -19
  69. package/dist/run-management/keptRun.d.ts.map +1 -1
  70. package/dist/run-management/keptRun.js +15 -20
  71. package/dist/run-management/keptRun.js.map +1 -1
  72. package/package.json +2 -7
  73. package/dist/corpus/cli.d.ts +0 -34
  74. package/dist/corpus/cli.d.ts.map +0 -1
  75. package/dist/corpus/cli.js +0 -128
  76. package/dist/corpus/cli.js.map +0 -1
  77. package/dist/mutation-ratchet/index.d.ts +0 -46
  78. package/dist/mutation-ratchet/index.d.ts.map +0 -1
  79. package/dist/mutation-ratchet/index.js +0 -46
  80. package/dist/mutation-ratchet/index.js.map +0 -1
  81. package/dist/mutation-ratchet/record.d.ts +0 -178
  82. package/dist/mutation-ratchet/record.d.ts.map +0 -1
  83. package/dist/mutation-ratchet/record.js +0 -314
  84. package/dist/mutation-ratchet/record.js.map +0 -1
  85. package/dist/mutation-ratchet/report.d.ts +0 -109
  86. package/dist/mutation-ratchet/report.d.ts.map +0 -1
  87. package/dist/mutation-ratchet/report.js +0 -156
  88. package/dist/mutation-ratchet/report.js.map +0 -1
package/dist/host.js CHANGED
@@ -12,6 +12,8 @@
12
12
  *
13
13
  * @SCN-RMG-004 — features/run-management.feature.
14
14
  */
15
+ import { equalsFormRefusal, givenTwiceRefusal, isEqualsForm, isMissingValue, missingValueRefusal, valueFlagsOf, } from "./cli-args.js";
16
+ import { cliRegistry } from "./cli-registry.js";
15
17
  /**
16
18
  * Parse the host argv into its leading global options (`--store`, `--run-id`) and the
17
19
  * command + its clean args. Global options are recognised ONLY before the command: the
@@ -20,26 +22,101 @@
20
22
  * --features f` yields the store + `balance` + `[--features, f]`, while `balance --store
21
23
  * /s` leaves `--store` in the command's args (a `--store` AFTER the command is the
22
24
  * command's own arg, never a host store directive) — the "global before command" contract.
25
+ * A modifier with nothing after it is refused (@SCN-CLI-067); an empty argv is the bare help.
23
26
  */
24
- export function parseHostInvocation(argv) {
25
- let store;
26
- let runId;
27
+ export function parseHostInvocation(argv, globalFlags = cliRegistry.global, commands = cliRegistry.commands.map((command) => command.name)) {
28
+ // The modifiers and the values they take are the registry's global scope, never a list kept here
29
+ // (@SCN-CLI-059): a modifier added later is read, and refused without its value, by construction.
30
+ const modifiers = valueFlagsOf(globalFlags);
31
+ const values = new Map();
27
32
  let i = 0;
28
33
  for (; i < argv.length; i++) {
29
- const token = argv[i];
30
- if (token === "--store" && i + 1 < argv.length) {
31
- store = argv[i + 1];
32
- i++;
33
- continue;
34
+ const token = argv[i] ?? "";
35
+ const modifier = modifiers.get(token);
36
+ if (modifier === undefined) {
37
+ const refusal = leadingFlagRefusal(token, modifiers, globalFlags);
38
+ if (refusal !== undefined)
39
+ return { commandArgs: [], refusal };
40
+ break; // first non-modifier token is the command
34
41
  }
35
- if (token === "--run-id" && i + 1 < argv.length) {
36
- runId = argv[i + 1];
37
- i++;
38
- continue;
39
- }
40
- break; // first non-global-option token is the command
42
+ const value = argv[i + 1];
43
+ const refusal = modifierRefusal(token, modifier, argv.slice(i + 1, i + 3), { values, commands });
44
+ if (refusal !== undefined)
45
+ return { commandArgs: [], refusal };
46
+ values.set(modifier.name, value ?? "");
47
+ i++;
48
+ }
49
+ if (i === argv.length && values.size > 0)
50
+ return { commandArgs: [], refusal: noCommandRefusal([...values.keys()]) };
51
+ return {
52
+ store: values.get("--store"),
53
+ runId: values.get("--run-id"),
54
+ command: argv[i],
55
+ commandArgs: argv.slice(i + 1),
56
+ };
57
+ }
58
+ /**
59
+ * The refusal a modifier earns where it stands, else undefined; `ahead` is its value and the token
60
+ * after it. Given twice, it is never read as its last value (@SCN-CLI-064): `--store A --store B` used
61
+ * to store under B without a word. Given no value, it is never handed the next token (@SCN-CLI-059):
62
+ * `--store --run-id r1` used to store under a directory called `--run-id`. Given the command as its
63
+ * value with no command after it, it has eaten the command (@SCN-CLI-066): `--store balance
64
+ * --features f` used to blame `--features`, and `--store balance` alone answered help at exit 0. A
65
+ * command name followed by the command is still a value: `--store balance balance …` is read.
66
+ */
67
+ function modifierRefusal(token, modifier, [value, after], read) {
68
+ if (read.values.has(modifier.name))
69
+ return givenTwiceRefusal(token, "spec-controller");
70
+ if (isMissingValue(value))
71
+ return missingValueRefusal(token, modifier, "spec-controller");
72
+ if (read.commands.includes(value ?? "") && (after === undefined || after.startsWith("-"))) {
73
+ return commandAsValueRefusal(token, modifier, value ?? "");
41
74
  }
42
- return { store, runId, command: argv[i], commandArgs: argv.slice(i + 1) };
75
+ return undefined;
76
+ }
77
+ /**
78
+ * The refusal for host modifiers with nothing after them: every modifier given, in one line, and the
79
+ * fix (@SCN-CLI-067). Given last, `--store <root>` used to answer the top-level help at exit 0 having
80
+ * run nothing, a false green for a CI step that lost its command word (3F-3686). Help after a
81
+ * modifier is a command here, and a bare `spec-controller` never reaches this.
82
+ */
83
+ function noCommandRefusal(given) {
84
+ return (`No command followed the host modifiers given (${given.join(", ")}), so nothing ran: ` +
85
+ `a host modifier needs a command after it, or --help.\n` +
86
+ `Run 'spec-controller --help' to see the commands and host modifiers this version accepts.`);
87
+ }
88
+ /** The refusal for a modifier that took the command as its value: the modifier, the command, and the fix. */
89
+ function commandAsValueRefusal(token, modifier, command) {
90
+ return (`The ${token} host modifier took '${command}', a command, as its value, and left no command to run.\n` +
91
+ `Give ${token} its own value, as in 'spec-controller ${token} ${modifier.arg ?? ""} ${command} …'. ` +
92
+ `Run 'spec-controller --help' to see the commands and host modifiers this version accepts.`);
93
+ }
94
+ /**
95
+ * The refusal a leading token that is no modifier earns, else undefined (it is the command). A
96
+ * modifier written `--store=<root>` is a form that is not read (@SCN-CLI-065), never a flag out of
97
+ * place; any other flag here is out of place, never a command (@SCN-CLI-060): `--exit-zero balance`
98
+ * used to answer `unknown command "--exit-zero"`. The top-level help stays where it was.
99
+ */
100
+ function leadingFlagRefusal(token, modifiers, globalFlags) {
101
+ if (isEqualsForm(token))
102
+ return equalsFormRefusal(token, modifiers, "spec-controller");
103
+ if (isFlagOutOfPlace(token))
104
+ return flagBeforeCommandRefusal(token, globalFlags);
105
+ return undefined;
106
+ }
107
+ /** The help the host answers in the command's place, which is the one flag it takes there. */
108
+ const TOP_LEVEL_HELP = new Set(["--help", "-h"]);
109
+ /** True for a flag-shaped token that is not the top-level help. */
110
+ function isFlagOutOfPlace(token) {
111
+ return token.length > 1 && token.startsWith("-") && !TOP_LEVEL_HELP.has(token);
112
+ }
113
+ /** The refusal for a flag given before the command: the flag, what goes there, and where it goes. */
114
+ function flagBeforeCommandRefusal(flag, globalFlags) {
115
+ const modifiers = globalFlags.map((modifier) => modifier.name).join(", ");
116
+ return (`The ${flag} flag was given before the command. Before the command, spec-controller takes ` +
117
+ `only its host modifiers (${modifiers}); a command's own flags go after it, as in ` +
118
+ `'spec-controller <command> ${flag}'.\n` +
119
+ `Run 'spec-controller --help' to see the commands and host modifiers this version accepts.`);
43
120
  }
44
121
  /**
45
122
  * Build the wrapped command argv: the command's clean args followed by a repeated
@@ -66,4 +143,31 @@ export function peekFlag(argv, name) {
66
143
  }
67
144
  return undefined;
68
145
  }
146
+ /**
147
+ * Answer a command the registry does not carry. It is given the command token and the rendered
148
+ * help, never the command's args, so no flag after the command can change the answer.
149
+ *
150
+ * An unknown command is a usage fault, so it exits 2, the published usage code
151
+ * (packages/spec-controller/README.md), which `--exit-zero` never masks (3F-3649).
152
+ *
153
+ * @SCN-CLI-056 @SCN-CLI-057 — features/cli-balance.feature.
154
+ */
155
+ export function answerUnknownCommand(command, help) {
156
+ return {
157
+ exitCode: 2,
158
+ stderr: `spec-controller: unknown command "${command}".\n\n${help}\n`,
159
+ };
160
+ }
161
+ /** The refusal for host modifiers given before a command that takes none, else undefined. */
162
+ export function modifiersNotTakenRefusal(inv, commands = cliRegistry.commands) {
163
+ const entry = commands.find((command) => command.name === inv.command);
164
+ if (entry?.acceptsHostModifiers !== false)
165
+ return undefined;
166
+ const given = [inv.store === undefined ? [] : ["--store"], inv.runId === undefined ? [] : ["--run-id"]].flat();
167
+ if (given.length === 0)
168
+ return undefined;
169
+ return (`spec-controller: usage error — '${entry.name}' takes no host modifier, and was given ${given.join(", ")}: ` +
170
+ `it answers a query and keeps no run.\n` +
171
+ `Run 'spec-controller ${entry.name} …' without them.`);
172
+ }
69
173
  //# sourceMappingURL=host.js.map
package/dist/host.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAcH;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAuB;IACzD,IAAI,KAAyB,CAAC;IAC9B,IAAI,KAAyB,CAAC;IAC9B,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACtB,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YAC/C,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACpB,CAAC,EAAE,CAAC;YACJ,SAAS;QACX,CAAC;QACD,IAAI,KAAK,KAAK,UAAU,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YAChD,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACpB,CAAC,EAAE,CAAC;YACJ,SAAS;QACX,CAAC;QACD,MAAM,CAAC,+CAA+C;IACxD,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;AAC5E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,WAA8B,EAAE,WAA8B;IAC5F,OAAO,CAAC,GAAG,WAAW,EAAE,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;AAChF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAuB,EAAE,IAAY;IAC5D,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;IACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YACrB,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1B,OAAO,KAAK,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;QAC5E,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
1
+ {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"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"}
@@ -23,11 +23,12 @@
23
23
  * PURE: `string → parse errors`. No fs, no process — the file READ lives beside it at the ingest
24
24
  * edge, as it does for every other input.
25
25
  *
26
- * WHY IT LIVES IN THE APP, NOT CORE. Core's barrel is the FROZEN v1 contract surface, guarded
27
- * both directions by @SCN-API-001 — a symbol added there reds CI. This validator is an INGEST
28
- * concern (it decides whether an input can be read at all), not part of the reconciliation
29
- * contract downstream consumers build on, so it belongs with the other readers at the app edge
30
- * and the frozen surface stays untouched.
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.
31
32
  */
32
33
  /** A Gherkin parse failure: the file could not be read as Gherkin at all. */
33
34
  export interface GherkinParseError {
@@ -73,13 +74,9 @@ export declare class UnreadableFeatureCorpusError extends Error {
73
74
  * refusal means for it.
74
75
  *
75
76
  * WHY A TYPED ERROR RATHER THAN AN EMPTY CORPUS. The obvious fix is for `discoverFeatures` to hand
76
- * back `[]` when the directory is not there. Measured, that would ship a NEW false green in a
77
- * second command: `spec-controller tags` reaches the same reader through `readCorpusInputs`, whose
78
- * GENERIC catch is the only thing turning an absent corpus — and, since `tags` carries no
79
- * `checkInputsExist`, a supplied-but-absent `--features` too — into `The corpus could not be
80
- * read: …` at exit 2. Tolerance at the reader turns both into `tags: OK …` at exit 0, inside the
81
- * slice whose whole purpose is killing a vacuous certification. Typed, both survive untouched:
82
- * this IS an `Error`, so it still lands in that generic catch.
77
+ * back `[]` when the directory is not there, and that would decide for every reader that absence
78
+ * is empty. Typed, each caller decides: `balance` reads it as no corpus (@SCN-CLI-017), and any
79
+ * other reader still meets a refusal. (The second command this guarded, `tags`, left at 3F-3635.)
83
80
  *
84
81
  * IT CARRIES THE DIRECTORY because the report is owed the name of what it examined — "I found no
85
82
  * record" and "I found no regression" must never wear one word (3F-2947), and a consumer told
@@ -1 +1 @@
1
- {"version":3,"file":"gherkinValidation.d.ts","sourceRoot":"","sources":["../../src/ingest/gherkinValidation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAKH,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,qFAAqF;IACrF,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAa/E;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,4BAA6B,SAAQ,KAAK;IACrD,QAAQ,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;gBAElC,MAAM,EAAE,SAAS,iBAAiB,EAAE;CAUjD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,8FAA8F;IAC9F,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,SAAS,EAAE,MAAM;CAK9B"}
1
+ {"version":3,"file":"gherkinValidation.d.ts","sourceRoot":"","sources":["../../src/ingest/gherkinValidation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAKH,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,qFAAqF;IACrF,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAa/E;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,4BAA6B,SAAQ,KAAK;IACrD,QAAQ,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;gBAElC,MAAM,EAAE,SAAS,iBAAiB,EAAE;CAUjD;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,8FAA8F;IAC9F,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,SAAS,EAAE,MAAM;CAK9B"}
@@ -23,11 +23,12 @@
23
23
  * PURE: `string → parse errors`. No fs, no process — the file READ lives beside it at the ingest
24
24
  * edge, as it does for every other input.
25
25
  *
26
- * WHY IT LIVES IN THE APP, NOT CORE. Core's barrel is the FROZEN v1 contract surface, guarded
27
- * both directions by @SCN-API-001 — a symbol added there reds CI. This validator is an INGEST
28
- * concern (it decides whether an input can be read at all), not part of the reconciliation
29
- * contract downstream consumers build on, so it belongs with the other readers at the app edge
30
- * and the frozen surface stays untouched.
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.
31
32
  */
32
33
  import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
33
34
  import { IdGenerator } from "@cucumber/messages";
@@ -88,13 +89,9 @@ export class UnreadableFeatureCorpusError extends Error {
88
89
  * refusal means for it.
89
90
  *
90
91
  * WHY A TYPED ERROR RATHER THAN AN EMPTY CORPUS. The obvious fix is for `discoverFeatures` to hand
91
- * back `[]` when the directory is not there. Measured, that would ship a NEW false green in a
92
- * second command: `spec-controller tags` reaches the same reader through `readCorpusInputs`, whose
93
- * GENERIC catch is the only thing turning an absent corpus — and, since `tags` carries no
94
- * `checkInputsExist`, a supplied-but-absent `--features` too — into `The corpus could not be
95
- * read: …` at exit 2. Tolerance at the reader turns both into `tags: OK …` at exit 0, inside the
96
- * slice whose whole purpose is killing a vacuous certification. Typed, both survive untouched:
97
- * this IS an `Error`, so it still lands in that generic catch.
92
+ * back `[]` when the directory is not there, and that would decide for every reader that absence
93
+ * is empty. Typed, each caller decides: `balance` reads it as no corpus (@SCN-CLI-017), and any
94
+ * other reader still meets a refusal. (The second command this guarded, `tags`, left at 3F-3635.)
98
95
  *
99
96
  * IT CARRIES THE DIRECTORY because the report is owed the name of what it examined — "I found no
100
97
  * record" and "I found no regression" must never wear one word (3F-2947), and a consumer told
@@ -1 +1 @@
1
- {"version":3,"file":"gherkinValidation.js","sourceRoot":"","sources":["../../src/ingest/gherkinValidation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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;;;;;;;;;;;;;GAaG;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"}
@@ -15,25 +15,6 @@
15
15
  import { type GherkinParseError } from "./gherkinValidation.js";
16
16
  import { type EvidenceObligations } from "@3f-consulting/spec-controller-core";
17
17
  import type { EvidenceReconciliation } from "@3f-consulting/spec-controller-core";
18
- /**
19
- * A reader over ONE resolution pass's records.
20
- *
21
- * EACH RECORD IS OPENED ONCE, WHICH IS WHY THIS IS A CLOSURE AND NOT A FUNCTION PER REFERENCE. A
22
- * manifest may name several gates of one record, and a reader opening it per entry would let two
23
- * references answer for two different states of the same file — a rewrite landing mid-pass, a gate
24
- * present for one and gone for the next. One read makes the pass's bars one reading of one record.
25
- *
26
- * THE REFUSAL IS REMEMBERED TOO. Cacheing only the successes would re-open an absent or unreadable
27
- * record per reference, which is the same disagreement by the other door — and the likelier door,
28
- * since a record being rewritten is unreadable for exactly the window that matters.
29
- *
30
- * THE SUBTRACTION IS THE RECORD'S, NOT THIS EDGE'S. `killFloorOf` spends the allowance; an edge
31
- * computing `detected - slack` itself would be a second place holding a fact about the record.
32
- */
33
- export declare function killFloorReaderFor(root: string): (reference: {
34
- readonly mutationGate: string;
35
- readonly recordFile?: string;
36
- }) => number | null;
37
18
  /**
38
19
  * The reconciler's resolved input paths — the CLI options threaded to the readers.
39
20
  * Every path is optional; a missing result file is tolerated (its evidence is
@@ -48,12 +29,6 @@ export interface ReconcileOptions {
48
29
  cucumber?: string;
49
30
  ci?: string;
50
31
  packageJson?: string;
51
- /**
52
- * The measurement artefact (@SCN-CIP-003) — the number each measured check posted, and
53
- * the credit side of every measured fitness check. Optional like the other result files: absent,
54
- * it contributes nothing, and each measured obligation it would have answered reads `missing`.
55
- */
56
- measurements?: string;
57
32
  /**
58
33
  * The CI RUN'S OWN RECORD of what each of its steps did (@SCN-LNT-021, @SCN-LNT-028) — the credit
59
34
  * side of every CONVERTED fitness check, written by the consumer's own pipeline and by neither
@@ -85,26 +60,6 @@ export interface ReconcileOptions {
85
60
  * refusal is not ours to interpret and rethrows.
86
61
  */
87
62
  export declare function featureCorpusParseErrors(featuresDir: string | undefined): GherkinParseError[];
88
- /**
89
- * The `@SCN` ids the feature corpus CARRIES MORE THAN ONCE (@SCN-LDG-020's guardrail, 3F-1775).
90
- *
91
- * An `@SCN` identifies exactly ONE scenario — that is the whole basis of attributing evidence to
92
- * it. If two scenarios carry the same id, a test citing it could be proving EITHER, and the tool
93
- * cannot know which: the corpus is ambiguous, and any verdict over it is a guess.
94
- *
95
- * WHY THIS EXISTS. Tag inheritance made this reachable: an `@SCN` hoisted to a `Feature:`/`Rule:`
96
- * is inherited by every scenario beneath it (cucumber does this, so we do — parity). Left
97
- * unhandled, ONE passing test then marked EVERY scenario sharing that id `balanced`, including
98
- * scenarios nothing proves — a BALANCED-WHEN-BROKEN run, exit 0. A new silent false-green,
99
- * introduced by the very fix that closed the last one.
100
- *
101
- * Refused at the edge (exit 2) rather than reconciled, under the Rule already shipped for the
102
- * observed side and the corrupt corpus (@SCN-CLI-014/015/016): an input the tool cannot read is a
103
- * usage error, never a verdict. Whether hoisting an `@SCN` is an authoring fault worth a richer
104
- * diagnostic is the deferred policy question (3F-1774's sibling); refusing to reconcile a corpus we
105
- * cannot read is not a policy — it is the floor.
106
- */
107
- export declare function featureCorpusDuplicateScnIds(featuresDir: string | undefined): string[];
108
63
  /**
109
64
  * Read the target's `feature-code → slug` map over the same `discoverFeatures` corpus
110
65
  * (@SCN-RPT-006) — each coded feature file's `@<FFF>` header (`featureCode`)
@@ -113,10 +68,9 @@ export declare function featureCorpusDuplicateScnIds(featuresDir: string | undef
113
68
  * the balance, threaded to `renderMarkdown` (via `writeOutputs`) as the 5th sibling arg
114
69
  * (markdown-only — the by-feature cut is markdown-only, so it is NOT serialised to JSON).
115
70
  * Uncoded files (`featureCode === null`) are omitted, so the by-feature emitter falls
116
- * back to the bare `FFF` for any id whose code no file registers. One-prefix-one-file holds
117
- * by invariant (`corpusFaults` raises `duplicate-feature-code`, @SCN-STG-010), so the map is
118
- * unambiguous. That invariant was `lintFeatureCodes`' until 3F-3313; it is the shipped command's
119
- * now, which is the first time a CONSUMER of this reader has it too.
71
+ * back to the bare `FFF` for any id whose code no file registers. When two files carry one
72
+ * code the map keeps the last, and that run lists `duplicate-feature-code` under Scenario code
73
+ * errors and fails (@SCN-STG-010, 3F-3635), so a report whose map is ambiguous never passes.
120
74
  */
121
75
  export declare function readFeatureNames(featuresDir?: string): Map<string, string>;
122
76
  /**
@@ -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;AA+B7C,OAAO,KAAK,EAIV,sBAAsB,EACvB,MAAM,qCAAqC,CAAC;AAwD7C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,GAMX,CAAC,SAAS,EAAE;IAAE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,KAAK,MAAM,GAAG,IAAI,CAc/F;AAED;;;;;;;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;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;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;AA2CD,wBAAgB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,sBAAsB,CAgE3E"}
1
+ {"version":3,"file":"ingestQualityChecks.d.ts","sourceRoot":"","sources":["../../src/ingest/ingestQualityChecks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAaH,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;AA4B7C;;;;;;;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;;;;;;;;;;;GAWG;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,CAuB9E;AAyCD,wBAAgB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,sBAAsB,CAoE3E"}
@@ -13,10 +13,10 @@
13
13
  * `package.json` to observe each fitness check the corpus cites.
14
14
  */
15
15
  import { readFileSync, existsSync } from "node:fs";
16
- import { basename, join } from "node:path";
16
+ import { basename } from "node:path";
17
17
  import { ciSourcePathsOf } from "./ciSourcePaths.js";
18
18
  import { discoverFeatures } from "./ingestScenarios.js";
19
- import { parseScenarios, countScnOccurrences, parseUntaggedScenarios, proseOnlyCodes, } from "@3f-consulting/spec-controller-core";
19
+ import { parseScenarios, countScnOccurrences, parseUntaggedScenarios, proseOnlyCodes, sharedScnCodesOf, } from "@3f-consulting/spec-controller-core";
20
20
  import { MissingFeatureCorpusError, UnreadableFeatureCorpusError, } from "./gherkinValidation.js";
21
21
  import { DEFAULT_DEFERRAL_TAGS } from "../deferralTags.js";
22
22
  import { evidenceObligations, } from "@3f-consulting/spec-controller-core";
@@ -30,12 +30,6 @@ import { parseCucumberEvidenceObservations, } from "@3f-consulting/spec-controll
30
30
  // their CALLER. What is left here is the reads: this edge opens the files and hands the bytes over.
31
31
  import { evidenceReconciliationFromSources } from "@3f-consulting/spec-controller-core";
32
32
  import { resolveTargetRoot } from "../run-management/resolveRunInputs.js";
33
- // The record reader, reached for the RECORD half of a bar that names a gate (@SCN-MUT-012): its
34
- // conventional record name, its parser, and the floor a gate's own figures imply. Through the
35
- // barrel, which is the surface a caller holds. (The directory is still spelled `mutation-ratchet`
36
- // and the ratchet it was named for is deleted — 3F-3337 left the name standing rather than move a
37
- // module inside a delete; filed as a finding.)
38
- import { gateRecordFor, killFloorOf, MalformedRecordError, parseGateRecords, RECORD_FILE, } from "../mutation-ratchet/index.js";
39
33
  /**
40
34
  * The per-input FALLBACK quality-check for each JS-ecosystem result source — the
41
35
  * source→quality-check mapping, SET at the CLI edge rather than hardcoded in the core
@@ -54,74 +48,12 @@ const CUCUMBER_QUALITY_CHECK = "integration";
54
48
  * publishes and `balance` is the command that reconciles; the engine holds neither, and a core
55
49
  * that assumed them would be asserting something about a command line it never sees.
56
50
  *
57
- * BOTH TOKENS, AND THE SECOND IS NOT DECORATION. This repository's own pipeline runs
58
- * `pnpm spec-controller tags` beside its balance step — the same binary, doing something that is
59
- * an ordinary step of the pipeline being reconciled — so an invocation naming the binary alone
60
- * would take that step off the books. The comparison is whole, adjacent shell tokens
51
+ * BOTH TOKENS, AND THE SECOND IS NOT DECORATION. A step running the same binary for anything but
52
+ * `balance` is an ordinary step of the pipeline being reconciled, so an invocation naming the
53
+ * binary alone would take that step off the books. The comparison is whole, adjacent shell tokens
61
54
  * (`parse/commandRuns`), so this names the reconciliation and nothing else the tool can do.
62
55
  */
63
56
  const RECONCILER_INVOCATION = "spec-controller balance";
64
- /**
65
- * A refusal met with an absent bar rather than let out of this edge.
66
- *
67
- * NOTHING IS THROWN OUT OF HERE, and that is the stance rather than caution. A refusal that escapes
68
- * costs the whole run its verdict over one bad character in a file nobody may even be reading — a
69
- * fault that is loud nowhere, where an absent bar is loud on the ledger. It is `thresholdsOf`'s own
70
- * stance for a malformed entry, applied to the half of a bar that now lives in a second file.
71
- *
72
- * TWO CLASSES AND NO MORE. The record library normalises every fault it can see — the YAML parser's
73
- * own included — into `MalformedRecordError` precisely so a caller catches all of them by catching
74
- * one; beside it sits the read that never got as far as a record. A bare catch would swallow a
75
- * defect in this edge as well, and answer it with the same silent absence.
76
- */
77
- function orNoBar(read) {
78
- try {
79
- return read();
80
- }
81
- catch (cause) {
82
- if (cause instanceof MalformedRecordError || isReadRefusal(cause))
83
- return null;
84
- throw cause;
85
- }
86
- }
87
- /** A filesystem refusal — the record was not there, or could not be opened. */
88
- function isReadRefusal(cause) {
89
- return cause instanceof Error && typeof cause.code === "string";
90
- }
91
- /**
92
- * A reader over ONE resolution pass's records.
93
- *
94
- * EACH RECORD IS OPENED ONCE, WHICH IS WHY THIS IS A CLOSURE AND NOT A FUNCTION PER REFERENCE. A
95
- * manifest may name several gates of one record, and a reader opening it per entry would let two
96
- * references answer for two different states of the same file — a rewrite landing mid-pass, a gate
97
- * present for one and gone for the next. One read makes the pass's bars one reading of one record.
98
- *
99
- * THE REFUSAL IS REMEMBERED TOO. Cacheing only the successes would re-open an absent or unreadable
100
- * record per reference, which is the same disagreement by the other door — and the likelier door,
101
- * since a record being rewritten is unreadable for exactly the window that matters.
102
- *
103
- * THE SUBTRACTION IS THE RECORD'S, NOT THIS EDGE'S. `killFloorOf` spends the allowance; an edge
104
- * computing `detected - slack` itself would be a second place holding a fact about the record.
105
- */
106
- export function killFloorReaderFor(root
107
- // The reader's shape is SPELLED here rather than imported, and that is the boundary working
108
- // rather than a duplication. `KillFloorReader` and the reference it takes are core-internal —
109
- // the registry-home question (increment D) still governs them — and this module is published, so
110
- // naming them by their `./internal` types would put an unpublished specifier back in the tarball
111
- // for a type alone. Structural typing is what makes the closure the engine's parameter accepts.
112
- ) {
113
- const opened = new Map();
114
- return (reference) => {
115
- const recordFile = reference.recordFile ?? RECORD_FILE;
116
- if (!opened.has(recordFile)) {
117
- opened.set(recordFile, orNoBar(() => parseGateRecords(readFileSync(join(root, recordFile), "utf8"), recordFile)));
118
- }
119
- const records = opened.get(recordFile);
120
- if (records == null)
121
- return null;
122
- return orNoBar(() => killFloorOf(gateRecordFor(reference.mutationGate, records, recordFile)));
123
- };
124
- }
125
57
  /** Read + JSON-parse a file if present, else return undefined. */
126
58
  function readJsonIfPresent(path) {
127
59
  if (!path || !existsSync(path))
@@ -142,7 +74,7 @@ function readJsonIfPresent(path) {
142
74
  * this by hand is five chances for one to stay intolerant, and a run that reports on a corpus for
143
75
  * four of its readings and throws on the fifth is worse than one that throws. The translation is
144
76
  * also why `discoverFeatures` is not the place to be tolerant — see its own note, and
145
- * `MissingFeatureCorpusError`'s: `tags` reaches the same reader and keeps refusing.
77
+ * `MissingFeatureCorpusError`'s.
146
78
  *
147
79
  * IT IS `[]`, NOT A VERDICT. Zero rows is what @SCN-CLI-019 reads as NOTHING TO RECONCILE, and a
148
80
  * corpus-less target that still has uncited credits keeps its suspense row and its exit 1
@@ -217,35 +149,6 @@ export function featureCorpusParseErrors(featuresDir) {
217
149
  throw err;
218
150
  }
219
151
  }
220
- /**
221
- * The `@SCN` ids the feature corpus CARRIES MORE THAN ONCE (@SCN-LDG-020's guardrail, 3F-1775).
222
- *
223
- * An `@SCN` identifies exactly ONE scenario — that is the whole basis of attributing evidence to
224
- * it. If two scenarios carry the same id, a test citing it could be proving EITHER, and the tool
225
- * cannot know which: the corpus is ambiguous, and any verdict over it is a guess.
226
- *
227
- * WHY THIS EXISTS. Tag inheritance made this reachable: an `@SCN` hoisted to a `Feature:`/`Rule:`
228
- * is inherited by every scenario beneath it (cucumber does this, so we do — parity). Left
229
- * unhandled, ONE passing test then marked EVERY scenario sharing that id `balanced`, including
230
- * scenarios nothing proves — a BALANCED-WHEN-BROKEN run, exit 0. A new silent false-green,
231
- * introduced by the very fix that closed the last one.
232
- *
233
- * Refused at the edge (exit 2) rather than reconciled, under the Rule already shipped for the
234
- * observed side and the corrupt corpus (@SCN-CLI-014/015/016): an input the tool cannot read is a
235
- * usage error, never a verdict. Whether hoisting an `@SCN` is an authoring fault worth a richer
236
- * diagnostic is the deferred policy question (3F-1774's sibling); refusing to reconcile a corpus we
237
- * cannot read is not a policy — it is the floor.
238
- */
239
- export function featureCorpusDuplicateScnIds(featuresDir) {
240
- const seen = new Set();
241
- const duplicates = new Set();
242
- for (const scenario of scenariosFromFeatures(featuresDir)) {
243
- if (seen.has(scenario.scnId))
244
- duplicates.add(scenario.scnId);
245
- seen.add(scenario.scnId);
246
- }
247
- return [...duplicates].sort();
248
- }
249
152
  /**
250
153
  * Read the target's `feature-code → slug` map over the same `discoverFeatures` corpus
251
154
  * (@SCN-RPT-006) — each coded feature file's `@<FFF>` header (`featureCode`)
@@ -254,10 +157,9 @@ export function featureCorpusDuplicateScnIds(featuresDir) {
254
157
  * the balance, threaded to `renderMarkdown` (via `writeOutputs`) as the 5th sibling arg
255
158
  * (markdown-only — the by-feature cut is markdown-only, so it is NOT serialised to JSON).
256
159
  * Uncoded files (`featureCode === null`) are omitted, so the by-feature emitter falls
257
- * back to the bare `FFF` for any id whose code no file registers. One-prefix-one-file holds
258
- * by invariant (`corpusFaults` raises `duplicate-feature-code`, @SCN-STG-010), so the map is
259
- * unambiguous. That invariant was `lintFeatureCodes`' until 3F-3313; it is the shipped command's
260
- * now, which is the first time a CONSUMER of this reader has it too.
160
+ * back to the bare `FFF` for any id whose code no file registers. When two files carry one
161
+ * code the map keeps the last, and that run lists `duplicate-feature-code` under Scenario code
162
+ * errors and fails (@SCN-STG-010, 3F-3635), so a report whose map is ambiguous never passes.
261
163
  */
262
164
  export function readFeatureNames(featuresDir) {
263
165
  const map = new Map();
@@ -283,9 +185,11 @@ export function readEvidenceObligations(features) {
283
185
  let scenarioCount = 0;
284
186
  const untagged = [];
285
187
  const sources = [];
188
+ const files = [];
286
189
  for (const f of featureFiles) {
287
190
  const source = readFileSync(f.path, "utf8");
288
191
  sources.push(source);
192
+ files.push({ feature: `${f.slug}.feature`, source });
289
193
  raw += countScnOccurrences(source);
290
194
  scenarioCount += parseScenarios(source).length;
291
195
  for (const scenario of parseUntaggedScenarios(source)) {
@@ -295,7 +199,7 @@ export function readEvidenceObligations(features) {
295
199
  untagged.sort((a, b) => a.feature.localeCompare(b.feature) || a.scenario.localeCompare(b.scenario));
296
200
  // The prose-only codes are a CROSS-CORPUS difference (a code tagged in any file isn't
297
201
  // dangling), so it reads all sources at once — not per-file like the others.
298
- return evidenceObligations(raw, scenarioCount, untagged, proseOnlyCodes(sources));
202
+ return evidenceObligations(raw, scenarioCount, untagged, proseOnlyCodes(sources), sharedScnCodesOf(files));
299
203
  }
300
204
  /**
301
205
  * Parse the target's RUNTIME observations ONCE — the tag-derived, per-tier observation set
@@ -323,9 +227,8 @@ export function runReconcile(opts) {
323
227
  const pkgPath = opts.packageJson ?? "package.json";
324
228
  const packageJson = readJsonIfPresent(pkgPath) ?? {};
325
229
  // The TARGET repo root — the credit locators are made relative to it, so a phantom-scenario row's
326
- // where-to-fix (ruling Q5) stays portable in a banked artefact, and a bar naming a mutation gate
327
- // finds that target's record rather than this tool's own (@SCN-MUT-012). Resolved ONCE here and
328
- // threaded, which is also what keeps `execFileSync` and `process.cwd()` out of the engine.
230
+ // where-to-fix (ruling Q5) stays portable in a banked artefact. Resolved ONCE here and threaded,
231
+ // which is also what keeps `execFileSync` and `process.cwd()` out of the engine.
329
232
  const root = resolveTargetRoot({ features: opts.features });
330
233
  // `--ci` names a workflow FILE or a DIRECTORY of them, and defaults to the directory
331
234
  // (@SCN-USG-006, 3F-2238): a pipeline split across files enforces its checks just as one
@@ -340,11 +243,19 @@ export function runReconcile(opts) {
340
243
  // keeping the core that way is the point. Resolving the list twice would be two chances for the
341
244
  // bytes and the names to fall out of the order they are paired by.
342
245
  const ciPaths = ciSourcePathsOf(ciPath);
246
+ // The corpus, discovered and read ONCE: its bytes, and — paired by index — which file each came
247
+ // from and the header code it carries, for the scenario code errors (3F-3635).
248
+ const corpusFiles = balanceCorpus(opts.features).map((f) => ({
249
+ slug: f.slug,
250
+ featureCode: f.featureCode,
251
+ source: readFileSync(f.path, "utf8"),
252
+ }));
343
253
  return evidenceReconciliationFromSources({
344
254
  // The corpus's bytes, read ONCE at this impure edge (3F-3088). The engine composes the three
345
255
  // readings a citing run needs from this one array, so they cannot be assembled from two
346
256
  // discoveries that disagree about the corpus (@SCN-LNT-009).
347
- featureSources: featureSources(opts.features),
257
+ featureSources: corpusFiles.map((f) => f.source),
258
+ featureFiles: corpusFiles.map(({ slug, featureCode }) => ({ slug, featureCode })),
348
259
  // The untagged half by its `<feature> : <scenario>` locator — parsed here because the locator
349
260
  // names the FILE, which the discovery knows and a source string does not.
350
261
  untaggedScenarios: untaggedScenariosFromFeatures(opts.features),
@@ -355,26 +266,22 @@ export function runReconcile(opts) {
355
266
  // half of the entry that does not come out of the workflow's own text. The directory it sits
356
267
  // in is the caller's `--ci`, not the consumer's subject, so it is not part of what they write.
357
268
  ciSourceNames: ciPaths.map((path) => basename(path)),
358
- measurements: readJsonIfPresent(opts.measurements) ?? {},
359
269
  // The CONVERTED checks' credit stream, read at this edge like every other report. Absent it is
360
270
  // undefined rather than empty, and that distinction reaches the reader (@SCN-LNT-027): "no
361
271
  // record" is a run nobody recorded, "a record holding nothing" is a run that recorded none of
362
272
  // these steps, and those are opposite facts.
363
- // STRUCTURALLY, NOT BY NAME, for the reason `measurements` above is read that way: this module
364
- // travels in the tarball and reaches core through the PUBLIC barrel alone (3F-3086), and
273
+ // STRUCTURALLY, NOT BY NAME: this module travels in the tarball and reaches core through the PUBLIC barrel alone (3F-3086), and
365
274
  // `CiStepRecord` is not on it. The shape is the document's, which is the consumer's, so what is
366
275
  // written here is what the file holds rather than a contract owed to anyone.
367
276
  ciStepRecord: readJsonIfPresent(opts.ciSteps),
368
277
  runtimeObservations: parseRuntimeObservations(opts, root),
369
- // The three things the engine's resolvers cannot hold, and each for its own reason: the
370
- // starter deferral set is the application layer's own policy, how this tool is invoked to
371
- // reconcile is a fact about this product's command line, and the reader opens a file on the
372
- // target's tree. THE PREFIX SET USED TO SIT HERE AND IS GONE (@SCN-USG-004, 3F-3343): the
278
+ // The two things the engine's resolvers cannot hold, and each for its own reason: the
279
+ // starter deferral set is the application layer's own policy, and how this tool is invoked to
280
+ // 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
373
281
  // engine no longer decides a check's membership by its name, so there is no convention left
374
282
  // for this edge to own.
375
283
  deferralTags: DEFAULT_DEFERRAL_TAGS,
376
284
  reconcilerInvocation: RECONCILER_INVOCATION,
377
- killFloor: killFloorReaderFor(root),
378
285
  });
379
286
  }
380
287
  // The static-check kind-counts are no longer read as a parallel census (@SCN-USG-001 / 3F-2118):