spec-controller 0.1.0-alpha.30 → 0.1.0-alpha.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -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/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 +14 -19
- package/dist/run-management/keptRun.d.ts.map +1 -1
- package/dist/run-management/keptRun.js +15 -20
- package/dist/run-management/keptRun.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -78,7 +78,9 @@ The whole report is written before the code is set, so a failing run still tells
|
|
|
78
78
|
its own rather than falling out of the process — a crash that exits `1` would be indistinguishable,
|
|
79
79
|
by status, from an honest disagreement. **An input the tool cannot read is part of `2`**: a mistyped
|
|
80
80
|
path, an unparseable report or a corpus the Gherkin parser rejects are all the invocation being
|
|
81
|
-
wrong, never a verdict about your code.
|
|
81
|
+
wrong, never a verdict about your code. **So is an output location it cannot write**: a `--format` path that is
|
|
82
|
+
read-only, a directory, under a regular file or in a directory that does not exist, and a
|
|
83
|
+
`--store` root the run directory cannot be created under, are refused at `2`, naming the location and the operating system's reason, before any report is printed.
|
|
82
84
|
|
|
83
85
|
**The enumeration is open at the top, and that is the one thing to design your pipeline around.** A
|
|
84
86
|
state that is genuinely none of the above gets a **new** code rather than being folded into the
|
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The output
|
|
3
|
-
*
|
|
4
|
-
* the payload via the PURE renderers (`renderMarkdown` / `renderJson` in `src/render/`)
|
|
5
|
-
* and performs the IO: stdout, or a file.
|
|
6
|
-
*
|
|
7
|
-
* This is the ONLY layer that performs IO — the renderers stay pure (`model → string`)
|
|
8
|
-
* and `planOutput` stays pure (parse+validate). So the writer carries the `@integration`
|
|
9
|
-
* evidence: its real stdout/file effect is asserted directly (the anti-watermelon rule),
|
|
10
|
-
* never a re-run of the `planOutput` `@unit`.
|
|
11
|
-
*
|
|
12
|
-
* FMT-001 (`@SCN-FMT-001`) writes markdown to stdout. The `json` format (FMT-002) and the
|
|
13
|
-
* file destination (FMT-002/003) are wired on the same `render` + `dest` switch below as
|
|
14
|
-
* later sub-issues plan them.
|
|
15
|
-
*
|
|
16
|
-
* Parent: @SCN-FMT-001 — the output-dispatch rework.
|
|
2
|
+
* The output writer: renders each planned output through the pure renderers and performs the only
|
|
3
|
+
* IO in the pipeline, to stdout or to a file.
|
|
17
4
|
*/
|
|
18
5
|
import type { FeatureNames } from "@3f-consulting/spec-controller-core";
|
|
19
6
|
import type { OutputPlan } from "./format.js";
|
|
@@ -22,12 +9,9 @@ import type { EvidenceObligations } from "@3f-consulting/spec-controller-core";
|
|
|
22
9
|
import type { EvidenceReconciliation } from "@3f-consulting/spec-controller-core";
|
|
23
10
|
/**
|
|
24
11
|
* The reconciliation payloads the writer renders — the balance plus its sibling reads
|
|
25
|
-
* (ctx / evidenceObligations / featureNames), exactly the arguments the pure renderers take.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* the Static checks cut): the renderers fold BOTH off the Ledger themselves (3F-2103 / 3F-2118 —
|
|
29
|
-
* the parallel censuses are retired), so the writer threads only whether each axis is in play,
|
|
30
|
-
* never a census object.
|
|
12
|
+
* (ctx / evidenceObligations / featureNames), exactly the arguments the pure renderers take.
|
|
13
|
+
* `withRuntimeObservations` and `withStaticChecks` gate the two observation axes; the renderers
|
|
14
|
+
* fold both off the Ledger themselves, so the writer threads only whether each axis is in play.
|
|
31
15
|
*/
|
|
32
16
|
export interface OutputPayloads {
|
|
33
17
|
readonly balance: EvidenceReconciliation;
|
|
@@ -38,8 +22,9 @@ export interface OutputPayloads {
|
|
|
38
22
|
readonly featureNames?: FeatureNames;
|
|
39
23
|
}
|
|
40
24
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
25
|
+
* Every file destination is proved writable before any output is emitted, so a refused run never
|
|
26
|
+
* leaves a report behind, on stdout or in a file, whose headline names an exit the process does not
|
|
27
|
+
* take. Writing files first and stdout last would not do: an earlier file would still be written.
|
|
43
28
|
*/
|
|
44
29
|
export declare function writeOutputs(plan: OutputPlan, payloads: OutputPayloads): void;
|
|
45
30
|
//# sourceMappingURL=writer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../../src/cli-balance/emit/writer.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../../src/cli-balance/emit/writer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AACxE,OAAO,KAAK,EAAgB,UAAU,EAAiB,MAAM,aAAa,CAAC;AAC3E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qCAAqC,CAAC;AACtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC/E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAGlF;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,sBAAsB,CAAC;IACzC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IACzB,QAAQ,CAAC,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IACnD,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IACpC,QAAQ,CAAC,YAAY,CAAC,EAAE,YAAY,CAAC;CACtC;AAQD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,cAAc,GAAG,IAAI,CAO7E"}
|
|
@@ -1,48 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The output
|
|
3
|
-
*
|
|
4
|
-
* the payload via the PURE renderers (`renderMarkdown` / `renderJson` in `src/render/`)
|
|
5
|
-
* and performs the IO: stdout, or a file.
|
|
6
|
-
*
|
|
7
|
-
* This is the ONLY layer that performs IO — the renderers stay pure (`model → string`)
|
|
8
|
-
* and `planOutput` stays pure (parse+validate). So the writer carries the `@integration`
|
|
9
|
-
* evidence: its real stdout/file effect is asserted directly (the anti-watermelon rule),
|
|
10
|
-
* never a re-run of the `planOutput` `@unit`.
|
|
11
|
-
*
|
|
12
|
-
* FMT-001 (`@SCN-FMT-001`) writes markdown to stdout. The `json` format (FMT-002) and the
|
|
13
|
-
* file destination (FMT-002/003) are wired on the same `render` + `dest` switch below as
|
|
14
|
-
* later sub-issues plan them.
|
|
15
|
-
*
|
|
16
|
-
* Parent: @SCN-FMT-001 — the output-dispatch rework.
|
|
2
|
+
* The output writer: renders each planned output through the pure renderers and performs the only
|
|
3
|
+
* IO in the pipeline, to stdout or to a file.
|
|
17
4
|
*/
|
|
18
5
|
import { writeFileSync } from "node:fs";
|
|
19
6
|
import { renderMarkdown } from "@3f-consulting/spec-controller-core";
|
|
20
7
|
import { renderJson } from "@3f-consulting/spec-controller-core";
|
|
21
|
-
|
|
8
|
+
import { probeWritable, writeOrRefuse } from "../../outputLocation.js";
|
|
22
9
|
function render(fmt, p) {
|
|
23
|
-
// The axis gates moved onto the concept's factory (3F-2233) — by the time a payload reaches a
|
|
24
|
-
// renderer the totals are already folded, so there is nothing left here to switch on.
|
|
25
10
|
return fmt === "json"
|
|
26
11
|
? renderJson(p.balance, p.ctx, p.evidenceObligations, p.featureNames)
|
|
27
12
|
: renderMarkdown(p.balance, p.ctx, p.evidenceObligations, p.featureNames);
|
|
28
13
|
}
|
|
29
14
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
15
|
+
* Every file destination is proved writable before any output is emitted, so a refused run never
|
|
16
|
+
* leaves a report behind, on stdout or in a file, whose headline names an exit the process does not
|
|
17
|
+
* take. Writing files first and stdout last would not do: an earlier file would still be written.
|
|
32
18
|
*/
|
|
33
19
|
export function writeOutputs(plan, payloads) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
process.stdout.write(rendered + "\n");
|
|
40
|
-
break;
|
|
41
|
-
case "file":
|
|
42
|
-
// The file destination (FMT-002/003) — same rendered bytes, to a caller path.
|
|
43
|
-
writeFileSync(output.dest.path, rendered + "\n", "utf8");
|
|
44
|
-
break;
|
|
45
|
-
}
|
|
20
|
+
const emissions = plan.outputs.map((output) => ({ output, text: render(output.fmt, payloads) + "\n" }));
|
|
21
|
+
for (const { output } of emissions) {
|
|
22
|
+
const dest = output.dest;
|
|
23
|
+
if (dest.kind === "file")
|
|
24
|
+
writeOrRefuse(refusalSubject(output), dest.path, () => probeWritable(dest.path));
|
|
46
25
|
}
|
|
26
|
+
for (const { output, text } of emissions)
|
|
27
|
+
emit(output, text);
|
|
28
|
+
}
|
|
29
|
+
function refusalSubject(output) {
|
|
30
|
+
return `The ${output.fmt} report could not be written`;
|
|
31
|
+
}
|
|
32
|
+
function emit(output, text) {
|
|
33
|
+
const dest = output.dest;
|
|
34
|
+
if (dest.kind === "stdout") {
|
|
35
|
+
process.stdout.write(text);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
writeOrRefuse(refusalSubject(output), dest.path, () => writeFileSync(dest.path, text, "utf8"));
|
|
47
39
|
}
|
|
48
40
|
//# sourceMappingURL=writer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writer.js","sourceRoot":"","sources":["../../../src/cli-balance/emit/writer.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"writer.js","sourceRoot":"","sources":["../../../src/cli-balance/emit/writer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,qCAAqC,CAAC;AAMjE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAiBvE,SAAS,MAAM,CAAC,GAAiB,EAAE,CAAiB;IAClD,OAAO,GAAG,KAAK,MAAM;QACnB,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,mBAAmB,EAAE,CAAC,CAAC,YAAY,CAAC;QACrE,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,mBAAmB,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC;AAC9E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,IAAgB,EAAE,QAAwB;IACrE,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;IACxG,KAAK,MAAM,EAAE,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;QACzB,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;YAAE,aAAa,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC7G,CAAC;IACD,KAAK,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,SAAS;QAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAC/D,CAAC;AAED,SAAS,cAAc,CAAC,MAAqB;IAC3C,OAAO,OAAO,MAAM,CAAC,GAAG,8BAA8B,CAAC;AACzD,CAAC;AAED,SAAS,IAAI,CAAC,MAAqB,EAAE,IAAY;IAC/C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;IACzB,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC3B,OAAO;IACT,CAAC;IACD,aAAa,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;AACjG,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function unwritableRefusal(subject: string, path: string, err: unknown): string;
|
|
2
|
+
/**
|
|
3
|
+
* The catch sits on the one write it guards rather than on the process, so a refusal can only ever
|
|
4
|
+
* name a location this call was asked to write, and a crash anywhere else is not dressed up as one.
|
|
5
|
+
*/
|
|
6
|
+
export declare function writeOrRefuse(subject: string, path: string, write: () => void): void;
|
|
7
|
+
/**
|
|
8
|
+
* Throws the operating system's own error when `path` cannot be written as a file, and leaves the
|
|
9
|
+
* path as it found it: an existing file is opened write-only without truncation (not "r+", which
|
|
10
|
+
* would also demand read permission), and an absent one is created exclusively and removed again,
|
|
11
|
+
* so a missing, read-only or non-directory parent answers with its real reason instead of one this
|
|
12
|
+
* module would have to invent.
|
|
13
|
+
*/
|
|
14
|
+
export declare function probeWritable(path: string): void;
|
|
15
|
+
//# sourceMappingURL=outputLocation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outputLocation.d.ts","sourceRoot":"","sources":["../src/outputLocation.ts"],"names":[],"mappings":"AAIA,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,CAGrF;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,IAAI,GAAG,IAAI,CAOpF;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAShD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { closeSync, constants, openSync, unlinkSync } from "node:fs";
|
|
2
|
+
const USAGE_EXIT_CODE = 2;
|
|
3
|
+
export function unwritableRefusal(subject, path, err) {
|
|
4
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
5
|
+
return `${subject}: ${path} (${reason})`;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The catch sits on the one write it guards rather than on the process, so a refusal can only ever
|
|
9
|
+
* name a location this call was asked to write, and a crash anywhere else is not dressed up as one.
|
|
10
|
+
*/
|
|
11
|
+
export function writeOrRefuse(subject, path, write) {
|
|
12
|
+
try {
|
|
13
|
+
write();
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
process.stderr.write(unwritableRefusal(subject, path, err) + "\n");
|
|
17
|
+
process.exit(USAGE_EXIT_CODE);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Throws the operating system's own error when `path` cannot be written as a file, and leaves the
|
|
22
|
+
* path as it found it: an existing file is opened write-only without truncation (not "r+", which
|
|
23
|
+
* would also demand read permission), and an absent one is created exclusively and removed again,
|
|
24
|
+
* so a missing, read-only or non-directory parent answers with its real reason instead of one this
|
|
25
|
+
* module would have to invent.
|
|
26
|
+
*/
|
|
27
|
+
export function probeWritable(path) {
|
|
28
|
+
try {
|
|
29
|
+
closeSync(openSync(path, constants.O_WRONLY));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (err.code !== "ENOENT")
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
closeSync(openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL));
|
|
37
|
+
unlinkSync(path);
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=outputLocation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outputLocation.js","sourceRoot":"","sources":["../src/outputLocation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErE,MAAM,eAAe,GAAG,CAAC,CAAC;AAE1B,MAAM,UAAU,iBAAiB,CAAC,OAAe,EAAE,IAAY,EAAE,GAAY;IAC3E,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAChE,OAAO,GAAG,OAAO,KAAK,IAAI,KAAK,MAAM,GAAG,CAAC;AAC3C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,OAAe,EAAE,IAAY,EAAE,KAAiB;IAC5E,IAAI,CAAC;QACH,KAAK,EAAE,CAAC;IACV,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;QACnE,OAAO,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAChC,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC;QACH,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC9C,OAAO;IACT,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,GAAG,CAAC;IAClE,CAAC;IACD,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,QAAQ,GAAG,SAAS,CAAC,OAAO,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IACrF,UAAU,CAAC,IAAI,CAAC,CAAC;AACnB,CAAC"}
|
|
@@ -15,24 +15,20 @@
|
|
|
15
15
|
* files under a temp root + the repo untouched — never a planner re-run).
|
|
16
16
|
*
|
|
17
17
|
* The store root is an INJECTED parameter — never hardcoded — so the scaffold is
|
|
18
|
-
* location-agnostic (proven against a temp root; the repo is never written).
|
|
19
|
-
* the dir + the routed outputs; the `run.yaml` stamp (RMG-002) and the `--run-id` mnemonic
|
|
20
|
-
* override (RMG-003) land in later sub-issues.
|
|
21
|
-
*
|
|
22
|
-
* Parent: @SCN-RMG-001 — the run-management layer.
|
|
18
|
+
* location-agnostic (proven against a temp root; the repo is never written).
|
|
23
19
|
*/
|
|
24
20
|
/** The two artefact filenames a kept run routes the tool's outputs into. */
|
|
25
21
|
export declare const RECONCILIATION_DOCUMENT = "spec-reconciliation.md";
|
|
26
22
|
/**
|
|
27
|
-
* The machine artefact filename
|
|
23
|
+
* The machine artefact filename — renamed `ledger.json` →
|
|
28
24
|
* `spec-reconciliation.json` (parity with `spec-reconciliation.md`): it is the machine form
|
|
29
25
|
* of the WHOLE reconciliation document, not just the ledger.
|
|
30
26
|
*/
|
|
31
27
|
export declare const RECONCILIATION_JSON = "spec-reconciliation.json";
|
|
32
|
-
/** The manifest filename a kept run stamps in the run dir
|
|
28
|
+
/** The manifest filename a kept run stamps in the run dir. */
|
|
33
29
|
export declare const RUN_MANIFEST = "run.yaml";
|
|
34
30
|
/**
|
|
35
|
-
* The run.yaml schema version this scaffold stamps. Bumped 1 → 2
|
|
31
|
+
* The run.yaml schema version this scaffold stamps. Bumped 1 → 2 when it added
|
|
36
32
|
* the tool identity (`tool_sha` / `tool_version`) — so a reader can tell a run that
|
|
37
33
|
* predates tool-identity recording (schema 1) from one that should carry it (schema 2).
|
|
38
34
|
* run.yaml is write-only (no production reader), so the bump breaks no read path.
|
|
@@ -47,7 +43,7 @@ export type RunMode = "balance" | "analyse" | "migrate";
|
|
|
47
43
|
export interface RunManifestMeta {
|
|
48
44
|
readonly sourceSha: string;
|
|
49
45
|
/**
|
|
50
|
-
* The TOOL's own git SHA — which spec-controller produced this run
|
|
46
|
+
* The TOOL's own git SHA — which spec-controller produced this run, distinct
|
|
51
47
|
* from the target's `sourceSha`. A string ("unknown" when the tool is not a git
|
|
52
48
|
* checkout), mirroring `sourceSha`'s own "unknown" fallback — so run.yaml is internally
|
|
53
49
|
* consistent (both facts use the same absent-marker, never a bare null).
|
|
@@ -73,7 +69,7 @@ export interface RunManifest {
|
|
|
73
69
|
readonly run_id: string;
|
|
74
70
|
readonly target: string;
|
|
75
71
|
readonly source_sha: string;
|
|
76
|
-
/** The TOOL's identity
|
|
72
|
+
/** The TOOL's identity — which spec-controller produced the run. */
|
|
77
73
|
readonly tool_sha: string;
|
|
78
74
|
readonly tool_version: string;
|
|
79
75
|
readonly mode: RunMode;
|
|
@@ -112,7 +108,7 @@ export type RouteOutputs = (formatSpecs: readonly string[]) => void;
|
|
|
112
108
|
*/
|
|
113
109
|
export declare function planKeptRun(spec: KeptRunSpec): KeptRunPlan;
|
|
114
110
|
/**
|
|
115
|
-
* Options for the pure run-id resolver
|
|
111
|
+
* Options for the pure run-id resolver: the operator's OPTIONAL `--run-id` mnemonic
|
|
116
112
|
* and the INJECTED clock instant the default timestamp derives from.
|
|
117
113
|
*/
|
|
118
114
|
export interface ResolveRunIdOptions {
|
|
@@ -129,14 +125,14 @@ export interface ResolveRunIdOptions {
|
|
|
129
125
|
* run-id naming the run dir (feeds `KeptRunSpec.runId`). No `--run-id` → an ISO-8601 UTC
|
|
130
126
|
* timestamp derived from the INJECTED `now` (filesystem-safe: no colons, no millis — matching
|
|
131
127
|
* the existing store run dir); `--run-id <mnemonic>` → that mnemonic verbatim (`acme-q3`, `e1`
|
|
132
|
-
* — an earlier precedent), which ignores the clock. No IO — proven `@unit
|
|
128
|
+
* — an earlier precedent), which ignores the clock. No IO — proven `@unit`.
|
|
133
129
|
* The clock is injected (never a bare `new Date()`) so the default timestamp is deterministic
|
|
134
130
|
* under test (the discipline the old `formatRunTimestamp` used). Just default-vs-override — no
|
|
135
131
|
* sanitisation / collision policy (deferred).
|
|
136
132
|
*/
|
|
137
133
|
export declare function resolveRunId(options: ResolveRunIdOptions): string;
|
|
138
134
|
/**
|
|
139
|
-
* PURE: build the run.yaml manifest object from the run metadata
|
|
135
|
+
* PURE: build the run.yaml manifest object from the run metadata. `purpose` and
|
|
140
136
|
* `links` are stamped EMPTY (the ad-hoc default; slugs are added by operator hand-edit of
|
|
141
137
|
* run.yaml afterward — the documented curation convention); `artefacts` carry the decided
|
|
142
138
|
* filenames. No IO — proven `@unit`.
|
|
@@ -145,12 +141,11 @@ export declare function planRunManifest(input: RunManifestInput): RunManifest;
|
|
|
145
141
|
/** PURE: serialise the manifest to YAML text the impure runner writes verbatim. */
|
|
146
142
|
export declare function serialiseRunManifest(manifest: RunManifest): string;
|
|
147
143
|
/**
|
|
148
|
-
* IMPURE runner: `mkdir -p` the run dir, route the tool's `--format` outputs into the two
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* creates the dir the location-agnostic tool writes into.
|
|
144
|
+
* IMPURE runner: `mkdir -p` the run dir, route the tool's `--format` outputs into the two files
|
|
145
|
+
* via the injected `route` — the document as `md:<documentPath>`, the reconciliation JSON as
|
|
146
|
+
* `json:<reconciliationJsonPath>` — and stamp `run.yaml` from the spec + `meta`. Returns the
|
|
147
|
+
* plan it realised. The `mkdir` is load-bearing: the tool's writer creates no directories, so the
|
|
148
|
+
* scaffold creates the one the location-agnostic tool writes into.
|
|
154
149
|
*/
|
|
155
150
|
export declare function executeKeptRun(spec: KeptRunSpec, meta: RunManifestMeta, route: RouteOutputs): KeptRunPlan;
|
|
156
151
|
//# sourceMappingURL=keptRun.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keptRun.d.ts","sourceRoot":"","sources":["../../src/run-management/keptRun.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"keptRun.d.ts","sourceRoot":"","sources":["../../src/run-management/keptRun.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,27 +15,24 @@
|
|
|
15
15
|
* files under a temp root + the repo untouched — never a planner re-run).
|
|
16
16
|
*
|
|
17
17
|
* The store root is an INJECTED parameter — never hardcoded — so the scaffold is
|
|
18
|
-
* location-agnostic (proven against a temp root; the repo is never written).
|
|
19
|
-
* the dir + the routed outputs; the `run.yaml` stamp (RMG-002) and the `--run-id` mnemonic
|
|
20
|
-
* override (RMG-003) land in later sub-issues.
|
|
21
|
-
*
|
|
22
|
-
* Parent: @SCN-RMG-001 — the run-management layer.
|
|
18
|
+
* location-agnostic (proven against a temp root; the repo is never written).
|
|
23
19
|
*/
|
|
24
20
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
25
21
|
import { join } from "node:path";
|
|
26
22
|
import { stringify } from "yaml";
|
|
23
|
+
import { writeOrRefuse } from "../outputLocation.js";
|
|
27
24
|
/** The two artefact filenames a kept run routes the tool's outputs into. */
|
|
28
25
|
export const RECONCILIATION_DOCUMENT = "spec-reconciliation.md";
|
|
29
26
|
/**
|
|
30
|
-
* The machine artefact filename
|
|
27
|
+
* The machine artefact filename — renamed `ledger.json` →
|
|
31
28
|
* `spec-reconciliation.json` (parity with `spec-reconciliation.md`): it is the machine form
|
|
32
29
|
* of the WHOLE reconciliation document, not just the ledger.
|
|
33
30
|
*/
|
|
34
31
|
export const RECONCILIATION_JSON = "spec-reconciliation.json";
|
|
35
|
-
/** The manifest filename a kept run stamps in the run dir
|
|
32
|
+
/** The manifest filename a kept run stamps in the run dir. */
|
|
36
33
|
export const RUN_MANIFEST = "run.yaml";
|
|
37
34
|
/**
|
|
38
|
-
* The run.yaml schema version this scaffold stamps. Bumped 1 → 2
|
|
35
|
+
* The run.yaml schema version this scaffold stamps. Bumped 1 → 2 when it added
|
|
39
36
|
* the tool identity (`tool_sha` / `tool_version`) — so a reader can tell a run that
|
|
40
37
|
* predates tool-identity recording (schema 1) from one that should carry it (schema 2).
|
|
41
38
|
* run.yaml is write-only (no production reader), so the bump breaks no read path.
|
|
@@ -60,7 +57,7 @@ export function planKeptRun(spec) {
|
|
|
60
57
|
* run-id naming the run dir (feeds `KeptRunSpec.runId`). No `--run-id` → an ISO-8601 UTC
|
|
61
58
|
* timestamp derived from the INJECTED `now` (filesystem-safe: no colons, no millis — matching
|
|
62
59
|
* the existing store run dir); `--run-id <mnemonic>` → that mnemonic verbatim (`acme-q3`, `e1`
|
|
63
|
-
* — an earlier precedent), which ignores the clock. No IO — proven `@unit
|
|
60
|
+
* — an earlier precedent), which ignores the clock. No IO — proven `@unit`.
|
|
64
61
|
* The clock is injected (never a bare `new Date()`) so the default timestamp is deterministic
|
|
65
62
|
* under test (the discipline the old `formatRunTimestamp` used). Just default-vs-override — no
|
|
66
63
|
* sanitisation / collision policy (deferred).
|
|
@@ -83,7 +80,7 @@ function formatRunTimestamp(instant) {
|
|
|
83
80
|
return instant.toISOString().replace(/\.\d+Z$/, "Z").replace(/:/g, "-");
|
|
84
81
|
}
|
|
85
82
|
/**
|
|
86
|
-
* PURE: build the run.yaml manifest object from the run metadata
|
|
83
|
+
* PURE: build the run.yaml manifest object from the run metadata. `purpose` and
|
|
87
84
|
* `links` are stamped EMPTY (the ad-hoc default; slugs are added by operator hand-edit of
|
|
88
85
|
* run.yaml afterward — the documented curation convention); `artefacts` carry the decided
|
|
89
86
|
* filenames. No IO — proven `@unit`.
|
|
@@ -113,21 +110,19 @@ export function serialiseRunManifest(manifest) {
|
|
|
113
110
|
return stringify(manifest);
|
|
114
111
|
}
|
|
115
112
|
/**
|
|
116
|
-
* IMPURE runner: `mkdir -p` the run dir, route the tool's `--format` outputs into the two
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* creates the dir the location-agnostic tool writes into.
|
|
113
|
+
* IMPURE runner: `mkdir -p` the run dir, route the tool's `--format` outputs into the two files
|
|
114
|
+
* via the injected `route` — the document as `md:<documentPath>`, the reconciliation JSON as
|
|
115
|
+
* `json:<reconciliationJsonPath>` — and stamp `run.yaml` from the spec + `meta`. Returns the
|
|
116
|
+
* plan it realised. The `mkdir` is load-bearing: the tool's writer creates no directories, so the
|
|
117
|
+
* scaffold creates the one the location-agnostic tool writes into.
|
|
122
118
|
*/
|
|
123
119
|
export function executeKeptRun(spec, meta, route) {
|
|
124
120
|
const plan = planKeptRun(spec);
|
|
125
|
-
mkdirSync(plan.runDir, { recursive: true });
|
|
121
|
+
writeOrRefuse("The stored run's directory could not be created", plan.runDir, () => mkdirSync(plan.runDir, { recursive: true }));
|
|
126
122
|
route([`md:${plan.documentPath}`, `json:${plan.reconciliationJsonPath}`]);
|
|
127
|
-
// Stamp run.yaml from the spec's run-id/target (so it can't drift from the run dir) + the
|
|
128
|
-
// manifest-only meta. The serialised YAML is valid — the @integration parses it back.
|
|
129
123
|
const manifest = planRunManifest({ runId: spec.runId, target: spec.target, ...meta });
|
|
130
|
-
|
|
124
|
+
const manifestPath = join(plan.runDir, RUN_MANIFEST);
|
|
125
|
+
writeOrRefuse("The stored run's run.yaml could not be written", manifestPath, () => writeFileSync(manifestPath, serialiseRunManifest(manifest)));
|
|
131
126
|
return plan;
|
|
132
127
|
}
|
|
133
128
|
//# sourceMappingURL=keptRun.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keptRun.js","sourceRoot":"","sources":["../../src/run-management/keptRun.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"keptRun.js","sourceRoot":"","sources":["../../src/run-management/keptRun.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-controller",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.31",
|
|
4
4
|
"description": "Reconcile the behaviour your scenarios name against the evidence that actually ran — the deterministic `balance` CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"reconciliation",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@cucumber/gherkin": "^41.0.0",
|
|
26
26
|
"@cucumber/messages": "^34.0.1",
|
|
27
27
|
"yaml": "^2.9.0",
|
|
28
|
-
"@3f-consulting/spec-controller-core": "0.1.0-alpha.
|
|
28
|
+
"@3f-consulting/spec-controller-core": "0.1.0-alpha.31"
|
|
29
29
|
},
|
|
30
30
|
"publishConfig": {
|
|
31
31
|
"access": "public"
|