halfcycle 0.3.17 → 0.3.19
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/README.md +1 -1
- package/bin/bin.bundle.mjs +224 -33
- package/dist/bin.js +571 -198
- package/dist/bin.js.map +3 -3
- package/dist/build-record/close-record.d.ts +124 -0
- package/dist/build-record/close-record.d.ts.map +1 -0
- package/dist/build-record/index.d.ts +8 -5
- package/dist/build-record/index.d.ts.map +1 -1
- package/dist/build-record/sources.d.ts +49 -18
- package/dist/build-record/sources.d.ts.map +1 -1
- package/dist/build-record/template.d.ts.map +1 -1
- package/dist/build-record/types.d.ts +49 -12
- package/dist/build-record/types.d.ts.map +1 -1
- package/dist/build-record/write.d.ts +60 -3
- package/dist/build-record/write.d.ts.map +1 -1
- package/dist/close-phase.d.ts +21 -2
- package/dist/close-phase.d.ts.map +1 -1
- package/dist/confirm-identity.d.ts +25 -0
- package/dist/confirm-identity.d.ts.map +1 -1
- package/dist/create-engagement.d.ts +29 -1
- package/dist/create-engagement.d.ts.map +1 -1
- package/dist/engagement-credential.d.ts +85 -2
- package/dist/engagement-credential.d.ts.map +1 -1
- package/dist/index.js +412 -142
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts.map +1 -1
- package/dist/open-phase.d.ts +58 -1
- package/dist/open-phase.d.ts.map +1 -1
- package/dist/own-engagement.d.ts +50 -10
- package/dist/own-engagement.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -54,11 +54,26 @@ var firedGuardSchema = z2.object({
|
|
|
54
54
|
*/
|
|
55
55
|
path: z2.string().optional()
|
|
56
56
|
}).strict();
|
|
57
|
+
var notRunCheckSchema = z2.object({
|
|
58
|
+
/** Which check did not run, by the same reference a fired check carries. */
|
|
59
|
+
patternRef: z2.string(),
|
|
60
|
+
/**
|
|
61
|
+
* Why it could not run, in plain words — what this client did not send, or
|
|
62
|
+
* that the check's kind is not implemented yet. Never the check's own rule.
|
|
63
|
+
*/
|
|
64
|
+
reason: z2.string()
|
|
65
|
+
}).strict();
|
|
57
66
|
var resultEnvelopeSchema = z2.object({
|
|
58
67
|
guardsFired: z2.array(firedGuardSchema),
|
|
59
68
|
severity: severitySchema.nullable(),
|
|
60
69
|
blocking: z2.boolean(),
|
|
61
|
-
explanation: z2.string()
|
|
70
|
+
explanation: z2.string(),
|
|
71
|
+
/**
|
|
72
|
+
* The checks that could not run on this edit, each named with the reason.
|
|
73
|
+
* Absent when every check ran — so an evaluation where nothing was skipped
|
|
74
|
+
* looks exactly as it always has, and an absent list is never an empty one.
|
|
75
|
+
*/
|
|
76
|
+
notRun: z2.array(notRunCheckSchema).optional()
|
|
62
77
|
}).strict();
|
|
63
78
|
var wireErrorSchema = z2.object({
|
|
64
79
|
statusCode: z2.number(),
|
|
@@ -170,6 +185,67 @@ var CREW_ROSTER = [
|
|
|
170
185
|
var CREW_ROSTER_SIZE = CREW_ROSTER.length;
|
|
171
186
|
var CREW_CALLSIGNS = CREW_ROSTER.map((member) => member.callsign);
|
|
172
187
|
|
|
188
|
+
// ../events/dist/phase-identity.js
|
|
189
|
+
var PHASE_SEGMENT_PREFIX = "phase-";
|
|
190
|
+
function isValidPhaseIdentity(identity) {
|
|
191
|
+
if (typeof identity === "number")
|
|
192
|
+
return Number.isInteger(identity) && identity >= 0;
|
|
193
|
+
if (typeof identity !== "string")
|
|
194
|
+
return false;
|
|
195
|
+
if (identity === "")
|
|
196
|
+
return false;
|
|
197
|
+
if (identity.includes("/"))
|
|
198
|
+
return false;
|
|
199
|
+
if (identity.includes("\\"))
|
|
200
|
+
return false;
|
|
201
|
+
if (identity.includes("\0"))
|
|
202
|
+
return false;
|
|
203
|
+
if (identity.startsWith("."))
|
|
204
|
+
return false;
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
var InvalidPhaseIdentityError = class extends Error {
|
|
208
|
+
/** The value that was refused, exactly as supplied. */
|
|
209
|
+
identity;
|
|
210
|
+
constructor(identity, allowedLocation) {
|
|
211
|
+
const subject = allowedLocation === void 0 ? "a file" : "a Build Record";
|
|
212
|
+
const where = allowedLocation === void 0 ? "" : `
|
|
213
|
+
Records are written only into ${allowedLocation.replace(/\\/g, "/")}/. Nothing was written.`;
|
|
214
|
+
super(`that phase identity cannot name ${subject}.
|
|
215
|
+
identity: ${describeIdentity(identity)}` + where);
|
|
216
|
+
this.name = "InvalidPhaseIdentityError";
|
|
217
|
+
this.identity = identity;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
function assertValidPhaseIdentity(identity, allowedLocation) {
|
|
221
|
+
if (!isValidPhaseIdentity(identity)) {
|
|
222
|
+
throw new InvalidPhaseIdentityError(identity, allowedLocation);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function renderPhaseSegment(identity) {
|
|
226
|
+
if (!isValidPhaseIdentity(identity)) {
|
|
227
|
+
throw new InvalidPhaseIdentityError(identity);
|
|
228
|
+
}
|
|
229
|
+
return `${PHASE_SEGMENT_PREFIX}${identity}`;
|
|
230
|
+
}
|
|
231
|
+
function describeIdentity(identity) {
|
|
232
|
+
if (identity === "")
|
|
233
|
+
return "(empty)";
|
|
234
|
+
if (typeof identity === "string")
|
|
235
|
+
return identity;
|
|
236
|
+
if (identity === null)
|
|
237
|
+
return "null";
|
|
238
|
+
if (identity === void 0)
|
|
239
|
+
return "none supplied";
|
|
240
|
+
if (typeof identity === "number" || typeof identity === "boolean")
|
|
241
|
+
return String(identity);
|
|
242
|
+
if (Array.isArray(identity))
|
|
243
|
+
return "(a list)";
|
|
244
|
+
if (typeof identity === "object")
|
|
245
|
+
return "(an object)";
|
|
246
|
+
return `(a ${typeof identity})`;
|
|
247
|
+
}
|
|
248
|
+
|
|
173
249
|
// dist/engagement-credential.js
|
|
174
250
|
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
175
251
|
import { platform as platform2 } from "node:os";
|
|
@@ -192,6 +268,7 @@ function engagementStateDir(engagementId, home) {
|
|
|
192
268
|
function engagementEnvPath(engagementId, home) {
|
|
193
269
|
return join2(engagementStateDir(engagementId, home), ENV_FILENAME);
|
|
194
270
|
}
|
|
271
|
+
var NOT_THIS_ACCOUNT_MARKER_FILENAME = "not-this-account";
|
|
195
272
|
var GUARD_ENV_KEYS = [
|
|
196
273
|
"GUARD_SERVICE_URL",
|
|
197
274
|
"GUARD_SERVICE_TOKEN",
|
|
@@ -233,33 +310,50 @@ function parseEnvText(raw) {
|
|
|
233
310
|
return out;
|
|
234
311
|
}
|
|
235
312
|
function reconcileEnvText(existing, values, keys = ENGAGEMENT_ENV_KEYS) {
|
|
236
|
-
const desired = new Map(keys.map((k) =>
|
|
313
|
+
const desired = new Map(keys.map((k) => {
|
|
314
|
+
const value = values[k];
|
|
315
|
+
return [k, value === void 0 ? "" : value];
|
|
316
|
+
}));
|
|
237
317
|
const line = (k) => `${k}=${shq(desired.get(k) ?? "")}`;
|
|
318
|
+
const written = keys.filter((k) => desired.get(k) !== null);
|
|
238
319
|
if (existing === null) {
|
|
320
|
+
if (written.length === 0)
|
|
321
|
+
return "";
|
|
239
322
|
return `${ENV_HEADER}
|
|
240
|
-
` +
|
|
323
|
+
` + written.map(line).join("\n") + "\n";
|
|
241
324
|
}
|
|
242
325
|
const seen = /* @__PURE__ */ new Set();
|
|
243
|
-
const out =
|
|
326
|
+
const out = [];
|
|
327
|
+
for (const existingLine of existing.split("\n")) {
|
|
244
328
|
const eq = existingLine.indexOf("=");
|
|
245
|
-
if (eq === -1)
|
|
246
|
-
|
|
329
|
+
if (eq === -1) {
|
|
330
|
+
out.push(existingLine);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
247
333
|
const lhs = existingLine.slice(0, eq);
|
|
248
334
|
const exported = /^\s*export\s+/.exec(lhs);
|
|
249
335
|
const prefix = exported === null ? "" : exported[0];
|
|
250
336
|
const key = lhs.slice(prefix.length).trim();
|
|
251
|
-
if (desired.has(key)) {
|
|
252
|
-
|
|
253
|
-
|
|
337
|
+
if (!desired.has(key)) {
|
|
338
|
+
out.push(existingLine);
|
|
339
|
+
continue;
|
|
254
340
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
341
|
+
seen.add(key);
|
|
342
|
+
if (desired.get(key) === null)
|
|
343
|
+
continue;
|
|
344
|
+
out.push(`${prefix}${line(key)}`);
|
|
345
|
+
}
|
|
346
|
+
const missing = written.filter((k) => !seen.has(k));
|
|
258
347
|
if (missing.length > 0) {
|
|
348
|
+
const header = existing.includes(ENV_HEADER) ? "" : `${ENV_HEADER}
|
|
349
|
+
`;
|
|
350
|
+
const block = header + missing.map(line).join("\n");
|
|
259
351
|
const trailingBlank = out.length > 0 && out[out.length - 1] === "";
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
352
|
+
if (trailingBlank)
|
|
353
|
+
out.splice(out.length - 1, 0, block);
|
|
354
|
+
else
|
|
355
|
+
out.push(`
|
|
356
|
+
${block}`);
|
|
263
357
|
}
|
|
264
358
|
let result = out.join("\n");
|
|
265
359
|
if (existing.endsWith("\n") && !result.endsWith("\n"))
|
|
@@ -273,17 +367,19 @@ function readEngagementEnv(engagementId, home) {
|
|
|
273
367
|
return null;
|
|
274
368
|
}
|
|
275
369
|
}
|
|
276
|
-
function writeEngagementEnv(engagementId, values, home) {
|
|
370
|
+
function writeEngagementEnv(engagementId, values, home, keys = ENGAGEMENT_ENV_KEYS) {
|
|
277
371
|
const path = engagementEnvPath(engagementId, home);
|
|
278
372
|
const dir = engagementStateDir(engagementId, home);
|
|
279
|
-
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
280
373
|
let existing;
|
|
281
374
|
try {
|
|
282
375
|
existing = readFileSync(path, "utf-8");
|
|
283
376
|
} catch {
|
|
284
377
|
existing = null;
|
|
285
378
|
}
|
|
286
|
-
const reconciled = reconcileEnvText(existing, values);
|
|
379
|
+
const reconciled = reconcileEnvText(existing, values, keys);
|
|
380
|
+
if (existing === null && reconciled === "")
|
|
381
|
+
return "skipped";
|
|
382
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
287
383
|
const outcome = existing === reconciled ? "skipped" : "written";
|
|
288
384
|
if (outcome === "written") {
|
|
289
385
|
writeFileSync(path, reconciled, { mode: 384 });
|
|
@@ -313,14 +409,15 @@ function engagementResolutionShell() {
|
|
|
313
409
|
# desktop session's PATH, so the id is cut out with sed \u2014 after flattening the
|
|
314
410
|
# file, because sed is line-oriented and the pin's line breaks are not a contract.
|
|
315
411
|
#
|
|
316
|
-
# Sets HALFCYCLE_ENV_FILE, HALFCYCLE_ENV_ENGAGEMENT
|
|
317
|
-
# the CALLER's shell and returns a status. Call
|
|
318
|
-
# never as \`$(halfcycle_env_file \u2026)\` \u2014 a
|
|
319
|
-
# would discard everything it set.
|
|
412
|
+
# Sets HALFCYCLE_ENV_FILE, HALFCYCLE_ENV_ENGAGEMENT, HALFCYCLE_ENV_PROBLEM and
|
|
413
|
+
# HALFCYCLE_ENV_NOT_THIS_ACCOUNT in the CALLER's shell and returns a status. Call
|
|
414
|
+
# it as \`if halfcycle_env_file \u2026\`, never as \`$(halfcycle_env_file \u2026)\` \u2014 a
|
|
415
|
+
# command substitution is a subshell and would discard everything it set.
|
|
320
416
|
halfcycle_env_file() {
|
|
321
417
|
HALFCYCLE_ENV_FILE=""
|
|
322
418
|
HALFCYCLE_ENV_ENGAGEMENT=""
|
|
323
419
|
HALFCYCLE_ENV_PROBLEM=""
|
|
420
|
+
HALFCYCLE_ENV_NOT_THIS_ACCOUNT=""
|
|
324
421
|
hc_pin="$1/.halfcycle/bundle.json"
|
|
325
422
|
if [ ! -f "$hc_pin" ]; then
|
|
326
423
|
HALFCYCLE_ENV_PROBLEM="no-pin"
|
|
@@ -336,6 +433,9 @@ halfcycle_env_file() {
|
|
|
336
433
|
HALFCYCLE_ENV_PROBLEM="no-home"
|
|
337
434
|
return 1
|
|
338
435
|
fi
|
|
436
|
+
if [ -f "$HOME/.halfcycle/${ENGAGEMENTS_DIR}/$hc_id/${NOT_THIS_ACCOUNT_MARKER_FILENAME}" ]; then
|
|
437
|
+
HALFCYCLE_ENV_NOT_THIS_ACCOUNT=1
|
|
438
|
+
fi
|
|
339
439
|
hc_env="$HOME/.halfcycle/${ENGAGEMENTS_DIR}/$hc_id/${ENV_FILENAME}"
|
|
340
440
|
if [ ! -f "$hc_env" ]; then
|
|
341
441
|
HALFCYCLE_ENV_PROBLEM="no-credential"
|
|
@@ -715,6 +815,13 @@ if halfcycle_env_file "$PROJECT_DIR"; then
|
|
|
715
815
|
set +a
|
|
716
816
|
fi
|
|
717
817
|
|
|
818
|
+
# Pass along WHY this machine has no credential, as a plain variable \u2014 the
|
|
819
|
+
# marker check above already answered it, and this is the one bit of that
|
|
820
|
+
# answer the runner binary needs to say the right thing on every edit.
|
|
821
|
+
if [ "$HALFCYCLE_ENV_NOT_THIS_ACCOUNT" = "1" ]; then
|
|
822
|
+
export HALFCYCLE_NOT_THIS_ACCOUNT=1
|
|
823
|
+
fi
|
|
824
|
+
|
|
718
825
|
exec node "$PROJECT_DIR/${VENDORED_BIN_REL}" "$@"
|
|
719
826
|
`;
|
|
720
827
|
}
|
|
@@ -730,13 +837,20 @@ MARKER_FILE="\${TMPDIR%/}/halfcycle-session-\${HASH}.ref"
|
|
|
730
837
|
git -C "$REPO_ROOT" rev-parse HEAD > "$MARKER_FILE" 2>/dev/null || true
|
|
731
838
|
|
|
732
839
|
# ---------------------------------------------------------------------------
|
|
733
|
-
# STANDING GUARD-COVERAGE STATEMENT (T-27).
|
|
840
|
+
# STANDING GUARD-COVERAGE STATEMENT (T-27; two arms since T-02b).
|
|
734
841
|
#
|
|
735
842
|
# A repository with no credential on this machine is UNGUARDED: the PostToolUse
|
|
736
843
|
# hook will find nothing to evaluate with, and it says so once per edit, after
|
|
737
844
|
# the edit. This says it once, up front, before anything is written \u2014 which is
|
|
738
845
|
# the one thing a session-start hook can do that nothing else can.
|
|
739
846
|
#
|
|
847
|
+
# TWO REASONS TO HAVE NO CREDENTIAL, TWO DIFFERENT THINGS TO SAY (T-02b,
|
|
848
|
+
# solo-project-ownership \u2014 outcome B). "You have not signed in here yet" is
|
|
849
|
+
# fixed by re-running the installer; "this is not your project" is permanent,
|
|
850
|
+
# and the same remedy would send the reader in a circle forever. The two are
|
|
851
|
+
# told apart by HALFCYCLE_ENV_NOT_THIS_ACCOUNT, set by \`halfcycle_env_file\`
|
|
852
|
+
# below from a machine-level marker beside the credential store.
|
|
853
|
+
#
|
|
740
854
|
# Everything below runs in a SUBSHELL: it sources a credential store, and a
|
|
741
855
|
# session-start hook must not leak an engagement's variables into whatever the
|
|
742
856
|
# editor runs next. Any failure inside it is swallowed; this script's exit
|
|
@@ -750,18 +864,33 @@ ${engagementResolutionShell()}
|
|
|
750
864
|
. "$HALFCYCLE_ENV_FILE"
|
|
751
865
|
set +a
|
|
752
866
|
fi
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
echo "[halfcycle]
|
|
760
|
-
echo "[halfcycle]
|
|
761
|
-
echo "[halfcycle]
|
|
762
|
-
echo "[halfcycle]
|
|
763
|
-
echo "[halfcycle]
|
|
764
|
-
echo "[halfcycle]
|
|
867
|
+
if [ "$HALFCYCLE_ENV_NOT_THIS_ACCOUNT" = "1" ]; then
|
|
868
|
+
# OUTCOME B (T-02b, solo-project-ownership). This is not the missing-keys
|
|
869
|
+
# case below wearing different words: there is nothing to fix, and the loop
|
|
870
|
+
# that follows is skipped entirely rather than run and then overridden \u2014 a
|
|
871
|
+
# repository whose credential is absent for THIS reason must never be told
|
|
872
|
+
# to re-run the installer, which can never obtain one for it.
|
|
873
|
+
echo "[halfcycle] This project belongs to another Halfcycle account, so nothing here is checked."
|
|
874
|
+
echo "[halfcycle] Your work carries on exactly as it would without Halfcycle \u2014 no edit is"
|
|
875
|
+
echo "[halfcycle] evaluated, and nothing you do is reported as having passed."
|
|
876
|
+
echo "[halfcycle] Halfcycle is Solo today \u2014 one developer per project \u2014 with Team plans coming."
|
|
877
|
+
echo "[halfcycle] Ask the project's owner for view access."
|
|
878
|
+
echo "[halfcycle] If this account is wrong, sign in as the one that owns the project and run"
|
|
879
|
+
echo "[halfcycle] \\"npx halfcycle\\" again."
|
|
880
|
+
else
|
|
881
|
+
hc_missing=""
|
|
882
|
+
for hc_key in ${GUARD_COVERAGE_REQUIRED_KEYS.join(" ")}; do
|
|
883
|
+
eval "hc_value=\\\${$hc_key:-}"
|
|
884
|
+
if [ -z "$hc_value" ]; then hc_missing="$hc_missing $hc_key"; fi
|
|
885
|
+
done
|
|
886
|
+
if [ -n "$hc_missing" ]; then
|
|
887
|
+
echo "[halfcycle] NO GUARD COVERAGE IN THIS REPOSITORY. Nothing will evaluate the edits made in"
|
|
888
|
+
echo "[halfcycle] this session: this machine has no Halfcycle credential for it, so the guard hook"
|
|
889
|
+
echo "[halfcycle] has nothing to call with (missing:$hc_missing)."
|
|
890
|
+
echo "[halfcycle] Run \\"npx halfcycle\\" in this repository to fix it \u2014 it signs you in through your"
|
|
891
|
+
echo "[halfcycle] browser if needed, writes the credential to \\$HOME/.halfcycle outside this tree,"
|
|
892
|
+
echo "[halfcycle] and needs nothing configured first."
|
|
893
|
+
fi
|
|
765
894
|
fi
|
|
766
895
|
) 2>/dev/null || true
|
|
767
896
|
exit 0
|
|
@@ -921,8 +1050,20 @@ if [ -z "$TOKEN" ]; then
|
|
|
921
1050
|
no-home)
|
|
922
1051
|
echo "halfcycle: HOME is not set, so the credential store cannot be located." >&2 ;;
|
|
923
1052
|
*)
|
|
924
|
-
|
|
925
|
-
|
|
1053
|
+
# OUTCOME B (T-02b, solo-project-ownership) is a \`no-credential\` PROBLEM
|
|
1054
|
+
# with a REASON attached, checked here rather than as a fifth case value
|
|
1055
|
+
# \u2014 see \`engagementResolutionShell\`'s own docblock for why. Re-running
|
|
1056
|
+
# this installer can never obtain a credential for this reason, so it is
|
|
1057
|
+
# the one sentence in this file that must not say to.
|
|
1058
|
+
if [ "$HALFCYCLE_ENV_NOT_THIS_ACCOUNT" = "1" ]; then
|
|
1059
|
+
echo "halfcycle: this project belongs to another Halfcycle account, so this machine has no" >&2
|
|
1060
|
+
echo "halfcycle: credential for it and the Halfcycle tools are not available here. Nothing is" >&2
|
|
1061
|
+
echo "halfcycle: broken \u2014 ask the project's owner for view access, or sign in as the account" >&2
|
|
1062
|
+
echo "halfcycle: that owns it and run \\"npx halfcycle\\" again." >&2
|
|
1063
|
+
else
|
|
1064
|
+
echo "halfcycle: this machine holds no credential for engagement $HALFCYCLE_ENV_ENGAGEMENT." >&2
|
|
1065
|
+
echo "halfcycle: run \\"npx halfcycle\\" in this repository \u2014 it will sign you in through your browser if needed, and write the credential to $HOME/.halfcycle, outside this repository." >&2
|
|
1066
|
+
fi ;;
|
|
926
1067
|
esac
|
|
927
1068
|
exit 1
|
|
928
1069
|
fi
|
|
@@ -1266,11 +1407,13 @@ var CreateEngagementRefused = class extends Error {
|
|
|
1266
1407
|
status;
|
|
1267
1408
|
serverMessage;
|
|
1268
1409
|
errorCode;
|
|
1269
|
-
|
|
1410
|
+
reason;
|
|
1411
|
+
constructor(status, serverMessage, errorCode, reason, message) {
|
|
1270
1412
|
super(message);
|
|
1271
1413
|
this.status = status;
|
|
1272
1414
|
this.serverMessage = serverMessage;
|
|
1273
1415
|
this.errorCode = errorCode;
|
|
1416
|
+
this.reason = reason;
|
|
1274
1417
|
this.name = "CreateEngagementRefused";
|
|
1275
1418
|
}
|
|
1276
1419
|
/**
|
|
@@ -1315,7 +1458,8 @@ function planeRefusal(detail) {
|
|
|
1315
1458
|
return void 0;
|
|
1316
1459
|
return {
|
|
1317
1460
|
message: candidate.message,
|
|
1318
|
-
error: typeof candidate.error === "string" ? candidate.error : void 0
|
|
1461
|
+
error: typeof candidate.error === "string" ? candidate.error : void 0,
|
|
1462
|
+
reason: typeof candidate.reason === "string" ? candidate.reason : void 0
|
|
1319
1463
|
};
|
|
1320
1464
|
}
|
|
1321
1465
|
var CONTROL_ORIGIN_HINT = `This CLI talks to ${DEFAULT_CONTROL_ORIGIN}; it must be the CONTROL origin \u2014 the one that serves /engagements and /board/enter-codes. The MCP server runs at a different address, and the platform supplies that one itself; you configure neither.`;
|
|
@@ -1349,7 +1493,7 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
|
1349
1493
|
const detail = await res.text().catch(() => "");
|
|
1350
1494
|
const refusal = res.status === 404 ? void 0 : planeRefusal(detail);
|
|
1351
1495
|
if (refusal) {
|
|
1352
|
-
throw new CreateEngagementRefused(res.status, refusal.message, refusal.error, `[bundle install] ${shape.action} failed: ${url} returned ${res.status}. ${refusal.message} ${shape.nothingHappened}.`);
|
|
1496
|
+
throw new CreateEngagementRefused(res.status, refusal.message, refusal.error, refusal.reason, `[bundle install] ${shape.action} failed: ${url} returned ${res.status}. ${refusal.message} ${shape.nothingHappened}.`);
|
|
1353
1497
|
}
|
|
1354
1498
|
const wrongOrigin = res.status === 404 ? `That address answered, but it does not serve ${shape.route} \u2014 so it is not the Halfcycle service. ` : "";
|
|
1355
1499
|
throw new Error(`[bundle install] ${shape.action} failed: ${url} returned ${res.status}. ${wrongOrigin}${detail ? `Response: ${detail.slice(0, 300)}. ` : ""}${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
|
|
@@ -1877,8 +2021,203 @@ function projectSeedGuard(fired, includeExplanation) {
|
|
|
1877
2021
|
// dist/build-record/sources.js
|
|
1878
2022
|
import { readFileSync as readFileSync7, existsSync as existsSync6 } from "node:fs";
|
|
1879
2023
|
import { createHash } from "node:crypto";
|
|
1880
|
-
import { join as
|
|
2024
|
+
import { join as join10 } from "node:path";
|
|
2025
|
+
|
|
2026
|
+
// dist/build-record/close-record.js
|
|
1881
2027
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2028
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2029
|
+
import { dirname as dirname4, join as join8 } from "node:path";
|
|
2030
|
+
var CLOSE_RECORD_FORMAT = "halfcycle-phase-close/v1";
|
|
2031
|
+
var BUILD_RECORD_ZONE_B_DIR = join8(ZONE_B_DIR, "build-record");
|
|
2032
|
+
var InvalidCloseRecordError = class extends Error {
|
|
2033
|
+
constructor(path, detail) {
|
|
2034
|
+
super(`[build-record] the phase close record at ${path} cannot be read \u2014 ${detail}`);
|
|
2035
|
+
this.name = "InvalidCloseRecordError";
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
function closeRecordPath(repoRoot, phase) {
|
|
2039
|
+
assertValidPhaseIdentity(phase, BUILD_RECORD_ZONE_B_DIR);
|
|
2040
|
+
return join8(repoRoot, BUILD_RECORD_ZONE_B_DIR, `${renderPhaseSegment(phase)}.close.json`);
|
|
2041
|
+
}
|
|
2042
|
+
function resolveCloseAtHead(repoRoot) {
|
|
2043
|
+
try {
|
|
2044
|
+
const closeCommit = execFileSync2("git", ["-C", repoRoot, "rev-parse", "--short", "HEAD"], {
|
|
2045
|
+
encoding: "utf-8",
|
|
2046
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2047
|
+
}).trim();
|
|
2048
|
+
const closedDate = execFileSync2("git", ["-C", repoRoot, "show", "-s", "--format=%cs", "HEAD"], {
|
|
2049
|
+
encoding: "utf-8",
|
|
2050
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2051
|
+
}).trim();
|
|
2052
|
+
if (closeCommit === "" || closedDate === "")
|
|
2053
|
+
return null;
|
|
2054
|
+
return { closeCommit, closedDate };
|
|
2055
|
+
} catch {
|
|
2056
|
+
return null;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
function writeCloseRecord(repoRoot, phase) {
|
|
2060
|
+
let path;
|
|
2061
|
+
try {
|
|
2062
|
+
path = closeRecordPath(repoRoot, phase);
|
|
2063
|
+
} catch (err) {
|
|
2064
|
+
return { recorded: false, path: null, reason: err instanceof Error ? err.message : String(err) };
|
|
2065
|
+
}
|
|
2066
|
+
const at = resolveCloseAtHead(repoRoot);
|
|
2067
|
+
if (at === null) {
|
|
2068
|
+
return {
|
|
2069
|
+
recorded: false,
|
|
2070
|
+
path,
|
|
2071
|
+
reason: `the commit could not be read from ${repoRoot} \u2014 there is no git available, no repository there, or no commit in it yet`
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
const record2 = {
|
|
2075
|
+
format: CLOSE_RECORD_FORMAT,
|
|
2076
|
+
phase,
|
|
2077
|
+
closeCommit: at.closeCommit,
|
|
2078
|
+
closedDate: at.closedDate
|
|
2079
|
+
};
|
|
2080
|
+
try {
|
|
2081
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
2082
|
+
writeFileSync5(path, JSON.stringify(record2, null, 2) + "\n", "utf-8");
|
|
2083
|
+
} catch (err) {
|
|
2084
|
+
return { recorded: false, path, reason: err instanceof Error ? err.message : String(err) };
|
|
2085
|
+
}
|
|
2086
|
+
return { recorded: true, path, closeCommit: at.closeCommit, closedDate: at.closedDate };
|
|
2087
|
+
}
|
|
2088
|
+
function parseCloseRecord(path, parsed) {
|
|
2089
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2090
|
+
throw new InvalidCloseRecordError(path, "it does not hold a record");
|
|
2091
|
+
}
|
|
2092
|
+
const record2 = parsed;
|
|
2093
|
+
if (record2.format !== CLOSE_RECORD_FORMAT) {
|
|
2094
|
+
throw new InvalidCloseRecordError(path, `it is not a ${CLOSE_RECORD_FORMAT} record (it says ${JSON.stringify(record2.format)})`);
|
|
2095
|
+
}
|
|
2096
|
+
const commit = record2.closeCommit;
|
|
2097
|
+
const date = record2.closedDate;
|
|
2098
|
+
const missing = [];
|
|
2099
|
+
if (typeof commit !== "string" || commit.trim() === "")
|
|
2100
|
+
missing.push("closeCommit");
|
|
2101
|
+
if (typeof date !== "string" || date.trim() === "")
|
|
2102
|
+
missing.push("closedDate");
|
|
2103
|
+
if (missing.length > 0) {
|
|
2104
|
+
throw new InvalidCloseRecordError(path, `${missing.join(" and ")} ${missing.length === 1 ? "is" : "are"} not recorded in it. A close records the commit and its date together or records neither, so a file carrying one of them was edited by hand.`);
|
|
2105
|
+
}
|
|
2106
|
+
if (typeof record2.phase !== "string" && typeof record2.phase !== "number") {
|
|
2107
|
+
throw new InvalidCloseRecordError(path, "it does not say which phase closed");
|
|
2108
|
+
}
|
|
2109
|
+
return {
|
|
2110
|
+
format: CLOSE_RECORD_FORMAT,
|
|
2111
|
+
phase: record2.phase,
|
|
2112
|
+
closeCommit: commit,
|
|
2113
|
+
closedDate: date
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// dist/build-record/write.js
|
|
2118
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
2119
|
+
import { basename, join as join9, resolve, relative as relative2, isAbsolute } from "node:path";
|
|
2120
|
+
|
|
2121
|
+
// dist/build-record/template.js
|
|
2122
|
+
function severityLabel(seed) {
|
|
2123
|
+
return `\`${seed.patternRef}\` (severity \`${seed.severity}\`)`;
|
|
2124
|
+
}
|
|
2125
|
+
var NOT_RECORDED = "not recorded";
|
|
2126
|
+
function renderCloseLine(record2) {
|
|
2127
|
+
const status = `**Status:** ${record2.status.toUpperCase()}`;
|
|
2128
|
+
const closed = `**Closed:** ${record2.closedDate ?? NOT_RECORDED}`;
|
|
2129
|
+
const commit = record2.closeCommit === null ? `**Close commit:** ${NOT_RECORDED}` : `**Close commit:** \`${record2.closeCommit}\``;
|
|
2130
|
+
return `${status} \xB7 ${closed} \xB7 ${commit}`;
|
|
2131
|
+
}
|
|
2132
|
+
function renderCloseProvenance(record2) {
|
|
2133
|
+
return record2.closeCommit === null && record2.closedDate === null ? "*The close date and the commit were not recorded when this phase closed, so this record does not name them. Nothing has been substituted for them.*" : "*The close date and the commit are the client's own assertion about their own repository, written down when the phase was closed. Halfcycle did not observe them and does not check them.*";
|
|
2134
|
+
}
|
|
2135
|
+
function renderDoneWhenRows(record2) {
|
|
2136
|
+
const rows = [];
|
|
2137
|
+
for (const [key, dw] of Object.entries(record2.acceptance.doneWhen)) {
|
|
2138
|
+
const mark = dw.result === "pass" ? "\u2705" : dw.result === "dropped" ? "\u2014" : dw.result;
|
|
2139
|
+
rows.push(`| ${key} | ${mark} | ${dw.detail} |`);
|
|
2140
|
+
}
|
|
2141
|
+
return rows.join("\n");
|
|
2142
|
+
}
|
|
2143
|
+
function renderBuildRecordMarkdown(record2) {
|
|
2144
|
+
const { phase } = record2;
|
|
2145
|
+
const companion = `${renderPhaseSegment(phase.id)}.json`;
|
|
2146
|
+
const guardsList = record2.seedGuards.map(severityLabel).join(", ");
|
|
2147
|
+
const lines = [
|
|
2148
|
+
// Fixed prose (vault-canonical template) + projected title slots.
|
|
2149
|
+
`# Build Record \u2014 Phase ${phase.id}: ${phase.name}`,
|
|
2150
|
+
"",
|
|
2151
|
+
`**Format:** \`${record2.format}\` (markdown + JSON pair; portable, readable without Halfcycle systems)`,
|
|
2152
|
+
"",
|
|
2153
|
+
"> This Build Record was assembled automatically at phase close by the method bundle (`packages/bundle`). Every data field below is projected from the companion JSON \u2014 no record content is hand-authored.",
|
|
2154
|
+
"",
|
|
2155
|
+
`**Engagement:** ${record2.engagement} (${record2.engagementType})`,
|
|
2156
|
+
`**Phase:** ${phase.id} \u2014 ${phase.name}`,
|
|
2157
|
+
renderCloseLine(record2),
|
|
2158
|
+
`**Companion:** [\`${companion}\`](${companion})`,
|
|
2159
|
+
"",
|
|
2160
|
+
renderCloseProvenance(record2),
|
|
2161
|
+
"",
|
|
2162
|
+
"## What this phase proved",
|
|
2163
|
+
"",
|
|
2164
|
+
`${record2.thesis} **Thesis held: ${record2.thesisHeld ? "yes" : "no"}.**`,
|
|
2165
|
+
"",
|
|
2166
|
+
"## Delivered",
|
|
2167
|
+
"",
|
|
2168
|
+
`- **Packages:** ${record2.delivered.packages.join(", ")}`,
|
|
2169
|
+
`- **Services:** ${record2.delivered.services.join(", ")}`,
|
|
2170
|
+
`- **Tools:** ${record2.delivered.tools.join(", ")}`,
|
|
2171
|
+
`- **Dogfood:** ${record2.delivered.dogfood}`,
|
|
2172
|
+
"",
|
|
2173
|
+
guardsList ? `Guards evaluated (INV-007-safe results): ${guardsList}. *(Guard \`channel\`/\`version\` are guard-asset metadata, excluded from the v1 record \u2014 INV-007/INV-001.)*` : "No guards fired during this phase.",
|
|
2174
|
+
"",
|
|
2175
|
+
`## Acceptance (independent walk \u2014 walker ${record2.acceptance.walker})`,
|
|
2176
|
+
"",
|
|
2177
|
+
`**Verdict:** ${record2.acceptance.verdict} \xB7 **Findings:** ${record2.acceptance.findings}`,
|
|
2178
|
+
"",
|
|
2179
|
+
"| Done-when | Result | Evidence |",
|
|
2180
|
+
"|---|---|---|",
|
|
2181
|
+
renderDoneWhenRows(record2),
|
|
2182
|
+
""
|
|
2183
|
+
];
|
|
2184
|
+
if (record2.acceptance.inv002ProdBypassProbe) {
|
|
2185
|
+
lines.push(`**INV-002 production bypass probe:** ${record2.acceptance.inv002ProdBypassProbe.result} \u2014 ${record2.acceptance.inv002ProdBypassProbe.detail}`, "");
|
|
2186
|
+
}
|
|
2187
|
+
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"}`, "");
|
|
2188
|
+
return lines.join("\n");
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// dist/build-record/write.js
|
|
2192
|
+
var BUILD_RECORD_DIR = join9("docs", "build-records");
|
|
2193
|
+
function serialiseRecordJson(record2) {
|
|
2194
|
+
return JSON.stringify(record2, null, 2) + "\n";
|
|
2195
|
+
}
|
|
2196
|
+
function assertInsideOutDir(outDir, candidate, identity) {
|
|
2197
|
+
const rel = relative2(outDir, resolve(candidate));
|
|
2198
|
+
const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute(rel) && rel === basename(rel);
|
|
2199
|
+
if (!isPlainFilename) {
|
|
2200
|
+
throw new InvalidPhaseIdentityError(identity, BUILD_RECORD_DIR);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
function writeBuildRecord(repoRoot, record2) {
|
|
2204
|
+
const outDir = resolve(repoRoot, BUILD_RECORD_DIR);
|
|
2205
|
+
const identity = record2.phase.id;
|
|
2206
|
+
if (!isValidPhaseIdentity(identity)) {
|
|
2207
|
+
throw new InvalidPhaseIdentityError(identity, BUILD_RECORD_DIR);
|
|
2208
|
+
}
|
|
2209
|
+
const segment = renderPhaseSegment(identity);
|
|
2210
|
+
const jsonPath = join9(outDir, `${segment}.json`);
|
|
2211
|
+
const mdPath = join9(outDir, `${segment}.md`);
|
|
2212
|
+
assertInsideOutDir(outDir, jsonPath, identity);
|
|
2213
|
+
assertInsideOutDir(outDir, mdPath, identity);
|
|
2214
|
+
mkdirSync6(outDir, { recursive: true });
|
|
2215
|
+
writeFileSync6(jsonPath, serialiseRecordJson(record2), "utf-8");
|
|
2216
|
+
writeFileSync6(mdPath, renderBuildRecordMarkdown(record2), "utf-8");
|
|
2217
|
+
return { jsonPath, mdPath };
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
// dist/build-record/sources.js
|
|
1882
2221
|
var MissingSourceError = class extends Error {
|
|
1883
2222
|
constructor(sourceName, detail) {
|
|
1884
2223
|
super(`[build-record] missing required source: ${sourceName} \u2014 ${detail}`);
|
|
@@ -1934,7 +2273,7 @@ function syntheticRunId(record2) {
|
|
|
1934
2273
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
1935
2274
|
}
|
|
1936
2275
|
function readGuardEvalLog(logDir, phaseId) {
|
|
1937
|
-
const logPath =
|
|
2276
|
+
const logPath = join10(logDir, `${renderPhaseSegment(phaseId)}.jsonl`);
|
|
1938
2277
|
if (!existsSync6(logPath)) {
|
|
1939
2278
|
throw new MissingSourceError("guard-eval log", `no runner guard-eval log at ${logPath} for phase ${phaseId}`);
|
|
1940
2279
|
}
|
|
@@ -1950,7 +2289,7 @@ function readGuardEvalLog(logDir, phaseId) {
|
|
|
1950
2289
|
return runs;
|
|
1951
2290
|
}
|
|
1952
2291
|
function readOrchestrationState(phasesDir, phaseId) {
|
|
1953
|
-
const statePath =
|
|
2292
|
+
const statePath = join10(phasesDir, `${renderPhaseSegment(phaseId)}-orchestration-state.json`);
|
|
1954
2293
|
if (!existsSync6(statePath)) {
|
|
1955
2294
|
throw new MissingSourceError("orchestration state", `no orchestration-state file at ${statePath} for phase ${phaseId}`);
|
|
1956
2295
|
}
|
|
@@ -1967,22 +2306,21 @@ function readOrchestrationState(phasesDir, phaseId) {
|
|
|
1967
2306
|
}
|
|
1968
2307
|
return { tasks };
|
|
1969
2308
|
}
|
|
1970
|
-
function
|
|
1971
|
-
|
|
1972
|
-
|
|
2309
|
+
function readPhaseClose(repoRoot, phaseId) {
|
|
2310
|
+
const path = closeRecordPath(repoRoot, phaseId);
|
|
2311
|
+
if (!existsSync6(path))
|
|
2312
|
+
return { closeCommit: null, closedDate: null };
|
|
2313
|
+
let parsed;
|
|
1973
2314
|
try {
|
|
1974
|
-
|
|
1975
|
-
encoding: "utf-8"
|
|
1976
|
-
}).trim();
|
|
1977
|
-
closedDate = execFileSync2("git", ["-C", repoRoot, "show", "-s", "--format=%cs", "HEAD"], { encoding: "utf-8" }).trim();
|
|
2315
|
+
parsed = JSON.parse(readFileSync7(path, "utf-8"));
|
|
1978
2316
|
} catch (err) {
|
|
1979
|
-
throw new
|
|
2317
|
+
throw new InvalidCloseRecordError(path, `it is not readable JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
2318
|
+
}
|
|
2319
|
+
const record2 = parseCloseRecord(path, parsed);
|
|
2320
|
+
if (renderPhaseSegment(record2.phase) !== renderPhaseSegment(phaseId)) {
|
|
2321
|
+
throw new InvalidCloseRecordError(path, `it records the close of ${String(record2.phase)}, not of ${String(phaseId)}`);
|
|
1980
2322
|
}
|
|
1981
|
-
|
|
1982
|
-
throw new MissingSourceError("git HEAD", `empty commit sha in ${repoRoot}`);
|
|
1983
|
-
if (!closedDate)
|
|
1984
|
-
throw new MissingSourceError("git HEAD", `empty commit date in ${repoRoot}`);
|
|
1985
|
-
return { closeCommit, closedDate };
|
|
2323
|
+
return { closeCommit: record2.closeCommit, closedDate: record2.closedDate };
|
|
1986
2324
|
}
|
|
1987
2325
|
function validateNarrated(narrated) {
|
|
1988
2326
|
const req = (v, name) => {
|
|
@@ -1993,6 +2331,7 @@ function validateNarrated(narrated) {
|
|
|
1993
2331
|
req(narrated.engagement, "engagement");
|
|
1994
2332
|
req(narrated.engagementType, "engagementType");
|
|
1995
2333
|
req(narrated.phase?.name, "phase.name");
|
|
2334
|
+
assertValidPhaseIdentity(narrated.phase?.id, BUILD_RECORD_DIR);
|
|
1996
2335
|
req(narrated.status, "status");
|
|
1997
2336
|
req(narrated.thesis, "thesis");
|
|
1998
2337
|
req(narrated.delivered, "delivered");
|
|
@@ -2008,7 +2347,7 @@ function validateNarrated(narrated) {
|
|
|
2008
2347
|
function loadSources(opts) {
|
|
2009
2348
|
const orchestration = readOrchestrationState(opts.phasesDir, opts.phaseId);
|
|
2010
2349
|
const guardEvalRuns = readGuardEvalLog(opts.guardEvalLogDir, opts.phaseId);
|
|
2011
|
-
const
|
|
2350
|
+
const close = readPhaseClose(opts.repoRoot, opts.phaseId);
|
|
2012
2351
|
const narrated = validateNarrated(opts.narrated);
|
|
2013
2352
|
if (!Array.isArray(opts.touchedInvariants)) {
|
|
2014
2353
|
throw new MissingSourceError("feature-spec Touches", "touchedInvariants must be an array");
|
|
@@ -2016,7 +2355,7 @@ function loadSources(opts) {
|
|
|
2016
2355
|
return {
|
|
2017
2356
|
orchestration,
|
|
2018
2357
|
narrated,
|
|
2019
|
-
|
|
2358
|
+
close,
|
|
2020
2359
|
guardEvalRuns,
|
|
2021
2360
|
touchedInvariants: opts.touchedInvariants
|
|
2022
2361
|
};
|
|
@@ -2046,15 +2385,15 @@ function deriveInvariantsExercised(touched) {
|
|
|
2046
2385
|
return [...new Set(touched)].sort((a, b) => a.localeCompare(b));
|
|
2047
2386
|
}
|
|
2048
2387
|
function assembleBuildRecord(sources) {
|
|
2049
|
-
const { narrated,
|
|
2388
|
+
const { narrated, close, orchestration } = sources;
|
|
2050
2389
|
return {
|
|
2051
2390
|
format: BUILD_RECORD_FORMAT,
|
|
2052
2391
|
engagement: narrated.engagement,
|
|
2053
2392
|
engagementType: narrated.engagementType,
|
|
2054
2393
|
phase: { id: narrated.phase.id, name: narrated.phase.name },
|
|
2055
2394
|
status: narrated.status,
|
|
2056
|
-
closedDate:
|
|
2057
|
-
closeCommit:
|
|
2395
|
+
closedDate: close.closedDate,
|
|
2396
|
+
closeCommit: close.closeCommit,
|
|
2058
2397
|
thesis: narrated.thesis,
|
|
2059
2398
|
thesisHeld: deriveThesisHeld(narrated.acceptance),
|
|
2060
2399
|
delivered: narrated.delivered,
|
|
@@ -2068,85 +2407,6 @@ function assembleBuildRecord(sources) {
|
|
|
2068
2407
|
};
|
|
2069
2408
|
}
|
|
2070
2409
|
|
|
2071
|
-
// dist/build-record/template.js
|
|
2072
|
-
function severityLabel(seed) {
|
|
2073
|
-
return `\`${seed.patternRef}\` (severity \`${seed.severity}\`)`;
|
|
2074
|
-
}
|
|
2075
|
-
function renderDoneWhenRows(record2) {
|
|
2076
|
-
const rows = [];
|
|
2077
|
-
for (const [key, dw] of Object.entries(record2.acceptance.doneWhen)) {
|
|
2078
|
-
const mark = dw.result === "pass" ? "\u2705" : dw.result === "dropped" ? "\u2014" : dw.result;
|
|
2079
|
-
rows.push(`| ${key} | ${mark} | ${dw.detail} |`);
|
|
2080
|
-
}
|
|
2081
|
-
return rows.join("\n");
|
|
2082
|
-
}
|
|
2083
|
-
function renderBuildRecordMarkdown(record2) {
|
|
2084
|
-
const { phase } = record2;
|
|
2085
|
-
const guardsList = record2.seedGuards.map(severityLabel).join(", ");
|
|
2086
|
-
const lines = [
|
|
2087
|
-
// Fixed prose (vault-canonical template) + projected title slots.
|
|
2088
|
-
`# Build Record \u2014 Phase ${phase.id}: ${phase.name}`,
|
|
2089
|
-
"",
|
|
2090
|
-
`**Format:** \`${record2.format}\` (markdown + JSON pair; portable, readable without Halfcycle systems)`,
|
|
2091
|
-
"",
|
|
2092
|
-
"> This Build Record was assembled automatically at phase close by the method bundle (`packages/bundle`). Every data field below is projected from the companion JSON \u2014 no record content is hand-authored.",
|
|
2093
|
-
"",
|
|
2094
|
-
`**Engagement:** ${record2.engagement} (${record2.engagementType})`,
|
|
2095
|
-
`**Phase:** ${phase.id} \u2014 ${phase.name}`,
|
|
2096
|
-
`**Status:** ${record2.status.toUpperCase()} \xB7 **Closed:** ${record2.closedDate} \xB7 **Close commit:** \`${record2.closeCommit}\``,
|
|
2097
|
-
`**Companion:** [\`phase-${phase.id}.json\`](phase-${phase.id}.json)`,
|
|
2098
|
-
"",
|
|
2099
|
-
"## What this phase proved",
|
|
2100
|
-
"",
|
|
2101
|
-
`${record2.thesis} **Thesis held: ${record2.thesisHeld ? "yes" : "no"}.**`,
|
|
2102
|
-
"",
|
|
2103
|
-
"## Delivered",
|
|
2104
|
-
"",
|
|
2105
|
-
`- **Packages:** ${record2.delivered.packages.join(", ")}`,
|
|
2106
|
-
`- **Services:** ${record2.delivered.services.join(", ")}`,
|
|
2107
|
-
`- **Tools:** ${record2.delivered.tools.join(", ")}`,
|
|
2108
|
-
`- **Dogfood:** ${record2.delivered.dogfood}`,
|
|
2109
|
-
"",
|
|
2110
|
-
guardsList ? `Guards evaluated (INV-007-safe results): ${guardsList}. *(Guard \`channel\`/\`version\` are guard-asset metadata, excluded from the v1 record \u2014 INV-007/INV-001.)*` : "No guards fired during this phase.",
|
|
2111
|
-
"",
|
|
2112
|
-
`## Acceptance (independent walk \u2014 walker ${record2.acceptance.walker})`,
|
|
2113
|
-
"",
|
|
2114
|
-
`**Verdict:** ${record2.acceptance.verdict} \xB7 **Findings:** ${record2.acceptance.findings}`,
|
|
2115
|
-
"",
|
|
2116
|
-
"| Done-when | Result | Evidence |",
|
|
2117
|
-
"|---|---|---|",
|
|
2118
|
-
renderDoneWhenRows(record2),
|
|
2119
|
-
""
|
|
2120
|
-
];
|
|
2121
|
-
if (record2.acceptance.inv002ProdBypassProbe) {
|
|
2122
|
-
lines.push(`**INV-002 production bypass probe:** ${record2.acceptance.inv002ProdBypassProbe.result} \u2014 ${record2.acceptance.inv002ProdBypassProbe.detail}`, "");
|
|
2123
|
-
}
|
|
2124
|
-
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"}`, "");
|
|
2125
|
-
return lines.join("\n");
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
// dist/build-record/write.js
|
|
2129
|
-
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2130
|
-
import { join as join9, resolve, relative as relative2, isAbsolute } from "node:path";
|
|
2131
|
-
var BUILD_RECORD_DIR = join9("docs", "build-records");
|
|
2132
|
-
function serialiseRecordJson(record2) {
|
|
2133
|
-
return JSON.stringify(record2, null, 2) + "\n";
|
|
2134
|
-
}
|
|
2135
|
-
function writeBuildRecord(repoRoot, record2) {
|
|
2136
|
-
const outDir = resolve(repoRoot, BUILD_RECORD_DIR);
|
|
2137
|
-
const expected = resolve(repoRoot, BUILD_RECORD_DIR);
|
|
2138
|
-
const rel = relative2(expected, outDir);
|
|
2139
|
-
if (rel !== "" || isAbsolute(rel)) {
|
|
2140
|
-
throw new Error(`[build-record] refusing to write outside ${BUILD_RECORD_DIR}`);
|
|
2141
|
-
}
|
|
2142
|
-
mkdirSync5(outDir, { recursive: true });
|
|
2143
|
-
const jsonPath = join9(outDir, `phase-${record2.phase.id}.json`);
|
|
2144
|
-
const mdPath = join9(outDir, `phase-${record2.phase.id}.md`);
|
|
2145
|
-
writeFileSync5(jsonPath, serialiseRecordJson(record2), "utf-8");
|
|
2146
|
-
writeFileSync5(mdPath, renderBuildRecordMarkdown(record2), "utf-8");
|
|
2147
|
-
return { jsonPath, mdPath };
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
2410
|
// dist/build-record/index.js
|
|
2151
2411
|
function assemblePhaseBuildRecord(opts) {
|
|
2152
2412
|
const sources = loadSources({
|
|
@@ -2163,9 +2423,13 @@ function assemblePhaseBuildRecord(opts) {
|
|
|
2163
2423
|
export {
|
|
2164
2424
|
BUILD_RECORD_DIR,
|
|
2165
2425
|
BUILD_RECORD_FORMAT,
|
|
2426
|
+
BUILD_RECORD_ZONE_B_DIR,
|
|
2427
|
+
CLOSE_RECORD_FORMAT,
|
|
2166
2428
|
CreateEngagementRefused,
|
|
2167
2429
|
FLAT_FILE_TRACKER_HOME,
|
|
2168
2430
|
HALFCYCLE_STATE_FORMAT,
|
|
2431
|
+
InvalidCloseRecordError,
|
|
2432
|
+
InvalidPhaseIdentityError,
|
|
2169
2433
|
MissingSourceError,
|
|
2170
2434
|
SetupManifest,
|
|
2171
2435
|
ZONE_B_DIR,
|
|
@@ -2173,8 +2437,10 @@ export {
|
|
|
2173
2437
|
applySetupWrites,
|
|
2174
2438
|
assembleBuildRecord,
|
|
2175
2439
|
assemblePhaseBuildRecord,
|
|
2440
|
+
assertValidPhaseIdentity,
|
|
2176
2441
|
buildEngagementRecord,
|
|
2177
2442
|
checkDrift,
|
|
2443
|
+
closeRecordPath,
|
|
2178
2444
|
contextIndexRow,
|
|
2179
2445
|
createEngagement,
|
|
2180
2446
|
deriveInvariantsExercised,
|
|
@@ -2185,29 +2451,33 @@ export {
|
|
|
2185
2451
|
engagementRecordRow,
|
|
2186
2452
|
flatFileTrackerDefault,
|
|
2187
2453
|
install,
|
|
2454
|
+
isValidPhaseIdentity,
|
|
2188
2455
|
layerDocumentRow,
|
|
2189
2456
|
loadSources,
|
|
2190
2457
|
mergeSettings,
|
|
2191
2458
|
mergeWithRecords,
|
|
2192
2459
|
mintBoardEnterCode,
|
|
2193
2460
|
mintOrReadIdentity,
|
|
2461
|
+
parseCloseRecord,
|
|
2194
2462
|
projectSeedGuard,
|
|
2195
2463
|
projectSeedGuards,
|
|
2196
2464
|
questionsToPut,
|
|
2197
2465
|
readBundlePin,
|
|
2198
|
-
readGitClose,
|
|
2199
2466
|
readGuardEvalLog,
|
|
2200
2467
|
readOrchestrationState,
|
|
2468
|
+
readPhaseClose,
|
|
2201
2469
|
readRemote,
|
|
2202
2470
|
readRootCommit,
|
|
2203
2471
|
renderAnchor,
|
|
2204
2472
|
renderBuildRecordMarkdown,
|
|
2205
2473
|
renderDiscovery,
|
|
2474
|
+
renderPhaseSegment,
|
|
2206
2475
|
runBootstrapScan,
|
|
2207
2476
|
scanLayers,
|
|
2208
2477
|
serialiseRecordJson,
|
|
2209
2478
|
trackerHomeRow,
|
|
2210
2479
|
validateNarrated,
|
|
2211
|
-
writeBuildRecord
|
|
2480
|
+
writeBuildRecord,
|
|
2481
|
+
writeCloseRecord
|
|
2212
2482
|
};
|
|
2213
2483
|
//# sourceMappingURL=index.js.map
|