halfcycle 0.3.32 → 0.3.33

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,42 +1,20 @@
1
1
  /**
2
- * @halfcycle/bundle — Build-Record assembler public API.
2
+ * @halfcycle/bundle — the Build Record's client half.
3
3
  *
4
- * At phase close, one bundle step (`halfcycle build-record <phase>`, D1) reads a
5
- * phase's LOCAL artefacts and writes `docs/build-records/phase-<N>.{md,json}`
6
- * with `format: "halfcycle-build-record/v1"`. No record content is hand-authored;
7
- * the markdown data slots project from the JSON.
4
+ * Closing a phase freezes its record on the service; the CLI fetches that frozen
5
+ * record and writes it into the client's repository as
6
+ * `docs/build-records/<segment>.{json,md}`. Nothing here assembles a record from
7
+ * local files: the local assembler, its `.workbench` close record and every local
8
+ * source it read left the shipped CLI in Connect Phase 4
9
+ * (`close-produces-the-record`), which is what removed the orchestration-state
10
+ * requirement no Connect client could meet (`#1400`).
8
11
  *
9
- * Two safety-by-construction seams:
10
- * INV-007 — guard rows enter only as `FiredGuard`-subset `SeedGuard`s
11
- * (`projectSeedGuards`); a `GuardRecord`/matcher/channel/version can
12
- * never appear (the wire types physically lack those fields).
13
- * INV-001 — every source is a LOCAL file; no hosted store / control-plane
14
- * import or call. `packages/bundle` ⊆ {core, events}.
12
+ * INV-001 — this module and everything it re-exports import only
13
+ * `@halfcycle/events` and Node builtins. The record's schema, the response schema
14
+ * and the phase grammar each have one home there; nothing here restates them.
15
15
  */
16
- export { BUILD_RECORD_FORMAT, projectSeedGuard, type SeedGuard, type Delivered, type DoneWhenResult, type Acceptance, type GatesEarnedKeep, type Instruments, type Tasks, type BuildRecordV1, type OrchestrationStateSource, type NarratedSource, type PhaseCloseSource, type BuildRecordSources, } from './types.js';
17
16
  export { isValidPhaseIdentity, assertValidPhaseIdentity, InvalidPhaseIdentityError, renderPhaseSegment, type PhaseIdentity, } from '@halfcycle/events';
