@mindrian_os/install 1.13.0-beta.16 → 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 (219) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +36 -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 +4 -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 +4 -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 +58 -0
  51. package/commands/mva-option.md +91 -0
  52. package/commands/new-project.md +4 -0
  53. package/commands/onboard.md +22 -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 +31 -88
  94. package/lib/agents/auto-explore-agent.cjs +82 -0
  95. package/lib/agents/mva/brain-classic-traps.cjs +77 -0
  96. package/lib/agents/mva/brain-cross-domain.cjs +79 -0
  97. package/lib/agents/mva/brain-similar-ventures.cjs +93 -0
  98. package/lib/agents/mva/dashboard-graph-neighborhood.cjs +72 -0
  99. package/lib/agents/mva/index.cjs +42 -0
  100. package/lib/agents/mva/six-hats-red-black.cjs +137 -0
  101. package/lib/agents/mva/tavily-funding-scan.cjs +147 -0
  102. package/lib/agents/mva/test-all-six-agents.cjs +467 -0
  103. package/lib/conversation/operator.cjs +64 -0
  104. package/lib/conversation/operator.test.cjs +160 -0
  105. package/lib/core/breakthrough/canary.cjs +134 -0
  106. package/lib/core/breakthrough/canary.test.cjs +136 -0
  107. package/lib/core/breakthrough/detectors.cjs +359 -0
  108. package/lib/core/breakthrough/detectors.test.cjs +333 -0
  109. package/lib/core/breakthrough/ethics-fence.cjs +127 -0
  110. package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
  111. package/lib/core/breakthrough/resurfacing.cjs +150 -0
  112. package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
  113. package/lib/core/breakthrough/review-queue.cjs +154 -0
  114. package/lib/core/breakthrough/review-queue.test.cjs +160 -0
  115. package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
  116. package/lib/core/breakthrough/scanner.cjs +426 -0
  117. package/lib/core/breakthrough/scanner.test.cjs +267 -0
  118. package/lib/core/breakthrough/schema.cjs +164 -0
  119. package/lib/core/breakthrough/schema.test.cjs +256 -0
  120. package/lib/core/breakthrough/scoring.cjs +293 -0
  121. package/lib/core/breakthrough/scoring.test.cjs +423 -0
  122. package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
  123. package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
  124. package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
  125. package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
  126. package/lib/core/first-touch-version-stamper.cjs +113 -0
  127. package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
  128. package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
  129. package/lib/core/llm-name-suggester.cjs +194 -0
  130. package/lib/core/llm-name-suggester.test.cjs +132 -0
  131. package/lib/core/mva-agent-contract.cjs +170 -0
  132. package/lib/core/mva-agent-contract.test.cjs +169 -0
  133. package/lib/core/mva-budget.cjs +75 -0
  134. package/lib/core/mva-budget.test.cjs +68 -0
  135. package/lib/core/mva-classifier.cjs +370 -0
  136. package/lib/core/mva-classifier.test.cjs +248 -0
  137. package/lib/core/mva-deck-builder.cjs +452 -0
  138. package/lib/core/mva-deck-builder.test.cjs +287 -0
  139. package/lib/core/mva-detect.smoke.test.cjs +197 -0
  140. package/lib/core/mva-dispatcher.cjs +110 -0
  141. package/lib/core/mva-dispatcher.test.cjs +216 -0
  142. package/lib/core/mva-option-router.cjs +292 -0
  143. package/lib/core/mva-option-router.test.cjs +483 -0
  144. package/lib/core/mva-orchestrator.cjs +365 -0
  145. package/lib/core/mva-orchestrator.test.cjs +908 -0
  146. package/lib/core/mva-progressive-renderer.cjs +194 -0
  147. package/lib/core/mva-progressive-renderer.test.cjs +157 -0
  148. package/lib/core/mva-rule-linter.cjs +213 -0
  149. package/lib/core/mva-rule-linter.test.cjs +336 -0
  150. package/lib/core/mva-state.cjs +159 -0
  151. package/lib/core/mva-telemetry.cjs +58 -0
  152. package/lib/core/mva-telemetry.test.cjs +196 -0
  153. package/lib/core/mva-vercel-deploy.cjs +168 -0
  154. package/lib/core/mva-vercel-deploy.test.cjs +239 -0
  155. package/lib/core/navigation/dashboard-helpers.cjs +145 -0
  156. package/lib/core/navigation/edges.cjs +35 -0
  157. package/lib/core/navigation/memory-events.cjs +126 -0
  158. package/lib/core/navigation.cjs +11 -0
  159. package/lib/core/resolve-vercel-key.cjs +107 -0
  160. package/lib/core/resolve-vercel-key.test.cjs +137 -0
  161. package/lib/core/room-auto-create.cjs +318 -0
  162. package/lib/core/room-auto-create.test.cjs +198 -0
  163. package/lib/core/room-discard-cascade.cjs +225 -0
  164. package/lib/core/room-discard-cascade.test.cjs +135 -0
  165. package/lib/core/room-name-validator.cjs +132 -0
  166. package/lib/core/room-name-validator.test.cjs +156 -0
  167. package/lib/core/room-naming-selector.cjs +357 -0
  168. package/lib/core/room-naming-selector.test.cjs +277 -0
  169. package/lib/core/room-receipt-emit.cjs +63 -0
  170. package/lib/core/room-skeleton-scaffold.cjs +315 -0
  171. package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
  172. package/lib/core/stale-copy-scanner.cjs +190 -0
  173. package/lib/core/state-aware-router.cjs +78 -0
  174. package/lib/core/telemetry/schema.cjs +168 -0
  175. package/lib/core/telemetry/schema.test.cjs +124 -0
  176. package/lib/core/telemetry/validator.cjs +197 -0
  177. package/lib/core/telemetry/validator.test.cjs +188 -0
  178. package/lib/core/telemetry/writer.cjs +141 -0
  179. package/lib/core/telemetry/writer.test.cjs +331 -0
  180. package/lib/core/terminal-capability.cjs +88 -0
  181. package/lib/core/venture-shape-nudge.cjs +163 -0
  182. package/lib/core/venture-shape-nudge.test.cjs +161 -0
  183. package/lib/core/visual-ops.cjs +70 -2
  184. package/lib/hmi/selector-dispatcher.cjs +90 -1
  185. package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
  186. package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
  187. package/lib/memory/body-shape-coverage.test.cjs +268 -0
  188. package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
  189. package/lib/memory/first-touch-version.test.cjs +198 -0
  190. package/lib/memory/help-coverage.test.cjs +108 -0
  191. package/lib/memory/help-renderer.test.cjs +145 -0
  192. package/lib/memory/palette-consistency.test.cjs +127 -0
  193. package/lib/memory/pending-tension-store.cjs +80 -0
  194. package/lib/memory/render-v2-disposition.test.cjs +199 -0
  195. package/lib/memory/run-feynman-tests.cjs +240 -0
  196. package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
  197. package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
  198. package/lib/memory/soft-alias.test.cjs +144 -0
  199. package/lib/memory/stale-copy-scanner.test.cjs +291 -0
  200. package/lib/memory/state-aware-router.test.cjs +90 -0
  201. package/lib/memory/statusline-two-row.test.cjs +338 -0
  202. package/lib/memory/terminal-capability.test.cjs +155 -0
  203. package/lib/render/ROOM.md +74 -22
  204. package/lib/sessionstart/budget-compressor.cjs +130 -0
  205. package/lib/sessionstart/contributor-interface.cjs +134 -0
  206. package/lib/sessionstart/contributor-isolator.cjs +128 -0
  207. package/lib/sessionstart/precedence-ladder.cjs +47 -0
  208. package/lib/statusline/governing-thought-truncator.cjs +45 -0
  209. package/lib/statusline/two-row-renderer.cjs +186 -0
  210. package/lib/statusline/version-resolver.cjs +81 -0
  211. package/package.json +1 -1
  212. package/references/visual/ROOM.md +55 -0
  213. package/references/visual/palette.json +54 -0
  214. package/skills/larry-personality/SKILL.md +34 -0
  215. package/skills/mva-pipeline/SKILL.md +129 -0
  216. package/skills/ui-system/SKILL.md +109 -1
  217. package/skills/ui-system/rules/dual-palette.md +156 -0
  218. package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
  219. package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
