@mindrian_os/install 1.13.0-beta.13 → 1.13.0-beta.16
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 +21 -11
- 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 +1 -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 +1 -0
- 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 +1 -0
- 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 +1 -0
- 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/cache-prune.cjs +114 -8
- 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/install-state.cjs +242 -0
- package/lib/core/navigation/edges.cjs +86 -0
- package/lib/core/navigation/insights.cjs +37 -0
- package/lib/core/navigation/memory-events.cjs +39 -0
- package/lib/core/navigation/packet.cjs +89 -9
- package/lib/core/navigation/projections.cjs +201 -0
- package/lib/core/navigation.cjs +25 -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 +36 -0
- 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 +1 -1
- package/references/design/email-template-standard.md +1 -1
- package/references/user-research/2026-04-05-leah-lawrence-session.md +3 -3
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* lib/core/install-state.cjs -- install-state.json read / write / migrate
|
|
5
|
+
* module.
|
|
6
|
+
*
|
|
7
|
+
* Extracts the inline session-start read+write+derive logic into a focused
|
|
8
|
+
* single-purpose module; adds the v1 -> v2 schema migration with additive-only
|
|
9
|
+
* semantics + future-version detection + atomic-write crash safety.
|
|
10
|
+
*
|
|
11
|
+
* Phase 126 Plan 07. Canon Part 6 (dog-fooding: schema evolution surfaces only
|
|
12
|
+
* via shipped harness -- v1 was shipped in v1.13.0-beta.13). Canon Part 7
|
|
13
|
+
* (reuse: extracts inline session-start write into a module; does NOT
|
|
14
|
+
* re-architect the write path).
|
|
15
|
+
*
|
|
16
|
+
* The wire shape evolves through a single integer sentinel (`schema_version`).
|
|
17
|
+
* Per CONTEXT.md D3 (LOCKED): integer not semver string (simpler comparison
|
|
18
|
+
* for additive-only migrations). Per Open Question 5 settlement: `===` integer
|
|
19
|
+
* equality is the comparison.
|
|
20
|
+
*
|
|
21
|
+
* The v1 -> v2 additive-only mapping (CONTEXT.md D3 + Plan 07 must_haves):
|
|
22
|
+
*
|
|
23
|
+
* v1 file (Phase 123, NO schema_version):
|
|
24
|
+
* {
|
|
25
|
+
* active_root, active_version, topology, installed_at,
|
|
26
|
+
* snapshot, surfaces?, resolved_at?, ...
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* v2 file (Phase 126):
|
|
30
|
+
* v1-fields-preserved-byte-identical +
|
|
31
|
+
* {
|
|
32
|
+
* schema_version: 2,
|
|
33
|
+
* topology_class: derived from topology (see deriveTopologyClass below),
|
|
34
|
+
* last_acceptance_run: null, // Plan 03 + Plan 05 fill this
|
|
35
|
+
* renderer_contract_version: 'unknown' // Plan 01 sets this
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* Future-version detection: schema_version > 2 -> warn on stderr + DEFER to
|
|
39
|
+
* /mos:doctor --fix. Do NOT downgrade. Do NOT touch the file. Return a typed
|
|
40
|
+
* sentinel so callers can decide.
|
|
41
|
+
*
|
|
42
|
+
* Atomic write: write to `<path>.tmp`, fsync, rename. A crash between write
|
|
43
|
+
* and rename leaves the original file untouched. The MOS_TEST_FORCE_FAIL=rename
|
|
44
|
+
* env-var hook mirrors scripts/doctor.cjs lines ~304-307 -- it injects a throw
|
|
45
|
+
* at the rename moment so the crash-recovery contract is testable.
|
|
46
|
+
*
|
|
47
|
+
* Canon Part 8: this module reads + writes LOCAL files only ($HOME/.mindrian/).
|
|
48
|
+
* Zero network calls. Zero Brain queries. Zero side-channel to remote services.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
const fs = require('node:fs');
|
|
52
|
+
const path = require('node:path');
|
|
53
|
+
|
|
54
|
+
const SCHEMA_VERSION = 2;
|
|
55
|
+
const STATE_FILE_PATH_REL = path.join('.mindrian', 'install-state.json');
|
|
56
|
+
|
|
57
|
+
function statePath(home) {
|
|
58
|
+
return path.join(home, STATE_FILE_PATH_REL);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* readInstallState({ home }) -> object | null
|
|
63
|
+
*
|
|
64
|
+
* Returns the parsed JSON object, or null when the file is absent OR
|
|
65
|
+
* unparseable. Never throws -- the goal is a robust read for downstream
|
|
66
|
+
* consumers (doctor, session-start, the migrator itself). A corrupt file is
|
|
67
|
+
* surfaced as null so the caller can decide whether to derive-from-scratch
|
|
68
|
+
* (session-start's existing behavior) or surface a finding (doctor class I).
|
|
69
|
+
*/
|
|
70
|
+
function readInstallState(opts) {
|
|
71
|
+
const home = (opts && opts.home) || '';
|
|
72
|
+
if (!home) return null;
|
|
73
|
+
const p = statePath(home);
|
|
74
|
+
if (!fs.existsSync(p)) return null;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
77
|
+
} catch (_) {
|
|
78
|
+
// Corrupt file -> null (mirrors doctor.cjs's class-I "absent" finding).
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* writeInstallState({ home, state }) -> void
|
|
85
|
+
*
|
|
86
|
+
* Atomic write: $HOME/.mindrian/install-state.json.tmp -> fsync -> rename.
|
|
87
|
+
*
|
|
88
|
+
* The MOS_TEST_FORCE_FAIL=rename env injection mirrors the doctor.cjs
|
|
89
|
+
* pattern -- if set, throws AFTER the .tmp write but BEFORE the rename so
|
|
90
|
+
* tests can verify the original file is left untouched.
|
|
91
|
+
*
|
|
92
|
+
* Test-only side note: when MOS_TEST_FORCE_FAIL=rename, the .tmp file is left
|
|
93
|
+
* on disk; the rename never happens; the original target is untouched. A
|
|
94
|
+
* subsequent successful call will overwrite the .tmp via openSync('w') so
|
|
95
|
+
* stale .tmp files are not a long-term hazard.
|
|
96
|
+
*/
|
|
97
|
+
function writeInstallState(opts) {
|
|
98
|
+
const home = opts.home;
|
|
99
|
+
const state = opts.state;
|
|
100
|
+
const p = statePath(home);
|
|
101
|
+
const tmp = p + '.tmp';
|
|
102
|
+
// Ensure parent dir exists. Idempotent.
|
|
103
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
104
|
+
// Atomic write: write to tmp, fsync (best-effort), rename.
|
|
105
|
+
const fd = fs.openSync(tmp, 'w');
|
|
106
|
+
try {
|
|
107
|
+
fs.writeSync(fd, JSON.stringify(state, null, 2) + '\n');
|
|
108
|
+
try { fs.fsyncSync(fd); } catch (_) { /* fsync best-effort -- some FS lack support */ }
|
|
109
|
+
} finally {
|
|
110
|
+
fs.closeSync(fd);
|
|
111
|
+
}
|
|
112
|
+
// Test injection point (mirrors scripts/doctor.cjs MOS_TEST_FORCE_FAIL).
|
|
113
|
+
// Throw AFTER the .tmp write so the original target is byte-identical.
|
|
114
|
+
if (process.env.MOS_TEST_FORCE_FAIL === 'rename') {
|
|
115
|
+
throw new Error('MOS_TEST_FORCE_FAIL=rename injection (test-only)');
|
|
116
|
+
}
|
|
117
|
+
fs.renameSync(tmp, p);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* deriveTopologyClass(topology) -> 'healthy' | 'missing' | 'drifted'
|
|
122
|
+
*
|
|
123
|
+
* Canonical 4-state taxonomy from CONTEXT.md D3 + Plan 03 needs.
|
|
124
|
+
*
|
|
125
|
+
* Mapping (see PLAN.md Task 2 behavior block):
|
|
126
|
+
* marketplace-cache -> healthy (Claude Code's standard install path)
|
|
127
|
+
* direct -> healthy (npx round-trip install -- @mindrian_os/install)
|
|
128
|
+
* dev-clone -> healthy (developer machine -- a clone with a remote)
|
|
129
|
+
* not-found -> missing (resolver returned null root)
|
|
130
|
+
* legacy -> drifted (the legacy hand-clone path -- pre-Phase 123)
|
|
131
|
+
* <unknown> -> drifted (conservative default for any new topology
|
|
132
|
+
* introduced after this module's release)
|
|
133
|
+
*/
|
|
134
|
+
function deriveTopologyClass(topology) {
|
|
135
|
+
switch (topology) {
|
|
136
|
+
case 'marketplace-cache': return 'healthy';
|
|
137
|
+
case 'direct': return 'healthy';
|
|
138
|
+
case 'dev-clone': return 'healthy';
|
|
139
|
+
case 'not-found': return 'missing';
|
|
140
|
+
case 'legacy': return 'drifted';
|
|
141
|
+
default: return 'drifted';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* migrateIfNeeded({ home }) -> {
|
|
147
|
+
* migrated: boolean,
|
|
148
|
+
* // present in v1 -> v2 path:
|
|
149
|
+
* fromVersion?: 1, toVersion?: 2,
|
|
150
|
+
* // present in v2 path:
|
|
151
|
+
* currentVersion?: 2,
|
|
152
|
+
* // present in future-version path:
|
|
153
|
+
* futureVersion?: true, currentVersion?: number, advice?: string,
|
|
154
|
+
* // present in no-file path:
|
|
155
|
+
* fileAbsent?: true,
|
|
156
|
+
* }
|
|
157
|
+
*
|
|
158
|
+
* Behavior matrix:
|
|
159
|
+
*
|
|
160
|
+
* File absent
|
|
161
|
+
* -> { migrated:false, fileAbsent:true }
|
|
162
|
+
* -> Do NOT create the file (creation is session-start's job).
|
|
163
|
+
*
|
|
164
|
+
* File present + no schema_version (or schema_version absent/null)
|
|
165
|
+
* -> Treat as v1. Run additive migration. Write back. Return
|
|
166
|
+
* { migrated:true, fromVersion:1, toVersion:2 }.
|
|
167
|
+
*
|
|
168
|
+
* File present + schema_version === 2
|
|
169
|
+
* -> No-op. Return { migrated:false, currentVersion:2 }.
|
|
170
|
+
*
|
|
171
|
+
* File present + schema_version > 2 (a future-version file)
|
|
172
|
+
* -> Emit stderr warn. Do NOT touch the file. Return
|
|
173
|
+
* { migrated:false, futureVersion:true, currentVersion:<n>,
|
|
174
|
+
* advice:'run /mos:doctor --fix' }.
|
|
175
|
+
*
|
|
176
|
+
* File present + schema_version < 2 (unexpected lower-than-v1, e.g. 0)
|
|
177
|
+
* -> Treat as v1 (coerce). Same outcome as the v1 path.
|
|
178
|
+
*
|
|
179
|
+
* File present + corrupt JSON
|
|
180
|
+
* -> readInstallState returned null -> indistinguishable from absent at
|
|
181
|
+
* this layer -> { migrated:false, fileAbsent:true }. doctor class I
|
|
182
|
+
* is the surface that catches corrupt-but-present and offers --fix.
|
|
183
|
+
*/
|
|
184
|
+
function migrateIfNeeded(opts) {
|
|
185
|
+
const home = opts.home;
|
|
186
|
+
const state = readInstallState({ home });
|
|
187
|
+
if (!state) return { migrated: false, fileAbsent: true };
|
|
188
|
+
|
|
189
|
+
const sv = state.schema_version;
|
|
190
|
+
if (typeof sv === 'undefined' || sv === null) {
|
|
191
|
+
// v1 (or unmarked) -> v2 additive migration.
|
|
192
|
+
return _runV1ToV2({ home, state });
|
|
193
|
+
}
|
|
194
|
+
if (sv === SCHEMA_VERSION) {
|
|
195
|
+
return { migrated: false, currentVersion: SCHEMA_VERSION };
|
|
196
|
+
}
|
|
197
|
+
if (typeof sv === 'number' && sv > SCHEMA_VERSION) {
|
|
198
|
+
// Future-version: warn + defer. Do NOT downgrade. Do NOT touch the file.
|
|
199
|
+
process.stderr.write(
|
|
200
|
+
'[install-state] schema_version ' + sv + ' is newer than the plugin understands (' +
|
|
201
|
+
SCHEMA_VERSION + '). Skipping migration; run /mos:doctor --fix.\n'
|
|
202
|
+
);
|
|
203
|
+
return {
|
|
204
|
+
migrated: false,
|
|
205
|
+
futureVersion: true,
|
|
206
|
+
currentVersion: sv,
|
|
207
|
+
advice: 'run /mos:doctor --fix',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// Unexpected lower-than-v1 (e.g. schema_version: 0 or a non-number) -- coerce
|
|
211
|
+
// to v1 and run the additive migration. This is a safety net; not expected
|
|
212
|
+
// to fire in practice.
|
|
213
|
+
return _runV1ToV2({ home, state });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Internal: perform the v1 -> v2 additive migration + write.
|
|
217
|
+
function _runV1ToV2(opts) {
|
|
218
|
+
const home = opts.home;
|
|
219
|
+
const state = opts.state;
|
|
220
|
+
// Object.assign preserves v1 fields byte-identically; new fields are
|
|
221
|
+
// appended last (since v2's schema_version is the sentinel, the order
|
|
222
|
+
// could be reshuffled but additive-on-top is the simplest invariant).
|
|
223
|
+
const v2 = Object.assign({}, state, {
|
|
224
|
+
schema_version: SCHEMA_VERSION,
|
|
225
|
+
topology_class: deriveTopologyClass(state.topology),
|
|
226
|
+
last_acceptance_run: null,
|
|
227
|
+
renderer_contract_version: 'unknown',
|
|
228
|
+
});
|
|
229
|
+
writeInstallState({ home, state: v2 });
|
|
230
|
+
return { migrated: true, fromVersion: 1, toVersion: SCHEMA_VERSION };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = {
|
|
234
|
+
SCHEMA_VERSION,
|
|
235
|
+
readInstallState,
|
|
236
|
+
writeInstallState,
|
|
237
|
+
migrateIfNeeded,
|
|
238
|
+
deriveTopologyClass,
|
|
239
|
+
// Test-only helpers (intentionally exported with leading underscore so
|
|
240
|
+
// consumers do not depend on them and grep can spot test surface usage).
|
|
241
|
+
_statePath: statePath,
|
|
242
|
+
};
|
|
@@ -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
|
};
|
|
@@ -70,6 +70,45 @@ const EVENT_TYPES = Object.freeze(new Set([
|
|
|
70
70
|
'brain_packet_rejected',
|
|
71
71
|
'brain_response_rejected',
|
|
72
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',
|
|
73
112
|
]));
|
|
74
113
|
|
|
75
114
|
function isPlainObject(v) {
|
|
@@ -15,6 +15,14 @@ const crypto = require('node:crypto');
|
|
|
15
15
|
const { getNeighborhood } = require('./neighborhood.cjs');
|
|
16
16
|
const { findContradictions, findUnsupportedClaims, findRelevantOpportunities } = require('./insights.cjs');
|
|
17
17
|
const { findRecentChanges } = require('./memory-events.cjs');
|
|
18
|
+
// Phase 125-03 -- framework_chain_hint stitch. projections.cjs (Plan 01) gives
|
|
19
|
+
// us the active framework set + hop depth from roomState; framework-chain-slice
|
|
20
|
+
// (Plan 02) fetches the FEEDS_INTO Cypher slice from the Brain. Both modules
|
|
21
|
+
// are pure-or-graceful: projections never throws, slice fetcher returns a
|
|
22
|
+
// degraded envelope rather than throwing. Required at module top because the
|
|
23
|
+
// stitch is a hot path the helper does NOT lazy-import.
|
|
24
|
+
const projections = require('./projections.cjs');
|
|
25
|
+
const chainSliceMod = require('../../brain/framework-chain-slice.cjs');
|
|
18
26
|
|
|
19
27
|
// Phase 110-02 (D-03 + D-09): privacy-mode opt-up. local_summary_only is the default and
|
|
20
28
|
// the only mode any of the 12 shipped Brain jobs ever requests (every $def.in.properties.privacy_mode
|
|
@@ -178,7 +186,57 @@ function getFocusType(db, focusNodeId) {
|
|
|
178
186
|
return row ? row.type : null;
|
|
179
187
|
}
|
|
180
188
|
|
|
181
|
-
|
|
189
|
+
// Phase 125-03 -- stitch helper. Reads roomState via projections, fetches the
|
|
190
|
+
// 1-3 hop FEEDS_INTO slice via Plan 02 fetcher (or mock from opts._mocks),
|
|
191
|
+
// returns the framework_chain_hint shape per CONTEXT.md Scope IN section B
|
|
192
|
+
// item 5 -- OR undefined when the active set is empty (which is the "absent
|
|
193
|
+
// from packet" path per Plan 04 schema: framework_chain_hint is optional on
|
|
194
|
+
// LocalGraphSummary; absence is meaningful).
|
|
195
|
+
//
|
|
196
|
+
// Contract:
|
|
197
|
+
// - roomState null / not-an-object -> undefined (cold-start packet ships without hint)
|
|
198
|
+
// - active frameworks empty array -> undefined (no anchor to fetch a slice for)
|
|
199
|
+
// - active frameworks non-empty + Brain reachable -> hint object (5 fields)
|
|
200
|
+
// - active frameworks non-empty + Brain unreachable -> hint object with degraded
|
|
201
|
+
// shape per Plan 02 fetcher contract (edges:[], brain_snapshot_id:null,
|
|
202
|
+
// slice_rationale carrying 'brain_unreachable' or similar marker). The hint
|
|
203
|
+
// is STILL attached so the ranker can distinguish "active set non-empty but
|
|
204
|
+
// Brain failed" from "active set empty" (RESEARCH G-08 / Tier 0 invariant).
|
|
205
|
+
//
|
|
206
|
+
// Async because the fetcher is async. Pure read-only flow: no db writes, no fs
|
|
207
|
+
// writes, no Brain writes -- only the Plan 02 read primitive (Cypher MATCH).
|
|
208
|
+
async function _surfaceFrameworkChainHint(db, roomState, opts) {
|
|
209
|
+
if (!roomState || typeof roomState !== 'object') return undefined;
|
|
210
|
+
const active = projections.resolveActiveFrameworks(roomState);
|
|
211
|
+
if (!Array.isArray(active) || active.length === 0) return undefined;
|
|
212
|
+
const hopDepth = projections.resolveHopDepth(roomState);
|
|
213
|
+
const depth = (hopDepth && (hopDepth.depth === 1 || hopDepth.depth === 2 || hopDepth.depth === 3))
|
|
214
|
+
? hopDepth.depth
|
|
215
|
+
: 3;
|
|
216
|
+
const activeNames = active.map(function (a) { return a.name; });
|
|
217
|
+
// Test seam: opts._mocks.fetchFrameworkChainSlice replaces the live fetcher
|
|
218
|
+
// for hermetic tests (no Brain network). Falls through to the shipped Plan
|
|
219
|
+
// 02 module when no mock is provided.
|
|
220
|
+
const fetcher = (opts && opts._mocks && typeof opts._mocks.fetchFrameworkChainSlice === 'function')
|
|
221
|
+
? opts._mocks.fetchFrameworkChainSlice
|
|
222
|
+
: chainSliceMod.fetchFrameworkChainSlice;
|
|
223
|
+
// Per Plan 02 contract, fetcher NEVER throws; it returns a degraded envelope
|
|
224
|
+
// on any failure path (Brain unreachable, query throws, sanitizer rejects).
|
|
225
|
+
// Wrapping in try/catch is defensive belt-and-suspenders in case a future
|
|
226
|
+
// mock or replacement misbehaves.
|
|
227
|
+
let slice;
|
|
228
|
+
try {
|
|
229
|
+
slice = await fetcher({ active_frameworks: activeNames, max_hops: depth });
|
|
230
|
+
} catch (_e) {
|
|
231
|
+
// Defensive: even if the fetcher contract is violated, do NOT crash the
|
|
232
|
+
// packet builder. Return undefined -> hint absent for this call.
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
if (!slice || typeof slice !== 'object') return undefined;
|
|
236
|
+
return slice;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function buildBrainPacket(db, job, focusNodeId, opts) {
|
|
182
240
|
const options = opts || {};
|
|
183
241
|
const mocks = options._mocks;
|
|
184
242
|
const roomId = options.roomId || null;
|
|
@@ -217,6 +275,15 @@ function buildBrainPacket(db, job, focusNodeId, opts) {
|
|
|
217
275
|
const recentChanges = findRecentChanges(db, Date.now() - 24 * 60 * 60 * 1000, { limit: 10 }).map(safeRecentChangeProjection);
|
|
218
276
|
const bankedOpportunities = surface_banked_opportunities(db, focusNodeId, mocks);
|
|
219
277
|
|
|
278
|
+
// Phase 125-03: framework_chain_hint stitch. Reads roomState from opts (callers
|
|
279
|
+
// pass it through; Plan 05 ranker passes its own roomState when calling
|
|
280
|
+
// buildBrainPacket explicitly). When opts.roomState is absent OR resolves to an
|
|
281
|
+
// empty active framework set, the hint is ABSENT from the packet per the
|
|
282
|
+
// CONTEXT.md "absent when active set empty" invariant (Plan 04 schema accepts
|
|
283
|
+
// both shapes since framework_chain_hint is NOT in LocalGraphSummary.required[]).
|
|
284
|
+
const roomState = (options && typeof options === 'object') ? options.roomState : null;
|
|
285
|
+
const frameworkChainHint = await _surfaceFrameworkChainHint(db, roomState, options || {});
|
|
286
|
+
|
|
220
287
|
return {
|
|
221
288
|
packet_version: '1.0',
|
|
222
289
|
job,
|
|
@@ -240,14 +307,22 @@ function buildBrainPacket(db, job, focusNodeId, opts) {
|
|
|
240
307
|
summary: focusSummary,
|
|
241
308
|
},
|
|
242
309
|
},
|
|
243
|
-
local_graph_summary:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
310
|
+
local_graph_summary: Object.assign(
|
|
311
|
+
{
|
|
312
|
+
nearest_claims: nearestClaims,
|
|
313
|
+
nearest_assumptions: nearestAssumptions,
|
|
314
|
+
contradictions,
|
|
315
|
+
unsupported_claims: unsupportedClaims,
|
|
316
|
+
recent_changes: recentChanges,
|
|
317
|
+
banked_opportunities: bankedOpportunities,
|
|
318
|
+
},
|
|
319
|
+
// Phase 125-03: conditionally include framework_chain_hint. Object.assign
|
|
320
|
+
// with an empty-object short-circuit guarantees the key is ABSENT (not
|
|
321
|
+
// present with an undefined value) when frameworkChainHint is undefined.
|
|
322
|
+
// This preserves the schema invariant in Plan 04 (the field is optional;
|
|
323
|
+
// absence is meaningful for the ranker in Plan 05).
|
|
324
|
+
(frameworkChainHint === undefined) ? {} : { framework_chain_hint: frameworkChainHint }
|
|
325
|
+
),
|
|
251
326
|
constraints: {
|
|
252
327
|
privacy: 'no_raw_artifact_text',
|
|
253
328
|
max_tokens: 1200,
|
|
@@ -265,4 +340,9 @@ module.exports = {
|
|
|
265
340
|
readRoomConfigPrivacyMode,
|
|
266
341
|
roomHasExcerptApproval,
|
|
267
342
|
PRIVACY_MODES,
|
|
343
|
+
// Phase 125-03 test seam: surface helper exposed for fine-grained unit tests
|
|
344
|
+
// in lib/memory/packet-chain-hint.test.cjs. NOT part of the public API; Plan
|
|
345
|
+
// 05 ranker reads the assembled local_graph_summary.framework_chain_hint
|
|
346
|
+
// directly off the packet rather than calling the helper.
|
|
347
|
+
_test: { _surfaceFrameworkChainHint },
|
|
268
348
|
};
|