@holmes-lab/holmes-kit 0.1.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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/bin/holmes-hook-antigravity.js +31 -0
- package/bin/holmes-kit.js +23 -0
- package/bin/holmes-mcp.js +34 -0
- package/bin/holmes-stop-antigravity.js +29 -0
- package/dist/.build-id +1 -0
- package/dist/holmes/cli/agents.js +168 -0
- package/dist/holmes/cli/doctor.js +625 -0
- package/dist/holmes/cli/gitignore-merge.js +84 -0
- package/dist/holmes/cli/governed-precondition.js +157 -0
- package/dist/holmes/cli/index.js +384 -0
- package/dist/holmes/cli/init.js +462 -0
- package/dist/holmes/cli/playbook-skills.js +711 -0
- package/dist/holmes/cli/roles-readme.js +134 -0
- package/dist/holmes/cli/settings-merge.js +122 -0
- package/dist/holmes/config/config.js +70 -0
- package/dist/holmes/context/bundler.js +114 -0
- package/dist/holmes/context/render.js +29 -0
- package/dist/holmes/context/tiers.js +110 -0
- package/dist/holmes/context/tokens.js +8 -0
- package/dist/holmes/cpg/cpg-scanner.js +213 -0
- package/dist/holmes/cpg/hash-cache.js +86 -0
- package/dist/holmes/cpg/language-parser-walk.js +917 -0
- package/dist/holmes/cpg/language-parser-worker.js +81 -0
- package/dist/holmes/cpg/language-parser.js +234 -0
- package/dist/holmes/cpg/scan-cache.js +108 -0
- package/dist/holmes/cpg/source-path.js +44 -0
- package/dist/holmes/cpg/test-files.js +84 -0
- package/dist/holmes/governance/constitution-debt.js +73 -0
- package/dist/holmes/governance/constitution-report.js +25 -0
- package/dist/holmes/governance/constitution.js +129 -0
- package/dist/holmes/governance/identity.js +30 -0
- package/dist/holmes/governance/ledger-lock.js +165 -0
- package/dist/holmes/governance/ledger-store.conformance.js +90 -0
- package/dist/holmes/governance/ledger-store.js +106 -0
- package/dist/holmes/governance/progress-ledger.js +83 -0
- package/dist/holmes/governance/provenance-chain.js +365 -0
- package/dist/holmes/governance/provenance-ledger.js +0 -0
- package/dist/holmes/governance/provenance-schema.js +47 -0
- package/dist/holmes/governance/replica-id.js +106 -0
- package/dist/holmes/governance/role-policy.js +137 -0
- package/dist/holmes/governance/trust-score.js +43 -0
- package/dist/holmes/guardrail/anchors.js +31 -0
- package/dist/holmes/guardrail/blind-spots.js +38 -0
- package/dist/holmes/guardrail/decision-ledger.js +107 -0
- package/dist/holmes/guardrail/executable-artifact.js +129 -0
- package/dist/holmes/guardrail/governance-history.js +101 -0
- package/dist/holmes/guardrail/phase.js +169 -0
- package/dist/holmes/guardrail/risk-classifier.js +450 -0
- package/dist/holmes/guardrail/risk-gate.js +160 -0
- package/dist/holmes/guardrail/risk-types.js +6 -0
- package/dist/holmes/guardrail/tspec-state.js +392 -0
- package/dist/holmes/guardrail/write-target.js +224 -0
- package/dist/holmes/hooks/adapters/antigravity.js +194 -0
- package/dist/holmes/hooks/pre-tool-use.js +1262 -0
- package/dist/holmes/hooks/stop.js +416 -0
- package/dist/holmes/mcp/basis.js +162 -0
- package/dist/holmes/mcp/handlers.js +1831 -0
- package/dist/holmes/mcp/server.js +71 -0
- package/dist/holmes/mcp/stdio-client.js +165 -0
- package/dist/holmes/mcp/supervisor.js +178 -0
- package/dist/holmes/mcp/tool-schemas.js +394 -0
- package/dist/holmes/mcp/validate-args.js +281 -0
- package/dist/holmes/messages/registry.js +50 -0
- package/dist/holmes/project/baseline.js +210 -0
- package/dist/holmes/project/change-source.js +233 -0
- package/dist/holmes/project/ignore.js +145 -0
- package/dist/holmes/project/root.js +113 -0
- package/dist/holmes/reverse/anchor.js +162 -0
- package/dist/holmes/reverse/cluster.js +187 -0
- package/dist/holmes/reverse/draft.js +151 -0
- package/dist/holmes/reverse/dynamic-wiring.js +47 -0
- package/dist/holmes/reverse/scan.js +194 -0
- package/dist/holmes/reverse/surface.js +154 -0
- package/dist/holmes/reverse/test-map.js +263 -0
- package/dist/holmes/review/coverage.js +33 -0
- package/dist/holmes/review/findings.js +123 -0
- package/dist/holmes/review/package.js +40 -0
- package/dist/holmes/review/review-targets.js +92 -0
- package/dist/holmes/review/scope.js +57 -0
- package/dist/holmes/review/test-evidence.js +77 -0
- package/dist/holmes/review/test-runner.js +572 -0
- package/dist/holmes/rtm/dataflow-taint.js +262 -0
- package/dist/holmes/rtm/gap-analyzer.js +27 -0
- package/dist/holmes/rtm/git-changes.js +72 -0
- package/dist/holmes/rtm/incremental.js +45 -0
- package/dist/holmes/rtm/localize.js +100 -0
- package/dist/holmes/rtm/rtm-builder.js +191 -0
- package/dist/holmes/rtm/rtm-check.js +89 -0
- package/dist/holmes/rtm/rtm-graph.js +232 -0
- package/dist/holmes/rtm/taint.js +92 -0
- package/dist/holmes/rtm/test-scope.js +336 -0
- package/dist/holmes/spec/approval-blockers.js +204 -0
- package/dist/holmes/spec/breaking-change.js +89 -0
- package/dist/holmes/spec/legacy-format.js +87 -0
- package/dist/holmes/spec/spec-digest.js +71 -0
- package/dist/holmes/spec/spec-parser.js +106 -0
- package/dist/holmes/spec/spec-store.conformance.js +118 -0
- package/dist/holmes/spec/spec-store.js +331 -0
- package/dist/holmes/spec/spec-types.js +177 -0
- package/dist/holmes/spec/validator.js +280 -0
- package/package.json +76 -0
- package/playbooks/adopt/PLAYBOOK.md +125 -0
- package/playbooks/author-slice/PLAYBOOK.md +119 -0
- package/playbooks/promote-slice/PLAYBOOK.md +134 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.citationDigest = citationDigest;
|
|
4
|
+
exports.pinCitations = pinCitations;
|
|
5
|
+
exports.verifyCitationDigests = verifyCitationDigests;
|
|
6
|
+
exports.validateCitations = validateCitations;
|
|
7
|
+
exports.validateSpec = validateSpec;
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const spec_types_1 = require("./spec-types");
|
|
10
|
+
const spec_digest_1 = require("./spec-digest");
|
|
11
|
+
const legacy_format_1 = require("./legacy-format");
|
|
12
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
13
|
+
/**
|
|
14
|
+
* The canonical `rev` value for a citation: a content digest of the source as it was read.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately computed from bytes with node:crypto and nothing else — no git, no network, no
|
|
17
|
+
* tracker API. A purely local harness with a wiki page saved to disk, a repository with no VCS, and
|
|
18
|
+
* an air-gapped machine all produce the same value for the same content, which is what makes this
|
|
19
|
+
* the one `rev` scheme that is always available. Truncated to 16 hex chars: this detects drift, it
|
|
20
|
+
* does not resist a motivated forger, and the surrounding provenance chain is what covers that.
|
|
21
|
+
*/
|
|
22
|
+
function citationDigest(content) {
|
|
23
|
+
return `sha256:${(0, node_crypto_1.createHash)('sha256').update(content).digest('hex').slice(0, 16)}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Fill in `rev` for citations whose `ref` resolves to readable content and that have none yet —
|
|
27
|
+
* the authoring counterpart of verifyCitationDigests, so nobody has to hash a file by hand.
|
|
28
|
+
*
|
|
29
|
+
* NEVER OVERWRITES AN EXISTING rev. That restriction is the whole safety property: re-pinning a
|
|
30
|
+
* stale citation would silently replace the evidence of drift with a fresh-looking digest, turning
|
|
31
|
+
* the one mechanism that detects upstream change into the mechanism that hides it. When an existing
|
|
32
|
+
* digest no longer matches, this reports drift and leaves the stored value alone — resolving it is a
|
|
33
|
+
* human decision (re-read the source and confirm the requirement still holds), not a rewrite.
|
|
34
|
+
*
|
|
35
|
+
* Pinning asserts "this content is what I am citing", so it only ever runs when explicitly invoked.
|
|
36
|
+
*/
|
|
37
|
+
function pinCitations(citations, read) {
|
|
38
|
+
const findings = [];
|
|
39
|
+
const pinned = [];
|
|
40
|
+
if (!Array.isArray(citations))
|
|
41
|
+
return { citations: [], pinned, findings };
|
|
42
|
+
const out = citations.map((entry, i) => {
|
|
43
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry))
|
|
44
|
+
return entry;
|
|
45
|
+
const c = { ...entry };
|
|
46
|
+
const ref = c.ref;
|
|
47
|
+
if (typeof ref !== 'string' || ref.trim() === '' || ref.trim().toUpperCase() === spec_types_1.CITATION_PLACEHOLDER)
|
|
48
|
+
return entry;
|
|
49
|
+
let content = null;
|
|
50
|
+
try {
|
|
51
|
+
content = read(ref);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
content = null;
|
|
55
|
+
}
|
|
56
|
+
if (content == null) {
|
|
57
|
+
// Not an error: an external ticket with no adapter simply cannot be pinned from here.
|
|
58
|
+
findings.push({ level: 'warn', code: 'citation-unresolvable', message: `source[${i}] "${ref}" could not be read here — pin it where the source is reachable` });
|
|
59
|
+
return entry;
|
|
60
|
+
}
|
|
61
|
+
const digest = citationDigest(content);
|
|
62
|
+
const existing = c.rev;
|
|
63
|
+
if (typeof existing === 'string' && existing !== '') {
|
|
64
|
+
if (existing.startsWith('sha256:') && existing !== digest) {
|
|
65
|
+
findings.push({ level: 'warn', code: 'citation-drift', message: `source[${i}] "${ref}" changed since it was cited (${existing} -> ${digest}) — re-read it and confirm the requirement still holds; the stored rev was left untouched` });
|
|
66
|
+
}
|
|
67
|
+
return entry; // an existing pin is evidence; it is never rewritten
|
|
68
|
+
}
|
|
69
|
+
c.rev = digest;
|
|
70
|
+
pinned.push(ref);
|
|
71
|
+
return c;
|
|
72
|
+
});
|
|
73
|
+
return { citations: out, pinned, findings };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Re-derive each citation's digest and report the ones that no longer match — the step that turns a
|
|
77
|
+
* stored `rev` string from decoration into actual drift detection.
|
|
78
|
+
*
|
|
79
|
+
* `read` resolves a `ref` to the source's current bytes, returning null when it cannot be fetched
|
|
80
|
+
* (an external ticket with no adapter configured, a deleted file). Unfetchable is NOT a failure: the
|
|
81
|
+
* point is to flag sources that DID change, not to demand that every citation be resolvable in every
|
|
82
|
+
* environment. Injected rather than importing fs so the rule stays pure and testable.
|
|
83
|
+
*/
|
|
84
|
+
function verifyCitationDigests(citations, read) {
|
|
85
|
+
const out = [];
|
|
86
|
+
if (!Array.isArray(citations))
|
|
87
|
+
return out;
|
|
88
|
+
citations.forEach((entry, i) => {
|
|
89
|
+
if (entry === null || typeof entry !== 'object')
|
|
90
|
+
return;
|
|
91
|
+
const c = entry;
|
|
92
|
+
const { ref, rev } = c;
|
|
93
|
+
if (typeof ref !== 'string' || typeof rev !== 'string' || !rev.startsWith('sha256:'))
|
|
94
|
+
return;
|
|
95
|
+
let current = null;
|
|
96
|
+
try {
|
|
97
|
+
current = read(ref);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
current = null;
|
|
101
|
+
}
|
|
102
|
+
if (current == null)
|
|
103
|
+
return; // unresolvable here — not evidence of drift
|
|
104
|
+
const now = citationDigest(current);
|
|
105
|
+
if (now !== rev) {
|
|
106
|
+
out.push({
|
|
107
|
+
level: 'warn',
|
|
108
|
+
code: 'citation-drift',
|
|
109
|
+
message: `source[${i}] "${ref}" changed since it was cited (${rev} -> ${now}) — re-read it and confirm the requirement still holds`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
const isPlaceholder = (s) => s.trim() === '' || s.trim().toUpperCase() === spec_types_1.CITATION_PLACEHOLDER;
|
|
116
|
+
/**
|
|
117
|
+
* Validate a REQ's `source` as a list of structured citations.
|
|
118
|
+
*
|
|
119
|
+
* Two tiers, mirroring how the T-SPEC coverage gate works. SHAPE problems are always errors — a
|
|
120
|
+
* citation the machine cannot parse is worse than none, because it looks like provenance. COMPLETENESS
|
|
121
|
+
* problems (a placeholder ref, no revision and no read date) are warnings while the REQ is still being
|
|
122
|
+
* written and become errors on approval, so a spec can be created and iterated without fighting the
|
|
123
|
+
* validator, but cannot be APPROVED while its provenance is a stub.
|
|
124
|
+
*
|
|
125
|
+
* Exported for direct testing: this is the rule set that decides whether a requirement's origin is
|
|
126
|
+
* auditable, and it should be pinned independently of the surrounding spec checks.
|
|
127
|
+
*/
|
|
128
|
+
function validateCitations(value, approved) {
|
|
129
|
+
const f = [];
|
|
130
|
+
const err = (code, message) => f.push({ level: 'error', code, message });
|
|
131
|
+
// A completeness gap blocks approval but not authoring.
|
|
132
|
+
const gap = (code, message) => f.push({ level: approved ? 'error' : 'warn', code, message });
|
|
133
|
+
if (typeof value === 'string') {
|
|
134
|
+
err('citation-legacy-string', `source must be a list of citations, not free text. Replace with:\n` +
|
|
135
|
+
` source:\n - kind: <${spec_types_1.CITATION_KINDS.slice(0, 4).join('|')}|…>\n ref: "<locator>"\n retrieved: YYYY-MM-DD`);
|
|
136
|
+
return f;
|
|
137
|
+
}
|
|
138
|
+
if (!Array.isArray(value)) {
|
|
139
|
+
err('citation-not-a-list', 'source must be a list of citations');
|
|
140
|
+
return f;
|
|
141
|
+
}
|
|
142
|
+
if (value.length === 0) {
|
|
143
|
+
err('citation-empty', 'source must cite at least one origin');
|
|
144
|
+
return f;
|
|
145
|
+
}
|
|
146
|
+
value.forEach((entry, i) => {
|
|
147
|
+
const at = `source[${i}]`;
|
|
148
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
149
|
+
err('citation-not-an-object', `${at} must be a mapping with kind/ref`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const c = entry;
|
|
153
|
+
const kind = c.kind;
|
|
154
|
+
if (typeof kind !== 'string' || !spec_types_1.CITATION_KINDS.includes(kind)) {
|
|
155
|
+
err('citation-bad-kind', `${at}.kind "${String(kind)}" is not one of: ${spec_types_1.CITATION_KINDS.join(', ')}`);
|
|
156
|
+
}
|
|
157
|
+
const ref = c.ref;
|
|
158
|
+
if (typeof ref !== 'string')
|
|
159
|
+
err('citation-missing-ref', `${at}.ref is required and must be a string`);
|
|
160
|
+
else if (isPlaceholder(ref))
|
|
161
|
+
gap('citation-placeholder', `${at}.ref is still "${spec_types_1.CITATION_PLACEHOLDER}" — cite the real origin`);
|
|
162
|
+
// `rev` pins WHAT was read, `retrieved` pins WHEN. A source with neither cannot be re-checked
|
|
163
|
+
// later, which defeats the point of citing it: an upstream edit becomes undetectable.
|
|
164
|
+
const rev = c.rev;
|
|
165
|
+
const retrieved = c.retrieved;
|
|
166
|
+
if (rev != null && typeof rev !== 'string')
|
|
167
|
+
err('citation-bad-rev', `${at}.rev must be a string`);
|
|
168
|
+
if (retrieved != null) {
|
|
169
|
+
// A YAML date scalar arrives as a Date; accept either that or an ISO string.
|
|
170
|
+
const asString = retrieved instanceof Date ? retrieved.toISOString().slice(0, 10) : retrieved;
|
|
171
|
+
if (typeof asString !== 'string' || !ISO_DATE.test(asString)) {
|
|
172
|
+
err('citation-bad-retrieved', `${at}.retrieved must be an ISO date (YYYY-MM-DD), got "${String(retrieved)}"`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const hasRev = typeof rev === 'string' && !isPlaceholder(rev);
|
|
176
|
+
const hasRetrieved = retrieved != null;
|
|
177
|
+
const mutable = typeof kind === 'string' && spec_types_1.MUTABLE_CITATION_KINDS.includes(kind);
|
|
178
|
+
if (mutable && !hasRev) {
|
|
179
|
+
// A mutable source without a version pin cannot be re-checked: the page is edited, the ticket
|
|
180
|
+
// is re-scoped, and the citation silently starts describing something that no longer exists.
|
|
181
|
+
gap('citation-mutable-unpinned', `${at}.kind "${String(kind)}" can change after being cited, so rev is required — use a content digest (sha256:…) of what was read`);
|
|
182
|
+
}
|
|
183
|
+
else if (!hasRev && !hasRetrieved) {
|
|
184
|
+
// Immutable kinds are pinned by ref + when it happened; one of the two must still be present.
|
|
185
|
+
gap('citation-unpinned', `${at} has neither rev nor retrieved — nothing pins what was read`);
|
|
186
|
+
}
|
|
187
|
+
if (c.note != null && typeof c.note !== 'string')
|
|
188
|
+
err('citation-bad-note', `${at}.note must be a string`);
|
|
189
|
+
});
|
|
190
|
+
return f;
|
|
191
|
+
}
|
|
192
|
+
function validateSpec(spec, resolve) {
|
|
193
|
+
const f = [];
|
|
194
|
+
const def = (0, spec_types_1.specTypeDef)(spec.type);
|
|
195
|
+
const err = (code, message) => f.push({ level: 'error', code, message });
|
|
196
|
+
// @implements A-SPEC-184
|
|
197
|
+
// Code and level are UNCHANGED — only the sentence improves. Interpolating `spec.type` printed the
|
|
198
|
+
// literal `undefined` for every pre-format document (155 of 483 here), naming neither the document
|
|
199
|
+
// nor the reason. `legacyMessage` names both, and distinguishes "older format, upgradable" from
|
|
200
|
+
// "a kind this tool does not have".
|
|
201
|
+
if (!def) {
|
|
202
|
+
err('unknown-type', (0, legacy_format_1.legacyMessage)(spec));
|
|
203
|
+
return { ok: false, findings: f };
|
|
204
|
+
}
|
|
205
|
+
if (!def.idRegex.test(spec.id))
|
|
206
|
+
err('bad-id', `id "${spec.id}" fails ${def.idRegex}`);
|
|
207
|
+
for (const field of def.requiredFields)
|
|
208
|
+
if (spec.frontmatter[field] == null)
|
|
209
|
+
err('missing-field', `missing frontmatter: ${field}`);
|
|
210
|
+
for (const sec of def.requiredSections)
|
|
211
|
+
if (!(sec in spec.sections))
|
|
212
|
+
err('missing-section', `missing section: ## ${sec}`);
|
|
213
|
+
// A REQ's origin is the one link in the chain that no downstream artefact can reconstruct, so its
|
|
214
|
+
// citation is checked for shape here rather than left as free text.
|
|
215
|
+
if (spec.type === 'REQ' && spec.frontmatter.source != null) {
|
|
216
|
+
f.push(...validateCitations(spec.frontmatter.source, spec.status === 'approved'));
|
|
217
|
+
}
|
|
218
|
+
// @implements A-SPEC-132
|
|
219
|
+
// Approval seals (REQ-132): an approved governed spec must carry the digest of the content that
|
|
220
|
+
// was approved, that digest must still match, and its snapshot of each sealed parent must match
|
|
221
|
+
// the parent's CURRENT seal. Errors (not warns) on approved specs — the constitution's ART-3
|
|
222
|
+
// blocks the Stop on validation errors, so seals are enforced with no new wiring. Drafts carry no
|
|
223
|
+
// seal obligations at all: the gap() asymmetry, applied to the graph's internal edges. Measured
|
|
224
|
+
// origin: an Edit rewriting an approved A-SPEC's body returned `allow` with no approval anywhere.
|
|
225
|
+
if (spec.status === 'approved') {
|
|
226
|
+
const seal = (0, spec_digest_1.sealOf)(spec);
|
|
227
|
+
if (!seal.approvedDigest) {
|
|
228
|
+
err('unsealed-approval', `approved without approved_digest — approval must seal content (spec_approve)`);
|
|
229
|
+
}
|
|
230
|
+
else if (seal.approvedDigest !== (0, spec_digest_1.specDigest)(spec)) {
|
|
231
|
+
err('post-approval-edit', `content differs from the sealed approved_digest — the approved text was edited after approval; re-approve via spec_approve`);
|
|
232
|
+
}
|
|
233
|
+
for (const pid of spec.dependsOn) {
|
|
234
|
+
const parent = resolve(pid);
|
|
235
|
+
if (!parent || parent.status !== 'approved')
|
|
236
|
+
continue; // dangling/draft parents are other findings' business
|
|
237
|
+
const parentSeal = (0, spec_digest_1.sealOf)(parent).approvedDigest;
|
|
238
|
+
if (!parentSeal)
|
|
239
|
+
continue; // an unsealed parent is ITS unsealed-approval, not this child's staleness
|
|
240
|
+
if (seal.parentDigests[pid] !== parentSeal) {
|
|
241
|
+
err('stale-parent', `parent ${pid} was re-approved (or never snapshotted) since this spec's seal — re-approve to re-baseline the chain`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// relations
|
|
246
|
+
if (def.parents.length === 0 && spec.dependsOn.length > 0)
|
|
247
|
+
err('root-has-parent', `${spec.type} must have empty depends_on`);
|
|
248
|
+
if (def.parents.length > 0 && spec.dependsOn.length === 0)
|
|
249
|
+
err('orphan', `${spec.type} requires a parent of type ${def.parents.join('|')}`);
|
|
250
|
+
for (const pid of spec.dependsOn) {
|
|
251
|
+
const parent = resolve(pid);
|
|
252
|
+
if (!parent) {
|
|
253
|
+
err('dangling-parent', `depends_on "${pid}" not found`);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (!def.parents.includes(parent.type))
|
|
257
|
+
err('wrong-parent-type', `parent ${pid} is ${parent.type}, expected ${def.parents.join('|')}`);
|
|
258
|
+
}
|
|
259
|
+
// gate: T-SPEC 4-quadrant coverage on approve attempt
|
|
260
|
+
if (spec.type === 'T-SPEC' && spec.status === 'approved') {
|
|
261
|
+
const cov = (spec.frontmatter.coverage ?? {});
|
|
262
|
+
for (const q of ['normal', 'corner', 'negative', 'boundary']) {
|
|
263
|
+
if (!cov[q]) {
|
|
264
|
+
err('coverage-gap', `T-SPEC approved requires coverage.${q}=true`);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
// BDD 4-quadrant AUTO-verification (diff ②): a declared-covered quadrant must actually carry a
|
|
268
|
+
// Given/When/Then scenario in its section — not just a true flag over an empty stub. This makes
|
|
269
|
+
// coverage:{…true} HONEST (the section proves the case exists), enforcing the 4분면 gate.
|
|
270
|
+
const section = spec.sections[`${q[0].toUpperCase()}${q.slice(1)} Cases`] ?? '';
|
|
271
|
+
// Require a real Given→When→Then scenario: the three clauses IN ORDER with substantive content
|
|
272
|
+
// between them (review F5 — the old unordered `given && when && then` passed bare keyword prose
|
|
273
|
+
// like "given when then"). A trailing given/when clause still needs its consequent `then`.
|
|
274
|
+
const hasGwt = /\bgiven\b.{2,}?\bwhen\b.{2,}?\bthen\b/is.test(section);
|
|
275
|
+
if (!hasGwt)
|
|
276
|
+
err('empty-quadrant', `coverage.${q}=true but '## ${q[0].toUpperCase()}${q.slice(1)} Cases' has no Given/When/Then scenario`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return { ok: f.filter((x) => x.level === 'error').length === 0, findings: f };
|
|
280
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@holmes-lab/holmes-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
|
|
5
|
+
"main": "dist/holmes/mcp/server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"holmes-kit": "bin/holmes-kit.js",
|
|
8
|
+
"holmes-mcp": "bin/holmes-mcp.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"dist/",
|
|
13
|
+
"playbooks/",
|
|
14
|
+
"CHANGELOG.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc && node -e \"const{execSync}=require('child_process'),fs=require('fs');const id=(()=>{try{return execSync('git rev-parse --short HEAD').toString().trim()}catch{return 'nogit'}})();fs.writeFileSync('dist/.build-id',id+'-'+Date.now().toString(36))\"",
|
|
21
|
+
"test": "npm run typecheck && jest",
|
|
22
|
+
"mcp": "node bin/holmes-mcp.js",
|
|
23
|
+
"release": "npm run build && node scripts/make-release.js",
|
|
24
|
+
"typecheck": "tsc --noEmit -p tsconfig.test.json"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"ase",
|
|
28
|
+
"agentic",
|
|
29
|
+
"spec-driven",
|
|
30
|
+
"rtm",
|
|
31
|
+
"cpg",
|
|
32
|
+
"traceability",
|
|
33
|
+
"mcp",
|
|
34
|
+
"guardrail"
|
|
35
|
+
],
|
|
36
|
+
"author": "SungNam Park <snpark.phd@gmail.com>",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
40
|
+
"@types/jest": "^29.5.12",
|
|
41
|
+
"@types/js-yaml": "^4.0.9",
|
|
42
|
+
"@types/node": "^20.19.39",
|
|
43
|
+
"jest": "^29.7.0",
|
|
44
|
+
"ts-jest": "^29.1.2"
|
|
45
|
+
},
|
|
46
|
+
"jest": {
|
|
47
|
+
"preset": "ts-jest",
|
|
48
|
+
"testEnvironment": "node",
|
|
49
|
+
"testMatch": [
|
|
50
|
+
"**/src/**/*.test.ts"
|
|
51
|
+
],
|
|
52
|
+
"testTimeout": 30000
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
56
|
+
"better-sqlite3": "^12.9.0",
|
|
57
|
+
"js-yaml": "^4.1.1",
|
|
58
|
+
"tree-sitter": "^0.21.1",
|
|
59
|
+
"tree-sitter-c-sharp": "^0.21.3",
|
|
60
|
+
"tree-sitter-cpp": "^0.21.0",
|
|
61
|
+
"tree-sitter-go": "^0.21.2",
|
|
62
|
+
"tree-sitter-java": "^0.21.0",
|
|
63
|
+
"tree-sitter-python": "^0.21.0",
|
|
64
|
+
"tree-sitter-rust": "^0.21.0",
|
|
65
|
+
"tree-sitter-typescript": "^0.21.2",
|
|
66
|
+
"typescript": "^5.0.0"
|
|
67
|
+
},
|
|
68
|
+
"repository": {
|
|
69
|
+
"type": "git",
|
|
70
|
+
"url": "git+https://github.com/snpark-io/holmes-kit.git"
|
|
71
|
+
},
|
|
72
|
+
"homepage": "https://github.com/snpark-io/holmes-kit#readme",
|
|
73
|
+
"bugs": {
|
|
74
|
+
"url": "https://github.com/snpark-io/holmes-kit/issues"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: holmes-adopt
|
|
3
|
+
description: >-
|
|
4
|
+
Use when reverse_draft refuses with "parentReqId is required" or "was not found in the spec store",
|
|
5
|
+
or when bringing an existing codebase with no specs under Holmes-Kit governance — recovering
|
|
6
|
+
traceability from legacy code via reverse_scan, reverse_draft, and reverse_anchor.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# adopt
|
|
10
|
+
|
|
11
|
+
스펙 없이 자란 코드베이스를 거버넌스 아래로 들이는 절차다. 도구는 셋이고 순서가 있다:
|
|
12
|
+
**`reverse_scan`(읽기 전용 조사) → REQ 작성(사람의 일) → `reverse_draft`(초안 생성) →
|
|
13
|
+
검토·승인 → `reverse_anchor`(앵커 삽입).** 이 순서가 강제되는 지점마다 도구가 거부하며,
|
|
14
|
+
그 거부들이 이 플레이북의 트리거다.
|
|
15
|
+
|
|
16
|
+
## 도구가 대신 써 주지 않는 것
|
|
17
|
+
|
|
18
|
+
`reverse_draft`는 `parentReqId` 없이 거부한다:
|
|
19
|
+
|
|
20
|
+
> parentReqId is required. A REQ states business intent, which does not exist in the code and
|
|
21
|
+
> cannot be recovered from it — write the REQ first, then re-run with its id.
|
|
22
|
+
|
|
23
|
+
이건 미구현이 아니라 설계다. **어떤 코드 경로도 REQ를 만들지 않는다.** 역공학은 코드가 무엇을
|
|
24
|
+
*하는지*를 복원하지, 무엇을 *위한지*는 복원하지 못한다 — 의도는 사람·티켓·사건에 있다. 그래서
|
|
25
|
+
REQ는 사람이 쓴다: 왜 이 코드베이스를 거버넌스 아래 들이는지, 무엇이 성공인지. 쓰는 법은
|
|
26
|
+
`author-slice`다. 그리고 승인까지 기다릴 필요는 없다 — `reverse_draft`의 부모 검사는 존재와
|
|
27
|
+
타입만 보므로 **draft REQ로 충분하다.**
|
|
28
|
+
|
|
29
|
+
부모 검사의 나머지 두 거부: 스토어에 없는 id는 `parent REQ <id> was not found in the spec store —
|
|
30
|
+
create it first`, REQ가 아닌 스펙을 대면 `<id> is a H-SPEC; drafts must hang off a REQ`.
|
|
31
|
+
|
|
32
|
+
## 절차
|
|
33
|
+
|
|
34
|
+
1. **`reverse_scan(root)` — 읽기 전용.** 트리를 바이트 하나 바꾸지 않고 A-SPEC 후보 클러스터,
|
|
35
|
+
응집도 증거, 커버리지 보고를 돌려준다. git 저장소일 필요도 없다(`isGit:false`로 계속한다).
|
|
36
|
+
보고에서 볼 것: 클러스터 목록과 각각의 키, **미해석 동적 배선과 미보정 언어** — 지표 0이
|
|
37
|
+
"깨끗함"이 아니라 "측정 불가"일 수 있음을 보고 자체가 말해 준다.
|
|
38
|
+
2. **부모 REQ를 쓴다.** `author-slice`의 REQ 절 그대로. 역공학 REQ의 요체는 채택의 의도다 —
|
|
39
|
+
이 코드베이스가 무엇이고, 왜 지금 거버넌스가 필요하고, 어디까지가 이번 채택의 범위인지.
|
|
40
|
+
3. **`reverse_draft(root, parentReqId)` — 기본은 dry-run이다.** `dryRun: false`를 명시해야
|
|
41
|
+
쓴다. 클러스터마다 H/A/T 3종이 전부 `draft`로, REQ→H→A→T로 연결되어 나온다. 먼저 dry-run
|
|
42
|
+
결과에서 `drafted` · `skipped` · `refused`를 읽고, 납득한 뒤에 쓰기로 재실행한다.
|
|
43
|
+
4. **특정 클러스터만 초안하려면 `cluster: <키>`.** 키는 scan 보고의 클러스터 키(디렉토리, per-file
|
|
44
|
+
분할이면 파일 경로)다. **오타 난 키는 조용히 0건이 아니라 거부다** — `availableClusters`
|
|
45
|
+
목록을 실어 돌려주므로, 목록에서 키를 집어 재시도한다.
|
|
46
|
+
5. **남은 TODO를 채우고 검토한다.** 초안은 **스캔이 판정할 수 있는 것을 이미 적어둔다** — 파일
|
|
47
|
+
목록·응집도에 더해, `Interfaces / Contracts`에 최상위 심볼 이름(중첩·밑줄 제외)이, `Test
|
|
48
|
+
Points`에 이 슬라이스에 붙은 기존 테스트가 들어 있다. `Done When`은 구조적 완료 기준으로
|
|
49
|
+
**완결되어 있어 승인을 막지 않는다.**
|
|
50
|
+
|
|
51
|
+
그래도 `Interfaces / Contracts`와 `Test Points`는 **여전히 승인을 막는다.** 심볼 목록은
|
|
52
|
+
*표면 후보*이지 이 슬라이스가 약속하는 계약이 아니고, 테스트 매칭은 파일 이름의 정확한
|
|
53
|
+
일치만 인정하므로(실측 재현율 53%) 붙은 목록이 전부라는 보장이 없다. 그래서 사람의 일이
|
|
54
|
+
**백지 작성에서 초안 편집으로** 바뀔 뿐 사라지지는 않는다 — 공개가 아닌 이름을 지우고,
|
|
55
|
+
어떤 성질을 고정할지 고른다. Intent·Objective·Behavior·Acceptance Criteria는 코드에서
|
|
56
|
+
복원되지 않으므로 그대로 사람이 쓴다. 승인은 `promote-slice`로.
|
|
57
|
+
6. **A-SPEC 승인 후 `reverse_anchor(root, mapping)` — 역시 기본 dry-run.** `@implements` 앵커를
|
|
58
|
+
소스에 삽입한다. **승인되지 않은 A-SPEC으로의 앵커는 거부된다** (`target A-SPEC <id> is not
|
|
59
|
+
approved`) — 역공학 초안은 코드를 묘사할 뿐이고, 묘사가 승인을 대신하지 않는다.
|
|
60
|
+
`reverse_draft` 결과의 `anchorPlan`이 파일→A-SPEC 매핑을 그대로 준다.
|
|
61
|
+
|
|
62
|
+
## 이미 스펙이 있는 저장소 — 옛 양식의 문서
|
|
63
|
+
|
|
64
|
+
스펙이 아예 없는 저장소만 채택 대상인 것은 아니다. 예전 양식으로 쓰인 문서가 잔뜩 있는
|
|
65
|
+
경우가 오히려 흔하고, 그때 도구는 **세 등급**으로 대한다.
|
|
66
|
+
|
|
67
|
+
| 등급 | 판정 | 도구의 태도 |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| **옛 형식** | `type:` 선언이 없으나 id로 종류를 알 수 있다 | 읽되 **강제하지 않는다**. 보강하면 쓸 수 있다 |
|
|
70
|
+
| **현행** | `type:`이 선언돼 있다 | 거버넌스 대상 |
|
|
71
|
+
| **미지원** | id로도 종류를 알 수 없다(예: `JOB-001`) | 읽되 그대로 둔다. 보강 대상이 아니다 |
|
|
72
|
+
|
|
73
|
+
**기본값은 옛 형식 유지다.** 도구가 저장소를 훑어 일괄 변환하지 않는다 — 어느 문서가 아직
|
|
74
|
+
살아 있는 사양인지는 사람만 안다. 그래서 일괄 실행 경로 자체가 없다.
|
|
75
|
+
|
|
76
|
+
옛 형식 문서를 부모로 대면 이렇게 말한다:
|
|
77
|
+
|
|
78
|
+
> `REQ-002에 type: 선언이 없습니다 — 현행보다 이전 양식으로 쓰인 문서입니다. id로 보면
|
|
79
|
+
> REQ입니다. 이 문서를 거버넌스 아래에서 쓰려면 spec_upgrade로 보강하십시오.`
|
|
80
|
+
|
|
81
|
+
**`spec_upgrade(id)` — 지목한 하나만 올린다.** `type:`을 선언하고, 옛 상태를
|
|
82
|
+
`legacy_status`로 보존하고, `status`를 `draft`로 둔다. 보강 후 남은 검증 소견을 함께
|
|
83
|
+
돌려주므로 다음에 무엇을 채워야 할지 바로 보인다.
|
|
84
|
+
|
|
85
|
+
**보강은 승인이 아니다.** 옛 문서가 `status: Approved`였더라도 `approved`가 되지 않는다 —
|
|
86
|
+
봉인 행위를 거치지 않은 승인은 승인이 아니기 때문이다. 규범으로 만들려면 정상 승인 경로를
|
|
87
|
+
밟는다.
|
|
88
|
+
|
|
89
|
+
**파일이 옮겨진다.** 스토어는 `<id>.md`로 쓰고 종류에 맞는 폴더에 둔다. `01_req/`에 몰려
|
|
90
|
+
있던 `A-SPEC-002_x.md`는 `03_a-spec/A-SPEC-002.md`가 된다 — git에서는 rename으로 보인다.
|
|
91
|
+
지목하지 않은 문서는 바이트 하나 바뀌지 않는다.
|
|
92
|
+
|
|
93
|
+
## 알아둘 동작
|
|
94
|
+
|
|
95
|
+
- **문서가 어디에 떨어지는가.** 초안은 `root`가 아니라 **서버의 스펙 스토어**(`HOLMES_SPECS`,
|
|
96
|
+
기본 `.ax/specs`)에 써진다. 서버가 대상 프로젝트 안에서 돌면 둘이 같지만, 다른 저장소를
|
|
97
|
+
`root`로 가리키면 그 저장소의 슬라이스가 **이쪽** 스토어에 초안된다 — 의도적으로만 할 일이다.
|
|
98
|
+
- **재실행은 안전하다.** 초안된 문서는 `reverse_cluster` 키를 지니고, 같은 키의 클러스터는
|
|
99
|
+
`skipped: already drafted`로 건너뛴다. 같은 트리에 두 번 돌려도 중복 초안은 없다.
|
|
100
|
+
- **테스트 디렉토리 클러스터는 초안되지 않는다** — "tests implement nothing"으로 거부된다.
|
|
101
|
+
대신 이름 증거로 프로덕션 슬라이스에 **부착**되어 그 A-SPEC의 테스트 증거가 된다.
|
|
102
|
+
- **id는 전 종류 단일 번호 공간에서 다음 번호를 받는다.** 기존 최대가 `T-SPEC-1291`이면 다음
|
|
103
|
+
초안은 1292부터다 — 종류별로 번호가 따로 가지 않는다.
|
|
104
|
+
- **초안은 출처를 지닌다.** `reverse_engineered: true`가 문서를 서술적(descriptive)으로 표시한다.
|
|
105
|
+
검증 전까지 이 문서는 "코드가 이렇다"는 주장이지 "이래야 한다"는 규범이 아니다.
|
|
106
|
+
|
|
107
|
+
## 흔한 오해
|
|
108
|
+
|
|
109
|
+
| 오해 | 사실 |
|
|
110
|
+
|---|---|
|
|
111
|
+
| "reverse_draft가 REQ부터 만들어 준다" | 어떤 코드 경로도 REQ를 만들지 않는다. 의도는 코드에 없다 |
|
|
112
|
+
| "부모 REQ도 approved여야 한다" | 존재와 타입만 본다. draft REQ로 충분하다 |
|
|
113
|
+
| "호출하면 바로 써진다" | 기본이 dry-run이다. `dryRun: false`가 명시적 opt-in이다 |
|
|
114
|
+
| "cluster 키 오타면 그냥 0건이겠지" | 거부 + `availableClusters`다. 0건 성공과 구별된다 |
|
|
115
|
+
| "초안 나왔으니 바로 anchor" | 앵커는 approved A-SPEC만 받는다. 승인이 먼저다 |
|
|
116
|
+
| "다른 프로젝트를 root로 주면 거기에 써진다" | 서버의 스토어에 써진다. root는 읽는 곳이다 |
|
|
117
|
+
|
|
118
|
+
## 검증
|
|
119
|
+
|
|
120
|
+
`src/holmes/playbooks/adopt.test.ts` — 상시 스위트 포함. 고정하는 것: 부모 검사의 세 거부 문구
|
|
121
|
+
완전 일치, dry-run 기본값, 오타 키의 거부+`availableClusters`, 테스트 클러스터 거부, 초안 3종의
|
|
122
|
+
draft 상태와 REQ→H→A→T 연결과 `reverse_engineered`/`reverse_cluster` 출처, `anchorPlan`의
|
|
123
|
+
파일→A-SPEC 매핑, 미승인 A-SPEC 앵커 거부, 단일 번호 공간. 순수 함수 사실(`draftSpecs` ·
|
|
124
|
+
`planAnchors` · `nextIdBase`)은 픽스처 없이, 핸들러 사실은 tmpdir 스토어로(A-SPEC-129 rev.3).
|
|
125
|
+
변이 검사로 판별력을 증명한 뒤 신뢰한다.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: holmes-author-slice
|
|
3
|
+
description: >-
|
|
4
|
+
Use when a Holmes-Kit gate denies with "선행 REQ가 없습니다", "approved H-SPEC이 없습니다" while
|
|
5
|
+
no H-SPEC exists yet, or "approved A-SPEC이 없습니다" while no A-SPEC exists yet — the required
|
|
6
|
+
document has not been written at all. Also use when starting a new slice from scratch and the
|
|
7
|
+
REQ→H-SPEC→A-SPEC→T-SPEC chain must be authored in order.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# author-slice
|
|
11
|
+
|
|
12
|
+
거부가 요구하는 문서가 **아직 없을 때** 이 플레이북이다. 문서는 있는데 `status`가 막고 있다면
|
|
13
|
+
`promote-slice`다 — `spec_list`로 먼저 갈라라. `approved H-SPEC이 없습니다` / `approved A-SPEC이
|
|
14
|
+
없습니다`는 "없음"과 "draft로 있음" 두 상황에서 같은 문구로 나오므로 문구가 아니라 스토어 상태가
|
|
15
|
+
판별 기준이고, `선행 REQ가 없습니다`만은 존재 검사라서 언제나 이쪽이다.
|
|
16
|
+
|
|
17
|
+
## 사슬과 세 거부
|
|
18
|
+
|
|
19
|
+
파이프라인 입구에는 선행 조건이 없다 — `AUTHOR_REQ`는 빈 스토어에서도 언제나 허용된다. 그 아래로는
|
|
20
|
+
각 단계가 앞 단계를 요구하고, 거부마다 `next_action`이 목적지를 준다. 이 플레이북은 그 목적지에서
|
|
21
|
+
**무엇을 쓰는가**다.
|
|
22
|
+
|
|
23
|
+
| 거부 | next_action | 이 플레이북에서 볼 곳 |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `선행 REQ가 없습니다.` | `spec_create(type='REQ', ...)` | 「REQ에 쓰는 것」 |
|
|
26
|
+
| `approved H-SPEC이 없습니다.` | `H-SPEC 완성 후 spec_validate → approve` | 「H-SPEC에 쓰는 것」 + `promote-slice` |
|
|
27
|
+
| `approved A-SPEC이 없습니다.` | `A-SPEC 먼저 작성·승인` | 「A-SPEC / T-SPEC에 쓰는 것」 + `promote-slice` |
|
|
28
|
+
|
|
29
|
+
`discipline` 필드는 항상 같은 한 줄이다: **No Spec, No Code — 선행 스펙 승인 전 구현 진입 금지.**
|
|
30
|
+
|
|
31
|
+
## 절차
|
|
32
|
+
|
|
33
|
+
1. **`spec_create`로 시작한다 — 빈 파일로 시작하지 않는다.** 생성된 스펙은 `draft`이고, 타입별 필수
|
|
34
|
+
섹션이 `TODO`로, 필수 frontmatter가 형태 갖춘 스텁으로 채워져 온다. 갓 만든 스펙은 그 자리에서
|
|
35
|
+
`missing-field` 0건으로 검증을 통과한다 — 손으로 만든 파일은 필드 목록부터 다시 발굴해야 한다.
|
|
36
|
+
2. **id 규칙.** 엔진이 강제하는 것은 형식뿐이다 — `REQ-\d{3,}` 꼴, 그리고 점 하위 번호
|
|
37
|
+
(`A-SPEC-129.2`, `T-SPEC-129.2`)는 A-SPEC과 T-SPEC이 허용된다. 번호 선택은 관습이다:
|
|
38
|
+
**체인 전체가 부모 REQ의 번호를 그대로 상속한다.**
|
|
39
|
+
|
|
40
|
+
> REQ-129 → H-SPEC-129 → A-SPEC-129 → **T-SPEC-129**
|
|
41
|
+
> A-SPEC-125.5 → **T-SPEC-125.5**
|
|
42
|
+
|
|
43
|
+
T-SPEC도 예외가 아니다 — 부모 A-SPEC의 번호를 **그대로** 쓴다. 읽는 사람이 계산할
|
|
44
|
+
것이 없어야 한다. 역공학 초안만 전 종류 단일 공간의 다음 번호를 받는다(`adopt`).
|
|
45
|
+
|
|
46
|
+
**옛 관습(`A-SPEC-129` → `T-SPEC-1291`)으로 쓰인 76건은 전부 개비했다**(REQ-187).
|
|
47
|
+
`specDigest`가 id를 해시하지 않아 봉인이 유지되므로 재승인 없이 이름과 `id:` 한 줄만
|
|
48
|
+
바꿨다. 이제 이 저장소의 T-SPEC은 예외 없이 거울이며, `tspec-mirror.test.ts`가 그 상태를
|
|
49
|
+
붙잡는다. 옛 id를 남긴 것은 `.ax/ledger`뿐이고, 그것은 그때의 사실이므로 그대로 둔다.
|
|
50
|
+
3. **`TODO`를 실제 내용으로 바꾼다.** 아래 타입별 절. 스텁 `source`(kind `other`, ref `TODO`)의
|
|
51
|
+
운명은 status가 정한다: **draft에서는 warn, approved에서는 같은 검사가 error로 승격**된다.
|
|
52
|
+
그래서 초안은 자유롭게 돌고, 승인은 실제 출처 없이는 검증을 통과하지 못한다 — placeholder를
|
|
53
|
+
교체하는 시점이 승인 전이어야 하는 이유가 문구가 아니라 메커니즘이다.
|
|
54
|
+
4. **승인은 `promote-slice`.** 이 플레이북은 문서를 만들고, 저 플레이북이 올린다. 특히 코드까지
|
|
55
|
+
가려면 T-SPEC의 `depends_on`이 대상 A-SPEC을 가리켜야 한다는 함정도 저쪽에 있다.
|
|
56
|
+
|
|
57
|
+
## REQ에 쓰는 것
|
|
58
|
+
|
|
59
|
+
필수 섹션: **Problem / Need · Desired Outcome · Constraints · Success Criteria · Out of Scope.**
|
|
60
|
+
필수 필드: `source` · `created`.
|
|
61
|
+
|
|
62
|
+
REQ는 사슬 전체에서 **코드로부터 복원할 수 없는 유일한 문서**다. 의도는 사람·티켓·사건·결정에
|
|
63
|
+
있지 소스에 없다. 그래서 `source`가 가장 중요한 필드다:
|
|
64
|
+
|
|
65
|
+
- 대화에서 나온 지시는 `kind: user-request` 또는 `conversation` + `retrieved` 날짜. 이 둘은 일어난
|
|
66
|
+
순간 고정되므로 `rev`가 필요 없다.
|
|
67
|
+
- 문서를 인용하면(`plan` · `adr` · `wiki` · `analysis` 등 변할 수 있는 것) `rev`가 **필수**다 —
|
|
68
|
+
내용 다이제스트(`sha256:…`)나 원본의 버전 핸들. 인용 시점에 새로 만든 토큰은 출처가 아니라
|
|
69
|
+
인용 행위를 식별할 뿐이라 거부된다.
|
|
70
|
+
- **존재하지 않는 문서를 인용하지 않는다.** 실측이 대화록에만 있다면 `conversation`으로 적고
|
|
71
|
+
"no artefact was written"을 note에 남긴다 — `analysis`로 적으면 있지도 않은 파일의 다이제스트를
|
|
72
|
+
요구받는다.
|
|
73
|
+
|
|
74
|
+
Success Criteria는 관측 가능한 문장으로 쓴다 — 각 항목이 나중에 테스트나 실측으로 확인 가능한가를
|
|
75
|
+
기준으로. Out of Scope는 빈 절이 아니라 **거절의 기록**이다: 고려했고 안 하기로 한 것과 그 이유.
|
|
76
|
+
|
|
77
|
+
## H-SPEC에 쓰는 것
|
|
78
|
+
|
|
79
|
+
필수 섹션: **Intent · Scope (In / Out) · Design Overview · Interfaces / Contracts · Acceptance
|
|
80
|
+
Criteria · Non-Functional · Assumptions · Open Questions.** 필수 필드: `req_type` · `owner`.
|
|
81
|
+
|
|
82
|
+
부모 REQ의 Success Criteria가 이 설계로 어떻게 달성되는지가 본문이다. Open Questions는 비워두는
|
|
83
|
+
칸이 아니다 — 아직 결정하지 않은 것을 결정하지 않았다고 적는 곳이고, 닫을 때는 근거와 함께 닫는다.
|
|
84
|
+
|
|
85
|
+
## A-SPEC / T-SPEC에 쓰는 것
|
|
86
|
+
|
|
87
|
+
A-SPEC 필수 섹션: **Objective · Inputs / Outputs · Behavior · Test Points · Files to Touch · Done
|
|
88
|
+
When.** 필수 필드: `slice` · `priority` · `independent_test`.
|
|
89
|
+
|
|
90
|
+
- 각 섹션이 넘어야 할 품질 바는 `promote-slice`의 「승인의 기준」이 **소유**한다 — 여기 반복하지
|
|
91
|
+
않는 것은 두 사본이 어긋나는 것을 막기 위해서다. 쓰는 시점에 그 바를 한 번 읽고 쓰면, 승인
|
|
92
|
+
시점에 보완할 것이 없다.
|
|
93
|
+
- 하나의 H-SPEC 아래 슬라이스가 여럿이면 점 하위 번호로 나눈다(`A-SPEC-129.2`). 그 슬라이스의
|
|
94
|
+
T-SPEC은 거울이므로 `T-SPEC-129.2`다 — 점 번호가 A-SPEC과 T-SPEC 둘에만 허용되는 이유다.
|
|
95
|
+
|
|
96
|
+
T-SPEC 필수 섹션은 4분면 그대로다: **Normal · Corner · Negative · Boundary Cases.** 필수 필드
|
|
97
|
+
`coverage`는 4분면 각각의 boolean 맵이다. 각 케이스는 Given/When/Then으로, **어느 4분면이 왜 그
|
|
98
|
+
케이스인지**가 드러나게 쓴다. 그리고 frontmatter `depends_on: [대상 A-SPEC]` — 이 간선이 없으면
|
|
99
|
+
승인해도 코드 게이트가 열리지 않는다 — 그때 게이트가 `depends_on`을 지목한다(`promote-slice`의 「T-SPEC 거부는 상태를 구별해 말한다」).
|
|
100
|
+
|
|
101
|
+
## 흔한 오해
|
|
102
|
+
|
|
103
|
+
| 오해 | 사실 |
|
|
104
|
+
|---|---|
|
|
105
|
+
| "파일을 직접 만들어도 된다" | 된다. 하지만 필수 필드를 손으로 발굴하게 된다. `spec_create`가 스텁을 준다 |
|
|
106
|
+
| "스텁 `source`로 승인까지 간다" | draft에서 warn이던 placeholder 검사가 approved에서 error로 승격된다 |
|
|
107
|
+
| "대화 내용도 `rev`를 지어내야 한다" | 아니다. `conversation`/`user-request`는 불변이라 `rev` 면제다. 지어낸 토큰이야말로 거부 대상 |
|
|
108
|
+
| "REQ-129.1로 REQ를 쪼갠다" | 점 하위 번호는 A-SPEC과 그 거울인 T-SPEC에만 허용된다 |
|
|
109
|
+
| "문서를 썼는데 아직 거부된다" | 작성과 승인은 다른 단계다. `status`는 `promote-slice` 소관 |
|
|
110
|
+
|
|
111
|
+
## 검증
|
|
112
|
+
|
|
113
|
+
`src/holmes/playbooks/author-slice.test.ts` — 상시 스위트 포함. 고정하는 것: 세 거부의
|
|
114
|
+
`message` · `next_action` · `discipline` 3필드 완전 일치, `AUTHOR_REQ`의 무조건 허용, 타입별 필수
|
|
115
|
+
섹션/필드 표(전사가 아니라 `SPEC_TYPES`를 직접 읽어서), `spec_create` 스텁의 형태와 caller-wins
|
|
116
|
+
규칙, placeholder 검사의 **draft-warn → approved-error 승격**, `spec_next`의 순서 규칙, 점 하위
|
|
117
|
+
번호가 A-SPEC과 T-SPEC에 한정되는 것, 그리고 스펙 폴더 밑 `.ts`가 author 게이트로 강등되지 않는다는 사실. 번호
|
|
118
|
+
상속 관습은 엔진이 강제하지 않으므로 테스트도 고정하지 않는다 — 관습은 관습이라 말한다. 변이
|
|
119
|
+
검사로 판별력을 증명한 뒤 신뢰한다.
|