@ijfw/memory-server 1.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.
Files changed (106) hide show
  1. package/bin/ijfw +27 -0
  2. package/bin/ijfw-dashboard +180 -0
  3. package/bin/ijfw-dispatch-plan +41 -0
  4. package/bin/ijfw-memorize +273 -0
  5. package/bin/ijfw-memory +51 -0
  6. package/fixtures/demo-target.js +28 -0
  7. package/package.json +53 -0
  8. package/src/api-client.js +190 -0
  9. package/src/audit-roster.js +315 -0
  10. package/src/caps.js +37 -0
  11. package/src/cold-scan-runner.mjs +37 -0
  12. package/src/compute/edges.js +155 -0
  13. package/src/compute/extract.js +560 -0
  14. package/src/compute/fts5.js +420 -0
  15. package/src/compute/graph-auto-index.js +191 -0
  16. package/src/compute/graph-lock.js +114 -0
  17. package/src/compute/index.js +18 -0
  18. package/src/compute/migration-runner.js +116 -0
  19. package/src/compute/migrations/001-initial.js +23 -0
  20. package/src/compute/migrations/002-porter-stemming-source.js +139 -0
  21. package/src/compute/migrations/003-tier-semantic.js +69 -0
  22. package/src/compute/migrations/004-kg-tables.js +83 -0
  23. package/src/compute/migrations/005-stale-candidate.js +72 -0
  24. package/src/compute/python-resolver.js +106 -0
  25. package/src/compute/runner-vm.js +185 -0
  26. package/src/compute/runner.js +416 -0
  27. package/src/compute/sandbox-detect.js +122 -0
  28. package/src/compute/sandbox-linux.js +164 -0
  29. package/src/compute/sandbox-macos.js +167 -0
  30. package/src/compute/sandbox-windows.js +63 -0
  31. package/src/compute/schema.sql +118 -0
  32. package/src/compute/staleness.js +239 -0
  33. package/src/compute/synonyms.js +367 -0
  34. package/src/compute/traverse.js +180 -0
  35. package/src/cost/aggregator.js +229 -0
  36. package/src/cost/pricing.js +134 -0
  37. package/src/cost/readers/claude.js +179 -0
  38. package/src/cost/readers/codex.js +131 -0
  39. package/src/cost/readers/gemini.js +111 -0
  40. package/src/cost/savings.js +243 -0
  41. package/src/cross-dispatcher.js +437 -0
  42. package/src/cross-orchestrator-cli.js +1885 -0
  43. package/src/cross-orchestrator.js +598 -0
  44. package/src/cross-project-search.js +114 -0
  45. package/src/dashboard-client.html +1180 -0
  46. package/src/dashboard-server.js +895 -0
  47. package/src/design-companion.js +81 -0
  48. package/src/dispatch/colon-syntax.js +732 -0
  49. package/src/dispatch-planner.js +235 -0
  50. package/src/dream/cooldown.js +105 -0
  51. package/src/dream/runner.mjs +373 -0
  52. package/src/dream/staleness-wiring.js +195 -0
  53. package/src/feedback-detector.js +57 -0
  54. package/src/hero-line.js +115 -0
  55. package/src/importers/claude-mem.js +152 -0
  56. package/src/importers/cli.js +311 -0
  57. package/src/importers/common.js +84 -0
  58. package/src/importers/discover.js +235 -0
  59. package/src/importers/rtk.js +107 -0
  60. package/src/intent-router.js +221 -0
  61. package/src/lib/atomic-io.js +201 -0
  62. package/src/lib/cache.js +33 -0
  63. package/src/lib/npm-view.js +104 -0
  64. package/src/lib/status-card.js +95 -0
  65. package/src/lib/token.js +85 -0
  66. package/src/memory/fts5.js +349 -0
  67. package/src/memory/migration-runner.js +116 -0
  68. package/src/memory/migrations/001-fts5-init.js +26 -0
  69. package/src/memory/migrations/002-tier-semantic.js +60 -0
  70. package/src/memory/migrations/003-stale-candidate.js +60 -0
  71. package/src/memory/reader.js +300 -0
  72. package/src/memory/recall-counter.js +76 -0
  73. package/src/memory/schema.sql +79 -0
  74. package/src/memory/search.js +431 -0
  75. package/src/memory/staleness.js +237 -0
  76. package/src/memory/tier-promotion.js +377 -0
  77. package/src/memory/tokenize.js +63 -0
  78. package/src/project-type-detector.js +866 -0
  79. package/src/prompt-check.js +171 -0
  80. package/src/ralph-allowlist.js +88 -0
  81. package/src/receipts.js +129 -0
  82. package/src/redactor.js +107 -0
  83. package/src/sandbox.js +275 -0
  84. package/src/sanitizer.js +69 -0
  85. package/src/scan-resume.js +167 -0
  86. package/src/schema.js +82 -0
  87. package/src/search-bm25.js +108 -0
  88. package/src/server.js +1414 -0
  89. package/src/swarm-config.js +80 -0
  90. package/src/trident/dispatch.js +211 -0
  91. package/src/trident/lens-health.js +253 -0
  92. package/src/update-apply.js +79 -0
  93. package/src/update-check.js +136 -0
  94. package/src/vectors.js +178 -0
  95. package/templates/design/bento-grid.md +84 -0
  96. package/templates/design/brutalist-luxe.md +82 -0
  97. package/templates/design/cinematic-dark.md +82 -0
  98. package/templates/design/data-dense-dashboard.md +88 -0
  99. package/templates/design/editorial-warm.md +81 -0
  100. package/templates/design/glassmorphic.md +84 -0
  101. package/templates/design/magazine-editorial.md +84 -0
  102. package/templates/design/maximalist-vibrant.md +85 -0
  103. package/templates/design/neo-swiss-tech.md +85 -0
  104. package/templates/design/swiss-minimal.md +80 -0
  105. package/templates/design/terminal-native.md +83 -0
  106. package/templates/design/warm-organic.md +84 -0
