@mindrian_os/install 1.13.0-beta.13 → 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 (116) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +16 -11
  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 +1 -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 +1 -0
  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 +1 -0
  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 +1 -0
  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/feynman/ROOM.md +25 -0
  92. package/lib/core/feynman/timeline-renderer.cjs +197 -0
  93. package/lib/core/feynman/timeline-runner.cjs +281 -0
  94. package/lib/core/navigation/edges.cjs +86 -0
  95. package/lib/core/navigation/insights.cjs +37 -0
  96. package/lib/core/navigation/memory-events.cjs +39 -0
  97. package/lib/core/navigation/packet.cjs +89 -9
  98. package/lib/core/navigation/projections.cjs +201 -0
  99. package/lib/core/navigation.cjs +25 -0
  100. package/lib/mcp/larry-server-instructions.md +1 -1
  101. package/lib/memory/brain-cypher-chain-slice.test.cjs +368 -0
  102. package/lib/memory/f-selector-ranker.test.cjs +593 -0
  103. package/lib/memory/navigation-projections.test.cjs +241 -0
  104. package/lib/memory/navigation-write-edge.test.cjs +206 -0
  105. package/lib/memory/packet-chain-hint.test.cjs +407 -0
  106. package/lib/memory/packet-schema-validation.test.cjs +317 -0
  107. package/lib/memory/per-command-jtbd-derivation.test.cjs +130 -0
  108. package/lib/memory/per-command-teaching.test.cjs +110 -0
  109. package/lib/memory/run-feynman-tests.cjs +36 -0
  110. package/lib/memory/selector-decisions.test.cjs +417 -0
  111. package/lib/memory/selector-miss.test.cjs +290 -0
  112. package/lib/workflow/f-selector-ranker.cjs +420 -0
  113. package/lib/workflow/selector-decisions.cjs +368 -0
  114. package/package.json +1 -1
  115. package/references/design/email-template-standard.md +1 -1
  116. package/references/user-research/2026-04-05-leah-lawrence-session.md +3 -3
