@hegemonart/get-design-done 1.19.0 → 1.19.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +24 -0
  4. package/SKILL.md +10 -4
  5. package/agents/README.md +53 -0
  6. package/agents/a11y-mapper.md +10 -0
  7. package/agents/component-benchmark-harvester.md +11 -0
  8. package/agents/component-benchmark-synthesizer.md +11 -0
  9. package/agents/component-taxonomy-mapper.md +10 -0
  10. package/agents/design-advisor.md +10 -0
  11. package/agents/design-assumptions-analyzer.md +10 -0
  12. package/agents/design-auditor.md +10 -0
  13. package/agents/design-authority-watcher.md +10 -0
  14. package/agents/design-component-generator.md +10 -0
  15. package/agents/design-context-checker-gate.md +10 -0
  16. package/agents/design-context-checker.md +10 -0
  17. package/agents/design-discussant.md +10 -0
  18. package/agents/design-doc-writer.md +12 -0
  19. package/agents/design-executor.md +10 -0
  20. package/agents/design-figma-writer.md +10 -0
  21. package/agents/design-fixer.md +10 -0
  22. package/agents/design-integration-checker-gate.md +10 -0
  23. package/agents/design-integration-checker.md +10 -0
  24. package/agents/design-paper-writer.md +10 -0
  25. package/agents/design-pattern-mapper.md +10 -0
  26. package/agents/design-pencil-writer.md +10 -0
  27. package/agents/design-phase-researcher.md +10 -0
  28. package/agents/design-plan-checker.md +10 -0
  29. package/agents/design-planner.md +10 -0
  30. package/agents/design-reflector.md +10 -0
  31. package/agents/design-research-synthesizer.md +10 -0
  32. package/agents/design-start-writer.md +10 -0
  33. package/agents/design-update-checker.md +10 -0
  34. package/agents/design-verifier-gate.md +10 -0
  35. package/agents/design-verifier.md +11 -0
  36. package/agents/gdd-graphify-sync.md +10 -0
  37. package/agents/gdd-intel-updater.md +10 -0
  38. package/agents/gdd-learnings-extractor.md +10 -0
  39. package/agents/motion-mapper.md +10 -0
  40. package/agents/token-mapper.md +10 -0
  41. package/agents/visual-hierarchy-mapper.md +10 -0
  42. package/hooks/gdd-decision-injector.js +30 -8
  43. package/package.json +9 -2
  44. package/reference/registry.json +7 -0
  45. package/reference/schemas/insight-line.schema.json +37 -0
  46. package/scripts/lib/design-search.cjs +206 -0
  47. package/scripts/lib/probe-optional.cjs +29 -0
  48. package/scripts/lib/relevance-counter.cjs +121 -0
  49. package/skills/complete-cycle/SKILL.md +40 -2
  50. package/skills/continue/SKILL.md +23 -0
  51. package/skills/pause/SKILL.md +40 -14
  52. package/skills/recall/SKILL.md +74 -0
  53. package/skills/resume/SKILL.md +34 -16
  54. package/skills/timeline/SKILL.md +65 -0
@@ -94,4 +94,14 @@ You MAY:
94
94
 
95
95
  Per 10.1-CONTEXT decision **D-21** (Lazy Checker Spawning): "Cheap Haiku gate agents at `agents/*-gate.md` decide whether to spawn full checker. Gate agent: reads DIFF of changed files, applies heuristic (design-system paths touched? copy strings touched? token files touched?), returns `{spawn: true|false, rationale: '...'}`. If false, skip full checker, log as `lazy_skipped: true` in telemetry." This gate is the verifier-specific instance of that pattern — full `design-verifier` is an XL-size spawn and the most expensive single agent in the pipeline, so gating it behind a cheap Haiku diff-scan yields the largest single cost win in Phase 10.1.
96
96
 
97
+ ## Record
98
+
99
+ At run-end, append one JSONL line to `.design/intel/insights.jsonl`:
100
+
101
+ ```json
102
+ {"ts":"<ISO-8601>","agent":"<name>","cycle":"<cycle from STATE.md>","stage":"<stage from STATE.md>","one_line_insight":"<what was produced or learned>","artifacts_written":["<files written>"]}
103
+ ```
104
+
105
+ Schema: `reference/schemas/insight-line.schema.json`. Use an empty `artifacts_written` array for read-only agents.
106
+
97
107
  ## GATE COMPLETE
