halfcycle 0.3.17 → 0.3.18

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.
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Source loading + fail-loud validation for the Build-Record assembler.
3
3
  *
4
- * INV-001 (local by construction): every source is a LOCAL file or git output.
5
- * The guard-eval log is a phase-scoped JSONL file the runner wrote locally
6
- * (E2-T-03b) — never a hosted store call. This module imports only
7
- * @halfcycle/events (for the wire schema) and Node builtins.
4
+ * INV-001 (local by construction): every source is a LOCAL file. The guard-eval
5
+ * log is a phase-scoped JSONL file the runner wrote locally (E2-T-03b) and the
6
+ * close commit/date come from the phase's own close record — never a hosted
7
+ * store call, and since `phase-close-commit` never a live git call either. This
8
+ * module imports only @halfcycle/events (for the wire schema), Node builtins and
9
+ * its own package.
8
10
  *
9
11
  * INV-007: the guard-eval log is parsed through `guardEvalRunSchema` from
10
12
  * @halfcycle/events, so only the FiredGuard-safe fields survive. A line
@@ -14,8 +16,8 @@
14
16
  * Fail-loud (AC2): a missing required source throws `MissingSourceError` naming
15
17
  * the input. Nothing is blanked or invented.
16
18
  */
17
- import { type GuardEvalRun } from '@halfcycle/events';
18
- import type { BuildRecordSources, GitSource, NarratedSource, OrchestrationStateSource } from './types.js';
19
+ import { type GuardEvalRun, type PhaseIdentity } from '@halfcycle/events';
20
+ import type { BuildRecordSources, NarratedSource, OrchestrationStateSource, PhaseCloseSource } from './types.js';
19
21
  import { type SeedGuard } from './types.js';
20
22
  /** Thrown when a required source artefact is missing — fail loud, never blank. */
