@selvakumaresra/specship 0.9.0 → 0.10.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 (86) hide show
  1. package/commands/ss-domain.md +98 -0
  2. package/commands/ss-triage.md +177 -0
  3. package/dist/bin/specship.js +249 -0
  4. package/dist/bin/specship.js.map +1 -1
  5. package/dist/db/schema.sql +1 -1
  6. package/dist/db/spec-queries.d.ts +29 -0
  7. package/dist/db/spec-queries.d.ts.map +1 -1
  8. package/dist/db/spec-queries.js +63 -0
  9. package/dist/db/spec-queries.js.map +1 -1
  10. package/dist/enforce/enforce.d.ts +70 -0
  11. package/dist/enforce/enforce.d.ts.map +1 -0
  12. package/dist/enforce/enforce.js +125 -0
  13. package/dist/enforce/enforce.js.map +1 -0
  14. package/dist/extraction/specs/markdown-spec-extractor.d.ts +21 -0
  15. package/dist/extraction/specs/markdown-spec-extractor.d.ts.map +1 -1
  16. package/dist/extraction/specs/markdown-spec-extractor.js +144 -0
  17. package/dist/extraction/specs/markdown-spec-extractor.js.map +1 -1
  18. package/dist/fitness/fitness.d.ts +75 -0
  19. package/dist/fitness/fitness.d.ts.map +1 -0
  20. package/dist/fitness/fitness.js +204 -0
  21. package/dist/fitness/fitness.js.map +1 -0
  22. package/dist/graph/maintainability.d.ts +101 -0
  23. package/dist/graph/maintainability.d.ts.map +1 -0
  24. package/dist/graph/maintainability.js +278 -0
  25. package/dist/graph/maintainability.js.map +1 -0
  26. package/dist/index.d.ts +45 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +79 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/installer/instructions-template.d.ts +5 -4
  31. package/dist/installer/instructions-template.d.ts.map +1 -1
  32. package/dist/installer/instructions-template.js +10 -4
  33. package/dist/installer/instructions-template.js.map +1 -1
  34. package/dist/installer/targets/claude.d.ts.map +1 -1
  35. package/dist/installer/targets/claude.js +4 -0
  36. package/dist/installer/targets/claude.js.map +1 -1
  37. package/dist/installer/targets/shared.d.ts.map +1 -1
  38. package/dist/installer/targets/shared.js +4 -0
  39. package/dist/installer/targets/shared.js.map +1 -1
  40. package/dist/mcp/fitness-tool.d.ts +12 -0
  41. package/dist/mcp/fitness-tool.d.ts.map +1 -0
  42. package/dist/mcp/fitness-tool.js +46 -0
  43. package/dist/mcp/fitness-tool.js.map +1 -0
  44. package/dist/mcp/maintainability-tool.d.ts +13 -0
  45. package/dist/mcp/maintainability-tool.d.ts.map +1 -0
  46. package/dist/mcp/maintainability-tool.js +64 -0
  47. package/dist/mcp/maintainability-tool.js.map +1 -0
  48. package/dist/mcp/server-instructions.d.ts +1 -1
  49. package/dist/mcp/server-instructions.d.ts.map +1 -1
  50. package/dist/mcp/server-instructions.js +3 -2
  51. package/dist/mcp/server-instructions.js.map +1 -1
  52. package/dist/mcp/spec-tools.d.ts.map +1 -1
  53. package/dist/mcp/spec-tools.js +115 -1
  54. package/dist/mcp/spec-tools.js.map +1 -1
  55. package/dist/mcp/tools.d.ts +13 -0
  56. package/dist/mcp/tools.d.ts.map +1 -1
  57. package/dist/mcp/tools.js +75 -0
  58. package/dist/mcp/tools.js.map +1 -1
  59. package/dist/resolution/domain-gap-seed.d.ts +60 -0
  60. package/dist/resolution/domain-gap-seed.d.ts.map +1 -0
  61. package/dist/resolution/domain-gap-seed.js +87 -0
  62. package/dist/resolution/domain-gap-seed.js.map +1 -0
  63. package/dist/resolution/spec-link-resolver.d.ts +46 -1
  64. package/dist/resolution/spec-link-resolver.d.ts.map +1 -1
  65. package/dist/resolution/spec-link-resolver.js +78 -0
  66. package/dist/resolution/spec-link-resolver.js.map +1 -1
  67. package/dist/server/routes/domain.js +0 -0
  68. package/dist/server/routes/maintainability.js +18 -0
  69. package/dist/server/server.js +4 -0
  70. package/dist/types.d.ts +3 -1
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/types.js +6 -0
  73. package/dist/types.js.map +1 -1
  74. package/dist/web/chunk-EZZBWC7Z.js +1 -0
  75. package/dist/web/{chunk-UBOZGQNK.js → chunk-IZQXYQNQ.js} +1 -1
  76. package/dist/web/chunk-JQXCRIK2.js +1 -0
  77. package/dist/web/{chunk-ASZ77FMZ.js → chunk-JT2YIHPL.js} +1 -1
  78. package/dist/web/{chunk-WLIMNDS3.js → chunk-ONKHTXHJ.js} +1 -1
  79. package/dist/web/{chunk-FHZHD2ZG.js → chunk-P5JX67LT.js} +1 -1
  80. package/dist/web/{chunk-RJLLYZEQ.js → chunk-PDTQX2UL.js} +1 -1
  81. package/dist/web/{chunk-RASJHUXS.js → chunk-XWDR6MPC.js} +1 -1
  82. package/dist/web/{chunk-D5OCNEJA.js → chunk-ZG7H3XPL.js} +1 -1
  83. package/dist/web/index.html +1 -1
  84. package/dist/web/main-GYPY5V5H.js +1 -0
  85. package/package.json +1 -1
  86. package/dist/web/main-P33TUYUP.js +0 -1
