spec-controller 0.1.0-alpha.4 → 0.1.0-alpha.40
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.
- package/README.md +34 -6
- package/dist/cli-allocate/cli.d.ts +2 -0
- package/dist/cli-allocate/cli.d.ts.map +1 -0
- package/dist/cli-allocate/cli.js +57 -0
- package/dist/cli-allocate/cli.js.map +1 -0
- package/dist/cli-allocate/readHistory.d.ts +29 -0
- package/dist/cli-allocate/readHistory.d.ts.map +1 -0
- package/dist/cli-allocate/readHistory.js +136 -0
- package/dist/cli-allocate/readHistory.js.map +1 -0
- package/dist/cli-allocate/readParts.d.ts +3 -0
- package/dist/cli-allocate/readParts.d.ts.map +1 -0
- package/dist/cli-allocate/readParts.js +28 -0
- package/dist/cli-allocate/readParts.js.map +1 -0
- package/dist/cli-allocate/readTree.d.ts +16 -0
- package/dist/cli-allocate/readTree.d.ts.map +1 -0
- package/dist/cli-allocate/readTree.js +106 -0
- package/dist/cli-allocate/readTree.js.map +1 -0
- package/dist/cli-allocate/refStore.d.ts +3 -0
- package/dist/cli-allocate/refStore.d.ts.map +1 -0
- package/dist/cli-allocate/refStore.js +47 -0
- package/dist/cli-allocate/refStore.js.map +1 -0
- package/dist/cli-allocate/request.d.ts +11 -0
- package/dist/cli-allocate/request.d.ts.map +1 -0
- package/dist/cli-allocate/request.js +29 -0
- package/dist/cli-allocate/request.js.map +1 -0
- package/dist/cli-allocate/stderr.d.ts +14 -0
- package/dist/cli-allocate/stderr.d.ts.map +1 -0
- package/dist/cli-allocate/stderr.js +33 -0
- package/dist/cli-allocate/stderr.js.map +1 -0
- package/dist/cli-allocate/unreadable.d.ts +24 -0
- package/dist/cli-allocate/unreadable.d.ts.map +1 -0
- package/dist/cli-allocate/unreadable.js +43 -0
- package/dist/cli-allocate/unreadable.js.map +1 -0
- package/dist/cli-args.d.ts +73 -18
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +244 -22
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-balance/cli.d.ts +31 -13
- package/dist/cli-balance/cli.d.ts.map +1 -1
- package/dist/cli-balance/cli.js +205 -251
- package/dist/cli-balance/cli.js.map +1 -1
- package/dist/cli-balance/emit/writer.d.ts +8 -23
- package/dist/cli-balance/emit/writer.d.ts.map +1 -1
- package/dist/cli-balance/emit/writer.js +24 -32
- package/dist/cli-balance/emit/writer.js.map +1 -1
- package/dist/cli-balance/exitStatus.d.ts +3 -0
- package/dist/cli-balance/exitStatus.d.ts.map +1 -0
- package/dist/cli-balance/exitStatus.js +12 -0
- package/dist/cli-balance/exitStatus.js.map +1 -0
- package/dist/cli-balance/reRender.d.ts +22 -0
- package/dist/cli-balance/reRender.d.ts.map +1 -0
- package/dist/cli-balance/reRender.js +36 -0
- package/dist/cli-balance/reRender.js.map +1 -0
- package/dist/cli-balance/unreadableInputs.d.ts +35 -0
- package/dist/cli-balance/unreadableInputs.d.ts.map +1 -0
- package/dist/cli-balance/unreadableInputs.js +101 -0
- package/dist/cli-balance/unreadableInputs.js.map +1 -0
- package/dist/cli-registry.d.ts +28 -0
- package/dist/cli-registry.d.ts.map +1 -1
- package/dist/cli-registry.js +40 -35
- package/dist/cli-registry.js.map +1 -1
- package/dist/cli.d.ts +4 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +37 -21
- package/dist/cli.js.map +1 -1
- package/dist/host.d.ts +28 -3
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +137 -17
- package/dist/host.js.map +1 -1
- package/dist/ingest/gherkinValidation.d.ts +3 -7
- package/dist/ingest/gherkinValidation.d.ts.map +1 -1
- package/dist/ingest/gherkinValidation.js +3 -7
- package/dist/ingest/gherkinValidation.js.map +1 -1
- package/dist/ingest/ingestQualityChecks.d.ts +25 -54
- package/dist/ingest/ingestQualityChecks.d.ts.map +1 -1
- package/dist/ingest/ingestQualityChecks.js +112 -145
- package/dist/ingest/ingestQualityChecks.js.map +1 -1
- package/dist/ingest/ingestScenarios.d.ts +11 -17
- package/dist/ingest/ingestScenarios.d.ts.map +1 -1
- package/dist/ingest/ingestScenarios.js +52 -62
- package/dist/ingest/ingestScenarios.js.map +1 -1
- package/dist/ingest/inputShapes.d.ts +47 -0
- package/dist/ingest/inputShapes.d.ts.map +1 -0
- package/dist/ingest/inputShapes.js +143 -0
- package/dist/ingest/inputShapes.js.map +1 -0
- package/dist/outputLocation.d.ts +15 -0
- package/dist/outputLocation.d.ts.map +1 -0
- package/dist/outputLocation.js +39 -0
- package/dist/outputLocation.js.map +1 -0
- package/dist/run-management/keptRun.d.ts +27 -25
- package/dist/run-management/keptRun.d.ts.map +1 -1
- package/dist/run-management/keptRun.js +20 -21
- package/dist/run-management/keptRun.js.map +1 -1
- package/dist/storedRun.d.ts +27 -0
- package/dist/storedRun.d.ts.map +1 -0
- package/dist/storedRun.js +49 -0
- package/dist/storedRun.js.map +1 -0
- package/package.json +2 -2
- package/dist/corpus/cli.d.ts +0 -34
- package/dist/corpus/cli.d.ts.map +0 -1
- package/dist/corpus/cli.js +0 -128
- package/dist/corpus/cli.js.map +0 -1
- package/dist/mutation-ratchet/index.d.ts +0 -51
- package/dist/mutation-ratchet/index.d.ts.map +0 -1
- package/dist/mutation-ratchet/index.js +0 -51
- package/dist/mutation-ratchet/index.js.map +0 -1
- package/dist/mutation-ratchet/record.d.ts +0 -178
- package/dist/mutation-ratchet/record.d.ts.map +0 -1
- package/dist/mutation-ratchet/record.js +0 -314
- package/dist/mutation-ratchet/record.js.map +0 -1
- package/dist/mutation-ratchet/report.d.ts +0 -109
- package/dist/mutation-ratchet/report.d.ts.map +0 -1
- package/dist/mutation-ratchet/report.js +0 -156
- package/dist/mutation-ratchet/report.js.map +0 -1
package/dist/cli-balance/cli.js
CHANGED
|
@@ -13,21 +13,24 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { existsSync, readFileSync } from "node:fs";
|
|
15
15
|
import { dirname } from "node:path";
|
|
16
|
-
import { runReconcile, readEvidenceObligations, readFeatureNames, featureCorpusParseErrors,
|
|
16
|
+
import { runReconcile, readEvidenceObligations, readFeatureNames, featureCorpusParseErrors, } from "../ingest/ingestQualityChecks.js";
|
|
17
17
|
import { planOutput } from "./emit/format.js";
|
|
18
18
|
import { writeOutputs } from "./emit/writer.js";
|
|
19
|
-
import {
|
|
19
|
+
import { checkReRenderFlags } from "./reRender.js";
|
|
20
|
+
import { checkInputPaths, checkInputShapes, checkSavedReconciliation, jsonInputsOf, reRenderOrRefusal, } from "./unreadableInputs.js";
|
|
21
|
+
import { statusFor } from "./exitStatus.js";
|
|
22
|
+
import { verdictCode, } from "@3f-consulting/spec-controller-core";
|
|
20
23
|
import { resolveSourceSha, resolveTargetRoot, resolveToolIdentity } from "../run-management/resolveRunInputs.js";
|
|
21
24
|
import { cliRegistry, renderCommandHelp } from "../cli-registry.js";
|
|
22
|
-
// THE ARGV READING AND ITS REGISTRY BOUNDING, SHARED WITH EVERY OTHER COMMAND
|
|
25
|
+
// THE ARGV READING AND ITS REGISTRY BOUNDING, SHARED WITH EVERY OTHER COMMAND. Both
|
|
23
26
|
// lived here while `balance` was the only command; a second command needs them, and a second
|
|
24
27
|
// copy of "what did the operator type" is the one thing this tree least wants two of.
|
|
25
|
-
import { parseArgs, checkUnknownFlags } from "../cli-args.js";
|
|
28
|
+
import { asksForHelp, parseArgs, checkArgvShape, checkUnknownFlags, checkMissingValues, checkMisplacedHostModifiers, } from "../cli-args.js";
|
|
26
29
|
/**
|
|
27
30
|
* Collect the `--format <spec>` values from argv in order (the flag is REPEATABLE, unlike
|
|
28
31
|
* the single-valued flags `parseArgs` records) — the raw specs `planOutput` parses. Empty
|
|
29
|
-
* when no `--format` was given, which `planOutput` reads as the md→stdout default
|
|
30
|
-
*
|
|
32
|
+
* when no `--format` was given, which `planOutput` reads as the md→stdout default.
|
|
33
|
+
* Several specs route in one invocation.
|
|
31
34
|
*/
|
|
32
35
|
function collectFormatSpecs(argv) {
|
|
33
36
|
const specs = [];
|
|
@@ -43,7 +46,7 @@ function collectFormatSpecs(argv) {
|
|
|
43
46
|
return specs;
|
|
44
47
|
}
|
|
45
48
|
/**
|
|
46
|
-
* The legacy-`--json` guard at the CLI edge
|
|
49
|
+
* The legacy-`--json` guard at the CLI edge. The earlier
|
|
47
50
|
* `--json <path>` flag is removed; supplying it must fail fast with a migration hint to
|
|
48
51
|
* `--format json:<path>`, so the removed flag never silently no-ops and drops output. PURE
|
|
49
52
|
* and `@unit`-testable: the removed flag is NOT a `--format` spec (planOutput never sees
|
|
@@ -60,8 +63,8 @@ export function checkLegacyJson(args) {
|
|
|
60
63
|
return null;
|
|
61
64
|
}
|
|
62
65
|
/**
|
|
63
|
-
* Source the provenance "Target repo:" name for a run
|
|
64
|
-
*
|
|
66
|
+
* Source the provenance "Target repo:" name for a run. The
|
|
67
|
+
* operator's `--target` is recorded VERBATIM — no resolution — defaulting to
|
|
65
68
|
* `"unknown-target"` when omitted (parallel to `sourceSha`'s `"unknown"`; the facts
|
|
66
69
|
* artefact is self-describing, so the line always renders). PURE and `@unit`-testable at
|
|
67
70
|
* the CLI edge, like `checkLegacyJson`. It never inspects `--features`: the prior
|
|
@@ -72,23 +75,6 @@ export function checkLegacyJson(args) {
|
|
|
72
75
|
export function sourceTarget(args) {
|
|
73
76
|
return args["target"] ?? "unknown-target";
|
|
74
77
|
}
|
|
75
|
-
/**
|
|
76
|
-
* Guard the supplied input-file/dir flags at the CLI edge (@SCN-CLI-002). A
|
|
77
|
-
* mistyped `--vitest` / `--cucumber` / `--features` / `--ci` / `--package-json`
|
|
78
|
-
* pointing at a non-existent path used to reconcile silently against ZERO evidence —
|
|
79
|
-
* a terrifying false all-red report meaning "you forgot the reports", not "your code
|
|
80
|
-
* is broken" (the dogfooding fault). These flags are Examples of ONE boundary
|
|
81
|
-
* rule: a supplied-but-absent input path is a hard error, and a flag added to the set
|
|
82
|
-
* inherits it rather than restating it. Scope is existence only
|
|
83
|
-
* (readability / file-vs-dir type-correctness out of scope; `exists` covers the dir
|
|
84
|
-
* flag and the file flags alike).
|
|
85
|
-
*
|
|
86
|
-
* PURE and `@unit`-testable like `checkLegacyJson` / `sourceTarget`: reads the parsed
|
|
87
|
-
* args + an injected `exists` fn (no fs), returns the FIRST supplied-but-absent
|
|
88
|
-
* `(flag, path)` as a typed error naming both — routed to stderr + a non-zero exit by
|
|
89
|
-
* `runBalance` (the shared typed-error surface) — else null. An OMITTED flag is not an
|
|
90
|
-
* error (legitimately optional: simply no evidence of that kind).
|
|
91
|
-
*/
|
|
92
78
|
/**
|
|
93
79
|
* The parsed args, as the reconciler's input paths — the one place a `--flag` name becomes a
|
|
94
80
|
* `ReconcileOptions` key.
|
|
@@ -104,16 +90,32 @@ function reconcileInputsOf(args) {
|
|
|
104
90
|
cucumber: args["cucumber"],
|
|
105
91
|
ci: args["ci"],
|
|
106
92
|
packageJson: args["package-json"],
|
|
107
|
-
measurements: args["measurements"],
|
|
108
93
|
ciSteps: args["ci-steps"],
|
|
109
94
|
};
|
|
110
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Guard the supplied input-file/dir flags at the CLI edge. A
|
|
98
|
+
* mistyped `--vitest` / `--cucumber` / `--features` / `--ci` / `--package-json`
|
|
99
|
+
* pointing at a non-existent path used to reconcile silently against ZERO evidence —
|
|
100
|
+
* a terrifying false all-red report meaning "you forgot the reports", not "your code
|
|
101
|
+
* is broken" (the dogfooding fault). These flags are Examples of ONE boundary
|
|
102
|
+
* rule: a supplied-but-absent input path is a hard error, and a flag added to the set
|
|
103
|
+
* inherits it rather than restating it. Scope is existence only
|
|
104
|
+
* (readability / file-vs-dir type-correctness out of scope; `exists` covers the dir
|
|
105
|
+
* flag and the file flags alike).
|
|
106
|
+
*
|
|
107
|
+
* PURE and `@unit`-testable like `checkLegacyJson` / `sourceTarget`: reads the parsed
|
|
108
|
+
* args + an injected `exists` fn (no fs), returns the FIRST supplied-but-absent
|
|
109
|
+
* `(flag, path)` as a typed error naming both — routed to stderr + a non-zero exit by
|
|
110
|
+
* `refuseUnreadableInputs` (the shared typed-error surface) — else null. An OMITTED flag is not an
|
|
111
|
+
* error (legitimately optional: simply no evidence of that kind).
|
|
112
|
+
*/
|
|
111
113
|
export function checkInputsExist(args, exists) {
|
|
112
114
|
// The input flags in a fixed order — the FIRST supplied-but-absent one is the
|
|
113
115
|
// reported error (deterministic; the operator fixes and re-runs). Keys are the
|
|
114
116
|
// parseArgs form (the `--` stripped); the message names the flag WITH its dashes.
|
|
115
117
|
const flags = [
|
|
116
|
-
"vitest", "cucumber", "features", "ci", "package-json", "
|
|
118
|
+
"vitest", "cucumber", "features", "ci", "package-json", "ci-steps",
|
|
117
119
|
];
|
|
118
120
|
for (const flag of flags) {
|
|
119
121
|
const path = args[flag];
|
|
@@ -124,53 +126,60 @@ export function checkInputsExist(args, exists) {
|
|
|
124
126
|
return null;
|
|
125
127
|
}
|
|
126
128
|
/**
|
|
127
|
-
* The JSON-BEARING input flags — the ones whose content is `JSON.parse`d
|
|
128
|
-
*
|
|
129
|
-
* read as raw TEXT and scanned for `run:` steps — neither is JSON, neither can crash the
|
|
130
|
-
* process, and on garbage both reconcile normally (verified, 3F-1742 Discovery).
|
|
129
|
+
* The JSON-BEARING input flags — the ones whose content is `JSON.parse`d. `--features` and `--ci`
|
|
130
|
+
* are read as Gherkin and YAML files on disk, and whether they can be read is `checkInputPaths`'s.
|
|
131
131
|
*/
|
|
132
132
|
const JSON_INPUT_FLAGS = [
|
|
133
|
-
"vitest", "cucumber", "package-json", "
|
|
133
|
+
"vitest", "cucumber", "package-json", "ci-steps",
|
|
134
134
|
];
|
|
135
135
|
/**
|
|
136
|
-
* Guard the READABILITY of the supplied JSON inputs at the CLI edge
|
|
136
|
+
* Guard the READABILITY of the supplied JSON inputs at the CLI edge.
|
|
137
137
|
* `checkInputsExist` already guards their PRESENCE — but a path that is present yet
|
|
138
138
|
* UNPARSEABLE fell straight through it: the reader threw mid-parse, nothing caught it, and
|
|
139
139
|
* the process hit Node's default exit 1 — the code reserved for a genuine OUT-OF-BALANCE
|
|
140
140
|
* verdict. A crashed invocation was therefore indistinguishable, by exit code, from an
|
|
141
|
-
* honest disagreement
|
|
142
|
-
* sibling to a missing path
|
|
141
|
+
* honest disagreement. An input the tool cannot read is a USAGE fault: exit 2,
|
|
142
|
+
* sibling to a missing path and an unrecognised flag.
|
|
143
143
|
*
|
|
144
144
|
* PURE and `@unit`-testable like its sibling guards: reads the parsed args + an injected
|
|
145
145
|
* `read` fn (no fs), returns the FIRST supplied-but-unparseable `(flag, path, failure)` as
|
|
146
|
-
* a typed error naming all three — routed to stderr + exit 2 by `
|
|
147
|
-
* An omitted flag is not an error, and
|
|
148
|
-
* check)
|
|
146
|
+
* a typed error naming all three — routed to stderr + exit 2 by `refuseUnreadableInputs` — else null.
|
|
147
|
+
* An omitted flag is not an error, and a file that cannot be read at all (a race after the
|
|
148
|
+
* existence check, a permission refused) is refused as one that could not be read, never as JSON
|
|
149
|
+
* that did not parse.
|
|
149
150
|
*/
|
|
150
151
|
export function checkInputsParse(args, read) {
|
|
151
152
|
for (const flag of JSON_INPUT_FLAGS) {
|
|
152
153
|
const path = args[flag];
|
|
153
154
|
if (path === undefined)
|
|
154
155
|
continue;
|
|
156
|
+
let text;
|
|
157
|
+
try {
|
|
158
|
+
text = read(path);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
return `The --${flag} file could not be read: ${path} (${failureOf(err)})`;
|
|
162
|
+
}
|
|
155
163
|
try {
|
|
156
|
-
JSON.parse(
|
|
164
|
+
JSON.parse(text);
|
|
157
165
|
}
|
|
158
166
|
catch (err) {
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
const failure = err instanceof Error ? err.message : String(err);
|
|
162
|
-
return `The --${flag} report is not valid JSON: ${path} (${failure})`;
|
|
167
|
+
// Anything that escapes this catch lands on Node's default exit 1 — the collision itself.
|
|
168
|
+
return `The --${flag} report is not valid JSON: ${path} (${failureOf(err)})`;
|
|
163
169
|
}
|
|
164
170
|
}
|
|
165
171
|
return null;
|
|
166
172
|
}
|
|
173
|
+
function failureOf(err) {
|
|
174
|
+
return err instanceof Error ? err.message : String(err);
|
|
175
|
+
}
|
|
167
176
|
/**
|
|
168
|
-
* Guard the readability of `--render-from-json` at the CLI edge
|
|
177
|
+
* Guard the readability of `--render-from-json` at the CLI edge. The
|
|
169
178
|
* render-from-saved-JSON branch RETURNS before the input guards ever run, so it honoured
|
|
170
179
|
* NEITHER: a missing path crashed to `ENOENT` and a malformed file to `SyntaxError`, both
|
|
171
|
-
* landing on Node's default exit 1 — the out-of-balance code
|
|
180
|
+
* landing on Node's default exit 1 — the out-of-balance code.
|
|
172
181
|
*
|
|
173
|
-
* The missing-path case was a live violation of
|
|
182
|
+
* The missing-path case was a live violation of the missing-input rule's own shipped principle: a
|
|
174
183
|
* supplied input path that does not exist is a hard error at exit 2, enforced for the five
|
|
175
184
|
* evidence flags and not for this one. The CLI shipped a guard that half-honoured its Rule.
|
|
176
185
|
*
|
|
@@ -197,96 +206,175 @@ export function checkRenderFromJson(args, read) {
|
|
|
197
206
|
return null;
|
|
198
207
|
}
|
|
199
208
|
export function runBalance(argv) {
|
|
200
|
-
// Answer `--help`/`-h` from the command/flag registry
|
|
209
|
+
// Answer `--help`/`-h` from the command/flag registry BEFORE any
|
|
201
210
|
// parse or reconcile: render the balance command's per-command scope (a line per
|
|
202
211
|
// registered flag — name + arg + description), leave process.exitCode at its default 0,
|
|
203
212
|
// and return. This intercepts --help so it never falls through parseArgs as a stray flag
|
|
204
213
|
// while balance runs anyway. The registry is the single source, so a flag that exists is
|
|
205
214
|
// a flag that appears in help — no hand-kept list to drift.
|
|
206
|
-
if (
|
|
215
|
+
if (asksForHelp(argv)) {
|
|
207
216
|
process.stdout.write(renderCommandHelp(cliRegistry, "balance") + "\n");
|
|
208
|
-
return;
|
|
217
|
+
return false;
|
|
209
218
|
}
|
|
210
219
|
// Capture the run-start instant up front — the run provenance's UTC timestamp,
|
|
211
220
|
// threaded through ctx.runStart to the renderers.
|
|
212
221
|
const runStart = new Date();
|
|
213
222
|
const args = parseArgs(argv);
|
|
214
|
-
// Legacy-`--json` guard
|
|
223
|
+
// Legacy-`--json` guard: the earlier `--json <path>` flag is
|
|
215
224
|
// removed. Reject it fast — before any reconcile or write — with a migration hint to
|
|
216
225
|
// `--format json:<path>`, so the removed flag never silently no-ops and drops output. The
|
|
217
226
|
// guard's typed error is routed to stderr + a non-zero exit, exactly as planOutput's error
|
|
218
227
|
// is below (the shared typed-error surface).
|
|
219
228
|
const legacyJsonError = checkLegacyJson(args);
|
|
220
|
-
if (legacyJsonError)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
//
|
|
229
|
+
if (legacyJsonError)
|
|
230
|
+
exitUsage(legacyJsonError);
|
|
231
|
+
// A host modifier after the command is out of place, not unknown, so it is
|
|
232
|
+
// refused before the drift advice.
|
|
233
|
+
const misplacedModifierError = checkMisplacedHostModifiers(args, cliRegistry, "balance");
|
|
234
|
+
if (misplacedModifierError)
|
|
235
|
+
exitUsage(misplacedModifierError);
|
|
236
|
+
// Every token is a flag or the value of the flag before it, refused before the unknown-flag
|
|
237
|
+
// check below.
|
|
238
|
+
const shapeError = checkArgvShape(argv, cliRegistry, "balance");
|
|
239
|
+
if (shapeError)
|
|
240
|
+
exitUsage(shapeError);
|
|
241
|
+
// Registry-bounded flag NAMES: `balance` accepts ONLY the flags its registry
|
|
242
|
+
// scope LISTS. An unregistered flag — a typo `--strcit`, a stray `--bogus` — is a USAGE error:
|
|
243
|
+
// name it on stderr, set process.exitCode = 2 (the enumerated usage code, the 0/1/2
|
|
244
|
+
// taxonomy — never 1, never 0), and RETURN before any reconcile. It bounds names only; that every
|
|
245
|
+
// other token is read or refused is the argv-shape check above.
|
|
246
|
+
// Runs AFTER the legacy-`--json` guard so `--json` keeps its specific migration hint, after the
|
|
247
|
+
// misplaced-modifier refusal, so a `--store` / `--run-id` written after the command is named as
|
|
248
|
+
// out of place rather than given pin advice, and after the argv-shape check, so `--format=json` is
|
|
249
|
+
// refused as a form and never given pin advice. This is the structural teeth behind "accepted =
|
|
234
250
|
// registry = help" — a flag can't affect behaviour without a registry entry.
|
|
235
251
|
const unknownFlagError = checkUnknownFlags(args, cliRegistry, "balance", resolveToolIdentity().toolVersion);
|
|
236
252
|
if (unknownFlagError) {
|
|
237
253
|
process.stderr.write(unknownFlagError + "\n");
|
|
238
254
|
process.exitCode = 2;
|
|
239
|
-
return;
|
|
255
|
+
return false;
|
|
240
256
|
}
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
//
|
|
251
|
-
//
|
|
257
|
+
// A flag that takes a value, given none, refused before anything reads
|
|
258
|
+
// the value `parseArgs` made up for it.
|
|
259
|
+
const missingValueError = checkMissingValues(argv, cliRegistry, "balance");
|
|
260
|
+
if (missingValueError)
|
|
261
|
+
exitUsage(missingValueError);
|
|
262
|
+
if (renderSavedReport(args))
|
|
263
|
+
return false;
|
|
264
|
+
refuseUnreadableInputs(args);
|
|
265
|
+
const inputs = reconcileInputsOf(args);
|
|
266
|
+
// Reconcile the obligation-vs-observation evidence into the Ledger. The runtime evidence-observation
|
|
267
|
+
// views (Test volume, the evidence grid) are FOLDS the renderers derive off this Ledger's posted
|
|
268
|
+
// credits (the census is retired), so they cannot drift from the
|
|
269
|
+
// reconciler's verdict: they ARE the same rows. `withRuntimeObservations: true` (below) tells the
|
|
270
|
+
// renderers the axis is in play.
|
|
271
|
+
const balance = runReconcile(inputs);
|
|
272
|
+
// Read the scenario-corpus census alongside the balance —
|
|
273
|
+
// the raw-vs-parsed @SCN-occurrence delta over the same discovered feature corpus.
|
|
274
|
+
// A sibling to the balance, threaded to the renderers via writeOutputs like ctx.
|
|
275
|
+
const evidenceObligations = readEvidenceObligations(args["features"]);
|
|
276
|
+
// The static-check kind-counts are a FOLD OVER THE LEDGER — the
|
|
277
|
+
// renderers count the balance's own posted static-check credits (`staticObservationTally`), so
|
|
278
|
+
// they cannot drift from the reconciler's verdict: they ARE the same rows. The parallel static
|
|
279
|
+
// census is RETIRED; `withStaticChecks: true` (below) tells the renderers the axis is in play.
|
|
280
|
+
// Read the target's feature-code→slug map alongside the balance
|
|
281
|
+
// — the by-feature cut's full-name labels. The 5th sibling, threaded to the MARKDOWN
|
|
282
|
+
// renderer via writeOutputs only (markdown-only; the by-feature cut isn't on JSON).
|
|
283
|
+
const featureNames = readFeatureNames(args["features"]);
|
|
284
|
+
const ctx = runContextOf(args, inputs, runStart);
|
|
285
|
+
// Plan the outputs from the --format specs (no flag → md→stdout) and write them via
|
|
286
|
+
// the writer — the pure plan / impure writer split. The plan
|
|
287
|
+
// parse+validate is pure; the writer is the only IO for the emitted report. This is
|
|
288
|
+
// the SOLE output path: the tool emits via --format and holds no storage opinion — the
|
|
289
|
+
// legacy runs/<target>/<ISO>/ auto-archive was removed, the
|
|
290
|
+
// caller now routes stored runs (spec-controller's run-management layer).
|
|
291
|
+
const planResult = planOutput(collectFormatSpecs(argv));
|
|
292
|
+
if (!planResult.ok)
|
|
293
|
+
exitUsage(planResult.error);
|
|
294
|
+
writeOutputs(planResult.plan, { balance, ctx, evidenceObligations, withRuntimeObservations: true, withStaticChecks: true, featureNames });
|
|
295
|
+
// Set after the report is written in full, never `process.exit`, so the buffered report is
|
|
296
|
+
// flushed intact rather than truncated mid-write.
|
|
297
|
+
const verdict = verdictCode({ ...balance, strict: ctx.strict === true });
|
|
298
|
+
const code = statusFor(verdict, args["exit-zero"] !== undefined);
|
|
299
|
+
// Assigned only when NON-ZERO, so a balanced run still LEAVES process.exitCode at its default
|
|
300
|
+
// rather than writing a 0 over it — the distinction the balanced-exit scenarios rest on.
|
|
301
|
+
if (code !== 0)
|
|
302
|
+
process.exitCode = code;
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* A usage or input fault: the message on stderr, then exit 2 — the enumerated usage code
|
|
307
|
+
*, distinct from the out-of-balance verdict code 1. An
|
|
308
|
+
* out-of-balance run must never masquerade as a usage error, nor the reverse.
|
|
309
|
+
*/
|
|
310
|
+
function exitUsage(message) {
|
|
311
|
+
process.stderr.write(message + "\n");
|
|
312
|
+
process.exit(2);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Render-from-saved-JSON mode: the markdown a banked spec-reconciliation.json renders to, on
|
|
316
|
+
* stdout, and the exit its saved verdict names — no reconcile, no live inputs. True when it
|
|
317
|
+
* answered the run, so `runBalance` returns before any reconcile.
|
|
318
|
+
*/
|
|
319
|
+
function renderSavedReport(args) {
|
|
320
|
+
// Here rather than with the live-input guards, because this mode returns before they run.
|
|
321
|
+
const ignoredFlagError = checkReRenderFlags(args);
|
|
322
|
+
if (ignoredFlagError)
|
|
323
|
+
exitUsage(ignoredFlagError);
|
|
252
324
|
const unreadableSavedReportError = checkRenderFromJson(args, (path) => readFileSync(path, "utf8"));
|
|
253
|
-
if (unreadableSavedReportError)
|
|
254
|
-
|
|
255
|
-
process.exit(2);
|
|
256
|
-
}
|
|
325
|
+
if (unreadableSavedReportError)
|
|
326
|
+
exitUsage(unreadableSavedReportError);
|
|
257
327
|
const rehydratePath = args["render-from-json"];
|
|
258
|
-
if (rehydratePath
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
328
|
+
if (rehydratePath === undefined)
|
|
329
|
+
return false;
|
|
330
|
+
const savedJson = readFileSync(rehydratePath, "utf8");
|
|
331
|
+
const notSavedError = checkSavedReconciliation(rehydratePath, savedJson);
|
|
332
|
+
if (notSavedError)
|
|
333
|
+
exitUsage(notSavedError);
|
|
334
|
+
const saved = reRenderOrRefusal(rehydratePath, savedJson, args["strict"] !== undefined);
|
|
335
|
+
if (typeof saved === "string")
|
|
336
|
+
exitUsage(saved);
|
|
337
|
+
process.stdout.write(saved.markdown + "\n");
|
|
338
|
+
const code = statusFor(saved.code, args["exit-zero"] !== undefined);
|
|
339
|
+
if (code !== 0)
|
|
340
|
+
process.exitCode = code;
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* The live-input guards, in the order they run: each halts at exit 2 BEFORE any reconcile,
|
|
345
|
+
* because an input the tool cannot read is a usage error, never a verdict.
|
|
346
|
+
*/
|
|
347
|
+
function refuseUnreadableInputs(args) {
|
|
348
|
+
// Input-existence guard: a supplied input-file/dir flag pointing at a
|
|
264
349
|
// non-existent path is a HARD ERROR — halt BEFORE reconciling, with a typed message
|
|
265
350
|
// naming the flag + path, never the silent zero-evidence all-red report a mistyped
|
|
266
351
|
// path used to produce (the dogfooding fault). Routed to stderr + a non-zero exit,
|
|
267
|
-
// exactly as the legacy-`--json` guard
|
|
352
|
+
// exactly as the legacy-`--json` guard (the shared typed-error surface). An
|
|
268
353
|
// omitted flag is legitimately optional and passes through untouched.
|
|
269
354
|
const missingInputError = checkInputsExist(args, existsSync);
|
|
270
|
-
if (missingInputError)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
// verdict code 1 (@SCN-CLI-004).
|
|
274
|
-
process.exit(2);
|
|
275
|
-
}
|
|
276
|
-
// Input-READABILITY guard (@SCN-CLI-014): existence is not enough. A report that is
|
|
355
|
+
if (missingInputError)
|
|
356
|
+
exitUsage(missingInputError);
|
|
357
|
+
// Input-READABILITY guard: existence is not enough. A report that is
|
|
277
358
|
// PRESENT but UNPARSEABLE (`--vitest /dev/null`, truncated JSON, non-JSON) used to throw
|
|
278
359
|
// mid-parse inside reconcileWithCensus, uncaught — and Node's default exit code is 1, the
|
|
279
|
-
// code reserved for a genuine OUT-OF-BALANCE verdict
|
|
360
|
+
// code reserved for a genuine OUT-OF-BALANCE verdict. So a tool that
|
|
280
361
|
// CRASHED before reconciling was indistinguishable, by exit code, from an honest
|
|
281
|
-
// disagreement
|
|
362
|
+
// disagreement. Runs immediately after the existence guard and BEFORE any
|
|
282
363
|
// reconcile, routing to the same stderr + exit 2 lane: an input the tool cannot read is a
|
|
283
364
|
// usage fault, sibling to a missing path and an unrecognised flag — never a verdict.
|
|
284
365
|
const unparseableInputError = checkInputsParse(args, (path) => readFileSync(path, "utf8"));
|
|
285
|
-
if (unparseableInputError)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
366
|
+
if (unparseableInputError)
|
|
367
|
+
exitUsage(unparseableInputError);
|
|
368
|
+
// A corpus or workflow path that is not the kind its flag reads, or may not be read, cannot be read at all.
|
|
369
|
+
const unreadablePathError = checkInputPaths(args);
|
|
370
|
+
if (unreadablePathError)
|
|
371
|
+
exitUsage(unreadablePathError);
|
|
372
|
+
// Parsing is not reading: a report that parses but is not in the shape its reader expects would
|
|
373
|
+
// be read as nothing, or read until it threw, so it is refused here with where its shape is wrong.
|
|
374
|
+
const misshapenInputError = checkInputShapes(jsonInputsOf(args, existsSync), (path) => readFileSync(path, "utf8"));
|
|
375
|
+
if (misshapenInputError)
|
|
376
|
+
exitUsage(misshapenInputError);
|
|
377
|
+
// The FEATURE-CORPUS integrity checkpoint. The feature corpus is an
|
|
290
378
|
// input too — the ledger's DEBIT side — and it was read by a hand-rolled TAG-LINE SCAN that
|
|
291
379
|
// never consulted a parser. So a malformed tag line simply stopped being a tag line and its
|
|
292
380
|
// scenario CEASED TO EXIST: one stray character deleted a failing row and turned an
|
|
@@ -297,10 +385,10 @@ export function runBalance(argv) {
|
|
|
297
385
|
// Validated with the REAL Gherkin parser (cucumber's own), so a file the tool would reconcile is
|
|
298
386
|
// at least a file cucumber would RUN. A file it rejects is an input we CANNOT READ: exit 2,
|
|
299
387
|
// halting BEFORE any reconcile, naming the file and the parse failure — the same Rule as
|
|
300
|
-
//
|
|
388
|
+
// the unparseable-report and unreadable-saved-report guards ("an input the tool cannot read is a usage error, never a
|
|
301
389
|
// verdict"), now on the obligation side. Never a verdict, and never a green.
|
|
302
390
|
//
|
|
303
|
-
// The corpus is EXTRACTED from the same real AST
|
|
391
|
+
// The corpus is EXTRACTED from the same real AST, so what the tool reconciles is
|
|
304
392
|
// what cucumber runs — Feature/Rule tag inheritance included. This guard decides whether the file
|
|
305
393
|
// can be read at all; parseScenarios then reads it exactly as cucumber would.
|
|
306
394
|
const featureParseErrors = featureCorpusParseErrors(args["features"]);
|
|
@@ -310,50 +398,16 @@ export function runBalance(argv) {
|
|
|
310
398
|
}
|
|
311
399
|
process.exit(2);
|
|
312
400
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const duplicateScnIds = featureCorpusDuplicateScnIds(args["features"]);
|
|
324
|
-
if (duplicateScnIds.length > 0) {
|
|
325
|
-
process.stderr.write(`The --features corpus names the same @SCN on more than one scenario: ` +
|
|
326
|
-
`${duplicateScnIds.join(", ")}.\n` +
|
|
327
|
-
`An @SCN identifies exactly one scenario — evidence citing a duplicated id cannot be ` +
|
|
328
|
-
`attributed, so no verdict over this corpus would be trustworthy. (An @SCN on a Feature: ` +
|
|
329
|
-
`or Rule: line is inherited by every scenario beneath it — name it on the scenario.)\n`);
|
|
330
|
-
process.exit(2);
|
|
331
|
-
}
|
|
332
|
-
const inputs = reconcileInputsOf(args);
|
|
333
|
-
// Reconcile the obligation-vs-observation evidence into the Ledger. The runtime evidence-observation
|
|
334
|
-
// views (Test volume, the evidence grid) are FOLDS the renderers derive off this Ledger's posted
|
|
335
|
-
// credits (@SCN-RPT-010 / 3F-2103 — the census is retired), so they cannot drift from the
|
|
336
|
-
// reconciler's verdict: they ARE the same rows. `withRuntimeObservations: true` (below) tells the
|
|
337
|
-
// renderers the axis is in play.
|
|
338
|
-
const balance = runReconcile(inputs);
|
|
339
|
-
// Read the scenario-corpus census alongside the balance (@SCN-RPT-008) —
|
|
340
|
-
// the raw-vs-parsed @SCN-occurrence delta over the same discovered feature corpus.
|
|
341
|
-
// A sibling to the balance, threaded to the renderers via writeOutputs like ctx.
|
|
342
|
-
const evidenceObligations = readEvidenceObligations(args["features"]);
|
|
343
|
-
// The static-check kind-counts are a FOLD OVER THE LEDGER (@SCN-USG-001 / 3F-2118) — the
|
|
344
|
-
// renderers count the balance's own posted static-check credits (`staticObservationTally`), so
|
|
345
|
-
// they cannot drift from the reconciler's verdict: they ARE the same rows. The parallel static
|
|
346
|
-
// census is RETIRED; `withStaticChecks: true` (below) tells the renderers the axis is in play.
|
|
347
|
-
// Read the target's feature-code→slug map alongside the balance (@SCN-RPT-006)
|
|
348
|
-
// — the by-feature cut's full-name labels. The 5th sibling, threaded to the MARKDOWN
|
|
349
|
-
// renderer via writeOutputs only (markdown-only; the by-feature cut isn't on JSON).
|
|
350
|
-
const featureNames = readFeatureNames(args["features"]);
|
|
351
|
-
// Build the run provenance ONCE (project design §1) — a sibling RunContext, the
|
|
352
|
-
// Ledger staying pure — reusing the existing runStart. The target is the
|
|
353
|
-
// operator's --target VERBATIM (unknown-target on omit; Fork A — no
|
|
354
|
-
// resolution, no basename guessing). Source SHA via the read-only target read, with
|
|
355
|
-
// --source-sha as the override (resolveSourceSha).
|
|
356
|
-
const ctx = {
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Build the run provenance ONCE (project design §1) — a sibling RunContext, the
|
|
404
|
+
* Ledger staying pure — reusing the existing runStart. The target is the
|
|
405
|
+
* operator's --target VERBATIM (unknown-target on omit; Fork A — no
|
|
406
|
+
* resolution, no basename guessing). Source SHA via the read-only target read, with
|
|
407
|
+
* --source-sha as the override (resolveSourceSha).
|
|
408
|
+
*/
|
|
409
|
+
function runContextOf(args, inputs, runStart) {
|
|
410
|
+
return {
|
|
357
411
|
target: sourceTarget(args),
|
|
358
412
|
inputs,
|
|
359
413
|
runStart,
|
|
@@ -362,118 +416,18 @@ export function runBalance(argv) {
|
|
|
362
416
|
targetDir: args["features"] !== undefined ? dirname(args["features"]) : undefined,
|
|
363
417
|
}),
|
|
364
418
|
// The TARGET repo root the recorded input paths are made portable against (fixed to
|
|
365
|
-
// the target root, not the run cwd
|
|
419
|
+
// the target root, not the run cwd) — the SAME
|
|
366
420
|
// root readEvidenceObservations passes to countRuntimeEvidenceObservationKinds, derived from
|
|
367
421
|
// --features, so a cross-repo reconcile records inputs relative (`features`) rather
|
|
368
422
|
// than leaking the target's absolute path.
|
|
369
423
|
root: resolveTargetRoot({ features: inputs.features }),
|
|
370
|
-
// The TOOL's own identity
|
|
424
|
+
// The TOOL's own identity — which spec-controller produced
|
|
371
425
|
// this run, distinct from the target's sourceSha. Read from the tool's OWN dir, never
|
|
372
426
|
// cwd (which is the target). One call, spread into the ctx; the same identity feeds
|
|
373
427
|
// the run.yaml manifest.
|
|
374
428
|
...resolveToolIdentity(),
|
|
429
|
+
strict: args["strict"] !== undefined,
|
|
375
430
|
};
|
|
376
|
-
// Plan the outputs from the --format specs (no flag → md→stdout) and write them via
|
|
377
|
-
// the writer — the pure plan / impure writer split (@SCN-FMT-001). The plan
|
|
378
|
-
// parse+validate is pure; the writer is the only IO for the emitted report. This is
|
|
379
|
-
// the SOLE output path: the tool emits via --format and holds no storage opinion — the
|
|
380
|
-
// legacy runs/<target>/<ISO>/ auto-archive was removed with @SCN-RUN-001, the
|
|
381
|
-
// caller now routes stored runs (spec-controller's run-management layer).
|
|
382
|
-
const planResult = planOutput(collectFormatSpecs(argv));
|
|
383
|
-
if (!planResult.ok) {
|
|
384
|
-
process.stderr.write(planResult.error + "\n");
|
|
385
|
-
// Usage/config error (bad --format spec) → exit 2 (@SCN-CLI-002), distinct from the
|
|
386
|
-
// out-of-balance verdict code 1 (@SCN-CLI-004).
|
|
387
|
-
process.exit(2);
|
|
388
|
-
}
|
|
389
|
-
writeOutputs(planResult.plan, { balance, ctx, evidenceObligations, withRuntimeObservations: true, withStaticChecks: true, featureNames });
|
|
390
|
-
// The CI gate (@SCN-CLI-004): `balance` is a GATE, not just a reporter. AFTER the report
|
|
391
|
-
// is written in full, map the whole-run verdict to the process exit code. Out-of-balance
|
|
392
|
-
// (ANY reconciling item anywhere: an out-of-balance scenario OR a suspense-row item such
|
|
393
|
-
// as a no-evidence-obligation static-check watermelon) sets process.exitCode = 1 and RETURNS — set,
|
|
394
|
-
// never `process.exit(1)`, so the buffered report is flushed intact rather than truncated
|
|
395
|
-
// mid-write. It exits EXACTLY 1 so it reds a CI build and can't masquerade as the usage
|
|
396
|
-
// error (2, above). An ENUMERATED status, never a count (8-bit wrap → false pass). A
|
|
397
|
-
// balanced run leaves process.exitCode at its default 0 and returns.
|
|
398
|
-
//
|
|
399
|
-
// The --strict escalation (@SCN-CLI-011): Pending Items are run-neutral by DEFAULT
|
|
400
|
-
// (@SCN-PND-008 — they never red a build), but `--strict` is the OPT-IN that fails an
|
|
401
|
-
// OTHERWISE-BALANCED run carrying them, exiting the DISTINCT code 3 (taxonomy 0 balanced /
|
|
402
|
-
// 1 out-of-balance / 2 usage / 3 pending-under-strict / 4 unsound / 5 nothing to reconcile —
|
|
403
|
-
// a Pending Item is not out-of-balance,
|
|
404
|
-
// so it must NEVER share code 1). Out-of-balance DOMINATES: the escalation is checked ONLY in
|
|
405
|
-
// the `isBalanced` branch, so an out-of-balance run exits 1 regardless of --strict. Like the
|
|
406
|
-
// CLI-004 gate this SETS process.exitCode (never process.exit) AFTER writeOutputs, so the
|
|
407
|
-
// buffered report is flushed intact. `--strict` is a registry-LISTED boolean, recorded by
|
|
408
|
-
// parseArgs as the string "true" — presence is what matters, so `!== undefined` reads it.
|
|
409
|
-
//
|
|
410
|
-
// The UNSOUND pre-emption (@SCN-CLI-012): a run carrying BROKEN evidence — a proving suite
|
|
411
|
-
// that failed to COLLECT, so the behaviour was never exercised — exits the DISTINCT code 4
|
|
412
|
-
// (taxonomy 0 balanced / 1 out-of-balance / 2 usage / 3 pending-under-strict / 4 unsound /
|
|
413
|
-
// 5 nothing to reconcile).
|
|
414
|
-
// Exit 1 says "the ledger disagrees"; exit 4 says "the ledger could not be TRUSTED to
|
|
415
|
-
// disagree" — you cannot trust a reconciliation whose evidence never materialised, so
|
|
416
|
-
// unsound DOMINATES.
|
|
417
|
-
//
|
|
418
|
-
// THE ORDER IS LOAD-BEARING, NOT COSMETIC. A broken run is ALWAYS out-of-balance: its
|
|
419
|
-
// `broken` item sits on the SUSPENSE row, so `isBalanced` is already false. Check it AFTER
|
|
420
|
-
// `isBalanced` and the branch is UNREACHABLE — every broken run would exit 1 and the
|
|
421
|
-
// collection failure would stay masked as an honest disagreement (the AWTY dogfood fault,
|
|
422
|
-
// 3F-1642). So `hasBrokenEvidence` is checked FIRST, PRE-EMPTING the balance verdict it
|
|
423
|
-
// would otherwise be swallowed by. @SCN-CLI-004 is a BYSTANDER, not an Update: its fixture
|
|
424
|
-
// is a SOUND out-of-balance run (no broken item), so it falls through this branch and still
|
|
425
|
-
// exits exactly 1 — the pre-emption fires only when evidence genuinely failed to collect.
|
|
426
|
-
//
|
|
427
|
-
// THE NOTHING-TO-RECONCILE PRE-EMPTION (@SCN-CLI-019): a run whose ledger carries ZERO Scenario
|
|
428
|
-
// Reconciliation rows — a target that declared nothing AND produced nothing — exits the DISTINCT
|
|
429
|
-
// code 5. It is its own state: not balanced (no accounts to agree), not out-of-balance (nothing
|
|
430
|
-
// disagrees), not a usage fault (the invocation was valid and `new-client` is a supported
|
|
431
|
-
// adoption state), not unsound (no proof failed; none was owed).
|
|
432
|
-
//
|
|
433
|
-
// IT IS NOT THE UNSOUND PRE-EMPTION ONE CODE FURTHER ON, THOUGH IT LOOKS LIKE IT — measured,
|
|
434
|
-
// because the ticket predicted it was. An empty ledger is MUTUALLY EXCLUSIVE with all three
|
|
435
|
-
// predicates above: with no rows there is no row to carry a broken item, a reconciling item or a
|
|
436
|
-
// Pending Item, so it fires none of them. Moved below `!isBalanced`, this branch STILL returns 5
|
|
437
|
-
// (the prescribed plant stayed green). Its position among the predicates is free, and it is
|
|
438
|
-
// first only so this ladder and `runVerdictSection` read in the SAME order — the shape
|
|
439
|
-
// @SCN-CLI-013's invariant leans on.
|
|
440
|
-
//
|
|
441
|
-
// WHAT IS LOAD-BEARING is its position against the DEFAULT below. "Balanced" is not a branch
|
|
442
|
-
// here, it is the `return 0` a run reaches by failing every test — so there is no "after the
|
|
443
|
-
// balanced case" to append a fifth code to. Appended after that return the branch is dead code
|
|
444
|
-
// and an empty run exits 0 (measured: the plant that DID red). That is why the fault survived a
|
|
445
|
-
// taxonomy that had already grown twice: a fifth code cannot be ADDED to this chain, it has to
|
|
446
|
-
// DISPLACE the default, and nothing about the other three says so.
|
|
447
|
-
//
|
|
448
|
-
// Keyed on the EMPTY LEDGER, never on the empty corpus: a corpus-less run carrying uncited
|
|
449
|
-
// credits HAS a row — its suspense row — and is an ordinary out-of-balance report at exit 1
|
|
450
|
-
// (@SCN-CLI-020). @SCN-CLI-004/005/010/012 are BYSTANDERS: every one of their fixtures carries
|
|
451
|
-
// rows, so none reaches this branch.
|
|
452
|
-
const code = verdictExitCode(balance.ledger, args["strict"] !== undefined);
|
|
453
|
-
// Assigned only when NON-ZERO, so a balanced run still LEAVES process.exitCode at its default
|
|
454
|
-
// rather than writing a 0 over it — the distinction @SCN-CLI-005 and @SCN-CLI-010 both rest on.
|
|
455
|
-
if (code !== 0)
|
|
456
|
-
process.exitCode = code;
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* THE DOMINANCE LADDER — the whole-run verdict as the enumerated status a consumer's CI branches
|
|
460
|
-
* on, and the ONE place the order argued for above is written down. Extracted from `runBalance`
|
|
461
|
-
* (3F-3344) so the ladder is a named thing with its own reading rather than a tail the reader
|
|
462
|
-
* arrives at, and so the pre-emption argument sits ON it.
|
|
463
|
-
*
|
|
464
|
-
* ENUMERATED, NEVER A COUNT — a count would 8-bit `& 0xFF` wrap into a false pass. Returns 0 for a
|
|
465
|
-
* balanced run; the caller leaves the process's default alone rather than writing that 0 back.
|
|
466
|
-
*/
|
|
467
|
-
function verdictExitCode(ledger, strict) {
|
|
468
|
-
if (hasNothingToReconcile(ledger))
|
|
469
|
-
return 5;
|
|
470
|
-
if (hasBrokenEvidence(ledger))
|
|
471
|
-
return 4;
|
|
472
|
-
if (!isBalanced(ledger))
|
|
473
|
-
return 1;
|
|
474
|
-
if (strict && hasPendingItems(ledger))
|
|
475
|
-
return 3;
|
|
476
|
-
return 0;
|
|
477
431
|
}
|
|
478
432
|
// Only run the balance command when this module is executed directly
|
|
479
433
|
// (`tsx cli-balance/cli.ts`), NOT when imported by the top-level `spec-controller`
|