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