@mindrian_os/install 1.13.0-beta.13 → 1.13.0-beta.16

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.
Files changed (118) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +21 -11
  3. package/README.md +74 -572
  4. package/commands/act.md +1 -0
  5. package/commands/admin.md +1 -0
  6. package/commands/analyze-needs.md +1 -0
  7. package/commands/analyze-systems.md +1 -0
  8. package/commands/analyze-timing.md +1 -0
  9. package/commands/auto-explore.md +1 -0
  10. package/commands/beautiful-question.md +1 -0
  11. package/commands/brain-derive.md +1 -0
  12. package/commands/build-knowledge.md +1 -0
  13. package/commands/build-thesis.md +1 -0
  14. package/commands/causal.md +1 -0
  15. package/commands/challenge-assumptions.md +1 -0
  16. package/commands/compare-ventures.md +1 -0
  17. package/commands/dashboard.md +1 -0
  18. package/commands/deep-grade.md +1 -0
  19. package/commands/diagnose.md +1 -0
  20. package/commands/diagnostics.md +1 -0
  21. package/commands/doctor.md +1 -0
  22. package/commands/dominant-designs.md +1 -0
  23. package/commands/explain-decision.md +1 -0
  24. package/commands/explore-domains.md +1 -0
  25. package/commands/explore-futures.md +1 -0
  26. package/commands/explore-trends.md +1 -0
  27. package/commands/export.md +1 -0
  28. package/commands/feynman-timeline-refresh.md +78 -0
  29. package/commands/file-meeting.md +1 -0
  30. package/commands/find-analogies.md +1 -0
  31. package/commands/find-bottlenecks.md +1 -0
  32. package/commands/find-connections.md +1 -0
  33. package/commands/funding.md +1 -0
  34. package/commands/grade.md +1 -0
  35. package/commands/graph.md +1 -0
  36. package/commands/hat-briefing.md +1 -0
  37. package/commands/heal.md +1 -0
  38. package/commands/help.md +1 -0
  39. package/commands/hmi-status.md +1 -0
  40. package/commands/jtbd.md +1 -0
  41. package/commands/leadership.md +1 -0
  42. package/commands/lean-canvas.md +1 -0
  43. package/commands/macro-trends.md +1 -0
  44. package/commands/map-unknowns.md +1 -0
  45. package/commands/memory.md +1 -0
  46. package/commands/models.md +1 -0
  47. package/commands/mos-reason.md +1 -0
  48. package/commands/mullins.md +1 -0
  49. package/commands/new-project.md +1 -0
  50. package/commands/onboard.md +1 -0
  51. package/commands/operator.md +1 -0
  52. package/commands/opportunities.md +1 -0
  53. package/commands/organize.md +1 -0
  54. package/commands/persona.md +1 -0
  55. package/commands/pipeline.md +1 -0
  56. package/commands/present.md +1 -0
  57. package/commands/publish.md +1 -0
  58. package/commands/query.md +1 -0
  59. package/commands/radar.md +1 -0
  60. package/commands/reanalyze.md +1 -0
  61. package/commands/research.md +1 -0
  62. package/commands/room.md +1 -0
  63. package/commands/rooms.md +1 -0
  64. package/commands/root-cause.md +1 -0
  65. package/commands/rs-experts.md +1 -0
  66. package/commands/rs-explain.md +1 -0
  67. package/commands/rs-fetch.md +1 -0
  68. package/commands/rs-thesis.md +1 -0
  69. package/commands/scenario-plan.md +1 -0
  70. package/commands/scheduled-tasks.md +1 -0
  71. package/commands/score-innovation.md +1 -0
  72. package/commands/scout.md +1 -0
  73. package/commands/setup.md +1 -0
  74. package/commands/snapshot.md +1 -0
  75. package/commands/speakers.md +1 -0
  76. package/commands/splash.md +1 -0
  77. package/commands/status.md +1 -0
  78. package/commands/structure-argument.md +1 -0
  79. package/commands/suggest-next.md +1 -0
  80. package/commands/systems-thinking.md +1 -0
  81. package/commands/think-hats.md +1 -0
  82. package/commands/update.md +1 -0
  83. package/commands/user-needs.md +1 -0
  84. package/commands/validate.md +1 -0
  85. package/commands/value-proposition.md +1 -0
  86. package/commands/vault.md +1 -0
  87. package/commands/visualize.md +1 -0
  88. package/commands/whitespace.md +1 -0
  89. package/commands/wiki.md +1 -0
  90. package/lib/brain/framework-chain-slice.cjs +193 -0
  91. package/lib/core/cache-prune.cjs +114 -8
  92. package/lib/core/feynman/ROOM.md +25 -0
  93. package/lib/core/feynman/timeline-renderer.cjs +197 -0
  94. package/lib/core/feynman/timeline-runner.cjs +281 -0
  95. package/lib/core/install-state.cjs +242 -0
  96. package/lib/core/navigation/edges.cjs +86 -0
  97. package/lib/core/navigation/insights.cjs +37 -0
  98. package/lib/core/navigation/memory-events.cjs +39 -0
  99. package/lib/core/navigation/packet.cjs +89 -9
  100. package/lib/core/navigation/projections.cjs +201 -0
  101. package/lib/core/navigation.cjs +25 -0
  102. package/lib/mcp/larry-server-instructions.md +1 -1
  103. package/lib/memory/brain-cypher-chain-slice.test.cjs +368 -0
  104. package/lib/memory/f-selector-ranker.test.cjs +593 -0
  105. package/lib/memory/navigation-projections.test.cjs +241 -0
  106. package/lib/memory/navigation-write-edge.test.cjs +206 -0
  107. package/lib/memory/packet-chain-hint.test.cjs +407 -0
  108. package/lib/memory/packet-schema-validation.test.cjs +317 -0
  109. package/lib/memory/per-command-jtbd-derivation.test.cjs +130 -0
  110. package/lib/memory/per-command-teaching.test.cjs +110 -0
  111. package/lib/memory/run-feynman-tests.cjs +36 -0
  112. package/lib/memory/selector-decisions.test.cjs +417 -0
  113. package/lib/memory/selector-miss.test.cjs +290 -0
  114. package/lib/workflow/f-selector-ranker.cjs +420 -0
  115. package/lib/workflow/selector-decisions.cjs +368 -0
  116. package/package.json +1 -1
  117. package/references/design/email-template-standard.md +1 -1
  118. package/references/user-research/2026-04-05-leah-lawrence-session.md +3 -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
