@mindrian_os/install 1.13.0-beta.12 → 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 +57 -10
- 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 +2 -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 +2 -1
- 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 +2 -1
- 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 +8 -3
- 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/active-plugin-root.cjs +71 -6
- package/lib/core/brain-client.cjs +451 -36
- package/lib/core/cache-prune.cjs +208 -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 +56 -1
- package/lib/core/navigation/neighborhood.cjs +5 -4
- package/lib/core/navigation/packet.cjs +176 -10
- package/lib/core/navigation/projections.cjs +201 -0
- package/lib/core/navigation.cjs +31 -0
- package/lib/core/resolve-brain-key.cjs +201 -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 +121 -0
- package/lib/memory/security-trifecta.test.cjs +23 -6
- 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 +4 -1
- package/references/design/email-template-standard.md +1 -1
- package/references/user-research/2026-04-05-leah-lawrence-session.md +3 -3
- package/skills/brain-connector/SKILL.md +9 -3
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Phase 125-04 -- Schema superset extension validator tests.
|
|
5
|
+
*
|
|
6
|
+
* Adds $defs.FrameworkChainHint and the optional framework_chain_hint property
|
|
7
|
+
* to $defs.LocalGraphSummary.properties. Canon Part 8 invariants preserved:
|
|
8
|
+
* - additionalProperties: false on every object node (leak-prevention teeth)
|
|
9
|
+
* - 12-job D-02 closed-vocabulary UNTOUCHED
|
|
10
|
+
* - LocalGraphSummary.required[] still has its original 6 entries
|
|
11
|
+
* (framework_chain_hint is OPTIONAL -- NOT added to required[])
|
|
12
|
+
*
|
|
13
|
+
* The ajv2020 validator (the same path lib/core/brain-client.cjs uses via
|
|
14
|
+
* _validatorFor + _ensureSchema) picks up the new optional field automatically
|
|
15
|
+
* once the schema file is updated. These tests pin the contract.
|
|
16
|
+
*
|
|
17
|
+
* Test 1 -- hint-absent packet validates (existing Phase 110 packet path)
|
|
18
|
+
* Test 2 -- hint-present packet validates (Plan 03's enriched packet path)
|
|
19
|
+
* Test 3 -- existing required[] preserved (packet missing nearest_claims rejected)
|
|
20
|
+
* Test 4 -- additionalProperties:false on LocalGraphSummary (extra field rejected)
|
|
21
|
+
* Test 5 -- additionalProperties:false on FrameworkChainHint (extra field rejected)
|
|
22
|
+
* Test 6 -- slice_scope enum enforced (4 rejected)
|
|
23
|
+
* Test 7 -- slice_scope enum accepts 1, 2, 3
|
|
24
|
+
* Test 8 -- edges array maxItems 50 enforced (51 rejected)
|
|
25
|
+
* Test 9 -- 12 jobs closed-vocab UNTOUCHED (count match)
|
|
26
|
+
* Test 10 -- each edge requires from + to + hop_distance (missing 'to' rejected)
|
|
27
|
+
* Test 11 -- null tolerance in edges (confidence:null accepted)
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const { test } = require('node:test');
|
|
31
|
+
const { ok, equal } = require('node:assert/strict');
|
|
32
|
+
const fs = require('node:fs');
|
|
33
|
+
const path = require('node:path');
|
|
34
|
+
|
|
35
|
+
const Ajv2020 = require('ajv/dist/2020').default || require('ajv/dist/2020');
|
|
36
|
+
|
|
37
|
+
const SCHEMA_PATH = path.join(__dirname, '..', '..', 'data', 'brain-packet-schema.json');
|
|
38
|
+
const SCHEMA = JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8'));
|
|
39
|
+
|
|
40
|
+
// The 12 D-02 closed-vocab jobs (Canon Part 8 invariant -- Plan 04 MUST NOT
|
|
41
|
+
// touch this list). Mirrors scripts/build-brain-packet-schema.cjs SHIPPED_JOBS
|
|
42
|
+
// and lib/core/brain-client.cjs SHIPPED_JOBS.
|
|
43
|
+
const SHIPPED_JOBS = Object.freeze([
|
|
44
|
+
'select_methodology',
|
|
45
|
+
'suggest_next_move',
|
|
46
|
+
'detect_contradiction',
|
|
47
|
+
'summarize_neighborhood',
|
|
48
|
+
'classify_room_budding',
|
|
49
|
+
'rank_assumptions',
|
|
50
|
+
'generate_feynman_explanation',
|
|
51
|
+
'strengthen_minto',
|
|
52
|
+
'prepare_investor_brief',
|
|
53
|
+
'opportunity_react',
|
|
54
|
+
'opportunity_reflect',
|
|
55
|
+
'opportunity_rank',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
// Compile a validator for one (job, half). Mirrors brain-client.cjs::_validatorFor
|
|
59
|
+
// -- the same wrapper-with-inline-$defs pattern (ajv@8 cannot resolve a deep
|
|
60
|
+
// JSON pointer into a schema indexed only by its $id; the wrapper carries
|
|
61
|
+
// $defs inline).
|
|
62
|
+
function validatorFor(job, half) {
|
|
63
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
64
|
+
return ajv.compile({
|
|
65
|
+
$id: 'urn:mindrian:test:' + job + ':' + half,
|
|
66
|
+
$ref: '#/$defs/' + job + '/' + half,
|
|
67
|
+
$defs: SCHEMA.$defs,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A minimal, valid Phase 110 packet under the suggest_next_move job. Every
|
|
72
|
+
// field that LocalGraphSummary.required[] requires is present with an empty
|
|
73
|
+
// array (per Plan 02 -- empty is valid). The Plan 03 hint is NOT included
|
|
74
|
+
// here; tests that want the hint add it explicitly.
|
|
75
|
+
function basePacket() {
|
|
76
|
+
return {
|
|
77
|
+
packet_version: '1.0',
|
|
78
|
+
job: 'suggest_next_move',
|
|
79
|
+
room_stage: 'unknown',
|
|
80
|
+
origin: 'navigation_api',
|
|
81
|
+
privacy_mode: 'local_summary_only',
|
|
82
|
+
active_context: {
|
|
83
|
+
jtbd: null,
|
|
84
|
+
operator: 'OPERATOR_X',
|
|
85
|
+
focus_node: { id: 'n1', type: 'claim', summary: 'sample focus node' },
|
|
86
|
+
},
|
|
87
|
+
local_graph_summary: {
|
|
88
|
+
nearest_claims: [],
|
|
89
|
+
nearest_assumptions: [],
|
|
90
|
+
contradictions: [],
|
|
91
|
+
unsupported_claims: [],
|
|
92
|
+
recent_changes: [],
|
|
93
|
+
banked_opportunities: { count: 0, items: [] },
|
|
94
|
+
},
|
|
95
|
+
constraints: { privacy: 'no_raw_artifact_text', max_tokens: 1200 },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// A fully populated FrameworkChainHint (per Plan 04 CONTEXT.md Scope IN B item 5).
|
|
100
|
+
function fullHint() {
|
|
101
|
+
return {
|
|
102
|
+
edges: [
|
|
103
|
+
{
|
|
104
|
+
from: 'JTBD',
|
|
105
|
+
to: 'Mullins 7 Domains',
|
|
106
|
+
confidence: 0.82,
|
|
107
|
+
transform_description: 'JTBD anchors customer pull; Mullins stress-tests market attractiveness',
|
|
108
|
+
hop_distance: 1,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
from: 'Mullins 7 Domains',
|
|
112
|
+
to: 'Porter Five Forces',
|
|
113
|
+
confidence: 0.71,
|
|
114
|
+
transform_description: 'Market attractiveness flows into industry structure',
|
|
115
|
+
hop_distance: 2,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
slice_scope: 2,
|
|
119
|
+
slice_rationale: 'ill-defined state with active JTBD; 2-hop slice',
|
|
120
|
+
brain_snapshot_id: 'sha256:b4a1f2c3',
|
|
121
|
+
fetched_at: '2026-05-13T17:00:00Z',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// =============================================================================
|
|
126
|
+
// Test 1 -- hint-absent packet validates (Phase 110 existing path stays GREEN)
|
|
127
|
+
// =============================================================================
|
|
128
|
+
test('packet-schema-validation: hint-absent packet validates', () => {
|
|
129
|
+
const v = validatorFor('suggest_next_move', 'in');
|
|
130
|
+
const valid = v(basePacket());
|
|
131
|
+
ok(valid, 'expected valid; got errors: ' + JSON.stringify(v.errors));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// =============================================================================
|
|
135
|
+
// Test 2 -- hint-present packet validates (Plan 03 enriched path)
|
|
136
|
+
// =============================================================================
|
|
137
|
+
test('packet-schema-validation: hint-present packet validates', () => {
|
|
138
|
+
const v = validatorFor('suggest_next_move', 'in');
|
|
139
|
+
const p = basePacket();
|
|
140
|
+
p.local_graph_summary.framework_chain_hint = fullHint();
|
|
141
|
+
const valid = v(p);
|
|
142
|
+
ok(valid, 'expected valid; got errors: ' + JSON.stringify(v.errors));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// =============================================================================
|
|
146
|
+
// Test 3 -- existing required[] preserved (Canon Part 8 leak-prevention teeth)
|
|
147
|
+
// =============================================================================
|
|
148
|
+
test('packet-schema-validation: missing nearest_claims is rejected', () => {
|
|
149
|
+
const v = validatorFor('suggest_next_move', 'in');
|
|
150
|
+
const p = basePacket();
|
|
151
|
+
delete p.local_graph_summary.nearest_claims;
|
|
152
|
+
const valid = v(p);
|
|
153
|
+
ok(!valid, 'expected rejection');
|
|
154
|
+
const msg = JSON.stringify(v.errors || []);
|
|
155
|
+
ok(/nearest_claims|required/i.test(msg),
|
|
156
|
+
'expected error mentioning nearest_claims or required; got: ' + msg);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// =============================================================================
|
|
160
|
+
// Test 4 -- additionalProperties:false on LocalGraphSummary
|
|
161
|
+
// =============================================================================
|
|
162
|
+
test('packet-schema-validation: extra field in local_graph_summary is rejected', () => {
|
|
163
|
+
const v = validatorFor('suggest_next_move', 'in');
|
|
164
|
+
const p = basePacket();
|
|
165
|
+
p.local_graph_summary.forbidden_field = 'should not be allowed';
|
|
166
|
+
const valid = v(p);
|
|
167
|
+
ok(!valid, 'expected rejection of unknown field in local_graph_summary');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// =============================================================================
|
|
171
|
+
// Test 5 -- additionalProperties:false on FrameworkChainHint
|
|
172
|
+
// =============================================================================
|
|
173
|
+
test('packet-schema-validation: extra field in framework_chain_hint is rejected', () => {
|
|
174
|
+
const v = validatorFor('suggest_next_move', 'in');
|
|
175
|
+
const p = basePacket();
|
|
176
|
+
p.local_graph_summary.framework_chain_hint = fullHint();
|
|
177
|
+
p.local_graph_summary.framework_chain_hint.forbidden_field = 'should not be allowed';
|
|
178
|
+
const valid = v(p);
|
|
179
|
+
ok(!valid, 'expected rejection of unknown field in framework_chain_hint');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// =============================================================================
|
|
183
|
+
// Test 6 -- slice_scope enum enforced (only 1, 2, 3 valid; 4 rejected)
|
|
184
|
+
// =============================================================================
|
|
185
|
+
test('packet-schema-validation: slice_scope=4 is rejected', () => {
|
|
186
|
+
const v = validatorFor('suggest_next_move', 'in');
|
|
187
|
+
const p = basePacket();
|
|
188
|
+
p.local_graph_summary.framework_chain_hint = fullHint();
|
|
189
|
+
p.local_graph_summary.framework_chain_hint.slice_scope = 4;
|
|
190
|
+
const valid = v(p);
|
|
191
|
+
ok(!valid, 'expected rejection of slice_scope=4');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// =============================================================================
|
|
195
|
+
// Test 7 -- slice_scope enum accepts 1, 2, 3
|
|
196
|
+
// =============================================================================
|
|
197
|
+
test('packet-schema-validation: slice_scope=1, 2, 3 all accepted', () => {
|
|
198
|
+
const v = validatorFor('suggest_next_move', 'in');
|
|
199
|
+
for (const scope of [1, 2, 3]) {
|
|
200
|
+
const p = basePacket();
|
|
201
|
+
p.local_graph_summary.framework_chain_hint = fullHint();
|
|
202
|
+
p.local_graph_summary.framework_chain_hint.slice_scope = scope;
|
|
203
|
+
const valid = v(p);
|
|
204
|
+
ok(valid, 'expected slice_scope=' + scope + ' to validate; errors: ' + JSON.stringify(v.errors));
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// =============================================================================
|
|
209
|
+
// Test 8 -- edges array maxItems 50 enforced (51 entries rejected)
|
|
210
|
+
// =============================================================================
|
|
211
|
+
test('packet-schema-validation: edges maxItems 50 enforced (51 rejected)', () => {
|
|
212
|
+
const v = validatorFor('suggest_next_move', 'in');
|
|
213
|
+
const p = basePacket();
|
|
214
|
+
p.local_graph_summary.framework_chain_hint = fullHint();
|
|
215
|
+
const edges = [];
|
|
216
|
+
for (let i = 0; i < 51; i++) {
|
|
217
|
+
edges.push({
|
|
218
|
+
from: 'F' + i,
|
|
219
|
+
to: 'F' + (i + 1),
|
|
220
|
+
confidence: 0.5,
|
|
221
|
+
transform_description: null,
|
|
222
|
+
hop_distance: 1,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
p.local_graph_summary.framework_chain_hint.edges = edges;
|
|
226
|
+
const valid = v(p);
|
|
227
|
+
ok(!valid, 'expected rejection of 51 edges (maxItems=50)');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// =============================================================================
|
|
231
|
+
// Test 9 -- 12 jobs closed-vocab UNTOUCHED (D-02 invariant)
|
|
232
|
+
// =============================================================================
|
|
233
|
+
test('packet-schema-validation: 12 jobs closed-vocab D-02 untouched', () => {
|
|
234
|
+
const defs = SCHEMA.$defs || {};
|
|
235
|
+
for (const job of SHIPPED_JOBS) {
|
|
236
|
+
ok(defs[job], 'expected $def for shipped job ' + job + ' to exist');
|
|
237
|
+
ok(defs[job].in, 'expected $def "' + job + '" to have an "in" sub-schema');
|
|
238
|
+
ok(defs[job].out, 'expected $def "' + job + '" to have an "out" sub-schema');
|
|
239
|
+
const jobConst = defs[job].in
|
|
240
|
+
&& defs[job].in.properties
|
|
241
|
+
&& defs[job].in.properties.job
|
|
242
|
+
&& defs[job].in.properties.job.const;
|
|
243
|
+
equal(jobConst, job,
|
|
244
|
+
'expected $def "' + job + '" in.properties.job.const to equal ' + job);
|
|
245
|
+
}
|
|
246
|
+
// Count the jobs by detecting which $defs have an "in" sub-schema (the
|
|
247
|
+
// per-job marker). Must be exactly 12 -- no additions allowed.
|
|
248
|
+
const jobCount = Object.keys(defs).filter((k) => defs[k] && defs[k].in).length;
|
|
249
|
+
equal(jobCount, 12,
|
|
250
|
+
'expected exactly 12 job $defs (D-02 closed vocabulary); got ' + jobCount);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// =============================================================================
|
|
254
|
+
// Test 10 -- each edge requires from + to + hop_distance
|
|
255
|
+
// =============================================================================
|
|
256
|
+
test('packet-schema-validation: edge missing "to" is rejected', () => {
|
|
257
|
+
const v = validatorFor('suggest_next_move', 'in');
|
|
258
|
+
const p = basePacket();
|
|
259
|
+
p.local_graph_summary.framework_chain_hint = fullHint();
|
|
260
|
+
// Edge with `to` field omitted entirely (not null -- absent).
|
|
261
|
+
p.local_graph_summary.framework_chain_hint.edges = [
|
|
262
|
+
{
|
|
263
|
+
from: 'JTBD',
|
|
264
|
+
confidence: 0.7,
|
|
265
|
+
transform_description: 'missing to field',
|
|
266
|
+
hop_distance: 1,
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
const valid = v(p);
|
|
270
|
+
ok(!valid, 'expected rejection of edge missing required "to" property');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// =============================================================================
|
|
274
|
+
// Test 11 -- null tolerance in edges (Plan 02 graceful handling)
|
|
275
|
+
// =============================================================================
|
|
276
|
+
test('packet-schema-validation: edge with confidence=null accepted', () => {
|
|
277
|
+
const v = validatorFor('suggest_next_move', 'in');
|
|
278
|
+
const p = basePacket();
|
|
279
|
+
p.local_graph_summary.framework_chain_hint = fullHint();
|
|
280
|
+
p.local_graph_summary.framework_chain_hint.edges = [
|
|
281
|
+
{
|
|
282
|
+
from: 'JTBD',
|
|
283
|
+
to: 'Mullins 7 Domains',
|
|
284
|
+
confidence: null,
|
|
285
|
+
transform_description: null,
|
|
286
|
+
hop_distance: 1,
|
|
287
|
+
},
|
|
288
|
+
];
|
|
289
|
+
const valid = v(p);
|
|
290
|
+
ok(valid, 'expected null confidence to be accepted; errors: ' + JSON.stringify(v.errors));
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// =============================================================================
|
|
294
|
+
// Self-assertion -- preserves the existing 6 LocalGraphSummary.required[]
|
|
295
|
+
// entries and framework_chain_hint is NOT one of them. This is the structural
|
|
296
|
+
// invariant from CONTEXT.md Scope IN B item 6 + the must_haves contract.
|
|
297
|
+
// =============================================================================
|
|
298
|
+
test('packet-schema-validation: LocalGraphSummary.required[] has 6 entries and excludes framework_chain_hint', () => {
|
|
299
|
+
const req = SCHEMA.$defs.LocalGraphSummary.required;
|
|
300
|
+
ok(Array.isArray(req), 'expected required[] array');
|
|
301
|
+
equal(req.length, 6,
|
|
302
|
+
'expected exactly 6 required entries on LocalGraphSummary; got ' + req.length);
|
|
303
|
+
ok(!req.includes('framework_chain_hint'),
|
|
304
|
+
'framework_chain_hint MUST be OPTIONAL -- not in required[]');
|
|
305
|
+
// The original 6 (in CONTEXT.md order):
|
|
306
|
+
for (const f of [
|
|
307
|
+
'nearest_claims',
|
|
308
|
+
'nearest_assumptions',
|
|
309
|
+
'contradictions',
|
|
310
|
+
'unsupported_claims',
|
|
311
|
+
'recent_changes',
|
|
312
|
+
'banked_opportunities',
|
|
313
|
+
]) {
|
|
314
|
+
ok(req.includes(f),
|
|
315
|
+
'expected ' + f + ' to remain in LocalGraphSummary.required[]');
|
|
316
|
+
}
|
|
317
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Phase 104.1 Plan 01 -- per-command-jtbd-derivation test. Asserts that the
|
|
5
|
+
* Phase 104.1 build script extension (scripts/build-command-registry.cjs)
|
|
6
|
+
* correctly derives `jtbd_label` and `jtbd_summary` from
|
|
7
|
+
* `lib/hmi/jtbd-taxonomy.json` via the per-command `serves_jtbd[0]`.
|
|
8
|
+
*
|
|
9
|
+
* Per Canon Part 7 (Reuse Before Build): the taxonomy is the single source
|
|
10
|
+
* of truth for JTBD content. Per-command frontmatter NEVER authors jtbd_label
|
|
11
|
+
* or jtbd_summary -- both are derived at registry-build time.
|
|
12
|
+
*
|
|
13
|
+
* Expected state: GREEN immediately after Plan 01 Task 1 lands (the registry
|
|
14
|
+
* already carries the derived fields; nothing about derivation depends on the
|
|
15
|
+
* teaching content sweep in Plan 02).
|
|
16
|
+
*
|
|
17
|
+
* Registered in:
|
|
18
|
+
* - tests/run-all-104.1.sh (Phase 104.1 scoped aggregator)
|
|
19
|
+
* - lib/memory/run-feynman-tests.cjs TEST_FILES[]
|
|
20
|
+
*
|
|
21
|
+
* Canon Part 8 boundary: this test reads ONLY the local plugin registry +
|
|
22
|
+
* the local taxonomy. Zero network surface, zero Brain calls.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const test = require('node:test');
|
|
26
|
+
const assert = require('node:assert');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
|
|
30
|
+
const registryPath = path.join(__dirname, '..', '..', 'data', 'command-registry.json');
|
|
31
|
+
const taxonomyPath = path.join(__dirname, '..', '..', 'lib', 'hmi', 'jtbd-taxonomy.json');
|
|
32
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
33
|
+
const taxonomy = JSON.parse(fs.readFileSync(taxonomyPath, 'utf8'));
|
|
34
|
+
const taxonomyById = Object.fromEntries(
|
|
35
|
+
(Array.isArray(taxonomy.entries) ? taxonomy.entries : []).map((e) => [e.id, e])
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
test('commands with serves_jtbd have non-null jtbd_label in registry', () => {
|
|
39
|
+
const offenders = registry.commands.filter(
|
|
40
|
+
(c) =>
|
|
41
|
+
Array.isArray(c.serves_jtbd) && c.serves_jtbd.length > 0 && !c.jtbd_label
|
|
42
|
+
);
|
|
43
|
+
assert.equal(
|
|
44
|
+
offenders.length,
|
|
45
|
+
0,
|
|
46
|
+
offenders.length +
|
|
47
|
+
' commands with serves_jtbd missing jtbd_label: ' +
|
|
48
|
+
offenders.slice(0, 3).map((c) => c.command).join(', ')
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('commands with serves_jtbd have non-null jtbd_summary in registry (excluding orphan taxonomy references)', () => {
|
|
53
|
+
// A command's serves_jtbd[0] may reference a JTBD id that is not in the
|
|
54
|
+
// current taxonomy (an orphan). In that case jtbd_summary is null and that
|
|
55
|
+
// is correct (the build script cannot derive a summary it does not have).
|
|
56
|
+
// Real offenders are commands whose serves_jtbd[0] IS in the taxonomy yet
|
|
57
|
+
// still emit a null jtbd_summary -- that is a derivation bug.
|
|
58
|
+
const offenders = registry.commands.filter(
|
|
59
|
+
(c) =>
|
|
60
|
+
Array.isArray(c.serves_jtbd) &&
|
|
61
|
+
c.serves_jtbd.length > 0 &&
|
|
62
|
+
!c.jtbd_summary
|
|
63
|
+
);
|
|
64
|
+
const orphans = offenders.filter((c) => !taxonomyById[c.serves_jtbd[0]]);
|
|
65
|
+
const realOffenders = offenders.filter((c) => taxonomyById[c.serves_jtbd[0]]);
|
|
66
|
+
assert.equal(
|
|
67
|
+
realOffenders.length,
|
|
68
|
+
0,
|
|
69
|
+
realOffenders.length +
|
|
70
|
+
' commands with valid serves_jtbd missing jtbd_summary (' +
|
|
71
|
+
orphans.length +
|
|
72
|
+
' orphan references found separately): ' +
|
|
73
|
+
realOffenders.slice(0, 3).map((c) => c.command).join(', ')
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('jtbd_label matches capitalize+space-replace of serves_jtbd[0]', () => {
|
|
78
|
+
// Spot-check: for a command with serves_jtbd ['find-bottleneck'],
|
|
79
|
+
// jtbd_label should be 'Find Bottleneck'.
|
|
80
|
+
const sample = registry.commands.find(
|
|
81
|
+
(c) => Array.isArray(c.serves_jtbd) && c.serves_jtbd[0] === 'find-bottleneck'
|
|
82
|
+
);
|
|
83
|
+
if (sample) {
|
|
84
|
+
assert.equal(
|
|
85
|
+
sample.jtbd_label,
|
|
86
|
+
'Find Bottleneck',
|
|
87
|
+
"jtbd_label for find-bottleneck should be 'Find Bottleneck', got '" +
|
|
88
|
+
sample.jtbd_label +
|
|
89
|
+
"'"
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
// Generic check: every jtbd_label should match the derivation rule.
|
|
93
|
+
for (const c of registry.commands) {
|
|
94
|
+
if (!Array.isArray(c.serves_jtbd) || c.serves_jtbd.length === 0) continue;
|
|
95
|
+
const id = c.serves_jtbd[0];
|
|
96
|
+
const expected = id
|
|
97
|
+
.split('-')
|
|
98
|
+
.map((w) => (w.length > 0 ? w.charAt(0).toUpperCase() + w.slice(1) : w))
|
|
99
|
+
.join(' ');
|
|
100
|
+
assert.equal(
|
|
101
|
+
c.jtbd_label,
|
|
102
|
+
expected,
|
|
103
|
+
'jtbd_label mismatch for ' +
|
|
104
|
+
c.command +
|
|
105
|
+
' (serves_jtbd[0]=' +
|
|
106
|
+
id +
|
|
107
|
+
"): expected '" +
|
|
108
|
+
expected +
|
|
109
|
+
"', got '" +
|
|
110
|
+
c.jtbd_label +
|
|
111
|
+
"'"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('jtbd_summary matches taxonomy entry.one_line verbatim', () => {
|
|
117
|
+
for (const c of registry.commands) {
|
|
118
|
+
if (!Array.isArray(c.serves_jtbd) || c.serves_jtbd.length === 0) continue;
|
|
119
|
+
const id = c.serves_jtbd[0];
|
|
120
|
+
const entry = taxonomyById[id];
|
|
121
|
+
if (!entry) continue; // orphan reference -- separate concern.
|
|
122
|
+
assert.equal(
|
|
123
|
+
c.jtbd_summary,
|
|
124
|
+
entry.one_line,
|
|
125
|
+
'jtbd_summary mismatch for ' +
|
|
126
|
+
c.command +
|
|
127
|
+
': expected verbatim taxonomy one_line'
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Phase 104.1 Plan 01 -- per-command-teaching presence + length + no-em-dash
|
|
5
|
+
* test. Mirrors the Phase 104 per-command-serves_jtbd test pattern: read
|
|
6
|
+
* data/command-registry.json, assert content invariants on every command.
|
|
7
|
+
*
|
|
8
|
+
* Initial state (Plan 01 RED): no command frontmatter has `teaching:` yet.
|
|
9
|
+
* - Test 1 (presence) FAILS by design -- Plan 02 fills 86 teaching strings.
|
|
10
|
+
* - Tests 2-4 (length / no-em-dash / sentence-count) PASS vacuously
|
|
11
|
+
* because they filter on `c.teaching` (null short-circuits to empty).
|
|
12
|
+
*
|
|
13
|
+
* GREEN state (after Plan 02 lands content): all 4 tests pass.
|
|
14
|
+
*
|
|
15
|
+
* Registered in:
|
|
16
|
+
* - tests/run-all-104.1.sh (Phase 104.1 scoped aggregator)
|
|
17
|
+
* - lib/memory/run-feynman-tests.cjs TEST_FILES[]
|
|
18
|
+
*
|
|
19
|
+
* Constraints enforced (from .planning/phases/104.1-per-command-teaching-content/104.1-CONTEXT.md D2):
|
|
20
|
+
* - non-empty teaching field on every command
|
|
21
|
+
* - 50-300 characters
|
|
22
|
+
* - no em-dashes (U+2014); double-hyphens `--` and hyphens `-` are fine
|
|
23
|
+
* - 1-2 sentences (terminal `.` / `!` / `?` count)
|
|
24
|
+
*
|
|
25
|
+
* Canon Part 8 boundary: this test reads ONLY the local plugin registry
|
|
26
|
+
* (data/command-registry.json). Zero network surface, zero Brain calls.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const test = require('node:test');
|
|
30
|
+
const assert = require('node:assert');
|
|
31
|
+
const path = require('path');
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
|
|
34
|
+
const registryPath = path.join(__dirname, '..', '..', 'data', 'command-registry.json');
|
|
35
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
36
|
+
|
|
37
|
+
test('every command has a non-empty teaching field', () => {
|
|
38
|
+
const missing = registry.commands.filter(
|
|
39
|
+
(c) => !c.teaching || c.teaching.length === 0
|
|
40
|
+
);
|
|
41
|
+
if (missing.length > 0) {
|
|
42
|
+
// RED until Plan 02 fills content. Print which commands are missing
|
|
43
|
+
// teaching field for diagnostic visibility (the WARNING tripwire in
|
|
44
|
+
// scripts/build-command-registry.cjs --check uses the same shape).
|
|
45
|
+
console.log(
|
|
46
|
+
'Missing teaching field on ' +
|
|
47
|
+
missing.length +
|
|
48
|
+
' commands: ' +
|
|
49
|
+
missing.map((c) => c.command).join(', ')
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
assert.equal(
|
|
53
|
+
missing.length,
|
|
54
|
+
0,
|
|
55
|
+
missing.length +
|
|
56
|
+
' of ' +
|
|
57
|
+
registry.commands.length +
|
|
58
|
+
' commands missing teaching field'
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('every teaching string is 50-300 characters', () => {
|
|
63
|
+
const offenders = registry.commands.filter(
|
|
64
|
+
(c) => c.teaching && (c.teaching.length < 50 || c.teaching.length > 300)
|
|
65
|
+
);
|
|
66
|
+
assert.equal(
|
|
67
|
+
offenders.length,
|
|
68
|
+
0,
|
|
69
|
+
offenders.length +
|
|
70
|
+
' teaching strings outside 50-300 char range: ' +
|
|
71
|
+
offenders
|
|
72
|
+
.slice(0, 3)
|
|
73
|
+
.map((c) => c.command + '(' + c.teaching.length + ')')
|
|
74
|
+
.join(', ')
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('no teaching string contains em-dashes (project no-em-dash rule)', () => {
|
|
79
|
+
// U+2014 EM DASH only. Allowed: U+002D HYPHEN-MINUS (single or doubled).
|
|
80
|
+
const emDashRegex = /—/;
|
|
81
|
+
const offenders = registry.commands.filter(
|
|
82
|
+
(c) => c.teaching && emDashRegex.test(c.teaching)
|
|
83
|
+
);
|
|
84
|
+
assert.equal(
|
|
85
|
+
offenders.length,
|
|
86
|
+
0,
|
|
87
|
+
offenders.length +
|
|
88
|
+
' teaching strings contain em-dashes: ' +
|
|
89
|
+
offenders.slice(0, 3).map((c) => c.command).join(', ')
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('every teaching string is 1-2 sentences', () => {
|
|
94
|
+
// Heuristic: count terminal punctuation (. ! ?) followed by whitespace or
|
|
95
|
+
// end-of-string. Common abbreviations may double-count -- worth refining if
|
|
96
|
+
// false positives surface during Plan 02 content review.
|
|
97
|
+
const sentenceCountRegex = /[.!?](?:\s|$)/g;
|
|
98
|
+
const offenders = registry.commands.filter((c) => {
|
|
99
|
+
if (!c.teaching) return false;
|
|
100
|
+
const count = (c.teaching.match(sentenceCountRegex) || []).length;
|
|
101
|
+
return count > 2 || count < 1;
|
|
102
|
+
});
|
|
103
|
+
assert.equal(
|
|
104
|
+
offenders.length,
|
|
105
|
+
0,
|
|
106
|
+
offenders.length +
|
|
107
|
+
' teaching strings not 1-2 sentences: ' +
|
|
108
|
+
offenders.slice(0, 3).map((c) => c.command).join(', ')
|
|
109
|
+
);
|
|
110
|
+
});
|