spectoflow 0.13.4 → 0.14.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
@@ -157,6 +157,11 @@ runs a skill. Improve a skill without touching the agent.
157
157
  Agents and skills follow real domain standards, cited in-file — TDD, OWASP ASVS/Top 10, C4/ADR,
158
158
  INVEST, Playwright E2E, Conventional Commits, and more — not generic one-liners.
159
159
 
160
+ A `governance` capability adds a **Spec Source Guardian** (skill `audit-source`): it keeps the spec
161
+ (intent) and the code/tests (reality) coherent — flagging drift in both directions, never auto-fixing,
162
+ surfacing findings to the Attention tab, and gating only at `done`/Major. It ships with a zero-dep
163
+ drift helper (`lib/spec-drift.js`) and an opt-in Claude Code `Stop` hook (`hooks/spec-drift.js`).
164
+
160
165
  ## Language
161
166
 
162
167
  `.spectoflow/config.json` → `language` (default `en`, incl. code comments). Switchable from the CLI
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectoflow",
3
- "version": "0.13.4",
3
+ "version": "0.14.0",
4
4
  "description": "Agent-agnostic spec-driven development framework + real-time local control plane. Markdown artifacts, intent router, workflow-by-scope.",
5
5
  "keywords": [
6
6
  "spec-driven-development",
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: spec-source-guardian
3
+ title: Spec Source Guardian
4
+ capability: governance
5
+ uses: [audit-source]
6
+ description: Keeps the spec (intent) and the code/tests (reality) in sync — flags drift, never silently fixes.
7
+ standards: [traceability matrix, EARS acceptance criteria, spec-anchored]
8
+ ---
9
+ # Spec Source Guardian
10
+
11
+ Stable team persona (the "who") for the `governance` capability. The *how* lives in the `audit-source`
12
+ skill (see `uses`). Delegate here to keep the **source of truth coherent**: the spec is the intent of
13
+ record, the code and tests are the enforced reality, and this role makes sure they don't drift apart.
14
+
15
+ ## Mandate
16
+ Audit the alignment **spec ↔ plan ↔ code ↔ tests** in both directions, and surface divergence as
17
+ advisory findings. It guards *coherence and traceability*, not spec authorship (that is the
18
+ business-analyst) and not code quality (that is the code-reviewer). It **never edits** the spec or the
19
+ code to "fix" drift — natural language is too ambiguous to auto-sync safely; it flags, and a human or
20
+ the owning capability resolves.
21
+
22
+ ## Operating standards
23
+ - **Spec-anchored, not spec-as-source.** Treat the spec as intent + decisions + acceptance criteria;
24
+ keep the code the source of truth and the **tests the enforcer**. Why: regenerating a system from
25
+ prose is lossy and unpredictable (Thoughtworks); the durable value is the spec's decisions and the
26
+ tests that pin them down, not the prose replacing the code.
27
+ - **Traceability, both directions.** Flag *orphan work* (code/tasks with no spec/decision backing) and
28
+ *dead spec* (requirements with no implementation or test). Why: drift hides in whichever direction
29
+ is unwatched; a one-way check misses half of it.
30
+ - **Acceptance criteria as the contract (EARS).** Verify the new/changed behaviour is expressed as
31
+ testable acceptance criteria and that a test actually encodes them. Why: a decision no test enforces
32
+ will silently rot; the test is what keeps intent and reality bound.
33
+ - **Advisory by default; gate only at `done` / Major.** Findings post to the Attention tab; only a
34
+ Major or a `done` is gated on unresolved drift (see `policy.md`). Why: blocking every edit is noise
35
+ and pushes teams to bypass the guardian entirely.
36
+
37
+ ## Definition of done
38
+ An audit exists that names each drift finding (direction + spec/plan/code/test involved + why), posts
39
+ it to Attention, and records a verdict (**aligned** or **drift**). No unresolved *warn*-level drift is
40
+ left unacknowledged when a Major or a `done` is declared.
41
+
42
+ ## Handoff
43
+ Posts findings to the group chat and the Attention tab via the `::spectoflow` sentinels (exact syntax
44
+ owned by the `audit-source` skill's Output contract). Drift returns to the owning capability
45
+ (business-analyst to update the spec, developer/qa to add the missing test) — it is not fixed here.
46
+
47
+ ## Guardrails
48
+ - Never edit a spec or code to reconcile drift — flag it; the owner decides how to resolve.
49
+ - Never auto-generate code from the spec, or rewrite the spec from the code — no lossy auto-sync.
50
+ - Never gate an ordinary edit; only a Major or a `done` may be gated, and only on unresolved drift.
51
+ - Prefer a `need` / Attention item over a hard block when intent is genuinely ambiguous.
52
+
53
+ ## References
54
+ - Thoughtworks — "Spec-driven development": code stays the source of truth, spec drives generation —
55
+ https://www.thoughtworks.com/en-us/insights/blog/agile-engineering-practices/spec-driven-development-unpacking-2025-new-engineering-practices
56
+ - Sean Grove (OpenAI), "The New Code" — specifications as the durable, versioned artifact —
57
+ https://lawwu.github.io/transcripts/8rABwKRsec4.html
58
+ - EARS (Easy Approach to Requirements Syntax) for testable acceptance criteria.
@@ -3,7 +3,11 @@
3
3
  A capability is a role; an agent implements it; a skill is the procedure it runs. Workflows ask for a
4
4
  capability, never a named agent — this keeps spectoflow agent-agnostic.
5
5
 
6
- Palette: intake · research · analysis · architecture · planning · testing · implementation · security · quality · design · operations.
6
+ Palette: intake · research · analysis · architecture · planning · testing · implementation · security · quality · design · operations · governance.
7
+
8
+ `governance` is the odd one out: it is **advisory, not a workflow step**. The `spec-source-guardian`
9
+ (skill `audit-source`) watches that the spec (intent) and the code/tests (reality) stay coherent, and
10
+ surfaces drift to the Attention tab; it gates only at `done`/Major (see `policy.md`), never mid-edit.
7
11
 
8
12
  | Project type | Active capabilities |
9
13
  |---|---|
@@ -328,6 +328,7 @@ function renderBoard(){
328
328
  board.append(renderPhase(ph,pl.file,filtered));
329
329
  }));