18
- export { MissingSourceError, projectSeedGuards, readGuardEvalLog, readOrchestrationState, readPhaseClose, validateNarrated, loadSources, } from './sources.js';
19
- export { BUILD_RECORD_ZONE_B_DIR, CLOSE_RECORD_FORMAT, InvalidCloseRecordError, closeRecordPath, parseCloseRecord, writeCloseRecord, type CloseRecordOutcome, type PhaseCloseRecord, } from './close-record.js';
20
- export { assembleBuildRecord, deriveTasks, deriveThesisHeld, deriveInvariantsExercised, } from './assemble.js';
21
- export { renderBuildRecordMarkdown } from './template.js';
22
- export { BUILD_RECORD_DIR, serialiseRecordJson, writeBuildRecord, type WriteResult, } from './write.js';
23
- import { type WriteResult } from './write.js';
24
- import type { NarratedSource } from './types.js';
25
- import type { PhaseIdentity } from '@halfcycle/events';
26
- /**
27
- * Assemble + write the Build Record for a phase from its local artefacts.
28
- *
29
- * Idempotent (INV-009): a deterministic function of the current artefacts —
30
- * re-running overwrites the pair with the same bytes if the inputs are
31
- * unchanged. Fails loud (throws `MissingSourceError`) if any required source is
32
- * absent — never blanks or invents a field.
33
- */
34
- export declare function assemblePhaseBuildRecord(opts: {
35
- repoRoot: string;
36
- phasesDir: string;
37
- guardEvalLogDir: string;
38
- phaseId: PhaseIdentity;
39
- narrated: NarratedSource;
40
- touchedInvariants: string[];
41
- }): WriteResult;
17
+ export { renderBuildRecordV2Markdown } from './render-v2.js';
18
+ export { BUILD_RECORD_DIR, NODE_RECORD_PAIR_IO, serialiseServedRecord, writeRecordPair, type RecordPairIo, type RecordPairResult, } from './write.js';
19
+ export { REFUSAL_FOLLOW_UP, REFUSAL_REASONS, buildRecordUrl, obtainBuildRecord, refetchCommand, type ObtainOutcome, } from './obtain.js';
42
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/build-record/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,KAAK,EACV,KAAK,aAAa,EAClB,KAAK,wBAAwB,EAC7B,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,YAAY,CAAC;AAMpB,OAAO,EACL,oBAAoB,EACpB,wBAAwB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,KAAK,aAAa,GACnB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,cAAc,EACd,gBAAgB,EAChB,WAAW,GACZ,MAAM,cAAc,CAAC;AAItB,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,uBAAuB,EACvB,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,GACtB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,gBAAgB,EAChB,yBAAyB,GAC1B,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,WAAW,GACjB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,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,WAAW,CAWd"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/build-record/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,OAAO,EACL,oBAAoB,EACpB,wBAAwB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,KAAK,aAAa,GACnB,MAAM,mBAAmB,CAAC;AAK3B,OAAO,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,KAAK,YAAY,EACjB,KAAK,gBAAgB,GACtB,MAAM,YAAY,CAAC;AAIpB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,KAAK,aAAa,GACnB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Fetch a closed phase's Build Record from the Halfcycle service and write the pair
3
+ * into the client's repository — the one path both `close-phase` (after a close)
4
+ * and `build-record <phase>` (the re-fetch) take, so the two cannot drift.
5
+ *
6
+ * ONE RESPONSE-HANDLING RULE. A body that parses under `phaseRecordResponseSchema`
7
+ * is authoritative, whatever its status. Otherwise a 404 whose body is the
8
+ * service's own missing-route answer (`error: 'Not Found'`, `Route GET … not
9
+ * found.`) means the service predates Build Records. Anything else — including the
10
+ * route's own 404 for an engagement that is gone — is reported by its status and
11
+ * the service's own sentence, never reworded as a missing route.
12
+ *
13
+ * THE PATH IS THE PHASE ARGUMENT'S (INV-023). The served `phase` is compared with
14
+ * the one asked for and a mismatch writes nothing; the writer then builds its path
15
+ * from the argument alone.
16
+ *
17
+ * WHAT IS WRITTEN IS WHAT ARRIVED (INV-007). The `.json` is the served record as
18
+ * `JSON.parse` returned it, key order included — not the schema's output, which
19
+ * would reorder keys. The schema parse decides only whether it is written at all.
20
+ *
21
+ * INV-001: Node builtins, the global `fetch` and `@halfcycle/events` only.
22
+ * SHIPS TO CLIENT DISK inside the CLI: every string here is a client-facing
23
+ * sentence, and no comment sits beside an object member.
24
+ */
25
+ import { type PhaseRecordRefusalReason } from '@halfcycle/events';
26
+ import type { RepoCredential } from '../open-phase.js';
27
+ import { type RecordPairIo } from './write.js';
28
+ /**
29
+ * What came of asking for a phase's record. `written`: the pair is on disk (paths
30
+ * relative to the repository). `exists`: a file this CLI will not replace is in the
31
+ * way; nothing was written. `withheld` and `refused`: the service's answer, with its
32
+ * own sentence. `unavailable`: the record could not be obtained, and `why` says so.
33
+ */
34
+ export type ObtainOutcome = {
35
+ kind: 'written';
36
+ jsonPath: string;
37
+ mdPath: string;
38
+ } | {
39
+ kind: 'exists';
40
+ path: string;
41
+ } | {
42
+ kind: 'withheld';
43
+ message: string;
44
+ } | {
45
+ kind: 'refused';
46
+ reason: PhaseRecordRefusalReason;
47
+ message: string;
48
+ } | {
49
+ kind: 'unavailable';
50
+ why: string;
51
+ };
52
+ /**
53
+ * The line the CLI adds after the service's own refusal sentence, per reason. The
54
+ * key set is the reason enum's: the type refuses a missing key at build time, and
55
+ * `close-produces-the-record.test.ts` compares the keys with
56
+ * `phaseRecordRefusalReasonSchema.options` read from the schema.
57
+ */
58
+ export declare const REFUSAL_FOLLOW_UP: {
59
+ readonly 'still-open': "Nothing was written. The record is produced when the phase closes.";
60
+ readonly reopened: "Nothing was written. A new record is produced when the phase closes again.";
61
+ readonly 'never-opened': "Nothing was written. Check the name: it must match the phase name exactly as it was opened.";
62
+ readonly 'never-closed': "Nothing was written.";
63
+ readonly 'closed-before-records': "Nothing was written.";
64
+ };
65
+ /** The reason enum's options, read from the schema, for the key-set check. */
66
+ export declare const REFUSAL_REASONS: readonly PhaseRecordRefusalReason[];
67
+ /** The address of a phase's record: the phase is one path segment, encoded once. */
68
+ export declare function buildRecordUrl(credential: RepoCredential, phase: string): string;
69
+ /**
70
+ * Ask the service for `phase`'s frozen record and, when it serves one, write the
71
+ * pair under `repoRoot`. Never throws: every way this can fail — the network, the
72
+ * service, a write — is an outcome the caller prints, because by the time this
73
+ * runs after a close the phase has already closed.
74
+ */
75
+ export declare function obtainBuildRecord(credential: RepoCredential, repoRoot: string, phase: string, io?: RecordPairIo): Promise<ObtainOutcome>;
76
+ /**
77
+ * How to fetch a phase's record later, as a line a person can paste: the phase is
78
+ * quoted for the shell unless it is plain letters, digits and `._+-`.
79
+ */
80
+ export declare function refetchCommand(phase: string): string;
81
+ //# sourceMappingURL=obtain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"obtain.d.ts","sourceRoot":"","sources":["../../src/build-record/obtain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAwC,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAErF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,wBAAwB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB;;;;;;CAM+B,CAAC;AAE9D,8EAA8E;AAC9E,eAAO,MAAM,eAAe,EAAE,SAAS,wBAAwB,EAA2C,CAAC;AAE3G,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,UAAU,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKhF;AASD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,cAAc,EAC1B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,EAAE,GAAE,YAAkC,GACrC,OAAO,CAAC,aAAa,CAAC,CA0ExB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGpD"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The v2 Build Record markdown renderer: a pure function from a parsed
3
+ * `halfcycle-build-record/v2` record to the markdown half of the pair.
4
+ *
5
+ * It owns every word a reader of the `.md` sees, and the rule that keeps the
6
+ * client's words and the phase name from forging the observed register. The two
7
+ * registers are told apart by position: everything Halfcycle's own systems
8
+ * recorded prints as plain lines under an "Observed by Halfcycle" label, and
9
+ * everything the client stated prints as quoted lines, and only quoted lines,
10
+ * under a "Stated by the client" label. No other quoted line exists.
11
+ *
12
+ * PURE. It reads no clock, environment or locale. Numbers are formatted with
13
+ * `String()`, never a locale-aware formatter; `closedAt` is reworded by text
14
+ * substitution, never through `Date`. It iterates only keys it names, keeps array
15
+ * order, and sorts closes by `sequence` only. The same record gives the same
16
+ * bytes, whatever key order a database hands back.
17
+ *
18
+ * IT DOES NOT PARSE. The caller parses the record first; the type here is the
19
+ * contract's own inferred type, imported as a type so nothing of the contract
20
+ * module is pulled into this file at runtime. The one cross-half rule it relies
21
+ * on — both halves describe the same closes, paired by `sequence` — it checks
22
+ * itself and throws on, because printing a close without its other half would
23
+ * put a blank where a statement belongs.
24
+ *
25
+ * EVERY STRING HERE SHIPS. The fixed prose is printed into the client's own
26
+ * repository, so it is written in client words and carries no internal
27
+ * vocabulary; the tests scan the emitted markdown, not this source. No comment
28
+ * here sits beside an object member, because the bundler keeps those verbatim.
29
+ */
30
+ import type { BuildRecordV2 } from '@halfcycle/events';
31
+ /**
32
+ * Print one string from the record as inert, visible markdown text.
33
+ *
34
+ * Per code point: a hidden character becomes `U+` and its code in uppercase hex,
35
+ * at least four digits; a markdown special character is escaped with a
36
+ * backslash; everything else prints as itself. An empty string prints
37
+ * `(blank)`, so a slot is never silently empty.
38
+ */
39
+ export declare function printRecordText(text: string): string;
40
+ /**
41
+ * Render a parsed v2 Build Record as markdown.
42
+ *
43
+ * Throws, in client words, if the two halves describe different closes — which a
44
+ * parsed record cannot do. Output uses `\n` only and ends in exactly one `\n`.
45
+ */
46
+ export declare function renderBuildRecordV2Markdown(record: BuildRecordV2): string;
47
+ //# sourceMappingURL=render-v2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-v2.d.ts","sourceRoot":"","sources":["../../src/build-record/render-v2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAyBvD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAapD;AAoQD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAyDzE"}
@@ -1,70 +1,38 @@
1
1
  /**
2
- * Write-disciplined Build-Record writer.
2
+ * The Build Record pair writer — what a close and the re-fetch write.
3
3
  *
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), and now
6
- * enforces it rather than claiming it (INV-023).
4
+ * Blast radius: the writer touches ONLY `docs/build-records/` in the target repo,
5
+ * the one path `WRITE_ALLOWLIST` gives the Build Record (INV-019), and it enforces
6
+ * that rather than claiming it (INV-023).
7
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.
8
+ * THE PATH COMES FROM THE CALLER'S PHASE ARGUMENT, never from the record. The
9
+ * identity is decided by `isValidPhaseIdentity`, which has one home in
10
+ * `@halfcycle/events`; the segment is `renderPhaseSegment`'s, from the same home.
11
+ * This module restates neither rule.
13
12
  *
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.
13
+ * THE RESIDUAL CHECK, AND WHY IT IS EXPORTED. `assertInsideOutDir` runs on every
14
+ * path, temporary ones included, and compares two operands that move
15
+ * independently: the allowed directory (from `repoRoot` and a constant) and the
16
+ * candidate path (from the phase). A hostile phase never reaches it — the
17
+ * identity check refuses first and the renderer refuses again — so what it still
18
+ * guards is CROSS-PACKAGE: a rendered segment that grew a separator, which only an
19
+ * edit in `packages/events` could produce and which no behavioural test of this
20
+ * package can arrange. It is therefore exported and called directly by
21
+ * `build-record-write-discipline.test.ts`, the only control that can tell it
22
+ * from a deleted one.
49
23
  *
50
- * The JSON is serialised with a stable 2-space indent + trailing newline; the
51
- * markdown data slots are projected from the same record object, so the pair
52
- * cannot disagree on data.
24
+ * Refusal is ALL-OR-NOTHING: every path is asserted, and both texts are rendered,
25
+ * before the `mkdirSync` and the first create, so a refused record leaves no file
26
+ * and no empty directory behind.
53
27
  */
54
- import type { BuildRecordV1 } from './types.js';
28
+ import { type BuildRecordV2 } from '@halfcycle/events';
55
29
  /** The one and only output directory (relative to the target repo root). */
56
30
  export declare const BUILD_RECORD_DIR: string;
57
- export interface WriteResult {
58
- jsonPath: string;
59
- mdPath: string;
60
- }
61
- /** Serialise the record JSON deterministically (2-space indent + trailing NL). */
62
- export declare function serialiseRecordJson(record: BuildRecordV1): string;
63
31
  /**
64
32
  * Assert that a joined output path is still a plain file directly inside
65
33
  * `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.