@@ -553,6 +553,17 @@ Emit a 2–4 sentence summary paragraph describing results, then:
553
553
  Emit `## GAPS FOUND` heading, then the full structured gap list (BLOCKER first, MAJOR, MINOR, COSMETIC), then on a new line:
554
554
 
555
555
  ```
556
+
557
+ ## Record
558
+
559
+ At run-end, append one JSONL line to `.design/intel/insights.jsonl`:
560
+
561
+ ```json
562
+ {"ts":"<ISO-8601>","agent":"<name>","cycle":"<cycle from STATE.md>","stage":"<stage from STATE.md>","one_line_insight":"<what was produced or learned>","artifacts_written":["<files written>"]}
563
+ ```
564
+
565
+ Schema: `reference/schemas/insight-line.schema.json`. Use an empty `artifacts_written` array for read-only agents.
566
+
556
567
  ## VERIFICATION COMPLETE
557
568
  ```
558
569
 
@@ -98,3 +98,13 @@ Graphify status: <status line>
98
98
  @.design/intel/files.json (if present)
99
99
 
100
100
  ## GRAPHIFY-SYNC COMPLETE
101
+
102
+ ## Record
103
+
104
+ At run-end, append one JSONL line to `.design/intel/insights.jsonl`:
105
+
106
+ ```json
107
+ {"ts":"<ISO-8601>","agent":"<name>","cycle":"<cycle from STATE.md>","stage":"<stage from STATE.md>","one_line_insight":"<what was produced or learned>","artifacts_written":["<files written>"]}
108
+ ```
109
+
110
+ Schema: `reference/schemas/insight-line.schema.json`. Use an empty `artifacts_written` array for read-only agents.
@@ -85,4 +85,14 @@ Generated: <timestamp>
85
85
  A slice is stale if its `generated` timestamp is older than the newest `mtime` in `files.json`.
86
86
  The updater does not need to check this manually — `build-intel.cjs` handles mtime comparison.
87
87
 
88
+ ## Record
89
+
90
+ At run-end, append one JSONL line to `.design/intel/insights.jsonl`:
91
+
92
+ ```json
93
+ {"ts":"<ISO-8601>","agent":"<name>","cycle":"<cycle from STATE.md>","stage":"<stage from STATE.md>","one_line_insight":"<what was produced or learned>","artifacts_written":["<files written>"]}
94
+ ```
95
+
96
+ Schema: `reference/schemas/insight-line.schema.json`. Use an empty `artifacts_written` array for read-only agents.
97
+
88
98
  ## INTEL UPDATE COMPLETE
@@ -82,4 +82,14 @@ For each learning with `Proposed reference update: yes`:
82
82
  @.design/intel/patterns.json (if present)
83
83
  @.design/learnings/LEARNINGS.md (if present)
84
84
 
85
+ ## Record
86
+
87
+ At run-end, append one JSONL line to `.design/intel/insights.jsonl`:
88
+
89
+ ```json
90
+ {"ts":"<ISO-8601>","agent":"<name>","cycle":"<cycle from STATE.md>","stage":"<stage from STATE.md>","one_line_insight":"<what was produced or learned>","artifacts_written":["<files written>"]}
91
+ ```
92
+
93
+ Schema: `reference/schemas/insight-line.schema.json`. Use an empty `artifacts_written` array for read-only agents.
94
+
85
95
  ## LEARNINGS EXTRACTION COMPLETE
