@specship/specship-win32-arm64 0.24.0 → 0.28.0
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/bin/specship.cmd +31 -1
- package/lib/dist/activation/cheatsheet.d.ts +17 -1
- package/lib/dist/activation/cheatsheet.d.ts.map +1 -1
- package/lib/dist/activation/cheatsheet.js +47 -3
- package/lib/dist/activation/cheatsheet.js.map +1 -1
- package/lib/dist/activation/hook-output.d.ts +80 -0
- package/lib/dist/activation/hook-output.d.ts.map +1 -0
- package/lib/dist/activation/hook-output.js +103 -0
- package/lib/dist/activation/hook-output.js.map +1 -0
- package/lib/dist/activation/search-intercept.d.ts +60 -29
- package/lib/dist/activation/search-intercept.d.ts.map +1 -1
- package/lib/dist/activation/search-intercept.js +159 -55
- package/lib/dist/activation/search-intercept.js.map +1 -1
- package/lib/dist/activation/steering.d.ts +11 -13
- package/lib/dist/activation/steering.d.ts.map +1 -1
- package/lib/dist/activation/steering.js +25 -10
- package/lib/dist/activation/steering.js.map +1 -1
- package/lib/dist/behaviour/behaviour-surface.d.ts +47 -0
- package/lib/dist/behaviour/behaviour-surface.d.ts.map +1 -1
- package/lib/dist/behaviour/behaviour-surface.js +108 -2
- package/lib/dist/behaviour/behaviour-surface.js.map +1 -1
- package/lib/dist/bin/node-version-check.d.ts +22 -7
- package/lib/dist/bin/node-version-check.d.ts.map +1 -1
- package/lib/dist/bin/node-version-check.js +39 -9
- package/lib/dist/bin/node-version-check.js.map +1 -1
- package/lib/dist/bin/specship.js +989 -116
- package/lib/dist/bin/specship.js.map +1 -1
- package/lib/dist/commands/specship/learn.md +27 -3
- package/lib/dist/commands/specship/spec.md +85 -15
- package/lib/dist/db/spec-queries.d.ts +73 -3
- package/lib/dist/db/spec-queries.d.ts.map +1 -1
- package/lib/dist/db/spec-queries.js +139 -5
- package/lib/dist/db/spec-queries.js.map +1 -1
- package/lib/dist/db/sqlite-adapter.d.ts +7 -5
- package/lib/dist/db/sqlite-adapter.d.ts.map +1 -1
- package/lib/dist/db/sqlite-adapter.js +11 -9
- package/lib/dist/db/sqlite-adapter.js.map +1 -1
- package/lib/dist/enforce/changed-files.d.ts +39 -0
- package/lib/dist/enforce/changed-files.d.ts.map +1 -0
- package/lib/dist/enforce/changed-files.js +114 -0
- package/lib/dist/enforce/changed-files.js.map +1 -0
- package/lib/dist/enforce/enforce.d.ts +53 -2
- package/lib/dist/enforce/enforce.d.ts.map +1 -1
- package/lib/dist/enforce/enforce.js +95 -8
- package/lib/dist/enforce/enforce.js.map +1 -1
- package/lib/dist/enforce/mutation.d.ts +46 -0
- package/lib/dist/enforce/mutation.d.ts.map +1 -0
- package/lib/dist/enforce/mutation.js +104 -0
- package/lib/dist/enforce/mutation.js.map +1 -0
- package/lib/dist/enforce/suppressions.d.ts +33 -0
- package/lib/dist/enforce/suppressions.d.ts.map +1 -0
- package/lib/dist/enforce/suppressions.js +159 -0
- package/lib/dist/enforce/suppressions.js.map +1 -0
- package/lib/dist/extraction/grammars.d.ts +10 -0
- package/lib/dist/extraction/grammars.d.ts.map +1 -1
- package/lib/dist/extraction/grammars.js +32 -3
- package/lib/dist/extraction/grammars.js.map +1 -1
- package/lib/dist/extraction/index.d.ts +7 -0
- package/lib/dist/extraction/index.d.ts.map +1 -1
- package/lib/dist/extraction/index.js.map +1 -1
- package/lib/dist/extraction/specs/markdown-spec-extractor.d.ts +7 -0
- package/lib/dist/extraction/specs/markdown-spec-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/specs/markdown-spec-extractor.js +97 -10
- package/lib/dist/extraction/specs/markdown-spec-extractor.js.map +1 -1
- package/lib/dist/extraction/wasm/tree-sitter-pascal.wasm.gz +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-scala.wasm.gz +0 -0
- package/lib/dist/graph/spec-coverage.d.ts +142 -0
- package/lib/dist/graph/spec-coverage.d.ts.map +1 -0
- package/lib/dist/graph/spec-coverage.js +276 -0
- package/lib/dist/graph/spec-coverage.js.map +1 -0
- package/lib/dist/health/smoke-check.js +2 -2
- package/lib/dist/health/smoke-check.js.map +1 -1
- package/lib/dist/index.d.ts +75 -7
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +191 -23
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/installer/gemini-commands.d.ts +107 -0
- package/lib/dist/installer/gemini-commands.d.ts.map +1 -0
- package/lib/dist/installer/gemini-commands.js +264 -0
- package/lib/dist/installer/gemini-commands.js.map +1 -0
- package/lib/dist/installer/index.d.ts +55 -8
- package/lib/dist/installer/index.d.ts.map +1 -1
- package/lib/dist/installer/index.js +72 -53
- package/lib/dist/installer/index.js.map +1 -1
- package/lib/dist/installer/init-offer.d.ts +14 -9
- package/lib/dist/installer/init-offer.d.ts.map +1 -1
- package/lib/dist/installer/init-offer.js +13 -9
- package/lib/dist/installer/init-offer.js.map +1 -1
- package/lib/dist/installer/install-defaults.d.ts +81 -0
- package/lib/dist/installer/install-defaults.d.ts.map +1 -0
- package/lib/dist/installer/install-defaults.js +95 -0
- package/lib/dist/installer/install-defaults.js.map +1 -0
- package/lib/dist/installer/instructions-template.d.ts +22 -0
- package/lib/dist/installer/instructions-template.d.ts.map +1 -1
- package/lib/dist/installer/instructions-template.js +37 -1
- package/lib/dist/installer/instructions-template.js.map +1 -1
- package/lib/dist/installer/targets/claude.d.ts +18 -0
- package/lib/dist/installer/targets/claude.d.ts.map +1 -1
- package/lib/dist/installer/targets/claude.js +70 -23
- package/lib/dist/installer/targets/claude.js.map +1 -1
- package/lib/dist/installer/targets/gemini.d.ts +85 -12
- package/lib/dist/installer/targets/gemini.d.ts.map +1 -1
- package/lib/dist/installer/targets/gemini.js +527 -30
- package/lib/dist/installer/targets/gemini.js.map +1 -1
- package/lib/dist/installer/targets/shared.d.ts +13 -0
- package/lib/dist/installer/targets/shared.d.ts.map +1 -1
- package/lib/dist/installer/targets/shared.js +22 -4
- package/lib/dist/installer/targets/shared.js.map +1 -1
- package/lib/dist/installer/targets/types.d.ts +12 -0
- package/lib/dist/installer/targets/types.d.ts.map +1 -1
- package/lib/dist/jira/auto-publish.d.ts +71 -4
- package/lib/dist/jira/auto-publish.d.ts.map +1 -1
- package/lib/dist/jira/auto-publish.js +186 -17
- package/lib/dist/jira/auto-publish.js.map +1 -1
- package/lib/dist/jira/reconcile.d.ts +24 -1
- package/lib/dist/jira/reconcile.d.ts.map +1 -1
- package/lib/dist/jira/reconcile.js +46 -2
- package/lib/dist/jira/reconcile.js.map +1 -1
- package/lib/dist/jira/repo-config.d.ts +9 -0
- package/lib/dist/jira/repo-config.d.ts.map +1 -1
- package/lib/dist/jira/repo-config.js +11 -0
- package/lib/dist/jira/repo-config.js.map +1 -1
- package/lib/dist/jira/types.d.ts +7 -0
- package/lib/dist/jira/types.d.ts.map +1 -1
- package/lib/dist/jira/types.js.map +1 -1
- package/lib/dist/knowledge/enforce.d.ts +75 -0
- package/lib/dist/knowledge/enforce.d.ts.map +1 -0
- package/lib/dist/knowledge/enforce.js +250 -0
- package/lib/dist/knowledge/enforce.js.map +1 -0
- package/lib/dist/knowledge/facts.d.ts +33 -0
- package/lib/dist/knowledge/facts.d.ts.map +1 -0
- package/lib/dist/knowledge/facts.js +117 -0
- package/lib/dist/knowledge/facts.js.map +1 -0
- package/lib/dist/knowledge/proposals.d.ts +88 -0
- package/lib/dist/knowledge/proposals.d.ts.map +1 -0
- package/lib/dist/knowledge/proposals.js +360 -0
- package/lib/dist/knowledge/proposals.js.map +1 -0
- package/lib/dist/knowledge/stale.d.ts +64 -0
- package/lib/dist/knowledge/stale.d.ts.map +1 -0
- package/lib/dist/knowledge/stale.js +174 -0
- package/lib/dist/knowledge/stale.js.map +1 -0
- package/lib/dist/mcp/fitness-tool.d.ts +5 -6
- package/lib/dist/mcp/fitness-tool.d.ts.map +1 -1
- package/lib/dist/mcp/fitness-tool.js +0 -16
- package/lib/dist/mcp/fitness-tool.js.map +1 -1
- package/lib/dist/mcp/gemini-session.d.ts +50 -0
- package/lib/dist/mcp/gemini-session.d.ts.map +1 -0
- package/lib/dist/mcp/gemini-session.js +159 -0
- package/lib/dist/mcp/gemini-session.js.map +1 -0
- package/lib/dist/mcp/health-tool.d.ts +26 -0
- package/lib/dist/mcp/health-tool.d.ts.map +1 -0
- package/lib/dist/mcp/health-tool.js +50 -0
- package/lib/dist/mcp/health-tool.js.map +1 -0
- package/lib/dist/mcp/jira-tools.d.ts.map +1 -1
- package/lib/dist/mcp/jira-tools.js +25 -0
- package/lib/dist/mcp/jira-tools.js.map +1 -1
- package/lib/dist/mcp/maintainability-tool.d.ts +5 -7
- package/lib/dist/mcp/maintainability-tool.d.ts.map +1 -1
- package/lib/dist/mcp/maintainability-tool.js +0 -16
- package/lib/dist/mcp/maintainability-tool.js.map +1 -1
- package/lib/dist/mcp/model-context.d.ts +31 -7
- package/lib/dist/mcp/model-context.d.ts.map +1 -1
- package/lib/dist/mcp/model-context.js +70 -21
- package/lib/dist/mcp/model-context.js.map +1 -1
- package/lib/dist/mcp/proxy.d.ts.map +1 -1
- package/lib/dist/mcp/proxy.js +5 -2
- package/lib/dist/mcp/proxy.js.map +1 -1
- package/lib/dist/mcp/server-instructions.d.ts +16 -15
- package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
- package/lib/dist/mcp/server-instructions.js +72 -5
- package/lib/dist/mcp/server-instructions.js.map +1 -1
- package/lib/dist/mcp/session.js +1 -1
- package/lib/dist/mcp/session.js.map +1 -1
- package/lib/dist/mcp/spec-tools.d.ts +9 -1
- package/lib/dist/mcp/spec-tools.d.ts.map +1 -1
- package/lib/dist/mcp/spec-tools.js +282 -49
- package/lib/dist/mcp/spec-tools.js.map +1 -1
- package/lib/dist/mcp/tools.d.ts +49 -9
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +210 -58
- package/lib/dist/mcp/tools.js.map +1 -1
- package/lib/dist/pulse/pulse.d.ts +93 -0
- package/lib/dist/pulse/pulse.d.ts.map +1 -0
- package/lib/dist/pulse/pulse.js +401 -0
- package/lib/dist/pulse/pulse.js.map +1 -0
- package/lib/dist/reflect/apply.d.ts.map +1 -1
- package/lib/dist/reflect/apply.js +38 -3
- package/lib/dist/reflect/apply.js.map +1 -1
- package/lib/dist/reflect/auto-sweep.d.ts +26 -0
- package/lib/dist/reflect/auto-sweep.d.ts.map +1 -0
- package/lib/dist/reflect/auto-sweep.js +90 -0
- package/lib/dist/reflect/auto-sweep.js.map +1 -0
- package/lib/dist/reflect/cli.d.ts +62 -0
- package/lib/dist/reflect/cli.d.ts.map +1 -0
- package/lib/dist/reflect/cli.js +163 -0
- package/lib/dist/reflect/cli.js.map +1 -0
- package/lib/dist/reflect/miner.d.ts.map +1 -1
- package/lib/dist/reflect/miner.js +119 -0
- package/lib/dist/reflect/miner.js.map +1 -1
- package/lib/dist/reflect/sweep.d.ts +1 -0
- package/lib/dist/reflect/sweep.d.ts.map +1 -1
- package/lib/dist/reflect/sweep.js +62 -13
- package/lib/dist/reflect/sweep.js.map +1 -1
- package/lib/dist/reflect/targets.d.ts +2 -0
- package/lib/dist/reflect/targets.d.ts.map +1 -1
- package/lib/dist/reflect/targets.js +33 -0
- package/lib/dist/reflect/targets.js.map +1 -1
- package/lib/dist/reflect/types.d.ts +19 -2
- package/lib/dist/reflect/types.d.ts.map +1 -1
- package/lib/dist/resolution/spec-link-resolver.d.ts +44 -2
- package/lib/dist/resolution/spec-link-resolver.d.ts.map +1 -1
- package/lib/dist/resolution/spec-link-resolver.js +132 -0
- package/lib/dist/resolution/spec-link-resolver.js.map +1 -1
- package/lib/dist/resolution/test-title-links.d.ts +62 -0
- package/lib/dist/resolution/test-title-links.d.ts.map +1 -0
- package/lib/dist/resolution/test-title-links.js +97 -0
- package/lib/dist/resolution/test-title-links.js.map +1 -0
- package/lib/dist/search/query-parser.d.ts.map +1 -1
- package/lib/dist/search/query-parser.js +6 -1
- package/lib/dist/search/query-parser.js.map +1 -1
- package/lib/dist/server/project-registry.js +1 -1
- package/lib/dist/server/routes/claude.js +24 -43
- package/lib/dist/server/routes/graph.js +53 -7
- package/lib/dist/server/routes/knowledge.js +118 -0
- package/lib/dist/server/routes/mcp.js +4 -3
- package/lib/dist/server/routes/projects.js +4 -4
- package/lib/dist/server/routes/spec.js +49 -7
- package/lib/dist/server/server.js +31 -17
- package/lib/dist/skills/spec-author/SKILL.md +164 -0
- package/lib/dist/skills/spec-author/references/format.md +210 -0
- package/lib/dist/skills/spec-author/references/gap-questions.md +152 -0
- package/lib/dist/skills/spec-author/references/quality-rubric.md +125 -0
- package/lib/dist/skills/spec-author/references/review-checklist.md +175 -0
- package/lib/dist/spec-lint/lint.d.ts +82 -0
- package/lib/dist/spec-lint/lint.d.ts.map +1 -0
- package/lib/dist/spec-lint/lint.js +491 -0
- package/lib/dist/spec-lint/lint.js.map +1 -0
- package/lib/dist/spec-quality/criterion-ids.d.ts +13 -0
- package/lib/dist/spec-quality/criterion-ids.d.ts.map +1 -0
- package/lib/dist/spec-quality/criterion-ids.js +38 -0
- package/lib/dist/spec-quality/criterion-ids.js.map +1 -0
- package/lib/dist/spec-quality/evidence-kind.d.ts +54 -0
- package/lib/dist/spec-quality/evidence-kind.d.ts.map +1 -0
- package/lib/dist/spec-quality/evidence-kind.js +183 -0
- package/lib/dist/spec-quality/evidence-kind.js.map +1 -0
- package/lib/dist/spec-quality/link-persist.d.ts +65 -0
- package/lib/dist/spec-quality/link-persist.d.ts.map +1 -0
- package/lib/dist/spec-quality/link-persist.js +249 -0
- package/lib/dist/spec-quality/link-persist.js.map +1 -0
- package/lib/dist/spec-quality/plan-scope.d.ts +38 -0
- package/lib/dist/spec-quality/plan-scope.d.ts.map +1 -0
- package/lib/dist/spec-quality/plan-scope.js +129 -0
- package/lib/dist/spec-quality/plan-scope.js.map +1 -0
- package/lib/dist/spec-quality/postcheck.d.ts +85 -0
- package/lib/dist/spec-quality/postcheck.d.ts.map +1 -0
- package/lib/dist/spec-quality/postcheck.js +390 -0
- package/lib/dist/spec-quality/postcheck.js.map +1 -0
- package/lib/dist/spec-quality/readiness.d.ts +72 -0
- package/lib/dist/spec-quality/readiness.d.ts.map +1 -0
- package/lib/dist/spec-quality/readiness.js +226 -0
- package/lib/dist/spec-quality/readiness.js.map +1 -0
- package/lib/dist/statusline/index.js.map +1 -1
- package/lib/dist/statusline/render.d.ts +1 -1
- package/lib/dist/statusline/render.d.ts.map +1 -1
- package/lib/dist/statusline/render.js +3 -1
- package/lib/dist/statusline/render.js.map +1 -1
- package/lib/dist/ui/assets/index-BzuUWUAE.js +50 -0
- package/lib/dist/ui/index.html +1 -1
- package/lib/dist/verify/report-ingest.d.ts +88 -0
- package/lib/dist/verify/report-ingest.d.ts.map +1 -0
- package/lib/dist/verify/report-ingest.js +269 -0
- package/lib/dist/verify/report-ingest.js.map +1 -0
- package/lib/dist/workflows/agents/claude.d.ts +16 -0
- package/lib/dist/workflows/agents/claude.d.ts.map +1 -0
- package/lib/dist/workflows/agents/claude.js +82 -0
- package/lib/dist/workflows/agents/claude.js.map +1 -0
- package/lib/dist/workflows/agents/gemini.d.ts +21 -0
- package/lib/dist/workflows/agents/gemini.d.ts.map +1 -0
- package/lib/dist/workflows/agents/gemini.js +124 -0
- package/lib/dist/workflows/agents/gemini.js.map +1 -0
- package/lib/dist/workflows/agents/index.d.ts +28 -0
- package/lib/dist/workflows/agents/index.d.ts.map +1 -0
- package/lib/dist/workflows/agents/index.js +104 -0
- package/lib/dist/workflows/agents/index.js.map +1 -0
- package/lib/dist/workflows/agents/types.d.ts +55 -0
- package/lib/dist/workflows/agents/types.d.ts.map +1 -0
- package/lib/dist/workflows/agents/types.js +46 -0
- package/lib/dist/workflows/agents/types.js.map +1 -0
- package/lib/dist/workflows/batch.d.ts +63 -0
- package/lib/dist/workflows/batch.d.ts.map +1 -0
- package/lib/dist/workflows/batch.js +151 -0
- package/lib/dist/workflows/batch.js.map +1 -0
- package/lib/dist/workflows/defaults/claude-design-implement.yaml +3 -4
- package/lib/dist/workflows/defaults/spec-author.yaml +11 -9
- package/lib/dist/workflows/defaults/spec-fix.yaml +103 -12
- package/lib/dist/workflows/defaults/spec-implement-mixed.yaml +21 -193
- package/lib/dist/workflows/defaults/spec-implement.yaml +701 -41
- package/lib/dist/workflows/defaults/spec-verify.yaml +60 -9
- package/lib/dist/workflows/discovery.d.ts +13 -0
- package/lib/dist/workflows/discovery.d.ts.map +1 -1
- package/lib/dist/workflows/discovery.js +79 -17
- package/lib/dist/workflows/discovery.js.map +1 -1
- package/lib/dist/workflows/executor.d.ts +18 -0
- package/lib/dist/workflows/executor.d.ts.map +1 -1
- package/lib/dist/workflows/executor.js +130 -33
- package/lib/dist/workflows/executor.js.map +1 -1
- package/lib/dist/workflows/gc.d.ts +50 -0
- package/lib/dist/workflows/gc.d.ts.map +1 -0
- package/lib/dist/workflows/gc.js +120 -0
- package/lib/dist/workflows/gc.js.map +1 -0
- package/lib/dist/workflows/merge-queue.d.ts +64 -0
- package/lib/dist/workflows/merge-queue.d.ts.map +1 -0
- package/lib/dist/workflows/merge-queue.js +162 -0
- package/lib/dist/workflows/merge-queue.js.map +1 -0
- package/lib/dist/workflows/runners/approval.d.ts +1 -1
- package/lib/dist/workflows/runners/approval.d.ts.map +1 -1
- package/lib/dist/workflows/runners/approval.js +11 -2
- package/lib/dist/workflows/runners/approval.js.map +1 -1
- package/lib/dist/workflows/runners/bash.d.ts +15 -0
- package/lib/dist/workflows/runners/bash.d.ts.map +1 -1
- package/lib/dist/workflows/runners/bash.js +50 -1
- package/lib/dist/workflows/runners/bash.js.map +1 -1
- package/lib/dist/workflows/runners/prompt.d.ts +16 -41
- package/lib/dist/workflows/runners/prompt.d.ts.map +1 -1
- package/lib/dist/workflows/runners/prompt.js +55 -120
- package/lib/dist/workflows/runners/prompt.js.map +1 -1
- package/lib/dist/workflows/runners/types.d.ts +12 -0
- package/lib/dist/workflows/runners/types.d.ts.map +1 -1
- package/lib/dist/workflows/schemas/workflow.d.ts +17 -0
- package/lib/dist/workflows/schemas/workflow.d.ts.map +1 -1
- package/lib/dist/workflows/schemas/workflow.js +27 -0
- package/lib/dist/workflows/schemas/workflow.js.map +1 -1
- package/lib/node_modules/.package-lock.json +1 -1
- package/lib/package.json +8 -4
- package/package.json +5 -3
- package/lib/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
- package/lib/dist/server/ingest/impact-query.js +0 -343
- package/lib/dist/server/routes/events.js +0 -152
- package/lib/dist/ui/assets/index-CjjaftDw.js +0 -50
- package/node.exe +0 -4
|
@@ -158,7 +158,8 @@ export async function registerGraphRoutes(app) {
|
|
|
158
158
|
/**
|
|
159
159
|
* GET /api/graph/health — feeds the Graph overview panel:
|
|
160
160
|
* - linkHealth: spec-link counts per state (verified/drifted/broken/orphaned/…)
|
|
161
|
-
* - edgeKinds:
|
|
161
|
+
* - edgeKinds: calls / implements-documents edge counts, plus `tests` —
|
|
162
|
+
* resolved test-evidence spec_links, not an edge bucket
|
|
162
163
|
* - hubs: the most-connected nodes (by total degree), for the "Most connected" list
|
|
163
164
|
* - anchored: nodes that are spec_links targets (by degree), for the "Anchored" list
|
|
164
165
|
*/
|
|
@@ -173,13 +174,12 @@ export async function registerGraphRoutes(app) {
|
|
|
173
174
|
}
|
|
174
175
|
const db = getDb(cg);
|
|
175
176
|
// Edge kinds — bucket by the node kinds the edge connects, mirroring the
|
|
176
|
-
// design's "calls / implements
|
|
177
|
-
//
|
|
177
|
+
// design's "calls / implements" legend. spec endpoints → implements,
|
|
178
|
+
// everything else → calls.
|
|
178
179
|
const edgeKinds = db.prepare(`
|
|
179
180
|
SELECT
|
|
180
181
|
CASE
|
|
181
182
|
WHEN ns.kind = 'spec' OR nt.kind = 'spec' THEN 'implements'
|
|
182
|
-
WHEN ns.kind = 'test' OR nt.kind = 'test' THEN 'tests'
|
|
183
183
|
ELSE 'calls'
|
|
184
184
|
END as bucket,
|
|
185
185
|
COUNT(*) as count
|
|
@@ -191,6 +191,16 @@ export async function registerGraphRoutes(app) {
|
|
|
191
191
|
const edgeKindMap = { calls: 0, implements: 0, tests: 0 };
|
|
192
192
|
for (const r of edgeKinds)
|
|
193
193
|
edgeKindMap[r.bucket] = r.count;
|
|
194
|
+
// Test evidence is a spec_links relationship, not an edge between node
|
|
195
|
+
// kinds — there is no `test` NodeKind, so the old `ns.kind = 'test'`
|
|
196
|
+
// bucket was permanently 0 (REQ-REVINT-008.A1). Count the resolved
|
|
197
|
+
// tests/validates links instead: a spec whose `verifies:` block points at
|
|
198
|
+
// a real symbol is exactly one test-evidence relationship.
|
|
199
|
+
const testEvidence = db.prepare(`
|
|
200
|
+
SELECT COUNT(*) as c FROM spec_links
|
|
201
|
+
WHERE kind IN ('tests', 'validates') AND resolved_node_id IS NOT NULL
|
|
202
|
+
`).get();
|
|
203
|
+
edgeKindMap['tests'] = testEvidence.c ?? 0;
|
|
194
204
|
// Synthesized (heuristic) edge count — dashed in the legend.
|
|
195
205
|
const synth = db.prepare(`SELECT COUNT(*) as c FROM edges WHERE provenance = 'heuristic'`).get();
|
|
196
206
|
edgeKindMap['synth'] = synth.c ?? 0;
|
|
@@ -236,6 +246,13 @@ export async function registerGraphRoutes(app) {
|
|
|
236
246
|
* among them. Isolated (degree-0) nodes are omitted — an overview shows the
|
|
237
247
|
* connected core. `total` reports the full node count so the UI can say
|
|
238
248
|
* "showing N of M".
|
|
249
|
+
*
|
|
250
|
+
* Traceability is unioned in on top of that cut (REQ-TVIZ-010.A1). A spec
|
|
251
|
+
* node's degree is 1–2, so the top-N pass pruned every spec↔code edge out of
|
|
252
|
+
* the whole-repo view: the one relationship the product exists to show was
|
|
253
|
+
* the first thing dropped. Spec nodes that participate in an edge — and the
|
|
254
|
+
* code nodes on the other end — are therefore kept regardless of degree. A
|
|
255
|
+
* spec node with no edges is still omitted; it would draw as an island.
|
|
239
256
|
*/
|
|
240
257
|
app.get('/api/graph/full', async (req, reply) => {
|
|
241
258
|
const cg = await resolveCg(app, req, reply);
|
|
@@ -255,15 +272,44 @@ export async function registerGraphRoutes(app) {
|
|
|
255
272
|
JOIN nodes n ON n.id = deg.node
|
|
256
273
|
ORDER BY deg.degree DESC
|
|
257
274
|
`).all(limit);
|
|
275
|
+
// Spec nodes with at least one edge, plus whatever sits at the other end
|
|
276
|
+
// of those edges — the traceability union (REQ-TVIZ-010.A1).
|
|
277
|
+
const traceRows = db.prepare(`
|
|
278
|
+
SELECT n.id, n.name, n.kind, n.file_path as filePath, COALESCE(deg.degree, 0) as degree
|
|
279
|
+
FROM nodes n
|
|
280
|
+
LEFT JOIN (
|
|
281
|
+
SELECT node, COUNT(*) as degree FROM (
|
|
282
|
+
SELECT source as node FROM edges
|
|
283
|
+
UNION ALL
|
|
284
|
+
SELECT target as node FROM edges
|
|
285
|
+
) GROUP BY node
|
|
286
|
+
) deg ON deg.node = n.id
|
|
287
|
+
WHERE n.id IN (
|
|
288
|
+
SELECT e.source FROM edges e JOIN nodes s ON s.id = e.source WHERE s.kind = 'spec'
|
|
289
|
+
UNION
|
|
290
|
+
SELECT e.target FROM edges e JOIN nodes t ON t.id = e.target WHERE t.kind = 'spec'
|
|
291
|
+
UNION
|
|
292
|
+
SELECT e.target FROM edges e JOIN nodes s ON s.id = e.source WHERE s.kind = 'spec'
|
|
293
|
+
UNION
|
|
294
|
+
SELECT e.source FROM edges e JOIN nodes t ON t.id = e.target WHERE t.kind = 'spec'
|
|
295
|
+
)
|
|
296
|
+
`).all();
|
|
258
297
|
const keep = new Set(nodeRows.map((n) => n.id));
|
|
298
|
+
const nodes = nodeRows.map((n) => ({ id: n.id, name: n.name, kind: n.kind, filePath: n.filePath, degree: n.degree }));
|
|
299
|
+
for (const n of traceRows) {
|
|
300
|
+
if (keep.has(n.id))
|
|
301
|
+
continue;
|
|
302
|
+
keep.add(n.id);
|
|
303
|
+
nodes.push({ id: n.id, name: n.name, kind: n.kind, filePath: n.filePath, degree: n.degree });
|
|
304
|
+
}
|
|
259
305
|
const allEdges = db.prepare(`SELECT source as "from", target as "to", kind, provenance FROM edges`).all();
|
|
260
306
|
const edges = allEdges.filter((e) => keep.has(e.from) && keep.has(e.to));
|
|
261
307
|
const totalRow = db.prepare(`SELECT COUNT(*) as c FROM nodes`).get();
|
|
262
308
|
return {
|
|
263
|
-
nodes
|
|
309
|
+
nodes,
|
|
264
310
|
edges,
|
|
265
|
-
total: totalRow.c ??
|
|
266
|
-
shown:
|
|
311
|
+
total: totalRow.c ?? nodes.length,
|
|
312
|
+
shown: nodes.length,
|
|
267
313
|
};
|
|
268
314
|
});
|
|
269
315
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/knowledge (REQ-KNOW-007).
|
|
3
|
+
*
|
|
4
|
+
* Read-only knowledge view: every domain fact with its id, title, type and the
|
|
5
|
+
* requirement ids it links to at the spec tier (`parentId` + `depends_on`),
|
|
6
|
+
* plus — for `type: external` facts — the dependency it is pinned to, the
|
|
7
|
+
* version it was verified against, and its freshness against the version
|
|
8
|
+
* installed in the project (`fresh` / `stale` / `unverifiable`).
|
|
9
|
+
*
|
|
10
|
+
* Like the domain route, it reaches the engine only through the dynamically
|
|
11
|
+
* loaded instance (`cg.getSpecQueries()`, `cg.getProjectRoot()`) and never
|
|
12
|
+
* runtime-imports the bare `@specship/specship` package (DOM-SPECSHIP-004).
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
function linkedIds(parentId, dependsOn) {
|
|
17
|
+
const out = [];
|
|
18
|
+
if (typeof parentId === 'string' && parentId.length > 0)
|
|
19
|
+
out.push(parentId);
|
|
20
|
+
if (Array.isArray(dependsOn)) {
|
|
21
|
+
for (const d of dependsOn)
|
|
22
|
+
if (typeof d === 'string' && d.length > 0)
|
|
23
|
+
out.push(d);
|
|
24
|
+
}
|
|
25
|
+
else if (typeof dependsOn === 'string' && dependsOn.length > 0) {
|
|
26
|
+
for (const d of dependsOn.split(','))
|
|
27
|
+
if (d.trim())
|
|
28
|
+
out.push(d.trim());
|
|
29
|
+
}
|
|
30
|
+
return [...new Set(out)];
|
|
31
|
+
}
|
|
32
|
+
/** Read one flat `key: value` from a markdown file's frontmatter. */
|
|
33
|
+
function frontmatterValue(file, key) {
|
|
34
|
+
let source;
|
|
35
|
+
try {
|
|
36
|
+
source = readFileSync(file, 'utf-8');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const lines = source.split(/\r?\n/);
|
|
42
|
+
if ((lines[0] ?? '').trim() !== '---')
|
|
43
|
+
return undefined;
|
|
44
|
+
for (let i = 1; i < lines.length; i++) {
|
|
45
|
+
const line = (lines[i] ?? '').trim();
|
|
46
|
+
if (line === '---')
|
|
47
|
+
break;
|
|
48
|
+
const idx = line.indexOf(':');
|
|
49
|
+
if (idx > 0 && line.slice(0, idx).trim() === key) {
|
|
50
|
+
return line.slice(idx + 1).trim().replace(/^(['"])(.*)\1$/, '$2') || undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
/** The version of `dependency` installed under `<projectRoot>/node_modules`. */
|
|
56
|
+
function installedVersion(projectRoot, dependency) {
|
|
57
|
+
if (dependency.includes('..'))
|
|
58
|
+
return undefined;
|
|
59
|
+
const pkg = path.join(projectRoot, 'node_modules', dependency, 'package.json');
|
|
60
|
+
if (!existsSync(pkg))
|
|
61
|
+
return undefined;
|
|
62
|
+
try {
|
|
63
|
+
const v = JSON.parse(readFileSync(pkg, 'utf-8')).version;
|
|
64
|
+
return typeof v === 'string' ? v : undefined;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export async function registerKnowledgeRoutes(app) {
|
|
71
|
+
app.get('/api/knowledge', async (req, reply) => {
|
|
72
|
+
const cg = await app.activeCg(req);
|
|
73
|
+
if (!cg) {
|
|
74
|
+
reply.code(409).send({ error: 'no project selected', code: 'no_project' });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const projectRoot = cg.getProjectRoot();
|
|
78
|
+
const facts = [];
|
|
79
|
+
for (const f of cg.getSpecQueries().getSpecsByKind('domain')) {
|
|
80
|
+
const meta = (f.metadata ?? {});
|
|
81
|
+
const type = typeof meta.type === 'string' ? meta.type : '';
|
|
82
|
+
const fact = {
|
|
83
|
+
id: f.id,
|
|
84
|
+
title: f.title,
|
|
85
|
+
type,
|
|
86
|
+
linkedRequirementIds: linkedIds(f.parentId, meta.depends_on),
|
|
87
|
+
};
|
|
88
|
+
if (type === 'external') {
|
|
89
|
+
const dependency = typeof meta.dependency === 'string' ? meta.dependency : undefined;
|
|
90
|
+
// `version` is a known top-level frontmatter key the extractor keeps as
|
|
91
|
+
// the numeric spec version, so a semver string may not survive into
|
|
92
|
+
// metadata — fall back to the fact's own frontmatter on disk.
|
|
93
|
+
const version = typeof meta.version === 'string'
|
|
94
|
+
? meta.version
|
|
95
|
+
: frontmatterValue(path.join(projectRoot, f.sourcePath), 'version');
|
|
96
|
+
// Freshness from the project's own node_modules only. The CLI's
|
|
97
|
+
// `specship knowledge stale` (findStaleExternalFacts) also consults the
|
|
98
|
+
// global npm root; the server stays local-only and never imports the
|
|
99
|
+
// engine package directly (DOM-SPECSHIP-004), so a dependency installed
|
|
100
|
+
// only globally reads `unverifiable` here — never `fresh`.
|
|
101
|
+
const installed = dependency ? installedVersion(projectRoot, dependency) : undefined;
|
|
102
|
+
fact.dependency = dependency;
|
|
103
|
+
fact.version = version;
|
|
104
|
+
fact.stale =
|
|
105
|
+
installed === undefined || version === undefined
|
|
106
|
+
? 'unverifiable'
|
|
107
|
+
: installed === version
|
|
108
|
+
? 'fresh'
|
|
109
|
+
: 'stale';
|
|
110
|
+
if (installed !== undefined)
|
|
111
|
+
fact.installedVersion = installed;
|
|
112
|
+
}
|
|
113
|
+
facts.push(fact);
|
|
114
|
+
}
|
|
115
|
+
facts.sort((a, b) => a.id.localeCompare(b.id));
|
|
116
|
+
return { facts };
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -400,17 +400,18 @@ function collectUsage(cg) {
|
|
|
400
400
|
if (!s || s.example)
|
|
401
401
|
continue;
|
|
402
402
|
try {
|
|
403
|
-
s.example = { tool, args: JSON.parse(row.input_json) };
|
|
403
|
+
s.example = { tool, args: JSON.parse(row.input_json), synthesized: false };
|
|
404
404
|
}
|
|
405
405
|
catch { /* skip bad row */ }
|
|
406
406
|
}
|
|
407
|
-
// Fallback example: the server's top tool with empty args.
|
|
407
|
+
// Fallback example: the server's top tool with empty args. Flagged
|
|
408
|
+
// synthesized so the page can't pass it off as a recorded call.
|
|
408
409
|
for (const s of byServer.values()) {
|
|
409
410
|
if (s.example)
|
|
410
411
|
continue;
|
|
411
412
|
const top = [...s.tools.entries()].sort((a, b) => b[1].calls - a[1].calls)[0];
|
|
412
413
|
if (top)
|
|
413
|
-
s.example = { tool: top[0], args: {} };
|
|
414
|
+
s.example = { tool: top[0], args: {}, synthesized: true };
|
|
414
415
|
}
|
|
415
416
|
return byServer;
|
|
416
417
|
}
|
|
@@ -79,10 +79,10 @@ export async function enumerate(claudeRoot, resolverOpts) {
|
|
|
79
79
|
return out;
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
* Bound on how many projects get a live driftCount per request
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
82
|
+
* Bound on how many projects get a live driftCount per request: every lookup
|
|
83
|
+
* opens the project's graph through the registry LRU (maxOpen 16), so an
|
|
84
|
+
* unbounded pass over a large project list churns open/close cycles on every
|
|
85
|
+
* GET. enumerate() sorts by recency, so
|
|
86
86
|
* the bounded prefix is exactly what the switcher surfaces first; the rest
|
|
87
87
|
* stay `null` (unknown), which the UI renders as no pill.
|
|
88
88
|
*/
|
|
@@ -130,6 +130,24 @@ export async function registerSpecRoutes(app) {
|
|
|
130
130
|
return;
|
|
131
131
|
return cg.getSpecFunnel();
|
|
132
132
|
});
|
|
133
|
+
/**
|
|
134
|
+
* GET /api/spec/coverage[?spec=<id>] — the test-coverage rollup
|
|
135
|
+
* (REQ-VSTATE-006.A2).
|
|
136
|
+
*
|
|
137
|
+
* Registered before `/api/spec/:id` and matched statically (same shape as
|
|
138
|
+
* `/api/spec/funnel`), so `coverage` is never read as a spec id. Computed on
|
|
139
|
+
* the dynamically-loaded instance — no runtime import of the package here.
|
|
140
|
+
*
|
|
141
|
+
* The verdicts come from `tests` links only, which is the point: the
|
|
142
|
+
* dashboard previously counted an `implements` link as "met".
|
|
143
|
+
*/
|
|
144
|
+
app.get('/api/spec/coverage', async (req, reply) => {
|
|
145
|
+
const cg = await resolveCg(app, req, reply);
|
|
146
|
+
if (!cg)
|
|
147
|
+
return;
|
|
148
|
+
const scope = req.query.spec && req.query.spec.length > 0 ? req.query.spec : undefined;
|
|
149
|
+
return cg.getSpecCoverage(scope);
|
|
150
|
+
});
|
|
133
151
|
app.get('/api/spec/:id', async (req, reply) => {
|
|
134
152
|
const cg = await resolveCg(app, req, reply);
|
|
135
153
|
if (!cg)
|
|
@@ -244,25 +262,32 @@ export async function registerSpecRoutes(app) {
|
|
|
244
262
|
const spec = sq.getSpecById(body.spec_id);
|
|
245
263
|
if (!spec)
|
|
246
264
|
return reply.code(404).send({ error: 'spec not found' });
|
|
265
|
+
// Validate the target against the graph before recording (REQ-REVINT-003.A3
|
|
266
|
+
// — same rule as the MCP tool). The dashboard's caller can't retry from a
|
|
267
|
+
// refusal the way an agent can, so an unresolvable target is recorded as
|
|
268
|
+
// `orphaned` and reported back as such — never as `implemented`.
|
|
269
|
+
const nodeKind = body.target_node_kind ?? 'function';
|
|
270
|
+
const node = cg
|
|
271
|
+
.getSpecLinkResolver()
|
|
272
|
+
.findLogicalTarget(body.target_file_path, body.target_qualified_name, nodeKind);
|
|
247
273
|
const now = Date.now();
|
|
248
274
|
const id = sq.upsertSpecLink({
|
|
249
275
|
specId: body.spec_id,
|
|
250
276
|
targetFilePath: body.target_file_path,
|
|
251
277
|
targetQualifiedName: body.target_qualified_name,
|
|
252
|
-
targetNodeKind:
|
|
253
|
-
resolvedNodeId:
|
|
278
|
+
targetNodeKind: nodeKind,
|
|
279
|
+
resolvedNodeId: node?.id,
|
|
254
280
|
kind: body.kind ?? 'implements',
|
|
255
|
-
state: 'implemented',
|
|
281
|
+
state: node ? 'implemented' : 'orphaned',
|
|
256
282
|
driftAxis: null,
|
|
257
283
|
specHashAtLink: spec.contentHash,
|
|
258
|
-
nodeSigAtLink:
|
|
284
|
+
nodeSigAtLink: node?.signature,
|
|
259
285
|
provenance: 'agent-asserted',
|
|
260
286
|
confidence: 1.0,
|
|
261
287
|
createdAt: now,
|
|
262
288
|
updatedAt: now,
|
|
263
289
|
});
|
|
264
|
-
|
|
265
|
-
return { id, ok: true };
|
|
290
|
+
return { id, ok: true, resolved: node !== null, state: node ? 'implemented' : 'orphaned' };
|
|
266
291
|
});
|
|
267
292
|
app.post('/api/spec/link-verify', async (req, reply) => {
|
|
268
293
|
const cg = await resolveCg(app, req, reply);
|
|
@@ -280,8 +305,13 @@ export async function registerSpecRoutes(app) {
|
|
|
280
305
|
// tool: promoting an `implements` link to verified requires the spec to
|
|
281
306
|
// have at least one test-evidence link (kind='tests'). Without this the
|
|
282
307
|
// dashboard's Verify button would bypass the gate.
|
|
308
|
+
// File-derived evidence only (REQ-REVINT-005.A2): an agent-asserted `tests`
|
|
309
|
+
// link is a DB-only row, so it can't underwrite a promotion.
|
|
283
310
|
if (body.result === 'pass' && link.kind === 'implements') {
|
|
284
|
-
const hasEvidence = sq
|
|
311
|
+
const hasEvidence = sq
|
|
312
|
+
.getLinksBySpec(link.specId)
|
|
313
|
+
.some((l) => l.kind === 'tests' &&
|
|
314
|
+
(l.provenance === 'spec-declaration' || l.provenance === 'code-comment'));
|
|
285
315
|
if (!hasEvidence) {
|
|
286
316
|
return reply.code(409).send({
|
|
287
317
|
error: `no test evidence linked to ${link.specId} — declare its proving test via a "verifies:" block in the spec (or an @verifies comment on the test) before promoting to verified`,
|
|
@@ -290,6 +320,18 @@ export async function registerSpecRoutes(app) {
|
|
|
290
320
|
}
|
|
291
321
|
}
|
|
292
322
|
sq.updateSpecLinkState(body.link_id, body.result === 'pass' ? 'verified' : 'broken', null);
|
|
323
|
+
// Persist the evidence behind the verdict (REQ-REVINT-004) — same record
|
|
324
|
+
// shape as the MCP tool, so both surfaces render one audit trail.
|
|
325
|
+
const verification = {
|
|
326
|
+
result: body.result,
|
|
327
|
+
verifiedAt: new Date().toISOString(),
|
|
328
|
+
};
|
|
329
|
+
if (body.reason)
|
|
330
|
+
verification.reason = body.reason;
|
|
331
|
+
const evidence = (body.evidence ?? []).filter((e) => typeof e === 'string' && e.length > 0);
|
|
332
|
+
if (evidence.length > 0)
|
|
333
|
+
verification.evidence = evidence;
|
|
334
|
+
sq.updateSpecLinkMetadata(body.link_id, { ...(link.metadata ?? {}), verification });
|
|
293
335
|
return { ok: true, state: body.result === 'pass' ? 'verified' : 'broken' };
|
|
294
336
|
});
|
|
295
337
|
/**
|
|
@@ -17,7 +17,6 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
17
17
|
import Fastify from 'fastify';
|
|
18
18
|
import cors from '@fastify/cors';
|
|
19
19
|
import { startWatcher, primaryProjectMatcher } from './ingest/index.js';
|
|
20
|
-
import { backfillDisplaced } from './ingest/impact-backfill.js';
|
|
21
20
|
import { recostUnpricedPrompts } from './ingest/pricing-backfill.js';
|
|
22
21
|
import { ProjectRegistry } from './project-registry.js';
|
|
23
22
|
import { makeStaticHandler, isAssetPath } from './static-handler.js';
|
|
@@ -28,10 +27,10 @@ import { registerClaudeRoutes } from './routes/claude.js';
|
|
|
28
27
|
import { registerStatusRoutes } from './routes/status.js';
|
|
29
28
|
import { registerMemoryRoutes } from './routes/memory.js';
|
|
30
29
|
import { registerProjectsRoutes } from './routes/projects.js';
|
|
31
|
-
import { registerEventsRoutes } from './routes/events.js';
|
|
32
30
|
import { registerReflectRoutes } from './routes/reflect.js';
|
|
33
31
|
import { registerMaintainabilityRoutes } from './routes/maintainability.js';
|
|
34
32
|
import { registerDomainRoutes } from './routes/domain.js';
|
|
33
|
+
import { registerKnowledgeRoutes } from './routes/knowledge.js';
|
|
35
34
|
import { registerMcpRoutes } from './routes/mcp.js';
|
|
36
35
|
import { registerConfigRoutes } from './routes/config.js';
|
|
37
36
|
import { readServerConfig } from './server-config.js';
|
|
@@ -112,12 +111,36 @@ export async function createServer(options) {
|
|
|
112
111
|
credentials: false,
|
|
113
112
|
});
|
|
114
113
|
}
|
|
114
|
+
// Test-only fault injection (REQ-TVIZ-004.A2). `E2E_FAULT` holds a
|
|
115
|
+
// comma-separated list of `/api` path substrings; a matching request is
|
|
116
|
+
// answered 500 instead of reaching its handler, so the e2e suite can prove
|
|
117
|
+
// the SPA degrades to an error state on any route without a code change per
|
|
118
|
+
// test. The hook is registered ONLY when the variable is set — with it unset
|
|
119
|
+
// (every production boot) there is no hook and no per-request work at all.
|
|
120
|
+
const faultPatterns = (process.env.E2E_FAULT ?? '')
|
|
121
|
+
.split(',')
|
|
122
|
+
.map((s) => s.trim())
|
|
123
|
+
.filter(Boolean);
|
|
124
|
+
if (faultPatterns.length > 0) {
|
|
125
|
+
console.error(`[specship-server] E2E_FAULT active — 500ing /api paths matching: ${faultPatterns.join(', ')}`);
|
|
126
|
+
app.addHook('onRequest', async (req, reply) => {
|
|
127
|
+
const pathOnly = req.url.split('?')[0] ?? req.url;
|
|
128
|
+
if (!pathOnly.startsWith('/api/'))
|
|
129
|
+
return;
|
|
130
|
+
const hit = faultPatterns.find((p) => pathOnly.includes(p));
|
|
131
|
+
if (!hit)
|
|
132
|
+
return;
|
|
133
|
+
await reply.code(500).send({ error: `injected fault (E2E_FAULT=${hit})`, code: 'e2e_fault' });
|
|
134
|
+
});
|
|
135
|
+
}
|
|
115
136
|
// Lazy-load specship. Used as the open() impl for the registry, and
|
|
116
137
|
// (only when a primary path is set) to open the primary instance below.
|
|
117
138
|
const cgMod = await loadSpecShip();
|
|
118
|
-
// maxOpen must exceed the /api/
|
|
119
|
-
//
|
|
120
|
-
//
|
|
139
|
+
// maxOpen must exceed the /api/projects drift sweep size
|
|
140
|
+
// (DRIFT_SWEEP_LIMIT = 10 in routes/projects.ts) plus the primary and a
|
|
141
|
+
// user-picked project, or the sweep churns open/close cycles through the
|
|
142
|
+
// LRU on every request. (The removed cross-project alert poller swept the
|
|
143
|
+
// same width — REQ-SURF-001.A2.)
|
|
121
144
|
const registry = new ProjectRegistry({ verbose, maxOpen: 16 }, (p) => cgMod.SpecShip.open(p));
|
|
122
145
|
// Primary project (optional). When set, specship-scoped routes default to
|
|
123
146
|
// it when no `?project=<slug>` is provided, and the analytics ingest
|
|
@@ -171,17 +194,8 @@ export async function createServer(options) {
|
|
|
171
194
|
ownedWatcher = true;
|
|
172
195
|
if (verbose)
|
|
173
196
|
console.error('[specship-server] JSONL ingest watcher started');
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
// a failure here must never abort server startup.
|
|
177
|
-
try {
|
|
178
|
-
backfillDisplaced(dbHandle, resolveGraph);
|
|
179
|
-
if (verbose)
|
|
180
|
-
console.error('[specship-server] specship-impact backfill complete');
|
|
181
|
-
}
|
|
182
|
-
catch (err) {
|
|
183
|
-
console.error('[specship-server] specship-impact backfill failed (non-fatal):', err instanceof Error ? err.message : String(err));
|
|
184
|
-
}
|
|
197
|
+
// The specship-impact boot backfill is gone with the engine it fed
|
|
198
|
+
// (REQ-SURF-005) — nothing reads displaced_files/resolution any more.
|
|
185
199
|
// Re-cost prompts ingested while their model family was unpriced,
|
|
186
200
|
// e.g. fable sessions stuck at $0 (REQ-DASHINT-001.A2). Idempotent.
|
|
187
201
|
try {
|
|
@@ -259,10 +273,10 @@ export async function createServer(options) {
|
|
|
259
273
|
await registerClaudeRoutes(app);
|
|
260
274
|
await registerMemoryRoutes(app);
|
|
261
275
|
await registerProjectsRoutes(app);
|
|
262
|
-
await registerEventsRoutes(app);
|
|
263
276
|
await registerReflectRoutes(app);
|
|
264
277
|
await registerMaintainabilityRoutes(app);
|
|
265
278
|
await registerDomainRoutes(app);
|
|
279
|
+
await registerKnowledgeRoutes(app);
|
|
266
280
|
// Chat routes removed (CHAT-REMOVE-DOC) — /api/chat and /api/chat/stream
|
|
267
281
|
// intentionally 404; the reviewer loop lives on Workflows + Runs.
|
|
268
282
|
await registerMcpRoutes(app);
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spec-author
|
|
3
|
+
description: Author SpecShip-compatible specs from a feature description — draft the markdown, walk a gap-filling checklist, run a quality review, and write the file under `specs/`. Use this whenever the user wants to write a new requirement, draft a spec for a feature they're about to build, capture acceptance criteria, define a contract, or asks "what should the spec for X look like." Also trigger on phrases like "write a spec", "draft a requirement", "create a REQ", "new spec for", "I want to add a feature and capture the requirements", "spec out", "let's write the requirements for". The skill produces a spec that `specship sync` parses cleanly, grounded in the project's existing code via `mcp__specship__specship_explore`. Do NOT use this skill for reverse-engineering existing code into specs, or for implementing an already-written spec (use `/specship:spec implement` for that).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Spec Author
|
|
7
|
+
|
|
8
|
+
You're helping the user write a SpecShip-compatible Markdown spec from scratch. Your job is to produce a draft that (a) parses cleanly under `specship sync`, (b) follows the project's quality rubric, and (c) is grounded in the project's actual code so the resulting implementation has a real target.
|
|
9
|
+
|
|
10
|
+
The user usually starts with a one-line feature description. You're going to turn that into a complete spec, asking only the questions the description doesn't already answer.
|
|
11
|
+
|
|
12
|
+
## What this skill does NOT do
|
|
13
|
+
|
|
14
|
+
- **It does not invent business motivation.** If the user doesn't tell you why a feature is needed, don't make it up. Mark the purpose as `[needs user confirmation]` instead.
|
|
15
|
+
- **It does not implement code.** Spec only. Implementation is `/specship:spec implement`'s job.
|
|
16
|
+
- **It does not promise a perfect spec.** Specs are negotiable; the skill produces a first draft that the user reviews. Every uncertain section carries a `[needs review]` flag so the user knows where to focus.
|
|
17
|
+
- **It does not bulk-author.** One requirement, or one document with N children, per invocation. "Spec the whole app" gets refused.
|
|
18
|
+
|
|
19
|
+
## The format your output must follow
|
|
20
|
+
|
|
21
|
+
Read `references/format.md` before writing the file. The spec parser at `src/extraction/specs/markdown-spec-extractor.ts` is strict about one thing: **every heading must have an `<!-- id: REQ-X -->` comment immediately above it**. Get this wrong and `specship sync` errors out.
|
|
22
|
+
|
|
23
|
+
Short version:
|
|
24
|
+
|
|
25
|
+
```markdown
|
|
26
|
+
---
|
|
27
|
+
id: AUTH-DOC
|
|
28
|
+
title: Authentication
|
|
29
|
+
owner: security
|
|
30
|
+
priority: high
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<!-- id: AUTH-DOC -->
|
|
34
|
+
# Authentication
|
|
35
|
+
|
|
36
|
+
This document covers login, session management, and rate-limiting.
|
|
37
|
+
|
|
38
|
+
<!-- id: REQ-AUTH-001 -->
|
|
39
|
+
## Failed logins MUST be rate-limited
|
|
40
|
+
|
|
41
|
+
The login endpoint rejects more than 5 failed attempts per IP per minute.
|
|
42
|
+
|
|
43
|
+
implementations:
|
|
44
|
+
- src/auth/login.ts:authenticate
|
|
45
|
+
- src/auth/rate-limit.ts:enforce
|
|
46
|
+
|
|
47
|
+
verifies:
|
|
48
|
+
- __tests__/auth-rate-limit.test.ts:rejectsSixthAttempt
|
|
49
|
+
|
|
50
|
+
## Acceptance
|
|
51
|
+
<!-- id: REQ-AUTH-001.A1 -->
|
|
52
|
+
- A 6th failed attempt within 60 s from the same IP returns 429 with `Retry-After`.
|
|
53
|
+
<!-- id: REQ-AUTH-001.A2 -->
|
|
54
|
+
- A successful login resets the counter for that IP.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`references/format.md` covers the edge cases: optional frontmatter fields, the `implementations:` and `verifies:` block syntax, hierarchy by heading depth, kind inference (document / requirement / acceptance), `.A1`-style child IDs for acceptance bullets.
|
|
58
|
+
|
|
59
|
+
## The loop
|
|
60
|
+
|
|
61
|
+
### Step 1 — Scope check
|
|
62
|
+
|
|
63
|
+
Confirm the user wants:
|
|
64
|
+
- **One requirement** under an existing document (most common — pick this if unsure), or
|
|
65
|
+
- **A new document with N child requirements** (when they're spec'ing a whole feature area), or
|
|
66
|
+
- **Refine an existing draft** (they paste markdown; you review and gap-fill).
|
|
67
|
+
|
|
68
|
+
If they say "spec the whole app", refuse and ask them to pick a feature area. Bulk-authored specs without per-REQ thinking are unreviewable.
|
|
69
|
+
|
|
70
|
+
### Step 2 — Ground in the code
|
|
71
|
+
|
|
72
|
+
If `mcp__specship__specship_explore` is available, call it on terms from the feature description before drafting. Two things this gives you:
|
|
73
|
+
|
|
74
|
+
- **Realistic file paths and symbol names** for the `implementations:` block. Don't write `implementations: - src/auth.ts:login` if the project has `src/services/auth-service.ts:AuthService.handleLogin`.
|
|
75
|
+
- **Existing conventions to align with.** If the codebase already has rate-limiting, your draft should reference the existing rate limiter rather than inventing a new one.
|
|
76
|
+
|
|
77
|
+
Skip this step only if (a) you don't have specship tools available, or (b) the feature is greenfield (no existing code to ground in).
|
|
78
|
+
|
|
79
|
+
### Step 3 — Draft the spec
|
|
80
|
+
|
|
81
|
+
Produce the full markdown body. Cover:
|
|
82
|
+
|
|
83
|
+
- **Frontmatter** (optional but recommended) — `id`, `title`, `owner`, `priority`.
|
|
84
|
+
- **Document body or single requirement** — depending on scope. Use RFC 2119 keywords (MUST / SHOULD / MAY) in the requirement title.
|
|
85
|
+
- **Acceptance criteria** — under a `## Acceptance` heading, one bullet per testable condition, each with its own `.A<N>` ID. Each bullet must be independently checkable — "MUST reject more than 5 attempts" not "MUST rate-limit".
|
|
86
|
+
- **`implementations:` block** — when you have grounding from Step 2, list the file paths + symbol names the implementation will live in. Speculative; the spec writer can adjust before implementation runs.
|
|
87
|
+
- **`verifies:` block** — the test-evidence declaration. Leave it out of a fresh spec unless the tests already exist; it's what promotes a link to `verified`.
|
|
88
|
+
- **`[needs user confirmation]` flags** — wherever you made an assumption (owner, priority, edge case behavior, exact threshold). Don't ask the user every question in one pass; mark the gap in the draft and surface it in Step 4.
|
|
89
|
+
|
|
90
|
+
Apply `references/quality-rubric.md`: no implementation leak, one concern per REQ, testable bullets, parent/child structure, RFC 2119 keywords used correctly.
|
|
91
|
+
|
|
92
|
+
### Step 4 — Gap-fill
|
|
93
|
+
|
|
94
|
+
Walk the checklist in `references/gap-questions.md` against your draft. Most categories — failure modes, TTLs, concurrency, observability, security, ownership — will already be answered by Steps 2–3. **Ask the user only about the gaps that remain**, no more than 3–5 questions in one round.
|
|
95
|
+
|
|
96
|
+
The catalog's **Project invariants** section is the one part you should always walk: a spec that contradicts a house invariant gets rejected at review, no matter how well-formed it is.
|
|
97
|
+
|
|
98
|
+
Phrase questions so the user can answer in one line each:
|
|
99
|
+
|
|
100
|
+
> A few details to nail down before I write the file:
|
|
101
|
+
> 1. Owner — `security`, or someone else?
|
|
102
|
+
> 2. When the 5-attempt threshold is hit, do we just return 429, or also fire a webhook / log?
|
|
103
|
+
> 3. Counter reset on successful login — only for that IP, or for all sessions on the account?
|
|
104
|
+
|
|
105
|
+
Update the draft with their answers. If they say "skip these for now," leave the markers in and proceed.
|
|
106
|
+
|
|
107
|
+
### Step 5 — Review
|
|
108
|
+
|
|
109
|
+
Run `references/review-checklist.md` against the final draft. Surface findings as a short numbered list:
|
|
110
|
+
|
|
111
|
+
> Quality review:
|
|
112
|
+
> 1. REQ-AUTH-001.A1 — could be more specific. Suggest naming the response header (`Retry-After`) and the duration.
|
|
113
|
+
> 2. The `implementations:` block points at `src/auth/login.ts:authenticate` — that symbol exists per `specship_node` ✓.
|
|
114
|
+
> 3. No explicit acceptance criterion for "counter reset on successful login" — add as A3?
|
|
115
|
+
|
|
116
|
+
Apply the user's choices to the draft.
|
|
117
|
+
|
|
118
|
+
### Step 6 — Write the file
|
|
119
|
+
|
|
120
|
+
Use the `Write` tool against `<project-root>/specs/<slug>.md`. Choose the slug from the feature name (kebab-case, no date prefix — the date is in git). If a file with that name already exists, append to it (with the new REQ markers) rather than overwriting; tell the user what you did.
|
|
121
|
+
|
|
122
|
+
After writing:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
Wrote specs/<slug>.md.
|
|
126
|
+
|
|
127
|
+
Next steps:
|
|
128
|
+
1. Review the file (a few `[needs review]` flags remain if you skipped questions in Step 4).
|
|
129
|
+
2. specship sync # picks up the new spec into the graph
|
|
130
|
+
3. /specship:spec implement <REQ-ID> # when you're ready to build it
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## When to use the workflow instead of the skill directly
|
|
134
|
+
|
|
135
|
+
This skill is the **lightweight path** — runs conversationally in Claude Code, no formal approval gates. For team contexts that want the same discipline `spec-implement` enforces, point the user at:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
specship workflow run spec-author --input DESCRIPTION="..."
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
That workflow wraps this same loop with two approval gates — one after gap-fill, one before file write — and a git-worktree-isolated artifact directory. Use it when (a) the spec author is reviewing a teammate's draft, or (b) the team has agreed every new spec gets a written review step.
|
|
142
|
+
|
|
143
|
+
## Anti-patterns
|
|
144
|
+
|
|
145
|
+
- **Writing the spec without grounding in code.** The result is a beautiful document that points at symbols which don't exist. Use `specship_explore` first.
|
|
146
|
+
- **Asking the user every question in the catalog.** Five questions is plenty per round; the rest become `[needs review]` markers the user resolves at their pace.
|
|
147
|
+
- **Specifying the implementation.** "MUST use JWT signed with RS256" is implementation. "MUST be cryptographically tamper-evident with a 24 h TTL" is contract. The skill steers toward contract; review and rewrite if you find yourself naming algorithms.
|
|
148
|
+
- **One REQ that does too much.** "MUST handle login, logout, and password reset" is three REQs. Split before drafting.
|
|
149
|
+
- **Forgetting the embedded `<!-- id: -->` markers.** Every heading needs one immediately above. Without it `specship sync` fails the file with `spec_missing_id`.
|
|
150
|
+
- **Reusing an ID already used in the same file.** The later occurrence wins (the store does `INSERT OR REPLACE`), so the earlier heading's content is silently overwritten.
|
|
151
|
+
- **Using today's date in the slug.** The date lives in git; the slug should be the feature name (`payment-retry.md`, not `2026-06-18-payment-retry.md`).
|
|
152
|
+
|
|
153
|
+
## Reference files
|
|
154
|
+
|
|
155
|
+
The references/ directory is organized by stage:
|
|
156
|
+
|
|
157
|
+
| File | Stage | What it covers |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `references/format.md` | Step 3 (draft) | Exact markdown the parser accepts |
|
|
160
|
+
| `references/quality-rubric.md` | Step 3 (draft) | What makes a "good" spec |
|
|
161
|
+
| `references/gap-questions.md` | Step 4 (gap-fill) | Catalog of questions to walk, plus the project invariants |
|
|
162
|
+
| `references/review-checklist.md` | Step 5 (review) | What to scan for before finalizing |
|
|
163
|
+
|
|
164
|
+
Read the relevant reference before each step. Don't paraphrase from memory — the format file in particular is the parser's contract.
|