@gobing-ai/spur 0.3.93 → 0.3.95
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/marketplace.json +1 -1
- package/README.md +42 -0
- package/board/index.d.ts +38 -0
- package/config/plugin-scripts.json +5 -0
- package/config/rules/boundary/test-subpath-boundary.yaml +48 -0
- package/config/rules/strict/runtime-boundaries.yaml +2 -1
- package/config/rules/structure/protected-files.yaml +4 -0
- package/config/workflows/feature-verification.yaml +7 -4
- package/config/workflows/idea-pipeline.yaml +10 -4
- package/config/workflows/task-pipeline.yaml +71 -52
- package/config/workflows/wrapup-pipeline.yaml +92 -17
- package/package.json +15 -11
- package/plugins/sp/README.md +1 -1
- package/plugins/sp/commands/dev-review-session.md +4 -4
- package/plugins/sp/commands/dev-review.md +16 -5
- package/plugins/sp/commands/dev-verify.md +7 -4
- package/plugins/sp/commands/dev-verifyall.md +4 -3
- package/plugins/sp/commands/dev-wrap.md +1 -1
- package/plugins/sp/hooks/context-post-tool.ts +2 -2
- package/plugins/sp/hooks/context-session-start.ts +2 -1
- package/plugins/sp/hooks/context-session-stop.ts +22 -14
- package/plugins/sp/hooks/pi/guard-extension.ts +112 -172
- package/plugins/sp/lib/idea-handoff.generated.mjs +2 -2
- package/plugins/sp/lib/inline-run.generated.d.mts +4 -2
- package/plugins/sp/lib/inline-run.generated.mjs +35 -14
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +3 -3
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +3 -3
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +1 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +9 -7
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +8 -7
- package/plugins/sp/scripts/feature-sync-bounded.mjs +5 -2
- package/plugins/sp/scripts/feature-sync-bounded.ts +3 -1
- package/plugins/sp/scripts/feature-verification-steps.ts +6 -6
- package/plugins/sp/scripts/history-anatomy-cache.mjs +3 -3
- package/plugins/sp/scripts/history-anatomy-cache.ts +4 -4
- package/plugins/sp/scripts/inline-run-setup.mjs +60 -12
- package/plugins/sp/scripts/inline-run-setup.ts +122 -15
- package/plugins/sp/scripts/pr-reviewing.mjs +2 -0
- package/plugins/sp/scripts/pr-reviewing.ts +1 -0
- package/plugins/sp/scripts/quality-gate.mjs +15 -6
- package/plugins/sp/scripts/quality-gate.ts +30 -9
- package/plugins/sp/scripts/residual-scan.mjs +4 -4
- package/plugins/sp/scripts/residual-scan.ts +5 -4
- package/plugins/sp/scripts/script-contract-check.ts +175 -46
- package/plugins/sp/scripts/script-root.mjs +125 -0
- package/plugins/sp/scripts/script-root.ts +195 -0
- package/plugins/sp/scripts/stage-registry-adapter.ts +1 -1
- package/plugins/sp/scripts/task-evidence-precheck.ts +4 -2
- package/plugins/sp/scripts/task-size-precheck.ts +3 -1
- package/plugins/sp/scripts/validate-commands.ts +12 -12
- package/plugins/sp/scripts/validate-flag-contracts.ts +42 -30
- package/plugins/sp/scripts/verify-answer-lint.ts +3 -1
- package/plugins/sp/scripts/workflow-step-profile.mjs +5 -2
- package/plugins/sp/scripts/workflow-step-profile.ts +3 -1
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +5 -2
- package/plugins/sp/scripts/wrapup-drift-probe.ts +4 -2
- package/plugins/sp/scripts/wrapup-steps.mjs +32 -8
- package/plugins/sp/scripts/wrapup-steps.ts +56 -10
- package/plugins/sp/skills/code-verification/SKILL.md +12 -7
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +11 -0
- package/plugins/sp/skills/functional-review/SKILL.md +1 -1
- package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
- package/plugins/sp/skills/session-review/SKILL.md +11 -9
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +9 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +8 -2
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +65 -17
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +16 -3
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +22 -5
- package/plugins/sp/tsconfig.json +8 -0
- package/spur.js +20934 -21087
- package/web/_astro/{BoardApp.CerSBgis.js → BoardApp.BxJuwD7I.js} +151 -152
- package/web/_astro/BoardApp.Cpxntzad.js +1 -0
- package/web/_astro/TaskDetail.C5bW4WGV.js +1 -0
- package/web/_astro/_commonjsHelpers.CqkleIqs.js +1 -0
- package/web/_astro/arc.CyjRvNMY.js +1 -0
- package/web/_astro/architectureDiagram-3BPJPVTR.B4lWRzJA.js +36 -0
- package/web/_astro/{blockDiagram-GPEHLZMM.DXVIiv0p.js → blockDiagram-GPEHLZMM.Be38USQ2.js} +1 -1
- package/web/_astro/board-facade-react-dom-client.B2U5wMvk.js +1 -0
- package/web/_astro/board-facade-react-dom.Cd2MuMhF.js +1 -0
- package/web/_astro/board-facade-react-jsx-dev-runtime.B_L3JzfC.js +1 -0
- package/web/_astro/board-facade-react-jsx-runtime.BY2yfHRi.js +1 -0
- package/web/_astro/board-facade-react-router-dom.Dy5pjzOw.js +1 -0
- package/web/_astro/board-facade-react-router.DjjqU27_.js +1 -0
- package/web/_astro/board-facade-react.Dgf4GRyj.js +1 -0
- package/web/_astro/{c4Diagram-AAUBKEIU.BbF_zCxW.js → c4Diagram-AAUBKEIU.DT9Fj5Qx.js} +1 -1
- package/web/_astro/channel.CI6N_tCg.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.CAgQHpPC.js → chunk-2J33WTMH.SeSBWLg5.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.BN-5tpw4.js → chunk-4BX2VUAB.DoO4VE14.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.CnPkEEr0.js → chunk-55IACEB6.DrmTFA53.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.BQzQeMVm.js → chunk-727SXJPM.Bi-TUb_V.js} +4 -4
- package/web/_astro/{chunk-AQP2D5EJ.B6xNyDnL.js → chunk-AQP2D5EJ.B-xp8Uhi.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.C7f9Ih78.js → chunk-FMBD7UC4.DGWrDnUU.js} +1 -1
- package/web/_astro/chunk-JMJ3UQ3L.BQJEJHu4.js +34 -0
- package/web/_astro/{chunk-ND2GUHAM.CNV1dFXT.js → chunk-ND2GUHAM._BagvPDy.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.Cudn2TkJ.js → chunk-QZHKN3VN.VOboYWQ0.js} +1 -1
- package/web/_astro/chunk-YNUBSHFH.CxeAygfi.js +7 -0
- package/web/_astro/{classDiagram-4FO5ZUOK.D1NwP50q.js → classDiagram-4FO5ZUOK.D_apfHS7.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.D1NwP50q.js → classDiagram-v2-Q7XG4LA2.D_apfHS7.js} +1 -1
- package/web/_astro/client.BhluLQqe.js +1 -0
- package/web/_astro/client.DBpSRLnx.js +9 -0
- package/web/_astro/cose-bilkent-S5V4N54A.DNLU_L-x.js +1 -0
- package/web/_astro/{cynefin-OW5HDTMX.BmK52w8G.js → cynefin-OW5HDTMX.D8borhV-.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.Bfy5CTDT.js → dagre-BM42HDAG.Uc0lvjcV.js} +2 -2
- package/web/_astro/diagram-2AECGRRQ.DDIxtDBo.js +43 -0
- package/web/_astro/diagram-5GNKFQAL.DVoDnD3H.js +10 -0
- package/web/_astro/{diagram-KO2AKTUF.CW_vMJ4z.js → diagram-KO2AKTUF.CRubTJ-0.js} +3 -3
- package/web/_astro/{diagram-LMA3HP47.B_8ZGF67.js → diagram-LMA3HP47.m9SeYcXQ.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.BppnHsdS.js → diagram-OG6HWLK6.C6s2ZjV5.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BEuHXcjJ.js → erDiagram-TEJ5UH35.nSzr5KTj.js} +5 -5
- package/web/_astro/{flowDiagram-I6XJVG4X.CH-UlnGr.js → flowDiagram-I6XJVG4X.DrZcys16.js} +4 -4
- package/web/_astro/ganttDiagram-6RSMTGT7.Do4M1b-K.js +292 -0
- package/web/_astro/{gitGraphDiagram-PVQCEYII.XnPxPPZN.js → gitGraphDiagram-PVQCEYII.BWc9oJ4l.js} +1 -1
- package/web/_astro/index.5dn-Zm-2.js +1 -0
- package/web/_astro/index.C-IqPpW2.js +1 -0
- package/web/_astro/index.CENnIEqT.css +1 -0
- package/web/_astro/index.Cyc_GA4f.js +1 -0
- package/web/_astro/index.tZfqwSut.js +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.JyjYRu_T.js → infoDiagram-5YYISTIA.BIdOctFk.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js → ishikawaDiagram-YF4QCWOH.CV5SuG2t.js} +5 -5
- package/web/_astro/{journeyDiagram-JHISSGLW.C_iymSyp.js → journeyDiagram-JHISSGLW.CxE-rEJ-.js} +1 -1
- package/web/_astro/jsx-runtime.DWOiEOII.js +1 -0
- package/web/_astro/{kanban-definition-UN3LZRKU.DdfW-Oqt.js → kanban-definition-UN3LZRKU.BPRdKWs_.js} +7 -7
- package/web/_astro/{linear.C2_IkbZT.js → linear.zRsuuDTE.js} +1 -1
- package/web/_astro/mermaid.core.jAJTcMKc.js +301 -0
- package/web/_astro/{mindmap-definition-RKZ34NQL.DAZIxQSK.js → mindmap-definition-RKZ34NQL.BffJxfCr.js} +2 -2
- package/web/_astro/{pieDiagram-4H26LBE5.CN8sIhKM.js → pieDiagram-4H26LBE5.CacwWaE3.js} +3 -3
- package/web/_astro/{quadrantDiagram-W4KKPZXB.3dGcX5GP.js → quadrantDiagram-W4KKPZXB.hXELD06-.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.BV2y4dd6.js → requirementDiagram-4Y6WPE33.DdEbPcqT.js} +3 -3
- package/web/_astro/{sankeyDiagram-5OEKKPKP.Cqo15Tvo.js → sankeyDiagram-5OEKKPKP.DgaDocKv.js} +4 -4
- package/web/_astro/{sequenceDiagram-3UESZ5HK.CROCPMJB.js → sequenceDiagram-3UESZ5HK.B0KScnAu.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.RfXZrkFE.js → stateDiagram-AJRCARHV.CV9M_WNc.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.CPXmbBs9.js → stateDiagram-v2-BHNVJYJU.BOAo74Et.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.DdgKTiO8.js → timeline-definition-PNZ67QCA.CqqepUMe.js} +3 -3
- package/web/_astro/{vennDiagram-CIIHVFJN.CPNVSHF1.js → vennDiagram-CIIHVFJN.DDqe4dOG.js} +5 -5
- package/web/_astro/{wardleyDiagram-YWT4CUSO.CQhA0Jyr.js → wardleyDiagram-YWT4CUSO.B3R9OPdO.js} +3 -3
- package/web/_astro/{xychartDiagram-2RQKCTM6.n61BWyy4.js → xychartDiagram-2RQKCTM6.B3SvgXpQ.js} +1 -1
- package/web/board-runtime.json +69 -0
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.eoTz0pZs.js +0 -1
- package/web/_astro/TaskDetail.DCqiC-OZ.js +0 -1
- package/web/_astro/arc.CPwg6Rw0.js +0 -1
- package/web/_astro/architectureDiagram-3BPJPVTR.DM_vp_hO.js +0 -36
- package/web/_astro/channel.MYZLKNwy.js +0 -1
- package/web/_astro/client.yhYJvxCU.js +0 -9
- package/web/_astro/cose-bilkent-S5V4N54A.B1wSL-Xb.js +0 -1
- package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +0 -43
- package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +0 -10
- package/web/_astro/ganttDiagram-6RSMTGT7.BO81S85v.js +0 -292
- package/web/_astro/index.De90oHcH.js +0 -1
- package/web/_astro/index.Hjbr15fG.css +0 -1
- package/web/_astro/mermaid.core.GAOYeSR0.js +0 -303
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* bun plugins/sp/scripts/inline-run-setup.ts --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]
|
|
35
35
|
* --status <done|failed> --ok <true|false> --duration-ms <n> [--spur-bin <path>]
|
|
36
36
|
* bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--spur-bin <path>]
|
|
37
|
+
* bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--task-file <path>]... [--spur-bin <path>]
|
|
37
38
|
*
|
|
38
39
|
* The `--fingerprint` mode prints the engine's proof-input digest for the given spec files and
|
|
39
40
|
* creates nothing (task 0862 R5).
|
|
@@ -51,6 +52,18 @@
|
|
|
51
52
|
* decision is still `ok: true` — the run continues — so exit 0 covers every model-level
|
|
52
53
|
* outcome; only an invalid options schema exits 1 (fail closed).
|
|
53
54
|
*
|
|
55
|
+
* The `--persist-out --from <worktree-path>` mode (task 0975 R1) is the WT-4a step a
|
|
56
|
+
* `--worktree` run executes BEFORE teardown: it copies the worktree's run provenance — the
|
|
57
|
+
* `runs`/`action_runs`/`phase_runs`/`transition_runs`/`workflow_states` rows and the two-file
|
|
58
|
+
* run records — into THIS tree's project DB and `.spur/run/` (cwd = the invoking tree).
|
|
59
|
+
* Repeatable `--task-file <path>` (0984 R2) forwards each merged task file so the app service
|
|
60
|
+
* also copies/verifies the `.spur/run/<file>` evidence that file cites; citation selection,
|
|
61
|
+
* safe-path validation and conflict behavior live entirely in the app service. Idempotent;
|
|
62
|
+
* collisions are reported as `skipped[{id,reason}]`, never overwrites. Exit 0 = persisted
|
|
63
|
+
* (possibly with skips); exit 1 = fail closed — an unreadable/unsafe/over-cap/unresolved
|
|
64
|
+
* citation or a divergent cited file fails the pass too — the driver routes to WT-5 and
|
|
65
|
+
* retains the worktree; usage errors exit 2.
|
|
66
|
+
*
|
|
54
67
|
* Exit 2 is reserved for usage errors.
|
|
55
68
|
*
|
|
56
69
|
* Env: SPUR_BIN
|
|
@@ -102,6 +115,13 @@ function usage(): never {
|
|
|
102
115
|
console.error(
|
|
103
116
|
' bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--spur-bin <path>]',
|
|
104
117
|
);
|
|
118
|
+
console.error(
|
|
119
|
+
' bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--task-file <path>]... [--spur-bin <path>]',
|
|
120
|
+
);
|
|
121
|
+
console.error(
|
|
122
|
+
' terminal-reason is a closed enum (0937 R2): done, paused-operator, failed-check, failed-agent, ' +
|
|
123
|
+
'failed-timeout, failed-guard, cancelled, interrupted, retry-exhausted',
|
|
124
|
+
);
|
|
105
125
|
console.error(
|
|
106
126
|
' bun plugins/sp/scripts/inline-run-setup.ts --decide --run-id <id> --node <state> --options-json <file> [--spur-bin <path>]',
|
|
107
127
|
);
|
|
@@ -313,7 +333,12 @@ async function printFingerprint(taskFile: string, featureFile: string, spurBin:
|
|
|
313
333
|
}
|
|
314
334
|
|
|
315
335
|
/** Terminal statuses the inline driver may declare when closing its run row. */
|
|
316
|
-
const CLOSE_STATUSES = new Set(['done', 'failed', 'paused']);
|
|
336
|
+
const CLOSE_STATUSES: ReadonlySet<string> = new Set<'done' | 'failed' | 'paused'>(['done', 'failed', 'paused']);
|
|
337
|
+
|
|
338
|
+
/** Narrow an argv `--status` to the close vocabulary (`CLOSE_STATUSES`). */
|
|
339
|
+
function isCloseStatus(status: string): status is 'done' | 'failed' | 'paused' {
|
|
340
|
+
return CLOSE_STATUSES.has(status);
|
|
341
|
+
}
|
|
317
342
|
|
|
318
343
|
/**
|
|
319
344
|
* 0937 R2: closed terminal-reason vocabulary the driver may declare on `--close`.
|
|
@@ -334,7 +359,12 @@ const TERMINAL_REASONS = new Set([
|
|
|
334
359
|
]);
|
|
335
360
|
|
|
336
361
|
/** Finalize statuses — a finish emission is terminal, so only done|failed are valid (0868 #4). */
|
|
337
|
-
const ACTION_STATUSES = new Set(['done', 'failed']);
|
|
362
|
+
const ACTION_STATUSES: ReadonlySet<string> = new Set<'done' | 'failed'>(['done', 'failed']);
|
|
363
|
+
|
|
364
|
+
/** Narrow an argv `--status` to the finalize vocabulary (`ACTION_STATUSES`). */
|
|
365
|
+
function isActionStatus(status: string): status is 'done' | 'failed' {
|
|
366
|
+
return ACTION_STATUSES.has(status);
|
|
367
|
+
}
|
|
338
368
|
|
|
339
369
|
/** Input for the ADR-117 emission modes (`--action` / `--close`). */
|
|
340
370
|
import type { WorkflowActionTraceWriter } from '@gobing-ai/spur-app';
|
|
@@ -346,7 +376,12 @@ interface TraceModeInput {
|
|
|
346
376
|
readonly kind: string;
|
|
347
377
|
/** Declared terminal reason (0937 R2) — validated against TERMINAL_REASONS before this point. */
|
|
348
378
|
readonly reason?: string;
|
|
349
|
-
|
|
379
|
+
/**
|
|
380
|
+
* Trace status: the finalize vocabulary plus the close-only `paused` (`CLOSE_STATUSES`).
|
|
381
|
+
* Both are assignable to the engine's `WorkflowStatus`; the plugin cannot import that
|
|
382
|
+
* type (standalone contract), so the literal union stands in for it.
|
|
383
|
+
*/
|
|
384
|
+
readonly status: 'done' | 'failed' | 'paused';
|
|
350
385
|
readonly ok: boolean;
|
|
351
386
|
readonly durationMs: number;
|
|
352
387
|
readonly spurBin: string;
|
|
@@ -371,7 +406,7 @@ function runRecordLogPath(runDir: string, runId: string): string {
|
|
|
371
406
|
* exit immediately after), and never throws: an unwritable log must not wedge the run
|
|
372
407
|
* (ADR-117 R3).
|
|
373
408
|
*/
|
|
374
|
-
function
|
|
409
|
+
function appendRunLogLine(runId: string, detail: string): void {
|
|
375
410
|
try {
|
|
376
411
|
const runDir = join(process.cwd(), '.spur', 'run');
|
|
377
412
|
if (!existsSync(runDir)) mkdirSync(runDir, { recursive: true });
|
|
@@ -393,7 +428,7 @@ function appendTraceFailureLine(runId: string, detail: string): void {
|
|
|
393
428
|
async function runTraceMode(input: TraceModeInput): Promise<number> {
|
|
394
429
|
const operation = input.close ? 'run.close' : 'action.finish';
|
|
395
430
|
const fail = (error: string): number => {
|
|
396
|
-
|
|
431
|
+
appendRunLogLine(
|
|
397
432
|
input.runId,
|
|
398
433
|
`trace-emission-failed operation=${operation} run=${input.runId}` +
|
|
399
434
|
`${input.node === '' ? '' : ` node=${input.node}`}${input.kind === '' ? '' : ` kind=${input.kind}`}: ${error}`,
|
|
@@ -420,7 +455,7 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
|
|
|
420
455
|
projectDb = await app.openInlineRunProjectDb(process.cwd());
|
|
421
456
|
const writer = app.createWorkflowActionTraceWriter(projectDb.adapter, (failure: unknown) => {
|
|
422
457
|
const detail = failure as { operation?: string; error?: string };
|
|
423
|
-
|
|
458
|
+
appendRunLogLine(
|
|
424
459
|
input.runId,
|
|
425
460
|
`trace-emission-failed operation=${detail.operation ?? operation} run=${input.runId}: ${detail.error ?? 'unknown error'}`,
|
|
426
461
|
);
|
|
@@ -443,6 +478,18 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
|
|
|
443
478
|
const failure = result.failure as { error?: string };
|
|
444
479
|
return fail(failure.error ?? 'unknown trace emission failure');
|
|
445
480
|
}
|
|
481
|
+
if (input.close && input.status === 'done' && result.actionRows === 0) {
|
|
482
|
+
// A run finalized `done` with ZERO recorded action rows is a bookkeeping defect
|
|
483
|
+
// (task 0975 R2): the row is already terminal — closeRun ran above — but the
|
|
484
|
+
// driver must surface this instead of reporting a clean close, and must never
|
|
485
|
+
// backfill rows. Exit 1 with the named code; the run record carries the finding.
|
|
486
|
+
const error = `run ${input.runId} closed done with zero action_runs rows`;
|
|
487
|
+
appendRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${error}`);
|
|
488
|
+
process.stdout.write(
|
|
489
|
+
`${JSON.stringify({ ok: false, runId: input.runId, error, code: 'NO_ACTION_ROWS', actionRows: 0 })}\n`,
|
|
490
|
+
);
|
|
491
|
+
return 1;
|
|
492
|
+
}
|
|
446
493
|
process.stdout.write(`${JSON.stringify({ ...result, runId: input.runId })}\n`);
|
|
447
494
|
return 0;
|
|
448
495
|
} catch (error) {
|
|
@@ -450,7 +497,7 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
|
|
|
450
497
|
// The run row must exist before --close can mark it terminal (R6); a missing row
|
|
451
498
|
// is a loud correctness failure, not a best-effort emission failure (finding #4).
|
|
452
499
|
const message = error instanceof Error ? error.message : String(error);
|
|
453
|
-
|
|
500
|
+
appendRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${message}`);
|
|
454
501
|
process.stdout.write(
|
|
455
502
|
`${JSON.stringify({ ok: false, runId: input.runId, error: message, code: 'RUN_NOT_FOUND' })}\n`,
|
|
456
503
|
);
|
|
@@ -485,6 +532,7 @@ async function runDecideMode(input: {
|
|
|
485
532
|
value?: string;
|
|
486
533
|
degraded?: boolean;
|
|
487
534
|
reason?: string;
|
|
535
|
+
source?: 'model' | 'default';
|
|
488
536
|
backend?: string | null;
|
|
489
537
|
confidence?: number | null;
|
|
490
538
|
resultFile?: string;
|
|
@@ -524,7 +572,13 @@ async function runDecideMode(input: {
|
|
|
524
572
|
return decideFailed(error instanceof Error ? error.message : String(error));
|
|
525
573
|
}
|
|
526
574
|
if (!outcome.ok) return decideFailed(outcome.error ?? 'decide failed without an error message');
|
|
527
|
-
process.stdout.write(`${JSON.stringify({
|
|
575
|
+
process.stdout.write(`${JSON.stringify({ runId: input.runId, node: input.node, ...outcome, ok: true })}\n`);
|
|
576
|
+
// 0976 R2: the run log names the decision's provenance, so a declared-default fallback is
|
|
577
|
+
// never read as a model decision. Best-effort through the same run-log appender.
|
|
578
|
+
appendRunLogLine(
|
|
579
|
+
input.runId,
|
|
580
|
+
`decide node=${input.node} value=${outcome.value ?? ''} source=${outcome.source ?? 'default'} reason=${outcome.reason ?? ''}`,
|
|
581
|
+
);
|
|
528
582
|
// Trace row is best-effort, exactly like --action: an emission failure never wedges the run.
|
|
529
583
|
return await runTraceMode({
|
|
530
584
|
runId: input.runId,
|
|
@@ -538,6 +592,44 @@ async function runDecideMode(input: {
|
|
|
538
592
|
});
|
|
539
593
|
}
|
|
540
594
|
|
|
595
|
+
/**
|
|
596
|
+
* `--persist-out --from <worktree-path>` (task 0975 R1; `--task-file` per 0984 R2): resolve
|
|
597
|
+
* the app through the shared chain and copy the worktree's inline-run provenance into THIS
|
|
598
|
+
* tree. No persistence policy lives here — the app's `persistWorktreeRuns` (→ domain
|
|
599
|
+
* `transferRunTables`) owns the DB transfer, the citation selection/copy rules, the
|
|
600
|
+
* idempotence and the conflict skips; this delegate only forwards the merged task file
|
|
601
|
+
* paths. Success prints `{ok:true,persisted:<n>,skipped:[…]}` and exits 0; any failure
|
|
602
|
+
* prints `{ok:false,error}` and exits 1 (the driver routes to WT-5 and retains the
|
|
603
|
+
* worktree).
|
|
604
|
+
*/
|
|
605
|
+
async function runPersistOutMode(input: {
|
|
606
|
+
from: string;
|
|
607
|
+
taskFiles: readonly string[];
|
|
608
|
+
spurBin: string;
|
|
609
|
+
}): Promise<number> {
|
|
610
|
+
try {
|
|
611
|
+
const { entry } = resolveAppEntry(input.spurBin);
|
|
612
|
+
const app = (await import(entry)) as {
|
|
613
|
+
persistWorktreeRuns: (i: {
|
|
614
|
+
fromWorkdir: string;
|
|
615
|
+
toWorkdir: string;
|
|
616
|
+
taskFiles?: readonly string[];
|
|
617
|
+
}) => Promise<{ ok: true; persisted: number; skipped: ReadonlyArray<{ id: string; reason: string }> }>;
|
|
618
|
+
};
|
|
619
|
+
const result = await app.persistWorktreeRuns({
|
|
620
|
+
fromWorkdir: input.from,
|
|
621
|
+
toWorkdir: process.cwd(),
|
|
622
|
+
...(input.taskFiles.length > 0 ? { taskFiles: input.taskFiles } : {}),
|
|
623
|
+
});
|
|
624
|
+
process.stdout.write(`${JSON.stringify({ ok: true, persisted: result.persisted, skipped: result.skipped })}\n`);
|
|
625
|
+
return 0;
|
|
626
|
+
} catch (error) {
|
|
627
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
628
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: message })}\n`);
|
|
629
|
+
return 1;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
541
633
|
async function main(): Promise<void> {
|
|
542
634
|
// The portable Node twin has no workspace imports; SQLite still uses Spur's existing Bun runtime.
|
|
543
635
|
if (!process.versions.bun) {
|
|
@@ -550,11 +642,13 @@ async function main(): Promise<void> {
|
|
|
550
642
|
let runId = '';
|
|
551
643
|
let file = '';
|
|
552
644
|
let fingerprint = false;
|
|
553
|
-
|
|
645
|
+
const taskFiles: string[] = [];
|
|
554
646
|
let featureFile = '';
|
|
555
647
|
let action = false;
|
|
556
648
|
let close = false;
|
|
557
649
|
let decide = false;
|
|
650
|
+
let persistOut = false;
|
|
651
|
+
let from = '';
|
|
558
652
|
let optionsJson = '';
|
|
559
653
|
let node = '';
|
|
560
654
|
let kind = '';
|
|
@@ -568,11 +662,13 @@ async function main(): Promise<void> {
|
|
|
568
662
|
if (argv[i] === '--run-id') runId = argv[++i] ?? '';
|
|
569
663
|
else if (argv[i] === '--file') file = argv[++i] ?? '';
|
|
570
664
|
else if (argv[i] === '--fingerprint') fingerprint = true;
|
|
571
|
-
else if (argv[i] === '--task-file')
|
|
665
|
+
else if (argv[i] === '--task-file') taskFiles.push(argv[++i] ?? '');
|
|
572
666
|
else if (argv[i] === '--feature-file') featureFile = argv[++i] ?? '';
|
|
573
667
|
else if (argv[i] === '--action') action = true;
|
|
574
668
|
else if (argv[i] === '--close') close = true;
|
|
575
669
|
else if (argv[i] === '--decide') decide = true;
|
|
670
|
+
else if (argv[i] === '--persist-out') persistOut = true;
|
|
671
|
+
else if (argv[i] === '--from') from = argv[++i] ?? '';
|
|
576
672
|
else if (argv[i] === '--options-json') optionsJson = argv[++i] ?? '';
|
|
577
673
|
else if (argv[i] === '--node') node = argv[++i] ?? '';
|
|
578
674
|
else if (argv[i] === '--kind') kind = argv[++i] ?? '';
|
|
@@ -585,9 +681,10 @@ async function main(): Promise<void> {
|
|
|
585
681
|
|
|
586
682
|
// Two mutually exclusive modes share this entry point: create/attach a run (run-id + file), or
|
|
587
683
|
// print the proof digest for the inline driver (task 0862 R5). Mixing them is a usage error.
|
|
684
|
+
// `--task-file` belongs to the fingerprint (exactly one) and persist-out (zero or more) modes.
|
|
588
685
|
if (fingerprint) {
|
|
589
|
-
if (runId !== '' || file !== '' ||
|
|
590
|
-
process.exit(await printFingerprint(
|
|
686
|
+
if (runId !== '' || file !== '' || taskFiles.length !== 1 || (taskFiles[0] ?? '').trim() === '') usage();
|
|
687
|
+
process.exit(await printFingerprint(taskFiles[0] ?? '', featureFile, spurBin));
|
|
591
688
|
}
|
|
592
689
|
|
|
593
690
|
// ADR-117 emission modes (task 0868): the inline driver reports one completed action
|
|
@@ -597,18 +694,28 @@ async function main(): Promise<void> {
|
|
|
597
694
|
// fingerprint, and trace modes; it requires the run id (filename-guarded), the state id
|
|
598
695
|
// for the trace row, and the options JSON file.
|
|
599
696
|
if (decide) {
|
|
600
|
-
if (action || close || fingerprint || file !== '' ||
|
|
697
|
+
if (action || close || fingerprint || file !== '' || taskFiles.length > 0) usage();
|
|
601
698
|
if (runId.trim() === '' || node.trim() === '' || optionsJson.trim() === '') usage();
|
|
602
699
|
if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
|
|
603
700
|
process.exit(await runDecideMode({ runId, node, optionsFile: optionsJson, spurBin }));
|
|
604
701
|
}
|
|
605
702
|
|
|
703
|
+
// Worktree provenance persist-out (task 0975 R1): mutually exclusive with every other
|
|
704
|
+
// mode; requires the source worktree path and accepts zero or more merged task files
|
|
705
|
+
// (0984 R2) whose cited `.spur/run/` evidence the app service copies/verifies. Runs
|
|
706
|
+
// with cwd = the invoking tree.
|
|
707
|
+
if (persistOut) {
|
|
708
|
+
if (fingerprint || decide || action || close || runId !== '' || file !== '') usage();
|
|
709
|
+
if (from.trim() === '' || taskFiles.some((taskFile) => taskFile.trim() === '')) usage();
|
|
710
|
+
process.exit(await runPersistOutMode({ from, taskFiles, spurBin }));
|
|
711
|
+
}
|
|
712
|
+
|
|
606
713
|
if (action || close) {
|
|
607
714
|
if (action && close) usage();
|
|
608
715
|
if (runId.trim() === '' || status.trim() === '') usage();
|
|
609
716
|
if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
|
|
610
717
|
if (close) {
|
|
611
|
-
if (!
|
|
718
|
+
if (!isCloseStatus(status)) usage();
|
|
612
719
|
// 0937 R2: a failed close requires a declared reason, and any declared reason
|
|
613
720
|
// must be a closed-enum value — both fail loudly BEFORE any write happens.
|
|
614
721
|
if (reason.trim() === '') {
|
|
@@ -631,7 +738,7 @@ async function main(): Promise<void> {
|
|
|
631
738
|
);
|
|
632
739
|
}
|
|
633
740
|
if (node.trim() === '' || kind.trim() === '') usage();
|
|
634
|
-
if (!
|
|
741
|
+
if (!isActionStatus(status)) usage();
|
|
635
742
|
// `--ok` and `--duration-ms` are required and exact for the action mode
|
|
636
743
|
// (review finding #2): a miscased `--ok True` or an omitted `--duration-ms`
|
|
637
744
|
// must be a loud usage error, never a silently-defaulted `ok=0` /
|
|
@@ -169,6 +169,7 @@ export function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
|
169
169
|
const booleans = new Set<string>();
|
|
170
170
|
for (let i = 0; i < rest.length; i++) {
|
|
171
171
|
const tok = rest[i];
|
|
172
|
+
if (tok === undefined) continue;
|
|
172
173
|
if (BOOL_FLAGS.has(tok)) {
|
|
173
174
|
booleans.add(tok);
|
|
174
175
|
} else if (VALUE_FLAGS.has(tok)) {
|
|
@@ -154,6 +154,9 @@ function lightScope(changedFiles, exists = existsSync) {
|
|
|
154
154
|
}
|
|
155
155
|
return { files, workspaces: [...workspaces].sort(), tests: [...tests].sort() };
|
|
156
156
|
}
|
|
157
|
+
function shQuote(arg) {
|
|
158
|
+
return /^[\w./@+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
159
|
+
}
|
|
157
160
|
function workspaceHasTypecheck(workspace) {
|
|
158
161
|
try {
|
|
159
162
|
const pkg = JSON.parse(readFileSync(join(workspace, "package.json"), "utf8"));
|
|
@@ -165,11 +168,11 @@ function workspaceHasTypecheck(workspace) {
|
|
|
165
168
|
function planLightChecks(scope, hasTypecheck = workspaceHasTypecheck) {
|
|
166
169
|
const plans = [];
|
|
167
170
|
if (scope.files.length > 0) {
|
|
168
|
-
plans.push({ id: "format-lint:changed", cmd: `bunx biome check ${scope.files.join(" ")}` });
|
|
171
|
+
plans.push({ id: "format-lint:changed", cmd: `bunx biome check ${scope.files.map(shQuote).join(" ")}` });
|
|
169
172
|
}
|
|
170
173
|
for (const workspace of scope.workspaces) {
|
|
171
174
|
if (hasTypecheck(workspace)) {
|
|
172
|
-
plans.push({ id: `typecheck:${workspace}`, cmd: `cd ${workspace} && bun run typecheck` });
|
|
175
|
+
plans.push({ id: `typecheck:${workspace}`, cmd: `cd ${shQuote(workspace)} && bun run typecheck` });
|
|
173
176
|
}
|
|
174
177
|
}
|
|
175
178
|
const testsByWorkspace = new Map;
|
|
@@ -180,7 +183,10 @@ function planLightChecks(scope, hasTypecheck = workspaceHasTypecheck) {
|
|
|
180
183
|
testsByWorkspace.set(workspace, paths);
|
|
181
184
|
}
|
|
182
185
|
for (const [workspace, paths] of testsByWorkspace) {
|
|
183
|
-
plans.push({
|
|
186
|
+
plans.push({
|
|
187
|
+
id: `test:${workspace}`,
|
|
188
|
+
cmd: `cd ${shQuote(workspace)} && bun test ${paths.map(shQuote).join(" ")}`
|
|
189
|
+
});
|
|
184
190
|
}
|
|
185
191
|
return plans;
|
|
186
192
|
}
|
|
@@ -376,7 +382,7 @@ function runQualityGate(mode, env, options = {}) {
|
|
|
376
382
|
appendFileSync(abs(logFile), `proof-digest: ${env.proofDigest ?? ""}
|
|
377
383
|
`);
|
|
378
384
|
let receiptFile;
|
|
379
|
-
if (
|
|
385
|
+
if (!noProgressSkip) {
|
|
380
386
|
if ((env.proofDigest ?? "").length > 0) {
|
|
381
387
|
receiptFile = join(runDir, `${env.wbs}-check-receipt.json`);
|
|
382
388
|
const receipt = buildReceipt({
|
|
@@ -404,17 +410,19 @@ function runQualityGate(mode, env, options = {}) {
|
|
|
404
410
|
return { status, attempts: gateAttempt, logFile, findingsFile, statusFile, attemptFile, receiptFile };
|
|
405
411
|
}
|
|
406
412
|
var QUALITY_GATE_USAGE = "usage: quality-gate.ts <run|recheck|light|status> (env: wbs, qualityGateCmd, gateProbeCmd, proofDigest, runId)";
|
|
407
|
-
function main(argv,
|
|
413
|
+
function main(argv, rawEnv = getEnvVars(), options = {}) {
|
|
408
414
|
const mode = argv[0];
|
|
409
415
|
if (mode !== "run" && mode !== "recheck" && mode !== "light" && mode !== "status") {
|
|
410
416
|
process.stderr.write(`${QUALITY_GATE_USAGE}
|
|
411
417
|
`);
|
|
412
418
|
return 2;
|
|
413
419
|
}
|
|
414
|
-
|
|
420
|
+
const wbs = rawEnv.wbs ?? "";
|
|
421
|
+
if (wbs.length === 0) {
|
|
415
422
|
process.stderr.write("quality-gate: env `wbs` is required\n");
|
|
416
423
|
return 2;
|
|
417
424
|
}
|
|
425
|
+
const env = { ...rawEnv, wbs };
|
|
418
426
|
if (mode === "light") {
|
|
419
427
|
runLightGate(env);
|
|
420
428
|
} else if (mode === "status") {
|
|
@@ -439,6 +447,7 @@ function main(argv, env = getEnvVars(), options = {}) {
|
|
|
439
447
|
export {
|
|
440
448
|
workspaceHasTypecheck,
|
|
441
449
|
tailLines,
|
|
450
|
+
shQuote,
|
|
442
451
|
scanCoverageShortfalls,
|
|
443
452
|
runShellCommand,
|
|
444
453
|
runQualityGate,
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
* output matches a SQLite busy/locked error; the delay defaults to 10 seconds and is
|
|
27
27
|
* overridable via `SPUR_QUALITY_GATE_RETRY_DELAY_MS` (tests).
|
|
28
28
|
*
|
|
29
|
-
* Check receipts (task 0939, ADR-124): `run`
|
|
30
|
-
* (`check-receipt/v1`) when `proofDigest` is set; without a digest no receipt is written and the
|
|
29
|
+
* Check receipts (task 0939, ADR-124): `run` and `recheck` write `.spur/run/<wbs>-check-receipt.json`
|
|
30
|
+
* (`check-receipt/v1`) when `proofDigest` is set (a no-progress skip keeps the receipt it matched); without a digest no receipt is written and the
|
|
31
31
|
* log says why. The gate executes `qualityGateCmd` as one unit, so the full-tier receipt carries
|
|
32
32
|
* a single `test` row for the whole `bun run spur-check` chain (lint | typecheck | test-pre-check
|
|
33
33
|
* | test | test-post-check). `light` accumulates: a sub-check already PASS in a light receipt at
|
|
@@ -335,6 +335,15 @@ export function lightScope(changedFiles: string[], exists: (p: string) => boolea
|
|
|
335
335
|
return { files, workspaces: [...workspaces].sort(), tests: [...tests].sort() };
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Quote one argument for `sh -c`. Changed-file names come from the working tree (untracked files
|
|
340
|
+
* included), so an unquoted `x$(cmd).ts` would run `cmd` and a space would split the argument.
|
|
341
|
+
* Plain paths pass through unchanged to keep receipts readable.
|
|
342
|
+
*/
|
|
343
|
+
export function shQuote(arg: string): string {
|
|
344
|
+
return /^[\w./@+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
345
|
+
}
|
|
346
|
+
|
|
338
347
|
export interface LightCheckPlan {
|
|
339
348
|
id: string;
|
|
340
349
|
cmd: string;
|
|
@@ -364,11 +373,11 @@ export function planLightChecks(
|
|
|
364
373
|
): LightCheckPlan[] {
|
|
365
374
|
const plans: LightCheckPlan[] = [];
|
|
366
375
|
if (scope.files.length > 0) {
|
|
367
|
-
plans.push({ id: 'format-lint:changed', cmd: `bunx biome check ${scope.files.join(' ')}` });
|
|
376
|
+
plans.push({ id: 'format-lint:changed', cmd: `bunx biome check ${scope.files.map(shQuote).join(' ')}` });
|
|
368
377
|
}
|
|
369
378
|
for (const workspace of scope.workspaces) {
|
|
370
379
|
if (hasTypecheck(workspace)) {
|
|
371
|
-
plans.push({ id: `typecheck:${workspace}`, cmd: `cd ${workspace} && bun run typecheck` });
|
|
380
|
+
plans.push({ id: `typecheck:${workspace}`, cmd: `cd ${shQuote(workspace)} && bun run typecheck` });
|
|
372
381
|
}
|
|
373
382
|
}
|
|
374
383
|
const testsByWorkspace = new Map<string, string[]>();
|
|
@@ -379,7 +388,10 @@ export function planLightChecks(
|
|
|
379
388
|
testsByWorkspace.set(workspace, paths);
|
|
380
389
|
}
|
|
381
390
|
for (const [workspace, paths] of testsByWorkspace) {
|
|
382
|
-
plans.push({
|
|
391
|
+
plans.push({
|
|
392
|
+
id: `test:${workspace}`,
|
|
393
|
+
cmd: `cd ${shQuote(workspace)} && bun test ${paths.map(shQuote).join(' ')}`,
|
|
394
|
+
});
|
|
383
395
|
}
|
|
384
396
|
return plans;
|
|
385
397
|
}
|
|
@@ -622,9 +634,12 @@ export function runQualityGate(
|
|
|
622
634
|
writeFileSync(abs(statusFile), `${status}\n`);
|
|
623
635
|
appendFileSync(abs(logFile), `proof-digest: ${env.proofDigest ?? ''}\n`);
|
|
624
636
|
|
|
625
|
-
// 0939 R2:
|
|
637
|
+
// 0939 R2: the gate writes the full-tier receipt only with a digest to bind it to. 0976 R1:
|
|
638
|
+
// `recheck` persists the receipt it evaluated too, so a second recheck at the same digest can
|
|
639
|
+
// take the no-progress skip. A skip leaves the FAIL receipt it matched untouched — a skip never
|
|
640
|
+
// rewrites a receipt, so it can never launder FAIL into PASS.
|
|
626
641
|
let receiptFile: string | undefined;
|
|
627
|
-
if (
|
|
642
|
+
if (!noProgressSkip) {
|
|
628
643
|
if ((env.proofDigest ?? '').length > 0) {
|
|
629
644
|
receiptFile = join(runDir, `${env.wbs}-check-receipt.json`);
|
|
630
645
|
const receipt = buildReceipt({
|
|
@@ -655,16 +670,22 @@ export function runQualityGate(
|
|
|
655
670
|
export const QUALITY_GATE_USAGE =
|
|
656
671
|
'usage: quality-gate.ts <run|recheck|light|status> (env: wbs, qualityGateCmd, gateProbeCmd, proofDigest, runId)';
|
|
657
672
|
|
|
658
|
-
export function main(
|
|
673
|
+
export function main(
|
|
674
|
+
argv: string[],
|
|
675
|
+
rawEnv: Partial<QualityGateEnv> = getEnvVars(),
|
|
676
|
+
options: QualityGateOptions = {},
|
|
677
|
+
): number {
|
|
659
678
|
const mode = argv[0];
|
|
660
679
|
if (mode !== 'run' && mode !== 'recheck' && mode !== 'light' && mode !== 'status') {
|
|
661
680
|
process.stderr.write(`${QUALITY_GATE_USAGE}\n`);
|
|
662
681
|
return 2;
|
|
663
682
|
}
|
|
664
|
-
|
|
683
|
+
const wbs = rawEnv.wbs ?? '';
|
|
684
|
+
if (wbs.length === 0) {
|
|
665
685
|
process.stderr.write('quality-gate: env `wbs` is required\n');
|
|
666
686
|
return 2;
|
|
667
687
|
}
|
|
688
|
+
const env: QualityGateEnv = { ...rawEnv, wbs };
|
|
668
689
|
if (mode === 'light') {
|
|
669
690
|
runLightGate(env);
|
|
670
691
|
} else if (mode === 'status') {
|
|
@@ -17,7 +17,7 @@ function getEnvVars() {
|
|
|
17
17
|
var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]";
|
|
18
18
|
var MARKER_PATTERN = /TODO|FIXME|XXX|HACK/;
|
|
19
19
|
var PRIORITY_PATTERN = /^P[1-4]/;
|
|
20
|
-
var NONE_FINDING = /^(none
|
|
20
|
+
var NONE_FINDING = /^(none( found)?|no (findings?|issues?)( found)?|\u2014)\s*(\(.*\))?\.?$/i;
|
|
21
21
|
var DISPOSITION_HEADER = /^(Disposition|Action|Status|Resolution|Fixed)$/i;
|
|
22
22
|
var RESOLVED_DISPOSITION = /^(FIXED|RESOLVED|DONE)\b/i;
|
|
23
23
|
var DEFERRED_DISPOSITION = /^DEFER(RED)?\b/i;
|
|
@@ -47,14 +47,14 @@ function locationOf(locationCell, finding) {
|
|
|
47
47
|
const cell = locationCell.trim().replace(/`/g, "");
|
|
48
48
|
if (cell.length > 0 && cell !== "\u2014")
|
|
49
49
|
return normalizeAnchor(cell);
|
|
50
|
-
const backtick = finding.match(/`([^`]+)`/);
|
|
51
|
-
return backtick ===
|
|
50
|
+
const backtick = finding.match(/`([^`]+)`/)?.[1];
|
|
51
|
+
return backtick === undefined ? "" : normalizeAnchor(backtick);
|
|
52
52
|
}
|
|
53
53
|
function parseReviewFindings(taskContent) {
|
|
54
54
|
const section = taskContent.split(/^### Review\b/m)[1];
|
|
55
55
|
if (section === undefined)
|
|
56
56
|
return [];
|
|
57
|
-
const body = section.split(/^### /m)[0];
|
|
57
|
+
const body = section.split(/^### /m)[0] ?? "";
|
|
58
58
|
const out = [];
|
|
59
59
|
const lines = body.split(`
|
|
60
60
|
`);
|
|
@@ -64,7 +64,8 @@ export interface ScanOptions {
|
|
|
64
64
|
|
|
65
65
|
const MARKER_PATTERN = /TODO|FIXME|XXX|HACK/;
|
|
66
66
|
const PRIORITY_PATTERN = /^P[1-4]/;
|
|
67
|
-
|
|
67
|
+
// Placeholder "no finding" cells, optionally with a trailing "(…)" note; "None of X…" is a real finding.
|
|
68
|
+
const NONE_FINDING = /^(none( found)?|no (findings?|issues?)( found)?|—)\s*(\(.*\))?\.?$/i;
|
|
68
69
|
const DISPOSITION_HEADER = /^(Disposition|Action|Status|Resolution|Fixed)$/i;
|
|
69
70
|
const RESOLVED_DISPOSITION = /^(FIXED|RESOLVED|DONE)\b/i;
|
|
70
71
|
const DEFERRED_DISPOSITION = /^DEFER(RED)?\b/i;
|
|
@@ -104,8 +105,8 @@ export function normalizeAnchor(location: string): string {
|
|
|
104
105
|
export function locationOf(locationCell: string, finding: string): string {
|
|
105
106
|
const cell = locationCell.trim().replace(/`/g, '');
|
|
106
107
|
if (cell.length > 0 && cell !== '—') return normalizeAnchor(cell);
|
|
107
|
-
const backtick = finding.match(/`([^`]+)`/);
|
|
108
|
-
return backtick ===
|
|
108
|
+
const backtick = finding.match(/`([^`]+)`/)?.[1];
|
|
109
|
+
return backtick === undefined ? '' : normalizeAnchor(backtick);
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
/**
|
|
@@ -119,7 +120,7 @@ export function parseReviewFindings(
|
|
|
119
120
|
): Array<{ priority: string; location: string; text: string; deferral?: string }> {
|
|
120
121
|
const section = taskContent.split(/^### Review\b/m)[1];
|
|
121
122
|
if (section === undefined) return [];
|
|
122
|
-
const body = section.split(/^### /m)[0];
|
|
123
|
+
const body = section.split(/^### /m)[0] ?? '';
|
|
123
124
|
const out: Array<{ priority: string; location: string; text: string; deferral?: string }> = [];
|
|
124
125
|
const lines = body.split('\n');
|
|
125
126
|
for (let i = 0; i < lines.length; i++) {
|