@mindrian_os/install 1.13.0-beta.17 → 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.
Files changed (182) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +26 -0
  3. package/commands/act.md +1 -0
  4. package/commands/admin.md +1 -0
  5. package/commands/analyze-needs.md +2 -0
  6. package/commands/analyze-systems.md +2 -0
  7. package/commands/analyze-timing.md +2 -0
  8. package/commands/auto-explore.md +2 -0
  9. package/commands/beautiful-question.md +2 -0
  10. package/commands/brain-derive.md +2 -0
  11. package/commands/build-knowledge.md +2 -0
  12. package/commands/build-thesis.md +2 -0
  13. package/commands/causal.md +2 -0
  14. package/commands/challenge-assumptions.md +2 -0
  15. package/commands/compare-ventures.md +2 -0
  16. package/commands/dashboard.md +2 -1
  17. package/commands/deep-grade.md +2 -0
  18. package/commands/diagnose.md +21 -1
  19. package/commands/diagnostics.md +14 -3
  20. package/commands/doctor.md +4 -1
  21. package/commands/dogfood-flush.md +92 -0
  22. package/commands/dominant-designs.md +2 -0
  23. package/commands/explain-decision.md +2 -0
  24. package/commands/explore-domains.md +2 -0
  25. package/commands/explore-futures.md +2 -0
  26. package/commands/explore-trends.md +2 -0
  27. package/commands/export.md +1 -0
  28. package/commands/feynman-timeline-refresh.md +2 -0
  29. package/commands/file-meeting.md +2 -0
  30. package/commands/find-analogies.md +1 -0
  31. package/commands/find-bottlenecks.md +2 -0
  32. package/commands/find-connections.md +2 -0
  33. package/commands/funding.md +1 -0
  34. package/commands/grade.md +2 -0
  35. package/commands/graph.md +1 -0
  36. package/commands/hat-briefing.md +1 -0
  37. package/commands/heal.md +22 -170
  38. package/commands/help.md +54 -334
  39. package/commands/hmi-status.md +23 -144
  40. package/commands/jtbd.md +1 -0
  41. package/commands/leadership.md +2 -0
  42. package/commands/lean-canvas.md +2 -0
  43. package/commands/macro-trends.md +2 -0
  44. package/commands/map-unknowns.md +2 -0
  45. package/commands/memory.md +1 -0
  46. package/commands/models.md +1 -0
  47. package/commands/mos-reason.md +2 -0
  48. package/commands/mos.md +139 -0
  49. package/commands/mullins.md +2 -0
  50. package/commands/mva-brief.md +2 -0
  51. package/commands/mva-option.md +2 -0
  52. package/commands/new-project.md +2 -0
  53. package/commands/onboard.md +20 -7
  54. package/commands/operator.md +1 -0
  55. package/commands/opportunities.md +1 -0
  56. package/commands/organize.md +22 -469
  57. package/commands/persona.md +1 -0
  58. package/commands/pipeline.md +2 -0
  59. package/commands/present.md +1 -0
  60. package/commands/publish.md +2 -0
  61. package/commands/query.md +24 -102
  62. package/commands/radar.md +2 -0
  63. package/commands/reanalyze.md +1 -0
  64. package/commands/research.md +2 -0
  65. package/commands/room.md +2 -0
  66. package/commands/rooms.md +1 -0
  67. package/commands/root-cause.md +2 -0
  68. package/commands/rs-experts.md +1 -0
  69. package/commands/rs-explain.md +1 -0
  70. package/commands/rs-fetch.md +1 -0
  71. package/commands/rs-thesis.md +1 -0
  72. package/commands/scenario-plan.md +2 -0
  73. package/commands/scheduled-tasks.md +1 -0
  74. package/commands/score-innovation.md +2 -0
  75. package/commands/scout.md +1 -0
  76. package/commands/setup.md +2 -0
  77. package/commands/snapshot.md +2 -0
  78. package/commands/speakers.md +1 -0
  79. package/commands/splash.md +5 -2
  80. package/commands/status.md +1 -0
  81. package/commands/structure-argument.md +2 -0
  82. package/commands/suggest-next.md +2 -0
  83. package/commands/systems-thinking.md +2 -0
  84. package/commands/think-hats.md +2 -0
  85. package/commands/update.md +2 -0
  86. package/commands/user-needs.md +2 -0
  87. package/commands/validate.md +2 -0
  88. package/commands/value-proposition.md +2 -0
  89. package/commands/vault.md +2 -0
  90. package/commands/visualize.md +24 -29
  91. package/commands/whitespace.md +2 -1
  92. package/commands/wiki.md +1 -0
  93. package/hooks/hooks.json +22 -88
  94. package/lib/agents/auto-explore-agent.cjs +82 -0
  95. package/lib/core/breakthrough/canary.cjs +134 -0
  96. package/lib/core/breakthrough/canary.test.cjs +136 -0
  97. package/lib/core/breakthrough/detectors.cjs +359 -0
  98. package/lib/core/breakthrough/detectors.test.cjs +333 -0
  99. package/lib/core/breakthrough/ethics-fence.cjs +127 -0
  100. package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
  101. package/lib/core/breakthrough/resurfacing.cjs +150 -0
  102. package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
  103. package/lib/core/breakthrough/review-queue.cjs +154 -0
  104. package/lib/core/breakthrough/review-queue.test.cjs +160 -0
  105. package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
  106. package/lib/core/breakthrough/scanner.cjs +426 -0
  107. package/lib/core/breakthrough/scanner.test.cjs +267 -0
  108. package/lib/core/breakthrough/schema.cjs +164 -0
  109. package/lib/core/breakthrough/schema.test.cjs +256 -0
  110. package/lib/core/breakthrough/scoring.cjs +293 -0
  111. package/lib/core/breakthrough/scoring.test.cjs +423 -0
  112. package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
  113. package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
  114. package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
  115. package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
  116. package/lib/core/first-touch-version-stamper.cjs +113 -0
  117. package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
  118. package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
  119. package/lib/core/llm-name-suggester.cjs +194 -0
  120. package/lib/core/llm-name-suggester.test.cjs +132 -0
  121. package/lib/core/mva-orchestrator.cjs +41 -0
  122. package/lib/core/mva-telemetry.cjs +31 -143
  123. package/lib/core/navigation/edges.cjs +35 -0
  124. package/lib/core/navigation/memory-events.cjs +126 -0
  125. package/lib/core/room-auto-create.cjs +318 -0
  126. package/lib/core/room-auto-create.test.cjs +198 -0
  127. package/lib/core/room-discard-cascade.cjs +225 -0
  128. package/lib/core/room-discard-cascade.test.cjs +135 -0
  129. package/lib/core/room-name-validator.cjs +132 -0
  130. package/lib/core/room-name-validator.test.cjs +156 -0
  131. package/lib/core/room-naming-selector.cjs +357 -0
  132. package/lib/core/room-naming-selector.test.cjs +277 -0
  133. package/lib/core/room-receipt-emit.cjs +63 -0
  134. package/lib/core/room-skeleton-scaffold.cjs +315 -0
  135. package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
  136. package/lib/core/stale-copy-scanner.cjs +190 -0
  137. package/lib/core/state-aware-router.cjs +78 -0
  138. package/lib/core/telemetry/schema.cjs +168 -0
  139. package/lib/core/telemetry/schema.test.cjs +124 -0
  140. package/lib/core/telemetry/validator.cjs +197 -0
  141. package/lib/core/telemetry/validator.test.cjs +188 -0
  142. package/lib/core/telemetry/writer.cjs +141 -0
  143. package/lib/core/telemetry/writer.test.cjs +331 -0
  144. package/lib/core/terminal-capability.cjs +88 -0
  145. package/lib/core/venture-shape-nudge.cjs +163 -0
  146. package/lib/core/venture-shape-nudge.test.cjs +161 -0
  147. package/lib/core/visual-ops.cjs +70 -2
  148. package/lib/hmi/selector-dispatcher.cjs +90 -1
  149. package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
  150. package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
  151. package/lib/memory/body-shape-coverage.test.cjs +268 -0
  152. package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
  153. package/lib/memory/first-touch-version.test.cjs +198 -0
  154. package/lib/memory/help-coverage.test.cjs +108 -0
  155. package/lib/memory/help-renderer.test.cjs +145 -0
  156. package/lib/memory/palette-consistency.test.cjs +127 -0
  157. package/lib/memory/pending-tension-store.cjs +80 -0
  158. package/lib/memory/render-v2-disposition.test.cjs +199 -0
  159. package/lib/memory/run-feynman-tests.cjs +213 -0
  160. package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
  161. package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
  162. package/lib/memory/soft-alias.test.cjs +144 -0
  163. package/lib/memory/stale-copy-scanner.test.cjs +291 -0
  164. package/lib/memory/state-aware-router.test.cjs +90 -0
  165. package/lib/memory/statusline-two-row.test.cjs +338 -0
  166. package/lib/memory/terminal-capability.test.cjs +155 -0
  167. package/lib/render/ROOM.md +74 -22
  168. package/lib/sessionstart/budget-compressor.cjs +130 -0
  169. package/lib/sessionstart/contributor-interface.cjs +134 -0
  170. package/lib/sessionstart/contributor-isolator.cjs +128 -0
  171. package/lib/sessionstart/precedence-ladder.cjs +47 -0
  172. package/lib/statusline/governing-thought-truncator.cjs +45 -0
  173. package/lib/statusline/two-row-renderer.cjs +186 -0
  174. package/lib/statusline/version-resolver.cjs +81 -0
  175. package/package.json +1 -1
  176. package/references/visual/ROOM.md +55 -0
  177. package/references/visual/palette.json +54 -0
  178. package/skills/larry-personality/SKILL.md +34 -0
  179. package/skills/ui-system/SKILL.md +109 -1
  180. package/skills/ui-system/rules/dual-palette.md +156 -0
  181. package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
  182. package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