@@ -210,4 +210,14 @@ If no violations found, emit: `## Micro-motion findings — CLEAN (0 violations)
210
210
 
211
211
  No modifications outside `.design/map/`. No git. No agent spawning.
212
212
 
213
+ ## Record
214
+
215
+ At run-end, append one JSONL line to `.design/intel/insights.jsonl`:
216
+
217
+ ```json
218
+ {"ts":"<ISO-8601>","agent":"<name>","cycle":"<cycle from STATE.md>","stage":"<stage from STATE.md>","one_line_insight":"<what was produced or learned>","artifacts_written":["<files written>"]}
219
+ ```
220
+
221
+ Schema: `reference/schemas/insight-line.schema.json`. Use an empty `artifacts_written` array for read-only agents.
222
+
213
223
  ## MOTION MAP COMPLETE
@@ -144,4 +144,14 @@ Total: N findings. (0 = clean)
144
144
 
145
145
  You MUST NOT modify anything outside `.design/map/`. Do not run git commands or spawn agents.
146
146
 
147
+ ## Record
148
+
149
+ At run-end, append one JSONL line to `.design/intel/insights.jsonl`:
150
+
151
+ ```json
152
+ {"ts":"<ISO-8601>","agent":"<name>","cycle":"<cycle from STATE.md>","stage":"<stage from STATE.md>","one_line_insight":"<what was produced or learned>","artifacts_written":["<files written>"]}
153
+ ```
154
+
155
+ Schema: `reference/schemas/insight-line.schema.json`. Use an empty `artifacts_written` array for read-only agents.
156
+
147
157
  ## TOKEN MAP COMPLETE
@@ -121,4 +121,14 @@ Total: N findings.
121
121
 
122
122
  No modifications outside `.design/map/`. No git. No agent spawning.
123
123
 
124
+ ## Record
125
+
126
+ At run-end, append one JSONL line to `.design/intel/insights.jsonl`:
127
+
128
+ ```json
129
+ {"ts":"<ISO-8601>","agent":"<name>","cycle":"<cycle from STATE.md>","stage":"<stage from STATE.md>","one_line_insight":"<what was produced or learned>","artifacts_written":["<files written>"]}
130
+ ```
131
+
132
+ Schema: `reference/schemas/insight-line.schema.json`. Use an empty `artifacts_written` array for read-only agents.
133
+
124
134
  ## VISUAL HIERARCHY MAP COMPLETE
@@ -25,6 +25,14 @@ const MIN_BYTES = 1500;
25
25
  const TOP_N = 15;
26
26
  const MATCHER_RE = /[\\/](?:\.design|reference|\.planning)[\\/][^\n]*\.md$/;
27
27
 
28
+ // Phase 19.5: try FTS5 backend first; fall back to grep silently.
29
+ let _designSearch = null;
30
+ try {
31
+ _designSearch = require(path.join(__dirname, '..', 'scripts', 'lib', 'design-search.cjs'));
32
+ } catch { /* not available in this install */ }
33
+
34
+ const BACKEND = _designSearch ? _designSearch.backendName() : null;
35
+
28
36
  function ripgrepAvailable() {
29
37
  try {
30
38
  const r = spawnSync('rg', ['--version'], { encoding: 'utf8', windowsHide: true });
@@ -103,7 +111,7 @@ function sortKeyFor(tag) {
103
111
  return 0;
104
112
  }
105
113
 
106
- function buildRecallBlock(matches, basename) {
114
+ function buildRecallBlock(matches, basename, backendLabel) {
107
115
  if (!matches.length) return null;
108
116
  const uniq = [];
109
117
  const seen = new Set();
@@ -125,10 +133,11 @@ function buildRecallBlock(matches, basename) {
125
133
  const excerpt = m.text.length > 140 ? m.text.slice(0, 137) + '…' : m.text;
126
134
  lines.push(`> - [${tag}] ${excerpt} (${path.relative(process.cwd(), m.file)}:${m.line})`);
127
135
  }
136
+ // backendLabel passed in from main()
128
137
  if (uniq.length > TOP_N) {
129
- lines.push(`> … (${uniq.length - TOP_N} more matches; use \`/gdd:recall <term>\` to expand. Grep backend; FTS5 upgrade in Phase 19.5.)`);
138
+ lines.push(`> … (${uniq.length - TOP_N} more matches; use \`/gdd:recall <term>\` to expand. Backend: ${backendLabel}.)`);
130
139
  } else {
131
- lines.push(`> (${uniq.length} match${uniq.length === 1 ? '' : 'es'} surfaced. Grep backend; FTS5 upgrade in Phase 19.5.)`);
140
+ lines.push(`> (${uniq.length} match${uniq.length === 1 ? '' : 'es'} surfaced. Backend: ${backendLabel}.)`);
132
141
  }
133
142
  lines.push('');
134
143
  return lines.join('\n');
@@ -173,13 +182,26 @@ async function main() {
173
182
  return;
174
183
  }
175
184
 
