@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
@@ -70,6 +70,14 @@
70
70
 
71
71
  'use strict';
72
72
 
73
+ // Phase 121-02 D-04: unified-stream selector_pick emit chokepoint.
74
+ // require()-loaded lazily inside emitSelectorPickUnified() so a missing writer
75
+ // module (or a stripped-down install) cannot crash the dispatcher at load.
76
+ const crypto = require('node:crypto');
77
+ function _sha256(s) {
78
+ return crypto.createHash('sha256').update(String(s || '')).digest('hex');
79
+ }
80
+
73
81
  const FREE_TEXT = 'Free-Text';
74
82
  const MODE_B_ZONE1_PREFIX = '⚠ Brain unreachable; running on local graph only.';
75
83
 
@@ -79,7 +87,12 @@ const MODE_B_ZONE1_PREFIX = '⚠ Brain unreachable; running on local graph only.
79
87
  // collision-safe lib/hmi/shape-f6-plan-review-renderer.cjs path; the umbrella 'F'
80
88
  // branch continues to route to Phase 101-01's lib/hmi/shape-f6-renderer.cjs via
81
89
  // JTBD logic preserved byte-stable per R1 invariant).
82
- const F_SUBSHAPES = ['F.0', 'F.1', 'F.2', 'F.3', 'F.4', 'F.5', 'F.6'];
90
+ // Phase 120-01: 'F.7' Breakthrough Surface appended (CONTEXT.md D-07..D-10 + D-20). Closed-vocab
91
+ // 5 verbs verbatim [Explore deeper]/[Confirm]/[File as decision]/[Dismiss]/[Back]; mandatory
92
+ // [Dismiss] guard (D-10). Routes to lib/hmi/shape-f7-breakthrough-renderer.cjs via the
93
+ // explicit requestedShape:'F.7' branch in dispatchShapeFSubShape. The umbrella 'F' branch
94
+ // does NOT resolve to F.7 -- breakthrough surfacing is opt-in via explicit dispatch.
95
+ const F_SUBSHAPES = ['F.0', 'F.1', 'F.2', 'F.3', 'F.4', 'F.5', 'F.6', 'F.7'];
83
96
  const F_UMBRELLA = 'F';
84
97
  const JUST_TALK = 'JUST_TALK';
85
98
  const COMPACTION_VIOLATION_CODE = 'render_v2_compaction_violation';
@@ -175,6 +188,58 @@ function justTalkRefuse(requestedShape) {
175
188
  };
176
189
  }
177
190
 
