@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,593 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 125-05 -- lib/workflow/f-selector-ranker.cjs test suite.
5
+ *
6
+ * Covers the 26 acceptance behaviors from 125-05-PLAN.md <behavior> block:
7
+ * Tests 1-10 Ranker continuous gradient (CONTEXT.md "Ranker continuous gradient")
8
+ * Tests 11-14 Why content scaling (D9)
9
+ * Tests 15-19 Visible investment feedback (D5 + D8)
10
+ * Tests 20-21 Continuation callable (D10)
11
+ * Tests 22-24 Teaching field read (D11)
12
+ * Tests 25-26 Cold-start / Tier 0 (RESEARCH G-08)
13
+ *
14
+ * Three-surface compatibility: pure CJS + node:test. No db / no fs writes /
15
+ * no Brain / no network. Tests use _test._setRegistry to inject deterministic
16
+ * registry fixtures so the suite never depends on disk state.
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const test = require('node:test');
22
+ const assert = require('node:assert');
23
+ const path = require('node:path');
24
+
25
+ const RANKER_PATH = path.resolve(__dirname, '..', 'workflow', 'f-selector-ranker.cjs');
26
+
27
+ function load() {
28
+ delete require.cache[require.resolve(RANKER_PATH)];
29
+ return require(RANKER_PATH);
30
+ }
31
+
32
+ // Deterministic registry fixture (4 commands; 2 eligible, 2 missing-content).
33
+ const FAKE_REGISTRY = {
34
+ commands: [
35
+ {
36
+ command: '/mos:beautiful-question',
37
+ kind: 'methodology',
38
+ frameworks: ['Beautiful Question Framework'],
39
+ serves_jtbd: ['find-problem', 'explore'],
40
+ jtbd_label: 'Find root problem',
41
+ jtbd_summary: 'Beautiful questions surface real problems under the symptom.',
42
+ teaching: 'When the team disagrees, beautiful questions widen the frame before narrowing it.',
43
+ autonomous_safe: true,
44
+ },
45
+ {
46
+ command: '/mos:swot',
47
+ kind: 'methodology',
48
+ frameworks: ['SWOT'],
49
+ serves_jtbd: ['validate-thesis'],
50
+ jtbd_label: 'Audit S/W/O/T',
51
+ jtbd_summary: 'SWOT structures internal vs external factors.',
52
+ teaching: 'SWOT is the first lens after JTBD because it binds qualitative factors to strategic posture.',
53
+ autonomous_safe: true,
54
+ },
55
+ {
56
+ command: '/mos:missing-teaching',
57
+ kind: 'methodology',
58
+ frameworks: ['X'],
59
+ serves_jtbd: ['y'],
60
+ jtbd_label: 'X',
61
+ jtbd_summary: 'has jtbd_summary',
62
+ // teaching MISSING
63
+ autonomous_safe: true,
64
+ },
65
+ {
66
+ command: '/mos:missing-summary',
67
+ kind: 'methodology',
68
+ frameworks: ['Y'],
69
+ serves_jtbd: ['z'],
70
+ jtbd_label: 'Y',
71
+ teaching: 'has teaching',
72
+ // jtbd_summary MISSING
73
+ autonomous_safe: true,
74
+ },
75
+ ],
76
+ };
77
+
78
+ function freshRanker() {
79
+ const r = load();
80
+ r._test._resetCaches();
81
+ r._test._setRegistry(FAKE_REGISTRY);
82
+ return r;
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Tests 1-10: Ranker continuous gradient (CONTEXT.md acceptance)
87
+ // ---------------------------------------------------------------------------
88
+
89
+ test('Test 1: investment_level 0 reduces to pure Brain confidence (other terms zeroed)', () => {
90
+ const r = freshRanker();
91
+ // No framework_invocations -> investment_level = 0.
92
+ // Provide a packet with known confidence for the beautiful-question framework.
93
+ const packet = {
94
+ local_graph_summary: {
95
+ framework_chain_hint: {
96
+ edges: [
97
+ { from: 'Beautiful Question Framework', to: 'SWOT', confidence: 0.9, hop_distance: 1 },
98
+ ],
99
+ },
100
+ },
101
+ };
102
+ const out = r.rankForSelector({ packetOptional: packet, roomState: {}, k: 3 });
103
+ assert.ok(out.length >= 1, 'returns at least one ranked command');
104
+ // At investment_level 0, score = brain_confidence * 0.40 / 0.40 = brain_confidence.
105
+ // For beautiful-question with framework match: score === 0.9 (within fp tolerance).
106
+ const bq = out.find((x) => x.command === '/mos:beautiful-question');
107
+ assert.ok(bq, 'beautiful-question included');
108
+ assert.strictEqual(bq.investment_level, 0);
109
+ assert.ok(Math.abs(bq.score - 0.9) < 0.001,
110
+ 'at investment_level 0, score equals brain_confidence (~0.9), got ' + bq.score);
111
+ });
112
+
113
+ test('Test 2: investment_level 1 uses full 3-signal formula at 40/30/30', () => {
114
+ const r = freshRanker();
115
+ const packet = {
116
+ local_graph_summary: {
117
+ framework_chain_hint: {
118
+ edges: [
119
+ { from: 'Beautiful Question Framework', to: 'SWOT', confidence: 0.8, hop_distance: 1 },
120
+ ],
121
+ },
122
+ },
123
+ };
124
+ const out = r.rankForSelector({
125
+ packetOptional: packet,
126
+ roomState: { framework_invocations: 10, problemType: 'UDP' },
127
+ k: 3,
128
+ });
129
+ const bq = out.find((x) => x.command === '/mos:beautiful-question');
130
+ assert.ok(bq);
131
+ assert.strictEqual(bq.investment_level, 1);
132
+ // problem_type_bind for UDP + Beautiful Question Framework = 1.0
133
+ // recency_decay defaults 0.5 -> (1-0.5) = 0.5
134
+ // num = 0.8*0.40 + 0.5*0.30*1 + 1.0*0.30*1 = 0.32 + 0.15 + 0.30 = 0.77
135
+ // den = 0.40 + 0.30 + 0.30 = 1.00
136
+ // score = 0.77
137
+ assert.ok(Math.abs(bq.score - 0.77) < 0.01,
138
+ 'full 3-signal score ~0.77; got ' + bq.score);
139
+ });
140
+
141
+ test('Test 3: scores normalize to 0..1 at every investment level', () => {
142
+ const r = freshRanker();
143
+ const packet = {
144
+ local_graph_summary: {
145
+ framework_chain_hint: {
146
+ edges: [
147
+ { from: 'Beautiful Question Framework', to: 'SWOT', confidence: 1.0, hop_distance: 1 },
148
+ ],
149
+ },
150
+ },
151
+ };
152
+ for (const inv of [0.0, 0.3, 0.5, 0.7, 1.0]) {
153
+ const out = r.rankForSelector({
154
+ packetOptional: packet,
155
+ roomState: { framework_invocations: Math.round(inv * 10), problemType: 'UDP' },
156
+ k: 3,
157
+ });
158
+ for (const item of out) {
159
+ assert.ok(item.score >= 0 && item.score <= 1,
160
+ 'score in [0,1] at inv=' + inv + ' got ' + item.score);
161
+ }
162
+ }
163
+ });
164
+
165
+ test('Test 4: no score discontinuity across the gradient (continuous)', () => {
166
+ const r = freshRanker();
167
+ const packet = {
168
+ local_graph_summary: {
169
+ framework_chain_hint: {
170
+ edges: [
171
+ { from: 'Beautiful Question Framework', to: 'SWOT', confidence: 0.6, hop_distance: 1 },
172
+ ],
173
+ },
174
+ },
175
+ };
176
+ // Rank twice at investment_level very close together (0.49 and 0.51 via 4.9 / 5.1).
177
+ // framework_invocations is /10 in computeInvestmentLevel.
178
+ const left = r.rankForSelector({
179
+ packetOptional: packet,
180
+ roomState: { framework_invocations: 4.9 },
181
+ k: 3,
182
+ });
183
+ const right = r.rankForSelector({
184
+ packetOptional: packet,
185
+ roomState: { framework_invocations: 5.1 },
186
+ k: 3,
187
+ });
188
+ for (let i = 0; i < Math.min(left.length, right.length); i++) {
189
+ const dl = Math.abs(left[i].score - right[i].score);
190
+ assert.ok(dl <= 0.1, 'score delta at adjacent inv must be small; got ' + dl);
191
+ }
192
+ });
193
+
194
+ test('Test 5: source field correctness (packet | chain | registry-only)', () => {
195
+ const r = freshRanker();
196
+ // packet present with edges -> source === 'packet'
197
+ const withPacket = r.rankForSelector({
198
+ packetOptional: {
199
+ local_graph_summary: {
200
+ framework_chain_hint: {
201
+ edges: [{ from: 'Beautiful Question Framework', to: 'SWOT', confidence: 0.5, hop_distance: 1 }],
202
+ },
203
+ },
204
+ },
205
+ roomState: {},
206
+ k: 3,
207
+ });
208
+ const bq1 = withPacket.find((x) => x.command === '/mos:beautiful-question');
209
+ assert.strictEqual(bq1.source, 'packet');
210
+
211
+ // no packet, command has frameworks -> 'chain'
212
+ const noPacket = r.rankForSelector({ roomState: {}, k: 3 });
213
+ const bq2 = noPacket.find((x) => x.command === '/mos:beautiful-question');
214
+ assert.strictEqual(bq2.source, 'chain');
215
+
216
+ // command without frameworks -> 'registry-only'
217
+ r._test._resetCaches();
218
+ r._test._setRegistry({
219
+ commands: [{
220
+ command: '/mos:bare',
221
+ kind: 'methodology',
222
+ frameworks: [],
223
+ serves_jtbd: ['x'],
224
+ jtbd_label: 'x', jtbd_summary: 'x', teaching: 'x',
225
+ }],
226
+ });
227
+ const bare = r.rankForSelector({ roomState: {}, k: 3 });
228
+ assert.strictEqual(bare.length, 1);
229
+ assert.strictEqual(bare[0].source, 'registry-only');
230
+ });
231
+
232
+ test('Test 6: commands missing jtbd_summary OR teaching are EXCLUDED (fail-closed)', () => {
233
+ const r = freshRanker();
234
+ const out = r.rankForSelector({ roomState: {}, k: 10 });
235
+ const slugs = out.map((x) => x.command);
236
+ assert.ok(slugs.indexOf('/mos:missing-teaching') === -1,
237
+ 'missing-teaching excluded');
238
+ assert.ok(slugs.indexOf('/mos:missing-summary') === -1,
239
+ 'missing-summary excluded');
240
+ assert.ok(slugs.indexOf('/mos:beautiful-question') !== -1, 'eligible included');
241
+ assert.ok(slugs.indexOf('/mos:swot') !== -1, 'eligible included');
242
+ });
243
+
244
+ test('Test 7: returns exactly k results when k eligible commands exist', () => {
245
+ const r = freshRanker();
246
+ // FAKE_REGISTRY has 2 eligible commands.
247
+ const out2 = r.rankForSelector({ roomState: {}, k: 2 });
248
+ assert.strictEqual(out2.length, 2);
249
+ const out1 = r.rankForSelector({ roomState: {}, k: 1 });
250
+ assert.strictEqual(out1.length, 1);
251
+ });
252
+
253
+ test('Test 8: returns fewer than k when fewer eligible commands exist', () => {
254
+ const r = freshRanker();
255
+ // 2 eligible commands; ask for 5.
256
+ const out = r.rankForSelector({ roomState: {}, k: 5 });
257
+ assert.strictEqual(out.length, 2,
258
+ 'returns 2 eligible commands when 5 requested; never pads with placeholders');
259
+ });
260
+
261
+ test('Test 9: synchronous -- no Promise return; no thenable', () => {
262
+ const r = freshRanker();
263
+ const out = r.rankForSelector({ roomState: {}, k: 3 });
264
+ assert.ok(Array.isArray(out), 'return value is an Array');
265
+ assert.strictEqual(typeof out.then, 'undefined',
266
+ 'return value is not a thenable / Promise');
267
+ });
268
+
269
+ test('Test 10: no Brain calls; no memory_event writes during ranking', () => {
270
+ // Source-level invariant: the module under test does not require brain-client
271
+ // and does not require navigation.cjs writeEdge/logMemoryEvent. Verify via grep.
272
+ const src = require('node:fs').readFileSync(RANKER_PATH, 'utf8');
273
+ assert.ok(src.indexOf('require(\'../core/brain-client.cjs\')') === -1,
274
+ 'no brain-client require');
275
+ assert.ok(src.indexOf('writeEdge') === -1, 'no writeEdge call');
276
+ assert.ok(src.indexOf('logMemoryEvent') === -1, 'no logMemoryEvent call');
277
+ });
278
+
279
+ // ---------------------------------------------------------------------------
280
+ // Tests 11-14: Why content scaling (D9)
281
+ // ---------------------------------------------------------------------------
282
+
283
+ test('Test 11: at investment_level 0.1, why === teaching', () => {
284
+ const r = freshRanker();
285
+ const out = r.rankForSelector({
286
+ roomState: { framework_invocations: 1 }, // level = 0.1
287
+ k: 3,
288
+ });
289
+ const bq = out.find((x) => x.command === '/mos:beautiful-question');
290
+ assert.ok(bq);
291
+ assert.strictEqual(bq.why,
292
+ 'When the team disagrees, beautiful questions widen the frame before narrowing it.');
293
+ });
294
+
295
+ test('Test 12: at investment_level 0.9, why === jtbd_summary', () => {
296
+ const r = freshRanker();
297
+ const out = r.rankForSelector({
298
+ roomState: { framework_invocations: 9 }, // level = 0.9
299
+ k: 3,
300
+ });
301
+ const bq = out.find((x) => x.command === '/mos:beautiful-question');
302
+ assert.ok(bq);
303
+ assert.strictEqual(bq.why,
304
+ 'Beautiful questions surface real problems under the symptom.');
305
+ });
306
+
307
+ test('Test 13: at investment_level 0.5, why is teaching + " -- " + jtbd_summary', () => {
308
+ const r = freshRanker();
309
+ const out = r.rankForSelector({
310
+ roomState: { framework_invocations: 5 }, // level = 0.5
311
+ k: 3,
312
+ });
313
+ const bq = out.find((x) => x.command === '/mos:beautiful-question');
314
+ assert.ok(bq);
315
+ assert.strictEqual(bq.why,
316
+ 'When the team disagrees, beautiful questions widen the frame before narrowing it.'
317
+ + ' -- '
318
+ + 'Beautiful questions surface real problems under the symptom.');
319
+ });
320
+
321
+ test('Test 14: selectWhyContent is pure (same inputs -> same output)', () => {
322
+ const r = load();
323
+ const a = r.selectWhyContent('summary', 'teaching', 0.5);
324
+ const b = r.selectWhyContent('summary', 'teaching', 0.5);
325
+ const c = r.selectWhyContent('summary', 'teaching', 0.5);
326
+ assert.strictEqual(a, b);
327
+ assert.strictEqual(b, c);
328
+ assert.strictEqual(a, 'teaching -- summary');
329
+ // exact-string sanity at the boundaries
330
+ assert.strictEqual(r.selectWhyContent('summary', 'teaching', 0.1), 'teaching');
331
+ assert.strictEqual(r.selectWhyContent('summary', 'teaching', 0.9), 'summary');
332
+ });
333
+
334
+ // ---------------------------------------------------------------------------
335
+ // Tests 15-19: Visible investment feedback (D5 + D8 acceptance)
336
+ // ---------------------------------------------------------------------------
337
+
338
+ test('Test 15: renderInvestmentBadge(0.0) mentions "Brain priors" or "Brain"', () => {
339
+ const r = load();
340
+ const s = r.renderInvestmentBadge(0.0);
341
+ assert.ok(typeof s === 'string' && s.length > 0);
342
+ assert.ok(s.indexOf('Brain priors') !== -1 || s.indexOf('Brain') !== -1,
343
+ 'badge mentions Brain at level 0; got "' + s + '"');
344
+ });
345
+
346
+ test('Test 16: renderInvestmentBadge(1.0) mentions "full local" or "local scoring"', () => {
347
+ const r = load();
348
+ const s = r.renderInvestmentBadge(1.0);
349
+ assert.ok(typeof s === 'string' && s.length > 0);
350
+ assert.ok(s.indexOf('full local') !== -1 || s.indexOf('local scoring') !== -1,
351
+ 'badge mentions full local at level 1; got "' + s + '"');
352
+ });
353
+
354
+ test('Test 17: renderInvestmentBadge(0.3 / 0.5 / 0.7) are non-empty human-readable', () => {
355
+ const r = load();
356
+ for (const lvl of [0.3, 0.5, 0.7]) {
357
+ const s = r.renderInvestmentBadge(lvl);
358
+ assert.ok(typeof s === 'string' && s.length > 0, 'non-empty at ' + lvl);
359
+ assert.ok(/Investment|Brain|local|invocations/.test(s),
360
+ 'human-readable at ' + lvl + '; got "' + s + '"');
361
+ }
362
+ });
363
+
364
+ test('Test 18: renderInvestmentBadge(x) result length <= 80 chars for x in [0,1]', () => {
365
+ const r = load();
366
+ for (const lvl of [0, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0]) {
367
+ const s = r.renderInvestmentBadge(lvl);
368
+ assert.ok(s.length <= 80, 'badge <= 80 chars at ' + lvl + '; got ' + s.length);
369
+ }
370
+ });
371
+
372
+ test('Test 19: renderSliceBadge(2, "ill-defined state; evolving") includes 2 + rationale; <= 80', () => {
373
+ const r = load();
374
+ const s = r.renderSliceBadge(2, 'ill-defined state; evolving');
375
+ assert.ok(s.length <= 80, 'badge <= 80 chars; got ' + s.length);
376
+ assert.ok(s.indexOf('2') !== -1, 'mentions 2');
377
+ assert.ok(s.indexOf('ill-defined') !== -1 || s.indexOf('evolving') !== -1,
378
+ 'mentions a portion of the rationale; got "' + s + '"');
379
+ });
380
+
381
+ // ---------------------------------------------------------------------------
382
+ // Tests 20-21: Continuation callable (D10 acceptance)
383
+ // ---------------------------------------------------------------------------
384
+
385
+ test('Test 20: rankForSelector called twice on same inputs returns deepEqual results (idempotent)', () => {
386
+ const r = freshRanker();
387
+ const input = {
388
+ packetOptional: {
389
+ local_graph_summary: {
390
+ framework_chain_hint: {
391
+ edges: [{ from: 'Beautiful Question Framework', to: 'SWOT', confidence: 0.7, hop_distance: 1 }],
392
+ },
393
+ },
394
+ },
395
+ roomState: { framework_invocations: 3, problemType: 'UDP' },
396
+ k: 3,
397
+ };
398
+ const a = r.rankForSelector(input);
399
+ const b = r.rankForSelector(input);
400
+ assert.deepStrictEqual(a, b, 'idempotent: same input -> same output');
401
+ });
402
+
403
+ test('Test 21: rankForSelector does NOT subscribe to memory_event (no listeners)', () => {
404
+ const src = require('node:fs').readFileSync(RANKER_PATH, 'utf8');
405
+ // Look for event-subscription patterns. Allow harmless substrings (none expected).
406
+ const patterns = [/\.on\(/, /\.addListener\(/, /EventEmitter/, /process\.on\(/];
407
+ for (const p of patterns) {
408
+ assert.ok(!p.test(src),
409
+ 'rank-related code must not subscribe to events; matched ' + String(p));
410
+ }
411
+ });
412
+
413
+ // ---------------------------------------------------------------------------
414
+ // Tests 22-24: Teaching field read (D11 acceptance)
415
+ // ---------------------------------------------------------------------------
416
+
417
+ test('Test 22: command with teaching present but jtbd_summary missing is EXCLUDED', () => {
418
+ const r = load();
419
+ r._test._resetCaches();
420
+ r._test._setRegistry({
421
+ commands: [{
422
+ command: '/mos:t-no-summary',
423
+ kind: 'methodology',
424
+ frameworks: ['X'],
425
+ serves_jtbd: ['x'],
426
+ jtbd_label: 'X',
427
+ teaching: 'I have teaching but no summary.',
428
+ }],
429
+ });
430
+ const out = r.rankForSelector({ roomState: {}, k: 3 });
431
+ assert.strictEqual(out.length, 0, 'no-summary command excluded; got ' + out.length);
432
+ });
433
+
434
+ test('Test 23: command with jtbd_summary present but teaching missing is EXCLUDED', () => {
435
+ const r = load();
436
+ r._test._resetCaches();
437
+ r._test._setRegistry({
438
+ commands: [{
439
+ command: '/mos:s-no-teaching',
440
+ kind: 'methodology',
441
+ frameworks: ['X'],
442
+ serves_jtbd: ['x'],
443
+ jtbd_label: 'X',
444
+ jtbd_summary: 'I have summary but no teaching.',
445
+ }],
446
+ });
447
+ const out = r.rankForSelector({ roomState: {}, k: 3 });
448
+ assert.strictEqual(out.length, 0, 'no-teaching command excluded; got ' + out.length);
449
+ });
450
+
451
+ test('Test 24: when both teaching + jtbd_summary present, teaching appears at low inv; summary at high', () => {
452
+ const r = freshRanker();
453
+ const lowOut = r.rankForSelector({ roomState: { framework_invocations: 0 }, k: 3 });
454
+ const highOut = r.rankForSelector({ roomState: { framework_invocations: 10 }, k: 3 });
455
+ const lowBq = lowOut.find((x) => x.command === '/mos:beautiful-question');
456
+ const highBq = highOut.find((x) => x.command === '/mos:beautiful-question');
457
+ assert.ok(lowBq, 'beautiful-question in low result');
458
+ assert.ok(highBq, 'beautiful-question in high result');
459
+ assert.strictEqual(lowBq.why,
460
+ 'When the team disagrees, beautiful questions widen the frame before narrowing it.');
461
+ assert.strictEqual(highBq.why,
462
+ 'Beautiful questions surface real problems under the symptom.');
463
+ });
464
+
465
+ // ---------------------------------------------------------------------------
466
+ // Tests 25-26: Cold-start / Tier 0 (RESEARCH G-08)
467
+ // ---------------------------------------------------------------------------
468
+
469
+ test('Test 25: rankForSelector({}) with no jtbd/no packet/no roomState returns at least 1', () => {
470
+ // Use the REAL registry (Phase 104.1 shipped, so every command is eligible).
471
+ const r = load();
472
+ r._test._resetCaches();
473
+ // Don't override; let the real registry load.
474
+ const out = r.rankForSelector({});
475
+ assert.ok(out.length >= 1, 'at least one ranked command; got ' + out.length);
476
+ // All items must carry investment_level === 0 (cold start).
477
+ assert.strictEqual(out[0].investment_level, 0);
478
+ // Source is 'chain' (real commands have frameworks) or 'registry-only' for empty-fw commands.
479
+ assert.ok(out[0].source === 'chain' || out[0].source === 'registry-only',
480
+ "cold-start source is 'chain' or 'registry-only'; got " + out[0].source);
481
+ });
482
+
483
+ test('Test 26: rankForSelector with packetOptional containing 0-edge hint still returns ranked results', () => {
484
+ const r = freshRanker();
485
+ const packet = {
486
+ local_graph_summary: {
487
+ framework_chain_hint: {
488
+ edges: [], // empty
489
+ slice_scope: 1,
490
+ slice_rationale: 'empty for test',
491
+ fetched_at: new Date().toISOString(),
492
+ },
493
+ },
494
+ };
495
+ const out = r.rankForSelector({ packetOptional: packet, roomState: {}, k: 3 });
496
+ assert.ok(out.length >= 1, 'tier-0 path returns ranked results even with empty hint edges');
497
+ // With 0 edges, _sourceFor falls through past the 'packet' branch -> 'chain' or 'registry-only'
498
+ for (const item of out) {
499
+ assert.ok(item.source === 'chain' || item.source === 'registry-only',
500
+ "source falls through to 'chain' or 'registry-only' when packet hint has 0 edges; got " + item.source);
501
+ }
502
+ });
503
+
504
+ // ---------------------------------------------------------------------------
505
+ // Bonus regression tests (additive; do not count toward the 26 plan tests)
506
+ // ---------------------------------------------------------------------------
507
+
508
+ test('Bonus A: applyDecayWeight opts hook is invoked when provided', () => {
509
+ const r = freshRanker();
510
+ let called = 0;
511
+ const decay = (base, commandId, _rs) => {
512
+ called += 1;
513
+ return base * 0.5;
514
+ };
515
+ const baseOut = r.rankForSelector({ roomState: {}, k: 3 });
516
+ const decayOut = r.rankForSelector({
517
+ roomState: {},
518
+ k: 3,
519
+ _applyDecayWeight: decay,
520
+ });
521
+ assert.ok(called >= 1, 'decay hook called at least once');
522
+ // Decayed scores should be <= base scores for the same command (modulo sort order).
523
+ for (const item of decayOut) {
524
+ const baseMatch = baseOut.find((x) => x.command === item.command);
525
+ if (baseMatch) {
526
+ assert.ok(item.score <= baseMatch.score + 0.0001,
527
+ 'decayed score <= base for ' + item.command);
528
+ }
529
+ }
530
+ });
531
+
532
+ test('Bonus B: empty registry returns []', () => {
533
+ const r = load();
534
+ r._test._resetCaches();
535
+ r._test._setRegistry({ commands: [] });
536
+ const out = r.rankForSelector({ roomState: {}, k: 3 });
537
+ assert.deepStrictEqual(out, []);
538
+ });
539
+
540
+ test('Bonus C: k defaults to 3 when not provided', () => {
541
+ const r = freshRanker();
542
+ // 2 eligible commands in fake; ensure we never exceed k default 3.
543
+ const out = r.rankForSelector({ roomState: {} });
544
+ assert.ok(out.length <= 3, 'default k cap of 3 honored');
545
+ });
546
+
547
+ // ===========================================================================
548
+ // Phase 125-07 (D8) -- renderNoneFitAffordance label helper.
549
+ //
550
+ // 5 tests covering the user-facing label string the F-selector renderer places
551
+ // alongside the F.0/F.1/F.2 affordances when none of the top-K fit. Per
552
+ // CONTEXT.md D8 Open Question #8 lean: "None fit -- tell me what you need".
553
+ // ===========================================================================
554
+
555
+ test('Test 27 (D8): renderNoneFitAffordance returns a non-empty string', () => {
556
+ const r = load();
557
+ const s = r.renderNoneFitAffordance();
558
+ assert.strictEqual(typeof s, 'string');
559
+ assert.ok(s.length > 0, 'returned string has length > 0');
560
+ });
561
+
562
+ test('Test 28 (D8): renderNoneFitAffordance string contains "None fit"', () => {
563
+ const r = load();
564
+ const s = r.renderNoneFitAffordance();
565
+ assert.ok(s.indexOf('None fit') !== -1, 'phrase "None fit" present in label');
566
+ });
567
+
568
+ test('Test 29 (D8): renderNoneFitAffordance contains user-input hint', () => {
569
+ const r = load();
570
+ const s = r.renderNoneFitAffordance().toLowerCase();
571
+ const hasHint =
572
+ s.indexOf('tell me') !== -1
573
+ || s.indexOf('describe') !== -1
574
+ || s.indexOf('what you need') !== -1;
575
+ assert.ok(hasHint,
576
+ 'label contains a user-input hint (tell me / describe / what you need); got: ' + s);
577
+ });
578
+
579
+ test('Test 30 (D8): renderNoneFitAffordance returns <= 80 chars (single-line render)', () => {
580
+ const r = load();
581
+ const s = r.renderNoneFitAffordance();
582
+ assert.ok(s.length <= 80,
583
+ 'label is single-line (<= 80 chars); got length=' + s.length + ' value=' + s);
584
+ });
585
+
586
+ test('Test 31 (D8): renderNoneFitAffordance is deterministic (idempotent)', () => {
587
+ const r = load();
588
+ const a = r.renderNoneFitAffordance();
589
+ const b = r.renderNoneFitAffordance();
590
+ const c = r.renderNoneFitAffordance();
591
+ assert.strictEqual(a, b, 'same return on second call');
592
+ assert.strictEqual(b, c, 'same return on third call');
593
+ });