21
23
  export declare class MissingSourceError extends Error {
@@ -39,41 +41,70 @@ export declare class MissingSourceError extends Error {
39
41
  */
40
42
  export declare function projectSeedGuards(runs: GuardEvalRun[]): SeedGuard[];
41
43
  /**
42
- * Read + parse the phase-scoped guard-eval log file (`<logDir>/phase-<N>.jsonl`).
44
+ * Read + parse the phase-scoped guard-eval log file, whose name comes from
45
+ * `renderPhaseSegment` — the same renderer the writer uses, so the reader cannot
46
+ * drift from it by inspection. The identity is a name or a number; an unsafe one
47
+ * is refused by the renderer before any path is joined.
48
+ *
43
49
  * Every line is legacy-normalised (see `normalizeLegacyLine`) then validated
44
50
  * through the strict `guardEvalRunSchema` — a mixed-era log (pre-amendment lines
45
51
  * with no `runId`/`outcome`, plus new-era `evaluated`/failure/`unconfigured`
46
52
  * lines) assembles cleanly, while an undeclared field still rejects. Fails loud
47
53
  * when the log file is absent (a required source per the field→source map).
48
54
  */
49
- export declare function readGuardEvalLog(logDir: string, phaseId: number): GuardEvalRun[];
50
- /** Load the orchestration-state task map; fail loud if the file is absent. */
51
- export declare function readOrchestrationState(phasesDir: string, phaseId: number): OrchestrationStateSource;
55
+ export declare function readGuardEvalLog(logDir: string, phaseId: PhaseIdentity): GuardEvalRun[];
52
56
  /**
53
- * Resolve the close commit + date from git HEAD. Never hand-typed.
54
- * `closeCommit` is the abbreviated HEAD sha; `closedDate` is HEAD's commit date
55
- * (YYYY-MM-DD). Fails loud if git is unavailable or the repo has no HEAD.
57
+ * Load the orchestration-state task map; fail loud if the file is absent.
58
+ * The filename is produced by `renderPhaseSegment`, so this reader and the
59
+ * Build-Record writer name a phase the same way by construction.
56
60
  */
57
- export declare function readGitClose(repoRoot: string): GitSource;
61
+ export declare function readOrchestrationState(phasesDir: string, phaseId: PhaseIdentity): OrchestrationStateSource;
62
+ /**
63
+ * Read what the phase's close recorded about the repository.
64
+ *
65
+ * THIS REPLACED A GIT CALL, and the replacement is the feature. The assembler
66
+ * used to run `git rev-parse --short HEAD` here, at ASSEMBLY time, so the commit
67
+ * and date a record named were a function of when someone happened to assemble
68
+ * it — close on Monday, assemble on Friday, and the record described Friday
69
+ * while claiming to describe the close. Nothing re-derives either value now.
70
+ *
71
+ * The filename comes from `closeRecordPath` — the same function the close-side
72
+ * writer calls, which builds it through the same renderer every other
73
+ * phase-scoped filename here goes through. Writer and reader cannot drift.
74
+ *
75
+ * AN ABSENT FILE IS ABSENCE, not a failure, and it is the one required-source
76
+ * reader here that does not fail loud. Closing a phase has never needed git, so
77
+ * a close that recorded nothing is legitimate — as is a close made before this
78
+ * file existed, whose commit is simply unrecoverable. Both read as `null` and
79
+ * the record says so. A PRESENT file that cannot be trusted still fails loud,
80
+ * because that is a broken input rather than an absent one.
81
+ */
82
+ export declare function readPhaseClose(repoRoot: string, phaseId: PhaseIdentity): PhaseCloseSource;
58
83
  /**
59
84
  * Validate that a caller-supplied narrated bundle has every required field.
60
85
  * The narrated fields are lifted from prose artefacts (phase doc, acceptance
61
86
  * report, COE notes, ENGAGEMENT ledger) by the loader; this guard makes the
62
87
  * "missing source fails loud" contract hold for them too.
88
+ *
89
+ * It also refuses an unsafe `phase.id` (INV-023). That field is not merely data:
90
+ * it is copied into the assembled record and interpolated into the record's own
91
+ * output filename, so it is the one narrated field that decides where the record
92
+ * is written. It is validated here because this is where every route converges.
63
93
  */
64
94
  export declare function validateNarrated(narrated: NarratedSource): NarratedSource;
65
95
  /**
66
96
  * Assemble the complete `BuildRecordSources` bundle from a phase's local
67
97
  * artefacts. This is the local-by-construction (INV-001) loader: it reads the
68
- * orchestration state, the runner's local guard-eval log, git HEAD, and the
69
- * caller-lifted narrated + touched-invariants inputs. Every required source is
70
- * present or it throws.
98
+ * orchestration state, the runner's local guard-eval log, the phase's own close
99
+ * record, and the caller-lifted narrated + touched-invariants inputs. Every
100
+ * required source is present or it throws — with one deliberate exception named
101
+ * at `readPhaseClose`, whose absence is a legitimate answer rather than a gap.
71
102
  */
72
103
  export declare function loadSources(opts: {
73
104
  repoRoot: string;
74
105
  phasesDir: string;
75
106
  guardEvalLogDir: string;
76
- phaseId: number;
107
+ phaseId: PhaseIdentity;
77
108
  narrated: NarratedSource;
78
109
  touchedInvariants: string[];
79
110
  }): BuildRecordSources;
@@ -1 +1 @@
1
- {"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["../../src/build-record/sources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,KAAK,EACV,kBAAkB,EAClB,SAAS,EACT,cAAc,EACd,wBAAwB,EACzB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAE9D,kFAAkF;AAClF,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAI/C;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,SAAS,EAAE,CAwBnE;AAwED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,YAAY,EAAE,CAoBhF;AAMD,8EAA8E;AAC9E,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,wBAAwB,CAsB1B;AAMD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAqBxD;AAMD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,cAAc,GAAG,cAAc,CAoBzE;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,cAAc,CAAC;IACzB,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B,GAAG,kBAAkB,CAerB"}
1
+ {"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["../../src/build-record/sources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,aAAa,EACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EACV,kBAAkB,EAClB,cAAc,EACd,wBAAwB,EACxB,gBAAgB,EACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAI9D,kFAAkF;AAClF,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAI/C;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,SAAS,EAAE,CAwBnE;AAwED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,YAAY,EAAE,CAoBvF;AAMD;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,aAAa,GACrB,wBAAwB,CAsB1B;AAMD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,gBAAgB,CAyBzF;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,cAAc,GAAG,cAAc,CA2BzE;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,aAAa,CAAC;IACvB,QAAQ,EAAE,cAAc,CAAC;IACzB,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B,GAAG,kBAAkB,CAerB"}
@@ -1 +1 @@
1
- {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/build-record/template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,YAAY,CAAC;AAe3D;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CA0EvE"}
1
+ {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/build-record/template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,YAAY,CAAC;AAsD3D;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAiFvE"}
@@ -12,7 +12,7 @@
12
12
  * `severity`, optional per-firing `explanation`). It is the ONLY guard-derived
13
13
  * shape in the record. There is no path from a `GuardRecord` to this file.
14
14
  */
15
- import type { FiredGuard, GuardEvalRun } from '@halfcycle/events';
15
+ import type { FiredGuard, GuardEvalRun, PhaseIdentity } from '@halfcycle/events';
16
16
  /** The v1 format string — a single literal, one home. */
17
17
  export declare const BUILD_RECORD_FORMAT: "halfcycle-build-record/v1";
18
18
  /**
@@ -81,18 +81,40 @@ export interface Tasks {
81
81
  landed: number;
82
82
  cancelled: string[];
83
83
  }
84
- /** The v1 Build Record — the JSON companion. Field order matters for the seed. */
84
+ /**
85
+ * The v1 Build Record — the JSON companion. Field order matters for the seed.
86
+ *
87
+ * `phase.id` is a `PhaseIdentity` — a name OR a number — because a phase is
88
+ * identified by whatever the client called it, and three of this repo's own
89
+ * phases are numbered while twenty-four are named. The widening is deliberately
90
+ * NOT a narrowing to `string`: `docs/build-records/phase-0.json` carries the
91
+ * JSON number `0`, the assembler copies the identity through untransformed, and
92
+ * `packages/bundle/test/build-record-phase0.test.ts` deep-equals the two. Under
93
+ * the union that conformance seed passes with no fixture edit; under `string`
94
+ * alone it would have needed a re-conform and a second operator approval.
95
+ *
96
+ * `closeCommit` and `closedDate` ADMIT ABSENCE, which is the v1 shape moving
97
+ * rather than a field being added. Both were required strings, on the premise
98
+ * that a record could always be given them because they were derived from git at
99
+ * assembly time. They are now captured at the close, and a client with no git
100
+ * closes successfully — so the pair has a legitimate "not recorded" state, and a
101
+ * required string could only have been satisfied by inventing one or by
102
+ * refusing to assemble the record at all. The conformance seed carries strings
103
+ * in both and is unaffected: widening a field admits the value it already held.
104
+ */
85
105
  export interface BuildRecordV1 {
86
106
  format: typeof BUILD_RECORD_FORMAT;
87
107
  engagement: string;
88
108
  engagementType: string;
89
109
  phase: {
90
- id: number;
110
+ id: PhaseIdentity;
91
111
  name: string;
92
112
  };
93
113
  status: string;
94
- closedDate: string;
95
- closeCommit: string;
114
+ /** The date the phase closed, or null when the close recorded none. */
115
+ closedDate: string | null;
116
+ /** The commit the phase closed at, or null when the close recorded none. */
117
+ closeCommit: string | null;
96
118
  thesis: string;
97
119
  thesisHeld: boolean;
98
120
  delivered: Delivered;
@@ -106,7 +128,8 @@ export interface BuildRecordV1 {
106
128
  }
107
129
  /**
108
130
  * Orchestration-state projection: task terminal states + engagement identity.
109
- * Loaded from `phase-<N>-orchestration-state.json` (Zone-B process artefact).
131
+ * Loaded from the phase-scoped orchestration-state file, whose name is produced
132
+ * by `renderPhaseSegment` and never spelled out here (Zone-B process artefact).
110
133
  * Terminal states: `merged`/`done` → landed; `cancelled` → cancelled.
111
134
  */
112
135
  export interface OrchestrationStateSource {
@@ -127,7 +150,7 @@ export interface NarratedSource {
127
150
  engagement: string;
128
151
  engagementType: string;
129
152
  phase: {
130
- id: number;
153
+ id: PhaseIdentity;
131
154
  name: string;
132
155
  };
133
156
  status: string;
@@ -138,10 +161,24 @@ export interface NarratedSource {
138
161
  instruments: Instruments;
139
162
  deviations: string[];
140
163
  }
141
- /** Git source — HEAD at close. Never hand-typed. */
142
- export interface GitSource {
143
- closeCommit: string;
144
- closedDate: string;
164
+ /**
165
+ * What the phase's close recorded about the repository.
166
+ *
167
+ * NOT resolved at assembly time, which is the change this type carries. Both
168
+ * values are captured when the phase closes and read back from the local close
169
+ * record (`close-record.ts`), so assembling the same phase twice returns the
170
+ * same document however much work has happened since.
171
+ *
172
+ * `null` on both is a close that recorded neither — a client with no git, no
173
+ * repository or no commit yet closes successfully and this reads as absence
174
+ * rather than as a blank or a value derived later. The pair moves together on
175
+ * both paths, so one is never null while the other is a string.
176
+ */
177
+ export interface PhaseCloseSource {
178
+ /** The commit the repository was on when the phase closed, or null if none was recorded. */
179
+ closeCommit: string | null;
180
+ /** The date of that commit, or null if none was recorded. */
181
+ closedDate: string | null;
145
182
  }
146
183
  /**
147
184
  * The complete typed source bundle. Every required source is present or the
@@ -151,7 +188,7 @@ export interface GitSource {
151
188
  export interface BuildRecordSources {
152
189
  orchestration: OrchestrationStateSource;
153
190
  narrated: NarratedSource;
154
- git: GitSource;
191
+ close: PhaseCloseSource;
155
192
  /** The runner's LOCAL guard-eval log for the phase (INV-007-safe wire records). */
156
193
  guardEvalRuns: GuardEvalRun[];
157
194
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/build-record/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/build-record/types.ts"],"names":[],"mappings":"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"}
@@ -2,8 +2,50 @@
2
2
  * Write-disciplined Build-Record writer.
3
3
  *
4
4
  * Blast radius: the writer touches ONLY `docs/build-records/` in the target
5
- * repo (same write-discipline as the bundle installer, E1-T-06). It refuses any
6
- * output directory that does not resolve under `docs/build-records/`.
5
+ * repo (same write-discipline as the bundle installer, E1-T-06), and now
6
+ * enforces it rather than claiming it (INV-023).
7
+ *
8
+ * THE RESIDUAL CHECK, AND WHY IT IS NOT DEFENCE IN DEPTH. The identity is
9
+ * validated where every caller converges (`validateNarrated`), but
10
+ * `assembleBuildRecord` and `writeBuildRecord` are exported individually too, so
11
+ * a library caller can compose them without entering `loadSources` at all. For
12
+ * that caller the assertion below is the ONLY guard.
13
+ *
14
+ * It asserts on the joined OUTPUT PATH, not on the output directory. The
15
+ * directory is a constant and the filename is the caller-influenced half, so a
16
+ * directory comparison guards the half that was never at risk — which is exactly
17
+ * how this function's previous check came to compare a value with itself and
18
+ * refuse nothing. The two operands here are genuinely different: `outDir` is
19
+ * derived from `repoRoot` and a constant and does not move when the record
20
+ * changes, while the candidate path is derived from `record.phase.id` and moves
21
+ * with every record.
22
+ *
23
+ * WHAT REFUSES FIRST CHANGED WHEN THE FILENAME MOVED TO THE RENDERER, and saying
24
+ * so is better than leaving a reader to discover it. The residual refusal for a
25
+ * library caller is now the `isValidPhaseIdentity` check in `writeBuildRecord`
26
+ * below, which throws `InvalidPhaseIdentityError` naming this module's allowed
27
+ * location — same error and same message a caller entering through
28
+ * `validateNarrated` gets. `assertInsideOutDir` still runs on every write, but it
29
+ * can no longer be the thing that fires on a hostile identity, because a hostile
30
+ * identity never reaches the join: the identity check refuses it and the renderer
31
+ * refuses it again.
32
+ *
33
+ * SO THE RESIDUAL GUARD IS UNREACHABLE FROM THIS PACKAGE, and that is measured
34
+ * rather than assumed: deleting both of its call sites leaves this package's
35
+ * whole suite green, and sixteen adversarial identities that pass the predicate
36
+ * were probed without one reaching it. What it still guards is real and it is
37
+ * CROSS-PACKAGE — the RENDERER'S OUTPUT. Every segment the renderer emits today
38
+ * is a plain basename, so an edit in `packages/events` that let a separator into
39
+ * one is the only way this can fire, and that edit is invisible from here. A
40
+ * guard whose subject lives in another package cannot be exercised by a
41
+ * behavioural test of this one, so it is exported and tested directly rather
42
+ * than left module-private, where nothing could reach it and nothing would
43
+ * notice its deletion.
44
+ *
45
+ * Refusal is ALL-OR-NOTHING because both paths are asserted BEFORE the first
46
+ * `writeFileSync` — and before the `mkdirSync`, so a refused record does not
47
+ * leave an empty directory behind either. There is no rollback and no two-phase
48
+ * commit; nothing here asks for one.
7
49
  *
8
50
  * The JSON is serialised with a stable 2-space indent + trailing newline; the
9
51
  * markdown data slots are projected from the same record object, so the pair
@@ -18,9 +60,24 @@ export interface WriteResult {
18
60
  }
19
61
  /** Serialise the record JSON deterministically (2-space indent + trailing NL). */
20
62
  export declare function serialiseRecordJson(record: BuildRecordV1): string;
63
+ /**
64
+ * Assert that a joined output path is still a plain file directly inside
65
+ * `outDir`. `relative` is computed between two values that move independently —
66
+ * the allowed directory, and a candidate path built from the record — so this
67
+ * comparison can actually fail.
68
+ *
69
+ * EXPORTED SO IT CAN BE TESTED AT ALL. Its subject is a filename segment that
70
+ * grew a separator, which only an edit in another package can produce, so no
71
+ * behavioural test of this package can reach it — see the module header. It is
72
+ * called directly by `build-record-write-discipline.test.ts` with a candidate
73
+ * outside the directory, which is the only control that can tell this guard
74
+ * from a deleted one.
75
+ */
76
+ export declare function assertInsideOutDir(outDir: string, candidate: string, identity: unknown): void;
21
77
  /**
22
78
  * Write the `.json` + `.md` pair for a phase into `docs/build-records/`.
23
- * Refuses to write outside that directory (write-discipline enforcement).
79
+ * Refuses — writing nothing at all — if the record's identity would put either
80
+ * file anywhere else.
24
81
  */
25
82
  export declare function writeBuildRecord(repoRoot: string, record: BuildRecordV1): WriteResult;
26
83
  //# sourceMappingURL=write.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../src/build-record/write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,QAAgC,CAAC;AAE9D,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,kFAAkF;AAClF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAEjE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,WAAW,CAerF"}
1
+ {"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../src/build-record/write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAIhD,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,QAAgC,CAAC;AAE9D,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,kFAAkF;AAClF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAEjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAM7F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,WAAW,CA2BrF"}
@@ -34,12 +34,25 @@
34
34
  * parses with. It never imports the control service.
35
35
  */
36
36
  import { type AcceptanceOutcome, type PhaseCloseDecision } from '@halfcycle/events';
37
- import type { RepoCredential } from './open-phase.js';
37
+ import { type PhaseStampFailure, type RepoCredential } from './open-phase.js';
38
+ import { type CloseRecordOutcome } from './build-record/close-record.js';
38
39
  /** What `POST /engagements/:id/accept` returns on 200 (the fields the CLI reports). */
39
40
  export interface ClosedPhase {
40
41
  engagementId: string;
41
42
  /** The lifecycle status after the close — `closed`. */
42
43
  status: string;
44
+ /**
45
+ * Set when the phase closed but this machine's record of it could not be
46
+ * cleared. `null` on every ordinary success. Until it is cleared, work on this
47
+ * machine is still filed under a phase that is no longer open.
48
+ */
49
+ stampFailure: PhaseStampFailure | null;
50
+ /**
51
+ * Whether the commit this phase closed at was written down on this machine,
52
+ * and where. When it was not, this says why, and the phase's Build Record will
53
+ * say the commit and the close date were not recorded.
54
+ */
55
+ closeRecord: CloseRecordOutcome;
43
56
  }
44
57
  /**
45
58
  * A refusal from the control plane, carried as an error the CLI can print whole.
@@ -105,10 +118,16 @@ export declare class ClosePhaseRefused extends Error {
105
118
  * for the same reason `openPhase` requires its entry decision: there
106
119
  * is no argument-less way to move the phase axis, so an unattributed
107
120
  * close is unreachable from any caller that compiles.
121
+ * @param repoRoot the repository whose commit this close is being recorded against.
122
+ * Optional so that a caller with no repository in hand — a test
123
+ * harness, a future non-repo surface — is not forced to invent one;
124
+ * omitting it records no commit rather than silently resolving the
125
+ * process's working directory, which would write a stranger's HEAD
126
+ * into this phase's record.
108
127
  *
109
128
  * @throws ClosePhaseRefused on a 4xx the plane explained, so the CLI prints the
110
129
  * plane's own sentence and exits non-zero.
111
130
  * @throws Error on transport failure or a body that is not the expected shape.
112
131
  */
113
- export declare function closePhase(credential: RepoCredential, phase: string, outcome: AcceptanceOutcome, close: PhaseCloseDecision): Promise<ClosedPhase>;
132
+ export declare function closePhase(credential: RepoCredential, phase: string, outcome: AcceptanceOutcome, close: PhaseCloseDecision, home?: string, repoRoot?: string): Promise<ClosedPhase>;
114
133
  //# sourceMappingURL=close-phase.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"close-phase.d.ts","sourceRoot":"","sources":["../src/close-phase.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,uFAAuF;AACvF,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;aAExB,MAAM,EAAE,MAAM;IAE9B;;;;;OAKG;aACa,OAAO,CAAC,EAAE,MAAM;IAChC;;;;;;;;;;OAUG;aACa,IAAI,CAAC,EAAE,MAAM;gBApBb,MAAM,EAAE,MAAM,EAC9B,OAAO,EAAE,MAAM;IACf;;;;;OAKG;IACa,OAAO,CAAC,EAAE,MAAM,YAAA;IAChC;;;;;;;;;;OAUG;IACa,IAAI,CAAC,EAAE,MAAM,YAAA;CAKhC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,UAAU,CAC9B,UAAU,EAAE,cAAc,EAC1B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,iBAAiB,EAC1B,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,WAAW,CAAC,CA8DtB"}
1
+ {"version":3,"file":"close-phase.d.ts","sourceRoot":"","sources":["../src/close-phase.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAmB,KAAK,iBAAiB,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC/F,OAAO,EAAoB,KAAK,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAE3F,uFAAuF;AACvF,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,YAAY,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACvC;;;;OAIG;IACH,WAAW,EAAE,kBAAkB,CAAC;CACjC;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;aAExB,MAAM,EAAE,MAAM;IAE9B;;;;;OAKG;aACa,OAAO,CAAC,EAAE,MAAM;IAChC;;;;;;;;;;OAUG;aACa,IAAI,CAAC,EAAE,MAAM;gBApBb,MAAM,EAAE,MAAM,EAC9B,OAAO,EAAE,MAAM;IACf;;;;;OAKG;IACa,OAAO,CAAC,EAAE,MAAM,YAAA;IAChC;;;;;;;;;;OAUG;IACa,IAAI,CAAC,EAAE,MAAM,YAAA;CAKhC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,UAAU,CAC9B,UAAU,EAAE,cAAc,EAC1B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,iBAAiB,EAC1B,KAAK,EAAE,kBAAkB,EACzB,IAAI,CAAC,EAAE,MAAM,EACb,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,WAAW,CAAC,CAwFtB"}
@@ -181,6 +181,25 @@ export declare const GUARD_COVERAGE_REQUIRED_KEYS: ("GUARD_SERVICE_TOKEN" | "GUA
181
181
  */
182
182
  export declare const ENGAGEMENT_ENV_KEYS: readonly ["HALFCYCLE_SERVICE_URL", "HALFCYCLE_MCP_URL", "HALFCYCLE_TOKEN", "HALFCYCLE_ENGAGEMENT_ID", "GUARD_SERVICE_URL", "GUARD_SERVICE_TOKEN", "GUARD_ENGAGEMENT_ID", "CONTROL_TELEMETRY_URL"];
183
183
  export type EngagementEnvKey = (typeof ENGAGEMENT_ENV_KEYS)[number];
184
+ /**
185
+ * The phase stamp — the one key in this file that is NOT a credential, named once
186
+ * here so the two verbs that move it (`open-phase.ts`, `close-phase.ts`) and this
187
+ * writer agree on the spelling.
188
+ *
189
+ * **Deliberately absent from `ENGAGEMENT_ENV_KEYS` above**, and that is the whole
190
+ * reason `reconcileEnvText`/`writeEngagementEnv` take a `keys` argument. It is not
191
+ * a secret, it is not written by the installer, and an install must not blank it:
192
+ * a re-install re-writes the eight credential keys and this key's line has to
193
+ * survive that untouched, which it does precisely because it is not in the list.
194
+ * The runner reads it out of this file (via the wrapper that sources it) to stamp
195
+ * the phase on every evaluation.
196
+ *
197
+ * `packages/mac-workspace-provider` writes the same name into the same file for a
198
+ * Studio-managed engagement (its own whole-file writer, INV-001-unreachable from
199
+ * here). Nothing is shared: this is the Connect writer, and the two topologies
200
+ * never run against one engagement.
201
+ */
202
+ export declare const PHASE_STAMP_ENV_KEY = "HALFCYCLE_PHASE";
184
203
  /**
185
204
  * Parse `KEY=value` lines. **PARSES, NEVER SOURCES** — a credential store is not
186
205
  * code, and `.` would execute whatever the file happens to contain. An optional
@@ -231,8 +250,31 @@ export declare function parseEnvText(raw: string): Record<string, string>;
231
250
  * install, not preserved as a foreign writer's key. A Studio-written value for it is
232
251
  * legitimately overwritten on the next Connect install; only `CONTROL_TELEMETRY_TOKEN`
233
252
  * is still genuinely foreign.
253
+ *
254
+ * **A `null` VALUE REMOVES THE KEY'S LINE. An absent one still writes `KEY=''`.**
255
+ * The two are different requests and this function had no way to make the first
256
+ * one: `values[k] ?? ''` mapped both to the empty string, so the only reachable
257
+ * way to "unset" a key was to write it blank. Blank is not absent to any reader of
258
+ * this file — the shell that SOURCES it sets the variable to the empty string, and
259
+ * the Build-Record write predicate refuses an empty phase identity, so a phase
260
+ * stamp cleared by blanking would fire a refusal on the ordinary project-scope
261
+ * return. `open-phase --none` and `close-phase` therefore pass `null` and the
262
+ * assignment leaves the file wherever it sat, `export ` prefix and all. Nothing is
263
+ * appended for a removed key.
264
+ *
265
+ * **The header block is appended AT MOST ONCE per file**, and that follows from the
266
+ * same pair of verbs: `HALFCYCLE_PHASE` leaves on every close and comes back on
267
+ * every open, so a header emitted with each re-appearance would grow the file by a
268
+ * comment block per phase. A file that already carries the header gets the new
269
+ * lines under the one it has.
270
+ *
271
+ * **Appended lines go BEFORE the file's final newline, not after a blank one.** The
272
+ * append used to land after the trailing empty element of the split, which put a
273
+ * blank line above the new block — invisible when a key is written once and never
274
+ * removed, and one blank line per phase once a key comes and goes. The
275
+ * pre-existing text is still an untouched prefix either way.
234
276
  */
235
- export declare function reconcileEnvText(existing: string | null, values: Record<string, string>, keys?: readonly string[]): string;
277
+ export declare function reconcileEnvText(existing: string | null, values: Record<string, string | null | undefined>, keys?: readonly string[]): string;
236
278
  /**
237
279
  * Read this engagement's machine-level credential, or `null` when this machine
238
280
  * holds none for it.
@@ -254,8 +296,22 @@ export type EnvWriteOutcome = 'written' | 'skipped';
254
296
  * The mode is re-applied on the skipped path too — a store that already existed at
255
297
  * the wrong mode is exactly the case `writeFileSync`'s `mode` option cannot fix,
256
298
  * and a re-install is the natural moment to repair it.
299
+ *
300
+ * **`keys` NARROWS WHAT THIS CALL OWNS, and it is why the phase stamp does not need
301
+ * a writer of its own.** Left off, this writes the eight credential keys, which is
302
+ * the installer's call and is unchanged. Passed a subset, it touches exactly those
303
+ * keys and copies every other line — ours and anyone else's — through untouched, so
304
+ * a caller that owns ONE key writes that key without re-implementing the
305
+ * read/merge/write/chmod sequence around it. That re-implementation is the failure
306
+ * this parameter exists to remove: a second writer that forgot `0o600` would loosen
307
+ * the credential file and pass every test that only checked which keys survived.
308
+ * A value of `null` deletes its key's line (see `reconcileEnvText`).
309
+ *
310
+ * The directory is created only when there is something to write. A removal-only
311
+ * call against a machine holding no store for this engagement leaves the disk
312
+ * alone rather than creating an empty file under a fresh directory.
257
313
  */
258
- export declare function writeEngagementEnv(engagementId: string, values: Record<string, string>, home?: string): EnvWriteOutcome;
314
+ export declare function writeEngagementEnv(engagementId: string, values: Record<string, string | null | undefined>, home?: string, keys?: readonly string[]): EnvWriteOutcome;
259
315
  /**
260
316
  * The shell function both generated consumers embed to find this file.
261
317
  *
@@ -1 +1 @@
1
- {"version":3,"file":"engagement-credential.d.ts","sourceRoot":"","sources":["../src/engagement-credential.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;AAiBH;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AAEtD,oFAAoF;AACpF,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7E;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,cAAc,8EAIjB,CAAC;AAEX;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,4BAA4B,mDAExC,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,mMAOtB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAmEpE;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUhE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,IAAI,GAAE,SAAS,MAAM,EAAwB,GAC5C,MAAM,CA8BR;AAMD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,MAAM,GACZ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAM/B;AAED,8DAA8D;AAC9D,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,SAAS,CAAC;AAEpD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,IAAI,CAAC,EAAE,MAAM,GACZ,eAAe,CAkBjB;AA4BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CA0ClD"}
1
+ {"version":3,"file":"engagement-credential.d.ts","sourceRoot":"","sources":["../src/engagement-credential.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;AAiBH;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AAEtD,oFAAoF;AACpF,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7E;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,cAAc,8EAIjB,CAAC;AAEX;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,4BAA4B,mDAExC,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,mMAOtB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpE;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAmErD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUhE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EACjD,IAAI,GAAE,SAAS,MAAM,EAAwB,GAC5C,MAAM,CAwDR;AAMD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,MAAM,GACZ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAM/B;AAED,8DAA8D;AAC9D,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,SAAS,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EACjD,IAAI,CAAC,EAAE,MAAM,EACb,IAAI,GAAE,SAAS,MAAM,EAAwB,GAC5C,eAAe,CAoBjB;AA4BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CA0ClD"}