@gobing-ai/spur 0.3.95 → 0.3.96

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.
Files changed (155) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/plugin-scripts.json +0 -54
  3. package/config/rules/boundary/sp-script-placement.yaml +19 -0
  4. package/config/rules/strict/runtime-boundaries.yaml +5 -0
  5. package/config/rules/structure/test-location.yaml +2 -0
  6. package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +1 -1
  7. package/config/rules/typescript/output-boundaries.yaml +15 -2
  8. package/config/script-placement-baseline.json +51 -0
  9. package/config/workflows/feature-verification.yaml +1 -1
  10. package/config/workflows/history-anatomy.yaml +23 -11
  11. package/config/workflows/idea-pipeline.yaml +80 -85
  12. package/config/workflows/pr-review.yaml +18 -9
  13. package/config/workflows/task-pipeline.yaml +42 -95
  14. package/config/workflows/wrapup-pipeline.yaml +89 -37
  15. package/package.json +1 -1
  16. package/plugins/sp/README.md +8 -10
  17. package/plugins/sp/agents/super-reviewer.md +43 -18
  18. package/plugins/sp/commands/dev-fixgha.md +83 -0
  19. package/plugins/sp/commands/dev-gitmsg.md +8 -6
  20. package/plugins/sp/commands/dev-gtd.md +2 -2
  21. package/plugins/sp/commands/dev-idea.md +22 -12
  22. package/plugins/sp/commands/dev-plan.md +8 -9
  23. package/plugins/sp/commands/dev-review.md +22 -13
  24. package/plugins/sp/commands/dev-verifyall.md +1 -1
  25. package/plugins/sp/commands/spur-init.md +2 -2
  26. package/plugins/sp/lib/history-anatomy.generated.d.mts +112 -0
  27. package/plugins/sp/lib/history-anatomy.generated.mjs +686 -0
  28. package/plugins/sp/lib/idea-handoff.generated.mjs +5 -4
  29. package/plugins/sp/lib/inline-run.generated.d.mts +11 -0
  30. package/plugins/sp/lib/inline-run.generated.mjs +25 -8
  31. package/plugins/sp/lib/quality-gate.generated.d.mts +104 -0
  32. package/plugins/sp/lib/quality-gate.generated.mjs +438 -0
  33. package/plugins/sp/lib/residual-scan.generated.d.mts +62 -0
  34. package/plugins/sp/lib/residual-scan.generated.mjs +210 -0
  35. package/plugins/sp/lib/spur-bin.ts +36 -0
  36. package/plugins/sp/lib/step-profile.generated.d.mts +71 -0
  37. package/plugins/sp/lib/step-profile.generated.mjs +174 -0
  38. package/plugins/sp/plugin.json +1 -1
  39. package/plugins/sp/references/roles.md +1 -1
  40. package/plugins/sp/scripts/history-anatomy-cache.mjs +20 -19
  41. package/plugins/sp/scripts/history-anatomy-cache.ts +23 -928
  42. package/plugins/sp/scripts/inline-run-setup.mjs +85 -320
  43. package/plugins/sp/scripts/inline-run-setup.ts +104 -667
  44. package/plugins/sp/scripts/quality-gate.mjs +34 -19
  45. package/plugins/sp/scripts/quality-gate.ts +12 -658
  46. package/plugins/sp/scripts/residual-scan.mjs +135 -156
  47. package/plugins/sp/scripts/residual-scan.ts +102 -499
  48. package/plugins/sp/scripts/script-root.mjs +5 -1
  49. package/plugins/sp/scripts/script-root.ts +5 -1
  50. package/plugins/sp/scripts/workflow-step-profile.mjs +25 -17
  51. package/plugins/sp/scripts/workflow-step-profile.ts +21 -315
  52. package/plugins/sp/scripts/wrapup-drift-probe.mjs +8 -2
  53. package/plugins/sp/scripts/wrapup-drift-probe.ts +3 -2
  54. package/plugins/sp/scripts/wrapup-steps.mjs +10 -32
  55. package/plugins/sp/scripts/wrapup-steps.ts +8 -36
  56. package/plugins/sp/skills/code-improvement/SKILL.md +5 -4
  57. package/plugins/sp/skills/code-verification/SKILL.md +34 -9
  58. package/plugins/sp/skills/code-verification/references/verdict-schema.md +3 -3
  59. package/plugins/sp/skills/functional-review/SKILL.md +7 -4
  60. package/plugins/sp/skills/functional-review/references/verdict-schema.md +1 -1
  61. package/plugins/sp/skills/history-anatomy/references/modes.md +2 -1
  62. package/plugins/sp/skills/next-feature/references/handoff-routing.md +1 -1
  63. package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
  64. package/plugins/sp/skills/spur-cli/SKILL.md +3 -3
  65. package/plugins/sp/skills/spur-cli/references/agent.md +10 -10
  66. package/plugins/sp/skills/spur-cli/references/features.md +17 -6
  67. package/plugins/sp/skills/spur-cli/references/init.md +17 -16
  68. package/plugins/sp/skills/spur-cli/references/self.md +3 -2
  69. package/plugins/sp/skills/spur-cli/references/serve.md +10 -10
  70. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +21 -6
  71. package/plugins/sp/skills/spur-cli/references/tasks.md +14 -8
  72. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +4 -3
  73. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +11 -9
  74. package/plugins/sp/skills/spur-dev/references/decision-brief.md +1 -1
  75. package/plugins/sp/skills/spur-dev/references/dev-operations.md +86 -70
  76. package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +3 -3
  77. package/plugins/sp/skills/spur-dev/references/execution-batch.md +77 -34
  78. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +5 -9
  79. package/plugins/sp/skills/spur-dev/references/feature-link-helper.md +3 -3
  80. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +31 -21
  81. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +19 -19
  82. package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +4 -3
  83. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +29 -3
  84. package/plugins/sp/skills/spur-doctor/SKILL.md +1 -1
  85. package/plugins/sp/skills/sys-architecture/SKILL.md +3 -2
  86. package/spur.js +1495 -369
  87. package/web/_astro/{BoardApp.Cpxntzad.js → BoardApp.BIjMatT1.js} +1 -1
  88. package/web/_astro/{BoardApp.BxJuwD7I.js → BoardApp.GvjIe9Z6.js} +42 -42
  89. package/web/_astro/{TaskDetail.C5bW4WGV.js → TaskDetail.BZM3EAFx.js} +1 -1
  90. package/web/_astro/{arc.CyjRvNMY.js → arc.HkRiZnoI.js} +1 -1
  91. package/web/_astro/{architectureDiagram-3BPJPVTR.B4lWRzJA.js → architectureDiagram-3BPJPVTR.BDdK-tgZ.js} +1 -1
  92. package/web/_astro/{blockDiagram-GPEHLZMM.Be38USQ2.js → blockDiagram-GPEHLZMM.CZ9kOBxx.js} +1 -1
  93. package/web/_astro/{c4Diagram-AAUBKEIU.DT9Fj5Qx.js → c4Diagram-AAUBKEIU.DXK9qeRD.js} +1 -1
  94. package/web/_astro/channel.DlhX2MEt.js +1 -0
  95. package/web/_astro/{chunk-2J33WTMH.SeSBWLg5.js → chunk-2J33WTMH.BTD2WeX5.js} +1 -1
  96. package/web/_astro/{chunk-4BX2VUAB.DoO4VE14.js → chunk-4BX2VUAB.Ci6Qbcvk.js} +1 -1
  97. package/web/_astro/{chunk-55IACEB6.DrmTFA53.js → chunk-55IACEB6.yl3zsj7p.js} +1 -1
  98. package/web/_astro/{chunk-727SXJPM.Bi-TUb_V.js → chunk-727SXJPM.ClXZsfyR.js} +1 -1
  99. package/web/_astro/{chunk-AQP2D5EJ.B-xp8Uhi.js → chunk-AQP2D5EJ.BmKWZBcP.js} +1 -1
  100. package/web/_astro/{chunk-FMBD7UC4.DGWrDnUU.js → chunk-FMBD7UC4.By30fcb7.js} +1 -1
  101. package/web/_astro/{chunk-ND2GUHAM._BagvPDy.js → chunk-ND2GUHAM.24NmrD-k.js} +1 -1
  102. package/web/_astro/{chunk-QZHKN3VN.VOboYWQ0.js → chunk-QZHKN3VN.BN4sKdcS.js} +1 -1
  103. package/web/_astro/{classDiagram-4FO5ZUOK.D_apfHS7.js → classDiagram-4FO5ZUOK.CJpoMPb5.js} +1 -1
  104. package/web/_astro/{classDiagram-v2-Q7XG4LA2.D_apfHS7.js → classDiagram-v2-Q7XG4LA2.CJpoMPb5.js} +1 -1
  105. package/web/_astro/{cose-bilkent-S5V4N54A.DNLU_L-x.js → cose-bilkent-S5V4N54A.BraxQ2Nt.js} +1 -1
  106. package/web/_astro/{cynefin-OW5HDTMX.D8borhV-.js → cynefin-OW5HDTMX.gzoU73oL.js} +1 -1
  107. package/web/_astro/{dagre-BM42HDAG.Uc0lvjcV.js → dagre-BM42HDAG.wyDfWySU.js} +1 -1
  108. package/web/_astro/{diagram-2AECGRRQ.DDIxtDBo.js → diagram-2AECGRRQ.h2_EbMxV.js} +1 -1
  109. package/web/_astro/{diagram-5GNKFQAL.DVoDnD3H.js → diagram-5GNKFQAL.Db6dcs89.js} +1 -1
  110. package/web/_astro/{diagram-KO2AKTUF.CRubTJ-0.js → diagram-KO2AKTUF.NVwzxu_U.js} +1 -1
  111. package/web/_astro/{diagram-LMA3HP47.m9SeYcXQ.js → diagram-LMA3HP47.B-eNgDQk.js} +1 -1
  112. package/web/_astro/{diagram-OG6HWLK6.C6s2ZjV5.js → diagram-OG6HWLK6.Bsa8ZRbJ.js} +1 -1
  113. package/web/_astro/{erDiagram-TEJ5UH35.nSzr5KTj.js → erDiagram-TEJ5UH35.BospRE5Q.js} +1 -1
  114. package/web/_astro/{flowDiagram-I6XJVG4X.DrZcys16.js → flowDiagram-I6XJVG4X.DMzvHwAA.js} +1 -1
  115. package/web/_astro/{ganttDiagram-6RSMTGT7.Do4M1b-K.js → ganttDiagram-6RSMTGT7.Dmdxgd-M.js} +1 -1
  116. package/web/_astro/{gitGraphDiagram-PVQCEYII.BWc9oJ4l.js → gitGraphDiagram-PVQCEYII.CaqdO0rL.js} +1 -1
  117. package/web/_astro/index.EoZlzLK-.css +1 -0
  118. package/web/_astro/{infoDiagram-5YYISTIA.BIdOctFk.js → infoDiagram-5YYISTIA.DDVD9Wn7.js} +1 -1
  119. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CV5SuG2t.js → ishikawaDiagram-YF4QCWOH.EKyCUFsL.js} +1 -1
  120. package/web/_astro/{journeyDiagram-JHISSGLW.CxE-rEJ-.js → journeyDiagram-JHISSGLW.By2-RkYB.js} +1 -1
  121. package/web/_astro/{kanban-definition-UN3LZRKU.BPRdKWs_.js → kanban-definition-UN3LZRKU.ar-WmlyR.js} +1 -1
  122. package/web/_astro/{linear.zRsuuDTE.js → linear.BnzMBgo_.js} +1 -1
  123. package/web/_astro/{mermaid.core.jAJTcMKc.js → mermaid.core.DSqeFu2Y.js} +4 -4
  124. package/web/_astro/{mindmap-definition-RKZ34NQL.BffJxfCr.js → mindmap-definition-RKZ34NQL.1XcF8Wh2.js} +1 -1
  125. package/web/_astro/{pieDiagram-4H26LBE5.CacwWaE3.js → pieDiagram-4H26LBE5.DnItHOdK.js} +1 -1
  126. package/web/_astro/{quadrantDiagram-W4KKPZXB.hXELD06-.js → quadrantDiagram-W4KKPZXB.BQQyrnBt.js} +1 -1
  127. package/web/_astro/{requirementDiagram-4Y6WPE33.DdEbPcqT.js → requirementDiagram-4Y6WPE33.Dmzxun82.js} +1 -1
  128. package/web/_astro/{sankeyDiagram-5OEKKPKP.DgaDocKv.js → sankeyDiagram-5OEKKPKP.BBIeRKYB.js} +1 -1
  129. package/web/_astro/{sequenceDiagram-3UESZ5HK.B0KScnAu.js → sequenceDiagram-3UESZ5HK.CUNVdBBf.js} +1 -1
  130. package/web/_astro/{stateDiagram-AJRCARHV.CV9M_WNc.js → stateDiagram-AJRCARHV.DlFf2sJm.js} +1 -1
  131. package/web/_astro/{stateDiagram-v2-BHNVJYJU.BOAo74Et.js → stateDiagram-v2-BHNVJYJU.DXR7I1H5.js} +1 -1
  132. package/web/_astro/{timeline-definition-PNZ67QCA.CqqepUMe.js → timeline-definition-PNZ67QCA.BcO-GyX2.js} +1 -1
  133. package/web/_astro/{vennDiagram-CIIHVFJN.DDqe4dOG.js → vennDiagram-CIIHVFJN.ChCECmmW.js} +1 -1
  134. package/web/_astro/{wardleyDiagram-YWT4CUSO.B3R9OPdO.js → wardleyDiagram-YWT4CUSO.CRaT12mD.js} +1 -1
  135. package/web/_astro/{xychartDiagram-2RQKCTM6.B3SvgXpQ.js → xychartDiagram-2RQKCTM6.B3lsHVpj.js} +1 -1
  136. package/web/index.html +2 -2
  137. package/plugins/sp/lib/artifact-digest.generated.d.mts +0 -7
  138. package/plugins/sp/lib/artifact-digest.generated.mjs +0 -48
  139. package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -301
  140. package/plugins/sp/scripts/feature-sync-bounded.ts +0 -481
  141. package/plugins/sp/scripts/idea-coverage-check.ts +0 -168
  142. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -298
  143. package/plugins/sp/scripts/record-feature-sync.mjs +0 -63
  144. package/plugins/sp/scripts/record-feature-sync.ts +0 -84
  145. package/plugins/sp/scripts/script-contract-check.ts +0 -506
  146. package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
  147. package/plugins/sp/scripts/surface-drift-inventory.ts +0 -989
  148. package/plugins/sp/scripts/task-evidence-precheck.ts +0 -189
  149. package/plugins/sp/scripts/task-size-precheck.ts +0 -212
  150. package/plugins/sp/scripts/transition-shim-check.ts +0 -238
  151. package/plugins/sp/scripts/validate-commands.ts +0 -689
  152. package/plugins/sp/scripts/validate-flag-contracts.ts +0 -890
  153. package/plugins/sp/scripts/verify-answer-lint.ts +0 -549
  154. package/web/_astro/channel.CI6N_tCg.js +0 -1
  155. package/web/_astro/index.CENnIEqT.css +0 -1
