@ijfw/memory-server 1.3.0 → 1.4.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 (64) hide show
  1. package/fixtures/team/book.json +47 -0
  2. package/fixtures/team/business.json +47 -0
  3. package/fixtures/team/content.json +47 -0
  4. package/fixtures/team/design.json +47 -0
  5. package/fixtures/team/mixed.json +59 -0
  6. package/fixtures/team/research.json +47 -0
  7. package/fixtures/team/software.json +47 -0
  8. package/package.json +1 -9
  9. package/src/active-extension-writer.js +116 -0
  10. package/src/blackboard.js +360 -0
  11. package/src/cli-run.js +91 -0
  12. package/src/codex-agents.js +177 -0
  13. package/src/compute/extract.js +3 -0
  14. package/src/compute/fts5.js +4 -4
  15. package/src/compute/graph-lock.js +0 -2
  16. package/src/compute/migrations/003-tier-semantic.js +3 -3
  17. package/src/compute/runner.js +44 -15
  18. package/src/compute/schema.sql +1 -1
  19. package/src/cross-orchestrator-cli.js +974 -13
  20. package/src/cross-orchestrator.js +9 -1
  21. package/src/dashboard-client.html +144 -1
  22. package/src/dashboard-server.js +75 -2
  23. package/src/design-intelligence.js +721 -0
  24. package/src/dispatch/colon-syntax.js +31 -3
  25. package/src/dispatch/domain-manifest.js +251 -0
  26. package/src/dispatch/extension.js +404 -0
  27. package/src/dispatch/override.js +221 -0
  28. package/src/dispatch-planner.js +1 -0
  29. package/src/dream/runner.mjs +3 -3
  30. package/src/extension-installer.js +1230 -0
  31. package/src/extension-manifest-schema.js +301 -0
  32. package/src/extension-signer.js +740 -0
  33. package/src/gate-result-formatter.js +95 -0
  34. package/src/gate-result-schema.js +274 -0
  35. package/src/gate-result.js +195 -0
  36. package/src/intent-router.js +2 -0
  37. package/src/lib/npm-view.js +1 -0
  38. package/src/memory/fts5.js +3 -3
  39. package/src/memory/migrations/002-tier-semantic.js +2 -2
  40. package/src/memory/staleness.js +1 -1
  41. package/src/memory/tier-promotion.js +6 -6
  42. package/src/memory/tokenize.js +1 -1
  43. package/src/memory-feedback.js +188 -0
  44. package/src/override-manifest-schema.js +146 -0
  45. package/src/override-resolver.js +699 -0
  46. package/src/override-use-registry.js +307 -0
  47. package/src/overrides/presets/academic.md +101 -0
  48. package/src/overrides/presets/book.md +87 -0
  49. package/src/overrides/presets/campaign.md +95 -0
  50. package/src/overrides/presets/screenplay.md +99 -0
  51. package/src/recovery/checkpoint.js +191 -0
  52. package/src/redactor.js +2 -0
  53. package/src/runtime-mediator.js +178 -0
  54. package/src/sandbox.js +17 -3
  55. package/src/server.js +94 -2
  56. package/src/swarm/dispatch-prompt.js +154 -0
  57. package/src/swarm/planner.js +399 -0
  58. package/src/swarm/review.js +136 -0
  59. package/src/swarm/worktree.js +239 -0
  60. package/src/team/generator.js +119 -0
  61. package/src/team/schemas.js +341 -0
  62. package/src/trident/dispatch.js +47 -0
  63. package/src/update-check.js +1 -1
  64. package/src/vectors.js +7 -8
@@ -1,6 +1,6 @@
1
1
  // IJFW v1.3.0 -- shared tokenization + Jaccard similarity for tier-promotion.
2
2
  //
3
- // Source authority: .planning/1.3.0/D-PILLAR-SPEC.md §1 (Episodic ->
3
+ // Source authority: .planning/1.3.0/D-PILLAR-SPEC.md section 1 (Episodic ->
4
4
  // Semantic supersession trigger B uses token-set Jaccard > 0.7).
5
5
  //
6
6
  // Zero-deps, deterministic. Lowercases, strips non-word chars, drops
