@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,160 @@
1
+ 'use strict';
2
+ /*
3
+ * Phase 120-03 Wave 2 Task 2 -- review-queue unit tests (Tests 11-14).
4
+ *
5
+ * Per CONTEXT.md "Claude's Discretion" item 3: a separate SQLite db at the
6
+ * rooms-home level ($ROOMS_HOME/.rooms/breakthrough-review-queue.db). Mirrors
7
+ * the Phase 119-01 rooms-meta.db precedent. Sibling pattern; does NOT pollute
8
+ * per-room room.db with cross-room review backlog.
9
+ */
10
+
11
+ const test = require('node:test');
12
+ const { strict: assert } = require('node:assert');
13
+ const fs = require('node:fs');
14
+ const os = require('node:os');
15
+ const path = require('node:path');
16
+
17
+ const QUEUE_PATH = path.resolve(__dirname, 'review-queue.cjs');
18
+ const reviewQueue = require('./review-queue.cjs');
19
+
20
+ const {
21
+ openReviewQueue,
22
+ insertReviewCandidate,
23
+ listPendingReviews,
24
+ REVIEW_QUEUE_DB_PATH,
25
+ TABLE_DDL,
26
+ } = reviewQueue;
27
+
28
+ function makeTmpRoomsHome(prefix) {
29
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
30
+ }
31
+
32
+ // --- T11: openReviewQueue creates db + table ---
33
+ test('T11: openReviewQueue creates .rooms/breakthrough-review-queue.db with the review_candidates table', () => {
34
+ const tmpHome = makeTmpRoomsHome('p120-03-rq-t11-');
35
+ try {
36
+ const result = openReviewQueue(tmpHome);
37
+ assert.equal(result.fallback, false,
38
+ 'expected non-fallback open, got: ' + JSON.stringify(result));
39
+ assert.ok(result.db);
40
+ const dbPath = path.join(tmpHome, '.rooms', 'breakthrough-review-queue.db');
41
+ assert.ok(fs.existsSync(dbPath), 'expected db file at ' + dbPath);
42
+ // Verify the table schema
43
+ const rows = result.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='review_candidates'").all();
44
+ assert.equal(rows.length, 1);
45
+ // Verify the 10 columns
46
+ const cols = result.db.prepare("PRAGMA table_info(review_candidates)").all();
47
+ const colNames = cols.map(c => c.name).sort();
48
+ assert.deepEqual(colNames, [
49
+ 'artifact_ids_json', 'breakthrough_id', 'confidence', 'id',
50
+ 'kind', 'queued_at', 'review_status', 'reviewed_at', 'room_slug', 'theme',
51
+ ]);
52
+ result.db.close();
53
+ } finally {
54
+ fs.rmSync(tmpHome, { recursive: true, force: true });
55
+ }
56
+ });
57
+
58
+ // --- T12: insertReviewCandidate row contents ---
59
+ test('T12: insertReviewCandidate row populates all 10 columns + defaults', () => {
60
+ const tmpHome = makeTmpRoomsHome('p120-03-rq-t12-');
61
+ try {
62
+ const result = openReviewQueue(tmpHome);
63
+ const candidate = {
64
+ id: 'bk:test:t12',
65
+ kind: 'convergence',
66
+ confidence: 0.42,
67
+ theme: 'test-theme',
68
+ artifact_ids: ['a1', 'a2', 'a3'],
69
+ };
70
+ const ins = insertReviewCandidate(result.db, candidate, 'test-room');
71
+ assert.equal(ins.ok, true);
72
+ const row = result.db.prepare("SELECT * FROM review_candidates WHERE id = ?").get(ins.queue_id);
73
+ assert.ok(row);
74
+ assert.equal(row.breakthrough_id, 'bk:test:t12');
75
+ assert.equal(row.kind, 'convergence');
76
+ assert.equal(row.confidence, 0.42);
77
+ assert.equal(row.theme, 'test-theme');
78
+ assert.equal(row.room_slug, 'test-room');
79
+ assert.deepEqual(JSON.parse(row.artifact_ids_json), ['a1', 'a2', 'a3']);
80
+ assert.ok(typeof row.queued_at === 'number');
81
+ assert.equal(row.reviewed_at, null);
82
+ assert.equal(row.review_status, 'pending');
83
+ result.db.close();
84
+ } finally {
85
+ fs.rmSync(tmpHome, { recursive: true, force: true });
86
+ }
87
+ });
88
+
89
+ // --- T13: listPendingReviews returns only status='pending' ---
90
+ test('T13: listPendingReviews filters by review_status = pending', () => {
91
+ const tmpHome = makeTmpRoomsHome('p120-03-rq-t13-');
92
+ try {
93
+ const result = openReviewQueue(tmpHome);
94
+ const db = result.db;
95
+ // Insert 3 candidates
96
+ const c1 = insertReviewCandidate(db, { id: 'bk:1', kind: 'convergence', confidence: 0.40, theme: 't1', artifact_ids: ['a1'] }, 'r1');
97
+ const c2 = insertReviewCandidate(db, { id: 'bk:2', kind: 'convergence', confidence: 0.41, theme: 't2', artifact_ids: ['a2'] }, 'r1');
98
+ const c3 = insertReviewCandidate(db, { id: 'bk:3', kind: 'convergence', confidence: 0.42, theme: 't3', artifact_ids: ['a3'] }, 'r1');
99
+ // Mark c2 as reviewed
100
+ db.prepare("UPDATE review_candidates SET review_status = 'reviewed', reviewed_at = ? WHERE id = ?")
101
+ .run(Date.now(), c2.queue_id);
102
+ const pending = listPendingReviews(db, 100);
103
+ assert.equal(pending.length, 2);
104
+ const ids = pending.map(p => p.id).sort();
105
+ assert.deepEqual(ids, [c1.queue_id, c3.queue_id].sort());
106
+ db.close();
107
+ } finally {
108
+ fs.rmSync(tmpHome, { recursive: true, force: true });
109
+ }
110
+ });
111
+
112
+ // --- T14: graceful degradation -- non-writable rooms_home ---
113
+ test('T14: openReviewQueue falls back to in-memory db when rooms_home is not writable', () => {
114
+ // Use a path that cannot exist as a directory (file path)
115
+ const tmpFile = fs.mkdtempSync(path.join(os.tmpdir(), 'p120-03-rq-t14-'));
116
+ const notADir = path.join(tmpFile, 'i-am-a-file.txt');
117
+ fs.writeFileSync(notADir, 'blocking');
118
+ try {
119
+ // notADir is a FILE, not a dir; mkdirSync inside should fail
120
+ const result = openReviewQueue(notADir);
121
+ // Either fallback=true (in-memory) OR open failed -- both are graceful (no throw)
122
+ assert.ok(typeof result === 'object');
123
+ if (result.db) {
124
+ // Falling back to in-memory db -- ensure inserts still work
125
+ const ins = insertReviewCandidate(result.db, {
126
+ id: 'bk:t14',
127
+ kind: 'convergence',
128
+ confidence: 0.40,
129
+ theme: 't',
130
+ artifact_ids: ['a'],
131
+ }, 'fallback');
132
+ assert.equal(ins.ok, true);
133
+ result.db.close();
134
+ } else {
135
+ // ok if it returned a no-db handle gracefully
136
+ assert.equal(result.fallback, true);
137
+ }
138
+ } finally {
139
+ fs.rmSync(tmpFile, { recursive: true, force: true });
140
+ }
141
+ });
142
+
143
+ // --- T15: REVIEW_QUEUE_DB_PATH constant ---
144
+ test('T15: REVIEW_QUEUE_DB_PATH constant exported verbatim', () => {
145
+ assert.equal(REVIEW_QUEUE_DB_PATH, '.rooms/breakthrough-review-queue.db');
146
+ });
147
+
148
+ // --- T16: TABLE_DDL contains review_candidates ---
149
+ test('T16: TABLE_DDL exported + creates the review_candidates table', () => {
150
+ assert.ok(typeof TABLE_DDL === 'string');
151
+ assert.ok(TABLE_DDL.indexOf('review_candidates') >= 0);
152
+ });
153
+
154
+ // --- Canon Part 8 + em-dash invariants ---
155
+ test('T17: Canon Part 8 source-grep + em-dash invariant on review-queue.cjs', () => {
156
+ const src = fs.readFileSync(QUEUE_PATH, 'utf8');
157
+ assert.ok(!/require\(.+brain-client/.test(src));
158
+ assert.ok(!/fetch.+brain\.mindrian/.test(src));
159
+ assert.equal(src.indexOf('—'), -1);
160
+ });
@@ -0,0 +1,229 @@
1
+ 'use strict';
2
+ /*
3
+ * Phase 120-03 Wave 2 Task 3 -- scanner D-17 voice-scaffold + D-18 ethics-fence
4
+ * integration tests (Tests 1-7).
5
+ *
6
+ * Coverage:
7
+ * T1 -- HARD_CEILING happy path: scanForBreakthroughs returns top with voice_line
8
+ * populated; surfaceBreakthrough passes voice_line via payload to pickShape;
9
+ * F.7 envelope's zones.body starts with the voice line.
10
+ * T2 -- SOFT_BAND route: scanForBreakthroughs does NOT return SOFT_BAND as top;
11
+ * ethics-fence.queueForReview inserts row in .rooms/breakthrough-review-queue.db;
12
+ * breakthrough_in_review_queue memory_event lands; NO breakthrough_surfaced.
13
+ * T3 -- HARD_FLOOR refusal: defensive (writeBreakthrough already refuses provenance-less
14
+ * inputs; we verify the classifier returns HARD_FLOOR at the ethics-fence layer).
15
+ * T4 -- BELOW_FLOOR soft-fire only: confidence < 0.35 candidates do not surface.
16
+ * T5 -- F.7 renderer accepts voice_line additively + closed-vocab preserved.
17
+ * T6 -- F.7 renderer without voice_line preserves Plan 120-01 byte-stable output.
18
+ * T7 -- D-17 violator voice line replaced with structural default before surface.
19
+ */
20
+
21
+ const test = require('node:test');
22
+ const { strict: assert } = require('node:assert');
23
+ const fs = require('node:fs');
24
+ const os = require('node:os');
25
+ const path = require('node:path');
26
+
27
+ const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
28
+ const scanner = require(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'scanner.cjs'));
29
+ const ethicsFence = require(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'ethics-fence.cjs'));
30
+ const reviewQueue = require(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'review-queue.cjs'));
31
+ const voiceScaffold = require(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'voice-scaffold.cjs'));
32
+ const navigation = require(path.join(REPO_ROOT, 'lib', 'core', 'navigation.cjs'));
33
+ const { openRoomDb } = require(path.join(REPO_ROOT, 'lib', 'core', 'room-db.cjs'));
34
+ const f7Renderer = require(path.join(REPO_ROOT, 'lib', 'hmi', 'shape-f7-breakthrough-renderer.cjs'));
35
+
36
+ function makeTmpRoom(prefix) {
37
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
38
+ fs.mkdirSync(path.join(dir, '.mindrian'), { recursive: true });
39
+ return dir;
40
+ }
41
+
42
+ function seedArtifact(db, id) {
43
+ const nowMs = Date.now();
44
+ db.prepare(
45
+ "INSERT INTO nodes (id, type, properties, source_path, created_by, review_status, created_at, last_seen_at) " +
46
+ "VALUES (?, 'artifact', '{}', 'test', 'system', 'confirmed', ?, ?)"
47
+ ).run(id, nowMs, nowMs);
48
+ }
49
+
50
+ function writeWhitespaceGap(roomDir, artifacts, theme, differential) {
51
+ const target = path.join(roomDir, '.mindrian', 'whitespace-results.json');
52
+ let payload = { gaps: [] };
53
+ if (fs.existsSync(target)) {
54
+ try { payload = JSON.parse(fs.readFileSync(target, 'utf8')); } catch (_e) {}
55
+ if (!Array.isArray(payload.gaps)) payload.gaps = [];
56
+ }
57
+ payload.gaps.push({
58
+ theme: theme,
59
+ artifacts: artifacts,
60
+ differential: differential,
61
+ sections: ['s1', 's2'],
62
+ detected_at: Date.now(),
63
+ });
64
+ fs.writeFileSync(target, JSON.stringify(payload));
65
+ }
66
+
67
+ // --- T1: HARD_CEILING happy path -- surfaceBreakthrough passes voice_line ---
68
+ test('T1: surfaceBreakthrough composes + passes voice_line into F.7 envelope', () => {
69
+ const roomDir = makeTmpRoom('p120-03-t3-t1-');
70
+ try {
71
+ const db = openRoomDb(roomDir);
72
+ seedArtifact(db, 'a1'); seedArtifact(db, 'a2'); seedArtifact(db, 'a3'); seedArtifact(db, 'a4');
73
+ writeWhitespaceGap(roomDir, ['a1', 'a2', 'a3', 'a4'], 'voice-line-test', 0.7);
74
+ db.close();
75
+
76
+ const result = scanner.scanForBreakthroughs(roomDir);
77
+ assert.ok(result.top, 'expected top candidate');
78
+ const db2 = openRoomDb(roomDir);
79
+ const surface = scanner.surfaceBreakthrough(result.top, {
80
+ db: db2, roomDir: roomDir, tier: 1, more_count: 0,
81
+ });
82
+ assert.equal(surface.ok, true);
83
+ // Voice line must be on the rendered envelope OR the surface result.
84
+ assert.ok(surface.voice_line, 'expected voice_line on surface result');
85
+ // Verify it passes the auditor.
86
+ const audit = voiceScaffold.auditVoiceLine(surface.voice_line);
87
+ assert.equal(audit.ok, true, 'surfaced voice_line must pass auditor, violations: ' +
88
+ JSON.stringify(audit.violations) + ' line: ' + surface.voice_line);
89
+ db2.close();
90
+ } finally {
91
+ fs.rmSync(roomDir, { recursive: true, force: true });
92
+ }
93
+ });
94
+
95
+ // --- T2: SOFT_BAND route -- queue + memory_event + no surface ---
96
+ test('T2: SOFT_BAND candidate routes to review queue (no surface event)', () => {
97
+ const roomDir = makeTmpRoom('p120-03-t3-t2-');
98
+ const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'p120-03-t3-t2-home-'));
99
+ try {
100
+ const db = openRoomDb(roomDir);
101
+
102
+ // Hand-craft a SOFT_BAND candidate (confidence 0.42) and exercise the
103
+ // ethics-fence directly. The scanner's scanForBreakthroughs is detector-driven
104
+ // and the math layer typically produces confidence in narrow bands -- so this
105
+ // test exercises the ethics-fence wiring directly for clarity.
106
+ const cand = {
107
+ id: 'bk:soft-band:t2',
108
+ kind: 'convergence',
109
+ confidence: 0.42,
110
+ artifact_ids: ['a1', 'a2'],
111
+ theme: 'soft-band-route',
112
+ detected_at: Date.now(),
113
+ };
114
+ const band = ethicsFence.classifyEthicsBand(cand);
115
+ assert.equal(band, 'SOFT_BAND');
116
+
117
+ const queued = ethicsFence.queueForReview(cand, tmpHome, { db: db, roomSlug: 'test-soft' });
118
+ assert.equal(queued.ok, true);
119
+
120
+ // Memory event landed
121
+ const events = navigation.findRecentChanges(db, 0, {
122
+ eventType: 'breakthrough_in_review_queue',
123
+ limit: 10,
124
+ });
125
+ assert.ok(events.length >= 1, 'expected breakthrough_in_review_queue event');
126
+
127
+ // No breakthrough_surfaced event
128
+ const surfaced = navigation.findRecentChanges(db, 0, {
129
+ eventType: 'breakthrough_surfaced',
130
+ limit: 10,
131
+ });
132
+ assert.equal(surfaced.length, 0,
133
+ 'SOFT_BAND candidates must NOT emit breakthrough_surfaced');
134
+ db.close();
135
+ } finally {
136
+ fs.rmSync(roomDir, { recursive: true, force: true });
137
+ fs.rmSync(tmpHome, { recursive: true, force: true });
138
+ }
139
+ });
140
+
141
+ // --- T3: HARD_FLOOR refusal at ethics-fence layer ---
142
+ test('T3: HARD_FLOOR candidate (no provenance) -> classifier refuses', () => {
143
+ const cand = { id: 'bk:hf:t3', kind: 'convergence', confidence: 0.80, artifact_ids: [], theme: 'no-prov' };
144
+ const band = ethicsFence.classifyEthicsBand(cand);
145
+ assert.equal(band, 'HARD_FLOOR');
146
+ });
147
+
148
+ // --- T4: BELOW_FLOOR -- soft-fire only ---
149
+ test('T4: BELOW_FLOOR candidate -> classifier returns BELOW_FLOOR (soft-fire territory)', () => {
150
+ const cand = { id: 'bk:bf:t4', kind: 'convergence', confidence: 0.25, artifact_ids: ['a1'], theme: 'below' };
151
+ const band = ethicsFence.classifyEthicsBand(cand);
152
+ assert.equal(band, 'BELOW_FLOOR');
153
+ });
154
+
155
+ // --- T5: F.7 renderer accepts voice_line additively ---
156
+ test('T5: F.7 renderer prepends voice_line to zones.body when present', () => {
157
+ const bk = {
158
+ id: 'bk:vl-test',
159
+ kind: 'convergence',
160
+ theme: 'voice-line-prepend',
161
+ artifact_ids: ['a1', 'a2', 'a3'],
162
+ detected_at: Date.now(),
163
+ };
164
+ const voiceLine = "You're seeing a convergence on X (artifacts a1, a2, a3) -- by uploading -- in the last hour.";
165
+ const envelope = f7Renderer.renderShapeF7Breakthrough({
166
+ tier: 1,
167
+ breakthrough: bk,
168
+ more_count: 0,
169
+ voice_line: voiceLine,
170
+ });
171
+ assert.ok(envelope.zones, 'expected zones in envelope');
172
+ // Voice line is the FIRST line in zones.body
173
+ const firstLine = envelope.zones.body.split('\n')[0];
174
+ assert.equal(firstLine, voiceLine,
175
+ 'expected first line of zones.body to be voice_line, got: ' + firstLine);
176
+ // Verbs preserved verbatim
177
+ assert.deepEqual(envelope.contract.verbs, ['Explore deeper', 'Confirm', 'File as decision', 'Dismiss', 'Back']);
178
+ assert.equal(envelope.contract.freeTextOffered, false);
179
+ assert.equal(envelope.contract.recommended, null);
180
+ });
181
+
182
+ // --- T6: F.7 renderer without voice_line -- existing byte-stable output ---
183
+ test('T6: F.7 renderer without voice_line preserves Plan 120-01 byte-stable output', () => {
184
+ const bk = {
185
+ id: 'bk:no-vl',
186
+ kind: 'convergence',
187
+ theme: 'no-voice-line',
188
+ artifact_ids: ['a1', 'a2', 'a3'],
189
+ detected_at: Date.now(),
190
+ };
191
+ const envelope = f7Renderer.renderShapeF7Breakthrough({
192
+ tier: 1,
193
+ breakthrough: bk,
194
+ more_count: 0,
195
+ // no voice_line
196
+ });
197
+ const firstLine = envelope.zones.body.split('\n')[0];
198
+ // First line is the title row, not the voice line
199
+ assert.ok(firstLine.indexOf('Convergence') >= 0,
200
+ 'expected first line to start with title (kind display), got: ' + firstLine);
201
+ assert.ok(firstLine.indexOf("You're seeing") < 0,
202
+ 'expected no voice line, got: ' + firstLine);
203
+ });
204
+
205
+ // --- T7: D-17 violator voice line replaced with structural default ---
206
+ test('T7: surfaceBreakthrough replaces D-17-violating voice line with structural default', () => {
207
+ // We test the gate logic directly: a roomState.mechanism_phrase that violates
208
+ // D-17 rule 4 (unbacked superlative) is replaced by the structural default.
209
+ // composeBreakthroughVoiceLine + auditVoiceLine are pure -- we drive them
210
+ // through the same code path the scanner uses (line composition then audit).
211
+ const bk = {
212
+ id: 'bk:bad-voice',
213
+ kind: 'convergence',
214
+ theme: 'X',
215
+ artifact_ids: ['a1', 'a2'],
216
+ detected_at: Date.now(),
217
+ };
218
+ // Bad mechanism phrase: contains the forbidden superlative 'breakthrough'
219
+ // without numeric backing -> rule 4 violation.
220
+ const badRoomState = { mechanism_phrase: 'by achieving a major breakthrough' };
221
+ const badLine = voiceScaffold.composeBreakthroughVoiceLine(bk, badRoomState);
222
+ const badAudit = voiceScaffold.auditVoiceLine(badLine);
223
+ assert.equal(badAudit.ok, false, 'expected violation, got pass: ' + badLine);
224
+ // The replacement (structural default; roomState=null) MUST pass auditor.
225
+ const defaultLine = voiceScaffold.composeBreakthroughVoiceLine(bk, null);
226
+ const defaultAudit = voiceScaffold.auditVoiceLine(defaultLine);
227
+ assert.equal(defaultAudit.ok, true,
228
+ 'structural default must pass auditor, violations: ' + JSON.stringify(defaultAudit.violations));
229
+ });