spec-controller 0.1.0-alpha.1 → 0.1.0-alpha.10

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 (69) hide show
  1. package/README.md +112 -0
  2. package/dist/cli-args.d.ts +73 -0
  3. package/dist/cli-args.d.ts.map +1 -0
  4. package/dist/cli-args.js +114 -0
  5. package/dist/cli-args.js.map +1 -0
  6. package/dist/cli-balance/cli.d.ts +20 -20
  7. package/dist/cli-balance/cli.d.ts.map +1 -1
  8. package/dist/cli-balance/cli.js +221 -202
  9. package/dist/cli-balance/cli.js.map +1 -1
  10. package/dist/cli-balance/emit/writer.d.ts +1 -1
  11. package/dist/cli-registry.d.ts +35 -12
  12. package/dist/cli-registry.d.ts.map +1 -1
  13. package/dist/cli-registry.js +95 -36
  14. package/dist/cli-registry.js.map +1 -1
  15. package/dist/cli.d.ts +5 -3
  16. package/dist/cli.d.ts.map +1 -1
  17. package/dist/cli.js +9 -3
  18. package/dist/cli.js.map +1 -1
  19. package/dist/corpus/cli.d.ts +34 -0
  20. package/dist/corpus/cli.d.ts.map +1 -0
  21. package/dist/corpus/cli.js +125 -0
  22. package/dist/corpus/cli.js.map +1 -0
  23. package/dist/deferralTags.d.ts +3 -3
  24. package/dist/deferralTags.js +3 -3
  25. package/dist/ingest/gherkinValidation.d.ts +29 -5
  26. package/dist/ingest/gherkinValidation.d.ts.map +1 -1
  27. package/dist/ingest/gherkinValidation.js +33 -5
  28. package/dist/ingest/gherkinValidation.js.map +1 -1
  29. package/dist/ingest/ingestQualityChecks.d.ts +15 -25
  30. package/dist/ingest/ingestQualityChecks.d.ts.map +1 -1
  31. package/dist/ingest/ingestQualityChecks.js +87 -104
  32. package/dist/ingest/ingestQualityChecks.js.map +1 -1
  33. package/dist/ingest/ingestScenarios.d.ts +2 -1
  34. package/dist/ingest/ingestScenarios.d.ts.map +1 -1
  35. package/dist/ingest/ingestScenarios.js +32 -3
  36. package/dist/ingest/ingestScenarios.js.map +1 -1
  37. package/dist/run-management/resolveRunInputs.d.ts +24 -11
  38. package/dist/run-management/resolveRunInputs.d.ts.map +1 -1
  39. package/dist/run-management/resolveRunInputs.js +49 -13
  40. package/dist/run-management/resolveRunInputs.js.map +1 -1
  41. package/package.json +2 -2
  42. package/dist/mutation-ratchet/index.d.ts +0 -48
  43. package/dist/mutation-ratchet/index.d.ts.map +0 -1
  44. package/dist/mutation-ratchet/index.js +0 -48
  45. package/dist/mutation-ratchet/index.js.map +0 -1
  46. package/dist/mutation-ratchet/ratchet.d.ts +0 -129
  47. package/dist/mutation-ratchet/ratchet.d.ts.map +0 -1
  48. package/dist/mutation-ratchet/ratchet.js +0 -222
  49. package/dist/mutation-ratchet/ratchet.js.map +0 -1
  50. package/dist/mutation-ratchet/ratchetCli.d.ts +0 -57
  51. package/dist/mutation-ratchet/ratchetCli.d.ts.map +0 -1
  52. package/dist/mutation-ratchet/ratchetCli.js +0 -139
  53. package/dist/mutation-ratchet/ratchetCli.js.map +0 -1
  54. package/dist/mutation-ratchet/reconcile.d.ts +0 -82
  55. package/dist/mutation-ratchet/reconcile.d.ts.map +0 -1
  56. package/dist/mutation-ratchet/reconcile.js +0 -67
  57. package/dist/mutation-ratchet/reconcile.js.map +0 -1
  58. package/dist/mutation-ratchet/record.d.ts +0 -210
  59. package/dist/mutation-ratchet/record.d.ts.map +0 -1
  60. package/dist/mutation-ratchet/record.js +0 -330
  61. package/dist/mutation-ratchet/record.js.map +0 -1
  62. package/dist/mutation-ratchet/report.d.ts +0 -83
  63. package/dist/mutation-ratchet/report.d.ts.map +0 -1
  64. package/dist/mutation-ratchet/report.js +0 -148
  65. package/dist/mutation-ratchet/report.js.map +0 -1
  66. package/dist/mutation-ratchet/verdict.d.ts +0 -177
  67. package/dist/mutation-ratchet/verdict.d.ts.map +0 -1
  68. package/dist/mutation-ratchet/verdict.js +0 -387
  69. package/dist/mutation-ratchet/verdict.js.map +0 -1
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # spec-controller
2
+
3
+ **One command, `balance`, that reconciles the behaviour your scenarios name against the evidence that actually ran.**
4
+
5
+ You hand it your Gherkin scenarios and the reports your tools already produce — a Vitest report, a Cucumber report, your CI run's own record of what its steps did — and it prints one table: which scenarios have living evidence, which do not, and which evidence ran that no scenario asked for.
6
+
7
+ It runs nothing itself. It reads what your pipeline already wrote.
8
+
9
+ Requires Node ≥ 22. ESM only.
10
+
11
+ ## Install
12
+
13
+ ```
14
+ npm install -g spec-controller@alpha
15
+ ```
16
+
17
+ Or without installing anything:
18
+
19
+ ```
20
+ npx spec-controller@alpha balance --help
21
+ ```
22
+
23
+ `@alpha` is not optional until `0.1.0` publishes: a bare install resolves `latest`, which npm set to `0.1.0-alpha.1` on the first publish, and that version has neither `tags` nor `--ci-steps`.
24
+
25
+ ## Your first run
26
+
27
+ Point it at your scenarios and give it nothing else:
28
+
29
+ ```
30
+ spec-controller balance --features ./features
31
+ ```
32
+
33
+ Every scenario comes back as a row whether or not any evidence exists for it, so on this first run the ones you have supplied no reports for read **missing** and the run comes back out-of-balance. That is the shape of the tool rather than a fault in your repository: the reconciliation starts from what you specified, not from what a tool happened to find, and there is no input for which the right answer is silence.
34
+
35
+ **`balance` fails your build out of the box.** An out-of-balance run exits `1`, so the first run above reds any pipeline you put it in. That is a deliberate departure from two tools it sits beside: Stryker's `thresholds.break` defaults to `null` (*"never let your build fail"*), and the Sonar scanner exits `0` whatever its quality gate says unless you set `sonar.qualitygate.wait`. Both make you opt in to gating. `balance` makes you opt out.
36
+
37
+ ### Report first, gate later
38
+
39
+ To bring it into a pipeline you already have without redding it on day one, add `--exit-zero`:
40
+
41
+ ```
42
+ spec-controller balance --features ./features --vitest ./vitest-report.json --exit-zero
43
+ ```
44
+
45
+ The report is byte for byte the one the gate prints: the same rows, the same out-of-balance count, and the same headline naming the code the gate would give. Only the exit status moves. Out-of-balance (`1`) and pending under `--strict` (`3`) exit `0`. Usage (`2`), unsound (`4`) and nothing to reconcile (`5`) still fail, because each means the run could not give a verdict at all, and a flag that suppresses verdicts does not suppress that.
46
+
47
+ Work the out-of-balance rows down, then delete `--exit-zero`, and the gate is on. Don't wrap the step in `continue-on-error` or `|| true` instead: those swallow the codes that mean the run could not be trusted, and they leave nothing in the command to say the gate is off.
48
+
49
+ Now give it the evidence:
50
+
51
+ ```
52
+ spec-controller balance \
53
+ --features ./features \
54
+ --vitest ./vitest-report.json \
55
+ --cucumber ./cucumber-report.json
56
+ ```
57
+
58
+ A scenario tagged `@unit` names an obligation — a unit test must exist and pass — and the evidence posts back by citing the scenario's `@SCN` code in its test name. Where the two agree the row is **balanced**. Where they do not, the row says which way: evidence missing, evidence failing, or evidence that cites a scenario nobody wrote.
59
+
60
+ ## What the exit code tells your pipeline
61
+
62
+ | Exit | What it says |
63
+ | --- | --- |
64
+ | `0` | balanced — every obligation has evidence, and it holds |
65
+ | `1` | out-of-balance — the two records disagree, and the report says where |
66
+ | `2` | usage — the invocation was wrong |
67
+ | `3` | balanced, but carrying Pending Items, and you asked for `--strict` |
68
+ | `4` | unsound — a proving suite failed to collect, so the ledger could not be trusted to disagree |
69
+ | `5` | nothing to reconcile — no scenario was declared and no evidence was produced, so the run had no basis for a verdict |
70
+
71
+ The whole report is written before the code is set, so a failing run still tells you why.
72
+
73
+ `--exit-zero` moves `1` and `3` to `0` and leaves every other code alone: see *Report first, gate later*.
74
+
75
+ **Node's default is never one of these.** A run that cannot reconcile ends on an enumerated code of
76
+ its own rather than falling out of the process — a crash that exits `1` would be indistinguishable,
77
+ by status, from an honest disagreement. **An input the tool cannot read is part of `2`**: a mistyped
78
+ path, an unparseable report or a corpus the Gherkin parser rejects are all the invocation being
79
+ wrong, never a verdict about your code.
80
+
81
+ **The enumeration is open at the top, and that is the one thing to design your pipeline around.** A
82
+ state that is genuinely none of the above gets a **new** code rather than being folded into the
83
+ nearest one — `5` is the most recent, added when a run over a target that had declared nothing and
84
+ produced nothing was found reporting `0`. So **treat an unrecognised non-zero code as a failure you
85
+ have not seen before**, not as a synonym for `1`. Matching `1` exactly is unaffected by a code being
86
+ added; treating *non-zero* as a red build is already correct, and only imprecise about why.
87
+
88
+ ## On CI
89
+
90
+ ```
91
+ spec-controller balance --features ./features \
92
+ --vitest ./vitest-report.json \
93
+ --format md:$GITHUB_STEP_SUMMARY \
94
+ --format json:reconciliation.json
95
+ ```
96
+
97
+ `--format` is repeatable and takes an optional `:<path>`. On GitHub Actions, writing Markdown to `$GITHUB_STEP_SUMMARY` puts the reconciliation on the run's summary page — the report is a table and that is the channel GitHub provides for one. No action, no plugin, no permission.
98
+
99
+ `spec-controller balance --help` lists every flag with a line each.
100
+
101
+ ## Where to go next
102
+
103
+ - **Full flags, worked examples and CI wiring** — [`doc/how_to_use.md`](https://github.com/3f-consulting/spec-controller/blob/main/doc/how_to_use.md)
104
+ - **Bringing a check you already have under the reconciliation** — [`doc/converting-a-fitness-check.md`](https://github.com/3f-consulting/spec-controller/blob/main/doc/converting-a-fitness-check.md)
105
+ - **Why `0.x` promises nothing, and what changes at `1.0`** — [`doc/compatibility.md`](https://github.com/3f-consulting/spec-controller/blob/main/doc/compatibility.md)
106
+ - **Building your own tooling on the engine instead of running a command** — [`@3f-consulting/spec-controller-core`](https://www.npmjs.com/package/@3f-consulting/spec-controller-core), the same reconciliation logic as a pure library with no CLI and no I/O.
107
+
108
+ Source, issues and history: [github.com/3f-consulting/spec-controller](https://github.com/3f-consulting/spec-controller).
109
+
110
+ ## Licence
111
+
112
+ MIT — see `LICENSE`, which travels in this package. © 2026 3F Consulting Ltd.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * THE ONE READING OF A COMMAND'S ARGV, and the one bounding of it against the registry.
3
+ *
4
+ * WHY IT IS ITS OWN MODULE (3F-3300). Both halves lived inside `cli-balance/cli.ts` while
5
+ * `balance` was the only command — `parseArgs` private to it, `checkUnknownFlags` exported for
6
+ * its own `@unit` arm. A second command needs both, and the two ways of giving it them were
7
+ * worse than moving them: copying `parseArgs` would put a second reading of argv in a tree whose
8
+ * whole subject is second readings, and importing them from `cli-balance/cli.ts` would have the
9
+ * corpus command pull the entire reconcile ingest chain into memory to answer a question about
10
+ * feature files.
11
+ *
12
+ * THE BOUNDING IS PER-COMMAND, which is what makes "accepted = registry = help" hold for every
13
+ * command rather than for the one this code was written beside. `checkUnknownFlags` took the
14
+ * registry and looked `balance` up inside itself; the command is a parameter now, exactly as it
15
+ * became one for `renderCommandHelp` (3F-3298). @SCN-CLI-009's scenario is unchanged and still
16
+ * asks about `balance`: what a second command gets is the same bounding BY CONSTRUCTION, and the
17
+ * residual — that an unregistered flag to `tags` is unproven by a scenario of its own — is named
18
+ * on 3F-3286 rather than left silent.
19
+ */
20
+ import type { CliRegistry } from "./cli-registry.js";
21
+ /**
22
+ * Parse `--flag value` pairs from a command's argv into a record, keys with the `--` stripped.
23
+ *
24
+ * A flag whose next token is another flag (or absent) records the string `"true"`, which is how
25
+ * the boolean flags — `--strict`, `--help` — are recognised: presence is what matters, so callers
26
+ * read them with `!== undefined`.
27
+ *
28
+ * PERMISSIVE BY ITSELF, AND BOUNDED BY THE CALLER. It records whatever it is given, including a
29
+ * flag no registry names; `checkUnknownFlags` below is what turns that into a usage error. The
30
+ * split is deliberate — the parse has no opinion about which command it is reading for, and the
31
+ * bounding has nothing else to do.
32
+ */
33
+ export declare function parseArgs(argv: readonly string[]): Record<string, string>;
34
+ /**
35
+ * The registry-bounded flag check at a command's CLI edge (@SCN-CLI-009). A command accepts ONLY
36
+ * the flags its per-command registry scope LISTS (by `name` or `alias`); an unregistered flag — a
37
+ * typo like `--strcit`, a stray `--bogus` — is a usage error, NOT silently swallowed the way the
38
+ * permissive parse above would leave it. This is the structural teeth behind "accepted = registry
39
+ * = help": a flag cannot affect behaviour without a registry entry, and so (by @SCN-CLI-008's
40
+ * guard) without appearing in that command's help.
41
+ *
42
+ * PURE and `@unit`-testable: the parsed args (keys already `--`-stripped) plus the registry and
43
+ * the command to bound against, returning the FIRST unregistered flag as a typed usage error
44
+ * naming it WITH its dashes — routed to stderr and the enumerated usage status 2 by the caller,
45
+ * never 1, never a silent run — else null. The global `--store`/`--run-id` modifiers never reach
46
+ * here: the top-level dispatcher consumes them BEFORE any command is dispatched, so a bounded
47
+ * parse sees only the command's own args.
48
+ *
49
+ * A COMMAND THE REGISTRY DOES NOT NAME ACCEPTS NOTHING, which is the honest reading rather than a
50
+ * degenerate one: an unregistered command has no scope, so every flag given to it is outside it.
51
+ * The dispatcher refuses such a command before this is ever reached, so the case is unreachable
52
+ * today and stated here so it cannot become a silent "accept everything" later.
53
+ *
54
+ * IT NAMES THE VERSION THAT REFUSED, AND THAT IS FOR THE PINNED READER (3F-3105, @SCN-CLI-009).
55
+ * A consumer pins spec-controller and then moves their own tree; the pinned reader is handed argv
56
+ * it was published too early — or too late — to understand, and an unadorned "unknown flag" is
57
+ * indistinguishable from a typo. Naming the running version makes version skew legible from the CI
58
+ * log alone, and naming the bump says what to do about it. `toolVersion` is a PARAMETER rather than
59
+ * a read performed here so this stays pure and `@unit`-testable; the callers pass
60
+ * `resolveToolIdentity().toolVersion`, the same value the report provenance carries.
61
+ *
62
+ * WHY THE PRODUCT SAYS THIS AND NOT THE CONSUMER. spec-controller's own repository is a consumer of
63
+ * its published self, and the ruling there is that it does NOT wrap itself — a dependency, a `run:`
64
+ * step, and the verdict its exit code gives. A consumer-side translator turning exit 2 into a skew
65
+ * refusal is exactly that forbidden wrapper, and no other adopter could write one either. So the
66
+ * refusal is the tool's own, which makes it universal rather than local.
67
+ *
68
+ * THE STATUS DOES NOT MOVE. The caller still routes this to stderr and exit 2. A mistyped `--strcit`
69
+ * is still a usage fault, and giving skew its own status would cost every adopter the ordinary
70
+ * reading to serve the rarer one.
71
+ */
72
+ export declare function checkUnknownFlags(args: Record<string, string>, registry: CliRegistry, command: string, toolVersion: string): string | null;
73
+ //# sourceMappingURL=cli-args.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-args.d.ts","sourceRoot":"","sources":["../src/cli-args.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgBzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GAClB,MAAM,GAAG,IAAI,CA0Bf"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * THE ONE READING OF A COMMAND'S ARGV, and the one bounding of it against the registry.
3
+ *
4
+ * WHY IT IS ITS OWN MODULE (3F-3300). Both halves lived inside `cli-balance/cli.ts` while
5
+ * `balance` was the only command — `parseArgs` private to it, `checkUnknownFlags` exported for
6
+ * its own `@unit` arm. A second command needs both, and the two ways of giving it them were
7
+ * worse than moving them: copying `parseArgs` would put a second reading of argv in a tree whose
8
+ * whole subject is second readings, and importing them from `cli-balance/cli.ts` would have the
9
+ * corpus command pull the entire reconcile ingest chain into memory to answer a question about
10
+ * feature files.
11
+ *
12
+ * THE BOUNDING IS PER-COMMAND, which is what makes "accepted = registry = help" hold for every
13
+ * command rather than for the one this code was written beside. `checkUnknownFlags` took the
14
+ * registry and looked `balance` up inside itself; the command is a parameter now, exactly as it
15
+ * became one for `renderCommandHelp` (3F-3298). @SCN-CLI-009's scenario is unchanged and still
16
+ * asks about `balance`: what a second command gets is the same bounding BY CONSTRUCTION, and the
17
+ * residual — that an unregistered flag to `tags` is unproven by a scenario of its own — is named
18
+ * on 3F-3286 rather than left silent.
19
+ */
20
+ /**
21
+ * Parse `--flag value` pairs from a command's argv into a record, keys with the `--` stripped.
22
+ *
23
+ * A flag whose next token is another flag (or absent) records the string `"true"`, which is how
24
+ * the boolean flags — `--strict`, `--help` — are recognised: presence is what matters, so callers
25
+ * read them with `!== undefined`.
26
+ *
27
+ * PERMISSIVE BY ITSELF, AND BOUNDED BY THE CALLER. It records whatever it is given, including a
28
+ * flag no registry names; `checkUnknownFlags` below is what turns that into a usage error. The
29
+ * split is deliberate — the parse has no opinion about which command it is reading for, and the
30
+ * bounding has nothing else to do.
31
+ */
32
+ export function parseArgs(argv) {
33
+ const args = {};
34
+ for (let i = 0; i < argv.length; i++) {
35
+ const a = argv[i];
36
+ if (a !== undefined && a.startsWith("--")) {
37
+ const key = a.slice(2);
38
+ const value = argv[i + 1];
39
+ if (value !== undefined && !value.startsWith("--")) {
40
+ args[key] = value;
41
+ i++;
42
+ }
43
+ else {
44
+ args[key] = "true";
45
+ }
46
+ }
47
+ }
48
+ return args;
49
+ }
50
+ /**
51
+ * The registry-bounded flag check at a command's CLI edge (@SCN-CLI-009). A command accepts ONLY
52
+ * the flags its per-command registry scope LISTS (by `name` or `alias`); an unregistered flag — a
53
+ * typo like `--strcit`, a stray `--bogus` — is a usage error, NOT silently swallowed the way the
54
+ * permissive parse above would leave it. This is the structural teeth behind "accepted = registry
55
+ * = help": a flag cannot affect behaviour without a registry entry, and so (by @SCN-CLI-008's
56
+ * guard) without appearing in that command's help.
57
+ *
58
+ * PURE and `@unit`-testable: the parsed args (keys already `--`-stripped) plus the registry and
59
+ * the command to bound against, returning the FIRST unregistered flag as a typed usage error
60
+ * naming it WITH its dashes — routed to stderr and the enumerated usage status 2 by the caller,
61
+ * never 1, never a silent run — else null. The global `--store`/`--run-id` modifiers never reach
62
+ * here: the top-level dispatcher consumes them BEFORE any command is dispatched, so a bounded
63
+ * parse sees only the command's own args.
64
+ *
65
+ * A COMMAND THE REGISTRY DOES NOT NAME ACCEPTS NOTHING, which is the honest reading rather than a
66
+ * degenerate one: an unregistered command has no scope, so every flag given to it is outside it.
67
+ * The dispatcher refuses such a command before this is ever reached, so the case is unreachable
68
+ * today and stated here so it cannot become a silent "accept everything" later.
69
+ *
70
+ * IT NAMES THE VERSION THAT REFUSED, AND THAT IS FOR THE PINNED READER (3F-3105, @SCN-CLI-009).
71
+ * A consumer pins spec-controller and then moves their own tree; the pinned reader is handed argv
72
+ * it was published too early — or too late — to understand, and an unadorned "unknown flag" is
73
+ * indistinguishable from a typo. Naming the running version makes version skew legible from the CI
74
+ * log alone, and naming the bump says what to do about it. `toolVersion` is a PARAMETER rather than
75
+ * a read performed here so this stays pure and `@unit`-testable; the callers pass
76
+ * `resolveToolIdentity().toolVersion`, the same value the report provenance carries.
77
+ *
78
+ * WHY THE PRODUCT SAYS THIS AND NOT THE CONSUMER. spec-controller's own repository is a consumer of
79
+ * its published self, and the ruling there is that it does NOT wrap itself — a dependency, a `run:`
80
+ * step, and the verdict its exit code gives. A consumer-side translator turning exit 2 into a skew
81
+ * refusal is exactly that forbidden wrapper, and no other adopter could write one either. So the
82
+ * refusal is the tool's own, which makes it universal rather than local.
83
+ *
84
+ * THE STATUS DOES NOT MOVE. The caller still routes this to stderr and exit 2. A mistyped `--strcit`
85
+ * is still a usage fault, and giving skew its own status would cost every adopter the ordinary
86
+ * reading to serve the rarer one.
87
+ */
88
+ export function checkUnknownFlags(args, registry, command, toolVersion) {
89
+ // The accepted set: every flag name AND alias in THIS command's scope, with the leading dashes
90
+ // stripped so it compares against the parse's `--`-stripped keys (e.g. "--features" and "-h"
91
+ // become "features" and "h"). Built from the registry alone — the single source — so
92
+ // "accepted = registry" holds by construction.
93
+ const entry = registry.commands.find((c) => c.name === command);
94
+ const accepted = new Set();
95
+ for (const flag of entry?.flags ?? []) {
96
+ accepted.add(flag.name.replace(/^-+/, ""));
97
+ for (const alias of flag.aliases ?? [])
98
+ accepted.add(alias.replace(/^-+/, ""));
99
+ }
100
+ // The FIRST parsed flag not in the accepted set is the reported error (Object.keys is argv
101
+ // order, so this is deterministic). The message names the flag WITH its dashes, and the command
102
+ // whose scope refused it — a consumer running two commands needs to know which help to open.
103
+ for (const key of Object.keys(args)) {
104
+ if (!accepted.has(key)) {
105
+ return (`Unknown flag --${key} for 'spec-controller ${command}' — this is spec-controller ` +
106
+ `${toolVersion}, and that flag is not in its ${command} scope.\n` +
107
+ `If this version is pinned, the pin and the tree it is reading have drifted apart: ` +
108
+ `bump the pin to a version whose '${command}' accepts --${key}.\n` +
109
+ `Otherwise run 'spec-controller ${command} --help' to see the flags this version accepts.`);
110
+ }
111
+ }
112
+ return null;
113
+ }
114
+ //# sourceMappingURL=cli-args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-args.js","sourceRoot":"","sources":["../src/cli-args.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,SAAS,CAAC,IAAuB;IAC/C,MAAM,IAAI,GAA2B,EAAE,CAAC;IACxC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1C,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YACvB,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1B,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;gBACnD,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;gBAClB,CAAC,EAAE,CAAC;YACN,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC;YACrB,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAA4B,EAC5B,QAAqB,EACrB,OAAe,EACf,WAAmB;IAEnB,+FAA+F;IAC/F,6FAA6F;IAC7F,qFAAqF;IACrF,+CAA+C;IAC/C,MAAM,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC;IAChE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IACnC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,KAAK,IAAI,EAAE,EAAE,CAAC;QACtC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;QAC3C,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,IAAI,EAAE;YAAE,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;IACjF,CAAC;IACD,2FAA2F;IAC3F,gGAAgG;IAChG,6FAA6F;IAC7F,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACvB,OAAO,CACL,kBAAkB,GAAG,yBAAyB,OAAO,8BAA8B;gBACnF,GAAG,WAAW,iCAAiC,OAAO,WAAW;gBACjE,oFAAoF;gBACpF,oCAAoC,OAAO,eAAe,GAAG,KAAK;gBAClE,kCAAkC,OAAO,iDAAiD,CAC3F,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -7,10 +7,10 @@
7
7
  *
8
8
  * Usage:
9
9
  * pnpm reconcile [--features <dir>] [--vitest <report.json>]
10
- * [--cucumber <report.json>] [--ci <ci.yml>]
10
+ * [--cucumber <report.json>] [--ci-steps <record.json>]
11
+ * [--ci <ci.yml>]
11
12
  * [--format <fmt>[:<path>]]
12
13
  */
13
- import type { CliRegistry } from "../cli-registry.js";
14
14
  /**
15
15
  * The legacy-`--json` guard at the CLI edge (@SCN-FMT-006). The earlier
16
16
  * `--json <path>` flag is removed; supplying it must fail fast with a migration hint to
@@ -32,6 +32,23 @@ export declare function checkLegacyJson(args: Record<string, string>): string |
32
32
  * explicit default.
33
33
  */
34
34
  export declare function sourceTarget(args: Record<string, string>): string;
35
+ /**
36
+ * Guard the supplied input-file/dir flags at the CLI edge (@SCN-CLI-002). A
37
+ * mistyped `--vitest` / `--cucumber` / `--features` / `--ci` / `--package-json`
38
+ * pointing at a non-existent path used to reconcile silently against ZERO evidence —
39
+ * a terrifying false all-red report meaning "you forgot the reports", not "your code
40
+ * is broken" (the dogfooding fault). These flags are Examples of ONE boundary
41
+ * rule: a supplied-but-absent input path is a hard error, and a flag added to the set
42
+ * inherits it rather than restating it. Scope is existence only
43
+ * (readability / file-vs-dir type-correctness out of scope; `exists` covers the dir
44
+ * flag and the file flags alike).
45
+ *
46
+ * PURE and `@unit`-testable like `checkLegacyJson` / `sourceTarget`: reads the parsed
47
+ * args + an injected `exists` fn (no fs), returns the FIRST supplied-but-absent
48
+ * `(flag, path)` as a typed error naming both — routed to stderr + a non-zero exit by
49
+ * `refuseUnreadableInputs` (the shared typed-error surface) — else null. An OMITTED flag is not an
50
+ * error (legitimately optional: simply no evidence of that kind).
51
+ */
35
52
  export declare function checkInputsExist(args: Record<string, string>, exists: (path: string) => boolean): string | null;
36
53
  /**
37
54
  * Guard the READABILITY of the supplied JSON inputs at the CLI edge (@SCN-CLI-014).
@@ -44,7 +61,7 @@ export declare function checkInputsExist(args: Record<string, string>, exists: (
44
61
  *
45
62
  * PURE and `@unit`-testable like its sibling guards: reads the parsed args + an injected
46
63
  * `read` fn (no fs), returns the FIRST supplied-but-unparseable `(flag, path, failure)` as
47
- * a typed error naming all three — routed to stderr + exit 2 by `runBalance` — else null.
64
+ * a typed error naming all three — routed to stderr + exit 2 by `refuseUnreadableInputs` — else null.
48
65
  * An omitted flag is not an error, and an unreadable file (a race after the existence
49
66
  * check) reports as the same input fault rather than escaping as a crash.
50
67
  */
@@ -66,22 +83,5 @@ export declare function checkInputsParse(args: Record<string, string>, read: (pa
66
83
  * not an error (the ordinary reconcile path).
67
84
  */
68
85
  export declare function checkRenderFromJson(args: Record<string, string>, read: (path: string) => string): string | null;
69
- /**
70
- * The registry-bounded flag check at the CLI edge (@SCN-CLI-009). `balance` accepts ONLY
71
- * the flags its per-command registry scope LISTS (by `name` or `alias`); an unregistered
72
- * flag — a typo like `--strcit`, a stray `--bogus` — is a usage error, NOT silently
73
- * swallowed the way the old permissive `parseArgs` did. This is the structural teeth behind
74
- * "accepted = registry = help": a flag can't affect behaviour without a registry entry (and
75
- * so, by CLI-008's guard, without appearing in help).
76
- *
77
- * PURE and `@unit`-testable like `checkLegacyJson` / `checkInputsExist`: reads the parsed
78
- * args (keys are the `--`-stripped flag names) + the registry, returns the FIRST unregistered
79
- * flag as a typed usage error naming it WITH its dashes — routed to stderr + exit 2 by
80
- * `runBalance` (per 3F-1414's `0` balanced / `1` out-of-balance / `2` usage taxonomy),
81
- * never exit 1, never a silent reconcile — else null. The global `--store`/`--run-id`
82
- * modifiers never reach here: the top-level dispatcher (`parseHostInvocation`) consumes them
83
- * BEFORE `balance` is dispatched, so the bounded parser sees only the command's own args.
84
- */
85
- export declare function checkUnknownFlags(args: Record<string, string>, registry: CliRegistry): string | null;
86
86
  export declare function runBalance(argv: string[]): void;
87
87
  //# sourceMappingURL=cli.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli-balance/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAsBH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AA0CtD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAS3E;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAEjE;AAqCD,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAChC,MAAM,GAAG,IAAI,CAYf;AAUD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAC7B,MAAM,GAAG,IAAI,CAef;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAC7B,MAAM,GAAG,IAAI,CAcf;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAmBpG;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CA8P/C"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli-balance/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAiDH;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAS3E;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAEjE;AAqBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAChC,MAAM,GAAG,IAAI,CAcf;AAYD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAC7B,MAAM,GAAG,IAAI,CAef;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAC7B,MAAM,GAAG,IAAI,CAcf;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAkJ/C"}