@@ -0,0 +1,235 @@
1
+ // --- dispatch-planner: decides shared-branch vs worktree-isolated parallelism ---
2
+ //
3
+ // Parses a plan markdown document, finds sub-wave `Files:` declarations, and
4
+ // computes a dispatch manifest: each sub-wave is either SHARED (no file overlap
5
+ // with peers in the same wave) or WORKTREE (overlaps -> needs isolation).
6
+ //
7
+ // Pure + synchronous. ESM. Zero deps. Filesystem only touched by caller.
8
+
9
+ const WAVE_HEADER = /^###\s+Wave\s+([0-9]+[A-Z])(?:-([A-Za-z0-9_+]+))?\b/;
10
+ // Bullet sub-wave form: `- **11A-mcp**: description`. Parsed as a child of
11
+ // the most recently seen Wave header.
12
+ const BULLET_SUB = /^\s*[-*]\s+\*\*\s*([0-9]+[A-Z])-([A-Za-z0-9_+]+)\s*\*\*\s*:/;
13
+ const FILES_LINE = /^\s*[*-]?\s*\*{0,2}Files:\*{0,2}\s*(.+?)\s*$/i;
14
+
15
+ // Parse a plan markdown string into an array of sub-waves.
16
+ // Shape: [{ wave, sub, files: string[] }] -- sub is optional.
17
+ // Accumulates Files: declarations (later lines append, not overwrite).
18
+ export function parsePlan(markdown) {
19
+ const lines = markdown.split(/\r?\n/);
20
+ const subwaves = [];
21
+ let currentWave = null;
22
+ let currentSub = null;
23
+
24
+ const push = (entry) => { if (entry) subwaves.push(entry); };
25
+
26
+ for (const line of lines) {
27
+ const h = line.match(WAVE_HEADER);
28
+ if (h) {
29
+ push(currentSub);
30
+ push(currentWave && !currentSub ? currentWave : null);
31
+ const wave = h[1];
32
+ const sub = h[2] ? `${wave}-${h[2]}` : null;
33
+ currentWave = { wave, sub, files: [] };
34
+ currentSub = sub ? currentWave : null; // header WITH sub acts as its own sub-wave
35
+ if (sub) { currentSub = currentWave; }
36
+ else { currentSub = null; }
37
+ continue;
38
+ }
39
+
40
+ const b = line.match(BULLET_SUB);
41
+ if (b) {
42
+ push(currentSub);
43
+ const wave = b[1];
44
+ currentSub = { wave, sub: `${wave}-${b[2]}`, files: [] };
45
+ continue;
46
+ }
47
+
48
+ const target = currentSub || currentWave;
49
+ if (!target) continue;
50
+ const f = line.match(FILES_LINE);
51
+ if (f) {
52
+ const add = f[1]
53
+ .split(/[,\s]+/)
54
+ .map((s) => s.replace(/^`|`$/g, '').trim())
55
+ .filter(Boolean);
56
+ for (const a of add) if (!target.files.includes(a)) target.files.push(a);
57
+ }
58
+ }
59
+ push(currentSub);
60
+ push(currentWave && (!currentSub || currentSub !== currentWave) ? currentWave : null);
61
+
62
+ // Deduplicate: if a wave was captured as both parent and sub, prefer the sub entry.
63
+ const seen = new Set();
64
+ const out = [];
65
+ for (const sw of subwaves) {
66
+ const key = `${sw.wave}::${sw.sub || ''}`;
67
+ if (seen.has(key)) continue;
68
+ seen.add(key);
69
+ out.push(sw);
70
+ }
71
+ return out;
72
+ }
73
+
74
+ // Compute pairwise file-set overlap within the same wave.
75
+ // Returns a map: subId -> string[] of peer subIds it conflicts with.
76
+ export function computeOverlaps(subwaves) {
77
+ const byWave = new Map();
78
+ for (const sw of subwaves) {
79
+ if (!byWave.has(sw.wave)) byWave.set(sw.wave, []);
80
+ byWave.get(sw.wave).push(sw);
81
+ }
82
+
83
+ const overlaps = new Map();
84
+ for (const group of byWave.values()) {
85
+ for (const sw of group) overlaps.set(idOf(sw), []);
86
+ for (let i = 0; i < group.length; i++) {
87
+ for (let j = i + 1; j < group.length; j++) {
88
+ const a = group[i], b = group[j];
89
+ if (intersects(a.files, b.files)) {
90
+ overlaps.get(idOf(a)).push(idOf(b));
91
+ overlaps.get(idOf(b)).push(idOf(a));
92
+ }
93
+ }
94
+ }
95
+ }
96
+ return overlaps;
97
+ }
98
+
99
+ // Build the dispatch manifest.
100
+ // options: { override: 'all-worktree' | 'all-shared' | null }
101
+ // Rules:
102
+ // 1. override 'all-worktree' -> every sub-wave WORKTREE.
103
+ // 2. override 'all-shared' -> every sub-wave SHARED (caller took the risk).
104
+ // 3. sub-wave missing Files: declaration -> WORKTREE (safe default).
105
+ // 4. sub-wave with overlap against peer -> WORKTREE.
106
+ // 5. otherwise SHARED.
107
+ export function buildManifest(subwaves, options = {}) {
108
+ const override = options.override || null;
109
+ const overlaps = computeOverlaps(subwaves);
110
+
111
+ return subwaves.map((sw) => {
112
+ const id = idOf(sw);
113
+ const peers = overlaps.get(id) || [];
114
+ let mode;
115
+ let reason;
116
+
117
+ if (override === 'all-worktree') {
118
+ mode = 'worktree';
119
+ reason = 'override:all-worktree';
120
+ } else if (override === 'all-shared') {
121
+ mode = 'shared';
122
+ reason = 'override:all-shared';
123
+ } else if (sw.files.length === 0) {
124
+ mode = 'worktree';
125
+ reason = 'no-files-declared';
126
+ } else if (peers.length > 0) {
127
+ mode = 'worktree';
128
+ reason = `overlap:${peers.join(',')}`;
129
+ } else {
130
+ mode = 'shared';
131
+ reason = 'disjoint';
132
+ }
133
+
134
+ return {
135
+ id,
136
+ wave: sw.wave,
137
+ sub: sw.sub,
138
+ files: sw.files.slice(),
139
+ mode,
140
+ reason,
141
+ overlaps_with: peers.slice(),
142
+ };
143
+ });
144
+ }
145
+
146
+ // One-line human summary for the workflow skill to echo before dispatch.
147
+ // Example: "Wave 12A: 3 shared + 2 worktree (overlap: 12A-mcp <-> 12A-cmd)."
148
+ export function manifestSummary(manifest) {
149
+ if (manifest.length === 0) return 'Wave: no sub-waves found.';
150
+ const byWave = new Map();
151
+ for (const m of manifest) {
152
+ if (!byWave.has(m.wave)) byWave.set(m.wave, []);
153
+ byWave.get(m.wave).push(m);
154
+ }
155
+ const parts = [];
156
+ for (const [wave, entries] of byWave) {
157
+ const shared = entries.filter((e) => e.mode === 'shared').length;
158
+ const worktree = entries.filter((e) => e.mode === 'worktree').length;
159
+ const pairs = new Set();
160
+ for (const e of entries) {
161
+ for (const peer of e.overlaps_with) {
162
+ const key = [e.id, peer].sort().join(' <-> ');
163
+ pairs.add(key);
164
+ }
165
+ }
166
+ const tail = pairs.size > 0 ? ` (overlap: ${[...pairs].join('; ')})` : '';
167
+ parts.push(`Wave ${wave}: ${shared} shared + ${worktree} worktree${tail}.`);
168
+ }
169
+ return parts.join(' ');
170
+ }
171
+
172
+ // Topologically ordered merge plan for worktree sub-waves.
173
+ // Current convention: merge in the order sub-waves were declared in the plan
174
+ // (no explicit `Depends:` declaration yet). Shared sub-waves are skipped since
175
+ // they already committed to the parent branch.
176
+ export function mergeOrder(manifest) {
177
+ return manifest
178
+ .filter((m) => m.mode === 'worktree')
179
+ .map((m) => m.id);
180
+ }
181
+
182
+ function idOf(sw) { return sw.sub || sw.wave; }
183
+
184
+ // Glob-aware intersection. Treats `*`/`**` as wildcards so a declaration
185
+ // like `claude/commands/*.md` conflicts with `claude/commands/status.md`.
186
+ // Returns true on any exact match OR glob-vs-literal match.
187
+ function intersects(a, b) {
188
+ if (a.length === 0 || b.length === 0) return false;
189
+ for (const x of a) {
190
+ for (const y of b) {
191
+ if (x === y) return true;
192
+ if (globsOverlap(x, y)) return true;
193
+ }
194
+ }
195
+ return false;
196
+ }
197
+
198
+ function globsOverlap(x, y) {
199
+ const xIsGlob = hasGlob(x);
200
+ const yIsGlob = hasGlob(y);
201
+ if (!xIsGlob && !yIsGlob) return false;
202
+ if (xIsGlob && matchesGlob(y, x)) return true;
203
+ if (yIsGlob && matchesGlob(x, y)) return true;
204
+ if (xIsGlob && yIsGlob) return globsCouldOverlap(x, y);
205
+ return false;
206
+ }
207
+
208
+ function hasGlob(s) { return /[*?]/.test(s); }
209
+
210
+ function matchesGlob(literal, glob) {
211
+ const re = new RegExp('^' + globToRegex(glob) + '$');
212
+ return re.test(literal);
213
+ }
214
+
215
+ // Approximate: two globs overlap if one's non-wildcard prefix is a prefix
216
+ // of the other's. Accurate enough for file-cluster declarations.
217
+ function globsCouldOverlap(a, b) {
218
+ const ap = a.split(/[*?]/)[0];
219
+ const bp = b.split(/[*?]/)[0];
220
+ return ap.startsWith(bp) || bp.startsWith(ap);
221
+ }
222
+
223
+ function globToRegex(glob) {
224
+ let out = '';
225
+ for (let i = 0; i < glob.length; i++) {
226
+ const c = glob[i];
227
+ if (c === '*') {
228
+ if (glob[i + 1] === '*') { out += '.*'; i++; }
229
+ else { out += '[^/]*'; }
230
+ } else if (c === '?') out += '[^/]';
231
+ else if ('.+^$|()[]{}\\'.includes(c)) out += '\\' + c;
232
+ else out += c;
233
+ }
234
+ return out;
235
+ }
@@ -0,0 +1,105 @@
1
+ // IJFW v1.3.0 Alpha -- D3 dream-cycle cooldown helper.
2
+ //
3
+ // Reads / writes `.ijfw/.dream-state.json` to enforce a 4-hour cooldown
4
+ // between consolidation runs. Replaces the legacy `SESSION_NUM % 5 == 0`
5
+ // startup-flag mechanism: dream cycles now fire INLINE at SessionEnd via
6
+ // detached spawn (mirrors the Phase 3 cold-scan pattern).
7
+ //
8
+ // Atomic write contract: write to `<file>.tmp.<pid>` then rename to the
9
+ // canonical name. Crash-safe: a half-written tmp never replaces a good
10
+ // state file. Reader tolerates absent / malformed state by returning
11
+ // `{ onCooldown: false }` so a corrupted state file silently re-enables
12
+ // a run rather than locking dreams out forever.
13
+ //
14
+ // Discipline:
15
+ // - ESM, zero deps.
16
+ // - Hooks must never crash; every fs op caught and treated as "no
17
+ // cooldown info available" -> safe to fire.
18
+ // - 4-hour window is hardcoded; override via IJFW_DREAM_COOLDOWN_MS for
19
+ // test fixtures (test-d3-dream-trigger.js relies on this).
20
+
21
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+
24
+ // 4 hours in ms. Override via env for test fixtures only -- the spec
25
+ // locks 4h as the production value.
26
+ export const COOLDOWN_MS_DEFAULT = 4 * 60 * 60 * 1000;
27
+
28
+ function cooldownMs() {
29
+ const override = Number(process.env.IJFW_DREAM_COOLDOWN_MS);
30
+ return Number.isFinite(override) && override >= 0 ? override : COOLDOWN_MS_DEFAULT;
31
+ }
32
+
33
+ function statePath(stateDir) {
34
+ return join(stateDir, '.dream-state.json');
35
+ }
36
+
37
+ /**
38
+ * Returns `true` when the last completed dream cycle finished within the
39
+ * cooldown window. Returns `false` on any read failure (corrupt JSON,
40
+ * absent file, missing dir) so dreams are never silently suppressed by
41
+ * a partial / damaged state file.
42
+ *
43
+ * @param {string} stateDir -- typically `<projectRoot>/.ijfw`
44
+ */
45
+ export function isOnCooldown(stateDir) {
46
+ if (!stateDir) return false;
47
+ try {
48
+ const raw = readFileSync(statePath(stateDir), 'utf8');
49
+ const obj = JSON.parse(raw);
50
+ const lastRunAt = obj && obj.last_run_at;
51
+ if (typeof lastRunAt !== 'string') return false;
52
+ const t = Date.parse(lastRunAt);
53
+ if (!Number.isFinite(t)) return false;
54
+ const age = Date.now() - t;
55
+ if (age < 0) return false; // clock skew -- treat as not-on-cooldown
56
+ return age < cooldownMs();
57
+ } catch {
58
+ // Absent or corrupt -- treat as "no recent run", let dream fire.
59
+ return false;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Atomically mark a dream cycle as completed. Writes
65
+ * `{ last_run_at, version }` to a tmp file then renames into place so a
66
+ * crash mid-write never leaves a half-formed state file.
67
+ *
68
+ * Returns `true` on success, `false` on any IO failure -- callers may
69
+ * surface the failure in the dream log, but should NOT abort the run on
70
+ * a bad mark; the runner finished successfully even if we couldn't
71
+ * persist the timestamp.
72
+ *
73
+ * @param {string} stateDir -- typically `<projectRoot>/.ijfw`
74
+ */
75
+ export function markCompleted(stateDir) {
76
+ if (!stateDir) return false;
77
+ try {
78
+ mkdirSync(stateDir, { recursive: true });
79
+ const target = statePath(stateDir);
80
+ const tmp = `${target}.tmp.${process.pid}`;
81
+ const payload = {
82
+ version: 1,
83
+ last_run_at: new Date().toISOString(),
84
+ };
85
+ writeFileSync(tmp, JSON.stringify(payload) + '\n', 'utf8');
86
+ renameSync(tmp, target);
87
+ return true;
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Read raw state for diagnostic surfaces (`/ijfw doctor`, dream log).
95
+ * Returns `null` on any failure.
96
+ */
97
+ export function readState(stateDir) {
98
+ if (!stateDir) return null;
99
+ try {
100
+ const raw = readFileSync(statePath(stateDir), 'utf8');
101
+ return JSON.parse(raw);
102
+ } catch {
103
+ return null;
104
+ }
105
+ }