@@ -0,0 +1,188 @@
1
+ /**
2
+ * memory-feedback.js
3
+ *
4
+ * IJFW v1.4.0 W7/B3 -- Memory Feedback Auto-Routing
5
+ *
6
+ * Reads .ijfw/memory/gate-receipts/ under a project root, detects repeated
7
+ * FAIL/FLAG patterns on the same affected_artifacts[].type, and returns
8
+ * one-liner markdown suggestion strings for surface in ijfw_memory_prelude.
9
+ *
10
+ * All entry points are best-effort: any error returns empty output without
11
+ * throwing. No PII leakage: suggestion text contains only artifact TYPE and
12
+ * counts, never IDs or full receipt content.
13
+ */
14
+
15
+ import { readdir, readFile, lstat } from 'node:fs/promises';
16
+ import { join } from 'node:path';
17
+
18
+ const RECEIPTS_SUBPATH = join('.ijfw', 'memory', 'gate-receipts');
19
+ const MAX_FILE_BYTES = 64 * 1024;
20
+ const FAIL_VERDICTS = new Set(['FAIL', 'FLAG']);
21
+
22
+ /**
23
+ * readRecentReceipts(projectRoot, limit)
24
+ *
25
+ * Reads .ijfw/memory/gate-receipts/*.json under projectRoot.
26
+ * Sorts by mtime descending, takes the first `limit` entries.
27
+ * Parses each JSON safely; skips malformed or structurally invalid files.
28
+ * Returns an array of parsed gate-result objects.
29
+ *
30
+ * @param {string} projectRoot
31
+ * @param {number} [limit=50]
32
+ * @returns {Promise<object[]>}
33
+ */
34
+ export async function readRecentReceipts(projectRoot, limit = 50) {
35
+ const receiptsDir = join(projectRoot, RECEIPTS_SUBPATH);
36
+
37
+ let entries;
38
+ try {
39
+ entries = await readdir(receiptsDir);
40
+ } catch {
41
+ return [];
42
+ }
43
+
44
+ const jsonFiles = entries.filter((e) => e.endsWith('.json'));
45
+ if (jsonFiles.length === 0) return [];
46
+
47
+ const withMtime = [];
48
+ for (const name of jsonFiles) {
49
+ const filePath = join(receiptsDir, name);
50
+ try {
51
+ // W7.1/B3-H-01 + B3-M-01: lstat (not stat) so symlinks are detected,
52
+ // pre-check size BEFORE readFile so a multi-GB attacker file cannot
53
+ // OOM the prelude on read.
54
+ const info = await lstat(filePath);
55
+ if (info.isSymbolicLink()) continue; // reject symlinks
56
+ if (!info.isFile()) continue; // only regular files
57
+ if (info.size > MAX_FILE_BYTES) continue; // size cap pre-read
58
+ withMtime.push({ filePath, mtime: info.mtimeMs });
59
+ } catch {
60
+ // unreadable entry -- skip
61
+ }
62
+ }
63
+
64
+ withMtime.sort((a, b) => b.mtime - a.mtime);
65
+ const candidates = withMtime.slice(0, limit);
66
+
67
+ const results = [];
68
+ for (const { filePath } of candidates) {
69
+ try {
70
+ const raw = await readFile(filePath, { encoding: 'utf8', flag: 'r' });
71
+ // Already size-bounded by pre-check above; this is belt-and-braces.
72
+ const bounded = raw.length > MAX_FILE_BYTES ? raw.slice(0, MAX_FILE_BYTES) : raw;
73
+ const parsed = JSON.parse(bounded);
74
+ if (
75
+ parsed &&
76
+ typeof parsed === 'object' &&
77
+ !Array.isArray(parsed) &&
78
+ typeof parsed.verdict === 'string' &&
79
+ Array.isArray(parsed.affected_artifacts)
80
+ ) {
81
+ results.push(parsed);
82
+ }
83
+ } catch {
84
+ // malformed JSON or read error -- skip
85
+ }
86
+ }
87
+
88
+ return results;
89
+ }
90
+
91
+ /**
92
+ * detectPatterns(receipts, opts)
93
+ *
94
+ * Examines the last `opts.window` (default 10) receipts for repeated FAIL/FLAG
95
+ * on the same affected_artifacts[].type value.
96
+ *
97
+ * If a single artifact_type appears in >= opts.threshold (default 3) receipts
98
+ * within the window with a FAIL or FLAG verdict, one pattern object is emitted
99
+ * for that artifact_type.
100
+ *
101
+ * @param {object[]} receipts
102
+ * @param {{ threshold?: number, window?: number }} [opts]
103
+ * @returns {Array<{ kind: string, artifact_type: string, count: number, threshold: number, sample: string[] }>}
104
+ */
105
+ export function detectPatterns(receipts, opts = {}) {
106
+ const threshold = typeof opts.threshold === 'number' ? opts.threshold : 3;
107
+ const window = typeof opts.window === 'number' ? opts.window : 10;
108
+
109
+ if (!Array.isArray(receipts) || receipts.length === 0) return [];
110
+
111
+ const windowReceipts = receipts.slice(0, window);
112
+
113
+ const countsByType = new Map();
114
+ const samplesByType = new Map();
115
+
116
+ for (const receipt of windowReceipts) {
117
+ if (!receipt || typeof receipt !== 'object') continue;
118
+ if (!FAIL_VERDICTS.has(receipt.verdict)) continue;
119
+ if (!Array.isArray(receipt.affected_artifacts)) continue;
120
+
121
+ const seenTypes = new Set();
122
+ for (const artifact of receipt.affected_artifacts) {
123
+ if (!artifact || typeof artifact !== 'object') continue;
124
+ if (typeof artifact.type !== 'string' || artifact.type.length === 0) continue;
125
+
126
+ const t = artifact.type;
127
+ if (seenTypes.has(t)) continue;
128
+ seenTypes.add(t);
129
+
130
+ countsByType.set(t, (countsByType.get(t) ?? 0) + 1);
131
+
132
+ if (!samplesByType.has(t)) samplesByType.set(t, []);
133
+ const gateId =
134
+ typeof receipt.gate_id === 'string' ? receipt.gate_id : 'unknown';
135
+ samplesByType.get(t).push(gateId);
136
+ }
137
+ }
138
+
139
+ const patterns = [];
140
+ for (const [artifact_type, count] of countsByType.entries()) {
141
+ if (count >= threshold) {
142
+ patterns.push({
143
+ kind: 'repeated-fail-on-same-artifact',
144
+ artifact_type,
145
+ count,
146
+ threshold,
147
+ sample: samplesByType.get(artifact_type) ?? [],
148
+ });
149
+ }
150
+ }
151
+
152
+ return patterns;
153
+ }
154
+
155
+ /**
156
+ * getFeedbackSuggestions(projectRoot, opts)
157
+ *
158
+ * Reads gate receipts, detects patterns, and returns an array of one-liner
159
+ * markdown bullet bodies (caller prepends "- ").
160
+ *
161
+ * Text format: "Pattern detected: <count>/<window> recent gates flagged on
162
+ * <artifact_type> -- consider reviewing <artifact_type> scope"
163
+ *
164
+ * Never throws; returns [] on any error.
165
+ *
166
+ * @param {string} projectRoot
167
+ * @param {{ threshold?: number, window?: number, limit?: number }} [opts]
168
+ * @returns {Promise<string[]>}
169
+ */
170
+ export async function getFeedbackSuggestions(projectRoot, opts = {}) {
171
+ try {
172
+ // W7.1: bound caller-supplied opts to defensible minimums so misconfigured
173
+ // callers cannot disable the feature or pass negative values.
174
+ const limit = Math.max(1, typeof opts.limit === 'number' ? opts.limit : 50);
175
+ const window = Math.max(1, typeof opts.window === 'number' ? opts.window : 10);
176
+ const threshold = Math.max(1, typeof opts.threshold === 'number' ? opts.threshold : 3);
177
+
178
+ const receipts = await readRecentReceipts(projectRoot, limit);
179
+ const patterns = detectPatterns(receipts, { threshold, window });
180
+
181
+ return patterns.map(
182
+ (p) =>
183
+ `Pattern detected: ${p.count}/${window} recent gates flagged on ${p.artifact_type} -- consider reviewing ${p.artifact_type} scope`,
184
+ );
185
+ } catch {
186
+ return [];
187
+ }
188
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * override-manifest-schema.js
3
+ *
4
+ * IJFW v1.4.0 Wave 0 / t2 — Override Manifest Schema
5
+ *
6
+ * Overrides are section-fenced markdown patches applied to base IJFW skills
7
+ * at DEPLOYMENT time (never runtime). The resolver merges base + tier chain
8
+ * and writes the result to every platform skill dir.
9
+ *
10
+ * File format (.ijfw/skill-overrides/project/<skill>/override.md):
11
+ * ---
12
+ * extends: [book, academic-style]
13
+ * scope: project
14
+ * skill: ijfw-critique
15
+ * ---
16
+ *
17
+ * <!-- ijfw-override: rubric -->
18
+ * ... section body ...
19
+ * <!-- ijfw-override-end -->
20
+ *
21
+ * 4-tier resolution (last-write-wins, project has final say per R4):
22
+ * 1. base presets ~/.ijfw/overrides/presets/
23
+ * 2. user ~/.ijfw/user-overrides/
24
+ * 3. org ~/.ijfw/org-overrides/
25
+ * 4. project .ijfw/skill-overrides/
26
+ *
27
+ * `extends:` chain depth-limited to 5; circular chains rejected.
28
+ *
29
+ * Hand-rolled validator. Zero new prod deps.
30
+ */
31
+
32
+ export const SCHEMA_VERSION = '1.0';
33
+
34
+ /**
35
+ * Ordered list. Earlier scopes are overridden by later ones (last-write-wins).
36
+ * Project always has final precedence over org > user > base presets.
37
+ */
38
+ export const OVERRIDE_SCOPES = Object.freeze(['base', 'user', 'org', 'project']);
39
+
40
+ export const BUILTIN_PRESETS = Object.freeze([
41
+ 'book',
42
+ 'campaign',
43
+ 'academic',
44
+ 'screenplay',
45
+ ]);
46
+
47
+ export const MAX_EXTENDS_DEPTH = 5;
48
+
49
+ export const SKILL_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
50
+ export const PRESET_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
51
+
52
+ /** Section fence markers used by the resolver. */
53
+ export const OVERRIDE_OPEN_FENCE = /<!--\s*ijfw-override:\s*([a-z][a-z0-9-]*)\s*-->/g;
54
+ export const OVERRIDE_CLOSE_FENCE = /<!--\s*ijfw-override-end\s*-->/;
55
+
56
+ function isString(v) {
57
+ return typeof v === 'string';
58
+ }
59
+
60
+ function isNonNullObject(v) {
61
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
62
+ }
63
+
64
+ /**
65
+ * validateOverrideManifest(obj) — validates the YAML frontmatter portion of
66
+ * an override file (parsed into an object). The body (section fences) is
67
+ * validated by the resolver, not here.
68
+ *
69
+ * @param {unknown} obj
70
+ * @returns {{valid: boolean, errors: string[]}}
71
+ */
72
+ export function validateOverrideManifest(obj) {
73
+ const errors = [];
74
+
75
+ if (!isNonNullObject(obj)) {
76
+ return { valid: false, errors: ['root: must be an object'] };
77
+ }
78
+
79
+ // scope (required)
80
+ if (!OVERRIDE_SCOPES.includes(obj.scope)) {
81
+ errors.push(
82
+ `scope: must be one of ${OVERRIDE_SCOPES.join('|')}, got ${JSON.stringify(obj.scope)}`,
83
+ );
84
+ }
85
+
86
+ // skill (required)
87
+ if (!isString(obj.skill) || !SKILL_NAME_PATTERN.test(obj.skill)) {
88
+ errors.push(
89
+ `skill: must be a kebab-case identifier matching ${SKILL_NAME_PATTERN}`,
90
+ );
91
+ }
92
+
93
+ // extends (optional, array of preset names)
94
+ if (obj.extends !== undefined) {
95
+ if (!Array.isArray(obj.extends)) {
96
+ errors.push('extends: must be an array of preset name strings (or omitted)');
97
+ } else {
98
+ obj.extends.forEach((p, i) => {
99
+ if (!isString(p) || !PRESET_NAME_PATTERN.test(p)) {
100
+ errors.push(
101
+ `extends[${i}]: must be a kebab-case preset name, got ${JSON.stringify(p)}`,
102
+ );
103
+ }
104
+ });
105
+ // Self-reference check (cycle detection across files is the resolver's job;
106
+ // this just blocks the trivially obvious case).
107
+ if (isString(obj.skill) && obj.extends.includes(obj.skill)) {
108
+ errors.push(`extends: must not include own skill "${obj.skill}" (circular)`);
109
+ }
110
+ }
111
+ }
112
+
113
+ return { valid: errors.length === 0, errors };
114
+ }
115
+
116
+ /**
117
+ * detectCircularExtends(graph, start, seen) — detects cycles in the extends
118
+ * graph. Resolver passes a Map<presetName, manifest>; we walk recursively.
119
+ *
120
+ * Exported for use by the resolver (t6) and by tests (t18).
121
+ *
122
+ * @param {Map<string, {extends?: string[]}>} graph
123
+ * @param {string} start
124
+ * @param {Set<string>} [seen]
125
+ * @param {number} [depth]
126
+ * @returns {{circular: boolean, chain: string[]}}
127
+ */
128
+ export function detectCircularExtends(graph, start, seen = new Set(), depth = 0) {
129
+ if (depth > MAX_EXTENDS_DEPTH) {
130
+ return { circular: true, chain: [...seen, start, '...(depth-exceeded)'] };
131
+ }
132
+ if (seen.has(start)) {
133
+ return { circular: true, chain: [...seen, start] };
134
+ }
135
+ const m = graph.get(start);
136
+ if (!m || !Array.isArray(m.extends) || m.extends.length === 0) {
137
+ return { circular: false, chain: [...seen, start] };
138
+ }
139
+ const next = new Set(seen);
140
+ next.add(start);
141
+ for (const parent of m.extends) {
142
+ const r = detectCircularExtends(graph, parent, next, depth + 1);
143
+ if (r.circular) return r;
144
+ }
145
+ return { circular: false, chain: [...next, start] };
146
+ }