34
+ * the allowed directory, and a candidate path built from the caller's phase — so
35
+ * this comparison can actually fail.
68
36
  *
69
37
  * EXPORTED SO IT CAN BE TESTED AT ALL. Its subject is a filename segment that
70
38
  * grew a separator, which only an edit in another package can produce, so no
@@ -75,9 +43,58 @@ export declare function serialiseRecordJson(record: BuildRecordV1): string;
75
43
  */
76
44
  export declare function assertInsideOutDir(outDir: string, candidate: string, identity: unknown): void;
77
45
  /**
78
- * Write the `.json` + `.md` pair for a phase into `docs/build-records/`.
79
- * Refuses — writing nothing at all — if the record's identity would put either
80
- * file anywhere else.
46
+ * The file operations `writeRecordPair` makes, behind one seam so a test can make
47
+ * the SECOND write fail and watch the first come back out. `createExclusive` opens
48
+ * with `wx` (never truncates an existing file) and removes what it created if the
49
+ * write itself fails; `rename` replaces in one step on the same filesystem.
50
+ */
51
+ export interface RecordPairIo {
52
+ createExclusive(path: string, text: string): void;
53
+ rename(from: string, to: string): void;
54
+ }
55
+ export declare const NODE_RECORD_PAIR_IO: RecordPairIo;
56
+ /**
57
+ * What `writeRecordPair` did. `created`: neither file existed. `up-to-date`: the
58
+ * `.json` already held these exact bytes, and the `.md` was rendered again.
59
+ * `replaced`: an earlier record of this same phase was replaced. `in-the-way`:
60
+ * a file this CLI did not write, or cannot recognise as this phase's earlier
61
+ * record, is at one of the two paths — nothing was written and `path` names it.
62
+ */
63
+ export type RecordPairResult = {
64
+ kind: 'created' | 'up-to-date' | 'replaced';
65
+ jsonPath: string;
66
+ mdPath: string;
67
+ } | {
68
+ kind: 'in-the-way';
69
+ path: string;
70
+ };
71
+ /** Serialise a served record for the `.json`: 2-space indent, trailing newline. */
72
+ export declare function serialiseServedRecord(rawRecord: unknown): string;
73
+ /**
74
+ * Write a phase's Build Record pair into `docs/build-records/`: the `.json` as the
75
+ * service sent it, and the `.md` rendered from the parsed record.
76
+ *
77
+ * THE PATH COMES FROM `phase`, THE CALLER'S VALIDATED ARGUMENT, and never from
78
+ * `record.phase` (INV-023). The caller compares the two before it gets here; this
79
+ * function does not rely on that, so a record naming `../../x` still lands at the
80
+ * argument's path.
81
+ *
82
+ * EXCLUSIVE CREATE IS THE EXISTENCE CHECK. There is no separate look first, which
83
+ * would leave a window between looking and writing. `.json` is opened `wx`, then
84
+ * `.md`; on `EEXIST` from either, any `.json` this call created is removed and the
85
+ * replace check decides: (a) the same `.json` bytes — up to date, and the `.md` is
86
+ * rendered again so a renderer change never refuses the CLI's own pair; (b) this
87
+ * phase's own earlier record — both files are replaced; (c) anything else —
88
+ * nothing is written and the file in the way is named.
89
+ *
90
+ * NO HALF PAIR. A replace writes `<final name>.tmp` beside each file and renames
91
+ * it over the old one; if the second rename fails, the first file gets its old
92
+ * bytes back. A fresh create whose second file fails removes the first. Every
93
+ * path, temporary ones included, is checked to be a plain file directly inside
94
+ * the directory before anything is created.
95
+ *
96
+ * @throws InvalidPhaseIdentityError when `phase` cannot name a file.
97
+ * @throws the file system's own error when a write fails; nothing is left behind.
81
98
  */
