halfcycle 0.3.32 → 0.3.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +1147 -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 +799 -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/bin.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// dist/bin.js
|
|
4
4
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
5
5
|
import { existsSync as existsSync5, readFileSync as readFileSync10, statSync } from "node:fs";
|
|
6
|
-
import { join as
|
|
6
|
+
import { join as join10 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// dist/install.js
|
|
9
9
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
@@ -203,6 +203,31 @@ var HALFCYCLE_HOOK_WIRING_PATHS = [
|
|
|
203
203
|
.../* @__PURE__ */ new Set([SETTINGS_REL, ...HALFCYCLE_HOOK_WIRING.map((entry) => entry.script), VENDORED_BIN_REL])
|
|
204
204
|
];
|
|
205
205
|
|
|
206
|
+
// ../core/dist/secret-shape.js
|
|
207
|
+
var SECRET_SHAPE_PATTERNS = [
|
|
208
|
+
// Assignment to a credential-named identifier: `apiKey = "..."`, `secret: '...'`,
|
|
209
|
+
// `password=...`, `token = ...`, `access_key: ...` — the value need not be quoted.
|
|
210
|
+
/\b(?:api[_-]?key|secret|password|passwd|token|access[_-]?key|private[_-]?key|client[_-]?secret|auth[_-]?token|bearer)\b\s*[:=]\s*\S+/i,
|
|
211
|
+
// Connection strings that embed credentials: `scheme://user:pass@host`.
|
|
212
|
+
/\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:[^\s:/@]+@/i,
|
|
213
|
+
// Bearer / authorization header values.
|
|
214
|
+
/\bauthorization\b\s*[:=]\s*['"]?\s*bearer\s+\S+/i,
|
|
215
|
+
// PEM / private-key block headers.
|
|
216
|
+
/-----BEGIN\s+(?:RSA\s+|EC\s+|OPENSSH\s+|DSA\s+)?PRIVATE\s+KEY-----/,
|
|
217
|
+
// JWT shape: three base64url segments separated by dots.
|
|
218
|
+
/\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/,
|
|
219
|
+
// A long high-entropy hex/base64 run adjacent to a credential-ish word on the
|
|
220
|
+
// same line — a raw key literal. Kept last (broadest, most likely false-drop).
|
|
221
|
+
/\b(?:key|token|secret|credential)\b[\s\S]{0,40}[A-Za-z0-9+/_-]{32,}/i
|
|
222
|
+
];
|
|
223
|
+
function looksLikeSecret(text) {
|
|
224
|
+
for (const pattern of SECRET_SHAPE_PATTERNS) {
|
|
225
|
+
if (pattern.test(text))
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
206
231
|
// ../events/dist/result.js
|
|
207
232
|
import { z as z2 } from "zod";
|
|
208
233
|
var firedGuardSchema = z2.object({
|
|
@@ -262,219 +287,31 @@ var wireErrorSchema = z2.object({
|
|
|
262
287
|
message: z2.string()
|
|
263
288
|
}).strict();
|
|
264
289
|
|
|
265
|
-
// ../events/dist/telemetry.js
|
|
266
|
-
import { z as z3 } from "zod";
|
|
267
|
-
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");
|
|
268
|
-
var guardEvalOutcomeSchema = z3.enum([
|
|
269
|
-
"evaluated",
|
|
270
|
-
"infra-error",
|
|
271
|
-
"contract-error",
|
|
272
|
-
"unconfigured"
|
|
273
|
-
]);
|
|
274
|
-
var guardEvalRunSchema = z3.object({
|
|
275
|
-
// `runId`/`outcome` are the E2 run-observability additions, declared
|
|
276
|
-
// field-level-OPTIONAL as the FIRST (expand) step of an expand-contract
|
|
277
|
-
// migration. Every new record populated by the migrated writer (T-07)
|
|
278
|
-
// carries both; a legacy line predating the amendment carries neither and
|
|
279
|
-
// must still parse (the mixed-era guard-eval log the Build-Record reader
|
|
280
|
-
// assembles from, and the runner emit not yet migrated). So the presence
|
|
281
|
-
// refinement below is skipped for a legacy line (no `outcome`), and the
|
|
282
|
-
// per-writer guarantee lives with the writer + reader-normalisation (T-07),
|
|
283
|
-
// where the schema may then be tightened to required. runId is uuid-checked
|
|
284
|
-
// WHEN present; outcome is enum-checked when present.
|
|
285
|
-
runId: z3.string().uuid().optional(),
|
|
286
|
-
engagementId: z3.string().optional(),
|
|
287
|
-
runType: z3.enum(["hook", "ci"]),
|
|
288
|
-
occurredAt: utcIso8601,
|
|
289
|
-
outcome: guardEvalOutcomeSchema.optional(),
|
|
290
|
-
failureReason: z3.string().optional(),
|
|
291
|
-
phase: z3.string().optional(),
|
|
292
|
-
scope: z3.record(z3.string(), z3.unknown()),
|
|
293
|
-
cost: z3.record(z3.string(), z3.unknown()).optional(),
|
|
294
|
-
result: resultEnvelopeSchema.optional()
|
|
295
|
-
}).strict().superRefine((run, ctx) => {
|
|
296
|
-
if (run.outcome === void 0)
|
|
297
|
-
return;
|
|
298
|
-
if (run.outcome === "evaluated" && run.result === void 0) {
|
|
299
|
-
ctx.addIssue({
|
|
300
|
-
code: z3.ZodIssueCode.custom,
|
|
301
|
-
path: ["result"],
|
|
302
|
-
message: "result is required when outcome is 'evaluated'"
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
if (run.outcome !== "evaluated" && run.result !== void 0) {
|
|
306
|
-
ctx.addIssue({
|
|
307
|
-
code: z3.ZodIssueCode.custom,
|
|
308
|
-
path: ["result"],
|
|
309
|
-
message: "result is only present when outcome is 'evaluated'"
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
if (run.outcome !== "unconfigured" && run.engagementId === void 0) {
|
|
313
|
-
ctx.addIssue({
|
|
314
|
-
code: z3.ZodIssueCode.custom,
|
|
315
|
-
path: ["engagementId"],
|
|
316
|
-
message: "engagementId is required unless outcome is 'unconfigured'"
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
|
|
321
290
|
// ../events/dist/engagement-lifecycle.js
|
|
291
|
+
import { z as z5 } from "zod";
|
|
292
|
+
|
|
293
|
+
// ../events/dist/lone-surrogates.js
|
|
294
|
+
var LONE_SURROGATE = new RegExp("\\p{Cs}", "u");
|
|
295
|
+
function hasLoneSurrogate(text) {
|
|
296
|
+
return LONE_SURROGATE.test(text);
|
|
297
|
+
}
|
|
298
|
+
var LONE_SURROGATE_MESSAGE = "must be valid text: it holds half of a character pair with no other half";
|
|
299
|
+
|
|
300
|
+
// ../events/dist/build-record.js
|
|
322
301
|
import { z as z4 } from "zod";
|
|
323
|
-
var utcIso86012 = z4.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");
|
|
324
|
-
var engagementTypeSchema = z4.enum(["client"]);
|
|
325
|
-
var lifecycleStatusSchema = z4.enum(["active", "delivering", "closed"]);
|
|
326
|
-
var createEngagementRequestSchema = z4.object({
|
|
327
|
-
name: z4.string(),
|
|
328
|
-
region: z4.string(),
|
|
329
|
-
engagementType: engagementTypeSchema
|
|
330
|
-
}).strict();
|
|
331
|
-
var createEngagementResponseSchema = z4.object({
|
|
332
|
-
engagementId: z4.string(),
|
|
333
|
-
name: z4.string(),
|
|
334
|
-
region: z4.string(),
|
|
335
|
-
status: lifecycleStatusSchema,
|
|
336
|
-
createdAt: utcIso86012,
|
|
337
|
-
sessionToken: z4.string()
|
|
338
|
-
}).strict();
|
|
339
|
-
var engagementStatusResponseSchema = z4.object({
|
|
340
|
-
engagementId: z4.string(),
|
|
341
|
-
name: z4.string(),
|
|
342
|
-
region: z4.string(),
|
|
343
|
-
status: lifecycleStatusSchema,
|
|
344
|
-
installLinked: z4.boolean(),
|
|
345
|
-
createdAt: utcIso86012,
|
|
346
|
-
updatedAt: utcIso86012
|
|
347
|
-
}).strict();
|
|
348
|
-
var linkInstallationRequestSchema = z4.object({
|
|
349
|
-
installationId: z4.string(),
|
|
350
|
-
engagementId: z4.string()
|
|
351
|
-
}).strict();
|
|
352
|
-
var startBuildRequestSchema = z4.object({
|
|
353
|
-
engagementId: z4.string()
|
|
354
|
-
}).strict();
|
|
355
|
-
var startBuildResponseSchema = z4.object({
|
|
356
|
-
engagementId: z4.string(),
|
|
357
|
-
status: lifecycleStatusSchema,
|
|
358
|
-
enqueuedAt: utcIso86012
|
|
359
|
-
}).strict();
|
|
360
|
-
var acceptanceOutcomeSchema = z4.object({
|
|
361
|
-
verdict: z4.enum(["clean", "defects"]),
|
|
362
|
-
findings: z4.number().int().nonnegative(),
|
|
363
|
-
findingsDetail: z4.array(z4.object({ summary: z4.string() }).strict())
|
|
364
|
-
}).strict();
|
|
365
|
-
var PHASE_CLOSE_REASON_MAX = 1900;
|
|
366
|
-
var phaseCloseDecisionSchema = z4.discriminatedUnion("decision", [
|
|
367
|
-
z4.object({
|
|
368
|
-
decision: z4.literal("done-when-met"),
|
|
369
|
-
/** Who is closing the phase. */
|
|
370
|
-
actor: z4.string()
|
|
371
|
-
}).strict(),
|
|
372
|
-
z4.object({
|
|
373
|
-
decision: z4.literal("override"),
|
|
374
|
-
/** Who is closing it anyway. */
|
|
375
|
-
actor: z4.string(),
|
|
376
|
-
/**
|
|
377
|
-
* Why, in their own words. Bounded: the reason is kept on the phase's
|
|
378
|
-
* permanent record of its own close, and that record refuses anything
|
|
379
|
-
* longer — so an over-long reason is refused here, before the close is
|
|
380
|
-
* attempted, rather than failing the close itself.
|
|
381
|
-
*/
|
|
382
|
-
reason: z4.string().max(PHASE_CLOSE_REASON_MAX)
|
|
383
|
-
}).strict()
|
|
384
|
-
]);
|
|
385
|
-
var acceptPhaseRequestSchema = z4.object({
|
|
386
|
-
phase: z4.string(),
|
|
387
|
-
acceptanceOutcome: acceptanceOutcomeSchema,
|
|
388
|
-
close: phaseCloseDecisionSchema.optional()
|
|
389
|
-
}).strict();
|
|
390
|
-
var coeFindingRequestSchema = z4.object({
|
|
391
|
-
phase: z4.string(),
|
|
392
|
-
description: z4.string(),
|
|
393
|
-
addRateTag: z4.enum(["seam-new", "seam-repeat", "model-limitation"]),
|
|
394
|
-
mappedPatternRef: z4.string().nullable()
|
|
395
|
-
}).strict();
|
|
396
|
-
var coeFindingResponseSchema = z4.object({
|
|
397
|
-
findingId: z4.string(),
|
|
398
|
-
engagementId: z4.string(),
|
|
399
|
-
phase: z4.string(),
|
|
400
|
-
description: z4.string(),
|
|
401
|
-
addRateTag: z4.enum(["seam-new", "seam-repeat", "model-limitation"]),
|
|
402
|
-
mappedPatternRef: z4.string().nullable(),
|
|
403
|
-
createdAt: utcIso86012
|
|
404
|
-
}).strict();
|
|
405
|
-
var operatorFindingResponseSchema = z4.object({
|
|
406
|
-
// The finding just recorded.
|
|
407
|
-
finding: coeFindingResponseSchema,
|
|
408
|
-
// Every finding recorded for that phase, oldest first, including this one.
|
|
409
|
-
phaseFindings: z4.array(coeFindingResponseSchema),
|
|
410
|
-
// Whether the phase's finding rate was recalculated to include this finding.
|
|
411
|
-
figuresRecomputed: z4.boolean()
|
|
412
|
-
}).strict();
|
|
413
|
-
var phaseNotOnRecordRefusalSchema = wireErrorSchema.extend({
|
|
414
|
-
// Always this value for this refusal.
|
|
415
|
-
error: z4.literal("PhaseNotOnRecord"),
|
|
416
|
-
// The phases that can take a finding, sorted.
|
|
417
|
-
phases: z4.array(z4.string())
|
|
418
|
-
}).strict();
|
|
419
|
-
var phaseEntryDecisionSchema = z4.discriminatedUnion("decision", [
|
|
420
|
-
z4.object({
|
|
421
|
-
decision: z4.literal("entry-condition-met"),
|
|
422
|
-
/** Who attests. */
|
|
423
|
-
actor: z4.string(),
|
|
424
|
-
/** What makes the entry condition hold, in the attester's own words. */
|
|
425
|
-
evidence: z4.string()
|
|
426
|
-
}).strict(),
|
|
427
|
-
z4.object({
|
|
428
|
-
decision: z4.literal("override"),
|
|
429
|
-
/** Who is opening it anyway. */
|
|
430
|
-
actor: z4.string(),
|
|
431
|
-
/** Why, in the operator's own words. */
|
|
432
|
-
reason: z4.string()
|
|
433
|
-
}).strict()
|
|
434
|
-
]);
|
|
435
|
-
var PHASE_ENTRY_DECISION_KINDS = [
|
|
436
|
-
"entry-condition-met",
|
|
437
|
-
"override"
|
|
438
|
-
];
|
|
439
|
-
var PHASE_CLOSE_DECISION_KINDS = ["phase-closed", "phase-close-override"];
|
|
440
|
-
var phaseDecisionKindSchema = z4.enum([
|
|
441
|
-
...PHASE_ENTRY_DECISION_KINDS,
|
|
442
|
-
...PHASE_CLOSE_DECISION_KINDS
|
|
443
|
-
]);
|
|
444
|
-
var openPhaseRequestSchema = z4.object({
|
|
445
|
-
phase: z4.string().nullable(),
|
|
446
|
-
entry: phaseEntryDecisionSchema
|
|
447
|
-
}).strict();
|
|
448
|
-
var phaseEntryDecisionRecordSchema = z4.object({
|
|
449
|
-
decisionId: z4.string(),
|
|
450
|
-
decision: z4.enum(PHASE_ENTRY_DECISION_KINDS),
|
|
451
|
-
actor: z4.string(),
|
|
452
|
-
/** The evidence (attestation) or the reason (override) — one basis per row. */
|
|
453
|
-
justification: z4.string(),
|
|
454
|
-
blockingFinding: z4.string().nullable(),
|
|
455
|
-
decidedAt: utcIso86012
|
|
456
|
-
}).strict();
|
|
457
|
-
var openPhaseResponseSchema = z4.object({
|
|
458
|
-
engagement: engagementStatusResponseSchema,
|
|
459
|
-
/** What is open now. `null` is project scope. */
|
|
460
|
-
phase: z4.string().nullable(),
|
|
461
|
-
/** What was open before this decision. `null` is project scope. */
|
|
462
|
-
previousPhase: z4.string().nullable(),
|
|
463
|
-
decision: phaseEntryDecisionRecordSchema
|
|
464
|
-
}).strict();
|
|
465
302
|
|
|
466
303
|
// ../events/dist/state.js
|
|
467
|
-
import { z as
|
|
468
|
-
var
|
|
304
|
+
import { z as z3 } from "zod";
|
|
305
|
+
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");
|
|
469
306
|
var STATE_RECORD_FORMAT = "halfcycle-state-record/v1";
|
|
470
|
-
var stepIdSchema =
|
|
471
|
-
var methodVersionSchema =
|
|
307
|
+
var stepIdSchema = z3.string().regex(/^(l[0-5](-l[0-5])?|xl)\.[a-z0-9-]+$/);
|
|
308
|
+
var methodVersionSchema = z3.string().regex(/^\d+\.\d+\.\d+$/);
|
|
472
309
|
var anchorPathRegex = /^(?!\/)(?!.*#)(?!method\/)(?!(.*\/)?docs\/method\/).+$/;
|
|
473
|
-
var recordKindSchema =
|
|
474
|
-
var methodLayerSchema =
|
|
475
|
-
var evidenceKindSchema =
|
|
476
|
-
var declarableEvidenceKindSchema =
|
|
477
|
-
var dispositionSchema =
|
|
310
|
+
var recordKindSchema = z3.enum(["artefact", "step-run", "gate", "intervention"]);
|
|
311
|
+
var methodLayerSchema = z3.enum(["l0", "l1", "l2", "l3", "l3-l4", "l4", "l5", "xl"]);
|
|
312
|
+
var evidenceKindSchema = z3.enum(["anchor", "diff", "repro", "question-answer", "no-op"]);
|
|
313
|
+
var declarableEvidenceKindSchema = z3.enum(["anchor", "diff", "repro", "question-answer"]);
|
|
314
|
+
var dispositionSchema = z3.enum([
|
|
478
315
|
"prevented",
|
|
479
316
|
"corrected-in-spec",
|
|
480
317
|
"corrected-in-scope",
|
|
@@ -484,20 +321,20 @@ var dispositionSchema = z5.enum([
|
|
|
484
321
|
"surfaced",
|
|
485
322
|
"filed-forward"
|
|
486
323
|
]);
|
|
487
|
-
var mechanismSchema =
|
|
488
|
-
var artefactStateSchema =
|
|
324
|
+
var mechanismSchema = z3.enum(["guard", "verdict", "served-step", "method-step"]);
|
|
325
|
+
var artefactStateSchema = z3.enum(["created", "updated"]);
|
|
489
326
|
var QUALIFIED_OUTCOMES = ["declined", "parked"];
|
|
490
|
-
var qualifiedOutcomeSchema =
|
|
327
|
+
var qualifiedOutcomeSchema = z3.enum(QUALIFIED_OUTCOMES);
|
|
491
328
|
function isQualifiedOutcome(outcome) {
|
|
492
329
|
return QUALIFIED_OUTCOMES.includes(outcome);
|
|
493
330
|
}
|
|
494
|
-
var runOutcomeSchema =
|
|
331
|
+
var runOutcomeSchema = z3.enum([
|
|
495
332
|
"completed",
|
|
496
333
|
"attempted-failed",
|
|
497
334
|
...QUALIFIED_OUTCOMES
|
|
498
335
|
]);
|
|
499
|
-
var gateKindSchema =
|
|
500
|
-
var gateVerdictSchema =
|
|
336
|
+
var gateKindSchema = z3.enum(["approval", "mechanical"]);
|
|
337
|
+
var gateVerdictSchema = z3.enum(["pass", "fail", ...QUALIFIED_OUTCOMES]);
|
|
501
338
|
function outcomeReasonIssue(outcome, reason) {
|
|
502
339
|
if (isQualifiedOutcome(outcome) && reason === void 0) {
|
|
503
340
|
return { message: `outcomeReason is required when the outcome is '${outcome}'` };
|
|
@@ -509,142 +346,527 @@ function outcomeReasonIssue(outcome, reason) {
|
|
|
509
346
|
}
|
|
510
347
|
return null;
|
|
511
348
|
}
|
|
512
|
-
var anchorEvidenceSchema =
|
|
513
|
-
kind:
|
|
514
|
-
path:
|
|
349
|
+
var anchorEvidenceSchema = z3.object({
|
|
350
|
+
kind: z3.literal("anchor"),
|
|
351
|
+
path: z3.string().regex(anchorPathRegex),
|
|
515
352
|
// Optional 1-based line number within the file at `path`. Present when the
|
|
516
353
|
// writer knows the exact line; omitted — never `0`, never `null` — when it
|
|
517
354
|
// does not, because a line number is not always obtainable. Absence is a
|
|
518
355
|
// valid, permanent state here, not a gap meant to be filled in later.
|
|
519
|
-
line:
|
|
356
|
+
line: z3.number().optional()
|
|
520
357
|
}).strict();
|
|
521
|
-
var diffEvidenceSchema =
|
|
522
|
-
kind:
|
|
523
|
-
ref:
|
|
358
|
+
var diffEvidenceSchema = z3.object({
|
|
359
|
+
kind: z3.literal("diff"),
|
|
360
|
+
ref: z3.string()
|
|
524
361
|
}).strict();
|
|
525
|
-
var reproEvidenceSchema =
|
|
526
|
-
kind:
|
|
527
|
-
steps:
|
|
362
|
+
var reproEvidenceSchema = z3.object({
|
|
363
|
+
kind: z3.literal("repro"),
|
|
364
|
+
steps: z3.string().max(2e3)
|
|
528
365
|
}).strict();
|
|
529
|
-
var questionAnswerEvidenceSchema =
|
|
530
|
-
kind:
|
|
531
|
-
question:
|
|
532
|
-
before:
|
|
533
|
-
after:
|
|
366
|
+
var questionAnswerEvidenceSchema = z3.object({
|
|
367
|
+
kind: z3.literal("question-answer"),
|
|
368
|
+
question: z3.string().max(2e3),
|
|
369
|
+
before: z3.string().max(2e3),
|
|
370
|
+
after: z3.string().max(2e3)
|
|
534
371
|
}).strict();
|
|
535
|
-
var noOpEvidenceSchema =
|
|
536
|
-
kind:
|
|
537
|
-
inspected:
|
|
538
|
-
unchangedBecause:
|
|
372
|
+
var noOpEvidenceSchema = z3.object({
|
|
373
|
+
kind: z3.literal("no-op"),
|
|
374
|
+
inspected: z3.string().max(2e3),
|
|
375
|
+
unchangedBecause: z3.string().max(2e3)
|
|
539
376
|
}).strict();
|
|
540
|
-
var evidenceSchema =
|
|
377
|
+
var evidenceSchema = z3.discriminatedUnion("kind", [
|
|
541
378
|
anchorEvidenceSchema,
|
|
542
379
|
diffEvidenceSchema,
|
|
543
380
|
reproEvidenceSchema,
|
|
544
381
|
questionAnswerEvidenceSchema,
|
|
545
382
|
noOpEvidenceSchema
|
|
546
383
|
]);
|
|
547
|
-
var stateRecordEnvelopeSchema =
|
|
548
|
-
format:
|
|
384
|
+
var stateRecordEnvelopeSchema = z3.object({
|
|
385
|
+
format: z3.literal(STATE_RECORD_FORMAT),
|
|
549
386
|
recordKind: recordKindSchema,
|
|
550
|
-
recordId:
|
|
387
|
+
recordId: z3.string().uuid(),
|
|
551
388
|
methodVersion: methodVersionSchema,
|
|
552
|
-
engagementId:
|
|
389
|
+
engagementId: z3.string(),
|
|
553
390
|
stepId: stepIdSchema,
|
|
554
391
|
// Immutable: names when the thing happened, never when a later disposition
|
|
555
392
|
// moved.
|
|
556
|
-
occurredAt:
|
|
393
|
+
occurredAt: utcIso8601,
|
|
557
394
|
// Equal to `occurredAt` on first write; advances on each re-emit OF A FINDING.
|
|
558
395
|
// Carried on the shared envelope so a "when did this last change" query is
|
|
559
396
|
// kind-agnostic. Kept rather than generalised to `updatedAt` because
|
|
560
397
|
// `disposition` is the only field any re-emit may change (§5.3).
|
|
561
|
-
dispositionAt:
|
|
398
|
+
dispositionAt: utcIso8601,
|
|
562
399
|
evidence: evidenceSchema,
|
|
563
400
|
// Which phase of the project this record belongs to, if any. Present on
|
|
564
401
|
// records written during phase-scoped work; absent on records that run once
|
|
565
402
|
// for the whole project rather than per phase. Absence is meaningful on its
|
|
566
403
|
// own — it is never replaced with a placeholder value, and it is read as
|
|
567
404
|
// "satisfies any phase", not as "unknown".
|
|
568
|
-
phase:
|
|
405
|
+
phase: z3.string().optional()
|
|
569
406
|
});
|
|
570
|
-
var artefactRefSchema =
|
|
571
|
-
name:
|
|
572
|
-
parent:
|
|
407
|
+
var artefactRefSchema = z3.object({
|
|
408
|
+
name: z3.string(),
|
|
409
|
+
parent: z3.string()
|
|
573
410
|
}).strict();
|
|
574
|
-
var coverageSchema =
|
|
575
|
-
asked:
|
|
576
|
-
answered:
|
|
411
|
+
var coverageSchema = z3.object({
|
|
412
|
+
asked: z3.number().int().nonnegative(),
|
|
413
|
+
answered: z3.number().int().nonnegative()
|
|
577
414
|
}).strict().superRefine((coverage, ctx) => {
|
|
578
415
|
if (coverage.answered > coverage.asked) {
|
|
579
416
|
ctx.addIssue({
|
|
580
|
-
code:
|
|
417
|
+
code: z3.ZodIssueCode.custom,
|
|
581
418
|
path: ["answered"],
|
|
582
419
|
message: "answered cannot be more than asked"
|
|
583
420
|
});
|
|
584
421
|
}
|
|
585
422
|
});
|
|
586
423
|
var artefactRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
587
|
-
recordKind:
|
|
424
|
+
recordKind: z3.literal("artefact"),
|
|
588
425
|
artefactRef: artefactRefSchema,
|
|
589
426
|
// Some layer wrote it, by construction — an empty array is not a legitimate
|
|
590
427
|
// state, so the schema says so.
|
|
591
|
-
writtenByLayers:
|
|
428
|
+
writtenByLayers: z3.array(methodLayerSchema).min(1),
|
|
592
429
|
state: artefactStateSchema
|
|
593
430
|
}).strict();
|
|
594
431
|
var stepRunRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
595
|
-
recordKind:
|
|
432
|
+
recordKind: z3.literal("step-run"),
|
|
596
433
|
runOutcome: runOutcomeSchema,
|
|
597
|
-
failureReason:
|
|
598
|
-
outcomeReason:
|
|
434
|
+
failureReason: z3.string().optional(),
|
|
435
|
+
outcomeReason: z3.string().max(2e3).optional()
|
|
599
436
|
}).strict().superRefine((rec, ctx) => {
|
|
600
437
|
if (rec.runOutcome === "attempted-failed" && rec.failureReason === void 0) {
|
|
601
438
|
ctx.addIssue({
|
|
602
|
-
code:
|
|
439
|
+
code: z3.ZodIssueCode.custom,
|
|
603
440
|
path: ["failureReason"],
|
|
604
441
|
message: "failureReason is required when runOutcome is 'attempted-failed'"
|
|
605
442
|
});
|
|
606
443
|
}
|
|
607
444
|
if (rec.runOutcome !== "attempted-failed" && rec.failureReason !== void 0) {
|
|
608
445
|
ctx.addIssue({
|
|
609
|
-
code:
|
|
446
|
+
code: z3.ZodIssueCode.custom,
|
|
610
447
|
path: ["failureReason"],
|
|
611
448
|
message: "failureReason is only present when runOutcome is 'attempted-failed'"
|
|
612
449
|
});
|
|
613
450
|
}
|
|
614
451
|
const issue = outcomeReasonIssue(rec.runOutcome, rec.outcomeReason);
|
|
615
452
|
if (issue) {
|
|
616
|
-
ctx.addIssue({ code:
|
|
453
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["outcomeReason"], ...issue });
|
|
617
454
|
}
|
|
618
455
|
});
|
|
619
456
|
var gateRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
620
|
-
recordKind:
|
|
457
|
+
recordKind: z3.literal("gate"),
|
|
621
458
|
gateKind: gateKindSchema,
|
|
622
459
|
verdict: gateVerdictSchema,
|
|
623
|
-
actor:
|
|
624
|
-
outcomeReason:
|
|
460
|
+
actor: z3.string(),
|
|
461
|
+
outcomeReason: z3.string().max(2e3).optional(),
|
|
625
462
|
coverage: coverageSchema.optional()
|
|
626
463
|
}).strict().superRefine((rec, ctx) => {
|
|
627
464
|
const issue = outcomeReasonIssue(rec.verdict, rec.outcomeReason);
|
|
628
465
|
if (issue) {
|
|
629
|
-
ctx.addIssue({ code:
|
|
466
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["outcomeReason"], ...issue });
|
|
630
467
|
}
|
|
631
468
|
});
|
|
632
469
|
var interventionRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
633
|
-
recordKind:
|
|
470
|
+
recordKind: z3.literal("intervention"),
|
|
634
471
|
layer: methodLayerSchema,
|
|
635
472
|
mechanism: mechanismSchema,
|
|
636
473
|
severity: severitySchema,
|
|
637
474
|
disposition: dispositionSchema,
|
|
638
|
-
summary:
|
|
475
|
+
summary: z3.string().max(2e3),
|
|
639
476
|
coverage: coverageSchema.optional()
|
|
640
477
|
}).strict();
|
|
641
|
-
var stateRecordSchema =
|
|
478
|
+
var stateRecordSchema = z3.discriminatedUnion("recordKind", [
|
|
642
479
|
artefactRecordSchema,
|
|
643
480
|
stepRunRecordSchema,
|
|
644
481
|
gateRecordSchema,
|
|
645
482
|
interventionRecordSchema
|
|
646
483
|
]);
|
|
647
484
|
|
|
485
|
+
// ../events/dist/control-characters.js
|
|
486
|
+
var CONTROL_CHARACTER_CLASS = "\\p{Cc}";
|
|
487
|
+
var CONTROL_CHARACTER = new RegExp(CONTROL_CHARACTER_CLASS, "u");
|
|
488
|
+
function hasControlCharacter(text) {
|
|
489
|
+
return CONTROL_CHARACTER.test(text);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ../events/dist/phase-identity.js
|
|
493
|
+
var PHASE_SEGMENT_PREFIX = "phase-";
|
|
494
|
+
var PHASE_NAME_MAX_BYTES = 200;
|
|
495
|
+
function isValidPhaseIdentity(identity) {
|
|
496
|
+
if (typeof identity === "number")
|
|
497
|
+
return Number.isInteger(identity) && identity >= 0;
|
|
498
|
+
if (typeof identity !== "string")
|
|
499
|
+
return false;
|
|
500
|
+
if (identity === "")
|
|
501
|
+
return false;
|
|
502
|
+
if (identity.includes("/"))
|
|
503
|
+
return false;
|
|
504
|
+
if (identity.includes("\\"))
|
|
505
|
+
return false;
|
|
506
|
+
if (identity.startsWith("."))
|
|
507
|
+
return false;
|
|
508
|
+
if (hasControlCharacter(identity))
|
|
509
|
+
return false;
|
|
510
|
+
if (utf8ByteLength(identity) > PHASE_NAME_MAX_BYTES)
|
|
511
|
+
return false;
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
function utf8ByteLength(text) {
|
|
515
|
+
let bytes = 0;
|
|
516
|
+
for (const ch of text) {
|
|
517
|
+
const cp = ch.codePointAt(0);
|
|
518
|
+
bytes += cp < 128 ? 1 : cp < 2048 ? 2 : cp < 65536 ? 3 : 4;
|
|
519
|
+
}
|
|
520
|
+
return bytes;
|
|
521
|
+
}
|
|
522
|
+
var InvalidPhaseIdentityError = class extends Error {
|
|
523
|
+
/** The value that was refused, exactly as supplied. */
|
|
524
|
+
identity;
|
|
525
|
+
constructor(identity, allowedLocation) {
|
|
526
|
+
const subject = allowedLocation === void 0 ? "a file" : "a Build Record";
|
|
527
|
+
const where = allowedLocation === void 0 ? "" : `
|
|
528
|
+
Records are written only into ${allowedLocation.replace(/\\/g, "/")}/. Nothing was written.`;
|
|
529
|
+
super(`that phase identity cannot name ${subject}.
|
|
530
|
+
identity: ${describeIdentity(identity)}` + where);
|
|
531
|
+
this.name = "InvalidPhaseIdentityError";
|
|
532
|
+
this.identity = identity;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
function assertValidPhaseIdentity(identity, allowedLocation) {
|
|
536
|
+
if (!isValidPhaseIdentity(identity)) {
|
|
537
|
+
throw new InvalidPhaseIdentityError(identity, allowedLocation);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function renderPhaseSegment(identity) {
|
|
541
|
+
if (!isValidPhaseIdentity(identity)) {
|
|
542
|
+
throw new InvalidPhaseIdentityError(identity);
|
|
543
|
+
}
|
|
544
|
+
return `${PHASE_SEGMENT_PREFIX}${identity}`;
|
|
545
|
+
}
|
|
546
|
+
function describeIdentity(identity) {
|
|
547
|
+
if (identity === "")
|
|
548
|
+
return "(empty)";
|
|
549
|
+
if (typeof identity === "string")
|
|
550
|
+
return identity;
|
|
551
|
+
if (identity === null)
|
|
552
|
+
return "null";
|
|
553
|
+
if (identity === void 0)
|
|
554
|
+
return "none supplied";
|
|
555
|
+
if (typeof identity === "number" || typeof identity === "boolean")
|
|
556
|
+
return String(identity);
|
|
557
|
+
if (Array.isArray(identity))
|
|
558
|
+
return "(a list)";
|
|
559
|
+
if (typeof identity === "object")
|
|
560
|
+
return "(an object)";
|
|
561
|
+
return `(a ${typeof identity})`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ../events/dist/build-record.js
|
|
565
|
+
var BUILD_RECORD_V2_FORMAT = "halfcycle-build-record/v2";
|
|
566
|
+
var STATEMENT_MAX = 2e3;
|
|
567
|
+
var DEVIATION_MAX = 500;
|
|
568
|
+
var DEVIATIONS_MAX = 20;
|
|
569
|
+
var ACTOR_MAX = 200;
|
|
570
|
+
var FINDING_MAX = 500;
|
|
571
|
+
var FINDINGS_SHOWN_MAX = 50;
|
|
572
|
+
var PHASE_CLOSE_REASON_MAX = 1900;
|
|
573
|
+
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");
|
|
574
|
+
var walkVerdictSchema = z4.enum(["clean", "defects"]);
|
|
575
|
+
var closeDecisionSchema = z4.enum(["done-when-met", "override"]);
|
|
576
|
+
function plainText(max) {
|
|
577
|
+
const base = max === void 0 ? z4.string() : z4.string().max(max);
|
|
578
|
+
return base.refine((v) => !hasControlCharacter(v), {
|
|
579
|
+
message: "must be one line with no control characters (no line breaks, tabs or NUL)"
|
|
580
|
+
}).refine((v) => !hasLoneSurrogate(v), { message: LONE_SURROGATE_MESSAGE });
|
|
581
|
+
}
|
|
582
|
+
function statementText(max) {
|
|
583
|
+
return plainText(max).refine((v) => !looksLikeSecret(v), {
|
|
584
|
+
message: "looks like a password or key \u2014 rephrase it"
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
var closeActorSchema = plainText(ACTOR_MAX);
|
|
588
|
+
var phaseNameSchema = z4.string().refine((v) => isValidPhaseIdentity(v), {
|
|
589
|
+
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`
|
|
590
|
+
});
|
|
591
|
+
var closeStatementsSchema = z4.object({
|
|
592
|
+
thesis: statementText(STATEMENT_MAX).nullable(),
|
|
593
|
+
outcome: statementText(STATEMENT_MAX).nullable(),
|
|
594
|
+
deviations: z4.array(statementText(DEVIATION_MAX)).max(DEVIATIONS_MAX),
|
|
595
|
+
closeCommit: closeCommitSchema.nullable()
|
|
596
|
+
}).strict();
|
|
597
|
+
var closedBySchema = z4.discriminatedUnion("kind", [
|
|
598
|
+
z4.object({ kind: z4.literal("account"), email: plainText().nullable() }).strict(),
|
|
599
|
+
z4.object({ kind: z4.literal("no-account") }).strict(),
|
|
600
|
+
z4.object({ kind: z4.literal("account-removed") }).strict(),
|
|
601
|
+
z4.object({ kind: z4.literal("not-recorded") }).strict()
|
|
602
|
+
]);
|
|
603
|
+
var unfinishedStepSchema = z4.object({
|
|
604
|
+
stepId: plainText(),
|
|
605
|
+
state: z4.enum(["parked", "failed"])
|
|
606
|
+
}).strict();
|
|
607
|
+
var crossedSchema = z4.discriminatedUnion("kind", [
|
|
608
|
+
z4.object({ kind: z4.literal("none") }).strict(),
|
|
609
|
+
z4.object({ kind: z4.literal("unfinished-steps"), steps: z4.array(unfinishedStepSchema).min(1) }).strict(),
|
|
610
|
+
z4.object({ kind: z4.literal("clean-over-findings"), findings: z4.number().int().min(1) }).strict(),
|
|
611
|
+
z4.object({ kind: z4.literal("not-recorded") }).strict()
|
|
612
|
+
]);
|
|
613
|
+
var seatCoverageSchema = z4.object({
|
|
614
|
+
enrolled: z4.number().int().nonnegative(),
|
|
615
|
+
conformant: z4.number().int().nonnegative()
|
|
616
|
+
}).strict().refine((c) => c.conformant <= c.enrolled, {
|
|
617
|
+
message: "conformant cannot exceed enrolled",
|
|
618
|
+
path: ["conformant"]
|
|
619
|
+
});
|
|
620
|
+
var observedCloseSchema = z4.object({
|
|
621
|
+
sequence: z4.number().int().min(1),
|
|
622
|
+
closedAt: utcIso8601,
|
|
623
|
+
closedBy: closedBySchema,
|
|
624
|
+
crossed: crossedSchema,
|
|
625
|
+
seatCoverage: seatCoverageSchema.nullable()
|
|
626
|
+
}).strict();
|
|
627
|
+
var couldNotRunCheckSchema = z4.object({
|
|
628
|
+
check: plainText(),
|
|
629
|
+
description: plainText().nullable(),
|
|
630
|
+
reason: plainText()
|
|
631
|
+
}).strict();
|
|
632
|
+
var firedCheckSchema = z4.object({
|
|
633
|
+
check: plainText(),
|
|
634
|
+
description: plainText().nullable(),
|
|
635
|
+
severity: severitySchema,
|
|
636
|
+
explanation: plainText().nullable(),
|
|
637
|
+
occurrences: z4.number().int().min(1)
|
|
638
|
+
}).strict();
|
|
639
|
+
var phaseChecksSchema = z4.discriminatedUnion("state", [
|
|
640
|
+
z4.object({ state: z4.literal("not-observed") }).strict(),
|
|
641
|
+
z4.object({
|
|
642
|
+
state: z4.literal("nothing-fired"),
|
|
643
|
+
runs: z4.number().int().min(1),
|
|
644
|
+
couldNotRun: z4.array(couldNotRunCheckSchema)
|
|
645
|
+
}).strict(),
|
|
646
|
+
z4.object({
|
|
647
|
+
state: z4.literal("fired"),
|
|
648
|
+
runs: z4.number().int().min(1),
|
|
649
|
+
couldNotRun: z4.array(couldNotRunCheckSchema),
|
|
650
|
+
fired: z4.array(firedCheckSchema).min(1)
|
|
651
|
+
}).strict()
|
|
652
|
+
]);
|
|
653
|
+
var observedSchema = z4.object({
|
|
654
|
+
engagementId: plainText(),
|
|
655
|
+
methodVersion: methodVersionSchema,
|
|
656
|
+
closes: z4.array(observedCloseSchema).min(1),
|
|
657
|
+
checks: phaseChecksSchema
|
|
658
|
+
}).strict().superRefine((o, ctx) => {
|
|
659
|
+
o.closes.forEach((close, i) => {
|
|
660
|
+
if (close.sequence !== i + 1) {
|
|
661
|
+
ctx.addIssue({
|
|
662
|
+
code: "custom",
|
|
663
|
+
message: "closes must be numbered 1, 2, 3 \u2026 in order with no gap",
|
|
664
|
+
path: ["closes", i, "sequence"]
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
var attestedWalkSchema = z4.object({
|
|
670
|
+
verdict: walkVerdictSchema,
|
|
671
|
+
findings: z4.array(plainText(FINDING_MAX)).max(FINDINGS_SHOWN_MAX),
|
|
672
|
+
findingsNotShown: z4.number().int().nonnegative()
|
|
673
|
+
}).strict();
|
|
674
|
+
var attestedStatementsSchema = z4.object({
|
|
675
|
+
thesis: plainText(STATEMENT_MAX).nullable(),
|
|
676
|
+
outcome: plainText(STATEMENT_MAX).nullable(),
|
|
677
|
+
deviations: z4.array(plainText(DEVIATION_MAX)).max(DEVIATIONS_MAX)
|
|
678
|
+
}).strict();
|
|
679
|
+
var attestedCloseSchema = z4.object({
|
|
680
|
+
sequence: z4.number().int().min(1),
|
|
681
|
+
decision: closeDecisionSchema,
|
|
682
|
+
actor: plainText(ACTOR_MAX).nullable(),
|
|
683
|
+
reason: plainText(PHASE_CLOSE_REASON_MAX).nullable(),
|
|
684
|
+
walk: attestedWalkSchema.nullable(),
|
|
685
|
+
closeCommit: closeCommitSchema.nullable(),
|
|
686
|
+
statements: attestedStatementsSchema
|
|
687
|
+
}).strict();
|
|
688
|
+
var attestedSchema = z4.object({
|
|
689
|
+
closes: z4.array(attestedCloseSchema)
|
|
690
|
+
}).strict();
|
|
691
|
+
var buildRecordV2Schema = z4.object({
|
|
692
|
+
format: z4.literal(BUILD_RECORD_V2_FORMAT),
|
|
693
|
+
phase: phaseNameSchema,
|
|
694
|
+
observed: observedSchema,
|
|
695
|
+
attested: attestedSchema
|
|
696
|
+
}).strict().superRefine((record2, ctx) => {
|
|
697
|
+
const observedBySequence = new Map(record2.observed.closes.map((c) => [c.sequence, c]));
|
|
698
|
+
const attestedSequences = record2.attested.closes.map((c) => c.sequence);
|
|
699
|
+
const sameSet = attestedSequences.length === observedBySequence.size && new Set(attestedSequences).size === attestedSequences.length && attestedSequences.every((s) => observedBySequence.has(s));
|
|
700
|
+
if (!sameSet) {
|
|
701
|
+
ctx.addIssue({
|
|
702
|
+
code: "custom",
|
|
703
|
+
message: "attested closes must match the observed closes one for one, by sequence",
|
|
704
|
+
path: ["attested", "closes"]
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
record2.attested.closes.forEach((close, i) => {
|
|
708
|
+
if (close.decision === "override" !== (close.reason !== null)) {
|
|
709
|
+
ctx.addIssue({
|
|
710
|
+
code: "custom",
|
|
711
|
+
message: "reason is given exactly when the decision is override",
|
|
712
|
+
path: ["attested", "closes", i, "reason"]
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
const observed = observedBySequence.get(close.sequence);
|
|
716
|
+
if (observed !== void 0 && observed.closedBy.kind === "account-removed" !== (close.actor === null)) {
|
|
717
|
+
ctx.addIssue({
|
|
718
|
+
code: "custom",
|
|
719
|
+
message: "actor is empty exactly when the closing account was removed",
|
|
720
|
+
path: ["attested", "closes", i, "actor"]
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// ../events/dist/engagement-lifecycle.js
|
|
727
|
+
var utcIso86012 = z5.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");
|
|
728
|
+
var engagementTypeSchema = z5.enum(["client"]);
|
|
729
|
+
var lifecycleStatusSchema = z5.enum(["active", "delivering", "closed"]);
|
|
730
|
+
var createEngagementRequestSchema = z5.object({
|
|
731
|
+
name: z5.string(),
|
|
732
|
+
region: z5.string(),
|
|
733
|
+
engagementType: engagementTypeSchema
|
|
734
|
+
}).strict();
|
|
735
|
+
var createEngagementResponseSchema = z5.object({
|
|
736
|
+
engagementId: z5.string(),
|
|
737
|
+
name: z5.string(),
|
|
738
|
+
region: z5.string(),
|
|
739
|
+
status: lifecycleStatusSchema,
|
|
740
|
+
createdAt: utcIso86012,
|
|
741
|
+
sessionToken: z5.string()
|
|
742
|
+
}).strict();
|
|
743
|
+
var engagementStatusResponseSchema = z5.object({
|
|
744
|
+
engagementId: z5.string(),
|
|
745
|
+
name: z5.string(),
|
|
746
|
+
region: z5.string(),
|
|
747
|
+
status: lifecycleStatusSchema,
|
|
748
|
+
installLinked: z5.boolean(),
|
|
749
|
+
createdAt: utcIso86012,
|
|
750
|
+
updatedAt: utcIso86012
|
|
751
|
+
}).strict();
|
|
752
|
+
var linkInstallationRequestSchema = z5.object({
|
|
753
|
+
installationId: z5.string(),
|
|
754
|
+
engagementId: z5.string()
|
|
755
|
+
}).strict();
|
|
756
|
+
var startBuildRequestSchema = z5.object({
|
|
757
|
+
engagementId: z5.string()
|
|
758
|
+
}).strict();
|
|
759
|
+
var startBuildResponseSchema = z5.object({
|
|
760
|
+
engagementId: z5.string(),
|
|
761
|
+
status: lifecycleStatusSchema,
|
|
762
|
+
enqueuedAt: utcIso86012
|
|
763
|
+
}).strict();
|
|
764
|
+
var acceptanceOutcomeSchema = z5.object({
|
|
765
|
+
verdict: walkVerdictSchema,
|
|
766
|
+
findings: z5.number().int().nonnegative(),
|
|
767
|
+
findingsDetail: z5.array(z5.object({ summary: z5.string().refine((v) => !hasLoneSurrogate(v), { message: LONE_SURROGATE_MESSAGE }) }).strict())
|
|
768
|
+
}).strict();
|
|
769
|
+
var phaseCloseDecisionSchema = z5.discriminatedUnion("decision", [
|
|
770
|
+
z5.object({
|
|
771
|
+
decision: z5.literal(closeDecisionSchema.enum["done-when-met"]),
|
|
772
|
+
/** Who is closing the phase. */
|
|
773
|
+
actor: closeActorSchema
|
|
774
|
+
}).strict(),
|
|
775
|
+
z5.object({
|
|
776
|
+
decision: z5.literal(closeDecisionSchema.enum.override),
|
|
777
|
+
/** Who is closing it anyway. */
|
|
778
|
+
actor: closeActorSchema,
|
|
779
|
+
/**
|
|
780
|
+
* Why, in their own words. Bounded: the reason is kept on the phase's
|
|
781
|
+
* permanent record of its own close, and that record refuses anything
|
|
782
|
+
* longer — so an over-long reason is refused here, before the close is
|
|
783
|
+
* attempted, rather than failing the close itself.
|
|
784
|
+
*/
|
|
785
|
+
reason: z5.string().max(PHASE_CLOSE_REASON_MAX).refine((v) => !hasLoneSurrogate(v), { message: LONE_SURROGATE_MESSAGE })
|
|
786
|
+
}).strict()
|
|
787
|
+
]);
|
|
788
|
+
var acceptPhaseRequestSchema = z5.object({
|
|
789
|
+
phase: phaseNameSchema,
|
|
790
|
+
acceptanceOutcome: acceptanceOutcomeSchema,
|
|
791
|
+
close: phaseCloseDecisionSchema.optional(),
|
|
792
|
+
statements: closeStatementsSchema.optional()
|
|
793
|
+
}).strict();
|
|
794
|
+
var coeFindingRequestSchema = z5.object({
|
|
795
|
+
phase: z5.string(),
|
|
796
|
+
description: z5.string(),
|
|
797
|
+
addRateTag: z5.enum(["seam-new", "seam-repeat", "model-limitation"]),
|
|
798
|
+
mappedPatternRef: z5.string().nullable()
|
|
799
|
+
}).strict();
|
|
800
|
+
var coeFindingResponseSchema = z5.object({
|
|
801
|
+
findingId: z5.string(),
|
|
802
|
+
engagementId: z5.string(),
|
|
803
|
+
phase: z5.string(),
|
|
804
|
+
description: z5.string(),
|
|
805
|
+
addRateTag: z5.enum(["seam-new", "seam-repeat", "model-limitation"]),
|
|
806
|
+
mappedPatternRef: z5.string().nullable(),
|
|
807
|
+
createdAt: utcIso86012
|
|
808
|
+
}).strict();
|
|
809
|
+
var operatorFindingResponseSchema = z5.object({
|
|
810
|
+
// The finding just recorded.
|
|
811
|
+
finding: coeFindingResponseSchema,
|
|
812
|
+
// Every finding recorded for that phase, oldest first, including this one.
|
|
813
|
+
phaseFindings: z5.array(coeFindingResponseSchema),
|
|
814
|
+
// Whether the phase's finding rate was recalculated to include this finding.
|
|
815
|
+
figuresRecomputed: z5.boolean()
|
|
816
|
+
}).strict();
|
|
817
|
+
var phaseNotOnRecordRefusalSchema = wireErrorSchema.extend({
|
|
818
|
+
// Always this value for this refusal.
|
|
819
|
+
error: z5.literal("PhaseNotOnRecord"),
|
|
820
|
+
// The phases that can take a finding, sorted.
|
|
821
|
+
phases: z5.array(z5.string())
|
|
822
|
+
}).strict();
|
|
823
|
+
var phaseEntryDecisionSchema = z5.discriminatedUnion("decision", [
|
|
824
|
+
z5.object({
|
|
825
|
+
decision: z5.literal("entry-condition-met"),
|
|
826
|
+
/** Who attests. */
|
|
827
|
+
actor: z5.string(),
|
|
828
|
+
/** What makes the entry condition hold, in the attester's own words. */
|
|
829
|
+
evidence: z5.string()
|
|
830
|
+
}).strict(),
|
|
831
|
+
z5.object({
|
|
832
|
+
decision: z5.literal("override"),
|
|
833
|
+
/** Who is opening it anyway. */
|
|
834
|
+
actor: z5.string(),
|
|
835
|
+
/** Why, in the operator's own words. */
|
|
836
|
+
reason: z5.string()
|
|
837
|
+
}).strict()
|
|
838
|
+
]);
|
|
839
|
+
var PHASE_ENTRY_DECISION_KINDS = [
|
|
840
|
+
"entry-condition-met",
|
|
841
|
+
"override"
|
|
842
|
+
];
|
|
843
|
+
var PHASE_CLOSE_DECISION_KINDS = ["phase-closed", "phase-close-override"];
|
|
844
|
+
var phaseDecisionKindSchema = z5.enum([
|
|
845
|
+
...PHASE_ENTRY_DECISION_KINDS,
|
|
846
|
+
...PHASE_CLOSE_DECISION_KINDS
|
|
847
|
+
]);
|
|
848
|
+
var openPhaseRequestSchema = z5.object({
|
|
849
|
+
phase: phaseNameSchema.nullable(),
|
|
850
|
+
entry: phaseEntryDecisionSchema
|
|
851
|
+
}).strict();
|
|
852
|
+
var phaseEntryDecisionRecordSchema = z5.object({
|
|
853
|
+
decisionId: z5.string(),
|
|
854
|
+
decision: z5.enum(PHASE_ENTRY_DECISION_KINDS),
|
|
855
|
+
actor: z5.string(),
|
|
856
|
+
/** The evidence (attestation) or the reason (override) — one basis per row. */
|
|
857
|
+
justification: z5.string(),
|
|
858
|
+
blockingFinding: z5.string().nullable(),
|
|
859
|
+
decidedAt: utcIso86012
|
|
860
|
+
}).strict();
|
|
861
|
+
var openPhaseResponseSchema = z5.object({
|
|
862
|
+
engagement: engagementStatusResponseSchema,
|
|
863
|
+
/** What is open now. `null` is project scope. */
|
|
864
|
+
phase: z5.string().nullable(),
|
|
865
|
+
/** What was open before this decision. `null` is project scope. */
|
|
866
|
+
previousPhase: z5.string().nullable(),
|
|
867
|
+
decision: phaseEntryDecisionRecordSchema
|
|
868
|
+
}).strict();
|
|
869
|
+
|
|
648
870
|
// ../events/dist/crew.js
|
|
649
871
|
import { z as z6 } from "zod";
|
|
650
872
|
var crewGroupSchema = z6.enum(["builder", "reviewer", "always-on"]);
|
|
@@ -841,7 +1063,7 @@ var CREW_CALLSIGNS = CREW_ROSTER.map((member) => member.callsign);
|
|
|
841
1063
|
|
|
842
1064
|
// ../events/dist/device-auth.js
|
|
843
1065
|
import { z as z7 } from "zod";
|
|
844
|
-
var
|
|
1066
|
+
var utcIso86013 = z7.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");
|
|
845
1067
|
var DEVICE_AUTH_STATUS = {
|
|
846
1068
|
/** The poll: minted, nobody has acted yet. Keep polling. 200. */
|
|
847
1069
|
PENDING: "pending",
|
|
@@ -886,7 +1108,7 @@ var deviceAuthStartResponseSchema = z7.object({
|
|
|
886
1108
|
deviceCode: z7.string().min(1),
|
|
887
1109
|
userCode: z7.string().min(1),
|
|
888
1110
|
verificationUrl: z7.string().min(1),
|
|
889
|
-
expiresAt:
|
|
1111
|
+
expiresAt: utcIso86013,
|
|
890
1112
|
pollIntervalMs: z7.number().int().positive()
|
|
891
1113
|
}).strict();
|
|
892
1114
|
var devicePollPendingSchema = z7.object({
|
|
@@ -897,7 +1119,7 @@ var devicePollApprovedSchema = z7.object({
|
|
|
897
1119
|
status: z7.literal(DEVICE_AUTH_STATUS.APPROVED),
|
|
898
1120
|
accountId: z7.string().min(1),
|
|
899
1121
|
credential: z7.string().min(1),
|
|
900
|
-
expiresAt:
|
|
1122
|
+
expiresAt: utcIso86013
|
|
901
1123
|
}).strict();
|
|
902
1124
|
var deviceAuthRefusalSchema = z7.object({
|
|
903
1125
|
status: z7.enum([
|
|
@@ -961,86 +1183,25 @@ function loopbackVerificationUrl(verificationUrl, target) {
|
|
|
961
1183
|
import { z as z8 } from "zod";
|
|
962
1184
|
var NOT_THIS_ACCOUNT_REASON = "not-this-account";
|
|
963
1185
|
var JOIN_REFUSAL_STATUS = 403;
|
|
964
|
-
var notThisAccountRefusalSchema = z8.object({
|
|
965
|
-
statusCode: z8.literal(JOIN_REFUSAL_STATUS),
|
|
966
|
-
error: z8.string(),
|
|
967
|
-
message: z8.string(),
|
|
968
|
-
reason: z8.literal(NOT_THIS_ACCOUNT_REASON)
|
|
969
|
-
}).strict();
|
|
970
|
-
var joinRefusalSchema = z8.union([notThisAccountRefusalSchema, wireErrorSchema]);
|
|
971
|
-
function isNotThisAccountRefusal(body) {
|
|
972
|
-
return typeof body === "object" && body !== null && body.reason === NOT_THIS_ACCOUNT_REASON;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// ../events/dist/account-identity.js
|
|
976
|
-
import { z as z9 } from "zod";
|
|
977
|
-
var accountIdentitySchema = z9.object({
|
|
978
|
-
accountId: z9.string().min(1),
|
|
979
|
-
email: z9.string().min(1).optional()
|
|
980
|
-
}).strict();
|
|
981
|
-
function safeParseAccountIdentity(payload) {
|
|
982
|
-
return accountIdentitySchema.safeParse(payload);
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
// ../events/dist/phase-identity.js
|
|
986
|
-
var PHASE_SEGMENT_PREFIX = "phase-";
|
|
987
|
-
function isValidPhaseIdentity(identity) {
|
|
988
|
-
if (typeof identity === "number")
|
|
989
|
-
return Number.isInteger(identity) && identity >= 0;
|
|
990
|
-
if (typeof identity !== "string")
|
|
991
|
-
return false;
|
|
992
|
-
if (identity === "")
|
|
993
|
-
return false;
|
|
994
|
-
if (identity.includes("/"))
|
|
995
|
-
return false;
|
|
996
|
-
if (identity.includes("\\"))
|
|
997
|
-
return false;
|
|
998
|
-
if (identity.includes("\0"))
|
|
999
|
-
return false;
|
|
1000
|
-
if (identity.startsWith("."))
|
|
1001
|
-
return false;
|
|
1002
|
-
return true;
|
|
1003
|
-
}
|
|
1004
|
-
var InvalidPhaseIdentityError = class extends Error {
|
|
1005
|
-
/** The value that was refused, exactly as supplied. */
|
|
1006
|
-
identity;
|
|
1007
|
-
constructor(identity, allowedLocation) {
|
|
1008
|
-
const subject = allowedLocation === void 0 ? "a file" : "a Build Record";
|
|
1009
|
-
const where = allowedLocation === void 0 ? "" : `
|
|
1010
|
-
Records are written only into ${allowedLocation.replace(/\\/g, "/")}/. Nothing was written.`;
|
|
1011
|
-
super(`that phase identity cannot name ${subject}.
|
|
1012
|
-
identity: ${describeIdentity(identity)}` + where);
|
|
1013
|
-
this.name = "InvalidPhaseIdentityError";
|
|
1014
|
-
this.identity = identity;
|
|
1015
|
-
}
|
|
1016
|
-
};
|
|
1017
|
-
function assertValidPhaseIdentity(identity, allowedLocation) {
|
|
1018
|
-
if (!isValidPhaseIdentity(identity)) {
|
|
1019
|
-
throw new InvalidPhaseIdentityError(identity, allowedLocation);
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
function renderPhaseSegment(identity) {
|
|
1023
|
-
if (!isValidPhaseIdentity(identity)) {
|
|
1024
|
-
throw new InvalidPhaseIdentityError(identity);
|
|
1025
|
-
}
|
|
1026
|
-
return `${PHASE_SEGMENT_PREFIX}${identity}`;
|
|
1186
|
+
var notThisAccountRefusalSchema = z8.object({
|
|
1187
|
+
statusCode: z8.literal(JOIN_REFUSAL_STATUS),
|
|
1188
|
+
error: z8.string(),
|
|
1189
|
+
message: z8.string(),
|
|
1190
|
+
reason: z8.literal(NOT_THIS_ACCOUNT_REASON)
|
|
1191
|
+
}).strict();
|
|
1192
|
+
var joinRefusalSchema = z8.union([notThisAccountRefusalSchema, wireErrorSchema]);
|
|
1193
|
+
function isNotThisAccountRefusal(body) {
|
|
1194
|
+
return typeof body === "object" && body !== null && body.reason === NOT_THIS_ACCOUNT_REASON;
|
|
1027
1195
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
if (typeof identity === "number" || typeof identity === "boolean")
|
|
1038
|
-
return String(identity);
|
|
1039
|
-
if (Array.isArray(identity))
|
|
1040
|
-
return "(a list)";
|
|
1041
|
-
if (typeof identity === "object")
|
|
1042
|
-
return "(an object)";
|
|
1043
|
-
return `(a ${typeof identity})`;
|
|
1196
|
+
|
|
1197
|
+
// ../events/dist/account-identity.js
|
|
1198
|
+
import { z as z9 } from "zod";
|
|
1199
|
+
var accountIdentitySchema = z9.object({
|
|
1200
|
+
accountId: z9.string().min(1),
|
|
1201
|
+
email: z9.string().min(1).optional()
|
|
1202
|
+
}).strict();
|
|
1203
|
+
function safeParseAccountIdentity(payload) {
|
|
1204
|
+
return accountIdentitySchema.safeParse(payload);
|
|
1044
1205
|
}
|
|
1045
1206
|
|
|
1046
1207
|
// ../events/dist/credential-wire.js
|
|
@@ -1054,10 +1215,47 @@ var engagementCredentialResponseSchema = z10.object({
|
|
|
1054
1215
|
}).strict();
|
|
1055
1216
|
var ciTokenExchangeResponseSchema = z10.object({
|
|
1056
1217
|
token: z10.string(),
|
|
1057
|
-
expiresAt:
|
|
1218
|
+
expiresAt: utcIso8601,
|
|
1058
1219
|
engagementId: z10.string()
|
|
1059
1220
|
}).strict();
|
|
1060
1221
|
|
|
1222
|
+
// ../events/dist/phase-record.js
|
|
1223
|
+
import { z as z11 } from "zod";
|
|
1224
|
+
var phaseRecordRefusalReasonSchema = z11.enum([
|
|
1225
|
+
"still-open",
|
|
1226
|
+
"reopened",
|
|
1227
|
+
"never-opened",
|
|
1228
|
+
"never-closed",
|
|
1229
|
+
"closed-before-records"
|
|
1230
|
+
]);
|
|
1231
|
+
var phaseRecordRecordSchema = z11.object({
|
|
1232
|
+
outcome: z11.literal("record"),
|
|
1233
|
+
phase: phaseNameSchema,
|
|
1234
|
+
record: buildRecordV2Schema
|
|
1235
|
+
}).strict().refine((body) => body.record.phase === body.phase, {
|
|
1236
|
+
message: "the record is for a different phase than the one asked for",
|
|
1237
|
+
path: ["record", "phase"]
|
|
1238
|
+
});
|
|
1239
|
+
var phaseRecordWithheldSchema = z11.object({
|
|
1240
|
+
outcome: z11.literal("withheld"),
|
|
1241
|
+
phase: phaseNameSchema,
|
|
1242
|
+
sequence: z11.number().int().min(1),
|
|
1243
|
+
closedAt: utcIso8601,
|
|
1244
|
+
message: z11.string()
|
|
1245
|
+
}).strict();
|
|
1246
|
+
var phaseRecordRefusedSchema = wireErrorSchema.extend({
|
|
1247
|
+
outcome: z11.literal("refused"),
|
|
1248
|
+
phase: phaseNameSchema,
|
|
1249
|
+
statusCode: z11.literal(409),
|
|
1250
|
+
error: z11.literal("PhaseRecordRefused"),
|
|
1251
|
+
reason: phaseRecordRefusalReasonSchema
|
|
1252
|
+
}).strict();
|
|
1253
|
+
var phaseRecordResponseSchema = z11.discriminatedUnion("outcome", [
|
|
1254
|
+
phaseRecordRecordSchema,
|
|
1255
|
+
phaseRecordWithheldSchema,
|
|
1256
|
+
phaseRecordRefusedSchema
|
|
1257
|
+
]);
|
|
1258
|
+
|
|
1061
1259
|
// dist/engagement-credential.js
|
|
1062
1260
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1063
1261
|
import { platform as platform2 } from "node:os";
|
|
@@ -2989,12 +3187,12 @@ function escapeHtml(value) {
|
|
|
2989
3187
|
}
|
|
2990
3188
|
function completionPage(result, engagementId) {
|
|
2991
3189
|
const approved = result === LOOPBACK_RESULT.APPROVED;
|
|
2992
|
-
const
|
|
3190
|
+
const headline2 = approved ? "Signed in. You can close this tab and return to your terminal." : "Sign-in declined. Nothing was created. You can close this tab.";
|
|
2993
3191
|
const onward = approved ? onwardTarget(engagementId) : null;
|
|
2994
3192
|
const href = onward === null ? "" : escapeHtml(onward.href);
|
|
2995
3193
|
const refresh = onward === null ? "" : `<meta http-equiv="refresh" content="${COMPLETION_REDIRECT_SECONDS};url=${href}">`;
|
|
2996
3194
|
const successLink = onward === null ? "" : `<p><a href="${href}" style="color:#19d3ff">${escapeHtml(onward.label)}</a></p>`;
|
|
2997
|
-
return '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="referrer" content="no-referrer">' + refresh + `<title>Halfcycle</title></head><body style="margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#0a0a0a;color:#fafafa;font:16px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif"><div style="max-width:26rem;padding:2rem;text-align:center"><p>${
|
|
3195
|
+
return '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="referrer" content="no-referrer">' + refresh + `<title>Halfcycle</title></head><body style="margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#0a0a0a;color:#fafafa;font:16px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif"><div style="max-width:26rem;padding:2rem;text-align:center"><p>${headline2}</p>${successLink}</div></body></html>`;
|
|
2998
3196
|
}
|
|
2999
3197
|
function isLoopbackPeer(remoteAddress) {
|
|
3000
3198
|
if (remoteAddress === void 0)
|
|
@@ -4220,193 +4418,250 @@ async function probeMcpOrigin(mcpUrl) {
|
|
|
4220
4418
|
return { reached: true, serverName: name };
|
|
4221
4419
|
}
|
|
4222
4420
|
|
|
4223
|
-
// dist/build-record/
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
const seed = {
|
|
4227
|
-
patternRef: fired.patternRef,
|
|
4228
|
-
severity: fired.severity
|
|
4229
|
-
};
|
|
4230
|
-
if (includeExplanation && fired.explanation)
|
|
4231
|
-
seed.explanation = fired.explanation;
|
|
4232
|
-
return seed;
|
|
4233
|
-
}
|
|
4234
|
-
|
|
4235
|
-
// dist/build-record/sources.js
|
|
4236
|
-
import { readFileSync as readFileSync6, existsSync as existsSync4 } from "node:fs";
|
|
4237
|
-
import { createHash as createHash3 } from "node:crypto";
|
|
4238
|
-
import { join as join8 } from "node:path";
|
|
4421
|
+
// dist/build-record/write.js
|
|
4422
|
+
import { closeSync, existsSync as existsSync4, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync6, renameSync, rmSync as rmSync2, writeSync } from "node:fs";
|
|
4423
|
+
import { basename as basename2, join as join6, resolve as resolve2, relative as relative2, isAbsolute as isAbsolute2 } from "node:path";
|
|
4239
4424
|
|
|
4240
|
-
// dist/build-record/
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
}
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
}
|
|
4256
|
-
function resolveCloseAtHead(repoRoot) {
|
|
4257
|
-
try {
|
|
4258
|
-
const closeCommit = execFileSync3("git", ["-C", repoRoot, "rev-parse", "--short", "HEAD"], {
|
|
4259
|
-
encoding: "utf-8",
|
|
4260
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
4261
|
-
}).trim();
|
|
4262
|
-
const closedDate = execFileSync3("git", ["-C", repoRoot, "show", "-s", "--format=%cs", "HEAD"], {
|
|
4263
|
-
encoding: "utf-8",
|
|
4264
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
4265
|
-
}).trim();
|
|
4266
|
-
if (closeCommit === "" || closedDate === "")
|
|
4267
|
-
return null;
|
|
4268
|
-
return { closeCommit, closedDate };
|
|
4269
|
-
} catch {
|
|
4270
|
-
return null;
|
|
4425
|
+
// dist/build-record/render-v2.js
|
|
4426
|
+
var HIDDEN = /[\p{Cc}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}]/u;
|
|
4427
|
+
var MARKDOWN_SPECIAL = /* @__PURE__ */ new Set(["\\", "`", "*", "_", "[", "]", "<", "|", "~", "#", "&", "$"]);
|
|
4428
|
+
function printRecordText(text) {
|
|
4429
|
+
if (text === "")
|
|
4430
|
+
return "(blank)";
|
|
4431
|
+
let out = "";
|
|
4432
|
+
for (const ch of text) {
|
|
4433
|
+
if (HIDDEN.test(ch)) {
|
|
4434
|
+
out += `U+${ch.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}`;
|
|
4435
|
+
} else if (MARKDOWN_SPECIAL.has(ch)) {
|
|
4436
|
+
out += `\\${ch}`;
|
|
4437
|
+
} else {
|
|
4438
|
+
out += ch;
|
|
4439
|
+
}
|
|
4271
4440
|
}
|
|
4441
|
+
return out;
|
|
4272
4442
|
}
|
|
4273
|
-
function
|
|
4274
|
-
let
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
path,
|
|
4285
|
-
reason: `the commit could not be read from ${repoRoot} \u2014 there is no git available, no repository there, or no commit in it yet`
|
|
4286
|
-
};
|
|
4287
|
-
}
|
|
4288
|
-
const record2 = {
|
|
4289
|
-
format: CLOSE_RECORD_FORMAT,
|
|
4290
|
-
phase,
|
|
4291
|
-
closeCommit: at.closeCommit,
|
|
4292
|
-
closedDate: at.closedDate
|
|
4443
|
+
function makePrinter() {
|
|
4444
|
+
let hidden = false;
|
|
4445
|
+
return {
|
|
4446
|
+
text(value) {
|
|
4447
|
+
if (!hidden && HIDDEN.test(value))
|
|
4448
|
+
hidden = true;
|
|
4449
|
+
return printRecordText(value);
|
|
4450
|
+
},
|
|
4451
|
+
sawHidden() {
|
|
4452
|
+
return hidden;
|
|
4453
|
+
}
|
|
4293
4454
|
};
|
|
4294
|
-
try {
|
|
4295
|
-
mkdirSync5(dirname3(path), { recursive: true });
|
|
4296
|
-
writeFileSync5(path, JSON.stringify(record2, null, 2) + "\n", "utf-8");
|
|
4297
|
-
} catch (err) {
|
|
4298
|
-
return { recorded: false, path, reason: err instanceof Error ? err.message : String(err) };
|
|
4299
|
-
}
|
|
4300
|
-
return { recorded: true, path, closeCommit: at.closeCommit, closedDate: at.closedDate };
|
|
4301
4455
|
}
|
|
4302
|
-
function
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
if (
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4456
|
+
function counted(n, one, many) {
|
|
4457
|
+
return `${String(n)} ${n === 1 ? one : many}`;
|
|
4458
|
+
}
|
|
4459
|
+
var TITLE_PREFIX = "# Build Record: ";
|
|
4460
|
+
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.";
|
|
4461
|
+
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.";
|
|
4462
|
+
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.";
|
|
4463
|
+
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.";
|
|
4464
|
+
var OBSERVED_LABEL = "**Observed by Halfcycle**";
|
|
4465
|
+
var STATED_LABEL = "**Stated by the client** \u2014 Halfcycle did not check these.";
|
|
4466
|
+
var MISMATCHED_HALVES = "This Build Record could not be written: its two parts describe different closes.";
|
|
4467
|
+
function renderChecks(checks, p) {
|
|
4468
|
+
const blocks = ["## Checks during this phase", OBSERVED_LABEL];
|
|
4469
|
+
if (checks.state === "not-observed") {
|
|
4470
|
+
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.");
|
|
4471
|
+
return blocks;
|
|
4472
|
+
}
|
|
4473
|
+
const runs = counted(checks.runs, "check run", "check runs");
|
|
4474
|
+
if (checks.state === "nothing-fired") {
|
|
4475
|
+
blocks.push(`**Checks ran and found nothing.** Halfcycle recorded ${runs} during this phase, and no check found a problem.`);
|
|
4476
|
+
} else {
|
|
4477
|
+
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.`);
|
|
4478
|
+
blocks.push(renderFired(checks.fired, p));
|
|
4319
4479
|
}
|
|
4320
|
-
if (
|
|
4321
|
-
|
|
4480
|
+
if (checks.couldNotRun.length === 0) {
|
|
4481
|
+
blocks.push("No check reported that it could not run.");
|
|
4482
|
+
} else {
|
|
4483
|
+
blocks.push("**Some checks could not run.** They neither passed nor failed during this phase:");
|
|
4484
|
+
blocks.push(renderCouldNotRun(checks.couldNotRun, p));
|
|
4322
4485
|
}
|
|
4323
|
-
return
|
|
4324
|
-
format: CLOSE_RECORD_FORMAT,
|
|
4325
|
-
phase: record2.phase,
|
|
4326
|
-
closeCommit: commit,
|
|
4327
|
-
closedDate: date
|
|
4328
|
-
};
|
|
4486
|
+
return blocks;
|
|
4329
4487
|
}
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4488
|
+
function renderFired(fired, p) {
|
|
4489
|
+
const lines = [];
|
|
4490
|
+
for (const entry of fired) {
|
|
4491
|
+
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)}`);
|
|
4492
|
+
lines.push(` - Seriousness: ${seriousness(entry.severity)}`);
|
|
4493
|
+
lines.push(` - Found: ${counted(entry.occurrences, "time", "times")} during this phase.`);
|
|
4494
|
+
lines.push(` - What it found: ${entry.explanation === null ? "Not included in this record." : p.text(entry.explanation)}`);
|
|
4495
|
+
lines.push(` - The name your terminal prints for this check: ${p.text(entry.check)}`);
|
|
4496
|
+
}
|
|
4497
|
+
return lines.join("\n");
|
|
4338
4498
|
}
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4499
|
+
function seriousness(severity) {
|
|
4500
|
+
switch (severity) {
|
|
4501
|
+
case "block":
|
|
4502
|
+
return "serious, it blocks the work.";
|
|
4503
|
+
case "warn":
|
|
4504
|
+
return "a warning.";
|
|
4505
|
+
case "info":
|
|
4506
|
+
return "for information only.";
|
|
4507
|
+
}
|
|
4345
4508
|
}
|
|
4346
|
-
function
|
|
4347
|
-
|
|
4509
|
+
function renderCouldNotRun(entries, p) {
|
|
4510
|
+
const lines = [];
|
|
4511
|
+
for (const entry of entries) {
|
|
4512
|
+
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)}`);
|
|
4513
|
+
lines.push(` - Why it could not run: ${p.text(entry.reason)}`);
|
|
4514
|
+
lines.push(` - The name your terminal prints for this check: ${p.text(entry.check)}`);
|
|
4515
|
+
}
|
|
4516
|
+
return lines.join("\n");
|
|
4348
4517
|
}
|
|
4349
|
-
function
|
|
4350
|
-
const
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4518
|
+
function headline(observed, attested) {
|
|
4519
|
+
const met = attested.decision === "done-when-met";
|
|
4520
|
+
switch (observed.crossed.kind) {
|
|
4521
|
+
case "none":
|
|
4522
|
+
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.";
|
|
4523
|
+
case "unfinished-steps":
|
|
4524
|
+
case "clean-over-findings":
|
|
4525
|
+
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.";
|
|
4526
|
+
case "not-recorded":
|
|
4527
|
+
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.";
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
function closedAtText(closedAt, p) {
|
|
4531
|
+
const m = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(?:\.\d+)?Z$/.exec(closedAt);
|
|
4532
|
+
return m === null ? p.text(closedAt) : `${m[1]} ${m[2]} UTC`;
|
|
4533
|
+
}
|
|
4534
|
+
function signedInAs(closedBy, p) {
|
|
4535
|
+
switch (closedBy.kind) {
|
|
4536
|
+
case "account":
|
|
4537
|
+
return closedBy.email === null ? "an account with no email address on file" : `the account ${p.text(closedBy.email)}`;
|
|
4538
|
+
case "no-account":
|
|
4539
|
+
return "a credential not linked to any person's account";
|
|
4540
|
+
case "account-removed":
|
|
4541
|
+
return "an account that has since been deleted";
|
|
4542
|
+
case "not-recorded":
|
|
4543
|
+
return "not recorded for this close";
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
function outstanding(crossed, p) {
|
|
4547
|
+
switch (crossed.kind) {
|
|
4548
|
+
case "none":
|
|
4549
|
+
return ["- Outstanding work Halfcycle could see: none."];
|
|
4550
|
+
case "not-recorded":
|
|
4551
|
+
return ["- Outstanding work Halfcycle could see: not recorded for this close."];
|
|
4552
|
+
case "clean-over-findings":
|
|
4553
|
+
return [
|
|
4554
|
+
`- Outstanding work Halfcycle could see: ${counted(crossed.findings, "problem", "problems")} recorded against this phase.`
|
|
4555
|
+
];
|
|
4556
|
+
case "unfinished-steps":
|
|
4557
|
+
return [
|
|
4558
|
+
`- 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:`,
|
|
4559
|
+
...crossed.steps.map((step) => ` - Step: ${p.text(step.stepId)} \u2014 ${step.state === "failed" ? "failed" : "set aside unfinished"}`)
|
|
4560
|
+
];
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
function machines(coverage) {
|
|
4564
|
+
const label = "- Machines signed in on this project at the close: ";
|
|
4565
|
+
if (coverage === null)
|
|
4566
|
+
return `${label}not recorded.`;
|
|
4567
|
+
if (coverage.enrolled === 0)
|
|
4568
|
+
return `${label}none.`;
|
|
4569
|
+
return `${label}${String(coverage.enrolled)}, of which ${String(coverage.conformant)} had Halfcycle's checks working as set up.`;
|
|
4570
|
+
}
|
|
4571
|
+
function walkLines(walk, p) {
|
|
4572
|
+
const label = "> - Acceptance walk (the finished work tried as a user would, by someone other than its builder): ";
|
|
4573
|
+
if (walk === null)
|
|
4574
|
+
return [`${label}none reported.`];
|
|
4575
|
+
const lines = [
|
|
4576
|
+
`${label}${walk.verdict === "clean" ? "reported as finding no problems." : "reported as finding problems."}`
|
|
4577
|
+
];
|
|
4578
|
+
walk.findings.forEach((finding, i) => {
|
|
4579
|
+
lines.push(`> - Finding ${String(i + 1)}: ${p.text(finding)}`);
|
|
4580
|
+
});
|
|
4581
|
+
if (walk.findingsNotShown > 0) {
|
|
4582
|
+
lines.push(`> - And ${counted(walk.findingsNotShown, "more finding", "more findings")} not shown here.`);
|
|
4354
4583
|
}
|
|
4355
|
-
return
|
|
4584
|
+
return lines;
|
|
4585
|
+
}
|
|
4586
|
+
function statedOrNot(value, p) {
|
|
4587
|
+
return value === null ? "Not stated by the client." : p.text(value);
|
|
4356
4588
|
}
|
|
4357
|
-
function
|
|
4358
|
-
const { phase } = record2;
|
|
4359
|
-
const companion = `${renderPhaseSegment(phase.id)}.json`;
|
|
4360
|
-
const guardsList = record2.seedGuards.map(severityLabel).join(", ");
|
|
4589
|
+
function renderStated(attested, p) {
|
|
4361
4590
|
const lines = [
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
""
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
`**Engagement:** ${record2.engagement} (${record2.engagementType})`,
|
|
4370
|
-
`**Phase:** ${phase.id} \u2014 ${phase.name}`,
|
|
4371
|
-
renderCloseLine(record2),
|
|
4372
|
-
`**Companion:** [\`${companion}\`](${companion})`,
|
|
4373
|
-
"",
|
|
4374
|
-
renderCloseProvenance(record2),
|
|
4375
|
-
"",
|
|
4376
|
-
"## What this phase proved",
|
|
4377
|
-
"",
|
|
4378
|
-
`${record2.thesis} **Thesis held: ${record2.thesisHeld ? "yes" : "no"}.**`,
|
|
4379
|
-
"",
|
|
4380
|
-
"## Delivered",
|
|
4381
|
-
"",
|
|
4382
|
-
`- **Packages:** ${record2.delivered.packages.join(", ")}`,
|
|
4383
|
-
`- **Services:** ${record2.delivered.services.join(", ")}`,
|
|
4384
|
-
`- **Tools:** ${record2.delivered.tools.join(", ")}`,
|
|
4385
|
-
`- **Dogfood:** ${record2.delivered.dogfood}`,
|
|
4386
|
-
"",
|
|
4387
|
-
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.",
|
|
4388
|
-
"",
|
|
4389
|
-
`## Acceptance (independent walk \u2014 walker ${record2.acceptance.walker})`,
|
|
4390
|
-
"",
|
|
4391
|
-
`**Verdict:** ${record2.acceptance.verdict} \xB7 **Findings:** ${record2.acceptance.findings}`,
|
|
4392
|
-
"",
|
|
4393
|
-
"| Done-when | Result | Evidence |",
|
|
4394
|
-
"|---|---|---|",
|
|
4395
|
-
renderDoneWhenRows(record2),
|
|
4396
|
-
""
|
|
4591
|
+
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.",
|
|
4592
|
+
`> - Name given: ${attested.actor === null ? "removed with the account that made this close" : p.text(attested.actor)}`,
|
|
4593
|
+
`> - Reason given: ${attested.reason === null ? "none." : p.text(attested.reason)}`,
|
|
4594
|
+
...walkLines(attested.walk, p),
|
|
4595
|
+
`> - Code closed at (commit): ${statedOrNot(attested.closeCommit, p)}`,
|
|
4596
|
+
`> - What the phase set out to prove: ${statedOrNot(attested.statements.thesis, p)}`,
|
|
4597
|
+
`> - What came out of it: ${statedOrNot(attested.statements.outcome, p)}`
|
|
4397
4598
|
];
|
|
4398
|
-
|
|
4399
|
-
|
|
4599
|
+
const deviations = attested.statements.deviations;
|
|
4600
|
+
if (deviations.length === 0) {
|
|
4601
|
+
lines.push("> - Changes from the plan: None stated by the client.");
|
|
4602
|
+
} else {
|
|
4603
|
+
lines.push("> - Changes from the plan:");
|
|
4604
|
+
deviations.forEach((deviation, i) => {
|
|
4605
|
+
lines.push(`> - Change ${String(i + 1)}: ${p.text(deviation)}`);
|
|
4606
|
+
});
|
|
4400
4607
|
}
|
|
4401
|
-
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"}`, "");
|
|
4402
4608
|
return lines.join("\n");
|
|
4403
4609
|
}
|
|
4610
|
+
function renderClose(heading, observed, attested, p) {
|
|
4611
|
+
return [
|
|
4612
|
+
heading,
|
|
4613
|
+
headline(observed, attested),
|
|
4614
|
+
OBSERVED_LABEL,
|
|
4615
|
+
[
|
|
4616
|
+
`- Closed at: ${closedAtText(observed.closedAt, p)}`,
|
|
4617
|
+
`- Signed in as: ${signedInAs(observed.closedBy, p)}`,
|
|
4618
|
+
...outstanding(observed.crossed, p),
|
|
4619
|
+
machines(observed.seatCoverage)
|
|
4620
|
+
].join("\n"),
|
|
4621
|
+
STATED_LABEL,
|
|
4622
|
+
renderStated(attested, p)
|
|
4623
|
+
];
|
|
4624
|
+
}
|
|
4625
|
+
function renderBuildRecordV2Markdown(record2) {
|
|
4626
|
+
const p = makePrinter();
|
|
4627
|
+
const attestedBySequence = /* @__PURE__ */ new Map();
|
|
4628
|
+
for (const close of record2.attested.closes)
|
|
4629
|
+
attestedBySequence.set(close.sequence, close);
|
|
4630
|
+
const observedSequences = new Set(record2.observed.closes.map((c) => c.sequence));
|
|
4631
|
+
if (attestedBySequence.size !== record2.attested.closes.length || observedSequences.size !== record2.observed.closes.length || attestedBySequence.size !== observedSequences.size || [...observedSequences].some((s) => !attestedBySequence.has(s))) {
|
|
4632
|
+
throw new Error(MISMATCHED_HALVES);
|
|
4633
|
+
}
|
|
4634
|
+
const phase = p.text(record2.phase);
|
|
4635
|
+
const closes = [...record2.observed.closes].sort((a, b) => a.sequence - b.sequence);
|
|
4636
|
+
const total = closes.length;
|
|
4637
|
+
const latest = closes[total - 1];
|
|
4638
|
+
const body = [
|
|
4639
|
+
`Phase: ${phase} (the name the client gave it)`,
|
|
4640
|
+
OBSERVED_LABEL,
|
|
4641
|
+
[
|
|
4642
|
+
`- 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.`,
|
|
4643
|
+
`- Halfcycle's reference for this project: ${p.text(record2.observed.engagementId)}`,
|
|
4644
|
+
`- Record format: ${p.text(record2.format)}`
|
|
4645
|
+
].join("\n"),
|
|
4646
|
+
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.`,
|
|
4647
|
+
...renderChecks(record2.observed.checks, p),
|
|
4648
|
+
...renderClose(total === 1 ? "## The close" : `## The latest close (close ${String(latest.sequence)} of ${String(total)})`, latest, attestedBySequence.get(latest.sequence), p)
|
|
4649
|
+
];
|
|
4650
|
+
if (total > 1) {
|
|
4651
|
+
body.push("## Earlier closes");
|
|
4652
|
+
for (const close of closes.slice(0, -1)) {
|
|
4653
|
+
body.push(...renderClose(`### Close ${String(close.sequence)} of ${String(total)}`, close, attestedBySequence.get(close.sequence), p));
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
const header = [`${TITLE_PREFIX}${phase}`, HEADER_WHAT, HEADER_REGISTERS, HEADER_KEPT];
|
|
4657
|
+
if (p.sawHidden())
|
|
4658
|
+
header.push(HEADER_HIDDEN);
|
|
4659
|
+
return `${[...header, ...body].join("\n\n")}
|
|
4660
|
+
`;
|
|
4661
|
+
}
|
|
4404
4662
|
|
|
4405
4663
|
// dist/build-record/write.js
|
|
4406
|
-
var BUILD_RECORD_DIR =
|
|
4407
|
-
function serialiseRecordJson(record2) {
|
|
4408
|
-
return JSON.stringify(record2, null, 2) + "\n";
|
|
4409
|
-
}
|
|
4664
|
+
var BUILD_RECORD_DIR = join6("docs", "build-records");
|
|
4410
4665
|
function assertInsideOutDir(outDir, candidate, identity) {
|
|
4411
4666
|
const rel = relative2(outDir, resolve2(candidate));
|
|
4412
4667
|
const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute2(rel) && rel === basename2(rel);
|
|
@@ -4414,230 +4669,221 @@ function assertInsideOutDir(outDir, candidate, identity) {
|
|
|
4414
4669
|
throw new InvalidPhaseIdentityError(identity, BUILD_RECORD_DIR);
|
|
4415
4670
|
}
|
|
4416
4671
|
}
|
|
4417
|
-
function
|
|
4418
|
-
const
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4672
|
+
function createExclusiveOnDisk(path, text) {
|
|
4673
|
+
const fd = openSync(path, "wx");
|
|
4674
|
+
try {
|
|
4675
|
+
writeSync(fd, text);
|
|
4676
|
+
} catch (err) {
|
|
4677
|
+
closeSync(fd);
|
|
4678
|
+
rmSync2(path, { force: true });
|
|
4679
|
+
throw err;
|
|
4422
4680
|
}
|
|
4423
|
-
|
|
4424
|
-
const jsonPath = join7(outDir, `${segment}.json`);
|
|
4425
|
-
const mdPath = join7(outDir, `${segment}.md`);
|
|
4426
|
-
assertInsideOutDir(outDir, jsonPath, identity);
|
|
4427
|
-
assertInsideOutDir(outDir, mdPath, identity);
|
|
4428
|
-
mkdirSync6(outDir, { recursive: true });
|
|
4429
|
-
writeFileSync6(jsonPath, serialiseRecordJson(record2), "utf-8");
|
|
4430
|
-
writeFileSync6(mdPath, renderBuildRecordMarkdown(record2), "utf-8");
|
|
4431
|
-
return { jsonPath, mdPath };
|
|
4681
|
+
closeSync(fd);
|
|
4432
4682
|
}
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
}
|
|
4683
|
+
function renameOnDisk(from, to) {
|
|
4684
|
+
renameSync(from, to);
|
|
4685
|
+
}
|
|
4686
|
+
var NODE_RECORD_PAIR_IO = {
|
|
4687
|
+
createExclusive: createExclusiveOnDisk,
|
|
4688
|
+
rename: renameOnDisk
|
|
4440
4689
|
};
|
|
4441
|
-
function
|
|
4442
|
-
|
|
4443
|
-
for (const run of runs) {
|
|
4444
|
-
if (!run.result)
|
|
4445
|
-
continue;
|
|
4446
|
-
for (const fired of run.result.guardsFired) {
|
|
4447
|
-
const seed = projectSeedGuard(
|
|
4448
|
-
fired,
|
|
4449
|
-
/* includeExplanation */
|
|
4450
|
-
false
|
|
4451
|
-
);
|
|
4452
|
-
const existing = byRef.get(seed.patternRef);
|
|
4453
|
-
if (!existing) {
|
|
4454
|
-
byRef.set(seed.patternRef, seed);
|
|
4455
|
-
} else if (existing.severity !== seed.severity) {
|
|
4456
|
-
byRef.set(seed.patternRef, pickHigherSeverity(existing, seed));
|
|
4457
|
-
}
|
|
4458
|
-
}
|
|
4459
|
-
}
|
|
4460
|
-
return [...byRef.values()].sort((a, b) => a.patternRef.localeCompare(b.patternRef));
|
|
4690
|
+
function serialiseServedRecord(rawRecord) {
|
|
4691
|
+
return JSON.stringify(rawRecord, null, 2) + "\n";
|
|
4461
4692
|
}
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
return (SEVERITY_ORDINAL[a.severity] ?? -1) >= (SEVERITY_ORDINAL[b.severity] ?? -1) ? a : b;
|
|
4693
|
+
function isEexist(err) {
|
|
4694
|
+
return typeof err === "object" && err !== null && err.code === "EEXIST";
|
|
4465
4695
|
}
|
|
4466
|
-
function
|
|
4467
|
-
|
|
4468
|
-
return
|
|
4696
|
+
function readIfPresent(path) {
|
|
4697
|
+
try {
|
|
4698
|
+
return readFileSync6(path, "utf-8");
|
|
4699
|
+
} catch {
|
|
4700
|
+
return null;
|
|
4469
4701
|
}
|
|
4470
|
-
const record2 = parsed;
|
|
4471
|
-
if (record2["outcome"] !== void 0)
|
|
4472
|
-
return parsed;
|
|
4473
|
-
return {
|
|
4474
|
-
...record2,
|
|
4475
|
-
runId: record2["runId"] ?? syntheticRunId(record2),
|
|
4476
|
-
outcome: "evaluated"
|
|
4477
|
-
};
|
|
4478
4702
|
}
|
|
4479
|
-
function
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
].join("|");
|
|
4486
|
-
const hex = createHash3("sha256").update(`legacy-guard-eval-run:${key}`).digest("hex");
|
|
4487
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
4488
|
-
}
|
|
4489
|
-
function readGuardEvalLog(logDir, phaseId) {
|
|
4490
|
-
const logPath = join8(logDir, `${renderPhaseSegment(phaseId)}.jsonl`);
|
|
4491
|
-
if (!existsSync4(logPath)) {
|
|
4492
|
-
throw new MissingSourceError("guard-eval log", `no runner guard-eval log at ${logPath} for phase ${phaseId}`);
|
|
4493
|
-
}
|
|
4494
|
-
const raw = readFileSync6(logPath, "utf-8");
|
|
4495
|
-
const runs = [];
|
|
4496
|
-
for (const line of raw.split("\n")) {
|
|
4497
|
-
const trimmed = line.trim();
|
|
4498
|
-
if (trimmed === "")
|
|
4499
|
-
continue;
|
|
4500
|
-
const parsed = JSON.parse(trimmed);
|
|
4501
|
-
runs.push(guardEvalRunSchema.parse(normalizeLegacyLine(parsed)));
|
|
4703
|
+
function isOwnEarlierRecord(existingText, served) {
|
|
4704
|
+
let parsedJson;
|
|
4705
|
+
try {
|
|
4706
|
+
parsedJson = JSON.parse(existingText);
|
|
4707
|
+
} catch {
|
|
4708
|
+
return false;
|
|
4502
4709
|
}
|
|
4503
|
-
|
|
4710
|
+
const existing = buildRecordV2Schema.safeParse(parsedJson);
|
|
4711
|
+
if (!existing.success)
|
|
4712
|
+
return false;
|
|
4713
|
+
const old = existing.data;
|
|
4714
|
+
if (old.observed.engagementId !== served.observed.engagementId)
|
|
4715
|
+
return false;
|
|
4716
|
+
if (old.phase !== served.phase)
|
|
4717
|
+
return false;
|
|
4718
|
+
const oldCloses = old.observed.closes.length;
|
|
4719
|
+
const newCloses = served.observed.closes.length;
|
|
4720
|
+
if (oldCloses < newCloses)
|
|
4721
|
+
return true;
|
|
4722
|
+
if (oldCloses > newCloses)
|
|
4723
|
+
return false;
|
|
4724
|
+
const oldSequences = new Set(old.observed.closes.map((c) => c.sequence));
|
|
4725
|
+
return served.observed.closes.every((c) => oldSequences.has(c.sequence));
|
|
4504
4726
|
}
|
|
4505
|
-
function
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4727
|
+
function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_PAIR_IO) {
|
|
4728
|
+
if (!isValidPhaseIdentity(phase)) {
|
|
4729
|
+
throw new InvalidPhaseIdentityError(phase, BUILD_RECORD_DIR);
|
|
4730
|
+
}
|
|
4731
|
+
const outDir = resolve2(repoRoot, BUILD_RECORD_DIR);
|
|
4732
|
+
const segment = renderPhaseSegment(phase);
|
|
4733
|
+
const jsonPath = join6(outDir, `${segment}.json`);
|
|
4734
|
+
const mdPath = join6(outDir, `${segment}.md`);
|
|
4735
|
+
const jsonTmp = `${jsonPath}.tmp`;
|
|
4736
|
+
const mdTmp = `${mdPath}.tmp`;
|
|
4737
|
+
for (const candidate of [jsonPath, mdPath, jsonTmp, mdTmp]) {
|
|
4738
|
+
assertInsideOutDir(outDir, candidate, phase);
|
|
4739
|
+
}
|
|
4740
|
+
const jsonText = serialiseServedRecord(rawRecord);
|
|
4741
|
+
const mdText = renderBuildRecordV2Markdown(record2);
|
|
4742
|
+
mkdirSync5(outDir, { recursive: true });
|
|
4743
|
+
let createdJson = false;
|
|
4744
|
+
try {
|
|
4745
|
+
io.createExclusive(jsonPath, jsonText);
|
|
4746
|
+
createdJson = true;
|
|
4747
|
+
io.createExclusive(mdPath, mdText);
|
|
4748
|
+
return { kind: "created", jsonPath, mdPath };
|
|
4749
|
+
} catch (err) {
|
|
4750
|
+
if (createdJson)
|
|
4751
|
+
rmSync2(jsonPath, { force: true });
|
|
4752
|
+
if (!isEexist(err))
|
|
4753
|
+
throw err;
|
|
4509
4754
|
}
|
|
4510
|
-
const
|
|
4511
|
-
if (
|
|
4512
|
-
|
|
4755
|
+
const existingJson = readIfPresent(jsonPath);
|
|
4756
|
+
if (existingJson === null) {
|
|
4757
|
+
return { kind: "in-the-way", path: existsSync4(jsonPath) ? jsonPath : mdPath };
|
|
4513
4758
|
}
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
throw new MissingSourceError("orchestration state", `task ${id} has no status`);
|
|
4518
|
-
}
|
|
4519
|
-
tasks[id] = { status: t.status };
|
|
4759
|
+
if (existingJson === jsonText) {
|
|
4760
|
+
replaceOne(io, mdTmp, mdPath, mdText);
|
|
4761
|
+
return { kind: "up-to-date", jsonPath, mdPath };
|
|
4520
4762
|
}
|
|
4521
|
-
|
|
4522
|
-
}
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
if (!existsSync4(path))
|
|
4526
|
-
return { closeCommit: null, closedDate: null };
|
|
4527
|
-
let parsed;
|
|
4763
|
+
if (!isOwnEarlierRecord(existingJson, record2)) {
|
|
4764
|
+
return { kind: "in-the-way", path: jsonPath };
|
|
4765
|
+
}
|
|
4766
|
+
replaceOne(io, jsonTmp, jsonPath, jsonText);
|
|
4528
4767
|
try {
|
|
4529
|
-
|
|
4768
|
+
replaceOne(io, mdTmp, mdPath, mdText);
|
|
4530
4769
|
} catch (err) {
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4770
|
+
try {
|
|
4771
|
+
replaceOne(io, jsonTmp, jsonPath, existingJson);
|
|
4772
|
+
} catch {
|
|
4773
|
+
rmSync2(jsonPath, { force: true });
|
|
4774
|
+
}
|
|
4775
|
+
throw err;
|
|
4536
4776
|
}
|
|
4537
|
-
return {
|
|
4777
|
+
return { kind: "replaced", jsonPath, mdPath };
|
|
4538
4778
|
}
|
|
4539
|
-
function
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
}
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
req(narrated.phase?.name, "phase.name");
|
|
4548
|
-
assertValidPhaseIdentity(narrated.phase?.id, BUILD_RECORD_DIR);
|
|
4549
|
-
req(narrated.status, "status");
|
|
4550
|
-
req(narrated.thesis, "thesis");
|
|
4551
|
-
req(narrated.delivered, "delivered");
|
|
4552
|
-
req(narrated.acceptance, "acceptance");
|
|
4553
|
-
req(narrated.acceptance?.verdict, "acceptance.verdict");
|
|
4554
|
-
req(narrated.gatesEarnedKeep, "gatesEarnedKeep");
|
|
4555
|
-
req(narrated.instruments, "instruments");
|
|
4556
|
-
if (!Array.isArray(narrated.deviations)) {
|
|
4557
|
-
throw new MissingSourceError("narrated artefact", "deviations must be an array");
|
|
4558
|
-
}
|
|
4559
|
-
return narrated;
|
|
4560
|
-
}
|
|
4561
|
-
function loadSources(opts) {
|
|
4562
|
-
const orchestration = readOrchestrationState(opts.phasesDir, opts.phaseId);
|
|
4563
|
-
const guardEvalRuns = readGuardEvalLog(opts.guardEvalLogDir, opts.phaseId);
|
|
4564
|
-
const close = readPhaseClose(opts.repoRoot, opts.phaseId);
|
|
4565
|
-
const narrated = validateNarrated(opts.narrated);
|
|
4566
|
-
if (!Array.isArray(opts.touchedInvariants)) {
|
|
4567
|
-
throw new MissingSourceError("feature-spec Touches", "touchedInvariants must be an array");
|
|
4779
|
+
function replaceOne(io, tmp, finalPath, text) {
|
|
4780
|
+
rmSync2(tmp, { force: true });
|
|
4781
|
+
try {
|
|
4782
|
+
io.createExclusive(tmp, text);
|
|
4783
|
+
io.rename(tmp, finalPath);
|
|
4784
|
+
} catch (err) {
|
|
4785
|
+
rmSync2(tmp, { force: true });
|
|
4786
|
+
throw err;
|
|
4568
4787
|
}
|
|
4569
|
-
return {
|
|
4570
|
-
orchestration,
|
|
4571
|
-
narrated,
|
|
4572
|
-
close,
|
|
4573
|
-
guardEvalRuns,
|
|
4574
|
-
touchedInvariants: opts.touchedInvariants
|
|
4575
|
-
};
|
|
4576
4788
|
}
|
|
4577
4789
|
|
|
4578
|
-
// dist/build-record/
|
|
4579
|
-
|
|
4580
|
-
var
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
}
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
for (const dw of Object.values(acceptance.doneWhen)) {
|
|
4591
|
-
if (dw.result === "dropped")
|
|
4592
|
-
continue;
|
|
4593
|
-
if (dw.result !== "pass")
|
|
4594
|
-
return false;
|
|
4595
|
-
}
|
|
4596
|
-
return true;
|
|
4790
|
+
// dist/build-record/obtain.js
|
|
4791
|
+
import { relative as relative3 } from "node:path";
|
|
4792
|
+
var REFUSAL_FOLLOW_UP = {
|
|
4793
|
+
"still-open": "Nothing was written. The record is produced when the phase closes.",
|
|
4794
|
+
"reopened": "Nothing was written. A new record is produced when the phase closes again.",
|
|
4795
|
+
"never-opened": "Nothing was written. Check the name: it must match the phase name exactly as it was opened.",
|
|
4796
|
+
"never-closed": "Nothing was written.",
|
|
4797
|
+
"closed-before-records": "Nothing was written."
|
|
4798
|
+
};
|
|
4799
|
+
var REFUSAL_REASONS = phaseRecordRefusalReasonSchema.options;
|
|
4800
|
+
function buildRecordUrl(credential, phase) {
|
|
4801
|
+
return `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/phases/${encodeURIComponent(phase)}/build-record`;
|
|
4597
4802
|
}
|
|
4598
|
-
function
|
|
4599
|
-
|
|
4803
|
+
function isMissingRoute(status, body) {
|
|
4804
|
+
if (status !== 404 || typeof body !== "object" || body === null)
|
|
4805
|
+
return false;
|
|
4806
|
+
const { error, message } = body;
|
|
4807
|
+
return error === "Not Found" && typeof message === "string" && /^Route GET .* not found\.$/s.test(message);
|
|
4600
4808
|
}
|
|
4601
|
-
function
|
|
4602
|
-
const
|
|
4809
|
+
async function obtainBuildRecord(credential, repoRoot, phase, io = NODE_RECORD_PAIR_IO) {
|
|
4810
|
+
const url = buildRecordUrl(credential, phase);
|
|
4811
|
+
let res;
|
|
4812
|
+
let text;
|
|
4813
|
+
try {
|
|
4814
|
+
res = await fetch(url, {
|
|
4815
|
+
method: "GET",
|
|
4816
|
+
headers: { authorization: `Bearer ${credential.token}` }
|
|
4817
|
+
});
|
|
4818
|
+
text = await res.text();
|
|
4819
|
+
} catch (err) {
|
|
4820
|
+
return {
|
|
4821
|
+
kind: "unavailable",
|
|
4822
|
+
why: `could not reach the Halfcycle service at ${url}: ${err instanceof Error ? err.message : String(err)}.`
|
|
4823
|
+
};
|
|
4824
|
+
}
|
|
4825
|
+
let raw = null;
|
|
4826
|
+
try {
|
|
4827
|
+
raw = JSON.parse(text);
|
|
4828
|
+
} catch {
|
|
4829
|
+
raw = null;
|
|
4830
|
+
}
|
|
4831
|
+
const parsed = phaseRecordResponseSchema.safeParse(raw);
|
|
4832
|
+
if (parsed.success) {
|
|
4833
|
+
const answer = parsed.data;
|
|
4834
|
+
if (answer.phase !== phase) {
|
|
4835
|
+
return {
|
|
4836
|
+
kind: "unavailable",
|
|
4837
|
+
why: `the service answered for a different phase than the one asked for, so nothing was written.`
|
|
4838
|
+
};
|
|
4839
|
+
}
|
|
4840
|
+
if (answer.outcome === "withheld")
|
|
4841
|
+
return { kind: "withheld", message: answer.message };
|
|
4842
|
+
if (answer.outcome === "refused") {
|
|
4843
|
+
return { kind: "refused", reason: answer.reason, message: answer.message };
|
|
4844
|
+
}
|
|
4845
|
+
const rawRecord = raw.record;
|
|
4846
|
+
let result;
|
|
4847
|
+
try {
|
|
4848
|
+
result = writeRecordPair(repoRoot, phase, rawRecord, answer.record, io);
|
|
4849
|
+
} catch (err) {
|
|
4850
|
+
return {
|
|
4851
|
+
kind: "unavailable",
|
|
4852
|
+
why: `the record arrived but could not be written: ${err instanceof Error ? err.message : String(err)}. Nothing was left half written.`
|
|
4853
|
+
};
|
|
4854
|
+
}
|
|
4855
|
+
if (result.kind === "in-the-way") {
|
|
4856
|
+
return { kind: "exists", path: relative3(repoRoot, result.path) };
|
|
4857
|
+
}
|
|
4858
|
+
return {
|
|
4859
|
+
kind: "written",
|
|
4860
|
+
jsonPath: relative3(repoRoot, result.jsonPath),
|
|
4861
|
+
mdPath: relative3(repoRoot, result.mdPath)
|
|
4862
|
+
};
|
|
4863
|
+
}
|
|
4864
|
+
if (isMissingRoute(res.status, raw)) {
|
|
4865
|
+
return { kind: "unavailable", why: "this Halfcycle service does not provide Build Records yet." };
|
|
4866
|
+
}
|
|
4867
|
+
const message = typeof raw === "object" && raw !== null && typeof raw.message === "string" ? raw.message : null;
|
|
4868
|
+
if (res.ok) {
|
|
4869
|
+
return {
|
|
4870
|
+
kind: "unavailable",
|
|
4871
|
+
why: `the service's answer was not a Build Record this version of the CLI can read, so nothing was written.`
|
|
4872
|
+
};
|
|
4873
|
+
}
|
|
4603
4874
|
return {
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
engagementType: narrated.engagementType,
|
|
4607
|
-
phase: { id: narrated.phase.id, name: narrated.phase.name },
|
|
4608
|
-
status: narrated.status,
|
|
4609
|
-
closedDate: close.closedDate,
|
|
4610
|
-
closeCommit: close.closeCommit,
|
|
4611
|
-
thesis: narrated.thesis,
|
|
4612
|
-
thesisHeld: deriveThesisHeld(narrated.acceptance),
|
|
4613
|
-
delivered: narrated.delivered,
|
|
4614
|
-
seedGuards: projectSeedGuards(sources.guardEvalRuns),
|
|
4615
|
-
acceptance: narrated.acceptance,
|
|
4616
|
-
gatesEarnedKeep: narrated.gatesEarnedKeep,
|
|
4617
|
-
instruments: narrated.instruments,
|
|
4618
|
-
invariantsExercised: deriveInvariantsExercised(sources.touchedInvariants),
|
|
4619
|
-
deviations: narrated.deviations,
|
|
4620
|
-
tasks: deriveTasks(orchestration)
|
|
4875
|
+
kind: "unavailable",
|
|
4876
|
+
why: `the service answered ${res.status}${message === null ? "." : `: ${message}`}`
|
|
4621
4877
|
};
|
|
4622
4878
|
}
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
const sources = loadSources({
|
|
4627
|
-
repoRoot: opts.repoRoot,
|
|
4628
|
-
phasesDir: opts.phasesDir,
|
|
4629
|
-
guardEvalLogDir: opts.guardEvalLogDir,
|
|
4630
|
-
phaseId: opts.phaseId,
|
|
4631
|
-
narrated: opts.narrated,
|
|
4632
|
-
touchedInvariants: opts.touchedInvariants
|
|
4633
|
-
});
|
|
4634
|
-
const record2 = assembleBuildRecord(sources);
|
|
4635
|
-
return writeBuildRecord(opts.repoRoot, record2);
|
|
4879
|
+
function refetchCommand(phase) {
|
|
4880
|
+
const quoted = /^[A-Za-z0-9._+-]+$/.test(phase) ? phase : `'${phase.replace(/'/g, `'\\''`)}'`;
|
|
4881
|
+
return `npx halfcycle@latest build-record ${quoted}`;
|
|
4636
4882
|
}
|
|
4637
4883
|
|
|
4638
4884
|
// dist/open-phase.js
|
|
4639
4885
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4640
|
-
import { join as
|
|
4886
|
+
import { join as join7 } from "node:path";
|
|
4641
4887
|
var OpenPhaseRefused = class extends Error {
|
|
4642
4888
|
status;
|
|
4643
4889
|
finding;
|
|
@@ -4658,7 +4904,7 @@ function readRepoCredential(repoRoot, env = process.env, home) {
|
|
|
4658
4904
|
if (pinned !== null)
|
|
4659
4905
|
assertEngagementId(pinned);
|
|
4660
4906
|
const fromStore = pinned === null ? null : readEngagementEnv(pinned, home);
|
|
4661
|
-
const looked = pinned === null ? `${
|
|
4907
|
+
const looked = pinned === null ? `${join7(repoRoot, ".halfcycle", "bundle.json")} (which names no engagement)` : engagementEnvPath(pinned, home);
|
|
4662
4908
|
const values = {};
|
|
4663
4909
|
for (const key of OPEN_PHASE_ENV_KEYS) {
|
|
4664
4910
|
const value = (env[key] ?? fromStore?.[key] ?? "").trim();
|
|
@@ -4675,7 +4921,7 @@ function readRepoCredential(repoRoot, env = process.env, home) {
|
|
|
4675
4921
|
}
|
|
4676
4922
|
function readPinnedEngagementId(repoRoot) {
|
|
4677
4923
|
try {
|
|
4678
|
-
const pin = JSON.parse(readFileSync7(
|
|
4924
|
+
const pin = JSON.parse(readFileSync7(join7(repoRoot, ".halfcycle", "bundle.json"), "utf-8"));
|
|
4679
4925
|
const id = pin[PIN_ENGAGEMENT_ID_FIELD];
|
|
4680
4926
|
return typeof id === "string" && id !== "" ? id : null;
|
|
4681
4927
|
} catch {
|
|
@@ -4739,6 +4985,35 @@ async function openPhase(credential, phase, entry, home) {
|
|
|
4739
4985
|
}
|
|
4740
4986
|
|
|
4741
4987
|
// dist/close-phase.js
|
|
4988
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
4989
|
+
var NO_STATEMENTS = { thesis: null, outcome: null, deviations: [] };
|
|
4990
|
+
function readCloseCommit(repoRoot) {
|
|
4991
|
+
if (repoRoot === void 0) {
|
|
4992
|
+
return {
|
|
4993
|
+
read: false,
|
|
4994
|
+
reason: "no repository was named for this close, so there was nowhere to read a commit from"
|
|
4995
|
+
};
|
|
4996
|
+
}
|
|
4997
|
+
let out;
|
|
4998
|
+
try {
|
|
4999
|
+
out = execFileSync3("git", ["-C", repoRoot, "rev-parse", "HEAD"], {
|
|
5000
|
+
encoding: "utf-8",
|
|
5001
|
+
env: gitEnvForNamedRepo(),
|
|
5002
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5003
|
+
}).trim();
|
|
5004
|
+
} catch (err) {
|
|
5005
|
+
const missingGit = typeof err === "object" && err !== null && err.code === "ENOENT";
|
|
5006
|
+
return {
|
|
5007
|
+
read: false,
|
|
5008
|
+
reason: missingGit ? "git is not available on this machine" : "this folder is not a git repository, or it has no commit yet"
|
|
5009
|
+
};
|
|
5010
|
+
}
|
|
5011
|
+
const checked = closeCommitSchema.safeParse(out);
|
|
5012
|
+
if (!checked.success) {
|
|
5013
|
+
return { read: false, reason: "git did not answer with a full commit hash" };
|
|
5014
|
+
}
|
|
5015
|
+
return { read: true, commit: checked.data };
|
|
5016
|
+
}
|
|
4742
5017
|
var ClosePhaseRefused = class extends Error {
|
|
4743
5018
|
status;
|
|
4744
5019
|
finding;
|
|
@@ -4751,14 +5026,28 @@ var ClosePhaseRefused = class extends Error {
|
|
|
4751
5026
|
this.name = "ClosePhaseRefused";
|
|
4752
5027
|
}
|
|
4753
5028
|
};
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
5029
|
+
var CloseRequestInvalid = class extends Error {
|
|
5030
|
+
constructor(message) {
|
|
5031
|
+
super(message);
|
|
5032
|
+
this.name = "CloseRequestInvalid";
|
|
4758
5033
|
}
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
5034
|
+
};
|
|
5035
|
+
async function closePhase(credential, phase, outcome, close, home, repoRoot, stated = NO_STATEMENTS) {
|
|
5036
|
+
const closeCommit = readCloseCommit(repoRoot);
|
|
5037
|
+
const body = {
|
|
5038
|
+
phase,
|
|
5039
|
+
acceptanceOutcome: outcome,
|
|
5040
|
+
close,
|
|
5041
|
+
statements: {
|
|
5042
|
+
thesis: stated.thesis,
|
|
5043
|
+
outcome: stated.outcome,
|
|
5044
|
+
deviations: stated.deviations,
|
|
5045
|
+
closeCommit: closeCommit.read ? closeCommit.commit : null
|
|
5046
|
+
}
|
|
5047
|
+
};
|
|
5048
|
+
const parsed = acceptPhaseRequestSchema.safeParse(body);
|
|
5049
|
+
if (!parsed.success) {
|
|
5050
|
+
throw new CloseRequestInvalid(`that close is not one this platform accepts, so nothing was sent: ${parsed.error.issues.map((i) => `${describeIssuePath(i.path)}: ${i.message}`).join("; ")}.`);
|
|
4762
5051
|
}
|
|
4763
5052
|
const url = `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/accept`;
|
|
4764
5053
|
let res;
|
|
@@ -4769,7 +5058,7 @@ async function closePhase(credential, phase, outcome, close, home, repoRoot) {
|
|
|
4769
5058
|
"content-type": "application/json",
|
|
4770
5059
|
authorization: `Bearer ${credential.token}`
|
|
4771
5060
|
},
|
|
4772
|
-
body: JSON.stringify(
|
|
5061
|
+
body: JSON.stringify(body)
|
|
4773
5062
|
});
|
|
4774
5063
|
} catch (err) {
|
|
4775
5064
|
throw new Error(`[halfcycle] Could not reach the Halfcycle service at ${url}: ${err instanceof Error ? err.message : String(err)}. The phase was not closed.`);
|
|
@@ -4781,17 +5070,24 @@ async function closePhase(credential, phase, outcome, close, home, repoRoot) {
|
|
|
4781
5070
|
const code = typeof body2?.error === "string" ? body2.error : void 0;
|
|
4782
5071
|
throw new ClosePhaseRefused(res.status, message, finding, code);
|
|
4783
5072
|
}
|
|
4784
|
-
const
|
|
4785
|
-
if (!
|
|
5073
|
+
const answer = await res.json().catch(() => null);
|
|
5074
|
+
if (!answer || typeof answer.engagementId !== "string" || typeof answer.status !== "string") {
|
|
4786
5075
|
throw new Error(`[halfcycle] The response from ${url} did not carry { engagementId, status }. The phase may or may not have closed \u2014 read the engagement's status before retrying.`);
|
|
4787
5076
|
}
|
|
4788
|
-
const closeRecord = repoRoot === void 0 ? {
|
|
4789
|
-
recorded: false,
|
|
4790
|
-
path: null,
|
|
4791
|
-
reason: "no repository was named for this close, so there was nowhere to read a commit from"
|
|
4792
|
-
} : writeCloseRecord(repoRoot, phase);
|
|
4793
5077
|
const stampFailure = writePhaseStamp(credential.engagementId, null, home);
|
|
4794
|
-
return { engagementId:
|
|
5078
|
+
return { engagementId: answer.engagementId, status: answer.status, stampFailure, closeCommit };
|
|
5079
|
+
}
|
|
5080
|
+
function describeIssuePath(path) {
|
|
5081
|
+
const [head, field] = path;
|
|
5082
|
+
if (head === "statements" && field === "thesis")
|
|
5083
|
+
return "--thesis";
|
|
5084
|
+
if (head === "statements" && field === "outcome")
|
|
5085
|
+
return "--outcome";
|
|
5086
|
+
if (head === "statements" && field === "deviations")
|
|
5087
|
+
return "--deviation";
|
|
5088
|
+
if (head === "phase")
|
|
5089
|
+
return "the phase name";
|
|
5090
|
+
return path.map(String).join(".") || "(the close)";
|
|
4795
5091
|
}
|
|
4796
5092
|
|
|
4797
5093
|
// dist/cli-contract.js
|
|
@@ -4908,9 +5204,9 @@ var PLACEHOLDERS = {
|
|
|
4908
5204
|
};
|
|
4909
5205
|
|
|
4910
5206
|
// dist/uninstall.js
|
|
4911
|
-
import { lstatSync as lstatSync2, readFileSync as readFileSync8, readdirSync, rmSync as
|
|
5207
|
+
import { lstatSync as lstatSync2, readFileSync as readFileSync8, readdirSync, rmSync as rmSync3, rmdirSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4912
5208
|
import { homedir as homedir2 } from "node:os";
|
|
4913
|
-
import { dirname as
|
|
5209
|
+
import { dirname as dirname3, join as join8, relative as relative4, sep } from "node:path";
|
|
4914
5210
|
var REMOVE_CREDENTIAL_FLAG = "--remove-credential";
|
|
4915
5211
|
function parseUninstallArgs(args2) {
|
|
4916
5212
|
let removeCredential = false;
|
|
@@ -4936,7 +5232,7 @@ function lexists(path) {
|
|
|
4936
5232
|
function symlinkOnPath(root, rel) {
|
|
4937
5233
|
let current = root;
|
|
4938
5234
|
for (const segment of rel.split("/")) {
|
|
4939
|
-
current =
|
|
5235
|
+
current = join8(current, segment);
|
|
4940
5236
|
try {
|
|
4941
5237
|
if (lstatSync2(current).isSymbolicLink())
|
|
4942
5238
|
return true;
|
|
@@ -4957,7 +5253,7 @@ var Refusal = class extends Error {
|
|
|
4957
5253
|
function readMergedJson(targetRepo, rel) {
|
|
4958
5254
|
if (symlinkOnPath(targetRepo, rel))
|
|
4959
5255
|
return { state: "symlink" };
|
|
4960
|
-
const path =
|
|
5256
|
+
const path = join8(targetRepo, rel);
|
|
4961
5257
|
if (!lexists(path))
|
|
4962
5258
|
return { state: "absent" };
|
|
4963
5259
|
let value;
|
|
@@ -4973,8 +5269,8 @@ function readMergedJson(targetRepo, rel) {
|
|
|
4973
5269
|
}
|
|
4974
5270
|
function validate(options) {
|
|
4975
5271
|
const { targetRepo, removeCredential, home } = options;
|
|
4976
|
-
const manifestPath =
|
|
4977
|
-
const pinPath =
|
|
5272
|
+
const manifestPath = join8(targetRepo, INSTALL_MANIFEST_REL);
|
|
5273
|
+
const pinPath = join8(targetRepo, BUNDLE_PIN_REL);
|
|
4978
5274
|
if (symlinkOnPath(targetRepo, INSTALL_MANIFEST_REL) || symlinkOnPath(targetRepo, BUNDLE_PIN_REL)) {
|
|
4979
5275
|
throw new Refusal(`.halfcycle, or a file Halfcycle keeps in it, is a symbolic link here, so nothing was followed and nothing was removed.`);
|
|
4980
5276
|
}
|
|
@@ -5018,8 +5314,8 @@ function validate(options) {
|
|
|
5018
5314
|
throw new Refusal(`the project id in ${BUNDLE_PIN_REL} is not a valid Halfcycle project id, so it cannot name a folder on this machine. Nothing was removed, here or under ~/.halfcycle.`);
|
|
5019
5315
|
}
|
|
5020
5316
|
if (engagementId !== void 0) {
|
|
5021
|
-
const expectedParent =
|
|
5022
|
-
if (
|
|
5317
|
+
const expectedParent = join8(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME);
|
|
5318
|
+
if (dirname3(engagementStateDir(engagementId, home)) !== expectedParent) {
|
|
5023
5319
|
throw new Refusal(`the project id in ${BUNDLE_PIN_REL} does not name a folder under ~/.halfcycle/engagements. Nothing was removed.`);
|
|
5024
5320
|
}
|
|
5025
5321
|
}
|
|
@@ -5170,7 +5466,7 @@ function trimGitignore(targetRepo, text, rec, writeSet) {
|
|
|
5170
5466
|
}
|
|
5171
5467
|
function coveredPathExists(targetRepo, line) {
|
|
5172
5468
|
const path = line.trim().replace(/^\//, "").replace(/\/$/, "");
|
|
5173
|
-
return path !== "" && lexists(
|
|
5469
|
+
return path !== "" && lexists(join8(targetRepo, path));
|
|
5174
5470
|
}
|
|
5175
5471
|
function uninstall(options) {
|
|
5176
5472
|
const result = {
|
|
@@ -5228,7 +5524,7 @@ function uninstall(options) {
|
|
|
5228
5524
|
removeRecordedFile(targetRepo, entry, writeSet, result, name);
|
|
5229
5525
|
}
|
|
5230
5526
|
for (const path of manifest.leftAlone) {
|
|
5231
|
-
if (lexists(
|
|
5527
|
+
if (lexists(join8(targetRepo, path)))
|
|
5232
5528
|
name(result.leftAlone, path, path);
|
|
5233
5529
|
}
|
|
5234
5530
|
switch (removeLegacyCiStanza(targetRepo)) {
|
|
@@ -5260,10 +5556,10 @@ function uninstall(options) {
|
|
|
5260
5556
|
const pinEntry = manifest.files.find((f) => f.path === BUNDLE_PIN_REL);
|
|
5261
5557
|
if (pinEntry !== void 0)
|
|
5262
5558
|
removeRecordedFile(targetRepo, pinEntry, writeSet, result, name);
|
|
5263
|
-
else if (lexists(
|
|
5559
|
+
else if (lexists(join8(targetRepo, BUNDLE_PIN_REL)))
|
|
5264
5560
|
name(result.leftAlone, BUNDLE_PIN_REL, BUNDLE_PIN_REL);
|
|
5265
5561
|
try {
|
|
5266
|
-
|
|
5562
|
+
rmSync3(join8(targetRepo, INSTALL_MANIFEST_REL), { force: true });
|
|
5267
5563
|
named.add(INSTALL_MANIFEST_REL);
|
|
5268
5564
|
} catch {
|
|
5269
5565
|
name(result.failed, INSTALL_MANIFEST_REL, INSTALL_MANIFEST_REL);
|
|
@@ -5293,14 +5589,14 @@ function applyMergedJson(targetRepo, rel, file, lists, name, edit) {
|
|
|
5293
5589
|
const { changed, created, adopted, keptNote } = edit(value);
|
|
5294
5590
|
if (keptNote !== void 0)
|
|
5295
5591
|
name(lists.kept, keptNote, rel);
|
|
5296
|
-
const path =
|
|
5592
|
+
const path = join8(targetRepo, rel);
|
|
5297
5593
|
const empty = Object.keys(value).length === 0;
|
|
5298
5594
|
try {
|
|
5299
5595
|
if (empty && created && !adopted) {
|
|
5300
|
-
|
|
5596
|
+
rmSync3(path);
|
|
5301
5597
|
name(lists.removed, rel, rel);
|
|
5302
5598
|
} else if (changed) {
|
|
5303
|
-
|
|
5599
|
+
writeFileSync5(path, serializeJson(value), "utf-8");
|
|
5304
5600
|
name(lists.restored, rel, rel);
|
|
5305
5601
|
}
|
|
5306
5602
|
if (adopted && lexists(path)) {
|
|
@@ -5320,7 +5616,7 @@ function removeRecordedFile(targetRepo, entry, writeSet, lists, name) {
|
|
|
5320
5616
|
name(lists.leftAlone, `${path} (a symbolic link, or inside one \u2014 not followed)`, path);
|
|
5321
5617
|
return;
|
|
5322
5618
|
}
|
|
5323
|
-
const abs =
|
|
5619
|
+
const abs = join8(targetRepo, path);
|
|
5324
5620
|
let isFile;
|
|
5325
5621
|
try {
|
|
5326
5622
|
isFile = lstatSync2(abs).isFile();
|
|
@@ -5345,7 +5641,7 @@ function removeRecordedFile(targetRepo, entry, writeSet, lists, name) {
|
|
|
5345
5641
|
}
|
|
5346
5642
|
}
|
|
5347
5643
|
try {
|
|
5348
|
-
|
|
5644
|
+
rmSync3(abs);
|
|
5349
5645
|
name(lists.removed, path, path);
|
|
5350
5646
|
} catch {
|
|
5351
5647
|
name(lists.failed, path, path);
|
|
@@ -5354,7 +5650,7 @@ function removeRecordedFile(targetRepo, entry, writeSet, lists, name) {
|
|
|
5354
5650
|
function removeEmptyCreatedDir(targetRepo, dir, writeSet, lists, name) {
|
|
5355
5651
|
if (!writeSet.dirs.has(dir) || symlinkOnPath(targetRepo, dir))
|
|
5356
5652
|
return;
|
|
5357
|
-
const abs =
|
|
5653
|
+
const abs = join8(targetRepo, dir);
|
|
5358
5654
|
try {
|
|
5359
5655
|
if (!lstatSync2(abs).isDirectory() || readdirSync(abs).length > 0)
|
|
5360
5656
|
return;
|
|
@@ -5374,7 +5670,7 @@ function trimGitignoreFile(targetRepo, rec, writeSet, lists, name) {
|
|
|
5374
5670
|
name(lists.leftAlone, `${GITIGNORE_REL} (a symbolic link \u2014 not followed)`, GITIGNORE_REL);
|
|
5375
5671
|
return;
|
|
5376
5672
|
}
|
|
5377
|
-
const path =
|
|
5673
|
+
const path = join8(targetRepo, GITIGNORE_REL);
|
|
5378
5674
|
let before;
|
|
5379
5675
|
try {
|
|
5380
5676
|
before = readFileSync8(path, "utf-8");
|
|
@@ -5392,10 +5688,10 @@ function trimGitignoreFile(targetRepo, rec, writeSet, lists, name) {
|
|
|
5392
5688
|
return;
|
|
5393
5689
|
try {
|
|
5394
5690
|
if (text === "" && rec.created) {
|
|
5395
|
-
|
|
5691
|
+
rmSync3(path);
|
|
5396
5692
|
name(lists.removed, GITIGNORE_REL, GITIGNORE_REL);
|
|
5397
5693
|
} else {
|
|
5398
|
-
|
|
5694
|
+
writeFileSync5(path, text, "utf-8");
|
|
5399
5695
|
name(lists.restored, GITIGNORE_REL, GITIGNORE_REL);
|
|
5400
5696
|
}
|
|
5401
5697
|
} catch {
|
|
@@ -5421,8 +5717,8 @@ function handleCredential(options, checked) {
|
|
|
5421
5717
|
try {
|
|
5422
5718
|
if (lstatSync2(dir).isSymbolicLink())
|
|
5423
5719
|
return { kind: "failed", dir, reason: "it is a symbolic link" };
|
|
5424
|
-
|
|
5425
|
-
|
|
5720
|
+
rmSync3(engagementEnvPath(engagementId, home), { force: true });
|
|
5721
|
+
rmSync3(notThisAccountMarkerPath(engagementId, home), { force: true });
|
|
5426
5722
|
if (readdirSync(dir).length === 0)
|
|
5427
5723
|
rmdirSync(dir);
|
|
5428
5724
|
} catch (err) {
|
|
@@ -5435,16 +5731,16 @@ function filesUnder(root, rel) {
|
|
|
5435
5731
|
const walk = (dirRel) => {
|
|
5436
5732
|
let names;
|
|
5437
5733
|
try {
|
|
5438
|
-
if (!lstatSync2(
|
|
5734
|
+
if (!lstatSync2(join8(root, dirRel)).isDirectory())
|
|
5439
5735
|
return;
|
|
5440
|
-
names = readdirSync(
|
|
5736
|
+
names = readdirSync(join8(root, dirRel));
|
|
5441
5737
|
} catch {
|
|
5442
5738
|
return;
|
|
5443
5739
|
}
|
|
5444
5740
|
for (const entry of names.sort()) {
|
|
5445
5741
|
const childRel = `${dirRel}/${entry}`;
|
|
5446
5742
|
try {
|
|
5447
|
-
if (lstatSync2(
|
|
5743
|
+
if (lstatSync2(join8(root, childRel)).isDirectory())
|
|
5448
5744
|
walk(childRel);
|
|
5449
5745
|
else
|
|
5450
5746
|
out.push(childRel);
|
|
@@ -5458,7 +5754,7 @@ function filesUnder(root, rel) {
|
|
|
5458
5754
|
}
|
|
5459
5755
|
function shownPath(path, home) {
|
|
5460
5756
|
const base = home ?? homedir2();
|
|
5461
|
-
const rel =
|
|
5757
|
+
const rel = relative4(base, path);
|
|
5462
5758
|
const under = rel !== "" && !rel.startsWith("..") && !rel.startsWith(sep);
|
|
5463
5759
|
return `${under ? `~/${rel.split(sep).join("/")}` : path}/`;
|
|
5464
5760
|
}
|
|
@@ -5518,7 +5814,7 @@ function renderUninstallReport(result, home) {
|
|
|
5518
5814
|
// dist/banner-facts.js
|
|
5519
5815
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
5520
5816
|
import { readFileSync as readFileSync9 } from "node:fs";
|
|
5521
|
-
import { basename as basename3, join as
|
|
5817
|
+
import { basename as basename3, join as join9, resolve as resolve3 } from "node:path";
|
|
5522
5818
|
var BRAND_URL = "halfcycle.ai";
|
|
5523
5819
|
var TAGLINE = [
|
|
5524
5820
|
"Fewer cycles.",
|
|
@@ -5531,7 +5827,7 @@ var TAGLINE_NARROW = [
|
|
|
5531
5827
|
];
|
|
5532
5828
|
function bundleVersion() {
|
|
5533
5829
|
try {
|
|
5534
|
-
const pkg = JSON.parse(readFileSync9(
|
|
5830
|
+
const pkg = JSON.parse(readFileSync9(join9(BUNDLE_ROOT, "package.json"), "utf-8"));
|
|
5535
5831
|
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
5536
5832
|
} catch {
|
|
5537
5833
|
return void 0;
|
|
@@ -5681,8 +5977,11 @@ var USAGE = ` halfcycle [install] [target-repo] [engagement-id] [self-build|cli
|
|
|
5681
5977
|
--yes / -y: use this machine's saved sign-in without being asked
|
|
5682
5978
|
halfcycle check-drift <target-repo>
|
|
5683
5979
|
halfcycle build-record <phase> [--repo <root>]
|
|
5980
|
+
fetch a closed phase's Build Record again and write it into docs/build-records/
|
|
5684
5981
|
halfcycle open-phase <phase|--none> --actor "\u2026" (--evidence "\u2026" | --override --reason "\u2026") [--repo <root>]
|
|
5685
5982
|
halfcycle close-phase <phase> --verdict <clean|defects> --actor "\u2026" [--finding "\u2026"]\u2026 [--override --reason "\u2026"] [--repo <root>]
|
|
5983
|
+
[--thesis "\u2026"] [--outcome "\u2026"] [--deviation "\u2026"]\u2026
|
|
5984
|
+
close the phase and write its Build Record into docs/build-records/
|
|
5686
5985
|
halfcycle uninstall [${REMOVE_CREDENTIAL_FLAG}]
|
|
5687
5986
|
take out what the installer added to this project, keeping anything you changed
|
|
5688
5987
|
${REMOVE_CREDENTIAL_FLAG}: also remove this machine's credential for the project, which every
|
|
@@ -5695,7 +5994,7 @@ var USAGE = ` halfcycle [install] [target-repo] [engagement-id] [self-build|cli
|
|
|
5695
5994
|
`;
|
|
5696
5995
|
function isHalfcycleMonorepo(dir) {
|
|
5697
5996
|
try {
|
|
5698
|
-
const pkg = JSON.parse(readFileSync10(
|
|
5997
|
+
const pkg = JSON.parse(readFileSync10(join10(dir, "package.json"), "utf-8"));
|
|
5699
5998
|
return pkg.name === "halfcycle-monorepo";
|
|
5700
5999
|
} catch {
|
|
5701
6000
|
return false;
|
|
@@ -6002,31 +6301,24 @@ ${USAGE}`);
|
|
|
6002
6301
|
return;
|
|
6003
6302
|
}
|
|
6004
6303
|
if (cmd === "build-record") {
|
|
6005
|
-
const phaseArg = rest
|
|
6006
|
-
const repoRoot = rest
|
|
6304
|
+
const phaseArg = firstPositional(rest);
|
|
6305
|
+
const repoRoot = flagValue(rest, "--repo") ?? process.cwd();
|
|
6007
6306
|
if (phaseArg === void 0) {
|
|
6008
6307
|
process.stderr.write("halfcycle: usage: halfcycle build-record <phase> [--repo <root>]\n");
|
|
6009
6308
|
process.exit(1);
|
|
6010
6309
|
return;
|
|
6011
6310
|
}
|
|
6012
|
-
const phaseId = phaseArg;
|
|
6013
6311
|
try {
|
|
6014
|
-
assertValidPhaseIdentity(
|
|
6015
|
-
const
|
|
6016
|
-
const
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
});
|
|
6025
|
-
process.stdout.write(`[halfcycle] Build Record written:
|
|
6026
|
-
${result.jsonPath}
|
|
6027
|
-
${result.mdPath}
|
|
6028
|
-
`);
|
|
6029
|
-
process.exit(0);
|
|
6312
|
+
assertValidPhaseIdentity(phaseArg, BUILD_RECORD_DIR);
|
|
6313
|
+
const credential = readRepoCredential(repoRoot);
|
|
6314
|
+
const obtained = await obtainBuildRecord(credential, repoRoot, phaseArg);
|
|
6315
|
+
if (obtained.kind === "written") {
|
|
6316
|
+
process.stdout.write(writtenLines(obtained));
|
|
6317
|
+
process.exit(0);
|
|
6318
|
+
return;
|
|
6319
|
+
}
|
|
6320
|
+
process.stderr.write(notWrittenLines("build-record", obtained, phaseArg));
|
|
6321
|
+
process.exit(1);
|
|
6030
6322
|
} catch (err) {
|
|
6031
6323
|
process.stderr.write(`halfcycle build-record failed: ${err instanceof Error ? err.message : String(err)}
|
|
6032
6324
|
`);
|
|
@@ -6159,26 +6451,36 @@ halfcycle open-phase: a phase name has to work as a single folder name on this m
|
|
|
6159
6451
|
process.exit(2);
|
|
6160
6452
|
return;
|
|
6161
6453
|
}
|
|
6454
|
+
const stated = readStatementFlags(rest);
|
|
6455
|
+
if (typeof stated === "string") {
|
|
6456
|
+
process.stderr.write(`halfcycle close-phase: ${stated} needs a value after it.
|
|
6457
|
+
`);
|
|
6458
|
+
process.exit(2);
|
|
6459
|
+
return;
|
|
6460
|
+
}
|
|
6162
6461
|
try {
|
|
6163
6462
|
const credential = readRepoCredential(repoRoot);
|
|
6164
6463
|
const closed = await closePhase(credential, phaseArg, {
|
|
6165
6464
|
verdict,
|
|
6166
6465
|
findings: findingsDetail.length,
|
|
6167
6466
|
findingsDetail
|
|
6168
|
-
}, closeDecision, void 0, repoRoot);
|
|
6467
|
+
}, closeDecision, void 0, repoRoot, stated);
|
|
6169
6468
|
process.stdout.write(`[halfcycle] Phase "${phaseArg}" closed as ${verdict}${findingsDetail.length > 0 ? ` with ${findingsDetail.length} finding(s)` : ""} by ${closeDecision.actor}. The engagement is now ${closed.status}.
|
|
6170
6469
|
`);
|
|
6171
6470
|
if (closeDecision.decision === "override") {
|
|
6172
6471
|
process.stdout.write(`[halfcycle] This close was an OVERRIDE: ${closeDecision.reason}
|
|
6173
6472
|
`);
|
|
6174
6473
|
}
|
|
6175
|
-
if (closed.
|
|
6176
|
-
process.
|
|
6474
|
+
if (!closed.closeCommit.read) {
|
|
6475
|
+
process.stderr.write(`halfcycle close-phase: the commit this phase closed at could not be read (${closed.closeCommit.reason}), so its Build Record shows the commit as not stated.
|
|
6177
6476
|
`);
|
|
6477
|
+
}
|
|
6478
|
+
const obtained = await obtainBuildRecord(credential, repoRoot, phaseArg);
|
|
6479
|
+
const recordWritten = obtained.kind === "written";
|
|
6480
|
+
if (obtained.kind === "written") {
|
|
6481
|
+
process.stdout.write(writtenLines(obtained));
|
|
6178
6482
|
} else {
|
|
6179
|
-
process.stderr.write(
|
|
6180
|
-
halfcycle close-phase: this phase's Build Record will say the close commit and the close date were not recorded.
|
|
6181
|
-
`);
|
|
6483
|
+
process.stderr.write(notWrittenLines("close-phase", obtained, phaseArg));
|
|
6182
6484
|
}
|
|
6183
6485
|
process.stdout.write(`[halfcycle] Open the next phase with: halfcycle open-phase <phase> --actor "you" --evidence "\u2026"
|
|
6184
6486
|
`);
|
|
@@ -6186,21 +6488,28 @@ halfcycle close-phase: this phase's Build Record will say the close commit and t
|
|
|
6186
6488
|
process.stderr.write(`halfcycle close-phase: the phase closed, but this machine's record of it could not be cleared: ${closed.stampFailure.reason}
|
|
6187
6489
|
halfcycle close-phase: until ${closed.stampFailure.path} can be written, guard evaluations here are still filed under the phase you just closed.
|
|
6188
6490
|
`);
|
|
6189
|
-
process.exit(1);
|
|
6190
|
-
return;
|
|
6191
6491
|
}
|
|
6192
|
-
process.exit(0);
|
|
6492
|
+
process.exit(recordWritten && !closed.stampFailure ? 0 : 1);
|
|
6193
6493
|
} catch (err) {
|
|
6494
|
+
if (err instanceof CloseRequestInvalid) {
|
|
6495
|
+
process.stderr.write(`halfcycle close-phase: ${err.message}
|
|
6496
|
+
`);
|
|
6497
|
+
process.exit(2);
|
|
6498
|
+
return;
|
|
6499
|
+
}
|
|
6194
6500
|
if (err instanceof ClosePhaseRefused) {
|
|
6195
6501
|
process.stderr.write(`halfcycle close-phase: refused (${err.status}). ${err.message}
|
|
6196
6502
|
`);
|
|
6503
|
+
if (err.status === 400) {
|
|
6504
|
+
process.stderr.write("halfcycle close-phase: Nothing was closed.\n");
|
|
6505
|
+
}
|
|
6197
6506
|
if (err.finding) {
|
|
6198
6507
|
process.stderr.write(`halfcycle close-phase: outstanding work \u2014 ${err.finding}
|
|
6199
6508
|
`);
|
|
6200
6509
|
process.stderr.write(`halfcycle close-phase: close it anyway with \`halfcycle close-phase ${phaseArg} --verdict ${verdict} --actor "${closeActor}" --override --reason "\u2026"\` \u2014 recorded, not waived.
|
|
6201
6510
|
`);
|
|
6202
6511
|
} else if (err.code === "PhaseAlreadyClosed") {
|
|
6203
|
-
process.stderr.write("halfcycle close-phase:
|
|
6512
|
+
process.stderr.write("halfcycle close-phase: to close a phase again, open it again first.\nhalfcycle close-phase: To get a closed phase's Build Record: npx halfcycle@latest build-record <the phase that closed>\n");
|
|
6204
6513
|
}
|
|
6205
6514
|
process.exit(1);
|
|
6206
6515
|
return;
|
|
@@ -6217,6 +6526,56 @@ Usage:
|
|
|
6217
6526
|
${USAGE}`);
|
|
6218
6527
|
process.exit(1);
|
|
6219
6528
|
}
|
|
6529
|
+
function readStatementFlags(argv) {
|
|
6530
|
+
let thesis = null;
|
|
6531
|
+
let outcome = null;
|
|
6532
|
+
const deviations = [];
|
|
6533
|
+
for (let i = 0; i < argv.length; i++) {
|
|
6534
|
+
const flag = argv[i];
|
|
6535
|
+
if (flag !== "--thesis" && flag !== "--outcome" && flag !== "--deviation")
|
|
6536
|
+
continue;
|
|
6537
|
+
const value = argv[i + 1];
|
|
6538
|
+
if (value === void 0 || value.startsWith("--"))
|
|
6539
|
+
return flag;
|
|
6540
|
+
if (flag === "--thesis")
|
|
6541
|
+
thesis = value;
|
|
6542
|
+
else if (flag === "--outcome")
|
|
6543
|
+
outcome = value;
|
|
6544
|
+
else
|
|
6545
|
+
deviations.push(value);
|
|
6546
|
+
i++;
|
|
6547
|
+
}
|
|
6548
|
+
return { thesis, outcome, deviations };
|
|
6549
|
+
}
|
|
6550
|
+
function writtenLines(written) {
|
|
6551
|
+
return `[halfcycle] Build Record written:
|
|
6552
|
+
${written.mdPath}
|
|
6553
|
+
${written.jsonPath}
|
|
6554
|
+
`;
|
|
6555
|
+
}
|
|
6556
|
+
function notWrittenLines(verb, outcome, phase) {
|
|
6557
|
+
const prefix = `halfcycle ${verb}:`;
|
|
6558
|
+
const lead = verb === "close-phase" ? `${prefix} the phase closed, but no Build Record was written. ` : `${prefix} `;
|
|
6559
|
+
switch (outcome.kind) {
|
|
6560
|
+
case "exists":
|
|
6561
|
+
return `${lead}A file is already at ${outcome.path} and it is not this phase's earlier record, so it was left as it is.
|
|
6562
|
+
${prefix} Move it away, then run: ${refetchCommand(phase)}
|
|
6563
|
+
`;
|
|
6564
|
+
case "withheld":
|
|
6565
|
+
return `${lead}${outcome.message}
|
|
6566
|
+
${prefix} Fetch it later with: ${refetchCommand(phase)}
|
|
6567
|
+
`;
|
|
6568
|
+
case "refused":
|
|
6569
|
+
return `${lead}${outcome.message}
|
|
6570
|
+
${prefix} ${REFUSAL_FOLLOW_UP[outcome.reason]}
|
|
6571
|
+
` + (verb === "close-phase" ? `${prefix} Fetch it later with: ${refetchCommand(phase)}
|
|
6572
|
+
` : "");
|
|
6573
|
+
case "unavailable":
|
|
6574
|
+
return `${lead}The Build Record could not be fetched: ${outcome.why}
|
|
6575
|
+
${prefix} Fetch it later with: ${refetchCommand(phase)}
|
|
6576
|
+
`;
|
|
6577
|
+
}
|
|
6578
|
+
}
|
|
6220
6579
|
function flagValue(argv, name) {
|
|
6221
6580
|
const i = argv.indexOf(name);
|
|
6222
6581
|
if (i < 0 || i + 1 >= argv.length)
|