@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,359 @@
1
+ 'use strict';
2
+ // Phase 120-00 Wave 1 -- the 4 breakthrough pattern detectors + classifyFireTier + the
3
+ // frozen threshold constants. Per CONTEXT.md D-01..D-06 + D-18 + D-20:
4
+ // - PURE functions over local graph state (Canon Part 8 -- no Brain coupling)
5
+ // - ALL chokepoint reads route via lib/core/navigation.cjs (Canon Part 9 D-06)
6
+ // - Every emitted Breakthrough carries provenance.artifact_ids (D-20 HARD FLOOR)
7
+ // - Reads Phase 117 math-layer JSON output; NEVER recomputes the math
8
+ //
9
+ // Canon Part 8 invariants (source-grep enforced by tests/test-120-00-scaffold.sh
10
+ // Gate 8 + the Test 12 in detectors.test.cjs):
11
+ // - NO require of lib/core/brain-client.cjs or any mcp-server-brain/* module.
12
+ // - NO fetch to brain.mindrian.* domain.
13
+ // - NO cross-user OR cross-room aggregation -- "cross-domain" in this module
14
+ // means cross-section WITHIN the same room (per CONTEXT.md scope D-04).
15
+ //
16
+ // Canon Part 9 invariant: the math layer (Phase 117) writes JSON files into
17
+ // roomDir/.mindrian/; detectors READ those files but NEVER re-execute the
18
+ // underlying Python scripts (no child_process.exec on hsi-*.py / rs-engine.py).
19
+ // Source-grep tripwire: zero matches for child_process.exec.+\.py.
20
+ //
21
+ // Em-dash HARD RULE (CLAUDE.md feedback_no_emdashes): use "--" not the U+2014
22
+ // character anywhere in this file (comments, log lines, strings).
23
+
24
+ const path = require('node:path');
25
+ const fs = require('node:fs');
26
+ const crypto = require('node:crypto');
27
+
28
+ // Pin the DETECTOR_THRESHOLDS to the verbatim values locked in CONTEXT.md
29
+ // D-01..D-06. Plan 120-02 session-start scanner reads these constants at
30
+ // scan time, so they MUST stay frozen-by-Object.freeze + immutable in test
31
+ // surface. Any change to these numbers is a canon-level decision, not a
32
+ // source-level edit -- requires re-running /gsd:discuss-phase 120.
33
+ const DETECTOR_THRESHOLDS = Object.freeze({
34
+ SOFT_FIRE_MIN_ARTIFACTS: 3, // D-02
35
+ SOFT_FIRE_MIN_CONFIDENCE: 0.25, // D-02
36
+ HARD_FIRE_MIN_ARTIFACTS: 4, // D-03
37
+ HARD_FIRE_CROSS_SECTION_BYPASS: 3, // D-03 OR clause
38
+ HARD_FIRE_MIN_CONFIDENCE: 0.35, // D-03
39
+ SEMANTIC_SIMILARITY_THRESHOLD: 0.40, // D-04 (within-room noise correlation pushes threshold up)
40
+ WINDOW_DAYS_DEFAULT: 14, // D-06 ethical fence (no surface for material > 14 days old)
41
+ });
42
+
43
+ const DETECTOR_TYPES = Object.freeze([
44
+ 'convergence',
45
+ 'contradiction_resolved',
46
+ 'cross_domain_analogy',
47
+ 'reverse_salient_closed',
48
+ ]);
49
+
50
+ // Reverse-salient lagging-component signal threshold (D-05 score-delta signal).
51
+ // signed_diff magnitude >= 0.5 means the score crossed its baseline meaningfully.
52
+ const RS_SCORE_DELTA_THRESHOLD = 0.5;
53
+
54
+ // Helper: build a deterministic Breakthrough id from kind + sorted artifact_ids + ts.
55
+ function buildBreakthroughId(kind, artifactIds, nowMs) {
56
+ const seed = kind + ':' + artifactIds.slice().sort().join(',') + ':' + nowMs;
57
+ const hash = crypto.createHash('sha256').update(seed).digest('hex').slice(0, 16);
58
+ return 'breakthrough:' + kind + ':' + hash;
59
+ }
60
+
61
+ // Helper: build a Breakthrough candidate with confidence formula applied. Per
62
+ // CONTEXT.md D-12 scoring blend: confidence = clip(0, 0.95, 0.25 + 0.10*count + 0.30*differential).
63
+ // The 0.10/0.30 weights deliberately bias the formula so cross-section linkage
64
+ // (an extra signal on top of count + differential) does NOT auto-promote a
65
+ // 3-artifact convergence past 0.35 -- classifyFireTier handles the bypass via
66
+ // the count-with-bypass OR clause + the conf >= 0.35 floor.
67
+ function buildCandidate(kind, artifactIds, theme, differential, crossSectionLinked, nowMs, windowMs) {
68
+ const filtered = (artifactIds || []).filter((s) => typeof s === 'string' && s.length > 0);
69
+ const baseConf = 0.25 + 0.10 * filtered.length + 0.30 * (typeof differential === 'number' ? differential : 0);
70
+ const confidence = Math.min(0.95, Math.max(0, baseConf));
71
+ return {
72
+ id: buildBreakthroughId(kind, filtered, nowMs),
73
+ kind: kind,
74
+ confidence: confidence,
75
+ artifact_ids: filtered,
76
+ theme: typeof theme === 'string' ? theme.slice(0, 200) : '',
77
+ differential: typeof differential === 'number' ? differential : 0,
78
+ cross_section_linked: !!crossSectionLinked,
79
+ detected_at: nowMs,
80
+ window_start: nowMs - windowMs,
81
+ window_end: nowMs,
82
+ };
83
+ }
84
+
85
+ function classifyFireTier(candidate) {
86
+ const T = DETECTOR_THRESHOLDS;
87
+ if (!candidate || !Array.isArray(candidate.artifact_ids)) return 'below_floor';
88
+ const count = candidate.artifact_ids.length;
89
+ const conf = typeof candidate.confidence === 'number' ? candidate.confidence : 0;
90
+ // D-03: hard-fire when (artifact_count >= 4) OR (>= 3 AND cross_section_linked),
91
+ // AND confidence >= 0.35.
92
+ const hard_eligible =
93
+ ((count >= T.HARD_FIRE_MIN_ARTIFACTS) ||
94
+ (count >= T.HARD_FIRE_CROSS_SECTION_BYPASS && candidate.cross_section_linked))
95
+ && conf >= T.HARD_FIRE_MIN_CONFIDENCE;
96
+ if (hard_eligible) return 'hard';
97
+ // D-02: soft-fire when artifact_count >= 3 AND confidence >= 0.25 (below hard tier).
98
+ const soft_eligible = count >= T.SOFT_FIRE_MIN_ARTIFACTS && conf >= T.SOFT_FIRE_MIN_CONFIDENCE;
99
+ if (soft_eligible) return 'soft';
100
+ return 'below_floor';
101
+ }
102
+
103
+ function partitionByTier(candidates) {
104
+ const hits = [];
105
+ const soft_fires = [];
106
+ for (const c of candidates) {
107
+ const tier = classifyFireTier(c);
108
+ if (tier === 'hard') hits.push(c);
109
+ else if (tier === 'soft') soft_fires.push(c);
110
+ // 'below_floor' falls off -- never surfaces, never logs to buffer.
111
+ }
112
+ return { hits: hits, soft_fires: soft_fires };
113
+ }
114
+
115
+ // Internal: resolve the math-layer JSON path within roomDir/.mindrian/. Returns
116
+ // null when roomDir is invalid OR the file does not exist (graceful degradation:
117
+ // Phase 117 may not have fired yet on a fresh room).
118
+ function resolveMathFile(roomState, basename) {
119
+ if (!roomState || typeof roomState.roomDir !== 'string') return null;
120
+ const fullPath = path.join(roomState.roomDir, '.mindrian', basename);
121
+ if (!fs.existsSync(fullPath)) return null;
122
+ return fullPath;
123
+ }
124
+
125
+ function readMathJson(roomState, basename) {
126
+ const p = resolveMathFile(roomState, basename);
127
+ if (!p) return null;
128
+ try {
129
+ const raw = fs.readFileSync(p, 'utf8');
130
+ return JSON.parse(raw);
131
+ } catch (_e) {
132
+ // Malformed JSON returns null -- the caller short-circuits to empty result.
133
+ return null;
134
+ }
135
+ }
136
+
137
+ // Compute the windowMs + nowMs for a detector call. Caps windowDays at 14 (D-06).
138
+ function resolveWindow(roomState, opts) {
139
+ const nowMs = (roomState && typeof roomState.now === 'number') ? roomState.now : Date.now();
140
+ const requestedDays = (opts && typeof opts.window_days === 'number') ? opts.window_days
141
+ : ((roomState && typeof roomState.window_days === 'number') ? roomState.window_days
142
+ : DETECTOR_THRESHOLDS.WINDOW_DAYS_DEFAULT);
143
+ const cappedDays = Math.min(DETECTOR_THRESHOLDS.WINDOW_DAYS_DEFAULT, Math.max(1, requestedDays));
144
+ return { nowMs: nowMs, windowMs: cappedDays * 24 * 3600 * 1000 };
145
+ }
146
+
147
+ // --- detectConvergence (D-01 detector 1) ---
148
+ // Reads whitespace.gaps[] from .mindrian/whitespace-results.json (Phase 117 math output).
149
+ // Each gap: {gap_id?, theme, artifacts: string[], differential: number, sections?: string[],
150
+ // detected_at?: number}. A gap with N artifacts where N >= 3 (after empty-string filtering)
151
+ // is a candidate. cross_section_linked = sections.length >= 2.
152
+ function detectConvergence(roomState, opts) {
153
+ const { nowMs, windowMs } = resolveWindow(roomState, opts);
154
+ const data = readMathJson(roomState, 'whitespace-results.json');
155
+ if (!data || !Array.isArray(data.gaps)) {
156
+ return { hits: [], soft_fires: [] };
157
+ }
158
+ const windowStart = nowMs - windowMs;
159
+ const candidates = [];
160
+ for (const gap of data.gaps) {
161
+ if (!gap || typeof gap !== 'object') continue;
162
+ // Window filter: detected_at older than windowStart is excluded (D-06).
163
+ if (typeof gap.detected_at === 'number' && gap.detected_at < windowStart) continue;
164
+ const artifacts = Array.isArray(gap.artifacts) ? gap.artifacts.filter((s) => typeof s === 'string' && s.length > 0) : [];
165
+ if (artifacts.length < DETECTOR_THRESHOLDS.SOFT_FIRE_MIN_ARTIFACTS) continue;
166
+ const sections = Array.isArray(gap.sections) ? gap.sections.filter((s) => typeof s === 'string' && s.length > 0) : [];
167
+ const crossSectionLinked = sections.length >= 2;
168
+ const candidate = buildCandidate(
169
+ 'convergence',
170
+ artifacts,
171
+ typeof gap.theme === 'string' ? gap.theme : '',
172
+ typeof gap.differential === 'number' ? gap.differential : 0,
173
+ crossSectionLinked,
174
+ nowMs,
175
+ windowMs
176
+ );
177
+ candidates.push(candidate);
178
+ }
179
+ return partitionByTier(candidates);
180
+ }
181
+
182
+ // --- detectContradictionResolved (D-01 detector 2) ---
183
+ // Reads CONTRADICTS edges from room.db within the window. When a CONTRADICTS edge
184
+ // in the window has properties.resolved === true, that's a candidate -- the pair
185
+ // of artifacts is the provenance. The "resolved" flag is set by Phase 116
186
+ // unresolved-tension-hook closure logic when a tension is resolved by a new artifact.
187
+ //
188
+ // Per Canon Part 9 D-06: this read goes through navigation.cjs. We lazy-require
189
+ // the navigation module so test code can override it (Test 6 seeds CONTRADICTS
190
+ // edges directly into the seeded db).
191
+ function detectContradictionResolved(roomState, opts) {
192
+ const { nowMs, windowMs } = resolveWindow(roomState, opts);
193
+ if (!roomState || !roomState.db) {
194
+ return { hits: [], soft_fires: [] };
195
+ }
196
+ const windowStart = nowMs - windowMs;
197
+ const candidates = [];
198
+ try {
199
+ // Query CONTRADICTS edges directly via the room.db handle. The edges table
200
+ // schema (per lib/core/lazygraph-ops.cjs::initSchema) is
201
+ // (source TEXT, target TEXT, type TEXT, properties TEXT).
202
+ // We filter by type='CONTRADICTS' and json_extract(properties,'$.resolved')='true'/1.
203
+ // The window filter applies to properties.resolved_at if present.
204
+ const rows = roomState.db.prepare(
205
+ "SELECT source, target, properties FROM edges WHERE type = 'CONTRADICTS'"
206
+ ).all();
207
+ for (const row of rows) {
208
+ if (!row || typeof row.properties !== 'string') continue;
209
+ let props;
210
+ try { props = JSON.parse(row.properties); } catch (_e) { continue; }
211
+ if (!props || props.resolved !== true) continue;
212
+ const resolvedAt = typeof props.resolved_at === 'number' ? props.resolved_at : nowMs;
213
+ if (resolvedAt < windowStart) continue;
214
+ const sourceId = typeof row.source === 'string' ? row.source : '';
215
+ const targetId = typeof row.target === 'string' ? row.target : '';
216
+ const artifacts = [sourceId, targetId].filter((s) => s.length > 0);
217
+ if (artifacts.length < 2) continue;
218
+ const candidate = buildCandidate(
219
+ 'contradiction_resolved',
220
+ artifacts,
221
+ typeof props.theme === 'string' ? props.theme : '',
222
+ typeof props.differential === 'number' ? props.differential : 0.5,
223
+ true, // cross-section is implicit for a CONTRADICTS pair (different artifacts)
224
+ nowMs,
225
+ windowMs
226
+ );
227
+ candidates.push(candidate);
228
+ }
229
+ } catch (_err) {
230
+ return { hits: [], soft_fires: [] };
231
+ }
232
+ // contradiction_resolved is inherently a 2-artifact event (source + target of the
233
+ // CONTRADICTS edge). The generic SOFT_FIRE_MIN_ARTIFACTS=3 floor was tuned for
234
+ // convergence (whitespace gaps with N>=3 by definition). For this detector we
235
+ // apply a separate tier rule: every resolved contradiction in the window fires
236
+ // at minimum at SOFT tier (since the act of resolution is itself the signal);
237
+ // promotion to HARD requires the confidence floor (>= 0.35) -- same as the
238
+ // generic D-03 hard floor.
239
+ const hits = [];
240
+ const soft_fires = [];
241
+ for (const c of candidates) {
242
+ if (c.confidence >= DETECTOR_THRESHOLDS.HARD_FIRE_MIN_CONFIDENCE) {
243
+ hits.push(c);
244
+ } else if (c.confidence >= DETECTOR_THRESHOLDS.SOFT_FIRE_MIN_CONFIDENCE) {
245
+ soft_fires.push(c);
246
+ }
247
+ }
248
+ return { hits: hits, soft_fires: soft_fires };
249
+ }
250
+
251
+ // --- detectCrossDomainAnalogy (D-01 detector 3) ---
252
+ // Reads analogy.zones[] from .mindrian/discovery-cycle-results.json (Phase 117 math output).
253
+ // Per CONTEXT.md SCOPE clarification + D-04: "cross-domain means cross-section within
254
+ // the same room, NEVER cross-user or cross-room aggregation". Cross-section is
255
+ // encoded by source_section !== target_section AND similarity >= 0.40.
256
+ function detectCrossDomainAnalogy(roomState, opts) {
257
+ const { nowMs, windowMs } = resolveWindow(roomState, opts);
258
+ const data = readMathJson(roomState, 'discovery-cycle-results.json');
259
+ if (!data) return { hits: [], soft_fires: [] };
260
+ // The file can either have analogy_whitespace.zones or analogy.zones depending on
261
+ // the Phase 117 writer version. Accept both.
262
+ const zones = (data.analogy_whitespace && Array.isArray(data.analogy_whitespace.zones)) ? data.analogy_whitespace.zones
263
+ : (data.analogy && Array.isArray(data.analogy.zones)) ? data.analogy.zones
264
+ : [];
265
+ if (!zones.length) return { hits: [], soft_fires: [] };
266
+ const windowStart = nowMs - windowMs;
267
+ const candidates = [];
268
+ for (const zone of zones) {
269
+ if (!zone || typeof zone !== 'object') continue;
270
+ if (typeof zone.detected_at === 'number' && zone.detected_at < windowStart) continue;
271
+ const similarity = typeof zone.similarity === 'number' ? zone.similarity : 0;
272
+ if (similarity < DETECTOR_THRESHOLDS.SEMANTIC_SIMILARITY_THRESHOLD) continue;
273
+ const sourceArt = typeof zone.source_artifact_id === 'string' ? zone.source_artifact_id : '';
274
+ const targetArt = typeof zone.target_artifact_id === 'string' ? zone.target_artifact_id : '';
275
+ const artifacts = [sourceArt, targetArt].filter((s) => s.length > 0);
276
+ if (artifacts.length < 2) continue;
277
+ const sourceSection = typeof zone.source_section === 'string' ? zone.source_section : '';
278
+ const targetSection = typeof zone.target_section === 'string' ? zone.target_section : '';
279
+ const crossSectionLinked = sourceSection !== targetSection && sourceSection.length > 0 && targetSection.length > 0;
280
+ // D-04 boost: similarity feeds the differential blend so high-similarity cross-section
281
+ // matches push past the soft floor naturally.
282
+ const candidate = buildCandidate(
283
+ 'cross_domain_analogy',
284
+ artifacts,
285
+ typeof zone.theme === 'string' ? zone.theme : sourceSection + ' x ' + targetSection,
286
+ similarity, // pass similarity as the differential -- 0.40+ -> 0.12+ added to baseConf
287
+ crossSectionLinked,
288
+ nowMs,
289
+ windowMs
290
+ );
291
+ candidates.push(candidate);
292
+ }
293
+ return partitionByTier(candidates);
294
+ }
295
+
296
+ // --- detectReverseSalientClosed (D-01 detector 4) ---
297
+ // Reads rs.pairs[] from .mindrian/.rs-engine-results.json (Phase 117 math output).
298
+ // Per CONTEXT.md D-05: BOTH signals required for hard-fire:
299
+ // (a) graph-level proof -- closure_edge_now_exists === true (a structural transition
300
+ // that did not hold at window_start but holds now), AND
301
+ // (b) score-delta -- |signed_diff| >= RS_SCORE_DELTA_THRESHOLD (the lagging-component
302
+ // score crossed its baseline within the window).
303
+ // Single-signal candidates return soft_fire only.
304
+ function detectReverseSalientClosed(roomState, opts) {
305
+ const { nowMs, windowMs } = resolveWindow(roomState, opts);
306
+ const data = readMathJson(roomState, '.rs-engine-results.json');
307
+ if (!data || !Array.isArray(data.pairs)) {
308
+ return { hits: [], soft_fires: [] };
309
+ }
310
+ const windowStart = nowMs - windowMs;
311
+ const candidates = [];
312
+ for (const pair of data.pairs) {
313
+ if (!pair || typeof pair !== 'object') continue;
314
+ if (typeof pair.detected_at === 'number' && pair.detected_at < windowStart) continue;
315
+ const sourceArt = typeof pair.source_artifact_id === 'string' ? pair.source_artifact_id : '';
316
+ const targetArt = typeof pair.target_artifact_id === 'string' ? pair.target_artifact_id : '';
317
+ const artifacts = [sourceArt, targetArt].filter((s) => s.length > 0);
318
+ if (artifacts.length < 2) continue;
319
+
320
+ const signedDiff = typeof pair.signed_diff === 'number' ? pair.signed_diff : 0;
321
+ const scoreDeltaSignal = Math.abs(signedDiff) >= RS_SCORE_DELTA_THRESHOLD;
322
+ const graphSignal = pair.closure_edge_now_exists === true;
323
+ // D-05 BOTH-signals invariant: only when both fire do we get a hard candidate.
324
+ // Single-signal candidates land at soft tier via the confidence floor manipulation:
325
+ // we cap differential at 0.3 for single-signal cases (drives conf below 0.35).
326
+ const bothSignals = scoreDeltaSignal && graphSignal;
327
+ const differential = bothSignals ? 0.6 : 0.2;
328
+
329
+ const sourceSection = typeof pair.source_section === 'string' ? pair.source_section : '';
330
+ const targetSection = typeof pair.target_section === 'string' ? pair.target_section : '';
331
+ const crossSectionLinked = sourceSection !== targetSection && sourceSection.length > 0 && targetSection.length > 0;
332
+
333
+ const candidate = buildCandidate(
334
+ 'reverse_salient_closed',
335
+ artifacts,
336
+ typeof pair.theme === 'string' ? pair.theme : '',
337
+ differential,
338
+ crossSectionLinked,
339
+ nowMs,
340
+ windowMs
341
+ );
342
+ candidates.push(candidate);
343
+ }
344
+ return partitionByTier(candidates);
345
+ }
346
+
347
+ module.exports = {
348
+ detectConvergence,
349
+ detectContradictionResolved,
350
+ detectCrossDomainAnalogy,
351
+ detectReverseSalientClosed,
352
+ classifyFireTier,
353
+ partitionByTier,
354
+ buildCandidate,
355
+ buildBreakthroughId,
356
+ DETECTOR_THRESHOLDS,
357
+ DETECTOR_TYPES,
358
+ RS_SCORE_DELTA_THRESHOLD,
359
+ };