82
- export declare function writeBuildRecord(repoRoot: string, record: BuildRecordV1): WriteResult;
99
+ export declare function writeRecordPair(repoRoot: string, phase: string, rawRecord: unknown, record: BuildRecordV2, io?: RecordPairIo): RecordPairResult;
83
100
  //# sourceMappingURL=write.d.ts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../src/build-record/write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAcH,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,mBAAmB,CAAC;AAE3B,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,QAAgC,CAAC;AAE9D;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAM7F;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAClD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CACxC;AAsBD,eAAO,MAAM,mBAAmB,EAAE,YAGjC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,SAAS,GAAG,YAAY,GAAG,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjF;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,CAEhE;AA0CD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,OAAO,EAClB,MAAM,EAAE,aAAa,EACrB,EAAE,GAAE,YAAkC,GACrC,gBAAgB,CAyDlB"}
@@ -29,13 +29,40 @@
29
29
  *
30
30
  * INV-001: this file imports the global `fetch` (Node 20), Node builtins, and
31
31
  * `@halfcycle/events` — one of the exactly two workspace packages a shippable may
32
- * import. `acceptanceOutcomeSchema` is the ONE home for the outcome's shape
33
- * (INV-003), so the CLI validates what it builds against the same schema the route
34
- * parses with. It never imports the control service.
32
+ * import. `acceptPhaseRequestSchema` is the ONE home for the request's shape
33
+ * (INV-003), so the CLI validates the whole body it builds against the same schema
34
+ * the route parses with. It never imports the control service.
35
+ *
36
+ * SINCE CONNECT PHASE 4 (`close-produces-the-record`) the close carries the
37
+ * client's statements and the full commit it closed at, and writes no local close
38
+ * note. The service freezes both into the phase's Build Record; the caller fetches
39
+ * that record afterwards (`build-record/obtain.ts`).
35
40
  */