@@ -0,0 +1,104 @@
1
+ /**
2
+ * GENERATED by scripts/commands/bundle-plugin-lib.ts — do not edit.
3
+ * Source of truth: packages/app/src/services/quality-gate.ts (task 1006 R1).
4
+ */
5
+ export declare const MAX_GATE_ATTEMPTS: number;
6
+ export declare const MAX_FINDINGS: number;
7
+ export declare const RETRY_DELAY_MS_DEFAULT: number;
8
+ export declare const RETRY_DELAY_MS_ENV: string;
9
+ export declare const LOCKED_PATTERN: RegExp;
10
+ export declare const FINDINGS_PATTERN: RegExp;
11
+ export declare const COVERAGE_ROW_PATTERN: RegExp;
12
+ export declare const RECEIPT_SCHEMA_VERSION: 'check-receipt/v1';
13
+ export declare interface QualityGateEnv {
14
+ wbs: string;
15
+ qualityGateCmd?: string;
16
+ gateProbeCmd?: string;
17
+ proofDigest?: string;
18
+ runId?: string;
19
+ [key: string]: string | undefined;
20
+ }
21
+ export declare interface QualityGateOptions {
22
+ cwd?: string;
23
+ }
24
+ export declare interface QualityGateResult {
25
+ status: 'PASS' | 'FAIL';
26
+ attempts: number;
27
+ logFile: string;
28
+ findingsFile: string;
29
+ statusFile: string;
30
+ attemptFile: string;
31
+ receiptFile?: string;
32
+ }
33
+ export declare function isTransientLock(attemptOutput: string): boolean;
34
+ export declare function extractFindings(logText: string): string;
35
+ export declare function tailLines(text: string, count: number): string;
36
+ export declare function retryMessage(attempt: number): string;
37
+ export declare interface CoverageThreshold {
38
+ functions?: number;
39
+ lines?: number;
40
+ }
41
+ export declare function parseCoverageThreshold(bunfigText: string): CoverageThreshold | null;
42
+ export declare function scanCoverageShortfalls(logText: string, threshold: CoverageThreshold): string[];
43
+ export declare type ReceiptTier = 'full' | 'light';
44
+ export declare type CheckStatus = 'PASS' | 'FAIL';
45
+ export declare type ReceiptReuseReason = 'missing' | 'failed' | 'stale' | 'light-only';
46
+ export declare interface CheckReceiptRow {
47
+ id: string;
48
+ cmd: string;
49
+ status: CheckStatus;
50
+ durationMs: number;
51
+ logPath: string;
52
+ }
53
+ export declare interface CheckReceipt {
54
+ schemaVersion: 'check-receipt/v1';
55
+ wbs: string;
56
+ runId: string;
57
+ tier: ReceiptTier;
58
+ inputDigest: string;
59
+ checks: CheckReceiptRow[];
60
+ status: CheckStatus;
61
+ completedAt: string;
62
+ }
63
+ export declare interface ReceiptReadStatus {
64
+ reuse: boolean;
65
+ reason: ReceiptReuseReason | 'ok';
66
+ }
67
+ export declare interface BuildReceiptInput {
68
+ wbs: string;
69
+ runId: string;
70
+ tier: ReceiptTier;
71
+ inputDigest: string;
72
+ checks: CheckReceiptRow[];
73
+ completedAt: string;
74
+ }
75
+ export declare function buildReceipt(input: BuildReceiptInput): CheckReceipt;
76
+ export declare function receiptFailsAtDigest(receipt: CheckReceipt | null, currentDigest: string): boolean;
77
+ export declare function readReceiptStatus(receiptPath: string, currentDigest: string): ReceiptReadStatus;
78
+ export declare interface LightScope {
79
+ files: string[];
80
+ workspaces: string[];
81
+ tests: string[];
82
+ }
83
+ export declare function lightScope(changedFiles: string[], exists?: (p: string) => boolean): LightScope;
84
+ export declare function shQuote(arg: string): string;
85
+ export declare interface LightCheckPlan {
86
+ id: string;
87
+ cmd: string;
88
+ }
89
+ export declare function workspaceHasTypecheck(workspace: string): boolean;
90
+ export declare function planLightChecks(scope: LightScope, hasTypecheck?: (workspace: string) => boolean): LightCheckPlan[];
91
+ export declare interface LightGateResult {
92
+ status: CheckStatus;
93
+ scope: LightScope;
94
+ checks: CheckReceiptRow[];
95
+ logFile: string;
96
+ receiptFile: string;
97
+ }
98
+ export declare function runLightGate(env: QualityGateEnv, options?: QualityGateOptions): LightGateResult;
99
+ export declare function runShellCommand(cmd: string, cwd: string | undefined): { output: string; code: number };
100
+ export declare function runQualityGate(
101
+ mode: 'run' | 'recheck',
102
+ env: QualityGateEnv,
103
+ options?: QualityGateOptions,
104
+ ): QualityGateResult;
@@ -0,0 +1,438 @@
1
+ // packages/app/src/services/quality-gate.ts
2
+ import { spawnSync } from "node:child_process";
3
+ import {
4
+ appendFileSync,
5
+ closeSync,
6
+ existsSync,
7
+ mkdirSync,
8
+ mkdtempSync,
9
+ openSync,
10
+ readFileSync,
11
+ rmSync,
12
+ statSync,
13
+ writeFileSync
14
+ } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
17
+ var MAX_GATE_ATTEMPTS = 5;
18
+ var MAX_FINDINGS = 20;
19
+ var RETRY_DELAY_MS_DEFAULT = 1e4;
20
+ var RETRY_DELAY_MS_ENV = "SPUR_QUALITY_GATE_RETRY_DELAY_MS";
21
+ var LOCKED_PATTERN = /SQLiteError: database is locked|SQLite database .*is busy|SQLITE_BUSY/;
22
+ var FINDINGS_PATTERN = /[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+/g;
23
+ var COVERAGE_ROW_PATTERN = /^\s*(\S+\.[A-Za-z]+)\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*(\S*)/;
24
+ function isTransientLock(attemptOutput) {
25
+ return LOCKED_PATTERN.test(attemptOutput);
26
+ }
27
+ function extractFindings(logText) {
28
+ const found = new Set;
29
+ for (const match of logText.matchAll(FINDINGS_PATTERN))
30
+ found.add(match[0]);
31
+ return [...found].sort().slice(0, MAX_FINDINGS).map((anchor) => `${anchor} `).join("");
32
+ }
33
+ function tailLines(text, count) {
34
+ const lines = text.split(`
35
+ `);
36
+ if (lines.length > 0 && lines[lines.length - 1] === "")
37
+ lines.pop();
38
+ const tail = lines.slice(Math.max(0, lines.length - count));
39
+ return tail.length === 0 ? "" : `${tail.join(`
40
+ `)}
41
+ `;
42
+ }
43
+ function retryMessage(attempt) {
44
+ return `quality gate: database is locked; retrying (${attempt}/${MAX_GATE_ATTEMPTS}) in 10s
45
+ `;
46
+ }
47
+ function parseCoverageThreshold(bunfigText) {
48
+ const block = bunfigText.match(/coverageThreshold\s*=\s*\{([^}]*)\}/);
49
+ if (!block)
50
+ return null;
51
+ const threshold = {};
52
+ for (const axis of ["functions", "lines"]) {
53
+ const raw = block[1]?.match(new RegExp(`\\b${axis}\\s*=\\s*([\\d.]+)`))?.[1];
54
+ const value = raw === undefined ? Number.NaN : Number.parseFloat(raw);
55
+ if (Number.isFinite(value))
56
+ threshold[axis] = value;
57
+ }
58
+ return threshold.functions === undefined && threshold.lines === undefined ? null : threshold;
59
+ }
60
+ function scanCoverageShortfalls(logText, threshold) {
61
+ const shortfalls = new Map;
62
+ for (const line of logText.split(`
63
+ `)) {
64
+ const row = line.match(COVERAGE_ROW_PATTERN);
65
+ if (!row)
66
+ continue;
67
+ const [, path, funcs, lines, uncovered] = row;
68
+ if (!path || path === "All files" || shortfalls.has(path))
69
+ continue;
70
+ const belowFunctions = threshold.functions !== undefined && Number.parseFloat(funcs ?? "") < threshold.functions * 100;
71
+ const belowLines = threshold.lines !== undefined && Number.parseFloat(lines ?? "") < threshold.lines * 100;
72
+ if (!belowFunctions && !belowLines)
73
+ continue;
74
+ const firstUncovered = uncovered?.match(/\d+/)?.[0] ?? "1";
75
+ shortfalls.set(path, `quality gate: coverage shortfall ${path}:${firstUncovered} funcs=${funcs} lines=${lines}`);
76
+ }
77
+ return [...shortfalls.values()];
78
+ }
79
+ var RECEIPT_SCHEMA_VERSION = "check-receipt/v1";
80
+ function buildReceipt(input) {
81
+ return {
82
+ schemaVersion: RECEIPT_SCHEMA_VERSION,
83
+ wbs: input.wbs,
84
+ runId: input.runId,
85
+ tier: input.tier,
86
+ inputDigest: input.inputDigest,
87
+ checks: input.checks,
88
+ status: input.checks.every((row) => row.status === "PASS") ? "PASS" : "FAIL",
89
+ completedAt: input.completedAt
90
+ };
91
+ }
92
+ function readReceipt(receiptPath) {
93
+ try {
94
+ return JSON.parse(readFileSync(receiptPath, "utf8"));
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+ function receiptFailsAtDigest(receipt, currentDigest) {
100
+ return receipt !== null && receipt.schemaVersion === RECEIPT_SCHEMA_VERSION && receipt.tier === "full" && receipt.status === "FAIL" && currentDigest.length > 0 && receipt.inputDigest === currentDigest;
101
+ }
102
+ function readReceiptStatus(receiptPath, currentDigest) {
103
+ const receipt = readReceipt(receiptPath);
104
+ if (receipt === null || receipt.schemaVersion !== RECEIPT_SCHEMA_VERSION) {
105
+ return { reuse: false, reason: "missing" };
106
+ }
107
+ if (receipt.status !== "PASS")
108
+ return { reuse: false, reason: "failed" };
109
+ if (currentDigest.length === 0 || receipt.inputDigest !== currentDigest) {
110
+ return { reuse: false, reason: "stale" };
111
+ }
112
+ if (receipt.tier !== "full")
113
+ return { reuse: false, reason: "light-only" };
114
+ return { reuse: true, reason: "ok" };
115
+ }
116
+ var TEST_FILE_PATTERN = /\.test\.tsx?$/;
117
+ function workspaceOf(file, exists) {
118
+ const segments = file.split("/");
119
+ for (let depth = 1;depth < segments.length; depth++) {
120
+ const prefix = segments.slice(0, depth).join("/");
121
+ if (exists(`${prefix}/package.json`))
122
+ return prefix;
123
+ }
124
+ return null;
125
+ }
126
+ function lightScope(changedFiles, exists = existsSync) {
127
+ const files = [];
128
+ const workspaces = new Set;
129
+ const tests = new Set;
130
+ for (const file of changedFiles) {
131
+ files.push(file);
132
+ const workspace = workspaceOf(file, exists);
133
+ if (workspace === null)
134
+ continue;
135
+ workspaces.add(workspace);
136
+ const rest = file.slice(workspace.length + 1);
137
+ if (rest.startsWith("src/")) {
138
+ const candidate = `${workspace}/tests/${rest.slice("src/".length).replace(/\.tsx?$/, (ext) => `.test${ext}`)}`;
139
+ if (exists(candidate))
140
+ tests.add(candidate);
141
+ } else if (rest.startsWith("tests/") && TEST_FILE_PATTERN.test(rest)) {
142
+ tests.add(file);
143
+ }
144
+ }
145
+ return { files, workspaces: [...workspaces].sort(), tests: [...tests].sort() };
146
+ }
147
+ function shQuote(arg) {
148
+ return /^[\w./@+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, `'\\''`)}'`;
149
+ }
150
+ function workspaceHasTypecheck(workspace) {
151
+ try {
152
+ const pkg = JSON.parse(readFileSync(join(workspace, "package.json"), "utf8"));
153
+ return typeof pkg.scripts?.typecheck === "string";
154
+ } catch {
155
+ return false;
156
+ }
157
+ }
158
+ function planLightChecks(scope, hasTypecheck = workspaceHasTypecheck) {
159
+ const plans = [];
160
+ if (scope.files.length > 0) {
161
+ plans.push({ id: "format-lint:changed", cmd: `bunx biome check ${scope.files.map(shQuote).join(" ")}` });
162
+ }
163
+ for (const workspace of scope.workspaces) {
164
+ if (hasTypecheck(workspace)) {
165
+ plans.push({ id: `typecheck:${workspace}`, cmd: `cd ${shQuote(workspace)} && bun run typecheck` });
166
+ }
167
+ }
168
+ const testsByWorkspace = new Map;
169
+ for (const test of scope.tests) {
170
+ const workspace = test.slice(0, test.indexOf("/tests/"));
171
+ const paths = testsByWorkspace.get(workspace) ?? [];
172
+ paths.push(test.slice(workspace.length + 1));
173
+ testsByWorkspace.set(workspace, paths);
174
+ }
175
+ for (const [workspace, paths] of testsByWorkspace) {
176
+ plans.push({
177
+ id: `test:${workspace}`,
178
+ cmd: `cd ${shQuote(workspace)} && bun test ${paths.map(shQuote).join(" ")}`
179
+ });
180
+ }
181
+ return plans;
182
+ }
183
+ function gitChangedFiles(cwd) {
184
+ const abs = (p) => cwd ? join(cwd, p) : p;
185
+ const changed = new Set;
186
+ for (const cmd of ["git diff --name-only HEAD", "git ls-files --others --exclude-standard"]) {
187
+ const result = runShellCommand(cmd, cwd);
188
+ for (const line of result.output.split(`
189
+ `)) {
190
+ const file = line.trim();
191
+ if (file.length > 0 && existsSync(abs(file)))
192
+ changed.add(file);
193
+ }
194
+ }
195
+ return [...changed].sort();
196
+ }
197
+ function receiptRunId(env) {
198
+ return (env.runId ?? "").length > 0 ? env.runId : `pipeline-${env.wbs}`;
199
+ }
200
+ function runLightGate(env, options = {}) {
201
+ const cwd = options.cwd;
202
+ const abs = (p) => cwd ? join(cwd, p) : p;
203
+ const runDir = join(".spur", "run");
204
+ mkdirSync(abs(runDir), { recursive: true });
205
+ const logFile = join(runDir, `${env.wbs}-light-gate.log`);
206
+ const receiptFile = join(runDir, `${env.wbs}-check-receipt.json`);
207
+ writeFileSync(abs(logFile), "");
208
+ const digest = env.proofDigest ?? "";
209
+ const scope = lightScope(gitChangedFiles(cwd), (p) => existsSync(abs(p)));
210
+ const plans = planLightChecks(scope, (workspace) => workspaceHasTypecheck(abs(workspace)));
211
+ const reusable = new Map;
212
+ let preserveFullReceipt = false;
213
+ if (digest.length > 0) {
214
+ try {
215
+ const prior = JSON.parse(readFileSync(abs(receiptFile), "utf8"));
216
+ if (prior?.schemaVersion === RECEIPT_SCHEMA_VERSION && prior.inputDigest === digest) {
217
+ if (prior.tier === "full") {
218
+ preserveFullReceipt = true;
219
+ } else {
220
+ for (const row of prior.checks ?? [])
221
+ if (row.status === "PASS")
222
+ reusable.set(row.id, row);
223
+ }
224
+ }
225
+ } catch {}
226
+ }
227
+ const checks = [];
228
+ let skipped = 0;
229
+ for (const plan of plans) {
230
+ const priorRow = reusable.get(plan.id);
231
+ if (priorRow !== undefined) {
232
+ checks.push(priorRow);
233
+ skipped++;
234
+ const line = `--- light ${plan.id}: check.reused — skipped (PASS at the same input digest)
235
+ `;
236
+ process.stdout.write(line);
237
+ appendFileSync(abs(logFile), line);
238
+ continue;
239
+ }
240
+ const startedAtMs = Date.now();
241
+ const result = runShellCommand(plan.cmd, cwd);
242
+ const durationMs = Date.now() - startedAtMs;
243
+ const status = result.code === 0 ? "PASS" : "FAIL";
244
+ appendFileSync(abs(logFile), `--- light ${plan.id}: ${status} (${durationMs}ms)
245
+ ${result.output}`);
246
+ checks.push({ id: plan.id, cmd: plan.cmd, status, durationMs, logPath: logFile });
247
+ }
248
+ const receipt = buildReceipt({
249
+ wbs: env.wbs,
250
+ runId: receiptRunId(env),
251
+ tier: "light",
252
+ inputDigest: digest,
253
+ checks,
254
+ completedAt: new Date().toISOString()
255
+ });
256
+ if (preserveFullReceipt) {
257
+ appendFileSync(abs(logFile), `--- light receipt: not written — the full-tier receipt at the same input digest is preserved
258
+ `);
259
+ process.stdout.write(`light gate ${receipt.status} (${scope.files.length} changed files; checks: ${checks.length},` + ` skipped: ${skipped}; receipt: ${receiptFile} preserved (full tier); log: ${logFile})
260
+ `);
261
+ } else {
262
+ writeFileSync(abs(receiptFile), `${JSON.stringify(receipt, null, 2)}
263
+ `);
264
+ process.stdout.write(`light gate ${receipt.status} (${scope.files.length} changed files; checks: ${checks.length},` + ` skipped: ${skipped}; receipt: ${receiptFile}; log: ${logFile})
265
+ `);
266
+ }
267
+ return { status: receipt.status, scope, checks, logFile, receiptFile };
268
+ }
269
+ function retryDelayMs(env) {
270
+ const raw = Number.parseInt(env[RETRY_DELAY_MS_ENV] ?? "", 10);
271
+ return Number.isFinite(raw) && raw >= 0 ? raw : RETRY_DELAY_MS_DEFAULT;
272
+ }
273
+ function gateSleep(ms) {
274
+ if (ms > 0)
275
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
276
+ }
277
+ function runShellCommand(cmd, cwd) {
278
+ const dir = mkdtempSync(join(tmpdir(), "spur-quality-gate-"));
279
+ const path = join(dir, "output");
280
+ const fd = openSync(path, "w");
281
+ try {
282
+ const result = spawnSync("sh", ["-c", cmd], { cwd, stdio: ["ignore", fd, fd] });
283
+ if (result.error !== undefined) {
284
+ return { output: `sh -c failed: ${result.error.message}
285
+ `, code: 1 };
286
+ }
287
+ return { output: readFileSync(path, "utf8"), code: result.status ?? 1 };
288
+ } finally {
289
+ closeSync(fd);
290
+ rmSync(dir, { recursive: true, force: true });
291
+ }
292
+ }
293
+ function runQualityGate(mode, env, options = {}) {
294
+ const cwd = options.cwd;
295
+ const runDir = join(".spur", "run");
296
+ mkdirSync(cwd ? join(cwd, runDir) : runDir, { recursive: true });
297
+ const rel = (name) => join(runDir, `${env.wbs}${name}`);
298
+ const logFile = rel("-test-gate.log");
299
+ const findingsFile = rel("-test-gate.findings");
300
+ const statusFile = rel("-test-gate.status");
301
+ const attemptFile = rel("-test-fix-attempt");
302
+ const abs = (p) => cwd ? join(cwd, p) : p;
303
+ writeFileSync(abs(logFile), "");
304
+ if (mode === "run")
305
+ writeFileSync(abs(attemptFile), `0
306
+ `);
307
+ const gateStartedAtMs = Date.now();
308
+ let gateRc = 0;
309
+ let gateAttempt = 0;
310
+ const reusePass = readReceiptStatus(abs(rel("-check-receipt.json")), env.proofDigest ?? "").reuse;
311
+ if (reusePass) {
312
+ const line = `check.reused — full-tier PASS receipt at input digest ${env.proofDigest ?? ""}; gate skipped
313
+ `;
314
+ process.stdout.write(line);
315
+ appendFileSync(abs(logFile), line);
316
+ writeFileSync(abs(findingsFile), extractFindings(readFileSync(abs(logFile), "utf8")));
317
+ writeFileSync(abs(statusFile), `PASS
318
+ `);
319
+ return { status: "PASS", attempts: 0, logFile, findingsFile, statusFile, attemptFile };
320
+ }
321
+ const noProgressSkip = mode === "recheck" && receiptFailsAtDigest(readReceipt(abs(rel("-check-receipt.json"))), env.proofDigest ?? "");
322
+ if (noProgressSkip) {
323
+ const line = `check.skipped-no-progress — full-tier FAIL receipt at input digest ${env.proofDigest ?? ""}; recheck skipped
324
+ `;
325
+ process.stdout.write(line);
326
+ appendFileSync(abs(logFile), line);
327
+ gateRc = 1;
328
+ }
329
+ if (mode === "recheck" && !noProgressSkip && (env.gateProbeCmd ?? "").length > 0) {
330
+ const probe = runShellCommand(env.gateProbeCmd ?? "", cwd);
331
+ writeFileSync(abs(`${logFile}.probe`), probe.output);
332
+ gateRc = probe.code;
333
+ appendFileSync(abs(logFile), probe.output);
334
+ rmSync(abs(`${logFile}.probe`), { force: true });
335
+ }
336
+ if (gateRc === 0) {
337
+ const delayMs = retryDelayMs(env);
338
+ for (gateAttempt = 1;gateAttempt <= MAX_GATE_ATTEMPTS; gateAttempt++) {
339
+ const attemptLogPath = `${logFile}.attempt-${gateAttempt}`;
340
+ const attempt = runShellCommand(env.qualityGateCmd ?? "", cwd);
341
+ writeFileSync(abs(attemptLogPath), attempt.output);
342
+ const locked = isTransientLock(attempt.output);
343
+ appendFileSync(abs(logFile), attempt.output);
344
+ rmSync(abs(attemptLogPath), { force: true });
345
+ gateRc = attempt.code;
346
+ if (gateRc === 0 || !locked || gateAttempt >= MAX_GATE_ATTEMPTS)
347
+ break;
348
+ const line = retryMessage(gateAttempt);
349
+ process.stdout.write(line);
350
+ appendFileSync(abs(logFile), line);
351
+ gateSleep(delayMs);
352
+ }
353
+ }
354
+ if (gateRc !== 0) {
355
+ const gateLog = existsSync(abs(logFile)) ? readFileSync(abs(logFile), "utf8") : "";
356
+ if (/^\s*0 fail\b/m.test(gateLog) && !/^\s*[1-9]\d*\s+fail\b/m.test(gateLog)) {
357
+ const bunfigPath = cwd ? join(cwd, "bunfig.toml") : "bunfig.toml";
358
+ const threshold = existsSync(bunfigPath) ? parseCoverageThreshold(readFileSync(bunfigPath, "utf8")) : null;
359
+ if (threshold) {
360
+ for (const shortfall of scanCoverageShortfalls(gateLog, threshold)) {
361
+ process.stdout.write(`${shortfall}
362
+ `);
363
+ appendFileSync(abs(logFile), `${shortfall}
364
+ `);
365
+ }
366
+ }
367
+ }
368
+ }
369
+ if (gateRc === 0) {
370
+ const bytes = existsSync(abs(logFile)) ? statSync(abs(logFile)).size : 0;
371
+ const attemptsLabel = gateAttempt > 0 ? String(gateAttempt) : "";
372
+ process.stdout.write(`quality gate PASS (attempts: ${attemptsLabel}; log: ${logFile}; bytes: ${bytes})
373
+ `);
374
+ } else {
375
+ process.stdout.write(`quality gate FAIL — last 40 lines follow (full log: ${logFile})
376
+ `);
377
+ process.stdout.write(tailLines(readFileSync(abs(logFile), "utf8"), 40));
378
+ }
379
+ writeFileSync(abs(findingsFile), extractFindings(readFileSync(abs(logFile), "utf8")));
380
+ const status = gateRc === 0 ? "PASS" : "FAIL";
381
+ writeFileSync(abs(statusFile), `${status}
382
+ `);
383
+ appendFileSync(abs(logFile), `proof-digest: ${env.proofDigest ?? ""}
384
+ `);
385
+ let receiptFile;
386
+ if (!noProgressSkip) {
387
+ if ((env.proofDigest ?? "").length > 0) {
388
+ receiptFile = join(runDir, `${env.wbs}-check-receipt.json`);
389
+ const receipt = buildReceipt({
390
+ wbs: env.wbs,
391
+ runId: receiptRunId(env),
392
+ tier: "full",
393
+ inputDigest: env.proofDigest ?? "",
394
+ checks: [
395
+ {
396
+ id: "test",
397
+ cmd: env.qualityGateCmd ?? "",
398
+ status,
399
+ durationMs: Date.now() - gateStartedAtMs,
400
+ logPath: logFile
401
+ }
402
+ ],
403
+ completedAt: new Date().toISOString()
404
+ });
405
+ writeFileSync(abs(receiptFile), `${JSON.stringify(receipt, null, 2)}
406
+ `);
407
+ } else {
408
+ appendFileSync(abs(logFile), "check-receipt: not written — env `proofDigest` is not set\n");
409
+ }
410
+ }
411
+ return { status, attempts: gateAttempt, logFile, findingsFile, statusFile, attemptFile, receiptFile };
412
+ }
413
+ export {
414
+ workspaceHasTypecheck,
415
+ tailLines,
416
+ shQuote,
417
+ scanCoverageShortfalls,
418
+ runShellCommand,
419
+ runQualityGate,
420
+ runLightGate,
421
+ retryMessage,
422
+ receiptFailsAtDigest,
423
+ readReceiptStatus,
424
+ planLightChecks,
425
+ parseCoverageThreshold,
426
+ lightScope,
427
+ isTransientLock,
428
+ extractFindings,
429
+ buildReceipt,
430
+ RETRY_DELAY_MS_ENV,
431
+ RETRY_DELAY_MS_DEFAULT,
432
+ RECEIPT_SCHEMA_VERSION,
433
+ MAX_GATE_ATTEMPTS,
434
+ MAX_FINDINGS,
435
+ LOCKED_PATTERN,
436
+ FINDINGS_PATTERN,
437
+ COVERAGE_ROW_PATTERN
438
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * GENERATED by scripts/commands/bundle-plugin-lib.ts — do not edit.
3
+ * Source of truth: packages/app/src/services/residual-scan.ts (task 1003 R5).
4
+ */
5
+ export declare type ResidualCategory = 'review-finding' | 'diff-marker' | 'unchecked-box' | 'staging-residue';
6
+ export declare type ResidualClass = 'blocking' | 'deferrable' | 'advisory' | 'housekeeping';
7
+ export declare interface ResidualItem {
8
+ id: string;
9
+ category: ResidualCategory;
10
+ class: ResidualClass;
11
+ priority?: string;
12
+ location: string;
13
+ text: string;
14
+ }
15
+ export declare interface ResidualArtifact {
16
+ wbs: string;
17
+ base: string | null;
18
+ scanned: Record<ResidualCategory, boolean>;
19
+ items: ResidualItem[];
20
+ counts: { blocking: number; deferrable: number; advisory: number; housekeeping: number };
21
+ }
22
+ export declare interface Deferral {
23
+ id: string;
24
+ reason: string;
25
+ }
26
+ export declare interface ResidualScanInputs {
27
+ wbs: string;
28
+ base: string | null;
29
+ taskContent: string;
30
+ addedLines: Array<{ file: string; line: number; text: string }>;
31
+ stagingResidue: string[];
32
+ deferrals: Deferral[];
33
+ }
34
+ export declare interface FoldResult {
35
+ verdict: 'PASS' | 'PARTIAL' | 'FAIL';
36
+ checks: Array<{ name: string; status: string; evidence: string }>;
37
+ findings: string;
38
+ }
39
+ export declare const ALLOW_PRAGMA: 'residual-scan:allow';
40
+ export declare function makeItemId(category: ResidualCategory, location: string, text: string): string;
41
+ export declare function normalizeAnchor(location: string): string;
42
+ export declare function locationOf(locationCell: string, finding: string): string;
43
+ export declare function parseReviewFindings(
44
+ taskContent: string,
45
+ ): Array<{ priority: string; location: string; text: string; deferral?: string }>;
46
+ export declare function parseDiffMarkers(
47
+ addedLines: Array<{ file: string; line: number; text: string }>,
48
+ ): Array<{ location: string; text: string }>;
49
+ export declare function findUncheckedBoxes(taskContent: string): Array<{ location: string; text: string }>;
50
+ export declare function classify(
51
+ items: Array<Omit<ResidualItem, 'id' | 'class'> & { category: ResidualCategory }>,
52
+ deferrals: Deferral[],
53
+ ): ResidualItem[];
54
+ export declare function scanResiduals(inputs: ResidualScanInputs): ResidualArtifact;
55
+ export declare function blockingAnchors(items: ResidualItem[]): string[];
56
+ export declare function foldVerdict(
57
+ verdict: { verdict: string; checks: Array<{ name: string; status: string; evidence: string }> },
58
+ scan: ResidualArtifact,
59
+ existingFindings: string,
60
+ maxFindings?: number,
61
+ ): FoldResult;
62
+ export declare function renderReport(wbs: string, items: ResidualItem[], attemptCount: number): string;