@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
@@ -0,0 +1,267 @@
1
+ 'use strict';
2
+
3
+ /*
4
+ * Phase 120-02 Wave 2 Task 3 -- scanner unit tests (Tests 1-9).
5
+ *
6
+ * Coverage:
7
+ * 1. scanForBreakthroughs cold-room (D-16 silence)
8
+ * 2. happy-path (one convergence hit)
9
+ * 3. D-13 resurfacing filter applied
10
+ * 4. D-14 confirmed filter applied
11
+ * 5. D-15 filed-as-decision filter applied
12
+ * 6. D-19 throttle filter applied
13
+ * 7. D-20 writeBreakthrough contract enforced
14
+ * 8. surfaceBreakthrough emits breakthrough_surfaced + flips properties.surfaced
15
+ * 9. surfaceBreakthrough D-20 enforcement at surface time (provenance check)
16
+ */
17
+
18
+ const test = require('node:test');
19
+ const { strict: assert } = require('node:assert');
20
+ const fs = require('node:fs');
21
+ const os = require('node:os');
22
+ const path = require('node:path');
23
+
24
+ const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
25
+ const scanner = require(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'scanner.cjs'));
26
+ const schema = require(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'schema.cjs'));
27
+ const navigation = require(path.join(REPO_ROOT, 'lib', 'core', 'navigation.cjs'));
28
+ const { openRoomDb } = require(path.join(REPO_ROOT, 'lib', 'core', 'room-db.cjs'));
29
+
30
+ function makeTmpRoom(prefix) {
31
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
32
+ fs.mkdirSync(path.join(dir, '.mindrian'), { recursive: true });
33
+ return dir;
34
+ }
35
+
36
+ function seedArtifact(db, id) {
37
+ const nowMs = Date.now();
38
+ db.prepare(
39
+ "INSERT INTO nodes (id, type, properties, source_path, created_by, review_status, created_at, last_seen_at) " +
40
+ "VALUES (?, 'artifact', '{}', 'test', 'system', 'confirmed', ?, ?)"
41
+ ).run(id, nowMs, nowMs);
42
+ }
43
+
44
+ function writeWhitespaceGap(roomDir, artifacts, theme, differential) {
45
+ const target = path.join(roomDir, '.mindrian', 'whitespace-results.json');
46
+ let payload = { gaps: [] };
47
+ if (fs.existsSync(target)) {
48
+ try { payload = JSON.parse(fs.readFileSync(target, 'utf8')); } catch (_e) {}
49
+ if (!Array.isArray(payload.gaps)) payload.gaps = [];
50
+ }
51
+ payload.gaps.push({
52
+ theme: theme,
53
+ artifacts: artifacts,
54
+ differential: differential,
55
+ sections: ['s1', 's2'],
56
+ detected_at: Date.now(),
57
+ });
58
+ fs.writeFileSync(target, JSON.stringify(payload));
59
+ }
60
+
61
+ test('120-02 Task 3 Test 1: scanForBreakthroughs cold-room (D-16 silence)', () => {
62
+ const roomDir = makeTmpRoom('p120-02-t3-t1-');
63
+ // No math files seeded; no events.
64
+ const result = scanner.scanForBreakthroughs(roomDir);
65
+ assert.equal(result.top, null);
66
+ assert.equal(result.more_count, 0);
67
+ assert.ok(Array.isArray(result.throttled_kinds));
68
+ });
69
+
70
+ test('120-02 Task 3 Test 2: scanForBreakthroughs happy-path single convergence hit', () => {
71
+ const roomDir = makeTmpRoom('p120-02-t3-t2-');
72
+ // Open db to seed artifacts (so DERIVED_FROM FK passes).
73
+ const db = openRoomDb(roomDir);
74
+ seedArtifact(db, 'a1'); seedArtifact(db, 'a2'); seedArtifact(db, 'a3'); seedArtifact(db, 'a4');
75
+ // Write a whitespace gap with 4 artifacts (hard-fire eligible per D-03).
76
+ writeWhitespaceGap(roomDir, ['a1', 'a2', 'a3', 'a4'], 'convergent-theme', 0.7);
77
+ db.close();
78
+
79
+ const result = scanner.scanForBreakthroughs(roomDir);
80
+ assert.ok(result.top, 'expected a top candidate');
81
+ assert.equal(result.top.kind, 'convergence');
82
+ assert.ok(Array.isArray(result.top.artifact_ids));
83
+ assert.ok(result.top.artifact_ids.length >= 3);
84
+ });
85
+
86
+ test('120-02 Task 3 Test 3: D-13 resurfacing filter -- dismissed-in-cooldown breakthrough filtered out', () => {
87
+ const roomDir = makeTmpRoom('p120-02-t3-t3-');
88
+ const db = openRoomDb(roomDir);
89
+ seedArtifact(db, 'a1'); seedArtifact(db, 'a2'); seedArtifact(db, 'a3'); seedArtifact(db, 'a4');
90
+ writeWhitespaceGap(roomDir, ['a1', 'a2', 'a3', 'a4'], 'cooldown-theme', 0.7);
91
+ // First scan: capture the deterministic breakthrough_id by running once.
92
+ const first = scanner.scanForBreakthroughs(roomDir);
93
+ // Re-open db for our seeding.
94
+ const db2 = openRoomDb(roomDir);
95
+ // Simulate a dismiss event for the breakthrough we just found, 3 days ago.
96
+ const targetId = first.top ? first.top.id : null;
97
+ if (targetId) {
98
+ const r = navigation.logMemoryEvent(db2, 'breakthrough_dismissed', {
99
+ breakthrough_id: targetId,
100
+ kind: 'convergence',
101
+ artifact_ids_at_dismiss: ['a1', 'a2', 'a3', 'a4'],
102
+ source_path: 'system:test',
103
+ created_by: 'system',
104
+ });
105
+ // Backdate to 3 days ago (still in cooldown).
106
+ db2.prepare("UPDATE nodes SET created_at = ? WHERE id = ?")
107
+ .run(Date.now() - 3 * 24 * 3600 * 1000, r.eventId);
108
+ }
109
+ db2.close();
110
+ // Second scan: the cooldown-active candidate must NOT come back as top.
111
+ const second = scanner.scanForBreakthroughs(roomDir);
112
+ if (second.top && targetId) {
113
+ assert.notEqual(second.top.id, targetId, 'cooldown-active breakthrough must not resurface');
114
+ } else {
115
+ assert.equal(second.top, null);
116
+ }
117
+ });
118
+
119
+ test('120-02 Task 3 Test 4: D-14 confirmed filter -- confirmed breakthrough filtered out', () => {
120
+ const roomDir = makeTmpRoom('p120-02-t3-t4-');
121
+ const db = openRoomDb(roomDir);
122
+ seedArtifact(db, 'a1'); seedArtifact(db, 'a2'); seedArtifact(db, 'a3'); seedArtifact(db, 'a4');
123
+ writeWhitespaceGap(roomDir, ['a1', 'a2', 'a3', 'a4'], 'confirm-theme', 0.7);
124
+ const first = scanner.scanForBreakthroughs(roomDir);
125
+ const targetId = first.top ? first.top.id : null;
126
+ const db2 = openRoomDb(roomDir);
127
+ if (targetId) {
128
+ navigation.logMemoryEvent(db2, 'breakthrough_confirmed', {
129
+ breakthrough_id: targetId,
130
+ kind: 'convergence',
131
+ source_path: 'system:test',
132
+ created_by: 'system',
133
+ });
134
+ }
135
+ db2.close();
136
+ const second = scanner.scanForBreakthroughs(roomDir);
137
+ if (second.top && targetId) {
138
+ assert.notEqual(second.top.id, targetId, 'confirmed once-only breakthrough must not resurface');
139
+ } else {
140
+ assert.equal(second.top, null);
141
+ }
142
+ });
143
+
144
+ test('120-02 Task 3 Test 5: D-15 filed-as-decision filter -- filed breakthrough filtered out', () => {
145
+ const roomDir = makeTmpRoom('p120-02-t3-t5-');
146
+ const db = openRoomDb(roomDir);
147
+ seedArtifact(db, 'a1'); seedArtifact(db, 'a2'); seedArtifact(db, 'a3'); seedArtifact(db, 'a4');
148
+ writeWhitespaceGap(roomDir, ['a1', 'a2', 'a3', 'a4'], 'filed-theme', 0.7);
149
+ const first = scanner.scanForBreakthroughs(roomDir);
150
+ const targetId = first.top ? first.top.id : null;
151
+ const db2 = openRoomDb(roomDir);
152
+ if (targetId) {
153
+ navigation.logMemoryEvent(db2, 'breakthrough_filed_as_decision', {
154
+ breakthrough_id: targetId,
155
+ kind: 'convergence',
156
+ source_path: 'system:test',
157
+ created_by: 'system',
158
+ });
159
+ }
160
+ db2.close();
161
+ const second = scanner.scanForBreakthroughs(roomDir);
162
+ if (second.top && targetId) {
163
+ assert.notEqual(second.top.id, targetId, 'filed-as-decision breakthrough must not resurface');
164
+ } else {
165
+ assert.equal(second.top, null);
166
+ }
167
+ });
168
+
169
+ test('120-02 Task 3 Test 6: D-19 throttle filter -- throttled kind excluded + throttled_kinds reported', () => {
170
+ const roomDir = makeTmpRoom('p120-02-t3-t6-');
171
+ const db = openRoomDb(roomDir);
172
+ // Seed 12 surfaced + 5 dismissed convergence events to trip D-19 (>30% dismiss rate
173
+ // on sample 12 > min sample 10).
174
+ for (let i = 0; i < 12; i++) {
175
+ navigation.logMemoryEvent(db, 'breakthrough_surfaced', {
176
+ breakthrough_id: 'bk:s' + i, kind: 'convergence',
177
+ source_path: 'system:test', created_by: 'system',
178
+ });
179
+ }
180
+ for (let i = 0; i < 5; i++) {
181
+ navigation.logMemoryEvent(db, 'breakthrough_dismissed', {
182
+ breakthrough_id: 'bk:s' + i, kind: 'convergence',
183
+ source_path: 'system:test', created_by: 'system',
184
+ });
185
+ }
186
+ seedArtifact(db, 'a1'); seedArtifact(db, 'a2'); seedArtifact(db, 'a3'); seedArtifact(db, 'a4');
187
+ writeWhitespaceGap(roomDir, ['a1', 'a2', 'a3', 'a4'], 'throttle-theme', 0.7);
188
+ db.close();
189
+
190
+ const result = scanner.scanForBreakthroughs(roomDir);
191
+ // The convergence-kind candidate is now throttled.
192
+ assert.ok(result.throttled_kinds.indexOf('convergence') >= 0,
193
+ 'convergence should be in throttled_kinds');
194
+ // And the convergence candidate must NOT come back as top (only convergence
195
+ // was detected; with it filtered out, top should be null).
196
+ assert.equal(result.top, null);
197
+ });
198
+
199
+ test('120-02 Task 3 Test 7: D-20 writeBreakthrough contract enforced -- top has Breakthrough node + DERIVED_FROM edges', () => {
200
+ const roomDir = makeTmpRoom('p120-02-t3-t7-');
201
+ const db = openRoomDb(roomDir);
202
+ seedArtifact(db, 'a1'); seedArtifact(db, 'a2'); seedArtifact(db, 'a3'); seedArtifact(db, 'a4');
203
+ writeWhitespaceGap(roomDir, ['a1', 'a2', 'a3', 'a4'], 'd20-contract', 0.7);
204
+ db.close();
205
+
206
+ const result = scanner.scanForBreakthroughs(roomDir);
207
+ assert.ok(result.top);
208
+ const targetId = result.top.id;
209
+
210
+ // Reopen db to verify the Breakthrough node + DERIVED_FROM edges landed.
211
+ const db2 = openRoomDb(roomDir);
212
+ const bkRow = db2.prepare("SELECT id, type FROM nodes WHERE id = ? AND type = 'breakthrough'").get(targetId);
213
+ assert.ok(bkRow, 'Breakthrough node must exist after scanForBreakthroughs');
214
+ const edgeRows = db2.prepare(
215
+ "SELECT COUNT(*) AS c FROM edges WHERE source = ? AND type = 'DERIVED_FROM'"
216
+ ).get(targetId);
217
+ assert.ok(edgeRows.c >= 1, 'D-20 invariant: at least one DERIVED_FROM edge required');
218
+ db2.close();
219
+ });
220
+
221
+ test('120-02 Task 3 Test 8: surfaceBreakthrough emits breakthrough_surfaced + flips properties.surfaced', () => {
222
+ const roomDir = makeTmpRoom('p120-02-t3-t8-');
223
+ const db = openRoomDb(roomDir);
224
+ seedArtifact(db, 'a1'); seedArtifact(db, 'a2'); seedArtifact(db, 'a3'); seedArtifact(db, 'a4');
225
+ // First scan persists the breakthrough.
226
+ writeWhitespaceGap(roomDir, ['a1', 'a2', 'a3', 'a4'], 'surface-test', 0.7);
227
+ db.close();
228
+ const result = scanner.scanForBreakthroughs(roomDir);
229
+ assert.ok(result.top);
230
+ const db2 = openRoomDb(roomDir);
231
+ const surface = scanner.surfaceBreakthrough(result.top, {
232
+ db: db2, roomDir: roomDir, tier: 1, more_count: 0,
233
+ });
234
+ assert.equal(surface.ok, true);
235
+ // breakthrough_surfaced event emitted.
236
+ const events = navigation.findRecentChanges(db2, 0, { eventType: 'breakthrough_surfaced', limit: 10 });
237
+ assert.ok(events.length >= 1);
238
+ assert.equal(events[0].properties.breakthrough_id, result.top.id);
239
+ // properties.surfaced flipped to true.
240
+ const bkRow = db2.prepare("SELECT properties FROM nodes WHERE id = ?").get(result.top.id);
241
+ const props = JSON.parse(bkRow.properties);
242
+ assert.equal(props.surfaced, true);
243
+ db2.close();
244
+ });
245
+
246
+ test('120-02 Task 3 Test 9: surfaceBreakthrough D-20 enforcement -- refuses provenance-less surfacing', () => {
247
+ const roomDir = makeTmpRoom('p120-02-t3-t9-');
248
+ const db = openRoomDb(roomDir);
249
+ // Insert a "fake" breakthrough node WITHOUT any DERIVED_FROM edges (simulating
250
+ // a constitutional bypass attempt).
251
+ const nowMs = Date.now();
252
+ db.prepare(
253
+ "INSERT INTO nodes (id, type, properties, source_path, created_by, review_status, created_at, last_seen_at) " +
254
+ "VALUES (?, 'breakthrough', ?, 'test', 'system', 'proposed', ?, ?)"
255
+ ).run('bk:no-prov', JSON.stringify({ kind: 'convergence', surfaced: false }), nowMs, nowMs);
256
+ const fakeBreakthrough = { id: 'bk:no-prov', kind: 'convergence', artifact_ids: [], confidence: 0.6 };
257
+ const surface = scanner.surfaceBreakthrough(fakeBreakthrough, {
258
+ db: db, roomDir: roomDir, tier: 1, more_count: 0,
259
+ });
260
+ assert.equal(surface.ok, false);
261
+ assert.equal(surface.reason, 'provenance_required');
262
+ // breakthrough_surface_blocked event should be emitted per the surface gate
263
+ // contract (key invariant from the prompt: D-20 enforcement point #4 emits this).
264
+ const blocked = navigation.findRecentChanges(db, 0, { eventType: 'breakthrough_surface_blocked', limit: 10 });
265
+ assert.equal(blocked.length, 1, 'breakthrough_surface_blocked event should land on provenance-less refusal');
266
+ db.close();
267
+ });
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+ // Phase 120-00 Wave 1 Task 3 -- Breakthrough node schema + writeBreakthrough atomic-transaction
3
+ // writer. Per CONTEXT.md D-18 HARD FLOOR + D-20 Cypher-provable principle: every
4
+ // Breakthrough node that lands in room.db MUST have at least one DERIVED_FROM edge to an
5
+ // Artifact node. The Cypher invariant
6
+ // MATCH (b:Breakthrough)-[:DERIVED_FROM]->(a:Artifact) WHERE b.id = $id RETURN count(a)
7
+ // is guaranteed >= 1 BY CONSTRUCTION (this function refuses to land a breakthrough without
8
+ // provenance) AND BY TRANSACTION (the node insert + N edge inserts are wrapped in a single
9
+ // SQLite transaction; partial state cannot land; node:sqlite does not expose better-sqlite3
10
+ // style db.transaction(fn) so we use BEGIN / COMMIT / ROLLBACK explicitly, matching the
11
+ // Phase 119-01 lib/core/room-discard-cascade.cjs pattern + Phase 109 nodes-provenance
12
+ // migration pattern).
13
+ //
14
+ // Canon Part 4: every choice is graph data. The Breakthrough node is the typed graph
15
+ // artifact of pattern detection.
16
+ // Canon Part 8: writes are LOCAL only; no Brain coupling. The HARD RULE source-grep
17
+ // tripwire fires in tests/test-120-00-scaffold.sh Gate 8 + schema.test.cjs Test 11.
18
+ // Canon Part 9: ALL edge writes route through navigation.cjs::writeEdge chokepoint.
19
+ // The node insert is direct INSERT into the `nodes` table (the same pattern as
20
+ // memory-events.cjs::logEvent which IS the chokepoint helper for memory_event nodes;
21
+ // schema.cjs mirrors this precedent as the per-type insert helper for breakthrough
22
+ // nodes).
23
+ //
24
+ // CONSTITUTIONAL: this function is the ONLY way breakthroughs land in room.db. Plan 120-02
25
+ // session-start scanner MUST route through this function. Bypassing it = Canon Part 4 + D-20
26
+ // violation. The Cypher-provable invariant is not a procedural promise -- it is a structural
27
+ // property of the SQL transaction wrapper.
28
+ //
29
+ // Em-dash HARD RULE (CLAUDE.md feedback_no_emdashes): zero U+2014 in source.
30
+
31
+ const crypto = require('node:crypto');
32
+ const navigation = require('../navigation.cjs');
33
+
34
+ const BREAKTHROUGH_NODE_TYPE = 'breakthrough';
35
+
36
+ // The 4 D-01 detector kinds. Must stay aligned with lib/core/breakthrough/detectors.cjs
37
+ // DETECTOR_TYPES (frozen array). Duplication is intentional -- this module does NOT
38
+ // require detectors.cjs (Canon Part 7 reuse-before-build does not apply when the
39
+ // inverse dependency would couple the schema layer to detector internals).
40
+ const BREAKTHROUGH_KIND = Object.freeze({
41
+ CONVERGENCE: 'convergence',
42
+ CONTRADICTION_RESOLVED: 'contradiction_resolved',
43
+ CROSS_DOMAIN_ANALOGY: 'cross_domain_analogy',
44
+ REVERSE_SALIENT_CLOSED: 'reverse_salient_closed',
45
+ });
46
+
47
+ // validateProvenance is the D-20 HARD FLOOR entry guard. Refuses any breakthrough
48
+ // without at least one non-empty artifact_id. Returns sanitized_artifact_ids on ok
49
+ // so writeBreakthrough can rely on a clean array (filtered for empty strings +
50
+ // non-string entries upstream of the SQLite write).
51
+ function validateProvenance(breakthrough) {
52
+ if (!breakthrough || typeof breakthrough !== 'object') {
53
+ return { ok: false, reason: 'invalid_input' };
54
+ }
55
+ if (typeof breakthrough.id !== 'string' || breakthrough.id.length === 0) {
56
+ return { ok: false, reason: 'breakthrough_id_required' };
57
+ }
58
+ const ids = Array.isArray(breakthrough.artifact_ids)
59
+ ? breakthrough.artifact_ids.filter((s) => typeof s === 'string' && s.length > 0)
60
+ : [];
61
+ if (ids.length === 0) {
62
+ // D-20 HARD FLOOR: every breakthrough must be Cypher-provable. Provenance-less
63
+ // breakthroughs are a CONSTITUTIONAL VIOLATION; the write is refused at the gate.
64
+ return { ok: false, reason: 'provenance_required' };
65
+ }
66
+ return { ok: true, sanitized_artifact_ids: ids };
67
+ }
68
+
69
+ // writeBreakthrough(db, breakthrough) -> {ok:true, breakthroughId, edgeIds:[...]} |
70
+ // {ok:false, reason:string}
71
+ // Atomic transaction wrapper. node:sqlite has no db.transaction(fn) -- we use BEGIN /
72
+ // COMMIT / ROLLBACK explicitly. If any step fails, the entire transaction rolls back --
73
+ // partial Breakthrough state cannot land.
74
+ //
75
+ // Inserts a row into `nodes` with type='breakthrough'. The properties JSON carries:
76
+ // kind, confidence, theme, differential, cross_section_linked,
77
+ // detected_at, window_start, window_end, surfaced (initially false).
78
+ // Plan 120-02 scanner flips properties.surfaced -> true at F.7 surface time.
79
+ //
80
+ // For each artifact_id, writes a DERIVED_FROM edge via navigation.writeEdge. The
81
+ // edge properties carry {detected_at, detector_kind}. If any writeEdge call fails,
82
+ // throws to trigger ROLLBACK.
83
+ function writeBreakthrough(db, breakthrough) {
84
+ const validation = validateProvenance(breakthrough);
85
+ if (!validation.ok) {
86
+ return { ok: false, reason: validation.reason };
87
+ }
88
+ if (!db || typeof db.prepare !== 'function' || typeof db.exec !== 'function') {
89
+ return { ok: false, reason: 'invalid_db' };
90
+ }
91
+ const artifactIds = validation.sanitized_artifact_ids;
92
+ const nowMs = Date.now();
93
+ const breakthroughId = breakthrough.id;
94
+ const detectorKind = typeof breakthrough.kind === 'string' ? breakthrough.kind : 'unknown';
95
+ const confidence = typeof breakthrough.confidence === 'number' ? breakthrough.confidence : 0;
96
+
97
+ // Build the properties payload for the breakthrough node. Theme sliced to 200 chars
98
+ // per the Phase 90-06 sanitizeDetailScalar precedent (Canon Part 8 boundary).
99
+ const props = {
100
+ kind: detectorKind,
101
+ confidence: confidence,
102
+ theme: typeof breakthrough.theme === 'string' ? breakthrough.theme.slice(0, 200) : '',
103
+ differential: typeof breakthrough.differential === 'number' ? breakthrough.differential : 0,
104
+ cross_section_linked: !!breakthrough.cross_section_linked,
105
+ detected_at: typeof breakthrough.detected_at === 'number' ? breakthrough.detected_at : nowMs,
106
+ window_start: typeof breakthrough.window_start === 'number' ? breakthrough.window_start : (nowMs - 14 * 24 * 3600 * 1000),
107
+ window_end: typeof breakthrough.window_end === 'number' ? breakthrough.window_end : nowMs,
108
+ surfaced: false,
109
+ };
110
+ const propsJson = JSON.stringify(props);
111
+
112
+ // Atomic transaction (node:sqlite BEGIN / COMMIT / ROLLBACK). Mirror the Phase 119-01
113
+ // room-discard-cascade.cjs pattern + the Phase 109 nodes-provenance migration pattern.
114
+ let edgeIds = [];
115
+ try {
116
+ db.exec('BEGIN');
117
+ } catch (err) {
118
+ return { ok: false, reason: 'transaction_begin_failed:' + (err.message || '').slice(0, 80) };
119
+ }
120
+
121
+ try {
122
+ // Step 1: insert the Breakthrough node directly into the nodes table.
123
+ // source_path + created_by + review_status filled per Phase 109 schema CHECK
124
+ // constraints (created_by IN ('user','larry','import','brain','system')).
125
+ db.prepare(
126
+ "INSERT INTO nodes (id, type, properties, source_path, created_by, confidence, review_status, created_at, last_seen_at) " +
127
+ "VALUES (?, 'breakthrough', ?, 'system:breakthrough-schema', 'system', ?, 'proposed', ?, ?)"
128
+ ).run(breakthroughId, propsJson, confidence, nowMs, nowMs);
129
+
130
+ // Step 2: insert N DERIVED_FROM edges via navigation.writeEdge.
131
+ for (const artifactId of artifactIds) {
132
+ const result = navigation.writeEdge(db, {
133
+ source_id: breakthroughId,
134
+ target_id: artifactId,
135
+ edge_type: 'DERIVED_FROM',
136
+ properties: { detected_at: nowMs, detector_kind: detectorKind },
137
+ });
138
+ if (!result.ok) {
139
+ throw new Error('writeEdge_failed:' + result.reason);
140
+ }
141
+ // The navigation.cjs writeEdge returns edge_id (snake_case per Phase 125-00).
142
+ edgeIds.push(result.edge_id);
143
+ }
144
+
145
+ db.exec('COMMIT');
146
+ } catch (err) {
147
+ try { db.exec('ROLLBACK'); } catch (_e2) { /* swallow */ }
148
+ const reason = err && err.message ? err.message : 'transaction_failed';
149
+ return { ok: false, reason: reason };
150
+ }
151
+
152
+ return {
153
+ ok: true,
154
+ breakthroughId: breakthroughId,
155
+ edgeIds: edgeIds,
156
+ };
157
+ }
158
+
159
+ module.exports = {
160
+ writeBreakthrough,
161
+ validateProvenance,
162
+ BREAKTHROUGH_NODE_TYPE,
163
+ BREAKTHROUGH_KIND,
164
+ };