@mindrian_os/install 1.13.0-beta.12 → 1.13.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +57 -10
- package/README.md +74 -572
- package/commands/act.md +1 -0
- package/commands/admin.md +1 -0
- package/commands/analyze-needs.md +1 -0
- package/commands/analyze-systems.md +1 -0
- package/commands/analyze-timing.md +1 -0
- package/commands/auto-explore.md +2 -0
- package/commands/beautiful-question.md +1 -0
- package/commands/brain-derive.md +1 -0
- package/commands/build-knowledge.md +1 -0
- package/commands/build-thesis.md +1 -0
- package/commands/causal.md +1 -0
- package/commands/challenge-assumptions.md +1 -0
- package/commands/compare-ventures.md +1 -0
- package/commands/dashboard.md +1 -0
- package/commands/deep-grade.md +1 -0
- package/commands/diagnose.md +1 -0
- package/commands/diagnostics.md +1 -0
- package/commands/doctor.md +2 -1
- package/commands/dominant-designs.md +1 -0
- package/commands/explain-decision.md +1 -0
- package/commands/explore-domains.md +1 -0
- package/commands/explore-futures.md +1 -0
- package/commands/explore-trends.md +1 -0
- package/commands/export.md +1 -0
- package/commands/feynman-timeline-refresh.md +78 -0
- package/commands/file-meeting.md +1 -0
- package/commands/find-analogies.md +1 -0
- package/commands/find-bottlenecks.md +1 -0
- package/commands/find-connections.md +1 -0
- package/commands/funding.md +1 -0
- package/commands/grade.md +1 -0
- package/commands/graph.md +1 -0
- package/commands/hat-briefing.md +1 -0
- package/commands/heal.md +1 -0
- package/commands/help.md +1 -0
- package/commands/hmi-status.md +1 -0
- package/commands/jtbd.md +1 -0
- package/commands/leadership.md +1 -0
- package/commands/lean-canvas.md +1 -0
- package/commands/macro-trends.md +1 -0
- package/commands/map-unknowns.md +1 -0
- package/commands/memory.md +1 -0
- package/commands/models.md +1 -0
- package/commands/mos-reason.md +1 -0
- package/commands/mullins.md +1 -0
- package/commands/new-project.md +1 -0
- package/commands/onboard.md +1 -0
- package/commands/operator.md +2 -1
- package/commands/opportunities.md +1 -0
- package/commands/organize.md +1 -0
- package/commands/persona.md +1 -0
- package/commands/pipeline.md +1 -0
- package/commands/present.md +1 -0
- package/commands/publish.md +1 -0
- package/commands/query.md +1 -0
- package/commands/radar.md +1 -0
- package/commands/reanalyze.md +1 -0
- package/commands/research.md +1 -0
- package/commands/room.md +1 -0
- package/commands/rooms.md +1 -0
- package/commands/root-cause.md +1 -0
- package/commands/rs-experts.md +1 -0
- package/commands/rs-explain.md +1 -0
- package/commands/rs-fetch.md +1 -0
- package/commands/rs-thesis.md +1 -0
- package/commands/scenario-plan.md +1 -0
- package/commands/scheduled-tasks.md +1 -0
- package/commands/score-innovation.md +1 -0
- package/commands/scout.md +1 -0
- package/commands/setup.md +8 -3
- package/commands/snapshot.md +1 -0
- package/commands/speakers.md +1 -0
- package/commands/splash.md +1 -0
- package/commands/status.md +1 -0
- package/commands/structure-argument.md +1 -0
- package/commands/suggest-next.md +1 -0
- package/commands/systems-thinking.md +1 -0
- package/commands/think-hats.md +1 -0
- package/commands/update.md +1 -0
- package/commands/user-needs.md +1 -0
- package/commands/validate.md +1 -0
- package/commands/value-proposition.md +1 -0
- package/commands/vault.md +1 -0
- package/commands/visualize.md +1 -0
- package/commands/whitespace.md +1 -0
- package/commands/wiki.md +1 -0
- package/lib/brain/framework-chain-slice.cjs +193 -0
- package/lib/core/active-plugin-root.cjs +71 -6
- package/lib/core/brain-client.cjs +451 -36
- package/lib/core/cache-prune.cjs +208 -0
- package/lib/core/feynman/ROOM.md +25 -0
- package/lib/core/feynman/timeline-renderer.cjs +197 -0
- package/lib/core/feynman/timeline-runner.cjs +281 -0
- package/lib/core/navigation/edges.cjs +86 -0
- package/lib/core/navigation/insights.cjs +37 -0
- package/lib/core/navigation/memory-events.cjs +56 -1
- package/lib/core/navigation/neighborhood.cjs +5 -4
- package/lib/core/navigation/packet.cjs +176 -10
- package/lib/core/navigation/projections.cjs +201 -0
- package/lib/core/navigation.cjs +31 -0
- package/lib/core/resolve-brain-key.cjs +201 -0
- package/lib/mcp/larry-server-instructions.md +1 -1
- package/lib/memory/brain-cypher-chain-slice.test.cjs +368 -0
- package/lib/memory/f-selector-ranker.test.cjs +593 -0
- package/lib/memory/navigation-projections.test.cjs +241 -0
- package/lib/memory/navigation-write-edge.test.cjs +206 -0
- package/lib/memory/packet-chain-hint.test.cjs +407 -0
- package/lib/memory/packet-schema-validation.test.cjs +317 -0
- package/lib/memory/per-command-jtbd-derivation.test.cjs +130 -0
- package/lib/memory/per-command-teaching.test.cjs +110 -0
- package/lib/memory/run-feynman-tests.cjs +121 -0
- package/lib/memory/security-trifecta.test.cjs +23 -6
- package/lib/memory/selector-decisions.test.cjs +417 -0
- package/lib/memory/selector-miss.test.cjs +290 -0
- package/lib/workflow/f-selector-ranker.cjs +420 -0
- package/lib/workflow/selector-decisions.cjs +368 -0
- package/package.json +4 -1
- package/references/design/email-template-standard.md +1 -1
- package/references/user-research/2026-04-05-leah-lawrence-session.md +3 -3
- package/skills/brain-connector/SKILL.md +9 -3
|
@@ -0,0 +1,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
|
+
};
|