@gobing-ai/spur 0.3.75 → 0.3.77
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/config/config.example.yaml +6 -0
- package/config/plugin-scripts.json +4 -0
- package/config/rules/README.md +1 -1
- package/config/rules/strict/runtime-boundaries.yaml +2 -0
- package/config/rules/typescript/no-eslint-suppressions.yaml +30 -0
- package/config/workflows/task-pipeline.yaml +19 -6
- package/package.json +9 -9
- package/plugins/sp/commands/dev-refineall.md +2 -2
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/inline-run-setup.ts +198 -0
- package/plugins/sp/scripts/verify-answer-lint.ts +134 -26
- package/plugins/sp/skills/code-verification/SKILL.md +4 -2
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +11 -2
- package/plugins/sp/skills/dogfood-testing/SKILL.md +48 -40
- package/plugins/sp/skills/dogfood-testing/references/monitor-ledger.md +34 -10
- package/plugins/sp/skills/dogfood-testing/references/report-template.md +10 -6
- package/plugins/sp/skills/spur-cli/references/tasks/section-editing.md +9 -3
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +5 -3
- package/plugins/sp/skills/spur-cli/references/workflows.md +3 -3
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +35 -7
- package/schemas/spur-config.schema.json +4 -0
- package/spur.js +10741 -9079
- package/web/_astro/{BoardApp.BOW1815F.js → BoardApp.CHQ1lycZ.js} +83 -83
- package/web/_astro/BoardApp.DV9kx0wo.js +1 -0
- package/web/_astro/{TaskDetail.CX6C7dSJ.js → TaskDetail.GKfQJ60c.js} +1 -1
- package/web/_astro/{arc.CwSvH1ji.js → arc.DWEtA3Tx.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.Dxne1RvP.js → architectureDiagram-3BPJPVTR.DB42oWmP.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.D9Fb14rC.js → blockDiagram-GPEHLZMM.rhv-zNQV.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.DmvWSFvv.js → c4Diagram-AAUBKEIU.Ci4-4VvY.js} +1 -1
- package/web/_astro/channel.BAI6xLeV.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.BCCktIrE.js → chunk-2J33WTMH.Cc9veUgf.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.BzCk9q27.js → chunk-4BX2VUAB.Bec9c4eI.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.CZXVgTk4.js → chunk-55IACEB6.DoV8S1iB.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.CJL8UIpX.js → chunk-727SXJPM.DwR-Qlyj.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.Dz8MDEdw.js → chunk-AQP2D5EJ.ND_a81WY.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.CkQeYYUw.js → chunk-FMBD7UC4.Wv_jwG48.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.KiC1QgzH.js → chunk-ND2GUHAM.CXKXCMmp.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.AJ08mw2e.js → chunk-QZHKN3VN.nkaoNYQq.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.DRWRDKzK.js → classDiagram-4FO5ZUOK.cMQcVlQu.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.DRWRDKzK.js → classDiagram-v2-Q7XG4LA2.cMQcVlQu.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.CavydfLP.js → cose-bilkent-S5V4N54A.OaDJ7Mr2.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.D_7o_a0B.js → cynefin-OW5HDTMX.Chi8IphF.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.LWk2dKgg.js → dagre-BM42HDAG.CzK2t_Fp.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.82lbq6aB.js → diagram-2AECGRRQ.DRvxlVS7.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.hyNL1pwY.js → diagram-5GNKFQAL.CnYvNdwA.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.BaLqVf-b.js → diagram-KO2AKTUF.CpLpMw5R.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.DQJxj0oz.js → diagram-LMA3HP47.JTb78qUA.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.DhlL8cG2.js → diagram-OG6HWLK6.Bk-1jDIb.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.HoTnXwkF.js → erDiagram-TEJ5UH35.D8hN9GZq.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.DkFKlZIZ.js → flowDiagram-I6XJVG4X.-6zQr6m5.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.9qPfOTDb.js → ganttDiagram-6RSMTGT7.DboLQ9ca.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.B4G18Dwc.js → gitGraphDiagram-PVQCEYII.4tYvJKGR.js} +1 -1
- package/web/_astro/{infoDiagram-5YYISTIA.e3KJkXAM.js → infoDiagram-5YYISTIA.Bd9rXpsB.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.CY3yddhD.js → ishikawaDiagram-YF4QCWOH.CvMoaf67.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.DaAKIO1t.js → journeyDiagram-JHISSGLW.Ccy1CA7y.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.DGkDddtc.js → kanban-definition-UN3LZRKU.0MaMqHNS.js} +1 -1
- package/web/_astro/{linear.DnDPcpd1.js → linear.CHXgcIbN.js} +1 -1
- package/web/_astro/{mermaid.core.1uBmxa9t.js → mermaid.core.Ca-kcelG.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.XFJVayxw.js → mindmap-definition-RKZ34NQL.BUIDlHa0.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.BXm2OgHh.js → pieDiagram-4H26LBE5.2dX3CU1s.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.DgB2p5fc.js → quadrantDiagram-W4KKPZXB.B3LBlRiv.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.jBR39bnS.js → requirementDiagram-4Y6WPE33.X12I2uNx.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.N3u9OhE3.js → sankeyDiagram-5OEKKPKP.BXohIHqx.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.CZhqZXNL.js → sequenceDiagram-3UESZ5HK.C37ZIUzg.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.DlNu1VEa.js → stateDiagram-AJRCARHV.BRgz317z.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.BEqi8NQV.js → stateDiagram-v2-BHNVJYJU.7VYSXN9-.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.BPcexclc.js → timeline-definition-PNZ67QCA.BVNz_HiN.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.BZalxKGQ.js → vennDiagram-CIIHVFJN.CHVDkPX4.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.BqOljod9.js → wardleyDiagram-YWT4CUSO.EQQ_qT9v.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.tkO4ppN5.js → xychartDiagram-2RQKCTM6.DrAT9WoP.js} +1 -1
- package/web/index.html +1 -1
- package/web/_astro/BoardApp.C4zimv1U.js +0 -1
- package/web/_astro/channel.CQHdDVp9.js +0 -1
|
@@ -130,6 +130,12 @@ agent:
|
|
|
130
130
|
# agent: omp
|
|
131
131
|
# model: minimax/MiniMax-M3
|
|
132
132
|
# tier: cheap
|
|
133
|
+
# A disabled profile stays visible to `spur agent doctor` but never routes
|
|
134
|
+
# (no role, team, stage, or explicit selection). Flip back to enabled by
|
|
135
|
+
# removing the line or setting disabled: false.
|
|
136
|
+
# - name: retired-profile
|
|
137
|
+
# agent: omp
|
|
138
|
+
# disabled: true
|
|
133
139
|
- name: pi-dsv4-flash-volc
|
|
134
140
|
agent: pi
|
|
135
141
|
# tier: standard
|
package/config/rules/README.md
CHANGED
|
@@ -9,7 +9,7 @@ install or to ts-libs.
|
|
|
9
9
|
|
|
10
10
|
| Category | Dir | Purpose |
|
|
11
11
|
|---|---|---|
|
|
12
|
-
| `typescript` | `typescript/` | TypeScript tooling, output boundaries, biome-suppression ban, no `debugger` |
|
|
12
|
+
| `typescript` | `typescript/` | TypeScript tooling, output boundaries, biome/eslint-suppression ban, no `debugger` |
|
|
13
13
|
| `strict` | `strict/` | Opt-in strict rules (runtime boundaries, HTTP boundaries, structural) |
|
|
14
14
|
| `boundary` | `boundary/` | DB/DAO boundary enforcement |
|
|
15
15
|
| `structure` | `structure/` | File layout, protected files, no focused/skipped tests |
|
|
@@ -69,7 +69,9 @@ rules:
|
|
|
69
69
|
# Synchronous bootstrap & path resolution:
|
|
70
70
|
- "packages/config/src/bundled-config.ts" # sync bundledConfigRoot() directory walk
|
|
71
71
|
- "packages/config/src/loader.ts" # sync resolveConfigFile() / schema specifier lookup
|
|
72
|
+
- "packages/config/src/executor-update.ts" # atomic single-entry config update (task 0797): O_EXCL lock, fsync'd tmp+rename commit, lstat symlink refusal, mode preservation — ts-runtime FileSystem seam has no lstat/fsync/chmod/O_EXCL
|
|
72
73
|
- "packages/app/src/services/project-start.ts" # sync CLI binary path checks before daemon spawn
|
|
74
|
+
- "apps/cli/src/commands/serve.ts" # sync resolveServeCwd() directory validation before server startup (task 0805 R2); mirrors task.ts sync-template exemption
|
|
73
75
|
|
|
74
76
|
# Synchronous template & workflow resolvers:
|
|
75
77
|
- "apps/cli/src/commands/task.ts" # sync loadTemplateContent/Bodies callbacks
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
$schema: "@gobing-ai/spur/schemas/rule-file.schema.json"
|
|
2
|
+
# No ESLint suppression comments. Spur lints with Biome, not ESLint; leftover
|
|
3
|
+
# `eslint-disable*` comments are dead suppressions that hide the real fix and
|
|
4
|
+
# would not even silence the live gate. Sibling of `no-biome-suppressions`
|
|
5
|
+
# (AGENTS.md: never suppress a gate to force green).
|
|
6
|
+
#
|
|
7
|
+
# Tests ARE in scope: an eslint-disable in a test is the same bypass.
|
|
8
|
+
include:
|
|
9
|
+
- "apps/**/*.ts"
|
|
10
|
+
- "apps/**/*.tsx"
|
|
11
|
+
- "packages/**/*.ts"
|
|
12
|
+
- "packages/**/*.tsx"
|
|
13
|
+
- "plugins/**/*.ts"
|
|
14
|
+
- "scripts/**/*.ts"
|
|
15
|
+
exclude:
|
|
16
|
+
- "**/node_modules/**"
|
|
17
|
+
- "**/dist/**"
|
|
18
|
+
- "**/vendors/**"
|
|
19
|
+
rules:
|
|
20
|
+
- id: no-eslint-suppressions
|
|
21
|
+
description: >
|
|
22
|
+
Do not use ESLint disable comments (file, line, or next-line forms).
|
|
23
|
+
Spur lints with Biome; an ESLint suppression does not silence the live
|
|
24
|
+
gate and hides the real fix. Remove the comment and fix the underlying
|
|
25
|
+
issue (AGENTS.md: no suppressions to force green).
|
|
26
|
+
severity: error
|
|
27
|
+
evaluator:
|
|
28
|
+
type: rg
|
|
29
|
+
config:
|
|
30
|
+
pattern: "eslint-disable"
|
|
@@ -270,12 +270,20 @@ states:
|
|
|
270
270
|
# WorkflowAppService.run(); the wbs fallback keeps a driver-less invocation from writing to
|
|
271
271
|
# a bare "-route-reason.txt". The log line carries the run id for the same reason — an
|
|
272
272
|
# unattributed append is log scraping, which R5 explicitly rejects as evidence.
|
|
273
|
+
# 0804 R8: the run id must be a single safe filename component before it
|
|
274
|
+
# becomes REASON_FILE — unresolved interpolation ($ { } vars.), path
|
|
275
|
+
# separators and dot traversal are a failed action (nonzero, no artifact).
|
|
276
|
+
# NOTE: no `#` comments inside the folded scalar — `>-` joins all same-indent
|
|
277
|
+
# lines into ONE shell line, so an in-scalar `#` comments out the rest of the
|
|
278
|
+
# command (the original R8 draft broke exactly this way; the case statement
|
|
279
|
+
# must stay one line).
|
|
273
280
|
- kind: shell
|
|
274
281
|
options:
|
|
275
282
|
command: >-
|
|
276
283
|
mkdir -p .spur/run .spur/memory &&
|
|
277
284
|
RUN_ID="$__runId" &&
|
|
278
285
|
if [ -z "$RUN_ID" ]; then RUN_ID="pipeline-$wbs"; fi &&
|
|
286
|
+
case "$RUN_ID" in *'$'*|*'{'*|*'}'*|*vars.*|*/*|*'\'*|*..*) echo "route-reason: refusing unsafe run id: $RUN_ID" >&2; exit 1 ;; *) : ;; esac &&
|
|
279
287
|
REASON_FILE=".spur/run/$RUN_ID-route-reason.txt" &&
|
|
280
288
|
if [ "$mode" = "fast" ]; then
|
|
281
289
|
echo "fast:evidence complete+consistent" > "$REASON_FILE";
|
|
@@ -335,7 +343,7 @@ states:
|
|
|
335
343
|
RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
|
|
336
344
|
printf '%s\n' "$RETRY_OUTPUT";
|
|
337
345
|
if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
|
|
338
|
-
if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked'; then return "$RETRY_RC"; fi;
|
|
346
|
+
if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked|SQLite database .*is busy|SQLITE_BUSY'; then return "$RETRY_RC"; fi;
|
|
339
347
|
sleep 2;
|
|
340
348
|
RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
|
|
341
349
|
printf '%s\n' "$RETRY_SECOND_OUTPUT";
|
|
@@ -431,7 +439,7 @@ states:
|
|
|
431
439
|
ATTEMPT_LOG="$LOG_FILE.attempt-$gate_attempt";
|
|
432
440
|
sh -c "$qualityGateCmd" > "$ATTEMPT_LOG" 2>&1; gate_rc=$?;
|
|
433
441
|
gate_locked=0;
|
|
434
|
-
grep -
|
|
442
|
+
grep -Eq 'SQLiteError: database is locked|SQLite database .*is busy|SQLITE_BUSY' "$ATTEMPT_LOG" && gate_locked=1;
|
|
435
443
|
cat "$ATTEMPT_LOG" >> "$LOG_FILE";
|
|
436
444
|
rm -f "$ATTEMPT_LOG";
|
|
437
445
|
if [ "$gate_rc" -eq 0 ] || [ "$gate_locked" -ne 1 ] || [ "$gate_attempt" -ge 5 ]; then break; fi;
|
|
@@ -547,7 +555,7 @@ states:
|
|
|
547
555
|
ATTEMPT_LOG="$LOG_FILE.attempt-$gate_attempt";
|
|
548
556
|
sh -c "$qualityGateCmd" > "$ATTEMPT_LOG" 2>&1; gate_rc=$?;
|
|
549
557
|
gate_locked=0;
|
|
550
|
-
grep -
|
|
558
|
+
grep -Eq 'SQLiteError: database is locked|SQLite database .*is busy|SQLITE_BUSY' "$ATTEMPT_LOG" && gate_locked=1;
|
|
551
559
|
cat "$ATTEMPT_LOG" >> "$LOG_FILE";
|
|
552
560
|
rm -f "$ATTEMPT_LOG";
|
|
553
561
|
if [ "$gate_rc" -eq 0 ] || [ "$gate_locked" -ne 1 ] || [ "$gate_attempt" -ge 5 ]; then break; fi;
|
|
@@ -747,7 +755,7 @@ states:
|
|
|
747
755
|
RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
|
|
748
756
|
printf '%s\n' "$RETRY_OUTPUT";
|
|
749
757
|
if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
|
|
750
|
-
if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked'; then return "$RETRY_RC"; fi;
|
|
758
|
+
if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked|SQLite database .*is busy|SQLITE_BUSY'; then return "$RETRY_RC"; fi;
|
|
751
759
|
sleep 2;
|
|
752
760
|
RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
|
|
753
761
|
printf '%s\n' "$RETRY_SECOND_OUTPUT";
|
|
@@ -794,7 +802,7 @@ states:
|
|
|
794
802
|
RETRY_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
|
|
795
803
|
printf '%s\n' "$RETRY_OUTPUT";
|
|
796
804
|
if [ "$RETRY_RC" -eq 0 ]; then return 0; fi;
|
|
797
|
-
if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked'; then return "$RETRY_RC"; fi;
|
|
805
|
+
if ! printf '%s' "$RETRY_OUTPUT" | grep -Eq 'ENOENT|EBUSY|ENOTEMPTY|database is locked|SQLite database .*is busy|SQLITE_BUSY'; then return "$RETRY_RC"; fi;
|
|
798
806
|
sleep 2;
|
|
799
807
|
RETRY_SECOND_OUTPUT=$("$@" 2>&1); RETRY_RC=$?;
|
|
800
808
|
printf '%s\n' "$RETRY_SECOND_OUTPUT";
|
|
@@ -1016,8 +1024,13 @@ transitions:
|
|
|
1016
1024
|
guard:
|
|
1017
1025
|
kind: shell
|
|
1018
1026
|
options:
|
|
1027
|
+
# 0804 R6: `--as done` projects the check onto the done target so open
|
|
1028
|
+
# Plan/AC checkboxes fail HERE — before the done action runs — mirroring
|
|
1029
|
+
# the CLI's own target-aware `--no-lifecycle` backstop (F92 R3). No `#`
|
|
1030
|
+
# comments inside the folded scalar: `>-` joins same-indent lines into
|
|
1031
|
+
# one shell line.
|
|
1019
1032
|
command: >-
|
|
1020
|
-
$spurBin task check $wbs &&
|
|
1033
|
+
$spurBin task check $wbs --as done &&
|
|
1021
1034
|
test "$(jq -r .verdict .spur/run/$wbs-verdict.json 2>/dev/null)" = PASS &&
|
|
1022
1035
|
test "$(jq -r '.proof.digest // ""' .spur/run/$wbs-verdict.json 2>/dev/null)" = "$proofDigest"
|
|
1023
1036
|
- from: record
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/spur",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.77",
|
|
4
4
|
"description": "Spur CLI — local-first harness for mainstream coding agents: constraint checking, workflow orchestration, agent health, and history analytics. Bun-native; exposes the `spur` command.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"spur",
|
|
@@ -53,14 +53,14 @@
|
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@commander-js/extra-typings": "^14.0.0",
|
|
56
|
-
"@gobing-ai/ts-db": "^0.4.
|
|
57
|
-
"@gobing-ai/ts-ai-runner": "^0.4.
|
|
58
|
-
"@gobing-ai/ts-dual-workflow-engine": "^0.4.
|
|
59
|
-
"@gobing-ai/ts-infra": "^0.4.
|
|
60
|
-
"@gobing-ai/ts-llm-jsonl-importer": "^0.4.
|
|
61
|
-
"@gobing-ai/ts-rule-engine": "^0.4.
|
|
62
|
-
"@gobing-ai/ts-runtime": "^0.4.
|
|
63
|
-
"@gobing-ai/ts-utils": "^0.4.
|
|
56
|
+
"@gobing-ai/ts-db": "^0.4.57",
|
|
57
|
+
"@gobing-ai/ts-ai-runner": "^0.4.57",
|
|
58
|
+
"@gobing-ai/ts-dual-workflow-engine": "^0.4.57",
|
|
59
|
+
"@gobing-ai/ts-infra": "^0.4.57",
|
|
60
|
+
"@gobing-ai/ts-llm-jsonl-importer": "^0.4.57",
|
|
61
|
+
"@gobing-ai/ts-rule-engine": "^0.4.57",
|
|
62
|
+
"@gobing-ai/ts-runtime": "^0.4.57",
|
|
63
|
+
"@gobing-ai/ts-utils": "^0.4.57",
|
|
64
64
|
"@types/bun": "1.3.14",
|
|
65
65
|
"@types/figlet": "^1.7.0",
|
|
66
66
|
"@types/node-notifier": "8.0.5",
|
|
@@ -23,7 +23,7 @@ operation, applied to a resolved set (typically every task under a feature). Pas
|
|
|
23
23
|
| `--agent` `<inline\|auto\|name>` | Who runs the model-bearing refinement. | omit |
|
|
24
24
|
| `--auto` | Skip objective HITL gates. | off |
|
|
25
25
|
| `--keep-going` | Continue past per-task failures. | off |
|
|
26
|
-
| `--status` `<s>` | Only refine tasks in
|
|
26
|
+
| `--status` `<s>` | Only refine tasks in these statuses (applied in-agent to the frozen set). | `backlog` + `todo` |
|
|
27
27
|
| `--json` | Emit structured JSON. | off |
|
|
28
28
|
| `--worktree` `[<name>]` | Run the batch in an isolated git worktree; FF-merge on success, retain on failure. Bare `--worktree` creates a fresh tree; `--worktree <name>` adopts an existing worktree by name/path/branch. | off |
|
|
29
29
|
|
|
@@ -39,7 +39,7 @@ For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag
|
|
|
39
39
|
Flags: `--feature` (sugar for `feature:<id>`), `--tasks <selector>`, shared refine flags
|
|
40
40
|
(`--focus`, `--description`, `--depth`, `--agent`, `--auto`),
|
|
41
41
|
plus `--keep-going`,
|
|
42
|
-
`--status` (default `backlog
|
|
42
|
+
`--status` (default `backlog` + `todo`),
|
|
43
43
|
`--json`, `--worktree` `[<name>]` (run the batch in an isolated git worktree — FF-merge onto the base ref on
|
|
44
44
|
full success, retain intact on any failure/halt/non-FF; bare form creates a fresh tree, `<name>`
|
|
45
45
|
form adopts an existing worktree by name/path/branch; see `execution-batch.md` § Worktree
|
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.77",
|
|
4
4
|
"description": "Spur — a local-first harness engineering toolkit that wraps mainstream coding agents with constraint checking, workflow orchestration, and history analytics.",
|
|
5
5
|
"extensions": {
|
|
6
6
|
"pi": ["./hooks/pi/guard-extension.ts"]
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* inline-run-setup — authoritative inline full-pipeline run identity (task 0804 R1).
|
|
4
|
+
*
|
|
5
|
+
* Unlike the subprocess path (`spur workflow run`), the interactive inline driver
|
|
6
|
+
* allocated a run id but never persisted an authoritative `runs` row, so bound
|
|
7
|
+
* `run.artifact` registration (0785 R3) correctly refused every inline record. This
|
|
8
|
+
* script is the thin delegate the driver now runs at Run setup: it resolves the spur
|
|
9
|
+
* repo checkout from the SPUR_BIN chain, imports the real app service
|
|
10
|
+
* (`createOrAttachInlineRun` / `openInlineRunProjectDb` from packages/app), and lets it
|
|
11
|
+
* resolve the SAME project-or-bundled definition the engine would launch, compute the
|
|
12
|
+
* canonical definition digest with the exported hash machinery, and create-or-attach the
|
|
13
|
+
* run row through the existing engine persistence adapter.
|
|
14
|
+
*
|
|
15
|
+
* The script itself contains NO direct SQL, NO second hasher and NO persistence policy —
|
|
16
|
+
* every rule lives in packages/app (0804 D1). On a bundle-only install there is no repo
|
|
17
|
+
* checkout to import the app service from, so the setup fails closed with actionable
|
|
18
|
+
* remediation guidance (point SPUR_BIN at a repo checkout); it never falls back to an
|
|
19
|
+
* unbound run (0804 R1 failure policy).
|
|
20
|
+
*
|
|
21
|
+
* Outcome JSON is written to `.spur/run/<run-id>-inline-setup.json` so the driver can
|
|
22
|
+
* seed the inline var overlay (`__runId`, `__definitionDigest`) that proof capture and
|
|
23
|
+
* bound registration verify against. Exit 0 = authoritative identity ready (created or
|
|
24
|
+
* idempotently attached); exit 1 = fail closed, the driver must stop.
|
|
25
|
+
*
|
|
26
|
+
* Repo-only script (ADR-065): it imports the app workspace source, so it runs under bun
|
|
27
|
+
* against a monorepo checkout only — the same posture as task-size-precheck.ts and
|
|
28
|
+
* task-evidence-precheck.ts.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]
|
|
32
|
+
*
|
|
33
|
+
* Env: SPUR_BIN
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
37
|
+
import { dirname, join, resolve } from 'node:path';
|
|
38
|
+
import { fileURLToPath } from 'node:url';
|
|
39
|
+
|
|
40
|
+
/** Outcome document written to `.spur/run/<run-id>-inline-setup.json`. */
|
|
41
|
+
interface SetupOutcome {
|
|
42
|
+
readonly ok: boolean;
|
|
43
|
+
readonly runId: string;
|
|
44
|
+
readonly attached?: boolean;
|
|
45
|
+
readonly definitionDigest?: string;
|
|
46
|
+
readonly workflowName?: string;
|
|
47
|
+
readonly workflowVersion?: string | null;
|
|
48
|
+
readonly resolvedPath?: string;
|
|
49
|
+
readonly layer?: string;
|
|
50
|
+
readonly workdir?: string;
|
|
51
|
+
readonly status?: string;
|
|
52
|
+
readonly error?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function usage(): never {
|
|
56
|
+
console.error(
|
|
57
|
+
'Usage: bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]',
|
|
58
|
+
);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The run id becomes a filename under `.spur/run/` (`<run-id>-inline-setup.json`), so it must be a
|
|
64
|
+
* single safe filename component before anything is written — the same guard class the
|
|
65
|
+
* task-pipeline.yaml route-reason action applies to `$__runId` (task 0804 R8). The allowlist
|
|
66
|
+
* refuses path separators, dot traversal (leading `.`), unresolved interpolation (`$`/`{`/`}`) and
|
|
67
|
+
* every other shell/unspecified metachar; valid UUID/timestamp-slug ids pass.
|
|
68
|
+
*/
|
|
69
|
+
const SAFE_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
70
|
+
|
|
71
|
+
function refuseUnsafeRunId(runId: string): never {
|
|
72
|
+
// Refuse BEFORE any outcome write: an unsafe id must never reach
|
|
73
|
+
// `.spur/run/<run-id>-inline-setup.json` (no traversal, no unintended file).
|
|
74
|
+
console.error(`inline-run-setup: refusing unsafe run id: ${runId}`);
|
|
75
|
+
console.error(
|
|
76
|
+
' The run id must be a single safe filename component (alphanumeric/._-, no leading dot, ' +
|
|
77
|
+
'no path separators, interpolation or traversal; same class as the task-pipeline ' +
|
|
78
|
+
'route-reason guard, task 0804 R8). Allocate a fresh run id (uuid or timestamp slug) and retry.',
|
|
79
|
+
);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve the spur repo checkout the same way the other prechecks resolve the CLI
|
|
85
|
+
* (--spur-bin > SPUR_BIN > monorepo-local CLI entry > PATH `spur`), then derive the repo
|
|
86
|
+
* root from the resolved main module. `bun <repo>/apps/cli/src/index.ts` → repo root is
|
|
87
|
+
* three levels up. A bundle-only install (`spur` on PATH, a bundled `spur.js`, or a spur
|
|
88
|
+
* binary without the app workspace) has no app entry to import — the caller fails that
|
|
89
|
+
* closed with remediation guidance.
|
|
90
|
+
*/
|
|
91
|
+
function resolveAppEntry(
|
|
92
|
+
spurBin: string,
|
|
93
|
+
): { entry: string; repoRoot: string } | { entry: null; repoRoot: null; chain: string } {
|
|
94
|
+
let candidates: string[] = [];
|
|
95
|
+
if (spurBin !== '') {
|
|
96
|
+
candidates = [spurBin];
|
|
97
|
+
} else {
|
|
98
|
+
// scripts/ -> plugins/sp/ -> <repo>/apps/cli/src/index.ts (fileURLToPath — raw
|
|
99
|
+
// pathname breaks on %-encoded paths, e.g. spaces in the checkout directory).
|
|
100
|
+
candidates = [fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url))];
|
|
101
|
+
}
|
|
102
|
+
for (const candidate of candidates) {
|
|
103
|
+
const tokens = candidate.split(/\s+/).filter(Boolean);
|
|
104
|
+
// The main module is the last path-like token (tolerates `bun <path>` /
|
|
105
|
+
// `bun run <path>` lead tokens). Only a TypeScript source entry proves a repo
|
|
106
|
+
// checkout; a bundled `spur.js` or a bare `spur` binary does not.
|
|
107
|
+
const mainModule = [...tokens].reverse().find((t) => t.endsWith('.ts'));
|
|
108
|
+
if (mainModule === undefined || !existsSync(mainModule)) continue;
|
|
109
|
+
// <repo>/apps/cli/src/index.ts → repo root; then require the app package source.
|
|
110
|
+
const srcDir = dirname(mainModule);
|
|
111
|
+
const repoRoot = resolve(srcDir, '..', '..', '..');
|
|
112
|
+
const appEntry = join(repoRoot, 'packages', 'app', 'src', 'index.ts');
|
|
113
|
+
if (existsSync(appEntry)) return { entry: appEntry, repoRoot };
|
|
114
|
+
return { entry: null, repoRoot: null, chain: `${candidate} (no ${appEntry})` };
|
|
115
|
+
}
|
|
116
|
+
return { entry: null, repoRoot: null, chain: spurBin === '' ? 'PATH spur (bundle-only install)' : spurBin };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function writeOutcome(runId: string, outcome: SetupOutcome): void {
|
|
120
|
+
const runDir = join(process.cwd(), '.spur', 'run');
|
|
121
|
+
if (!existsSync(runDir)) mkdirSync(runDir, { recursive: true });
|
|
122
|
+
writeFileSync(join(runDir, `${runId}-inline-setup.json`), `${JSON.stringify(outcome, null, 4)}\n`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function main(): Promise<void> {
|
|
126
|
+
let runId = '';
|
|
127
|
+
let file = '';
|
|
128
|
+
let spurBin = process.env.SPUR_BIN ?? '';
|
|
129
|
+
const argv = process.argv.slice(2);
|
|
130
|
+
for (let i = 0; i < argv.length; i++) {
|
|
131
|
+
if (argv[i] === '--run-id') runId = argv[++i] ?? '';
|
|
132
|
+
else if (argv[i] === '--file') file = argv[++i] ?? '';
|
|
133
|
+
else if (argv[i] === '--spur-bin') spurBin = argv[++i] ?? spurBin;
|
|
134
|
+
}
|
|
135
|
+
if (runId.trim() === '' || file.trim() === '') usage();
|
|
136
|
+
if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
|
|
137
|
+
|
|
138
|
+
const { entry, repoRoot, chain } = resolveAppEntry(spurBin);
|
|
139
|
+
if (entry === null || repoRoot === null) {
|
|
140
|
+
const outcome: SetupOutcome = {
|
|
141
|
+
ok: false,
|
|
142
|
+
runId,
|
|
143
|
+
error:
|
|
144
|
+
`inline run setup failed closed: no monorepo checkout of spur is reachable via ${chain}. ` +
|
|
145
|
+
'The authoritative run identity must be persisted by the app service ' +
|
|
146
|
+
'(packages/app/src/services/inline-run-setup.ts); a bundle-only install cannot do this. ' +
|
|
147
|
+
'Remediation: point SPUR_BIN at a repo checkout, e.g. ' +
|
|
148
|
+
'SPUR_BIN="bun /path/to/spur/apps/cli/src/index.ts". The pipeline must not run unbound.',
|
|
149
|
+
};
|
|
150
|
+
writeOutcome(runId, outcome);
|
|
151
|
+
console.error(`inline-run-setup: FAIL for run ${runId}`);
|
|
152
|
+
console.error(` ${outcome.error}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Dynamic import by absolute path: the app source graph resolves its own workspace
|
|
157
|
+
// dependencies from the repo checkout, never from this plugin script's location.
|
|
158
|
+
const app = (await import(entry)) as {
|
|
159
|
+
createOrAttachInlineRun: (input: {
|
|
160
|
+
workdir: string;
|
|
161
|
+
getDb: () => Promise<unknown>;
|
|
162
|
+
file: string;
|
|
163
|
+
runId: string;
|
|
164
|
+
}) => Promise<SetupOutcome & { ok: boolean }>;
|
|
165
|
+
openInlineRunProjectDb: (workdir: string) => Promise<{ adapter: unknown; close: () => void }>;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const workdir = process.cwd();
|
|
169
|
+
const projectDb = await app.openInlineRunProjectDb(workdir);
|
|
170
|
+
let exitCode = 0;
|
|
171
|
+
try {
|
|
172
|
+
const result = await app.createOrAttachInlineRun({
|
|
173
|
+
workdir,
|
|
174
|
+
getDb: async () => projectDb.adapter,
|
|
175
|
+
file,
|
|
176
|
+
runId,
|
|
177
|
+
});
|
|
178
|
+
writeOutcome(runId, result);
|
|
179
|
+
if (!result.ok) {
|
|
180
|
+
console.error(`inline-run-setup: FAIL for run ${runId}`);
|
|
181
|
+
console.error(` ${result.error}`);
|
|
182
|
+
exitCode = 1;
|
|
183
|
+
} else {
|
|
184
|
+
console.error(
|
|
185
|
+
`inline-run-setup: ${result.attached ? 'attached' : 'created'} run ${runId} ` +
|
|
186
|
+
`(${result.workflowName}, layer ${result.layer}, digest ${result.definitionDigest}, status ${result.status})`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
} finally {
|
|
190
|
+
projectDb.close();
|
|
191
|
+
}
|
|
192
|
+
process.exit(exitCode);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
main().catch((e: unknown) => {
|
|
196
|
+
console.error(`inline-run-setup: FAIL — ${e instanceof Error ? e.message : String(e)}`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
});
|
|
@@ -257,29 +257,124 @@ function extractRequirementIds(taskContent: string): string[] {
|
|
|
257
257
|
return [...ids];
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
// ─── Canonical AC identity resolution (task 0804 R4) ─────────────────────────
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Strip the tolerated AC-id wrappers (bracket tags, `Scenario:` prefix) until
|
|
264
|
+
* fixpoint — the shared first step of `normalizeAcTitle` and the `AC-N` alias
|
|
265
|
+
* path in `resolveAcIdentity`.
|
|
266
|
+
*/
|
|
267
|
+
function stripAcWrappers(title: string): string {
|
|
268
|
+
let out = title.trim();
|
|
269
|
+
let prev: string;
|
|
270
|
+
do {
|
|
271
|
+
prev = out;
|
|
272
|
+
out = out
|
|
273
|
+
.replace(/^\[[^\]]*\]\s*/, '')
|
|
274
|
+
.replace(/\s*\[[^\]]*\]\s*$/, '')
|
|
275
|
+
.replace(/^Scenario:\s*/i, '')
|
|
276
|
+
.trim();
|
|
277
|
+
} while (out !== prev);
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Normalize an AC identity to its canonical key, mirroring the documented
|
|
283
|
+
* matching behavior of feature-check `rowMatchesScenario` + ac-style-guide
|
|
284
|
+
* "Four accepted id forms" (exact/bare title, `Scenario:` prefix, bracket
|
|
285
|
+
* tags, `AC-N` ordinal) without importing that private matcher or adopting
|
|
286
|
+
* its permissive trailing-Gherkin fallback (0804 R4). Comparison is
|
|
287
|
+
* case/quote/whitespace-insensitive. A paraphrase normalizes differently
|
|
288
|
+
* and still fails.
|
|
289
|
+
*/
|
|
290
|
+
function normalizeAcTitle(title: string): string {
|
|
291
|
+
return stripAcWrappers(title)
|
|
292
|
+
.replace(/^R\d+\s*[:\-—]?\s*/, '')
|
|
293
|
+
.toLowerCase()
|
|
294
|
+
.replace(/[ʼ‘’“”]/g, '')
|
|
295
|
+
.replace(/\s+/g, ' ')
|
|
296
|
+
.trim();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Declared AC identities keyed by canonical normalized title, plus the two
|
|
301
|
+
* AC-N ordinal sources (task scenario list and linked-feature scenario list).
|
|
302
|
+
* A checklist-declared spelling always wins over the positional alias.
|
|
303
|
+
*/
|
|
304
|
+
interface AcIdentityIndex {
|
|
305
|
+
/** normalized canonical title → a declared spelling (label, token, or title). */
|
|
306
|
+
readonly byTitle: Map<string, string>;
|
|
307
|
+
/** AC-N → task scenario title at that 1-based ordinal. */
|
|
308
|
+
readonly taskScenarios: string[];
|
|
309
|
+
/** AC-N → feature scenario title at that 1-based ordinal. */
|
|
310
|
+
readonly featureScenarios: string[];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function buildAcIdentityIndex(taskContent: string, featureContent: string | null): AcIdentityIndex {
|
|
314
|
+
const byTitle = new Map<string, string>();
|
|
315
|
+
// Not named `declare`: Bun's TS transpiler treats a call to an identifier
|
|
316
|
+
// named `declare` as an ambient-declaration modifier and drops it.
|
|
317
|
+
const declareIdentity = (spelling: string): void => {
|
|
318
|
+
const key = normalizeAcTitle(spelling);
|
|
319
|
+
if (key !== '' && !byTitle.has(key)) byTitle.set(key, spelling);
|
|
320
|
+
};
|
|
262
321
|
const section = sectionBetween(taskContent, 'Acceptance Criteria');
|
|
263
|
-
// Checkbox labels (`- [x] AC1 (R1): …`, 0726) and plain bullets (`- AC1: Given …`,
|
|
264
|
-
// 0713/0727) both yield the label text up to `:` plus its leading token.
|
|
265
322
|
for (const m of section.matchAll(/^[-*]\s+(?:\[[ xX]\]\s+)?(.+?)\s*(?::|$)/gm)) {
|
|
266
323
|
const label = (m[1] ?? '').trim();
|
|
267
324
|
if (!label) continue;
|
|
268
|
-
|
|
325
|
+
declareIdentity(label);
|
|
269
326
|
const leading = label.split(/\s+/)[0] ?? '';
|
|
270
|
-
if (leading && leading !== label)
|
|
327
|
+
if (leading && leading !== label) declareIdentity(leading);
|
|
271
328
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
329
|
+
const scenarioTitles = (content: string): string[] =>
|
|
330
|
+
[...content.matchAll(/^[ \t]*Scenario:\s*(.+)\s*$/gm)].map((m) => (m[1] ?? '').trim()).filter((t) => t !== '');
|
|
331
|
+
const taskScenarios = scenarioTitles(sectionBetween(taskContent, 'Acceptance Criteria'));
|
|
332
|
+
const featureScenarios = featureContent !== null ? scenarioTitles(featureContent) : [];
|
|
333
|
+
for (const title of [...taskScenarios, ...featureScenarios]) declareIdentity(title);
|
|
334
|
+
return { byTitle, taskScenarios, featureScenarios };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Resolution outcome for one AC row id. */
|
|
338
|
+
type AcIdentityResolution = { ok: true; canonical: string } | { ok: false; error: string };
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Resolve an answer-file AC row id to one canonical task identity (0804 R4):
|
|
342
|
+
* exact/declared title forms first, then the documented `AC-N` positional
|
|
343
|
+
* alias — accepted only against a real scenario ordinal, and refused with an
|
|
344
|
+
* actionable diagnostic when task and feature ordinals disagree. Undeclared
|
|
345
|
+
* `ACn` tokens, paraphrases and invented ordinals never resolve.
|
|
346
|
+
*/
|
|
347
|
+
function resolveAcIdentity(rowId: string, index: AcIdentityIndex): AcIdentityResolution {
|
|
348
|
+
const canonical = index.byTitle.get(normalizeAcTitle(rowId));
|
|
349
|
+
if (canonical !== undefined) return { ok: true, canonical };
|
|
350
|
+
// Strip the tolerated wrappers, then try the documented `AC-N` alias.
|
|
351
|
+
const stripped = stripAcWrappers(rowId);
|
|
352
|
+
const ordinal = /^AC-(\d+)$/i.exec(stripped);
|
|
353
|
+
if (ordinal !== null) {
|
|
354
|
+
const n = Number(ordinal[1]);
|
|
355
|
+
const taskTitle = index.taskScenarios[n - 1];
|
|
356
|
+
const featureTitle = index.featureScenarios[n - 1];
|
|
357
|
+
const candidates = [...new Set([taskTitle, featureTitle].filter((t): t is string => t !== undefined))];
|
|
358
|
+
if (candidates.length === 0) {
|
|
359
|
+
return {
|
|
360
|
+
ok: false,
|
|
361
|
+
error:
|
|
362
|
+
`AC id "${rowId}" uses the AC-${n} positional alias but no scenario exists at that ordinal ` +
|
|
363
|
+
'(task scenario list and linked-feature scenario list) — cite the exact scenario title or checklist label',
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
if (candidates.length > 1) {
|
|
367
|
+
return {
|
|
368
|
+
ok: false,
|
|
369
|
+
error:
|
|
370
|
+
`AC id "${rowId}" is ambiguous: task AC #${n} ("${taskTitle}") and feature scenario #${n} ` +
|
|
371
|
+
`("${featureTitle}") are different scenarios with different ordering — cite the exact title`,
|
|
372
|
+
};
|
|
280
373
|
}
|
|
374
|
+
const resolved = index.byTitle.get(normalizeAcTitle(candidates[0] ?? ''));
|
|
375
|
+
if (resolved !== undefined) return { ok: true, canonical: resolved };
|
|
281
376
|
}
|
|
282
|
-
return
|
|
377
|
+
return { ok: false, error: '' };
|
|
283
378
|
}
|
|
284
379
|
|
|
285
380
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
@@ -341,7 +436,7 @@ function main(): void {
|
|
|
341
436
|
}
|
|
342
437
|
|
|
343
438
|
const reqIds = extractRequirementIds(taskContent);
|
|
344
|
-
const
|
|
439
|
+
const acIndex = buildAcIdentityIndex(taskContent, featureContent);
|
|
345
440
|
|
|
346
441
|
// Requirement rows: completeness, no unknowns, no duplicates, valid status, non-empty evidence.
|
|
347
442
|
const seenReq = new Set<string>();
|
|
@@ -358,19 +453,31 @@ function main(): void {
|
|
|
358
453
|
if (!seenReq.has(id)) add(`missing requirement row for "${id}"`);
|
|
359
454
|
}
|
|
360
455
|
|
|
361
|
-
// AC rows: identity must
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
|
|
456
|
+
// AC rows: identity must resolve to ONE canonical task AC identity — a
|
|
457
|
+
// checklist label/token or a scenario title in any ac-style-guide form
|
|
458
|
+
// (exact/bare title, `Scenario:` prefix, bracket tags, declared AC-N
|
|
459
|
+
// alias; 0804 R4). Alias-equivalent spellings of the same identity are
|
|
460
|
+
// duplicates even when the raw strings differ. Status and evidence type
|
|
461
|
+
// must normalize; evidence non-empty. AC completeness is the verifier's
|
|
462
|
+
// authoring contract, not a lint rejection class (0726 R3).
|
|
463
|
+
const seenAc = new Map<string, string>(); // canonical key → first raw row id
|
|
365
464
|
for (const row of tables.acs) {
|
|
366
|
-
|
|
465
|
+
const resolution = resolveAcIdentity(row.id, acIndex);
|
|
466
|
+
const canonicalKey = resolution.ok ? normalizeAcTitle(resolution.canonical) : null;
|
|
467
|
+
if (!resolution.ok) {
|
|
468
|
+
if (resolution.error !== '') add(`line ${row.line}: ${resolution.error}`);
|
|
469
|
+
else
|
|
470
|
+
add(
|
|
471
|
+
`line ${row.line}: AC ID "${row.id.slice(0, 60)}" matches no task AC checklist label or scenario title ` +
|
|
472
|
+
'(accepted forms: exact title, bare title, `Scenario:` prefix, bracket tags, declared AC-N alias)',
|
|
473
|
+
);
|
|
474
|
+
} else if (canonicalKey !== null && seenAc.has(canonicalKey)) {
|
|
475
|
+
const first = seenAc.get(canonicalKey) ?? '';
|
|
367
476
|
add(
|
|
368
|
-
`line ${row.line}: AC
|
|
477
|
+
`line ${row.line}: duplicate AC row "${row.id.slice(0, 60)}" — alias-equivalent to "${first.slice(0, 60)}"`,
|
|
369
478
|
);
|
|
370
|
-
} else if (seenAc.has(row.id)) {
|
|
371
|
-
add(`line ${row.line}: duplicate AC row "${row.id.slice(0, 60)}"`);
|
|
372
479
|
}
|
|
373
|
-
seenAc.
|
|
480
|
+
if (canonicalKey !== null && !seenAc.has(canonicalKey)) seenAc.set(canonicalKey, row.id);
|
|
374
481
|
if (normalizeAcStatus(row.status) === null)
|
|
375
482
|
add(`line ${row.line}: invalid AC status "${row.status}" (MET | PARTIAL | UNMET | N/A)`);
|
|
376
483
|
if (normalizeEvidenceType(row.evidenceType) === null)
|
|
@@ -396,4 +503,5 @@ function main(): void {
|
|
|
396
503
|
process.exit(0);
|
|
397
504
|
}
|
|
398
505
|
|
|
399
|
-
|
|
506
|
+
// CLI entry (guarded so the helpers stay importable for focused tests).
|
|
507
|
+
if (import.meta.main) main();
|
|
@@ -117,8 +117,8 @@ assign a per-requirement status:
|
|
|
117
117
|
| **PARTIAL** | Evidence for part of the requirement only |
|
|
118
118
|
| **UNMET** | No implementation evidence found |
|
|
119
119
|
|
|
120
|
-
Record the evidence string (repo-relative
|
|
121
|
-
in `## Testing`.
|
|
120
|
+
Record the evidence string (repo-relative `file:line`, command, or test name) per requirement —
|
|
121
|
+
this lands in `## Testing`.
|
|
122
122
|
|
|
123
123
|
**Line-anchor verification (anti-stale-citation rule).** Every `file:line` evidence citation
|
|
124
124
|
written into the Testing table MUST be re-read at the cited lines this run, and the re-read content
|
|
@@ -142,6 +142,8 @@ classifies it as external and never raises `L4.stale-line-anchor` for it (R1). D
|
|
|
142
142
|
that lives in this repo — in-repo evidence MUST use the repo-relative backtick form
|
|
143
143
|
`` `path:line` `` / `` `path:start-end` ``, and citing it in the external form still reports (R2).
|
|
144
144
|
|
|
145
|
+
**Concrete anchors (0804 R9):** cite existing `file:line`s, never globs — `references/verdict-schema.md`.
|
|
146
|
+
|
|
145
147
|
### Step 5 — Acceptance Criteria guard
|
|
146
148
|
|
|
147
149
|
If the task has a non-empty Acceptance Criteria section, evaluate every checklist item and every
|