@selvakumaresra/specship 0.4.0 → 0.6.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.
Files changed (57) hide show
  1. package/README.md +17 -7
  2. package/commands/ss-brainstorm.md +68 -0
  3. package/dist/analytics/specship-impact.d.ts +72 -0
  4. package/dist/analytics/specship-impact.d.ts.map +1 -0
  5. package/dist/analytics/specship-impact.js +216 -0
  6. package/dist/analytics/specship-impact.js.map +1 -0
  7. package/dist/bin/specship.js +177 -4
  8. package/dist/bin/specship.js.map +1 -1
  9. package/dist/db/migrations.d.ts +1 -1
  10. package/dist/db/migrations.d.ts.map +1 -1
  11. package/dist/db/migrations.js +15 -1
  12. package/dist/db/migrations.js.map +1 -1
  13. package/dist/db/schema.sql +8 -0
  14. package/dist/extraction/specs/markdown-spec-extractor.d.ts +19 -0
  15. package/dist/extraction/specs/markdown-spec-extractor.d.ts.map +1 -1
  16. package/dist/extraction/specs/markdown-spec-extractor.js +132 -11
  17. package/dist/extraction/specs/markdown-spec-extractor.js.map +1 -1
  18. package/dist/index.d.ts +37 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +64 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/installer/index.d.ts +2 -2
  23. package/dist/installer/index.d.ts.map +1 -1
  24. package/dist/installer/targets/claude.d.ts.map +1 -1
  25. package/dist/installer/targets/claude.js +2 -0
  26. package/dist/installer/targets/claude.js.map +1 -1
  27. package/dist/mcp/spec-tools.d.ts.map +1 -1
  28. package/dist/mcp/spec-tools.js +87 -6
  29. package/dist/mcp/spec-tools.js.map +1 -1
  30. package/dist/resolution/brief-link-resolver.d.ts +114 -0
  31. package/dist/resolution/brief-link-resolver.d.ts.map +1 -0
  32. package/dist/resolution/brief-link-resolver.js +261 -0
  33. package/dist/resolution/brief-link-resolver.js.map +1 -0
  34. package/dist/server/ingest/impact-backfill.js +69 -0
  35. package/dist/server/ingest/impact-query.js +343 -0
  36. package/dist/server/ingest/index.js +2 -1
  37. package/dist/server/ingest/ingestor.js +41 -6
  38. package/dist/server/ingest/specship-classify.js +153 -0
  39. package/dist/server/routes/claude.js +32 -0
  40. package/dist/server/routes/spec.js +103 -0
  41. package/dist/server/server.js +26 -2
  42. package/dist/types.d.ts +1 -1
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js +4 -0
  45. package/dist/types.js.map +1 -1
  46. package/dist/web/chunk-JQ534IB6.js +6 -0
  47. package/dist/web/chunk-O7434ZMN.js +1 -0
  48. package/dist/web/chunk-RASJHUXS.js +1 -0
  49. package/dist/web/chunk-TQ3P2QZO.js +1 -0
  50. package/dist/web/chunk-WCHGDXWC.js +1 -0
  51. package/dist/web/index.html +1 -1
  52. package/dist/web/main-QAP4FTDP.js +1 -0
  53. package/package.json +1 -1
  54. package/dist/web/chunk-2YUJNZ2Y.js +0 -6
  55. package/dist/web/chunk-B3YPFY6A.js +0 -1
  56. package/dist/web/chunk-GWPVKJIY.js +0 -1
  57. package/dist/web/main-R53HA54V.js +0 -1
package/README.md CHANGED
@@ -24,7 +24,7 @@
24
24
  Requires Node.js 22.5+ (Node 24.x recommended — its bundled SQLite has FTS5, which SpecShip needs):
25
25
 
26
26
  ```bash
27
- npm i -g @selvakumaresra/specship@0.1.0
27
+ npm i -g @selvakumaresra/specship@0.5.0
28
28
  ```
29
29
 
30
30
  Offline / air-gapped client workstation? Clone or copy this repo onto the machine and run:
@@ -121,7 +121,7 @@ When Claude Code explores a codebase, it spawns **Explore agents** that scan fil
121
121
  | **Always Fresh** | File watcher uses native OS events (FSEvents/inotify/ReadDirectoryChangesW) with debounced auto-sync — the graph stays current as you code, zero config |
122
122
  | **20+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Objective-C, Swift, Kotlin, Dart, Lua, Luau, Svelte, Liquid, Pascal/Delphi |
123
123
  | **Framework-aware Routes** | Recognizes web-framework routing files and links URL patterns to their handlers across 14 frameworks |
124
- | **Desktop UI** | Live dashboard for cost, drift, tool-call heatmap, cache analytics, plus a project picker that auto-discovers every project you've used Claude Code in |
124
+ | **Desktop UI** | Live dashboard for cost, drift, tool-call heatmap, cache analytics, SpecShip token impact, plus a project picker that auto-discovers every project you've used Claude Code in |
125
125
  | **100% Local** | No data leaves your machine. No API keys. No external services. SQLite database only |
126
126
 
127
127
  <details>
@@ -207,6 +207,15 @@ One big number — your **cache read rate** — plus a 2×2 breakdown: creation
207
207
 
