halfcycle 0.3.32 → 0.3.34
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 +45 -25
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +1152 -788
- 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/index.js +804 -571
- package/dist/index.js.map +3 -3
- 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
|
@@ -178,6 +178,31 @@ var HALFCYCLE_HOOK_WIRING_PATHS = [
|
|
|
178
178
|
.../* @__PURE__ */ new Set([SETTINGS_REL, ...HALFCYCLE_HOOK_WIRING.map((entry) => entry.script), VENDORED_BIN_REL])
|
|
179
179
|
];
|
|
180
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
|
+
|
|
181
206
|
// ../events/dist/result.js
|
|
182
207
|
import { z as z2 } from "zod";
|
|
183
208
|
var firedGuardSchema = z2.object({
|
|
@@ -237,74 +262,28 @@ var wireErrorSchema = z2.object({
|
|
|
237
262
|
message: z2.string()
|
|
238
263
|
}).strict();
|
|
239
264
|
|
|
240
|
-
// ../events/dist/
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
"contract-error",
|
|
247
|
-
"unconfigured"
|
|
248
|
-
]);
|
|
249
|
-
var guardEvalRunSchema = z3.object({
|
|
250
|
-
// `runId`/`outcome` are the E2 run-observability additions, declared
|
|
251
|
-
// field-level-OPTIONAL as the FIRST (expand) step of an expand-contract
|
|
252
|
-
// migration. Every new record populated by the migrated writer (T-07)
|
|
253
|
-
// carries both; a legacy line predating the amendment carries neither and
|
|
254
|
-
// must still parse (the mixed-era guard-eval log the Build-Record reader
|
|
255
|
-
// assembles from, and the runner emit not yet migrated). So the presence
|
|
256
|
-
// refinement below is skipped for a legacy line (no `outcome`), and the
|
|
257
|
-
// per-writer guarantee lives with the writer + reader-normalisation (T-07),
|
|
258
|
-
// where the schema may then be tightened to required. runId is uuid-checked
|
|
259
|
-
// WHEN present; outcome is enum-checked when present.
|
|
260
|
-
runId: z3.string().uuid().optional(),
|
|
261
|
-
engagementId: z3.string().optional(),
|
|
262
|
-
runType: z3.enum(["hook", "ci"]),
|
|
263
|
-
occurredAt: utcIso8601,
|
|
264
|
-
outcome: guardEvalOutcomeSchema.optional(),
|
|
265
|
-
failureReason: z3.string().optional(),
|
|
266
|
-
phase: z3.string().optional(),
|
|
267
|
-
scope: z3.record(z3.string(), z3.unknown()),
|
|
268
|
-
cost: z3.record(z3.string(), z3.unknown()).optional(),
|
|
269
|
-
result: resultEnvelopeSchema.optional()
|
|
270
|
-
}).strict().superRefine((run, ctx) => {
|
|
271
|
-
if (run.outcome === void 0)
|
|
272
|
-
return;
|
|
273
|
-
if (run.outcome === "evaluated" && run.result === void 0) {
|
|
274
|
-
ctx.addIssue({
|
|
275
|
-
code: z3.ZodIssueCode.custom,
|
|
276
|
-
path: ["result"],
|
|
277
|
-
message: "result is required when outcome is 'evaluated'"
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
if (run.outcome !== "evaluated" && run.result !== void 0) {
|
|
281
|
-
ctx.addIssue({
|
|
282
|
-
code: z3.ZodIssueCode.custom,
|
|
283
|
-
path: ["result"],
|
|
284
|
-
message: "result is only present when outcome is 'evaluated'"
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
if (run.outcome !== "unconfigured" && run.engagementId === void 0) {
|
|
288
|
-
ctx.addIssue({
|
|
289
|
-
code: z3.ZodIssueCode.custom,
|
|
290
|
-
path: ["engagementId"],
|
|
291
|
-
message: "engagementId is required unless outcome is 'unconfigured'"
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
});
|
|
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";
|
|
295
271
|
|
|
296
|
-
// ../events/dist/
|
|
272
|
+
// ../events/dist/build-record.js
|
|
297
273
|
import { z as z4 } from "zod";
|
|
298
|
-
|
|
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");
|
|
299
278
|
var STATE_RECORD_FORMAT = "halfcycle-state-record/v1";
|
|
300
|
-
var stepIdSchema =
|
|
301
|
-
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+$/);
|
|
302
281
|
var anchorPathRegex = /^(?!\/)(?!.*#)(?!method\/)(?!(.*\/)?docs\/method\/).+$/;
|
|
303
|
-
var recordKindSchema =
|
|
304
|
-
var methodLayerSchema =
|
|
305
|
-
var evidenceKindSchema =
|
|
306
|
-
var declarableEvidenceKindSchema =
|
|
307
|
-
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([
|
|
308
287
|
"prevented",
|
|
309
288
|
"corrected-in-spec",
|
|
310
289
|
"corrected-in-scope",
|
|
@@ -314,20 +293,20 @@ var dispositionSchema = z4.enum([
|
|
|
314
293
|
"surfaced",
|
|
315
294
|
"filed-forward"
|
|
316
295
|
]);
|
|
317
|
-
var mechanismSchema =
|
|
318
|
-
var artefactStateSchema =
|
|
296
|
+
var mechanismSchema = z3.enum(["guard", "verdict", "served-step", "method-step"]);
|
|
297
|
+
var artefactStateSchema = z3.enum(["created", "updated"]);
|
|
319
298
|
var QUALIFIED_OUTCOMES = ["declined", "parked"];
|
|
320
|
-
var qualifiedOutcomeSchema =
|
|
299
|
+
var qualifiedOutcomeSchema = z3.enum(QUALIFIED_OUTCOMES);
|
|
321
300
|
function isQualifiedOutcome(outcome) {
|
|
322
301
|
return QUALIFIED_OUTCOMES.includes(outcome);
|
|
323
302
|
}
|
|
324
|
-
var runOutcomeSchema =
|
|
303
|
+
var runOutcomeSchema = z3.enum([
|
|
325
304
|
"completed",
|
|
326
305
|
"attempted-failed",
|
|
327
306
|
...QUALIFIED_OUTCOMES
|
|
328
307
|
]);
|
|
329
|
-
var gateKindSchema =
|
|
330
|
-
var gateVerdictSchema =
|
|
308
|
+
var gateKindSchema = z3.enum(["approval", "mechanical"]);
|
|
309
|
+
var gateVerdictSchema = z3.enum(["pass", "fail", ...QUALIFIED_OUTCOMES]);
|
|
331
310
|
function outcomeReasonIssue(outcome, reason) {
|
|
332
311
|
if (isQualifiedOutcome(outcome) && reason === void 0) {
|
|
333
312
|
return { message: `outcomeReason is required when the outcome is '${outcome}'` };
|
|
@@ -339,142 +318,383 @@ function outcomeReasonIssue(outcome, reason) {
|
|
|
339
318
|
}
|
|
340
319
|
return null;
|
|
341
320
|
}
|
|
342
|
-
var anchorEvidenceSchema =
|
|
343
|
-
kind:
|
|
344
|
-
path:
|
|
321
|
+
var anchorEvidenceSchema = z3.object({
|
|
322
|
+
kind: z3.literal("anchor"),
|
|
323
|
+
path: z3.string().regex(anchorPathRegex),
|
|
345
324
|
// Optional 1-based line number within the file at `path`. Present when the
|
|
346
325
|
// writer knows the exact line; omitted — never `0`, never `null` — when it
|
|
347
326
|
// does not, because a line number is not always obtainable. Absence is a
|
|
348
327
|
// valid, permanent state here, not a gap meant to be filled in later.
|
|
349
|
-
line:
|
|
328
|
+
line: z3.number().optional()
|
|
350
329
|
}).strict();
|
|
351
|
-
var diffEvidenceSchema =
|
|
352
|
-
kind:
|
|
353
|
-
ref:
|
|
330
|
+
var diffEvidenceSchema = z3.object({
|
|
331
|
+
kind: z3.literal("diff"),
|
|
332
|
+
ref: z3.string()
|
|
354
333
|
}).strict();
|
|
355
|
-
var reproEvidenceSchema =
|
|
356
|
-
kind:
|
|
357
|
-
steps:
|
|
334
|
+
var reproEvidenceSchema = z3.object({
|
|
335
|
+
kind: z3.literal("repro"),
|
|
336
|
+
steps: z3.string().max(2e3)
|
|
358
337
|
}).strict();
|
|
359
|
-
var questionAnswerEvidenceSchema =
|
|
360
|
-
kind:
|
|
361
|
-
question:
|
|
362
|
-
before:
|
|
363
|
-
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)
|
|
364
343
|
}).strict();
|
|
365
|
-
var noOpEvidenceSchema =
|
|
366
|
-
kind:
|
|
367
|
-
inspected:
|
|
368
|
-
unchangedBecause:
|
|
344
|
+
var noOpEvidenceSchema = z3.object({
|
|
345
|
+
kind: z3.literal("no-op"),
|
|
346
|
+
inspected: z3.string().max(2e3),
|
|
347
|
+
unchangedBecause: z3.string().max(2e3)
|
|
369
348
|
}).strict();
|
|
370
|
-
var evidenceSchema =
|
|
349
|
+
var evidenceSchema = z3.discriminatedUnion("kind", [
|
|
371
350
|
anchorEvidenceSchema,
|
|
372
351
|
diffEvidenceSchema,
|
|
373
352
|
reproEvidenceSchema,
|
|
374
353
|
questionAnswerEvidenceSchema,
|
|
375
354
|
noOpEvidenceSchema
|
|
376
355
|
]);
|
|
377
|
-
var stateRecordEnvelopeSchema =
|
|
378
|
-
format:
|
|
356
|
+
var stateRecordEnvelopeSchema = z3.object({
|
|
357
|
+
format: z3.literal(STATE_RECORD_FORMAT),
|
|
379
358
|
recordKind: recordKindSchema,
|
|
380
|
-
recordId:
|
|
359
|
+
recordId: z3.string().uuid(),
|
|
381
360
|
methodVersion: methodVersionSchema,
|
|
382
|
-
engagementId:
|
|
361
|
+
engagementId: z3.string(),
|
|
383
362
|
stepId: stepIdSchema,
|
|
384
363
|
// Immutable: names when the thing happened, never when a later disposition
|
|
385
364
|
// moved.
|
|
386
|
-
occurredAt:
|
|
365
|
+
occurredAt: utcIso8601,
|
|
387
366
|
// Equal to `occurredAt` on first write; advances on each re-emit OF A FINDING.
|
|
388
367
|
// Carried on the shared envelope so a "when did this last change" query is
|
|
389
368
|
// kind-agnostic. Kept rather than generalised to `updatedAt` because
|
|
390
369
|
// `disposition` is the only field any re-emit may change (§5.3).
|
|
391
|
-
dispositionAt:
|
|
370
|
+
dispositionAt: utcIso8601,
|
|
392
371
|
evidence: evidenceSchema,
|
|
393
372
|
// Which phase of the project this record belongs to, if any. Present on
|
|
394
373
|
// records written during phase-scoped work; absent on records that run once
|
|
395
374
|
// for the whole project rather than per phase. Absence is meaningful on its
|
|
396
375
|
// own — it is never replaced with a placeholder value, and it is read as
|
|
397
376
|
// "satisfies any phase", not as "unknown".
|
|
398
|
-
phase:
|
|
377
|
+
phase: z3.string().optional()
|
|
399
378
|
});
|
|
400
|
-
var artefactRefSchema =
|
|
401
|
-
name:
|
|
402
|
-
parent:
|
|
379
|
+
var artefactRefSchema = z3.object({
|
|
380
|
+
name: z3.string(),
|
|
381
|
+
parent: z3.string()
|
|
403
382
|
}).strict();
|
|
404
|
-
var coverageSchema =
|
|
405
|
-
asked:
|
|
406
|
-
answered:
|
|
383
|
+
var coverageSchema = z3.object({
|
|
384
|
+
asked: z3.number().int().nonnegative(),
|
|
385
|
+
answered: z3.number().int().nonnegative()
|
|
407
386
|
}).strict().superRefine((coverage, ctx) => {
|
|
408
387
|
if (coverage.answered > coverage.asked) {
|
|
409
388
|
ctx.addIssue({
|
|
410
|
-
code:
|
|
389
|
+
code: z3.ZodIssueCode.custom,
|
|
411
390
|
path: ["answered"],
|
|
412
391
|
message: "answered cannot be more than asked"
|
|
413
392
|
});
|
|
414
393
|
}
|
|
415
394
|
});
|
|
416
395
|
var artefactRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
417
|
-
recordKind:
|
|
396
|
+
recordKind: z3.literal("artefact"),
|
|
418
397
|
artefactRef: artefactRefSchema,
|
|
419
398
|
// Some layer wrote it, by construction — an empty array is not a legitimate
|
|
420
399
|
// state, so the schema says so.
|
|
421
|
-
writtenByLayers:
|
|
400
|
+
writtenByLayers: z3.array(methodLayerSchema).min(1),
|
|
422
401
|
state: artefactStateSchema
|
|
423
402
|
}).strict();
|
|
424
403
|
var stepRunRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
425
|
-
recordKind:
|
|
404
|
+
recordKind: z3.literal("step-run"),
|
|
426
405
|
runOutcome: runOutcomeSchema,
|
|
427
|
-
failureReason:
|
|
428
|
-
outcomeReason:
|
|
406
|
+
failureReason: z3.string().optional(),
|
|
407
|
+
outcomeReason: z3.string().max(2e3).optional()
|
|
429
408
|
}).strict().superRefine((rec, ctx) => {
|
|
430
409
|
if (rec.runOutcome === "attempted-failed" && rec.failureReason === void 0) {
|
|
431
410
|
ctx.addIssue({
|
|
432
|
-
code:
|
|
411
|
+
code: z3.ZodIssueCode.custom,
|
|
433
412
|
path: ["failureReason"],
|
|
434
413
|
message: "failureReason is required when runOutcome is 'attempted-failed'"
|
|
435
414
|
});
|
|
436
415
|
}
|
|
437
416
|
if (rec.runOutcome !== "attempted-failed" && rec.failureReason !== void 0) {
|
|
438
417
|
ctx.addIssue({
|
|
439
|
-
code:
|
|
418
|
+
code: z3.ZodIssueCode.custom,
|
|
440
419
|
path: ["failureReason"],
|
|
441
420
|
message: "failureReason is only present when runOutcome is 'attempted-failed'"
|
|
442
421
|
});
|
|
443
422
|
}
|
|
444
423
|
const issue = outcomeReasonIssue(rec.runOutcome, rec.outcomeReason);
|
|
445
424
|
if (issue) {
|
|
446
|
-
ctx.addIssue({ code:
|
|
425
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["outcomeReason"], ...issue });
|
|
447
426
|
}
|
|
448
427
|
});
|
|
449
428
|
var gateRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
450
|
-
recordKind:
|
|
429
|
+
recordKind: z3.literal("gate"),
|
|
451
430
|
gateKind: gateKindSchema,
|
|
452
431
|
verdict: gateVerdictSchema,
|
|
453
|
-
actor:
|
|
454
|
-
outcomeReason:
|
|
432
|
+
actor: z3.string(),
|
|
433
|
+
outcomeReason: z3.string().max(2e3).optional(),
|
|
455
434
|
coverage: coverageSchema.optional()
|
|
456
435
|
}).strict().superRefine((rec, ctx) => {
|
|
457
436
|
const issue = outcomeReasonIssue(rec.verdict, rec.outcomeReason);
|
|
458
437
|
if (issue) {
|
|
459
|
-
ctx.addIssue({ code:
|
|
438
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["outcomeReason"], ...issue });
|
|
460
439
|
}
|
|
461
440
|
});
|
|
462
441
|
var interventionRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
463
|
-
recordKind:
|
|
442
|
+
recordKind: z3.literal("intervention"),
|
|
464
443
|
layer: methodLayerSchema,
|
|
465
444
|
mechanism: mechanismSchema,
|
|
466
445
|
severity: severitySchema,
|
|
467
446
|
disposition: dispositionSchema,
|
|
468
|
-
summary:
|
|
447
|
+
summary: z3.string().max(2e3),
|
|
469
448
|
coverage: coverageSchema.optional()
|
|
470
449
|
}).strict();
|
|
471
|
-
var stateRecordSchema =
|
|
450
|
+
var stateRecordSchema = z3.discriminatedUnion("recordKind", [
|
|
472
451
|
artefactRecordSchema,
|
|
473
452
|
stepRunRecordSchema,
|
|
474
453
|
gateRecordSchema,
|
|
475
454
|
interventionRecordSchema
|
|
476
455
|
]);
|
|
477
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
|
+
|
|
478
698
|
// ../events/dist/crew.js
|
|
479
699
|
import { z as z5 } from "zod";
|
|
480
700
|
var crewGroupSchema = z5.enum(["builder", "reviewer", "always-on"]);
|
|
@@ -671,7 +891,7 @@ var CREW_CALLSIGNS = CREW_ROSTER.map((member) => member.callsign);
|
|
|
671
891
|
|
|
672
892
|
// ../events/dist/device-auth.js
|
|
673
893
|
import { z as z6 } from "zod";
|
|
674
|
-
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");
|
|
675
895
|
var DEVICE_AUTH_STATUS = {
|
|
676
896
|
/** The poll: minted, nobody has acted yet. Keep polling. 200. */
|
|
677
897
|
PENDING: "pending",
|
|
@@ -716,7 +936,7 @@ var deviceAuthStartResponseSchema = z6.object({
|
|
|
716
936
|
deviceCode: z6.string().min(1),
|
|
717
937
|
userCode: z6.string().min(1),
|
|
718
938
|
verificationUrl: z6.string().min(1),
|
|
719
|
-
expiresAt:
|
|
939
|
+
expiresAt: utcIso86012,
|
|
720
940
|
pollIntervalMs: z6.number().int().positive()
|
|
721
941
|
}).strict();
|
|
722
942
|
var devicePollPendingSchema = z6.object({
|
|
@@ -727,7 +947,7 @@ var devicePollApprovedSchema = z6.object({
|
|
|
727
947
|
status: z6.literal(DEVICE_AUTH_STATUS.APPROVED),
|
|
728
948
|
accountId: z6.string().min(1),
|
|
729
949
|
credential: z6.string().min(1),
|
|
730
|
-
expiresAt:
|
|
950
|
+
expiresAt: utcIso86012
|
|
731
951
|
}).strict();
|
|
732
952
|
var deviceAuthRefusalSchema = z6.object({
|
|
733
953
|
status: z6.enum([
|
|
@@ -765,67 +985,6 @@ var deviceConfirmResponseSchema = z6.union([
|
|
|
765
985
|
]);
|
|
766
986
|
var DASHBOARD_URL = "https://app.halfcycle.ai";
|
|
767
987
|
|
|
768
|
-
// ../events/dist/phase-identity.js
|
|
769
|
-
var PHASE_SEGMENT_PREFIX = "phase-";
|
|
770
|
-
function isValidPhaseIdentity(identity) {
|
|
771
|
-
if (typeof identity === "number")
|
|
772
|
-
return Number.isInteger(identity) && identity >= 0;
|
|
773
|
-
if (typeof identity !== "string")
|
|
774
|
-
return false;
|
|
775
|
-
if (identity === "")
|
|
776
|
-
return false;
|
|
777
|
-
if (identity.includes("/"))
|
|
778
|
-
return false;
|
|
779
|
-
if (identity.includes("\\"))
|
|
780
|
-
return false;
|
|
781
|
-
if (identity.includes("\0"))
|
|
782
|
-
return false;
|
|
783
|
-
if (identity.startsWith("."))
|
|
784
|
-
return false;
|
|
785
|
-
return true;
|
|
786
|
-
}
|
|
787
|
-
var InvalidPhaseIdentityError = class extends Error {
|
|
788
|
-
/** The value that was refused, exactly as supplied. */
|
|
789
|
-
identity;
|
|
790
|
-
constructor(identity, allowedLocation) {
|
|
791
|
-
const subject = allowedLocation === void 0 ? "a file" : "a Build Record";
|
|
792
|
-
const where = allowedLocation === void 0 ? "" : `
|
|
793
|
-
Records are written only into ${allowedLocation.replace(/\\/g, "/")}/. Nothing was written.`;
|
|
794
|
-
super(`that phase identity cannot name ${subject}.
|
|
795
|
-
identity: ${describeIdentity(identity)}` + where);
|
|
796
|
-
this.name = "InvalidPhaseIdentityError";
|
|
797
|
-
this.identity = identity;
|
|
798
|
-
}
|
|
799
|
-
};
|
|
800
|
-
function assertValidPhaseIdentity(identity, allowedLocation) {
|
|
801
|
-
if (!isValidPhaseIdentity(identity)) {
|
|
802
|
-
throw new InvalidPhaseIdentityError(identity, allowedLocation);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
function renderPhaseSegment(identity) {
|
|
806
|
-
if (!isValidPhaseIdentity(identity)) {
|
|
807
|
-
throw new InvalidPhaseIdentityError(identity);
|
|
808
|
-
}
|
|
809
|
-
return `${PHASE_SEGMENT_PREFIX}${identity}`;
|
|
810
|
-
}
|
|
811
|
-
function describeIdentity(identity) {
|
|
812
|
-
if (identity === "")
|
|
813
|
-
return "(empty)";
|
|
814
|
-
if (typeof identity === "string")
|
|
815
|
-
return identity;
|
|
816
|
-
if (identity === null)
|
|
817
|
-
return "null";
|
|
818
|
-
if (identity === void 0)
|
|
819
|
-
return "none supplied";
|
|
820
|
-
if (typeof identity === "number" || typeof identity === "boolean")
|
|
821
|
-
return String(identity);
|
|
822
|
-
if (Array.isArray(identity))
|
|
823
|
-
return "(a list)";
|
|
824
|
-
if (typeof identity === "object")
|
|
825
|
-
return "(an object)";
|
|
826
|
-
return `(a ${typeof identity})`;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
988
|
// ../events/dist/credential-wire.js
|
|
830
989
|
import { z as z7 } from "zod";
|
|
831
990
|
var engagementCredentialResponseSchema = z7.object({
|
|
@@ -837,10 +996,47 @@ var engagementCredentialResponseSchema = z7.object({
|
|
|
837
996
|
}).strict();
|
|
838
997
|
var ciTokenExchangeResponseSchema = z7.object({
|
|
839
998
|
token: z7.string(),
|
|
840
|
-
expiresAt:
|
|
999
|
+
expiresAt: utcIso8601,
|
|
841
1000
|
engagementId: z7.string()
|
|
842
1001
|
}).strict();
|
|
843
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
|
+
|
|
844
1040
|
// dist/engagement-credential.js
|
|
845
1041
|
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
846
1042
|
import { platform as platform2 } from "node:os";
|
|
@@ -3264,193 +3460,253 @@ function buildEngagementRecord(input) {
|
|
|
3264
3460
|
return { ok: true, document, rows };
|
|
3265
3461
|
}
|
|
3266
3462
|
|
|
3267
|
-
// dist/build-record/
|
|
3268
|
-
var
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
}
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
import { join as join10 } from "node:path";
|
|
3283
|
-
|
|
3284
|
-
// dist/build-record/close-record.js
|
|
3285
|
-
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
3286
|
-
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3287
|
-
import { dirname as dirname4, join as join8 } from "node:path";
|
|
3288
|
-
var CLOSE_RECORD_FORMAT = "halfcycle-phase-close/v1";
|
|
3289
|
-
var BUILD_RECORD_ZONE_B_DIR = join8(ZONE_B_DIR, "build-record");
|
|
3290
|
-
var InvalidCloseRecordError = class extends Error {
|
|
3291
|
-
constructor(path, detail) {
|
|
3292
|
-
super(`[build-record] the phase close record at ${path} cannot be read \u2014 ${detail}`);
|
|
3293
|
-
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
|
+
}
|
|
3294
3478
|
}
|
|
3295
|
-
|
|
3296
|
-
function closeRecordPath(repoRoot, phase) {
|
|
3297
|
-
assertValidPhaseIdentity(phase, BUILD_RECORD_ZONE_B_DIR);
|
|
3298
|
-
return join8(repoRoot, BUILD_RECORD_ZONE_B_DIR, `${renderPhaseSegment(phase)}.close.json`);
|
|
3479
|
+
return out;
|
|
3299
3480
|
}
|
|
3300
|
-
function
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
return { closeCommit, closedDate };
|
|
3313
|
-
} catch {
|
|
3314
|
-
return null;
|
|
3315
|
-
}
|
|
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
|
+
};
|
|
3316
3493
|
}
|
|
3317
|
-
function
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
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. The Halfcycle method is a way of building software that Halfcycle runs inside the project's AI coding agent: it works out what is being built, plans the work, reviews it, and checks it as it is built. A phase is one stage of that work. Its finish conditions are the tests of done set out in the phase's plan; this record does not list them. This record says how this phase ended.";
|
|
3499
|
+
var HEADER_REGISTERS = "The client is the person or team running this project. Halfcycle is the service that supplies the method and runs the checks. 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_CHECKS = "Halfcycle's checks are automatic rules drawn from the ways real software builds break. They are set up to run each time the project's AI coding agent edits a file. A check that finds something either warns or stops the work, depending on how serious it is.";
|
|
3501
|
+
var HEADER_KEPT = "Halfcycle derived this record and kept a copy of it when the phase closed. It changes that copy 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 the kept copy, when the phase closed or when the record was fetched again. So a later deletion reaches this file the next time the record is fetched. 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.";
|
|
3502
|
+
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.";
|
|
3503
|
+
var OBSERVED_LABEL = "**Observed by Halfcycle**";
|
|
3504
|
+
var STATED_LABEL = "**Stated by the client** \u2014 Halfcycle did not check these.";
|
|
3505
|
+
var MISMATCHED_HALVES = "This Build Record could not be written: its two parts describe different closes.";
|
|
3506
|
+
function renderChecks(checks, p) {
|
|
3507
|
+
const blocks = ["## Checks during this phase", OBSERVED_LABEL];
|
|
3508
|
+
if (checks.state === "not-observed") {
|
|
3509
|
+
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.");
|
|
3510
|
+
return blocks;
|
|
3511
|
+
}
|
|
3512
|
+
const runs = counted(checks.runs, "check run", "check runs");
|
|
3513
|
+
if (checks.state === "nothing-fired") {
|
|
3514
|
+
blocks.push(`**Checks ran and found nothing.** Halfcycle recorded ${runs} during this phase, and no check found a problem.`);
|
|
3515
|
+
} else {
|
|
3516
|
+
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.`);
|
|
3517
|
+
blocks.push(renderFired(checks.fired, p));
|
|
3331
3518
|
}
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
};
|
|
3338
|
-
try {
|
|
3339
|
-
mkdirSync5(dirname4(path), { recursive: true });
|
|
3340
|
-
writeFileSync5(path, JSON.stringify(record2, null, 2) + "\n", "utf-8");
|
|
3341
|
-
} catch (err) {
|
|
3342
|
-
return { recorded: false, path, reason: err instanceof Error ? err.message : String(err) };
|
|
3519
|
+
if (checks.couldNotRun.length === 0) {
|
|
3520
|
+
blocks.push("No check reported that it could not run.");
|
|
3521
|
+
} else {
|
|
3522
|
+
blocks.push("**Some checks could not run.** They neither passed nor failed during this phase:");
|
|
3523
|
+
blocks.push(renderCouldNotRun(checks.couldNotRun, p));
|
|
3343
3524
|
}
|
|
3344
|
-
return
|
|
3525
|
+
return blocks;
|
|
3345
3526
|
}
|
|
3346
|
-
function
|
|
3347
|
-
|
|
3348
|
-
|
|
3527
|
+
function renderFired(fired, p) {
|
|
3528
|
+
const lines = [];
|
|
3529
|
+
for (const entry of fired) {
|
|
3530
|
+
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)}`);
|
|
3531
|
+
lines.push(` - Seriousness: ${seriousness(entry.severity)}`);
|
|
3532
|
+
lines.push(` - Found: ${counted(entry.occurrences, "time", "times")} during this phase.`);
|
|
3533
|
+
lines.push(` - What it found: ${entry.explanation === null ? "Not included in this record." : p.text(entry.explanation)}`);
|
|
3534
|
+
lines.push(` - The name your terminal prints for this check: ${p.text(entry.check)}`);
|
|
3349
3535
|
}
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3536
|
+
return lines.join("\n");
|
|
3537
|
+
}
|
|
3538
|
+
function seriousness(severity) {
|
|
3539
|
+
switch (severity) {
|
|
3540
|
+
case "block":
|
|
3541
|
+
return "serious, it blocks the work.";
|
|
3542
|
+
case "warn":
|
|
3543
|
+
return "a warning.";
|
|
3544
|
+
case "info":
|
|
3545
|
+
return "for information only.";
|
|
3353
3546
|
}
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
const
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
if (missing.length > 0) {
|
|
3362
|
-
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.`);
|
|
3547
|
+
}
|
|
3548
|
+
function renderCouldNotRun(entries, p) {
|
|
3549
|
+
const lines = [];
|
|
3550
|
+
for (const entry of entries) {
|
|
3551
|
+
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)}`);
|
|
3552
|
+
lines.push(` - Why it could not run: ${p.text(entry.reason)}`);
|
|
3553
|
+
lines.push(` - The name your terminal prints for this check: ${p.text(entry.check)}`);
|
|
3363
3554
|
}
|
|
3364
|
-
|
|
3365
|
-
|
|
3555
|
+
return lines.join("\n");
|
|
3556
|
+
}
|
|
3557
|
+
function headline(observed, attested) {
|
|
3558
|
+
const met = attested.decision === "done-when-met";
|
|
3559
|
+
const problemsReported = attested.walk !== null && attested.walk.verdict === "defects";
|
|
3560
|
+
switch (observed.crossed.kind) {
|
|
3561
|
+
case "none":
|
|
3562
|
+
if (!met) {
|
|
3563
|
+
return "**Closed by override.** Halfcycle saw nothing outstanding, and the person closing the phase chose to override rather than state its finish conditions were met.";
|
|
3564
|
+
}
|
|
3565
|
+
return problemsReported ? "**Closed with problems reported.** Halfcycle saw nothing outstanding, and the person closing the phase reported problems found in its acceptance walk." : "**Closed cleanly.** Halfcycle saw nothing outstanding, and the person closing the phase stated its finish conditions were met.";
|
|
3566
|
+
case "unfinished-steps":
|
|
3567
|
+
case "clean-over-findings":
|
|
3568
|
+
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.";
|
|
3569
|
+
case "not-recorded":
|
|
3570
|
+
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.";
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
function closedAtText(closedAt, p) {
|
|
3574
|
+
const m = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(?:\.\d+)?Z$/.exec(closedAt);
|
|
3575
|
+
return m === null ? p.text(closedAt) : `${m[1]} ${m[2]} UTC`;
|
|
3576
|
+
}
|
|
3577
|
+
function signedInAs(closedBy, p) {
|
|
3578
|
+
switch (closedBy.kind) {
|
|
3579
|
+
case "account":
|
|
3580
|
+
return closedBy.email === null ? "an account with no email address on file" : `the account ${p.text(closedBy.email)}`;
|
|
3581
|
+
case "no-account":
|
|
3582
|
+
return "a credential not linked to any person's account";
|
|
3583
|
+
case "account-removed":
|
|
3584
|
+
return "an account that has since been deleted";
|
|
3585
|
+
case "not-recorded":
|
|
3586
|
+
return "not recorded for this close";
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
function outstanding(crossed, p) {
|
|
3590
|
+
switch (crossed.kind) {
|
|
3591
|
+
case "none":
|
|
3592
|
+
return ["- Outstanding work Halfcycle could see: none."];
|
|
3593
|
+
case "not-recorded":
|
|
3594
|
+
return ["- Outstanding work Halfcycle could see: not recorded for this close."];
|
|
3595
|
+
case "clean-over-findings":
|
|
3596
|
+
return [
|
|
3597
|
+
`- Outstanding work Halfcycle could see: ${counted(crossed.findings, "problem", "problems")} recorded against this phase.`
|
|
3598
|
+
];
|
|
3599
|
+
case "unfinished-steps":
|
|
3600
|
+
return [
|
|
3601
|
+
`- 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:`,
|
|
3602
|
+
...crossed.steps.map((step) => ` - Step: ${p.text(step.stepId)} \u2014 ${step.state === "failed" ? "failed" : "set aside unfinished"}`)
|
|
3603
|
+
];
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
function machines(coverage) {
|
|
3607
|
+
const label = "- Computers signed in to Halfcycle for this project when the phase closed: ";
|
|
3608
|
+
if (coverage === null)
|
|
3609
|
+
return `${label}not recorded.`;
|
|
3610
|
+
if (coverage.enrolled === 0)
|
|
3611
|
+
return `${label}none.`;
|
|
3612
|
+
return `${label}${String(coverage.enrolled)}. Of these, ${String(coverage.conformant)} had reported Halfcycle's checks as fully set up.`;
|
|
3613
|
+
}
|
|
3614
|
+
function walkLines(walk, p) {
|
|
3615
|
+
const label = "> - Acceptance walk (the finished work tried as a user would, by someone other than its builder): ";
|
|
3616
|
+
if (walk === null)
|
|
3617
|
+
return [`${label}none reported.`];
|
|
3618
|
+
const lines = [
|
|
3619
|
+
`${label}${walk.verdict === "clean" ? "reported as finding no problems." : "reported as finding problems."}`
|
|
3620
|
+
];
|
|
3621
|
+
walk.findings.forEach((finding, i) => {
|
|
3622
|
+
lines.push(`> - Finding ${String(i + 1)}: ${p.text(finding)}`);
|
|
3623
|
+
});
|
|
3624
|
+
if (walk.findingsNotShown > 0) {
|
|
3625
|
+
lines.push(`> - And ${counted(walk.findingsNotShown, "more finding", "more findings")} not shown here.`);
|
|
3366
3626
|
}
|
|
3367
|
-
return
|
|
3368
|
-
format: CLOSE_RECORD_FORMAT,
|
|
3369
|
-
phase: record2.phase,
|
|
3370
|
-
closeCommit: commit,
|
|
3371
|
-
closedDate: date
|
|
3372
|
-
};
|
|
3627
|
+
return lines;
|
|
3373
3628
|
}
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
// dist/build-record/template.js
|
|
3380
|
-
function severityLabel(seed) {
|
|
3381
|
-
return `\`${seed.patternRef}\` (severity \`${seed.severity}\`)`;
|
|
3382
|
-
}
|
|
3383
|
-
var NOT_RECORDED = "not recorded";
|
|
3384
|
-
function renderCloseLine(record2) {
|
|
3385
|
-
const status = `**Status:** ${record2.status.toUpperCase()}`;
|
|
3386
|
-
const closed = `**Closed:** ${record2.closedDate ?? NOT_RECORDED}`;
|
|
3387
|
-
const commit = record2.closeCommit === null ? `**Close commit:** ${NOT_RECORDED}` : `**Close commit:** \`${record2.closeCommit}\``;
|
|
3388
|
-
return `${status} \xB7 ${closed} \xB7 ${commit}`;
|
|
3389
|
-
}
|
|
3390
|
-
function renderCloseProvenance(record2) {
|
|
3391
|
-
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.*";
|
|
3392
|
-
}
|
|
3393
|
-
function renderDoneWhenRows(record2) {
|
|
3394
|
-
const rows = [];
|
|
3395
|
-
for (const [key, dw] of Object.entries(record2.acceptance.doneWhen)) {
|
|
3396
|
-
const mark = dw.result === "pass" ? "\u2705" : dw.result === "dropped" ? "\u2014" : dw.result;
|
|
3397
|
-
rows.push(`| ${key} | ${mark} | ${dw.detail} |`);
|
|
3398
|
-
}
|
|
3399
|
-
return rows.join("\n");
|
|
3400
|
-
}
|
|
3401
|
-
function renderBuildRecordMarkdown(record2) {
|
|
3402
|
-
const { phase } = record2;
|
|
3403
|
-
const companion = `${renderPhaseSegment(phase.id)}.json`;
|
|
3404
|
-
const guardsList = record2.seedGuards.map(severityLabel).join(", ");
|
|
3629
|
+
function statedOrNot(value, p) {
|
|
3630
|
+
return value === null ? "Not stated by the client." : p.text(value);
|
|
3631
|
+
}
|
|
3632
|
+
function renderStated(attested, p) {
|
|
3405
3633
|
const lines = [
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
""
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
`**Engagement:** ${record2.engagement} (${record2.engagementType})`,
|
|
3414
|
-
`**Phase:** ${phase.id} \u2014 ${phase.name}`,
|
|
3415
|
-
renderCloseLine(record2),
|
|
3416
|
-
`**Companion:** [\`${companion}\`](${companion})`,
|
|
3417
|
-
"",
|
|
3418
|
-
renderCloseProvenance(record2),
|
|
3419
|
-
"",
|
|
3420
|
-
"## What this phase proved",
|
|
3421
|
-
"",
|
|
3422
|
-
`${record2.thesis} **Thesis held: ${record2.thesisHeld ? "yes" : "no"}.**`,
|
|
3423
|
-
"",
|
|
3424
|
-
"## Delivered",
|
|
3425
|
-
"",
|
|
3426
|
-
`- **Packages:** ${record2.delivered.packages.join(", ")}`,
|
|
3427
|
-
`- **Services:** ${record2.delivered.services.join(", ")}`,
|
|
3428
|
-
`- **Tools:** ${record2.delivered.tools.join(", ")}`,
|
|
3429
|
-
`- **Dogfood:** ${record2.delivered.dogfood}`,
|
|
3430
|
-
"",
|
|
3431
|
-
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 guard firing was recorded on this machine during this phase.",
|
|
3432
|
-
"",
|
|
3433
|
-
`## Acceptance (independent walk \u2014 walker ${record2.acceptance.walker})`,
|
|
3434
|
-
"",
|
|
3435
|
-
`**Verdict:** ${record2.acceptance.verdict} \xB7 **Findings:** ${record2.acceptance.findings}`,
|
|
3436
|
-
"",
|
|
3437
|
-
"| Done-when | Result | Evidence |",
|
|
3438
|
-
"|---|---|---|",
|
|
3439
|
-
renderDoneWhenRows(record2),
|
|
3440
|
-
""
|
|
3634
|
+
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.",
|
|
3635
|
+
`> - Name given: ${attested.actor === null ? "removed with the account that made this close" : p.text(attested.actor)}`,
|
|
3636
|
+
`> - Reason given: ${attested.reason === null ? "none." : p.text(attested.reason)}`,
|
|
3637
|
+
...walkLines(attested.walk, p),
|
|
3638
|
+
`> - Code closed at (commit): ${statedOrNot(attested.closeCommit, p)}`,
|
|
3639
|
+
`> - What the phase set out to prove: ${statedOrNot(attested.statements.thesis, p)}`,
|
|
3640
|
+
`> - What came out of it: ${statedOrNot(attested.statements.outcome, p)}`
|
|
3441
3641
|
];
|
|
3442
|
-
|
|
3443
|
-
|
|
3642
|
+
const deviations = attested.statements.deviations;
|
|
3643
|
+
if (deviations.length === 0) {
|
|
3644
|
+
lines.push("> - Changes from the plan: None stated by the client.");
|
|
3645
|
+
} else {
|
|
3646
|
+
lines.push("> - Changes from the plan:");
|
|
3647
|
+
deviations.forEach((deviation, i) => {
|
|
3648
|
+
lines.push(`> - Change ${String(i + 1)}: ${p.text(deviation)}`);
|
|
3649
|
+
});
|
|
3444
3650
|
}
|
|
3445
|
-
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"}`, "");
|
|
3446
3651
|
return lines.join("\n");
|
|
3447
3652
|
}
|
|
3653
|
+
function renderClose(heading, observed, attested, p) {
|
|
3654
|
+
return [
|
|
3655
|
+
heading,
|
|
3656
|
+
headline(observed, attested),
|
|
3657
|
+
OBSERVED_LABEL,
|
|
3658
|
+
[
|
|
3659
|
+
`- Closed at: ${closedAtText(observed.closedAt, p)}`,
|
|
3660
|
+
`- Signed in as: ${signedInAs(observed.closedBy, p)}`,
|
|
3661
|
+
...outstanding(observed.crossed, p),
|
|
3662
|
+
machines(observed.seatCoverage)
|
|
3663
|
+
].join("\n"),
|
|
3664
|
+
STATED_LABEL,
|
|
3665
|
+
renderStated(attested, p)
|
|
3666
|
+
];
|
|
3667
|
+
}
|
|
3668
|
+
function renderBuildRecordV2Markdown(record2) {
|
|
3669
|
+
const p = makePrinter();
|
|
3670
|
+
const attestedBySequence = /* @__PURE__ */ new Map();
|
|
3671
|
+
for (const close of record2.attested.closes)
|
|
3672
|
+
attestedBySequence.set(close.sequence, close);
|
|
3673
|
+
const observedSequences = new Set(record2.observed.closes.map((c) => c.sequence));
|
|
3674
|
+
if (attestedBySequence.size !== record2.attested.closes.length || observedSequences.size !== record2.observed.closes.length || attestedBySequence.size !== observedSequences.size || [...observedSequences].some((s) => !attestedBySequence.has(s))) {
|
|
3675
|
+
throw new Error(MISMATCHED_HALVES);
|
|
3676
|
+
}
|
|
3677
|
+
const phase = p.text(record2.phase);
|
|
3678
|
+
const closes = [...record2.observed.closes].sort((a, b) => a.sequence - b.sequence);
|
|
3679
|
+
const total = closes.length;
|
|
3680
|
+
const latest = closes[total - 1];
|
|
3681
|
+
const body = [
|
|
3682
|
+
`Phase: ${phase} (the name the client gave it)`,
|
|
3683
|
+
OBSERVED_LABEL,
|
|
3684
|
+
[
|
|
3685
|
+
`- Method version: ${p.text(record2.observed.methodVersion)}. The Halfcycle method is updated from time to time, and each update has its own version number. This is the one the project was run under; it does not change during a project.`,
|
|
3686
|
+
`- Project identifier: ${p.text(record2.observed.engagementId)}. Halfcycle keeps this project's records under this identifier.`,
|
|
3687
|
+
`- Record format: ${p.text(record2.format)}. This names the layout this file and the .json file beside it follow, for software that reads them.`
|
|
3688
|
+
].join("\n"),
|
|
3689
|
+
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.`,
|
|
3690
|
+
...renderChecks(record2.observed.checks, p),
|
|
3691
|
+
...renderClose(total === 1 ? "## The close" : `## The latest close (close ${String(latest.sequence)} of ${String(total)})`, latest, attestedBySequence.get(latest.sequence), p)
|
|
3692
|
+
];
|
|
3693
|
+
if (total > 1) {
|
|
3694
|
+
body.push("## Earlier closes");
|
|
3695
|
+
for (const close of closes.slice(0, -1)) {
|
|
3696
|
+
body.push(...renderClose(`### Close ${String(close.sequence)} of ${String(total)}`, close, attestedBySequence.get(close.sequence), p));
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
const header = [`${TITLE_PREFIX}${phase}`, HEADER_WHAT, HEADER_REGISTERS, HEADER_CHECKS, HEADER_KEPT];
|
|
3700
|
+
if (p.sawHidden())
|
|
3701
|
+
header.push(HEADER_HIDDEN);
|
|
3702
|
+
return `${[...header, ...body].join("\n\n")}
|
|
3703
|
+
`;
|
|
3704
|
+
}
|
|
3448
3705
|
|
|
3449
3706
|
// dist/build-record/write.js
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
}
|
|
3707
|
+
import { closeSync, existsSync as existsSync6, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync7, renameSync, rmSync as rmSync2, writeSync } from "node:fs";
|
|
3708
|
+
import { basename, join as join8, resolve, relative as relative2, isAbsolute as isAbsolute2 } from "node:path";
|
|
3709
|
+
var BUILD_RECORD_DIR = join8("docs", "build-records");
|
|
3454
3710
|
function assertInsideOutDir(outDir, candidate, identity) {
|
|
3455
3711
|
const rel = relative2(outDir, resolve(candidate));
|
|
3456
3712
|
const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute2(rel) && rel === basename(rel);
|
|
@@ -3458,252 +3714,236 @@ function assertInsideOutDir(outDir, candidate, identity) {
|
|
|
3458
3714
|
throw new InvalidPhaseIdentityError(identity, BUILD_RECORD_DIR);
|
|
3459
3715
|
}
|
|
3460
3716
|
}
|
|
3461
|
-
function
|
|
3462
|
-
const
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3717
|
+
function createExclusiveOnDisk(path, text) {
|
|
3718
|
+
const fd = openSync(path, "wx");
|
|
3719
|
+
try {
|
|
3720
|
+
writeSync(fd, text);
|
|
3721
|
+
} catch (err) {
|
|
3722
|
+
closeSync(fd);
|
|
3723
|
+
rmSync2(path, { force: true });
|
|
3724
|
+
throw err;
|
|
3466
3725
|
}
|
|
3467
|
-
|
|
3468
|
-
const jsonPath = join9(outDir, `${segment}.json`);
|
|
3469
|
-
const mdPath = join9(outDir, `${segment}.md`);
|
|
3470
|
-
assertInsideOutDir(outDir, jsonPath, identity);
|
|
3471
|
-
assertInsideOutDir(outDir, mdPath, identity);
|
|
3472
|
-
mkdirSync6(outDir, { recursive: true });
|
|
3473
|
-
writeFileSync6(jsonPath, serialiseRecordJson(record2), "utf-8");
|
|
3474
|
-
writeFileSync6(mdPath, renderBuildRecordMarkdown(record2), "utf-8");
|
|
3475
|
-
return { jsonPath, mdPath };
|
|
3726
|
+
closeSync(fd);
|
|
3476
3727
|
}
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
}
|
|
3728
|
+
function renameOnDisk(from, to) {
|
|
3729
|
+
renameSync(from, to);
|
|
3730
|
+
}
|
|
3731
|
+
var NODE_RECORD_PAIR_IO = {
|
|
3732
|
+
createExclusive: createExclusiveOnDisk,
|
|
3733
|
+
rename: renameOnDisk
|
|
3484
3734
|
};
|
|
3485
|
-
function
|
|
3486
|
-
|
|
3487
|
-
for (const run of runs) {
|
|
3488
|
-
if (!run.result)
|
|
3489
|
-
continue;
|
|
3490
|
-
for (const fired of run.result.guardsFired) {
|
|
3491
|
-
const seed = projectSeedGuard(
|
|
3492
|
-
fired,
|
|
3493
|
-
/* includeExplanation */
|
|
3494
|
-
false
|
|
3495
|
-
);
|
|
3496
|
-
const existing = byRef.get(seed.patternRef);
|
|
3497
|
-
if (!existing) {
|
|
3498
|
-
byRef.set(seed.patternRef, seed);
|
|
3499
|
-
} else if (existing.severity !== seed.severity) {
|
|
3500
|
-
byRef.set(seed.patternRef, pickHigherSeverity(existing, seed));
|
|
3501
|
-
}
|
|
3502
|
-
}
|
|
3503
|
-
}
|
|
3504
|
-
return [...byRef.values()].sort((a, b) => a.patternRef.localeCompare(b.patternRef));
|
|
3735
|
+
function serialiseServedRecord(rawRecord) {
|
|
3736
|
+
return JSON.stringify(rawRecord, null, 2) + "\n";
|
|
3505
3737
|
}
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
return (SEVERITY_ORDINAL[a.severity] ?? -1) >= (SEVERITY_ORDINAL[b.severity] ?? -1) ? a : b;
|
|
3738
|
+
function isEexist(err) {
|
|
3739
|
+
return typeof err === "object" && err !== null && err.code === "EEXIST";
|
|
3509
3740
|
}
|
|
3510
|
-
function
|
|
3511
|
-
|
|
3512
|
-
return
|
|
3741
|
+
function readIfPresent(path) {
|
|
3742
|
+
try {
|
|
3743
|
+
return readFileSync7(path, "utf-8");
|
|
3744
|
+
} catch {
|
|
3745
|
+
return null;
|
|
3513
3746
|
}
|
|
3514
|
-
const record2 = parsed;
|
|
3515
|
-
if (record2["outcome"] !== void 0)
|
|
3516
|
-
return parsed;
|
|
3517
|
-
return {
|
|
3518
|
-
...record2,
|
|
3519
|
-
runId: record2["runId"] ?? syntheticRunId(record2),
|
|
3520
|
-
outcome: "evaluated"
|
|
3521
|
-
};
|
|
3522
3747
|
}
|
|
3523
|
-
function
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
].join("|");
|
|
3530
|
-
const hex = createHash3("sha256").update(`legacy-guard-eval-run:${key}`).digest("hex");
|
|
3531
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
3532
|
-
}
|
|
3533
|
-
function readGuardEvalLog(logDir, phaseId) {
|
|
3534
|
-
const logPath = join10(logDir, `${renderPhaseSegment(phaseId)}.jsonl`);
|
|
3535
|
-
if (!existsSync6(logPath)) {
|
|
3536
|
-
throw new MissingSourceError("guard-eval log", `no runner guard-eval log at ${logPath} for phase ${phaseId}`);
|
|
3537
|
-
}
|
|
3538
|
-
const raw = readFileSync7(logPath, "utf-8");
|
|
3539
|
-
const runs = [];
|
|
3540
|
-
for (const line of raw.split("\n")) {
|
|
3541
|
-
const trimmed = line.trim();
|
|
3542
|
-
if (trimmed === "")
|
|
3543
|
-
continue;
|
|
3544
|
-
const parsed = JSON.parse(trimmed);
|
|
3545
|
-
runs.push(guardEvalRunSchema.parse(normalizeLegacyLine(parsed)));
|
|
3748
|
+
function isOwnEarlierRecord(existingText, served) {
|
|
3749
|
+
let parsedJson;
|
|
3750
|
+
try {
|
|
3751
|
+
parsedJson = JSON.parse(existingText);
|
|
3752
|
+
} catch {
|
|
3753
|
+
return false;
|
|
3546
3754
|
}
|
|
3547
|
-
|
|
3755
|
+
const existing = buildRecordV2Schema.safeParse(parsedJson);
|
|
3756
|
+
if (!existing.success)
|
|
3757
|
+
return false;
|
|
3758
|
+
const old = existing.data;
|
|
3759
|
+
if (old.observed.engagementId !== served.observed.engagementId)
|
|
3760
|
+
return false;
|
|
3761
|
+
if (old.phase !== served.phase)
|
|
3762
|
+
return false;
|
|
3763
|
+
const oldCloses = old.observed.closes.length;
|
|
3764
|
+
const newCloses = served.observed.closes.length;
|
|
3765
|
+
if (oldCloses < newCloses)
|
|
3766
|
+
return true;
|
|
3767
|
+
if (oldCloses > newCloses)
|
|
3768
|
+
return false;
|
|
3769
|
+
const oldSequences = new Set(old.observed.closes.map((c) => c.sequence));
|
|
3770
|
+
return served.observed.closes.every((c) => oldSequences.has(c.sequence));
|
|
3548
3771
|
}
|
|
3549
|
-
function
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3772
|
+
function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_PAIR_IO) {
|
|
3773
|
+
if (!isValidPhaseIdentity(phase)) {
|
|
3774
|
+
throw new InvalidPhaseIdentityError(phase, BUILD_RECORD_DIR);
|
|
3775
|
+
}
|
|
3776
|
+
const outDir = resolve(repoRoot, BUILD_RECORD_DIR);
|
|
3777
|
+
const segment = renderPhaseSegment(phase);
|
|
3778
|
+
const jsonPath = join8(outDir, `${segment}.json`);
|
|
3779
|
+
const mdPath = join8(outDir, `${segment}.md`);
|
|
3780
|
+
const jsonTmp = `${jsonPath}.tmp`;
|
|
3781
|
+
const mdTmp = `${mdPath}.tmp`;
|
|
3782
|
+
for (const candidate of [jsonPath, mdPath, jsonTmp, mdTmp]) {
|
|
3783
|
+
assertInsideOutDir(outDir, candidate, phase);
|
|
3784
|
+
}
|
|
3785
|
+
const jsonText = serialiseServedRecord(rawRecord);
|
|
3786
|
+
const mdText = renderBuildRecordV2Markdown(record2);
|
|
3787
|
+
mkdirSync5(outDir, { recursive: true });
|
|
3788
|
+
let createdJson = false;
|
|
3789
|
+
try {
|
|
3790
|
+
io.createExclusive(jsonPath, jsonText);
|
|
3791
|
+
createdJson = true;
|
|
3792
|
+
io.createExclusive(mdPath, mdText);
|
|
3793
|
+
return { kind: "created", jsonPath, mdPath };
|
|
3794
|
+
} catch (err) {
|
|
3795
|
+
if (createdJson)
|
|
3796
|
+
rmSync2(jsonPath, { force: true });
|
|
3797
|
+
if (!isEexist(err))
|
|
3798
|
+
throw err;
|
|
3553
3799
|
}
|
|
3554
|
-
const
|
|
3555
|
-
if (
|
|
3556
|
-
|
|
3800
|
+
const existingJson = readIfPresent(jsonPath);
|
|
3801
|
+
if (existingJson === null) {
|
|
3802
|
+
return { kind: "in-the-way", path: existsSync6(jsonPath) ? jsonPath : mdPath };
|
|
3557
3803
|
}
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
throw new MissingSourceError("orchestration state", `task ${id} has no status`);
|
|
3562
|
-
}
|
|
3563
|
-
tasks[id] = { status: t.status };
|
|
3804
|
+
if (existingJson === jsonText) {
|
|
3805
|
+
replaceOne(io, mdTmp, mdPath, mdText);
|
|
3806
|
+
return { kind: "up-to-date", jsonPath, mdPath };
|
|
3564
3807
|
}
|
|
3565
|
-
|
|
3566
|
-
}
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
if (!existsSync6(path))
|
|
3570
|
-
return { closeCommit: null, closedDate: null };
|
|
3571
|
-
let parsed;
|
|
3808
|
+
if (!isOwnEarlierRecord(existingJson, record2)) {
|
|
3809
|
+
return { kind: "in-the-way", path: jsonPath };
|
|
3810
|
+
}
|
|
3811
|
+
replaceOne(io, jsonTmp, jsonPath, jsonText);
|
|
3572
3812
|
try {
|
|
3573
|
-
|
|
3813
|
+
replaceOne(io, mdTmp, mdPath, mdText);
|
|
3574
3814
|
} catch (err) {
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3815
|
+
try {
|
|
3816
|
+
replaceOne(io, jsonTmp, jsonPath, existingJson);
|
|
3817
|
+
} catch {
|
|
3818
|
+
rmSync2(jsonPath, { force: true });
|
|
3819
|
+
}
|
|
3820
|
+
throw err;
|
|
3580
3821
|
}
|
|
3581
|
-
return {
|
|
3822
|
+
return { kind: "replaced", jsonPath, mdPath };
|
|
3582
3823
|
}
|
|
3583
|
-
function
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
}
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
req(narrated.phase?.name, "phase.name");
|
|
3592
|
-
assertValidPhaseIdentity(narrated.phase?.id, BUILD_RECORD_DIR);
|
|
3593
|
-
req(narrated.status, "status");
|
|
3594
|
-
req(narrated.thesis, "thesis");
|
|
3595
|
-
req(narrated.delivered, "delivered");
|
|
3596
|
-
req(narrated.acceptance, "acceptance");
|
|
3597
|
-
req(narrated.acceptance?.verdict, "acceptance.verdict");
|
|
3598
|
-
req(narrated.gatesEarnedKeep, "gatesEarnedKeep");
|
|
3599
|
-
req(narrated.instruments, "instruments");
|
|
3600
|
-
if (!Array.isArray(narrated.deviations)) {
|
|
3601
|
-
throw new MissingSourceError("narrated artefact", "deviations must be an array");
|
|
3602
|
-
}
|
|
3603
|
-
return narrated;
|
|
3604
|
-
}
|
|
3605
|
-
function loadSources(opts) {
|
|
3606
|
-
const orchestration = readOrchestrationState(opts.phasesDir, opts.phaseId);
|
|
3607
|
-
const guardEvalRuns = readGuardEvalLog(opts.guardEvalLogDir, opts.phaseId);
|
|
3608
|
-
const close = readPhaseClose(opts.repoRoot, opts.phaseId);
|
|
3609
|
-
const narrated = validateNarrated(opts.narrated);
|
|
3610
|
-
if (!Array.isArray(opts.touchedInvariants)) {
|
|
3611
|
-
throw new MissingSourceError("feature-spec Touches", "touchedInvariants must be an array");
|
|
3824
|
+
function replaceOne(io, tmp, finalPath, text) {
|
|
3825
|
+
rmSync2(tmp, { force: true });
|
|
3826
|
+
try {
|
|
3827
|
+
io.createExclusive(tmp, text);
|
|
3828
|
+
io.rename(tmp, finalPath);
|
|
3829
|
+
} catch (err) {
|
|
3830
|
+
rmSync2(tmp, { force: true });
|
|
3831
|
+
throw err;
|
|
3612
3832
|
}
|
|
3613
|
-
return {
|
|
3614
|
-
orchestration,
|
|
3615
|
-
narrated,
|
|
3616
|
-
close,
|
|
3617
|
-
guardEvalRuns,
|
|
3618
|
-
touchedInvariants: opts.touchedInvariants
|
|
3619
|
-
};
|
|
3620
3833
|
}
|
|
3621
3834
|
|
|
3622
|
-
// dist/build-record/
|
|
3623
|
-
|
|
3624
|
-
var
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
}
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
for (const dw of Object.values(acceptance.doneWhen)) {
|
|
3635
|
-
if (dw.result === "dropped")
|
|
3636
|
-
continue;
|
|
3637
|
-
if (dw.result !== "pass")
|
|
3638
|
-
return false;
|
|
3639
|
-
}
|
|
3640
|
-
return true;
|
|
3835
|
+
// dist/build-record/obtain.js
|
|
3836
|
+
import { relative as relative3 } from "node:path";
|
|
3837
|
+
var REFUSAL_FOLLOW_UP = {
|
|
3838
|
+
"still-open": "Nothing was written. The record is produced when the phase closes.",
|
|
3839
|
+
"reopened": "Nothing was written. A new record is produced when the phase closes again.",
|
|
3840
|
+
"never-opened": "Nothing was written. Check the name: it must match the phase name exactly as it was opened.",
|
|
3841
|
+
"never-closed": "Nothing was written.",
|
|
3842
|
+
"closed-before-records": "Nothing was written."
|
|
3843
|
+
};
|
|
3844
|
+
var REFUSAL_REASONS = phaseRecordRefusalReasonSchema.options;
|
|
3845
|
+
function buildRecordUrl(credential, phase) {
|
|
3846
|
+
return `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/phases/${encodeURIComponent(phase)}/build-record`;
|
|
3641
3847
|
}
|
|
3642
|
-
function
|
|
3643
|
-
|
|
3848
|
+
function isMissingRoute(status, body) {
|
|
3849
|
+
if (status !== 404 || typeof body !== "object" || body === null)
|
|
3850
|
+
return false;
|
|
3851
|
+
const { error, message } = body;
|
|
3852
|
+
return error === "Not Found" && typeof message === "string" && /^Route GET .* not found\.$/s.test(message);
|
|
3644
3853
|
}
|
|
3645
|
-
function
|
|
3646
|
-
const
|
|
3854
|
+
async function obtainBuildRecord(credential, repoRoot, phase, io = NODE_RECORD_PAIR_IO) {
|
|
3855
|
+
const url = buildRecordUrl(credential, phase);
|
|
3856
|
+
let res;
|
|
3857
|
+
let text;
|
|
3858
|
+
try {
|
|
3859
|
+
res = await fetch(url, {
|
|
3860
|
+
method: "GET",
|
|
3861
|
+
headers: { authorization: `Bearer ${credential.token}` }
|
|
3862
|
+
});
|
|
3863
|
+
text = await res.text();
|
|
3864
|
+
} catch (err) {
|
|
3865
|
+
return {
|
|
3866
|
+
kind: "unavailable",
|
|
3867
|
+
why: `could not reach the Halfcycle service at ${url}: ${err instanceof Error ? err.message : String(err)}.`
|
|
3868
|
+
};
|
|
3869
|
+
}
|
|
3870
|
+
let raw = null;
|
|
3871
|
+
try {
|
|
3872
|
+
raw = JSON.parse(text);
|
|
3873
|
+
} catch {
|
|
3874
|
+
raw = null;
|
|
3875
|
+
}
|
|
3876
|
+
const parsed = phaseRecordResponseSchema.safeParse(raw);
|
|
3877
|
+
if (parsed.success) {
|
|
3878
|
+
const answer = parsed.data;
|
|
3879
|
+
if (answer.phase !== phase) {
|
|
3880
|
+
return {
|
|
3881
|
+
kind: "unavailable",
|
|
3882
|
+
why: `the service answered for a different phase than the one asked for, so nothing was written.`
|
|
3883
|
+
};
|
|
3884
|
+
}
|
|
3885
|
+
if (answer.outcome === "withheld")
|
|
3886
|
+
return { kind: "withheld", message: answer.message };
|
|
3887
|
+
if (answer.outcome === "refused") {
|
|
3888
|
+
return { kind: "refused", reason: answer.reason, message: answer.message };
|
|
3889
|
+
}
|
|
3890
|
+
const rawRecord = raw.record;
|
|
3891
|
+
let result;
|
|
3892
|
+
try {
|
|
3893
|
+
result = writeRecordPair(repoRoot, phase, rawRecord, answer.record, io);
|
|
3894
|
+
} catch (err) {
|
|
3895
|
+
return {
|
|
3896
|
+
kind: "unavailable",
|
|
3897
|
+
why: `the record arrived but could not be written: ${err instanceof Error ? err.message : String(err)}. Nothing was left half written.`
|
|
3898
|
+
};
|
|
3899
|
+
}
|
|
3900
|
+
if (result.kind === "in-the-way") {
|
|
3901
|
+
return { kind: "exists", path: relative3(repoRoot, result.path) };
|
|
3902
|
+
}
|
|
3903
|
+
return {
|
|
3904
|
+
kind: "written",
|
|
3905
|
+
jsonPath: relative3(repoRoot, result.jsonPath),
|
|
3906
|
+
mdPath: relative3(repoRoot, result.mdPath)
|
|
3907
|
+
};
|
|
3908
|
+
}
|
|
3909
|
+
if (isMissingRoute(res.status, raw)) {
|
|
3910
|
+
return { kind: "unavailable", why: "this Halfcycle service does not provide Build Records yet." };
|
|
3911
|
+
}
|
|
3912
|
+
const message = typeof raw === "object" && raw !== null && typeof raw.message === "string" ? raw.message : null;
|
|
3913
|
+
if (res.ok) {
|
|
3914
|
+
return {
|
|
3915
|
+
kind: "unavailable",
|
|
3916
|
+
why: `the service's answer was not a Build Record this version of the CLI can read, so nothing was written.`
|
|
3917
|
+
};
|
|
3918
|
+
}
|
|
3647
3919
|
return {
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
engagementType: narrated.engagementType,
|
|
3651
|
-
phase: { id: narrated.phase.id, name: narrated.phase.name },
|
|
3652
|
-
status: narrated.status,
|
|
3653
|
-
closedDate: close.closedDate,
|
|
3654
|
-
closeCommit: close.closeCommit,
|
|
3655
|
-
thesis: narrated.thesis,
|
|
3656
|
-
thesisHeld: deriveThesisHeld(narrated.acceptance),
|
|
3657
|
-
delivered: narrated.delivered,
|
|
3658
|
-
seedGuards: projectSeedGuards(sources.guardEvalRuns),
|
|
3659
|
-
acceptance: narrated.acceptance,
|
|
3660
|
-
gatesEarnedKeep: narrated.gatesEarnedKeep,
|
|
3661
|
-
instruments: narrated.instruments,
|
|
3662
|
-
invariantsExercised: deriveInvariantsExercised(sources.touchedInvariants),
|
|
3663
|
-
deviations: narrated.deviations,
|
|
3664
|
-
tasks: deriveTasks(orchestration)
|
|
3920
|
+
kind: "unavailable",
|
|
3921
|
+
why: `the service answered ${res.status}${message === null ? "." : `: ${message}`}`
|
|
3665
3922
|
};
|
|
3666
3923
|
}
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
const sources = loadSources({
|
|
3671
|
-
repoRoot: opts.repoRoot,
|
|
3672
|
-
phasesDir: opts.phasesDir,
|
|
3673
|
-
guardEvalLogDir: opts.guardEvalLogDir,
|
|
3674
|
-
phaseId: opts.phaseId,
|
|
3675
|
-
narrated: opts.narrated,
|
|
3676
|
-
touchedInvariants: opts.touchedInvariants
|
|
3677
|
-
});
|
|
3678
|
-
const record2 = assembleBuildRecord(sources);
|
|
3679
|
-
return writeBuildRecord(opts.repoRoot, record2);
|
|
3924
|
+
function refetchCommand(phase) {
|
|
3925
|
+
const quoted = /^[A-Za-z0-9._+-]+$/.test(phase) ? phase : `'${phase.replace(/'/g, `'\\''`)}'`;
|
|
3926
|
+
return `npx halfcycle@latest build-record ${quoted}`;
|
|
3680
3927
|
}
|
|
3681
3928
|
export {
|
|
3682
3929
|
BUILD_RECORD_DIR,
|
|
3683
|
-
BUILD_RECORD_FORMAT,
|
|
3684
|
-
BUILD_RECORD_ZONE_B_DIR,
|
|
3685
|
-
CLOSE_RECORD_FORMAT,
|
|
3686
3930
|
CreateEngagementRefused,
|
|
3687
3931
|
FLAT_FILE_TRACKER_HOME,
|
|
3688
3932
|
HALFCYCLE_STATE_FORMAT,
|
|
3689
|
-
InvalidCloseRecordError,
|
|
3690
3933
|
InvalidPhaseIdentityError,
|
|
3691
|
-
|
|
3934
|
+
NODE_RECORD_PAIR_IO,
|
|
3935
|
+
REFUSAL_FOLLOW_UP,
|
|
3936
|
+
REFUSAL_REASONS,
|
|
3692
3937
|
SetupManifest,
|
|
3693
3938
|
ZONE_B_DIR,
|
|
3694
3939
|
agentsPointerRow,
|
|
3695
3940
|
applySetupWrites,
|
|
3696
|
-
assembleBuildRecord,
|
|
3697
|
-
assemblePhaseBuildRecord,
|
|
3698
3941
|
assertValidPhaseIdentity,
|
|
3699
3942
|
buildEngagementRecord,
|
|
3943
|
+
buildRecordUrl,
|
|
3700
3944
|
checkDrift,
|
|
3701
|
-
closeRecordPath,
|
|
3702
3945
|
contextIndexRow,
|
|
3703
3946
|
createEngagement,
|
|
3704
|
-
deriveInvariantsExercised,
|
|
3705
|
-
deriveTasks,
|
|
3706
|
-
deriveThesisHeld,
|
|
3707
3947
|
discoverRepository,
|
|
3708
3948
|
documentSectionRow,
|
|
3709
3949
|
engagementRecordRow,
|
|
@@ -3711,31 +3951,24 @@ export {
|
|
|
3711
3951
|
install,
|
|
3712
3952
|
isValidPhaseIdentity,
|
|
3713
3953
|
layerDocumentRow,
|
|
3714
|
-
loadSources,
|
|
3715
3954
|
mergeSettings,
|
|
3716
3955
|
mergeWithRecords,
|
|
3717
3956
|
mintBoardEnterCode,
|
|
3718
3957
|
mintOrReadIdentity,
|
|
3719
|
-
|
|
3720
|
-
projectSeedGuard,
|
|
3721
|
-
projectSeedGuards,
|
|
3958
|
+
obtainBuildRecord,
|
|
3722
3959
|
questionsToPut,
|
|
3723
3960
|
readBundlePin,
|
|
3724
|
-
readGuardEvalLog,
|
|
3725
|
-
readOrchestrationState,
|
|
3726
|
-
readPhaseClose,
|
|
3727
3961
|
readRemote,
|
|
3728
3962
|
readRootCommit,
|
|
3963
|
+
refetchCommand,
|
|
3729
3964
|
renderAnchor,
|
|
3730
|
-
|
|
3965
|
+
renderBuildRecordV2Markdown,
|
|
3731
3966
|
renderDiscovery,
|
|
3732
3967
|
renderPhaseSegment,
|
|
3733
3968
|
runBootstrapScan,
|
|
3734
3969
|
scanLayers,
|
|
3735
|
-
|
|
3970
|
+
serialiseServedRecord,
|
|
3736
3971
|
trackerHomeRow,
|
|
3737
|
-
|
|
3738
|
-
writeBuildRecord,
|
|
3739
|
-
writeCloseRecord
|
|
3972
|
+
writeRecordPair
|
|
3740
3973
|
};
|
|
3741
3974
|
//# sourceMappingURL=index.js.map
|