@mindrian_os/install 1.13.0-beta.17 → 1.13.0-beta.21
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/.mcp.json +6 -1
- package/CHANGELOG.md +31 -0
- package/README.md +51 -56
- package/bin/mindrian-brain-mcp-client.cjs +152 -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 +6 -2
- 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 +2 -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 +2 -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 +2 -0
- package/commands/mva-option.md +2 -0
- package/commands/new-project.md +2 -0
- package/commands/onboard.md +20 -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 +22 -88
- package/lib/agents/auto-explore-agent.cjs +82 -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/directive-envelope.cjs +175 -0
- package/lib/core/directive-envelope.test.cjs +225 -0
- package/lib/core/doctor/class-m-brain-smoke.cjs +278 -0
- package/lib/core/doctor/class-m-brain-smoke.test.cjs +310 -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/mcp-profiles.cjs +1 -1
- package/lib/core/migration-snapshot.cjs +172 -0
- package/lib/core/migration-snapshot.test.cjs +174 -0
- package/lib/core/mindrian-brain-shim.test.cjs +214 -0
- package/lib/core/mva-orchestrator.cjs +41 -0
- package/lib/core/mva-telemetry.cjs +31 -143
- package/lib/core/navigation/edges.cjs +35 -0
- package/lib/core/navigation/memory-events.cjs +126 -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/rs-nl-to-query.cjs +1 -1
- 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 +200 -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/tier0-messaging.cjs +109 -0
- package/lib/core/tier0-messaging.test.cjs +218 -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/brain-derivation-graceful-degradation.test.cjs +2 -2
- 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/mos-status-renderer.test.cjs +2 -2
- package/lib/memory/navigation-engine-core.test.cjs +1 -1
- 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 +223 -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/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
package/hooks/hooks.json
CHANGED
|
@@ -18,88 +18,10 @@
|
|
|
18
18
|
"hooks": [
|
|
19
19
|
{
|
|
20
20
|
"type": "command",
|
|
21
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/
|
|
22
|
-
"timeout":
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"matcher": "startup|clear|compact",
|
|
28
|
-
"hooks": [
|
|
29
|
-
{
|
|
30
|
-
"type": "command",
|
|
31
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/memory-resume-nudge.cjs\"",
|
|
32
|
-
"timeout": 3000
|
|
33
|
-
}
|
|
34
|
-
]
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"matcher": "startup|clear|compact",
|
|
38
|
-
"hooks": [
|
|
39
|
-
{
|
|
40
|
-
"type": "command",
|
|
41
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/migrate-stale-user-settings.cjs\" --auto --quiet",
|
|
42
|
-
"timeout": 2000
|
|
43
|
-
}
|
|
44
|
-
]
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
"matcher": "startup|clear|compact",
|
|
48
|
-
"hooks": [
|
|
49
|
-
{
|
|
50
|
-
"type": "command",
|
|
51
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/statusline-fallback-echo.cjs\"",
|
|
52
|
-
"timeout": 2000
|
|
53
|
-
}
|
|
54
|
-
]
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
"matcher": "startup|clear|compact",
|
|
58
|
-
"hooks": [
|
|
59
|
-
{
|
|
60
|
-
"type": "command",
|
|
61
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/check-onboard-statusline.cjs\"",
|
|
62
|
-
"timeout": 2000
|
|
63
|
-
}
|
|
64
|
-
]
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
"matcher": "startup|clear|compact",
|
|
68
|
-
"hooks": [
|
|
69
|
-
{
|
|
70
|
-
"type": "command",
|
|
71
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/preflight-tension-surface.cjs\"",
|
|
72
|
-
"timeout": 3000
|
|
73
|
-
}
|
|
74
|
-
]
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
"matcher": "startup|clear|compact",
|
|
78
|
-
"hooks": [
|
|
79
|
-
{
|
|
80
|
-
"type": "command",
|
|
81
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/preflight-doctor.cjs\"",
|
|
82
|
-
"timeout": 2000
|
|
83
|
-
}
|
|
84
|
-
]
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
"matcher": "startup|clear|compact",
|
|
88
|
-
"hooks": [
|
|
89
|
-
{
|
|
90
|
-
"type": "command",
|
|
91
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/preflight-release-drift.cjs\"",
|
|
92
|
-
"timeout": 2000
|
|
93
|
-
}
|
|
94
|
-
]
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
"matcher": "startup|clear|compact",
|
|
98
|
-
"hooks": [
|
|
99
|
-
{
|
|
100
|
-
"type": "command",
|
|
101
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/preflight-auto-explore.cjs\"",
|
|
102
|
-
"timeout": 3000
|
|
21
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/sessionstart-coordinator.cjs\"",
|
|
22
|
+
"timeout": 10000,
|
|
23
|
+
"async": false,
|
|
24
|
+
"statusMessage": "Loading room context..."
|
|
103
25
|
}
|
|
104
26
|
]
|
|
105
27
|
},
|
|
@@ -108,8 +30,10 @@
|
|
|
108
30
|
"hooks": [
|
|
109
31
|
{
|
|
110
32
|
"type": "command",
|
|
111
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/
|
|
112
|
-
"timeout":
|
|
33
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/sessionstart-npm-reconcile.cjs\"",
|
|
34
|
+
"timeout": 60000,
|
|
35
|
+
"async": true,
|
|
36
|
+
"statusMessage": "Reconciling dependencies..."
|
|
113
37
|
}
|
|
114
38
|
]
|
|
115
39
|
},
|
|
@@ -118,10 +42,10 @@
|
|
|
118
42
|
"hooks": [
|
|
119
43
|
{
|
|
120
44
|
"type": "command",
|
|
121
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/
|
|
122
|
-
"timeout":
|
|
123
|
-
"async":
|
|
124
|
-
"statusMessage": "
|
|
45
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/check-pending-breakthrough.cjs\"",
|
|
46
|
+
"timeout": 2000,
|
|
47
|
+
"async": false,
|
|
48
|
+
"statusMessage": "Scanning for breakthroughs..."
|
|
125
49
|
}
|
|
126
50
|
]
|
|
127
51
|
}
|
|
@@ -280,6 +204,16 @@
|
|
|
280
204
|
"timeout": 3000
|
|
281
205
|
}
|
|
282
206
|
]
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"matcher": "SlashCommand",
|
|
210
|
+
"hooks": [
|
|
211
|
+
{
|
|
212
|
+
"type": "command",
|
|
213
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/telemetry-command-invocation.cjs\"",
|
|
214
|
+
"timeout": 5000
|
|
215
|
+
}
|
|
216
|
+
]
|
|
283
217
|
}
|
|
284
218
|
],
|
|
285
219
|
"UserPromptSubmit": [
|
|
@@ -46,6 +46,26 @@ const path = require('node:path');
|
|
|
46
46
|
const crypto = require('node:crypto');
|
|
47
47
|
const store = require('../memory/explored-materials-store.cjs');
|
|
48
48
|
|
|
49
|
+
// Phase 121-02 D-06: vocabulary maps for unified auto_explore_decision emit.
|
|
50
|
+
// Phase 117 source_pipeline {domain, reverse-salients, cross-domain}
|
|
51
|
+
// -> unified finding_type {whitespace, reverse_salient, cross_domain}.
|
|
52
|
+
// Phase 117 F.1 verbs {EXPLORE, LATER, SKIP}
|
|
53
|
+
// -> unified user_response {kept, redid, ignored}.
|
|
54
|
+
// FREE_TEXT is intentionally EXCLUDED -- it is a system fallback when the
|
|
55
|
+
// navigator types prose; it is not a clean engagement signal.
|
|
56
|
+
// auto_explore_skipped (system-driven suppress paths) is also EXCLUDED --
|
|
57
|
+
// those fire from emitSkipped, never from handleUserResponse.
|
|
58
|
+
const D06_FINDING_TYPE_MAP = Object.freeze({
|
|
59
|
+
'domain': 'whitespace',
|
|
60
|
+
'reverse-salients': 'reverse_salient',
|
|
61
|
+
'cross-domain': 'cross_domain',
|
|
62
|
+
});
|
|
63
|
+
const D06_USER_RESPONSE_MAP = Object.freeze({
|
|
64
|
+
EXPLORE: 'kept',
|
|
65
|
+
LATER: 'redid',
|
|
66
|
+
SKIP: 'ignored',
|
|
67
|
+
});
|
|
68
|
+
|
|
49
69
|
// ---------- Constants ----------
|
|
50
70
|
|
|
51
71
|
const MATERIAL_ID_LEN = 32;
|
|
@@ -773,6 +793,36 @@ function handleUserResponse(args) {
|
|
|
773
793
|
});
|
|
774
794
|
} catch (_e) { /* never throw on telemetry */ }
|
|
775
795
|
|
|
796
|
+
// Phase 121-02 D-06: capture user F.1 decision into the unified
|
|
797
|
+
// ~/.mindrian/telemetry/v1.13/events-YYYY-WNN.jsonl stream (Plan 121-00
|
|
798
|
+
// writer chokepoint). Emit ONLY for the 3 engagement verbs
|
|
799
|
+
// (EXPLORE -> kept, LATER -> redid, SKIP -> ignored). FREE_TEXT is a
|
|
800
|
+
// system fallback and is intentionally excluded. auto_explore_skipped
|
|
801
|
+
// (fingerprint/fire suppress paths) is NEVER routed through this
|
|
802
|
+
// function and therefore cannot emit auto_explore_decision either.
|
|
803
|
+
// Non-blocking: try/catch swallows any writer breach so the handler
|
|
804
|
+
// still returns ok:true to its caller (the F.1 surface).
|
|
805
|
+
try {
|
|
806
|
+
if (D06_USER_RESPONSE_MAP[userResponse]) {
|
|
807
|
+
let writer = null;
|
|
808
|
+
try { writer = require('../core/telemetry/writer.cjs'); } catch (_e) { writer = null; }
|
|
809
|
+
if (writer && typeof writer.emit === 'function') {
|
|
810
|
+
const findingType = D06_FINDING_TYPE_MAP[String(finding.source_pipeline || '')] || 'whitespace';
|
|
811
|
+
const score = (typeof finding.score === 'number' && Number.isFinite(finding.score))
|
|
812
|
+
? finding.score
|
|
813
|
+
: 0;
|
|
814
|
+
const slugSrc = String(roomSlug || (roomDir ? path.basename(roomDir) : '')).slice(0, 256);
|
|
815
|
+
const slugHash = crypto.createHash('sha256').update(slugSrc).digest('hex');
|
|
816
|
+
writer.emit('auto_explore_decision', {
|
|
817
|
+
finding_type: String(findingType).slice(0, 64),
|
|
818
|
+
user_response: D06_USER_RESPONSE_MAP[userResponse],
|
|
819
|
+
domain_match_score: score,
|
|
820
|
+
room_slug_sha256: slugHash,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
} catch (_e) { /* never throw on telemetry */ }
|
|
825
|
+
|
|
776
826
|
return {
|
|
777
827
|
ok: true,
|
|
778
828
|
response: userResponse,
|
|
@@ -1014,6 +1064,36 @@ function _resetDriftCacheForTests() {
|
|
|
1014
1064
|
_driftEmittedThisSession = false;
|
|
1015
1065
|
}
|
|
1016
1066
|
|
|
1067
|
+
/**
|
|
1068
|
+
* Phase 119-00 helper: read $ROOMS_HOME/.rooms/registry.json and return true
|
|
1069
|
+
* iff there is no active room (registry missing OR registry.active is empty
|
|
1070
|
+
* string).
|
|
1071
|
+
*
|
|
1072
|
+
* Used by scripts/auto-explore-fingerprint.cjs to decide whether to call
|
|
1073
|
+
* lib/core/room-auto-create.cjs::autoCreatePlaceholderRoom as a sibling action.
|
|
1074
|
+
*
|
|
1075
|
+
* Per CONTEXT.md D-01: only the "first material lands AND no active room"
|
|
1076
|
+
* configuration triggers auto-create. An active room means the user already
|
|
1077
|
+
* chose a room; never overwrite.
|
|
1078
|
+
*
|
|
1079
|
+
* Pure file-read; no network, no Brain MCP. Canon Part 8 boundary preserved.
|
|
1080
|
+
*
|
|
1081
|
+
* @param {string} roomsHome absolute path to $ROOMS_HOME (or $MINDRIAN_ROOMS_HOME)
|
|
1082
|
+
* @returns {boolean}
|
|
1083
|
+
*/
|
|
1084
|
+
function detectNoActiveRoom(roomsHome) {
|
|
1085
|
+
if (!roomsHome || typeof roomsHome !== 'string') return true;
|
|
1086
|
+
const registryPath = path.join(roomsHome, '.rooms', 'registry.json');
|
|
1087
|
+
try {
|
|
1088
|
+
if (!fs.existsSync(registryPath)) return true;
|
|
1089
|
+
const raw = fs.readFileSync(registryPath, 'utf8');
|
|
1090
|
+
const reg = JSON.parse(raw);
|
|
1091
|
+
return !(reg && typeof reg.active === 'string' && reg.active.length > 0);
|
|
1092
|
+
} catch (_e) {
|
|
1093
|
+
return true; // unreadable registry -> treat as no-active-room (fail-open to auto-create)
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1017
1097
|
// ---------- Module exports ----------
|
|
1018
1098
|
|
|
1019
1099
|
module.exports = {
|
|
@@ -1033,6 +1113,8 @@ module.exports = {
|
|
|
1033
1113
|
emitSkipped,
|
|
1034
1114
|
emitSanitizerHit,
|
|
1035
1115
|
emitBrainCanonDrift,
|
|
1116
|
+
// Phase 119-00 sibling helper (D-01 invariant check for the auto-create hook):
|
|
1117
|
+
detectNoActiveRoom,
|
|
1036
1118
|
// Constants:
|
|
1037
1119
|
BQ_TEMPLATE_REGISTRY,
|
|
1038
1120
|
CANONICAL_CHAIN_ORDER,
|
|
@@ -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
|
+
});
|