@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.
Files changed (199) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.mcp.json +6 -1
  3. package/CHANGELOG.md +31 -0
  4. package/README.md +51 -56
  5. package/bin/mindrian-brain-mcp-client.cjs +152 -0
  6. package/commands/act.md +1 -0
  7. package/commands/admin.md +1 -0
  8. package/commands/analyze-needs.md +2 -0
  9. package/commands/analyze-systems.md +2 -0
  10. package/commands/analyze-timing.md +2 -0
  11. package/commands/auto-explore.md +2 -0
  12. package/commands/beautiful-question.md +2 -0
  13. package/commands/brain-derive.md +2 -0
  14. package/commands/build-knowledge.md +2 -0
  15. package/commands/build-thesis.md +2 -0
  16. package/commands/causal.md +2 -0
  17. package/commands/challenge-assumptions.md +2 -0
  18. package/commands/compare-ventures.md +2 -0
  19. package/commands/dashboard.md +2 -1
  20. package/commands/deep-grade.md +2 -0
  21. package/commands/diagnose.md +21 -1
  22. package/commands/diagnostics.md +14 -3
  23. package/commands/doctor.md +6 -2
  24. package/commands/dogfood-flush.md +92 -0
  25. package/commands/dominant-designs.md +2 -0
  26. package/commands/explain-decision.md +2 -0
  27. package/commands/explore-domains.md +2 -0
  28. package/commands/explore-futures.md +2 -0
  29. package/commands/explore-trends.md +2 -0
  30. package/commands/export.md +1 -0
  31. package/commands/feynman-timeline-refresh.md +2 -0
  32. package/commands/file-meeting.md +2 -0
  33. package/commands/find-analogies.md +1 -0
  34. package/commands/find-bottlenecks.md +2 -0
  35. package/commands/find-connections.md +2 -0
  36. package/commands/funding.md +1 -0
  37. package/commands/grade.md +2 -0
  38. package/commands/graph.md +1 -0
  39. package/commands/hat-briefing.md +1 -0
  40. package/commands/heal.md +22 -170
  41. package/commands/help.md +54 -334
  42. package/commands/hmi-status.md +23 -144
  43. package/commands/jtbd.md +1 -0
  44. package/commands/leadership.md +2 -0
  45. package/commands/lean-canvas.md +2 -0
  46. package/commands/macro-trends.md +2 -0
  47. package/commands/map-unknowns.md +2 -0
  48. package/commands/memory.md +1 -0
  49. package/commands/models.md +1 -0
  50. package/commands/mos-reason.md +2 -0
  51. package/commands/mos.md +139 -0
  52. package/commands/mullins.md +2 -0
  53. package/commands/mva-brief.md +2 -0
  54. package/commands/mva-option.md +2 -0
  55. package/commands/new-project.md +2 -0
  56. package/commands/onboard.md +20 -7
  57. package/commands/operator.md +1 -0
  58. package/commands/opportunities.md +1 -0
  59. package/commands/organize.md +22 -469
  60. package/commands/persona.md +1 -0
  61. package/commands/pipeline.md +2 -0
  62. package/commands/present.md +1 -0
  63. package/commands/publish.md +2 -0
  64. package/commands/query.md +24 -102
  65. package/commands/radar.md +2 -0
  66. package/commands/reanalyze.md +1 -0
  67. package/commands/research.md +2 -0
  68. package/commands/room.md +2 -0
  69. package/commands/rooms.md +1 -0
  70. package/commands/root-cause.md +2 -0
  71. package/commands/rs-experts.md +1 -0
  72. package/commands/rs-explain.md +1 -0
  73. package/commands/rs-fetch.md +1 -0
  74. package/commands/rs-thesis.md +1 -0
  75. package/commands/scenario-plan.md +2 -0
  76. package/commands/scheduled-tasks.md +1 -0
  77. package/commands/score-innovation.md +2 -0
  78. package/commands/scout.md +1 -0
  79. package/commands/setup.md +2 -0
  80. package/commands/snapshot.md +2 -0
  81. package/commands/speakers.md +1 -0
  82. package/commands/splash.md +5 -2
  83. package/commands/status.md +1 -0
  84. package/commands/structure-argument.md +2 -0
  85. package/commands/suggest-next.md +2 -0
  86. package/commands/systems-thinking.md +2 -0
  87. package/commands/think-hats.md +2 -0
  88. package/commands/update.md +2 -0
  89. package/commands/user-needs.md +2 -0
  90. package/commands/validate.md +2 -0
  91. package/commands/value-proposition.md +2 -0
  92. package/commands/vault.md +2 -0
  93. package/commands/visualize.md +24 -29
  94. package/commands/whitespace.md +2 -1
  95. package/commands/wiki.md +1 -0
  96. package/hooks/hooks.json +22 -88
  97. package/lib/agents/auto-explore-agent.cjs +82 -0
  98. package/lib/core/breakthrough/canary.cjs +134 -0
  99. package/lib/core/breakthrough/canary.test.cjs +136 -0
  100. package/lib/core/breakthrough/detectors.cjs +359 -0
  101. package/lib/core/breakthrough/detectors.test.cjs +333 -0
  102. package/lib/core/breakthrough/ethics-fence.cjs +127 -0
  103. package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
  104. package/lib/core/breakthrough/resurfacing.cjs +150 -0
  105. package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
  106. package/lib/core/breakthrough/review-queue.cjs +154 -0
  107. package/lib/core/breakthrough/review-queue.test.cjs +160 -0
  108. package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
  109. package/lib/core/breakthrough/scanner.cjs +426 -0
  110. package/lib/core/breakthrough/scanner.test.cjs +267 -0
  111. package/lib/core/breakthrough/schema.cjs +164 -0
  112. package/lib/core/breakthrough/schema.test.cjs +256 -0
  113. package/lib/core/breakthrough/scoring.cjs +293 -0
  114. package/lib/core/breakthrough/scoring.test.cjs +423 -0
  115. package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
  116. package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
  117. package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
  118. package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
  119. package/lib/core/directive-envelope.cjs +175 -0
  120. package/lib/core/directive-envelope.test.cjs +225 -0
  121. package/lib/core/doctor/class-m-brain-smoke.cjs +278 -0
  122. package/lib/core/doctor/class-m-brain-smoke.test.cjs +310 -0
  123. package/lib/core/first-touch-version-stamper.cjs +113 -0
  124. package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
  125. package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
  126. package/lib/core/llm-name-suggester.cjs +194 -0
  127. package/lib/core/llm-name-suggester.test.cjs +132 -0
  128. package/lib/core/mcp-profiles.cjs +1 -1
  129. package/lib/core/migration-snapshot.cjs +172 -0
  130. package/lib/core/migration-snapshot.test.cjs +174 -0
  131. package/lib/core/mindrian-brain-shim.test.cjs +214 -0
  132. package/lib/core/mva-orchestrator.cjs +41 -0
  133. package/lib/core/mva-telemetry.cjs +31 -143
  134. package/lib/core/navigation/edges.cjs +35 -0
  135. package/lib/core/navigation/memory-events.cjs +126 -0
  136. package/lib/core/room-auto-create.cjs +318 -0
  137. package/lib/core/room-auto-create.test.cjs +198 -0
  138. package/lib/core/room-discard-cascade.cjs +225 -0
  139. package/lib/core/room-discard-cascade.test.cjs +135 -0
  140. package/lib/core/room-name-validator.cjs +132 -0
  141. package/lib/core/room-name-validator.test.cjs +156 -0
  142. package/lib/core/room-naming-selector.cjs +357 -0
  143. package/lib/core/room-naming-selector.test.cjs +277 -0
  144. package/lib/core/room-receipt-emit.cjs +63 -0
  145. package/lib/core/room-skeleton-scaffold.cjs +315 -0
  146. package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
  147. package/lib/core/rs-nl-to-query.cjs +1 -1
  148. package/lib/core/stale-copy-scanner.cjs +190 -0
  149. package/lib/core/state-aware-router.cjs +78 -0
  150. package/lib/core/telemetry/schema.cjs +168 -0
  151. package/lib/core/telemetry/schema.test.cjs +124 -0
  152. package/lib/core/telemetry/validator.cjs +200 -0
  153. package/lib/core/telemetry/validator.test.cjs +188 -0
  154. package/lib/core/telemetry/writer.cjs +141 -0
  155. package/lib/core/telemetry/writer.test.cjs +331 -0
  156. package/lib/core/terminal-capability.cjs +88 -0
  157. package/lib/core/tier0-messaging.cjs +109 -0
  158. package/lib/core/tier0-messaging.test.cjs +218 -0
  159. package/lib/core/venture-shape-nudge.cjs +163 -0
  160. package/lib/core/venture-shape-nudge.test.cjs +161 -0
  161. package/lib/core/visual-ops.cjs +70 -2
  162. package/lib/hmi/selector-dispatcher.cjs +90 -1
  163. package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
  164. package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
  165. package/lib/memory/body-shape-coverage.test.cjs +268 -0
  166. package/lib/memory/brain-derivation-graceful-degradation.test.cjs +2 -2
  167. package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
  168. package/lib/memory/first-touch-version.test.cjs +198 -0
  169. package/lib/memory/help-coverage.test.cjs +108 -0
  170. package/lib/memory/help-renderer.test.cjs +145 -0
  171. package/lib/memory/mos-status-renderer.test.cjs +2 -2
  172. package/lib/memory/navigation-engine-core.test.cjs +1 -1
  173. package/lib/memory/palette-consistency.test.cjs +127 -0
  174. package/lib/memory/pending-tension-store.cjs +80 -0
  175. package/lib/memory/render-v2-disposition.test.cjs +199 -0
  176. package/lib/memory/run-feynman-tests.cjs +223 -0
  177. package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
  178. package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
  179. package/lib/memory/soft-alias.test.cjs +144 -0
  180. package/lib/memory/stale-copy-scanner.test.cjs +291 -0
  181. package/lib/memory/state-aware-router.test.cjs +90 -0
  182. package/lib/memory/statusline-two-row.test.cjs +338 -0
  183. package/lib/memory/terminal-capability.test.cjs +155 -0
  184. package/lib/render/ROOM.md +74 -22
  185. package/lib/sessionstart/budget-compressor.cjs +130 -0
  186. package/lib/sessionstart/contributor-interface.cjs +134 -0
  187. package/lib/sessionstart/contributor-isolator.cjs +128 -0
  188. package/lib/sessionstart/precedence-ladder.cjs +47 -0
  189. package/lib/statusline/governing-thought-truncator.cjs +45 -0
  190. package/lib/statusline/two-row-renderer.cjs +186 -0
  191. package/lib/statusline/version-resolver.cjs +81 -0
  192. package/package.json +1 -1
  193. package/references/visual/ROOM.md +55 -0
  194. package/references/visual/palette.json +54 -0
  195. package/skills/larry-personality/SKILL.md +34 -0
  196. package/skills/ui-system/SKILL.md +109 -1
  197. package/skills/ui-system/rules/dual-palette.md +156 -0
  198. package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
  199. 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/operator-update.cjs\"",
22
- "timeout": 3000
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/restore-post-compact-context.cjs\"",
112
- "timeout": 3000
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/sessionstart-npm-reconcile.cjs\"",
122
- "timeout": 60000,
123
- "async": true,
124
- "statusMessage": "Reconciling dependencies..."
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
+ });