36
41
  import { type AcceptanceOutcome, type PhaseCloseDecision } from '@halfcycle/events';
37
42
  import { type PhaseStampFailure, type RepoCredential } from './open-phase.js';
38
- import { type CloseRecordOutcome } from './build-record/close-record.js';
43
+ /**
44
+ * Whether the commit this phase closed at could be read, and if so the full hash
45
+ * that travelled with the close. When it could not, `reason` says why in words a
46
+ * developer can act on; the record shows the commit as not stated.
47
+ */
48
+ export type CloseCommitOutcome = {
49
+ read: true;
50
+ commit: string;
51
+ } | {
52
+ read: false;
53
+ reason: string;
54
+ };
55
+ /**
56
+ * The client's own words on a close: what the phase set out to prove, what came of
57
+ * it, and how the work departed from the plan. `null` / empty when not stated.
58
+ */
59
+ export interface StatedStatements {
60
+ thesis: string | null;
61
+ outcome: string | null;
62
+ deviations: string[];
63
+ }
64
+ /** Nothing stated — what a close sends when no statement flag was given. */
65
+ export declare const NO_STATEMENTS: StatedStatements;
39
66
  /** What `POST /engagements/:id/accept` returns on 200 (the fields the CLI reports). */
40
67
  export interface ClosedPhase {
41
68
  engagementId: string;
@@ -47,13 +74,20 @@ export interface ClosedPhase {
47
74
  * machine is still filed under a phase that is no longer open.
48
75
  */
49
76
  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;
77
+ /** The commit sent with the close, or why none could be read. */
78
+ closeCommit: CloseCommitOutcome;
56
79
  }
80
+ /**
81
+ * Read the full HEAD commit of the NAMED repository, before the close is sent.
82
+ *
83
+ * `gitEnvForNamedRepo` strips the variables that tell git which repository to use,
84
+ * so an absolute `GIT_DIR` inherited from a hook cannot substitute another
85
+ * repository's HEAD for this one's. git's own stderr is discarded: a failure here
86
+ * is expected, the close still goes ahead, and the caller says why in plain words.
87
+ * The hash is checked by the contract's own `closeCommitSchema`, so a short or
88
+ * malformed answer is never sent.
89
+ */
90
+ export declare function readCloseCommit(repoRoot: string | undefined): CloseCommitOutcome;
57
91
  /**
58
92
  * A refusal from the control plane, carried as an error the CLI can print whole.
59
93
  *
@@ -108,6 +142,13 @@ export declare class ClosePhaseRefused extends Error {
108
142
  */
109
143
  code?: string | undefined);