330
330
  if(!shown) board.append(noMatchState());
331
+ updatePhaseToggleAll();
331
332
  }
332
333
  // Kanban view — one column per status, filtered by the text search (columns already are the statuses).
333
334
  function renderKanban(board, tasks){
@@ -435,16 +436,26 @@ function backlogRow(r){
435
436
  return tr;
436
437
  }
437
438
 
438
- // ---- collapsed-phase state (persisted per phase title, guarded for private mode) ----
439
- function loadCollapsed(){
440
- try{ const raw=localStorage.getItem('spf-collapsed'); const arr=raw?JSON.parse(raw):[]; return new Set(Array.isArray(arr)?arr:[]); }
439
+ // ---- phase expand state: we track which phases are EXPANDED (default = none), so on a big
440
+ // project the board opens compact — just phase headers with progress — and the user opens what
441
+ // they need. Persisted per phase title (guarded for private mode). ----
442
+ function loadExpanded(){
443
+ try{ const raw=localStorage.getItem('spf-expanded'); const arr=raw?JSON.parse(raw):[]; return new Set(Array.isArray(arr)?arr:[]); }
441
444
  catch{ return new Set(); }
442
445
  }
443
- function saveCollapsed(set){ try{ localStorage.setItem('spf-collapsed', JSON.stringify([...set])); }catch{} }
444
- let collapsedPhases=loadCollapsed();
446
+ function saveExpanded(set){ try{ localStorage.setItem('spf-expanded', JSON.stringify([...set])); }catch{} }
447
+ let expandedPhases=loadExpanded();
448
+ function allPhaseTitles(){ const t=new Set(); (P.plans||[]).forEach(pl=> pl.phases.forEach(ph=> t.add(ph.title))); return [...t]; }
449
+ function updatePhaseToggleAll(){
450
+ const btn=$('#phaseToggleAll'); if(!btn) return;
451
+ const titles=allPhaseTitles();
452
+ const allOpen = titles.length>0 && titles.every(t=> expandedPhases.has(t));
453
+ btn.textContent = allOpen ? 'Collapse all' : 'Expand all';
454
+ btn.dataset.state = allOpen ? 'open' : 'closed';
455
+ }
445
456
 
446
457
  function renderPhase(ph,file,filteredTasks){
447
- const isCollapsed=collapsedPhases.has(ph.title);
458
+ const isCollapsed=!expandedPhases.has(ph.title);
448
459
  const sec=el('section','phase'+(isCollapsed?' is-collapsed':''));
449
460
  const head=el('div','phase-head'); head.tabIndex=0; head.setAttribute('role','button'); head.setAttribute('aria-expanded',String(!isCollapsed));
450
461
  head.append(el('span','chevron'));
@@ -458,8 +469,8 @@ function renderPhase(ph,file,filteredTasks){
458
469
  const toggle=()=>{
459
470
  const now=sec.classList.toggle('is-collapsed');
460
471
  head.setAttribute('aria-expanded',String(!now));
461
- if(now) collapsedPhases.add(ph.title); else collapsedPhases.delete(ph.title);
462
- saveCollapsed(collapsedPhases);
472
+ if(now) expandedPhases.delete(ph.title); else expandedPhases.add(ph.title);
473
+ saveExpanded(expandedPhases); updatePhaseToggleAll();
463
474
  };
464
475
  head.addEventListener('click',toggle);
465
476
  head.addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); toggle(); } });