@@ -0,0 +1,98 @@
1
+ ---
2
+ description: Capture a human-confirmed domain fact (a term, rule, decision, or constraint) for an undocumented entity or spec. Grounds in the real gap-seed, interviews per-type, and writes only on explicit confirmation.
3
+ argument-hint: [entity-or-spec name | --type rule]
4
+ allowed-tools: mcp__specship__specship_explore, mcp__specship__specship_spec, Bash, Write
5
+ ---
6
+
7
+ # SpecShip Domain Capture
8
+
9
+ Capture a **domain fact** — a piece of the project's ubiquitous language or a
10
+ stated business rule — and persist it under `specs/domain/` as a `domain` spec.
11
+ Domain facts attach at the **spec tier** (linked to a requirement spec) and
12
+ inherit code links + drift through that spec.
13
+
14
+ **Governing principle — propose, never auto-apply.** A fact reaches disk ONLY on
15
+ explicit human confirmation. If the human does not confirm, you write **nothing**.
16
+
17
+ ## 1. Find what is undocumented (ground in the real gap-seed)
18
+
19
+ Run the gap-seed pass and read its output. This lists the **real** code entities
20
+ and specs that no domain fact yet covers — never invent a generic prompt.
21
+
22
+ ```bash
23
+ specship domain-gaps --json
24
+ ```
25
+
26
+ The JSON has `entities` (undocumented `class`/`struct`/`interface`/`route`/`component`
27
+ nodes, each with `qualifiedName`, `kind`, `filePath`), `specs` (undocumented
28
+ non-domain specs, each with `id`, `title`, `kind`), and a `coverage` rollup of
29
+ `{documented, gaps}`.
30
+
31
+ - If `$ARGUMENTS` names a specific entity or spec, target that one.
32
+ - Otherwise pick the most central few gaps to offer the human.
33
+ - If `coverage.gaps` is `0`, tell the human the domain layer is fully covered and stop.
34
+
35
+ ## 2. Understand the candidate before asking
36
+
37
+ For the entity or spec you are about to ask about, ground yourself in the code so
38
+ your questions are specific, not generic:
39
+
40
+ - `mcp__specship__specship_explore` naming the gap entity (and neighbours) to read its real source.
41
+ - `mcp__specship__specship_spec` on the spec you intend to link the fact to, to see its requirements and current code links.
42
+
43
+ ## 3. Interview — per-type, targeted, citing the gap
44
+
45
+ Ask about **the named gap**, not "describe your domain". Frame the questions by
46
+ the fact `type` you are capturing:
47
+
48
+ - **term** — "What does `<EntityName>` mean in this product's language? What's the one-sentence definition a new teammate needs?"
49
+ - **rule** — "What invariant must always hold for `<EntityName>` / the `<SpecTitle>` flow? State it as MUST/NEVER."
50
+ - **decision** — "What was decided about `<EntityName>` and why was the alternative rejected?"
51
+ - **constraint** — "What external limit (regulatory, performance, contractual) bounds `<EntityName>`?"
52
+
53
+ Confirm with the human:
54
+ - the **type** (`term` | `rule` | `decision` | `constraint`),
55
+ - the **statement** (the fact body),
56
+ - the **spec to link** (a requirement `id` from the gap-seed / `specship_spec`),
57
+ - a `DOM-<AREA>-NNN` **id** (AREA = a short uppercase domain tag, NNN zero-padded; pick the next free number for that AREA).
58
+
59
+ ## 4. Confirm, then write (and ONLY then)
60
+
61
+ Show the human the exact fact you are about to write and ask for explicit
62
+ confirmation (e.g. "Write this fact? (yes/no)"). **If they do not clearly
63
+ confirm, stop and write nothing — the command is a no-op.**
64
+
65
+ On confirmation, `Write` the file to `specs/domain/<area>.md` (one fact per file
66
+ is simplest; append to an existing area file only if the human asks):
67
+
68
+ ```markdown
69
+ ---
70
+ id: DOM-PAY-001
71
+ title: Settlement currency
72
+ type: rule
73
+ depends_on: REQ-PAY-004
74
+ ---
75
+ # Settlement currency
76
+
77
+ All payments settle in the merchant's account currency, never the buyer's.
78
+ ```
79
+
80
+ Frontmatter rules:
81
+ - `id` — `DOM-<AREA>-NNN`, distinct from `REQ-` ids.
82
+ - `type` — exactly one of `term`, `rule`, `decision`, `constraint`.
83
+ - Link with `depends_on: <REQ-ID>` (comma-separate multiple) and/or `parent_id: <REQ-ID>`. A fact with no link is allowed — it indexes as an unlinked gap, never an error.
84
+
85
+ ## 5. Index it
86
+
87
+ ```bash
88
+ specship sync
89
+ ```
90
+
91
+ The fact now projects as a `spec:DOM-…` node, is returned by `specship_explore`
92
+ and `specship_spec`, and inherits the linked requirement's code links + drift.
93
+
94
+ ## Manual authoring is equivalent
95
+
96
+ This command is a convenience, not a gate. A human can hand-create the same
97
+ `specs/domain/<area>.md` file with the frontmatter above and run `specship sync`
98
+ to get the **identical** indexed fact — no command required.
@@ -0,0 +1,177 @@
1
+ ---
2
+ description: Route a small change (a bug, an error log, or a one-line enhancement) to the existing spec it belongs to and append to it — a new requirement or acceptance criterion — only on your explicit confirmation. Falls back to authoring a new spec when nothing fits.
3
+ argument-hint: <bug | error log | one-line enhancement>
4
+ allowed-tools: Read, Edit, Write, Bash, mcp__specship__specship_spec, mcp__specship__specship_explore, mcp__specship__specship_search, mcp__specship__specship_node
5
+ ---
6
+
7
+ # SpecShip Triage: `$ARGUMENTS`
8
+
9
+ A **front door** for small changes. Take the prompt, find the **existing** spec
10
+ it belongs to, and **add to it** — a new requirement or a new acceptance
11
+ criterion — rather than spawning a fresh spec. Only fall back to authoring a new
12
+ spec when nothing fits.
13
+
14
+ You match and author; SpecShip retrieves and ranks; **the human gates the
15
+ write**. This is distinct from `/ss-fix` (which repairs a drifted/broken link
16
+ for a *known* spec) and `/ss-brainstorm` + `/ss-spec-author` (which author a
17
+ *new* spec from scratch).
18
+
19
+ **Governing principle — propose, never auto-apply.** A spec file is edited ONLY
20
+ on explicit human confirmation. If the human does not confirm, you write
21
+ **nothing** — the command is a no-op. Never auto-create a spec.
22
+
23
+ If `$ARGUMENTS` is empty, ask the human for the bug / error / enhancement.
24
+
25
+ ## 1. Classify the input
26
+
27
+ Decide which of three classes the prompt is, and say so out loud before any
28
+ retrieval or write (REQ-TRIAGE-002.A3):
29
+
30
+ - **error log** — contains a stack trace, an exception, a `file:line`, or a
31
+ symbol/function name from a crash. Route via the code→spec path (step 2b).
32
+ - **bug** — prose describing wrong behaviour, possibly naming a symbol.
33
+ - **enhancement** — prose proposing a new/changed capability.
34
+
35
+ ## 2. Retrieve candidate specs
36
+
37
+ ### 2a. Prose (bug / enhancement) → spec query
38
+
39
+ Call `specship_spec` with a free-text **`query`** built from the salient terms
40
+ of the prompt. It returns scored, ranked candidate specs (id, title, kind,
41
+ relevance **score**, snippet) over the spec index — enough to act on without a
42
+ follow-up fetch (REQ-TRIAGE-002.A1).
43
+
44
+ ```
45
+ specship_spec { "query": "<key terms from the prompt>" }
46
+ ```
47
+
48
+ For a bug that names a symbol, you may *also* run step 2b and blend the results
49
+ (a spec reached by both the prose query and the code→spec path is the strongest
50
+ candidate).
51
+
52
+ ### 2b. Error log → code → spec
53
+
54
+ Parse the `file:line` / symbol out of the trace, then walk code → owning spec:
55
+
56
+ - `mcp__specship__specship_explore` (or `specship_search`) naming the symbol(s)
57
+ from the trace to locate the implicated code.
58
+ - `mcp__specship__specship_node` on the implicated symbol — its response renders
59
+ the **linked specs** for that node. The owning requirement is the spec the
60
+ crashing code already implements (REQ-TRIAGE-002.A2).
61
+
62
+ ## 3. Present the ranked match + recommended target
63
+
64
+ Before proposing any change, show the human (REQ-TRIAGE-002.A3):
65
+
66
+ - the **detected input class** (bug / error log / enhancement),
67
+ - the **ranked candidates** with their scores, and
68
+ - the **recommended target** — which document (for a new requirement) or which
69
+ requirement (for a new criterion), and why.
70
+
71
+ **Ambiguity:** when several candidates score closely, present the **top N** and
72
+ ask the human to choose — never auto-select among them (REQ-TRIAGE-002.A4).
73
+
74
+ ## 4. No confident match → offer a new spec, don't create one
75
+
76
+ If the top candidate's score is **below the match floor** (no candidate is
77
+ clearly the right home), do NOT extend a spec on your own (REQ-TRIAGE-004):
78
+
79
+ 1. State **"no confident match"** and show the weak candidates **with their
80
+ scores** (A1).
81
+ 2. Offer the human a choice (A2): route to **`/ss-spec-author`** (or
82
+ `/ss-brainstorm`) to author a new spec; **append anyway** to the top weak
83
+ candidate; or **cancel**.
84
+ 3. Never create a new spec without an explicit human choice (A3).
85
+
86
+ Use judgment for the floor — a top score that's roughly on par with unrelated
87
+ specs, or a snippet that doesn't actually concern the change, is "no confident
88
+ match" even if the number isn't zero. SpecShip ranks; you and the human decide.
89
+
90
+ ## 5. Choose the granularity (requirement vs criterion)
91
+
92
+ On a confident match, pick what to append by intent (REQ-TRIAGE-003.A2):
93
+
94
+ - **A distinct new concern** → a **new requirement** under the matched document.
95
+ - **A bug / regression an existing requirement should already have covered** →
96
+ a **new acceptance criterion** on that requirement. This leaves the
97
+ requirement's existing code links intact — you are extending its contract,
98
+ not rewriting it. Error logs and most bugs land here as a regression guard.
99
+
100
+ ## 6. Auto-derive the id (next in series, collision-checked)
101
+
102
+ Inspect the index / target file to find the next free id (REQ-TRIAGE-003.A3):
103
+
104
+ - **New requirement** → next `REQ-<AREA>-<NNN>` for that document's AREA
105
+ (zero-padded NNN; one past the current max for that AREA).
106
+ - **New criterion** → next `REQ-<ID>.A<N>` on the owning requirement (one past
107
+ its current max `A<N>`).
108
+
109
+ Confirm the id collides with nothing already in the index — read the target spec
110
+ file (or `specship_spec` on the document/requirement to list its children /
111
+ acceptance bullets) before settling on the number.
112
+
113
+ ## 7. Preview the exact diff → confirm (and ONLY then write)
114
+
115
+ Show the human the **exact change** (REQ-TRIAGE-003.A1): the target spec file
116
+ and the precise block to be inserted, with its `<!-- id: -->` marker. Then ask
117
+ for explicit confirmation and offer the alternatives:
118
+
119
+ > `confirm` · `edit` (adjust the wording first) · `new spec instead` (hand off
120
+ > to `/ss-spec-author`) · `cancel`
121
+
122
+ **If the human does not clearly confirm, stop and write nothing.**
123
+
124
+ A new **requirement** block:
125
+
126
+ ```markdown
127
+ <!-- id: REQ-<AREA>-<NNN> -->
128
+ ## <Title with a MUST / SHOULD / MAY keyword>
129
+
130
+ <one-concern normative description>
131
+
132
+ ## Acceptance
133
+ <!-- id: REQ-<AREA>-<NNN>.A1 -->
134
+ - <testable happy-path bullet>
135
+ <!-- id: REQ-<AREA>-<NNN>.A2 -->
136
+ - <testable failure-path bullet>
137
+ ```
138
+
139
+ A new **acceptance criterion** appended to the owning requirement's
140
+ `## Acceptance` list:
141
+
142
+ ```markdown
143
+ <!-- id: REQ-<ID>.A<N> -->
144
+ - <testable bullet covering the bug / regression>
145
+ ```
146
+
147
+ On confirmation, append it with `Edit` (or `Write`) into the matched spec file —
148
+ a new requirement after the document's existing requirements; a new criterion at
149
+ the end of the owning requirement's `## Acceptance` block. Mirror the file's
150
+ existing marker style exactly.
151
+
152
+ ## 8. Index and hand off
153
+
154
+ ```bash
155
+ specship sync
156
+ ```
157
+
158
+ The appended requirement / criterion must index **cleanly** — `specship sync`
159
+ reports no spec error and the new `<!-- id: -->` is unique (REQ-TRIAGE-003.A4).
160
+ If sync flags a duplicate or malformed id, fix it before reporting done; a
161
+ duplicate-looking append should be flagged, not silently doubled.
162
+
163
+ Then point the human at:
164
+ - `/ss-spec-review <ID>` to review the change, and
165
+ - `/ss-implement <ID>` (then `specship_link_assert`) when ready to build.
166
+
167
+ ## Anti-patterns
168
+
169
+ - **Writing before confirmation** — the single most important rule (step 7).
170
+ - **Auto-creating a new spec** on a weak match instead of offering the choice
171
+ (step 4).
172
+ - **Rewriting a requirement's existing normative prose** in place — append a
173
+ criterion or a new requirement; don't mutate the contract that code links to.
174
+ - **Auto-fixing the bug** — that's `/ss-implement`. Triage routes and records;
175
+ it does not change code.
176
+ - **Spawning a new doc for a change that belongs on an existing spec** — the
177
+ whole point is to *add to* the right spec.
@@ -1186,6 +1186,197 @@ function main() {
1186
1186
  process.exit(1);
1187
1187
  }
