create-cadmo 0.2.0 → 0.3.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/README.md CHANGED
@@ -14,6 +14,6 @@ Drops into the current directory, never overwriting anything:
14
14
  - `cadmo/plan.md` — the internal route: tasks, constraints, how to validate
15
15
  - `cadmo/decision.md` — architecture decisions with a "when to reconsider" trigger
16
16
  - `cadmo/spec-drift.mjs` + a ready CI workflow — **specs must change with the code they watch** (the drift guard)
17
- - `.claude/commands/cadmo-*.md` — `/cadmo-gate`, `/cadmo-spec`, `/cadmo-done` slash commands (when Claude Code is detected, or `--claude`)
17
+ - `.claude/commands/cadmo-*.md` — `/cadmo-gate`, `/cadmo-spec`, `/cadmo-done` slash commands (when Claude Code is detected, or `--claude`). **Pick one source for the commands:** if you install the [Cadmo plugin](https://github.com/tiagotorres91/cadmo) (`/plugin install cadmo@cadmo`, which provides `/cadmo:gate` etc.), run the scaffolder with `--no-claude` — otherwise you'll have both spellings.
18
18
 
19
19
  The method, docs and protocols: **https://github.com/tiagotorres91/cadmo**
package/index.js CHANGED
@@ -29,6 +29,7 @@ if (args.includes('--help') || args.includes('-h')) {
29
29
  cadmo/plan.md the internal route: tasks, constraints, validation
30
30
  cadmo/decision.md ADR with a "when to reconsider" trigger
31
31
  cadmo/spec-drift.mjs the drift guard: specs must change with the code they watch
32
+ cadmo/cadmo-grammar.mjs the shared front-matter/glob grammar the guard imports
32
33
  cadmo/spec-drift-workflow.yml ready CI workflow (move into .github/workflows/)
33
34
  .claude/commands/cadmo-*.md (when Claude Code is detected, or --claude)
34
35
 
@@ -46,6 +47,7 @@ const FILES = [
46
47
  ['spec.md', path.join('cadmo', 'spec.md')],
47
48
  ['plan.md', path.join('cadmo', 'plan.md')],
48
49
  ['decision.md', path.join('cadmo', 'decision.md')],
50
+ ['cadmo-grammar.mjs', path.join('cadmo', 'cadmo-grammar.mjs')],
49
51
  ['spec-drift.mjs', path.join('cadmo', 'spec-drift.mjs')],
50
52
  ['spec-drift-workflow.yml', path.join('cadmo', 'spec-drift-workflow.yml')],
51
53
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-cadmo",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Scaffold the Cadmo method into your project \u2014 a right-sized method for building software with AI as your pair.",
5
5
  "keywords": [
6
6
  "cadmo",
@@ -29,7 +29,7 @@
29
29
  "create-cadmo": "index.js"
30
30
  },
31
31
  "engines": {
32
- "node": ">=16"
32
+ "node": ">=18"
33
33
  },
34
34
  "scripts": {
35
35
  "test": "node --test"
@@ -0,0 +1,88 @@
1
+ // cadmo-grammar — THE single grammar for Cadmo front matter and watch-globs.
2
+ // Every consumer (spec-drift, cadmo-score, the plugin's Stop hook, the scaffolded
3
+ // copy) uses THIS logic; the copies are byte-identical and guarded by
4
+ // scripts/check-template-sync.sh. Round-4 review found three divergent
5
+ // reimplementations (one carrying an already-fixed globstar bug) — hence this file.
6
+ //
7
+ // Accepted `watches:` forms (all equivalent):
8
+ // watches: | watches: | watches: [a/**, b.py] | watches: a/**
9
+ // - a/** | - a/** |
10
+ // - b.py | - b.py |
11
+ // (indented block, unindented block, inline array, single inline value)
12
+
13
+ export function frontMatter(text) {
14
+ if (!text.startsWith('---')) return null;
15
+ const end = text.indexOf('\n---', 3);
16
+ if (end < 0) return null;
17
+ return { fm: text.slice(3, end), end };
18
+ }
19
+
20
+ function unquote(s) {
21
+ return s.trim().replace(/^["']|["']$/g, '');
22
+ }
23
+
24
+ export function parseWatches(text) {
25
+ const parsed = frontMatter(text);
26
+ if (!parsed) return null;
27
+ const lines = parsed.fm.split(/\r?\n/);
28
+ const watches = [];
29
+ let inWatches = false;
30
+ for (const line of lines) {
31
+ const head = line.match(/^watches:\s*(.*)$/);
32
+ if (head) {
33
+ inWatches = true;
34
+ const inline = head[1].trim();
35
+ if (inline.startsWith('[')) {
36
+ for (const part of inline.replace(/^\[|\]\s*$/g, '').split(',')) {
37
+ const v = unquote(part);
38
+ if (v) watches.push(v);
39
+ }
40
+ inWatches = false; // inline array is complete on this line
41
+ } else if (inline) {
42
+ watches.push(unquote(inline)); // single inline value
43
+ inWatches = false;
44
+ }
45
+ continue;
46
+ }
47
+ if (inWatches) {
48
+ const item = line.match(/^\s*-\s+(.+?)\s*$/); // indented OR unindented list item
49
+ if (item) watches.push(unquote(item[1]));
50
+ else if (line.trim() !== '') inWatches = false; // next key ends the block
51
+ }
52
+ }
53
+ return watches.length ? watches : null;
54
+ }
55
+
56
+ export function parseReviewed(text) {
57
+ const parsed = frontMatter(text);
58
+ if (!parsed) return null;
59
+ for (const line of parsed.fm.split(/\r?\n/)) {
60
+ const m = line.match(/^reviewed:\s*(\S+)\s*$/);
61
+ if (m) return m[1];
62
+ }
63
+ return null;
64
+ }
65
+
66
+ // Glob -> RegExp. `*` matches within one path segment; `**` matches zero or more
67
+ // segments (gitignore semantics: src/**/auth.js matches src/auth.js).
68
+ // Zero-backslash implementation (escaping via char classes / fromCharCode).
69
+ const BS = String.fromCharCode(92);
70
+ export function globToRegex(glob) {
71
+ let esc = '';
72
+ for (const ch of glob) {
73
+ if (ch === '*' || ch === '/') esc += ch;
74
+ else if ('.+^$(){}|[]?'.includes(ch) || ch === BS) esc += BS + ch;
75
+ else esc += ch;
76
+ }
77
+ esc = esc.split('**').join('__DSTAR__');
78
+ esc = esc.split('*').join('[^/]*');
79
+ esc = esc.split('/__DSTAR__/').join('/(?:.*/)?'); // a/**/b -> a/b and a/x/y/b
80
+ esc = esc.split('__DSTAR__/').join('(?:.*/)?'); // **/b -> b and x/b
81
+ esc = esc.split('/__DSTAR__').join('(?:/.*)?'); // a/** -> a and a/x
82
+ esc = esc.split('__DSTAR__').join('.*'); // bare **
83
+ return new RegExp('^' + esc + '$');
84
+ }
85
+
86
+ export function anyMatch(watches, file) {
87
+ return watches.some((w) => globToRegex(w).test(file));
88
+ }
@@ -1,3 +1,6 @@
1
+ ---
2
+ description: Run the definition of done honestly — checked against evidence (run the commands), not memory
3
+ ---
1
4
  # /cadmo-done — the definition of done, checked honestly
2
5
 
3
6
  Run the definition of done for: **$ARGUMENTS** (or the work we just finished).
@@ -1,3 +1,6 @@
1
+ ---
2
+ description: Open a value gate — is this worth building? (a 5-line business case before any spec)
3
+ ---
1
4
  # /cadmo-gate — open a value gate
2
5
 
3
6
  Open a value gate for: **$ARGUMENTS**
@@ -1,3 +1,6 @@
1
+ ---
2
+ description: Write the spec — acceptance criteria first, for the client to validate before any code
3
+ ---
1
4
  # /cadmo-spec — write the spec, criteria first
2
5
 
3
6
  Write the spec for: **$ARGUMENTS**
package/templates/plan.md CHANGED
@@ -1,21 +1,26 @@
1
- # <Demand name> — plan (internal)
2
-
3
- Pairs with `spec.md` (acceptance criteria live there don't repeat them).
4
-
5
- ## Objective
6
- What changes in the world when this is done (1–3 lines).
7
-
8
- ## Investigation (optional)
9
- What was tried/considered before and why it was discarded (3–6 lines) avoids re-investigating dead ends.
10
-
11
- ## Context & constraints
12
- Current state, decisions already made, what NOT to change.
13
-
14
- ## Technical plan
15
- Files/tables/endpoints touched; approach; alternatives discarded.
16
-
17
- ## Tasks
18
- - [ ] Small, verifiable (status kept here — any session resumes without getting lost)
19
-
20
- ## How to validate
21
- Commands/flows that prove the acceptance criteria (build, tests, e2e, real manual flow).
1
+ # <Demand name> — plan (internal)
2
+
3
+ For new-and-relevant work this pairs with `spec.md`; for a standard 3+ step fix it stands alone.
4
+
5
+ ## Objective
6
+ What changes in the world when this is done (1–3 lines).
7
+
8
+ ## Acceptance criteria
9
+ - [ ] Observable and specific ("login returns a 24h JWT", not "user logs in")
10
+ - [ ] If there IS a separate `spec.md`, the criteria live THERE — don't duplicate; delete this section.
11
+ If this plan stands alone (no spec), this is their home — criteria before code, always.
12
+
13
+ ## Investigation (optional)
14
+ What was tried/considered before and why it was discarded (3–6 lines) — avoids re-investigating dead ends.
15
+
16
+ ## Context & constraints
17
+ Current state, decisions already made, what NOT to change.
18
+
19
+ ## Technical plan
20
+ Files/tables/endpoints touched; approach; alternatives discarded.
21
+
22
+ ## Tasks
23
+ - [ ] Small, verifiable (status kept here — any session resumes without getting lost)
24
+
25
+ ## How to validate
26
+ Commands/flows that prove the acceptance criteria (build, tests, e2e, real manual flow).
@@ -1,12 +1,15 @@
1
1
  # Example workflow — copy to .github/workflows/spec-drift.yml in your project.
2
- # Fails a PR when a file watched by a spec changes and the spec doesn't change with it.
3
- # Escape hatch (audited, never silent): write "spec-drift: skip <reason>" in the PR body.
2
+ # Fails when a file watched by a spec changes and the spec doesn't change with it.
3
+ # Works on pull_request AND on push (so it fires even if you ship straight to main).
4
+ # Audited escape hatch (never silent): write "spec-drift: skip -- <reason>" in the
5
+ # latest commit message.
4
6
  name: spec-drift
5
7
  on:
6
8
  pull_request:
9
+ push:
10
+ branches: [main]
7
11
  jobs:
8
12
  drift:
9
- if: "${{ !contains(github.event.pull_request.body, 'spec-drift: skip') }}"
10
13
  runs-on: ubuntu-latest
11
14
  steps:
12
15
  - uses: actions/checkout@v4
@@ -16,4 +19,15 @@ jobs:
16
19
  with:
17
20
  node-version: "20"
18
21
  - name: specs must change with the code they govern
19
- run: node cadmo/spec-drift.mjs --base origin/${{ github.base_ref }}
22
+ run: |
23
+ if git log -1 --format=%B | grep -qi 'spec-drift: skip'; then
24
+ echo "spec-drift: skip found in commit message — skipping."
25
+ exit 0
26
+ fi
27
+ if [ "${{ github.event_name }}" = "pull_request" ]; then
28
+ BASE="origin/${{ github.base_ref }}"
29
+ else
30
+ BASE="${{ github.event.before }}"
31
+ git cat-file -e "${BASE}^{commit}" 2>/dev/null || BASE="$(git rev-parse HEAD~1)"
32
+ fi
33
+ node cadmo/spec-drift.mjs --base "$BASE"
@@ -1,32 +1,49 @@
1
1
  #!/usr/bin/env node
2
2
  /*
3
- * spec-drift — the rule that keeps documentation honest:
4
- * a spec declares which files implement it; if those files change and the spec
5
- * doesn't change in the same diff, the check fails.
3
+ * spec-drift — the rule that keeps documentation honest.
6
4
  *
7
- * No dependencies. How it works:
8
- * 1. Scans the repo for .md files with a `watches:` list in their front matter.
9
- * 2. Diffs the current branch against --base (default: origin/main).
10
- * 3. For each spec: if a watched file changed and the spec didn't → DRIFT.
5
+ * Two layers, both zero-dependency:
11
6
  *
12
- * Usage:
13
- * node spec-drift.mjs [--base origin/main] [--dir .]
7
+ * DRIFT (v1): a spec declares which files implement it (`watches:`); if those
8
+ * files change in the diff and the spec doesn't change in the same diff → DRIFT.
9
+ * Cheap, but shallow: co-change in a diff proves the spec was TOUCHED, not that
10
+ * its content is still TRUE.
11
+ *
12
+ * SUSPECT (v2, opt-in): a "reviewed-state" layer. `--stamp <spec>` records a
13
+ * sha256 over the current content of the spec's watched files as `reviewed: <hash>`
14
+ * — a human affirming "I re-read this code and this spec is still true." The check
15
+ * recomputes that hash; a mismatch reports SUSPECT even if the spec was edited.
16
+ * Honesty note: the hash proves bytes unchanged, not semantic truth — a human
17
+ * still has to actually re-read before re-stamping. The full-length sha256 is
18
+ * an honesty marker, not an adversarial control: for adversarial assurance use
19
+ * signed tags / Sigstore (see cadmo-validate's N2/N3 levels).
20
+ *
21
+ * SUSPECT scoping ("checks activate by surface touched"): by default, SUSPECT only
22
+ * fires for specs whose WATCHED SET intersects the current diff — an innocent
23
+ * README-only PR must not fail CI for a stamp a previous merge skipped. Use
24
+ * `--suspect-all` for the full repo-state check (push-to-main / scheduled runs).
25
+ *
26
+ * A spec WITHOUT a `reviewed:` key gets the DRIFT check only — SUSPECT is opt-in
27
+ * (right-sizing: pay the re-read discipline only where it earns its keep).
14
28
  *
15
- * Spec front matter example:
16
- * ---
17
- * watches:
18
- * - src/billing/**
19
- * - api/invoices.py
20
- * ---
29
+ * Grammar (front matter forms, glob semantics) lives in ./cadmo-grammar.mjs —
30
+ * the SINGLE grammar shared by spec-drift, cadmo-score and the plugin hook.
31
+ *
32
+ * Usage:
33
+ * node spec-drift.mjs [--base origin/main] [--dir .] [--suspect-all]
34
+ * node spec-drift.mjs --stamp <spec.md> [--dir .]
21
35
  *
22
36
  * Escape hatch (audited, not silent): if a change genuinely doesn't alter any
23
37
  * documented rule, say so where reviewers can see it — e.g. run the CI step
24
- * only when the PR body does NOT contain "spec-drift: skip — <reason>".
38
+ * only when the commit message does NOT contain "spec-drift: skip — <reason>".
25
39
  */
26
40
  import { execFileSync } from 'node:child_process';
41
+ import crypto from 'node:crypto';
27
42
  import fs from 'node:fs';
28
43
  import path from 'node:path';
44
+ import { frontMatter, parseWatches as watchesOf, parseReviewed as reviewedOf, globToRegex } from './cadmo-grammar.mjs';
29
45
 
46
+ const BS = String.fromCharCode(92);
30
47
  const args = process.argv.slice(2);
31
48
  function opt(name, fallback) {
32
49
  const i = args.indexOf(name);
@@ -34,6 +51,12 @@ function opt(name, fallback) {
34
51
  }
35
52
  const BASE = opt('--base', 'origin/main');
36
53
  const ROOT = path.resolve(opt('--dir', '.'));
54
+ const STAMP = opt('--stamp', null);
55
+ const SUSPECT_ALL = args.includes('--suspect-all');
56
+ if (args.includes('--stamp') && !STAMP) {
57
+ console.error('spec-drift --stamp: missing <spec.md> argument.');
58
+ process.exit(2);
59
+ }
37
60
 
38
61
  // --- collect specs that declare `watches:` in front matter ---
39
62
  function* mdFiles(dir) {
@@ -45,40 +68,82 @@ function* mdFiles(dir) {
45
68
  }
46
69
  }
47
70
 
48
- function parseWatches(file) {
49
- const text = fs.readFileSync(file, 'utf8');
50
- if (!text.startsWith('---')) return null;
51
- const end = text.indexOf('\n---', 3);
52
- if (end < 0) return null;
53
- const fm = text.slice(3, end);
54
- const lines = fm.split('\n');
55
- const watches = [];
56
- let inWatches = false;
57
- for (const line of lines) {
58
- if (/^watches:\s*$/.test(line)) { inWatches = true; continue; }
59
- if (inWatches) {
60
- const m = line.match(/^\s+-\s+(.+?)\s*$/);
61
- if (m) watches.push(m[1].replace(/^["']|["']$/g, ''));
62
- else if (!/^\s/.test(line)) inWatches = false;
63
- }
71
+ const readSpec = (file) => fs.readFileSync(file, 'utf8');
72
+
73
+ // Add or update a top-level `reviewed:` key, preserving everything else
74
+ // (including the file's EOL style — a CRLF file must not gain a lone LF line).
75
+ function setReviewed(text, hash) {
76
+ const eol = text.includes('\r\n') ? '\r\n' : '\n';
77
+ const parsed = frontMatter(text);
78
+ const rest = text.slice(parsed.end); // '\n---' … onwards
79
+ let lines = parsed.fm.split(/\r?\n/);
80
+ let found = false;
81
+ lines = lines.map((l) => {
82
+ if (/^reviewed:\s*/.test(l)) { found = true; return `reviewed: ${hash}`; }
83
+ return l;
84
+ });
85
+ if (!found) {
86
+ if (lines.length && lines[lines.length - 1] === '') lines[lines.length - 1] = `reviewed: ${hash}`;
87
+ else lines.push(`reviewed: ${hash}`);
88
+ }
89
+ return '---' + lines.join(eol) + rest;
90
+ }
91
+
92
+ // --- reviewed-state hashing ---
93
+ // Line endings are normalized so a CRLF working tree (git autocrlf) and the
94
+ // LF-stored HEAD blob hash identically. Watched files are text source.
95
+ function norm(buf) {
96
+ return buf.toString('utf8').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
97
+ }
98
+
99
+ function matched(watches, files) {
100
+ const regexes = watches.map(globToRegex);
101
+ return files.filter((f) => regexes.some((r) => r.test(f))).sort();
102
+ }
103
+
104
+ function hashWatchedDisk(watches) {
105
+ const tracked = execFileSync('git', ['ls-files'], { cwd: ROOT, encoding: 'utf8' }).split('\n').filter(Boolean);
106
+ const h = crypto.createHash('sha256');
107
+ for (const f of matched(watches, tracked)) {
108
+ h.update(f); h.update('\n');
109
+ h.update(norm(fs.readFileSync(path.join(ROOT, f))));
64
110
  }
65
- return watches.length ? watches : null;
111
+ return h.digest('hex');
66
112
  }
67
113
 
68
- // --- minimal glob → regex (supports ** and *) ---
69
- function globToRegex(glob) {
70
- // zero-backslash implementation: escape specials via char classes, never via backslash
71
- const BS = String.fromCharCode(92);
72
- let esc = '';
73
- for (const ch of glob) {
74
- if (ch === '*' || ch === '/') esc += ch;
75
- else if ('.+^$(){}|[]?'.includes(ch) || ch === BS) esc += BS + ch;
76
- else esc += ch;
114
+ function hashWatchedHead(watches) {
115
+ const tracked = execFileSync('git', ['ls-tree', '-r', 'HEAD', '--name-only'], { cwd: ROOT, encoding: 'utf8' }).split('\n').filter(Boolean);
116
+ const h = crypto.createHash('sha256');
117
+ for (const f of matched(watches, tracked)) {
118
+ h.update(f); h.update('\n');
119
+ h.update(norm(execFileSync('git', ['show', `HEAD:${f}`], { cwd: ROOT })));
77
120
  }
78
- esc = esc.split('**').join('__DSTAR__');
79
- esc = esc.split('*').join('[^/]*');
80
- esc = esc.split('__DSTAR__').join('.*');
81
- return new RegExp('^' + esc + '$');
121
+ return h.digest('hex');
122
+ }
123
+
124
+ // --- --stamp mode: re-affirm a spec after re-reading its watched files ---
125
+ if (STAMP) {
126
+ const specPath = path.resolve(ROOT, STAMP);
127
+ if (!fs.existsSync(specPath)) {
128
+ console.error(`spec-drift --stamp: no such spec: ${STAMP}`);
129
+ process.exit(2);
130
+ }
131
+ const watches = watchesOf(readSpec(specPath));
132
+ if (!watches) {
133
+ console.error(`spec-drift --stamp: ${STAMP} has no \`watches:\` front matter — nothing to stamp.`);
134
+ process.exit(2);
135
+ }
136
+ const hash = hashWatchedDisk(watches);
137
+ // Footgun guard: stamping uncommitted content records a hash HEAD can never match.
138
+ try {
139
+ const head = hashWatchedHead(watches);
140
+ if (head !== hash) {
141
+ console.error('spec-drift --stamp: WARNING — watched files differ between working tree and HEAD; you are stamping UNCOMMITTED content. Commit the watched changes (or stamp after committing) or the check will report SUSPECT.');
142
+ }
143
+ } catch { /* no HEAD yet (fresh repo) — nothing to compare */ }
144
+ fs.writeFileSync(specPath, setReviewed(readSpec(specPath), hash));
145
+ console.log(`spec-drift: stamped ${STAMP} — reviewed: ${hash}`);
146
+ process.exit(0);
82
147
  }
83
148
 
84
149
  // --- changed files vs base ---
@@ -93,8 +158,9 @@ try {
93
158
 
94
159
  const specs = [];
95
160
  for (const f of mdFiles(ROOT)) {
96
- const watches = parseWatches(f);
97
- if (watches) specs.push({ spec: path.relative(ROOT, f).split(String.fromCharCode(92)).join('/'), watches });
161
+ const text = readSpec(f);
162
+ const watches = watchesOf(text);
163
+ if (watches) specs.push({ spec: path.relative(ROOT, f).split(BS).join('/'), watches, reviewed: reviewedOf(text) });
98
164
  }
99
165
 
100
166
  if (!specs.length) {
@@ -104,17 +170,30 @@ if (!specs.length) {
104
170
 
105
171
  const changedSet = new Set(changed);
106
172
  let drift = 0;
107
- for (const { spec, watches } of specs) {
173
+ let suspect = 0;
174
+ for (const { spec, watches, reviewed } of specs) {
108
175
  const regexes = watches.map(globToRegex);
109
- const hits = changed.filter(c => regexes.some(r => r.test(c)));
176
+ const hits = changed.filter((c) => regexes.some((r) => r.test(c)));
110
177
  if (hits.length && !changedSet.has(spec)) {
111
178
  drift++;
112
179
  console.error(`DRIFT: ${hits.join(', ')} changed, but ${spec} (which watches ${watches.join(', ')}) did not change in this diff.`);
113
180
  }
181
+ // SUSPECT: opt-in via `reviewed:`, and diff-scoped unless --suspect-all —
182
+ // only a diff that touches this spec's watched surface puts it on trial here.
183
+ if (reviewed && (SUSPECT_ALL || hits.length)) {
184
+ const now = hashWatchedHead(watches);
185
+ if (now !== reviewed) {
186
+ suspect++;
187
+ console.error(`SUSPECT: ${spec} was reviewed at ${reviewed.slice(0, 12)}… but its watched files hash ${now.slice(0, 12)}… at HEAD — the code changed since the last review. Re-read the watched files, then re-run: node spec-drift.mjs --stamp ${spec}`);
188
+ }
189
+ }
114
190
  }
115
191
 
116
- if (drift) {
117
- console.error(`\nspec-drift: ${drift} spec(s) out of sync. Update the spec in the same change — or state why no documented rule changed (visibly, where reviewers can see it).`);
192
+ if (drift || suspect) {
193
+ const parts = [];
194
+ if (drift) parts.push(`${drift} drifting`);
195
+ if (suspect) parts.push(`${suspect} suspect`);
196
+ console.error(`\nspec-drift: ${parts.join(', ')}. Update the spec in the same change (or state why no documented rule changed, visibly); re-stamp any SUSPECT spec after actually re-reading its watched files — the hash matches bytes, only you can vouch for the meaning.`);
118
197
  process.exit(1);
119
198
  }
120
199
  console.log(`spec-drift: ${specs.length} spec(s) checked against ${changed.length} changed file(s) — in sync ✓`);
package/templates/spec.md CHANGED
@@ -1,24 +1,38 @@
1
- # Spec <name>
2
-
3
- **Status:** draft — awaiting validation (<who>) · **Demand:** <link/id>
4
-
5
- ## Goal & context
6
- The business problem and what changes in the world (2–5 lines, in the client's language).
7
- (Relevant work already passed the value gate its success metric becomes an acceptance criterion here.)
8
-
9
- ## Acceptance criteria
10
- - [ ] Simple ones in free form
11
- - [ ] Critical ones (money / data / integration / security) in EARS:
12
- WHEN <trigger>, THE SYSTEM SHALL <behavior>
13
-
14
- ## Data (AI features only)
15
- Source, owner, access, quality/bias, corpus refresh plan.
16
-
17
- ## Risks (critical or AI demands only)
18
- Risk response. For AI: corpus drift, provider API change, cost, regulation.
19
-
20
- ## Out of scope
21
- What this delivery does NOT cover (controlled expectations).
22
-
23
- ## Impact on stable specs
24
- Which sections of the system/domain specs will be updated on delivery (the absorption).
1
+ <!-- To enforce this spec against code, add real YAML front matter as the VERY FIRST lines:
2
+ ---
3
+ watches:
4
+ - src/the-area-this-governs/**
5
+ - api/the-endpoint.py
6
+ ---
7
+ Then spec-drift fails any change to those files that doesn't update this spec. -->
8
+
9
+ # Spec — <name>
10
+
11
+ **Status:** draft awaiting validation (<who>) · **Demand:** <link/id>
12
+
13
+ ## Goal & context
14
+ The business problem and what changes in the world (2–5 lines, in the client's language).
15
+ (Relevant work already passed the value gate — its success metric becomes an acceptance criterion here.)
16
+
17
+ ## Acceptance criteria
18
+ - [ ] Simple ones in free form
19
+ - [ ] Critical ones (money / data / integration / security) in EARS:
20
+ WHEN <trigger>, THE SYSTEM SHALL <behavior>
21
+
22
+ ## Data (AI features only)
23
+ Source, owner, access, quality/bias, corpus refresh plan.
24
+
25
+ ## Risks (critical or AI demands only)
26
+ Risk → response. For AI: corpus drift, provider API change, cost, regulation.
27
+
28
+ ## Out of scope
29
+ What this delivery does NOT cover (controlled expectations).
30
+
31
+ ## Impact on stable specs
32
+ Which sections of the system/domain specs will be updated on delivery (the absorption).
33
+
34
+ ## Impact on existing data (only if this touches persisted state)
35
+ Does this change data that already exists — a DB schema/migration, saved user records, files a
36
+ prior version produced? Name what existing data is affected and the migration/backfill plan (or
37
+ "none — new data only"). A rule that changes *how* something is computed does not retroactively
38
+ fix rows already written; say how the old ones are handled.