110
144
  }
145
+ /**
146
+ * A close refused on this machine, before anything was sent: the request failed the
147
+ * shared request schema. The CLI prints the message and exits as a usage error.
148
+ */
149
+ export declare class CloseRequestInvalid extends Error {
150
+ constructor(message: string);
151
+ }
111
152
  /**
112
153
  * Record a phase's acceptance and close it.
113
154
  *
@@ -118,16 +159,18 @@ export declare class ClosePhaseRefused extends Error {
118
159
  * for the same reason `openPhase` requires its entry decision: there
119
160
  * is no argument-less way to move the phase axis, so an unattributed
120
161
  * close is unreachable from any caller that compiles.
121
- * @param repoRoot the repository whose commit this close is being recorded against.
162
+ * @param repoRoot the repository whose HEAD this close is sent with.
122
163
  * Optional so that a caller with no repository in hand — a test
123
164
  * 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.
165
+ * omitting it sends no commit rather than silently resolving the
166
+ * process's working directory, which would send a stranger's HEAD.
167
+ * @param stated the client's own statements; each absent one is sent as not
168
+ * stated (`null`, or no deviations).
127
169
  *
128
170
  * @throws ClosePhaseRefused on a 4xx the plane explained, so the CLI prints the
129
171
  * plane's own sentence and exits non-zero.
130
- * @throws Error on transport failure or a body that is not the expected shape.
172
+ * @throws Error before anything is sent when the request fails the shared schema;
173
+ * on transport failure; or on a body that is not the expected shape.
131
174
  */
132
- export declare function closePhase(credential: RepoCredential, phase: string, outcome: AcceptanceOutcome, close: PhaseCloseDecision, home?: string, repoRoot?: string): Promise<ClosedPhase>;
175
+ export declare function closePhase(credential: RepoCredential, phase: string, outcome: AcceptanceOutcome, close: PhaseCloseDecision, home?: string, repoRoot?: string, stated?: StatedStatements): Promise<ClosedPhase>;
133
176
  //# 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,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"}
1
+ {"version":3,"file":"close-phase.d.ts","sourceRoot":"","sources":["../src/close-phase.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAGH,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAmB,KAAK,iBAAiB,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAG/F;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAElG;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,4EAA4E;AAC5E,eAAO,MAAM,aAAa,EAAE,gBAAkE,CAAC;AAE/F,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,iEAAiE;IACjE,WAAW,EAAE,kBAAkB,CAAC;CACjC;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,kBAAkB,CA4BhF;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;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;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,EACjB,MAAM,GAAE,gBAAgC,GACvC,OAAO,CAAC,WAAW,CAAC,CAyEtB"}