@@ -0,0 +1,420 @@
1
+ 'use strict';
2
+ /*
3
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
4
+ *
5
+ * Phase 125-05 -- F-selector top-K ranker. Pure synchronous function.
6
+ * ====================================================================
7
+ * Implements CONTEXT.md D1-D11. Plan 05 of the 9-plan F-selector ranker
8
+ * phase. The heart of Phase 125 -- turns the packet (Phase 110) + projections
9
+ * (Plan 01) + command registry (Phase 122 + Phase 104.1) into a top-K
10
+ * ranked F-selector list with investment-aware content selection (D9) and
11
+ * decay-weight integration (D7).
12
+ *
13
+ * HARD PRECONDITION: Phase 104.1 ships jtbd_label + jtbd_summary + teaching
14
+ * fields in data/command-registry.json. The ranker fails closed on every
15
+ * command if Phase 104.1 hasn't shipped (returns empty list, no crash).
16
+ * Verified at module load via the contract: every command must have all
17
+ * three fields or it is excluded from F-selector output.
18
+ *
19
+ * Canon Part 7 (reuse over build): extends shipped command-resolver +
20
+ * chain-recommender + packet without modifying their closed surfaces.
21
+ * Canon Part 8 (Graph Boundary): zero Brain calls (consumes packet, never
22
+ * issues query); LOCAL only. No network. No db writes. No I/O writes.
23
+ * Canon Part 9 (Memory Locality): this ranker is the "ranks structured
24
+ * packets" face -- SQL navigates; Brain reasons over packets that have
25
+ * already been built; this module just ranks what's on the table.
26
+ *
27
+ * Function signatures LOCKED in 125-CONTEXT.md "Function signatures":
28
+ * rankForSelector({jtbd, problemType, focusNodeId, roomState,
29
+ * packetOptional, k=3}) -> Array<RankedItem>
30
+ * selectWhyContent(jtbd_summary, teaching, investment_level) -> string
31
+ * renderInvestmentBadge(investment_level) -> string
32
+ * renderSliceBadge(slice_scope, slice_rationale) -> string
33
+ *
34
+ * Returned RankedItem shape (CONTEXT.md):
35
+ * {
36
+ * command: '/mos:slug',
37
+ * jtbd_label: string, // from Phase 104.1
38
+ * jtbd_summary: string, // from Phase 104.1
39
+ * teaching: string, // from Phase 104.1 (D11)
40
+ * framework: string, // implementation detail
41
+ * score: number, // 0..1 (D7 decay applied)
42
+ * why: string, // shape scales with investment (D9)
43
+ * source: 'packet'|'chain'|'registry-only',
44
+ * investment_level: number, // 0..1
45
+ * }
46
+ *
47
+ * D4 scoring formula (CONTEXT.md verbatim):
48
+ * score = (
49
+ * brain_confidence * 0.40 // always weighted
50
+ * + (1 - recency_decay) * 0.30 * investment_level // grows with use
51
+ * + problem_type_bind * 0.30 * investment_level // grows with use
52
+ * ) / (0.40 + 0.30*investment_level + 0.30*investment_level) // normalize 0..1
53
+ *
54
+ * License: BSL 1.1.
55
+ */
56
+
57
+ const path = require('node:path');
58
+ const fs = require('node:fs');
59
+ const projections = require('../core/navigation/projections.cjs');
60
+
61
+ const REGISTRY_PATH = path.join(__dirname, '..', '..', 'data', 'command-registry.json');
62
+ const TAXONOMY_PATH = path.join(__dirname, '..', 'hmi', 'jtbd-taxonomy.json');
63
+
64
+ // DEFAULT_SEED is duplicated from lib/brain/chain-recommender.cjs intentionally
65
+ // to keep rankForSelector synchronous + module-independent for the cold-start
66
+ // path. If chain-recommender ever exports DEFAULT_SEED as a const, switch to
67
+ // import. Minor Canon Part 7 (reuse) drift accepted in exchange for zero
68
+ // cross-module coupling on the hot ranking path.
69
+ const DEFAULT_SEED = 'Beautiful Question Framework';
70
+
71
+ // Per-process caches. The registry + taxonomy are generated artifacts that do
72
+ // not change during a run; reading each once is the command-resolver.cjs
73
+ // precedent. Tests can override the registry via _test._setRegistry.
74
+ let _registryCache = null;
75
+ let _taxonomyCache = null;
76
+
77
+ function _loadRegistry() {
78
+ if (_registryCache) return _registryCache;
79
+ try {
80
+ _registryCache = JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8'));
81
+ } catch (_e) {
82
+ _registryCache = { commands: [] };
83
+ }
84
+ return _registryCache;
85
+ }
86
+
87
+ function _loadTaxonomy() {
88
+ if (_taxonomyCache) return _taxonomyCache;
89
+ try {
90
+ _taxonomyCache = JSON.parse(fs.readFileSync(TAXONOMY_PATH, 'utf8'));
91
+ } catch (_e) {
92
+ _taxonomyCache = { entries: [] };
93
+ }
94
+ return _taxonomyCache;
95
+ }
96
+
97
+ // Test seam. Lets tests reset between cases AND inject a fake registry shape
98
+ // so the ranker doesn't have to read disk. NOT exposed via the public API.
99
+ function _resetCaches() {
100
+ _registryCache = null;
101
+ _taxonomyCache = null;
102
+ }
103
+ function _setRegistry(obj) {
104
+ _registryCache = obj || { commands: [] };
105
+ }
106
+ function _setTaxonomy(obj) {
107
+ _taxonomyCache = obj || { entries: [] };
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // D9 implementation -- content selection by investment_level.
112
+ // Pure. Same inputs -> same output. No I/O. No state mutation.
113
+ //
114
+ // investment_level < 0.4 -> teaching (Larry-voice prose)
115
+ // investment_level >= 0.7 -> jtbd_summary (terse rationale)
116
+ // 0.4 <= investment_level < 0.7 -> stitched: teaching + ' -- ' + jtbd_summary
117
+ //
118
+ // The separator is ' -- ' (double-hyphen with spaces) per the project no-em-
119
+ // dash rule + CONTEXT.md D9 (Open question 9 resolution: lean double-hyphen
120
+ // over parens or newline).
121
+ // ---------------------------------------------------------------------------
122
+ function selectWhyContent(jtbd_summary, teaching, investment_level) {
123
+ const hasJtbd = typeof jtbd_summary === 'string' && jtbd_summary.length > 0;
124
+ const hasTeaching = typeof teaching === 'string' && teaching.length > 0;
125
+ if (!hasJtbd && !hasTeaching) return '';
126
+ if (!hasJtbd) return teaching;
127
+ if (!hasTeaching) return jtbd_summary;
128
+ const lvl = (typeof investment_level === 'number') ? investment_level : 0;
129
+ if (lvl < 0.4) return teaching;
130
+ if (lvl >= 0.7) return jtbd_summary;
131
+ // Mid-band: stitch with the ' -- ' separator.
132
+ return teaching + ' -- ' + jtbd_summary;
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // D5 visible-investment badge. Single-line, <= 80 chars. The renderer (F-
137
+ // selector consumer) drops this string into the intelligence strip.
138
+ // ---------------------------------------------------------------------------
139
+ function renderInvestmentBadge(investment_level) {
140
+ const lvl = (typeof investment_level === 'number')
141
+ ? Math.max(0, Math.min(1, investment_level))
142
+ : 0;
143
+ const pct = Math.round(lvl * 100);
144
+ const remaining = Math.max(0, 10 - Math.round(lvl * 10));
145
+ let line;
146
+ if (lvl === 0) {
147
+ line = 'Investment: 0% -- Brain priors only (10 invocations to full local)';
148
+ } else if (lvl >= 1.0) {
149
+ line = 'Investment: 100% -- full local scoring with Brain confidence';
150
+ } else {
151
+ line = 'Investment: ' + pct + '% -- Brain + local signal ('
152
+ + remaining + ' invocations to full)';
153
+ }
154
+ return (line.length > 80) ? line.slice(0, 80) : line;
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // D5 slice rationale badge. Single-line, <= 80 chars.
159
+ // ---------------------------------------------------------------------------
160
+ function renderSliceBadge(slice_scope, slice_rationale) {
161
+ const scope = (slice_scope === 1 || slice_scope === 2 || slice_scope === 3)
162
+ ? slice_scope : 3;
163
+ const rationaleStr = (typeof slice_rationale === 'string') ? slice_rationale : '';
164
+ const head = 'Slice: ' + scope + ' hop' + (scope === 1 ? '' : 's') + ' -- ';
165
+ const room = Math.max(0, 80 - head.length);
166
+ const rationaleShort = rationaleStr.slice(0, room);
167
+ return head + rationaleShort;
168
+ }
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // Phase 125-07 -- D8 none-fit affordance label. The user-facing string the
172
+ // F-selector renderer places alongside F.0/F.1/F.2 affordances when none of
173
+ // the top-K fit the user's intent. Per CONTEXT.md Open Question #8 lean
174
+ // ("None fit -- tell me what you need"). Larry-voice; clear; <= 80 chars
175
+ // for the single-line intelligence strip. Deterministic (idempotent across
176
+ // calls -- the renderer can cache freely).
177
+ //
178
+ // Wave-1 user test of the wording remains an Open Question for v2 (per
179
+ // Open Q #8). When that test lands, swap the literal here -- no consumer
180
+ // change needed because the renderer treats this as an opaque label string.
181
+ //
182
+ // The consumer (F-selector renderer) is responsible for the follow-up
183
+ // /mos:do call when the user picks this affordance. recordSelectorMiss
184
+ // (Plan 07; same Phase 125-07 wave) is the capture writer that pairs with
185
+ // this label.
186
+ // ---------------------------------------------------------------------------
187
+ function renderNoneFitAffordance() {
188
+ return 'None fit -- tell me what you need';
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // D7 decay-weight integration. When opts._applyDecayWeight is provided
193
+ // (typically by the F-selector renderer that wires Plan 05 + Plan 06), apply
194
+ // it. When absent, default no-op (returns base score untouched). Plan 06
195
+ // ships the actual decay function; Plan 05 stays callable in its absence.
196
+ // ---------------------------------------------------------------------------
197
+ function _applyDecay(applyDecayWeight, baseScore, commandId, roomState) {
198
+ if (typeof applyDecayWeight !== 'function') return baseScore;
199
+ try {
200
+ const adjusted = applyDecayWeight(baseScore, commandId, roomState);
201
+ return (typeof adjusted === 'number' && isFinite(adjusted)) ? adjusted : baseScore;
202
+ } catch (_e) {
203
+ return baseScore;
204
+ }
205
+ }
206
+
207
+ // JTBD selection per RESEARCH G-01: lean roomState.activeJtbd if it matches
208
+ // one of the command's serves_jtbd entries; otherwise fall back to serves_jtbd[0].
209
+ function _resolveJtbdForCommand(cmd, roomState) {
210
+ const serves = Array.isArray(cmd.serves_jtbd) ? cmd.serves_jtbd : [];
211
+ if (serves.length === 0) return null;
212
+ const activeJtbd =
213
+ (roomState && typeof roomState.activeJtbd === 'string') ? roomState.activeJtbd : null;
214
+ if (activeJtbd && serves.indexOf(activeJtbd) !== -1) return activeJtbd;
215
+ return serves[0];
216
+ }
217
+
218
+ // Extract brain_confidence for this command's framework from the packet's
219
+ // framework_chain_hint. The highest confidence edge that touches the
220
+ // framework (as source or target) wins. Returns null when no signal.
221
+ function _brainConfidenceFromPacket(packetOptional, framework) {
222
+ if (!packetOptional || !packetOptional.local_graph_summary) return null;
223
+ const hint = packetOptional.local_graph_summary.framework_chain_hint;
224
+ if (!hint || !Array.isArray(hint.edges)) return null;
225
+ let best = null;
226
+ for (const e of hint.edges) {
227
+ if (!e) continue;
228
+ const touches = (e.from === framework) || (e.to === framework);
229
+ if (touches && typeof e.confidence === 'number') {
230
+ if (best === null || e.confidence > best) best = e.confidence;
231
+ }
232
+ }
233
+ return best;
234
+ }
235
+
236
+ // D4 recency_decay term: 1.0 = fully decayed (old); 0.0 = fresh.
237
+ // Computed from roomState.lastInvokedAt[command] if available (ms epoch),
238
+ // else 0.5 default. Linear ramp: 0..30 days -> 0..1.
239
+ function _recencyDecay(cmd, roomState) {
240
+ if (!roomState || !roomState.lastInvokedAt) return 0.5;
241
+ const last = roomState.lastInvokedAt[cmd.command];
242
+ if (typeof last !== 'number' || !isFinite(last)) return 0.5;
243
+ const ageDays = (Date.now() - last) / (24 * 3600 * 1000);
244
+ if (ageDays <= 0) return 0;
245
+ return Math.min(1.0, ageDays / 30);
246
+ }
247
+
248
+ // D4 problem_type_bind term: 1.0 if the command's frameworks align with
249
+ // roomState.problemType's preferred shape; 0.0 otherwise; 0.5 default when
250
+ // problemType is absent.
251
+ // Simple proxy (Plan 05 v1; Plan 06+ may refine via Brain chain weight):
252
+ // WDP + kind=methodology -> 1.0
253
+ // UDP/IDP + frameworks includes 'Beautiful Question Framework'-> 1.0
254
+ // else -> 0.5
255
+ function _problemTypeBind(cmd, roomState) {
256
+ if (!roomState || typeof roomState.problemType !== 'string') return 0.5;
257
+ const pt = roomState.problemType.toUpperCase();
258
+ const fws = Array.isArray(cmd.frameworks) ? cmd.frameworks : [];
259
+ if (pt === 'WDP' && cmd.kind === 'methodology') return 1.0;
260
+ if ((pt === 'UDP' || pt === 'IDP') && fws.indexOf('Beautiful Question Framework') !== -1) {
261
+ return 1.0;
262
+ }
263
+ return 0.5;
264
+ }
265
+
266
+ // D4 scoring formula. Numerator + denominator coded with the exact 0.40 /
267
+ // 0.30 / 0.30 weights from CONTEXT.md. Normalizes to 0..1 at every
268
+ // investment_level; investment_level=0 reduces to pure brain_confidence
269
+ // (no discontinuity).
270
+ function _scoreCommand({ cmd, packetOptional, roomState, investment_level }) {
271
+ const fw = (Array.isArray(cmd.frameworks) && cmd.frameworks.length > 0)
272
+ ? cmd.frameworks[0] : DEFAULT_SEED;
273
+ const bc = _brainConfidenceFromPacket(packetOptional, fw);
274
+ const brain_confidence = (typeof bc === 'number') ? bc : 0.5;
275
+ const recency_decay = _recencyDecay(cmd, roomState);
276
+ const problem_type_bind = _problemTypeBind(cmd, roomState);
277
+ const inv = investment_level;
278
+ const numerator =
279
+ brain_confidence * 0.40
280
+ + (1 - recency_decay) * 0.30 * inv
281
+ + problem_type_bind * 0.30 * inv;
282
+ const denominator = 0.40 + 0.30 * inv + 0.30 * inv;
283
+ if (denominator <= 0) return 0;
284
+ return numerator / denominator;
285
+ }
286
+
287
+ // Per-command source attribution per Test 5 (CONTEXT.md acceptance):
288
+ // packet has framework_chain_hint with edges -> 'packet'
289
+ // command has frameworks[] (chain-recommender-derivable)-> 'chain'
290
+ // neither -> 'registry-only'
291
+ function _sourceFor(packetOptional, cmd) {
292
+ if (packetOptional
293
+ && packetOptional.local_graph_summary
294
+ && packetOptional.local_graph_summary.framework_chain_hint) {
295
+ const hint = packetOptional.local_graph_summary.framework_chain_hint;
296
+ if (Array.isArray(hint.edges) && hint.edges.length > 0) return 'packet';
297
+ }
298
+ if (Array.isArray(cmd.frameworks) && cmd.frameworks.length > 0) return 'chain';
299
+ return 'registry-only';
300
+ }
301
+
302
+ // ---------------------------------------------------------------------------
303
+ // MAIN SIGNATURE -- rankForSelector. Pure synchronous function. No Promise.
304
+ // No await. No Brain calls. No db writes. No memory_event writes. No event
305
+ // subscriptions (D10 invariant).
306
+ // ---------------------------------------------------------------------------
307
+ function rankForSelector(args) {
308
+ const o = args || {};
309
+ // jtbd, problemType, focusNodeId are intentionally read off args even when
310
+ // we don't immediately use them here (the LOCKED CONTEXT.md signature
311
+ // preserves them as ranker inputs; consumers may surface them via roomState
312
+ // patching for future tuning passes).
313
+ const jtbd = (typeof o.jtbd === 'string') ? o.jtbd : null;
314
+ const problemType = (typeof o.problemType === 'string') ? o.problemType : null;
315
+ const focusNodeId = (typeof o.focusNodeId === 'string') ? o.focusNodeId : null;
316
+ const roomState = (o.roomState && typeof o.roomState === 'object') ? o.roomState : {};
317
+ const packetOptional = (o.packetOptional && typeof o.packetOptional === 'object')
318
+ ? o.packetOptional : null;
319
+ const k = (typeof o.k === 'number' && o.k > 0) ? Math.floor(o.k) : 3;
320
+ const applyDecayWeight = (typeof o._applyDecayWeight === 'function')
321
+ ? o._applyDecayWeight : null;
322
+
323
+ // Patch roomState with jtbd / problemType if caller supplied them at the
324
+ // top level but not on roomState. Non-destructive: tests that pass
325
+ // roomState verbatim should see no mutation.
326
+ let effectiveRoomState = roomState;
327
+ if (jtbd && !roomState.activeJtbd) {
328
+ effectiveRoomState = Object.assign({}, roomState, { activeJtbd: jtbd });
329
+ }
330
+ if (problemType && !effectiveRoomState.problemType) {
331
+ effectiveRoomState = Object.assign({}, effectiveRoomState, { problemType });
332
+ }
333
+
334
+ // Snapshot investment level at rank time (D9 invariant: all returned items
335
+ // see the same investment_level).
336
+ const { level: investment_level } = projections.computeInvestmentLevel(effectiveRoomState);
337
+
338
+ const reg = _loadRegistry();
339
+ const commands = Array.isArray(reg.commands) ? reg.commands : [];
340
+ const scored = [];
341
+
342
+ for (const cmd of commands) {
343
+ if (!cmd || typeof cmd.command !== 'string') continue;
344
+
345
+ // D6 fail-closed: jtbd_summary required.
346
+ const jtbd_summary = (typeof cmd.jtbd_summary === 'string' && cmd.jtbd_summary.length > 0)
347
+ ? cmd.jtbd_summary : null;
348
+ if (jtbd_summary === null) continue;
349
+
350
+ // D11 fail-closed: teaching required.
351
+ const teaching = (typeof cmd.teaching === 'string' && cmd.teaching.length > 0)
352
+ ? cmd.teaching : null;
353
+ if (teaching === null) continue;
354
+
355
+ const jtbd_label = (typeof cmd.jtbd_label === 'string') ? cmd.jtbd_label : '';
356
+ const framework = (Array.isArray(cmd.frameworks) && cmd.frameworks.length > 0)
357
+ ? cmd.frameworks[0] : DEFAULT_SEED;
358
+
359
+ const baseScore = _scoreCommand({
360
+ cmd, packetOptional, roomState: effectiveRoomState, investment_level,
361
+ });
362
+ const adjustedScore = _applyDecay(
363
+ applyDecayWeight, baseScore, cmd.command, effectiveRoomState,
364
+ );
365
+
366
+ const why = selectWhyContent(jtbd_summary, teaching, investment_level);
367
+ const source = _sourceFor(packetOptional, cmd);
368
+
369
+ scored.push({
370
+ command: cmd.command,
371
+ jtbd_label,
372
+ jtbd_summary,
373
+ teaching,
374
+ framework,
375
+ score: Math.max(0, Math.min(1, adjustedScore)),
376
+ why,
377
+ source,
378
+ investment_level,
379
+ });
380
+ }
381
+
382
+ // Sort score desc. Stable on ties.
383
+ scored.sort((a, b) => b.score - a.score);
384
+
385
+ // Tier 0 cold-start fallback: when zero eligible commands but k > 0, the
386
+ // ranker degrades gracefully to []. The HARD PRECONDITION (Phase 104.1
387
+ // shipped) makes this branch effectively unreachable in production --
388
+ // every command has the required content. Kept for future-proofing
389
+ // against registry regressions.
390
+ if (scored.length === 0) return [];
391
+
392
+ // Reference focusNodeId so static linters don't flag it as unused. It is
393
+ // part of the LOCKED signature for downstream tuning (Phase 117 may pass
394
+ // it through for hop-depth selection).
395
+ if (focusNodeId) { /* reserved for future hop-relative scoring */ }
396
+
397
+ return scored.slice(0, k);
398
+ }
399
+
400
+ module.exports = {
401
+ rankForSelector,
402
+ selectWhyContent,
403
+ renderInvestmentBadge,
404
+ renderSliceBadge,
405
+ renderNoneFitAffordance,
406
+ // Test seam (private; consumed only by lib/memory/f-selector-ranker.test.cjs).
407
+ _test: {
408
+ _resetCaches,
409
+ _setRegistry,
410
+ _setTaxonomy,
411
+ _scoreCommand,
412
+ _brainConfidenceFromPacket,
413
+ _resolveJtbdForCommand,
414
+ _sourceFor,
415
+ _applyDecay,
416
+ _recencyDecay,
417
+ _problemTypeBind,
418
+ DEFAULT_SEED,
419
+ },
420
+ };