191
+ /**
192
+ * Phase 121-02 D-04: emit selector_pick event into the unified
193
+ * ~/.mindrian/telemetry/v1.13/events-YYYY-WNN.jsonl stream (Plan 121-00
194
+ * writer chokepoint). Wrapped in try/catch -- telemetry NEVER fails Larry's
195
+ * turn. Canon Part 8: room slug is sha256-hashed; payload fields are scalar
196
+ * enums / numbers / booleans only. The emit is gated by validateEventPayload
197
+ * inside writer.emit() so adversarial fixtures (Cypher in verb_chosen, etc.)
198
+ * cannot leak. Non-blocking: any thrown error is swallowed so the pickShape
199
+ * dispatch resolution still returns to the caller.
200
+ */
201
+ function emitSelectorPickUnified(roomDir, subShape, rendered, payloadIn) {
202
+ try {
203
+ let writer;
204
+ try {
205
+ writer = require('../core/telemetry/writer.cjs');
206
+ } catch (_e) {
207
+ return; // missing writer module -- soft skip, do not crash dispatch
208
+ }
209
+ if (!writer || typeof writer.emit !== 'function') return;
210
+ const contract = (rendered && rendered.contract) ? rendered.contract : {};
211
+ const verbs = Array.isArray(contract.verbs) ? contract.verbs : [];
212
+ const mode = (typeof contract.mode === 'string') ? contract.mode : 'A';
213
+ const recommended = contract.recommended;
214
+ const payload = (payloadIn && typeof payloadIn === 'object') ? payloadIn : {};
215
+ const rankerConfidence = (typeof payload.rankerConfidence === 'number')
216
+ ? payload.rankerConfidence
217
+ : 0;
218
+ const recommendedRendered = (recommended !== null && recommended !== undefined)
219
+ ? true
220
+ : Boolean(payload.recommendedRendered);
221
+ const verbChosen = (typeof payload.verb_chosen === 'string' && payload.verb_chosen.length > 0)
222
+ ? payload.verb_chosen
223
+ : (typeof contract.recommended === 'string' ? contract.recommended : '');
224
+ const slugSource = (typeof roomDir === 'string' && roomDir.length > 0)
225
+ ? String(roomDir).split('/').filter(Boolean).pop() || roomDir
226
+ : (String(process.cwd()).split('/').filter(Boolean).pop() || 'default-room');
227
+ writer.emit('selector_pick', {
228
+ sub_shape: String(subShape || '').slice(0, 64),
229
+ mode: String(mode || 'A').slice(0, 64),
230
+ ranker_confidence: rankerConfidence,
231
+ recommended_rendered: Boolean(recommendedRendered),
232
+ options_count: Number.isInteger(verbs.length) ? verbs.length : 0,
233
+ room_slug_sha256: _sha256(slugSource),
234
+ verb_chosen: String(verbChosen || '').slice(0, 64),
235
+ });
236
+ } catch (_e) {
237
+ // Canon Part 8 validation failure or any other emit failure: swallow.
238
+ // The dispatch resolution must still return cleanly per non-blocking
239
+ // contract (selector-pick-capture test 3 verifies this).
240
+ }
241
+ }
242
+
178
243
  /**
179
244
  * Phase 88.2-04: emit selector presentation telemetry. Wrapped in try/catch --
180
245
  * telemetry NEVER fails Larry's turn. Canon Part 8: LOCAL ledger only, room
@@ -354,6 +419,25 @@ function dispatchShapeFSubShape(requestedShape, args) {
354
419
  personaContext: (typeof payloadObj.personaContext === 'string' && payloadObj.personaContext.length > 0)
355
420
  ? payloadObj.personaContext : undefined,
356
421
  };
422
+ } else if (requestedShape === 'F.7') {
423
+ // Phase 120-01: F.7 Breakthrough Surface (closed-vocab; 5 verbs verbatim).
424
+ // Per CONTEXT.md D-07: built NEW (not a reuse of F.4).
425
+ // Per CONTEXT.md D-08: 5 verbs locked verbatim [Explore deeper]/[Confirm]/
426
+ // [File as decision]/[Dismiss]/[Back].
427
+ // Per CONTEXT.md D-10: [Dismiss] is MANDATORY (renderer asserts presence on
428
+ // every render).
429
+ // Per CONTEXT.md D-20: payload.breakthrough carries provenance.artifact_ids[] --
430
+ // F.7 renderer refuses to render if artifact_ids.length === 0 (HARD FLOOR
431
+ // structural enforcement at the surface layer; defense in depth on top of
432
+ // writeBreakthrough's validateProvenance in lib/core/breakthrough/schema.cjs).
433
+ mod = safeRequire('./shape-f7-breakthrough-renderer.cjs');
434
+ fnName = 'renderShapeF7Breakthrough';
435
+ inputArgs = {
436
+ tier: tier,
437
+ header: callerHeader,
438
+ breakthrough: (payloadObj.breakthrough && typeof payloadObj.breakthrough === 'object') ? payloadObj.breakthrough : null,
439
+ more_count: (Number.isInteger(payloadObj.more_count) && payloadObj.more_count >= 0) ? payloadObj.more_count : 0,
440
+ };
357
441
  }
358
442
 
359
443
  if (!mod || typeof mod[fnName] !== 'function') {
@@ -496,6 +580,11 @@ function pickShape(args) {
496
580
  const payloadObj = (opts.payload && typeof opts.payload === 'object') ? opts.payload : {};
497
581
  if (payloadObj.emitTelemetry === true) {
498
582
  emitPresentationTelemetry(opts.roomDir, result.shape, result.rendered, operator);
583
+ // Phase 121-02 D-04: capture selector pick (88.2/125 ranker) into
584
+ // the unified ~/.mindrian/telemetry/v1.13/events-YYYY-WNN.jsonl
585
+ // stream (Plan 121-00 writer chokepoint). Non-blocking; never
586
+ // throws back to caller.
587
+ emitSelectorPickUnified(opts.roomDir, result.shape, result.rendered, payloadObj);
499
588
  }
500
589
  }
501
590
  }
@@ -0,0 +1,222 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 120-01 -- Shape F.7 Breakthrough Surface renderer.
5
+ *
6
+ * Per CONTEXT.md D-07..D-10 (the 5 verbs LOCKED VERBATIM + closed-vocab + mandatory dismiss):
7
+ * - Built NEW (D-07): F.4 maps content-to-user-actionable-observation;
8
+ * F.7 maps user-actions-to-recognition-of-user-own-work. Different speech acts.
9
+ * - 5 verbs LOCKED VERBATIM (D-08): [Explore deeper] / [Confirm] / [File as decision] /
10
+ * [Dismiss] / [Back]. Order is structural, not stylistic.
11
+ * - [File as decision] (D-09) emits breakthrough_filed_as_decision via the Plan 120-02
12
+ * scanner consumer; F.7 only renders the verb -- it does NOT itself emit events.
13
+ * - [Dismiss] (D-10) is MANDATORY -- the renderer asserts its presence; refuses to
14
+ * render a contract without it (defense in depth against accidental drift).
15
+ *
16
+ * Per CONTEXT.md D-11..D-12 (top-1 with "More breakthroughs (N)" affordance):
17
+ * - Top-1 surface; "More breakthroughs (N)" affordance for queued candidates.
18
+ * - more_count is rendered in zones.footer IFF more_count > 0.
19
+ *
20
+ * Per CONTEXT.md D-20 HARD FLOOR (Cypher-provable):
21
+ * - breakthrough.artifact_ids[] must be non-empty AT RENDER TIME. The renderer is
22
+ * the second structural enforcement point (the first is writeBreakthrough in
23
+ * Plan 120-00 schema.cjs). Defense in depth: if a caller somehow bypasses
24
+ * writeBreakthrough's validateProvenance, F.7 refuses to render the surfacing.
25
+ *
26
+ * Per CONTEXT.md D-17 voice scaffold rule 3 (time anchor):
27
+ * - The title line includes a human-readable time anchor (e.g., "in the last 8 hours")
28
+ * derived from breakthrough.detected_at via formatTimeAnchor.
29
+ *
30
+ * Canon Part 3: F.7 IS a Decision Gate primitive instance. LOCAL context only
31
+ * (the breakthrough artifact_ids + theme); BRAIN context is none (D-20 makes
32
+ * breakthroughs locally Cypher-provable); SIGNAL context is none (cross-room
33
+ * forbidden per Part 8).
34
+ *
35
+ * Canon Part 4: every choice is graph data. Each of the 5 verbs maps to a typed
36
+ * event the Plan 120-02 scanner consumer emits at verb-dispatch time.
37
+ *
38
+ * Canon Part 8: zero Brain coupling. The renderer is pure: no FS reads, no DB
39
+ * writes, no fetch calls. Mirrors the F.4 / F.6 pattern byte-for-byte.
40
+ *
41
+ * Pure CJS, node built-ins only, zero deps (Phase 87 invariant).
42
+ */
43
+ 'use strict';
44
+
45
+ // D-08: the 5 verbs LOCKED VERBATIM. Object.freeze prevents accidental mutation
46
+ // of the canonical array; downstream code that needs a mutable copy must call
47
+ // F7_VERBS.slice() (the dispatcher does this in inputArgs).
48
+ const F7_VERBS = Object.freeze([
49
+ 'Explore deeper',
50
+ 'Confirm',
51
+ 'File as decision',
52
+ 'Dismiss',
53
+ 'Back',
54
+ ]);
55
+
56
+ // D-10: structural index of 'Dismiss' in F7_VERBS. assertContractInvariants uses
57
+ // this to surface a precise 'dismiss_required' reason when the contract drifts.
58
+ const F7_DISMISS_INDEX = 3;
59
+
60
+ // Map the four detector kinds Plan 120-00 emits to their canonical display
61
+ // strings. Unknown kinds fall back to the raw kind string (and finally to
62
+ // 'Pattern' if kind is falsy).
63
+ const KIND_DISPLAY_NAMES = Object.freeze({
64
+ convergence: 'Convergence',
65
+ contradiction_resolved: 'Contradiction resolved',
66
+ cross_domain_analogy: 'Cross-domain analogy',
67
+ reverse_salient_closed: 'Reverse salient closed',
68
+ });
69
+
70
+ // D-17 rule 3: human-readable time anchor with bucket boundaries chosen to
71
+ // match the spec examples (in the last hour / in the last N hours / today /
72
+ // in the last day / this week / in the last two weeks / on YYYY-MM-DD).
73
+ // D-06 ethical fence: ages > 14 days surface the calendar date verbatim so
74
+ // the user can see exactly how stale the recognition is.
75
+ function formatTimeAnchor(detected_at) {
76
+ const now = Date.now();
77
+ const ts = (typeof detected_at === 'number' && Number.isFinite(detected_at)) ? detected_at : now;
78
+ const ageMs = Math.max(0, now - ts);
79
+ const ageHours = ageMs / (3600 * 1000);
80
+ const ageDays = ageHours / 24;
81
+ if (ageHours < 1) return 'in the last hour';
82
+ if (ageHours < 8) return 'in the last ' + Math.floor(ageHours) + ' hours';
83
+ if (ageHours < 24) return 'today';
84
+ if (ageDays < 2) return 'in the last day';
85
+ if (ageDays < 7) return 'this week';
86
+ if (ageDays < 14) return 'in the last two weeks';
87
+ // Older than the 14-day ethical fence (D-06) -- include calendar date for transparency.
88
+ const d = new Date(ts);
89
+ const yyyy = d.getUTCFullYear();
90
+ const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
91
+ const dd = String(d.getUTCDate()).padStart(2, '0');
92
+ return 'on ' + yyyy + '-' + mm + '-' + dd;
93
+ }
94
+
95
+ // D-10 MANDATORY-Dismiss structural guard. Returns {ok:true} when the verbs
96
+ // array deep-equals F7_VERBS; otherwise returns {ok:false, reason} with a
97
+ // precise reason code so consumers can surface a meaningful error envelope.
98
+ //
99
+ // Reason codes:
100
+ // verb_count_mismatch -- verbs.length !== 5
101
+ // dismiss_required -- the slot at F7_DISMISS_INDEX is not literally 'Dismiss'
102
+ // verb_order_mismatch -- some other slot diverges from the canonical array
103
+ function assertContractInvariants(verbs) {
104
+ if (!Array.isArray(verbs) || verbs.length !== F7_VERBS.length) {
105
+ return { ok: false, reason: 'verb_count_mismatch' };
106
+ }
107
+ for (let i = 0; i < F7_VERBS.length; i++) {
108
+ if (verbs[i] !== F7_VERBS[i]) {
109
+ return { ok: false, reason: i === F7_DISMISS_INDEX ? 'dismiss_required' : 'verb_order_mismatch' };
110
+ }
111
+ }
112
+ return { ok: true };
113
+ }
114
+
115
+ /**
116
+ * Render the F.7 Breakthrough Surface.
117
+ *
118
+ * @param {object} args
119
+ * @param {number} [args.tier] tier integer; informational only -- F.7 is closed-vocab
120
+ * in every tier (no recommended marker; no mode branch)
121
+ * @param {string} [args.header] caller-supplied header line; default 'Breakthrough'
122
+ * @param {object} args.breakthrough the breakthrough candidate (must carry artifact_ids[])
123
+ * @param {number} [args.more_count] non-negative integer; number of queued candidates
124
+ * surfaced via the "More breakthroughs (N)" footer
125
+ * @param {string} [args.voice_line] Phase 120-03 D-17 additive slot. When present + non-empty,
126
+ * prepended as the first line of zones.body above the title.
127
+ * Caller (Plan 120-03 scanner.surfaceBreakthrough) audits
128
+ * the line via voice-scaffold.auditVoiceLine before passing;
129
+ * this renderer does NOT re-audit (single source of truth).
130
+ *
131
+ * @returns {object} { zones: {header, body, footer}, contract: {...} } OR
132
+ * { error: 'provenance_required' | 'dismiss_required' | 'verb_count_mismatch' | 'verb_order_mismatch' }
133
+ */
134
+ function renderShapeF7Breakthrough(args) {
135
+ const a = (args && typeof args === 'object') ? args : {};
136
+ const more_count = (Number.isInteger(a.more_count) && a.more_count >= 0) ? a.more_count : 0;
137
+ const bk = a.breakthrough;
138
+
139
+ // D-20 HARD FLOOR defense in depth -- refuse to render a provenance-less
140
+ // breakthrough. The Plan 120-00 schema layer rejects writes with empty
141
+ // artifact_ids; this is the SECOND structural enforcement point at the
142
+ // surface layer (in case writeBreakthrough is somehow bypassed).
143
+ if (!bk || typeof bk !== 'object' || !Array.isArray(bk.artifact_ids) || bk.artifact_ids.length === 0) {
144
+ return { error: 'provenance_required' };
145
+ }
146
+
147
+ // Build the locked-verbatim verbs array and assert the D-10 invariants.
148
+ // Defense in depth: the source-of-truth F7_VERBS is Object.freeze-d above;
149
+ // assertContractInvariants on the mutable copy gives a final structural
150
+ // assertion before composition.
151
+ const verbs = F7_VERBS.slice();
152
+ const check = assertContractInvariants(verbs);
153
+ if (!check.ok) {
154
+ return { error: check.reason };
155
+ }
156
+
157
+ // Row 1 (title): kind capitalized + theme + time anchor.
158
+ // Theme is sliced to 120 chars at the surface layer (defense in depth on top
159
+ // of the schema-layer 200-char sanitizer in Plan 120-00).
160
+ const kindKey = (typeof bk.kind === 'string') ? bk.kind : '';
161
+ const kindDisplay = KIND_DISPLAY_NAMES[kindKey] || kindKey || 'Pattern';
162
+ const themeRaw = (typeof bk.theme === 'string') ? bk.theme : '';
163
+ const theme = themeRaw.slice(0, 120);
164
+ const timeAnchor = formatTimeAnchor(bk.detected_at || Date.now());
165
+ const titleLine = kindDisplay + (theme ? ': ' + theme : '') + ' (' + timeAnchor + ')';
166
+
167
+ // Row 2 (verbs): the 5 verbs in [X] brackets, separated by double-space.
168
+ // Bracket style matches the F.4 / F.6 closed-vocab convention.
169
+ const verbsLine = verbs.map(function (v) { return '[' + v + ']'; }).join(' ');
170
+
171
+ // Phase 120-03 D-17 additive: optional voice_line prepend.
172
+ //
173
+ // When args.voice_line is a non-empty string, it is prepended as the FIRST line
174
+ // of zones.body, above the title line. This is the conversational opener
175
+ // composed by Plan 120-03 voice-scaffold.composeBreakthroughVoiceLine. The
176
+ // caller (scanner.surfaceBreakthrough) audits the line via auditVoiceLine
177
+ // before passing; this renderer does NOT re-audit (single source of truth, no
178
+ // duplicated regex logic at the surface layer).
179
+ //
180
+ // The contract.verbs / freeTextOffered / recommended / breakthrough_id /
181
+ // more_count fields are PRESERVED byte-stably across both branches (with-line
182
+ // and without-line). Plan 120-01 11-test byte-stability invariant is preserved
183
+ // when voice_line is absent (the without-line branch is the legacy code path).
184
+ const voiceLineRaw = (typeof a.voice_line === 'string') ? a.voice_line : '';
185
+ const voiceLine = voiceLineRaw.length > 0 ? voiceLineRaw : '';
186
+ const body = voiceLine
187
+ ? (voiceLine + '\n' + titleLine + '\n' + verbsLine)
188
+ : (titleLine + '\n' + verbsLine);
189
+
190
+ const header = (typeof a.header === 'string' && a.header.length > 0) ? a.header : 'Breakthrough';
191
+ // D-11 affordance: only surface "More breakthroughs (N)" when N > 0; empty
192
+ // footer otherwise so the renderer never lies about queued candidates.
193
+ const footer = (more_count > 0) ? ('More breakthroughs (' + more_count + ')') : '';
194
+
195
+ return {
196
+ zones: {
197
+ header: header,
198
+ body: body,
199
+ signals: '',
200
+ footer: footer,
201
+ },
202
+ contract: {
203
+ shape: 'F.7',
204
+ keyboard: 'askuserquestion',
205
+ verbs: verbs,
206
+ freeTextOffered: false, // closed-vocab; mirrors F.3 / F.4 / F.6 plan-review
207
+ mode: 'closed',
208
+ recommended: null, // closed-vocab does not mark a recommended verb
209
+ breakthrough_id: bk.id,
210
+ more_count: more_count,
211
+ },
212
+ };
213
+ }
214
+
215
+ module.exports = {
216
+ renderShapeF7Breakthrough: renderShapeF7Breakthrough,
217
+ F7_VERBS: F7_VERBS,
218
+ F7_DISMISS_INDEX: F7_DISMISS_INDEX,
219
+ formatTimeAnchor: formatTimeAnchor,
220
+ assertContractInvariants: assertContractInvariants,
221
+ KIND_DISPLAY_NAMES: KIND_DISPLAY_NAMES,
222
+ };
@@ -0,0 +1,233 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 120-01 Task 1 -- F.7 Breakthrough Surface renderer test suite.
5
+ *
6
+ * Per 120-01-PLAN.md behavior tests (11 tests):
7
+ * T1 F7_VERBS verbatim lock (5 strings, exact order, Object.freeze invariant)
8
+ * T2 F7_DISMISS_INDEX === 3 (zero-indexed position of 'Dismiss')
9
+ * T3 renderShapeF7Breakthrough happy path (contract envelope)
10
+ * T4 Two-row layout (title + verbs)
11
+ * T5 [Dismiss] MANDATORY guard (D-10 -- assertContractInvariants refuses without 'Dismiss')
12
+ * T6 D-20 HARD FLOOR -- empty artifact_ids returns {error: 'provenance_required'}
13
+ * T7 "More breakthroughs (N)" affordance (D-11; only when more_count > 0)
14
+ * T8 Closed-vocab carve-out -- contract.freeTextOffered === false; no Free-Text
15
+ * T9 formatTimeAnchor buckets (in the last hour / today / this week / etc.)
16
+ * T10 Canon Part 8 source-grep (zero brain-client / fetch brain.mindrian / cross-room aggregat*)
17
+ * T11 Em-dash invariant (HARD RULE; zero U+2014 in the renderer source)
18
+ *
19
+ * Pure CJS, node built-ins only, zero deps (Phase 87 invariant).
20
+ */
21
+ 'use strict';
22
+
23
+ const assert = require('node:assert/strict');
24
+ const test = require('node:test');
25
+ const fs = require('node:fs');
26
+ const path = require('node:path');
27
+
28
+ const RENDERER_PATH = path.resolve(__dirname, 'shape-f7-breakthrough-renderer.cjs');
29
+ const renderer = require('./shape-f7-breakthrough-renderer.cjs');
30
+ const {
31
+ renderShapeF7Breakthrough,
32
+ F7_VERBS,
33
+ F7_DISMISS_INDEX,
34
+ formatTimeAnchor,
35
+ assertContractInvariants,
36
+ KIND_DISPLAY_NAMES,
37
+ } = renderer;
38
+
39
+ // --- T1: F7_VERBS verbatim lock ---
40
+ test('T1: F7_VERBS is the 5 verbs verbatim in exact order', () => {
41
+ assert.deepEqual(F7_VERBS, ['Explore deeper', 'Confirm', 'File as decision', 'Dismiss', 'Back']);
42
+ assert.equal(F7_VERBS.length, 5);
43
+ });
44
+
45
+ test('T1b: F7_VERBS is Object.freeze-d -- mutation does not change the array', () => {
46
+ const before = F7_VERBS.slice();
47
+ // Frozen arrays: push throws in strict mode OR silently no-ops in sloppy mode.
48
+ // Either way, the canonical array contents must be unchanged after the attempt.
49
+ try { F7_VERBS.push('Free-Text'); } catch (_e) { /* expected in strict */ }
50
+ assert.deepEqual(F7_VERBS.slice(0, 5), before);
51
+ assert.equal(F7_VERBS.length, 5, 'mutation must not extend the frozen array');
52
+ });
53
+
54
+ // --- T2: F7_DISMISS_INDEX ---
55
+ test('T2: F7_DISMISS_INDEX === 3 (zero-indexed slot of Dismiss in F7_VERBS)', () => {
56
+ assert.equal(F7_DISMISS_INDEX, 3);
57
+ assert.equal(F7_VERBS[F7_DISMISS_INDEX], 'Dismiss');
58
+ });
59
+
60
+ // --- T3: happy path ---
61
+ test('T3: renderShapeF7Breakthrough returns canonical {zones, contract} envelope', () => {
62
+ const now = Date.now();
63
+ const out = renderShapeF7Breakthrough({
64
+ tier: 1,
65
+ breakthrough: {
66
+ id: 'bk:1',
67
+ kind: 'convergence',
68
+ theme: 'X',
69
+ confidence: 0.55,
70
+ artifact_ids: ['a1', 'a2', 'a3', 'a4'],
71
+ detected_at: now - 3600 * 1000,
72
+ },
73
+ more_count: 2,
74
+ });
75
+ assert.equal(typeof out, 'object');
76
+ assert.equal(out.contract.shape, 'F.7');
77
+ assert.deepEqual(out.contract.verbs, F7_VERBS);
78
+ assert.equal(out.contract.freeTextOffered, false);
79
+ assert.equal(out.contract.mode, 'closed');
80
+ assert.equal(out.contract.recommended, null);
81
+ assert.equal(out.contract.breakthrough_id, 'bk:1');
82
+ assert.equal(out.contract.more_count, 2);
83
+ assert.equal(typeof out.zones.header, 'string');
84
+ assert.equal(typeof out.zones.body, 'string');
85
+ });
86
+
87
+ // --- T4: two-row layout ---
88
+ test('T4: zones.body contains title row + 5-verb row with [X] brackets', () => {
89
+ const now = Date.now();
90
+ const out = renderShapeF7Breakthrough({
91
+ tier: 1,
92
+ breakthrough: {
93
+ id: 'bk:1',
94
+ kind: 'convergence',
95
+ theme: 'X',
96
+ artifact_ids: ['a1', 'a2', 'a3'],
97
+ detected_at: now - 1800 * 1000,
98
+ },
99
+ more_count: 0,
100
+ });
101
+ // Row 1: title with kind capitalized + theme + time anchor
102
+ assert.match(out.zones.body, /Convergence: X/);
103
+ assert.match(out.zones.body, /\(in the last hour\)/i);
104
+ // Row 2: 5 verbs in [X] brackets in the locked order
105
+ assert.match(out.zones.body, /\[Explore deeper\]/);
106
+ assert.match(out.zones.body, /\[Confirm\]/);
107
+ assert.match(out.zones.body, /\[File as decision\]/);
108
+ assert.match(out.zones.body, /\[Dismiss\]/);
109
+ assert.match(out.zones.body, /\[Back\]/);
110
+ });
111
+
112
+ // --- T5: D-10 mandatory Dismiss guard ---
113
+ test('T5: assertContractInvariants refuses a verbs array missing Dismiss (D-10 MANDATORY)', () => {
114
+ // Direct invariant call: a verbs array with the same length but Dismiss replaced.
115
+ const broken = ['Explore deeper', 'Confirm', 'File as decision', 'NotDismiss', 'Back'];
116
+ const result = assertContractInvariants(broken);
117
+ assert.equal(result.ok, false);
118
+ assert.equal(result.reason, 'dismiss_required');
119
+ });
120
+
121
+ test('T5b: assertContractInvariants refuses a wrong-count verbs array', () => {
122
+ const tooShort = ['Explore deeper', 'Confirm', 'File as decision', 'Dismiss'];
123
+ const r1 = assertContractInvariants(tooShort);
124
+ assert.equal(r1.ok, false);
125
+ assert.equal(r1.reason, 'verb_count_mismatch');
126
+
127
+ const wrong = ['Explore deeper', 'wrongverb', 'File as decision', 'Dismiss', 'Back'];
128
+ const r2 = assertContractInvariants(wrong);
129
+ assert.equal(r2.ok, false);
130
+ assert.equal(r2.reason, 'verb_order_mismatch');
131
+ });
132
+
133
+ // --- T6: D-20 HARD FLOOR -- empty artifact_ids ---
134
+ test('T6: D-20 HARD FLOOR -- empty artifact_ids returns {error: provenance_required}', () => {
135
+ const out = renderShapeF7Breakthrough({
136
+ tier: 1,
137
+ breakthrough: { id: 'bk:1', kind: 'convergence', theme: 'X', artifact_ids: [], detected_at: Date.now() },
138
+ more_count: 0,
139
+ });
140
+ assert.equal(typeof out, 'object');
141
+ assert.equal(out.error, 'provenance_required');
142
+ assert.equal(out.zones, undefined);
143
+ assert.equal(out.contract, undefined);
144
+ });
145
+
146
+ test('T6b: missing breakthrough returns provenance_required', () => {
147
+ const out = renderShapeF7Breakthrough({ tier: 1, more_count: 0 });
148
+ assert.equal(out.error, 'provenance_required');
149
+ });
150
+
151
+ test('T6c: artifact_ids not an array returns provenance_required', () => {
152
+ const out = renderShapeF7Breakthrough({
153
+ tier: 1,
154
+ breakthrough: { id: 'bk:1', kind: 'convergence', theme: 'X', artifact_ids: 'not-array', detected_at: Date.now() },
155
+ more_count: 0,
156
+ });
157
+ assert.equal(out.error, 'provenance_required');
158
+ });
159
+
160
+ // --- T7: More breakthroughs (N) affordance ---
161
+ test('T7: more_count > 0 renders "More breakthroughs (N)" in zones.footer', () => {
162
+ const out = renderShapeF7Breakthrough({
163
+ tier: 1,
164
+ breakthrough: { id: 'bk:1', kind: 'convergence', theme: 'X', artifact_ids: ['a1'], detected_at: Date.now() },
165
+ more_count: 2,
166
+ });
167
+ assert.match(out.zones.footer, /More breakthroughs \(2\)/);
168
+ });
169
+
170
+ test('T7b: more_count === 0 leaves zones.footer empty (no affordance)', () => {
171
+ const out = renderShapeF7Breakthrough({
172
+ tier: 1,
173
+ breakthrough: { id: 'bk:1', kind: 'convergence', theme: 'X', artifact_ids: ['a1'], detected_at: Date.now() },
174
+ more_count: 0,
175
+ });
176
+ assert.equal(out.zones.footer.indexOf('More breakthroughs'), -1);
177
+ });
178
+
179
+ // --- T8: closed-vocab carve-out ---
180
+ test('T8: contract.freeTextOffered === false; verbs does NOT contain Free-Text', () => {
181
+ const out = renderShapeF7Breakthrough({
182
+ tier: 1,
183
+ breakthrough: { id: 'bk:1', kind: 'convergence', theme: 'X', artifact_ids: ['a1'], detected_at: Date.now() },
184
+ more_count: 0,
185
+ });
186
+ assert.equal(out.contract.freeTextOffered, false);
187
+ assert.equal(out.contract.verbs.indexOf('Free-Text'), -1);
188
+ assert.equal(out.contract.verbs.length, 5);
189
+ });
190
+
191
+ // --- T9: formatTimeAnchor buckets ---
192
+ test('T9: formatTimeAnchor returns the correct bucket string per age', () => {
193
+ const now = Date.now();
194
+ // < 1 hour -> "in the last hour"
195
+ assert.equal(formatTimeAnchor(now - 30 * 60 * 1000), 'in the last hour');
196
+ // 1-8 hours -> "in the last N hours"
197
+ assert.match(formatTimeAnchor(now - 4 * 3600 * 1000), /in the last \d+ hours/);
198
+ // 8-24 hours -> "today"
199
+ assert.equal(formatTimeAnchor(now - 12 * 3600 * 1000), 'today');
200
+ // 1-2 days -> "in the last day"
201
+ assert.equal(formatTimeAnchor(now - 36 * 3600 * 1000), 'in the last day');
202
+ // 2-7 days -> "this week"
203
+ assert.equal(formatTimeAnchor(now - 4 * 24 * 3600 * 1000), 'this week');
204
+ // 7-14 days -> "in the last two weeks"
205
+ assert.equal(formatTimeAnchor(now - 10 * 24 * 3600 * 1000), 'in the last two weeks');
206
+ // > 14 days -> "on YYYY-MM-DD"
207
+ assert.match(formatTimeAnchor(now - 30 * 24 * 3600 * 1000), /^on \d{4}-\d{2}-\d{2}$/);
208
+ });
209
+
210
+ // --- T10: Canon Part 8 source-grep ---
211
+ test('T10: Canon Part 8 invariant -- zero brain-client / brain.mindrian / cross-room aggregat in renderer source', () => {
212
+ const src = fs.readFileSync(RENDERER_PATH, 'utf8');
213
+ assert.equal(/require\([^)]*brain-client[^)]*\)/.test(src), false, 'must not require brain-client');
214
+ assert.equal(/fetch\([^)]*brain\.mindrian[^)]*\)/.test(src), false, 'must not fetch brain.mindrian');
215
+ assert.equal(/cross.{0,3}room.{0,3}aggregat/i.test(src), false, 'must not mention cross-room aggregat');
216
+ });
217
+
218
+ // --- T11: em-dash invariant ---
219
+ test('T11: em-dash invariant -- zero U+2014 in renderer source (HARD RULE)', () => {
220
+ const src = fs.readFileSync(RENDERER_PATH, 'utf8');
221
+ // U+2014 is the em-dash. The test file itself avoids the literal char so this
222
+ // suite stays em-dash-free.
223
+ const emDash = String.fromCharCode(0x2014);
224
+ assert.equal(src.indexOf(emDash), -1, 'renderer source must contain zero em-dashes');
225
+ });
226
+
227
+ // --- Bonus: KIND_DISPLAY_NAMES sanity ---
228
+ test('Bonus: KIND_DISPLAY_NAMES covers the four known detector kinds', () => {
229
+ assert.equal(KIND_DISPLAY_NAMES.convergence, 'Convergence');
230
+ assert.equal(KIND_DISPLAY_NAMES.contradiction_resolved, 'Contradiction resolved');
231
+ assert.equal(KIND_DISPLAY_NAMES.cross_domain_analogy, 'Cross-domain analogy');
232
+ assert.equal(KIND_DISPLAY_NAMES.reverse_salient_closed, 'Reverse salient closed');
233
+ });