@mindrian_os/install 1.13.0-beta.13 → 1.13.0-beta.14
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 +16 -11
- package/README.md +74 -572
- package/commands/act.md +1 -0
- package/commands/admin.md +1 -0
- package/commands/analyze-needs.md +1 -0
- package/commands/analyze-systems.md +1 -0
- package/commands/analyze-timing.md +1 -0
- package/commands/auto-explore.md +1 -0
- package/commands/beautiful-question.md +1 -0
- package/commands/brain-derive.md +1 -0
- package/commands/build-knowledge.md +1 -0
- package/commands/build-thesis.md +1 -0
- package/commands/causal.md +1 -0
- package/commands/challenge-assumptions.md +1 -0
- package/commands/compare-ventures.md +1 -0
- package/commands/dashboard.md +1 -0
- package/commands/deep-grade.md +1 -0
- package/commands/diagnose.md +1 -0
- package/commands/diagnostics.md +1 -0
- package/commands/doctor.md +1 -0
- package/commands/dominant-designs.md +1 -0
- package/commands/explain-decision.md +1 -0
- package/commands/explore-domains.md +1 -0
- package/commands/explore-futures.md +1 -0
- package/commands/explore-trends.md +1 -0
- package/commands/export.md +1 -0
- package/commands/feynman-timeline-refresh.md +78 -0
- package/commands/file-meeting.md +1 -0
- package/commands/find-analogies.md +1 -0
- package/commands/find-bottlenecks.md +1 -0
- package/commands/find-connections.md +1 -0
- package/commands/funding.md +1 -0
- package/commands/grade.md +1 -0
- package/commands/graph.md +1 -0
- package/commands/hat-briefing.md +1 -0
- package/commands/heal.md +1 -0
- package/commands/help.md +1 -0
- package/commands/hmi-status.md +1 -0
- package/commands/jtbd.md +1 -0
- package/commands/leadership.md +1 -0
- package/commands/lean-canvas.md +1 -0
- package/commands/macro-trends.md +1 -0
- package/commands/map-unknowns.md +1 -0
- package/commands/memory.md +1 -0
- package/commands/models.md +1 -0
- package/commands/mos-reason.md +1 -0
- package/commands/mullins.md +1 -0
- package/commands/new-project.md +1 -0
- package/commands/onboard.md +1 -0
- package/commands/operator.md +1 -0
- package/commands/opportunities.md +1 -0
- package/commands/organize.md +1 -0
- package/commands/persona.md +1 -0
- package/commands/pipeline.md +1 -0
- package/commands/present.md +1 -0
- package/commands/publish.md +1 -0
- package/commands/query.md +1 -0
- package/commands/radar.md +1 -0
- package/commands/reanalyze.md +1 -0
- package/commands/research.md +1 -0
- package/commands/room.md +1 -0
- package/commands/rooms.md +1 -0
- package/commands/root-cause.md +1 -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 +1 -0
- package/commands/scheduled-tasks.md +1 -0
- package/commands/score-innovation.md +1 -0
- package/commands/scout.md +1 -0
- package/commands/setup.md +1 -0
- package/commands/snapshot.md +1 -0
- package/commands/speakers.md +1 -0
- package/commands/splash.md +1 -0
- package/commands/status.md +1 -0
- package/commands/structure-argument.md +1 -0
- package/commands/suggest-next.md +1 -0
- package/commands/systems-thinking.md +1 -0
- package/commands/think-hats.md +1 -0
- package/commands/update.md +1 -0
- package/commands/user-needs.md +1 -0
- package/commands/validate.md +1 -0
- package/commands/value-proposition.md +1 -0
- package/commands/vault.md +1 -0
- package/commands/visualize.md +1 -0
- package/commands/whitespace.md +1 -0
- package/commands/wiki.md +1 -0
- package/lib/brain/framework-chain-slice.cjs +193 -0
- package/lib/core/feynman/ROOM.md +25 -0
- package/lib/core/feynman/timeline-renderer.cjs +197 -0
- package/lib/core/feynman/timeline-runner.cjs +281 -0
- package/lib/core/navigation/edges.cjs +86 -0
- package/lib/core/navigation/insights.cjs +37 -0
- package/lib/core/navigation/memory-events.cjs +39 -0
- package/lib/core/navigation/packet.cjs +89 -9
- package/lib/core/navigation/projections.cjs +201 -0
- package/lib/core/navigation.cjs +25 -0
- package/lib/mcp/larry-server-instructions.md +1 -1
- package/lib/memory/brain-cypher-chain-slice.test.cjs +368 -0
- package/lib/memory/f-selector-ranker.test.cjs +593 -0
- package/lib/memory/navigation-projections.test.cjs +241 -0
- package/lib/memory/navigation-write-edge.test.cjs +206 -0
- package/lib/memory/packet-chain-hint.test.cjs +407 -0
- package/lib/memory/packet-schema-validation.test.cjs +317 -0
- package/lib/memory/per-command-jtbd-derivation.test.cjs +130 -0
- package/lib/memory/per-command-teaching.test.cjs +110 -0
- package/lib/memory/run-feynman-tests.cjs +36 -0
- package/lib/memory/selector-decisions.test.cjs +417 -0
- package/lib/memory/selector-miss.test.cjs +290 -0
- package/lib/workflow/f-selector-ranker.cjs +420 -0
- package/lib/workflow/selector-decisions.cjs +368 -0
- package/package.json +1 -1
- package/references/design/email-template-standard.md +1 -1
- package/references/user-research/2026-04-05-leah-lawrence-session.md +3 -3
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
* Phase 125-01 -- lib/core/navigation/projections.cjs test suite.
|
|
4
|
+
*
|
|
5
|
+
* Covers the 8 acceptance bullets from 125-CONTEXT.md "Projections" section
|
|
6
|
+
* (D1 + D2 + D3) via 16 behaviors:
|
|
7
|
+
* resolveActiveFrameworks (Tests 1-5) D1 multi-signal precedence
|
|
8
|
+
* resolveHopDepth (Tests 6-9) D2 depth + rationale; default WIDE
|
|
9
|
+
* computeInvestmentLevel (Tests 10-15) D3 linear gradient 0..1
|
|
10
|
+
* purity invariant (Test 16) all three helpers pure + idempotent
|
|
11
|
+
*
|
|
12
|
+
* Three-surface compatibility: pure CJS + node built-ins + node:test. No
|
|
13
|
+
* surface-specific branches. No db / no fs / no Brain / no network.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const test = require('node:test');
|
|
19
|
+
const assert = require('node:assert');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
|
|
22
|
+
const PROJECTIONS_PATH = path.resolve(__dirname, '..', 'core', 'navigation', 'projections.cjs');
|
|
23
|
+
|
|
24
|
+
function loadProjections() {
|
|
25
|
+
delete require.cache[require.resolve(PROJECTIONS_PATH)];
|
|
26
|
+
return require(PROJECTIONS_PATH);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ----------- resolveActiveFrameworks (D1) -----------
|
|
30
|
+
|
|
31
|
+
test('Test 1: resolveActiveFrameworks({}) returns []', () => {
|
|
32
|
+
const { resolveActiveFrameworks } = loadProjections();
|
|
33
|
+
const out = resolveActiveFrameworks({});
|
|
34
|
+
assert.ok(Array.isArray(out), 'returns an array');
|
|
35
|
+
assert.strictEqual(out.length, 0, 'empty for empty roomState');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('Test 1b: resolveActiveFrameworks(null/undefined) returns []', () => {
|
|
39
|
+
const { resolveActiveFrameworks } = loadProjections();
|
|
40
|
+
assert.deepStrictEqual(resolveActiveFrameworks(null), []);
|
|
41
|
+
assert.deepStrictEqual(resolveActiveFrameworks(undefined), []);
|
|
42
|
+
assert.deepStrictEqual(resolveActiveFrameworks('not-an-object'), []);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('Test 2: resolveActiveFrameworks extracts Mullins 7 Domains from governing_thought', () => {
|
|
46
|
+
const { resolveActiveFrameworks } = loadProjections();
|
|
47
|
+
const out = resolveActiveFrameworks({
|
|
48
|
+
governing_thought: 'Apply Mullins 7 Domains to validate market attractiveness',
|
|
49
|
+
});
|
|
50
|
+
assert.ok(out.length >= 1, 'at least one entry');
|
|
51
|
+
const fw = out.find((x) => x.name === 'Mullins 7 Domains');
|
|
52
|
+
assert.ok(fw, 'Mullins 7 Domains is present');
|
|
53
|
+
assert.strictEqual(fw.source, 'governing_thought');
|
|
54
|
+
assert.strictEqual(fw.weight, 1.0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('Test 3: resolveActiveFrameworks precedence -- governing_thought sorts first by weight', () => {
|
|
58
|
+
const { resolveActiveFrameworks } = loadProjections();
|
|
59
|
+
const out = resolveActiveFrameworks({
|
|
60
|
+
governing_thought: 'Use SWOT to assess the position',
|
|
61
|
+
activeJtbd: 'find-problem',
|
|
62
|
+
});
|
|
63
|
+
assert.ok(out.length >= 1);
|
|
64
|
+
// First entry has the highest weight; governing_thought wins (1.0 > 0.75)
|
|
65
|
+
assert.strictEqual(out[0].source, 'governing_thought');
|
|
66
|
+
assert.strictEqual(out[0].weight, 1.0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('Test 4: resolveActiveFrameworks reads brainAnchors with source=brain_md weight 0.5', () => {
|
|
70
|
+
const { resolveActiveFrameworks } = loadProjections();
|
|
71
|
+
const out = resolveActiveFrameworks({
|
|
72
|
+
brainAnchors: ['SWOT', 'Porter Five Forces'],
|
|
73
|
+
});
|
|
74
|
+
assert.strictEqual(out.length, 2);
|
|
75
|
+
const names = out.map((x) => x.name).sort();
|
|
76
|
+
assert.deepStrictEqual(names, ['Porter Five Forces', 'SWOT']);
|
|
77
|
+
for (const e of out) {
|
|
78
|
+
assert.strictEqual(e.source, 'brain_md');
|
|
79
|
+
assert.strictEqual(e.weight, 0.5);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('Test 5: resolveActiveFrameworks 4-signal output ordered by weight desc', () => {
|
|
84
|
+
const { resolveActiveFrameworks } = loadProjections();
|
|
85
|
+
const out = resolveActiveFrameworks({
|
|
86
|
+
governing_thought: 'Use SWOT to find white space',
|
|
87
|
+
activeJtbd: 'find-problem',
|
|
88
|
+
brainAnchors: ['Wardley Map'],
|
|
89
|
+
recentFrameworks: ['Cynefin'],
|
|
90
|
+
});
|
|
91
|
+
// All 4 sources should appear (assuming activeJtbd resolves to *some* framework
|
|
92
|
+
// and the registry is loadable; if it doesn't resolve, that signal silently
|
|
93
|
+
// drops -- still strictly weight-desc ordered)
|
|
94
|
+
// Weights monotonically descend.
|
|
95
|
+
for (let i = 1; i < out.length; i++) {
|
|
96
|
+
assert.ok(out[i - 1].weight >= out[i].weight,
|
|
97
|
+
`weight at ${i-1} (${out[i-1].weight}) >= weight at ${i} (${out[i].weight})`);
|
|
98
|
+
}
|
|
99
|
+
// First entry is governing_thought (the highest weight, 1.0)
|
|
100
|
+
assert.strictEqual(out[0].source, 'governing_thought');
|
|
101
|
+
// Sources observed are a subset of the 4 canonical sources
|
|
102
|
+
const allowed = new Set(['governing_thought', 'activeJtbd', 'brain_md', 'memory_event']);
|
|
103
|
+
for (const e of out) {
|
|
104
|
+
assert.ok(allowed.has(e.source), `unknown source ${e.source}`);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ----------- resolveHopDepth (D2) -----------
|
|
109
|
+
|
|
110
|
+
test('Test 6: resolveHopDepth({problemType: "WDP", governing_thought: "strong"}) returns depth 1', () => {
|
|
111
|
+
const { resolveHopDepth } = loadProjections();
|
|
112
|
+
const out = resolveHopDepth({ problemType: 'WDP', governing_thought: 'strong' });
|
|
113
|
+
assert.strictEqual(out.depth, 1);
|
|
114
|
+
assert.ok(typeof out.rationale === 'string' && out.rationale.length > 0);
|
|
115
|
+
const r = out.rationale.toLowerCase();
|
|
116
|
+
assert.ok(r.includes('well-defined') || r.includes('execution'),
|
|
117
|
+
`rationale mentions well-defined or execution: "${out.rationale}"`);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('Test 7: resolveHopDepth({problemType: "IDP"}) returns depth 2', () => {
|
|
121
|
+
const { resolveHopDepth } = loadProjections();
|
|
122
|
+
const out = resolveHopDepth({ problemType: 'IDP' });
|
|
123
|
+
assert.strictEqual(out.depth, 2);
|
|
124
|
+
assert.ok(typeof out.rationale === 'string' && out.rationale.length > 0);
|
|
125
|
+
const r = out.rationale.toLowerCase();
|
|
126
|
+
assert.ok(r.includes('ill-defined') || r.includes('evolving') || r.includes('exploratory'),
|
|
127
|
+
`rationale mentions ill-defined / evolving / exploratory: "${out.rationale}"`);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('Test 8: resolveHopDepth on unknown problemType returns depth 3 (default WIDE)', () => {
|
|
131
|
+
const { resolveHopDepth } = loadProjections();
|
|
132
|
+
const out = resolveHopDepth({ problemType: 'WDP-but-actually-unknown' });
|
|
133
|
+
assert.strictEqual(out.depth, 3);
|
|
134
|
+
assert.ok(typeof out.rationale === 'string' && out.rationale.length > 0);
|
|
135
|
+
const r = out.rationale.toLowerCase();
|
|
136
|
+
assert.ok(r.includes('wicked') || r.includes('no anchor') || r.includes('default') || r.includes('ambiguity'),
|
|
137
|
+
`rationale mentions wicked / no anchor / default / ambiguity: "${out.rationale}"`);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('Test 9: resolveHopDepth({}) returns {depth: 3, ...} (default WIDE on ambiguity per D2)', () => {
|
|
141
|
+
const { resolveHopDepth } = loadProjections();
|
|
142
|
+
const out = resolveHopDepth({});
|
|
143
|
+
assert.strictEqual(out.depth, 3);
|
|
144
|
+
assert.ok(typeof out.rationale === 'string' && out.rationale.length > 0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('Test 9b: resolveHopDepth(null/undefined) returns depth 3', () => {
|
|
148
|
+
const { resolveHopDepth } = loadProjections();
|
|
149
|
+
assert.strictEqual(resolveHopDepth(null).depth, 3);
|
|
150
|
+
assert.strictEqual(resolveHopDepth(undefined).depth, 3);
|
|
151
|
+
assert.strictEqual(resolveHopDepth('not-an-object').depth, 3);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ----------- computeInvestmentLevel (D3) -----------
|
|
155
|
+
|
|
156
|
+
test('Test 10: computeInvestmentLevel({framework_invocations: 0}) returns level 0', () => {
|
|
157
|
+
const { computeInvestmentLevel } = loadProjections();
|
|
158
|
+
const out = computeInvestmentLevel({ framework_invocations: 0 });
|
|
159
|
+
assert.strictEqual(out.level, 0);
|
|
160
|
+
assert.ok(typeof out.label === 'string' && out.label.length > 0);
|
|
161
|
+
assert.strictEqual(out.label, 'fresh room, ranking with Brain priors');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('Test 11: computeInvestmentLevel({framework_invocations: 3}) returns level 0.3 label includes Brain', () => {
|
|
165
|
+
const { computeInvestmentLevel } = loadProjections();
|
|
166
|
+
const out = computeInvestmentLevel({ framework_invocations: 3 });
|
|
167
|
+
assert.strictEqual(out.level, 0.3);
|
|
168
|
+
assert.ok(typeof out.label === 'string' && out.label.length > 0);
|
|
169
|
+
const l = out.label.toLowerCase();
|
|
170
|
+
assert.ok(l.includes('brain') && (l.includes('local signal') || l.includes('local')),
|
|
171
|
+
`label includes Brain and local signal: "${out.label}"`);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('Test 12: computeInvestmentLevel({framework_invocations: 5}) returns level 0.5 balanced', () => {
|
|
175
|
+
const { computeInvestmentLevel } = loadProjections();
|
|
176
|
+
const out = computeInvestmentLevel({ framework_invocations: 5 });
|
|
177
|
+
assert.strictEqual(out.level, 0.5);
|
|
178
|
+
assert.ok(typeof out.label === 'string' && out.label.length > 0);
|
|
179
|
+
assert.ok(out.label.toLowerCase().includes('balanced'),
|
|
180
|
+
`label includes 'balanced': "${out.label}"`);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('Test 13: computeInvestmentLevel({framework_invocations: 10}) returns level 1.0 full local', () => {
|
|
184
|
+
const { computeInvestmentLevel } = loadProjections();
|
|
185
|
+
const out = computeInvestmentLevel({ framework_invocations: 10 });
|
|
186
|
+
assert.strictEqual(out.level, 1.0);
|
|
187
|
+
assert.ok(typeof out.label === 'string' && out.label.length > 0);
|
|
188
|
+
const l = out.label.toLowerCase();
|
|
189
|
+
assert.ok(l.includes('full local') || l.includes('full'),
|
|
190
|
+
`label includes 'full local' or 'full': "${out.label}"`);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('Test 14: computeInvestmentLevel({framework_invocations: 15}) caps at 1.0', () => {
|
|
194
|
+
const { computeInvestmentLevel } = loadProjections();
|
|
195
|
+
const out = computeInvestmentLevel({ framework_invocations: 15 });
|
|
196
|
+
assert.strictEqual(out.level, 1.0);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('Test 15: computeInvestmentLevel({}) returns level 0 (cold-start with no counter)', () => {
|
|
200
|
+
const { computeInvestmentLevel } = loadProjections();
|
|
201
|
+
const out = computeInvestmentLevel({});
|
|
202
|
+
assert.strictEqual(out.level, 0);
|
|
203
|
+
assert.ok(typeof out.label === 'string' && out.label.length > 0);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('Test 15b: computeInvestmentLevel(null/undefined) returns level 0', () => {
|
|
207
|
+
const { computeInvestmentLevel } = loadProjections();
|
|
208
|
+
assert.strictEqual(computeInvestmentLevel(null).level, 0);
|
|
209
|
+
assert.strictEqual(computeInvestmentLevel(undefined).level, 0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('Test 15c: computeInvestmentLevel handles negative input by clamping to 0', () => {
|
|
213
|
+
const { computeInvestmentLevel } = loadProjections();
|
|
214
|
+
const out = computeInvestmentLevel({ framework_invocations: -3 });
|
|
215
|
+
assert.strictEqual(out.level, 0);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ----------- Purity invariant (Test 16) -----------
|
|
219
|
+
|
|
220
|
+
test('Test 16: All three functions are pure -- twice with identical input -> deepEqual result', () => {
|
|
221
|
+
const { resolveActiveFrameworks, resolveHopDepth, computeInvestmentLevel } = loadProjections();
|
|
222
|
+
const roomState = {
|
|
223
|
+
problemType: 'IDP',
|
|
224
|
+
governing_thought: 'Use SWOT to assess the venture',
|
|
225
|
+
activeJtbd: 'find-problem',
|
|
226
|
+
brainAnchors: ['Porter Five Forces', 'Wardley Map'],
|
|
227
|
+
recentFrameworks: ['Cynefin'],
|
|
228
|
+
framework_invocations: 4,
|
|
229
|
+
};
|
|
230
|
+
const a1 = resolveActiveFrameworks(roomState);
|
|
231
|
+
const a2 = resolveActiveFrameworks(roomState);
|
|
232
|
+
assert.deepStrictEqual(a1, a2, 'resolveActiveFrameworks is pure');
|
|
233
|
+
|
|
234
|
+
const h1 = resolveHopDepth(roomState);
|
|
235
|
+
const h2 = resolveHopDepth(roomState);
|
|
236
|
+
assert.deepStrictEqual(h1, h2, 'resolveHopDepth is pure');
|
|
237
|
+
|
|
238
|
+
const c1 = computeInvestmentLevel(roomState);
|
|
239
|
+
const c2 = computeInvestmentLevel(roomState);
|
|
240
|
+
assert.deepStrictEqual(c1, c2, 'computeInvestmentLevel is pure');
|
|
241
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 125-00 -- writeEdge chokepoint primitive tests.
|
|
3
|
+
// Adds the writeEdge edge-write surface to lib/core/navigation.cjs (the closed
|
|
4
|
+
// chokepoint per Phase 109). Mirrors the Phase 110-03 logMemoryEvent additive
|
|
5
|
+
// re-export precedent.
|
|
6
|
+
//
|
|
7
|
+
// Per Plan 125-00 frontmatter <behavior>: 9 scenarios -- happy-path (DEFERRED),
|
|
8
|
+
// allowlist enforcement, REJECTED edge shape, missing source_id, missing
|
|
9
|
+
// target_id, non-serializable properties, UPSERT idempotency, write-isolation
|
|
10
|
+
// across handles, and the extensibility-Set membership check.
|
|
11
|
+
//
|
|
12
|
+
// CRITICAL: this test REQUIRES navigation.cjs (the chokepoint) -- NOT the
|
|
13
|
+
// internal edges.cjs -- to prove the Task 2 re-export wiring is intact.
|
|
14
|
+
|
|
15
|
+
const { test } = require('node:test');
|
|
16
|
+
const { ok, equal } = require('node:assert/strict');
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const os = require('node:os');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
|
|
21
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
22
|
+
const { openRoomDb } = require(path.join(REPO_ROOT, 'lib', 'core', 'room-db.cjs'));
|
|
23
|
+
const navigation = require(path.join(REPO_ROOT, 'lib', 'core', 'navigation.cjs'));
|
|
24
|
+
const edgesInternal = require(path.join(REPO_ROOT, 'lib', 'core', 'navigation', 'edges.cjs'));
|
|
25
|
+
|
|
26
|
+
function freshDb() {
|
|
27
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'p125-00-writeedge-'));
|
|
28
|
+
const db = openRoomDb(dir);
|
|
29
|
+
return { dir, db };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The shipped edges table has FK constraints on (source, target) -> nodes(id),
|
|
33
|
+
// per lib/core/lazygraph-ops.cjs initSchema(). Tests must insert anchor nodes
|
|
34
|
+
// before writeEdge, mirroring the fixture pattern in
|
|
35
|
+
// tests/test-navigation-packet-builder.cjs (Phase 109-07).
|
|
36
|
+
function seedAnchorNode(db, id, type) {
|
|
37
|
+
const nowMs = Date.now();
|
|
38
|
+
db.prepare(
|
|
39
|
+
"INSERT OR IGNORE INTO nodes (id, type, properties, source_path, created_by, confidence, review_status, created_at, last_seen_at) " +
|
|
40
|
+
"VALUES (?, ?, '{}', 'p125-00-fixture', 'system', NULL, 'confirmed', ?, ?)"
|
|
41
|
+
).run(id, type, nowMs, nowMs);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test('Test 1: happy-path DEFERRED edge writes row with full properties', () => {
|
|
45
|
+
const { db } = freshDb();
|
|
46
|
+
seedAnchorNode(db, 'cmd:beautiful-question', 'command');
|
|
47
|
+
seedAnchorNode(db, 'framework:Beautiful Question Framework', 'framework');
|
|
48
|
+
const r = navigation.writeEdge(db, {
|
|
49
|
+
source_id: 'cmd:beautiful-question',
|
|
50
|
+
target_id: 'framework:Beautiful Question Framework',
|
|
51
|
+
edge_type: 'DEFERRED',
|
|
52
|
+
properties: {
|
|
53
|
+
reason: 'not now',
|
|
54
|
+
decision_id: 'memory_event:xyz',
|
|
55
|
+
expires_at: '2026-06-12T00:00:00Z',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
ok(r.ok, 'writeEdge should return ok:true on valid input');
|
|
59
|
+
equal(r.type, 'DEFERRED');
|
|
60
|
+
equal(r.source, 'cmd:beautiful-question');
|
|
61
|
+
equal(r.target, 'framework:Beautiful Question Framework');
|
|
62
|
+
ok(typeof r.edge_id === 'string' && r.edge_id.startsWith('edge:DEFERRED:'), 'edge_id should be a non-empty string with edge:DEFERRED: prefix');
|
|
63
|
+
|
|
64
|
+
const row = db.prepare(
|
|
65
|
+
"SELECT properties FROM edges WHERE source = ? AND target = ? AND type = ?"
|
|
66
|
+
).get('cmd:beautiful-question', 'framework:Beautiful Question Framework', 'DEFERRED');
|
|
67
|
+
ok(row, 'row should be present after writeEdge');
|
|
68
|
+
const props = JSON.parse(row.properties);
|
|
69
|
+
equal(props.reason, 'not now');
|
|
70
|
+
equal(props.decision_id, 'memory_event:xyz');
|
|
71
|
+
equal(props.expires_at, '2026-06-12T00:00:00Z');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('Test 2: edge_type allowlist enforcement rejects unknown types', () => {
|
|
75
|
+
const { db } = freshDb();
|
|
76
|
+
const r = navigation.writeEdge(db, {
|
|
77
|
+
source_id: 'cmd:x',
|
|
78
|
+
target_id: 'framework:Y',
|
|
79
|
+
edge_type: 'NOT_A_REAL_TYPE',
|
|
80
|
+
properties: {},
|
|
81
|
+
});
|
|
82
|
+
equal(r.ok, false);
|
|
83
|
+
equal(r.reason, 'invalid_edge_type');
|
|
84
|
+
equal(r.detail, 'NOT_A_REAL_TYPE');
|
|
85
|
+
// Verify no row was inserted.
|
|
86
|
+
const row = db.prepare(
|
|
87
|
+
"SELECT 1 FROM edges WHERE source = ? AND target = ? AND type = ?"
|
|
88
|
+
).get('cmd:x', 'framework:Y', 'NOT_A_REAL_TYPE');
|
|
89
|
+
equal(row, undefined, 'no row should be inserted on rejected edge_type');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('Test 3: REJECTED edge writes row without expires_at', () => {
|
|
93
|
+
const { db } = freshDb();
|
|
94
|
+
seedAnchorNode(db, 'cmd:beautiful-question', 'command');
|
|
95
|
+
seedAnchorNode(db, 'framework:Beautiful Question Framework', 'framework');
|
|
96
|
+
const r = navigation.writeEdge(db, {
|
|
97
|
+
source_id: 'cmd:beautiful-question',
|
|
98
|
+
target_id: 'framework:Beautiful Question Framework',
|
|
99
|
+
edge_type: 'REJECTED',
|
|
100
|
+
properties: { reason: 'wrong approach', decision_id: 'memory_event:abc' },
|
|
101
|
+
});
|
|
102
|
+
ok(r.ok);
|
|
103
|
+
equal(r.type, 'REJECTED');
|
|
104
|
+
const row = db.prepare(
|
|
105
|
+
"SELECT properties FROM edges WHERE source = ? AND target = ? AND type = ?"
|
|
106
|
+
).get('cmd:beautiful-question', 'framework:Beautiful Question Framework', 'REJECTED');
|
|
107
|
+
ok(row);
|
|
108
|
+
const props = JSON.parse(row.properties);
|
|
109
|
+
equal(props.reason, 'wrong approach');
|
|
110
|
+
equal(props.decision_id, 'memory_event:abc');
|
|
111
|
+
equal(Object.prototype.hasOwnProperty.call(props, 'expires_at'), false, 'REJECTED edge should not carry expires_at');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('Test 4: missing source_id rejected with invalid_source_id', () => {
|
|
115
|
+
const { db } = freshDb();
|
|
116
|
+
const r = navigation.writeEdge(db, {
|
|
117
|
+
source_id: '',
|
|
118
|
+
target_id: 'framework:X',
|
|
119
|
+
edge_type: 'DEFERRED',
|
|
120
|
+
properties: {},
|
|
121
|
+
});
|
|
122
|
+
equal(r.ok, false);
|
|
123
|
+
equal(r.reason, 'invalid_source_id');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('Test 5: missing target_id rejected with invalid_target_id', () => {
|
|
127
|
+
const { db } = freshDb();
|
|
128
|
+
const r = navigation.writeEdge(db, {
|
|
129
|
+
source_id: 'cmd:x',
|
|
130
|
+
target_id: null,
|
|
131
|
+
edge_type: 'DEFERRED',
|
|
132
|
+
properties: {},
|
|
133
|
+
});
|
|
134
|
+
equal(r.ok, false);
|
|
135
|
+
equal(r.reason, 'invalid_target_id');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('Test 6: non-serializable properties rejected with properties_serialize_failed', () => {
|
|
139
|
+
const { db } = freshDb();
|
|
140
|
+
const circular = {};
|
|
141
|
+
circular.self = circular;
|
|
142
|
+
const r = navigation.writeEdge(db, {
|
|
143
|
+
source_id: 'cmd:x',
|
|
144
|
+
target_id: 'framework:Y',
|
|
145
|
+
edge_type: 'DEFERRED',
|
|
146
|
+
properties: circular,
|
|
147
|
+
});
|
|
148
|
+
equal(r.ok, false);
|
|
149
|
+
equal(r.reason, 'properties_serialize_failed');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('Test 7: UPSERT idempotency -- second write with different props replaces row', () => {
|
|
153
|
+
const { db } = freshDb();
|
|
154
|
+
seedAnchorNode(db, 'cmd:same', 'command');
|
|
155
|
+
seedAnchorNode(db, 'framework:Same', 'framework');
|
|
156
|
+
const r1 = navigation.writeEdge(db, {
|
|
157
|
+
source_id: 'cmd:same',
|
|
158
|
+
target_id: 'framework:Same',
|
|
159
|
+
edge_type: 'DEFERRED',
|
|
160
|
+
properties: { reason: 'first', decision_id: 'memory_event:1' },
|
|
161
|
+
});
|
|
162
|
+
ok(r1.ok);
|
|
163
|
+
const r2 = navigation.writeEdge(db, {
|
|
164
|
+
source_id: 'cmd:same',
|
|
165
|
+
target_id: 'framework:Same',
|
|
166
|
+
edge_type: 'DEFERRED',
|
|
167
|
+
properties: { reason: 'second', decision_id: 'memory_event:2' },
|
|
168
|
+
});
|
|
169
|
+
ok(r2.ok);
|
|
170
|
+
const rows = db.prepare(
|
|
171
|
+
"SELECT properties FROM edges WHERE source = ? AND target = ? AND type = ?"
|
|
172
|
+
).all('cmd:same', 'framework:Same', 'DEFERRED');
|
|
173
|
+
equal(rows.length, 1, 'UPSERT must keep exactly one row for (source, target, type)');
|
|
174
|
+
const props = JSON.parse(rows[0].properties);
|
|
175
|
+
equal(props.reason, 'second');
|
|
176
|
+
equal(props.decision_id, 'memory_event:2');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('Test 8: write-isolation -- second handle to same path sees the row', () => {
|
|
180
|
+
const { dir, db } = freshDb();
|
|
181
|
+
seedAnchorNode(db, 'cmd:iso', 'command');
|
|
182
|
+
seedAnchorNode(db, 'framework:Iso', 'framework');
|
|
183
|
+
const r = navigation.writeEdge(db, {
|
|
184
|
+
source_id: 'cmd:iso',
|
|
185
|
+
target_id: 'framework:Iso',
|
|
186
|
+
edge_type: 'DEFERRED',
|
|
187
|
+
properties: { reason: 'isolation-test', decision_id: 'memory_event:iso' },
|
|
188
|
+
});
|
|
189
|
+
ok(r.ok);
|
|
190
|
+
// Open a second handle to the same room dir; the WAL-mode DB must surface the row.
|
|
191
|
+
const db2 = openRoomDb(dir);
|
|
192
|
+
const row = db2.prepare(
|
|
193
|
+
"SELECT properties FROM edges WHERE source = ? AND target = ? AND type = ?"
|
|
194
|
+
).get('cmd:iso', 'framework:Iso', 'DEFERRED');
|
|
195
|
+
ok(row, 'second handle must see the row -- no stuck transaction');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('Test 9: ALLOWED_EDGE_TYPES Set membership (extensibility check)', () => {
|
|
199
|
+
// Plan 125-00 ships DEFERRED + REJECTED for v1. Future Phases 116/117/118
|
|
200
|
+
// extend this Set additively (mirrors EVENT_TYPES Set precedent in
|
|
201
|
+
// memory-events.cjs). Test asserts a FLOOR -- present-and-additive -- not
|
|
202
|
+
// an exact size, so a future phase extension cannot regress this baseline.
|
|
203
|
+
ok(edgesInternal.ALLOWED_EDGE_TYPES instanceof Set, 'ALLOWED_EDGE_TYPES must be a Set');
|
|
204
|
+
ok(edgesInternal.ALLOWED_EDGE_TYPES.has('DEFERRED'), 'DEFERRED must be in v1 allowlist');
|
|
205
|
+
ok(edgesInternal.ALLOWED_EDGE_TYPES.has('REJECTED'), 'REJECTED must be in v1 allowlist');
|
|
206
|
+
});
|