@mindrian_os/install 1.13.0-beta.16 → 1.13.0-beta.19
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 +36 -0
- package/commands/act.md +1 -0
- package/commands/admin.md +1 -0
- package/commands/analyze-needs.md +2 -0
- package/commands/analyze-systems.md +2 -0
- package/commands/analyze-timing.md +2 -0
- package/commands/auto-explore.md +2 -0
- package/commands/beautiful-question.md +2 -0
- package/commands/brain-derive.md +2 -0
- package/commands/build-knowledge.md +2 -0
- package/commands/build-thesis.md +2 -0
- package/commands/causal.md +2 -0
- package/commands/challenge-assumptions.md +2 -0
- package/commands/compare-ventures.md +2 -0
- package/commands/dashboard.md +2 -1
- package/commands/deep-grade.md +2 -0
- package/commands/diagnose.md +21 -1
- package/commands/diagnostics.md +14 -3
- package/commands/doctor.md +4 -1
- package/commands/dogfood-flush.md +92 -0
- package/commands/dominant-designs.md +2 -0
- package/commands/explain-decision.md +2 -0
- package/commands/explore-domains.md +2 -0
- package/commands/explore-futures.md +2 -0
- package/commands/explore-trends.md +2 -0
- package/commands/export.md +1 -0
- package/commands/feynman-timeline-refresh.md +2 -0
- package/commands/file-meeting.md +4 -0
- package/commands/find-analogies.md +1 -0
- package/commands/find-bottlenecks.md +2 -0
- package/commands/find-connections.md +2 -0
- package/commands/funding.md +1 -0
- package/commands/grade.md +4 -0
- package/commands/graph.md +1 -0
- package/commands/hat-briefing.md +1 -0
- package/commands/heal.md +22 -170
- package/commands/help.md +54 -334
- package/commands/hmi-status.md +23 -144
- package/commands/jtbd.md +1 -0
- package/commands/leadership.md +2 -0
- package/commands/lean-canvas.md +2 -0
- package/commands/macro-trends.md +2 -0
- package/commands/map-unknowns.md +2 -0
- package/commands/memory.md +1 -0
- package/commands/models.md +1 -0
- package/commands/mos-reason.md +2 -0
- package/commands/mos.md +139 -0
- package/commands/mullins.md +2 -0
- package/commands/mva-brief.md +58 -0
- package/commands/mva-option.md +91 -0
- package/commands/new-project.md +4 -0
- package/commands/onboard.md +22 -7
- package/commands/operator.md +1 -0
- package/commands/opportunities.md +1 -0
- package/commands/organize.md +22 -469
- package/commands/persona.md +1 -0
- package/commands/pipeline.md +2 -0
- package/commands/present.md +1 -0
- package/commands/publish.md +2 -0
- package/commands/query.md +24 -102
- package/commands/radar.md +2 -0
- package/commands/reanalyze.md +1 -0
- package/commands/research.md +2 -0
- package/commands/room.md +2 -0
- package/commands/rooms.md +1 -0
- package/commands/root-cause.md +2 -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 +2 -0
- package/commands/scheduled-tasks.md +1 -0
- package/commands/score-innovation.md +2 -0
- package/commands/scout.md +1 -0
- package/commands/setup.md +2 -0
- package/commands/snapshot.md +2 -0
- package/commands/speakers.md +1 -0
- package/commands/splash.md +5 -2
- package/commands/status.md +1 -0
- package/commands/structure-argument.md +2 -0
- package/commands/suggest-next.md +2 -0
- package/commands/systems-thinking.md +2 -0
- package/commands/think-hats.md +2 -0
- package/commands/update.md +2 -0
- package/commands/user-needs.md +2 -0
- package/commands/validate.md +2 -0
- package/commands/value-proposition.md +2 -0
- package/commands/vault.md +2 -0
- package/commands/visualize.md +24 -29
- package/commands/whitespace.md +2 -1
- package/commands/wiki.md +1 -0
- package/hooks/hooks.json +31 -88
- package/lib/agents/auto-explore-agent.cjs +82 -0
- package/lib/agents/mva/brain-classic-traps.cjs +77 -0
- package/lib/agents/mva/brain-cross-domain.cjs +79 -0
- package/lib/agents/mva/brain-similar-ventures.cjs +93 -0
- package/lib/agents/mva/dashboard-graph-neighborhood.cjs +72 -0
- package/lib/agents/mva/index.cjs +42 -0
- package/lib/agents/mva/six-hats-red-black.cjs +137 -0
- package/lib/agents/mva/tavily-funding-scan.cjs +147 -0
- package/lib/agents/mva/test-all-six-agents.cjs +467 -0
- package/lib/conversation/operator.cjs +64 -0
- package/lib/conversation/operator.test.cjs +160 -0
- package/lib/core/breakthrough/canary.cjs +134 -0
- package/lib/core/breakthrough/canary.test.cjs +136 -0
- package/lib/core/breakthrough/detectors.cjs +359 -0
- package/lib/core/breakthrough/detectors.test.cjs +333 -0
- package/lib/core/breakthrough/ethics-fence.cjs +127 -0
- package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
- package/lib/core/breakthrough/resurfacing.cjs +150 -0
- package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
- package/lib/core/breakthrough/review-queue.cjs +154 -0
- package/lib/core/breakthrough/review-queue.test.cjs +160 -0
- package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
- package/lib/core/breakthrough/scanner.cjs +426 -0
- package/lib/core/breakthrough/scanner.test.cjs +267 -0
- package/lib/core/breakthrough/schema.cjs +164 -0
- package/lib/core/breakthrough/schema.test.cjs +256 -0
- package/lib/core/breakthrough/scoring.cjs +293 -0
- package/lib/core/breakthrough/scoring.test.cjs +423 -0
- package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
- package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
- package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
- package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
- package/lib/core/first-touch-version-stamper.cjs +113 -0
- package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
- package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
- package/lib/core/llm-name-suggester.cjs +194 -0
- package/lib/core/llm-name-suggester.test.cjs +132 -0
- package/lib/core/mva-agent-contract.cjs +170 -0
- package/lib/core/mva-agent-contract.test.cjs +169 -0
- package/lib/core/mva-budget.cjs +75 -0
- package/lib/core/mva-budget.test.cjs +68 -0
- package/lib/core/mva-classifier.cjs +370 -0
- package/lib/core/mva-classifier.test.cjs +248 -0
- package/lib/core/mva-deck-builder.cjs +452 -0
- package/lib/core/mva-deck-builder.test.cjs +287 -0
- package/lib/core/mva-detect.smoke.test.cjs +197 -0
- package/lib/core/mva-dispatcher.cjs +110 -0
- package/lib/core/mva-dispatcher.test.cjs +216 -0
- package/lib/core/mva-option-router.cjs +292 -0
- package/lib/core/mva-option-router.test.cjs +483 -0
- package/lib/core/mva-orchestrator.cjs +365 -0
- package/lib/core/mva-orchestrator.test.cjs +908 -0
- package/lib/core/mva-progressive-renderer.cjs +194 -0
- package/lib/core/mva-progressive-renderer.test.cjs +157 -0
- package/lib/core/mva-rule-linter.cjs +213 -0
- package/lib/core/mva-rule-linter.test.cjs +336 -0
- package/lib/core/mva-state.cjs +159 -0
- package/lib/core/mva-telemetry.cjs +58 -0
- package/lib/core/mva-telemetry.test.cjs +196 -0
- package/lib/core/mva-vercel-deploy.cjs +168 -0
- package/lib/core/mva-vercel-deploy.test.cjs +239 -0
- package/lib/core/navigation/dashboard-helpers.cjs +145 -0
- package/lib/core/navigation/edges.cjs +35 -0
- package/lib/core/navigation/memory-events.cjs +126 -0
- package/lib/core/navigation.cjs +11 -0
- package/lib/core/resolve-vercel-key.cjs +107 -0
- package/lib/core/resolve-vercel-key.test.cjs +137 -0
- package/lib/core/room-auto-create.cjs +318 -0
- package/lib/core/room-auto-create.test.cjs +198 -0
- package/lib/core/room-discard-cascade.cjs +225 -0
- package/lib/core/room-discard-cascade.test.cjs +135 -0
- package/lib/core/room-name-validator.cjs +132 -0
- package/lib/core/room-name-validator.test.cjs +156 -0
- package/lib/core/room-naming-selector.cjs +357 -0
- package/lib/core/room-naming-selector.test.cjs +277 -0
- package/lib/core/room-receipt-emit.cjs +63 -0
- package/lib/core/room-skeleton-scaffold.cjs +315 -0
- package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
- package/lib/core/stale-copy-scanner.cjs +190 -0
- package/lib/core/state-aware-router.cjs +78 -0
- package/lib/core/telemetry/schema.cjs +168 -0
- package/lib/core/telemetry/schema.test.cjs +124 -0
- package/lib/core/telemetry/validator.cjs +197 -0
- package/lib/core/telemetry/validator.test.cjs +188 -0
- package/lib/core/telemetry/writer.cjs +141 -0
- package/lib/core/telemetry/writer.test.cjs +331 -0
- package/lib/core/terminal-capability.cjs +88 -0
- package/lib/core/venture-shape-nudge.cjs +163 -0
- package/lib/core/venture-shape-nudge.test.cjs +161 -0
- package/lib/core/visual-ops.cjs +70 -2
- package/lib/hmi/selector-dispatcher.cjs +90 -1
- package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
- package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
- package/lib/memory/body-shape-coverage.test.cjs +268 -0
- package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
- package/lib/memory/first-touch-version.test.cjs +198 -0
- package/lib/memory/help-coverage.test.cjs +108 -0
- package/lib/memory/help-renderer.test.cjs +145 -0
- package/lib/memory/palette-consistency.test.cjs +127 -0
- package/lib/memory/pending-tension-store.cjs +80 -0
- package/lib/memory/render-v2-disposition.test.cjs +199 -0
- package/lib/memory/run-feynman-tests.cjs +240 -0
- package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
- package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
- package/lib/memory/soft-alias.test.cjs +144 -0
- package/lib/memory/stale-copy-scanner.test.cjs +291 -0
- package/lib/memory/state-aware-router.test.cjs +90 -0
- package/lib/memory/statusline-two-row.test.cjs +338 -0
- package/lib/memory/terminal-capability.test.cjs +155 -0
- package/lib/render/ROOM.md +74 -22
- package/lib/sessionstart/budget-compressor.cjs +130 -0
- package/lib/sessionstart/contributor-interface.cjs +134 -0
- package/lib/sessionstart/contributor-isolator.cjs +128 -0
- package/lib/sessionstart/precedence-ladder.cjs +47 -0
- package/lib/statusline/governing-thought-truncator.cjs +45 -0
- package/lib/statusline/two-row-renderer.cjs +186 -0
- package/lib/statusline/version-resolver.cjs +81 -0
- package/package.json +1 -1
- package/references/visual/ROOM.md +55 -0
- package/references/visual/palette.json +54 -0
- package/skills/larry-personality/SKILL.md +34 -0
- package/skills/mva-pipeline/SKILL.md +129 -0
- package/skills/ui-system/SKILL.md +109 -1
- package/skills/ui-system/rules/dual-palette.md +156 -0
- package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
- package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 118-05 Plan 05 -- operator helper tests for transitionViaMVAOption.
|
|
5
|
+
*
|
|
6
|
+
* This is the FIRST test file for lib/conversation/operator.cjs. Plan 118-05
|
|
7
|
+
* Task 1 adds the transitionViaMVAOption(roomDir, optionId) helper -- a thin
|
|
8
|
+
* additive wrapper around the existing transition() function. The helper is
|
|
9
|
+
* called by lib/core/mva-option-router.cjs when the user picks 1, 2, or 3
|
|
10
|
+
* from the 30-second MVA brief footer.
|
|
11
|
+
*
|
|
12
|
+
* The 9 existing transition rules are NOT modified. The OPERATORS array is
|
|
13
|
+
* NOT modified. This is a small additive surface.
|
|
14
|
+
*
|
|
15
|
+
* Option 1 -> JUST_TALK (via 'manual_reset' trigger; existing ANY rule)
|
|
16
|
+
* Option 2 -> NO transition (stub per binding decision B6 OPTION A; Phase 119
|
|
17
|
+
* will wire the BUILD_ROOM path)
|
|
18
|
+
* Option 3 -> METHODOLOGY (via 'mos_command' trigger; existing ANY rule)
|
|
19
|
+
*
|
|
20
|
+
* Pure CJS, node built-ins only.
|
|
21
|
+
*/
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const test = require('node:test');
|
|
25
|
+
const assert = require('node:assert/strict');
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
const os = require('node:os');
|
|
29
|
+
|
|
30
|
+
const operator = require('./operator.cjs');
|
|
31
|
+
|
|
32
|
+
// Hermetic temp roomDir per test (so we never pollute real rooms).
|
|
33
|
+
function makeTempRoom() {
|
|
34
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'op-mva-option-'));
|
|
35
|
+
return dir;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cleanupTempRoom(dir) {
|
|
39
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------- Task 1: transitionViaMVAOption helper ----------
|
|
43
|
+
|
|
44
|
+
test('Test 1: transitionViaMVAOption(roomDir, 1) -> JUST_TALK', () => {
|
|
45
|
+
const roomDir = makeTempRoom();
|
|
46
|
+
try {
|
|
47
|
+
// Move to a non-JUST_TALK state first so the transition is meaningful.
|
|
48
|
+
// From the default JUST_TALK, step to EXPLORE_CAPTURE.
|
|
49
|
+
const pre = operator.transition(roomDir, 'EXPLORE_CAPTURE', 'user_message');
|
|
50
|
+
assert.equal(pre.success, true);
|
|
51
|
+
assert.equal(operator.getCurrent(roomDir).current, 'EXPLORE_CAPTURE');
|
|
52
|
+
|
|
53
|
+
const result = operator.transitionViaMVAOption(roomDir, 1);
|
|
54
|
+
assert.equal(result.ok, true, 'option 1 should succeed');
|
|
55
|
+
assert.equal(result.new_state, 'JUST_TALK', 'option 1 must end in JUST_TALK');
|
|
56
|
+
assert.equal(result.from, 'EXPLORE_CAPTURE', 'from must be the previous state');
|
|
57
|
+
assert.equal(operator.getCurrent(roomDir).current, 'JUST_TALK');
|
|
58
|
+
} finally {
|
|
59
|
+
cleanupTempRoom(roomDir);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('Test 2: transitionViaMVAOption(roomDir, 2) -> no transition (stub)', () => {
|
|
64
|
+
const roomDir = makeTempRoom();
|
|
65
|
+
try {
|
|
66
|
+
// Move to METHODOLOGY first so we can verify "no transition" preserves it.
|
|
67
|
+
const pre = operator.transition(roomDir, 'METHODOLOGY', 'mos_command');
|
|
68
|
+
assert.equal(pre.success, true);
|
|
69
|
+
assert.equal(operator.getCurrent(roomDir).current, 'METHODOLOGY');
|
|
70
|
+
|
|
71
|
+
const result = operator.transitionViaMVAOption(roomDir, 2);
|
|
72
|
+
assert.equal(result.ok, true, 'option 2 returns ok:true (stub but valid)');
|
|
73
|
+
assert.equal(result.no_transition, true, 'option 2 must flag no_transition');
|
|
74
|
+
assert.equal(result.reason, 'option_2_stub', 'reason must be option_2_stub');
|
|
75
|
+
assert.equal(result.new_state, 'METHODOLOGY', 'state unchanged after stub option');
|
|
76
|
+
assert.equal(operator.getCurrent(roomDir).current, 'METHODOLOGY');
|
|
77
|
+
} finally {
|
|
78
|
+
cleanupTempRoom(roomDir);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('Test 3: transitionViaMVAOption(roomDir, 3) -> METHODOLOGY', () => {
|
|
83
|
+
const roomDir = makeTempRoom();
|
|
84
|
+
try {
|
|
85
|
+
// From JUST_TALK default, option 3 should jump straight to METHODOLOGY
|
|
86
|
+
// via the ANY -> METHODOLOGY 'mos_command' rule.
|
|
87
|
+
assert.equal(operator.getCurrent(roomDir).current, 'JUST_TALK');
|
|
88
|
+
|
|
89
|
+
const result = operator.transitionViaMVAOption(roomDir, 3);
|
|
90
|
+
assert.equal(result.ok, true, 'option 3 should succeed');
|
|
91
|
+
assert.equal(result.new_state, 'METHODOLOGY', 'option 3 must end in METHODOLOGY');
|
|
92
|
+
assert.equal(result.from, 'JUST_TALK', 'from must be the previous state');
|
|
93
|
+
assert.equal(operator.getCurrent(roomDir).current, 'METHODOLOGY');
|
|
94
|
+
} finally {
|
|
95
|
+
cleanupTempRoom(roomDir);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('Test 4: transitionViaMVAOption(roomDir, 99) -> invalid_option error', () => {
|
|
100
|
+
const roomDir = makeTempRoom();
|
|
101
|
+
try {
|
|
102
|
+
const before = operator.getCurrent(roomDir).current;
|
|
103
|
+
const result = operator.transitionViaMVAOption(roomDir, 99);
|
|
104
|
+
assert.equal(result.ok, false);
|
|
105
|
+
assert.equal(result.error, 'invalid_option');
|
|
106
|
+
assert.deepEqual(result.valid_options, [1, 2, 3]);
|
|
107
|
+
// State unchanged
|
|
108
|
+
assert.equal(operator.getCurrent(roomDir).current, before);
|
|
109
|
+
} finally {
|
|
110
|
+
cleanupTempRoom(roomDir);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('Test 4b: transitionViaMVAOption rejects non-integer optionId', () => {
|
|
115
|
+
const roomDir = makeTempRoom();
|
|
116
|
+
try {
|
|
117
|
+
const r1 = operator.transitionViaMVAOption(roomDir, '1');
|
|
118
|
+
assert.equal(r1.ok, false, 'string "1" is rejected (strict int check)');
|
|
119
|
+
assert.equal(r1.error, 'invalid_option');
|
|
120
|
+
|
|
121
|
+
const r2 = operator.transitionViaMVAOption(roomDir, 0);
|
|
122
|
+
assert.equal(r2.ok, false, '0 is rejected');
|
|
123
|
+
|
|
124
|
+
const r3 = operator.transitionViaMVAOption(roomDir, null);
|
|
125
|
+
assert.equal(r3.ok, false, 'null is rejected');
|
|
126
|
+
} finally {
|
|
127
|
+
cleanupTempRoom(roomDir);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ---------- Task 1 Test 5: no regression on OPERATORS or rules ----------
|
|
132
|
+
|
|
133
|
+
test('Test 5a: OPERATORS array is unchanged (5 entries; canonical order)', () => {
|
|
134
|
+
assert.deepEqual(operator.OPERATORS, [
|
|
135
|
+
'JUST_TALK',
|
|
136
|
+
'EXPLORE_CAPTURE',
|
|
137
|
+
'BUILD_ROOM',
|
|
138
|
+
'METHODOLOGY',
|
|
139
|
+
'DECISION_GATE',
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('Test 5b: TRANSITION_RULES has 9 entries (Phase 99 D-08 invariant preserved)', () => {
|
|
144
|
+
assert.equal(operator.TRANSITION_RULES.length, 9);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('Test 5c: existing transition rules still work (JUST_TALK -> EXPLORE_CAPTURE on user_message)', () => {
|
|
148
|
+
const roomDir = makeTempRoom();
|
|
149
|
+
try {
|
|
150
|
+
const r = operator.transition(roomDir, 'EXPLORE_CAPTURE', 'user_message');
|
|
151
|
+
assert.equal(r.success, true);
|
|
152
|
+
assert.equal(r.current, 'EXPLORE_CAPTURE');
|
|
153
|
+
} finally {
|
|
154
|
+
cleanupTempRoom(roomDir);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('Test 5d: transitionViaMVAOption exported as top-level function', () => {
|
|
159
|
+
assert.equal(typeof operator.transitionViaMVAOption, 'function');
|
|
160
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Phase 120-02 Wave 2 Task 1 -- D-19 per-detector dismissal-rate canary. Per
|
|
4
|
+
* CONTEXT.md D-19 verbatim:
|
|
5
|
+
*
|
|
6
|
+
* Track per-detector dismissal rate over a 100-fire rolling window. If the
|
|
7
|
+
* rate crosses 30%, auto-throttle that detector to soft-fire-only until
|
|
8
|
+
* manually reviewed. This is the user-telling-us-with-the-dismiss-button
|
|
9
|
+
* signal -- catches drift before it shows up in any other metric.
|
|
10
|
+
*
|
|
11
|
+
* STATIC thresholds for v1.13.0 (ML-tuned weights deferred to v1.14.0 per
|
|
12
|
+
* CONTEXT.md OUT OF SCOPE). The auto-throttle recovery surface (the user-facing
|
|
13
|
+
* "this detector needs review" affordance) is deferred to Phase 121
|
|
14
|
+
* housekeeping per CONTEXT.md Deferred Ideas. v1.13.0 only emits the
|
|
15
|
+
* `breakthrough_throttled` event for /mos:doctor to find.
|
|
16
|
+
*
|
|
17
|
+
* Canon Part 8 + Part 9: ALL reads via navigation.cjs::findRecentChanges
|
|
18
|
+
* chokepoint (Canon Part 9 D-06); pure LOCAL; no Brain coupling.
|
|
19
|
+
*
|
|
20
|
+
* Em-dash HARD RULE (CLAUDE.md feedback_no_emdashes): use "--" not U+2014.
|
|
21
|
+
*
|
|
22
|
+
* Pure CJS, node built-ins only, zero deps.
|
|
23
|
+
*
|
|
24
|
+
* Coordination note: scoring.cjs (Plan 120-01 Task 3) also exports the D-19
|
|
25
|
+
* canary constants + isThrottledKind. canary.cjs is the PRIMARY ownership for
|
|
26
|
+
* v1.13.0+ -- scoring.cjs's copy is kept byte-stable for backwards compat with
|
|
27
|
+
* Plan 120-01 acceptance; downstream code should import from canary.cjs going
|
|
28
|
+
* forward. The two surfaces share the same threshold values verbatim.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const navigation = require('../navigation.cjs');
|
|
32
|
+
|
|
33
|
+
// CONTEXT.md D-19 verbatim lock. Object.freeze is not strictly required for
|
|
34
|
+
// primitive numbers (they are immutable by value) but the named exports +
|
|
35
|
+
// scaffold-harness shell grep + unit test (Test 15) form the three-layer
|
|
36
|
+
// invariant defense.
|
|
37
|
+
const D19_DISMISSAL_THRESHOLD = 0.30;
|
|
38
|
+
const D19_FIRE_WINDOW = 100;
|
|
39
|
+
const D19_MIN_SAMPLE = 10;
|
|
40
|
+
|
|
41
|
+
// 90-day search window for canary lookups. Older events stay in log but the
|
|
42
|
+
// canary considers them stale (mirrors the scoring.cjs PRIOR_WINDOW_DAYS=90).
|
|
43
|
+
const CANARY_WINDOW_MS = 90 * 24 * 3600 * 1000;
|
|
44
|
+
const CANARY_QUERY_LIMIT = 500;
|
|
45
|
+
|
|
46
|
+
// computeDismissalRate -- D-19 canary computation for a single detector kind.
|
|
47
|
+
//
|
|
48
|
+
// Returns { rate, sample_size, throttled }:
|
|
49
|
+
// - rate: dismissed_in_window / sample_size (or 0 if sample_size = 0).
|
|
50
|
+
// - sample_size: number of recent surfaces for kind, capped at D19_FIRE_WINDOW.
|
|
51
|
+
// - throttled: true IFF sample_size >= D19_MIN_SAMPLE AND rate > D19_DISMISSAL_THRESHOLD.
|
|
52
|
+
//
|
|
53
|
+
// Implementation:
|
|
54
|
+
// 1. Read recent breakthrough_surfaced events for the kind (most-recent first).
|
|
55
|
+
// 2. Take the top D19_FIRE_WINDOW (the rolling window).
|
|
56
|
+
// 3. Read recent breakthrough_dismissed events; match against the in-window
|
|
57
|
+
// surfaced ids (a dismiss for an out-of-window fire is noise).
|
|
58
|
+
// 4. Apply the D19_MIN_SAMPLE floor before throttling kicks in (1-of-2 = 50%
|
|
59
|
+
// would otherwise trip the 30% canary on essentially noise).
|
|
60
|
+
//
|
|
61
|
+
// Graceful degradation: missing/invalid kind or db -> returns the zero-state
|
|
62
|
+
// object. Chokepoint throw -> same. Fail-open: never lock out a detector on
|
|
63
|
+
// infra hiccup.
|
|
64
|
+
function computeDismissalRate(kind, db) {
|
|
65
|
+
try {
|
|
66
|
+
if (!kind || !db) return { rate: 0, sample_size: 0, throttled: false };
|
|
67
|
+
const since = Date.now() - CANARY_WINDOW_MS;
|
|
68
|
+
const surfaced = navigation.findRecentChanges(db, since, {
|
|
69
|
+
eventType: 'breakthrough_surfaced',
|
|
70
|
+
limit: CANARY_QUERY_LIMIT,
|
|
71
|
+
}) || [];
|
|
72
|
+
const dismissed = navigation.findRecentChanges(db, since, {
|
|
73
|
+
eventType: 'breakthrough_dismissed',
|
|
74
|
+
limit: CANARY_QUERY_LIMIT,
|
|
75
|
+
}) || [];
|
|
76
|
+
const kindSurfaced = surfaced
|
|
77
|
+
.filter(function (e) { return e && e.properties && e.properties.kind === kind; })
|
|
78
|
+
.slice(0, D19_FIRE_WINDOW);
|
|
79
|
+
const sample = kindSurfaced.length;
|
|
80
|
+
const fireIds = new Set(
|
|
81
|
+
kindSurfaced
|
|
82
|
+
.map(function (e) { return e.properties && e.properties.breakthrough_id; })
|
|
83
|
+
.filter(function (id) { return typeof id === 'string' && id.length > 0; })
|
|
84
|
+
);
|
|
85
|
+
const dismissedInWindow = dismissed.filter(function (e) {
|
|
86
|
+
return e && e.properties && e.properties.kind === kind &&
|
|
87
|
+
typeof e.properties.breakthrough_id === 'string' &&
|
|
88
|
+
fireIds.has(e.properties.breakthrough_id);
|
|
89
|
+
}).length;
|
|
90
|
+
const rate = sample > 0 ? dismissedInWindow / sample : 0;
|
|
91
|
+
const throttled = sample >= D19_MIN_SAMPLE && rate > D19_DISMISSAL_THRESHOLD;
|
|
92
|
+
return { rate: rate, sample_size: sample, throttled: throttled };
|
|
93
|
+
} catch (_e) {
|
|
94
|
+
return { rate: 0, sample_size: 0, throttled: false };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// isThrottled -- thin convenience wrapper. Use computeDismissalRate when the
|
|
99
|
+
// caller also needs the rate + sample_size scalars (e.g., for breakthrough_throttled
|
|
100
|
+
// event payload). Per-kind isolation is enforced by the kind filter inside
|
|
101
|
+
// computeDismissalRate -- one drifting detector cannot throttle siblings.
|
|
102
|
+
function isThrottled(kind, db) {
|
|
103
|
+
return computeDismissalRate(kind, db).throttled;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// emitThrottleEvent -- writes a breakthrough_throttled memory_event via the
|
|
107
|
+
// navigation.cjs chokepoint. Payload carries the canary scalars + a wall-clock
|
|
108
|
+
// timestamp + a source_path so /mos:doctor can find throttled detectors at
|
|
109
|
+
// session-start.
|
|
110
|
+
//
|
|
111
|
+
// The auto-throttle recovery surface (the user-facing affordance for unthrottling)
|
|
112
|
+
// is DEFERRED to Phase 121 housekeeping. v1.13.0 just persists the signal.
|
|
113
|
+
function emitThrottleEvent(kind, db, result) {
|
|
114
|
+
const safeResult = (result && typeof result === 'object') ? result : { rate: 0, sample_size: 0 };
|
|
115
|
+
const payload = {
|
|
116
|
+
kind: kind,
|
|
117
|
+
rate: typeof safeResult.rate === 'number' ? safeResult.rate : 0,
|
|
118
|
+
sample_size: typeof safeResult.sample_size === 'number' ? safeResult.sample_size : 0,
|
|
119
|
+
threshold: D19_DISMISSAL_THRESHOLD,
|
|
120
|
+
throttled_at: Date.now(),
|
|
121
|
+
source_path: 'system:breakthrough-canary',
|
|
122
|
+
created_by: 'system',
|
|
123
|
+
};
|
|
124
|
+
return navigation.logMemoryEvent(db, 'breakthrough_throttled', payload);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
computeDismissalRate: computeDismissalRate,
|
|
129
|
+
isThrottled: isThrottled,
|
|
130
|
+
emitThrottleEvent: emitThrottleEvent,
|
|
131
|
+
D19_DISMISSAL_THRESHOLD: D19_DISMISSAL_THRESHOLD,
|
|
132
|
+
D19_FIRE_WINDOW: D19_FIRE_WINDOW,
|
|
133
|
+
D19_MIN_SAMPLE: D19_MIN_SAMPLE,
|
|
134
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Phase 120-02 Wave 2 Task 1 -- D-19 per-detector dismissal-rate canary unit tests.
|
|
5
|
+
*
|
|
6
|
+
* Tests 15-23 cover:
|
|
7
|
+
* - D19 constants verbatim (D19_DISMISSAL_THRESHOLD=0.30 / D19_FIRE_WINDOW=100 /
|
|
8
|
+
* D19_MIN_SAMPLE=10)
|
|
9
|
+
* - computeDismissalRate empty / below-sample / happy-path
|
|
10
|
+
* - isThrottled per-kind isolation
|
|
11
|
+
* - emitThrottleEvent writes breakthrough_throttled via navigation.logMemoryEvent
|
|
12
|
+
* - Canon Part 8 source-grep + em-dash HARD RULE (Tests 22 + 23)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const test = require('node:test');
|
|
16
|
+
const { strict: assert } = require('node:assert');
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const os = require('node:os');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
|
|
21
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
22
|
+
const canary = require(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'canary.cjs'));
|
|
23
|
+
const navigation = require(path.join(REPO_ROOT, 'lib', 'core', 'navigation.cjs'));
|
|
24
|
+
const { openRoomDb } = require(path.join(REPO_ROOT, 'lib', 'core', 'room-db.cjs'));
|
|
25
|
+
|
|
26
|
+
function makeTmpDb(prefix) {
|
|
27
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
28
|
+
const db = openRoomDb(dir);
|
|
29
|
+
return { dir, db };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function seedSurfacedDismissedPair(db, kind, breakthroughId, dismissed) {
|
|
33
|
+
const surfacedR = navigation.logMemoryEvent(db, 'breakthrough_surfaced', {
|
|
34
|
+
breakthrough_id: breakthroughId,
|
|
35
|
+
kind: kind,
|
|
36
|
+
source_path: 'system:test',
|
|
37
|
+
created_by: 'system',
|
|
38
|
+
});
|
|
39
|
+
assert.equal(surfacedR.ok, true);
|
|
40
|
+
if (dismissed) {
|
|
41
|
+
const dismR = navigation.logMemoryEvent(db, 'breakthrough_dismissed', {
|
|
42
|
+
breakthrough_id: breakthroughId,
|
|
43
|
+
kind: kind,
|
|
44
|
+
source_path: 'system:test',
|
|
45
|
+
created_by: 'system',
|
|
46
|
+
});
|
|
47
|
+
assert.equal(dismR.ok, true);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test('120-02 Task 1 Test 15: D-19 constants verbatim', () => {
|
|
52
|
+
assert.equal(canary.D19_DISMISSAL_THRESHOLD, 0.30);
|
|
53
|
+
assert.equal(canary.D19_FIRE_WINDOW, 100);
|
|
54
|
+
assert.equal(canary.D19_MIN_SAMPLE, 10);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('120-02 Task 1 Test 16: computeDismissalRate on empty db -> rate=0 sample=0 throttled=false', () => {
|
|
58
|
+
const { dir, db } = makeTmpDb('p120-02-t1-t16-');
|
|
59
|
+
const r = canary.computeDismissalRate('convergence', db);
|
|
60
|
+
assert.equal(r.rate, 0);
|
|
61
|
+
assert.equal(r.sample_size, 0);
|
|
62
|
+
assert.equal(r.throttled, false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('120-02 Task 1 Test 17: computeDismissalRate below D19_MIN_SAMPLE -> throttled stays false', () => {
|
|
66
|
+
const { dir, db } = makeTmpDb('p120-02-t1-t17-');
|
|
67
|
+
// 5 surfaced + 2 dismissed; 40% rate; but sample < D19_MIN_SAMPLE=10 -> not throttled.
|
|
68
|
+
for (let i = 0; i < 3; i++) seedSurfacedDismissedPair(db, 'convergence', 'bk:' + i, false);
|
|
69
|
+
for (let i = 3; i < 5; i++) seedSurfacedDismissedPair(db, 'convergence', 'bk:' + i, true);
|
|
70
|
+
const r = canary.computeDismissalRate('convergence', db);
|
|
71
|
+
assert.equal(r.sample_size, 5);
|
|
72
|
+
// 2 out of 5 = 0.4 rate, but throttled gates on sample size.
|
|
73
|
+
assert.ok(r.rate > 0);
|
|
74
|
+
assert.equal(r.throttled, false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('120-02 Task 1 Test 18: computeDismissalRate with sample >= floor + rate above threshold -> throttled=true', () => {
|
|
78
|
+
const { dir, db } = makeTmpDb('p120-02-t1-t18-');
|
|
79
|
+
// 12 surfaced + 5 dismissed (5/12 ~= 0.42 > 0.30); sample 12 > 10 floor.
|
|
80
|
+
for (let i = 0; i < 7; i++) seedSurfacedDismissedPair(db, 'convergence', 'bk:s' + i, false);
|
|
81
|
+
for (let i = 0; i < 5; i++) seedSurfacedDismissedPair(db, 'convergence', 'bk:d' + i, true);
|
|
82
|
+
const r = canary.computeDismissalRate('convergence', db);
|
|
83
|
+
assert.equal(r.sample_size, 12);
|
|
84
|
+
assert.ok(r.rate > 0.30, 'rate should exceed 0.30 threshold');
|
|
85
|
+
assert.equal(r.throttled, true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('120-02 Task 1 Test 19: isThrottled returns true when computeDismissalRate.throttled is true', () => {
|
|
89
|
+
const { dir, db } = makeTmpDb('p120-02-t1-t19-');
|
|
90
|
+
for (let i = 0; i < 7; i++) seedSurfacedDismissedPair(db, 'convergence', 'bk:s' + i, false);
|
|
91
|
+
for (let i = 0; i < 5; i++) seedSurfacedDismissedPair(db, 'convergence', 'bk:d' + i, true);
|
|
92
|
+
assert.equal(canary.isThrottled('convergence', db), true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('120-02 Task 1 Test 20: isThrottled isolated per-kind -- throttled convergence does NOT throttle cross_domain_analogy', () => {
|
|
96
|
+
const { dir, db } = makeTmpDb('p120-02-t1-t20-');
|
|
97
|
+
// Throttle convergence.
|
|
98
|
+
for (let i = 0; i < 7; i++) seedSurfacedDismissedPair(db, 'convergence', 'bk:cs' + i, false);
|
|
99
|
+
for (let i = 0; i < 5; i++) seedSurfacedDismissedPair(db, 'convergence', 'bk:cd' + i, true);
|
|
100
|
+
// cross_domain_analogy has no events.
|
|
101
|
+
assert.equal(canary.isThrottled('convergence', db), true);
|
|
102
|
+
assert.equal(canary.isThrottled('cross_domain_analogy', db), false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('120-02 Task 1 Test 21: emitThrottleEvent writes breakthrough_throttled memory_event', () => {
|
|
106
|
+
const { dir, db } = makeTmpDb('p120-02-t1-t21-');
|
|
107
|
+
const rate = { rate: 0.35, sample_size: 100, throttled: true };
|
|
108
|
+
const r = canary.emitThrottleEvent('convergence', db, rate);
|
|
109
|
+
assert.equal(r.ok, true);
|
|
110
|
+
assert.match(r.eventId, /^memory_event:breakthrough_throttled:/);
|
|
111
|
+
// Verify the event landed.
|
|
112
|
+
const found = navigation.findRecentChanges(db, 0, { eventType: 'breakthrough_throttled', limit: 10 });
|
|
113
|
+
assert.equal(found.length, 1);
|
|
114
|
+
assert.equal(found[0].properties.kind, 'convergence');
|
|
115
|
+
assert.equal(found[0].properties.rate, 0.35);
|
|
116
|
+
assert.equal(found[0].properties.threshold, 0.30);
|
|
117
|
+
assert.equal(found[0].properties.sample_size, 100);
|
|
118
|
+
assert.ok(typeof found[0].properties.throttled_at === 'number');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('120-02 Task 1 Test 22: Canon Part 8 source-grep -- zero Brain coupling in canary.cjs', () => {
|
|
122
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'canary.cjs'), 'utf8');
|
|
123
|
+
assert.equal(/require\s*\(\s*['"][^'"]*brain-client[^'"]*['"]\s*\)/.test(src), false,
|
|
124
|
+
'canary.cjs must not require brain-client');
|
|
125
|
+
assert.equal(/fetch\s*\(\s*['"][^'"]*brain\.mindrian/.test(src), false,
|
|
126
|
+
'canary.cjs must not fetch brain.mindrian.*');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('120-02 Task 1 Test 23: em-dash HARD RULE -- zero U+2014 in canary.cjs', () => {
|
|
130
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'canary.cjs'), 'utf8');
|
|
131
|
+
let count = 0;
|
|
132
|
+
for (const ch of src) {
|
|
133
|
+
if (ch.charCodeAt(0) === 0x2014) count++;
|
|
134
|
+
}
|
|
135
|
+
assert.equal(count, 0, 'canary.cjs must contain zero U+2014 em-dash characters');
|
|
136
|
+
});
|