@mindrian_os/install 1.13.0-beta.12 → 1.13.0-beta.14
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +57 -10
- package/README.md +74 -572
- package/commands/act.md +1 -0
- package/commands/admin.md +1 -0
- package/commands/analyze-needs.md +1 -0
- package/commands/analyze-systems.md +1 -0
- package/commands/analyze-timing.md +1 -0
- package/commands/auto-explore.md +2 -0
- package/commands/beautiful-question.md +1 -0
- package/commands/brain-derive.md +1 -0
- package/commands/build-knowledge.md +1 -0
- package/commands/build-thesis.md +1 -0
- package/commands/causal.md +1 -0
- package/commands/challenge-assumptions.md +1 -0
- package/commands/compare-ventures.md +1 -0
- package/commands/dashboard.md +1 -0
- package/commands/deep-grade.md +1 -0
- package/commands/diagnose.md +1 -0
- package/commands/diagnostics.md +1 -0
- package/commands/doctor.md +2 -1
- package/commands/dominant-designs.md +1 -0
- package/commands/explain-decision.md +1 -0
- package/commands/explore-domains.md +1 -0
- package/commands/explore-futures.md +1 -0
- package/commands/explore-trends.md +1 -0
- package/commands/export.md +1 -0
- package/commands/feynman-timeline-refresh.md +78 -0
- package/commands/file-meeting.md +1 -0
- package/commands/find-analogies.md +1 -0
- package/commands/find-bottlenecks.md +1 -0
- package/commands/find-connections.md +1 -0
- package/commands/funding.md +1 -0
- package/commands/grade.md +1 -0
- package/commands/graph.md +1 -0
- package/commands/hat-briefing.md +1 -0
- package/commands/heal.md +1 -0
- package/commands/help.md +1 -0
- package/commands/hmi-status.md +1 -0
- package/commands/jtbd.md +1 -0
- package/commands/leadership.md +1 -0
- package/commands/lean-canvas.md +1 -0
- package/commands/macro-trends.md +1 -0
- package/commands/map-unknowns.md +1 -0
- package/commands/memory.md +1 -0
- package/commands/models.md +1 -0
- package/commands/mos-reason.md +1 -0
- package/commands/mullins.md +1 -0
- package/commands/new-project.md +1 -0
- package/commands/onboard.md +1 -0
- package/commands/operator.md +2 -1
- package/commands/opportunities.md +1 -0
- package/commands/organize.md +1 -0
- package/commands/persona.md +1 -0
- package/commands/pipeline.md +1 -0
- package/commands/present.md +1 -0
- package/commands/publish.md +1 -0
- package/commands/query.md +1 -0
- package/commands/radar.md +1 -0
- package/commands/reanalyze.md +1 -0
- package/commands/research.md +1 -0
- package/commands/room.md +1 -0
- package/commands/rooms.md +1 -0
- package/commands/root-cause.md +1 -0
- package/commands/rs-experts.md +1 -0
- package/commands/rs-explain.md +1 -0
- package/commands/rs-fetch.md +1 -0
- package/commands/rs-thesis.md +1 -0
- package/commands/scenario-plan.md +1 -0
- package/commands/scheduled-tasks.md +1 -0
- package/commands/score-innovation.md +1 -0
- package/commands/scout.md +1 -0
- package/commands/setup.md +8 -3
- package/commands/snapshot.md +1 -0
- package/commands/speakers.md +1 -0
- package/commands/splash.md +1 -0
- package/commands/status.md +1 -0
- package/commands/structure-argument.md +1 -0
- package/commands/suggest-next.md +1 -0
- package/commands/systems-thinking.md +1 -0
- package/commands/think-hats.md +1 -0
- package/commands/update.md +1 -0
- package/commands/user-needs.md +1 -0
- package/commands/validate.md +1 -0
- package/commands/value-proposition.md +1 -0
- package/commands/vault.md +1 -0
- package/commands/visualize.md +1 -0
- package/commands/whitespace.md +1 -0
- package/commands/wiki.md +1 -0
- package/lib/brain/framework-chain-slice.cjs +193 -0
- package/lib/core/active-plugin-root.cjs +71 -6
- package/lib/core/brain-client.cjs +451 -36
- package/lib/core/cache-prune.cjs +208 -0
- package/lib/core/feynman/ROOM.md +25 -0
- package/lib/core/feynman/timeline-renderer.cjs +197 -0
- package/lib/core/feynman/timeline-runner.cjs +281 -0
- package/lib/core/navigation/edges.cjs +86 -0
- package/lib/core/navigation/insights.cjs +37 -0
- package/lib/core/navigation/memory-events.cjs +56 -1
- package/lib/core/navigation/neighborhood.cjs +5 -4
- package/lib/core/navigation/packet.cjs +176 -10
- package/lib/core/navigation/projections.cjs +201 -0
- package/lib/core/navigation.cjs +31 -0
- package/lib/core/resolve-brain-key.cjs +201 -0
- package/lib/mcp/larry-server-instructions.md +1 -1
- package/lib/memory/brain-cypher-chain-slice.test.cjs +368 -0
- package/lib/memory/f-selector-ranker.test.cjs +593 -0
- package/lib/memory/navigation-projections.test.cjs +241 -0
- package/lib/memory/navigation-write-edge.test.cjs +206 -0
- package/lib/memory/packet-chain-hint.test.cjs +407 -0
- package/lib/memory/packet-schema-validation.test.cjs +317 -0
- package/lib/memory/per-command-jtbd-derivation.test.cjs +130 -0
- package/lib/memory/per-command-teaching.test.cjs +110 -0
- package/lib/memory/run-feynman-tests.cjs +121 -0
- package/lib/memory/security-trifecta.test.cjs +23 -6
- package/lib/memory/selector-decisions.test.cjs +417 -0
- package/lib/memory/selector-miss.test.cjs +290 -0
- package/lib/workflow/f-selector-ranker.cjs +420 -0
- package/lib/workflow/selector-decisions.cjs +368 -0
- package/package.json +4 -1
- package/references/design/email-template-standard.md +1 -1
- package/references/user-research/2026-04-05-leah-lawrence-session.md +3 -3
- package/skills/brain-connector/SKILL.md +9 -3
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 124-02 -- FEYNMAN.md timeline runner
|
|
3
|
+
// =========================================
|
|
4
|
+
// The side-effect orchestrator. Walks a room's section folders, finds each FEYNMAN.md,
|
|
5
|
+
// applies the D-02 sentinel-bounded merge over the body returned by the pure renderer
|
|
6
|
+
// (Plan 124-01), writes back atomically (.tmp + rename), updates the timeline_last_rendered
|
|
7
|
+
// frontmatter, and logs a memory_event on every refresh attempt (success or failure).
|
|
8
|
+
//
|
|
9
|
+
// Honors the D-09 watermark contract: skip when the frontmatter timeline_last_rendered ISO
|
|
10
|
+
// is >= the SQL MAX(memory_event.created_at) ISO for the section (second-resolution).
|
|
11
|
+
//
|
|
12
|
+
// The renderer (timeline-renderer.cjs) is the pure function. The runner is the impure shell
|
|
13
|
+
// around it. tests/test-feynman-timeline-runner.cjs is the integration test for this module.
|
|
14
|
+
//
|
|
15
|
+
// Canon Part 9: every refresh logs a typed memory_event making the regenerate-vs-skip
|
|
16
|
+
// decision legible in the local mind. Canon Part 8: zero net new Brain surface.
|
|
17
|
+
// Canon Part 7: reuses the atomic .tmp + rename idiom from scripts/vault-section-minto-generator.cjs;
|
|
18
|
+
// the frontmatter line-walk parser idiom from scripts/frontmatter-schema-validator.cjs.
|
|
19
|
+
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
const crypto = require('node:crypto');
|
|
23
|
+
|
|
24
|
+
const renderer = require('./timeline-renderer.cjs');
|
|
25
|
+
const navigation = require('../navigation.cjs');
|
|
26
|
+
|
|
27
|
+
// ---------- Sentinel contract (D-02) ----------
|
|
28
|
+
|
|
29
|
+
const SENTINEL_START = '<!-- TIMELINE_AUTO_START -->';
|
|
30
|
+
const SENTINEL_END = '<!-- TIMELINE_AUTO_END -->';
|
|
31
|
+
const HEADER = '## Timeline (auto)';
|
|
32
|
+
|
|
33
|
+
// ---------- Frontmatter helpers (hand-rolled; no gray-matter) ----------
|
|
34
|
+
|
|
35
|
+
function parseFrontmatter(content) {
|
|
36
|
+
// Returns { fm: { [key]: value }, fmOrder: [keys...], body: '<rest>', hadFrontmatter: bool }.
|
|
37
|
+
const lines = content.split('\n');
|
|
38
|
+
if (lines.length === 0 || lines[0].trim() !== '---') {
|
|
39
|
+
return { fm: {}, fmOrder: [], body: content, hadFrontmatter: false };
|
|
40
|
+
}
|
|
41
|
+
let endIdx = -1;
|
|
42
|
+
for (let i = 1; i < lines.length; i++) {
|
|
43
|
+
if (lines[i].trim() === '---') { endIdx = i; break; }
|
|
44
|
+
}
|
|
45
|
+
if (endIdx === -1) {
|
|
46
|
+
return { fm: {}, fmOrder: [], body: content, hadFrontmatter: false };
|
|
47
|
+
}
|
|
48
|
+
const fmLines = lines.slice(1, endIdx);
|
|
49
|
+
const bodyLines = lines.slice(endIdx + 1);
|
|
50
|
+
const fm = {};
|
|
51
|
+
const fmOrder = [];
|
|
52
|
+
for (const ln of fmLines) {
|
|
53
|
+
const m = ln.match(/^([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$/);
|
|
54
|
+
if (!m) continue;
|
|
55
|
+
const key = m[1];
|
|
56
|
+
const raw = m[2];
|
|
57
|
+
let val;
|
|
58
|
+
if (raw === '' || raw === 'null') val = null;
|
|
59
|
+
else if (raw === 'true') val = true;
|
|
60
|
+
else if (raw === 'false') val = false;
|
|
61
|
+
else if (/^-?\d+(\.\d+)?$/.test(raw)) val = Number(raw);
|
|
62
|
+
else if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) val = raw.slice(1, -1);
|
|
63
|
+
else val = raw;
|
|
64
|
+
fm[key] = val;
|
|
65
|
+
if (fmOrder.indexOf(key) === -1) fmOrder.push(key);
|
|
66
|
+
}
|
|
67
|
+
return { fm: fm, fmOrder: fmOrder, body: bodyLines.join('\n'), hadFrontmatter: true };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function serializeFrontmatter(fm, fmOrder, body, hadFrontmatter) {
|
|
71
|
+
// If fm is empty AND there was no original frontmatter -> return body unchanged.
|
|
72
|
+
const keys = Object.keys(fm);
|
|
73
|
+
if (keys.length === 0 && !hadFrontmatter) return body;
|
|
74
|
+
// Honor fmOrder for existing keys; append any new keys not in fmOrder at the end.
|
|
75
|
+
const order = fmOrder.slice();
|
|
76
|
+
for (const k of keys) {
|
|
77
|
+
if (order.indexOf(k) === -1) order.push(k);
|
|
78
|
+
}
|
|
79
|
+
const out = ['---'];
|
|
80
|
+
for (const k of order) {
|
|
81
|
+
if (!(k in fm)) continue;
|
|
82
|
+
const v = fm[k];
|
|
83
|
+
if (v === null) out.push(k + ':');
|
|
84
|
+
else if (typeof v === 'boolean' || typeof v === 'number') out.push(k + ': ' + String(v));
|
|
85
|
+
else out.push(k + ': ' + String(v));
|
|
86
|
+
}
|
|
87
|
+
out.push('---');
|
|
88
|
+
return out.join('\n') + '\n' + body;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------- Sentinel-bounded merge (D-02 hard invariant) ----------
|
|
92
|
+
|
|
93
|
+
function mergeSentinelSection(body, renderedBody) {
|
|
94
|
+
// Case A: the sentinel pair exists -- replace the content between.
|
|
95
|
+
const startIdx = body.indexOf(SENTINEL_START);
|
|
96
|
+
const endIdx = body.indexOf(SENTINEL_END);
|
|
97
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
98
|
+
const before = body.slice(0, startIdx + SENTINEL_START.length);
|
|
99
|
+
const after = body.slice(endIdx);
|
|
100
|
+
return before + '\n' + renderedBody + '\n' + after;
|
|
101
|
+
}
|
|
102
|
+
// Case B: no sentinel pair -- append at end-of-file (ensure trailing newline).
|
|
103
|
+
const sep = body.endsWith('\n') ? '' : '\n';
|
|
104
|
+
return body + sep + '\n' + HEADER + '\n\n' + SENTINEL_START + '\n' + renderedBody + '\n' + SENTINEL_END + '\n';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function bodyOutsideSentinels(body) {
|
|
108
|
+
// For the SHA256 byte-preservation check. Returns the body with the sentinel block excised
|
|
109
|
+
// (everything from SENTINEL_START to SENTINEL_END inclusive removed, plus the lines they
|
|
110
|
+
// sit on). If no sentinels, returns the body unchanged.
|
|
111
|
+
const startIdx = body.indexOf(SENTINEL_START);
|
|
112
|
+
const endIdx = body.indexOf(SENTINEL_END);
|
|
113
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return body;
|
|
114
|
+
// Find the start of the line containing SENTINEL_START (back up to the previous newline)
|
|
115
|
+
// and the end of the line containing SENTINEL_END (forward to the next newline).
|
|
116
|
+
let lineStart = body.lastIndexOf('\n', startIdx);
|
|
117
|
+
if (lineStart === -1) lineStart = 0;
|
|
118
|
+
let lineEnd = body.indexOf('\n', endIdx + SENTINEL_END.length);
|
|
119
|
+
if (lineEnd === -1) lineEnd = body.length;
|
|
120
|
+
return body.slice(0, lineStart) + body.slice(lineEnd);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sha256Hex(s) {
|
|
124
|
+
return crypto.createHash('sha256').update(s, 'utf8').digest('hex');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------- Atomic write (D-02 hard invariant: no partial-write corruption) ----------
|
|
128
|
+
|
|
129
|
+
function atomicWrite(targetPath, content) {
|
|
130
|
+
const dir = path.dirname(targetPath);
|
|
131
|
+
const base = path.basename(targetPath);
|
|
132
|
+
const tmp = path.join(dir, '.' + base + '.tmp.' + process.pid + '.' + Date.now());
|
|
133
|
+
const fd = fs.openSync(tmp, 'w');
|
|
134
|
+
try {
|
|
135
|
+
fs.writeSync(fd, content, 0, 'utf8');
|
|
136
|
+
try { fs.fsyncSync(fd); } catch (_) { /* fsync best-effort on platforms without it */ }
|
|
137
|
+
} finally {
|
|
138
|
+
fs.closeSync(fd);
|
|
139
|
+
}
|
|
140
|
+
fs.renameSync(tmp, targetPath);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------- Section walk ----------
|
|
144
|
+
|
|
145
|
+
function safeIsDir(p) { try { return fs.statSync(p).isDirectory(); } catch (_) { return false; } }
|
|
146
|
+
function safeIsFile(p) { try { return fs.statSync(p).isFile(); } catch (_) { return false; } }
|
|
147
|
+
|
|
148
|
+
function findFeynmanSections(roomDir) {
|
|
149
|
+
// Subdirectories of roomDir that contain a FEYNMAN.md at <roomDir>/<section>/FEYNMAN.md.
|
|
150
|
+
if (!safeIsDir(roomDir)) return [];
|
|
151
|
+
let entries;
|
|
152
|
+
try {
|
|
153
|
+
entries = fs.readdirSync(roomDir, { withFileTypes: true });
|
|
154
|
+
} catch (_) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
const out = [];
|
|
158
|
+
for (const ent of entries) {
|
|
159
|
+
if (!ent.isDirectory()) continue;
|
|
160
|
+
if (ent.name.startsWith('.')) continue;
|
|
161
|
+
if (ent.name === 'node_modules') continue;
|
|
162
|
+
const feyPath = path.join(roomDir, ent.name, 'FEYNMAN.md');
|
|
163
|
+
if (safeIsFile(feyPath)) out.push({ slug: ent.name, feyPath: feyPath });
|
|
164
|
+
}
|
|
165
|
+
out.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------- Watermark check (D-09) ----------
|
|
170
|
+
|
|
171
|
+
function shouldSkipWatermark(db, sectionSlug, fmRendered) {
|
|
172
|
+
if (typeof fmRendered !== 'string' || fmRendered.length === 0) return false;
|
|
173
|
+
let summary;
|
|
174
|
+
try {
|
|
175
|
+
summary = navigation.firstCapturedLastTouchedBySection(db, sectionSlug);
|
|
176
|
+
} catch (_) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
if (!summary || summary.total_events === 0) return false; // no events; refresh to empty-state if needed.
|
|
180
|
+
const sqlIso = renderer.isoSecond(summary.last_touched_ms);
|
|
181
|
+
if (!sqlIso) return false;
|
|
182
|
+
// ISO 8601 second-resolution strings are lex-sortable.
|
|
183
|
+
return sqlIso <= fmRendered;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------- The orchestrator ----------
|
|
187
|
+
|
|
188
|
+
function refreshSection(roomDir, sectionSlug, opts) {
|
|
189
|
+
const options = opts || {};
|
|
190
|
+
const db = options.db;
|
|
191
|
+
const now_ms = Number.isFinite(options.now_ms) ? options.now_ms : Date.now();
|
|
192
|
+
const force = options.force === true;
|
|
193
|
+
const feyPath = path.join(roomDir, sectionSlug, 'FEYNMAN.md');
|
|
194
|
+
|
|
195
|
+
if (!safeIsFile(feyPath)) {
|
|
196
|
+
return { status: 'skipped_no_feynman', reason: 'no_feynman_md_in_section_dir' };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const rawContent = fs.readFileSync(feyPath, 'utf8');
|
|
201
|
+
const parsed = parseFrontmatter(rawContent);
|
|
202
|
+
|
|
203
|
+
// D-09 watermark check.
|
|
204
|
+
if (!force && db && typeof parsed.fm.timeline_last_rendered === 'string') {
|
|
205
|
+
if (shouldSkipWatermark(db, sectionSlug, parsed.fm.timeline_last_rendered)) {
|
|
206
|
+
return { status: 'skipped_watermark', reason: 'sql_older_than_rendered' };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Render (D-05). Reads ONLY via navigation.cjs.
|
|
211
|
+
const rendered = renderer.renderTimeline(db, sectionSlug, { now_ms: now_ms });
|
|
212
|
+
|
|
213
|
+
// Merge (D-02 hard invariant).
|
|
214
|
+
const newBody = mergeSentinelSection(parsed.body, rendered.markdown_body);
|
|
215
|
+
|
|
216
|
+
// Update frontmatter watermark (D-09).
|
|
217
|
+
const newIso = renderer.isoSecond(now_ms);
|
|
218
|
+
const newFm = Object.assign({}, parsed.fm, { timeline_last_rendered: newIso });
|
|
219
|
+
// Force frontmatter emission on every refresh (true so the watermark always lands at
|
|
220
|
+
// the top, even when the original file had no fences).
|
|
221
|
+
const newContent = serializeFrontmatter(newFm, parsed.fmOrder, newBody, true);
|
|
222
|
+
|
|
223
|
+
// Atomic write.
|
|
224
|
+
atomicWrite(feyPath, newContent);
|
|
225
|
+
|
|
226
|
+
// Log success memory_event (D-10).
|
|
227
|
+
if (db) {
|
|
228
|
+
try {
|
|
229
|
+
navigation.logMemoryEvent(db, 'feynman_timeline_refreshed', {
|
|
230
|
+
source_path: 'feynman:' + sectionSlug,
|
|
231
|
+
created_by: 'system',
|
|
232
|
+
});
|
|
233
|
+
} catch (_) { /* logging failure does not corrupt the refresh; the write already landed */ }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { status: 'refreshed', written_path: feyPath, watermark: newIso };
|
|
237
|
+
} catch (err) {
|
|
238
|
+
const reason = (err && err.message) ? String(err.message).slice(0, 200) : 'unknown_error';
|
|
239
|
+
if (db) {
|
|
240
|
+
try {
|
|
241
|
+
navigation.logMemoryEvent(db, 'feynman_timeline_refresh_failed', {
|
|
242
|
+
source_path: 'feynman:' + sectionSlug,
|
|
243
|
+
created_by: 'system',
|
|
244
|
+
reason: reason,
|
|
245
|
+
});
|
|
246
|
+
} catch (_) { /* secondary failure is swallowed */ }
|
|
247
|
+
}
|
|
248
|
+
return { status: 'failed', reason: reason };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function refreshAll(roomDir, opts) {
|
|
253
|
+
const options = opts || {};
|
|
254
|
+
const refreshed = [];
|
|
255
|
+
const skipped = [];
|
|
256
|
+
const failed = [];
|
|
257
|
+
const sections = findFeynmanSections(roomDir);
|
|
258
|
+
for (const s of sections) {
|
|
259
|
+
const r = refreshSection(roomDir, s.slug, options);
|
|
260
|
+
if (r.status === 'refreshed') refreshed.push({ slug: s.slug, written_path: r.written_path, watermark: r.watermark });
|
|
261
|
+
else if (r.status === 'failed') failed.push({ slug: s.slug, reason: r.reason });
|
|
262
|
+
else skipped.push({ slug: s.slug, reason: r.reason || r.status });
|
|
263
|
+
}
|
|
264
|
+
return { refreshed: refreshed, skipped: skipped, failed: failed };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = {
|
|
268
|
+
refreshAll,
|
|
269
|
+
refreshSection,
|
|
270
|
+
parseFrontmatter,
|
|
271
|
+
serializeFrontmatter,
|
|
272
|
+
mergeSentinelSection,
|
|
273
|
+
bodyOutsideSentinels,
|
|
274
|
+
sha256Hex,
|
|
275
|
+
findFeynmanSections,
|
|
276
|
+
shouldSkipWatermark,
|
|
277
|
+
atomicWrite,
|
|
278
|
+
SENTINEL_START,
|
|
279
|
+
SENTINEL_END,
|
|
280
|
+
HEADER,
|
|
281
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 125-00 -- navigation.cjs edge-write primitive (per CONTEXT.md Pass 3 GAP-2
|
|
3
|
+
// resolution). Adds writeEdge to the navigation.cjs closed surface as an additive
|
|
4
|
+
// extension following the Phase 110-03 logMemoryEvent precedent.
|
|
5
|
+
//
|
|
6
|
+
// Canon Part 4: every choice is graph data; this is the chokepoint primitive that
|
|
7
|
+
// lets Plan 06 selector-decisions.cjs + future Phases 116/117/118 write typed
|
|
8
|
+
// cascade edges without bypassing the closed surface.
|
|
9
|
+
//
|
|
10
|
+
// Canon Part 7: reuse-before-build -- the UPSERT statement mirrors
|
|
11
|
+
// lib/core/lazygraph-ops.cjs::upsertEdge (lines 990-1019) so sibling agents
|
|
12
|
+
// emit edges via the same UPSERT shape and the navigation chokepoint stays
|
|
13
|
+
// the single door for write traffic.
|
|
14
|
+
//
|
|
15
|
+
// Canon Part 8 invariant: writeEdge takes (db, params) -- a db handle owned by
|
|
16
|
+
// the caller (via openRoomDb) -- so this module never opens room.db itself. Zero
|
|
17
|
+
// direct room-db.cjs require here. The navigation pre-commit hook treats this
|
|
18
|
+
// file as part of the navigation/* allow-list, NOT as a bypass.
|
|
19
|
+
|
|
20
|
+
const crypto = require('node:crypto');
|
|
21
|
+
|
|
22
|
+
// Closed edge-type allowlist enforced by writeEdge. Mirrors the EVENT_TYPES Set
|
|
23
|
+
// pattern in lib/core/navigation/memory-events.cjs. Phase 125 ships DEFERRED +
|
|
24
|
+
// REJECTED (D7 typed cascade edge surface for F.1 defer / F.2 reject). Future
|
|
25
|
+
// phases (e.g. Phase 116 tension resolution, Phase 117 auto-explore, Phase 118
|
|
26
|
+
// MVA) extend this Set additively without canon amendment -- same idiom as the
|
|
27
|
+
// EVENT_TYPES additive blocks for Phases 88.2-00 / 89-07 / 116-00 / 117-00 /
|
|
28
|
+
// 110-02.
|
|
29
|
+
//
|
|
30
|
+
// Tests assert a FLOOR (DEFERRED + REJECTED present) and Set-instance shape --
|
|
31
|
+
// not an exact size -- so additive extensions cannot regress baseline.
|
|
32
|
+
const ALLOWED_EDGE_TYPES = Object.freeze(new Set([
|
|
33
|
+
// Phase 125 D7 -- F-selector decision edges (LOCKED LOCAL per Canon Part 8).
|
|
34
|
+
'DEFERRED',
|
|
35
|
+
'REJECTED',
|
|
36
|
+
]));
|
|
37
|
+
|
|
38
|
+
function isPlainObject(v) {
|
|
39
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// writeEdge(db, params) -- the 15th-style additive re-export on the
|
|
43
|
+
// navigation.cjs closed surface (see navigation.cjs header comment for the
|
|
44
|
+
// canonical re-export pattern alongside logMemoryEvent + firstCapturedLastTouchedBySection).
|
|
45
|
+
//
|
|
46
|
+
// Positional db (first arg, owned by caller via openRoomDb), params object
|
|
47
|
+
// (second arg) with: { source_id, target_id, edge_type, properties }.
|
|
48
|
+
//
|
|
49
|
+
// Returns { ok: true, edge_id, type, source, target } on success, or
|
|
50
|
+
// { ok: false, reason, detail? } on validation / write failure. Defensive --
|
|
51
|
+
// never throws on caller input. The underlying prepare/run is sync per the
|
|
52
|
+
// node:sqlite contract (matches the rest of the navigation module).
|
|
53
|
+
function writeEdge(db, params) {
|
|
54
|
+
if (!params || typeof params !== 'object') {
|
|
55
|
+
return { ok: false, reason: 'invalid_params' };
|
|
56
|
+
}
|
|
57
|
+
const { source_id, target_id, edge_type, properties } = params;
|
|
58
|
+
if (typeof source_id !== 'string' || source_id.length === 0) {
|
|
59
|
+
return { ok: false, reason: 'invalid_source_id' };
|
|
60
|
+
}
|
|
61
|
+
if (typeof target_id !== 'string' || target_id.length === 0) {
|
|
62
|
+
return { ok: false, reason: 'invalid_target_id' };
|
|
63
|
+
}
|
|
64
|
+
if (typeof edge_type !== 'string' || !ALLOWED_EDGE_TYPES.has(edge_type)) {
|
|
65
|
+
return { ok: false, reason: 'invalid_edge_type', detail: String(edge_type).slice(0, 40) };
|
|
66
|
+
}
|
|
67
|
+
const props = isPlainObject(properties) ? properties : {};
|
|
68
|
+
let propsJson;
|
|
69
|
+
try {
|
|
70
|
+
propsJson = JSON.stringify(props);
|
|
71
|
+
} catch (_e) {
|
|
72
|
+
return { ok: false, reason: 'properties_serialize_failed' };
|
|
73
|
+
}
|
|
74
|
+
const edgeId = 'edge:' + edge_type + ':' + Date.now() + ':' + crypto.randomBytes(4).toString('hex');
|
|
75
|
+
try {
|
|
76
|
+
db.prepare(
|
|
77
|
+
'INSERT INTO edges (source, target, type, properties) VALUES (?, ?, ?, ?) ' +
|
|
78
|
+
'ON CONFLICT(source, target, type) DO UPDATE SET properties = excluded.properties'
|
|
79
|
+
).run(source_id, target_id, edge_type, propsJson);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
return { ok: false, reason: 'edge_write_failed', detail: String(e.message || '').slice(0, 80) };
|
|
82
|
+
}
|
|
83
|
+
return { ok: true, edge_id: edgeId, type: edge_type, source: source_id, target: target_id };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { ALLOWED_EDGE_TYPES, writeEdge };
|
|
@@ -339,6 +339,42 @@ function findSurfaceableTensions(db, roomId, opts) {
|
|
|
339
339
|
return candidates;
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Phase 124-01 Plan: firstCapturedLastTouchedBySection
|
|
344
|
+
* ----------------------------------------------------
|
|
345
|
+
* For a given section slug (the source_section identity for a folder), return
|
|
346
|
+
* { first_captured_ms, last_touched_ms, total_events } across all memory_event
|
|
347
|
+
* rows scoped to that section (source_path === slug OR source_path LIKE slug/%).
|
|
348
|
+
*
|
|
349
|
+
* Used by lib/core/feynman/timeline-renderer.cjs for the D-05 summary line
|
|
350
|
+
* ("first captured {first_iso}, last touched {last_iso}"). Returns nulls + 0
|
|
351
|
+
* count when the section has zero memory_event rows -- the empty-state cue.
|
|
352
|
+
*
|
|
353
|
+
* Canon Part 9: pure SQL read against the local room.db; zero Brain surface;
|
|
354
|
+
* zero filesystem reads. Canon Part 8-safe by construction.
|
|
355
|
+
*/
|
|
356
|
+
function firstCapturedLastTouchedBySection(db, sectionSlug) {
|
|
357
|
+
if (!db || typeof db.prepare !== 'function' || typeof sectionSlug !== 'string' || sectionSlug.length === 0) {
|
|
358
|
+
return { first_captured_ms: null, last_touched_ms: null, total_events: 0 };
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
const row = db.prepare(
|
|
362
|
+
"SELECT MIN(created_at) AS first_ms, MAX(created_at) AS last_ms, COUNT(*) AS n " +
|
|
363
|
+
"FROM nodes " +
|
|
364
|
+
"WHERE type = 'memory_event' " +
|
|
365
|
+
"AND (source_path = ? OR source_path LIKE ?)"
|
|
366
|
+
).get(sectionSlug, sectionSlug + '/%');
|
|
367
|
+
const total = (row && Number.isFinite(row.n)) ? row.n : 0;
|
|
368
|
+
return {
|
|
369
|
+
first_captured_ms: total > 0 && Number.isFinite(row.first_ms) ? row.first_ms : null,
|
|
370
|
+
last_touched_ms: total > 0 && Number.isFinite(row.last_ms) ? row.last_ms : null,
|
|
371
|
+
total_events: total,
|
|
372
|
+
};
|
|
373
|
+
} catch (_) {
|
|
374
|
+
return { first_captured_ms: null, last_touched_ms: null, total_events: 0 };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
342
378
|
module.exports = {
|
|
343
379
|
findContradictions,
|
|
344
380
|
findUnsupportedClaims,
|
|
@@ -347,4 +383,5 @@ module.exports = {
|
|
|
347
383
|
findOpenQuestions,
|
|
348
384
|
findRelevantOpportunities,
|
|
349
385
|
findSurfaceableTensions,
|
|
386
|
+
firstCapturedLastTouchedBySection,
|
|
350
387
|
};
|
|
@@ -47,13 +47,68 @@ const EVENT_TYPES = Object.freeze(new Set([
|
|
|
47
47
|
// fired (fingerprint detection -> spawn) -> finding_surfaced (drain -> F.1)
|
|
48
48
|
// -> user_response (Explore/Skip/Later/Free-text) OR skipped (suppression).
|
|
49
49
|
// brain_canon_drift_observed (FourLenses Brain vs FiveLenses Canon; emitted once per
|
|
50
|
-
// session by 117-05 emitBrainCanonDrift via this EVENT_TYPES string
|
|
50
|
+
// session by 117-05 emitBrainCanonDrift via this EVENT_TYPES string).
|
|
51
|
+
// Size-invariant note: additive set; downstream phases extend (see the Phase 88.2-00,
|
|
52
|
+
// 89-07-00, 116-00, 117-00, 110-02 blocks). Tests assert a FLOOR + named membership,
|
|
53
|
+
// not an exact count -- so a future phase adding an event type cannot regress baseline.
|
|
51
54
|
'auto_explore_fired',
|
|
52
55
|
'auto_explore_finding_surfaced',
|
|
53
56
|
'auto_explore_user_response',
|
|
54
57
|
'auto_explore_skipped',
|
|
55
58
|
'auto_explore_sanitizer_hit',
|
|
56
59
|
'brain_canon_drift_observed',
|
|
60
|
+
// Phase 110-02 extension (Brain Context Packet Contract; D-07 + D-10 telemetry mirror):
|
|
61
|
+
// brain_packet_rejected -> an outbound packet failed in-schema validation in
|
|
62
|
+
// brain-client.sendPacket (reject hard -- thrown error).
|
|
63
|
+
// brain_response_rejected -> a Brain response failed out-schema validation -> degraded
|
|
64
|
+
// soft, NOT ingested, no partial-ingest.
|
|
65
|
+
// brain_legacy_path_used -> the forward-looking deprecation guard fired (no current
|
|
66
|
+
// call site shipped in 110-02; see brain-client.cjs).
|
|
67
|
+
// Additive extension only; mirrors the Phase 116-00 5-tension-strings idiom and the
|
|
68
|
+
// 117-00 6-auto_explore-strings idiom. logEvent already rejects event_type values
|
|
69
|
+
// outside EVENT_TYPES -- so these are accepted only because they are now IN the Set.
|
|
70
|
+
'brain_packet_rejected',
|
|
71
|
+
'brain_response_rejected',
|
|
72
|
+
'brain_legacy_path_used',
|
|
73
|
+
// Phase 124-02 extension (FEYNMAN.md Temporal Awareness; D-10 telemetry mirror):
|
|
74
|
+
// feynman_timeline_refreshed -> the runner successfully rendered + wrote the
|
|
75
|
+
// sentinel-bounded ## Timeline (auto) section for one
|
|
76
|
+
// FEYNMAN.md (per section, per refresh).
|
|
77
|
+
// feynman_timeline_refresh_failed -> the runner caught an exception during render or write;
|
|
78
|
+
// watermark NOT updated; FEYNMAN.md NOT corrupted (atomic
|
|
79
|
+
// .tmp write means the original is preserved on failure).
|
|
80
|
+
// Additive extension only; mirrors the Phase 110-02 3-string idiom verbatim. logEvent already
|
|
81
|
+
// rejects event_type values outside EVENT_TYPES -- so these are accepted only because they
|
|
82
|
+
// are now IN the Set. Set size grows by 2 (was 35 before Phase 124; now 37 baseline; coexists
|
|
83
|
+
// with the Phase 125-01 framework_invoked extension which adds +1 in parallel).
|
|
84
|
+
'feynman_timeline_refreshed',
|
|
85
|
+
'feynman_timeline_refresh_failed',
|
|
86
|
+
// Phase 125-01 extension (counter source for D3 continuous investment gradient).
|
|
87
|
+
// Each /mos:* methodology command run logs one framework_invoked event with payload
|
|
88
|
+
// {framework, command, timestamp}. computeInvestmentLevel(roomState) projects
|
|
89
|
+
// COUNT(memory_event WHERE event_type='framework_invoked') -> investment_level.
|
|
90
|
+
// The actual emission site is a follow-on instrumentation pass (lib/render/render-v2.cjs
|
|
91
|
+
// or per-command hook); Plan 01 only ships the event_type allowlist entry so
|
|
92
|
+
// computeInvestmentLevel can call findRecentChanges with this filter and get 0 (cold)
|
|
93
|
+
// back from a fresh room. The instrumentation is non-blocking for Plan 01 acceptance
|
|
94
|
+
// since computeInvestmentLevel reads from roomState.framework_invocations (which a
|
|
95
|
+
// separate caller will populate by calling findRecentChanges + counting); when no
|
|
96
|
+
// such count is available, the helper returns level: 0 -- the cold-start path.
|
|
97
|
+
'framework_invoked',
|
|
98
|
+
// Phase 125-06 extension (D7: F-selector reject/defer become typed graph signal).
|
|
99
|
+
// Emitted by lib/workflow/selector-decisions.cjs::recordSelectorDecision.
|
|
100
|
+
// Payload: {decision: 'defer'|'reject', command, framework, reason, edge_semantic,
|
|
101
|
+
// expires_at|null, score_at_decision, investment_level_at_decision}. The corresponding
|
|
102
|
+
// DEFERRED/REJECTED cascade edge is written via navigation.writeEdge (Plan 00).
|
|
103
|
+
// Canon Part 4: every choice is graph data. The F-selector's adaptive-questioning
|
|
104
|
+
// surface relies on this signal to apply decay-weight in subsequent rankings.
|
|
105
|
+
'f_selector_decision',
|
|
106
|
+
// Phase 125-07 extension (D8: none-fit affordance + ranker-miss capture).
|
|
107
|
+
// Emitted by lib/workflow/selector-decisions.cjs::recordSelectorMiss.
|
|
108
|
+
// Payload: {top_k_offered: [{command, score}, ...], user_intent: <verbatim user text>,
|
|
109
|
+
// investment_level_at_decision}. NO cascade edge written (miss is temporal-only).
|
|
110
|
+
// Canon Part 8: user_intent stays LOCAL; never sent to Brain.
|
|
111
|
+
'f_selector_miss',
|
|
57
112
|
]));
|
|
58
113
|
|
|
59
114
|
function isPlainObject(v) {
|
|
@@ -11,20 +11,20 @@
|
|
|
11
11
|
// Canon Part 9: SELECT supersedes folder scanning; this is the load-bearing
|
|
12
12
|
// query for the acceptance test.
|
|
13
13
|
|
|
14
|
-
const NEIGHBORHOOD_SQL = "WITH RECURSIVE neighborhood(id, type, edge_path, depth, edge_type_in, last_seen_at, confidence, source_section, review_status, created_by, source_path) AS ( "
|
|
14
|
+
const NEIGHBORHOOD_SQL = "WITH RECURSIVE neighborhood(id, type, edge_path, depth, edge_type_in, last_seen_at, created_at, confidence, source_section, review_status, created_by, source_path) AS ( "
|
|
15
15
|
+ "SELECT n.id, n.type, json_array(n.id) AS edge_path, 0 AS depth, NULL AS edge_type_in, "
|
|
16
|
-
+ "n.last_seen_at, n.confidence, n.source_section, n.review_status, n.created_by, n.source_path "
|
|
16
|
+
+ "n.last_seen_at, n.created_at, n.confidence, n.source_section, n.review_status, n.created_by, n.source_path "
|
|
17
17
|
+ "FROM nodes n WHERE n.id = :focus_node_id "
|
|
18
18
|
+ "UNION ALL "
|
|
19
19
|
+ "SELECT next_n.id, next_n.type, json_insert(nh.edge_path, '$[#]', next_n.id) AS edge_path, "
|
|
20
20
|
+ "nh.depth + 1 AS depth, e.type AS edge_type_in, "
|
|
21
|
-
+ "next_n.last_seen_at, next_n.confidence, next_n.source_section, next_n.review_status, next_n.created_by, next_n.source_path "
|
|
21
|
+
+ "next_n.last_seen_at, next_n.created_at, next_n.confidence, next_n.source_section, next_n.review_status, next_n.created_by, next_n.source_path "
|
|
22
22
|
+ "FROM neighborhood nh JOIN edges e ON e.source = nh.id JOIN nodes next_n ON next_n.id = e.target "
|
|
23
23
|
+ "WHERE nh.depth < :max_depth "
|
|
24
24
|
+ "AND json_array_length(nh.edge_path) < (:max_depth + 1) "
|
|
25
25
|
+ "AND nh.id != next_n.id "
|
|
26
26
|
+ ") "
|
|
27
|
-
+ "SELECT id, type, edge_path, depth, edge_type_in, source_path, review_status, created_by, confidence, last_seen_at, "
|
|
27
|
+
+ "SELECT id, type, edge_path, depth, edge_type_in, source_path, review_status, created_by, created_at, confidence, last_seen_at, "
|
|
28
28
|
+ "( "
|
|
29
29
|
+ "CASE edge_type_in "
|
|
30
30
|
+ "WHEN 'CONTRADICTS' THEN 1.0 "
|
|
@@ -70,6 +70,7 @@ function getNeighborhood(db, focusNodeId, opts) {
|
|
|
70
70
|
sourcePath: r.source_path,
|
|
71
71
|
reviewStatus: r.review_status,
|
|
72
72
|
createdBy: r.created_by,
|
|
73
|
+
createdAt: r.created_at,
|
|
73
74
|
confidence: r.confidence,
|
|
74
75
|
lastSeenAt: r.last_seen_at,
|
|
75
76
|
}));
|