@gobing-ai/spur 0.3.85 → 0.3.87
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/pipeline-budgets.json +0 -25
- package/config/plugin-scripts.json +4 -5
- package/config/rules/boundary/env-var-hygiene.yaml +41 -0
- package/config/templates/feature/default.md +2 -0
- package/config/templates/task/brainstorm.md +2 -2
- package/config/templates/task/feature-impl.md +2 -2
- package/config/templates/task/issue.md +2 -2
- package/config/templates/task/meta.md +2 -2
- package/config/templates/task/review.md +2 -2
- package/config/templates/task/standard.md +2 -2
- package/config/transition-shims.json +1 -1
- package/config/workflow-candidates.json +19 -0
- package/config/workflows/feature-lifecycle.yaml +10 -2
- package/config/workflows/feature-verification.yaml +71 -0
- package/config/workflows/idea-pipeline.yaml +87 -39
- package/config/workflows/task-pipeline.yaml +43 -12
- package/config/workflows/wrapup-pipeline.yaml +70 -7
- package/package.json +9 -9
- package/plugins/sp/README.md +7 -7
- package/plugins/sp/commands/dev-idea.md +9 -2
- package/plugins/sp/commands/dev-refactor.md +33 -0
- package/plugins/sp/hooks/agent-hint.ts +5 -4
- package/plugins/sp/hooks/careful-guard.ts +3 -1
- package/plugins/sp/hooks/context-post-tool.ts +2 -1
- package/plugins/sp/hooks/context-session-start.ts +4 -3
- package/plugins/sp/hooks/context-session-stop.ts +2 -1
- package/plugins/sp/hooks/pi/guard-extension.ts +4 -3
- package/plugins/sp/hooks/task-write-guard.ts +4 -3
- package/plugins/sp/lib/idea-handoff.generated.mjs +260 -260
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +13 -4
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +6 -4
- package/plugins/sp/scripts/feature-sync-bounded.mjs +10 -2
- package/plugins/sp/scripts/feature-sync-bounded.ts +2 -1
- package/plugins/sp/scripts/idea-coverage-check.ts +168 -0
- package/plugins/sp/scripts/idea-handoff.mjs +6 -1
- package/plugins/sp/scripts/idea-handoff.ts +4 -1
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +115 -4
- package/plugins/sp/scripts/inline-run-setup.ts +195 -2
- package/plugins/sp/scripts/pr-reviewing.mjs +8 -1
- package/plugins/sp/scripts/pr-reviewing.ts +2 -1
- package/plugins/sp/scripts/quality-gate.mjs +8 -1
- package/plugins/sp/scripts/quality-gate.ts +2 -1
- package/plugins/sp/scripts/surface-drift-inventory.ts +1 -4
- package/plugins/sp/scripts/task-evidence-precheck.ts +2 -1
- package/plugins/sp/scripts/task-size-precheck.ts +6 -4
- package/plugins/sp/scripts/verify-answer-lint.ts +2 -1
- package/plugins/sp/scripts/workflow-step-profile.mjs +10 -2
- package/plugins/sp/scripts/workflow-step-profile.ts +2 -1
- package/plugins/sp/scripts/wrapup-steps.mjs +8 -1
- package/plugins/sp/scripts/wrapup-steps.ts +2 -1
- package/plugins/sp/skills/brainstorm/SKILL.md +4 -0
- package/plugins/sp/skills/code-refactoring/SKILL.md +155 -0
- package/plugins/sp/skills/code-refactoring/references/finding-schema.md +74 -0
- package/plugins/sp/skills/code-refactoring/references/fix-ladder.md +52 -0
- package/plugins/sp/skills/code-refactoring/references/focus-detection.md +44 -0
- package/plugins/sp/skills/code-refactoring/references/refactor-finding.schema.json +95 -0
- package/plugins/sp/skills/spec-decomposition/references/decomposition.md +23 -16
- package/plugins/sp/skills/spur-cli/references/features/acceptance-criteria.md +4 -2
- package/plugins/sp/skills/spur-cli/references/features.md +6 -1
- package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +2 -2
- package/plugins/sp/skills/spur-cli/references/workflows/operations.md +3 -3
- package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +1 -1
- package/plugins/sp/skills/spur-cli/references/workflows.md +8 -7
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +24 -0
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +4 -5
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +18 -2
- package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +1 -1
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +22 -3
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +2 -2
- package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +11 -1
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +91 -1
- package/plugins/sp/skills/taste-refactoring-api/SKILL.md +44 -1
- package/plugins/sp/skills/taste-refactoring-api/references/protocol-modes.md +31 -0
- package/plugins/sp/skills/taste-refactoring-architect/SKILL.md +43 -0
- package/plugins/sp/skills/taste-refactoring-tests/SKILL.md +42 -0
- package/plugins/sp/skills/taste-refactoring-ui/SKILL.md +43 -0
- package/schemas/state-machine-workflow.schema.json +5 -0
- package/schemas/task-batch.schema.json +2 -2
- package/schemas/transition-flow-workflow.schema.json +5 -0
- package/spur.js +21106 -20669
- package/web/_astro/BoardApp.CJiqp5pS.js +1 -0
- package/web/_astro/{BoardApp.yW425dRZ.js → BoardApp.yBBcFXWP.js} +4 -4
- package/web/_astro/{TaskDetail.CGwJAinW.js → TaskDetail.DqFJbRFc.js} +1 -1
- package/web/_astro/{arc.B-qNXzSO.js → arc.DL-BpHoi.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.Bdg-xiji.js → architectureDiagram-3BPJPVTR.9IdQYyDq.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.DFArg3Kp.js → blockDiagram-GPEHLZMM.BKsFCqTl.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.BkOLcjyx.js → c4Diagram-AAUBKEIU.DhwI0dh1.js} +1 -1
- package/web/_astro/channel.CX5453qQ.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.DN3c20V4.js → chunk-2J33WTMH.B3QVmQ9S.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.B64K0Ttd.js → chunk-4BX2VUAB.DBSuqs9F.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.C-AJvNwa.js → chunk-55IACEB6.BSYTWAYD.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.DMkr46uS.js → chunk-727SXJPM.cWVuxXfS.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.4_WGbI64.js → chunk-AQP2D5EJ.DpU_Ob3d.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.cbFW_lVE.js → chunk-FMBD7UC4.BykFkyji.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.CDW_-2O1.js → chunk-ND2GUHAM.DwgHlMdY.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.BoYOt_yg.js → chunk-QZHKN3VN.CusXUGWM.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.BNT6XpHQ.js → classDiagram-4FO5ZUOK.fx0ObzkN.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.BNT6XpHQ.js → classDiagram-v2-Q7XG4LA2.fx0ObzkN.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.CI1DQxR3.js → cose-bilkent-S5V4N54A.Z4HgOlsd.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.A8ARH61p.js → cynefin-OW5HDTMX.B5dIZHJu.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.u28ZmqwR.js → dagre-BM42HDAG.DT70Q_Yw.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.Dx8JcrGc.js → diagram-2AECGRRQ.DqHA3XBF.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.BXJUG9GY.js → diagram-5GNKFQAL.BmCem957.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.BtlMuWqW.js → diagram-KO2AKTUF.sn0-hrE0.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.C4T7qaAy.js → diagram-LMA3HP47.BSHe9tVc.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.D5HECE10.js → diagram-OG6HWLK6.DHIc-86k.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.B_hMR3yw.js → erDiagram-TEJ5UH35.Bxayrs7v.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.BAmpcYl3.js → flowDiagram-I6XJVG4X.BkzoE_5I.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.DV1bSWK-.js → ganttDiagram-6RSMTGT7.okT6CvTo.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.DxYKkKrJ.js → gitGraphDiagram-PVQCEYII.CJuYbhC7.js} +1 -1
- package/web/_astro/{infoDiagram-5YYISTIA.BAWY2xMb.js → infoDiagram-5YYISTIA.RqLy7nBo.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.CZssRn6V.js → ishikawaDiagram-YF4QCWOH.BwIcoagw.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.C--muARd.js → journeyDiagram-JHISSGLW.UB1VbWtH.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.D_QCK4et.js → kanban-definition-UN3LZRKU.AaxMKpTk.js} +1 -1
- package/web/_astro/{linear.DzrmTtZ0.js → linear.Nv_xOUjP.js} +1 -1
- package/web/_astro/{mermaid.core.Da03W3iu.js → mermaid.core.Bc4LqQgX.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.6cy-8hR_.js → mindmap-definition-RKZ34NQL.oKUvU_qi.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.C5rS1pdU.js → pieDiagram-4H26LBE5.DQk0oo03.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.w56GZZ6Q.js → quadrantDiagram-W4KKPZXB.BdDjESDa.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.CipX3Pwu.js → requirementDiagram-4Y6WPE33.C2u9hUeH.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.C0VVzgJm.js → sankeyDiagram-5OEKKPKP.CDEoiJST.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.BX2dUUbF.js → sequenceDiagram-3UESZ5HK.D_hT_GAT.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.ypCdgODQ.js → stateDiagram-AJRCARHV.DI8RYG0b.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.In0baEtg.js → stateDiagram-v2-BHNVJYJU.Bkxz4DnP.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.CJN4Vkvl.js → timeline-definition-PNZ67QCA.DSY-kH3-.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.Cf8KkIPY.js → vennDiagram-CIIHVFJN.CpaDtuGr.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.DCZBo9xw.js → wardleyDiagram-YWT4CUSO.DujQWvo8.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.Cm4v_MiJ.js → xychartDiagram-2RQKCTM6.DcM5Y4b9.js} +1 -1
- package/web/index.html +1 -1
- package/config/workflows/basic.yaml +0 -146
- package/config/workflows/docs-pipeline.yaml +0 -350
- package/config/workflows/feature-dev.yaml +0 -288
- package/plugins/sp/scripts/feature-dev-precheck.mjs +0 -146
- package/plugins/sp/scripts/feature-dev-precheck.ts +0 -238
- package/web/_astro/BoardApp.CHenHFia.js +0 -1
- package/web/_astro/channel.CbDHK5UQ.js +0 -1
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.87",
|
|
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"]
|
|
@@ -58,7 +58,7 @@ roles:
|
|
|
58
58
|
stages: [implement, test, wrap]
|
|
59
59
|
- id: reviewer
|
|
60
60
|
tier: capable-1
|
|
61
|
-
commands: [dev-verify, dev-verifyall, dev-review, dev-review-session, dev-pr-review, dev-dogfood, rule-scan, dev-find-conflict, dev-find-issue]
|
|
61
|
+
commands: [dev-verify, dev-verifyall, dev-review, dev-review-session, dev-pr-review, dev-dogfood, dev-refactor, rule-scan, dev-find-conflict, dev-find-issue]
|
|
62
62
|
stages: [verify, review, dogfood]
|
|
63
63
|
- id: planner
|
|
64
64
|
tier: capable-2
|
|
@@ -8,6 +8,15 @@ import { spawn, spawnSync } from "child_process";
|
|
|
8
8
|
import { existsSync, mkdirSync, readlinkSync, writeFileSync } from "fs";
|
|
9
9
|
import { dirname, isAbsolute, join, resolve } from "path";
|
|
10
10
|
|
|
11
|
+
// ../ts-libs/packages/utils/dist/env.js
|
|
12
|
+
function getEnvVar(name, fallback) {
|
|
13
|
+
const raw = process.env[name];
|
|
14
|
+
return raw === undefined ? fallback : raw;
|
|
15
|
+
}
|
|
16
|
+
function getEnvVars() {
|
|
17
|
+
return process.env;
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
// plugins/sp/scripts/daily-summary/logger.ts
|
|
12
21
|
var state = { console: true, file: false };
|
|
13
22
|
function emit(stream, message) {
|
|
@@ -103,7 +112,7 @@ var defaultProcessSpawner = (cmd, args, env) => {
|
|
|
103
112
|
try {
|
|
104
113
|
const proc = spawn(cmd, args, {
|
|
105
114
|
stdio: ["ignore", "pipe", "pipe"],
|
|
106
|
-
env: env ??
|
|
115
|
+
env: env ?? getEnvVars()
|
|
107
116
|
});
|
|
108
117
|
const stdoutChunks = [];
|
|
109
118
|
const stderrChunks = [];
|
|
@@ -130,7 +139,7 @@ function setProcessSpawner(next) {
|
|
|
130
139
|
}
|
|
131
140
|
async function getCcusageData(date) {
|
|
132
141
|
try {
|
|
133
|
-
const env = { ...
|
|
142
|
+
const env = { ...getEnvVars() };
|
|
134
143
|
const ccusageCheck = await processSpawner("ccusage", ["--version"], env);
|
|
135
144
|
if (ccusageCheck.exitCode !== 0) {
|
|
136
145
|
return null;
|
|
@@ -279,10 +288,10 @@ async function getGitCommits(date) {
|
|
|
279
288
|
}
|
|
280
289
|
}
|
|
281
290
|
async function promptUser() {
|
|
282
|
-
if (
|
|
291
|
+
if (getEnvVar("SP_DAILY_SUMMARY_NO_PROMPT") === "1") {
|
|
283
292
|
return { learnings: "", issuesFixed: "", pending: "" };
|
|
284
293
|
}
|
|
285
|
-
if (
|
|
294
|
+
if (getEnvVar("RD3_DAILY_SUMMARY_NO_PROMPT") === "1") {
|
|
286
295
|
logger.warn("[deprecate] RD3_DAILY_SUMMARY_NO_PROMPT is deprecated; use SP_DAILY_SUMMARY_NO_PROMPT");
|
|
287
296
|
return { learnings: "", issuesFixed: "", pending: "" };
|
|
288
297
|
}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import { spawn, spawnSync } from 'node:child_process';
|
|
12
12
|
import { existsSync, mkdirSync, readlinkSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
14
|
+
import { getEnvVar, getEnvVars } from '@gobing-ai/ts-utils';
|
|
15
|
+
|
|
14
16
|
import { logger } from './logger';
|
|
15
17
|
|
|
16
18
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
@@ -225,7 +227,7 @@ export const defaultProcessSpawner: ProcessSpawner = (cmd, args, env) => {
|
|
|
225
227
|
try {
|
|
226
228
|
const proc = spawn(cmd, args, {
|
|
227
229
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
228
|
-
env: env ??
|
|
230
|
+
env: env ?? getEnvVars(),
|
|
229
231
|
});
|
|
230
232
|
const stdoutChunks: Buffer[] = [];
|
|
231
233
|
const stderrChunks: Buffer[] = [];
|
|
@@ -258,7 +260,7 @@ export function setProcessSpawner(next?: ProcessSpawner): void {
|
|
|
258
260
|
export async function getCcusageData(date: string): Promise<CcusageData | null> {
|
|
259
261
|
try {
|
|
260
262
|
// Check if ccusage is available
|
|
261
|
-
const env = { ...
|
|
263
|
+
const env = { ...getEnvVars() };
|
|
262
264
|
const ccusageCheck = await processSpawner('ccusage', ['--version'], env);
|
|
263
265
|
if (ccusageCheck.exitCode !== 0) {
|
|
264
266
|
return null;
|
|
@@ -473,10 +475,10 @@ export async function getGitCommits(date: string): Promise<GitCommit[]> {
|
|
|
473
475
|
// ─── User Input ─────────────────────────────────────────────────────────────
|
|
474
476
|
|
|
475
477
|
export async function promptUser(): Promise<UserAnnotations> {
|
|
476
|
-
if (
|
|
478
|
+
if (getEnvVar('SP_DAILY_SUMMARY_NO_PROMPT') === '1') {
|
|
477
479
|
return { learnings: '', issuesFixed: '', pending: '' };
|
|
478
480
|
}
|
|
479
|
-
if (
|
|
481
|
+
if (getEnvVar('RD3_DAILY_SUMMARY_NO_PROMPT') === '1') {
|
|
480
482
|
logger.warn('[deprecate] RD3_DAILY_SUMMARY_NO_PROMPT is deprecated; use SP_DAILY_SUMMARY_NO_PROMPT');
|
|
481
483
|
return { learnings: '', issuesFixed: '', pending: '' };
|
|
482
484
|
}
|
|
@@ -8,6 +8,14 @@ import { createHash } from "node:crypto";
|
|
|
8
8
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { dirname } from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
// ../ts-libs/packages/utils/dist/env.js
|
|
13
|
+
function getEnvVar(name, fallback) {
|
|
14
|
+
const raw = process.env[name];
|
|
15
|
+
return raw === undefined ? fallback : raw;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// plugins/sp/scripts/feature-sync-bounded.ts
|
|
11
19
|
function classifySyncResult(result) {
|
|
12
20
|
if (result.proposal.gateBlocked === true)
|
|
13
21
|
return "blocked";
|
|
@@ -95,8 +103,8 @@ through unchanged.
|
|
|
95
103
|
|
|
96
104
|
Exit: 0 = sync handled (applied / no-op / suppressed-blocked / live-blocked).`;
|
|
97
105
|
function defaultSpurBin() {
|
|
98
|
-
if (
|
|
99
|
-
return
|
|
106
|
+
if (getEnvVar("SPUR_BIN"))
|
|
107
|
+
return getEnvVar("SPUR_BIN");
|
|
100
108
|
const local = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
|
|
101
109
|
if (existsSync(local))
|
|
102
110
|
return `bun ${local}`;
|
|
@@ -24,6 +24,7 @@ import { createHash } from 'node:crypto';
|
|
|
24
24
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
25
25
|
import { dirname } from 'node:path';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { getEnvVar } from '@gobing-ai/ts-utils';
|
|
27
28
|
|
|
28
29
|
// ── Local types (match packages/app FeatureService shapes; no package import) ───────────
|
|
29
30
|
|
|
@@ -250,7 +251,7 @@ Exit: 0 = sync handled (applied / no-op / suppressed-blocked / live-blocked).`;
|
|
|
250
251
|
* keeps ad-hoc invocations from silently hitting a stale PATH install.
|
|
251
252
|
*/
|
|
252
253
|
export function defaultSpurBin(): string {
|
|
253
|
-
if (
|
|
254
|
+
if (getEnvVar('SPUR_BIN')) return getEnvVar('SPUR_BIN');
|
|
254
255
|
// scripts/ -> plugins/sp/ -> <repo>/apps/cli/src/index.ts (fileURLToPath — raw pathname breaks
|
|
255
256
|
// on %-encoded paths, e.g. spaces in the checkout directory)
|
|
256
257
|
const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* idea-coverage-check — requirement-inventory ↔ AC coverage gate for the idea pipeline
|
|
4
|
+
* (task 0887 R4).
|
|
5
|
+
*
|
|
6
|
+
* Cross-checks the `## Requirement inventory` section of the idea-evaluation report
|
|
7
|
+
* (R3) against the `# covers: I<n>, ...` comment lines of the generated acceptance
|
|
8
|
+
* criteria (R4): every inventory item that is not explicitly `[deferred: ...]` must be
|
|
9
|
+
* covered by at least one scenario. An `[unclear: ...]` marker does not exempt an item —
|
|
10
|
+
* it still demands coverage or an explicit deferral.
|
|
11
|
+
*
|
|
12
|
+
* Writes PASS/FAIL to `.spur/run/<run-id>-idea-coverage.status` and always exits 0
|
|
13
|
+
* (soft action, task 0769 pattern): the ac-generate guards in idea-pipeline.yaml read
|
|
14
|
+
* the status file, so a missing or failing checker fails closed through the guard, not
|
|
15
|
+
* through the exit code.
|
|
16
|
+
*
|
|
17
|
+
* Ships with the plugin to arbitrary projects, so it stays node-builtin-only —
|
|
18
|
+
* no workspace imports.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* bun plugins/sp/scripts/idea-coverage-check.ts --run-id <id> --report <path>
|
|
22
|
+
* --ac <path> [--out <path>]
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
26
|
+
import { dirname, join } from 'node:path';
|
|
27
|
+
|
|
28
|
+
interface ParsedArgs {
|
|
29
|
+
runId: string;
|
|
30
|
+
reportPath: string;
|
|
31
|
+
acPath: string;
|
|
32
|
+
outPath: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** `- **I3** — ask text` / `- I3. ask text` — the R3 inventory item form. */
|
|
36
|
+
const INVENTORY_ITEM_RE = /^\s*[-*]\s+\**I(\d+)\**\s*[.:—-]?\s+(.*)$/;
|
|
37
|
+
|
|
38
|
+
/** `# covers: I1, I3` — the R4 scenario coverage comment. */
|
|
39
|
+
const COVERS_RE = /^\s*#\s*covers:\s*(.+)$/i;
|
|
40
|
+
|
|
41
|
+
/** Scenario header — the anchor a covers-comment attaches to. */
|
|
42
|
+
const SCENARIO_RE = /^\s*Scenario(?:\s+Outline)?:/;
|
|
43
|
+
|
|
44
|
+
function parseArgs(argv: string[]): ParsedArgs {
|
|
45
|
+
let runId = '';
|
|
46
|
+
let reportPath = '';
|
|
47
|
+
let acPath = '';
|
|
48
|
+
let outPath = '';
|
|
49
|
+
for (let i = 0; i < argv.length; i++) {
|
|
50
|
+
const value = argv[i + 1];
|
|
51
|
+
switch (argv[i]) {
|
|
52
|
+
case '--run-id':
|
|
53
|
+
runId = value ?? '';
|
|
54
|
+
i++;
|
|
55
|
+
break;
|
|
56
|
+
case '--report':
|
|
57
|
+
reportPath = value ?? '';
|
|
58
|
+
i++;
|
|
59
|
+
break;
|
|
60
|
+
case '--ac':
|
|
61
|
+
acPath = value ?? '';
|
|
62
|
+
i++;
|
|
63
|
+
break;
|
|
64
|
+
case '--out':
|
|
65
|
+
outPath = value ?? '';
|
|
66
|
+
i++;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (runId === '' || reportPath === '' || acPath === '') {
|
|
71
|
+
process.stderr.write(
|
|
72
|
+
'usage: idea-coverage-check.ts --run-id <id> --report <path> --ac <path> [--out <path>]\n',
|
|
73
|
+
);
|
|
74
|
+
process.exit(2);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
runId,
|
|
78
|
+
reportPath,
|
|
79
|
+
acPath,
|
|
80
|
+
outPath: outPath !== '' ? outPath : join('.spur', 'run', `${runId}-idea-coverage.status`),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Inventory ids from the report's `## Requirement inventory` section, split into
|
|
86
|
+
* covered-owing (no `[deferred:` marker) and exempt (deferred) ids. An `[unclear:`
|
|
87
|
+
* marker is informational — the item still owes coverage.
|
|
88
|
+
*/
|
|
89
|
+
function parseInventory(report: string): { owing: Set<string>; deferred: Set<string>; hasSection: boolean } {
|
|
90
|
+
const lines = report.split('\n');
|
|
91
|
+
const start = lines.findIndex((line) => /^#{1,6}\s*Requirement inventory\s*$/i.test(line));
|
|
92
|
+
if (start === -1) return { owing: new Set(), deferred: new Set(), hasSection: false };
|
|
93
|
+
const owing = new Set<string>();
|
|
94
|
+
const deferred = new Set<string>();
|
|
95
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
96
|
+
if (/^#{1,6}\s/.test(lines[i] ?? '')) break;
|
|
97
|
+
const match = INVENTORY_ITEM_RE.exec(lines[i] ?? '');
|
|
98
|
+
if (match === null) continue;
|
|
99
|
+
const id = `I${match[1]}`;
|
|
100
|
+
if (/\[\s*deferred\s*:/i.test(match[2] ?? '')) deferred.add(id);
|
|
101
|
+
else owing.add(id);
|
|
102
|
+
}
|
|
103
|
+
return { owing, deferred, hasSection: true };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Coverage map from the AC content: scenario-count per `I<n>` id. */
|
|
107
|
+
function parseCoverage(ac: string): Map<string, number> {
|
|
108
|
+
const covered = new Map<string, number>();
|
|
109
|
+
let inScenario = false;
|
|
110
|
+
for (const line of ac.split('\n')) {
|
|
111
|
+
if (SCENARIO_RE.test(line)) {
|
|
112
|
+
inScenario = true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!inScenario) continue;
|
|
116
|
+
const match = COVERS_RE.exec(line);
|
|
117
|
+
if (match === null) continue;
|
|
118
|
+
for (const raw of (match[1] ?? '').split(',')) {
|
|
119
|
+
const id = raw.trim().toUpperCase();
|
|
120
|
+
if (/^I\d+$/.test(id)) covered.set(id, (covered.get(id) ?? 0) + 1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return covered;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function writeStatus(outPath: string, verdict: 'PASS' | 'FAIL', runId: string, detail: string): void {
|
|
127
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
128
|
+
writeFileSync(outPath, `${verdict}\n`);
|
|
129
|
+
// Sibling reason file: the feature-check HITL prompt surfaces WHY next to the bare
|
|
130
|
+
// status letter, without guards having to parse a multi-line status file.
|
|
131
|
+
writeFileSync(`${outPath}.reason`, `${verdict} run=${runId} ${detail}\n`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function main(): void {
|
|
135
|
+
const args = parseArgs(process.argv.slice(2));
|
|
136
|
+
|
|
137
|
+
if (!existsSync(args.reportPath) || !existsSync(args.acPath)) {
|
|
138
|
+
const detail = `missing input (report=${existsSync(args.reportPath) ? 'ok' : 'absent'}, ac=${existsSync(args.acPath) ? 'ok' : 'absent'})`;
|
|
139
|
+
writeStatus(args.outPath, 'FAIL', args.runId, detail);
|
|
140
|
+
process.stdout.write(`idea-coverage-check FAIL run=${args.runId} ${detail}\n`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const inventory = parseInventory(readFileSync(args.reportPath, 'utf8'));
|
|
145
|
+
const covered = parseCoverage(readFileSync(args.acPath, 'utf8'));
|
|
146
|
+
|
|
147
|
+
if (!inventory.hasSection || inventory.owing.size + inventory.deferred.size === 0) {
|
|
148
|
+
const detail = `no Requirement inventory items in ${args.reportPath}`;
|
|
149
|
+
writeStatus(args.outPath, 'FAIL', args.runId, detail);
|
|
150
|
+
process.stdout.write(`idea-coverage-check FAIL run=${args.runId} ${detail}\n`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const uncovered = [...inventory.owing].filter((id) => (covered.get(id) ?? 0) === 0).sort();
|
|
155
|
+
const shape = `inventory=${inventory.owing.size + inventory.deferred.size} (deferred=${inventory.deferred.size})`;
|
|
156
|
+
if (uncovered.length > 0) {
|
|
157
|
+
const detail = `${shape} covered=${inventory.owing.size - uncovered.length} uncovered=${uncovered.join(',')}`;
|
|
158
|
+
writeStatus(args.outPath, 'FAIL', args.runId, detail);
|
|
159
|
+
process.stdout.write(`idea-coverage-check FAIL run=${args.runId} ${detail}\n`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const detail = `${shape} all covered`;
|
|
164
|
+
writeStatus(args.outPath, 'PASS', args.runId, detail);
|
|
165
|
+
process.stdout.write(`idea-coverage-check PASS run=${args.runId} ${detail}\n`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
main();
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
|
+
// ../ts-libs/packages/utils/dist/env.js
|
|
5
|
+
function getEnvVars() {
|
|
6
|
+
return process.env;
|
|
7
|
+
}
|
|
8
|
+
|
|
4
9
|
// plugins/sp/scripts/idea-handoff.ts
|
|
5
10
|
var libUrl = new URL("../lib/idea-handoff.generated.mjs", import.meta.url).href;
|
|
6
11
|
async function loadHandoffLib() {
|
|
7
12
|
return await import(libUrl);
|
|
8
13
|
}
|
|
9
14
|
var IDEA_HANDOFF_USAGE = "usage: idea-handoff.ts (env: __runId, featureId, optional spurBin) \u2014 no subcommands";
|
|
10
|
-
async function main(argv, env =
|
|
15
|
+
async function main(argv, env = getEnvVars()) {
|
|
11
16
|
if (argv.length > 0) {
|
|
12
17
|
process.stderr.write(`${IDEA_HANDOFF_USAGE}
|
|
13
18
|
`);
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { getEnvVars } from '@gobing-ai/ts-utils';
|
|
4
|
+
|
|
2
5
|
/**
|
|
3
6
|
* idea-handoff — portable entrypoint for the idea-pipeline finalization (task 0824, feature I21).
|
|
4
7
|
*
|
|
@@ -29,7 +32,7 @@ export async function loadHandoffLib(): Promise<typeof import('../lib/idea-hando
|
|
|
29
32
|
export const IDEA_HANDOFF_USAGE =
|
|
30
33
|
'usage: idea-handoff.ts (env: __runId, featureId, optional spurBin) — no subcommands';
|
|
31
34
|
|
|
32
|
-
export async function main(argv: string[], env: NodeJS.ProcessEnv =
|
|
35
|
+
export async function main(argv: string[], env: NodeJS.ProcessEnv = getEnvVars()): Promise<number> {
|
|
33
36
|
if (argv.length > 0) {
|
|
34
37
|
process.stderr.write(`${IDEA_HANDOFF_USAGE}\n`);
|
|
35
38
|
return 2;
|
|
@@ -11,8 +11,14 @@
|
|
|
11
11
|
* check is a symmetric set diff: an element present in one and absent in the
|
|
12
12
|
* other fails the check and names the element.
|
|
13
13
|
*
|
|
14
|
-
* The set is defined in {@link DOCUMENTED} below
|
|
15
|
-
*
|
|
14
|
+
* The set is defined in {@link DOCUMENTED} below. Task 0881 (0877 R7): the harness
|
|
15
|
+
* enumerates state from BOTH reference sets — this constant and the driver
|
|
16
|
+
* markdown's mirror list — plus the YAML union, as a three-way symmetric diff, so
|
|
17
|
+
* removing a kind from either reference cannot escape parity. It also scans task
|
|
18
|
+
* frontmatter `dependencies[]` for spurious edges: a referenced wbs with no task
|
|
19
|
+
* file (post-0875, dependency edges no longer move the planning digest —
|
|
20
|
+
* `packages/app/src/services/task-readiness.ts` — so over-declared edges would
|
|
21
|
+
* otherwise be silently unbound).
|
|
16
22
|
*
|
|
17
23
|
* Usage:
|
|
18
24
|
* bun plugins/sp/scripts/inline-pipeline-parity-check.ts
|
|
@@ -43,12 +49,97 @@ const DOCUMENTED = {
|
|
|
43
49
|
'run.artifact',
|
|
44
50
|
'command.gate',
|
|
45
51
|
]),
|
|
46
|
-
guards: new Set(['always', 'shell']),
|
|
52
|
+
guards: new Set(['always', 'shell', 'action-ok', 'contract-violation']),
|
|
47
53
|
} as const;
|
|
48
54
|
|
|
49
55
|
/** Directory of workflow definitions the driver is responsible for. */
|
|
50
56
|
const WORKFLOW_DIR = join('config', 'workflows');
|
|
51
57
|
|
|
58
|
+
/** The driver reference doc — the second reference set (0881). */
|
|
59
|
+
const DRIVER_REF = join('plugins', 'sp', 'skills', 'spur-dev', 'references', 'inline-pipeline-driver.md');
|
|
60
|
+
|
|
61
|
+
/** Task corpus dirs scanned for spurious `dependencies[]` edges (0881). */
|
|
62
|
+
const TASK_DIRS = ['docs/tasks', 'docs/tasks5'];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse the driver reference's mirrored kind lists (`**Actions:** … · `kind` …`) into
|
|
66
|
+
* reference sets. Returns null when the doc is absent (fixture roots).
|
|
67
|
+
*/
|
|
68
|
+
function collectMarkdownReferenceKinds(root: string): { actions: Set<string>; guards: Set<string> } | null {
|
|
69
|
+
let md: string;
|
|
70
|
+
try {
|
|
71
|
+
md = readFileSync(join(root, DRIVER_REF), 'utf8');
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const read = (label: RegExp): Set<string> => {
|
|
76
|
+
const line = md.split('\n').find((l) => label.test(l));
|
|
77
|
+
const out = new Set<string>();
|
|
78
|
+
if (!line) return out;
|
|
79
|
+
for (const m of line.matchAll(/`([^`]+)`/g)) out.add(m[1] ?? '');
|
|
80
|
+
out.delete('');
|
|
81
|
+
return out;
|
|
82
|
+
};
|
|
83
|
+
return { actions: read(/^\*\*Actions:/), guards: read(/^\*\*Guards/) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Scan task frontmatter `dependencies[]` for spurious edges: a referenced wbs that
|
|
88
|
+
* resolves to no task file in the corpus (0881 — post-0875 these edges no longer
|
|
89
|
+
* move the planning digest, so over-declared ones are otherwise silently unbound).
|
|
90
|
+
*/
|
|
91
|
+
function checkTaskDependencyEdges(root: string, errors: string[]): number {
|
|
92
|
+
const wbss = new Set<string>();
|
|
93
|
+
const depFiles: { path: string; deps: string[] }[] = [];
|
|
94
|
+
for (const dir of TASK_DIRS) {
|
|
95
|
+
const abs = join(root, dir);
|
|
96
|
+
let entries: string[] = [];
|
|
97
|
+
try {
|
|
98
|
+
entries = readdirSync(abs);
|
|
99
|
+
} catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
for (const e of entries) {
|
|
103
|
+
const m = /^(\d+)_.*\.md$/.exec(e);
|
|
104
|
+
if (!m?.[1]) continue;
|
|
105
|
+
wbss.add(m[1]);
|
|
106
|
+
const raw = readFileSync(join(abs, e), 'utf8');
|
|
107
|
+
const fm = /^---\n([\s\S]*?)\n---/.exec(raw);
|
|
108
|
+
if (!fm?.[1]) continue;
|
|
109
|
+
const deps: string[] = [];
|
|
110
|
+
const inline = /^dependencies:\s*\[(.*)\]/m.exec(fm[1]);
|
|
111
|
+
if (inline?.[1]) {
|
|
112
|
+
for (const part of inline[1].split(',')) {
|
|
113
|
+
const w = part.trim().replace(/["']/g, '');
|
|
114
|
+
if (w !== '') deps.push(w);
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
const block = /^dependencies:\s*$/m.exec(fm[1]);
|
|
118
|
+
if (block) {
|
|
119
|
+
for (const line of fm[1].split('\n')) {
|
|
120
|
+
const item = /^\s*-\s*["']?(\d+)["']?\s*$/.exec(line);
|
|
121
|
+
if (item?.[1]) deps.push(item[1]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (deps.length > 0) depFiles.push({ path: join(dir, e), deps });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
let count = 0;
|
|
129
|
+
for (const { path, deps } of depFiles) {
|
|
130
|
+
for (const dep of deps) {
|
|
131
|
+
// Only wbs-shaped values are machine dependency edges; prose refs in the
|
|
132
|
+
// legacy corpus are descriptive text, never consumed as edges.
|
|
133
|
+
if (!/^\d{3,4}$/.test(dep)) continue;
|
|
134
|
+
if (!wbss.has(dep)) {
|
|
135
|
+
errors.push(`spurious dependency edge: ${path} depends on ${dep} but no task with wbs ${dep} exists`);
|
|
136
|
+
count += 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return count;
|
|
141
|
+
}
|
|
142
|
+
|
|
52
143
|
/** Walk a state list and yield every `kind:` value found in `onEnter` action
|
|
53
144
|
* lists. Skips the top-level workflow `kind:` (e.g. `state-machine`). */
|
|
54
145
|
function collectActionKinds(states: unknown): Set<string> {
|
|
@@ -170,6 +261,26 @@ async function main(): Promise<number> {
|
|
|
170
261
|
errors.push(`guard kind "${x}" documented in inline-pipeline-driver.md but never used in any workflow`);
|
|
171
262
|
}
|
|
172
263
|
|
|
264
|
+
// Three-way parity with the markdown mirror (0881): a kind dropped from either
|
|
265
|
+
// reference set — constant or doc — is caught, not just YAML drift.
|
|
266
|
+
const ref = collectMarkdownReferenceKinds(root);
|
|
267
|
+
if (ref !== null) {
|
|
268
|
+
for (const x of diff(ref.actions, DOCUMENTED.actions).onlyInA)
|
|
269
|
+
errors.push(`action kind "${x}" in the driver markdown but absent from the DOCUMENTED set`);
|
|
270
|
+
for (const x of diff(ref.actions, DOCUMENTED.actions).onlyInB)
|
|
271
|
+
errors.push(`action kind "${x}" in DOCUMENTED but deleted from the driver markdown reference`);
|
|
272
|
+
for (const x of diff(ref.guards, DOCUMENTED.guards).onlyInA)
|
|
273
|
+
errors.push(`guard kind "${x}" in the driver markdown but absent from the DOCUMENTED set`);
|
|
274
|
+
for (const x of diff(ref.guards, DOCUMENTED.guards).onlyInB)
|
|
275
|
+
errors.push(`guard kind "${x}" in DOCUMENTED but deleted from the driver markdown reference`);
|
|
276
|
+
for (const x of diff(ref.actions, unionActions).onlyInB)
|
|
277
|
+
errors.push(`action kind "${x}" in the references but never used in any workflow`);
|
|
278
|
+
for (const x of diff(ref.guards, unionGuards).onlyInB)
|
|
279
|
+
errors.push(`guard kind "${x}" in the references but never used in any workflow`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const spuriousEdges = checkTaskDependencyEdges(root, errors);
|
|
283
|
+
|
|
173
284
|
if (errors.length > 0) {
|
|
174
285
|
process.stderr.write(`inline-pipeline-parity-check: ${errors.length} divergence(s)\n`);
|
|
175
286
|
for (const e of errors) process.stderr.write(` - ${e}\n`);
|
|
@@ -177,7 +288,7 @@ async function main(): Promise<number> {
|
|
|
177
288
|
}
|
|
178
289
|
|
|
179
290
|
process.stdout.write(
|
|
180
|
-
`inline-pipeline-parity-check: ok (${unionActions.size} actions, ${unionGuards.size} guards agree across ${files.length} workflows)\n`,
|
|
291
|
+
`inline-pipeline-parity-check: ok (${unionActions.size} actions, ${unionGuards.size} guards agree across ${files.length} workflows and both reference sets; ${spuriousEdges} spurious dependency edges)\n`,
|
|
181
292
|
);
|
|
182
293
|
return 0;
|
|
183
294
|
}
|