dev-loops 0.6.0 → 0.7.1
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/.claude-plugin/plugin.json +1 -1
- package/.claude/agents/dev-loop.md +2 -1
- package/.claude/agents/review.md +2 -1
- package/.claude/commands/{auto.md → loop-auto.md} +1 -1
- package/.claude/commands/loop-continue.md +15 -0
- package/.claude/commands/loop-enqueue.md +22 -0
- package/.claude/commands/loop-grill.md +17 -0
- package/.claude/commands/{info.md → loop-info.md} +2 -2
- package/.claude/commands/loop-queue-status.md +24 -0
- package/.claude/commands/{start-spike.md → loop-start-spike.md} +1 -1
- package/.claude/commands/{start.md → loop-start.md} +1 -1
- package/.claude/commands/{status.md → loop-status.md} +1 -1
- package/.claude/hooks/_bash-command-classify.mjs +333 -29
- package/.claude/hooks/_hook-decisions.mjs +138 -15
- package/.claude/hooks/pre-tool-use-bash-gate.mjs +36 -15
- package/.claude/skills/copilot-pr-followup/SKILL.md +24 -12
- package/.claude/skills/dev-loop/SKILL.md +15 -11
- package/.claude/skills/docs/acceptance-criteria-verification.md +6 -2
- package/.claude/skills/docs/anti-patterns.md +2 -1
- package/.claude/skills/docs/artifact-authority-contract.md +22 -4
- package/.claude/skills/docs/copilot-loop-operations.md +1 -1
- package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
- package/.claude/skills/docs/issue-intake-procedure.md +1 -0
- package/.claude/skills/docs/local-planning-flow.md +1 -1
- package/.claude/skills/docs/local-planning-worked-example.md +1 -1
- package/.claude/skills/docs/merge-preconditions.md +17 -1
- package/.claude/skills/docs/plan-file-contract.md +1 -1
- package/.claude/skills/docs/public-dev-loop-contract.md +1 -1
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +90 -76
- package/.claude/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
- package/.claude/skills/local-implementation/SKILL.md +25 -14
- package/.claude/skills/loop-grill/SKILL.md +163 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +95 -0
- package/README.md +9 -9
- package/agents/dev-loop.agent.md +3 -2
- package/agents/developer.agent.md +1 -1
- package/agents/docs.agent.md +1 -1
- package/agents/fixer.agent.md +1 -1
- package/agents/quality.agent.md +1 -1
- package/agents/refiner.agent.md +1 -1
- package/agents/review.agent.md +3 -2
- package/cli/index.mjs +35 -42
- package/extension/README.md +4 -4
- package/extension/checks.ts +1 -5
- package/extension/harness-types.ts +1 -0
- package/extension/index.ts +6 -0
- package/extension/pi-extension-adapter.ts +2 -0
- package/extension/presentation.ts +7 -19
- package/package.json +9 -10
- package/scripts/github/capture-review-threads.mjs +2 -9
- package/scripts/github/comment-issue.mjs +2 -9
- package/scripts/github/create-label.mjs +133 -0
- package/scripts/github/detect-checkpoint-evidence.mjs +170 -14
- package/scripts/github/detect-linked-issue-pr.mjs +22 -6
- package/scripts/github/edit-pr.mjs +259 -0
- package/scripts/github/fetch-ci-logs.mjs +2 -9
- package/scripts/github/list-issues.mjs +2 -9
- package/scripts/github/manage-sub-issues.mjs +46 -10
- package/scripts/github/offer-human-handoff.mjs +8 -5
- package/scripts/github/post-gate-findings.mjs +14 -2
- package/scripts/github/probe-ci-status.mjs +8 -2
- package/scripts/github/probe-copilot-review.mjs +21 -14
- package/scripts/github/ready-for-review.mjs +26 -8
- package/scripts/github/reconcile-draft-gate.mjs +7 -3
- package/scripts/github/reply-resolve-review-thread.mjs +16 -5
- package/scripts/github/reply-resolve-review-threads.mjs +69 -7
- package/scripts/github/request-copilot-review.mjs +8 -2
- package/scripts/github/resolve-handoff-candidates.mjs +7 -3
- package/scripts/github/resolve-tracker-local-spec.mjs +9 -3
- package/scripts/github/stage-reviewer-draft.mjs +10 -2
- package/scripts/github/tick-verified-checkboxes.mjs +202 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +31 -10
- package/scripts/github/verify-fresh-review-context.mjs +150 -31
- package/scripts/github/view-pr.mjs +150 -0
- package/scripts/github/write-gate-context.mjs +248 -61
- package/scripts/github/write-gate-findings-log.mjs +75 -1
- package/scripts/lib/jq-output.mjs +18 -0
- package/scripts/loop/_post-convergence-change.mjs +211 -0
- package/scripts/loop/_pr-runner-coordination.mjs +70 -0
- package/scripts/loop/_repo-root-resolver.mjs +47 -0
- package/scripts/loop/build-handoff-envelope.mjs +14 -4
- package/scripts/loop/check-retro-tooling.mjs +14 -3
- package/scripts/loop/checkpoint-contract.mjs +7 -4
- package/scripts/loop/cleanup-worktree.mjs +12 -3
- package/scripts/loop/conductor-monitor.mjs +12 -18
- package/scripts/loop/copilot-pr-handoff.mjs +135 -14
- package/scripts/loop/debt-remediate.mjs +24 -12
- package/scripts/loop/detect-change-scope.mjs +36 -7
- package/scripts/loop/detect-copilot-loop-state.mjs +11 -5
- package/scripts/loop/detect-copilot-session-activity.mjs +7 -3
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +7 -3
- package/scripts/loop/detect-internal-only-pr.mjs +8 -2
- package/scripts/loop/detect-issue-refinement-artifact.mjs +6 -3
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +149 -66
- package/scripts/loop/detect-reviewer-loop-state.mjs +12 -2
- package/scripts/loop/detect-tracker-first-loop-state.mjs +9 -1
- package/scripts/loop/detect-tracker-pr-state.mjs +10 -3
- package/scripts/loop/ensure-worktree.mjs +8 -3
- package/scripts/loop/info.mjs +12 -4
- package/scripts/loop/inspect-run-viewer/constants.mjs +4 -18
- package/scripts/loop/inspect-run-viewer/vendor/mermaid.min.js +3405 -0
- package/scripts/loop/inspect-run-viewer-ci-changes.mjs +18 -6
- package/scripts/loop/inspect-run-viewer.mjs +67 -4
- package/scripts/loop/inspect-run.mjs +8 -2
- package/scripts/loop/outer-loop.mjs +8 -4
- package/scripts/loop/pr-runner-coordination.mjs +2 -9
- package/scripts/loop/pre-commit-branch-guard.mjs +16 -10
- package/scripts/loop/pre-flight-gate.mjs +9 -9
- package/scripts/loop/pre-pr-ready-gate.mjs +8 -4
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +13 -4
- package/scripts/loop/provision-worktree.mjs +74 -3
- package/scripts/loop/resolve-dev-loop-startup.mjs +76 -7
- package/scripts/loop/resolve-gate-dispatch.mjs +134 -0
- package/scripts/loop/resolve-pr-conflicts.mjs +14 -7
- package/scripts/loop/run-conductor-cycle.mjs +8 -2
- package/scripts/loop/run-queue.mjs +18 -9
- package/scripts/loop/run-refinement-audit.mjs +8 -9
- package/scripts/loop/run-watch-cycle.mjs +2 -9
- package/scripts/loop/sanctioned-commands.mjs +104 -0
- package/scripts/loop/steer-loop.mjs +29 -16
- package/scripts/loop/validate-pr-body-spec.mjs +207 -0
- package/scripts/loop/watch-initial-copilot-pr.mjs +8 -3
- package/scripts/pages/build-site.mjs +59 -8
- package/scripts/pages/build-state-atlas.mjs +441 -0
- package/scripts/projects/_resolve-project.mjs +148 -0
- package/scripts/projects/add-queue-item.mjs +60 -54
- package/scripts/projects/archive-done-items.mjs +49 -84
- package/scripts/projects/ensure-queue-board.mjs +10 -36
- package/scripts/projects/list-queue-items.mjs +116 -65
- package/scripts/projects/move-queue-item.mjs +28 -49
- package/scripts/projects/reconcile-queue.mjs +253 -0
- package/scripts/projects/reorder-queue-item.mjs +41 -47
- package/scripts/projects/resolve-active-board-item.mjs +108 -46
- package/scripts/projects/sync-item-status.mjs +15 -10
- package/scripts/refine/_refine-helpers.mjs +21 -5
- package/scripts/refine/exit-spike.mjs +18 -8
- package/scripts/refine/promote-plan.mjs +22 -16
- package/scripts/refine/prose-linkage-detector.mjs +1 -1
- package/scripts/refine/refine-plan-file.mjs +13 -4
- package/scripts/refine/refinement-completeness-checker.mjs +1 -1
- package/scripts/refine/scaffold-spike-file.mjs +4 -8
- package/scripts/refine/scope-boundary-cross-checker.mjs +1 -1
- package/scripts/refine/tree-integrity-validator.mjs +1 -1
- package/scripts/refine/validate-plan-file.mjs +1 -1
- package/scripts/refine/validate-spike-file.mjs +1 -1
- package/scripts/refine/verify.mjs +11 -6
- package/scripts/release/assert-core-dependency-version.mjs +123 -0
- package/scripts/release/extract-changelog-section.mjs +16 -2
- package/skills/copilot-pr-followup/SKILL.md +24 -12
- package/skills/dev-loop/SKILL.md +10 -6
- package/skills/docs/acceptance-criteria-verification.md +6 -2
- package/skills/docs/anti-patterns.md +2 -1
- package/skills/docs/artifact-authority-contract.md +22 -4
- package/skills/docs/copilot-loop-operations.md +1 -1
- package/skills/docs/cross-harness-regression-contract.md +60 -0
- package/skills/docs/issue-intake-procedure.md +1 -0
- package/skills/docs/local-planning-flow.md +1 -1
- package/skills/docs/local-planning-worked-example.md +1 -1
- package/skills/docs/merge-preconditions.md +17 -1
- package/skills/docs/plan-file-contract.md +1 -1
- package/skills/docs/public-dev-loop-contract.md +1 -1
- package/skills/docs/retrospective-checkpoint-contract.md +90 -76
- package/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/skills/docs/workflow-handoff-contract.md +39 -0
- package/skills/local-implementation/SKILL.md +25 -14
- package/skills/loop-grill/SKILL.md +165 -0
- package/.claude/commands/continue.md +0 -15
- package/scripts/loop/conductor.mjs +0 -233
- package/scripts/loop/detect-stale-runner.mjs +0 -265
- package/scripts/loop/pre-push-main-guard.mjs +0 -117
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { formatCliError } from "../_core-helpers.mjs";
|
|
35
35
|
import { requireTokenValue as readSharedTokenValue } from "../_cli-primitives.mjs";
|
|
36
36
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
37
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
37
38
|
const SUBMIT_USAGE = `Usage:
|
|
38
39
|
steer-loop.mjs submit --repo <owner/name> --pr <number>
|
|
39
40
|
--kind stop_at_next_safe_gate --directive <text> --seq <n>
|
|
@@ -61,7 +62,9 @@ Optional:
|
|
|
61
62
|
Output (stdout, JSON):
|
|
62
63
|
{ "ok": true, "acknowledgement": { ... }, "result": { ... }, "steeringState": { ... } }
|
|
63
64
|
Error output (stderr, JSON):
|
|
64
|
-
{ "ok": false, "error": "...", "usage": "..." }
|
|
65
|
+
{ "ok": false, "error": "...", "usage": "..." }
|
|
66
|
+
|
|
67
|
+
${JQ_OUTPUT_USAGE}`.trim();
|
|
65
68
|
const STATUS_USAGE = `Usage:
|
|
66
69
|
steer-loop.mjs status --run-id <id> [--state-file <path>]
|
|
67
70
|
steer-loop.mjs status --repo <owner/name> --pr <number> [--state-file <path>]
|
|
@@ -75,7 +78,9 @@ Optional:
|
|
|
75
78
|
Output (stdout, JSON):
|
|
76
79
|
{ "ok": true, "status": { ... } }
|
|
77
80
|
Error output (stderr, JSON):
|
|
78
|
-
{ "ok": false, "error": "...", "usage": "..." }
|
|
81
|
+
{ "ok": false, "error": "...", "usage": "..." }
|
|
82
|
+
|
|
83
|
+
${JQ_OUTPUT_USAGE}`.trim();
|
|
79
84
|
const PROMOTE_USAGE = `Usage:
|
|
80
85
|
steer-loop.mjs promote --run-id <id> --loop-state <state> [--state-file <path>]
|
|
81
86
|
steer-loop.mjs promote --repo <owner/name> --pr <number>
|
|
@@ -93,7 +98,9 @@ Optional:
|
|
|
93
98
|
Output (stdout, JSON):
|
|
94
99
|
{ "ok": true, "promotedCount": <n>, "promoted": [ ... ], "steeringState": { ... } }
|
|
95
100
|
Error output (stderr, JSON):
|
|
96
|
-
{ "ok": false, "error": "...", "usage": "..." }
|
|
101
|
+
{ "ok": false, "error": "...", "usage": "..." }
|
|
102
|
+
|
|
103
|
+
${JQ_OUTPUT_USAGE}`.trim();
|
|
97
104
|
const TOP_USAGE = `Usage:
|
|
98
105
|
steer-loop.mjs <subcommand> [options]
|
|
99
106
|
Subcommands:
|
|
@@ -171,6 +178,7 @@ export function parseSubmitCliArgs(argv) {
|
|
|
171
178
|
"event-id": { type: "string" },
|
|
172
179
|
"copilot-input": { type: "string" },
|
|
173
180
|
"reviewer-input": { type: "string" },
|
|
181
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
174
182
|
},
|
|
175
183
|
allowPositionals: true,
|
|
176
184
|
strict: false,
|
|
@@ -250,6 +258,7 @@ export function parseSubmitCliArgs(argv) {
|
|
|
250
258
|
options.reviewerInputPath = readRequiredOptionValue(token, SUBMIT_USAGE);
|
|
251
259
|
continue;
|
|
252
260
|
}
|
|
261
|
+
if (matchJqOutputToken(token, options, (t) => readRequiredOptionValue(t, SUBMIT_USAGE))) continue;
|
|
253
262
|
throw usageError(`Unknown argument: ${token.rawName}`, SUBMIT_USAGE);
|
|
254
263
|
}
|
|
255
264
|
if (!options.help) {
|
|
@@ -290,6 +299,7 @@ export function parseStatusCliArgs(argv) {
|
|
|
290
299
|
repo: { type: "string" },
|
|
291
300
|
pr: { type: "string" },
|
|
292
301
|
"state-file": { type: "string" },
|
|
302
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
293
303
|
},
|
|
294
304
|
allowPositionals: true,
|
|
295
305
|
strict: false,
|
|
@@ -324,6 +334,7 @@ export function parseStatusCliArgs(argv) {
|
|
|
324
334
|
options.stateFile = readRequiredOptionValue(token, STATUS_USAGE);
|
|
325
335
|
continue;
|
|
326
336
|
}
|
|
337
|
+
if (matchJqOutputToken(token, options, (t) => readRequiredOptionValue(t, STATUS_USAGE))) continue;
|
|
327
338
|
throw usageError(`Unknown argument: ${token.rawName}`, STATUS_USAGE);
|
|
328
339
|
}
|
|
329
340
|
if (!options.help) {
|
|
@@ -357,6 +368,7 @@ export function parsePromoteCliArgs(argv) {
|
|
|
357
368
|
pr: { type: "string" },
|
|
358
369
|
"state-file": { type: "string" },
|
|
359
370
|
"loop-state": { type: "string" },
|
|
371
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
360
372
|
},
|
|
361
373
|
allowPositionals: true,
|
|
362
374
|
strict: false,
|
|
@@ -399,6 +411,7 @@ export function parsePromoteCliArgs(argv) {
|
|
|
399
411
|
options.loopState = val;
|
|
400
412
|
continue;
|
|
401
413
|
}
|
|
414
|
+
if (matchJqOutputToken(token, options, (t) => readRequiredOptionValue(t, PROMOTE_USAGE))) continue;
|
|
402
415
|
throw usageError(`Unknown argument: ${token.rawName}`, PROMOTE_USAGE);
|
|
403
416
|
}
|
|
404
417
|
if (!options.help) {
|
|
@@ -651,7 +664,7 @@ function rejectUnsteerableInspection(inspection, { runId, eventId, seq, directiv
|
|
|
651
664
|
}
|
|
652
665
|
export async function runSubmit(
|
|
653
666
|
argv = [],
|
|
654
|
-
{ stdout = process.stdout, cwd = process.cwd(), env = process.env, ghCommand = "gh" } = {},
|
|
667
|
+
{ stdout = process.stdout, stderr = process.stderr, cwd = process.cwd(), env = process.env, ghCommand = "gh" } = {},
|
|
655
668
|
) {
|
|
656
669
|
const options = parseSubmitCliArgs(argv);
|
|
657
670
|
if (options.help) {
|
|
@@ -793,12 +806,12 @@ export async function runSubmit(
|
|
|
793
806
|
} catch {
|
|
794
807
|
steeringState = createSteeringState(runId, target);
|
|
795
808
|
}
|
|
796
|
-
|
|
809
|
+
process.exitCode = emitResult({
|
|
797
810
|
ok: true,
|
|
798
811
|
acknowledgement: validationRejection.acknowledgement,
|
|
799
812
|
result: validationRejection.result,
|
|
800
813
|
steeringState,
|
|
801
|
-
}
|
|
814
|
+
}, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
802
815
|
return;
|
|
803
816
|
}
|
|
804
817
|
const { steeringState: newState, result } = await withStateFileLock(stateFilePath, async () => {
|
|
@@ -828,9 +841,9 @@ export async function runSubmit(
|
|
|
828
841
|
safePointCategory,
|
|
829
842
|
readbackPath,
|
|
830
843
|
});
|
|
831
|
-
|
|
844
|
+
process.exitCode = emitResult({ ok: true, acknowledgement, result, steeringState: newState }, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
832
845
|
}
|
|
833
|
-
export async function runStatus(argv = [], { stdout = process.stdout, cwd = process.cwd() } = {}) {
|
|
846
|
+
export async function runStatus(argv = [], { stdout = process.stdout, stderr = process.stderr, cwd = process.cwd() } = {}) {
|
|
834
847
|
const options = parseStatusCliArgs(argv);
|
|
835
848
|
if (options.help) {
|
|
836
849
|
stdout.write(`${STATUS_USAGE}\n`);
|
|
@@ -843,9 +856,9 @@ export async function runStatus(argv = [], { stdout = process.stdout, cwd = proc
|
|
|
843
856
|
const stateFilePath = options.stateFile ?? (target ? defaultStateFilePathForTarget(target, cwd) : defaultStateFilePath(runId, cwd));
|
|
844
857
|
const steeringState = await loadOrCreateSteeringState(stateFilePath, runId, target);
|
|
845
858
|
const status = getSteeringStatus(steeringState);
|
|
846
|
-
|
|
859
|
+
process.exitCode = emitResult({ ok: true, status }, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
847
860
|
}
|
|
848
|
-
export async function runPromote(argv = [], { stdout = process.stdout, cwd = process.cwd() } = {}) {
|
|
861
|
+
export async function runPromote(argv = [], { stdout = process.stdout, stderr = process.stderr, cwd = process.cwd() } = {}) {
|
|
849
862
|
const options = parsePromoteCliArgs(argv);
|
|
850
863
|
if (options.help) {
|
|
851
864
|
stdout.write(`${PROMOTE_USAGE}\n`);
|
|
@@ -864,16 +877,16 @@ export async function runPromote(argv = [], { stdout = process.stdout, cwd = pro
|
|
|
864
877
|
}
|
|
865
878
|
return nextState;
|
|
866
879
|
});
|
|
867
|
-
|
|
880
|
+
process.exitCode = emitResult({
|
|
868
881
|
ok: true,
|
|
869
882
|
promotedCount: promotedState.promoted.length,
|
|
870
883
|
promoted: promotedState.promoted,
|
|
871
884
|
steeringState: promotedState.steeringState,
|
|
872
|
-
}
|
|
885
|
+
}, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
873
886
|
}
|
|
874
887
|
export async function runCli(
|
|
875
888
|
argv = process.argv.slice(2),
|
|
876
|
-
{ stdout = process.stdout, cwd = process.cwd(), env = process.env, ghCommand = "gh" } = {},
|
|
889
|
+
{ stdout = process.stdout, stderr = process.stderr, cwd = process.cwd(), env = process.env, ghCommand = "gh" } = {},
|
|
877
890
|
) {
|
|
878
891
|
const [subcommand, ...rest] = argv;
|
|
879
892
|
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
@@ -881,13 +894,13 @@ export async function runCli(
|
|
|
881
894
|
return;
|
|
882
895
|
}
|
|
883
896
|
if (subcommand === "submit") {
|
|
884
|
-
return runSubmit(rest, { stdout, cwd, env, ghCommand });
|
|
897
|
+
return runSubmit(rest, { stdout, stderr, cwd, env, ghCommand });
|
|
885
898
|
}
|
|
886
899
|
if (subcommand === "promote") {
|
|
887
|
-
return runPromote(rest, { stdout, cwd });
|
|
900
|
+
return runPromote(rest, { stdout, stderr, cwd });
|
|
888
901
|
}
|
|
889
902
|
if (subcommand === "status") {
|
|
890
|
-
return runStatus(rest, { stdout, cwd });
|
|
903
|
+
return runStatus(rest, { stdout, stderr, cwd });
|
|
891
904
|
}
|
|
892
905
|
const error = usageError(`Unknown subcommand: ${subcommand}`, TOP_USAGE);
|
|
893
906
|
throw error;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validate that a PR body carries the invariants required to serve as the
|
|
4
|
+
* lightweight spec-of-record (issue #1025): Objective/why, in-scope, explicit
|
|
5
|
+
* non-goals, testable Acceptance criteria, Definition of done, and Open
|
|
6
|
+
* questions/risks.
|
|
7
|
+
*
|
|
8
|
+
* Thin CLI wrapper (parallel to detect-issue-refinement-artifact.mjs): fetches
|
|
9
|
+
* the PR body via `gh pr view <n> --json body` and runs the shared pure
|
|
10
|
+
* validator `validatePrBodySpec` (reuses issue-refinement-artifact markdown
|
|
11
|
+
* logic — no parallel validator). Fails closed (non-zero + per-section reason)
|
|
12
|
+
* when any invariant is missing.
|
|
13
|
+
*/
|
|
14
|
+
import { readFile } from "node:fs/promises";
|
|
15
|
+
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
16
|
+
import { requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
17
|
+
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
18
|
+
import { parseArgs } from "node:util";
|
|
19
|
+
import { validatePrBodySpec } from "@dev-loops/core/loop/issue-refinement-artifact";
|
|
20
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
21
|
+
|
|
22
|
+
const USAGE = `Usage:
|
|
23
|
+
validate-pr-body-spec.mjs --repo <owner/name> --pr <number> [--expected-issue <n>]
|
|
24
|
+
validate-pr-body-spec.mjs --input <path> [--expected-issue <n>]
|
|
25
|
+
Validate that a PR body carries the lightweight spec-of-record invariants
|
|
26
|
+
(Objective/why, In scope, Explicit non-goals, testable Acceptance criteria,
|
|
27
|
+
Definition of done, Open questions/risks, and a GitHub closing-keyword issue
|
|
28
|
+
reference such as \`Closes #123\`).
|
|
29
|
+
Required (exactly one):
|
|
30
|
+
--repo <owner/name> --pr <number> Fetch the PR body via gh and validate it
|
|
31
|
+
--input <path> Path to a JSON file with { "body": "..." }
|
|
32
|
+
(optional "repo" and "pr" fields are echoed
|
|
33
|
+
back in the result when present)
|
|
34
|
+
Optional:
|
|
35
|
+
--expected-issue <n> Positive integer; the referenced closing
|
|
36
|
+
issue(s) must include this number, else
|
|
37
|
+
"closes_wrong_issue".
|
|
38
|
+
Success output (stdout, JSON):
|
|
39
|
+
{
|
|
40
|
+
"ok": true | false,
|
|
41
|
+
"checker": "validate-pr-body-spec",
|
|
42
|
+
"repo": "owner/name" | null,
|
|
43
|
+
"pr": 123 | null,
|
|
44
|
+
"errors": [ { "code": "missing_...", "message": "..." } ],
|
|
45
|
+
"sections": [...],
|
|
46
|
+
"acItems": [...],
|
|
47
|
+
"dodItems": [...],
|
|
48
|
+
"closesIssues": [...]
|
|
49
|
+
}
|
|
50
|
+
Exit: 0 when every invariant is present; 1 (fail closed) when any is missing.
|
|
51
|
+
Error output (stderr, JSON):
|
|
52
|
+
{ "ok": false, "error": "...", "usage": "..." }
|
|
53
|
+
${JQ_OUTPUT_USAGE}`.trim();
|
|
54
|
+
|
|
55
|
+
const parseError = buildParseError(USAGE);
|
|
56
|
+
|
|
57
|
+
export function parseValidatePrBodySpecCliArgs(argv) {
|
|
58
|
+
const options = {
|
|
59
|
+
help: false,
|
|
60
|
+
repo: undefined,
|
|
61
|
+
pr: undefined,
|
|
62
|
+
input: undefined,
|
|
63
|
+
expectedIssue: undefined,
|
|
64
|
+
};
|
|
65
|
+
const { tokens } = parseArgs({
|
|
66
|
+
args: [...argv],
|
|
67
|
+
options: {
|
|
68
|
+
help: { type: "boolean", short: "h" },
|
|
69
|
+
repo: { type: "string" },
|
|
70
|
+
pr: { type: "string" },
|
|
71
|
+
input: { type: "string" },
|
|
72
|
+
"expected-issue": { type: "string" },
|
|
73
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
74
|
+
},
|
|
75
|
+
allowPositionals: true,
|
|
76
|
+
strict: false,
|
|
77
|
+
tokens: true,
|
|
78
|
+
});
|
|
79
|
+
for (const token of tokens) {
|
|
80
|
+
if (token.kind === "positional") {
|
|
81
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
82
|
+
}
|
|
83
|
+
if (token.kind !== "option") {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (token.name === "help") {
|
|
87
|
+
options.help = true;
|
|
88
|
+
return options;
|
|
89
|
+
}
|
|
90
|
+
if (token.name === "repo") {
|
|
91
|
+
options.repo = requireTokenValue(token, parseError).trim();
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (token.name === "pr") {
|
|
95
|
+
const value = requireTokenValue(token, parseError);
|
|
96
|
+
if (!/^\d+$/.test(value) || Number(value) === 0) {
|
|
97
|
+
throw parseError("--pr must be a positive integer");
|
|
98
|
+
}
|
|
99
|
+
options.pr = Number(value);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (token.name === "input") {
|
|
103
|
+
options.input = requireTokenValue(token, parseError).trim();
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (token.name === "expected-issue") {
|
|
107
|
+
const value = requireTokenValue(token, parseError);
|
|
108
|
+
if (!/^\d+$/.test(value) || Number(value) === 0) {
|
|
109
|
+
throw parseError("--expected-issue must be a positive integer");
|
|
110
|
+
}
|
|
111
|
+
options.expectedIssue = Number(value);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
115
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
116
|
+
}
|
|
117
|
+
if (options.help) {
|
|
118
|
+
return options;
|
|
119
|
+
}
|
|
120
|
+
const hasInput = typeof options.input === "string" && options.input.length > 0;
|
|
121
|
+
const hasAnyRemote = (typeof options.repo === "string" && options.repo.length > 0) || Number.isInteger(options.pr);
|
|
122
|
+
const hasRemotePair = typeof options.repo === "string" && options.repo.length > 0 && Number.isInteger(options.pr);
|
|
123
|
+
// --input and the remote mode are mutually exclusive input sources: a stray
|
|
124
|
+
// --repo/--pr alongside --input must fail closed, not be silently ignored.
|
|
125
|
+
if (hasInput && hasAnyRemote) {
|
|
126
|
+
throw parseError("--input is mutually exclusive with --repo/--pr; provide exactly one input mode");
|
|
127
|
+
}
|
|
128
|
+
if (hasInput === hasRemotePair) {
|
|
129
|
+
throw parseError("Provide exactly one of --input <path> or --repo <owner/name> --pr <number>");
|
|
130
|
+
}
|
|
131
|
+
return options;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function fetchPrBody({ repo, pr }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
135
|
+
const result = await runChild(
|
|
136
|
+
ghCommand,
|
|
137
|
+
["pr", "view", String(pr), "--repo", repo, "--json", "body"],
|
|
138
|
+
env,
|
|
139
|
+
);
|
|
140
|
+
if (result.code !== 0) {
|
|
141
|
+
const detail = result.stderr.trim() || `exit code ${result.code}`;
|
|
142
|
+
throw new Error(`gh command failed: ${detail}`);
|
|
143
|
+
}
|
|
144
|
+
const payload = parseJsonText(result.stdout, { label: "gh pr view" });
|
|
145
|
+
if (!payload || typeof payload !== "object") {
|
|
146
|
+
throw new Error("Invalid gh pr view payload: missing body");
|
|
147
|
+
}
|
|
148
|
+
return typeof payload.body === "string" ? payload.body : "";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function loadInputPayload(inputPath) {
|
|
152
|
+
const text = await readFile(inputPath, "utf8");
|
|
153
|
+
const payload = parseJsonText(text, { label: `input file ${inputPath}` });
|
|
154
|
+
if (!payload || typeof payload !== "object") {
|
|
155
|
+
throw new Error(`Input file ${inputPath} must be a JSON object`);
|
|
156
|
+
}
|
|
157
|
+
return payload;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function validatePrBodySpecFromOptions(options, { env = process.env, ghCommand = "gh" } = {}) {
|
|
161
|
+
if (typeof options.input === "string" && options.input.length > 0) {
|
|
162
|
+
const payload = await loadInputPayload(options.input);
|
|
163
|
+
const body = typeof payload.body === "string" ? payload.body : "";
|
|
164
|
+
const repo = typeof payload.repo === "string" ? payload.repo : options.repo ?? null;
|
|
165
|
+
const pr = Number.isInteger(payload.pr) ? payload.pr : options.pr ?? null;
|
|
166
|
+
return { ...validatePrBodySpec({ body, expectedIssue: options.expectedIssue ?? null }), repo, pr };
|
|
167
|
+
}
|
|
168
|
+
parseRepoSlug(options.repo);
|
|
169
|
+
const body = await fetchPrBody({ repo: options.repo, pr: options.pr }, { env, ghCommand });
|
|
170
|
+
return {
|
|
171
|
+
...validatePrBodySpec({ body, expectedIssue: options.expectedIssue ?? null }),
|
|
172
|
+
repo: options.repo,
|
|
173
|
+
pr: options.pr,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function runCli(
|
|
178
|
+
argv = process.argv.slice(2),
|
|
179
|
+
{ stdout = process.stdout, stderr = process.stderr, env = process.env, ghCommand = "gh" } = {},
|
|
180
|
+
) {
|
|
181
|
+
let options;
|
|
182
|
+
try {
|
|
183
|
+
options = parseValidatePrBodySpecCliArgs(argv);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
stderr.write(`${formatCliError(error, { usage: USAGE })}\n`);
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
if (options.help) {
|
|
189
|
+
stdout.write(`${USAGE}\n`);
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const result = await validatePrBodySpecFromOptions(options, { env, ghCommand });
|
|
194
|
+
// Fail closed: a body missing any invariant maps result.ok=false to exit 1.
|
|
195
|
+
return emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
196
|
+
} catch (error) {
|
|
197
|
+
stderr.write(`${formatCliError(error)}\n`);
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
203
|
+
const code = await runCli();
|
|
204
|
+
if (code !== 0) {
|
|
205
|
+
process.exitCode = code;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -5,6 +5,7 @@ import { parseArgs } from "node:util";
|
|
|
5
5
|
import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
6
6
|
import { parseIssueNumber, requireTokenValue } from "../_cli-primitives.mjs";
|
|
7
7
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
8
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
8
9
|
import { detectInitialCopilotPrState, LINKED_PR_STATE } from "./detect-initial-copilot-pr-state.mjs";
|
|
9
10
|
import { enforcePersistentInternalWaitTimeout } from "@dev-loops/core/loop/timeout-policy";
|
|
10
11
|
import {
|
|
@@ -45,9 +46,11 @@ Error output (stderr, JSON):
|
|
|
45
46
|
{ "ok": false, "error": "...", "usage": "..." }
|
|
46
47
|
gh/runtime failures:
|
|
47
48
|
{ "ok": false, "error": "..." }
|
|
49
|
+
${JQ_OUTPUT_USAGE}
|
|
48
50
|
Exit codes:
|
|
49
51
|
0 Success (ready_for_followup, timed_out, and prior_linked_pr_closed_unmerged are all ok:true)
|
|
50
|
-
1 Argument error or gh failure
|
|
52
|
+
1 Argument error or gh failure
|
|
53
|
+
2 Invalid --jq filter`.trim();
|
|
51
54
|
const parseError = buildParseError(USAGE);
|
|
52
55
|
function rejectRemovedFlag(token) {
|
|
53
56
|
throw parseError(
|
|
@@ -108,6 +111,7 @@ export function parseWatchInitialCopilotPrCliArgs(argv) {
|
|
|
108
111
|
help: { type: "boolean", short: "h" },
|
|
109
112
|
repo: { type: "string" },
|
|
110
113
|
issue: { type: "string" },
|
|
114
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
111
115
|
},
|
|
112
116
|
allowPositionals: true,
|
|
113
117
|
strict: false,
|
|
@@ -135,6 +139,7 @@ export function parseWatchInitialCopilotPrCliArgs(argv) {
|
|
|
135
139
|
options.issue = parseIssueNumber(requireTokenValue(token, parseError), parseError);
|
|
136
140
|
continue;
|
|
137
141
|
}
|
|
142
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
138
143
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
139
144
|
}
|
|
140
145
|
if (options.repo === undefined || options.issue === undefined) {
|
|
@@ -251,7 +256,7 @@ export async function watchInitialCopilotPr(
|
|
|
251
256
|
}
|
|
252
257
|
export async function runCli(
|
|
253
258
|
argv = process.argv.slice(2),
|
|
254
|
-
{ stdout = process.stdout, env = process.env, ghCommand = "gh" } = {},
|
|
259
|
+
{ stdout = process.stdout, stderr = process.stderr, env = process.env, ghCommand = "gh" } = {},
|
|
255
260
|
) {
|
|
256
261
|
const options = parseWatchInitialCopilotPrCliArgs(argv);
|
|
257
262
|
if (options.help) {
|
|
@@ -259,7 +264,7 @@ export async function runCli(
|
|
|
259
264
|
return;
|
|
260
265
|
}
|
|
261
266
|
const result = await watchInitialCopilotPr(options, { env, ghCommand });
|
|
262
|
-
|
|
267
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
263
268
|
}
|
|
264
269
|
if (isDirectCliRun(import.meta.url)) {
|
|
265
270
|
runCli().catch((error) => {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
9
9
|
import { dirname, join, resolve, parse as parsePath } from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { buildStateAtlasHtml } from './build-state-atlas.mjs';
|
|
11
12
|
|
|
12
13
|
const REPO_ROOT_DEFAULT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
13
14
|
|
|
@@ -46,27 +47,59 @@ export const DECKS = [
|
|
|
46
47
|
// Resolve a deck's published filename: distinct outFile when set, else file.
|
|
47
48
|
const deckOut = (deck) => deck.outFile ?? deck.file;
|
|
48
49
|
|
|
50
|
+
// The State atlas: a generated page (site/state-atlas.html) rendering every
|
|
51
|
+
// dev-loops state machine as mermaid diagrams straight from the code's tables.
|
|
52
|
+
export const STATE_ATLAS = { file: 'state-atlas.html', label: 'State atlas' };
|
|
53
|
+
|
|
49
54
|
// The other resources linked from the navigation, in order.
|
|
50
55
|
export const NAV_LINKS = [
|
|
51
56
|
...ARTICLES.map((a) => ({ file: a.file, label: a.navLabel })),
|
|
52
57
|
...DECKS.map((d) => ({ file: deckOut(d), label: d.navLabel })),
|
|
58
|
+
{ file: STATE_ATLAS.file, label: STATE_ATLAS.label },
|
|
53
59
|
];
|
|
54
60
|
|
|
55
61
|
// Nav styling, appended to each article page's own <style> block so it reuses
|
|
56
62
|
// the article design-system variables (--heading/--kicker/--accent-soft).
|
|
57
63
|
const NAV_CSS = `
|
|
58
|
-
.site-nav { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem 1.1rem; max-width:
|
|
64
|
+
.site-nav { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem 1.1rem; max-width: 48rem; margin: 0 auto; padding: 0.9rem clamp(1.1rem, 5vw, 2rem); border-bottom: 1px solid rgba(148, 163, 184, 0.16); }
|
|
65
|
+
@media (min-width: 900px) { .site-nav { max-width: 56rem; } }
|
|
59
66
|
.site-nav-brand { font-weight: 700; letter-spacing: -0.01em; color: var(--heading); text-decoration: none; border: 0; margin-right: auto; }
|
|
60
67
|
.site-nav-links { display: flex; flex-wrap: wrap; gap: 0.5rem 1.1rem; }
|
|
61
68
|
.site-nav a { color: var(--kicker); text-decoration: none; font-size: 0.9rem; border: 0; }
|
|
62
|
-
.site-nav a:hover { color: var(--accent-soft); }
|
|
69
|
+
.site-nav a:hover { color: var(--accent-soft); }
|
|
70
|
+
.site-nav-gh { display: inline-flex; align-items: center; }
|
|
71
|
+
.site-nav-gh svg { width: 1.125rem; height: 1.125rem; fill: currentColor; display: block; }`;
|
|
72
|
+
|
|
73
|
+
// The repository the site links to from its nav. Derived from the effective
|
|
74
|
+
// repoRoot's package.json repository.url (single source of truth) so a repo
|
|
75
|
+
// rename/move updates the nav link too, rather than drifting from a hardcoded
|
|
76
|
+
// copy. Resolved inside buildSite from its repoRoot param (not at module load)
|
|
77
|
+
// so --repo-root / buildSite({ repoRoot }) reads the right package.json and no
|
|
78
|
+
// I/O runs merely on import. The '.git' suffix is stripped for the web URL.
|
|
79
|
+
// npm's `repository` field may be a string ("github:owner/repo" or a full URL)
|
|
80
|
+
// or an object { type, url }. Accept both; fail with an explicit, actionable
|
|
81
|
+
// message when neither yields a URL, rather than an implicit TypeError.
|
|
82
|
+
export async function resolveRepoUrl(repoRoot) {
|
|
83
|
+
const { repository } = JSON.parse(await readFile(join(repoRoot, 'package.json'), 'utf8'));
|
|
84
|
+
const raw = typeof repository === 'string' ? repository : repository?.url;
|
|
85
|
+
if (!raw) {
|
|
86
|
+
throw new Error(`cannot resolve repo URL: package.json at ${repoRoot} has no repository.url`);
|
|
87
|
+
}
|
|
88
|
+
return raw
|
|
89
|
+
.replace(/^github:/, 'https://github.com/')
|
|
90
|
+
.replace(/\.git$/, '');
|
|
91
|
+
}
|
|
92
|
+
// Inline SVG (GitHub octicon mark) so it renders under the page's strict CSP
|
|
93
|
+
// (img-src is data:-only; no external icon).
|
|
94
|
+
const GITHUB_ICON = '<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>';
|
|
63
95
|
|
|
64
|
-
function navMarkup() {
|
|
96
|
+
function navMarkup(repoUrl) {
|
|
65
97
|
const links = NAV_LINKS.map((l) => ` <a href="${l.file}">${l.label}</a>`).join('\n');
|
|
66
98
|
return `<nav class="site-nav" aria-label="dev-loops resources">
|
|
67
99
|
<a class="site-nav-brand" href="index.html">dev-loops</a>
|
|
68
100
|
<div class="site-nav-links">
|
|
69
101
|
${links}
|
|
102
|
+
<a class="site-nav-gh" href="${repoUrl}" aria-label="dev-loops on GitHub">${GITHUB_ICON}</a>
|
|
70
103
|
</div>
|
|
71
104
|
</nav>`;
|
|
72
105
|
}
|
|
@@ -75,13 +108,13 @@ ${links}
|
|
|
75
108
|
// markup right after <body>. Idempotent enough for assembly (each source file
|
|
76
109
|
// is read once). Throws if the page lacks the expected anchors so a structural
|
|
77
110
|
// drift fails the build rather than publishing an un-navigable page.
|
|
78
|
-
export function injectNav(html) {
|
|
111
|
+
export function injectNav(html, repoUrl) {
|
|
79
112
|
if (!html.includes('</style>') || !/<body[^>]*>/.test(html)) {
|
|
80
113
|
throw new Error('cannot inject nav: page is missing a <style> block or <body> tag');
|
|
81
114
|
}
|
|
82
115
|
return html
|
|
83
116
|
.replace('</style>', `${NAV_CSS}\n</style>`)
|
|
84
|
-
.replace(/<body([^>]*)>/, `<body$1>\n ${navMarkup()}`);
|
|
117
|
+
.replace(/<body([^>]*)>/, `<body$1>\n ${navMarkup(repoUrl)}`);
|
|
85
118
|
}
|
|
86
119
|
|
|
87
120
|
export async function buildSite({ repoRoot = REPO_ROOT_DEFAULT, outDir } = {}) {
|
|
@@ -100,14 +133,16 @@ export async function buildSite({ repoRoot = REPO_ROOT_DEFAULT, outDir } = {}) {
|
|
|
100
133
|
await rm(out, { recursive: true, force: true });
|
|
101
134
|
await mkdir(out, { recursive: true });
|
|
102
135
|
|
|
136
|
+
const repoUrl = await resolveRepoUrl(repoRoot);
|
|
137
|
+
|
|
103
138
|
// Landing page: the intro article, navigable, published as index.html.
|
|
104
139
|
const landingHtml = await readFile(join(articlesDir, LANDING.file), 'utf8');
|
|
105
|
-
await writeFile(join(out, 'index.html'), injectNav(landingHtml), 'utf8');
|
|
140
|
+
await writeFile(join(out, 'index.html'), injectNav(landingHtml, repoUrl), 'utf8');
|
|
106
141
|
|
|
107
142
|
// Deep-dive article: published with the same nav so the set is navigable.
|
|
108
143
|
for (const article of ARTICLES) {
|
|
109
144
|
const html = await readFile(join(articlesDir, article.file), 'utf8');
|
|
110
|
-
await writeFile(join(out, article.file), injectNav(html), 'utf8');
|
|
145
|
+
await writeFile(join(out, article.file), injectNav(html, repoUrl), 'utf8');
|
|
111
146
|
}
|
|
112
147
|
|
|
113
148
|
// Decks: self-contained slide renders, copied as-is (no nav injection).
|
|
@@ -115,9 +150,25 @@ export async function buildSite({ repoRoot = REPO_ROOT_DEFAULT, outDir } = {}) {
|
|
|
115
150
|
await cp(join(decksDir, deck.file), join(out, deckOut(deck)));
|
|
116
151
|
}
|
|
117
152
|
|
|
153
|
+
// State atlas: generated from the core state tables at build time, then given
|
|
154
|
+
// the same shared nav as the article pages so it can never drift from the code.
|
|
155
|
+
await writeFile(join(out, STATE_ATLAS.file), injectNav(buildStateAtlasHtml(), repoUrl), 'utf8');
|
|
156
|
+
|
|
157
|
+
// Vendored mermaid runtime the atlas page references (Pages has no CSP, so we
|
|
158
|
+
// load it as an external asset rather than inlining ~3MB into the page).
|
|
159
|
+
const mermaidSrc = join(repoRoot, 'scripts', 'loop', 'inspect-run-viewer', 'vendor', 'mermaid.min.js');
|
|
160
|
+
await mkdir(join(out, 'assets'), { recursive: true });
|
|
161
|
+
await cp(mermaidSrc, join(out, 'assets', 'mermaid.min.js'));
|
|
162
|
+
|
|
118
163
|
return {
|
|
119
164
|
out,
|
|
120
|
-
files: [
|
|
165
|
+
files: [
|
|
166
|
+
'index.html',
|
|
167
|
+
...ARTICLES.map((a) => a.file),
|
|
168
|
+
...DECKS.map((d) => deckOut(d)),
|
|
169
|
+
STATE_ATLAS.file,
|
|
170
|
+
'assets/mermaid.min.js',
|
|
171
|
+
],
|
|
121
172
|
};
|
|
122
173
|
}
|
|
123
174
|
|