@@ -953,6 +964,14 @@ $$('#statusChips .fchip').forEach(b=> b.addEventListener('click', ()=>{ filter.s
953
964
  $('#search').addEventListener('input', e=>{ filter.q=e.target.value; renderBoard(); });
954
965
  // board view switch — List (phase-grouped) vs Kanban (columns by status), persisted per viewer
955
966
  $$('#boardViewToggle .vt-btn').forEach(b=> b.addEventListener('click', ()=>{ boardView=b.dataset.view; try{ localStorage.setItem('spf-board-view',boardView); }catch{} renderBoard(); }));
967
+ // expand / collapse all phases (List view) — keeps a big board compact by default
968
+ const phaseToggleAllBtn=$('#phaseToggleAll');
969
+ if(phaseToggleAllBtn) phaseToggleAllBtn.addEventListener('click', ()=>{
970
+ const titles=allPhaseTitles();
971
+ const allOpen = titles.length>0 && titles.every(t=> expandedPhases.has(t));
972
+ if(allOpen) expandedPhases.clear(); else titles.forEach(t=> expandedPhases.add(t));
973
+ saveExpanded(expandedPhases); renderBoard();
974
+ });
956
975
  // backlog: independent filters + sortable column headers — client-side only (reset to page 1 on change)
957
976
  $$('#backlogStatusChips .fchip').forEach(b=> b.addEventListener('click', ()=>{ backlogFilter.status=b.dataset.status; backlogPage=1; renderBacklog(); }));
958
977
  $('#backlogSearch').addEventListener('input', e=>{ backlogFilter.q=e.target.value; backlogPage=1; renderBacklog(); });
@@ -65,6 +65,7 @@
65
65
  <button class="vt-btn active" data-view="list" title="Grouped by phase"><span class="tab-ico" data-icon="backlog"></span><span>List</span></button>
66
66
  <button class="vt-btn" data-view="kanban" title="Columns by status"><span class="tab-ico" data-icon="board"></span><span>Kanban</span></button>
67
67
  </div>
68
+ <button class="mini-btn" id="phaseToggleAll" title="Expand or collapse all phases">Expand all</button>
68
69
  <input type="search" id="search" class="search" placeholder="Filter tasks…" autocomplete="off" />
69
70
  </div>
70
71
  <div class="board" id="board"></div>
@@ -815,3 +815,9 @@ body.booting .wf-step2 { opacity:0; animation:rise .4s cubic-bezier(.2,.8,.2,1)
815
815
  .kanban-col-body { display:flex; flex-direction:column; gap:8px; padding:10px; }
816
816
  .kanban-empty { color:var(--faint); font-size:12px; text-align:center; padding:10px 0; }
817
817
  .kanban .task { width:auto; }
818
+
819
+ /* ---- board compactness: expand/collapse-all button + kanban column scroll (v0.13.5) ---- */
820
+ .mini-btn { font:inherit; font-size:11.5px; padding:5px 12px; border:1px solid var(--line); border-radius:999px; background:var(--surface); color:var(--muted); cursor:pointer; flex-shrink:0; }
821
+ .mini-btn:hover { color:var(--ink); border-color:var(--cool); }
822
+ .main:has(.board.is-kanban) #phaseToggleAll { display:none; } /* phases are a List concept */
823
+ .kanban-col-body { max-height:64vh; overflow-y:auto; } /* keep columns compact on a big project */
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+ /*
3
+ * Claude Code "Stop" hook (OPT-IN) — the spec-source-guardian's watchdog.
4
+ *
5
+ * At the end of an agent turn it runs the deterministic spec-drift check on the git working tree and
6
+ * appends any source-of-truth signals to the dashboard's Attention tab. Advisory only: it NEVER edits
7
+ * code or specs and NEVER fails the agent (always exits 0).
8
+ *
9
+ * Enable it by adding this to your project's .claude/settings.json (spectoflow does NOT wire it
10
+ * automatically, to avoid clobbering your settings):
11
+ *
12
+ * {
13
+ * "hooks": {
14
+ * "Stop": [
15
+ * { "hooks": [ { "type": "command", "command": "node .spectoflow/hooks/spec-drift.js" } ] }
16
+ * ]
17
+ * }
18
+ * }
19
+ *
20
+ * Deduped against still-open Attention items, so it never spams.
21
+ */
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const { execFileSync } = require('child_process');
25
+ const { classifyChange, coverageSignals } = require('../lib/spec-drift');
26
+
27
+ let input = '';
28
+ process.stdin.on('data', (c) => { input += c; });
29
+ process.stdin.on('end', () => { try { run(); } catch { /* never fail the agent */ } process.exit(0); });
30
+
31
+ function run() {
32
+ const root = process.cwd();
33
+ let changed = [];
34
+ try {
35
+ changed = execFileSync('git', ['status', '--porcelain', '-uall'], { cwd: root, encoding: 'utf8' })
36
+ .split('\n').map((l) => l.slice(3).trim()).filter(Boolean);
37
+ } catch {}
38
+ const ls = (d) => { try { return fs.readdirSync(path.join(root, d)).filter((f) => f.endsWith('.md')); } catch { return []; } };
39
+ const signals = [...classifyChange(changed), ...coverageSignals({ specs: ls('specs'), plans: ls('plans') })];
40
+ if (!signals.length) return;
41
+
42
+ const rp = path.join(root, '.spectoflow', 'runtime.json');
43
+ let rt = {};
44
+ try { rt = JSON.parse(fs.readFileSync(rp, 'utf8')); } catch {}
45
+ rt.attention = rt.attention || [];
46
+ const open = new Set(rt.attention.filter((a) => a.status !== 'resolved').map((a) => a.text));
47
+ let added = false;
48
+ for (const s of signals) {
49
+ const text = 'Source-of-truth: ' + s.msg;
50
+ if (open.has(text)) continue;
51
+ rt.attention.unshift({
52
+ id: 'att' + Date.now().toString(36) + Math.floor(Math.random() * 1e3).toString(36),
53
+ at: new Date().toISOString(), by: 'spec-source-guardian', source: 'agent', status: 'open', text,
54
+ });
55
+ added = true;
56
+ }
57
+ if (added) { try { fs.writeFileSync(rp, JSON.stringify(rt, null, 2) + '\n'); } catch {} }
58
+ }
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+ /*
3
+ * spec-drift — a small, ZERO-DEPENDENCY helper for the spec-source-guardian.
4
+ *
5
+ * It computes deterministic, ADVISORY signals about source-of-truth drift between the spec (intent)
6
+ * and the code/tests (reality). It never edits anything and never blocks — natural-language specs
7
+ * can't be auto-synced safely, so this only *flags* divergence for a human (or the guardian agent)
8
+ * to judge. That is the honest, spec-anchored stance: the spec stays the intent of record, the code
9
+ * and tests stay the enforced reality, and a guardian keeps them from silently drifting apart.
10
+ *
11
+ * Pure, unit-tested functions: classifyChange(paths), coverageSignals({specs, plans}).
12
+ * CLI: `node spec-drift.js` — inspects the git working tree + specs/plans and prints signals, and
13
+ * emits `::spectoflow attention msg=…` lines that the dashboard turns into Attention items.
14
+ */
15
+
16
+ const CODE_EXT = /\.(js|jsx|ts|tsx|py|go|rs|java|rb|php|c|cc|cpp|h|hpp|cs|kt|swift|scala|sql|sh)$/i;
17
+ const isSpec = (p) => /(^|\/)specs?\//.test(p);
18
+ const isPlan = (p) => /(^|\/)plans?\//.test(p);
19
+ const isFramework = (p) => /(^|\/)\.spectoflow\//.test(p);
20
+ const isTest = (p) => (/(^|[._-])(test|spec|e2e)s?([._-]|\/|$)/i.test(p) || /(^|\/)(tests?|__tests__)\//i.test(p)) && !isSpec(p) && !isPlan(p);
21
+ const isCode = (p) => CODE_EXT.test(p) && !isFramework(p) && !isTest(p);
22
+
23
+ // Advisory drift signals for the set of paths changed in one unit of work → [{level, msg}].
24
+ function classifyChange(paths) {
25
+ const list = (paths || []).map(String);
26
+ const has = (fn) => list.some(fn);
27
+ const codeOrTest = has((p) => (isCode(p) || isTest(p)) && !isSpec(p) && !isPlan(p) && !isFramework(p));
28
+ const specOrPlan = has((p) => isSpec(p) || isPlan(p));
29
+ const out = [];
30
+ if (codeOrTest && !specOrPlan)
31
+ out.push({ level: 'warn', msg: 'code/tests changed but no specs/ or plans/ file was updated — confirm the spec still matches, or update it.' });
32
+ if (has(isSpec) && !has(isCode) && !has(isTest))
33
+ out.push({ level: 'info', msg: 'a spec changed but no code/tests followed — make sure a plan task and tests carry the new intent.' });
34
+ return out;
35
+ }
36
+
37
+ // Coverage signals from the presence of spec/plan markdown files (not their content) → [{level, msg}].
38
+ function coverageSignals({ specs, plans } = {}) {
39
+ const s = (specs || []).length, p = (plans || []).length;
40
+ const out = [];
41
+ if (p > 0 && s === 0)
42
+ out.push({ level: 'warn', msg: 'plans/ has work but specs/ is empty — the plan has no spec to trace back to (source of truth missing).' });
43
+ if (s > 0 && p === 0)
44
+ out.push({ level: 'info', msg: 'specs/ exists but plans/ is empty — the spec is not yet broken into traceable, testable tasks.' });
45
+ return out;
46
+ }
47
+
48
+ module.exports = { classifyChange, coverageSignals, isCode, isTest, isSpec, isPlan };
49
+
50
+ // ---- CLI ------------------------------------------------------------------
51
+ if (require.main === module) {
52
+ const fs = require('fs'), path = require('path'), { execFileSync } = require('child_process');
53
+ const root = process.cwd();
54
+ let changed = [];
55
+ try {
56
+ changed = execFileSync('git', ['status', '--porcelain', '-uall'], { cwd: root, encoding: 'utf8' })
57
+ .split('\n').map((l) => l.slice(3).trim()).filter(Boolean);
58
+ } catch {}
59
+ const ls = (d) => { try { return fs.readdirSync(path.join(root, d)).filter((f) => f.endsWith('.md')); } catch { return []; } };
60
+ const signals = [...classifyChange(changed), ...coverageSignals({ specs: ls('specs'), plans: ls('plans') })];
61
+ if (!signals.length) { console.log('spec-drift: no source-of-truth signals — spec and reality look aligned.'); process.exit(0); }
62
+ console.log('spec-drift — advisory source-of-truth signals (judge, do not auto-apply):');
63
+ for (const s of signals) {
64
+ console.log(` [${s.level}] ${s.msg}`);
65
+ console.log(`::spectoflow attention msg=Source-of-truth: ${s.msg}`);
66
+ }
67
+ process.exit(0);
68
+ }
@@ -6,6 +6,10 @@ Orthogonal to mode. Even in autopilot, these require **explicit human approval**
6
6
  - **Destructive migration** (irreversible drop/alter, data deletion, purge).
7
7
  - **Security change** (auth, permissions, secrets, network exposure, session lifetime).
8
8
  - **Committing spend / external side effect** (payment, purchase, mass send).
9
+ - **Source-of-truth drift at `done` / Major** (governance) — before a Major, or before a task flips to
10
+ `done`, the `spec-source-guardian`'s drift check must be clean (or the drift explicitly accepted):
11
+ the change is reflected in the spec **and** covered by a test. This gate *acknowledges*, it does not
12
+ auto-fix — unresolved drift blocks the "done" until resolved or accepted.
9
13
 
10
14
  When a step hits a gate: stop, explain the act and its risk in one line, ask [Approve / Cancel /
11
15
  Modify], and record the decision in the runtime log. Overridable per project (add or relax gates).
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: audit-source
3
+ description: Audit spec ↔ code ↔ tests alignment; surface drift as advisory findings, never auto-fix.
4
+ capability: governance
5
+ inputs: The specs/, plans/, code and tests; optionally the recent change set (git diff).
6
+ outputs: A drift report + Attention items; a verdict (aligned / drift).
7
+ standard: spec-anchored traceability
8
+ ---
9
+ # Audit source-of-truth
10
+
11
+ Scoped audit that keeps the **spec (intent)** and the **code/tests (reality)** coherent by flagging
12
+ drift in both directions. Advisory — it reports, it never edits or auto-syncs.
13
+
14
+ ## When to use
15
+ - Before a Major or a `done` (the governance gate in `policy.md`).
16
+ - After a batch of changes, to catch drift early (a `Stop` hook can trigger it — see `hooks/`).
17
+ - On demand: "is the spec still the source of truth for what's built?"
18
+
19
+ ## Method
20
+ 1. **Build the trace.** For each spec section/decision, find the plan task(s), the code, and the
21
+ test(s) that carry it; for each plan task and code change, find the spec section it serves.
22
+ 2. **Flag both directions.**
23
+ - *Orphan work* — code or a task with no spec/decision backing → the decision is undocumented.
24
+ - *Dead spec* — a requirement with no implementation or no test → intent that will rot.
25
+ 3. **Check the enforcer.** Confirm the new/changed behaviour has **acceptance criteria** (EARS /
26
+ Given-When-Then) and that a **test actually encodes them** — tests are what bind intent to reality.
27
+ 4. **Run the deterministic helper** (optional, zero-dep): `node .spectoflow/lib/spec-drift.js` — it
28
+ inspects the git working tree + specs/plans and prints coupling/coverage signals (e.g. "code
29
+ changed but no specs/ or plans/ updated"). Treat its output as signals to judge, not verdicts.
30
+ 5. **Judge, don't auto-apply.** Where intent is ambiguous, raise a `need` rather than guess. Never
31
+ regenerate code from the spec or rewrite the spec from the code.
32
+
33
+ ## Output contract
34
+ Write findings as a report / task comment (granular, one line at a time), each carrying: the drift
35
+ direction, the spec/plan/code/test involved, and why it matters. End with a verdict: **aligned** or
36
+ **drift**. Surface to the orchestrator, group chat, and the **Attention tab** with:
37
+
38
+ ```
39
+ ::spectoflow role=governance kind=review msg=<verdict + counts>
40
+ ::spectoflow attention msg=Source-of-truth: <one drift finding, actionable>
41
+ ```
42
+
43
+ The `attention` line becomes an item in the dashboard's Attention tab (the user can edit, resolve, or
44
+ **validate → task**). Do not edit the spec or the code to reconcile drift — that is the owner's call.
45
+
46
+ ## Quality bar
47
+ - [ ] Trace built **both directions** (orphan work *and* dead spec), not just one.
48
+ - [ ] Each new/changed behaviour has testable acceptance criteria **and** a test that encodes them.
49
+ - [ ] Findings are actionable (name the spec section / task / file, and the resolution owner).
50
+ - [ ] Nothing was auto-fixed, regenerated, or silently reconciled.
51
+ - [ ] A clear verdict (aligned / drift) is stated; unresolved *warn* drift is surfaced to Attention.
52
+
53
+ ## References
54
+ - Thoughtworks — spec-driven development (code as source of truth, tests as enforcer) —
55
+ https://www.thoughtworks.com/en-us/insights/blog/agile-engineering-practices/spec-driven-development-unpacking-2025-new-engineering-practices
56
+ - Augment Code — spec-anchored vs spec-as-source, drift & traceability —
57
+ https://www.augmentcode.com/guides/spec-as-source-of-truth-rebuildable-codebase