@mindrian_os/install 1.13.0-beta.17 → 1.13.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +6 -1
- package/CHANGELOG.md +31 -0
- package/README.md +51 -56
- package/bin/mindrian-brain-mcp-client.cjs +152 -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 +6 -2
- 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 +2 -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 +2 -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 +2 -0
- package/commands/mva-option.md +2 -0
- package/commands/new-project.md +2 -0
- package/commands/onboard.md +20 -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 +22 -88
- package/lib/agents/auto-explore-agent.cjs +82 -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/directive-envelope.cjs +175 -0
- package/lib/core/directive-envelope.test.cjs +225 -0
- package/lib/core/doctor/class-m-brain-smoke.cjs +278 -0
- package/lib/core/doctor/class-m-brain-smoke.test.cjs +310 -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/mcp-profiles.cjs +1 -1
- package/lib/core/migration-snapshot.cjs +172 -0
- package/lib/core/migration-snapshot.test.cjs +174 -0
- package/lib/core/mindrian-brain-shim.test.cjs +214 -0
- package/lib/core/mva-orchestrator.cjs +41 -0
- package/lib/core/mva-telemetry.cjs +31 -143
- package/lib/core/navigation/edges.cjs +35 -0
- package/lib/core/navigation/memory-events.cjs +126 -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/rs-nl-to-query.cjs +1 -1
- 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 +200 -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/tier0-messaging.cjs +109 -0
- package/lib/core/tier0-messaging.test.cjs +218 -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/brain-derivation-graceful-degradation.test.cjs +2 -2
- 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/mos-status-renderer.test.cjs +2 -2
- package/lib/memory/navigation-engine-core.test.cjs +1 -1
- 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 +223 -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/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,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Phase 127-02 Task 1 (TDD RED -> GREEN) -- tier0-messaging chokepoint tests.
|
|
6
|
+
*
|
|
7
|
+
* Covers BRAIN-MCP-127-09 acceptance:
|
|
8
|
+
* Test 1: DIRECTOR_NOT_AVAILABLE constant locks the wire string.
|
|
9
|
+
* Test 2: tier0Response("brain_ask") shape (status / reason / command_context
|
|
10
|
+
* / upgrade_hint regex / fallback_advice regex).
|
|
11
|
+
* Test 3: tier0Response(null) coerces command_context to "unknown".
|
|
12
|
+
* Test 4: isAvailable() returns false when MINDRIAN_BRAIN_KEY is unset.
|
|
13
|
+
* Test 5: isAvailable() returns true when MINDRIAN_BRAIN_KEY is set in env.
|
|
14
|
+
* Test 6: larryTier0Hint() returns a one-line string under 120 chars,
|
|
15
|
+
* contains "Brain" and "key", zero em-dashes.
|
|
16
|
+
* Test 7: Shim's tier0Response(name) is byte-identical to chokepoint's
|
|
17
|
+
* tier0Response(name) -- delegation property (no duplicate shape).
|
|
18
|
+
* Test 8: Plan 127-00's mindrian-brain-shim.test.cjs file still passes after
|
|
19
|
+
* the refactor (non-breaking chokepoint introduction).
|
|
20
|
+
*
|
|
21
|
+
* Canon parts:
|
|
22
|
+
* - Part 7 (reuse): the shim's local tier0Response is a one-line passthrough.
|
|
23
|
+
* - Part 8 (graph boundary): no network IO in this chokepoint; isAvailable
|
|
24
|
+
* delegates to brain-client.cjs (which is the existing chokepoint).
|
|
25
|
+
*
|
|
26
|
+
* HARD RULE: no em-dashes.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const assert = require('node:assert/strict');
|
|
30
|
+
const fs = require('node:fs');
|
|
31
|
+
const path = require('node:path');
|
|
32
|
+
const cp = require('node:child_process');
|
|
33
|
+
|
|
34
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
35
|
+
const CHOKEPOINT_PATH = path.join(REPO_ROOT, 'lib', 'core', 'tier0-messaging.cjs');
|
|
36
|
+
const SHIM_PATH = path.join(REPO_ROOT, 'bin', 'mindrian-brain-mcp-client.cjs');
|
|
37
|
+
const SHIM_TEST_PATH = path.join(REPO_ROOT, 'lib', 'core', 'mindrian-brain-shim.test.cjs');
|
|
38
|
+
|
|
39
|
+
let passed = 0;
|
|
40
|
+
let failed = 0;
|
|
41
|
+
|
|
42
|
+
function ok(name) {
|
|
43
|
+
passed += 1;
|
|
44
|
+
process.stdout.write(' ok ' + name + '\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function fail(name, err) {
|
|
48
|
+
failed += 1;
|
|
49
|
+
process.stdout.write(' FAIL ' + name + '\n');
|
|
50
|
+
if (err) process.stdout.write(' ' + (err.message || String(err)) + '\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Test 1: DIRECTOR_NOT_AVAILABLE constant locks the wire string.
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
(function test1_director_constant() {
|
|
57
|
+
const label = 'DIRECTOR_NOT_AVAILABLE constant equals exact wire string';
|
|
58
|
+
try {
|
|
59
|
+
const mod = require(CHOKEPOINT_PATH);
|
|
60
|
+
assert.equal(mod.DIRECTOR_NOT_AVAILABLE, 'DIRECTOR_NOT_AVAILABLE',
|
|
61
|
+
'exported DIRECTOR_NOT_AVAILABLE must equal the literal wire string "DIRECTOR_NOT_AVAILABLE"');
|
|
62
|
+
ok(label);
|
|
63
|
+
} catch (e) { fail(label, e); }
|
|
64
|
+
})();
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Test 2: tier0Response("brain_ask") shape.
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
(function test2_response_shape() {
|
|
70
|
+
const label = 'tier0Response("brain_ask") returns canonical sentinel shape';
|
|
71
|
+
try {
|
|
72
|
+
delete require.cache[CHOKEPOINT_PATH];
|
|
73
|
+
const mod = require(CHOKEPOINT_PATH);
|
|
74
|
+
const r = mod.tier0Response('brain_ask');
|
|
75
|
+
assert.equal(r.status, 'DIRECTOR_NOT_AVAILABLE', 'status must be DIRECTOR_NOT_AVAILABLE');
|
|
76
|
+
assert.equal(r.reason, 'MINDRIAN_BRAIN_KEY not set', 'reason must be exact string');
|
|
77
|
+
assert.equal(r.command_context, 'brain_ask', 'command_context must echo input');
|
|
78
|
+
assert.match(r.upgrade_hint, /brain-access/, 'upgrade_hint must reference brain-access URL');
|
|
79
|
+
assert.match(r.fallback_advice, /Larry/, 'fallback_advice must mention Larry');
|
|
80
|
+
ok(label);
|
|
81
|
+
} catch (e) { fail(label, e); }
|
|
82
|
+
})();
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Test 3: tier0Response(null) coerces to "unknown".
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
(function test3_null_defensive() {
|
|
88
|
+
const label = 'tier0Response(null) coerces command_context to "unknown"';
|
|
89
|
+
try {
|
|
90
|
+
delete require.cache[CHOKEPOINT_PATH];
|
|
91
|
+
const mod = require(CHOKEPOINT_PATH);
|
|
92
|
+
const r1 = mod.tier0Response(null);
|
|
93
|
+
const r2 = mod.tier0Response(undefined);
|
|
94
|
+
const r3 = mod.tier0Response('');
|
|
95
|
+
const r4 = mod.tier0Response(123);
|
|
96
|
+
assert.equal(r1.command_context, 'unknown', 'null -> unknown');
|
|
97
|
+
assert.equal(r2.command_context, 'unknown', 'undefined -> unknown');
|
|
98
|
+
assert.equal(r3.command_context, 'unknown', 'empty string -> unknown');
|
|
99
|
+
assert.equal(r4.command_context, 'unknown', 'non-string -> unknown');
|
|
100
|
+
ok(label);
|
|
101
|
+
} catch (e) { fail(label, e); }
|
|
102
|
+
})();
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Test 4: isAvailable() returns false when MINDRIAN_BRAIN_KEY is unset.
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
(function test4_isAvailable_false() {
|
|
108
|
+
const label = 'isAvailable() returns false when MINDRIAN_BRAIN_KEY is unset and no env-file';
|
|
109
|
+
try {
|
|
110
|
+
// Use a subprocess with hermetic HOME and unset MINDRIAN_BRAIN_KEY so we
|
|
111
|
+
// do not pollute this process's brain-client memoized cache.
|
|
112
|
+
const tmpHome = fs.mkdtempSync(path.join(require('node:os').tmpdir(), 'tier0-msg-test-'));
|
|
113
|
+
const script = 'process.env.HOME=' + JSON.stringify(tmpHome) + ';'
|
|
114
|
+
+ 'delete process.env.MINDRIAN_BRAIN_KEY;'
|
|
115
|
+
+ 'const m = require(' + JSON.stringify(CHOKEPOINT_PATH) + ');'
|
|
116
|
+
+ 'process.stdout.write(JSON.stringify({ available: m.isAvailable() }));';
|
|
117
|
+
const out = cp.execFileSync(process.execPath, ['-e', script], {
|
|
118
|
+
encoding: 'utf8',
|
|
119
|
+
env: Object.assign({}, process.env, { HOME: tmpHome, MINDRIAN_BRAIN_KEY: '' }),
|
|
120
|
+
});
|
|
121
|
+
const parsed = JSON.parse(out);
|
|
122
|
+
assert.equal(parsed.available, false, 'isAvailable must be false with no key');
|
|
123
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
124
|
+
ok(label);
|
|
125
|
+
} catch (e) { fail(label, e); }
|
|
126
|
+
})();
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Test 5: isAvailable() returns true when MINDRIAN_BRAIN_KEY is set.
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
(function test5_isAvailable_true() {
|
|
132
|
+
const label = 'isAvailable() returns true when MINDRIAN_BRAIN_KEY is set in env';
|
|
133
|
+
try {
|
|
134
|
+
const tmpHome = fs.mkdtempSync(path.join(require('node:os').tmpdir(), 'tier0-msg-test-'));
|
|
135
|
+
const script = 'const m = require(' + JSON.stringify(CHOKEPOINT_PATH) + ');'
|
|
136
|
+
+ 'process.stdout.write(JSON.stringify({ available: m.isAvailable() }));';
|
|
137
|
+
const out = cp.execFileSync(process.execPath, ['-e', script], {
|
|
138
|
+
encoding: 'utf8',
|
|
139
|
+
env: Object.assign({}, process.env, { HOME: tmpHome, MINDRIAN_BRAIN_KEY: 'test-key-fixture-127-02' }),
|
|
140
|
+
});
|
|
141
|
+
const parsed = JSON.parse(out);
|
|
142
|
+
assert.equal(parsed.available, true, 'isAvailable must be true with key set');
|
|
143
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
144
|
+
ok(label);
|
|
145
|
+
} catch (e) { fail(label, e); }
|
|
146
|
+
})();
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Test 6: larryTier0Hint() shape.
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
(function test6_larry_hint() {
|
|
152
|
+
const label = 'larryTier0Hint() one-line string, <120 chars, contains "Brain" + "key", zero em-dashes';
|
|
153
|
+
try {
|
|
154
|
+
delete require.cache[CHOKEPOINT_PATH];
|
|
155
|
+
const mod = require(CHOKEPOINT_PATH);
|
|
156
|
+
const hint = mod.larryTier0Hint();
|
|
157
|
+
assert.equal(typeof hint, 'string', 'must return a string');
|
|
158
|
+
assert.ok(hint.length <= 120, 'must be <= 120 chars (got ' + hint.length + ')');
|
|
159
|
+
assert.ok(!/\n/.test(hint), 'must be a single line (no newlines)');
|
|
160
|
+
assert.match(hint, /Brain/, 'must contain "Brain"');
|
|
161
|
+
assert.match(hint, /key/, 'must contain "key"');
|
|
162
|
+
assert.ok(!/[\u2014\u2013]/.test(hint), 'must contain zero em-dashes (U+2014) or en-dashes (U+2013)');
|
|
163
|
+
ok(label);
|
|
164
|
+
} catch (e) { fail(label, e); }
|
|
165
|
+
})();
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Test 7: Shim's tier0Response is byte-identical to chokepoint's tier0Response
|
|
169
|
+
// (delegation property -- the shim's local function is a one-line
|
|
170
|
+
// passthrough to the chokepoint).
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
(function test7_delegation_property() {
|
|
173
|
+
const label = 'shim tier0Response delegates to chokepoint (byte-identical output for all 6 tool names)';
|
|
174
|
+
try {
|
|
175
|
+
delete require.cache[CHOKEPOINT_PATH];
|
|
176
|
+
const chokepoint = require(CHOKEPOINT_PATH);
|
|
177
|
+
// Read the shim source and verify it requires the chokepoint module.
|
|
178
|
+
const src = fs.readFileSync(SHIM_PATH, 'utf8');
|
|
179
|
+
assert.match(src, /require\(['"][^'"]*tier0-messaging\.cjs['"]\)/,
|
|
180
|
+
'shim source must require lib/core/tier0-messaging.cjs');
|
|
181
|
+
// Verify the chokepoint's response matches the wire shape for each tool.
|
|
182
|
+
const toolNames = ['brain_ask', 'brain_query', 'brain_schema', 'brain_search', 'brain_stats', 'brain_write'];
|
|
183
|
+
for (const name of toolNames) {
|
|
184
|
+
const r = chokepoint.tier0Response(name);
|
|
185
|
+
assert.equal(r.status, 'DIRECTOR_NOT_AVAILABLE', 'chokepoint status for ' + name);
|
|
186
|
+
assert.equal(r.command_context, name, 'chokepoint command_context for ' + name);
|
|
187
|
+
assert.equal(r.reason, 'MINDRIAN_BRAIN_KEY not set', 'chokepoint reason for ' + name);
|
|
188
|
+
}
|
|
189
|
+
ok(label);
|
|
190
|
+
} catch (e) { fail(label, e); }
|
|
191
|
+
})();
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Test 8: Plan 127-00's mindrian-brain-shim.test.cjs still passes after
|
|
195
|
+
// the refactor (non-breaking chokepoint introduction).
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
(function test8_shim_tests_preserved() {
|
|
198
|
+
const label = 'plan 127-00 shim test file still PASSES after refactor';
|
|
199
|
+
try {
|
|
200
|
+
const result = cp.spawnSync(process.execPath, [SHIM_TEST_PATH], {
|
|
201
|
+
encoding: 'utf8',
|
|
202
|
+
timeout: 15000,
|
|
203
|
+
});
|
|
204
|
+
if (result.status !== 0) {
|
|
205
|
+
throw new Error('shim test exited ' + result.status + '\nstdout:\n' + result.stdout + '\nstderr:\n' + result.stderr);
|
|
206
|
+
}
|
|
207
|
+
// Confirm all 6 tests passed.
|
|
208
|
+
assert.match(result.stdout, /PASSED:\s*6/, 'expected 6 shim tests to pass; got:\n' + result.stdout);
|
|
209
|
+
assert.match(result.stdout, /FAILED:\s*0/, 'expected 0 shim test failures; got:\n' + result.stdout);
|
|
210
|
+
ok(label);
|
|
211
|
+
} catch (e) { fail(label, e); }
|
|
212
|
+
})();
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Summary
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
process.stdout.write('\nPASSED: ' + passed + '\nFAILED: ' + failed + '\n');
|
|
218
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
5
|
+
*
|
|
6
|
+
* Phase 119-00 Wave 1 -- Venture-Shaped-Turn Nudge (D-02 detector).
|
|
7
|
+
*
|
|
8
|
+
* shouldSurfaceNudge(roomDir, opts) -> {surface, turn_count, threshold, skip_reason?}
|
|
9
|
+
*
|
|
10
|
+
* D-02 invariant: after N=3 consecutive venture-shaped conversational turns
|
|
11
|
+
* with no upload AND no active room, surface an F.1 selector offering
|
|
12
|
+
* [upload material] / [/mos:new-project] / [keep talking]. This is the ONLY
|
|
13
|
+
* auto-create-adjacent path for prompt-only sessions (D-01 prohibits
|
|
14
|
+
* auto-creating from prompts alone).
|
|
15
|
+
*
|
|
16
|
+
* D-01 invariant: the upload path takes precedence. If any
|
|
17
|
+
* 'auto_explore_fired' event appears in the recent window, the nudge does
|
|
18
|
+
* NOT surface -- the Phase 117 detector is already handling first-material.
|
|
19
|
+
*
|
|
20
|
+
* Canon Part 8 boundary -- user_text classification:
|
|
21
|
+
* The user-conversational "venture-shaped" classification CANNOT run on
|
|
22
|
+
* raw user text inside this module. The event-log schema (per Canon Part
|
|
23
|
+
* 8 fence + Phase 90-06 cross-room aggregator) structurally excludes
|
|
24
|
+
* user_text -- the memory_event log is queried by cross-room aggregators
|
|
25
|
+
* and storing raw user content there would breach the boundary.
|
|
26
|
+
*
|
|
27
|
+
* Alternative signal contract: the upstream telemetry surface (the
|
|
28
|
+
* PostToolUse / UserPromptSubmit hook OR the Phase 115 dual-path-detector
|
|
29
|
+
* seam) classifies sentences at WRITE TIME via
|
|
30
|
+
* lib/core/mva-classifier.cjs::classify and stores ONLY a scalar boolean
|
|
31
|
+
* `venture_classified` + classification_source enum in
|
|
32
|
+
* f_selector_decision.properties. The nudge module reads the scalar at
|
|
33
|
+
* decision time.
|
|
34
|
+
*
|
|
35
|
+
* v1.13.0 safe-default: if `properties.venture_classified` is structurally
|
|
36
|
+
* absent at execution time, the nudge degrades to surface:false with
|
|
37
|
+
* skip_reason:'venture_classification_unavailable'. A v1.14.0 phase wires
|
|
38
|
+
* the upstream classification at the f_selector_decision emission site.
|
|
39
|
+
*
|
|
40
|
+
* Canon Part 9 chokepoint:
|
|
41
|
+
* - All reads route through lib/core/navigation.cjs::findRecentChanges.
|
|
42
|
+
* - room.db is opened via node:sqlite DatabaseSync directly (mirrors the
|
|
43
|
+
* Phase 117 scripts/auto-explore-fingerprint.cjs pattern; node:sqlite
|
|
44
|
+
* usage from a CLI-adjacent module is not a chokepoint violation per
|
|
45
|
+
* the Phase 109-06 pre-commit hook which gates only direct require of
|
|
46
|
+
* room-db.cjs).
|
|
47
|
+
*
|
|
48
|
+
* Pure CJS, node built-ins + lib/core/navigation. NO require of room-db.cjs.
|
|
49
|
+
* NO require of dual-path-detector.cjs at read-time (the upstream
|
|
50
|
+
* classification has already happened at write-time per the alternative
|
|
51
|
+
* signal contract above). NO require of brain-client. Canon Part 8 preserved.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
const path = require('node:path');
|
|
55
|
+
const fs = require('node:fs');
|
|
56
|
+
|
|
57
|
+
const VENTURE_NUDGE_THRESHOLD = 3;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* shouldSurfaceNudge(roomDir, opts) -> { surface, turn_count, threshold, skip_reason? }
|
|
61
|
+
*
|
|
62
|
+
* @param {string} roomDir - absolute path to a room directory.
|
|
63
|
+
* @param {object} [opts] - reserved for future extension (D-02 threshold override).
|
|
64
|
+
* @returns {object}
|
|
65
|
+
*/
|
|
66
|
+
function shouldSurfaceNudge(roomDir, opts) {
|
|
67
|
+
const options = opts || {};
|
|
68
|
+
const threshold = Number.isInteger(options.threshold) && options.threshold > 0
|
|
69
|
+
? options.threshold
|
|
70
|
+
: VENTURE_NUDGE_THRESHOLD;
|
|
71
|
+
|
|
72
|
+
// Guard 1: no roomDir -> safe default.
|
|
73
|
+
if (!roomDir || typeof roomDir !== 'string') {
|
|
74
|
+
return { surface: false, turn_count: 0, threshold: threshold, skip_reason: 'no_room_dir' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Guard 2: room.db missing -> cold-start safe default. No false positives.
|
|
78
|
+
const dbPath = path.join(roomDir, '.mindrian', 'room.db');
|
|
79
|
+
if (!fs.existsSync(dbPath)) {
|
|
80
|
+
return { surface: false, turn_count: 0, threshold: threshold, skip_reason: 'no_room_db' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Open the db read-only via node:sqlite DatabaseSync. Mirrors the Phase 117
|
|
84
|
+
// scripts/auto-explore-fingerprint.cjs::dailyCapHit() pattern. Lazy-require
|
|
85
|
+
// so environments lacking node:sqlite degrade to surface:false.
|
|
86
|
+
let DatabaseSync;
|
|
87
|
+
try {
|
|
88
|
+
({ DatabaseSync } = require('node:sqlite'));
|
|
89
|
+
} catch (_e) {
|
|
90
|
+
return { surface: false, turn_count: 0, threshold: threshold, skip_reason: 'no_sqlite' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let db;
|
|
94
|
+
try {
|
|
95
|
+
db = new DatabaseSync(dbPath);
|
|
96
|
+
} catch (_e) {
|
|
97
|
+
return { surface: false, turn_count: 0, threshold: threshold, skip_reason: 'db_open_failed' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let recent;
|
|
101
|
+
try {
|
|
102
|
+
const navigation = require('./navigation.cjs');
|
|
103
|
+
// Window = last 24 hours; cap at 200 events for safety.
|
|
104
|
+
const sinceMs = Date.now() - (24 * 60 * 60 * 1000);
|
|
105
|
+
recent = navigation.findRecentChanges(db, sinceMs, { limit: 200 });
|
|
106
|
+
} catch (_e) {
|
|
107
|
+
try { db.close(); } catch (_ignore) { /* graceful */ }
|
|
108
|
+
return { surface: false, turn_count: 0, threshold: threshold, skip_reason: 'navigation_query_failed' };
|
|
109
|
+
} finally {
|
|
110
|
+
try { db.close(); } catch (_ignore) { /* graceful */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!Array.isArray(recent)) {
|
|
114
|
+
return { surface: false, turn_count: 0, threshold: threshold, skip_reason: 'empty_window' };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// D-01 invariant short-circuit: upload path takes precedence.
|
|
118
|
+
for (const event of recent) {
|
|
119
|
+
if (event && event.eventType === 'auto_explore_fired') {
|
|
120
|
+
return { surface: false, turn_count: 0, threshold: threshold, skip_reason: 'upload_path_active' };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Filter to user-conversational-turn signal events. For v1.13.0-beta.18
|
|
125
|
+
// scope, "conversational turn" is approximated by f_selector_decision
|
|
126
|
+
// (the event that fires on every meaningful F-shape selection per Phase
|
|
127
|
+
// 125-06). focus_changed is intentionally excluded -- it is a navigation
|
|
128
|
+
// event, not a conversation turn. Precise per-turn detection is deferred
|
|
129
|
+
// to Phase 121 trajectory telemetry.
|
|
130
|
+
//
|
|
131
|
+
// Per Canon Part 8 alternative signal contract: count only events where
|
|
132
|
+
// properties.venture_classified === true. If the field is structurally
|
|
133
|
+
// absent (NO event in the window carries it), degrade to safe-default
|
|
134
|
+
// skip_reason:'venture_classification_unavailable' -- the v1.14.0
|
|
135
|
+
// upstream-classification surface has not yet wired the scalar.
|
|
136
|
+
let turnCount = 0;
|
|
137
|
+
let sawClassifiedField = false;
|
|
138
|
+
for (const event of recent) {
|
|
139
|
+
if (!event || event.eventType !== 'f_selector_decision') continue;
|
|
140
|
+
const props = event.properties || {};
|
|
141
|
+
if (typeof props.venture_classified === 'boolean') {
|
|
142
|
+
sawClassifiedField = true;
|
|
143
|
+
if (props.venture_classified === true) turnCount += 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!sawClassifiedField) {
|
|
148
|
+
return {
|
|
149
|
+
surface: false,
|
|
150
|
+
turn_count: 0,
|
|
151
|
+
threshold: threshold,
|
|
152
|
+
skip_reason: 'venture_classification_unavailable',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const surface = turnCount >= threshold;
|
|
157
|
+
return { surface: surface, turn_count: turnCount, threshold: threshold };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
shouldSurfaceNudge: shouldSurfaceNudge,
|
|
162
|
+
VENTURE_NUDGE_THRESHOLD: VENTURE_NUDGE_THRESHOLD,
|
|
163
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Phase 119-00 Wave 1 -- venture-shape-nudge module tests.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the lib/memory/selector-decisions.test.cjs fixture pattern
|
|
7
|
+
* (fs.mkdtempSync + openRoomDb). Tests are allowed to require room-db.cjs
|
|
8
|
+
* directly per the Phase 109-06 pre-commit chokepoint allow-list for tests/.
|
|
9
|
+
*
|
|
10
|
+
* Covers the 7 behavior axes from 119-00-PLAN.md Task 1:
|
|
11
|
+
* Test 1 -- cold-start floor (Test 5 in plan: no room.db -> surface false)
|
|
12
|
+
* Test 2 -- venture-shape accumulation to threshold (Test 6)
|
|
13
|
+
* Test 3 -- D-01 invariant: auto_explore_fired short-circuits (Test 7)
|
|
14
|
+
* Test 4 -- venture_classification_unavailable safe-default (Test 7b)
|
|
15
|
+
* Test 5 -- venture_classified present happy-path (Test 7c)
|
|
16
|
+
* Test 6 -- no Brain client require (Test 8; source-grep audit)
|
|
17
|
+
* Test 7 -- no-room-dir guard
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { test } = require('node:test');
|
|
21
|
+
const { strict: assert } = require('node:assert');
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const os = require('node:os');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
|
|
26
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
27
|
+
const { openRoomDb } = require(path.join(REPO_ROOT, 'lib', 'core', 'room-db.cjs'));
|
|
28
|
+
const navigation = require(path.join(REPO_ROOT, 'lib', 'core', 'navigation.cjs'));
|
|
29
|
+
const ventureShapeNudge = require(path.join(REPO_ROOT, 'lib', 'core', 'venture-shape-nudge.cjs'));
|
|
30
|
+
|
|
31
|
+
function freshRoom() {
|
|
32
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'p119-00-nudge-'));
|
|
33
|
+
fs.mkdirSync(path.join(dir, '.mindrian'), { recursive: true });
|
|
34
|
+
const db = openRoomDb(dir);
|
|
35
|
+
return { dir, db };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function closeQuiet(db) {
|
|
39
|
+
try { db.close(); } catch (_e) { /* graceful */ }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('Test 1: cold-start floor -- no events in db returns surface:false with venture_classification_unavailable', () => {
|
|
43
|
+
const { dir, db } = freshRoom();
|
|
44
|
+
closeQuiet(db);
|
|
45
|
+
const r = ventureShapeNudge.shouldSurfaceNudge(dir, {});
|
|
46
|
+
assert.equal(r.surface, false);
|
|
47
|
+
assert.equal(r.turn_count, 0);
|
|
48
|
+
assert.equal(r.threshold, 3);
|
|
49
|
+
assert.equal(r.skip_reason, 'venture_classification_unavailable');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('Test 2: 3 venture_classified=true events reach threshold (surface=true)', () => {
|
|
53
|
+
const { dir, db } = freshRoom();
|
|
54
|
+
for (let i = 0; i < 3; i++) {
|
|
55
|
+
const result = navigation.logMemoryEvent(db, 'f_selector_decision', {
|
|
56
|
+
venture_classified: true,
|
|
57
|
+
classification_source: 'haiku-4-5',
|
|
58
|
+
source_path: 'test:venture-shape-nudge',
|
|
59
|
+
created_by: 'system',
|
|
60
|
+
});
|
|
61
|
+
assert.equal(result.ok, true, 'logMemoryEvent should accept f_selector_decision; got reason=' + (result.reason || ''));
|
|
62
|
+
}
|
|
63
|
+
closeQuiet(db);
|
|
64
|
+
const r = ventureShapeNudge.shouldSurfaceNudge(dir, {});
|
|
65
|
+
assert.equal(r.surface, true);
|
|
66
|
+
assert.equal(r.turn_count, 3);
|
|
67
|
+
assert.equal(r.threshold, 3);
|
|
68
|
+
assert.equal(r.skip_reason, undefined);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('Test 3: D-01 invariant -- auto_explore_fired short-circuits regardless of turn count', () => {
|
|
72
|
+
const { dir, db } = freshRoom();
|
|
73
|
+
// Seed 5 venture-shaped turns AND one auto_explore_fired event.
|
|
74
|
+
for (let i = 0; i < 5; i++) {
|
|
75
|
+
navigation.logMemoryEvent(db, 'f_selector_decision', {
|
|
76
|
+
venture_classified: true,
|
|
77
|
+
classification_source: 'haiku-4-5',
|
|
78
|
+
source_path: 'test:venture-shape-nudge',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
navigation.logMemoryEvent(db, 'auto_explore_fired', {
|
|
82
|
+
material_id: 'abcd1234',
|
|
83
|
+
relative_file_path: 'docs/sample.md',
|
|
84
|
+
room_slug: 'test-room',
|
|
85
|
+
tier: 1,
|
|
86
|
+
surfacing_count: 0,
|
|
87
|
+
brain_baseline_present: false,
|
|
88
|
+
source_path: 'test:venture-shape-nudge',
|
|
89
|
+
});
|
|
90
|
+
closeQuiet(db);
|
|
91
|
+
const r = ventureShapeNudge.shouldSurfaceNudge(dir, {});
|
|
92
|
+
assert.equal(r.surface, false);
|
|
93
|
+
assert.equal(r.skip_reason, 'upload_path_active');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('Test 4: venture_classification_unavailable safe-default when field absent', () => {
|
|
97
|
+
const { dir, db } = freshRoom();
|
|
98
|
+
// Seed events without venture_classified field at all.
|
|
99
|
+
for (let i = 0; i < 5; i++) {
|
|
100
|
+
navigation.logMemoryEvent(db, 'f_selector_decision', {
|
|
101
|
+
command: 'mos:test',
|
|
102
|
+
framework: 'test-framework',
|
|
103
|
+
source_path: 'test:venture-shape-nudge',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
closeQuiet(db);
|
|
107
|
+
const r = ventureShapeNudge.shouldSurfaceNudge(dir, {});
|
|
108
|
+
assert.equal(r.surface, false);
|
|
109
|
+
assert.equal(r.skip_reason, 'venture_classification_unavailable');
|
|
110
|
+
assert.equal(r.turn_count, 0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('Test 5: mixed venture_classified true/false -- only true counts toward threshold', () => {
|
|
114
|
+
const { dir, db } = freshRoom();
|
|
115
|
+
// 3 true + 1 false; threshold=3 met.
|
|
116
|
+
for (let i = 0; i < 3; i++) {
|
|
117
|
+
navigation.logMemoryEvent(db, 'f_selector_decision', {
|
|
118
|
+
venture_classified: true,
|
|
119
|
+
classification_source: 'haiku-4-5',
|
|
120
|
+
source_path: 'test:venture-shape-nudge',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
navigation.logMemoryEvent(db, 'f_selector_decision', {
|
|
124
|
+
venture_classified: false,
|
|
125
|
+
classification_source: 'haiku-4-5',
|
|
126
|
+
source_path: 'test:venture-shape-nudge',
|
|
127
|
+
});
|
|
128
|
+
closeQuiet(db);
|
|
129
|
+
const r = ventureShapeNudge.shouldSurfaceNudge(dir, {});
|
|
130
|
+
assert.equal(r.surface, true);
|
|
131
|
+
assert.equal(r.turn_count, 3);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('Test 6: no Brain client require -- source-grep audit', () => {
|
|
135
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'lib', 'core', 'venture-shape-nudge.cjs'), 'utf8');
|
|
136
|
+
// Canon Part 8: zero brain-client require, zero mcp-server-brain require,
|
|
137
|
+
// zero fetch to brain.mindrian.
|
|
138
|
+
assert.equal(/require\([^)]*brain-client/.test(src), false, 'brain-client require detected');
|
|
139
|
+
assert.equal(/require\([^)]*mcp-server-brain/.test(src), false, 'mcp-server-brain require detected');
|
|
140
|
+
assert.equal(/brain\.mindrian/.test(src), false, 'brain.mindrian fetch reference detected');
|
|
141
|
+
// Canon Part 9: zero direct room-db.cjs require.
|
|
142
|
+
assert.equal(/require\([^)]*room-db\.cjs/.test(src), false, 'direct room-db.cjs require detected');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('Test 7: no-room-dir guard returns safe default', () => {
|
|
146
|
+
const r = ventureShapeNudge.shouldSurfaceNudge('', {});
|
|
147
|
+
assert.equal(r.surface, false);
|
|
148
|
+
assert.equal(r.skip_reason, 'no_room_dir');
|
|
149
|
+
|
|
150
|
+
const r2 = ventureShapeNudge.shouldSurfaceNudge(null, {});
|
|
151
|
+
assert.equal(r2.surface, false);
|
|
152
|
+
|
|
153
|
+
// Non-existent path -> no_room_db.
|
|
154
|
+
const r3 = ventureShapeNudge.shouldSurfaceNudge('/tmp/p119-nonexistent-' + Date.now(), {});
|
|
155
|
+
assert.equal(r3.surface, false);
|
|
156
|
+
assert.equal(r3.skip_reason, 'no_room_db');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('Test 8: VENTURE_NUDGE_THRESHOLD constant exported and equals 3', () => {
|
|
160
|
+
assert.equal(ventureShapeNudge.VENTURE_NUDGE_THRESHOLD, 3);
|
|
161
|
+
});
|
package/lib/core/visual-ops.cjs
CHANGED
|
@@ -173,8 +173,19 @@ function formatSectionHeader(sectionName, entryCount, stage) {
|
|
|
173
173
|
// MERMAID DIAGRAM GENERATORS
|
|
174
174
|
// ═══════════════════════════════════════════════════════════════
|
|
175
175
|
|
|
176
|
-
// De Stijl hex colors for Mermaid style directives (no ANSI in diagrams)
|
|
177
|
-
|
|
176
|
+
// De Stijl hex colors for Mermaid style directives (no ANSI in diagrams).
|
|
177
|
+
//
|
|
178
|
+
// Phase 121.5-03: DS_HEX is now SOURCED FROM references/visual/palette.json
|
|
179
|
+
// (the canonical De Stijl source-of-truth per ODD 3 resolution + Canon Part 7).
|
|
180
|
+
// The literal fallback below is graceful degradation only -- if palette.json
|
|
181
|
+
// fails to load (unlikely in a properly-installed plugin), the shipping
|
|
182
|
+
// values are frozen here verbatim from the pre-121.5 hardcoded DS_HEX so
|
|
183
|
+
// downstream Mermaid + dashboard rendering keeps working.
|
|
184
|
+
//
|
|
185
|
+
// Test seam: MINDRIAN_PALETTE_PATH env var overrides the default palette
|
|
186
|
+
// path. Used by lib/memory/statusline-two-row.test.cjs (test 11) to verify
|
|
187
|
+
// the wiring without touching the live palette.json.
|
|
188
|
+
const FALLBACK_DS_HEX = {
|
|
178
189
|
red: '#A63D2F',
|
|
179
190
|
blue: '#1E3A6E',
|
|
180
191
|
yellow: '#C8A43C',
|
|
@@ -186,6 +197,63 @@ const DS_HEX = {
|
|
|
186
197
|
bg: '#0D0D0D'
|
|
187
198
|
};
|
|
188
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Compose DS_HEX from references/visual/palette.json. Preserves the legacy
|
|
202
|
+
* key names (red / blue / yellow / green / sienna / amethyst / cream / muted
|
|
203
|
+
* / bg) so existing consumers (Mermaid generators, dashboard exporters) keep
|
|
204
|
+
* working byte-identical. Also exposes the palette tier keys via dot
|
|
205
|
+
* extension for new consumers (e.g. mondrian_red, muted_green, saturated_blue).
|
|
206
|
+
*/
|
|
207
|
+
function loadPaletteDsHex() {
|
|
208
|
+
const path = require('path');
|
|
209
|
+
const fs = require('fs');
|
|
210
|
+
const candidate = process.env.MINDRIAN_PALETTE_PATH ||
|
|
211
|
+
path.join(__dirname, '..', '..', 'references', 'visual', 'palette.json');
|
|
212
|
+
try {
|
|
213
|
+
const palette = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
214
|
+
const base = palette.base || {};
|
|
215
|
+
const a = palette.palette_a_discovery || {};
|
|
216
|
+
const b = palette.palette_b_build || {};
|
|
217
|
+
const ext = palette.extended || {};
|
|
218
|
+
const composed = {
|
|
219
|
+
// Legacy DS_HEX keys (preserve backward compat for Mermaid + dashboard)
|
|
220
|
+
red: base.mondrian_red || FALLBACK_DS_HEX.red,
|
|
221
|
+
blue: base.mondrian_blue || FALLBACK_DS_HEX.blue,
|
|
222
|
+
yellow: base.mondrian_yellow || FALLBACK_DS_HEX.yellow,
|
|
223
|
+
green: base.success_green || FALLBACK_DS_HEX.green,
|
|
224
|
+
sienna: ext.sienna || FALLBACK_DS_HEX.sienna,
|
|
225
|
+
amethyst: base.amethyst || FALLBACK_DS_HEX.amethyst,
|
|
226
|
+
cream: base.cream || FALLBACK_DS_HEX.cream,
|
|
227
|
+
muted: base.gray_meta || FALLBACK_DS_HEX.muted,
|
|
228
|
+
bg: ext.bg || base.mondrian_black || FALLBACK_DS_HEX.bg,
|
|
229
|
+
// Tier-keyed mirrors (new consumers can use the explicit names)
|
|
230
|
+
mondrian_red: base.mondrian_red,
|
|
231
|
+
mondrian_blue: base.mondrian_blue,
|
|
232
|
+
mondrian_yellow: base.mondrian_yellow,
|
|
233
|
+
mondrian_black: base.mondrian_black,
|
|
234
|
+
mondrian_white: base.mondrian_white,
|
|
235
|
+
gray_meta: base.gray_meta,
|
|
236
|
+
success_green: base.success_green,
|
|
237
|
+
muted_red: a.muted_red,
|
|
238
|
+
muted_green: a.muted_green,
|
|
239
|
+
teal_accent: a.teal_accent,
|
|
240
|
+
saturated_red: b.saturated_red,
|
|
241
|
+
saturated_blue: b.saturated_blue,
|
|
242
|
+
saturated_yellow: b.saturated_yellow,
|
|
243
|
+
};
|
|
244
|
+
return composed;
|
|
245
|
+
} catch (e) {
|
|
246
|
+
// Graceful degradation per Canon Part 7. One warning to stderr, then
|
|
247
|
+
// fall back to the frozen literal.
|
|
248
|
+
if (process.env.MINDRIAN_PALETTE_DEBUG) {
|
|
249
|
+
try { process.stderr.write('[visual-ops] palette.json load failed: ' + e.message + '\n'); } catch (_) {}
|
|
250
|
+
}
|
|
251
|
+
return Object.assign({}, FALLBACK_DS_HEX);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const DS_HEX = loadPaletteDsHex();
|
|
256
|
+
|
|
189
257
|
// Map stage names to hex colors for Mermaid
|
|
190
258
|
const STAGE_HEX = {
|
|
191
259
|
preopportunity: DS_HEX.muted,
|