208
208
  > **Project & time scope.** Every numeric on this page respects the project picker's active selection and the range selector top-right. Switching projects re-fetches without a page reload.
209
209
 
210
+ #### SpecShip Impact · is SpecShip earning its keep?
211
+
212
+ A dedicated page that puts SpecShip's own token cost next to what it saved, per prompt → session → project → all-projects (the Session Detail page carries the same per-prompt chip and per-session line). Two numbers, deliberately kept apart:
213
+
214
+ - **Spend — measured.** The exact tokens SpecShip's own tool calls put into the conversation.
215
+ - **Saved — estimated.** For each code-graph query, the size of the files its symbols live in — what a `Read` of them would have cost — deduplicated per prompt.
216
+
217
+ **"Saved" is a conservative lower bound.** It credits only a single direct read of the named files, *not* the multi-call grep + read exploration (re-reads, dead-ends, extra turns) SpecShip actually replaces — the end-to-end win that the [benchmarks](#why-specship) measure with a with-vs-without comparison. So treat spend as exact and saved as a floor; a fixed per-session tool-definition overhead is shown separately, and every estimate is marked `est.`.
218
+
210
219
  #### Beyond the dashboard
211
220
 
212
221
  Same shell, same shortcuts, more depth:
@@ -219,6 +228,7 @@ Same shell, same shortcuts, more depth:
219
228
  | **Workflows + Runs** | Run any bundled or project-tier workflow, watch its DAG advance live via SSE, approve / reject pause gates, inspect per-step artifacts (plan.md, diff.md, test_results.md). |
220
229
  | **Chat** | A codegraph-aware companion chat with slash commands, collapsible tool calls, and per-turn cost footers. |
221
230
  | **Sessions / Heatmap / Costs / Compare / Tips** | The Claude Code analytics suite — sessions deep-dive with per-prompt expand, file/tool/subagent heatmap drilldowns, per-day cost line + by-model donut, cross-project comparison. |
231
+ | **SpecShip Impact** | Measured tokens SpecShip's tools spent vs. an estimated (conservative) count of tokens saved, per prompt / session / project / all-projects, with a spend-vs-saved trend and per-tool breakdown. |
222
232
  | **Memory** | The full `CLAUDE.md` hierarchy (managed / user / project / subdir) plus `@import` resolution plus `~/.claude/memory/*.md` agent-written notes. Sources view + Effective (merged-precedence) view. |
223
233
  | **Design system** | The visual reference for the entire app — every token, palette swatch, semantic state, button/pill family, and the 4 px node-color legibility test that the graph relies on. |
224
234
 
@@ -252,7 +262,7 @@ SpecShip detects web-framework routing files and emits `route` nodes linked by `
252
262
  ## Quick Start
253
263
 
254
264
  ```bash
255
- npm i -g @selvakumaresra/specship@0.1.0
265
+ npm i -g @selvakumaresra/specship@0.5.0
256
266
  specship install --yes # writes ~/.claude.json + ~/.claude/settings.json
257
267
  cd your-project && specship init -i
258
268
  # restart Claude Code
@@ -284,7 +294,7 @@ specship install --no-permissions # skip auto-allow list
284
294
 
285
295
  **Install globally:**
286
296
  ```bash
287
- npm install -g @selvakumaresra/specship@0.1.0
297
+ npm install -g @selvakumaresra/specship@0.5.0
288
298
  ```
289
299
 
290
300
  **Add to `~/.claude.json`:**
@@ -470,7 +480,7 @@ that drive the graph directly: `DatabaseConnection`, `QueryBuilder`,
470
480
 
471
481
  **Embedding requirements**
472
482
 
473
- - Install from npm (`npm i @selvakumaresra/specship@0.1.0`) so the matching
483
+ - Install from npm (`npm i @selvakumaresra/specship@0.5.0`) so the matching
474
484
  per-platform package — which carries the compiled library and its
475
485
  dependencies — is fetched alongside the shim.
476
486
  - The API runs on **your** runtime, so it needs **Node 22.5+** for the built-in
@@ -551,9 +561,9 @@ See [Get Started](#get-started) for the one-line install commands.
551
561
 
552
562
  **MCP hits `database is locked`** — current builds shouldn't: SpecShip bundles its own Node runtime and uses Node's built-in `node:sqlite` in WAL mode, where concurrent reads never block on a writer. If you still see it:
553
563
 
554
- - **You're on an old (pre-0.9) install.** Reinstall: `npm i -g @selvakumaresra/specship@0.1.0`.
555
- - **`specship status` shows `Journal:` other than `wal`** — WAL couldn't be enabled on this filesystem (common on network shares and WSL2 `/mnt`), so reads can block on writes. Move the project (with its `.specship/` folder) onto a local disk.
564
+ - **You're on an old (pre-0.9) install.** Reinstall: `npm i -g @selvakumaresra/specship@0.5.0`.
556
565
 
566
+ **`specship status` shows `Journal:` other than `wal`** — WAL couldn't be enabled on this filesystem (common on network shares and WSL2 `/mnt`), so reads can block on writes. Move the project (with its `.specship/` folder) onto a local disk.
557
567
  **MCP server not connecting** — Ensure the project is initialized/indexed, verify the path in your MCP config, and check that `specship serve --mcp` works from the command line.
558
568
 
559
569
  **Missing symbols** — The MCP server auto-syncs on save (wait a couple seconds). Run `specship sync` manually if needed. Check that the file's language is supported and isn't inside a `.gitignore`d or default-excluded directory (e.g. `node_modules`, `dist`).
@@ -0,0 +1,68 @@
1
+ ---
2
+ description: Brainstorm a requirement — analyse, ground in code, explore approaches, loop with you, and ONLY on your explicit confirmation write a design brief and hand off to /ss-spec-author. Nothing is written until you confirm.
3
+ argument-hint: <requirement to brainstorm>
4
+ allowed-tools: Read, Write, Edit, Bash, mcp__specship__specship_explore, mcp__specship__specship_search, mcp__specship__specship_node, mcp__specship__specship_files, mcp__specship__specship_spec
5
+ ---
6
+
7
+ # SpecShip Brainstorm: `$ARGUMENTS`
8
+
9
+ The **divergent** front of spec-driven development. You explore the problem with the human and DECIDE; `/ss-spec-author` formalizes the decision into a spec. Run this conversationally — do NOT batch.
10
+
11
+ ## The hard rule (read first)
12
+
13
+ **Write NOTHING to disk until the human EXPLICITLY confirms.** No brief, no spec, no scratch file during the loop. Treat the default as no-write. A vague "maybe", "looks ok", silence, or a follow-up question is **not** confirmation — only an unambiguous "yes, write it" / "confirmed" / "go ahead" counts. If the human ends the conversation without confirming, you have produced zero files, and that is correct.
14
+
15
+ ## The loop
16
+
17
+ 1. **Scope check.** Refuse "brainstorm the whole app" — pick one feature area. If `$ARGUMENTS` is empty, ask what they want to brainstorm.
18
+ 2. **Ground in code.** Call `mcp__specship__specship_explore` (and `specship_search`) on terms from `$ARGUMENTS` to find where similar features live, conventions to mirror, and which files the work will likely touch. Summarize what you found.
19
+ 3. **Approaches.** Propose **2–3 distinct approaches** with trade-offs, and lead with your recommendation and why.
20
+ 4. **Clarify.** Ask the human **one question at a time** about the things the graph can't tell you — UX, edge cases, acceptance criteria, non-goals. Don't dump a list.
21
+ 5. **Iterate** 3–4 until the human is satisfied with a direction. Then ask: *"Want me to write this up as a brief and hand it to /ss-spec-author?"* — and WAIT for an explicit yes.
22
+
23
+ ## On explicit confirmation (and only then)
24
+
25
+ 1. Derive a kebab-case `<slug>` from the feature.
26
+ 2. Write the brief to **`specs/<slug>/brief.md`** using the format below. Leave the `spec:` field **unset** for now.
27
+ 3. Hand off: invoke **`/ss-spec-author`** with the brief — pass the brief's path so spec-author reads it and does NOT re-ground in code (the brief already has the grounding). spec-author assigns the real spec **ID** and writes `specs/<ID>.md`.
28
+ 4. Once spec-author has written the spec: set the brief's `spec:` field to the new ID, and add a **`brief:`** field to the spec's frontmatter holding the path to the brief **relative to the spec file's own directory**. For the usual flat layout (`specs/<ID>.md`) that is exactly `brief: <slug>/brief.md`; if spec-author ever nests the spec (e.g. `specs/<area>/<ID>.md`), write the correct relative path instead (e.g. `../<slug>/brief.md`) so the dashboard can resolve it. This links the two both ways.
29
+ 5. If spec-author fails, STOP and tell the human: the brief exists with `spec:` unset; retry is re-running `/ss-spec-author` with the same brief path. Do not hand-write a spec.
30
+ 6. Point them at `/ss-spec-review <ID>` then `/ss-implement <ID>`.
31
+
32
+ ## Brief format (`specs/<slug>/brief.md`)
33
+
34
+ ```markdown
35
+ ---
36
+ slug: <slug>
37
+ spec: # set to the REQ-… id after /ss-spec-author writes the spec
38
+ created: <date>
39
+ ---
40
+
41
+ # Brainstorm: <feature>
42
+
43
+ ## Problem
44
+ <what we're solving and why>
45
+
46
+ ## Code grounding
47
+ <relevant files / symbols / conventions found via specship_explore>
48
+
49
+ ## Approaches considered
50
+ 1. <A> — <trade-offs>
51
+ 2. <B> — <trade-offs>
52
+ **Chosen: <X>** — <rationale>
53
+
54
+ ## Key decisions
55
+ <the calls made during the loop>
56
+
57
+ ## Edge cases & non-goals
58
+ <…>
59
+
60
+ ## Acceptance criteria
61
+ <…>
62
+ ```
63
+
64
+ ## Anti-patterns
65
+ - **Writing before confirmation.** The single most important rule — see above.
66
+ - **Re-interviewing about taste / proposing your own variants without grounding.** Ground first, then propose.
67
+ - **Duplicating spec-author.** You decide; spec-author formats. Don't write the formal `specs/<ID>.md` yourself.
68
+ - **Treating a question as confirmation.** Only an explicit yes writes files.
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Pure classifiers and symbol-extraction helpers for SpecShip Token Impact.
3
+ *
4
+ * No I/O, no DB, no imports beyond types. Intended to be imported by the
5
+ * ingestor (Task 4) and the aggregation endpoint (later tasks).
6
+ */
7
+ /**
8
+ * Returns true iff `name` is routed through the SpecShip MCP server
9
+ * (i.e. starts with `mcp__specship__`).
10
+ */
11
+ export declare function isSpecshipTool(name: string): boolean;
12
+ /**
13
+ * Returns true iff the tool returns code-graph source that can displace a
14
+ * native Read/Grep. See SOURCE_RETURNING_TOOLS for the authoritative set.
15
+ */
16
+ export declare function isSourceReturningTool(name: string): boolean;
17
+ /**
18
+ * Extracts the symbol names a tool call was asking about from its serialised
19
+ * input JSON. Returns `[]` when no symbols are resolvable.
20
+ *
21
+ * Heuristic for `specship_explore` and `specship_search` (which both accept a
22
+ * free-form `query` string):
23
+ * - Tokenise on whitespace.
24
+ * - If ALL tokens match SYMBOL_TOKEN_RE AND there are ≤ MAX_SYMBOL_BAG_TOKENS
25
+ * tokens, treat as a symbol bag and return the token list.
26
+ * - Otherwise (natural-language prose detected) return [].
27
+ *
28
+ * Input keys verified against src/mcp/tools.ts:
29
+ * - specship_node / callers / callees / impact → `symbol` (string, required)
30
+ * - specship_explore → `query` (string, required)
31
+ * - specship_search → `query` (string, required)
32
+ * - specship_files / others → [] (no named-symbol input)
33
+ */
34
+ export declare function extractRequestedSymbols(toolName: string, inputJson: string | null | undefined): string[];
35
+ /**
36
+ * Minimal interface for the graph-backed read-equivalent estimator.
37
+ * Keeps the ingestor and tests decoupled from the full SpecShip class.
38
+ */
39
+ export interface GraphLike {
40
+ estimateReadEquivalent(symbols: string[]): {
41
+ files: {
42
+ path: string;
43
+ size: number;
44
+ }[];
45
+ resolved: boolean;
46
+ };
47
+ }
48
+ /**
49
+ * Classify one tool call and compute the three ingest columns:
50
+ * - `isSpecship` — 1 if this is any `mcp__specship__*` call, else 0.
51
+ * - `resolution` — 'resolved' | 'unresolved' | 'n/a' | null.
52
+ * - `displacedFiles` — JSON string `[[path,size],…]` or null.
53
+ *
54
+ * Logic:
55
+ * 1. Not a specship tool → { isSpecship:0, resolution:null, displacedFiles:null }.
56
+ * 2. Specship + source-returning + result has content:
57
+ * a. No symbols extractable → unresolved / null.
58
+ * b. No graph available → unresolved / null.
59
+ * c. graph.estimateReadEquivalent returns resolved=true → resolved + files JSON.
60
+ * d. resolved=false → unresolved / null.
61
+ * 3. Specship but not a source-returning call, or zero-length result → n/a / null.
62
+ */
63
+ export declare function classifyToolCall(call: {
64
+ toolName: string;
65
+ inputJson: string | null | undefined;
66
+ resultLength: number;
67
+ }, graph: GraphLike | null): {
68
+ isSpecship: 0 | 1;
69
+ resolution: 'resolved' | 'unresolved' | 'n/a' | null;
70
+ displacedFiles: string | null;
71
+ };
72
+ //# sourceMappingURL=specship-impact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"specship-impact.d.ts","sourceRoot":"","sources":["../../src/analytics/specship-impact.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAsEH;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAI3D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACnC,MAAM,EAAE,CA+BV;AAMD;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG;QACzC,KAAK,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QACxC,QAAQ,EAAE,OAAO,CAAC;KACnB,CAAC;CACH;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,EACtF,KAAK,EAAE,SAAS,GAAG,IAAI,GACtB;IAAE,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC;IAAC,UAAU,EAAE,UAAU,GAAG,YAAY,GAAG,KAAK,GAAG,IAAI,CAAC;IAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CA0C5G"}
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ /**
3
+ * Pure classifiers and symbol-extraction helpers for SpecShip Token Impact.
4
+ *
5
+ * No I/O, no DB, no imports beyond types. Intended to be imported by the
6
+ * ingestor (Task 4) and the aggregation endpoint (later tasks).
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.isSpecshipTool = isSpecshipTool;
10
+ exports.isSourceReturningTool = isSourceReturningTool;
11
+ exports.extractRequestedSymbols = extractRequestedSymbols;
12
+ exports.classifyToolCall = classifyToolCall;
13
+ // ---------------------------------------------------------------------------
14
+ // Constants
15
+ // ---------------------------------------------------------------------------
16
+ /** Prefix shared by every MCP tool routed through the specship server. */
17
+ const MCP_SPECSHIP_PREFIX = 'mcp__specship__';
18
+ /**
19
+ * Strip-prefix base names of SpecShip tools that return code-graph source.
20
+ * These are the tools a SpecShip call can displace a native Read/Grep with.
21
+ * Excluded: designer_*, specship_link_assert, specship_link_verify,
22
+ * specship_spec, specship_drifted, specship_status — they don't return
23
+ * indexed source symbols.
24
+ */
25
+ const SOURCE_RETURNING_TOOLS = new Set([
26
+ 'specship_explore',
27
+ 'specship_node',
28
+ 'specship_callers',
29
+ 'specship_callees',
30
+ 'specship_impact',
31
+ 'specship_search',
32
+ 'specship_files',
33
+ ]);
34
+ /**
35
+ * Tools whose input carries a `symbol` key (single identifier string).
36
+ * Verified against src/mcp/tools.ts inputSchema `required: ['symbol']`.
37
+ */
38
+ const SYMBOL_KEY_TOOLS = new Set([
39
+ 'specship_node',
40
+ 'specship_callers',
41
+ 'specship_callees',
42
+ 'specship_impact',
43
+ ]);
44
+ /**
45
+ * Regex for a single valid identifier token (with optional Class.method
46
+ * qualifier). A token matches if every character is a JS/TS identifier char
47
+ * or a single interior dot separating two identifier segments.
48
+ *
49
+ * Class.method tokens are kept whole — downstream resolution will try the
50
+ * qualified name, which biases an overloaded name to the named type's own
51
+ * definition (e.g. `DataRequest.task` → DataRequest's `task`, not the
52
+ * abstract base).
53
+ */
54
+ const SYMBOL_TOKEN_RE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)?$/;
55
+ /**
56
+ * Regex that a token must match to be considered "code-ish" rather than
57
+ * natural-language prose. Pure lowercase ASCII words (e.g. "find", "the",
58
+ * "all") look like prose; real symbol names almost always contain at least
59
+ * one of: uppercase letter, underscore, dollar sign, digit, or a dot
60
+ * (qualifying separator). A token matches this regex if it has any of those
61
+ * signals.
62
+ *
63
+ * Examples that pass: AuthService handleRequest _private $el renderV2 UserService.login
64
+ * Examples that fail: find all the auth handlers does updating
65
+ */
66
+ const CODE_SIGNAL_RE = /[A-Z_$\d.]/;
67
+ /** Cap on how many symbol-shaped tokens we pull from one query (bounds the
68
+ * downstream graph lookups; queries naming more than this are rare). */
69
+ const MAX_SYMBOLS_PER_QUERY = 16;
70
+ // ---------------------------------------------------------------------------
71
+ // Public API
72
+ // ---------------------------------------------------------------------------
73
+ /**
74
+ * Returns true iff `name` is routed through the SpecShip MCP server
75
+ * (i.e. starts with `mcp__specship__`).
76
+ */
77
+ function isSpecshipTool(name) {
78
+ return name.startsWith(MCP_SPECSHIP_PREFIX);
79
+ }
80
+ /**
81
+ * Returns true iff the tool returns code-graph source that can displace a
82
+ * native Read/Grep. See SOURCE_RETURNING_TOOLS for the authoritative set.
83
+ */
84
+ function isSourceReturningTool(name) {
85
+ if (!name.startsWith(MCP_SPECSHIP_PREFIX))
86
+ return false;
87
+ const base = name.slice(MCP_SPECSHIP_PREFIX.length);
88
+ return SOURCE_RETURNING_TOOLS.has(base);
89
+ }
90
+ /**
91
+ * Extracts the symbol names a tool call was asking about from its serialised
92
+ * input JSON. Returns `[]` when no symbols are resolvable.
93
+ *
94
+ * Heuristic for `specship_explore` and `specship_search` (which both accept a
95
+ * free-form `query` string):
96
+ * - Tokenise on whitespace.
97
+ * - If ALL tokens match SYMBOL_TOKEN_RE AND there are ≤ MAX_SYMBOL_BAG_TOKENS
98
+ * tokens, treat as a symbol bag and return the token list.
99
+ * - Otherwise (natural-language prose detected) return [].
100
+ *
101
+ * Input keys verified against src/mcp/tools.ts:
102
+ * - specship_node / callers / callees / impact → `symbol` (string, required)
103
+ * - specship_explore → `query` (string, required)
104
+ * - specship_search → `query` (string, required)
105
+ * - specship_files / others → [] (no named-symbol input)
106
+ */
107
+ function extractRequestedSymbols(toolName, inputJson) {
108
+ if (!inputJson)
109
+ return [];
110
+ let parsed;
111
+ try {
112
+ parsed = JSON.parse(inputJson);
113
+ }
114
+ catch {
115
+ return [];
116
+ }
117
+ if (parsed === null || typeof parsed !== 'object')
118
+ return [];
119
+ const args = parsed;
120
+ if (!toolName.startsWith(MCP_SPECSHIP_PREFIX))
121
+ return [];
122
+ const base = toolName.slice(MCP_SPECSHIP_PREFIX.length);
123
+ // --- symbol-keyed tools (specship_node, callers, callees, impact) ---------
124
+ if (SYMBOL_KEY_TOOLS.has(base)) {
125
+ const sym = args['symbol'];
126
+ return typeof sym === 'string' && sym.length > 0 ? [sym] : [];
127
+ }
128
+ // --- query-keyed tools (specship_explore, specship_search) ----------------
129
+ if (base === 'specship_explore' || base === 'specship_search') {
130
+ const q = args['query'];
131
+ if (typeof q !== 'string' || q.length === 0)
132
+ return [];
133
+ return symbolBagFromQuery(q);
134
+ }
135
+ // --- all other specship tools (files, status, spec, drifted, designer_*…) -
136
+ return [];
137
+ }
138
+ /**
139
+ * Classify one tool call and compute the three ingest columns:
140
+ * - `isSpecship` — 1 if this is any `mcp__specship__*` call, else 0.
141
+ * - `resolution` — 'resolved' | 'unresolved' | 'n/a' | null.
142
+ * - `displacedFiles` — JSON string `[[path,size],…]` or null.
143
+ *
144
+ * Logic:
145
+ * 1. Not a specship tool → { isSpecship:0, resolution:null, displacedFiles:null }.
146
+ * 2. Specship + source-returning + result has content:
147
+ * a. No symbols extractable → unresolved / null.
148
+ * b. No graph available → unresolved / null.
149
+ * c. graph.estimateReadEquivalent returns resolved=true → resolved + files JSON.
150
+ * d. resolved=false → unresolved / null.
151
+ * 3. Specship but not a source-returning call, or zero-length result → n/a / null.
152
+ */
153
+ function classifyToolCall(call, graph) {
154
+ const { toolName, inputJson, resultLength } = call;
155
+ if (!isSpecshipTool(toolName)) {
156
+ return { isSpecship: 0, resolution: null, displacedFiles: null };
157
+ }
158
+ // It's a specship tool. Check if it returns source AND returned something.
159
+ if (isSourceReturningTool(toolName) && resultLength > 0) {
160
+ const symbols = extractRequestedSymbols(toolName, inputJson);
161
+ if (symbols.length === 0) {
162
+ return { isSpecship: 1, resolution: 'unresolved', displacedFiles: null };
163
+ }
164
+ if (graph === null) {
165
+ return { isSpecship: 1, resolution: 'unresolved', displacedFiles: null };
166
+ }
167
+ // A locked/corrupt index can make estimateReadEquivalent throw. The estimate
168
+ // is best-effort decoration on the tool call — it must NEVER break core
169
+ // transcript ingest. Degrade to 'unresolved' on any failure.
170
+ let files;
171
+ let resolved;
172
+ try {
173
+ ({ files, resolved } = graph.estimateReadEquivalent(symbols));
174
+ }
175
+ catch {
176
+ return { isSpecship: 1, resolution: 'unresolved', displacedFiles: null };
177
+ }
178
+ if (resolved) {
179
+ return {
180
+ isSpecship: 1,
181
+ resolution: 'resolved',
182
+ displacedFiles: JSON.stringify(files.map((f) => [f.path, f.size])),
183
+ };
184
+ }
185
+ else {
186
+ return { isSpecship: 1, resolution: 'unresolved', displacedFiles: null };
187
+ }
188
+ }
189
+ // Specship tool but not source-returning, or returned nothing (designer/spec/mutating tools).
190
+ return { isSpecship: 1, resolution: 'n/a', displacedFiles: null };
191
+ }
192
+ // ---------------------------------------------------------------------------
193
+ // Internal helpers
194
+ // ---------------------------------------------------------------------------
195
+ /**
196
+ * Pull the symbol-shaped tokens out of a free-form `query`.
197
+ *
198
+ * Real `explore`/`search` queries are MIXED bags — symbol names interleaved
199
+ * with lowercase keywords ("live", "save", "install", "the") and often 10+
200
+ * tokens long. So we FILTER (not all-or-nothing): keep each token that matches
201
+ * SYMBOL_TOKEN_RE (a valid identifier or `Class.method`) AND CODE_SIGNAL_RE
202
+ * (has an uppercase letter, underscore, dollar, digit, or dot — i.e. looks like
203
+ * code, not a plain lowercase word). Drop the rest.
204
+ *
205
+ * A pure natural-language question ("how does the user log in") has no code-ish
206
+ * tokens, so it yields [] — still correctly treated as "no symbols". Capped at
207
+ * MAX_SYMBOLS_PER_QUERY to bound the downstream graph lookups.
208
+ */
209
+ function symbolBagFromQuery(query) {
210
+ const symbols = query
211
+ .trim()
212
+ .split(/\s+/)
213
+ .filter((t) => SYMBOL_TOKEN_RE.test(t) && CODE_SIGNAL_RE.test(t));
214
+ return symbols.slice(0, MAX_SYMBOLS_PER_QUERY);
215
+ }
216
+ //# sourceMappingURL=specship-impact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"specship-impact.js","sourceRoot":"","sources":["../../src/analytics/specship-impact.ts"],"names":[],"mappings":"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"}
@@ -1676,8 +1676,8 @@ function main() {
1676
1676
  program
1677
1677
  .command('install')
1678
1678
  .description('Install specship MCP server into Claude Code')
1679
- .option('-l, --location <where>', 'Install location: "global" or "local". Default: prompt')
1680
- .option('-y, --yes', 'Non-interactive: defaults to --location=global, auto-allow on')
1679
+ .option('-l, --location <where>', 'Install location: "global" or "local". Default: prompt (local)')
1680
+ .option('-y, --yes', 'Non-interactive: defaults to --location=local, auto-allow on')
1681
1681
  .option('--no-permissions', 'Skip writing the auto-allow permissions list')
1682
1682
  .option('--no-sdd', 'Skip the spec-driven-development steering (CLAUDE.md rule + spec-author nudge hook)')
1683
1683
  .option('--print-config', 'Print MCP config snippet for Claude Code and exit (no file writes)')
@@ -1787,8 +1787,8 @@ function main() {
1787
1787
  program
1788
1788
  .command('uninstall')
1789
1789
  .description('Remove specship from Claude Code')
1790
- .option('-l, --location <where>', 'Uninstall location: "global" or "local". Default: prompt')
1791
- .option('-y, --yes', 'Non-interactive: defaults to --location=global')
1790
+ .option('-l, --location <where>', 'Uninstall location: "global" or "local". Default: prompt (local)')
1791
+ .option('-y, --yes', 'Non-interactive: defaults to --location=local')
1792
1792
  // vestigial — kept so existing `--target claude` invocations keep working.
1793
1793
  .option('-t, --target <ids>', '(vestigial) accepted: "claude" | "auto" | "all" | "none"')
1794
1794
  .action(async (opts) => {
@@ -1861,6 +1861,179 @@ function main() {
1861
1861
  cg.close();
1862
1862
  }
1863
1863
  });
1864
+ // @implements REQ-FUNNEL-004
1865
+ program
1866
+ .command('spec [id]')
1867
+ .description('Spec lifecycle funnel (idea → spec → implemented). With an id, show that spec/brief detail.')
1868
+ .option('--json', 'emit JSON')
1869
+ .action(async (id, options) => {
1870
+ const projectRoot = path.resolve(process.cwd());
1871
+ if (!(0, directory_1.isInitialized)(projectRoot)) {
1872
+ error(`SpecShip not initialized in ${projectRoot}. Run \`specship init -i\` first.`);
1873
+ process.exit(1);
1874
+ }
1875
+ const { default: SpecShip } = await loadSpecShip();
1876
+ const { summarizeBriefFunnel, resolveBriefLink, findBriefsForSpec } = await Promise.resolve().then(() => __importStar(require('../resolution/brief-link-resolver')));
1877
+ const cg = await SpecShip.open(projectRoot);
1878
+ try {
1879
+ const sq = cg.getSpecQueries();
1880
+ // Implementation rollup for a document's requirements.
1881
+ const docRollup = (docId) => {
1882
+ const reqs = sq.getSpecsByParent(docId).filter((s) => s.kind === 'requirement');
1883
+ const r = { requirements: reqs.length, implemented: 0, verified: 0, drifted: 0, broken: 0, orphaned: 0 };
1884
+ for (const req of reqs) {
1885
+ for (const lk of sq.getLinksBySpec(req.id)) {
1886
+ if (lk.state === 'implemented')
1887
+ r.implemented++;
1888
+ else if (lk.state === 'verified')
1889
+ r.verified++;
1890
+ else if (lk.state === 'drifted')
1891
+ r.drifted++;
1892
+ else if (lk.state === 'broken')
1893
+ r.broken++;
1894
+ else if (lk.state === 'orphaned')
1895
+ r.orphaned++;
1896
+ }
1897
+ }
1898
+ return r;
1899
+ };
1900
+ const degraded = (r) => r.drifted + r.broken + r.orphaned;
1901
+ // ---- Detail view (an id was given) ----
1902
+ if (id) {
1903
+ const spec = sq.getSpecById(id);
1904
+ if (!spec) {
1905
+ if (options.json) {
1906
+ // eslint-disable-next-line no-console
1907
+ console.log(JSON.stringify({ error: 'not_found', id }, null, 2));
1908
+ }
1909
+ else {
1910
+ error(`No spec or brief with id "${id}".`);
1911
+ }
1912
+ process.exit(1);
1913
+ }
1914
+ if (spec.kind === 'brief') {
1915
+ const entry = summarizeBriefFunnel(sq, spec);
1916
+ const link = resolveBriefLink(sq, spec);
1917
+ if (options.json) {
1918
+ // eslint-disable-next-line no-console
1919
+ console.log(JSON.stringify({ ...entry, briefSide: link.briefSide, specSide: link.specSide }, null, 2));
1920
+ }
1921
+ else {
1922
+ /* eslint-disable no-console */
1923
+ console.log(`${spec.id} (brief) ${spec.title}`);
1924
+ console.log(` state: ${entry.state}`);
1925
+ if (entry.state === 'conflict') {
1926
+ console.log(` ⚠ conflict: brief → ${link.briefSide}, spec → ${link.specSide} (resolve the mismatched pointer)`);
1927
+ }
1928
+ else if (entry.linkedSpecId) {
1929
+ console.log(` linked spec: ${entry.linkedSpecId}`);
1930
+ }
1931
+ if (entry.rollup) {
1932
+ const r = entry.rollup;
1933
+ console.log(` rollup: ${r.requirements} reqs · ${r.implemented} implemented · ${r.verified} verified · ${degraded(r)} degraded`);
1934
+ }
1935
+ /* eslint-enable no-console */
1936
+ }
1937
+ }
1938
+ else {
1939
+ const links = sq.getLinksBySpec(spec.id);
1940
+ const briefs = spec.kind === 'document' ? findBriefsForSpec(sq, spec.id) : [];
1941
+ if (options.json) {
1942
+ const detail = { id: spec.id, kind: spec.kind, title: spec.title, links };
1943
+ if (spec.kind === 'document')
1944
+ detail.rollup = docRollup(spec.id);
1945
+ detail.briefs = briefs.map((b) => b.briefId);
1946
+ // eslint-disable-next-line no-console
1947
+ console.log(JSON.stringify(detail, null, 2));
1948
+ }
1949
+ else {
1950
+ /* eslint-disable no-console */
1951
+ console.log(`${spec.id} (${spec.kind}) ${spec.title}`);
1952
+ if (spec.kind === 'document') {
1953
+ const r = docRollup(spec.id);
1954
+ console.log(` rollup: ${r.requirements} reqs · ${r.implemented} implemented · ${r.verified} verified · ${degraded(r)} degraded`);
1955
+ if (briefs.length)
1956
+ console.log(` from briefs: ${briefs.map((b) => b.briefId).join(', ')}`);
1957
+ }
1958
+ for (const lk of links) {
1959
+ console.log(` [${lk.state}] → ${lk.targetFilePath}:${lk.targetQualifiedName}`);
1960
+ }
1961
+ /* eslint-enable no-console */
1962
+ }
1963
+ }
1964
+ return;
1965
+ }
1966
+ // ---- Funnel view (no id) ----
1967
+ const all = sq.getAllSpecs();
1968
+ const briefs = all.filter((s) => s.kind === 'brief');
1969
+ const documents = all.filter((s) => s.kind === 'document');
1970
+ const requirements = all.filter((s) => s.kind === 'requirement');
1971
+ const entries = briefs.map((b) => summarizeBriefFunnel(sq, b));
1972
+ const byState = (st) => entries.filter((e) => e.state === st);
1973
+ const links = sq.getAllLinks();
1974
+ const lc = (st) => links.filter((l) => l.state === st).length;
1975
+ if (all.length === 0) {
1976
+ if (options.json) {
1977
+ // eslint-disable-next-line no-console
1978
+ console.log(JSON.stringify({ summary: { ideas: 0, specified: 0, conflicts: 0, documents: 0, requirements: 0 }, specs: [], briefs: [] }, null, 2));
1979
+ }
1980
+ else {
1981
+ // eslint-disable-next-line no-console
1982
+ console.log('No specs or briefs found. Author one with /ss-spec-author or /ss-brainstorm.');
1983
+ }
1984
+ return;
1985
+ }
1986
+ if (options.json) {
1987
+ // eslint-disable-next-line no-console
1988
+ console.log(JSON.stringify({
1989
+ summary: {
1990
+ ideas: byState('idea').length,
1991
+ specified: byState('specified').length,
1992
+ conflicts: byState('conflict').length,
1993
+ documents: documents.length,
1994
+ requirements: requirements.length,
1995
+ links: { implemented: lc('implemented'), verified: lc('verified'), drifted: lc('drifted'), broken: lc('broken'), orphaned: lc('orphaned') },
1996
+ },
1997
+ specs: documents.map((d) => ({ id: d.id, title: d.title, rollup: docRollup(d.id) })),
1998
+ briefs: entries,
1999
+ }, null, 2));
2000
+ return;
2001
+ }
2002
+ /* eslint-disable no-console */
2003
+ console.log('Spec lifecycle funnel');
2004
+ console.log(` ideas ${byState('idea').length}`);
2005
+ console.log(` specified ${byState('specified').length}`);
2006
+ if (byState('conflict').length)
2007
+ console.log(` conflicts ${byState('conflict').length} ⚠`);
2008
+ console.log(` documents ${documents.length}`);
2009
+ console.log(` requirements ${requirements.length} (implemented ${lc('implemented')} · verified ${lc('verified')} · degraded ${lc('drifted') + lc('broken') + lc('orphaned')})`);
2010
+ if (documents.length) {
2011
+ console.log('\nDocuments:');
2012
+ for (const d of documents) {
2013
+ const r = docRollup(d.id);
2014
+ console.log(` ${d.id} — ${d.title} [${r.requirements} reqs · ${r.implemented} impl · ${r.verified} ver${degraded(r) ? ` · ${degraded(r)} degraded` : ''}]`);
2015
+ }
2016
+ }
2017
+ const ideas = byState('idea');
2018
+ if (ideas.length) {
2019
+ console.log('\nIdeas (unlinked briefs):');
2020
+ for (const e of ideas) {
2021
+ const b = sq.getSpecById(e.briefId);
2022
+ console.log(` ${e.briefId} — ${b?.title ?? ''}`);
2023
+ }
2024
+ }
2025
+ const conflicts = byState('conflict');
2026
+ if (conflicts.length) {
2027
+ console.log('\nConflicts (mismatched brief↔spec pointers):');
2028
+ for (const e of conflicts)
2029
+ console.log(` ${e.briefId} ⚠`);
2030
+ }
2031
+ /* eslint-enable no-console */
2032
+ }
2033
+ finally {
2034
+ cg.close();
2035
+ }
2036
+ });
1864
2037
  program
1865
2038
  .command('workflow <action> [arg]')
1866
2039
  .description('Workflow engine: list | run <name> | resume <runId> | cancel <runId> | approve <runId> | reject <runId> | runs')