+ });
@@ -1310,6 +1310,42 @@ const TEST_FILES = [
1310
1310
  path.join(REPO_ROOT, 'tests', 'test-doctor-acceptance.cjs'),
1311
1311
  path.join(REPO_ROOT, 'tests', 'test-cache-prune.cjs'),
1312
1312
  path.join(REPO_ROOT, 'tests', 'test-resolve-brain-key.cjs'),
1313
+ // Phase 124-00: FEYNMAN.md Temporal Awareness Wave 0 substrate (4 stubs filled by Plans 124-01 / 124-02 / 124-04).
1314
+ // test-feynman-timeline-renderer.cjs -> 124-01 (TEMPORAL-124-02 + -04 + -05 + -07: renderer unit + D-05 template + thresholds + section scoping)
1315
+ // test-feynman-timeline-empty-state.cjs -> 124-01 (TEMPORAL-124-04: empty-state placeholder)
1316
+ // test-feynman-timeline-runner.cjs -> 124-02 (TEMPORAL-124-01 + -03 + -08 + -09: sentinel-bounded merge + body byte-identical + watermark + idempotent + memory_event + EVENT_TYPES +2)
1317
+ // test-feynman-timeline-canon-part-9-invariant.cjs -> 124-04 (TEMPORAL-124-10: forbidden-substring sweep + fs-instrument allow-list)
1318
+ path.join(REPO_ROOT, 'tests', 'test-feynman-timeline-renderer.cjs'),
1319
+ path.join(REPO_ROOT, 'tests', 'test-feynman-timeline-empty-state.cjs'),
1320
+ path.join(REPO_ROOT, 'tests', 'test-feynman-timeline-runner.cjs'),
1321
+ path.join(REPO_ROOT, 'tests', 'test-feynman-timeline-canon-part-9-invariant.cjs'),
1322
+ // Phase 125 F-Selector Ranker (8 plans: Plan 00 writeEdge through Plan 07 miss).
1323
+ // navigation-write-edge.test.cjs -> Plan 125-00 (writeEdge primitive)
1324
+ // navigation-projections.test.cjs -> Plan 125-01 (projection helpers)
1325
+ // brain-cypher-chain-slice.test.cjs -> Plan 125-02 (Brain Cypher slice)
1326
+ // packet-chain-hint.test.cjs -> Plan 125-03 (packet builder ext)
1327
+ // packet-schema-validation.test.cjs -> Plan 125-04 (schema superset)
1328
+ // f-selector-ranker.test.cjs -> Plan 125-05 + 125-07 D8 label tests (34 tests)
1329
+ // selector-decisions.test.cjs -> Plan 125-06 (D7 decisions + decay; 17 tests)
1330
+ // selector-miss.test.cjs -> Plan 125-07 (D8 miss capture; 10 tests)
1331
+ // Aggregator: tests/run-all-125.sh.
1332
+ path.join(REPO_ROOT, 'lib', 'memory', 'navigation-write-edge.test.cjs'),
1333
+ path.join(REPO_ROOT, 'lib', 'memory', 'navigation-projections.test.cjs'),
1334
+ path.join(REPO_ROOT, 'lib', 'memory', 'brain-cypher-chain-slice.test.cjs'),
1335
+ path.join(REPO_ROOT, 'lib', 'memory', 'packet-chain-hint.test.cjs'),
1336
+ path.join(REPO_ROOT, 'lib', 'memory', 'packet-schema-validation.test.cjs'),
1337
+ path.join(REPO_ROOT, 'lib', 'memory', 'f-selector-ranker.test.cjs'),
1338
+ path.join(REPO_ROOT, 'lib', 'memory', 'selector-decisions.test.cjs'),
1339
+ path.join(REPO_ROOT, 'lib', 'memory', 'selector-miss.test.cjs'),
1340
+ // Phase 104.1 (per-command-teaching-content) block. RED-by-design until
1341
+ // Plan 02 lands the 86 teaching strings; the jtbd-derivation suite is
1342
+ // GREEN immediately after Plan 01 Task 1.
1343
+ // per-command-teaching.test.cjs -> 104.1-01 Task 2 (RED today; GREEN after Plan 02)
1344
+ // per-command-jtbd-derivation.test.cjs -> 104.1-01 Task 3 (GREEN immediately)
1345
+ // Aggregator: tests/run-all-104.1.sh. Documented at
1346
+ // .planning/phases/104.1-per-command-teaching-content/104.1-01-PLAN.md.
1347
+ path.join(REPO_ROOT, 'lib', 'memory', 'per-command-teaching.test.cjs'),
1348
+ path.join(REPO_ROOT, 'lib', 'memory', 'per-command-jtbd-derivation.test.cjs'),
1313
1349
  ];
1314
1350
 
1315
1351
  // Exit code convention for child tests: