@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.
Files changed (123) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +57 -10
  3. package/README.md +74 -572
  4. package/commands/act.md +1 -0
  5. package/commands/admin.md +1 -0
  6. package/commands/analyze-needs.md +1 -0
  7. package/commands/analyze-systems.md +1 -0
  8. package/commands/analyze-timing.md +1 -0
  9. package/commands/auto-explore.md +2 -0
  10. package/commands/beautiful-question.md +1 -0
  11. package/commands/brain-derive.md +1 -0
  12. package/commands/build-knowledge.md +1 -0
  13. package/commands/build-thesis.md +1 -0
  14. package/commands/causal.md +1 -0
  15. package/commands/challenge-assumptions.md +1 -0
  16. package/commands/compare-ventures.md +1 -0
  17. package/commands/dashboard.md +1 -0
  18. package/commands/deep-grade.md +1 -0
  19. package/commands/diagnose.md +1 -0
  20. package/commands/diagnostics.md +1 -0
  21. package/commands/doctor.md +2 -1
  22. package/commands/dominant-designs.md +1 -0
  23. package/commands/explain-decision.md +1 -0
  24. package/commands/explore-domains.md +1 -0
  25. package/commands/explore-futures.md +1 -0
  26. package/commands/explore-trends.md +1 -0
  27. package/commands/export.md +1 -0
  28. package/commands/feynman-timeline-refresh.md +78 -0
  29. package/commands/file-meeting.md +1 -0
  30. package/commands/find-analogies.md +1 -0
  31. package/commands/find-bottlenecks.md +1 -0
  32. package/commands/find-connections.md +1 -0
  33. package/commands/funding.md +1 -0
  34. package/commands/grade.md +1 -0
  35. package/commands/graph.md +1 -0
  36. package/commands/hat-briefing.md +1 -0
  37. package/commands/heal.md +1 -0
  38. package/commands/help.md +1 -0
  39. package/commands/hmi-status.md +1 -0
  40. package/commands/jtbd.md +1 -0
  41. package/commands/leadership.md +1 -0
  42. package/commands/lean-canvas.md +1 -0
  43. package/commands/macro-trends.md +1 -0
  44. package/commands/map-unknowns.md +1 -0
  45. package/commands/memory.md +1 -0
  46. package/commands/models.md +1 -0
  47. package/commands/mos-reason.md +1 -0
  48. package/commands/mullins.md +1 -0
  49. package/commands/new-project.md +1 -0
  50. package/commands/onboard.md +1 -0
  51. package/commands/operator.md +2 -1
  52. package/commands/opportunities.md +1 -0
  53. package/commands/organize.md +1 -0
  54. package/commands/persona.md +1 -0
  55. package/commands/pipeline.md +1 -0
  56. package/commands/present.md +1 -0
  57. package/commands/publish.md +1 -0
  58. package/commands/query.md +1 -0
  59. package/commands/radar.md +1 -0
  60. package/commands/reanalyze.md +1 -0
  61. package/commands/research.md +1 -0
  62. package/commands/room.md +1 -0
  63. package/commands/rooms.md +1 -0
  64. package/commands/root-cause.md +1 -0
  65. package/commands/rs-experts.md +1 -0
  66. package/commands/rs-explain.md +1 -0
  67. package/commands/rs-fetch.md +1 -0
  68. package/commands/rs-thesis.md +1 -0
  69. package/commands/scenario-plan.md +1 -0
  70. package/commands/scheduled-tasks.md +1 -0
  71. package/commands/score-innovation.md +1 -0
  72. package/commands/scout.md +1 -0
  73. package/commands/setup.md +8 -3
  74. package/commands/snapshot.md +1 -0
  75. package/commands/speakers.md +1 -0
  76. package/commands/splash.md +1 -0
  77. package/commands/status.md +1 -0
  78. package/commands/structure-argument.md +1 -0
  79. package/commands/suggest-next.md +1 -0
  80. package/commands/systems-thinking.md +1 -0
  81. package/commands/think-hats.md +1 -0
  82. package/commands/update.md +1 -0
  83. package/commands/user-needs.md +1 -0
  84. package/commands/validate.md +1 -0
  85. package/commands/value-proposition.md +1 -0
  86. package/commands/vault.md +1 -0
  87. package/commands/visualize.md +1 -0
  88. package/commands/whitespace.md +1 -0
  89. package/commands/wiki.md +1 -0
  90. package/lib/brain/framework-chain-slice.cjs +193 -0
  91. package/lib/core/active-plugin-root.cjs +71 -6
  92. package/lib/core/brain-client.cjs +451 -36
  93. package/lib/core/cache-prune.cjs +208 -0
  94. package/lib/core/feynman/ROOM.md +25 -0
  95. package/lib/core/feynman/timeline-renderer.cjs +197 -0
  96. package/lib/core/feynman/timeline-runner.cjs +281 -0
  97. package/lib/core/navigation/edges.cjs +86 -0
  98. package/lib/core/navigation/insights.cjs +37 -0
  99. package/lib/core/navigation/memory-events.cjs +56 -1
  100. package/lib/core/navigation/neighborhood.cjs +5 -4
  101. package/lib/core/navigation/packet.cjs +176 -10
  102. package/lib/core/navigation/projections.cjs +201 -0
  103. package/lib/core/navigation.cjs +31 -0
  104. package/lib/core/resolve-brain-key.cjs +201 -0
  105. package/lib/mcp/larry-server-instructions.md +1 -1
  106. package/lib/memory/brain-cypher-chain-slice.test.cjs +368 -0
  107. package/lib/memory/f-selector-ranker.test.cjs +593 -0
  108. package/lib/memory/navigation-projections.test.cjs +241 -0
  109. package/lib/memory/navigation-write-edge.test.cjs +206 -0
  110. package/lib/memory/packet-chain-hint.test.cjs +407 -0
  111. package/lib/memory/packet-schema-validation.test.cjs +317 -0
  112. package/lib/memory/per-command-jtbd-derivation.test.cjs +130 -0
  113. package/lib/memory/per-command-teaching.test.cjs +110 -0
  114. package/lib/memory/run-feynman-tests.cjs +121 -0
  115. package/lib/memory/security-trifecta.test.cjs +23 -6
  116. package/lib/memory/selector-decisions.test.cjs +417 -0
  117. package/lib/memory/selector-miss.test.cjs +290 -0
  118. package/lib/workflow/f-selector-ranker.cjs +420 -0
  119. package/lib/workflow/selector-decisions.cjs +368 -0
  120. package/package.json +4 -1
  121. package/references/design/email-template-standard.md +1 -1
  122. package/references/user-research/2026-04-05-leah-lawrence-session.md +3 -3
  123. package/skills/brain-connector/SKILL.md +9 -3
@@ -0,0 +1,407 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 125-03 -- buildBrainPacket framework_chain_hint stitch test suite.
5
+ *
6
+ * Covers the 10 behaviors from 125-03-PLAN.md Task 1:
7
+ * Test 1 active set non-empty -> hint present (all 5 fields)
8
+ * Test 2 active set empty -> hint ABSENT (Object.prototype.hasOwnProperty.call false)
9
+ * Test 3 existing 6 fields preserved in BOTH paths
10
+ * Test 4 slice_scope is integer 1|2|3 (matches resolveHopDepth output)
11
+ * Test 5 slice_rationale is non-empty string
12
+ * Test 6 Brain unreachable -> hint still present (degraded shape; rationale carries marker)
13
+ * Test 7 no roomState -> hint ABSENT
14
+ * Test 8 mocked fetcher invoked exactly once per buildBrainPacket call
15
+ * Test 9 packet.packet_version remains '1.0' (Phase 110 invariant)
16
+ * Test 10 no regression -- existing buildBrainPacket fields still present + populated
17
+ *
18
+ * Three-surface compatibility: pure CJS + node built-ins + node:test. The
19
+ * fixture uses fs.mkdtempSync + openRoomDb (no Brain network, no Cypher,
20
+ * just a hermetic local SQLite room). The framework-chain-slice fetcher is
21
+ * mocked via opts._mocks.fetchFrameworkChainSlice -- no live brain-client
22
+ * calls happen during this test.
23
+ */
24
+
25
+ 'use strict';
26
+
27
+ const test = require('node:test');
28
+ const assert = require('node:assert');
29
+ const fs = require('node:fs');
30
+ const os = require('node:os');
31
+ const path = require('node:path');
32
+
33
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
34
+ const { openRoomDb } = require(path.join(REPO_ROOT, 'lib', 'core', 'room-db.cjs'));
35
+ const navigation = require(path.join(REPO_ROOT, 'lib', 'core', 'navigation.cjs'));
36
+ const packet = require(path.join(REPO_ROOT, 'lib', 'core', 'navigation', 'packet.cjs'));
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Hermetic fixture: a local SQLite room with the focus + a couple neighbors.
40
+ // Mirrors the makeRoom() shape in tests/test-navigation-packet-builder.cjs,
41
+ // pared down to the minimum needed to exercise local_graph_summary stitching.
42
+ // ---------------------------------------------------------------------------
43
+ function makeRoom() {
44
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'phase-125-03-pkt-'));
45
+ fs.mkdirSync(path.join(tmp, '.mindrian'), { recursive: true });
46
+ const db = openRoomDb(tmp);
47
+ const nowMs = Date.now();
48
+ const insN = db.prepare(
49
+ "INSERT OR IGNORE INTO nodes (id, type, properties, source_path, created_by, confidence, review_status, created_at, last_seen_at, source_section) VALUES (?, ?, ?, ?, 'user', ?, ?, ?, ?, ?)"
50
+ );
51
+ insN.run('room:test', 'room', '{}', 'fixture', 1.0, 'confirmed', nowMs, nowMs, null);
52
+ insN.run('decision:focus', 'decision', JSON.stringify({ summary: 'pick a framework' }),
53
+ 'fixture/d.md', 0.7, 'confirmed', nowMs, nowMs, 'design');
54
+ insN.run('claim:c1', 'claim', JSON.stringify({ summary: 'claim 1' }),
55
+ 'fixture/c1.md', 0.6, 'confirmed', nowMs, nowMs, 'design');
56
+ insN.run('assumption:a1', 'assumption', JSON.stringify({ claim: 'risky' }),
57
+ 'fixture/a1.md', 0.4, 'proposed', nowMs, nowMs, 'design');
58
+ const insE = db.prepare("INSERT OR IGNORE INTO edges (source, target, type, properties) VALUES (?, ?, ?, '{}')");
59
+ insE.run('decision:focus', 'claim:c1', 'INFORMS');
60
+ insE.run('decision:focus', 'assumption:a1', 'ASSUMES');
61
+ return { tmp, db };
62
+ }
63
+
64
+ function cleanup(tmp) {
65
+ try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (_) { /* ignore */ }
66
+ }
67
+
68
+ function defaultMocks() {
69
+ return {
70
+ jtbd: { getCurrent: () => ({ current: null }) },
71
+ operator: { getCurrent: () => ({ current: null }) },
72
+ };
73
+ }
74
+
75
+ // A mock fetcher that returns a well-formed hint envelope. Matches the
76
+ // contract from lib/brain/framework-chain-slice.cjs::fetchFrameworkChainSlice
77
+ // (Plan 02 shipped).
78
+ function makeMockFetcher(opts) {
79
+ const o = opts || {};
80
+ const calls = [];
81
+ const slice_scope = (o.slice_scope === 1 || o.slice_scope === 2 || o.slice_scope === 3) ? o.slice_scope : 2;
82
+ const fetcher = async function (args) {
83
+ calls.push(args);
84
+ if (o.degraded) {
85
+ return {
86
+ edges: [],
87
+ slice_scope: slice_scope,
88
+ slice_rationale: 'brain_unreachable',
89
+ brain_snapshot_id: null,
90
+ fetched_at: new Date().toISOString(),
91
+ };
92
+ }
93
+ return {
94
+ edges: [
95
+ { from: 'SWOT', to: 'Porter Five Forces', confidence: 0.8, transform_description: 'natural follow-on', hop_distance: 1 },
96
+ { from: 'SWOT', to: 'Lean Canvas', confidence: 0.65, transform_description: null, hop_distance: 1 },
97
+ ],
98
+ slice_scope: slice_scope,
99
+ slice_rationale: 'brain_reachable; 2 edges fetched; 1 active_frameworks; max_hops=' + slice_scope,
100
+ brain_snapshot_id: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd01',
101
+ fetched_at: new Date().toISOString(),
102
+ };
103
+ };
104
+ fetcher.calls = calls;
105
+ return fetcher;
106
+ }
107
+
108
+ // roomState that resolves a single governing_thought-driven active framework
109
+ // (matches Plan 01's KNOWN_FRAMEWORK_HINTS list -- "SWOT" is one of the 16).
110
+ function activeRoomState() {
111
+ return {
112
+ problemType: 'WDP',
113
+ governing_thought: 'Run SWOT first to map the position',
114
+ framework_invocations: 3,
115
+ activeJtbd: null,
116
+ brainAnchors: [],
117
+ recentFrameworks: [],
118
+ };
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Test 1 -- active set non-empty -> hint present (all 5 fields)
123
+ // ---------------------------------------------------------------------------
124
+ test('Test 1: hint present with all 5 fields when active set non-empty', async () => {
125
+ const { tmp, db } = makeRoom();
126
+ try {
127
+ const fetcher = makeMockFetcher();
128
+ const p = await navigation.buildBrainPacket(db, 'suggest_next_move', 'decision:focus', {
129
+ _mocks: Object.assign({}, defaultMocks(), { fetchFrameworkChainSlice: fetcher }),
130
+ roomId: 'test',
131
+ roomState: activeRoomState(),
132
+ });
133
+ assert.ok(Object.prototype.hasOwnProperty.call(p.local_graph_summary, 'framework_chain_hint'),
134
+ 'framework_chain_hint key is present');
135
+ const hint = p.local_graph_summary.framework_chain_hint;
136
+ assert.ok(hint && typeof hint === 'object', 'hint is a non-null object');
137
+ for (const k of ['edges', 'slice_scope', 'slice_rationale', 'brain_snapshot_id', 'fetched_at']) {
138
+ assert.ok(Object.prototype.hasOwnProperty.call(hint, k), 'hint has key: ' + k);
139
+ }
140
+ assert.ok(Array.isArray(hint.edges), 'edges is an array');
141
+ db.close();
142
+ } finally { cleanup(tmp); }
143
+ });
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Test 2 -- active set empty -> hint ABSENT (verified via hasOwnProperty)
147
+ // ---------------------------------------------------------------------------
148
+ test('Test 2: hint ABSENT when active set empty (no governing_thought, no anchors, no JTBD)', async () => {
149
+ const { tmp, db } = makeRoom();
150
+ try {
151
+ const fetcher = makeMockFetcher();
152
+ const p = await navigation.buildBrainPacket(db, 'suggest_next_move', 'decision:focus', {
153
+ _mocks: Object.assign({}, defaultMocks(), { fetchFrameworkChainSlice: fetcher }),
154
+ roomId: 'test',
155
+ roomState: {
156
+ problemType: 'WDP',
157
+ governing_thought: '',
158
+ activeJtbd: null,
159
+ brainAnchors: [],
160
+ recentFrameworks: [],
161
+ framework_invocations: 0,
162
+ },
163
+ });
164
+ assert.strictEqual(
165
+ Object.prototype.hasOwnProperty.call(p.local_graph_summary, 'framework_chain_hint'),
166
+ false,
167
+ 'framework_chain_hint key must be ABSENT when active set is empty'
168
+ );
169
+ // Fetcher must NOT have been called (no active set means short-circuit).
170
+ assert.strictEqual(fetcher.calls.length, 0, 'fetcher not called when active set empty');
171
+ db.close();
172
+ } finally { cleanup(tmp); }
173
+ });
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Test 3 -- existing 6 fields preserved in BOTH paths
177
+ // ---------------------------------------------------------------------------
178
+ test('Test 3: existing 6 local_graph_summary fields preserved (active set non-empty)', async () => {
179
+ const { tmp, db } = makeRoom();
180
+ try {
181
+ const fetcher = makeMockFetcher();
182
+ const p = await navigation.buildBrainPacket(db, 'suggest_next_move', 'decision:focus', {
183
+ _mocks: Object.assign({}, defaultMocks(), { fetchFrameworkChainSlice: fetcher }),
184
+ roomId: 'test',
185
+ roomState: activeRoomState(),
186
+ });
187
+ const lgs = p.local_graph_summary;
188
+ for (const k of ['nearest_claims', 'nearest_assumptions', 'contradictions',
189
+ 'unsupported_claims', 'recent_changes', 'banked_opportunities']) {
190
+ assert.ok(Object.prototype.hasOwnProperty.call(lgs, k), 'existing field still present: ' + k);
191
+ }
192
+ assert.ok(Array.isArray(lgs.nearest_claims), 'nearest_claims is an array');
193
+ assert.ok(Array.isArray(lgs.nearest_assumptions), 'nearest_assumptions is an array');
194
+ assert.ok(Array.isArray(lgs.contradictions), 'contradictions is an array');
195
+ assert.ok(Array.isArray(lgs.unsupported_claims), 'unsupported_claims is an array');
196
+ assert.ok(Array.isArray(lgs.recent_changes), 'recent_changes is an array');
197
+ assert.ok(typeof lgs.banked_opportunities === 'object', 'banked_opportunities is an object');
198
+ db.close();
199
+ } finally { cleanup(tmp); }
200
+ });
201
+
202
+ test('Test 3b: existing 6 local_graph_summary fields preserved (active set empty)', async () => {
203
+ const { tmp, db } = makeRoom();
204
+ try {
205
+ const p = await navigation.buildBrainPacket(db, 'suggest_next_move', 'decision:focus', {
206
+ _mocks: defaultMocks(),
207
+ roomId: 'test',
208
+ });
209
+ const lgs = p.local_graph_summary;
210
+ for (const k of ['nearest_claims', 'nearest_assumptions', 'contradictions',
211
+ 'unsupported_claims', 'recent_changes', 'banked_opportunities']) {
212
+ assert.ok(Object.prototype.hasOwnProperty.call(lgs, k), 'existing field still present: ' + k);
213
+ }
214
+ db.close();
215
+ } finally { cleanup(tmp); }
216
+ });
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Test 4 -- slice_scope is integer 1|2|3 (matches resolveHopDepth)
220
+ // ---------------------------------------------------------------------------
221
+ test('Test 4: slice_scope is an integer in {1, 2, 3} and matches resolveHopDepth', async () => {
222
+ const { tmp, db } = makeRoom();
223
+ try {
224
+ // WDP + governing_thought -> depth 1 per resolveHopDepth (Plan 01).
225
+ const fetcher = makeMockFetcher({ slice_scope: 1 });
226
+ const p = await navigation.buildBrainPacket(db, 'suggest_next_move', 'decision:focus', {
227
+ _mocks: Object.assign({}, defaultMocks(), { fetchFrameworkChainSlice: fetcher }),
228
+ roomId: 'test',
229
+ roomState: activeRoomState(),
230
+ });
231
+ const hint = p.local_graph_summary.framework_chain_hint;
232
+ assert.strictEqual(typeof hint.slice_scope, 'number', 'slice_scope is a number');
233
+ assert.ok([1, 2, 3].includes(hint.slice_scope), 'slice_scope in {1,2,3}; got ' + hint.slice_scope);
234
+ // The mock returned 1 only because the fetcher honors the max_hops arg
235
+ // it received from the helper. Verify the helper called fetcher with the
236
+ // depth the projection resolved (WDP + governing_thought -> 1).
237
+ assert.strictEqual(fetcher.calls.length, 1, 'fetcher invoked exactly once');
238
+ assert.strictEqual(fetcher.calls[0].max_hops, 1,
239
+ 'fetcher received max_hops=1 (WDP + governing_thought -> depth 1)');
240
+ db.close();
241
+ } finally { cleanup(tmp); }
242
+ });
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // Test 5 -- slice_rationale is non-empty string
246
+ // ---------------------------------------------------------------------------
247
+ test('Test 5: slice_rationale is a non-empty string when hint present', async () => {
248
+ const { tmp, db } = makeRoom();
249
+ try {
250
+ const fetcher = makeMockFetcher();
251
+ const p = await navigation.buildBrainPacket(db, 'suggest_next_move', 'decision:focus', {
252
+ _mocks: Object.assign({}, defaultMocks(), { fetchFrameworkChainSlice: fetcher }),
253
+ roomId: 'test',
254
+ roomState: activeRoomState(),
255
+ });
256
+ const hint = p.local_graph_summary.framework_chain_hint;
257
+ assert.strictEqual(typeof hint.slice_rationale, 'string', 'slice_rationale is a string');
258
+ assert.ok(hint.slice_rationale.length > 0, 'slice_rationale is non-empty');
259
+ db.close();
260
+ } finally { cleanup(tmp); }
261
+ });
262
+
263
+ // ---------------------------------------------------------------------------
264
+ // Test 6 -- Brain unreachable -> hint STILL PRESENT (degraded shape;
265
+ // distinguishes "active set non-empty but Brain failed" from "active set empty")
266
+ // ---------------------------------------------------------------------------
267
+ test('Test 6: Brain unreachable -> hint still attached with degraded shape (rationale carries marker)', async () => {
268
+ const { tmp, db } = makeRoom();
269
+ try {
270
+ const fetcher = makeMockFetcher({ degraded: true });
271
+ const p = await navigation.buildBrainPacket(db, 'suggest_next_move', 'decision:focus', {
272
+ _mocks: Object.assign({}, defaultMocks(), { fetchFrameworkChainSlice: fetcher }),
273
+ roomId: 'test',
274
+ roomState: activeRoomState(),
275
+ });
276
+ assert.ok(Object.prototype.hasOwnProperty.call(p.local_graph_summary, 'framework_chain_hint'),
277
+ 'hint still attached even when Brain returned degraded envelope');
278
+ const hint = p.local_graph_summary.framework_chain_hint;
279
+ assert.ok(Array.isArray(hint.edges), 'edges is still an array');
280
+ assert.strictEqual(hint.edges.length, 0, 'degraded edges is empty array');
281
+ assert.strictEqual(hint.brain_snapshot_id, null, 'degraded brain_snapshot_id is null');
282
+ assert.ok(typeof hint.slice_rationale === 'string' && hint.slice_rationale.indexOf('brain_unreachable') !== -1,
283
+ 'slice_rationale carries brain_unreachable marker: got "' + hint.slice_rationale + '"');
284
+ db.close();
285
+ } finally { cleanup(tmp); }
286
+ });
287
+
288
+ // ---------------------------------------------------------------------------
289
+ // Test 7 -- no roomState -> hint ABSENT
290
+ // ---------------------------------------------------------------------------
291
+ test('Test 7: no roomState in opts -> hint ABSENT', async () => {
292
+ const { tmp, db } = makeRoom();
293
+ try {
294
+ const p = await navigation.buildBrainPacket(db, 'suggest_next_move', 'decision:focus', {
295
+ _mocks: defaultMocks(),
296
+ roomId: 'test',
297
+ // No roomState key at all.
298
+ });
299
+ assert.strictEqual(
300
+ Object.prototype.hasOwnProperty.call(p.local_graph_summary, 'framework_chain_hint'),
301
+ false,
302
+ 'hint absent when opts.roomState is undefined'
303
+ );
304
+ db.close();
305
+ } finally { cleanup(tmp); }
306
+ });
307
+
308
+ // ---------------------------------------------------------------------------
309
+ // Test 8 -- mocked fetcher invoked exactly once per buildBrainPacket call
310
+ // ---------------------------------------------------------------------------
311
+ test('Test 8: opts._mocks.fetchFrameworkChainSlice is invoked exactly once', async () => {
312
+ const { tmp, db } = makeRoom();
313
+ try {
314
+ const fetcher = makeMockFetcher();
315
+ await navigation.buildBrainPacket(db, 'suggest_next_move', 'decision:focus', {
316
+ _mocks: Object.assign({}, defaultMocks(), { fetchFrameworkChainSlice: fetcher }),
317
+ roomId: 'test',
318
+ roomState: activeRoomState(),
319
+ });
320
+ assert.strictEqual(fetcher.calls.length, 1, 'fetcher invoked exactly once (not 0, not 2)');
321
+ // Verify the fetcher receives the active framework names + max_hops.
322
+ const args = fetcher.calls[0];
323
+ assert.ok(Array.isArray(args.active_frameworks), 'fetcher.active_frameworks is an array');
324
+ assert.ok(args.active_frameworks.length > 0, 'fetcher.active_frameworks non-empty');
325
+ assert.ok([1, 2, 3].includes(args.max_hops), 'fetcher.max_hops in {1,2,3}; got ' + args.max_hops);
326
+ db.close();
327
+ } finally { cleanup(tmp); }
328
+ });
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Test 9 -- packet.packet_version remains '1.0' (Phase 110 invariant)
332
+ // ---------------------------------------------------------------------------
333
+ test('Test 9: packet_version remains "1.0" with and without hint', async () => {
334
+ const { tmp, db } = makeRoom();
335
+ try {
336
+ const fetcher = makeMockFetcher();
337
+ const pWithHint = await navigation.buildBrainPacket(db, 'suggest_next_move', 'decision:focus', {
338
+ _mocks: Object.assign({}, defaultMocks(), { fetchFrameworkChainSlice: fetcher }),
339
+ roomId: 'test',
340
+ roomState: activeRoomState(),
341
+ });
342
+ assert.strictEqual(pWithHint.packet_version, '1.0', 'packet_version is "1.0" with hint');
343
+ const pNoHint = await navigation.buildBrainPacket(db, 'suggest_next_move', 'decision:focus', {
344
+ _mocks: defaultMocks(),
345
+ roomId: 'test',
346
+ });
347
+ assert.strictEqual(pNoHint.packet_version, '1.0', 'packet_version is "1.0" without hint');
348
+ db.close();
349
+ } finally { cleanup(tmp); }
350
+ });
351
+
352
+ // ---------------------------------------------------------------------------
353
+ // Test 10 -- _surfaceFrameworkChainHint test seam is exported + behaves as
354
+ // the helper invariant requires (no roomState -> undefined; empty active ->
355
+ // undefined; non-empty active -> hint object).
356
+ // ---------------------------------------------------------------------------
357
+ test('Test 10: _surfaceFrameworkChainHint test seam -- no roomState returns undefined', async () => {
358
+ assert.ok(packet._test && typeof packet._test._surfaceFrameworkChainHint === 'function',
359
+ 'packet._test._surfaceFrameworkChainHint is exported');
360
+ const out = await packet._test._surfaceFrameworkChainHint(null, null, {});
361
+ assert.strictEqual(out, undefined, 'undefined when roomState is null');
362
+ });
363
+
364
+ test('Test 10b: _surfaceFrameworkChainHint -- empty active set returns undefined', async () => {
365
+ const out = await packet._test._surfaceFrameworkChainHint(null, {
366
+ problemType: 'WDP',
367
+ governing_thought: '',
368
+ activeJtbd: null,
369
+ brainAnchors: [],
370
+ recentFrameworks: [],
371
+ }, { _mocks: { fetchFrameworkChainSlice: makeMockFetcher() } });
372
+ assert.strictEqual(out, undefined, 'undefined when active set resolves empty');
373
+ });
374
+
375
+ test('Test 10c: _surfaceFrameworkChainHint -- non-empty active set returns hint object', async () => {
376
+ const fetcher = makeMockFetcher();
377
+ const out = await packet._test._surfaceFrameworkChainHint(null, activeRoomState(), {
378
+ _mocks: { fetchFrameworkChainSlice: fetcher },
379
+ });
380
+ assert.ok(out && typeof out === 'object', 'returns a hint object');
381
+ for (const k of ['edges', 'slice_scope', 'slice_rationale', 'brain_snapshot_id', 'fetched_at']) {
382
+ assert.ok(Object.prototype.hasOwnProperty.call(out, k), 'hint has key: ' + k);
383
+ }
384
+ assert.strictEqual(fetcher.calls.length, 1, 'fetcher invoked exactly once by the helper');
385
+ });
386
+
387
+ // ---------------------------------------------------------------------------
388
+ // Test 11 -- regression floor: a hint-bearing packet retains all 6 existing
389
+ // fields with non-empty arrays where the fixture seeds them (nearest_claims,
390
+ // nearest_assumptions, banked_opportunities.count >= 0).
391
+ // ---------------------------------------------------------------------------
392
+ test('Test 11: regression -- nearest_claims and assumptions populated even with hint present', async () => {
393
+ const { tmp, db } = makeRoom();
394
+ try {
395
+ const fetcher = makeMockFetcher();
396
+ const p = await navigation.buildBrainPacket(db, 'suggest_next_move', 'decision:focus', {
397
+ _mocks: Object.assign({}, defaultMocks(), { fetchFrameworkChainSlice: fetcher }),
398
+ roomId: 'test',
399
+ roomState: activeRoomState(),
400
+ });
401
+ const lgs = p.local_graph_summary;
402
+ assert.ok(lgs.nearest_claims.length >= 1, 'nearest_claims populated (got ' + lgs.nearest_claims.length + ')');
403
+ assert.ok(lgs.nearest_assumptions.length >= 1, 'nearest_assumptions populated (got ' + lgs.nearest_assumptions.length + ')');
404
+ assert.strictEqual(typeof lgs.banked_opportunities.count, 'number', 'banked_opportunities.count is a number');
405
+ db.close();
406
+ } finally { cleanup(tmp); }
407
+ });