@selvakumaresra/specship 0.7.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.
- package/commands/ss-domain.md +98 -0
- package/commands/ss-triage.md +177 -0
- package/dist/bin/specship.js +308 -0
- package/dist/bin/specship.js.map +1 -1
- package/dist/db/migrations.d.ts +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +28 -1
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/schema.sql +26 -2
- package/dist/db/spec-queries.d.ts +29 -0
- package/dist/db/spec-queries.d.ts.map +1 -1
- package/dist/db/spec-queries.js +63 -0
- package/dist/db/spec-queries.js.map +1 -1
- package/dist/enforce/enforce.d.ts +70 -0
- package/dist/enforce/enforce.d.ts.map +1 -0
- package/dist/enforce/enforce.js +125 -0
- package/dist/enforce/enforce.js.map +1 -0
- package/dist/extraction/specs/markdown-spec-extractor.d.ts +21 -0
- package/dist/extraction/specs/markdown-spec-extractor.d.ts.map +1 -1
- package/dist/extraction/specs/markdown-spec-extractor.js +144 -0
- package/dist/extraction/specs/markdown-spec-extractor.js.map +1 -1
- package/dist/fitness/fitness.d.ts +75 -0
- package/dist/fitness/fitness.d.ts.map +1 -0
- package/dist/fitness/fitness.js +204 -0
- package/dist/fitness/fitness.js.map +1 -0
- package/dist/graph/maintainability.d.ts +101 -0
- package/dist/graph/maintainability.d.ts.map +1 -0
- package/dist/graph/maintainability.js +278 -0
- package/dist/graph/maintainability.js.map +1 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +149 -1
- package/dist/index.js.map +1 -1
- package/dist/installer/instructions-template.d.ts +5 -4
- package/dist/installer/instructions-template.d.ts.map +1 -1
- package/dist/installer/instructions-template.js +10 -4
- package/dist/installer/instructions-template.js.map +1 -1
- package/dist/installer/targets/claude.d.ts.map +1 -1
- package/dist/installer/targets/claude.js +4 -0
- package/dist/installer/targets/claude.js.map +1 -1
- package/dist/installer/targets/shared.d.ts.map +1 -1
- package/dist/installer/targets/shared.js +4 -0
- package/dist/installer/targets/shared.js.map +1 -1
- package/dist/mcp/fitness-tool.d.ts +12 -0
- package/dist/mcp/fitness-tool.d.ts.map +1 -0
- package/dist/mcp/fitness-tool.js +46 -0
- package/dist/mcp/fitness-tool.js.map +1 -0
- package/dist/mcp/maintainability-tool.d.ts +13 -0
- package/dist/mcp/maintainability-tool.d.ts.map +1 -0
- package/dist/mcp/maintainability-tool.js +64 -0
- package/dist/mcp/maintainability-tool.js.map +1 -0
- package/dist/mcp/server-instructions.d.ts +1 -1
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +3 -2
- package/dist/mcp/server-instructions.js.map +1 -1
- package/dist/mcp/spec-tools.d.ts.map +1 -1
- package/dist/mcp/spec-tools.js +115 -1
- package/dist/mcp/spec-tools.js.map +1 -1
- package/dist/mcp/tools.d.ts +13 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +75 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/reflect/apply.d.ts +31 -0
- package/dist/reflect/apply.d.ts.map +1 -0
- package/dist/reflect/apply.js +286 -0
- package/dist/reflect/apply.js.map +1 -0
- package/dist/reflect/hash.d.ts +20 -0
- package/dist/reflect/hash.d.ts.map +1 -0
- package/dist/reflect/hash.js +36 -0
- package/dist/reflect/hash.js.map +1 -0
- package/dist/reflect/index.d.ts +16 -0
- package/dist/reflect/index.d.ts.map +1 -0
- package/dist/reflect/index.js +43 -0
- package/dist/reflect/index.js.map +1 -0
- package/dist/reflect/miner.d.ts +21 -0
- package/dist/reflect/miner.d.ts.map +1 -0
- package/dist/reflect/miner.js +463 -0
- package/dist/reflect/miner.js.map +1 -0
- package/dist/reflect/store.d.ts +31 -0
- package/dist/reflect/store.d.ts.map +1 -0
- package/dist/reflect/store.js +101 -0
- package/dist/reflect/store.js.map +1 -0
- package/dist/reflect/sweep.d.ts +26 -0
- package/dist/reflect/sweep.d.ts.map +1 -0
- package/dist/reflect/sweep.js +42 -0
- package/dist/reflect/sweep.js.map +1 -0
- package/dist/reflect/targets.d.ts +52 -0
- package/dist/reflect/targets.d.ts.map +1 -0
- package/dist/reflect/targets.js +192 -0
- package/dist/reflect/targets.js.map +1 -0
- package/dist/reflect/types.d.ts +96 -0
- package/dist/reflect/types.d.ts.map +1 -0
- package/dist/reflect/types.js +13 -0
- package/dist/reflect/types.js.map +1 -0
- package/dist/resolution/domain-gap-seed.d.ts +60 -0
- package/dist/resolution/domain-gap-seed.d.ts.map +1 -0
- package/dist/resolution/domain-gap-seed.js +87 -0
- package/dist/resolution/domain-gap-seed.js.map +1 -0
- package/dist/resolution/spec-link-resolver.d.ts +46 -1
- package/dist/resolution/spec-link-resolver.d.ts.map +1 -1
- package/dist/resolution/spec-link-resolver.js +78 -0
- package/dist/resolution/spec-link-resolver.js.map +1 -1
- package/dist/server/routes/domain.js +0 -0
- package/dist/server/routes/events.js +20 -0
- package/dist/server/routes/maintainability.js +18 -0
- package/dist/server/routes/reflect.js +93 -0
- package/dist/server/server.js +6 -0
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -1
- package/dist/web/chunk-EZZBWC7Z.js +1 -0
- package/dist/web/chunk-ITHLF4GI.js +1 -0
- package/dist/web/{chunk-UBOZGQNK.js → chunk-IZQXYQNQ.js} +1 -1
- package/dist/web/chunk-JBO7ZIPO.js +1 -0
- package/dist/web/chunk-JQXCRIK2.js +1 -0
- package/dist/web/{chunk-ASZ77FMZ.js → chunk-JT2YIHPL.js} +1 -1
- package/dist/web/{chunk-WLIMNDS3.js → chunk-ONKHTXHJ.js} +1 -1
- package/dist/web/{chunk-FHZHD2ZG.js → chunk-P5JX67LT.js} +1 -1
- package/dist/web/chunk-PDTQX2UL.js +6 -0
- package/dist/web/chunk-TPDB5GJN.js +1 -0
- package/dist/web/{chunk-RASJHUXS.js → chunk-XWDR6MPC.js} +1 -1
- package/dist/web/chunk-Y6WWDS4R.js +1 -0
- package/dist/web/chunk-Z5L3T5EO.js +1 -0
- package/dist/web/{chunk-D5OCNEJA.js → chunk-ZG7H3XPL.js} +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/main-GYPY5V5H.js +1 -0
- package/dist/workflows/executor.d.ts.map +1 -1
- package/dist/workflows/executor.js +3 -0
- package/dist/workflows/executor.js.map +1 -1
- package/dist/workflows/runners/prompt.d.ts +31 -8
- package/dist/workflows/runners/prompt.d.ts.map +1 -1
- package/dist/workflows/runners/prompt.js +117 -23
- package/dist/workflows/runners/prompt.js.map +1 -1
- package/dist/workflows/runners/types.d.ts +7 -1
- package/dist/workflows/runners/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/web/chunk-HGBF7AY5.js +0 -1
- package/dist/web/chunk-JQ534IB6.js +0 -6
- package/dist/web/chunk-MVOMVPYB.js +0 -1
- package/dist/web/chunk-NZEZCT65.js +0 -1
- package/dist/web/chunk-WQWYTRFN.js +0 -1
- package/dist/web/main-LHCYPOXF.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.
|
package/dist/bin/specship.js
CHANGED
|
@@ -1127,6 +1127,256 @@ function main() {
|
|
|
1127
1127
|
};
|
|
1128
1128
|
renderNode(root, '', true, 0);
|
|
1129
1129
|
}
|
|
1130
|
+
/**
|
|
1131
|
+
* specship reflect
|
|
1132
|
+
*
|
|
1133
|
+
* Run a reflection pass over the ingested Claude Code transcripts and print the
|
|
1134
|
+
* self-improvement proposals it surfaces (REQ-REFLECT-006.A1). Headless — no
|
|
1135
|
+
* dashboard required. With no usable transcript history, prints an empty state.
|
|
1136
|
+
*/
|
|
1137
|
+
program
|
|
1138
|
+
.command('reflect [path]')
|
|
1139
|
+
.description('Mine ingested transcripts for self-improvement proposals (memory rules, skills, hooks)')
|
|
1140
|
+
.option('-j, --json', 'Output as JSON')
|
|
1141
|
+
.action(async (pathArg, options) => {
|
|
1142
|
+
const projectPath = resolveProjectPath(pathArg);
|
|
1143
|
+
try {
|
|
1144
|
+
if (!(0, directory_1.isInitialized)(projectPath)) {
|
|
1145
|
+
error(`SpecShip not initialized in ${projectPath}`);
|
|
1146
|
+
process.exit(1);
|
|
1147
|
+
}
|
|
1148
|
+
const { default: SpecShip } = await loadSpecShip();
|
|
1149
|
+
const cg = await SpecShip.open(projectPath);
|
|
1150
|
+
const result = cg.reflectAnalyze();
|
|
1151
|
+
if (options.json) {
|
|
1152
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1153
|
+
cg.destroy();
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
if (result.empty || result.open.length === 0) {
|
|
1157
|
+
info('No proposals yet — not enough signal in the ingested transcripts.');
|
|
1158
|
+
info('Tip: run the dashboard with `specship serve --ui --ingest` to build transcript history.');
|
|
1159
|
+
cg.destroy();
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
const sevColor = {
|
|
1163
|
+
high: chalk.red,
|
|
1164
|
+
warn: chalk.yellow,
|
|
1165
|
+
info: chalk.cyan,
|
|
1166
|
+
};
|
|
1167
|
+
const typeLabel = {
|
|
1168
|
+
memory_rule: 'memory/rule',
|
|
1169
|
+
skill: 'skill',
|
|
1170
|
+
hook: 'hook',
|
|
1171
|
+
};
|
|
1172
|
+
console.log(chalk.bold(`\nReflection proposals (${result.open.length}):\n`));
|
|
1173
|
+
for (const p of result.open) {
|
|
1174
|
+
const sev = (sevColor[p.severity] ?? chalk.white)(p.severity.toUpperCase().padEnd(5));
|
|
1175
|
+
console.log(`${sev} ${chalk.bold(p.title)}`);
|
|
1176
|
+
console.log(chalk.dim(` ${typeLabel[p.type] ?? p.type} → ${p.targetPath}`));
|
|
1177
|
+
console.log(chalk.dim(` ${p.evidence.detail}`));
|
|
1178
|
+
console.log(chalk.dim(` ${p.body}`));
|
|
1179
|
+
console.log();
|
|
1180
|
+
}
|
|
1181
|
+
info('Review and apply proposals from the dashboard Improvements page (preview-diff → confirm).');
|
|
1182
|
+
cg.destroy();
|
|
1183
|
+
}
|
|
1184
|
+
catch (err) {
|
|
1185
|
+
error(`reflect failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1186
|
+
process.exit(1);
|
|
1187
|
+
}
|
|
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
|
+
});
|
|
1130
1380
|
/**
|
|
1131
1381
|
* specship serve
|
|
1132
1382
|
*/
|
|
@@ -1861,6 +2111,64 @@ function main() {
|
|
|
1861
2111
|
cg.close();
|
|
1862
2112
|
}
|
|
1863
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
|
+
});
|
|
1864
2172
|
// @implements REQ-FUNNEL-004
|
|
1865
2173
|
program
|
|
1866
2174
|
.command('spec [id]')
|