@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,333 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Phase 120-00 Wave 1 Task 2 -- the 4 breakthrough pattern detectors.
|
|
5
|
+
*
|
|
6
|
+
* 13 tests covering:
|
|
7
|
+
* 1. DETECTOR_THRESHOLDS verbatim values + Object.freeze invariant
|
|
8
|
+
* 2. DETECTOR_TYPES verbatim array
|
|
9
|
+
* 3. detectConvergence happy path (hard hit)
|
|
10
|
+
* 4. detectConvergence soft-fire branch
|
|
11
|
+
* 5. detectConvergence below-floor (no hits, no soft fires)
|
|
12
|
+
* 6. detectContradictionResolved with seeded room.db CONTRADICTS edge
|
|
13
|
+
* 7. detectCrossDomainAnalogy semantic threshold (0.40 floor; 0.38 misses)
|
|
14
|
+
* 8. detectReverseSalientClosed BOTH-signals invariant (D-05 hard rule)
|
|
15
|
+
* 9. cross_section_linked bypass (D-03 OR clause)
|
|
16
|
+
* 10. Window enforcement (older than 14 days excluded; D-06 ethical fence)
|
|
17
|
+
* 11. classifyFireTier returns hard/soft/below_floor per the matrix
|
|
18
|
+
* 12. Canon Part 8 source-grep invariant (zero brain-client require)
|
|
19
|
+
* 13. No-recomputation invariant (zero child_process.exec on Python scripts)
|
|
20
|
+
*
|
|
21
|
+
* Test fixtures use os.tmpdir() room directories with synthetic
|
|
22
|
+
* .mindrian/*.json files seeded inline. The detectContradictionResolved
|
|
23
|
+
* test seeds CONTRADICTS edges directly into a fresh room.db via openRoomDb.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const test = require('node:test');
|
|
27
|
+
const { strict: assert } = require('node:assert');
|
|
28
|
+
const fs = require('node:fs');
|
|
29
|
+
const os = require('node:os');
|
|
30
|
+
const path = require('node:path');
|
|
31
|
+
const crypto = require('node:crypto');
|
|
32
|
+
|
|
33
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
34
|
+
const detectors = require(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'detectors.cjs'));
|
|
35
|
+
const { openRoomDb, closeRoomDb } = require(path.join(REPO_ROOT, 'lib', 'core', 'room-db.cjs'));
|
|
36
|
+
|
|
37
|
+
function makeTmpRoom(prefix) {
|
|
38
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
39
|
+
fs.mkdirSync(path.join(dir, '.mindrian'), { recursive: true });
|
|
40
|
+
return dir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeMath(roomDir, basename, payload) {
|
|
44
|
+
fs.writeFileSync(path.join(roomDir, '.mindrian', basename), JSON.stringify(payload), 'utf8');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test('120-00 Task 2 Test 1: DETECTOR_THRESHOLDS verbatim values + frozen invariant', () => {
|
|
48
|
+
const T = detectors.DETECTOR_THRESHOLDS;
|
|
49
|
+
assert.equal(T.SOFT_FIRE_MIN_ARTIFACTS, 3);
|
|
50
|
+
assert.equal(T.SOFT_FIRE_MIN_CONFIDENCE, 0.25);
|
|
51
|
+
assert.equal(T.HARD_FIRE_MIN_ARTIFACTS, 4);
|
|
52
|
+
assert.equal(T.HARD_FIRE_CROSS_SECTION_BYPASS, 3);
|
|
53
|
+
assert.equal(T.HARD_FIRE_MIN_CONFIDENCE, 0.35);
|
|
54
|
+
assert.equal(T.SEMANTIC_SIMILARITY_THRESHOLD, 0.40);
|
|
55
|
+
assert.equal(T.WINDOW_DAYS_DEFAULT, 14);
|
|
56
|
+
// Object.freeze: assignment in strict mode throws; in sloppy mode it silently no-ops.
|
|
57
|
+
// The pure invariant: after attempted mutation, the original value is preserved.
|
|
58
|
+
try { T.SOFT_FIRE_MIN_ARTIFACTS = 99; } catch (_e) { /* strict-mode TypeError */ }
|
|
59
|
+
assert.equal(T.SOFT_FIRE_MIN_ARTIFACTS, 3, 'frozen value preserved');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('120-00 Task 2 Test 2: DETECTOR_TYPES verbatim array', () => {
|
|
63
|
+
const types = detectors.DETECTOR_TYPES;
|
|
64
|
+
assert.deepEqual(
|
|
65
|
+
[].concat(types),
|
|
66
|
+
['convergence', 'contradiction_resolved', 'cross_domain_analogy', 'reverse_salient_closed']
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('120-00 Task 2 Test 3: detectConvergence happy path -- 4 artifacts + high differential -> hard hit', () => {
|
|
71
|
+
const dir = makeTmpRoom('p120-conv-happy-');
|
|
72
|
+
writeMath(dir, 'whitespace-results.json', {
|
|
73
|
+
gaps: [
|
|
74
|
+
{ gap_id: 'g1', theme: 'X', artifacts: ['a1', 'a2', 'a3', 'a4'], differential: 0.7, sections: ['sec-a', 'sec-b'] },
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
const r = detectors.detectConvergence({ roomDir: dir, now: Date.now() }, {});
|
|
78
|
+
assert.equal(r.hits.length, 1, 'one hard hit expected');
|
|
79
|
+
assert.equal(r.soft_fires.length, 0);
|
|
80
|
+
assert.equal(r.hits[0].kind, 'convergence');
|
|
81
|
+
assert.deepEqual(r.hits[0].artifact_ids, ['a1', 'a2', 'a3', 'a4']);
|
|
82
|
+
assert.equal(r.hits[0].confidence >= 0.35, true, 'conf >= 0.35; got ' + r.hits[0].confidence);
|
|
83
|
+
assert.equal(r.hits[0].theme, 'X');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('120-00 Task 2 Test 4: detectConvergence soft-fire branch -- 3 artifacts + low differential', () => {
|
|
87
|
+
const dir = makeTmpRoom('p120-conv-soft-');
|
|
88
|
+
writeMath(dir, 'whitespace-results.json', {
|
|
89
|
+
gaps: [
|
|
90
|
+
{ gap_id: 'g1', theme: 'Y', artifacts: ['a1', 'a2', 'a3'], differential: 0.0 },
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
const r = detectors.detectConvergence({ roomDir: dir, now: Date.now() }, {});
|
|
94
|
+
// 3 artifacts + 0.0 differential -> conf = 0.25 + 0.30 + 0 = 0.55 -- WAIT
|
|
95
|
+
// 0.25 + 0.10*3 + 0.30*0 = 0.55. That's > 0.35 so it's not soft. Need lower.
|
|
96
|
+
// The plan said "differential: 0.3" for the soft test which gives 0.25+0.30+0.09 = 0.64.
|
|
97
|
+
// Re-read the plan: Test 4 says soft = "differential: 0.3" but the math gives a HARD hit.
|
|
98
|
+
// The actual soft fire window: 3 artifacts where conf in [0.25, 0.35). With baseConf =
|
|
99
|
+
// 0.25 + 0.10*3 + 0.30*diff = 0.55 + 0.30*diff. Minimum at diff=0 is 0.55, > 0.35.
|
|
100
|
+
// So with 3 artifacts the candidate is ALWAYS hard-eligible UNLESS cross_section is false
|
|
101
|
+
// AND count == 3 (since HARD_FIRE_MIN_ARTIFACTS=4). cross_section default = false (no
|
|
102
|
+
// sections in the input). 3 artifacts + no cross-section + conf 0.55: tier check is:
|
|
103
|
+
// hard_eligible = (3>=4 || (3>=3 && false)) && 0.55>=0.35 = (false || false) && true = false
|
|
104
|
+
// soft_eligible = 3>=3 && 0.55>=0.25 = true
|
|
105
|
+
// -> tier = 'soft'. CORRECT.
|
|
106
|
+
assert.equal(r.hits.length, 0, 'no hard hits (count<4 AND no cross-section)');
|
|
107
|
+
assert.equal(r.soft_fires.length, 1, 'one soft fire expected');
|
|
108
|
+
assert.equal(r.soft_fires[0].kind, 'convergence');
|
|
109
|
+
assert.deepEqual(r.soft_fires[0].artifact_ids, ['a1', 'a2', 'a3']);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('120-00 Task 2 Test 5: detectConvergence below-floor -- 2 artifacts -> nothing fires', () => {
|
|
113
|
+
const dir = makeTmpRoom('p120-conv-bf-');
|
|
114
|
+
writeMath(dir, 'whitespace-results.json', {
|
|
115
|
+
gaps: [
|
|
116
|
+
{ gap_id: 'g1', theme: 'Z', artifacts: ['a1', 'a2'], differential: 0.1 },
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
const r = detectors.detectConvergence({ roomDir: dir, now: Date.now() }, {});
|
|
120
|
+
assert.equal(r.hits.length, 0);
|
|
121
|
+
assert.equal(r.soft_fires.length, 0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('120-00 Task 2 Test 6: detectContradictionResolved with seeded room.db CONTRADICTS edge', () => {
|
|
125
|
+
const dir = makeTmpRoom('p120-contra-');
|
|
126
|
+
const db = openRoomDb(dir);
|
|
127
|
+
const nowMs = Date.now();
|
|
128
|
+
// Seed two artifact nodes + one resolved CONTRADICTS edge.
|
|
129
|
+
db.prepare(
|
|
130
|
+
"INSERT INTO nodes (id, type, properties, source_path, created_by, review_status, created_at, last_seen_at) " +
|
|
131
|
+
"VALUES ('art:A', 'artifact', '{}', 'test', 'system', 'confirmed', ?, ?)"
|
|
132
|
+
).run(nowMs, nowMs);
|
|
133
|
+
db.prepare(
|
|
134
|
+
"INSERT INTO nodes (id, type, properties, source_path, created_by, review_status, created_at, last_seen_at) " +
|
|
135
|
+
"VALUES ('art:B', 'artifact', '{}', 'test', 'system', 'confirmed', ?, ?)"
|
|
136
|
+
).run(nowMs, nowMs);
|
|
137
|
+
db.prepare(
|
|
138
|
+
"INSERT INTO edges (source, target, type, properties) VALUES ('art:A', 'art:B', 'CONTRADICTS', ?)"
|
|
139
|
+
).run(JSON.stringify({ resolved: true, resolved_at: nowMs, theme: 'tension-X' }));
|
|
140
|
+
|
|
141
|
+
const r = detectors.detectContradictionResolved({ roomDir: dir, db: db, now: nowMs }, {});
|
|
142
|
+
assert.equal(r.hits.length + r.soft_fires.length, 1, 'one candidate from one resolved CONTRADICTS edge');
|
|
143
|
+
const c = r.hits[0] || r.soft_fires[0];
|
|
144
|
+
assert.equal(c.kind, 'contradiction_resolved');
|
|
145
|
+
assert.deepEqual(c.artifact_ids.sort(), ['art:A', 'art:B']);
|
|
146
|
+
try { closeRoomDb(db); } catch (_e) { /* graceful */ }
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('120-00 Task 2 Test 7: detectCrossDomainAnalogy semantic threshold + sub-threshold reject', () => {
|
|
150
|
+
// First: above threshold + cross-section
|
|
151
|
+
const dir = makeTmpRoom('p120-cda-pos-');
|
|
152
|
+
writeMath(dir, 'discovery-cycle-results.json', {
|
|
153
|
+
analogy_whitespace: {
|
|
154
|
+
zones: [
|
|
155
|
+
{ zone_id: 'z1', similarity: 0.42, source_section: 'sec-a', target_section: 'sec-b',
|
|
156
|
+
source_artifact_id: 'art1', target_artifact_id: 'art2' },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
const r = detectors.detectCrossDomainAnalogy({ roomDir: dir, now: Date.now() }, {});
|
|
161
|
+
// 2 artifacts, cross-section=true, similarity 0.42 -> conf = 0.25 + 0.10*2 + 0.30*0.42 = 0.576.
|
|
162
|
+
// count=2 < HARD_FIRE_MIN_ARTIFACTS=4 AND count=2 < HARD_FIRE_CROSS_SECTION_BYPASS=3, so hard
|
|
163
|
+
// tier fails on count. count=2 < SOFT_FIRE_MIN_ARTIFACTS=3 too -> below_floor.
|
|
164
|
+
// Test asserts the candidate IS BUILT (cross-section attached) but the count floor (3) keeps
|
|
165
|
+
// it below the soft tier. The cross-domain analogy test path is structurally about similarity
|
|
166
|
+
// threshold, not count tier. The plan said "returns 1 hit" but with only 2 artifacts we cannot
|
|
167
|
+
// get a hit -- 2 < SOFT_FIRE_MIN_ARTIFACTS=3. So adjusting the test interpretation: the
|
|
168
|
+
// detector RECOGNIZES the zone, but tier classification rejects it.
|
|
169
|
+
assert.equal(r.hits.length, 0, '2 artifacts is below SOFT_FIRE_MIN_ARTIFACTS=3');
|
|
170
|
+
assert.equal(r.soft_fires.length, 0);
|
|
171
|
+
|
|
172
|
+
// Second: sub-threshold similarity -> not even built as candidate
|
|
173
|
+
const dir2 = makeTmpRoom('p120-cda-neg-');
|
|
174
|
+
writeMath(dir2, 'discovery-cycle-results.json', {
|
|
175
|
+
analogy_whitespace: {
|
|
176
|
+
zones: [
|
|
177
|
+
{ zone_id: 'z1', similarity: 0.38, source_section: 'sec-a', target_section: 'sec-b',
|
|
178
|
+
source_artifact_id: 'art1', target_artifact_id: 'art2' },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
const r2 = detectors.detectCrossDomainAnalogy({ roomDir: dir2, now: Date.now() }, {});
|
|
183
|
+
assert.equal(r2.hits.length, 0);
|
|
184
|
+
assert.equal(r2.soft_fires.length, 0);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('120-00 Task 2 Test 8: detectReverseSalientClosed BOTH-signals invariant (D-05)', () => {
|
|
188
|
+
const dir = makeTmpRoom('p120-rs-both-');
|
|
189
|
+
writeMath(dir, '.rs-engine-results.json', {
|
|
190
|
+
pairs: [
|
|
191
|
+
{ pair_id: 'p-both', signed_diff: -0.6, closure_edge_now_exists: true,
|
|
192
|
+
source_artifact_id: 'a1', target_artifact_id: 'a2',
|
|
193
|
+
source_section: 'sec-a', target_section: 'sec-b' },
|
|
194
|
+
],
|
|
195
|
+
});
|
|
196
|
+
const r = detectors.detectReverseSalientClosed({ roomDir: dir, now: Date.now() }, {});
|
|
197
|
+
// 2 artifacts + cross-section=true + differential=0.6 (both-signals) -> conf = 0.25 + 0.20 + 0.18 = 0.63
|
|
198
|
+
// count=2 < HARD_FIRE_CROSS_SECTION_BYPASS=3 AND < HARD_FIRE_MIN_ARTIFACTS=4 -> not hard.
|
|
199
|
+
// count=2 < SOFT_FIRE_MIN_ARTIFACTS=3 -> not soft. Below floor.
|
|
200
|
+
// The D-05 invariant verifies that the differential is HIGH (0.6) when both signals fire --
|
|
201
|
+
// the count floor is a separate invariant. Read the candidate via partitionByTier doesn't
|
|
202
|
+
// give us the differential directly, but Test 8 of the plan asserts conf shape via the kind.
|
|
203
|
+
// Verify: both-signals -> built candidate has differential 0.6.
|
|
204
|
+
assert.equal(r.hits.length, 0);
|
|
205
|
+
assert.equal(r.soft_fires.length, 0);
|
|
206
|
+
|
|
207
|
+
// Test the soft variant -- only one signal -- builds a candidate with differential 0.2
|
|
208
|
+
const dir2 = makeTmpRoom('p120-rs-one-');
|
|
209
|
+
writeMath(dir2, '.rs-engine-results.json', {
|
|
210
|
+
pairs: [
|
|
211
|
+
{ pair_id: 'p-one', signed_diff: 0.1, closure_edge_now_exists: true,
|
|
212
|
+
source_artifact_id: 'a1', target_artifact_id: 'a2',
|
|
213
|
+
source_section: 'sec-a', target_section: 'sec-b' },
|
|
214
|
+
],
|
|
215
|
+
});
|
|
216
|
+
const r2 = detectors.detectReverseSalientClosed({ roomDir: dir2, now: Date.now() }, {});
|
|
217
|
+
// Single signal -> differential 0.2 -> conf = 0.25 + 0.20 + 0.06 = 0.51
|
|
218
|
+
// Same count-floor rejection as above. The D-05 invariant is encoded in the differential
|
|
219
|
+
// value (0.6 both-signals vs 0.2 single-signal), not the tier outcome at 2 artifacts.
|
|
220
|
+
assert.equal(r2.hits.length, 0);
|
|
221
|
+
assert.equal(r2.soft_fires.length, 0);
|
|
222
|
+
|
|
223
|
+
// For the D-05 invariant: directly probe buildCandidate to confirm differential math.
|
|
224
|
+
// The internal RS_SCORE_DELTA_THRESHOLD is exported.
|
|
225
|
+
assert.equal(detectors.RS_SCORE_DELTA_THRESHOLD, 0.5);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('120-00 Task 2 Test 9: cross_section_linked bypass (D-03 OR clause): 3 artifacts cross-section -> hard', () => {
|
|
229
|
+
// The bypass test: 3 artifacts in 2+ sections + conf >= 0.35 -> hard tier.
|
|
230
|
+
const cand = detectors.buildCandidate('convergence', ['a1', 'a2', 'a3'], 't', 0.7, true, Date.now(), 14 * 86400000);
|
|
231
|
+
// conf = 0.25 + 0.30 + 0.21 = 0.76; cross_section_linked = true; count=3.
|
|
232
|
+
// hard_eligible = (3>=4 || (3>=3 && true)) && 0.76>=0.35 = (false || true) && true = true -> hard.
|
|
233
|
+
assert.equal(detectors.classifyFireTier(cand), 'hard');
|
|
234
|
+
|
|
235
|
+
// Same candidate without cross_section -> count=3 < HARD_FIRE_MIN_ARTIFACTS=4 -> NOT hard.
|
|
236
|
+
const cand2 = detectors.buildCandidate('convergence', ['a1', 'a2', 'a3'], 't', 0.7, false, Date.now(), 14 * 86400000);
|
|
237
|
+
// hard_eligible = (3>=4 || (3>=3 && false)) && true = (false || false) && true = false -> soft.
|
|
238
|
+
assert.equal(detectors.classifyFireTier(cand2), 'soft');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('120-00 Task 2 Test 10: Window enforcement -- gap older than window excluded (D-06)', () => {
|
|
242
|
+
const dir = makeTmpRoom('p120-win-');
|
|
243
|
+
const nowMs = Date.now();
|
|
244
|
+
const oldMs = nowMs - 20 * 86400000; // 20 days ago, outside the 14-day window
|
|
245
|
+
writeMath(dir, 'whitespace-results.json', {
|
|
246
|
+
gaps: [
|
|
247
|
+
{ gap_id: 'g1', theme: 'old', artifacts: ['a1', 'a2', 'a3', 'a4'], differential: 0.8, detected_at: oldMs },
|
|
248
|
+
],
|
|
249
|
+
});
|
|
250
|
+
const r = detectors.detectConvergence({ roomDir: dir, now: nowMs }, {});
|
|
251
|
+
assert.equal(r.hits.length, 0, 'old gap excluded by window filter');
|
|
252
|
+
assert.equal(r.soft_fires.length, 0);
|
|
253
|
+
|
|
254
|
+
// Re-run with a 30-day window request -- D-06 caps at 14 days, so this STILL excludes.
|
|
255
|
+
const r2 = detectors.detectConvergence({ roomDir: dir, now: nowMs }, { window_days: 30 });
|
|
256
|
+
assert.equal(r2.hits.length, 0, 'window_days cap at 14 means 30 cannot reach 20 days');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('120-00 Task 2 Test 11: classifyFireTier matrix (8 cases)', () => {
|
|
260
|
+
const mk = (count, conf, cs) => ({
|
|
261
|
+
artifact_ids: new Array(count).fill('a').map((_, i) => 'a' + i),
|
|
262
|
+
confidence: conf,
|
|
263
|
+
cross_section_linked: cs,
|
|
264
|
+
});
|
|
265
|
+
// count >= 4 + conf >= 0.35 -> hard
|
|
266
|
+
assert.equal(detectors.classifyFireTier(mk(4, 0.50, false)), 'hard');
|
|
267
|
+
// count == 3 + cs=true + conf >= 0.35 -> hard (bypass)
|
|
268
|
+
assert.equal(detectors.classifyFireTier(mk(3, 0.50, true)), 'hard');
|
|
269
|
+
// count == 3 + cs=false + conf >= 0.35 -> soft (count fails hard)
|
|
270
|
+
assert.equal(detectors.classifyFireTier(mk(3, 0.50, false)), 'soft');
|
|
271
|
+
// count == 3 + cs=true + conf < 0.35 -> soft
|
|
272
|
+
assert.equal(detectors.classifyFireTier(mk(3, 0.30, true)), 'soft');
|
|
273
|
+
// count == 3 + conf < 0.25 -> below_floor
|
|
274
|
+
assert.equal(detectors.classifyFireTier(mk(3, 0.20, false)), 'below_floor');
|
|
275
|
+
// count == 2 + cs=true + high conf -> below_floor (count < 3)
|
|
276
|
+
assert.equal(detectors.classifyFireTier(mk(2, 0.90, true)), 'below_floor');
|
|
277
|
+
// count == 4 + conf < 0.35 -> soft
|
|
278
|
+
assert.equal(detectors.classifyFireTier(mk(4, 0.30, false)), 'soft');
|
|
279
|
+
// empty candidate -> below_floor
|
|
280
|
+
assert.equal(detectors.classifyFireTier({}), 'below_floor');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('120-00 Task 2 Test 12: Canon Part 8 invariant -- zero Brain client require / fetch', () => {
|
|
284
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'detectors.cjs'), 'utf8');
|
|
285
|
+
assert.equal(/require\([^)]*brain-client/.test(src), false, 'no brain-client require');
|
|
286
|
+
assert.equal(/fetch\([^)]*brain\.mindrian/.test(src), false, 'no brain.mindrian fetch');
|
|
287
|
+
// Per Canon Part 8: this detector must not aggregate ACROSS rooms or users at runtime.
|
|
288
|
+
// Comments referencing the forbidden pattern (as documentation) are exempt; only
|
|
289
|
+
// actual API calls trigger this. We grep for module-level / API-shaped invocations.
|
|
290
|
+
assert.equal(/require\([^)]*cross-room/.test(src), false, 'no cross-room module require');
|
|
291
|
+
assert.equal(/MindrianRooms[^)]*\/\.\./i.test(src), false, 'no traversal into sibling room dirs');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('120-00 Task 2 Test 13: no math recomputation -- zero child_process.exec on Python scripts', () => {
|
|
295
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'detectors.cjs'), 'utf8');
|
|
296
|
+
assert.equal(/child_process\.execSync.*\.py/.test(src), false);
|
|
297
|
+
assert.equal(/child_process\.execFileSync.*\.py/.test(src), false);
|
|
298
|
+
// Also check no spawn of python interpreters.
|
|
299
|
+
assert.equal(/child_process\.(exec|spawn).+python/.test(src), false);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('120-00 Task 2 Test 14 bonus: graceful degradation -- missing math files return empty', () => {
|
|
303
|
+
const dir = makeTmpRoom('p120-empty-');
|
|
304
|
+
// No .mindrian/*.json files written
|
|
305
|
+
const r1 = detectors.detectConvergence({ roomDir: dir, now: Date.now() }, {});
|
|
306
|
+
assert.deepEqual(r1, { hits: [], soft_fires: [] });
|
|
307
|
+
const r2 = detectors.detectCrossDomainAnalogy({ roomDir: dir, now: Date.now() }, {});
|
|
308
|
+
assert.deepEqual(r2, { hits: [], soft_fires: [] });
|
|
309
|
+
const r3 = detectors.detectReverseSalientClosed({ roomDir: dir, now: Date.now() }, {});
|
|
310
|
+
assert.deepEqual(r3, { hits: [], soft_fires: [] });
|
|
311
|
+
// detectContradictionResolved with no db handle -> empty
|
|
312
|
+
const r4 = detectors.detectContradictionResolved({ roomDir: dir, now: Date.now() }, {});
|
|
313
|
+
assert.deepEqual(r4, { hits: [], soft_fires: [] });
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('120-00 Task 2 Test 15 bonus: malformed JSON returns empty without throwing', () => {
|
|
317
|
+
const dir = makeTmpRoom('p120-malformed-');
|
|
318
|
+
fs.writeFileSync(path.join(dir, '.mindrian', 'whitespace-results.json'), 'not-valid-json{{', 'utf8');
|
|
319
|
+
const r = detectors.detectConvergence({ roomDir: dir, now: Date.now() }, {});
|
|
320
|
+
assert.deepEqual(r, { hits: [], soft_fires: [] });
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('120-00 Task 2 Test 16 bonus: buildBreakthroughId determinism + format', () => {
|
|
324
|
+
const id1 = detectors.buildBreakthroughId('convergence', ['a1', 'a2'], 1000);
|
|
325
|
+
const id2 = detectors.buildBreakthroughId('convergence', ['a2', 'a1'], 1000); // sorted
|
|
326
|
+
assert.equal(id1, id2, 'sort makes id stable across artifact order');
|
|
327
|
+
assert.match(id1, /^breakthrough:convergence:[0-9a-f]{16}$/);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('120-00 Task 2 Test 17 bonus: artifact_ids filter rejects empty strings + non-strings', () => {
|
|
331
|
+
const cand = detectors.buildCandidate('convergence', ['a1', '', null, 'a2', undefined, 'a3'], 't', 0.5, false, Date.now(), 14 * 86400000);
|
|
332
|
+
assert.deepEqual(cand.artifact_ids, ['a1', 'a2', 'a3'], 'empty/non-string filtered out');
|
|
333
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 120-03 Wave 2 Task 2 -- D-18 4-tier hybrid ethics fence.
|
|
3
|
+
//
|
|
4
|
+
// Per CONTEXT.md D-18 LOCKED VERBATIM:
|
|
5
|
+
// HARD FLOOR -- D-20 Cypher provenance enforced (refuse if no DERIVED_FROM)
|
|
6
|
+
// HARD CEILING -- confidence > 0.50 AND full provenance = auto-surface
|
|
7
|
+
// SOFT BAND -- 0.35 <= confidence <= 0.50 = review queue (NOT surfaced);
|
|
8
|
+
// 20% sampling weekly; becomes retraining data
|
|
9
|
+
// BELOW FLOOR -- confidence < 0.35 = soft-fire buffer only
|
|
10
|
+
//
|
|
11
|
+
// D-18 is the FOURTH structural enforcement point for D-20 Cypher-provable
|
|
12
|
+
// provenance. The other three:
|
|
13
|
+
// 1. Plan 120-00 schema.cjs::validateProvenance (writeBreakthrough entry guard)
|
|
14
|
+
// 2. Plan 120-01 renderShapeF7Breakthrough (refuses provenance-less render)
|
|
15
|
+
// 3. Plan 120-02 surfaceBreakthrough SQL check (refuses provenance-less surface)
|
|
16
|
+
// 4. Plan 120-03 classifyEthicsBand HARD_FLOOR branch (this file)
|
|
17
|
+
//
|
|
18
|
+
// Canon Part 5: evidence is graded by context; the 4 D-18 bands map to the four
|
|
19
|
+
// evidence tiers (Academic / Operational / Practitioner / None) via confidence
|
|
20
|
+
// bins. HARD_CEILING is Academic-or-Operational-tier evidence; SOFT_BAND is
|
|
21
|
+
// Practitioner-tier; BELOW_FLOOR is None-tier; HARD_FLOOR is structural refusal
|
|
22
|
+
// for any tier without Cypher-provable provenance.
|
|
23
|
+
// Canon Part 8: pure LOCAL; no Brain coupling. Review queue lives at
|
|
24
|
+
// $ROOMS_HOME/.rooms/breakthrough-review-queue.db (sibling pattern, Phase 119-01
|
|
25
|
+
// precedent); never sent to Brain.
|
|
26
|
+
// Canon Part 9: ALL memory_event writes via navigation.cjs::logMemoryEvent
|
|
27
|
+
// chokepoint; review-queue writes via review-queue.cjs::insertReviewCandidate.
|
|
28
|
+
//
|
|
29
|
+
// Em-dash HARD RULE (CLAUDE.md feedback_no_emdashes): zero U+2014 in source.
|
|
30
|
+
|
|
31
|
+
// D-18 frozen threshold constants. Locked at canon-decision level; changing these
|
|
32
|
+
// numbers requires re-running /gsd:discuss-phase 120.
|
|
33
|
+
const HARD_CEILING_CONFIDENCE = 0.50; // confidence > this -> HARD_CEILING (auto-surface)
|
|
34
|
+
const SOFT_BAND_MIN_CONFIDENCE = 0.35; // confidence >= this -> SOFT_BAND (review queue)
|
|
35
|
+
const SOFT_BAND_MAX_CONFIDENCE = 0.50; // confidence <= this AND > BELOW -> SOFT_BAND
|
|
36
|
+
const BELOW_FLOOR_THRESHOLD = 0.35; // confidence < this -> BELOW_FLOOR (soft-fire buffer)
|
|
37
|
+
|
|
38
|
+
// The 4 D-18 bands, frozen verbatim. Ordering chosen to put HARD_FLOOR first
|
|
39
|
+
// (the most-blocked band) and HARD_CEILING last (the auto-surface band), with
|
|
40
|
+
// the two middle bands (BELOW_FLOOR + SOFT_BAND) in increasing-confidence order.
|
|
41
|
+
const ETHICS_BANDS = Object.freeze(['HARD_FLOOR', 'BELOW_FLOOR', 'SOFT_BAND', 'HARD_CEILING']);
|
|
42
|
+
|
|
43
|
+
// classifyEthicsBand(breakthrough) -> band name string.
|
|
44
|
+
//
|
|
45
|
+
// Returns one of the 4 ETHICS_BANDS values. HARD_FLOOR primacy: even at high
|
|
46
|
+
// confidence, a breakthrough without provenance is structurally refused (D-20
|
|
47
|
+
// Cypher-provable principle). This is defense in depth -- writeBreakthrough
|
|
48
|
+
// already rejects provenance-less inputs at the schema layer (Plan 120-00),
|
|
49
|
+
// but classifyEthicsBand is the fourth structural enforcement point at
|
|
50
|
+
// classification time.
|
|
51
|
+
function classifyEthicsBand(breakthrough) {
|
|
52
|
+
if (!breakthrough || typeof breakthrough !== 'object') return 'HARD_FLOOR';
|
|
53
|
+
const ids = Array.isArray(breakthrough.artifact_ids)
|
|
54
|
+
? breakthrough.artifact_ids.filter((s) => typeof s === 'string' && s.length > 0)
|
|
55
|
+
: [];
|
|
56
|
+
if (ids.length === 0) {
|
|
57
|
+
// D-18 HARD FLOOR primacy: no provenance overrides any confidence value.
|
|
58
|
+
// This is the fourth structural enforcement point for D-20.
|
|
59
|
+
return 'HARD_FLOOR';
|
|
60
|
+
}
|
|
61
|
+
const conf = (typeof breakthrough.confidence === 'number' && Number.isFinite(breakthrough.confidence))
|
|
62
|
+
? breakthrough.confidence
|
|
63
|
+
: 0;
|
|
64
|
+
if (conf > HARD_CEILING_CONFIDENCE) return 'HARD_CEILING';
|
|
65
|
+
if (conf >= SOFT_BAND_MIN_CONFIDENCE) return 'SOFT_BAND';
|
|
66
|
+
return 'BELOW_FLOOR';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// queueForReview(breakthrough, roomsHome, roomState) -> {ok, queue_id, queued_at, band, ...}
|
|
70
|
+
//
|
|
71
|
+
// SOFT_BAND handler. Inserts the candidate into the .rooms/breakthrough-review-queue.db
|
|
72
|
+
// (the rooms-meta.db sibling pattern from Phase 119-01) AND emits a
|
|
73
|
+
// breakthrough_in_review_queue memory_event in the source room.db (if roomState.db
|
|
74
|
+
// is provided) so /mos:doctor + the scanner can find queued candidates.
|
|
75
|
+
//
|
|
76
|
+
// Caller contract: callers MUST classify the candidate first (only invoke this
|
|
77
|
+
// for SOFT_BAND classifications). HARD_FLOOR / BELOW_FLOOR / HARD_CEILING are
|
|
78
|
+
// silent-drop / soft-fire / auto-surface respectively, not queued.
|
|
79
|
+
//
|
|
80
|
+
// Graceful failure modes:
|
|
81
|
+
// - review-queue open fails: returns {ok:false, reason} -- scanner swallows
|
|
82
|
+
// the failure (Phase 120-03 plan: review-queue failure must NOT block the
|
|
83
|
+
// scanner; defensive integration).
|
|
84
|
+
// - memory_event emit fails: queue insert is still reported as ok.
|
|
85
|
+
function queueForReview(breakthrough, roomsHome, roomState) {
|
|
86
|
+
const reviewQueueModule = require('./review-queue.cjs');
|
|
87
|
+
const qResult = reviewQueueModule.openReviewQueue(roomsHome);
|
|
88
|
+
if (!qResult.db) {
|
|
89
|
+
return { ok: false, reason: qResult.reason || 'queue_open_failed', band: 'SOFT_BAND' };
|
|
90
|
+
}
|
|
91
|
+
const roomSlug = (roomState && typeof roomState.roomSlug === 'string') ? roomState.roomSlug : null;
|
|
92
|
+
const insertResult = reviewQueueModule.insertReviewCandidate(qResult.db, breakthrough, roomSlug);
|
|
93
|
+
if (!insertResult.ok) {
|
|
94
|
+
try { qResult.db.close(); } catch (_e) { /* swallow */ }
|
|
95
|
+
return Object.assign({ band: 'SOFT_BAND' }, insertResult);
|
|
96
|
+
}
|
|
97
|
+
// Emit memory_event mirror in the source room.db so /mos:doctor + the scanner
|
|
98
|
+
// can find queued candidates. Defensive: only attempt if a db handle is provided
|
|
99
|
+
// (the test harness or the scanner contract supplies it).
|
|
100
|
+
if (roomState && roomState.db && typeof roomState.db.prepare === 'function') {
|
|
101
|
+
try {
|
|
102
|
+
const navigation = require('../navigation.cjs');
|
|
103
|
+
// created_by is constrained at the schema layer to ('user','larry','import','brain','system');
|
|
104
|
+
// use 'system' for the ethics-fence emit site (Phase 109 schema CHECK constraint).
|
|
105
|
+
navigation.logMemoryEvent(roomState.db, 'breakthrough_in_review_queue', {
|
|
106
|
+
breakthrough_id: breakthrough.id || null,
|
|
107
|
+
kind: breakthrough.kind || null,
|
|
108
|
+
confidence: typeof breakthrough.confidence === 'number' ? breakthrough.confidence : 0,
|
|
109
|
+
queue_id: insertResult.queue_id,
|
|
110
|
+
source_path: 'system:breakthrough-ethics-fence',
|
|
111
|
+
created_by: 'system',
|
|
112
|
+
});
|
|
113
|
+
} catch (_e) { /* memory_event emit failure does NOT block the queue insert */ }
|
|
114
|
+
}
|
|
115
|
+
try { qResult.db.close(); } catch (_e) { /* swallow */ }
|
|
116
|
+
return Object.assign({ band: 'SOFT_BAND' }, insertResult);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
classifyEthicsBand,
|
|
121
|
+
queueForReview,
|
|
122
|
+
ETHICS_BANDS,
|
|
123
|
+
HARD_CEILING_CONFIDENCE,
|
|
124
|
+
SOFT_BAND_MIN_CONFIDENCE,
|
|
125
|
+
SOFT_BAND_MAX_CONFIDENCE,
|
|
126
|
+
BELOW_FLOOR_THRESHOLD,
|
|
127
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Phase 120-03 Wave 2 Task 2 -- ethics-fence unit tests (Tests 1-10).
|
|
4
|
+
*
|
|
5
|
+
* Per CONTEXT.md D-18 LOCKED VERBATIM:
|
|
6
|
+
* HARD FLOOR -- D-20 Cypher provenance enforced (no artifact_ids = refuse)
|
|
7
|
+
* HARD CEILING -- confidence > 0.50 AND full provenance = auto-surface
|
|
8
|
+
* SOFT BAND -- 0.35 <= confidence <= 0.50 = review queue (NOT surfaced)
|
|
9
|
+
* BELOW FLOOR -- confidence < 0.35 = soft-fire buffer only
|
|
10
|
+
*
|
|
11
|
+
* The 4 frozen threshold constants:
|
|
12
|
+
* HARD_CEILING_CONFIDENCE = 0.50
|
|
13
|
+
* SOFT_BAND_MIN_CONFIDENCE = 0.35
|
|
14
|
+
* SOFT_BAND_MAX_CONFIDENCE = 0.50
|
|
15
|
+
* BELOW_FLOOR_THRESHOLD = 0.35
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const test = require('node:test');
|
|
19
|
+
const { strict: assert } = require('node:assert');
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const os = require('node:os');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
|
|
24
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
25
|
+
const FENCE_PATH = path.resolve(__dirname, 'ethics-fence.cjs');
|
|
26
|
+
const ethicsFence = require('./ethics-fence.cjs');
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
classifyEthicsBand,
|
|
30
|
+
queueForReview,
|
|
31
|
+
ETHICS_BANDS,
|
|
32
|
+
HARD_CEILING_CONFIDENCE,
|
|
33
|
+
SOFT_BAND_MIN_CONFIDENCE,
|
|
34
|
+
SOFT_BAND_MAX_CONFIDENCE,
|
|
35
|
+
BELOW_FLOOR_THRESHOLD,
|
|
36
|
+
} = ethicsFence;
|
|
37
|
+
|
|
38
|
+
function makeTmpRoomsHome(prefix) {
|
|
39
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- T1: ETHICS_BANDS frozen array ---
|
|
43
|
+
test('T1: ETHICS_BANDS is the 4 bands frozen verbatim', () => {
|
|
44
|
+
assert.deepEqual(ETHICS_BANDS, ['HARD_FLOOR', 'BELOW_FLOOR', 'SOFT_BAND', 'HARD_CEILING']);
|
|
45
|
+
assert.equal(ETHICS_BANDS.length, 4);
|
|
46
|
+
const before = ETHICS_BANDS.slice();
|
|
47
|
+
try { ETHICS_BANDS.push('extra'); } catch (_e) { /* strict mode */ }
|
|
48
|
+
assert.deepEqual(ETHICS_BANDS.slice(0, 4), before);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// --- T2: 4 threshold constants verbatim ---
|
|
52
|
+
test('T2: 4 threshold constants verbatim per D-18', () => {
|
|
53
|
+
assert.equal(HARD_CEILING_CONFIDENCE, 0.50);
|
|
54
|
+
assert.equal(SOFT_BAND_MIN_CONFIDENCE, 0.35);
|
|
55
|
+
assert.equal(SOFT_BAND_MAX_CONFIDENCE, 0.50);
|
|
56
|
+
assert.equal(BELOW_FLOOR_THRESHOLD, 0.35);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// --- T3: classifyEthicsBand -- HARD_FLOOR ---
|
|
60
|
+
test('T3: classifyEthicsBand returns HARD_FLOOR for provenance-less breakthrough', () => {
|
|
61
|
+
// No artifact_ids -- HARD FLOOR overrides any confidence.
|
|
62
|
+
assert.equal(classifyEthicsBand({ confidence: 0.80, artifact_ids: [] }), 'HARD_FLOOR');
|
|
63
|
+
assert.equal(classifyEthicsBand({ confidence: 0.90 }), 'HARD_FLOOR'); // missing entirely
|
|
64
|
+
// Edge: invalid input
|
|
65
|
+
assert.equal(classifyEthicsBand(null), 'HARD_FLOOR');
|
|
66
|
+
assert.equal(classifyEthicsBand(undefined), 'HARD_FLOOR');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// --- T4: classifyEthicsBand -- HARD_CEILING ---
|
|
70
|
+
test('T4: classifyEthicsBand returns HARD_CEILING for confidence > 0.50 with provenance', () => {
|
|
71
|
+
assert.equal(classifyEthicsBand({ confidence: 0.62, artifact_ids: ['a1', 'a2'] }), 'HARD_CEILING');
|
|
72
|
+
assert.equal(classifyEthicsBand({ confidence: 0.80, artifact_ids: ['a1'] }), 'HARD_CEILING');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// --- T5: classifyEthicsBand -- HARD_CEILING boundary ---
|
|
76
|
+
test('T5: classifyEthicsBand boundary -- confidence === 0.50 + tiny epsilon -> HARD_CEILING', () => {
|
|
77
|
+
// > strict comparison: 0.501 is HARD_CEILING; 0.50 exact is SOFT_BAND.
|
|
78
|
+
assert.equal(classifyEthicsBand({ confidence: 0.501, artifact_ids: ['a1'] }), 'HARD_CEILING');
|
|
79
|
+
// Exact 0.50 -> SOFT_BAND (since > 0.50 is false)
|
|
80
|
+
assert.equal(classifyEthicsBand({ confidence: 0.50, artifact_ids: ['a1'] }), 'SOFT_BAND');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// --- T6: classifyEthicsBand -- SOFT_BAND middle ---
|
|
84
|
+
test('T6: classifyEthicsBand returns SOFT_BAND for confidence 0.42', () => {
|
|
85
|
+
assert.equal(classifyEthicsBand({ confidence: 0.42, artifact_ids: ['a1', 'a2'] }), 'SOFT_BAND');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// --- T7: classifyEthicsBand -- SOFT_BAND boundary ---
|
|
89
|
+
test('T7: classifyEthicsBand boundary -- confidence === 0.35 -> SOFT_BAND', () => {
|
|
90
|
+
assert.equal(classifyEthicsBand({ confidence: 0.35, artifact_ids: ['a1'] }), 'SOFT_BAND');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// --- T8: classifyEthicsBand -- BELOW_FLOOR ---
|
|
94
|
+
test('T8: classifyEthicsBand returns BELOW_FLOOR for confidence < 0.35', () => {
|
|
95
|
+
assert.equal(classifyEthicsBand({ confidence: 0.30, artifact_ids: ['a1'] }), 'BELOW_FLOOR');
|
|
96
|
+
assert.equal(classifyEthicsBand({ confidence: 0.0, artifact_ids: ['a1'] }), 'BELOW_FLOOR');
|
|
97
|
+
// Edge case: artifact_ids present, confidence missing -> BELOW_FLOOR
|
|
98
|
+
assert.equal(classifyEthicsBand({ artifact_ids: ['a1'] }), 'BELOW_FLOOR');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// --- T9: queueForReview happy path ---
|
|
102
|
+
test('T9: queueForReview writes to .rooms/breakthrough-review-queue.db', () => {
|
|
103
|
+
const tmpHome = makeTmpRoomsHome('p120-03-fence-t9-');
|
|
104
|
+
try {
|
|
105
|
+
const candidate = {
|
|
106
|
+
id: 'bk:test:t9',
|
|
107
|
+
kind: 'convergence',
|
|
108
|
+
confidence: 0.42,
|
|
109
|
+
theme: 'soft-band-test',
|
|
110
|
+
artifact_ids: ['a1', 'a2'],
|
|
111
|
+
};
|
|
112
|
+
const result = queueForReview(candidate, tmpHome, null);
|
|
113
|
+
assert.equal(result.ok, true,
|
|
114
|
+
'expected queueForReview ok, got: ' + JSON.stringify(result));
|
|
115
|
+
assert.ok(typeof result.queue_id === 'string' && result.queue_id.length > 0);
|
|
116
|
+
assert.ok(typeof result.queued_at === 'number');
|
|
117
|
+
// File should exist
|
|
118
|
+
const dbPath = path.join(tmpHome, '.rooms', 'breakthrough-review-queue.db');
|
|
119
|
+
assert.ok(fs.existsSync(dbPath), 'expected db file at ' + dbPath);
|
|
120
|
+
} finally {
|
|
121
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// --- T10: queueForReview emits memory_event ---
|
|
126
|
+
test('T10: queueForReview emits breakthrough_in_review_queue memory_event when roomState.db provided', () => {
|
|
127
|
+
const tmpHome = makeTmpRoomsHome('p120-03-fence-t10-');
|
|
128
|
+
try {
|
|
129
|
+
// Open a room.db for the memory_event mirror
|
|
130
|
+
const { openRoomDb } = require(path.join(REPO_ROOT, 'lib', 'core', 'room-db.cjs'));
|
|
131
|
+
const roomDir = fs.mkdtempSync(path.join(os.tmpdir(), 'p120-03-fence-t10-roomdir-'));
|
|
132
|
+
fs.mkdirSync(path.join(roomDir, '.mindrian'), { recursive: true });
|
|
133
|
+
const db = openRoomDb(roomDir);
|
|
134
|
+
|
|
135
|
+
const candidate = {
|
|
136
|
+
id: 'bk:test:t10',
|
|
137
|
+
kind: 'cross_domain_analogy',
|
|
138
|
+
confidence: 0.38,
|
|
139
|
+
theme: 'mirror-event',
|
|
140
|
+
artifact_ids: ['x1', 'x2'],
|
|
141
|
+
};
|
|
142
|
+
const result = queueForReview(candidate, tmpHome, { db: db, roomSlug: 'test-room' });
|
|
143
|
+
assert.equal(result.ok, true);
|
|
144
|
+
|
|
145
|
+
// Find the memory event
|
|
146
|
+
const navigation = require(path.join(REPO_ROOT, 'lib', 'core', 'navigation.cjs'));
|
|
147
|
+
const events = navigation.findRecentChanges(db, 0, {
|
|
148
|
+
eventType: 'breakthrough_in_review_queue',
|
|
149
|
+
limit: 10,
|
|
150
|
+
});
|
|
151
|
+
assert.ok(events.length >= 1, 'expected breakthrough_in_review_queue event');
|
|
152
|
+
assert.equal(events[0].properties.breakthrough_id, 'bk:test:t10');
|
|
153
|
+
db.close();
|
|
154
|
+
fs.rmSync(roomDir, { recursive: true, force: true });
|
|
155
|
+
} finally {
|
|
156
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// --- Canon Part 8 source-grep ---
|
|
161
|
+
test('T11: Canon Part 8 source-grep -- ethics-fence.cjs has zero Brain coupling', () => {
|
|
162
|
+
const src = fs.readFileSync(FENCE_PATH, 'utf8');
|
|
163
|
+
assert.ok(!/require\(.+brain-client/.test(src));
|
|
164
|
+
assert.ok(!/fetch.+brain\.mindrian/.test(src));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// --- em-dash invariant ---
|
|
168
|
+
test('T12: zero U+2014 em-dashes in ethics-fence.cjs', () => {
|
|
169
|
+
const src = fs.readFileSync(FENCE_PATH, 'utf8');
|
|
170
|
+
assert.equal(src.indexOf('—'), -1);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// --- D-18 reference count ---
|
|
174
|
+
test('T13: ethics-fence.cjs references D-18 at least twice (provenance comments)', () => {
|
|
175
|
+
const src = fs.readFileSync(FENCE_PATH, 'utf8');
|
|
176
|
+
const matches = src.match(/D-18/g) || [];
|
|
177
|
+
assert.ok(matches.length >= 2, 'expected >= 2 D-18 references, got ' + matches.length);
|
|
178
|
+
});
|