176
- const useRg = ripgrepAvailable();
177
- const hits = [];
178
- for (const src of sources) {
179
- hits.push(...(useRg ? grepLinesRg(src, terms) : grepLinesNode(src, terms)));
185
+ const useRgGlobal = ripgrepAvailable();
186
+ let hits = [];
187
+ if (BACKEND === 'fts5' && _designSearch) {
188
+ // FTS5 path: single query across all indexed docs
189
+ try {
190
+ const query = terms.join(' OR ');
191
+ hits = _designSearch.search(query, cwd, { limit: TOP_N * 3 });
192
+ } catch { hits = []; }
193
+ if (!hits.length) {
194
+ // FTS5 db may be stale — rebuild silently then retry
195
+ try { _designSearch.reindex(cwd); hits = _designSearch.search(terms.join(' OR '), cwd, { limit: TOP_N * 3 }); } catch { hits = []; }
196
+ }
197
+ } else {
198
+ for (const src of sources) {
199
+ hits.push(...(useRgGlobal ? grepLinesRg(src, terms) : grepLinesNode(src, terms)));
200
+ }
180
201
  }
181
202
 
182
- const block = buildRecallBlock(hits, basename);
203
+ const backendLabel = BACKEND || (useRgGlobal ? 'ripgrep' : 'node-grep');
204
+ const block = buildRecallBlock(hits, basename, backendLabel);
183
205
  if (!block) {
184
206
  process.stdout.write(JSON.stringify({ continue: true }));
185
207
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hegemonart/get-design-done",
3
- "version": "1.19.0",
3
+ "version": "1.19.5",
4
4
  "description": "A Claude Code plugin for systematic design improvement",
5
5
  "author": "Hegemon",
6
6
  "homepage": "https://github.com/hegemonart/get-design-done",
@@ -82,7 +82,14 @@
82
82
  "information-architecture",
83
83
  "form-patterns",
84
84
  "data-viz",
85
- "platforms"
85
+ "platforms",
86
+ "cross-cycle-memory",
87
+ "fts5",
88
+ "checkpoints",
89
+ "experience-archive",
90
+ "recall",
91
+ "relevance-counter",
92
+ "record-contract"
86
93
  ],
87
94
  "skills": [
88
95
  "SKILL.md"
@@ -380,6 +380,13 @@
380
380
  "type": "heuristic",
381
381
  "description": "Nav pattern catalog, menu-depth rules, card sort/tree test benchmarks, wayfinding, faceted nav"
382
382
  },
383
+ {
384
+ "name": "insight-line.schema",
385
+ "path": "reference/schemas/insight-line.schema.json",
386
+ "type": "schema",
387
+ "tier": "haiku",
388
+ "description": "JSONL schema for agent run-end insight lines written to .design/intel/insights.jsonl"
389
+ },
383
390
  {
384
391
  "name": "intel-schema",
385
392
  "path": "reference/intel-schema.md",
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "AgentInsightLine",
4
+ "description": "One JSONL line appended by an agent to .design/intel/insights.jsonl at run-end.",
5
+ "type": "object",
6
+ "required": ["ts", "agent", "cycle", "stage", "one_line_insight", "artifacts_written"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "ts": {
10
+ "type": "string",
11
+ "format": "date-time",
12
+ "description": "ISO 8601 timestamp of the agent run completion."
13
+ },
14
+ "agent": {
15
+ "type": "string",
16
+ "description": "Agent name matching the frontmatter 'name' field (e.g. 'design-planner')."
17
+ },
18
+ "cycle": {
19
+ "type": "string",
20
+ "description": "Active cycle ID from STATE.md (e.g. 'cycle-1'). Empty string if not in a cycle."
21
+ },
22
+ "stage": {
23
+ "type": "string",
24
+ "description": "Pipeline stage from STATE.md (e.g. 'plan', 'design', 'verify')."
25
+ },
26
+ "one_line_insight": {
27
+ "type": "string",
28
+ "maxLength": 200,
29
+ "description": "One declarative sentence: what this agent produced or learned this run."
30
+ },
31
+ "artifacts_written": {
32
+ "type": "array",
33
+ "items": { "type": "string" },
34
+ "description": "Relative paths of files written during this run. Empty array for read-only agents."
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,206 @@
1
+ 'use strict';
2
+ /**
3
+ * design-search.cjs — cross-cycle recall search backend.
4
+ *
5
+ * Priority chain:
6
+ * 1. FTS5 via better-sqlite3 (fast, ranked) — when module is available
7
+ * 2. ripgrep — when rg is on PATH
8
+ * 3. Node fs line scan — universal fallback
9
+ *
10
+ * Public API:
11
+ * search(query, projectRoot, opts?) → [{file, line, text}]
12
+ * reindex(projectRoot) → void (rebuilds FTS5 DB; no-op on grep path)
13
+ * backendName() → 'fts5' | 'ripgrep' | 'node-grep'
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { spawnSync } = require('child_process');
19
+ const { probeOptional } = require('./probe-optional.cjs');
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Backend selection (evaluated once at module load)
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const Database = probeOptional('better-sqlite3');
26
+
27
+ let _fts5Supported = false;
28
+ if (Database) {
29
+ try {
30
+ const probe = new Database(':memory:');
31
+ probe.exec('CREATE VIRTUAL TABLE _p USING fts5(t)');
32
+ probe.close();
33
+ _fts5Supported = true;
34
+ } catch { /* fts5 extension not compiled in */ }
35
+ }
36
+
37
+ function _rgAvailable() {
38
+ try {
39
+ const r = spawnSync('rg', ['--version'], { encoding: 'utf8', windowsHide: true });
40
+ return r.status === 0;
41
+ } catch { return false; }
42
+ }
43
+
44
+ const _hasRg = _rgAvailable();
45
+
46
+ function backendName() {
47
+ if (_fts5Supported) return 'fts5';
48
+ if (_hasRg) return 'ripgrep';
49
+ return 'node-grep';
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Index paths
54
+ // ---------------------------------------------------------------------------
55
+
56
+ const INDEXED_GLOBS = [
57
+ '.design/archive/**/*.md',
58
+ '.design/learnings/LEARNINGS.md',
59
+ '.design/CYCLES.md',
60
+ ];
61
+
62
+ function _dbPath(projectRoot) {
63
+ return path.join(projectRoot, '.design', 'search.db');
64
+ }
65
+
66
+ function _collectFiles(projectRoot) {
67
+ const results = [];
68
+ function walk(dir) {
69
+ let entries;
70
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
71
+ for (const e of entries) {
72
+ const full = path.join(dir, e.name);
73
+ if (e.isDirectory()) { walk(full); continue; }
74
+ if (e.name.endsWith('.md')) results.push(full);
75
+ }
76
+ }
77
+ walk(path.join(projectRoot, '.design', 'archive'));
78
+ for (const rel of [
79
+ path.join('.design', 'learnings', 'LEARNINGS.md'),
80
+ path.join('.design', 'CYCLES.md'),
81
+ ]) {
82
+ const full = path.join(projectRoot, rel);
83
+ if (fs.existsSync(full)) results.push(full);
84
+ }
85
+ // STATE.md decision blocks
86
+ const state = path.join(projectRoot, '.design', 'STATE.md');
87
+ if (fs.existsSync(state)) results.push(state);
88
+ return results;
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // FTS5 backend
93
+ // ---------------------------------------------------------------------------
94
+
95
+ function _openDb(projectRoot) {
96
+ const dbPath = _dbPath(projectRoot);
97
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
98
+ const db = new Database(dbPath);
99
+ db.exec(`
100
+ CREATE VIRTUAL TABLE IF NOT EXISTS docs USING fts5(
101
+ file UNINDEXED, line UNINDEXED, text, tokenize='trigram'
102
+ );
103
+ `);
104
+ return db;
105
+ }
106
+
107
+ function reindex(projectRoot) {
108
+ if (!_fts5Supported) return;
109
+ const db = _openDb(projectRoot);
110
+ db.exec('DELETE FROM docs');
111
+ const insert = db.prepare('INSERT INTO docs(file, line, text) VALUES (?,?,?)');
112
+ const txn = db.transaction((files) => {
113
+ for (const file of files) {
114
+ let content;
115
+ try { content = fs.readFileSync(file, 'utf8'); } catch { continue; }
116
+ const lines = content.split(/\r?\n/);
117
+ for (let i = 0; i < lines.length; i++) {
118
+ const t = lines[i].trim();
119
+ if (t) insert.run(file, i + 1, t);
120
+ }
121
+ }
122
+ });
123
+ txn(_collectFiles(projectRoot));
124
+ db.close();
125
+ }
126
+
127
+ function _searchFts5(query, projectRoot, limit) {
128
+ const dbPath = _dbPath(projectRoot);
129
+ if (!fs.existsSync(dbPath)) reindex(projectRoot);
130
+ const db = new Database(dbPath, { readonly: true });
131
+ try {
132
+ const rows = db.prepare(
133
+ `SELECT file, line, text FROM docs WHERE docs MATCH ? ORDER BY rank LIMIT ?`
134
+ ).all(query, limit);
135
+ return rows.map(r => ({ file: r.file, line: r.line, text: r.text }));
136
+ } finally {
137
+ db.close();
138
+ }
139
+ }
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // Ripgrep backend
143
+ // ---------------------------------------------------------------------------
144
+
145
+ function _escapeRe(s) {
146
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
147
+ }
148
+
149
+ function _searchRg(query, projectRoot, limit) {
150
+ const terms = query.split(/\s+/).filter(Boolean);
151
+ const pattern = terms.map(_escapeRe).join('|');
152
+ const targets = _collectFiles(projectRoot);
153
+ if (!targets.length || !pattern) return [];
154
+ const r = spawnSync('rg', ['-n', '--no-heading', '-i', '-S', pattern, ...targets], {
155
+ encoding: 'utf8', windowsHide: true,
156
+ });
157
+ const results = [];
158
+ for (const line of (r.stdout || '').split(/\r?\n/)) {
159
+ const m = line.match(/^(.+?):(\d+):(.*)$/);
160
+ if (m) results.push({ file: m[1], line: Number(m[2]), text: m[3].trim() });
161
+ if (results.length >= limit) break;
162
+ }
163
+ return results;
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Node fs fallback backend
168
+ // ---------------------------------------------------------------------------
169
+
170
+ function _searchNode(query, projectRoot, limit) {
171
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
172
+ const files = _collectFiles(projectRoot);
173
+ const results = [];
174
+ for (const file of files) {
175
+ let content;
176
+ try { content = fs.readFileSync(file, 'utf8'); } catch { continue; }
177
+ const lines = content.split(/\r?\n/);
178
+ for (let i = 0; i < lines.length; i++) {
179
+ const lower = lines[i].toLowerCase();
180
+ if (terms.every(t => lower.includes(t))) {
181
+ results.push({ file, line: i + 1, text: lines[i].trim() });
182
+ if (results.length >= limit) return results;
183
+ }
184
+ }
185
+ }
186
+ return results;
187
+ }
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Public search
191
+ // ---------------------------------------------------------------------------
192
+
193
+ /**
194
+ * @param {string} query
195
+ * @param {string} projectRoot absolute path to the project (contains .design/)
196
+ * @param {{ limit?: number }} [opts]
197
+ * @returns {{ file: string, line: number, text: string }[]}
198
+ */
199
+ function search(query, projectRoot, opts = {}) {
200
+ const limit = opts.limit ?? 20;
201
+ if (_fts5Supported) return _searchFts5(query, projectRoot, limit);
202
+ if (_hasRg) return _searchRg(query, projectRoot, limit);
203
+ return _searchNode(query, projectRoot, limit);
204
+ }
205
+
206
+ module.exports = { search, reindex, backendName };
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+ /**
3
+ * probe-optional.cjs — safely require optional native dependencies.
4
+ *
5
+ * Usage:
6
+ * const { probeOptional } = require('./probe-optional.cjs');
7
+ * const Database = probeOptional('better-sqlite3');
8
+ * if (Database) { ... } else { // fallback }
9
+ *
10
+ * Returns the module if available and natively compatible, null otherwise.
11
+ * Swallows MODULE_NOT_FOUND and native binding errors silently — callers
12
+ * must implement their own fallback path.
13
+ */
14
+ function probeOptional(name) {
15
+ try {
16
+ return require(name);
17
+ } catch (e) {
18
+ if (
19
+ e.code === 'MODULE_NOT_FOUND' ||
20
+ e.message?.includes('was compiled against a different Node.js version') ||
21
+ e.message?.includes('NODE_MODULE_VERSION')
22
+ ) {
23
+ return null;
24
+ }
25
+ throw e;
26
+ }
27
+ }
28
+
29
+ module.exports = { probeOptional };
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+ /**
3
+ * relevance-counter.cjs — tracks per-learning signal counts.
4
+ *
5
+ * Signals: 'cited' | 'surfaced' | 'dismissed'
6
+ *
7
+ * Writes to `.design/learnings/.relevance.json` under an atomic-write
8
+ * pattern (write tmp → rename) guarded by a `.relevance.lock` file so
9
+ * concurrent agents don't clobber each other.
10
+ *
11
+ * Public API:
12
+ * record(id, signal, designDir) → void
13
+ * load(designDir) → { [id]: { cited, surfaced, dismissed, last_used } }
14
+ * shouldPromote(id, designDir) → boolean (cited >= 8)
15
+ * shouldPrune(id, age, designDir) → boolean (cited === 0 and age >= 4 cycles)
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const os = require('os');
21
+
22
+ const SIGNALS = new Set(['cited', 'surfaced', 'dismissed']);
23
+ const PROMOTE_THRESHOLD = 8;
24
+
25
+ function _counterPath(designDir) {
26
+ return path.join(designDir, 'learnings', '.relevance.json');
27
+ }
28
+
29
+ function _lockPath(designDir) {
30
+ return path.join(designDir, 'learnings', '.relevance.lock');
31
+ }
32
+
33
+ function _acquireLock(lockPath, timeout = 5000) {
34
+ const deadline = Date.now() + timeout;
35
+ while (Date.now() < deadline) {
36
+ try {
37
+ fs.writeFileSync(lockPath, String(process.pid), { flag: 'wx' });
38
+ return true;
39
+ } catch (e) {
40
+ if (e.code !== 'EEXIST') throw e;
41
+ // Check for stale lock (> 30s old)
42
+ try {
43
+ const stat = fs.statSync(lockPath);
44
+ if (Date.now() - stat.mtimeMs > 30000) {
45
+ fs.unlinkSync(lockPath);
46
+ continue;
47
+ }
48
+ } catch { /* lock was removed between our check and unlink */ }
49
+ // Brief spin
50
+ const end = Date.now() + 50;
51
+ while (Date.now() < end) { /* spin */ }
52
+ }
53
+ }
54
+ return false;
55
+ }
56
+
57
+ function _releaseLock(lockPath) {
58
+ try { fs.unlinkSync(lockPath); } catch { /* already gone */ }
59
+ }
60
+
61
+ function load(designDir) {
62
+ const p = _counterPath(designDir);
63
+ try {
64
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
65
+ } catch {
66
+ return {};
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Record a signal for a learning entry id.
72
+ * @param {string} id Learning ID (e.g. "L-01")
73
+ * @param {'cited'|'surfaced'|'dismissed'} signal
74
+ * @param {string} designDir Path to the .design/ directory
75
+ */
76
+ function record(id, signal, designDir) {
77
+ if (!SIGNALS.has(signal)) throw new Error(`Unknown signal: ${signal}`);
78
+ fs.mkdirSync(path.join(designDir, 'learnings'), { recursive: true });
79
+
80
+ const lockPath = _lockPath(designDir);
81
+ if (!_acquireLock(lockPath)) {
82
+ // Non-fatal — skip the update rather than corrupting the store
83
+ return;
84
+ }
85
+
86
+ try {
87
+ const data = load(designDir);
88
+ if (!data[id]) {
89
+ data[id] = { cited: 0, surfaced: 0, dismissed: 0, last_used: null };
90
+ }
91
+ data[id][signal]++;
92
+ data[id].last_used = new Date().toISOString();
93
+
94
+ // Atomic write: tmp → rename
95
+ const counterPath = _counterPath(designDir);
96
+ const tmp = counterPath + '.tmp.' + process.pid;
97
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', 'utf8');
98
+ fs.renameSync(tmp, counterPath);
99
+ } finally {
100
+ _releaseLock(lockPath);
101
+ }
102
+ }
103
+
104
+ function shouldPromote(id, designDir) {
105
+ const data = load(designDir);
106
+ return (data[id]?.cited ?? 0) >= PROMOTE_THRESHOLD;
107
+ }
108
+
109
+ /**
110
+ * @param {string} id
111
+ * @param {number} ageCycles number of cycles since last_used (or since created)
112
+ * @param {string} designDir
113
+ */
114
+ function shouldPrune(id, ageCycles, designDir) {
115
+ const data = load(designDir);
116
+ const entry = data[id];
117
+ if (!entry) return false;
118
+ return entry.cited === 0 && ageCycles >= 4;
119
+ }
120
+
121
+ module.exports = { record, load, shouldPromote, shouldPrune };