@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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +36 -0
- package/commands/act.md +1 -0
- package/commands/admin.md +1 -0
- package/commands/analyze-needs.md +2 -0
- package/commands/analyze-systems.md +2 -0
- package/commands/analyze-timing.md +2 -0
- package/commands/auto-explore.md +2 -0
- package/commands/beautiful-question.md +2 -0
- package/commands/brain-derive.md +2 -0
- package/commands/build-knowledge.md +2 -0
- package/commands/build-thesis.md +2 -0
- package/commands/causal.md +2 -0
- package/commands/challenge-assumptions.md +2 -0
- package/commands/compare-ventures.md +2 -0
- package/commands/dashboard.md +2 -1
- package/commands/deep-grade.md +2 -0
- package/commands/diagnose.md +21 -1
- package/commands/diagnostics.md +14 -3
- package/commands/doctor.md +4 -1
- package/commands/dogfood-flush.md +92 -0
- package/commands/dominant-designs.md +2 -0
- package/commands/explain-decision.md +2 -0
- package/commands/explore-domains.md +2 -0
- package/commands/explore-futures.md +2 -0
- package/commands/explore-trends.md +2 -0
- package/commands/export.md +1 -0
- package/commands/feynman-timeline-refresh.md +2 -0
- package/commands/file-meeting.md +4 -0
- package/commands/find-analogies.md +1 -0
- package/commands/find-bottlenecks.md +2 -0
- package/commands/find-connections.md +2 -0
- package/commands/funding.md +1 -0
- package/commands/grade.md +4 -0
- package/commands/graph.md +1 -0
- package/commands/hat-briefing.md +1 -0
- package/commands/heal.md +22 -170
- package/commands/help.md +54 -334
- package/commands/hmi-status.md +23 -144
- package/commands/jtbd.md +1 -0
- package/commands/leadership.md +2 -0
- package/commands/lean-canvas.md +2 -0
- package/commands/macro-trends.md +2 -0
- package/commands/map-unknowns.md +2 -0
- package/commands/memory.md +1 -0
- package/commands/models.md +1 -0
- package/commands/mos-reason.md +2 -0
- package/commands/mos.md +139 -0
- package/commands/mullins.md +2 -0
- package/commands/mva-brief.md +58 -0
- package/commands/mva-option.md +91 -0
- package/commands/new-project.md +4 -0
- package/commands/onboard.md +22 -7
- package/commands/operator.md +1 -0
- package/commands/opportunities.md +1 -0
- package/commands/organize.md +22 -469
- package/commands/persona.md +1 -0
- package/commands/pipeline.md +2 -0
- package/commands/present.md +1 -0
- package/commands/publish.md +2 -0
- package/commands/query.md +24 -102
- package/commands/radar.md +2 -0
- package/commands/reanalyze.md +1 -0
- package/commands/research.md +2 -0
- package/commands/room.md +2 -0
- package/commands/rooms.md +1 -0
- package/commands/root-cause.md +2 -0
- package/commands/rs-experts.md +1 -0
- package/commands/rs-explain.md +1 -0
- package/commands/rs-fetch.md +1 -0
- package/commands/rs-thesis.md +1 -0
- package/commands/scenario-plan.md +2 -0
- package/commands/scheduled-tasks.md +1 -0
- package/commands/score-innovation.md +2 -0
- package/commands/scout.md +1 -0
- package/commands/setup.md +2 -0
- package/commands/snapshot.md +2 -0
- package/commands/speakers.md +1 -0
- package/commands/splash.md +5 -2
- package/commands/status.md +1 -0
- package/commands/structure-argument.md +2 -0
- package/commands/suggest-next.md +2 -0
- package/commands/systems-thinking.md +2 -0
- package/commands/think-hats.md +2 -0
- package/commands/update.md +2 -0
- package/commands/user-needs.md +2 -0
- package/commands/validate.md +2 -0
- package/commands/value-proposition.md +2 -0
- package/commands/vault.md +2 -0
- package/commands/visualize.md +24 -29
- package/commands/whitespace.md +2 -1
- package/commands/wiki.md +1 -0
- package/hooks/hooks.json +31 -88
- package/lib/agents/auto-explore-agent.cjs +82 -0
- package/lib/agents/mva/brain-classic-traps.cjs +77 -0
- package/lib/agents/mva/brain-cross-domain.cjs +79 -0
- package/lib/agents/mva/brain-similar-ventures.cjs +93 -0
- package/lib/agents/mva/dashboard-graph-neighborhood.cjs +72 -0
- package/lib/agents/mva/index.cjs +42 -0
- package/lib/agents/mva/six-hats-red-black.cjs +137 -0
- package/lib/agents/mva/tavily-funding-scan.cjs +147 -0
- package/lib/agents/mva/test-all-six-agents.cjs +467 -0
- package/lib/conversation/operator.cjs +64 -0
- package/lib/conversation/operator.test.cjs +160 -0
- package/lib/core/breakthrough/canary.cjs +134 -0
- package/lib/core/breakthrough/canary.test.cjs +136 -0
- package/lib/core/breakthrough/detectors.cjs +359 -0
- package/lib/core/breakthrough/detectors.test.cjs +333 -0
- package/lib/core/breakthrough/ethics-fence.cjs +127 -0
- package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
- package/lib/core/breakthrough/resurfacing.cjs +150 -0
- package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
- package/lib/core/breakthrough/review-queue.cjs +154 -0
- package/lib/core/breakthrough/review-queue.test.cjs +160 -0
- package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
- package/lib/core/breakthrough/scanner.cjs +426 -0
- package/lib/core/breakthrough/scanner.test.cjs +267 -0
- package/lib/core/breakthrough/schema.cjs +164 -0
- package/lib/core/breakthrough/schema.test.cjs +256 -0
- package/lib/core/breakthrough/scoring.cjs +293 -0
- package/lib/core/breakthrough/scoring.test.cjs +423 -0
- package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
- package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
- package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
- package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
- package/lib/core/first-touch-version-stamper.cjs +113 -0
- package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
- package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
- package/lib/core/llm-name-suggester.cjs +194 -0
- package/lib/core/llm-name-suggester.test.cjs +132 -0
- package/lib/core/mva-agent-contract.cjs +170 -0
- package/lib/core/mva-agent-contract.test.cjs +169 -0
- package/lib/core/mva-budget.cjs +75 -0
- package/lib/core/mva-budget.test.cjs +68 -0
- package/lib/core/mva-classifier.cjs +370 -0
- package/lib/core/mva-classifier.test.cjs +248 -0
- package/lib/core/mva-deck-builder.cjs +452 -0
- package/lib/core/mva-deck-builder.test.cjs +287 -0
- package/lib/core/mva-detect.smoke.test.cjs +197 -0
- package/lib/core/mva-dispatcher.cjs +110 -0
- package/lib/core/mva-dispatcher.test.cjs +216 -0
- package/lib/core/mva-option-router.cjs +292 -0
- package/lib/core/mva-option-router.test.cjs +483 -0
- package/lib/core/mva-orchestrator.cjs +365 -0
- package/lib/core/mva-orchestrator.test.cjs +908 -0
- package/lib/core/mva-progressive-renderer.cjs +194 -0
- package/lib/core/mva-progressive-renderer.test.cjs +157 -0
- package/lib/core/mva-rule-linter.cjs +213 -0
- package/lib/core/mva-rule-linter.test.cjs +336 -0
- package/lib/core/mva-state.cjs +159 -0
- package/lib/core/mva-telemetry.cjs +58 -0
- package/lib/core/mva-telemetry.test.cjs +196 -0
- package/lib/core/mva-vercel-deploy.cjs +168 -0
- package/lib/core/mva-vercel-deploy.test.cjs +239 -0
- package/lib/core/navigation/dashboard-helpers.cjs +145 -0
- package/lib/core/navigation/edges.cjs +35 -0
- package/lib/core/navigation/memory-events.cjs +126 -0
- package/lib/core/navigation.cjs +11 -0
- package/lib/core/resolve-vercel-key.cjs +107 -0
- package/lib/core/resolve-vercel-key.test.cjs +137 -0
- package/lib/core/room-auto-create.cjs +318 -0
- package/lib/core/room-auto-create.test.cjs +198 -0
- package/lib/core/room-discard-cascade.cjs +225 -0
- package/lib/core/room-discard-cascade.test.cjs +135 -0
- package/lib/core/room-name-validator.cjs +132 -0
- package/lib/core/room-name-validator.test.cjs +156 -0
- package/lib/core/room-naming-selector.cjs +357 -0
- package/lib/core/room-naming-selector.test.cjs +277 -0
- package/lib/core/room-receipt-emit.cjs +63 -0
- package/lib/core/room-skeleton-scaffold.cjs +315 -0
- package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
- package/lib/core/stale-copy-scanner.cjs +190 -0
- package/lib/core/state-aware-router.cjs +78 -0
- package/lib/core/telemetry/schema.cjs +168 -0
- package/lib/core/telemetry/schema.test.cjs +124 -0
- package/lib/core/telemetry/validator.cjs +197 -0
- package/lib/core/telemetry/validator.test.cjs +188 -0
- package/lib/core/telemetry/writer.cjs +141 -0
- package/lib/core/telemetry/writer.test.cjs +331 -0
- package/lib/core/terminal-capability.cjs +88 -0
- package/lib/core/venture-shape-nudge.cjs +163 -0
- package/lib/core/venture-shape-nudge.test.cjs +161 -0
- package/lib/core/visual-ops.cjs +70 -2
- package/lib/hmi/selector-dispatcher.cjs +90 -1
- package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
- package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
- package/lib/memory/body-shape-coverage.test.cjs +268 -0
- package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
- package/lib/memory/first-touch-version.test.cjs +198 -0
- package/lib/memory/help-coverage.test.cjs +108 -0
- package/lib/memory/help-renderer.test.cjs +145 -0
- package/lib/memory/palette-consistency.test.cjs +127 -0
- package/lib/memory/pending-tension-store.cjs +80 -0
- package/lib/memory/render-v2-disposition.test.cjs +199 -0
- package/lib/memory/run-feynman-tests.cjs +240 -0
- package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
- package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
- package/lib/memory/soft-alias.test.cjs +144 -0
- package/lib/memory/stale-copy-scanner.test.cjs +291 -0
- package/lib/memory/state-aware-router.test.cjs +90 -0
- package/lib/memory/statusline-two-row.test.cjs +338 -0
- package/lib/memory/terminal-capability.test.cjs +155 -0
- package/lib/render/ROOM.md +74 -22
- package/lib/sessionstart/budget-compressor.cjs +130 -0
- package/lib/sessionstart/contributor-interface.cjs +134 -0
- package/lib/sessionstart/contributor-isolator.cjs +128 -0
- package/lib/sessionstart/precedence-ladder.cjs +47 -0
- package/lib/statusline/governing-thought-truncator.cjs +45 -0
- package/lib/statusline/two-row-renderer.cjs +186 -0
- package/lib/statusline/version-resolver.cjs +81 -0
- package/package.json +1 -1
- package/references/visual/ROOM.md +55 -0
- package/references/visual/palette.json +54 -0
- package/skills/larry-personality/SKILL.md +34 -0
- package/skills/mva-pipeline/SKILL.md +129 -0
- package/skills/ui-system/SKILL.md +109 -1
- package/skills/ui-system/rules/dual-palette.md +156 -0
- package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
- 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
|
+
};
|