1188
1188
  });
1189
+ /**
1190
+ * specship maintainability
1191
+ *
1192
+ * Report graph-derived maintainability signals (REQ-MAINT-003) — coupling, size
1193
+ * hotspots, dependency cycles, dead-code candidates. Advisory (exit 0); the
1194
+ * `--strict` flag is the gating-ready shape consumed later by enforcement mode.
1195
+ */
1196
+ program
1197
+ .command('maintainability [path]')
1198
+ .alias('maint')
1199
+ .description('Report graph-derived maintainability signals (coupling, size, cycles, dead code)')
1200
+ .option('-j, --json', 'Output as JSON')
1201
+ .option('--strict', 'Exit non-zero if any signal has findings (gating-ready; default advisory)')
1202
+ .action(async (pathArg, options) => {
1203
+ const projectPath = resolveProjectPath(pathArg);
1204
+ try {
1205
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1206
+ error(`SpecShip not initialized in ${projectPath}`);
1207
+ process.exit(1);
1208
+ }
1209
+ const { default: SpecShip } = await loadSpecShip();
1210
+ const cg = await SpecShip.open(projectPath);
1211
+ const r = cg.getMaintainability();
1212
+ if (options.json) {
1213
+ console.log(JSON.stringify(r, null, 2));
1214
+ cg.destroy();
1215
+ if (options.strict && !r.clean)
1216
+ process.exit(1);
1217
+ return;
1218
+ }
1219
+ if (r.clean) {
1220
+ success('Maintainability: clean — nothing past threshold.');
1221
+ cg.destroy();
1222
+ return;
1223
+ }
1224
+ const CAP = 10;
1225
+ const section = (title, count) => console.log(chalk.bold(`\n${title} (${count})`));
1226
+ if (r.coupling.length) {
1227
+ section('Coupling hotspots', r.coupling.length);
1228
+ for (const c of r.coupling.slice(0, CAP))
1229
+ console.log(chalk.dim(' ') + `${c.name} ${chalk.dim(`(${c.reason}) — ${c.filePath}`)}`);
1230
+ }
1231
+ if (r.oversized.length) {
1232
+ section('Oversized symbols', r.oversized.length);
1233
+ for (const o of r.oversized.slice(0, CAP))
1234
+ console.log(chalk.dim(' ') + `${o.name} ${chalk.dim(`(${o.reason}) — ${o.filePath}`)}`);
1235
+ }
1236
+ if (r.godFiles.length) {
1237
+ section('God files', r.godFiles.length);
1238
+ for (const f of r.godFiles.slice(0, CAP))
1239
+ console.log(chalk.dim(' ') + `${f.filePath} ${chalk.dim(`(${f.reason})`)}`);
1240
+ }
1241
+ if (r.cycles.length) {
1242
+ section('Dependency cycles', r.cycles.length);
1243
+ for (const c of r.cycles.slice(0, CAP))
1244
+ console.log(chalk.dim(' ') + c.files.join(' → '));
1245
+ }
1246
+ if (r.deadCode.length) {
1247
+ section('Dead-code candidates', r.deadCode.length);
1248
+ for (const d of r.deadCode.slice(0, CAP))
1249
+ console.log(chalk.dim(' ') + `${d.name} ${chalk.dim(`— ${d.filePath}:${d.startLine}`)}`);
1250
+ if (r.deadCode.length > CAP)
1251
+ console.log(chalk.dim(` …and ${r.deadCode.length - CAP} more (dead-code is heuristic; tune in ${'specship.config.json'})`));
1252
+ }
1253
+ console.log();
1254
+ info(`Thresholds: highDegree=${r.thresholds.highDegree} largeSymbolLines=${r.thresholds.largeSymbolLines} godFileSymbols=${r.thresholds.godFileSymbols} — override in specship.config.json`);
1255
+ cg.destroy();
1256
+ if (options.strict)
1257
+ process.exit(1);
1258
+ }
1259
+ catch (err) {
1260
+ error(`maintainability failed: ${err instanceof Error ? err.message : String(err)}`);
1261
+ process.exit(1);
1262
+ }
1263
+ });
1264
+ /**
1265
+ * specship fitness
1266
+ *
1267
+ * Evaluate the project's architecture-fitness rules (specship.config.json
1268
+ * `fitness.rules`) against the graph (REQ-FITNESS-003). Headless CI gate: exits
1269
+ * non-zero on any violation OR config error (a no-match rule is a config error,
1270
+ * never a silent pass).
1271
+ */
1272
+ program
1273
+ .command('fitness [path]')
1274
+ .description('Check architecture-fitness rules against the code graph (CI gate; exits non-zero on violation)')
1275
+ .option('-j, --json', 'Output as JSON')
1276
+ .action(async (pathArg, options) => {
1277
+ const projectPath = resolveProjectPath(pathArg);
1278
+ try {
1279
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1280
+ error(`SpecShip not initialized in ${projectPath}`);
1281
+ process.exit(1);
1282
+ }
1283
+ const { default: SpecShip } = await loadSpecShip();
1284
+ const cg = await SpecShip.open(projectPath);
1285
+ const r = cg.getFitness();
1286
+ const fail = r.violations.length > 0 || r.configErrors.length > 0;
1287
+ if (options.json) {
1288
+ console.log(JSON.stringify(r, null, 2));
1289
+ cg.destroy();
1290
+ process.exit(fail ? 1 : 0);
1291
+ }
1292
+ if (r.ruleCount === 0) {
1293
+ info('No architecture-fitness rules declared. Add a `fitness.rules` array to specship.config.json.');
1294
+ cg.destroy();
1295
+ return;
1296
+ }
1297
+ if (r.clean) {
1298
+ success(`Architecture fitness: all ${r.ruleCount} rule(s) pass.`);
1299
+ cg.destroy();
1300
+ return;
1301
+ }
1302
+ if (r.configErrors.length) {
1303
+ console.log(chalk.bold(chalk.red(`\nConfig errors (${r.configErrors.length}):`)));
1304
+ for (const e of r.configErrors)
1305
+ console.log(chalk.red(` ✗ ${e.rule}: ${e.message}`));
1306
+ }
1307
+ if (r.violations.length) {
1308
+ console.log(chalk.bold(chalk.red(`\nViolations (${r.violations.length}):`)));
1309
+ for (const v of r.violations.slice(0, 50)) {
1310
+ console.log(` ${chalk.red('✗')} [${v.rule}] ${v.source} → ${v.target}`);
1311
+ console.log(chalk.dim(` ${v.detail} — ${v.location}`));
1312
+ }
1313
+ if (r.violations.length > 50)
1314
+ console.log(chalk.dim(` …and ${r.violations.length - 50} more`));
1315
+ }
1316
+ cg.destroy();
1317
+ process.exit(1);
1318
+ }
1319
+ catch (err) {
1320
+ error(`fitness failed: ${err instanceof Error ? err.message : String(err)}`);
1321
+ process.exit(1);
1322
+ }
1323
+ });
1324
+ /**
1325
+ * specship check
1326
+ *
1327
+ * The enforcement gate (REQ-ENFORCE-001/002/003): runs the harness checks
1328
+ * (drift + fitness + maintainability + behaviour) and exits non-zero if any
1329
+ * GATING check fails. Which checks gate vs advise is configured in
1330
+ * specship.config.json `enforce.gate`; with no config every check is advisory
1331
+ * and the command exits 0 (opt-in — never breaks an existing repo).
1332
+ */
1333
+ program
1334
+ .command('check [path]')
1335
+ .description('Run the enforcement gate (drift + fitness + maintainability + behaviour); exits non-zero on a gating failure')
1336
+ .option('-j, --json', 'Output as JSON')
1337
+ .action(async (pathArg, options) => {
1338
+ const projectPath = resolveProjectPath(pathArg);
1339
+ try {
1340
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1341
+ error(`SpecShip not initialized in ${projectPath}`);
1342
+ process.exit(1);
1343
+ }
1344
+ const { default: SpecShip } = await loadSpecShip();
1345
+ const cg = await SpecShip.open(projectPath);
1346
+ const r = cg.getEnforce();
1347
+ if (options.json) {
1348
+ console.log(JSON.stringify(r, null, 2));
1349
+ cg.destroy();
1350
+ process.exit(r.passed ? 0 : 1);
1351
+ }
1352
+ console.log(chalk.bold('\nEnforcement gate\n'));
1353
+ for (const c of r.checks) {
1354
+ const tag = c.gating ? chalk.dim('[gating]') : chalk.dim('[advisory]');
1355
+ const mark = c.passed ? chalk.green('✓') : (c.gating ? chalk.red('✗') : chalk.yellow('•'));
1356
+ console.log(` ${mark} ${c.check.padEnd(16)} ${tag} ${c.passed ? 'pass' : `${c.findings.length} finding(s)`}`);
1357
+ if (!c.passed)
1358
+ for (const f of c.findings.slice(0, 8))
1359
+ console.log(chalk.dim(` ${f}`));
1360
+ if (!c.passed && c.findings.length > 8)
1361
+ console.log(chalk.dim(` …and ${c.findings.length - 8} more`));
1362
+ }
1363
+ console.log();
1364
+ if (r.passed) {
1365
+ success(r.gatedFailures.length === 0 && r.checks.some((c) => c.gating)
1366
+ ? 'All gating checks pass.'
1367
+ : 'Pass (no gating checks failed).');
1368
+ cg.destroy();
1369
+ return;
1370
+ }
1371
+ error(`Gating checks failed: ${r.gatedFailures.join(', ')}`);
1372
+ cg.destroy();
1373
+ process.exit(1);
1374
+ }
1375
+ catch (err) {
1376
+ error(`check failed: ${err instanceof Error ? err.message : String(err)}`);
1377
+ process.exit(1);
1378
+ }
1379
+ });
1189
1380
  /**
1190
1381
  * specship serve
1191
1382
  */
