halfcycle 0.3.31 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/bin.bundle.mjs +355 -65
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +1336 -873
- package/dist/bin.js.map +3 -3
- package/dist/build-record/index.d.ts +14 -36
- package/dist/build-record/index.d.ts.map +1 -1
- package/dist/build-record/obtain.d.ts +81 -0
- package/dist/build-record/obtain.d.ts.map +1 -0
- package/dist/build-record/render-v2.d.ts +47 -0
- package/dist/build-record/render-v2.d.ts.map +1 -0
- package/dist/build-record/write.d.ts +77 -60
- package/dist/build-record/write.d.ts.map +1 -1
- package/dist/close-phase.d.ts +59 -16
- package/dist/close-phase.d.ts.map +1 -1
- package/dist/identity.d.ts +39 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/index.js +993 -671
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts +60 -40
- package/dist/install.d.ts.map +1 -1
- package/package.json +3 -1
- package/dist/build-record/assemble.d.ts +0 -37
- package/dist/build-record/assemble.d.ts.map +0 -1
- package/dist/build-record/close-record.d.ts +0 -124
- package/dist/build-record/close-record.d.ts.map +0 -1
- package/dist/build-record/sources.d.ts +0 -111
- package/dist/build-record/sources.d.ts.map +0 -1
- package/dist/build-record/template.d.ts +0 -29
- package/dist/build-record/template.d.ts.map +0 -1
- package/dist/build-record/types.d.ts +0 -200
- package/dist/build-record/types.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
// dist/install.js
|
|
2
2
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3
3
|
import { createHash as createHash2 } from "node:crypto";
|
|
4
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4
|
+
import { existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5
5
|
import { dirname as dirname2, join as join5, posix, relative } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
|
-
// ../events/dist/result.js
|
|
9
|
-
import { z as z2 } from "zod";
|
|
10
|
-
|
|
11
8
|
// ../core/dist/guard-record.js
|
|
12
9
|
import { z } from "zod";
|
|
13
10
|
var severitySchema = z.enum(["info", "warn", "block"]);
|
|
@@ -113,7 +110,101 @@ ${block}`);
|
|
|
113
110
|
return result;
|
|
114
111
|
}
|
|
115
112
|
|
|
113
|
+
// ../core/dist/hook-wiring.js
|
|
114
|
+
var SETTINGS_REL = ".claude/settings.json";
|
|
115
|
+
var VENDORED_BIN_REL = ".halfcycle/bin/bin.bundle.mjs";
|
|
116
|
+
var SESSION_START_MARKER_REL = ".claude/hooks/session-start-marker.sh";
|
|
117
|
+
var GUARD_RUNNER_REL = ".claude/hooks/guard-runner.sh";
|
|
118
|
+
var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
|
|
119
|
+
var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
|
|
120
|
+
var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
|
|
121
|
+
var HALFCYCLE_HOOK_WIRING = [
|
|
122
|
+
{
|
|
123
|
+
event: "SessionStart",
|
|
124
|
+
matcher: null,
|
|
125
|
+
timeout: 5,
|
|
126
|
+
command: `bash ./${SESSION_START_MARKER_REL}`,
|
|
127
|
+
script: SESSION_START_MARKER_REL
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
event: "SessionStart",
|
|
131
|
+
matcher: null,
|
|
132
|
+
timeout: 10,
|
|
133
|
+
command: `bash ./${GUARD_RUNNER_REL} refresh-credential`,
|
|
134
|
+
script: GUARD_RUNNER_REL
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
event: "UserPromptSubmit",
|
|
138
|
+
matcher: null,
|
|
139
|
+
timeout: 5,
|
|
140
|
+
command: `bash ./${USER_PROMPT_REMINDER_REL}`,
|
|
141
|
+
script: USER_PROMPT_REMINDER_REL
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
event: "PostToolUse",
|
|
145
|
+
matcher: WRITE_CAPABLE_TOOLS.join("|"),
|
|
146
|
+
timeout: 60,
|
|
147
|
+
command: `bash ./${GUARD_RUNNER_REL} hook post-tool-use`,
|
|
148
|
+
script: GUARD_RUNNER_REL
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
event: "Stop",
|
|
152
|
+
matcher: null,
|
|
153
|
+
timeout: 60,
|
|
154
|
+
command: `bash ./${GUARD_RUNNER_REL} hook stop`,
|
|
155
|
+
script: GUARD_RUNNER_REL
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
event: "SubagentStop",
|
|
159
|
+
matcher: null,
|
|
160
|
+
timeout: 60,
|
|
161
|
+
command: `bash ./${GUARD_RUNNER_REL} hook subagent-stop`,
|
|
162
|
+
script: GUARD_RUNNER_REL
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
event: "SessionEnd",
|
|
166
|
+
matcher: null,
|
|
167
|
+
timeout: 5,
|
|
168
|
+
command: `bash ./${SESSION_END_MARKER_REL}`,
|
|
169
|
+
script: SESSION_END_MARKER_REL
|
|
170
|
+
}
|
|
171
|
+
];
|
|
172
|
+
var RETIRED_HOOK_COMMANDS = [];
|
|
173
|
+
var HALFCYCLE_HOOK_COMMANDS = /* @__PURE__ */ new Set([
|
|
174
|
+
...HALFCYCLE_HOOK_WIRING.map((entry) => entry.command),
|
|
175
|
+
...RETIRED_HOOK_COMMANDS
|
|
176
|
+
]);
|
|
177
|
+
var HALFCYCLE_HOOK_WIRING_PATHS = [
|
|
178
|
+
.../* @__PURE__ */ new Set([SETTINGS_REL, ...HALFCYCLE_HOOK_WIRING.map((entry) => entry.script), VENDORED_BIN_REL])
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
// ../core/dist/secret-shape.js
|
|
182
|
+
var SECRET_SHAPE_PATTERNS = [
|
|
183
|
+
// Assignment to a credential-named identifier: `apiKey = "..."`, `secret: '...'`,
|
|
184
|
+
// `password=...`, `token = ...`, `access_key: ...` — the value need not be quoted.
|
|
185
|
+
/\b(?:api[_-]?key|secret|password|passwd|token|access[_-]?key|private[_-]?key|client[_-]?secret|auth[_-]?token|bearer)\b\s*[:=]\s*\S+/i,
|
|
186
|
+
// Connection strings that embed credentials: `scheme://user:pass@host`.
|
|
187
|
+
/\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:[^\s:/@]+@/i,
|
|
188
|
+
// Bearer / authorization header values.
|
|
189
|
+
/\bauthorization\b\s*[:=]\s*['"]?\s*bearer\s+\S+/i,
|
|
190
|
+
// PEM / private-key block headers.
|
|
191
|
+
/-----BEGIN\s+(?:RSA\s+|EC\s+|OPENSSH\s+|DSA\s+)?PRIVATE\s+KEY-----/,
|
|
192
|
+
// JWT shape: three base64url segments separated by dots.
|
|
193
|
+
/\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/,
|
|
194
|
+
// A long high-entropy hex/base64 run adjacent to a credential-ish word on the
|
|
195
|
+
// same line — a raw key literal. Kept last (broadest, most likely false-drop).
|
|
196
|
+
/\b(?:key|token|secret|credential)\b[\s\S]{0,40}[A-Za-z0-9+/_-]{32,}/i
|
|
197
|
+
];
|
|
198
|
+
function looksLikeSecret(text) {
|
|
199
|
+
for (const pattern of SECRET_SHAPE_PATTERNS) {
|
|
200
|
+
if (pattern.test(text))
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
116
206
|
// ../events/dist/result.js
|
|
207
|
+
import { z as z2 } from "zod";
|
|
117
208
|
var firedGuardSchema = z2.object({
|
|
118
209
|
guardId: z2.string(),
|
|
119
210
|
patternRef: z2.string(),
|
|
@@ -171,74 +262,28 @@ var wireErrorSchema = z2.object({
|
|
|
171
262
|
message: z2.string()
|
|
172
263
|
}).strict();
|
|
173
264
|
|
|
174
|
-
// ../events/dist/
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
"contract-error",
|
|
181
|
-
"unconfigured"
|
|
182
|
-
]);
|
|
183
|
-
var guardEvalRunSchema = z3.object({
|
|
184
|
-
// `runId`/`outcome` are the E2 run-observability additions, declared
|
|
185
|
-
// field-level-OPTIONAL as the FIRST (expand) step of an expand-contract
|
|
186
|
-
// migration. Every new record populated by the migrated writer (T-07)
|
|
187
|
-
// carries both; a legacy line predating the amendment carries neither and
|
|
188
|
-
// must still parse (the mixed-era guard-eval log the Build-Record reader
|
|
189
|
-
// assembles from, and the runner emit not yet migrated). So the presence
|
|
190
|
-
// refinement below is skipped for a legacy line (no `outcome`), and the
|
|
191
|
-
// per-writer guarantee lives with the writer + reader-normalisation (T-07),
|
|
192
|
-
// where the schema may then be tightened to required. runId is uuid-checked
|
|
193
|
-
// WHEN present; outcome is enum-checked when present.
|
|
194
|
-
runId: z3.string().uuid().optional(),
|
|
195
|
-
engagementId: z3.string().optional(),
|
|
196
|
-
runType: z3.enum(["hook", "ci"]),
|
|
197
|
-
occurredAt: utcIso8601,
|
|
198
|
-
outcome: guardEvalOutcomeSchema.optional(),
|
|
199
|
-
failureReason: z3.string().optional(),
|
|
200
|
-
phase: z3.string().optional(),
|
|
201
|
-
scope: z3.record(z3.string(), z3.unknown()),
|
|
202
|
-
cost: z3.record(z3.string(), z3.unknown()).optional(),
|
|
203
|
-
result: resultEnvelopeSchema.optional()
|
|
204
|
-
}).strict().superRefine((run, ctx) => {
|
|
205
|
-
if (run.outcome === void 0)
|
|
206
|
-
return;
|
|
207
|
-
if (run.outcome === "evaluated" && run.result === void 0) {
|
|
208
|
-
ctx.addIssue({
|
|
209
|
-
code: z3.ZodIssueCode.custom,
|
|
210
|
-
path: ["result"],
|
|
211
|
-
message: "result is required when outcome is 'evaluated'"
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
if (run.outcome !== "evaluated" && run.result !== void 0) {
|
|
215
|
-
ctx.addIssue({
|
|
216
|
-
code: z3.ZodIssueCode.custom,
|
|
217
|
-
path: ["result"],
|
|
218
|
-
message: "result is only present when outcome is 'evaluated'"
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
if (run.outcome !== "unconfigured" && run.engagementId === void 0) {
|
|
222
|
-
ctx.addIssue({
|
|
223
|
-
code: z3.ZodIssueCode.custom,
|
|
224
|
-
path: ["engagementId"],
|
|
225
|
-
message: "engagementId is required unless outcome is 'unconfigured'"
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
});
|
|
265
|
+
// ../events/dist/lone-surrogates.js
|
|
266
|
+
var LONE_SURROGATE = new RegExp("\\p{Cs}", "u");
|
|
267
|
+
function hasLoneSurrogate(text) {
|
|
268
|
+
return LONE_SURROGATE.test(text);
|
|
269
|
+
}
|
|
270
|
+
var LONE_SURROGATE_MESSAGE = "must be valid text: it holds half of a character pair with no other half";
|
|
229
271
|
|
|
230
|
-
// ../events/dist/
|
|
272
|
+
// ../events/dist/build-record.js
|
|
231
273
|
import { z as z4 } from "zod";
|
|
232
|
-
|
|
274
|
+
|
|
275
|
+
// ../events/dist/state.js
|
|
276
|
+
import { z as z3 } from "zod";
|
|
277
|
+
var utcIso8601 = z3.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z");
|
|
233
278
|
var STATE_RECORD_FORMAT = "halfcycle-state-record/v1";
|
|
234
|
-
var stepIdSchema =
|
|
235
|
-
var methodVersionSchema =
|
|
279
|
+
var stepIdSchema = z3.string().regex(/^(l[0-5](-l[0-5])?|xl)\.[a-z0-9-]+$/);
|
|
280
|
+
var methodVersionSchema = z3.string().regex(/^\d+\.\d+\.\d+$/);
|
|
236
281
|
var anchorPathRegex = /^(?!\/)(?!.*#)(?!method\/)(?!(.*\/)?docs\/method\/).+$/;
|
|
237
|
-
var recordKindSchema =
|
|
238
|
-
var methodLayerSchema =
|
|
239
|
-
var evidenceKindSchema =
|
|
240
|
-
var declarableEvidenceKindSchema =
|
|
241
|
-
var dispositionSchema =
|
|
282
|
+
var recordKindSchema = z3.enum(["artefact", "step-run", "gate", "intervention"]);
|
|
283
|
+
var methodLayerSchema = z3.enum(["l0", "l1", "l2", "l3", "l3-l4", "l4", "l5", "xl"]);
|
|
284
|
+
var evidenceKindSchema = z3.enum(["anchor", "diff", "repro", "question-answer", "no-op"]);
|
|
285
|
+
var declarableEvidenceKindSchema = z3.enum(["anchor", "diff", "repro", "question-answer"]);
|
|
286
|
+
var dispositionSchema = z3.enum([
|
|
242
287
|
"prevented",
|
|
243
288
|
"corrected-in-spec",
|
|
244
289
|
"corrected-in-scope",
|
|
@@ -248,20 +293,20 @@ var dispositionSchema = z4.enum([
|
|
|
248
293
|
"surfaced",
|
|
249
294
|
"filed-forward"
|
|
250
295
|
]);
|
|
251
|
-
var mechanismSchema =
|
|
252
|
-
var artefactStateSchema =
|
|
296
|
+
var mechanismSchema = z3.enum(["guard", "verdict", "served-step", "method-step"]);
|
|
297
|
+
var artefactStateSchema = z3.enum(["created", "updated"]);
|
|
253
298
|
var QUALIFIED_OUTCOMES = ["declined", "parked"];
|
|
254
|
-
var qualifiedOutcomeSchema =
|
|
299
|
+
var qualifiedOutcomeSchema = z3.enum(QUALIFIED_OUTCOMES);
|
|
255
300
|
function isQualifiedOutcome(outcome) {
|
|
256
301
|
return QUALIFIED_OUTCOMES.includes(outcome);
|
|
257
302
|
}
|
|
258
|
-
var runOutcomeSchema =
|
|
303
|
+
var runOutcomeSchema = z3.enum([
|
|
259
304
|
"completed",
|
|
260
305
|
"attempted-failed",
|
|
261
306
|
...QUALIFIED_OUTCOMES
|
|
262
307
|
]);
|
|
263
|
-
var gateKindSchema =
|
|
264
|
-
var gateVerdictSchema =
|
|
308
|
+
var gateKindSchema = z3.enum(["approval", "mechanical"]);
|
|
309
|
+
var gateVerdictSchema = z3.enum(["pass", "fail", ...QUALIFIED_OUTCOMES]);
|
|
265
310
|
function outcomeReasonIssue(outcome, reason) {
|
|
266
311
|
if (isQualifiedOutcome(outcome) && reason === void 0) {
|
|
267
312
|
return { message: `outcomeReason is required when the outcome is '${outcome}'` };
|
|
@@ -273,142 +318,383 @@ function outcomeReasonIssue(outcome, reason) {
|
|
|
273
318
|
}
|
|
274
319
|
return null;
|
|
275
320
|
}
|
|
276
|
-
var anchorEvidenceSchema =
|
|
277
|
-
kind:
|
|
278
|
-
path:
|
|
321
|
+
var anchorEvidenceSchema = z3.object({
|
|
322
|
+
kind: z3.literal("anchor"),
|
|
323
|
+
path: z3.string().regex(anchorPathRegex),
|
|
279
324
|
// Optional 1-based line number within the file at `path`. Present when the
|
|
280
325
|
// writer knows the exact line; omitted — never `0`, never `null` — when it
|
|
281
326
|
// does not, because a line number is not always obtainable. Absence is a
|
|
282
327
|
// valid, permanent state here, not a gap meant to be filled in later.
|
|
283
|
-
line:
|
|
328
|
+
line: z3.number().optional()
|
|
284
329
|
}).strict();
|
|
285
|
-
var diffEvidenceSchema =
|
|
286
|
-
kind:
|
|
287
|
-
ref:
|
|
330
|
+
var diffEvidenceSchema = z3.object({
|
|
331
|
+
kind: z3.literal("diff"),
|
|
332
|
+
ref: z3.string()
|
|
288
333
|
}).strict();
|
|
289
|
-
var reproEvidenceSchema =
|
|
290
|
-
kind:
|
|
291
|
-
steps:
|
|
334
|
+
var reproEvidenceSchema = z3.object({
|
|
335
|
+
kind: z3.literal("repro"),
|
|
336
|
+
steps: z3.string().max(2e3)
|
|
292
337
|
}).strict();
|
|
293
|
-
var questionAnswerEvidenceSchema =
|
|
294
|
-
kind:
|
|
295
|
-
question:
|
|
296
|
-
before:
|
|
297
|
-
after:
|
|
338
|
+
var questionAnswerEvidenceSchema = z3.object({
|
|
339
|
+
kind: z3.literal("question-answer"),
|
|
340
|
+
question: z3.string().max(2e3),
|
|
341
|
+
before: z3.string().max(2e3),
|
|
342
|
+
after: z3.string().max(2e3)
|
|
298
343
|
}).strict();
|
|
299
|
-
var noOpEvidenceSchema =
|
|
300
|
-
kind:
|
|
301
|
-
inspected:
|
|
302
|
-
unchangedBecause:
|
|
344
|
+
var noOpEvidenceSchema = z3.object({
|
|
345
|
+
kind: z3.literal("no-op"),
|
|
346
|
+
inspected: z3.string().max(2e3),
|
|
347
|
+
unchangedBecause: z3.string().max(2e3)
|
|
303
348
|
}).strict();
|
|
304
|
-
var evidenceSchema =
|
|
349
|
+
var evidenceSchema = z3.discriminatedUnion("kind", [
|
|
305
350
|
anchorEvidenceSchema,
|
|
306
351
|
diffEvidenceSchema,
|
|
307
352
|
reproEvidenceSchema,
|
|
308
353
|
questionAnswerEvidenceSchema,
|
|
309
354
|
noOpEvidenceSchema
|
|
310
355
|
]);
|
|
311
|
-
var stateRecordEnvelopeSchema =
|
|
312
|
-
format:
|
|
356
|
+
var stateRecordEnvelopeSchema = z3.object({
|
|
357
|
+
format: z3.literal(STATE_RECORD_FORMAT),
|
|
313
358
|
recordKind: recordKindSchema,
|
|
314
|
-
recordId:
|
|
359
|
+
recordId: z3.string().uuid(),
|
|
315
360
|
methodVersion: methodVersionSchema,
|
|
316
|
-
engagementId:
|
|
361
|
+
engagementId: z3.string(),
|
|
317
362
|
stepId: stepIdSchema,
|
|
318
363
|
// Immutable: names when the thing happened, never when a later disposition
|
|
319
364
|
// moved.
|
|
320
|
-
occurredAt:
|
|
365
|
+
occurredAt: utcIso8601,
|
|
321
366
|
// Equal to `occurredAt` on first write; advances on each re-emit OF A FINDING.
|
|
322
367
|
// Carried on the shared envelope so a "when did this last change" query is
|
|
323
368
|
// kind-agnostic. Kept rather than generalised to `updatedAt` because
|
|
324
369
|
// `disposition` is the only field any re-emit may change (§5.3).
|
|
325
|
-
dispositionAt:
|
|
370
|
+
dispositionAt: utcIso8601,
|
|
326
371
|
evidence: evidenceSchema,
|
|
327
372
|
// Which phase of the project this record belongs to, if any. Present on
|
|
328
373
|
// records written during phase-scoped work; absent on records that run once
|
|
329
374
|
// for the whole project rather than per phase. Absence is meaningful on its
|
|
330
375
|
// own — it is never replaced with a placeholder value, and it is read as
|
|
331
376
|
// "satisfies any phase", not as "unknown".
|
|
332
|
-
phase:
|
|
377
|
+
phase: z3.string().optional()
|
|
333
378
|
});
|
|
334
|
-
var artefactRefSchema =
|
|
335
|
-
name:
|
|
336
|
-
parent:
|
|
379
|
+
var artefactRefSchema = z3.object({
|
|
380
|
+
name: z3.string(),
|
|
381
|
+
parent: z3.string()
|
|
337
382
|
}).strict();
|
|
338
|
-
var coverageSchema =
|
|
339
|
-
asked:
|
|
340
|
-
answered:
|
|
383
|
+
var coverageSchema = z3.object({
|
|
384
|
+
asked: z3.number().int().nonnegative(),
|
|
385
|
+
answered: z3.number().int().nonnegative()
|
|
341
386
|
}).strict().superRefine((coverage, ctx) => {
|
|
342
387
|
if (coverage.answered > coverage.asked) {
|
|
343
388
|
ctx.addIssue({
|
|
344
|
-
code:
|
|
389
|
+
code: z3.ZodIssueCode.custom,
|
|
345
390
|
path: ["answered"],
|
|
346
391
|
message: "answered cannot be more than asked"
|
|
347
392
|
});
|
|
348
393
|
}
|
|
349
394
|
});
|
|
350
395
|
var artefactRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
351
|
-
recordKind:
|
|
396
|
+
recordKind: z3.literal("artefact"),
|
|
352
397
|
artefactRef: artefactRefSchema,
|
|
353
398
|
// Some layer wrote it, by construction — an empty array is not a legitimate
|
|
354
399
|
// state, so the schema says so.
|
|
355
|
-
writtenByLayers:
|
|
400
|
+
writtenByLayers: z3.array(methodLayerSchema).min(1),
|
|
356
401
|
state: artefactStateSchema
|
|
357
402
|
}).strict();
|
|
358
403
|
var stepRunRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
359
|
-
recordKind:
|
|
404
|
+
recordKind: z3.literal("step-run"),
|
|
360
405
|
runOutcome: runOutcomeSchema,
|
|
361
|
-
failureReason:
|
|
362
|
-
outcomeReason:
|
|
406
|
+
failureReason: z3.string().optional(),
|
|
407
|
+
outcomeReason: z3.string().max(2e3).optional()
|
|
363
408
|
}).strict().superRefine((rec, ctx) => {
|
|
364
409
|
if (rec.runOutcome === "attempted-failed" && rec.failureReason === void 0) {
|
|
365
410
|
ctx.addIssue({
|
|
366
|
-
code:
|
|
411
|
+
code: z3.ZodIssueCode.custom,
|
|
367
412
|
path: ["failureReason"],
|
|
368
413
|
message: "failureReason is required when runOutcome is 'attempted-failed'"
|
|
369
414
|
});
|
|
370
415
|
}
|
|
371
416
|
if (rec.runOutcome !== "attempted-failed" && rec.failureReason !== void 0) {
|
|
372
417
|
ctx.addIssue({
|
|
373
|
-
code:
|
|
418
|
+
code: z3.ZodIssueCode.custom,
|
|
374
419
|
path: ["failureReason"],
|
|
375
420
|
message: "failureReason is only present when runOutcome is 'attempted-failed'"
|
|
376
421
|
});
|
|
377
422
|
}
|
|
378
423
|
const issue = outcomeReasonIssue(rec.runOutcome, rec.outcomeReason);
|
|
379
424
|
if (issue) {
|
|
380
|
-
ctx.addIssue({ code:
|
|
425
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["outcomeReason"], ...issue });
|
|
381
426
|
}
|
|
382
427
|
});
|
|
383
428
|
var gateRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
384
|
-
recordKind:
|
|
429
|
+
recordKind: z3.literal("gate"),
|
|
385
430
|
gateKind: gateKindSchema,
|
|
386
431
|
verdict: gateVerdictSchema,
|
|
387
|
-
actor:
|
|
388
|
-
outcomeReason:
|
|
432
|
+
actor: z3.string(),
|
|
433
|
+
outcomeReason: z3.string().max(2e3).optional(),
|
|
389
434
|
coverage: coverageSchema.optional()
|
|
390
435
|
}).strict().superRefine((rec, ctx) => {
|
|
391
436
|
const issue = outcomeReasonIssue(rec.verdict, rec.outcomeReason);
|
|
392
437
|
if (issue) {
|
|
393
|
-
ctx.addIssue({ code:
|
|
438
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["outcomeReason"], ...issue });
|
|
394
439
|
}
|
|
395
440
|
});
|
|
396
441
|
var interventionRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
397
|
-
recordKind:
|
|
442
|
+
recordKind: z3.literal("intervention"),
|
|
398
443
|
layer: methodLayerSchema,
|
|
399
444
|
mechanism: mechanismSchema,
|
|
400
445
|
severity: severitySchema,
|
|
401
446
|
disposition: dispositionSchema,
|
|
402
|
-
summary:
|
|
447
|
+
summary: z3.string().max(2e3),
|
|
403
448
|
coverage: coverageSchema.optional()
|
|
404
449
|
}).strict();
|
|
405
|
-
var stateRecordSchema =
|
|
450
|
+
var stateRecordSchema = z3.discriminatedUnion("recordKind", [
|
|
406
451
|
artefactRecordSchema,
|
|
407
452
|
stepRunRecordSchema,
|
|
408
453
|
gateRecordSchema,
|
|
409
454
|
interventionRecordSchema
|
|
410
455
|
]);
|
|
411
456
|
|
|
457
|
+
// ../events/dist/control-characters.js
|
|
458
|
+
var CONTROL_CHARACTER_CLASS = "\\p{Cc}";
|
|
459
|
+
var CONTROL_CHARACTER = new RegExp(CONTROL_CHARACTER_CLASS, "u");
|
|
460
|
+
function hasControlCharacter(text) {
|
|
461
|
+
return CONTROL_CHARACTER.test(text);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ../events/dist/phase-identity.js
|
|
465
|
+
var PHASE_SEGMENT_PREFIX = "phase-";
|
|
466
|
+
var PHASE_NAME_MAX_BYTES = 200;
|
|
467
|
+
function isValidPhaseIdentity(identity) {
|
|
468
|
+
if (typeof identity === "number")
|
|
469
|
+
return Number.isInteger(identity) && identity >= 0;
|
|
470
|
+
if (typeof identity !== "string")
|
|
471
|
+
return false;
|
|
472
|
+
if (identity === "")
|
|
473
|
+
return false;
|
|
474
|
+
if (identity.includes("/"))
|
|
475
|
+
return false;
|
|
476
|
+
if (identity.includes("\\"))
|
|
477
|
+
return false;
|
|
478
|
+
if (identity.startsWith("."))
|
|
479
|
+
return false;
|
|
480
|
+
if (hasControlCharacter(identity))
|
|
481
|
+
return false;
|
|
482
|
+
if (utf8ByteLength(identity) > PHASE_NAME_MAX_BYTES)
|
|
483
|
+
return false;
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
function utf8ByteLength(text) {
|
|
487
|
+
let bytes = 0;
|
|
488
|
+
for (const ch of text) {
|
|
489
|
+
const cp = ch.codePointAt(0);
|
|
490
|
+
bytes += cp < 128 ? 1 : cp < 2048 ? 2 : cp < 65536 ? 3 : 4;
|
|
491
|
+
}
|
|
492
|
+
return bytes;
|
|
493
|
+
}
|
|
494
|
+
var InvalidPhaseIdentityError = class extends Error {
|
|
495
|
+
/** The value that was refused, exactly as supplied. */
|
|
496
|
+
identity;
|
|
497
|
+
constructor(identity, allowedLocation) {
|
|
498
|
+
const subject = allowedLocation === void 0 ? "a file" : "a Build Record";
|
|
499
|
+
const where = allowedLocation === void 0 ? "" : `
|
|
500
|
+
Records are written only into ${allowedLocation.replace(/\\/g, "/")}/. Nothing was written.`;
|
|
501
|
+
super(`that phase identity cannot name ${subject}.
|
|
502
|
+
identity: ${describeIdentity(identity)}` + where);
|
|
503
|
+
this.name = "InvalidPhaseIdentityError";
|
|
504
|
+
this.identity = identity;
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
function assertValidPhaseIdentity(identity, allowedLocation) {
|
|
508
|
+
if (!isValidPhaseIdentity(identity)) {
|
|
509
|
+
throw new InvalidPhaseIdentityError(identity, allowedLocation);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function renderPhaseSegment(identity) {
|
|
513
|
+
if (!isValidPhaseIdentity(identity)) {
|
|
514
|
+
throw new InvalidPhaseIdentityError(identity);
|
|
515
|
+
}
|
|
516
|
+
return `${PHASE_SEGMENT_PREFIX}${identity}`;
|
|
517
|
+
}
|
|
518
|
+
function describeIdentity(identity) {
|
|
519
|
+
if (identity === "")
|
|
520
|
+
return "(empty)";
|
|
521
|
+
if (typeof identity === "string")
|
|
522
|
+
return identity;
|
|
523
|
+
if (identity === null)
|
|
524
|
+
return "null";
|
|
525
|
+
if (identity === void 0)
|
|
526
|
+
return "none supplied";
|
|
527
|
+
if (typeof identity === "number" || typeof identity === "boolean")
|
|
528
|
+
return String(identity);
|
|
529
|
+
if (Array.isArray(identity))
|
|
530
|
+
return "(a list)";
|
|
531
|
+
if (typeof identity === "object")
|
|
532
|
+
return "(an object)";
|
|
533
|
+
return `(a ${typeof identity})`;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ../events/dist/build-record.js
|
|
537
|
+
var BUILD_RECORD_V2_FORMAT = "halfcycle-build-record/v2";
|
|
538
|
+
var STATEMENT_MAX = 2e3;
|
|
539
|
+
var DEVIATION_MAX = 500;
|
|
540
|
+
var DEVIATIONS_MAX = 20;
|
|
541
|
+
var ACTOR_MAX = 200;
|
|
542
|
+
var FINDING_MAX = 500;
|
|
543
|
+
var FINDINGS_SHOWN_MAX = 50;
|
|
544
|
+
var PHASE_CLOSE_REASON_MAX = 1900;
|
|
545
|
+
var closeCommitSchema = z4.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/, "must be a full commit hash: 40 or 64 lowercase hex characters");
|
|
546
|
+
var walkVerdictSchema = z4.enum(["clean", "defects"]);
|
|
547
|
+
var closeDecisionSchema = z4.enum(["done-when-met", "override"]);
|
|
548
|
+
function plainText(max) {
|
|
549
|
+
const base = max === void 0 ? z4.string() : z4.string().max(max);
|
|
550
|
+
return base.refine((v) => !hasControlCharacter(v), {
|
|
551
|
+
message: "must be one line with no control characters (no line breaks, tabs or NUL)"
|
|
552
|
+
}).refine((v) => !hasLoneSurrogate(v), { message: LONE_SURROGATE_MESSAGE });
|
|
553
|
+
}
|
|
554
|
+
function statementText(max) {
|
|
555
|
+
return plainText(max).refine((v) => !looksLikeSecret(v), {
|
|
556
|
+
message: "looks like a password or key \u2014 rephrase it"
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
var closeActorSchema = plainText(ACTOR_MAX);
|
|
560
|
+
var phaseNameSchema = z4.string().refine((v) => isValidPhaseIdentity(v), {
|
|
561
|
+
message: `must be a phase name: not empty, no slash or backslash, not starting with a dot, no control characters, at most ${PHASE_NAME_MAX_BYTES} bytes`
|
|
562
|
+
});
|
|
563
|
+
var closeStatementsSchema = z4.object({
|
|
564
|
+
thesis: statementText(STATEMENT_MAX).nullable(),
|
|
565
|
+
outcome: statementText(STATEMENT_MAX).nullable(),
|
|
566
|
+
deviations: z4.array(statementText(DEVIATION_MAX)).max(DEVIATIONS_MAX),
|
|
567
|
+
closeCommit: closeCommitSchema.nullable()
|
|
568
|
+
}).strict();
|
|
569
|
+
var closedBySchema = z4.discriminatedUnion("kind", [
|
|
570
|
+
z4.object({ kind: z4.literal("account"), email: plainText().nullable() }).strict(),
|
|
571
|
+
z4.object({ kind: z4.literal("no-account") }).strict(),
|
|
572
|
+
z4.object({ kind: z4.literal("account-removed") }).strict(),
|
|
573
|
+
z4.object({ kind: z4.literal("not-recorded") }).strict()
|
|
574
|
+
]);
|
|
575
|
+
var unfinishedStepSchema = z4.object({
|
|
576
|
+
stepId: plainText(),
|
|
577
|
+
state: z4.enum(["parked", "failed"])
|
|
578
|
+
}).strict();
|
|
579
|
+
var crossedSchema = z4.discriminatedUnion("kind", [
|
|
580
|
+
z4.object({ kind: z4.literal("none") }).strict(),
|
|
581
|
+
z4.object({ kind: z4.literal("unfinished-steps"), steps: z4.array(unfinishedStepSchema).min(1) }).strict(),
|
|
582
|
+
z4.object({ kind: z4.literal("clean-over-findings"), findings: z4.number().int().min(1) }).strict(),
|
|
583
|
+
z4.object({ kind: z4.literal("not-recorded") }).strict()
|
|
584
|
+
]);
|
|
585
|
+
var seatCoverageSchema = z4.object({
|
|
586
|
+
enrolled: z4.number().int().nonnegative(),
|
|
587
|
+
conformant: z4.number().int().nonnegative()
|
|
588
|
+
}).strict().refine((c) => c.conformant <= c.enrolled, {
|
|
589
|
+
message: "conformant cannot exceed enrolled",
|
|
590
|
+
path: ["conformant"]
|
|
591
|
+
});
|
|
592
|
+
var observedCloseSchema = z4.object({
|
|
593
|
+
sequence: z4.number().int().min(1),
|
|
594
|
+
closedAt: utcIso8601,
|
|
595
|
+
closedBy: closedBySchema,
|
|
596
|
+
crossed: crossedSchema,
|
|
597
|
+
seatCoverage: seatCoverageSchema.nullable()
|
|
598
|
+
}).strict();
|
|
599
|
+
var couldNotRunCheckSchema = z4.object({
|
|
600
|
+
check: plainText(),
|
|
601
|
+
description: plainText().nullable(),
|
|
602
|
+
reason: plainText()
|
|
603
|
+
}).strict();
|
|
604
|
+
var firedCheckSchema = z4.object({
|
|
605
|
+
check: plainText(),
|
|
606
|
+
description: plainText().nullable(),
|
|
607
|
+
severity: severitySchema,
|
|
608
|
+
explanation: plainText().nullable(),
|
|
609
|
+
occurrences: z4.number().int().min(1)
|
|
610
|
+
}).strict();
|
|
611
|
+
var phaseChecksSchema = z4.discriminatedUnion("state", [
|
|
612
|
+
z4.object({ state: z4.literal("not-observed") }).strict(),
|
|
613
|
+
z4.object({
|
|
614
|
+
state: z4.literal("nothing-fired"),
|
|
615
|
+
runs: z4.number().int().min(1),
|
|
616
|
+
couldNotRun: z4.array(couldNotRunCheckSchema)
|
|
617
|
+
}).strict(),
|
|
618
|
+
z4.object({
|
|
619
|
+
state: z4.literal("fired"),
|
|
620
|
+
runs: z4.number().int().min(1),
|
|
621
|
+
couldNotRun: z4.array(couldNotRunCheckSchema),
|
|
622
|
+
fired: z4.array(firedCheckSchema).min(1)
|
|
623
|
+
}).strict()
|
|
624
|
+
]);
|
|
625
|
+
var observedSchema = z4.object({
|
|
626
|
+
engagementId: plainText(),
|
|
627
|
+
methodVersion: methodVersionSchema,
|
|
628
|
+
closes: z4.array(observedCloseSchema).min(1),
|
|
629
|
+
checks: phaseChecksSchema
|
|
630
|
+
}).strict().superRefine((o, ctx) => {
|
|
631
|
+
o.closes.forEach((close, i) => {
|
|
632
|
+
if (close.sequence !== i + 1) {
|
|
633
|
+
ctx.addIssue({
|
|
634
|
+
code: "custom",
|
|
635
|
+
message: "closes must be numbered 1, 2, 3 \u2026 in order with no gap",
|
|
636
|
+
path: ["closes", i, "sequence"]
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
var attestedWalkSchema = z4.object({
|
|
642
|
+
verdict: walkVerdictSchema,
|
|
643
|
+
findings: z4.array(plainText(FINDING_MAX)).max(FINDINGS_SHOWN_MAX),
|
|
644
|
+
findingsNotShown: z4.number().int().nonnegative()
|
|
645
|
+
}).strict();
|
|
646
|
+
var attestedStatementsSchema = z4.object({
|
|
647
|
+
thesis: plainText(STATEMENT_MAX).nullable(),
|
|
648
|
+
outcome: plainText(STATEMENT_MAX).nullable(),
|
|
649
|
+
deviations: z4.array(plainText(DEVIATION_MAX)).max(DEVIATIONS_MAX)
|
|
650
|
+
}).strict();
|
|
651
|
+
var attestedCloseSchema = z4.object({
|
|
652
|
+
sequence: z4.number().int().min(1),
|
|
653
|
+
decision: closeDecisionSchema,
|
|
654
|
+
actor: plainText(ACTOR_MAX).nullable(),
|
|
655
|
+
reason: plainText(PHASE_CLOSE_REASON_MAX).nullable(),
|
|
656
|
+
walk: attestedWalkSchema.nullable(),
|
|
657
|
+
closeCommit: closeCommitSchema.nullable(),
|
|
658
|
+
statements: attestedStatementsSchema
|
|
659
|
+
}).strict();
|
|
660
|
+
var attestedSchema = z4.object({
|
|
661
|
+
closes: z4.array(attestedCloseSchema)
|
|
662
|
+
}).strict();
|
|
663
|
+
var buildRecordV2Schema = z4.object({
|
|
664
|
+
format: z4.literal(BUILD_RECORD_V2_FORMAT),
|
|
665
|
+
phase: phaseNameSchema,
|
|
666
|
+
observed: observedSchema,
|
|
667
|
+
attested: attestedSchema
|
|
668
|
+
}).strict().superRefine((record2, ctx) => {
|
|
669
|
+
const observedBySequence = new Map(record2.observed.closes.map((c) => [c.sequence, c]));
|
|
670
|
+
const attestedSequences = record2.attested.closes.map((c) => c.sequence);
|
|
671
|
+
const sameSet = attestedSequences.length === observedBySequence.size && new Set(attestedSequences).size === attestedSequences.length && attestedSequences.every((s) => observedBySequence.has(s));
|
|
672
|
+
if (!sameSet) {
|
|
673
|
+
ctx.addIssue({
|
|
674
|
+
code: "custom",
|
|
675
|
+
message: "attested closes must match the observed closes one for one, by sequence",
|
|
676
|
+
path: ["attested", "closes"]
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
record2.attested.closes.forEach((close, i) => {
|
|
680
|
+
if (close.decision === "override" !== (close.reason !== null)) {
|
|
681
|
+
ctx.addIssue({
|
|
682
|
+
code: "custom",
|
|
683
|
+
message: "reason is given exactly when the decision is override",
|
|
684
|
+
path: ["attested", "closes", i, "reason"]
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
const observed = observedBySequence.get(close.sequence);
|
|
688
|
+
if (observed !== void 0 && observed.closedBy.kind === "account-removed" !== (close.actor === null)) {
|
|
689
|
+
ctx.addIssue({
|
|
690
|
+
code: "custom",
|
|
691
|
+
message: "actor is empty exactly when the closing account was removed",
|
|
692
|
+
path: ["attested", "closes", i, "actor"]
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
412
698
|
// ../events/dist/crew.js
|
|
413
699
|
import { z as z5 } from "zod";
|
|
414
700
|
var crewGroupSchema = z5.enum(["builder", "reviewer", "always-on"]);
|
|
@@ -605,7 +891,7 @@ var CREW_CALLSIGNS = CREW_ROSTER.map((member) => member.callsign);
|
|
|
605
891
|
|
|
606
892
|
// ../events/dist/device-auth.js
|
|
607
893
|
import { z as z6 } from "zod";
|
|
608
|
-
var
|
|
894
|
+
var utcIso86012 = z6.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z");
|
|
609
895
|
var DEVICE_AUTH_STATUS = {
|
|
610
896
|
/** The poll: minted, nobody has acted yet. Keep polling. 200. */
|
|
611
897
|
PENDING: "pending",
|
|
@@ -650,7 +936,7 @@ var deviceAuthStartResponseSchema = z6.object({
|
|
|
650
936
|
deviceCode: z6.string().min(1),
|
|
651
937
|
userCode: z6.string().min(1),
|
|
652
938
|
verificationUrl: z6.string().min(1),
|
|
653
|
-
expiresAt:
|
|
939
|
+
expiresAt: utcIso86012,
|
|
654
940
|
pollIntervalMs: z6.number().int().positive()
|
|
655
941
|
}).strict();
|
|
656
942
|
var devicePollPendingSchema = z6.object({
|
|
@@ -661,7 +947,7 @@ var devicePollApprovedSchema = z6.object({
|
|
|
661
947
|
status: z6.literal(DEVICE_AUTH_STATUS.APPROVED),
|
|
662
948
|
accountId: z6.string().min(1),
|
|
663
949
|
credential: z6.string().min(1),
|
|
664
|
-
expiresAt:
|
|
950
|
+
expiresAt: utcIso86012
|
|
665
951
|
}).strict();
|
|
666
952
|
var deviceAuthRefusalSchema = z6.object({
|
|
667
953
|
status: z6.enum([
|
|
@@ -699,67 +985,6 @@ var deviceConfirmResponseSchema = z6.union([
|
|
|
699
985
|
]);
|
|
700
986
|
var DASHBOARD_URL = "https://app.halfcycle.ai";
|
|
701
987
|
|
|
702
|
-
// ../events/dist/phase-identity.js
|
|
703
|
-
var PHASE_SEGMENT_PREFIX = "phase-";
|
|
704
|
-
function isValidPhaseIdentity(identity) {
|
|
705
|
-
if (typeof identity === "number")
|
|
706
|
-
return Number.isInteger(identity) && identity >= 0;
|
|
707
|
-
if (typeof identity !== "string")
|
|
708
|
-
return false;
|
|
709
|
-
if (identity === "")
|
|
710
|
-
return false;
|
|
711
|
-
if (identity.includes("/"))
|
|
712
|
-
return false;
|
|
713
|
-
if (identity.includes("\\"))
|
|
714
|
-
return false;
|
|
715
|
-
if (identity.includes("\0"))
|
|
716
|
-
return false;
|
|
717
|
-
if (identity.startsWith("."))
|
|
718
|
-
return false;
|
|
719
|
-
return true;
|
|
720
|
-
}
|
|
721
|
-
var InvalidPhaseIdentityError = class extends Error {
|
|
722
|
-
/** The value that was refused, exactly as supplied. */
|
|
723
|
-
identity;
|
|
724
|
-
constructor(identity, allowedLocation) {
|
|
725
|
-
const subject = allowedLocation === void 0 ? "a file" : "a Build Record";
|
|
726
|
-
const where = allowedLocation === void 0 ? "" : `
|
|
727
|
-
Records are written only into ${allowedLocation.replace(/\\/g, "/")}/. Nothing was written.`;
|
|
728
|
-
super(`that phase identity cannot name ${subject}.
|
|
729
|
-
identity: ${describeIdentity(identity)}` + where);
|
|
730
|
-
this.name = "InvalidPhaseIdentityError";
|
|
731
|
-
this.identity = identity;
|
|
732
|
-
}
|
|
733
|
-
};
|
|
734
|
-
function assertValidPhaseIdentity(identity, allowedLocation) {
|
|
735
|
-
if (!isValidPhaseIdentity(identity)) {
|
|
736
|
-
throw new InvalidPhaseIdentityError(identity, allowedLocation);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
function renderPhaseSegment(identity) {
|
|
740
|
-
if (!isValidPhaseIdentity(identity)) {
|
|
741
|
-
throw new InvalidPhaseIdentityError(identity);
|
|
742
|
-
}
|
|
743
|
-
return `${PHASE_SEGMENT_PREFIX}${identity}`;
|
|
744
|
-
}
|
|
745
|
-
function describeIdentity(identity) {
|
|
746
|
-
if (identity === "")
|
|
747
|
-
return "(empty)";
|
|
748
|
-
if (typeof identity === "string")
|
|
749
|
-
return identity;
|
|
750
|
-
if (identity === null)
|
|
751
|
-
return "null";
|
|
752
|
-
if (identity === void 0)
|
|
753
|
-
return "none supplied";
|
|
754
|
-
if (typeof identity === "number" || typeof identity === "boolean")
|
|
755
|
-
return String(identity);
|
|
756
|
-
if (Array.isArray(identity))
|
|
757
|
-
return "(a list)";
|
|
758
|
-
if (typeof identity === "object")
|
|
759
|
-
return "(an object)";
|
|
760
|
-
return `(a ${typeof identity})`;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
988
|
// ../events/dist/credential-wire.js
|
|
764
989
|
import { z as z7 } from "zod";
|
|
765
990
|
var engagementCredentialResponseSchema = z7.object({
|
|
@@ -771,10 +996,47 @@ var engagementCredentialResponseSchema = z7.object({
|
|
|
771
996
|
}).strict();
|
|
772
997
|
var ciTokenExchangeResponseSchema = z7.object({
|
|
773
998
|
token: z7.string(),
|
|
774
|
-
expiresAt:
|
|
999
|
+
expiresAt: utcIso8601,
|
|
775
1000
|
engagementId: z7.string()
|
|
776
1001
|
}).strict();
|
|
777
1002
|
|
|
1003
|
+
// ../events/dist/phase-record.js
|
|
1004
|
+
import { z as z8 } from "zod";
|
|
1005
|
+
var phaseRecordRefusalReasonSchema = z8.enum([
|
|
1006
|
+
"still-open",
|
|
1007
|
+
"reopened",
|
|
1008
|
+
"never-opened",
|
|
1009
|
+
"never-closed",
|
|
1010
|
+
"closed-before-records"
|
|
1011
|
+
]);
|
|
1012
|
+
var phaseRecordRecordSchema = z8.object({
|
|
1013
|
+
outcome: z8.literal("record"),
|
|
1014
|
+
phase: phaseNameSchema,
|
|
1015
|
+
record: buildRecordV2Schema
|
|
1016
|
+
}).strict().refine((body) => body.record.phase === body.phase, {
|
|
1017
|
+
message: "the record is for a different phase than the one asked for",
|
|
1018
|
+
path: ["record", "phase"]
|
|
1019
|
+
});
|
|
1020
|
+
var phaseRecordWithheldSchema = z8.object({
|
|
1021
|
+
outcome: z8.literal("withheld"),
|
|
1022
|
+
phase: phaseNameSchema,
|
|
1023
|
+
sequence: z8.number().int().min(1),
|
|
1024
|
+
closedAt: utcIso8601,
|
|
1025
|
+
message: z8.string()
|
|
1026
|
+
}).strict();
|
|
1027
|
+
var phaseRecordRefusedSchema = wireErrorSchema.extend({
|
|
1028
|
+
outcome: z8.literal("refused"),
|
|
1029
|
+
phase: phaseNameSchema,
|
|
1030
|
+
statusCode: z8.literal(409),
|
|
1031
|
+
error: z8.literal("PhaseRecordRefused"),
|
|
1032
|
+
reason: phaseRecordRefusalReasonSchema
|
|
1033
|
+
}).strict();
|
|
1034
|
+
var phaseRecordResponseSchema = z8.discriminatedUnion("outcome", [
|
|
1035
|
+
phaseRecordRecordSchema,
|
|
1036
|
+
phaseRecordWithheldSchema,
|
|
1037
|
+
phaseRecordRefusedSchema
|
|
1038
|
+
]);
|
|
1039
|
+
|
|
778
1040
|
// dist/engagement-credential.js
|
|
779
1041
|
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
780
1042
|
import { platform as platform2 } from "node:os";
|
|
@@ -925,14 +1187,36 @@ halfcycle_env_file() {
|
|
|
925
1187
|
// dist/identity.js
|
|
926
1188
|
import { execFileSync } from "node:child_process";
|
|
927
1189
|
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
928
|
-
import { join as join3 } from "node:path";
|
|
1190
|
+
import { isAbsolute, join as join3 } from "node:path";
|
|
929
1191
|
import { randomUUID } from "node:crypto";
|
|
930
1192
|
var PROJECT_IDENTITY_NOTE = 'projectId is NOT the engagement id \u2014 it identifies this repository/worktree to Halfcycle, not an engagement. The engagement id lives in .halfcycle/bundle.json, in its "engagementId" field.';
|
|
931
|
-
|
|
1193
|
+
var REPOSITORY_LOCATING_GIT_VARS = [
|
|
1194
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
1195
|
+
"GIT_COMMON_DIR",
|
|
1196
|
+
"GIT_DIR",
|
|
1197
|
+
"GIT_GRAFT_FILE",
|
|
1198
|
+
"GIT_IMPLICIT_WORK_TREE",
|
|
1199
|
+
"GIT_INDEX_FILE",
|
|
1200
|
+
"GIT_NO_REPLACE_OBJECTS",
|
|
1201
|
+
"GIT_OBJECT_DIRECTORY",
|
|
1202
|
+
"GIT_PREFIX",
|
|
1203
|
+
"GIT_REPLACE_REF_BASE",
|
|
1204
|
+
"GIT_SHALLOW_FILE",
|
|
1205
|
+
"GIT_WORK_TREE"
|
|
1206
|
+
];
|
|
1207
|
+
function gitEnvForNamedRepo() {
|
|
1208
|
+
const env = { ...process.env };
|
|
1209
|
+
for (const name of REPOSITORY_LOCATING_GIT_VARS)
|
|
1210
|
+
delete env[name];
|
|
1211
|
+
return env;
|
|
1212
|
+
}
|
|
1213
|
+
function git(targetRepoRoot, args, stdin) {
|
|
932
1214
|
try {
|
|
933
1215
|
const out = execFileSync("git", ["-C", targetRepoRoot, ...args], {
|
|
934
1216
|
encoding: "utf-8",
|
|
935
|
-
|
|
1217
|
+
env: gitEnvForNamedRepo(),
|
|
1218
|
+
...stdin === void 0 ? {} : { input: stdin },
|
|
1219
|
+
stdio: [stdin === void 0 ? "ignore" : "pipe", "pipe", "ignore"]
|
|
936
1220
|
});
|
|
937
1221
|
const trimmed = out.trim();
|
|
938
1222
|
return trimmed.length > 0 ? trimmed : null;
|
|
@@ -950,6 +1234,38 @@ function readRootCommit(targetRepoRoot) {
|
|
|
950
1234
|
function readRemote(targetRepoRoot) {
|
|
951
1235
|
return git(targetRepoRoot, ["remote", "get-url", "origin"]);
|
|
952
1236
|
}
|
|
1237
|
+
function readIgnoredPaths(targetRepoRoot, paths) {
|
|
1238
|
+
if (paths.length === 0)
|
|
1239
|
+
return null;
|
|
1240
|
+
const out = git(targetRepoRoot, ["check-ignore", "-v", "-z", "--stdin"], paths.map((p) => `${p}\0`).join(""));
|
|
1241
|
+
if (out === null)
|
|
1242
|
+
return null;
|
|
1243
|
+
const fields = out.split("\0");
|
|
1244
|
+
if (fields[fields.length - 1] === "")
|
|
1245
|
+
fields.pop();
|
|
1246
|
+
if (fields.length % 4 !== 0)
|
|
1247
|
+
return null;
|
|
1248
|
+
const ignored = [];
|
|
1249
|
+
for (let i = 0; i < fields.length; i += 4) {
|
|
1250
|
+
const [source, line, pattern, path] = fields.slice(i, i + 4);
|
|
1251
|
+
if (pattern.startsWith("!"))
|
|
1252
|
+
continue;
|
|
1253
|
+
const lineNumber = Number.parseInt(line, 10);
|
|
1254
|
+
if (!Number.isFinite(lineNumber))
|
|
1255
|
+
return null;
|
|
1256
|
+
ignored.push({ path, pattern, source, line: lineNumber });
|
|
1257
|
+
}
|
|
1258
|
+
if (ignored.length === 0)
|
|
1259
|
+
return null;
|
|
1260
|
+
if (ignored.every((entry) => isAbsolute(entry.source)))
|
|
1261
|
+
return ignored;
|
|
1262
|
+
if (git(targetRepoRoot, ["rev-parse", "--show-prefix"]) === null)
|
|
1263
|
+
return ignored;
|
|
1264
|
+
const toplevel = git(targetRepoRoot, ["rev-parse", "--show-toplevel"]);
|
|
1265
|
+
if (toplevel === null)
|
|
1266
|
+
return null;
|
|
1267
|
+
return ignored.map((entry) => isAbsolute(entry.source) ? entry : { ...entry, source: join3(toplevel, entry.source) });
|
|
1268
|
+
}
|
|
953
1269
|
function mintOrReadIdentity(targetRepoRoot) {
|
|
954
1270
|
const path = join3(targetRepoRoot, ".halfcycle", "project.json");
|
|
955
1271
|
if (existsSync(path)) {
|
|
@@ -1399,18 +1715,12 @@ var BUNDLE_PIN_REL = ".halfcycle/bundle.json";
|
|
|
1399
1715
|
var PROJECT_IDENTITY_REL = ".halfcycle/project.json";
|
|
1400
1716
|
var CREW_ROSTER_REL = ".halfcycle/crew.json";
|
|
1401
1717
|
var CAPTURED_INDEX_REL = "test/fixtures/captured/manifest.json";
|
|
1402
|
-
var SETTINGS_REL = ".claude/settings.json";
|
|
1403
1718
|
var GITIGNORE_REL = ".gitignore";
|
|
1404
1719
|
function commandStubRel(name) {
|
|
1405
1720
|
return `.claude/commands/${name}.md`;
|
|
1406
1721
|
}
|
|
1407
1722
|
var RETIRED_WRITE_SET_FILES = [];
|
|
1408
|
-
var RETIRED_HOOK_COMMANDS = [];
|
|
1409
1723
|
var RETIRED_DENY_PATTERNS = [];
|
|
1410
|
-
function installerHookCommands() {
|
|
1411
|
-
const settings = JSON.parse(generateSettingsJson());
|
|
1412
|
-
return Object.values(settings.hooks ?? {}).flatMap((entries) => entries.flatMap((entry) => entry.hooks.map((h) => h.command)));
|
|
1413
|
-
}
|
|
1414
1724
|
function closedWriteSet() {
|
|
1415
1725
|
const files = /* @__PURE__ */ new Set([
|
|
1416
1726
|
...(readPluginManifest().commands ?? []).map((cmd) => commandStubRel(cmd.name)),
|
|
@@ -1436,7 +1746,7 @@ function closedWriteSet() {
|
|
|
1436
1746
|
merged,
|
|
1437
1747
|
dirs,
|
|
1438
1748
|
ownedDirs,
|
|
1439
|
-
hookCommands:
|
|
1749
|
+
hookCommands: new Set(HALFCYCLE_HOOK_COMMANDS),
|
|
1440
1750
|
denyRuleSha256: new Set([...DENY_PATTERNS, ...RETIRED_DENY_PATTERNS].map((rule) => sha256Hex(rule))),
|
|
1441
1751
|
gitignoreLines: /* @__PURE__ */ new Set([GITIGNORE_HEADER, ...REQUIRED_GITIGNORE_ENTRIES, LEGACY_ENV_LOCAL_REL])
|
|
1442
1752
|
};
|
|
@@ -1521,6 +1831,54 @@ function assertManifestInWriteSet(manifest, writeSet) {
|
|
|
1521
1831
|
throw new Error(`[bundle install] the install record names paths outside the write set: ${strays.join(", ")}`);
|
|
1522
1832
|
}
|
|
1523
1833
|
}
|
|
1834
|
+
function findNotShared(targetRepo, manifest, collidedPaths) {
|
|
1835
|
+
const merged = [
|
|
1836
|
+
SETTINGS_REL,
|
|
1837
|
+
...manifest.mcp !== void 0 ? [MCP_REGISTRATION_REL] : [],
|
|
1838
|
+
...manifest.gitignore.created || manifest.gitignore.appended.length > 0 ? [GITIGNORE_REL] : []
|
|
1839
|
+
];
|
|
1840
|
+
const collided = new Set(collidedPaths);
|
|
1841
|
+
const dirVerdicts = /* @__PURE__ */ new Map();
|
|
1842
|
+
const travelsThroughPlainDirs = (rel) => {
|
|
1843
|
+
for (let dir = posix.dirname(rel); dir !== "."; dir = posix.dirname(dir)) {
|
|
1844
|
+
let plain = dirVerdicts.get(dir);
|
|
1845
|
+
if (plain === void 0) {
|
|
1846
|
+
const abs = join5(targetRepo, dir);
|
|
1847
|
+
plain = !lstatOrNull(abs)?.isSymbolicLink() && lstatOrNull(join5(abs, ".git")) === null;
|
|
1848
|
+
dirVerdicts.set(dir, plain);
|
|
1849
|
+
}
|
|
1850
|
+
if (!plain)
|
|
1851
|
+
return false;
|
|
1852
|
+
}
|
|
1853
|
+
return true;
|
|
1854
|
+
};
|
|
1855
|
+
const paths = [
|
|
1856
|
+
.../* @__PURE__ */ new Set([
|
|
1857
|
+
...manifest.files.filter((f) => !("perMachine" in f)).map((f) => f.path),
|
|
1858
|
+
...merged,
|
|
1859
|
+
INSTALL_MANIFEST_REL
|
|
1860
|
+
])
|
|
1861
|
+
].filter((rel) => !collided.has(rel)).filter((rel) => lstatOrNull(join5(targetRepo, rel)) !== null).filter(travelsThroughPlainDirs).sort();
|
|
1862
|
+
const ignored = readIgnoredPaths(targetRepo, paths);
|
|
1863
|
+
if (ignored === null)
|
|
1864
|
+
return null;
|
|
1865
|
+
const sorted = [...ignored].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
1866
|
+
const named = sorted.find((entry) => entry.path === SETTINGS_REL) ?? sorted[0];
|
|
1867
|
+
return {
|
|
1868
|
+
path: named.path,
|
|
1869
|
+
pattern: named.pattern,
|
|
1870
|
+
source: named.source,
|
|
1871
|
+
line: named.line,
|
|
1872
|
+
others: sorted.length - 1
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
function lstatOrNull(path) {
|
|
1876
|
+
try {
|
|
1877
|
+
return lstatSync(path);
|
|
1878
|
+
} catch {
|
|
1879
|
+
return null;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1524
1882
|
function isPlainObject(value) {
|
|
1525
1883
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1526
1884
|
}
|
|
@@ -1635,9 +1993,15 @@ var DENY_PATTERNS = [
|
|
|
1635
1993
|
"Bash(git push --no-verify:*)",
|
|
1636
1994
|
"Bash(git commit --no-verify:*)"
|
|
1637
1995
|
];
|
|
1638
|
-
var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
|
|
1639
1996
|
var USER_PROMPT_REMINDER_SENTENCE = "Where this project stands, and what order to work in, is recorded in the Halfcycle service and nowhere in this repository \u2014 call halfcycle_resolve before you answer or plan, because the files here will look complete and current and still be wrong about position.";
|
|
1640
1997
|
function generateSettingsJson() {
|
|
1998
|
+
const hooks = {};
|
|
1999
|
+
for (const { event, matcher, timeout, command } of HALFCYCLE_HOOK_WIRING) {
|
|
2000
|
+
(hooks[event] ??= []).push({
|
|
2001
|
+
...matcher === null ? {} : { matcher },
|
|
2002
|
+
hooks: [{ type: "command", command, timeout }]
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
1641
2005
|
const settings = {
|
|
1642
2006
|
$schema: "https://json.schemastore.org/claude-code-settings.json",
|
|
1643
2007
|
permissions: {
|
|
@@ -1645,88 +2009,10 @@ function generateSettingsJson() {
|
|
|
1645
2009
|
// and is the machine owner's grant to make, not ours.
|
|
1646
2010
|
deny: [...DENY_PATTERNS]
|
|
1647
2011
|
},
|
|
1648
|
-
hooks
|
|
1649
|
-
SessionStart: [
|
|
1650
|
-
{
|
|
1651
|
-
hooks: [
|
|
1652
|
-
{
|
|
1653
|
-
type: "command",
|
|
1654
|
-
command: "bash ./.claude/hooks/session-start-marker.sh",
|
|
1655
|
-
timeout: 5
|
|
1656
|
-
}
|
|
1657
|
-
]
|
|
1658
|
-
},
|
|
1659
|
-
{
|
|
1660
|
-
hooks: [
|
|
1661
|
-
{
|
|
1662
|
-
type: "command",
|
|
1663
|
-
command: "bash ./.claude/hooks/guard-runner.sh refresh-credential",
|
|
1664
|
-
timeout: 10
|
|
1665
|
-
}
|
|
1666
|
-
]
|
|
1667
|
-
}
|
|
1668
|
-
],
|
|
1669
|
-
UserPromptSubmit: [
|
|
1670
|
-
{
|
|
1671
|
-
hooks: [
|
|
1672
|
-
{
|
|
1673
|
-
type: "command",
|
|
1674
|
-
command: "bash ./.claude/hooks/user-prompt-reminder.sh",
|
|
1675
|
-
timeout: 5
|
|
1676
|
-
}
|
|
1677
|
-
]
|
|
1678
|
-
}
|
|
1679
|
-
],
|
|
1680
|
-
PostToolUse: [
|
|
1681
|
-
{
|
|
1682
|
-
matcher: WRITE_CAPABLE_TOOLS.join("|"),
|
|
1683
|
-
hooks: [
|
|
1684
|
-
{
|
|
1685
|
-
type: "command",
|
|
1686
|
-
command: "bash ./.claude/hooks/guard-runner.sh hook post-tool-use",
|
|
1687
|
-
timeout: 60
|
|
1688
|
-
}
|
|
1689
|
-
]
|
|
1690
|
-
}
|
|
1691
|
-
],
|
|
1692
|
-
Stop: [
|
|
1693
|
-
{
|
|
1694
|
-
hooks: [
|
|
1695
|
-
{
|
|
1696
|
-
type: "command",
|
|
1697
|
-
command: "bash ./.claude/hooks/guard-runner.sh hook stop",
|
|
1698
|
-
timeout: 60
|
|
1699
|
-
}
|
|
1700
|
-
]
|
|
1701
|
-
}
|
|
1702
|
-
],
|
|
1703
|
-
SubagentStop: [
|
|
1704
|
-
{
|
|
1705
|
-
hooks: [
|
|
1706
|
-
{
|
|
1707
|
-
type: "command",
|
|
1708
|
-
command: "bash ./.claude/hooks/guard-runner.sh hook subagent-stop",
|
|
1709
|
-
timeout: 60
|
|
1710
|
-
}
|
|
1711
|
-
]
|
|
1712
|
-
}
|
|
1713
|
-
],
|
|
1714
|
-
SessionEnd: [
|
|
1715
|
-
{
|
|
1716
|
-
hooks: [
|
|
1717
|
-
{
|
|
1718
|
-
type: "command",
|
|
1719
|
-
command: "bash ./.claude/hooks/session-end-marker.sh",
|
|
1720
|
-
timeout: 5
|
|
1721
|
-
}
|
|
1722
|
-
]
|
|
1723
|
-
}
|
|
1724
|
-
]
|
|
1725
|
-
}
|
|
2012
|
+
hooks
|
|
1726
2013
|
};
|
|
1727
2014
|
return JSON.stringify(settings, null, 2) + "\n";
|
|
1728
2015
|
}
|
|
1729
|
-
var VENDORED_BIN_REL = ".halfcycle/bin/bin.bundle.mjs";
|
|
1730
2016
|
var GENERATED_GUARD_RUNNER_HEADER = "# Governance hook wrapper \u2014 generated by";
|
|
1731
2017
|
function generateGuardRunnerWrapper() {
|
|
1732
2018
|
return `#!/usr/bin/env bash
|
|
@@ -1948,14 +2234,16 @@ var CI_STANZA_KNOWN_HEADER_HASHES = [
|
|
|
1948
2234
|
function isTrackedAndClean(targetRepo, relPath) {
|
|
1949
2235
|
try {
|
|
1950
2236
|
execFileSync2("git", ["-C", targetRepo, "ls-files", "--error-unmatch", "--", relPath], {
|
|
1951
|
-
stdio: "ignore"
|
|
2237
|
+
stdio: "ignore",
|
|
2238
|
+
env: gitEnvForNamedRepo()
|
|
1952
2239
|
});
|
|
1953
2240
|
} catch {
|
|
1954
2241
|
return false;
|
|
1955
2242
|
}
|
|
1956
2243
|
try {
|
|
1957
2244
|
execFileSync2("git", ["-C", targetRepo, "diff", "--quiet", "HEAD", "--", relPath], {
|
|
1958
|
-
stdio: "ignore"
|
|
2245
|
+
stdio: "ignore",
|
|
2246
|
+
env: gitEnvForNamedRepo()
|
|
1959
2247
|
});
|
|
1960
2248
|
} catch {
|
|
1961
2249
|
return false;
|
|
@@ -2533,6 +2821,7 @@ async function install(options) {
|
|
|
2533
2821
|
const installManifest = finishManifest(draft);
|
|
2534
2822
|
assertManifestInWriteSet(installManifest, writeSet);
|
|
2535
2823
|
recordOwned(report, join5(targetRepo, INSTALL_MANIFEST_REL), targetRepo, serializeManifest(installManifest), INSTALL_MANIFEST_REL);
|
|
2824
|
+
const notShared = findNotShared(targetRepo, installManifest, report.collidedPaths);
|
|
2536
2825
|
return {
|
|
2537
2826
|
version: manifest.version,
|
|
2538
2827
|
writtenPaths: report.writtenPaths,
|
|
@@ -2541,7 +2830,8 @@ async function install(options) {
|
|
|
2541
2830
|
collidedPaths: report.collidedPaths,
|
|
2542
2831
|
replacedPaths: report.replacedPaths,
|
|
2543
2832
|
bootstrapScanRan: scanResult.ran,
|
|
2544
|
-
legacyCredentialKept: migration.outcome === "kept" ? { keys: migration.unbacked, missing: migration.missingForAdoption } : null
|
|
2833
|
+
legacyCredentialKept: migration.outcome === "kept" ? { keys: migration.unbacked, missing: migration.missingForAdoption } : null,
|
|
2834
|
+
notShared
|
|
2545
2835
|
};
|
|
2546
2836
|
}
|
|
2547
2837
|
|
|
@@ -2722,7 +3012,7 @@ async function mintBoardEnterCode(baseUrl, sessionToken) {
|
|
|
2722
3012
|
}
|
|
2723
3013
|
|
|
2724
3014
|
// dist/setup/manifest.js
|
|
2725
|
-
import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3015
|
+
import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2726
3016
|
import { dirname as dirname3, join as join6, posix as posix2 } from "node:path";
|
|
2727
3017
|
var DECLARED_ROOT_FILES = ["CLAUDE.md", "AGENTS.md", "ENGAGEMENT.md"];
|
|
2728
3018
|
var DECLARED_DIR_PREFIX = "docs";
|
|
@@ -2810,7 +3100,7 @@ function firstSymlinkOnPath(targetRepo, path) {
|
|
|
2810
3100
|
for (let i = 0; i < segments.length; i++) {
|
|
2811
3101
|
walked = join6(walked, segments[i]);
|
|
2812
3102
|
try {
|
|
2813
|
-
if (
|
|
3103
|
+
if (lstatSync2(walked).isSymbolicLink())
|
|
2814
3104
|
return segments.slice(0, i + 1).join("/");
|
|
2815
3105
|
} catch {
|
|
2816
3106
|
return null;
|
|
@@ -3170,446 +3460,485 @@ function buildEngagementRecord(input) {
|
|
|
3170
3460
|
return { ok: true, document, rows };
|
|
3171
3461
|
}
|
|
3172
3462
|
|
|
3173
|
-
// dist/build-record/
|
|
3174
|
-
var
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
}
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
import { join as join10 } from "node:path";
|
|
3189
|
-
|
|
3190
|
-
// dist/build-record/close-record.js
|
|
3191
|
-
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
3192
|
-
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3193
|
-
import { dirname as dirname4, join as join8 } from "node:path";
|
|
3194
|
-
var CLOSE_RECORD_FORMAT = "halfcycle-phase-close/v1";
|
|
3195
|
-
var BUILD_RECORD_ZONE_B_DIR = join8(ZONE_B_DIR, "build-record");
|
|
3196
|
-
var InvalidCloseRecordError = class extends Error {
|
|
3197
|
-
constructor(path, detail) {
|
|
3198
|
-
super(`[build-record] the phase close record at ${path} cannot be read \u2014 ${detail}`);
|
|
3199
|
-
this.name = "InvalidCloseRecordError";
|
|
3463
|
+
// dist/build-record/render-v2.js
|
|
3464
|
+
var HIDDEN = /[\p{Cc}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}]/u;
|
|
3465
|
+
var MARKDOWN_SPECIAL = /* @__PURE__ */ new Set(["\\", "`", "*", "_", "[", "]", "<", "|", "~", "#", "&", "$"]);
|
|
3466
|
+
function printRecordText(text) {
|
|
3467
|
+
if (text === "")
|
|
3468
|
+
return "(blank)";
|
|
3469
|
+
let out = "";
|
|
3470
|
+
for (const ch of text) {
|
|
3471
|
+
if (HIDDEN.test(ch)) {
|
|
3472
|
+
out += `U+${ch.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}`;
|
|
3473
|
+
} else if (MARKDOWN_SPECIAL.has(ch)) {
|
|
3474
|
+
out += `\\${ch}`;
|
|
3475
|
+
} else {
|
|
3476
|
+
out += ch;
|
|
3477
|
+
}
|
|
3200
3478
|
}
|
|
3201
|
-
|
|
3202
|
-
function closeRecordPath(repoRoot, phase) {
|
|
3203
|
-
assertValidPhaseIdentity(phase, BUILD_RECORD_ZONE_B_DIR);
|
|
3204
|
-
return join8(repoRoot, BUILD_RECORD_ZONE_B_DIR, `${renderPhaseSegment(phase)}.close.json`);
|
|
3479
|
+
return out;
|
|
3205
3480
|
}
|
|
3206
|
-
function
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
return { closeCommit, closedDate };
|
|
3219
|
-
} catch {
|
|
3220
|
-
return null;
|
|
3221
|
-
}
|
|
3481
|
+
function makePrinter() {
|
|
3482
|
+
let hidden = false;
|
|
3483
|
+
return {
|
|
3484
|
+
text(value) {
|
|
3485
|
+
if (!hidden && HIDDEN.test(value))
|
|
3486
|
+
hidden = true;
|
|
3487
|
+
return printRecordText(value);
|
|
3488
|
+
},
|
|
3489
|
+
sawHidden() {
|
|
3490
|
+
return hidden;
|
|
3491
|
+
}
|
|
3492
|
+
};
|
|
3222
3493
|
}
|
|
3223
|
-
function
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3494
|
+
function counted(n, one, many) {
|
|
3495
|
+
return `${String(n)} ${n === 1 ? one : many}`;
|
|
3496
|
+
}
|
|
3497
|
+
var TITLE_PREFIX = "# Build Record: ";
|
|
3498
|
+
var HEADER_WHAT = "This is the Build Record for one phase of a software project run with the Halfcycle method. A phase is a stage of the work with an agreed finish line; this record says how this one ended.";
|
|
3499
|
+
var HEADER_REGISTERS = "Two kinds of content are kept apart here. Lines under **Observed by Halfcycle** come from Halfcycle's own records, made as the work happened. Lines under **Stated by the client** are quoted, and are the client's own word: Halfcycle did not check them. Halfcycle witnessed the method being run. The client attests the code.";
|
|
3500
|
+
var HEADER_KEPT = "Halfcycle derived this record and kept it when the phase closed, and changes it afterwards in one way only: if an account that closed this phase is later deleted, its email is removed, and so is the name given at any close Halfcycle recorded as made by that account. The client's Halfcycle command-line tool wrote this file from that kept copy when the phase closed or when it was fetched again, so a later deletion changes the kept copy and reaches this file at the next fetch; the same record, as data, is in the .json file of the same name beside this one. If the phase is closed again, this file is replaced by a new record that describes every close, this one included.";
|
|
3501
|
+
var HEADER_HIDDEN = "Some text in this record contains characters that do not print. Each is shown as U+ and its code of four or more hex digits; the .json file holds the text exactly.";
|
|
3502
|
+
var OBSERVED_LABEL = "**Observed by Halfcycle**";
|
|
3503
|
+
var STATED_LABEL = "**Stated by the client** \u2014 Halfcycle did not check these.";
|
|
3504
|
+
var MISMATCHED_HALVES = "This Build Record could not be written: its two parts describe different closes.";
|
|
3505
|
+
function renderChecks(checks, p) {
|
|
3506
|
+
const blocks = ["## Checks during this phase", OBSERVED_LABEL];
|
|
3507
|
+
if (checks.state === "not-observed") {
|
|
3508
|
+
blocks.push("**Nothing was observed.** Halfcycle has no record of any check running during this phase, so this record cannot say whether the work had problems. This is not a clean result.");
|
|
3509
|
+
return blocks;
|
|
3510
|
+
}
|
|
3511
|
+
const runs = counted(checks.runs, "check run", "check runs");
|
|
3512
|
+
if (checks.state === "nothing-fired") {
|
|
3513
|
+
blocks.push(`**Checks ran and found nothing.** Halfcycle recorded ${runs} during this phase, and no check found a problem.`);
|
|
3514
|
+
} else {
|
|
3515
|
+
blocks.push(`**Checks found problems.** Halfcycle recorded ${runs} during this phase. Each check that found something is listed by what it checks, in plain words, then how serious it is, what it found, and the name your terminal prints for it.`);
|
|
3516
|
+
blocks.push(renderFired(checks.fired, p));
|
|
3237
3517
|
}
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
};
|
|
3244
|
-
try {
|
|
3245
|
-
mkdirSync5(dirname4(path), { recursive: true });
|
|
3246
|
-
writeFileSync5(path, JSON.stringify(record2, null, 2) + "\n", "utf-8");
|
|
3247
|
-
} catch (err) {
|
|
3248
|
-
return { recorded: false, path, reason: err instanceof Error ? err.message : String(err) };
|
|
3518
|
+
if (checks.couldNotRun.length === 0) {
|
|
3519
|
+
blocks.push("No check reported that it could not run.");
|
|
3520
|
+
} else {
|
|
3521
|
+
blocks.push("**Some checks could not run.** They neither passed nor failed during this phase:");
|
|
3522
|
+
blocks.push(renderCouldNotRun(checks.couldNotRun, p));
|
|
3249
3523
|
}
|
|
3250
|
-
return
|
|
3524
|
+
return blocks;
|
|
3251
3525
|
}
|
|
3252
|
-
function
|
|
3253
|
-
|
|
3254
|
-
|
|
3526
|
+
function renderFired(fired, p) {
|
|
3527
|
+
const lines = [];
|
|
3528
|
+
for (const entry of fired) {
|
|
3529
|
+
lines.push(entry.description === null ? "- Check: A check found something here, and no plain-language description of it is on record." : `- Check: ${p.text(entry.description)}`);
|
|
3530
|
+
lines.push(` - Seriousness: ${seriousness(entry.severity)}`);
|
|
3531
|
+
lines.push(` - Found: ${counted(entry.occurrences, "time", "times")} during this phase.`);
|
|
3532
|
+
lines.push(` - What it found: ${entry.explanation === null ? "Not included in this record." : p.text(entry.explanation)}`);
|
|
3533
|
+
lines.push(` - The name your terminal prints for this check: ${p.text(entry.check)}`);
|
|
3255
3534
|
}
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3535
|
+
return lines.join("\n");
|
|
3536
|
+
}
|
|
3537
|
+
function seriousness(severity) {
|
|
3538
|
+
switch (severity) {
|
|
3539
|
+
case "block":
|
|
3540
|
+
return "serious, it blocks the work.";
|
|
3541
|
+
case "warn":
|
|
3542
|
+
return "a warning.";
|
|
3543
|
+
case "info":
|
|
3544
|
+
return "for information only.";
|
|
3259
3545
|
}
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
const
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
if (missing.length > 0) {
|
|
3268
|
-
throw new InvalidCloseRecordError(path, `${missing.join(" and ")} ${missing.length === 1 ? "is" : "are"} not recorded in it. A close records the commit and its date together or records neither, so a file carrying one of them was edited by hand.`);
|
|
3546
|
+
}
|
|
3547
|
+
function renderCouldNotRun(entries, p) {
|
|
3548
|
+
const lines = [];
|
|
3549
|
+
for (const entry of entries) {
|
|
3550
|
+
lines.push(entry.description === null ? "- Check: A check could not run here, and no plain-language description of it is on record." : `- Check: ${p.text(entry.description)}`);
|
|
3551
|
+
lines.push(` - Why it could not run: ${p.text(entry.reason)}`);
|
|
3552
|
+
lines.push(` - The name your terminal prints for this check: ${p.text(entry.check)}`);
|
|
3269
3553
|
}
|
|
3270
|
-
|
|
3271
|
-
|
|
3554
|
+
return lines.join("\n");
|
|
3555
|
+
}
|
|
3556
|
+
function headline(observed, attested) {
|
|
3557
|
+
const met = attested.decision === "done-when-met";
|
|
3558
|
+
switch (observed.crossed.kind) {
|
|
3559
|
+
case "none":
|
|
3560
|
+
return met ? "**Closed cleanly.** Halfcycle saw nothing outstanding, and the person closing the phase stated its finish conditions were met." : "**Closed by override.** Halfcycle saw nothing outstanding, and the person closing the phase chose to override rather than state its finish conditions were met.";
|
|
3561
|
+
case "unfinished-steps":
|
|
3562
|
+
case "clean-over-findings":
|
|
3563
|
+
return met ? "**Closed over outstanding work.** Halfcycle saw work still outstanding, and the person closing the phase stated its finish conditions were met anyway." : "**Closed over outstanding work.** Halfcycle saw work still outstanding, and the person closing the phase chose to override it.";
|
|
3564
|
+
case "not-recorded":
|
|
3565
|
+
return met ? "**Outstanding work not recorded.** Halfcycle has no record of what was outstanding at this close. The person closing the phase stated its finish conditions were met." : "**Outstanding work not recorded.** Halfcycle has no record of what was outstanding at this close. The person closing the phase chose to override.";
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
function closedAtText(closedAt, p) {
|
|
3569
|
+
const m = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(?:\.\d+)?Z$/.exec(closedAt);
|
|
3570
|
+
return m === null ? p.text(closedAt) : `${m[1]} ${m[2]} UTC`;
|
|
3571
|
+
}
|
|
3572
|
+
function signedInAs(closedBy, p) {
|
|
3573
|
+
switch (closedBy.kind) {
|
|
3574
|
+
case "account":
|
|
3575
|
+
return closedBy.email === null ? "an account with no email address on file" : `the account ${p.text(closedBy.email)}`;
|
|
3576
|
+
case "no-account":
|
|
3577
|
+
return "a credential not linked to any person's account";
|
|
3578
|
+
case "account-removed":
|
|
3579
|
+
return "an account that has since been deleted";
|
|
3580
|
+
case "not-recorded":
|
|
3581
|
+
return "not recorded for this close";
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
function outstanding(crossed, p) {
|
|
3585
|
+
switch (crossed.kind) {
|
|
3586
|
+
case "none":
|
|
3587
|
+
return ["- Outstanding work Halfcycle could see: none."];
|
|
3588
|
+
case "not-recorded":
|
|
3589
|
+
return ["- Outstanding work Halfcycle could see: not recorded for this close."];
|
|
3590
|
+
case "clean-over-findings":
|
|
3591
|
+
return [
|
|
3592
|
+
`- Outstanding work Halfcycle could see: ${counted(crossed.findings, "problem", "problems")} recorded against this phase.`
|
|
3593
|
+
];
|
|
3594
|
+
case "unfinished-steps":
|
|
3595
|
+
return [
|
|
3596
|
+
`- Outstanding work Halfcycle could see: ${counted(crossed.steps.length, "step", "steps")} of the method left unfinished. Each is shown by the method's own label for it:`,
|
|
3597
|
+
...crossed.steps.map((step) => ` - Step: ${p.text(step.stepId)} \u2014 ${step.state === "failed" ? "failed" : "set aside unfinished"}`)
|
|
3598
|
+
];
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
function machines(coverage) {
|
|
3602
|
+
const label = "- Machines signed in on this project at the close: ";
|
|
3603
|
+
if (coverage === null)
|
|
3604
|
+
return `${label}not recorded.`;
|
|
3605
|
+
if (coverage.enrolled === 0)
|
|
3606
|
+
return `${label}none.`;
|
|
3607
|
+
return `${label}${String(coverage.enrolled)}, of which ${String(coverage.conformant)} had Halfcycle's checks working as set up.`;
|
|
3608
|
+
}
|
|
3609
|
+
function walkLines(walk, p) {
|
|
3610
|
+
const label = "> - Acceptance walk (the finished work tried as a user would, by someone other than its builder): ";
|
|
3611
|
+
if (walk === null)
|
|
3612
|
+
return [`${label}none reported.`];
|
|
3613
|
+
const lines = [
|
|
3614
|
+
`${label}${walk.verdict === "clean" ? "reported as finding no problems." : "reported as finding problems."}`
|
|
3615
|
+
];
|
|
3616
|
+
walk.findings.forEach((finding, i) => {
|
|
3617
|
+
lines.push(`> - Finding ${String(i + 1)}: ${p.text(finding)}`);
|
|
3618
|
+
});
|
|
3619
|
+
if (walk.findingsNotShown > 0) {
|
|
3620
|
+
lines.push(`> - And ${counted(walk.findingsNotShown, "more finding", "more findings")} not shown here.`);
|
|
3272
3621
|
}
|
|
3273
|
-
return
|
|
3274
|
-
format: CLOSE_RECORD_FORMAT,
|
|
3275
|
-
phase: record2.phase,
|
|
3276
|
-
closeCommit: commit,
|
|
3277
|
-
closedDate: date
|
|
3278
|
-
};
|
|
3622
|
+
return lines;
|
|
3279
3623
|
}
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
// dist/build-record/template.js
|
|
3286
|
-
function severityLabel(seed) {
|
|
3287
|
-
return `\`${seed.patternRef}\` (severity \`${seed.severity}\`)`;
|
|
3288
|
-
}
|
|
3289
|
-
var NOT_RECORDED = "not recorded";
|
|
3290
|
-
function renderCloseLine(record2) {
|
|
3291
|
-
const status = `**Status:** ${record2.status.toUpperCase()}`;
|
|
3292
|
-
const closed = `**Closed:** ${record2.closedDate ?? NOT_RECORDED}`;
|
|
3293
|
-
const commit = record2.closeCommit === null ? `**Close commit:** ${NOT_RECORDED}` : `**Close commit:** \`${record2.closeCommit}\``;
|
|
3294
|
-
return `${status} \xB7 ${closed} \xB7 ${commit}`;
|
|
3295
|
-
}
|
|
3296
|
-
function renderCloseProvenance(record2) {
|
|
3297
|
-
return record2.closeCommit === null && record2.closedDate === null ? "*The close date and the commit were not recorded when this phase closed, so this record does not name them. Nothing has been substituted for them.*" : "*The close date and the commit are the client's own assertion about their own repository, written down when the phase was closed. Halfcycle did not observe them and does not check them.*";
|
|
3298
|
-
}
|
|
3299
|
-
function renderDoneWhenRows(record2) {
|
|
3300
|
-
const rows = [];
|
|
3301
|
-
for (const [key, dw] of Object.entries(record2.acceptance.doneWhen)) {
|
|
3302
|
-
const mark = dw.result === "pass" ? "\u2705" : dw.result === "dropped" ? "\u2014" : dw.result;
|
|
3303
|
-
rows.push(`| ${key} | ${mark} | ${dw.detail} |`);
|
|
3304
|
-
}
|
|
3305
|
-
return rows.join("\n");
|
|
3306
|
-
}
|
|
3307
|
-
function renderBuildRecordMarkdown(record2) {
|
|
3308
|
-
const { phase } = record2;
|
|
3309
|
-
const companion = `${renderPhaseSegment(phase.id)}.json`;
|
|
3310
|
-
const guardsList = record2.seedGuards.map(severityLabel).join(", ");
|
|
3624
|
+
function statedOrNot(value, p) {
|
|
3625
|
+
return value === null ? "Not stated by the client." : p.text(value);
|
|
3626
|
+
}
|
|
3627
|
+
function renderStated(attested, p) {
|
|
3311
3628
|
const lines = [
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
""
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
`**Engagement:** ${record2.engagement} (${record2.engagementType})`,
|
|
3320
|
-
`**Phase:** ${phase.id} \u2014 ${phase.name}`,
|
|
3321
|
-
renderCloseLine(record2),
|
|
3322
|
-
`**Companion:** [\`${companion}\`](${companion})`,
|
|
3323
|
-
"",
|
|
3324
|
-
renderCloseProvenance(record2),
|
|
3325
|
-
"",
|
|
3326
|
-
"## What this phase proved",
|
|
3327
|
-
"",
|
|
3328
|
-
`${record2.thesis} **Thesis held: ${record2.thesisHeld ? "yes" : "no"}.**`,
|
|
3329
|
-
"",
|
|
3330
|
-
"## Delivered",
|
|
3331
|
-
"",
|
|
3332
|
-
`- **Packages:** ${record2.delivered.packages.join(", ")}`,
|
|
3333
|
-
`- **Services:** ${record2.delivered.services.join(", ")}`,
|
|
3334
|
-
`- **Tools:** ${record2.delivered.tools.join(", ")}`,
|
|
3335
|
-
`- **Dogfood:** ${record2.delivered.dogfood}`,
|
|
3336
|
-
"",
|
|
3337
|
-
guardsList ? `Guards evaluated (results only): ${guardsList}. *(What a guard reported about this phase is recorded; the guard's own definition and its history are not.)*` : "No guards fired during this phase.",
|
|
3338
|
-
"",
|
|
3339
|
-
`## Acceptance (independent walk \u2014 walker ${record2.acceptance.walker})`,
|
|
3340
|
-
"",
|
|
3341
|
-
`**Verdict:** ${record2.acceptance.verdict} \xB7 **Findings:** ${record2.acceptance.findings}`,
|
|
3342
|
-
"",
|
|
3343
|
-
"| Done-when | Result | Evidence |",
|
|
3344
|
-
"|---|---|---|",
|
|
3345
|
-
renderDoneWhenRows(record2),
|
|
3346
|
-
""
|
|
3629
|
+
attested.decision === "done-when-met" ? "> - Decision: the phase's agreed finish conditions were met." : "> - Decision: close the phase by override, without stating its finish conditions were met.",
|
|
3630
|
+
`> - Name given: ${attested.actor === null ? "removed with the account that made this close" : p.text(attested.actor)}`,
|
|
3631
|
+
`> - Reason given: ${attested.reason === null ? "none." : p.text(attested.reason)}`,
|
|
3632
|
+
...walkLines(attested.walk, p),
|
|
3633
|
+
`> - Code closed at (commit): ${statedOrNot(attested.closeCommit, p)}`,
|
|
3634
|
+
`> - What the phase set out to prove: ${statedOrNot(attested.statements.thesis, p)}`,
|
|
3635
|
+
`> - What came out of it: ${statedOrNot(attested.statements.outcome, p)}`
|
|
3347
3636
|
];
|
|
3348
|
-
|
|
3349
|
-
|
|
3637
|
+
const deviations = attested.statements.deviations;
|
|
3638
|
+
if (deviations.length === 0) {
|
|
3639
|
+
lines.push("> - Changes from the plan: None stated by the client.");
|
|
3640
|
+
} else {
|
|
3641
|
+
lines.push("> - Changes from the plan:");
|
|
3642
|
+
deviations.forEach((deviation, i) => {
|
|
3643
|
+
lines.push(`> - Change ${String(i + 1)}: ${p.text(deviation)}`);
|
|
3644
|
+
});
|
|
3350
3645
|
}
|
|
3351
|
-
lines.push("## Gates that earned their keep", "", `**${record2.gatesEarnedKeep.defectsCaughtPreHuman}** defects were caught by the gates before the human walk; the walk itself found **${record2.gatesEarnedKeep.bugsReachingHumanWalk}**. Named: ${record2.gatesEarnedKeep.named.join("; ")}.`, "", "## Instruments", "", `- **Marginal-cost self-accounting:** ${record2.instruments.marginalCostSelfAccounting}`, `- **COE add-rate:** seam-new ${record2.instruments.coeAddRate.seamNew}, seam-repeat ${record2.instruments.coeAddRate.seamRepeat}, model-limitation ${record2.instruments.coeAddRate.modelLimitation}. ${record2.instruments.coeAddRate.notes}`, "", "## Invariants exercised", "", record2.invariantsExercised.join(", "), "", "## Deviations & decisions recorded", "", ...record2.deviations.map((d) => `- ${d}`), "", "## Tasks", "", `- **Planned:** ${record2.tasks.planned} \xB7 **Landed:** ${record2.tasks.landed} \xB7 **Cancelled:** ${record2.tasks.cancelled.length > 0 ? record2.tasks.cancelled.join(", ") : "none"}`, "");
|
|
3352
3646
|
return lines.join("\n");
|
|
3353
3647
|
}
|
|
3648
|
+
function renderClose(heading, observed, attested, p) {
|
|
3649
|
+
return [
|
|
3650
|
+
heading,
|
|
3651
|
+
headline(observed, attested),
|
|
3652
|
+
OBSERVED_LABEL,
|
|
3653
|
+
[
|
|
3654
|
+
`- Closed at: ${closedAtText(observed.closedAt, p)}`,
|
|
3655
|
+
`- Signed in as: ${signedInAs(observed.closedBy, p)}`,
|
|
3656
|
+
...outstanding(observed.crossed, p),
|
|
3657
|
+
machines(observed.seatCoverage)
|
|
3658
|
+
].join("\n"),
|
|
3659
|
+
STATED_LABEL,
|
|
3660
|
+
renderStated(attested, p)
|
|
3661
|
+
];
|
|
3662
|
+
}
|
|
3663
|
+
function renderBuildRecordV2Markdown(record2) {
|
|
3664
|
+
const p = makePrinter();
|
|
3665
|
+
const attestedBySequence = /* @__PURE__ */ new Map();
|
|
3666
|
+
for (const close of record2.attested.closes)
|
|
3667
|
+
attestedBySequence.set(close.sequence, close);
|
|
3668
|
+
const observedSequences = new Set(record2.observed.closes.map((c) => c.sequence));
|
|
3669
|
+
if (attestedBySequence.size !== record2.attested.closes.length || observedSequences.size !== record2.observed.closes.length || attestedBySequence.size !== observedSequences.size || [...observedSequences].some((s) => !attestedBySequence.has(s))) {
|
|
3670
|
+
throw new Error(MISMATCHED_HALVES);
|
|
3671
|
+
}
|
|
3672
|
+
const phase = p.text(record2.phase);
|
|
3673
|
+
const closes = [...record2.observed.closes].sort((a, b) => a.sequence - b.sequence);
|
|
3674
|
+
const total = closes.length;
|
|
3675
|
+
const latest = closes[total - 1];
|
|
3676
|
+
const body = [
|
|
3677
|
+
`Phase: ${phase} (the name the client gave it)`,
|
|
3678
|
+
OBSERVED_LABEL,
|
|
3679
|
+
[
|
|
3680
|
+
`- Method version: ${p.text(record2.observed.methodVersion)}. This is the version of the Halfcycle method the project was run under; it does not change during a project.`,
|
|
3681
|
+
`- Halfcycle's reference for this project: ${p.text(record2.observed.engagementId)}`,
|
|
3682
|
+
`- Record format: ${p.text(record2.format)}`
|
|
3683
|
+
].join("\n"),
|
|
3684
|
+
total === 1 ? "This phase was closed once." : `This phase was closed ${String(total)} times. The latest close is described first; each earlier close follows, as it was recorded when it was made.`,
|
|
3685
|
+
...renderChecks(record2.observed.checks, p),
|
|
3686
|
+
...renderClose(total === 1 ? "## The close" : `## The latest close (close ${String(latest.sequence)} of ${String(total)})`, latest, attestedBySequence.get(latest.sequence), p)
|
|
3687
|
+
];
|
|
3688
|
+
if (total > 1) {
|
|
3689
|
+
body.push("## Earlier closes");
|
|
3690
|
+
for (const close of closes.slice(0, -1)) {
|
|
3691
|
+
body.push(...renderClose(`### Close ${String(close.sequence)} of ${String(total)}`, close, attestedBySequence.get(close.sequence), p));
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
const header = [`${TITLE_PREFIX}${phase}`, HEADER_WHAT, HEADER_REGISTERS, HEADER_KEPT];
|
|
3695
|
+
if (p.sawHidden())
|
|
3696
|
+
header.push(HEADER_HIDDEN);
|
|
3697
|
+
return `${[...header, ...body].join("\n\n")}
|
|
3698
|
+
`;
|
|
3699
|
+
}
|
|
3354
3700
|
|
|
3355
3701
|
// dist/build-record/write.js
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
}
|
|
3702
|
+
import { closeSync, existsSync as existsSync6, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync7, renameSync, rmSync as rmSync2, writeSync } from "node:fs";
|
|
3703
|
+
import { basename, join as join8, resolve, relative as relative2, isAbsolute as isAbsolute2 } from "node:path";
|
|
3704
|
+
var BUILD_RECORD_DIR = join8("docs", "build-records");
|
|
3360
3705
|
function assertInsideOutDir(outDir, candidate, identity) {
|
|
3361
3706
|
const rel = relative2(outDir, resolve(candidate));
|
|
3362
|
-
const isPlainFilename = rel !== "" && rel !== ".." && !
|
|
3707
|
+
const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute2(rel) && rel === basename(rel);
|
|
3363
3708
|
if (!isPlainFilename) {
|
|
3364
3709
|
throw new InvalidPhaseIdentityError(identity, BUILD_RECORD_DIR);
|
|
3365
3710
|
}
|
|
3366
3711
|
}
|
|
3367
|
-
function
|
|
3368
|
-
const
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3712
|
+
function createExclusiveOnDisk(path, text) {
|
|
3713
|
+
const fd = openSync(path, "wx");
|
|
3714
|
+
try {
|
|
3715
|
+
writeSync(fd, text);
|
|
3716
|
+
} catch (err) {
|
|
3717
|
+
closeSync(fd);
|
|
3718
|
+
rmSync2(path, { force: true });
|
|
3719
|
+
throw err;
|
|
3372
3720
|
}
|
|
3373
|
-
|
|
3374
|
-
const jsonPath = join9(outDir, `${segment}.json`);
|
|
3375
|
-
const mdPath = join9(outDir, `${segment}.md`);
|
|
3376
|
-
assertInsideOutDir(outDir, jsonPath, identity);
|
|
3377
|
-
assertInsideOutDir(outDir, mdPath, identity);
|
|
3378
|
-
mkdirSync6(outDir, { recursive: true });
|
|
3379
|
-
writeFileSync6(jsonPath, serialiseRecordJson(record2), "utf-8");
|
|
3380
|
-
writeFileSync6(mdPath, renderBuildRecordMarkdown(record2), "utf-8");
|
|
3381
|
-
return { jsonPath, mdPath };
|
|
3721
|
+
closeSync(fd);
|
|
3382
3722
|
}
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
}
|
|
3723
|
+
function renameOnDisk(from, to) {
|
|
3724
|
+
renameSync(from, to);
|
|
3725
|
+
}
|
|
3726
|
+
var NODE_RECORD_PAIR_IO = {
|
|
3727
|
+
createExclusive: createExclusiveOnDisk,
|
|
3728
|
+
rename: renameOnDisk
|
|
3390
3729
|
};
|
|
3391
|
-
function
|
|
3392
|
-
|
|
3393
|
-
for (const run of runs) {
|
|
3394
|
-
if (!run.result)
|
|
3395
|
-
continue;
|
|
3396
|
-
for (const fired of run.result.guardsFired) {
|
|
3397
|
-
const seed = projectSeedGuard(
|
|
3398
|
-
fired,
|
|
3399
|
-
/* includeExplanation */
|
|
3400
|
-
false
|
|
3401
|
-
);
|
|
3402
|
-
const existing = byRef.get(seed.patternRef);
|
|
3403
|
-
if (!existing) {
|
|
3404
|
-
byRef.set(seed.patternRef, seed);
|
|
3405
|
-
} else if (existing.severity !== seed.severity) {
|
|
3406
|
-
byRef.set(seed.patternRef, pickHigherSeverity(existing, seed));
|
|
3407
|
-
}
|
|
3408
|
-
}
|
|
3409
|
-
}
|
|
3410
|
-
return [...byRef.values()].sort((a, b) => a.patternRef.localeCompare(b.patternRef));
|
|
3730
|
+
function serialiseServedRecord(rawRecord) {
|
|
3731
|
+
return JSON.stringify(rawRecord, null, 2) + "\n";
|
|
3411
3732
|
}
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
return (SEVERITY_ORDINAL[a.severity] ?? -1) >= (SEVERITY_ORDINAL[b.severity] ?? -1) ? a : b;
|
|
3733
|
+
function isEexist(err) {
|
|
3734
|
+
return typeof err === "object" && err !== null && err.code === "EEXIST";
|
|
3415
3735
|
}
|
|
3416
|
-
function
|
|
3417
|
-
|
|
3418
|
-
return
|
|
3736
|
+
function readIfPresent(path) {
|
|
3737
|
+
try {
|
|
3738
|
+
return readFileSync7(path, "utf-8");
|
|
3739
|
+
} catch {
|
|
3740
|
+
return null;
|
|
3419
3741
|
}
|
|
3420
|
-
const record2 = parsed;
|
|
3421
|
-
if (record2["outcome"] !== void 0)
|
|
3422
|
-
return parsed;
|
|
3423
|
-
return {
|
|
3424
|
-
...record2,
|
|
3425
|
-
runId: record2["runId"] ?? syntheticRunId(record2),
|
|
3426
|
-
outcome: "evaluated"
|
|
3427
|
-
};
|
|
3428
3742
|
}
|
|
3429
|
-
function
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
].join("|");
|
|
3436
|
-
const hex = createHash3("sha256").update(`legacy-guard-eval-run:${key}`).digest("hex");
|
|
3437
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
3438
|
-
}
|
|
3439
|
-
function readGuardEvalLog(logDir, phaseId) {
|
|
3440
|
-
const logPath = join10(logDir, `${renderPhaseSegment(phaseId)}.jsonl`);
|
|
3441
|
-
if (!existsSync6(logPath)) {
|
|
3442
|
-
throw new MissingSourceError("guard-eval log", `no runner guard-eval log at ${logPath} for phase ${phaseId}`);
|
|
3443
|
-
}
|
|
3444
|
-
const raw = readFileSync7(logPath, "utf-8");
|
|
3445
|
-
const runs = [];
|
|
3446
|
-
for (const line of raw.split("\n")) {
|
|
3447
|
-
const trimmed = line.trim();
|
|
3448
|
-
if (trimmed === "")
|
|
3449
|
-
continue;
|
|
3450
|
-
const parsed = JSON.parse(trimmed);
|
|
3451
|
-
runs.push(guardEvalRunSchema.parse(normalizeLegacyLine(parsed)));
|
|
3743
|
+
function isOwnEarlierRecord(existingText, served) {
|
|
3744
|
+
let parsedJson;
|
|
3745
|
+
try {
|
|
3746
|
+
parsedJson = JSON.parse(existingText);
|
|
3747
|
+
} catch {
|
|
3748
|
+
return false;
|
|
3452
3749
|
}
|
|
3453
|
-
|
|
3750
|
+
const existing = buildRecordV2Schema.safeParse(parsedJson);
|
|
3751
|
+
if (!existing.success)
|
|
3752
|
+
return false;
|
|
3753
|
+
const old = existing.data;
|
|
3754
|
+
if (old.observed.engagementId !== served.observed.engagementId)
|
|
3755
|
+
return false;
|
|
3756
|
+
if (old.phase !== served.phase)
|
|
3757
|
+
return false;
|
|
3758
|
+
const oldCloses = old.observed.closes.length;
|
|
3759
|
+
const newCloses = served.observed.closes.length;
|
|
3760
|
+
if (oldCloses < newCloses)
|
|
3761
|
+
return true;
|
|
3762
|
+
if (oldCloses > newCloses)
|
|
3763
|
+
return false;
|
|
3764
|
+
const oldSequences = new Set(old.observed.closes.map((c) => c.sequence));
|
|
3765
|
+
return served.observed.closes.every((c) => oldSequences.has(c.sequence));
|
|
3454
3766
|
}
|
|
3455
|
-
function
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
throw new MissingSourceError("orchestration state", `no orchestration-state file at ${statePath} for phase ${phaseId}`);
|
|
3767
|
+
function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_PAIR_IO) {
|
|
3768
|
+
if (!isValidPhaseIdentity(phase)) {
|
|
3769
|
+
throw new InvalidPhaseIdentityError(phase, BUILD_RECORD_DIR);
|
|
3459
3770
|
}
|
|
3460
|
-
const
|
|
3461
|
-
|
|
3462
|
-
|
|
3771
|
+
const outDir = resolve(repoRoot, BUILD_RECORD_DIR);
|
|
3772
|
+
const segment = renderPhaseSegment(phase);
|
|
3773
|
+
const jsonPath = join8(outDir, `${segment}.json`);
|
|
3774
|
+
const mdPath = join8(outDir, `${segment}.md`);
|
|
3775
|
+
const jsonTmp = `${jsonPath}.tmp`;
|
|
3776
|
+
const mdTmp = `${mdPath}.tmp`;
|
|
3777
|
+
for (const candidate of [jsonPath, mdPath, jsonTmp, mdTmp]) {
|
|
3778
|
+
assertInsideOutDir(outDir, candidate, phase);
|
|
3779
|
+
}
|
|
3780
|
+
const jsonText = serialiseServedRecord(rawRecord);
|
|
3781
|
+
const mdText = renderBuildRecordV2Markdown(record2);
|
|
3782
|
+
mkdirSync5(outDir, { recursive: true });
|
|
3783
|
+
let createdJson = false;
|
|
3784
|
+
try {
|
|
3785
|
+
io.createExclusive(jsonPath, jsonText);
|
|
3786
|
+
createdJson = true;
|
|
3787
|
+
io.createExclusive(mdPath, mdText);
|
|
3788
|
+
return { kind: "created", jsonPath, mdPath };
|
|
3789
|
+
} catch (err) {
|
|
3790
|
+
if (createdJson)
|
|
3791
|
+
rmSync2(jsonPath, { force: true });
|
|
3792
|
+
if (!isEexist(err))
|
|
3793
|
+
throw err;
|
|
3463
3794
|
}
|
|
3464
|
-
const
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
throw new MissingSourceError("orchestration state", `task ${id} has no status`);
|
|
3468
|
-
}
|
|
3469
|
-
tasks[id] = { status: t.status };
|
|
3795
|
+
const existingJson = readIfPresent(jsonPath);
|
|
3796
|
+
if (existingJson === null) {
|
|
3797
|
+
return { kind: "in-the-way", path: existsSync6(jsonPath) ? jsonPath : mdPath };
|
|
3470
3798
|
}
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
if (!
|
|
3476
|
-
return {
|
|
3477
|
-
|
|
3799
|
+
if (existingJson === jsonText) {
|
|
3800
|
+
replaceOne(io, mdTmp, mdPath, mdText);
|
|
3801
|
+
return { kind: "up-to-date", jsonPath, mdPath };
|
|
3802
|
+
}
|
|
3803
|
+
if (!isOwnEarlierRecord(existingJson, record2)) {
|
|
3804
|
+
return { kind: "in-the-way", path: jsonPath };
|
|
3805
|
+
}
|
|
3806
|
+
replaceOne(io, jsonTmp, jsonPath, jsonText);
|
|
3478
3807
|
try {
|
|
3479
|
-
|
|
3808
|
+
replaceOne(io, mdTmp, mdPath, mdText);
|
|
3480
3809
|
} catch (err) {
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3810
|
+
try {
|
|
3811
|
+
replaceOne(io, jsonTmp, jsonPath, existingJson);
|
|
3812
|
+
} catch {
|
|
3813
|
+
rmSync2(jsonPath, { force: true });
|
|
3814
|
+
}
|
|
3815
|
+
throw err;
|
|
3486
3816
|
}
|
|
3487
|
-
return {
|
|
3817
|
+
return { kind: "replaced", jsonPath, mdPath };
|
|
3488
3818
|
}
|
|
3489
|
-
function
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
}
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
req(narrated.phase?.name, "phase.name");
|
|
3498
|
-
assertValidPhaseIdentity(narrated.phase?.id, BUILD_RECORD_DIR);
|
|
3499
|
-
req(narrated.status, "status");
|
|
3500
|
-
req(narrated.thesis, "thesis");
|
|
3501
|
-
req(narrated.delivered, "delivered");
|
|
3502
|
-
req(narrated.acceptance, "acceptance");
|
|
3503
|
-
req(narrated.acceptance?.verdict, "acceptance.verdict");
|
|
3504
|
-
req(narrated.gatesEarnedKeep, "gatesEarnedKeep");
|
|
3505
|
-
req(narrated.instruments, "instruments");
|
|
3506
|
-
if (!Array.isArray(narrated.deviations)) {
|
|
3507
|
-
throw new MissingSourceError("narrated artefact", "deviations must be an array");
|
|
3508
|
-
}
|
|
3509
|
-
return narrated;
|
|
3510
|
-
}
|
|
3511
|
-
function loadSources(opts) {
|
|
3512
|
-
const orchestration = readOrchestrationState(opts.phasesDir, opts.phaseId);
|
|
3513
|
-
const guardEvalRuns = readGuardEvalLog(opts.guardEvalLogDir, opts.phaseId);
|
|
3514
|
-
const close = readPhaseClose(opts.repoRoot, opts.phaseId);
|
|
3515
|
-
const narrated = validateNarrated(opts.narrated);
|
|
3516
|
-
if (!Array.isArray(opts.touchedInvariants)) {
|
|
3517
|
-
throw new MissingSourceError("feature-spec Touches", "touchedInvariants must be an array");
|
|
3819
|
+
function replaceOne(io, tmp, finalPath, text) {
|
|
3820
|
+
rmSync2(tmp, { force: true });
|
|
3821
|
+
try {
|
|
3822
|
+
io.createExclusive(tmp, text);
|
|
3823
|
+
io.rename(tmp, finalPath);
|
|
3824
|
+
} catch (err) {
|
|
3825
|
+
rmSync2(tmp, { force: true });
|
|
3826
|
+
throw err;
|
|
3518
3827
|
}
|
|
3519
|
-
return {
|
|
3520
|
-
orchestration,
|
|
3521
|
-
narrated,
|
|
3522
|
-
close,
|
|
3523
|
-
guardEvalRuns,
|
|
3524
|
-
touchedInvariants: opts.touchedInvariants
|
|
3525
|
-
};
|
|
3526
3828
|
}
|
|
3527
3829
|
|
|
3528
|
-
// dist/build-record/
|
|
3529
|
-
|
|
3530
|
-
var
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
}
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
for (const dw of Object.values(acceptance.doneWhen)) {
|
|
3541
|
-
if (dw.result === "dropped")
|
|
3542
|
-
continue;
|
|
3543
|
-
if (dw.result !== "pass")
|
|
3544
|
-
return false;
|
|
3545
|
-
}
|
|
3546
|
-
return true;
|
|
3830
|
+
// dist/build-record/obtain.js
|
|
3831
|
+
import { relative as relative3 } from "node:path";
|
|
3832
|
+
var REFUSAL_FOLLOW_UP = {
|
|
3833
|
+
"still-open": "Nothing was written. The record is produced when the phase closes.",
|
|
3834
|
+
"reopened": "Nothing was written. A new record is produced when the phase closes again.",
|
|
3835
|
+
"never-opened": "Nothing was written. Check the name: it must match the phase name exactly as it was opened.",
|
|
3836
|
+
"never-closed": "Nothing was written.",
|
|
3837
|
+
"closed-before-records": "Nothing was written."
|
|
3838
|
+
};
|
|
3839
|
+
var REFUSAL_REASONS = phaseRecordRefusalReasonSchema.options;
|
|
3840
|
+
function buildRecordUrl(credential, phase) {
|
|
3841
|
+
return `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/phases/${encodeURIComponent(phase)}/build-record`;
|
|
3547
3842
|
}
|
|
3548
|
-
function
|
|
3549
|
-
|
|
3843
|
+
function isMissingRoute(status, body) {
|
|
3844
|
+
if (status !== 404 || typeof body !== "object" || body === null)
|
|
3845
|
+
return false;
|
|
3846
|
+
const { error, message } = body;
|
|
3847
|
+
return error === "Not Found" && typeof message === "string" && /^Route GET .* not found\.$/s.test(message);
|
|
3550
3848
|
}
|
|
3551
|
-
function
|
|
3552
|
-
const
|
|
3849
|
+
async function obtainBuildRecord(credential, repoRoot, phase, io = NODE_RECORD_PAIR_IO) {
|
|
3850
|
+
const url = buildRecordUrl(credential, phase);
|
|
3851
|
+
let res;
|
|
3852
|
+
let text;
|
|
3853
|
+
try {
|
|
3854
|
+
res = await fetch(url, {
|
|
3855
|
+
method: "GET",
|
|
3856
|
+
headers: { authorization: `Bearer ${credential.token}` }
|
|
3857
|
+
});
|
|
3858
|
+
text = await res.text();
|
|
3859
|
+
} catch (err) {
|
|
3860
|
+
return {
|
|
3861
|
+
kind: "unavailable",
|
|
3862
|
+
why: `could not reach the Halfcycle service at ${url}: ${err instanceof Error ? err.message : String(err)}.`
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
3865
|
+
let raw = null;
|
|
3866
|
+
try {
|
|
3867
|
+
raw = JSON.parse(text);
|
|
3868
|
+
} catch {
|
|
3869
|
+
raw = null;
|
|
3870
|
+
}
|
|
3871
|
+
const parsed = phaseRecordResponseSchema.safeParse(raw);
|
|
3872
|
+
if (parsed.success) {
|
|
3873
|
+
const answer = parsed.data;
|
|
3874
|
+
if (answer.phase !== phase) {
|
|
3875
|
+
return {
|
|
3876
|
+
kind: "unavailable",
|
|
3877
|
+
why: `the service answered for a different phase than the one asked for, so nothing was written.`
|
|
3878
|
+
};
|
|
3879
|
+
}
|
|
3880
|
+
if (answer.outcome === "withheld")
|
|
3881
|
+
return { kind: "withheld", message: answer.message };
|
|
3882
|
+
if (answer.outcome === "refused") {
|
|
3883
|
+
return { kind: "refused", reason: answer.reason, message: answer.message };
|
|
3884
|
+
}
|
|
3885
|
+
const rawRecord = raw.record;
|
|
3886
|
+
let result;
|
|
3887
|
+
try {
|
|
3888
|
+
result = writeRecordPair(repoRoot, phase, rawRecord, answer.record, io);
|
|
3889
|
+
} catch (err) {
|
|
3890
|
+
return {
|
|
3891
|
+
kind: "unavailable",
|
|
3892
|
+
why: `the record arrived but could not be written: ${err instanceof Error ? err.message : String(err)}. Nothing was left half written.`
|
|
3893
|
+
};
|
|
3894
|
+
}
|
|
3895
|
+
if (result.kind === "in-the-way") {
|
|
3896
|
+
return { kind: "exists", path: relative3(repoRoot, result.path) };
|
|
3897
|
+
}
|
|
3898
|
+
return {
|
|
3899
|
+
kind: "written",
|
|
3900
|
+
jsonPath: relative3(repoRoot, result.jsonPath),
|
|
3901
|
+
mdPath: relative3(repoRoot, result.mdPath)
|
|
3902
|
+
};
|
|
3903
|
+
}
|
|
3904
|
+
if (isMissingRoute(res.status, raw)) {
|
|
3905
|
+
return { kind: "unavailable", why: "this Halfcycle service does not provide Build Records yet." };
|
|
3906
|
+
}
|
|
3907
|
+
const message = typeof raw === "object" && raw !== null && typeof raw.message === "string" ? raw.message : null;
|
|
3908
|
+
if (res.ok) {
|
|
3909
|
+
return {
|
|
3910
|
+
kind: "unavailable",
|
|
3911
|
+
why: `the service's answer was not a Build Record this version of the CLI can read, so nothing was written.`
|
|
3912
|
+
};
|
|
3913
|
+
}
|
|
3553
3914
|
return {
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
engagementType: narrated.engagementType,
|
|
3557
|
-
phase: { id: narrated.phase.id, name: narrated.phase.name },
|
|
3558
|
-
status: narrated.status,
|
|
3559
|
-
closedDate: close.closedDate,
|
|
3560
|
-
closeCommit: close.closeCommit,
|
|
3561
|
-
thesis: narrated.thesis,
|
|
3562
|
-
thesisHeld: deriveThesisHeld(narrated.acceptance),
|
|
3563
|
-
delivered: narrated.delivered,
|
|
3564
|
-
seedGuards: projectSeedGuards(sources.guardEvalRuns),
|
|
3565
|
-
acceptance: narrated.acceptance,
|
|
3566
|
-
gatesEarnedKeep: narrated.gatesEarnedKeep,
|
|
3567
|
-
instruments: narrated.instruments,
|
|
3568
|
-
invariantsExercised: deriveInvariantsExercised(sources.touchedInvariants),
|
|
3569
|
-
deviations: narrated.deviations,
|
|
3570
|
-
tasks: deriveTasks(orchestration)
|
|
3915
|
+
kind: "unavailable",
|
|
3916
|
+
why: `the service answered ${res.status}${message === null ? "." : `: ${message}`}`
|
|
3571
3917
|
};
|
|
3572
3918
|
}
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
const sources = loadSources({
|
|
3577
|
-
repoRoot: opts.repoRoot,
|
|
3578
|
-
phasesDir: opts.phasesDir,
|
|
3579
|
-
guardEvalLogDir: opts.guardEvalLogDir,
|
|
3580
|
-
phaseId: opts.phaseId,
|
|
3581
|
-
narrated: opts.narrated,
|
|
3582
|
-
touchedInvariants: opts.touchedInvariants
|
|
3583
|
-
});
|
|
3584
|
-
const record2 = assembleBuildRecord(sources);
|
|
3585
|
-
return writeBuildRecord(opts.repoRoot, record2);
|
|
3919
|
+
function refetchCommand(phase) {
|
|
3920
|
+
const quoted = /^[A-Za-z0-9._+-]+$/.test(phase) ? phase : `'${phase.replace(/'/g, `'\\''`)}'`;
|
|
3921
|
+
return `npx halfcycle@latest build-record ${quoted}`;
|
|
3586
3922
|
}
|
|
3587
3923
|
export {
|
|
3588
3924
|
BUILD_RECORD_DIR,
|
|
3589
|
-
BUILD_RECORD_FORMAT,
|
|
3590
|
-
BUILD_RECORD_ZONE_B_DIR,
|
|
3591
|
-
CLOSE_RECORD_FORMAT,
|
|
3592
3925
|
CreateEngagementRefused,
|
|
3593
3926
|
FLAT_FILE_TRACKER_HOME,
|
|
3594
3927
|
HALFCYCLE_STATE_FORMAT,
|
|
3595
|
-
InvalidCloseRecordError,
|
|
3596
3928
|
InvalidPhaseIdentityError,
|
|
3597
|
-
|
|
3929
|
+
NODE_RECORD_PAIR_IO,
|
|
3930
|
+
REFUSAL_FOLLOW_UP,
|
|
3931
|
+
REFUSAL_REASONS,
|
|
3598
3932
|
SetupManifest,
|
|
3599
3933
|
ZONE_B_DIR,
|
|
3600
3934
|
agentsPointerRow,
|
|
3601
3935
|
applySetupWrites,
|
|
3602
|
-
assembleBuildRecord,
|
|
3603
|
-
assemblePhaseBuildRecord,
|
|
3604
3936
|
assertValidPhaseIdentity,
|
|
3605
3937
|
buildEngagementRecord,
|
|
3938
|
+
buildRecordUrl,
|
|
3606
3939
|
checkDrift,
|
|
3607
|
-
closeRecordPath,
|
|
3608
3940
|
contextIndexRow,
|
|
3609
3941
|
createEngagement,
|
|
3610
|
-
deriveInvariantsExercised,
|
|
3611
|
-
deriveTasks,
|
|
3612
|
-
deriveThesisHeld,
|
|
3613
3942
|
discoverRepository,
|
|
3614
3943
|
documentSectionRow,
|
|
3615
3944
|
engagementRecordRow,
|
|
@@ -3617,31 +3946,24 @@ export {
|
|
|
3617
3946
|
install,
|
|
3618
3947
|
isValidPhaseIdentity,
|
|
3619
3948
|
layerDocumentRow,
|
|
3620
|
-
loadSources,
|
|
3621
3949
|
mergeSettings,
|
|
3622
3950
|
mergeWithRecords,
|
|
3623
3951
|
mintBoardEnterCode,
|
|
3624
3952
|
mintOrReadIdentity,
|
|
3625
|
-
|
|
3626
|
-
projectSeedGuard,
|
|
3627
|
-
projectSeedGuards,
|
|
3953
|
+
obtainBuildRecord,
|
|
3628
3954
|
questionsToPut,
|
|
3629
3955
|
readBundlePin,
|
|
3630
|
-
readGuardEvalLog,
|
|
3631
|
-
readOrchestrationState,
|
|
3632
|
-
readPhaseClose,
|
|
3633
3956
|
readRemote,
|
|
3634
3957
|
readRootCommit,
|
|
3958
|
+
refetchCommand,
|
|
3635
3959
|
renderAnchor,
|
|
3636
|
-
|
|
3960
|
+
renderBuildRecordV2Markdown,
|
|
3637
3961
|
renderDiscovery,
|
|
3638
3962
|
renderPhaseSegment,
|
|
3639
3963
|
runBootstrapScan,
|
|
3640
3964
|
scanLayers,
|
|
3641
|
-
|
|
3965
|
+
serialiseServedRecord,
|
|
3642
3966
|
trackerHomeRow,
|
|
3643
|
-
|
|
3644
|
-
writeBuildRecord,
|
|
3645
|
-
writeCloseRecord
|
|
3967
|
+
writeRecordPair
|
|
3646
3968
|
};
|
|
3647
3969
|
//# sourceMappingURL=index.js.map
|