@@ -0,0 +1,150 @@
1
+ 'use strict';
2
+ /*
3
+ * Phase 120-02 Wave 2 Task 1 -- D-13..D-15 resurfacing rules. Per CONTEXT.md verbatim:
4
+ *
5
+ * D-13 (dismissed): 7-day cooldown MINIMUM, AND only if new artifacts have accumulated
6
+ * since the dismiss. BOTH conditions REQUIRED. Time passing alone
7
+ * does NOT license resurfacing (user verbatim 2026-05-16:
8
+ * "that's manipulation").
9
+ *
10
+ * D-14 (confirmed): once-only. Never resurface. Resurfacing what the user already
11
+ * accepted is patronizing.
12
+ *
13
+ * D-15 (filed-as-decision): never resurface as breakthrough. May be referenced as
14
+ * ENABLES in future related breakthroughs via Phase 88
15
+ * decision-log machinery -- the decision becomes load-bearing
16
+ * for future patterns.
17
+ *
18
+ * Canon Part 8: pure LOCAL; no Brain coupling; no cross-room aggregation.
19
+ * ALL reads via navigation.cjs::findRecentChanges chokepoint (Canon Part 9 D-06).
20
+ *
21
+ * Canon Part 9: SQL is the local mind; resurfacing is a read-only function over the
22
+ * event log. The composite isEligibleForSurfacing is a pure predicate -- no writes,
23
+ * no side effects.
24
+ *
25
+ * Em-dash HARD RULE (CLAUDE.md feedback_no_emdashes): use "--" not U+2014.
26
+ *
27
+ * Pure CJS, node built-ins only, zero deps.
28
+ */
29
+
30
+ const navigation = require('../navigation.cjs');
31
+
32
+ // CONTEXT.md D-13 verbatim: 7 days * 24 hours * 3600 sec * 1000 ms = 604800000 ms.
33
+ const SEVEN_DAY_COOLDOWN_MS = 7 * 24 * 3600 * 1000;
34
+
35
+ // 90-day search window for resurfacing lookups (older events stay in log but the
36
+ // resurfacing predicates only look at trailing 90 days for memory locality + perf).
37
+ const RESURFACING_WINDOW_MS = 90 * 24 * 3600 * 1000;
38
+ const RESURFACING_QUERY_LIMIT = 500;
39
+
40
+ // findFirstEventForBreakthrough -- chokepoint-routed lookup for a specific event type
41
+ // against a specific breakthrough_id. Returns the most-recent matching event (since
42
+ // findRecentChanges orders DESC) or null if none. Graceful-degradation on any throw.
43
+ function findFirstEventForBreakthrough(id, eventType, db) {
44
+ try {
45
+ if (!db || typeof id !== 'string' || typeof eventType !== 'string') return null;
46
+ const since = Date.now() - RESURFACING_WINDOW_MS;
47
+ const events = navigation.findRecentChanges(db, since, {
48
+ eventType: eventType,
49
+ limit: RESURFACING_QUERY_LIMIT,
50
+ }) || [];
51
+ return events.find(function (e) {
52
+ return e && e.properties && e.properties.breakthrough_id === id;
53
+ }) || null;
54
+ } catch (_e) {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ // D-14 once-only marker. A breakthrough that has ever been confirmed cannot resurface
60
+ // as a breakthrough -- the user already accepted it; re-surfacing is patronizing.
61
+ function hasBeenConfirmed(id, db) {
62
+ return !!findFirstEventForBreakthrough(id, 'breakthrough_confirmed', db);
63
+ }
64
+
65
+ // D-15 never-resurface marker. A breakthrough that has been filed as a decision is
66
+ // now a first-class decision (Canon Part 4 bridge via Phase 88 decision-log).
67
+ // Re-surfacing it as a breakthrough would conflate two different graph entities.
68
+ function hasBeenFiledAsDecision(id, db) {
69
+ return !!findFirstEventForBreakthrough(id, 'breakthrough_filed_as_decision', db);
70
+ }
71
+
72
+ // D-13 first half. Returns true when EITHER (a) the breakthrough has never been
73
+ // dismissed (vacuously eligible) OR (b) the most-recent dismiss is older than 7 days.
74
+ function dismissalCooldownExpired(id, db) {
75
+ const dismiss = findFirstEventForBreakthrough(id, 'breakthrough_dismissed', db);
76
+ if (!dismiss) return true; // vacuously eligible re: cooldown
77
+ const ts = (typeof dismiss.createdAt === 'number') ? dismiss.createdAt : 0;
78
+ const ageMs = Date.now() - ts;
79
+ return ageMs >= SEVEN_DAY_COOLDOWN_MS;
80
+ }
81
+
82
+ // D-13 second half. The scanner passes the CURRENT candidate's artifact_ids[] length
83
+ // via opts.current_artifact_count; this function compares it against the baseline
84
+ // stored in the breakthrough_dismissed event's properties.artifact_ids_at_dismiss[].
85
+ // Returns true IFF current > baseline. Returns false on missing baseline (defensive).
86
+ //
87
+ // Note: callers should normally use isEligibleForSurfacing (the composite gate). This
88
+ // helper is exported for granular telemetry + per-rule unit tests.
89
+ function newArtifactsAccumulated(id, db, opts) {
90
+ const dismiss = findFirstEventForBreakthrough(id, 'breakthrough_dismissed', db);
91
+ if (!dismiss) return false; // cannot measure delta without baseline
92
+ const baselineIds = (dismiss.properties && Array.isArray(dismiss.properties.artifact_ids_at_dismiss))
93
+ ? dismiss.properties.artifact_ids_at_dismiss : [];
94
+ const baselineCount = baselineIds.length;
95
+ const currentCount = (opts && Number.isInteger(opts.current_artifact_count))
96
+ ? opts.current_artifact_count : 0;
97
+ return currentCount > baselineCount;
98
+ }
99
+
100
+ // Composite resurfacing gate. Encodes D-13 BOTH-condition + D-14 + D-15.
101
+ //
102
+ // Priority order (highest precedence first):
103
+ // 1. D-14 confirmed once-only -> reason: 'confirmed_once_only'
104
+ // 2. D-15 filed-as-decision -> reason: 'filed_as_decision_never_resurfaces'
105
+ // 3. D-13 first half (cooldown active) -> reason: 'dismiss_cooldown_active'
106
+ // 4. D-13 second half (no new artifacts) -> reason: 'dismiss_no_new_artifacts'
107
+ // 5. Eligible -> { eligible: true }
108
+ //
109
+ // The BOTH-condition rule for D-13 is structurally enforced by two SEPARATE checks
110
+ // joined with explicit AND semantics (priority 3 must clear before priority 4 is
111
+ // even checked). This prevents the OR drift that would land if a single helper
112
+ // merged the two conditions. User verbatim: "that's manipulation".
113
+ function isEligibleForSurfacing(id, db, opts) {
114
+ // D-14 first (highest priority): confirmed once-only.
115
+ if (hasBeenConfirmed(id, db)) {
116
+ return { eligible: false, reason: 'confirmed_once_only' };
117
+ }
118
+ // D-15 next: filed-as-decision never resurfaces.
119
+ if (hasBeenFiledAsDecision(id, db)) {
120
+ return { eligible: false, reason: 'filed_as_decision_never_resurfaces' };
121
+ }
122
+ // D-13 first half: cooldown must have expired.
123
+ if (!dismissalCooldownExpired(id, db)) {
124
+ return { eligible: false, reason: 'dismiss_cooldown_active' };
125
+ }
126
+ // D-13 second half: IF the breakthrough was previously dismissed (i.e., a baseline
127
+ // exists), new artifacts MUST have accumulated. The check is gated on existence of
128
+ // the dismiss event -- a never-dismissed breakthrough skips the artifacts check
129
+ // (it's vacuously eligible per priority 5).
130
+ const dismiss = findFirstEventForBreakthrough(id, 'breakthrough_dismissed', db);
131
+ if (dismiss) {
132
+ const baselineCount = (dismiss.properties && Array.isArray(dismiss.properties.artifact_ids_at_dismiss))
133
+ ? dismiss.properties.artifact_ids_at_dismiss.length : 0;
134
+ const currentCount = (opts && Number.isInteger(opts.current_artifact_count))
135
+ ? opts.current_artifact_count : 0;
136
+ if (currentCount <= baselineCount) {
137
+ return { eligible: false, reason: 'dismiss_no_new_artifacts' };
138
+ }
139
+ }
140
+ return { eligible: true };
141
+ }
142
+
143
+ module.exports = {
144
+ hasBeenConfirmed: hasBeenConfirmed,
145
+ hasBeenFiledAsDecision: hasBeenFiledAsDecision,
146
+ dismissalCooldownExpired: dismissalCooldownExpired,
147
+ newArtifactsAccumulated: newArtifactsAccumulated,
148
+ isEligibleForSurfacing: isEligibleForSurfacing,
149
+ SEVEN_DAY_COOLDOWN_MS: SEVEN_DAY_COOLDOWN_MS,
150
+ };
@@ -0,0 +1,233 @@
1
+ 'use strict';
2
+
3
+ /*
4
+ * Phase 120-02 Wave 2 Task 1 -- D-13..D-15 resurfacing rules unit tests.
5
+ *
6
+ * Tests 1-14 cover:
7
+ * - SEVEN_DAY_COOLDOWN_MS verbatim lock (D-13)
8
+ * - hasBeenConfirmed (D-14 once-only marker)
9
+ * - hasBeenFiledAsDecision (D-15 never-resurface marker)
10
+ * - dismissalCooldownExpired (D-13 first half)
11
+ * - newArtifactsAccumulated (D-13 second half; user verbatim:
12
+ * "Time passing alone doesn't license resurfacing -- that's manipulation")
13
+ * - isEligibleForSurfacing composite gate enforcing D-13 BOTH-condition rule
14
+ *
15
+ * Canon Part 8 source-grep + em-dash HARD RULE asserted in Tests 22 + 23
16
+ * (covered jointly with canary.test.cjs counterpart -- see canary.test.cjs).
17
+ */
18
+
19
+ const test = require('node:test');
20
+ const { strict: assert } = require('node:assert');
21
+ const fs = require('node:fs');
22
+ const os = require('node:os');
23
+ const path = require('node:path');
24
+
25
+ const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
26
+ const resurfacing = require(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'resurfacing.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 makeTmpDb(prefix) {
31
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
32
+ const db = openRoomDb(dir);
33
+ return { dir, db };
34
+ }
35
+
36
+ function seedNodeAsArtifact(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
+ // Helper: write a memory_event with a controlled created_at (for cooldown tests).
45
+ function backdateLastEvent(db, eventId, backdatedMs) {
46
+ db.prepare("UPDATE nodes SET created_at = ? WHERE id = ?").run(backdatedMs, eventId);
47
+ }
48
+
49
+ test('120-02 Task 1 Test 1: SEVEN_DAY_COOLDOWN_MS verbatim = 604800000', () => {
50
+ assert.equal(resurfacing.SEVEN_DAY_COOLDOWN_MS, 7 * 24 * 3600 * 1000);
51
+ assert.equal(resurfacing.SEVEN_DAY_COOLDOWN_MS, 604800000);
52
+ });
53
+
54
+ test('120-02 Task 1 Test 2: hasBeenConfirmed returns true given a breakthrough_confirmed event', () => {
55
+ const { dir, db } = makeTmpDb('p120-02-t1-t2-');
56
+ const r = navigation.logMemoryEvent(db, 'breakthrough_confirmed', {
57
+ breakthrough_id: 'bk:1',
58
+ kind: 'convergence',
59
+ source_path: 'system:test',
60
+ created_by: 'system',
61
+ });
62
+ assert.equal(r.ok, true);
63
+ assert.equal(resurfacing.hasBeenConfirmed('bk:1', db), true);
64
+ });
65
+
66
+ test('120-02 Task 1 Test 3: hasBeenConfirmed returns false on empty event log', () => {
67
+ const { dir, db } = makeTmpDb('p120-02-t1-t3-');
68
+ assert.equal(resurfacing.hasBeenConfirmed('bk:never', db), false);
69
+ });
70
+
71
+ test('120-02 Task 1 Test 4: hasBeenFiledAsDecision returns true given a breakthrough_filed_as_decision event', () => {
72
+ const { dir, db } = makeTmpDb('p120-02-t1-t4-');
73
+ const r = navigation.logMemoryEvent(db, 'breakthrough_filed_as_decision', {
74
+ breakthrough_id: 'bk:2',
75
+ kind: 'convergence',
76
+ source_path: 'system:test',
77
+ created_by: 'system',
78
+ });
79
+ assert.equal(r.ok, true);
80
+ assert.equal(resurfacing.hasBeenFiledAsDecision('bk:2', db), true);
81
+ });
82
+
83
+ test('120-02 Task 1 Test 5: dismissalCooldownExpired returns false when dismissed 3 days ago', () => {
84
+ const { dir, db } = makeTmpDb('p120-02-t1-t5-');
85
+ const r = navigation.logMemoryEvent(db, 'breakthrough_dismissed', {
86
+ breakthrough_id: 'bk:3',
87
+ kind: 'convergence',
88
+ artifact_ids_at_dismiss: ['a1', 'a2', 'a3'],
89
+ source_path: 'system:test',
90
+ created_by: 'system',
91
+ });
92
+ assert.equal(r.ok, true);
93
+ // Backdate the event to 3 days ago.
94
+ backdateLastEvent(db, r.eventId, Date.now() - 3 * 24 * 3600 * 1000);
95
+ assert.equal(resurfacing.dismissalCooldownExpired('bk:3', db), false);
96
+ });
97
+
98
+ test('120-02 Task 1 Test 6: dismissalCooldownExpired returns true when dismissed 8 days ago', () => {
99
+ const { dir, db } = makeTmpDb('p120-02-t1-t6-');
100
+ const r = navigation.logMemoryEvent(db, 'breakthrough_dismissed', {
101
+ breakthrough_id: 'bk:4',
102
+ kind: 'convergence',
103
+ artifact_ids_at_dismiss: ['a1'],
104
+ source_path: 'system:test',
105
+ created_by: 'system',
106
+ });
107
+ assert.equal(r.ok, true);
108
+ backdateLastEvent(db, r.eventId, Date.now() - 8 * 24 * 3600 * 1000);
109
+ assert.equal(resurfacing.dismissalCooldownExpired('bk:4', db), true);
110
+ });
111
+
112
+ test('120-02 Task 1 Test 7: dismissalCooldownExpired returns true on never-dismissed breakthrough', () => {
113
+ const { dir, db } = makeTmpDb('p120-02-t1-t7-');
114
+ // Vacuously eligible: cooldown rule applies only IF the breakthrough was dismissed.
115
+ assert.equal(resurfacing.dismissalCooldownExpired('bk:fresh', db), true);
116
+ });
117
+
118
+ test('120-02 Task 1 Test 8: newArtifactsAccumulated returns true when current_artifact_count exceeds baseline', () => {
119
+ const { dir, db } = makeTmpDb('p120-02-t1-t8-');
120
+ const r = navigation.logMemoryEvent(db, 'breakthrough_dismissed', {
121
+ breakthrough_id: 'bk:5',
122
+ kind: 'convergence',
123
+ artifact_ids_at_dismiss: ['a1', 'a2', 'a3'],
124
+ source_path: 'system:test',
125
+ created_by: 'system',
126
+ });
127
+ assert.equal(r.ok, true);
128
+ backdateLastEvent(db, r.eventId, Date.now() - 8 * 24 * 3600 * 1000);
129
+ // Baseline = 3 at dismiss; current count = 4 -> accumulated.
130
+ const elig4 = resurfacing.isEligibleForSurfacing('bk:5', db, { current_artifact_count: 4 });
131
+ assert.equal(elig4.eligible, true);
132
+ // Baseline = 3 at dismiss; current count = 3 -> NOT accumulated.
133
+ const elig3 = resurfacing.isEligibleForSurfacing('bk:5', db, { current_artifact_count: 3 });
134
+ assert.equal(elig3.eligible, false);
135
+ assert.equal(elig3.reason, 'dismiss_no_new_artifacts');
136
+ });
137
+
138
+ test('120-02 Task 1 Test 9: isEligibleForSurfacing returns eligible:true on never-seen breakthrough', () => {
139
+ const { dir, db } = makeTmpDb('p120-02-t1-t9-');
140
+ const elig = resurfacing.isEligibleForSurfacing('bk:fresh', db, { current_artifact_count: 4 });
141
+ assert.equal(elig.eligible, true);
142
+ });
143
+
144
+ test('120-02 Task 1 Test 10: isEligibleForSurfacing D-14 confirmed once-only blocked', () => {
145
+ const { dir, db } = makeTmpDb('p120-02-t1-t10-');
146
+ navigation.logMemoryEvent(db, 'breakthrough_confirmed', {
147
+ breakthrough_id: 'bk:c',
148
+ source_path: 'system:test',
149
+ created_by: 'system',
150
+ });
151
+ const elig = resurfacing.isEligibleForSurfacing('bk:c', db, { current_artifact_count: 99 });
152
+ assert.equal(elig.eligible, false);
153
+ assert.equal(elig.reason, 'confirmed_once_only');
154
+ });
155
+
156
+ test('120-02 Task 1 Test 11: isEligibleForSurfacing D-15 filed-as-decision blocked', () => {
157
+ const { dir, db } = makeTmpDb('p120-02-t1-t11-');
158
+ navigation.logMemoryEvent(db, 'breakthrough_filed_as_decision', {
159
+ breakthrough_id: 'bk:f',
160
+ source_path: 'system:test',
161
+ created_by: 'system',
162
+ });
163
+ const elig = resurfacing.isEligibleForSurfacing('bk:f', db, { current_artifact_count: 99 });
164
+ assert.equal(elig.eligible, false);
165
+ assert.equal(elig.reason, 'filed_as_decision_never_resurfaces');
166
+ });
167
+
168
+ test('120-02 Task 1 Test 12: isEligibleForSurfacing D-13 dismissed in cooldown blocked', () => {
169
+ const { dir, db } = makeTmpDb('p120-02-t1-t12-');
170
+ const r = navigation.logMemoryEvent(db, 'breakthrough_dismissed', {
171
+ breakthrough_id: 'bk:d',
172
+ artifact_ids_at_dismiss: ['a1'],
173
+ source_path: 'system:test',
174
+ created_by: 'system',
175
+ });
176
+ backdateLastEvent(db, r.eventId, Date.now() - 3 * 24 * 3600 * 1000);
177
+ const elig = resurfacing.isEligibleForSurfacing('bk:d', db, { current_artifact_count: 99 });
178
+ assert.equal(elig.eligible, false);
179
+ assert.equal(elig.reason, 'dismiss_cooldown_active');
180
+ });
181
+
182
+ test('120-02 Task 1 Test 13: isEligibleForSurfacing D-13 cooldown expired BUT no new artifacts blocked (BOTH-condition lock)', () => {
183
+ // Per CONTEXT.md D-13 + user verbatim 2026-05-16: "Time passing alone doesn't
184
+ // license resurfacing -- that's manipulation". This test prevents OR drift
185
+ // (the BOTH-condition rule MUST stay AND, not OR).
186
+ const { dir, db } = makeTmpDb('p120-02-t1-t13-');
187
+ const r = navigation.logMemoryEvent(db, 'breakthrough_dismissed', {
188
+ breakthrough_id: 'bk:13',
189
+ artifact_ids_at_dismiss: ['a1', 'a2', 'a3'],
190
+ source_path: 'system:test',
191
+ created_by: 'system',
192
+ });
193
+ backdateLastEvent(db, r.eventId, Date.now() - 8 * 24 * 3600 * 1000);
194
+ // Cooldown expired (8 > 7) but artifact count unchanged (3 == 3 baseline).
195
+ const elig = resurfacing.isEligibleForSurfacing('bk:13', db, { current_artifact_count: 3 });
196
+ assert.equal(elig.eligible, false);
197
+ assert.equal(elig.reason, 'dismiss_no_new_artifacts');
198
+ });
199
+
200
+ test('120-02 Task 1 Test 14: isEligibleForSurfacing D-13 BOTH conditions met -> eligible', () => {
201
+ const { dir, db } = makeTmpDb('p120-02-t1-t14-');
202
+ const r = navigation.logMemoryEvent(db, 'breakthrough_dismissed', {
203
+ breakthrough_id: 'bk:14',
204
+ artifact_ids_at_dismiss: ['a1', 'a2'],
205
+ source_path: 'system:test',
206
+ created_by: 'system',
207
+ });
208
+ backdateLastEvent(db, r.eventId, Date.now() - 8 * 24 * 3600 * 1000);
209
+ // Cooldown expired AND new artifacts accumulated (5 > 2).
210
+ const elig = resurfacing.isEligibleForSurfacing('bk:14', db, { current_artifact_count: 5 });
211
+ assert.equal(elig.eligible, true);
212
+ });
213
+
214
+ test('120-02 Task 1 Test 22: Canon Part 8 source-grep -- zero Brain coupling in resurfacing.cjs', () => {
215
+ const src = fs.readFileSync(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'resurfacing.cjs'), 'utf8');
216
+ // Real API call patterns only -- not documentation prose (cf. 120-00 Deviation 2).
217
+ assert.equal(/require\s*\(\s*['"][^'"]*brain-client[^'"]*['"]\s*\)/.test(src), false,
218
+ 'resurfacing.cjs must not require brain-client');
219
+ assert.equal(/fetch\s*\(\s*['"][^'"]*brain\.mindrian/.test(src), false,
220
+ 'resurfacing.cjs must not fetch brain.mindrian.*');
221
+ });
222
+
223
+ test('120-02 Task 1 Test 23: em-dash HARD RULE -- zero U+2014 in resurfacing.cjs', () => {
224
+ const src = fs.readFileSync(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'resurfacing.cjs'), 'utf8');
225
+ // The U+2014 byte sequence in UTF-8 is 0xE2 0x80 0x94. Use codepoint check
226
+ // instead of including the literal character in the test source (which would
227
+ // make this test self-trip).
228
+ let count = 0;
229
+ for (const ch of src) {
230
+ if (ch.charCodeAt(0) === 0x2014) count++;
231
+ }
232
+ assert.equal(count, 0, 'resurfacing.cjs must contain zero U+2014 em-dash characters');
233
+ });
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+ // Phase 120-03 Wave 2 Task 2 -- SOFT_BAND breakthrough review queue.
3
+ //
4
+ // Per CONTEXT.md "Claude's Discretion" item 3: a separate
5
+ // `.rooms/breakthrough-review-queue.db` SQLite database at the rooms-home level.
6
+ // Mirrors the Phase 119-01 rooms-meta.db precedent (lib/core/room-discard-cascade.cjs::
7
+ // _emitPartialFailure pattern). Sibling pattern; does NOT pollute per-room room.db
8
+ // with cross-room review backlog.
9
+ //
10
+ // Per CONTEXT.md D-18 SOFT_BAND semantics:
11
+ // Confidence 0.35 <= conf <= 0.50 candidates queue here (NOT surfaced).
12
+ // Sample 20% manually each week to check for drift. These become retraining data.
13
+ //
14
+ // Canon Part 4: every choice is graph data. The review queue is the typed-row
15
+ // surface for SOFT_BAND breakthrough candidates; rows carry kind + confidence +
16
+ // theme + provenance ids (artifact_ids_json) for review.
17
+ // Canon Part 8: queue stays LOCAL to the user's machine; no Brain coupling.
18
+ // Canon Part 9: schema is queryable via standard SQLite; the sample-20% weekly
19
+ // audit is a manual SQL query against this db.
20
+ //
21
+ // Em-dash HARD RULE (CLAUDE.md feedback_no_emdashes): zero U+2014 in source.
22
+
23
+ const fs = require('node:fs');
24
+ const path = require('node:path');
25
+ const crypto = require('node:crypto');
26
+
27
+ // Use Node's built-in node:sqlite DatabaseSync (Phase 109 SQL navigation spine
28
+ // contract, mirrored from lib/core/room-db.cjs). Fall back to a no-op surface
29
+ // only on require failure (env without sqlite, e.g. older Node versions; node:sqlite
30
+ // is GA from Node 22.5+).
31
+ let DatabaseSync;
32
+ try {
33
+ ({ DatabaseSync } = require('node:sqlite'));
34
+ } catch (_e) {
35
+ DatabaseSync = null;
36
+ }
37
+
38
+ // The 10-column DDL for the review_candidates table. Matches the test contract
39
+ // in review-queue.test.cjs T11 (PRAGMA table_info returns these 10 column names).
40
+ const TABLE_DDL = "CREATE TABLE IF NOT EXISTS review_candidates ( " +
41
+ "id TEXT PRIMARY KEY, " +
42
+ "room_slug TEXT, " +
43
+ "breakthrough_id TEXT NOT NULL, " +
44
+ "kind TEXT NOT NULL, " +
45
+ "confidence REAL NOT NULL, " +
46
+ "theme TEXT, " +
47
+ "artifact_ids_json TEXT, " +
48
+ "queued_at INTEGER NOT NULL, " +
49
+ "reviewed_at INTEGER, " +
50
+ "review_status TEXT NOT NULL DEFAULT 'pending'" +
51
+ ")";
52
+
53
+ const REVIEW_QUEUE_DB_PATH = '.rooms/breakthrough-review-queue.db';
54
+
55
+ // openReviewQueue(roomsHome) -> {db, dbPath?, fallback, reason?}
56
+ //
57
+ // Opens (creates if needed) the .rooms/breakthrough-review-queue.db at the given
58
+ // rooms-home. Returns a handle with the db object, the resolved dbPath, and a
59
+ // fallback flag. On EACCES / mkdir failure, falls back to an in-memory db so the
60
+ // scanner can still insert (just not persistent across sessions).
61
+ //
62
+ // Graceful failure modes:
63
+ // - node:sqlite not available -> {db: null, fallback: true, reason: 'node_sqlite_not_available'}
64
+ // - mkdir fails -> in-memory db opened, fallback:true, reason: 'rooms_home_not_writable:<msg>'
65
+ // - in-memory open ALSO fails -> {db: null, fallback: true, reason: 'open_failed:<msg>'}
66
+ function openReviewQueue(roomsHome) {
67
+ if (!DatabaseSync) {
68
+ return { db: null, fallback: true, reason: 'node_sqlite_not_available' };
69
+ }
70
+ try {
71
+ const metaDir = path.join(roomsHome, '.rooms');
72
+ fs.mkdirSync(metaDir, { recursive: true, mode: 0o755 });
73
+ const dbPath = path.join(metaDir, 'breakthrough-review-queue.db');
74
+ const db = new DatabaseSync(dbPath);
75
+ db.exec(TABLE_DDL);
76
+ return { db: db, dbPath: dbPath, fallback: false };
77
+ } catch (err) {
78
+ // Graceful fallback: in-memory db so the scanner can still insert; not persistent.
79
+ try {
80
+ const db = new DatabaseSync(':memory:');
81
+ db.exec(TABLE_DDL);
82
+ const errMsg = (err && err.message) ? err.message.slice(0, 80) : 'unknown';
83
+ return { db: db, fallback: true, reason: 'rooms_home_not_writable:' + errMsg };
84
+ } catch (err2) {
85
+ const errMsg2 = (err2 && err2.message) ? err2.message.slice(0, 80) : 'unknown';
86
+ return { db: null, fallback: true, reason: 'open_failed:' + errMsg2 };
87
+ }
88
+ }
89
+ }
90
+
91
+ // insertReviewCandidate(db, breakthrough, roomSlug) -> {ok, queue_id, queued_at} | {ok:false, reason}
92
+ //
93
+ // Inserts a SOFT_BAND candidate into the review queue. Generates a fresh queue_id
94
+ // of the form "review:<8 hex bytes>" so callers do not need to coordinate IDs.
95
+ // queued_at is Date.now() at insert time.
96
+ //
97
+ // review_status defaults to 'pending' per the DDL DEFAULT clause. reviewed_at
98
+ // stays NULL until a manual reviewer updates it (manual SQL or future
99
+ // /mos:doctor --review-queue-sweep command, deferred to v1.14.0).
100
+ //
101
+ // Theme is sliced to 200 chars to match the Phase 90-06 sanitizeDetailScalar
102
+ // precedent (Canon Part 8 boundary -- bounded payload size).
103
+ function insertReviewCandidate(db, breakthrough, roomSlug) {
104
+ if (!db || typeof db.prepare !== 'function') {
105
+ return { ok: false, reason: 'no_db' };
106
+ }
107
+ const bk = breakthrough || {};
108
+ const queueId = 'review:' + crypto.randomBytes(8).toString('hex');
109
+ const queued_at = Date.now();
110
+ try {
111
+ db.prepare(
112
+ "INSERT INTO review_candidates (id, room_slug, breakthrough_id, kind, confidence, theme, artifact_ids_json, queued_at, review_status) " +
113
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending')"
114
+ ).run(
115
+ queueId,
116
+ (typeof roomSlug === 'string' ? roomSlug : null),
117
+ bk.id || null,
118
+ (typeof bk.kind === 'string' ? bk.kind : 'unknown'),
119
+ (typeof bk.confidence === 'number' && Number.isFinite(bk.confidence) ? bk.confidence : 0),
120
+ (typeof bk.theme === 'string' ? bk.theme.slice(0, 200) : null),
121
+ JSON.stringify(Array.isArray(bk.artifact_ids) ? bk.artifact_ids : []),
122
+ queued_at
123
+ );
124
+ return { ok: true, queue_id: queueId, queued_at: queued_at };
125
+ } catch (err) {
126
+ return { ok: false, reason: (err && err.message) ? err.message : 'insert_failed' };
127
+ }
128
+ }
129
+
130
+ // listPendingReviews(db, limit) -> [{id, room_slug, breakthrough_id, kind, confidence, theme, queued_at}, ...]
131
+ //
132
+ // Returns rows where review_status = 'pending', ordered by queued_at DESC. Limit
133
+ // defaults to 100; non-positive / non-integer limits use the default.
134
+ function listPendingReviews(db, limit) {
135
+ if (!db || typeof db.prepare !== 'function') return [];
136
+ const lim = (Number.isInteger(limit) && limit > 0) ? limit : 100;
137
+ try {
138
+ return db.prepare(
139
+ "SELECT id, room_slug, breakthrough_id, kind, confidence, theme, queued_at " +
140
+ "FROM review_candidates WHERE review_status = 'pending' " +
141
+ "ORDER BY queued_at DESC LIMIT ?"
142
+ ).all(lim);
143
+ } catch (_e) {
144
+ return [];
145
+ }
146
+ }
147
+
148
+ module.exports = {
149
+ openReviewQueue,
150
+ insertReviewCandidate,
151
+ listPendingReviews,
152
+ REVIEW_QUEUE_DB_PATH,
153
+ TABLE_DDL,
154
+ };