@@ -0,0 +1,426 @@
1
+ 'use strict';
2
+ /*
3
+ * Phase 120-02 Wave 2 Task 3 -- The breakthrough scanner orchestrator. Wires:
4
+ * - Plan 120-00 detectors.cjs (the 4 pattern types)
5
+ * - Plan 120-00 schema.writeBreakthrough (the D-20 HARD FLOOR atomic writer)
6
+ * - Plan 120-01 scoring.pickTopWithAffordance (the D-11 + D-12 ranker)
7
+ * - Plan 120-02 resurfacing.isEligibleForSurfacing (D-13..D-15)
8
+ * - Plan 120-02 canary.computeDismissalRate (D-19)
9
+ * - Plan 120-01 selector-dispatcher.pickShape (F.7 surfacing)
10
+ *
11
+ * Per CONTEXT.md D-16 empty-state lock: if no eligible candidates, return
12
+ * top=null. The hook script then emits continue:true with NO additionalContext
13
+ * (silence; no placeholder; trust users to notice presence vs absence).
14
+ *
15
+ * Per CONTEXT.md D-20 third structural enforcement point: surfaceBreakthrough
16
+ * verifies the Breakthrough node has at least one DERIVED_FROM edge BEFORE
17
+ * dispatching F.7. The other two enforcement points are schema.cjs::
18
+ * validateProvenance + writeBreakthrough transaction (Plan 120-00) and
19
+ * shape-f7-breakthrough-renderer.cjs::renderShapeF7Breakthrough's artifact_ids
20
+ * check (Plan 120-01). Defense in depth.
21
+ *
22
+ * Canon Part 8 + Part 9 + Part 10 sub-claim 5: pure LOCAL; chokepoint-routed;
23
+ * the math IS the surface; variable reward fires automatically at session
24
+ * start. Zero Brain coupling; zero cross-room aggregation.
25
+ *
26
+ * Em-dash HARD RULE (CLAUDE.md feedback_no_emdashes): use "--" not U+2014.
27
+ */
28
+
29
+ const path = require('node:path');
30
+ const detectors = require('./detectors.cjs');
31
+ const schema = require('./schema.cjs');
32
+ const scoring = require('./scoring.cjs');
33
+ const resurfacing = require('./resurfacing.cjs');
34
+ const canary = require('./canary.cjs');
35
+ // Plan 120-03 wires: voice-scaffold (D-17 4-rule auditor) + ethics-fence (D-18
36
+ // 4-tier hybrid fence). These are pure modules with zero Brain coupling.
37
+ const voiceScaffold = require('./voice-scaffold.cjs');
38
+ const ethicsFence = require('./ethics-fence.cjs');
39
+ const navigation = require('../navigation.cjs');
40
+ const roomDb = require('../room-db.cjs');
41
+ const crypto = require('node:crypto');
42
+ let selectorDispatcher = null;
43
+ try {
44
+ selectorDispatcher = require('../../hmi/selector-dispatcher.cjs');
45
+ } catch (_e) {
46
+ selectorDispatcher = null;
47
+ }
48
+
49
+ // Phase 121-02 D-07: lazy-require the unified telemetry writer at first emit.
50
+ // A missing writer module (stripped-down install) must not crash the scanner,
51
+ // so failures of require() degrade to a soft skip (no emit).
52
+ function _sha256Hex(s) {
53
+ return crypto.createHash('sha256').update(String(s || '')).digest('hex');
54
+ }
55
+
56
+ // applyThrottleFilter -- D-19 per-detector dismissal-rate canary filter. For
57
+ // every detector kind whose dismissal rate exceeds the D-19 threshold over the
58
+ // rolling 100-fire window, drops all candidates of that kind from the hard-fire
59
+ // ranking AND emits a breakthrough_throttled memory_event so /mos:doctor can
60
+ // surface the throttled detector. Returns { kept, throttled_kinds }.
61
+ function applyThrottleFilter(candidates, db) {
62
+ const throttledKinds = [];
63
+ if (!Array.isArray(candidates) || !db) {
64
+ return { kept: Array.isArray(candidates) ? candidates : [], throttled_kinds: throttledKinds };
65
+ }
66
+ for (const kind of detectors.DETECTOR_TYPES) {
67
+ const rateResult = canary.computeDismissalRate(kind, db);
68
+ if (rateResult.throttled) {
69
+ throttledKinds.push(kind);
70
+ // Emit breakthrough_throttled event so /mos:doctor can surface throttled
71
+ // detectors at session-start. Best-effort: never throws on infra hiccup.
72
+ try { canary.emitThrottleEvent(kind, db, rateResult); } catch (_e) { /* swallow */ }
73
+ }
74
+ }
75
+ const kept = throttledKinds.length === 0
76
+ ? candidates
77
+ : candidates.filter(function (c) {
78
+ return c && typeof c.kind === 'string' && throttledKinds.indexOf(c.kind) === -1;
79
+ });
80
+ return { kept: kept, throttled_kinds: throttledKinds };
81
+ }
82
+
83
+ // applyResurfacingFilter -- D-13..D-15 resurfacing rules filter. Drops any
84
+ // candidate whose breakthrough_id has been confirmed (D-14 once-only),
85
+ // filed-as-decision (D-15 never-resurfaces), or dismissed-and-still-in-cooldown
86
+ // (D-13 BOTH-condition: 7-day cooldown AND new-artifacts-accumulated).
87
+ function applyResurfacingFilter(candidates, db) {
88
+ if (!Array.isArray(candidates) || !db) {
89
+ return Array.isArray(candidates) ? candidates : [];
90
+ }
91
+ return candidates.filter(function (c) {
92
+ if (!c || typeof c.id !== 'string') return false;
93
+ const elig = resurfacing.isEligibleForSurfacing(c.id, db, {
94
+ current_artifact_count: Array.isArray(c.artifact_ids) ? c.artifact_ids.length : 0,
95
+ });
96
+ return elig && elig.eligible === true;
97
+ });
98
+ }
99
+
100
+ // scanForBreakthroughs -- the session-start orchestrator. Runs the 4 detectors,
101
+ // applies the D-19 throttle filter + D-13..D-15 resurfacing filter, ranks
102
+ // surviving candidates via the Plan 120-01 scoring formula, persists the top
103
+ // via writeBreakthrough (D-20 HARD FLOOR), and returns
104
+ // { top, more_count, throttled_kinds, queued, reason? }.
105
+ //
106
+ // D-16 empty-state silence: returns top=null whenever no eligible hard-fire
107
+ // candidate survives the filters. The caller hook then emits continue:true with
108
+ // NO additionalContext.
109
+ function scanForBreakthroughs(roomDir, opts) {
110
+ const options = opts || {};
111
+ const nowMs = (typeof options.now === 'number' && Number.isFinite(options.now)) ? options.now : Date.now();
112
+
113
+ if (typeof roomDir !== 'string' || roomDir.length === 0) {
114
+ return { top: null, more_count: 0, throttled_kinds: [], reason: 'invalid_room_dir' };
115
+ }
116
+
117
+ // Open db via the canonical room-db chokepoint. openRoomDb returns the bare
118
+ // DatabaseSync handle per the Phase 109-02 contract. If room.db cannot be
119
+ // opened (e.g., missing or corrupt), degrade to D-16 silence.
120
+ let db;
121
+ try {
122
+ db = roomDb.openRoomDb(roomDir);
123
+ } catch (_e) {
124
+ return { top: null, more_count: 0, throttled_kinds: [], reason: 'no_room_db' };
125
+ }
126
+
127
+ const roomState = { roomDir: roomDir, db: db, now: nowMs };
128
+
129
+ // Run all 4 detectors. Each returns { hits, soft_fires }. Failures degrade
130
+ // gracefully -- a broken Phase 117 math output file should not block the
131
+ // surface; the failing detector returns empty and the others still surface.
132
+ let allHits = [];
133
+ const allSoftFires = [];
134
+ try {
135
+ const c1 = detectors.detectConvergence(roomState, options);
136
+ const c2 = detectors.detectContradictionResolved(roomState, options);
137
+ const c3 = detectors.detectCrossDomainAnalogy(roomState, options);
138
+ const c4 = detectors.detectReverseSalientClosed(roomState, options);
139
+ allHits = [].concat(c1.hits || [], c2.hits || [], c3.hits || [], c4.hits || []);
140
+ allSoftFires.push(
141
+ ...(c1.soft_fires || []),
142
+ ...(c2.soft_fires || []),
143
+ ...(c3.soft_fires || []),
144
+ ...(c4.soft_fires || [])
145
+ );
146
+ } catch (_e) {
147
+ return { top: null, more_count: 0, throttled_kinds: [], reason: 'detector_error' };
148
+ }
149
+
150
+ // D-19: apply the canary throttle filter. Drops any kind whose dismissal rate
151
+ // exceeds 30% over the rolling 100-fire window. Reports throttled_kinds for
152
+ // /mos:doctor traceability.
153
+ const throttleResult = applyThrottleFilter(allHits, db);
154
+ let candidates = throttleResult.kept;
155
+
156
+ // D-13..D-15: apply the resurfacing filter. Drops confirmed / filed-as-decision /
157
+ // in-cooldown dismissed candidates.
158
+ candidates = applyResurfacingFilter(candidates, db);
159
+
160
+ // Plan 120-03: D-18 4-tier ethics fence. Partition candidates into the 4 bands.
161
+ // HARD_CEILING -> proceeds to ranking (auto-surface)
162
+ // SOFT_BAND -> routed to .rooms/breakthrough-review-queue.db + memory_event
163
+ // breakthrough_in_review_queue (no surface, no ranking)
164
+ // HARD_FLOOR -> structurally impossible at this point (writeBreakthrough
165
+ // upstream refuses provenance-less inputs) BUT we apply
166
+ // defense in depth and silently drop the candidate.
167
+ // BELOW_FLOOR -> soft-fire territory already handled by detectors.classifyFireTier;
168
+ // silently drop here (no double-buffer).
169
+ // The HARD_CEILING partition is the only set that proceeds to scoring + surface.
170
+ const hardCeilingHits = [];
171
+ if (Array.isArray(candidates) && candidates.length > 0) {
172
+ // Resolve a rooms-home for SOFT_BAND queue persistence. Prefer the explicit
173
+ // option, then MINDRIAN_ROOMS_HOME env, then ~/MindrianRooms default.
174
+ const roomsHome = options.roomsHome
175
+ || (typeof process.env.MINDRIAN_ROOMS_HOME === 'string' && process.env.MINDRIAN_ROOMS_HOME.length > 0
176
+ ? process.env.MINDRIAN_ROOMS_HOME
177
+ : path.join(process.env.HOME || '', 'MindrianRooms'));
178
+ for (const c of candidates) {
179
+ const band = ethicsFence.classifyEthicsBand(c);
180
+ if (band === 'HARD_CEILING') {
181
+ hardCeilingHits.push(c);
182
+ } else if (band === 'SOFT_BAND') {
183
+ // Best-effort queue insert + memory_event mirror. Failure does NOT block
184
+ // the scanner -- the SOFT_BAND candidate is silently dropped if persistence
185
+ // fails (per the Plan 120-03 graceful-failure invariant).
186
+ try {
187
+ ethicsFence.queueForReview(c, roomsHome, { db: db, roomSlug: options.roomSlug });
188
+ } catch (_e) { /* swallow */ }
189
+ }
190
+ // HARD_FLOOR + BELOW_FLOOR: silent drop. Defense in depth at the ethics layer.
191
+ }
192
+ candidates = hardCeilingHits;
193
+ }
194
+
195
+ // Soft-fire telemetry per CONTEXT.md D-01..D-02. Emit a breakthrough_detected_soft
196
+ // event for every soft-tier candidate -- this populates the retraining signal
197
+ // mentioned in the discuss-phase D-01 verbatim. Best-effort writes.
198
+ for (const sf of allSoftFires) {
199
+ if (!sf || typeof sf.id !== 'string') continue;
200
+ try {
201
+ navigation.logMemoryEvent(db, 'breakthrough_detected_soft', {
202
+ breakthrough_id: sf.id,
203
+ kind: typeof sf.kind === 'string' ? sf.kind : 'unknown',
204
+ confidence: typeof sf.confidence === 'number' ? sf.confidence : 0,
205
+ artifact_count: Array.isArray(sf.artifact_ids) ? sf.artifact_ids.length : 0,
206
+ source_path: 'system:breakthrough-scanner',
207
+ created_by: 'system',
208
+ });
209
+ } catch (_e) { /* swallow */ }
210
+ }
211
+
212
+ if (!Array.isArray(candidates) || candidates.length === 0) {
213
+ // D-16 empty-state silence: no eligible candidates survived the filters.
214
+ return {
215
+ top: null,
216
+ more_count: 0,
217
+ throttled_kinds: throttleResult.throttled_kinds,
218
+ };
219
+ }
220
+
221
+ // D-11 + D-12: rank by the 5-component scoring formula; pick top-1; expose
222
+ // the rest via the "More breakthroughs (N)" affordance.
223
+ const pick = scoring.pickTopWithAffordance(candidates, roomState, nowMs);
224
+ if (!pick || !pick.top) {
225
+ return {
226
+ top: null,
227
+ more_count: 0,
228
+ throttled_kinds: throttleResult.throttled_kinds,
229
+ };
230
+ }
231
+
232
+ // D-20 HARD FLOOR: persist the top breakthrough via writeBreakthrough. The
233
+ // atomic transaction inserts the Breakthrough node AND its DERIVED_FROM edges.
234
+ // If the write is refused (provenance-less), do not surface; degrade silently.
235
+ const writeResult = schema.writeBreakthrough(db, pick.top);
236
+ if (!writeResult || !writeResult.ok) {
237
+ return {
238
+ top: null,
239
+ more_count: 0,
240
+ throttled_kinds: throttleResult.throttled_kinds,
241
+ reason: 'writeBreakthrough_refused:' + (writeResult ? writeResult.reason : 'unknown'),
242
+ };
243
+ }
244
+
245
+ return {
246
+ top: pick.top,
247
+ more_count: pick.more_count,
248
+ throttled_kinds: throttleResult.throttled_kinds,
249
+ queued: pick.queued,
250
+ };
251
+ }
252
+
253
+ // surfaceBreakthrough -- the third D-20 structural enforcement point. Reads the
254
+ // canonical SQL invariant (SELECT COUNT(*) FROM edges WHERE source=? AND
255
+ // type='DERIVED_FROM') BEFORE dispatching F.7. If the count is < 1, emits a
256
+ // breakthrough_surface_blocked event for /mos:doctor traceability and refuses
257
+ // to surface. This catches constitutional bypass attempts (a fake Breakthrough
258
+ // node inserted outside the writeBreakthrough chokepoint).
259
+ //
260
+ // On success: dispatches F.7 via selector-dispatcher.pickShape, emits a
261
+ // breakthrough_surfaced memory_event, and flips the Breakthrough node's
262
+ // properties.surfaced = true.
263
+ function surfaceBreakthrough(breakthrough, options) {
264
+ const opts = options || {};
265
+ const db = opts.db;
266
+ if (!db || !breakthrough || typeof breakthrough.id !== 'string' || breakthrough.id.length === 0) {
267
+ return { ok: false, reason: 'invalid_input' };
268
+ }
269
+
270
+ // D-20 third structural enforcement: provenance MUST exist on disk before
271
+ // F.7 dispatch. Defense in depth on top of schema.cjs::validateProvenance
272
+ // and the renderer's artifact_ids check.
273
+ let provenanceCount = 0;
274
+ try {
275
+ const row = db.prepare(
276
+ "SELECT COUNT(*) AS c FROM edges WHERE source = ? AND type = 'DERIVED_FROM'"
277
+ ).get(breakthrough.id);
278
+ provenanceCount = (row && typeof row.c === 'number') ? row.c : 0;
279
+ } catch (_e) {
280
+ provenanceCount = 0;
281
+ }
282
+ if (provenanceCount < 1) {
283
+ // Emit breakthrough_surface_blocked so /mos:doctor can find the refusal.
284
+ // Best-effort: never throws.
285
+ try {
286
+ navigation.logMemoryEvent(db, 'breakthrough_surface_blocked', {
287
+ breakthrough_id: breakthrough.id,
288
+ kind: typeof breakthrough.kind === 'string' ? breakthrough.kind : 'unknown',
289
+ reason: 'provenance_required',
290
+ source_path: 'system:breakthrough-scanner',
291
+ created_by: 'system',
292
+ });
293
+ } catch (_e) { /* swallow */ }
294
+ return { ok: false, reason: 'provenance_required' };
295
+ }
296
+
297
+ // Plan 120-03: D-17 voice scaffold composition + audit gate.
298
+ //
299
+ // Compose the conversational opener via the 4-rule scaffold. If the composed
300
+ // line fails the D-17 auditor (rule 1-4 violation), replace it with the
301
+ // structural default (roomState=null variant of composeBreakthroughVoiceLine
302
+ // which is auditor-safe BY CONSTRUCTION -- see voice-scaffold Test 4).
303
+ //
304
+ // This is defense in depth: no D-17-violating voice line EVER reaches the F.7
305
+ // surface, regardless of caller-supplied mechanism_phrase content. The auditor
306
+ // IS the structural enforcement (D-20 meta-principle).
307
+ let voiceLine = '';
308
+ try {
309
+ const composed = voiceScaffold.composeBreakthroughVoiceLine(breakthrough, opts.roomState || null);
310
+ const audit = voiceScaffold.auditVoiceLine(composed);
311
+ voiceLine = audit.ok
312
+ ? composed
313
+ : voiceScaffold.composeBreakthroughVoiceLine(breakthrough, null);
314
+ } catch (_e) {
315
+ voiceLine = '';
316
+ }
317
+
318
+ // Dispatch F.7 via the Phase 88.2 selector dispatcher (Plan 120-01 registration).
319
+ // If the dispatcher is unavailable (e.g., during isolated unit testing of the
320
+ // surface gate), degrade gracefully -- we still emit the breakthrough_surfaced
321
+ // event and flip the node property because those are the load-bearing side
322
+ // effects per the test contract.
323
+ let rendered = null;
324
+ if (selectorDispatcher && typeof selectorDispatcher.pickShape === 'function') {
325
+ try {
326
+ rendered = selectorDispatcher.pickShape({
327
+ requestedShape: 'F.7',
328
+ roomDir: opts.roomDir,
329
+ tier: typeof opts.tier === 'number' ? opts.tier : 1,
330
+ payload: {
331
+ breakthrough: breakthrough,
332
+ more_count: typeof opts.more_count === 'number' ? opts.more_count : 0,
333
+ voice_line: voiceLine,
334
+ },
335
+ });
336
+ } catch (_e) {
337
+ rendered = null;
338
+ }
339
+ }
340
+
341
+ // Emit breakthrough_surfaced memory_event.
342
+ try {
343
+ navigation.logMemoryEvent(db, 'breakthrough_surfaced', {
344
+ breakthrough_id: breakthrough.id,
345
+ kind: typeof breakthrough.kind === 'string' ? breakthrough.kind : 'unknown',
346
+ confidence: typeof breakthrough.confidence === 'number' ? breakthrough.confidence : 0,
347
+ score: typeof breakthrough.score === 'number' ? breakthrough.score : null,
348
+ more_count: typeof opts.more_count === 'number' ? opts.more_count : 0,
349
+ source_path: 'system:breakthrough-scanner',
350
+ created_by: 'system',
351
+ });
352
+ } catch (_e) { /* swallow */ }
353
+
354
+ // Phase 121-02 D-07: capture breakthrough surfacing + F.7 verb + ethics tier
355
+ // + voice audit pass/fail into the unified events-YYYY-WNN.jsonl stream
356
+ // (Plan 121-00 writer chokepoint). The dismissal signal is valuable
357
+ // regardless of whether the voice audit passed (Test 3). Throttled-by-
358
+ // canary and provenance-blocked surfaces NEVER reach this point so they
359
+ // structurally cannot emit (Test 4 + scanForBreakthroughs upstream
360
+ // applyThrottleFilter).
361
+ //
362
+ // verb_chosen is populated from opts.verb_chosen when the caller pre-resolves
363
+ // the F.7 choice (the typical end-to-end path); falls back to '' when the
364
+ // surface is dispatched without a pre-selection. The dismissal vs accept
365
+ // outcome is later captured by selector_pick (D-04) when the F.7 user pick
366
+ // resolves. This event records the surfacing context + the proposed verb at
367
+ // the dispatch boundary.
368
+ //
369
+ // Non-blocking: try/catch swallows writer breaches; the scanner still
370
+ // returns ok:true to its caller.
371
+ try {
372
+ let writer = null;
373
+ try {
374
+ writer = require('../telemetry/writer.cjs');
375
+ } catch (_e) {
376
+ writer = null;
377
+ }
378
+ if (writer && typeof writer.emit === 'function') {
379
+ const detector = (typeof breakthrough.detector_type === 'string' && breakthrough.detector_type.length > 0)
380
+ ? breakthrough.detector_type
381
+ : (typeof breakthrough.kind === 'string' ? breakthrough.kind : 'unknown');
382
+ const ethicsTier = (typeof breakthrough.ethics_tier === 'string' && breakthrough.ethics_tier.length > 0)
383
+ ? breakthrough.ethics_tier
384
+ : 'NEUTRAL';
385
+ const voiceAudit = Boolean(breakthrough.voice_audit_pass);
386
+ const verbChosen = (opts && typeof opts.verb_chosen === 'string') ? opts.verb_chosen : '';
387
+ const slugSrc = (opts && typeof opts.roomDir === 'string' && opts.roomDir.length > 0)
388
+ ? String(opts.roomDir).split('/').filter(Boolean).pop() || opts.roomDir
389
+ : (String(process.cwd()).split('/').filter(Boolean).pop() || 'default-room');
390
+ writer.emit('breakthrough_dismissed', {
391
+ detector_type: String(detector).slice(0, 64),
392
+ verb_chosen: String(verbChosen).slice(0, 64),
393
+ ethics_tier: String(ethicsTier).slice(0, 64),
394
+ voice_audit_pass: voiceAudit,
395
+ room_slug_sha256: _sha256Hex(slugSrc),
396
+ });
397
+ }
398
+ } catch (_e) {
399
+ // Telemetry MUST never crash the surfacing pipeline.
400
+ }
401
+
402
+ // Flip Breakthrough node properties.surfaced = true (best-effort update).
403
+ try {
404
+ const row = db.prepare(
405
+ "SELECT properties FROM nodes WHERE id = ? AND type = 'breakthrough'"
406
+ ).get(breakthrough.id);
407
+ if (row) {
408
+ let props = {};
409
+ try { props = JSON.parse(row.properties); } catch (_e) { props = {}; }
410
+ props.surfaced = true;
411
+ props.surfaced_at = Date.now();
412
+ db.prepare(
413
+ "UPDATE nodes SET properties = ?, last_seen_at = ? WHERE id = ?"
414
+ ).run(JSON.stringify(props), Date.now(), breakthrough.id);
415
+ }
416
+ } catch (_e) { /* swallow */ }
417
+
418
+ return { ok: true, rendered: rendered, voice_line: voiceLine };
419
+ }
420
+
421
+ module.exports = {
422
+ scanForBreakthroughs: scanForBreakthroughs,
423
+ surfaceBreakthrough: surfaceBreakthrough,
424
+ applyResurfacingFilter: applyResurfacingFilter,
425
+ applyThrottleFilter: applyThrottleFilter,
426
+ };