@@ -1920,6 +2111,64 @@ function main() {
1920
2111
  cg.close();
1921
2112
  }
1922
2113
  });
2114
+ // @implements REQ-DOMAIN-003
2115
+ // Thin surface over the read-only gap-seed pass (SpecShip.getDomainGapSeed,
2116
+ // REQ-DOMAIN-003) so the `/ss-domain` capture command can cite the SAME real
2117
+ // undocumented entities/specs the library computes (REQ-DOMAIN-004.A4) without a
2118
+ // new MCP tool (REQ-DOMAIN-005) or a runtime package import. Writes nothing.
2119
+ program
2120
+ .command('domain-gaps [path]')
2121
+ .description('List code entities and specs not yet covered by a domain fact (the domain gap-seed). Feeds the /ss-domain capture interview.')
2122
+ .option('-l, --limit <n>', 'max entities and specs to print in text mode (default: 50)')
2123
+ .option('--json', 'emit JSON')
2124
+ .action(async (pathArg, options) => {
2125
+ const projectRoot = path.resolve(pathArg ?? process.cwd());
2126
+ if (!(0, directory_1.isInitialized)(projectRoot)) {
2127
+ error(`SpecShip not initialized in ${projectRoot}. Run \`specship init -i\` first.`);
2128
+ process.exit(1);
2129
+ }
2130
+ const { default: SpecShip } = await loadSpecShip();
2131
+ const cg = await SpecShip.open(projectRoot);
2132
+ try {
2133
+ const seed = cg.getDomainGapSeed();
2134
+ if (options.json) {
2135
+ // eslint-disable-next-line no-console
2136
+ console.log(JSON.stringify(seed, null, 2));
2137
+ return;
2138
+ }
2139
+ const limit = options.limit ? Math.max(1, parseInt(options.limit, 10) || 50) : 50;
2140
+ const { documented, gaps } = seed.coverage;
2141
+ const total = documented + gaps;
2142
+ /* eslint-disable no-console */
2143
+ console.log(`Domain coverage: ${documented}/${total} documented · ${gaps} gap${gaps === 1 ? '' : 's'}`);
2144
+ if (gaps === 0) {
2145
+ console.log('✨ Every in-scope entity and spec is covered by a domain fact.');
2146
+ }
2147
+ else {
2148
+ if (seed.entities.length > 0) {
2149
+ console.log(`\nUndocumented entities (${seed.entities.length}):`);
2150
+ for (const e of seed.entities.slice(0, limit)) {
2151
+ console.log(` [${e.kind}] ${e.qualifiedName} — ${e.filePath}`);
2152
+ }
2153
+ if (seed.entities.length > limit)
2154
+ console.log(` … and ${seed.entities.length - limit} more`);
2155
+ }
2156
+ if (seed.specs.length > 0) {
2157
+ console.log(`\nUndocumented specs (${seed.specs.length}):`);
2158
+ for (const s of seed.specs.slice(0, limit)) {
2159
+ console.log(` [${s.kind}] ${s.id} — ${s.title}`);
2160
+ }
2161
+ if (seed.specs.length > limit)
2162
+ console.log(` … and ${seed.specs.length - limit} more`);
2163
+ }
2164
+ console.log(`\nCapture a fact for any of these with \`/ss-domain\`.`);
2165
+ }
2166
+ /* eslint-enable no-console */
2167
+ }
2168
+ finally {
2169
+ cg.close();
2170
+ }
2171
+ });
1923
2172
  // @implements REQ-FUNNEL-004
1924
2173
  program
1925
2174
  .command('spec [id]')