@mindrian_os/install 1.13.0-beta.11 → 1.13.0-beta.12
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 +26 -3
- package/bin/cli.js +114 -57
- package/commands/act.md +16 -2
- package/commands/pipeline.md +16 -1
- package/commands/suggest-next.md +17 -3
- package/lib/core/active-plugin-root.cjs +142 -0
- package/lib/core/framework-chain-composer.cjs +156 -43
- package/lib/core/migrations/phase-109-nodes-provenance.cjs +47 -0
- package/lib/hmi/jtbd-taxonomy.json +2 -1
- package/lib/memory/framework-chain-composer.test.cjs +54 -20
- package/lib/memory/navigation-hook-resolver.test.cjs +177 -0
- package/lib/memory/run-feynman-tests.cjs +17 -0
- package/lib/memory/suggest-next-workflow.test.cjs +176 -0
- package/lib/memory/workflow-layer-e2e.test.cjs +262 -0
- package/lib/workflow/ROOM.md +1 -1
- package/package.json +1 -1
- package/references/brain/command-triggers-schema.md +10 -221
- package/references/methodology/index.md +11 -74
- package/skills/brain-connector/SKILL.md +3 -5
- package/skills/pws-methodology/SKILL.md +7 -5
|
@@ -12,6 +12,31 @@
|
|
|
12
12
|
* an engine.offer_next_step candidate consumed by the navigation engine
|
|
13
13
|
* (Plan 91-00) and rendered through the offer presenter (Plan 91-04).
|
|
14
14
|
*
|
|
15
|
+
* Phase 122-04 -- routed through the resolver (the only door)
|
|
16
|
+
* ==========================================================
|
|
17
|
+
* proposeNextFramework() now resolves the next framework's /mos: command
|
|
18
|
+
* via lib/workflow/command-resolver.cjs commandsForFramework() -- the SOLE
|
|
19
|
+
* deterministic framework -> command path, reading only the generated
|
|
20
|
+
* data/command-registry.json. When the registry has no command for that
|
|
21
|
+
* framework, command degrades to null ("no /mos: for [framework] yet" --
|
|
22
|
+
* degrade, not fabricate per WORKFLOW-LAYER-SPEC reliability rule 5); the
|
|
23
|
+
* offer presenter already treats a null/empty command as not-an-offer.
|
|
24
|
+
* proposeNextFramework also returns a workflow field -- the resolver's
|
|
25
|
+
* composeWorkflow([completed, next, ...successors]) array -- so a multi-hop
|
|
26
|
+
* FEEDS_INTO chain is available as data on the proposal (a future plan can
|
|
27
|
+
* carry it into offer_next_step / shape-f1-renderer; this plan only puts
|
|
28
|
+
* the data there). mapFrameworkToCommandSlug() relies solely on the
|
|
29
|
+
* resolver (then FALLBACK_COMMAND_SLUG) so any remaining caller also gets
|
|
30
|
+
* the resolver answer.
|
|
31
|
+
*
|
|
32
|
+
* Phase 122-05 -- the residual map pruned
|
|
33
|
+
* =======================================
|
|
34
|
+
* FRAMEWORK_TO_COMMAND_SLUG is now Object.freeze({}) -- the resolver is the
|
|
35
|
+
* ONLY framework-to-command door (data/command-registry.json, generated from
|
|
36
|
+
* frontmatter; WORKFLOW-LAYER-SPEC reliability rule 1). The empty table is
|
|
37
|
+
* kept only as a back-compat export. KNOWN_FRAMEWORKS stays exported as a
|
|
38
|
+
* name-recognition bootstrap (it is NOT the framework-to-command source).
|
|
39
|
+
*
|
|
15
40
|
* Canon Part 2 Engine 1 + Appendix E:
|
|
16
41
|
* Framework chains power Act 1 -> BONO Orchestration handoffs.
|
|
17
42
|
* FEEDS_INTO is the Brain-flagged graph infrastructure (~40 edges in
|
|
@@ -49,6 +74,11 @@
|
|
|
49
74
|
const fs = require('node:fs');
|
|
50
75
|
const path = require('node:path');
|
|
51
76
|
|
|
77
|
+
// The resolver (the only door). Required lazily inside the functions that
|
|
78
|
+
// need it so a missing module never crashes a caller that does not use the
|
|
79
|
+
// resolver path -- but it is an in-repo sibling so this never fails in
|
|
80
|
+
// practice. Reads only data/command-registry.json; never touches the Brain.
|
|
81
|
+
|
|
52
82
|
// ---------- Frozen constants ----------
|
|
53
83
|
|
|
54
84
|
// Confidence gates per locked decision in PLAN frontmatter:
|
|
@@ -63,12 +93,14 @@ const RECOMMENDED_FLOOR = 0.7;
|
|
|
63
93
|
// plans may layer richer signal on top).
|
|
64
94
|
const RECENT_WRITE_WINDOW_MS = 5 * 60 * 1000;
|
|
65
95
|
|
|
66
|
-
// Bootstrap KNOWN_FRAMEWORKS list.
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
96
|
+
// Bootstrap KNOWN_FRAMEWORKS list. NAME-RECOGNITION BOOTSTRAP ONLY -- this is
|
|
97
|
+
// NOT the framework-to-command source (that is OWNED by lib/workflow/
|
|
98
|
+
// command-resolver.cjs, reading the generated data/command-registry.json).
|
|
99
|
+
// detectCompletedFramework() uses this list to recognize a framework name in a
|
|
100
|
+
// governing thought / a filed-artifact slug; the list is conservative; a
|
|
101
|
+
// Brain-derived FEEDS_INTO edge may reference a framework outside this set, in
|
|
102
|
+
// which case completion detection falls through to the mtime slug fallback.
|
|
103
|
+
// The list is extensible; future plans may pull from the Brain frameworks
|
|
72
104
|
// catalog directly.
|
|
73
105
|
const KNOWN_FRAMEWORKS = Object.freeze([
|
|
74
106
|
'SWOT Analysis',
|
|
@@ -91,31 +123,14 @@ const KNOWN_FRAMEWORKS = Object.freeze([
|
|
|
91
123
|
'Rich Pictures',
|
|
92
124
|
]);
|
|
93
125
|
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
'value chain analysis': 'beautiful-question',
|
|
103
|
-
'business model canvas': 'beautiful-question',
|
|
104
|
-
'lean canvas': 'lean-canvas',
|
|
105
|
-
'jobs-to-be-done': 'beautiful-question',
|
|
106
|
-
'value proposition canvas': 'beautiful-question',
|
|
107
|
-
'5 whys': 'beautiful-question',
|
|
108
|
-
'first principles': 'beautiful-question',
|
|
109
|
-
'design thinking': 'beautiful-question',
|
|
110
|
-
'blue ocean strategy': 'beautiful-question',
|
|
111
|
-
"innovator's dilemma": 'beautiful-question',
|
|
112
|
-
'7 s framework': 'beautiful-question',
|
|
113
|
-
'balanced scorecard': 'beautiful-question',
|
|
114
|
-
'mullins': 'mullins',
|
|
115
|
-
'beautiful question': 'beautiful-question',
|
|
116
|
-
'soft systems': 'beautiful-question',
|
|
117
|
-
'rich pictures': 'beautiful-question',
|
|
118
|
-
});
|
|
126
|
+
// Framework-to-command mapping is OWNED by lib/workflow/command-resolver.cjs
|
|
127
|
+
// (the generated data/command-registry.json, built from each command's
|
|
128
|
+
// frontmatter -- WORKFLOW-LAYER-SPEC reliability rule 1: a single source of
|
|
129
|
+
// truth, nothing else asserts the mapping). Phase 122-05 pruned this table to
|
|
130
|
+
// an empty Object.freeze({}); it is kept ONLY as an empty back-compat export
|
|
131
|
+
// so any caller that still imports FRAMEWORK_TO_COMMAND_SLUG does not crash.
|
|
132
|
+
// Do NOT add entries here -- declare `frameworks:` in the command frontmatter.
|
|
133
|
+
const FRAMEWORK_TO_COMMAND_SLUG = Object.freeze({});
|
|
119
134
|
|
|
120
135
|
// Default fallback command when the next framework has no known mapping.
|
|
121
136
|
const FALLBACK_COMMAND_SLUG = 'beautiful-question';
|
|
@@ -304,7 +319,12 @@ function detectCompletedFramework(roomDir, sectionPath, reasoning) {
|
|
|
304
319
|
* confidence: number,
|
|
305
320
|
* source: 'FEEDS_INTO',
|
|
306
321
|
* phase_indicator: string|null,
|
|
307
|
-
* command: string,
|
|
322
|
+
* command: string|null, // '/mos:<slug>' from the resolver, or null
|
|
323
|
+
* // when the registry has no command for
|
|
324
|
+
* // `next` yet (degrade, do not fabricate)
|
|
325
|
+
* workflow: Array|null, // resolver.composeWorkflow([completed, next, ...])
|
|
326
|
+
* // -- the multi-hop chain as data; the engine
|
|
327
|
+
* // does not propagate it yet (future plan)
|
|
308
328
|
* reason: string, // grounding text (FEEDS_INTO + Brain + confidence)
|
|
309
329
|
* recommended_eligible: boolean, // true when confidence >= 0.7
|
|
310
330
|
* }
|
|
@@ -353,12 +373,44 @@ function proposeNextFramework(completedFramework, edges) {
|
|
|
353
373
|
// the noise gate (we cannot certify a confidenceless edge).
|
|
354
374
|
if (conf === null || conf < NOISE_FLOOR) return null;
|
|
355
375
|
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
376
|
+
// Resolve next -> /mos: command via the resolver (the only door). The
|
|
377
|
+
// resolver reads only data/command-registry.json; it never touches the
|
|
378
|
+
// Brain. When the registry has no command for `next`, command degrades
|
|
379
|
+
// to null -- "no /mos: for [framework] yet" -- never a fabricated one
|
|
380
|
+
// (WORKFLOW-LAYER-SPEC reliability rule 5). The offer presenter already
|
|
381
|
+
// treats a null/empty command as not-an-offer.
|
|
382
|
+
let resolver = null;
|
|
383
|
+
try {
|
|
384
|
+
resolver = require('../workflow/command-resolver.cjs');
|
|
385
|
+
} catch (_e) {
|
|
386
|
+
resolver = null;
|
|
387
|
+
}
|
|
388
|
+
let command = null;
|
|
389
|
+
let workflow = null;
|
|
390
|
+
if (resolver) {
|
|
391
|
+
try {
|
|
392
|
+
const cmds = resolver.commandsForFramework(top.to);
|
|
393
|
+
command = (Array.isArray(cmds) && cmds.length > 0) ? cmds[0] : null;
|
|
394
|
+
} catch (_e) {
|
|
395
|
+
command = null;
|
|
396
|
+
}
|
|
397
|
+
// Multi-step path: build the resolver's composeWorkflow for the chain
|
|
398
|
+
// [completedFramework, next, ...further FEEDS_INTO successors up to ~3].
|
|
399
|
+
// This puts the multi-hop chain on the proposal as data; the navigation
|
|
400
|
+
// engine does not propagate `workflow` into offer_next_step in this plan
|
|
401
|
+
// (the presenter / shape-f1-renderer wiring is a future plan's job).
|
|
402
|
+
try {
|
|
403
|
+
const chain = collectForwardChain(completedFramework, edges, 3);
|
|
404
|
+
workflow = resolver.composeWorkflow(chain);
|
|
405
|
+
} catch (_e) {
|
|
406
|
+
workflow = null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
359
409
|
|
|
360
410
|
// Grounding-rule reason: must contain FEEDS_INTO + Brain + the
|
|
361
|
-
// confidence number per Plan 91-04 presenter contract + Test 13.
|
|
411
|
+
// confidence number per Plan 91-04 presenter contract + Test 13. When
|
|
412
|
+
// there is no command for `next`, the reason still names the framework
|
|
413
|
+
// (the consumer prints "run [framework] manually" rather than a command).
|
|
362
414
|
const confStr = conf.toFixed(2);
|
|
363
415
|
const reason =
|
|
364
416
|
completedFramework + ' FEEDS_INTO ' + top.to +
|
|
@@ -370,26 +422,81 @@ function proposeNextFramework(completedFramework, edges) {
|
|
|
370
422
|
source: 'FEEDS_INTO',
|
|
371
423
|
phase_indicator: (typeof top.phase_indicator === 'string') ? top.phase_indicator : null,
|
|
372
424
|
command: command,
|
|
425
|
+
workflow: workflow,
|
|
373
426
|
reason: reason,
|
|
374
427
|
recommended_eligible: conf >= RECOMMENDED_FLOOR,
|
|
375
428
|
};
|
|
376
429
|
}
|
|
377
430
|
|
|
431
|
+
/**
|
|
432
|
+
* collectForwardChain(start, edges, maxHops) -> [start, next, ...]
|
|
433
|
+
*
|
|
434
|
+
* Walks the highest-confidence FEEDS_INTO edge from `start`, then from that
|
|
435
|
+
* successor, etc., up to `maxHops` hops. Cycle-safe (a framework already in
|
|
436
|
+
* the chain stops the walk). Only follows edges that pass the noise floor.
|
|
437
|
+
* Returns at least [start]. Pure: no I/O.
|
|
438
|
+
*
|
|
439
|
+
* @param {string} start
|
|
440
|
+
* @param {Array} edges
|
|
441
|
+
* @param {number} maxHops
|
|
442
|
+
* @returns {string[]}
|
|
443
|
+
*/
|
|
444
|
+
function collectForwardChain(start, edges, maxHops) {
|
|
445
|
+
const chain = [start];
|
|
446
|
+
if (!Array.isArray(edges) || edges.length === 0) return chain;
|
|
447
|
+
const hops = (typeof maxHops === 'number' && maxHops > 0) ? maxHops : 3;
|
|
448
|
+
const seen = new Set([String(start).toLowerCase()]);
|
|
449
|
+
let current = start;
|
|
450
|
+
for (let i = 0; i < hops; i += 1) {
|
|
451
|
+
const cur = String(current).toLowerCase();
|
|
452
|
+
let best = null;
|
|
453
|
+
for (const e of edges) {
|
|
454
|
+
if (!e || typeof e !== 'object') continue;
|
|
455
|
+
if (typeof e.from !== 'string' || typeof e.to !== 'string') continue;
|
|
456
|
+
if (e.from.toLowerCase() !== cur) continue;
|
|
457
|
+
const c = (typeof e.confidence === 'number') ? e.confidence : null;
|
|
458
|
+
if (c === null || c < NOISE_FLOOR) continue;
|
|
459
|
+
if (best === null || c > best.confidence) best = { to: e.to, confidence: c };
|
|
460
|
+
}
|
|
461
|
+
if (best === null) break;
|
|
462
|
+
const nextLc = best.to.toLowerCase();
|
|
463
|
+
if (seen.has(nextLc)) break;
|
|
464
|
+
chain.push(best.to);
|
|
465
|
+
seen.add(nextLc);
|
|
466
|
+
current = best.to;
|
|
467
|
+
}
|
|
468
|
+
return chain;
|
|
469
|
+
}
|
|
470
|
+
|
|
378
471
|
/**
|
|
379
472
|
* mapFrameworkToCommandSlug(name) -> slug
|
|
380
473
|
*
|
|
381
|
-
* Maps a framework name (in any case) to a /mos: command slug via the
|
|
382
|
-
*
|
|
383
|
-
*
|
|
474
|
+
* Maps a framework name (in any case) to a /mos: command slug via the resolver
|
|
475
|
+
* (lib/workflow/command-resolver.cjs commandsForFramework -- the ONLY door,
|
|
476
|
+
* reads only data/command-registry.json, never touches the Brain), falling
|
|
477
|
+
* back to FALLBACK_COMMAND_SLUG ('beautiful-question') when the registry has no
|
|
478
|
+
* command for `name` or the resolver is unavailable. Phase 122-05 removed the
|
|
479
|
+
* legacy in-module table (FRAMEWORK_TO_COMMAND_SLUG is now empty) -- the
|
|
480
|
+
* resolver is authoritative.
|
|
481
|
+
*
|
|
482
|
+
* Note: proposeNextFramework() does NOT use this helper -- it calls
|
|
483
|
+
* commandsForFramework() directly so it can degrade to command:null (the
|
|
484
|
+
* helper keeps a non-null fallback for back-compat with callers that expect
|
|
485
|
+
* a slug string).
|
|
384
486
|
*
|
|
385
487
|
* @param {string} name
|
|
386
488
|
* @returns {string}
|
|
387
489
|
*/
|
|
388
490
|
function mapFrameworkToCommandSlug(name) {
|
|
389
491
|
if (typeof name !== 'string' || name.length === 0) return FALLBACK_COMMAND_SLUG;
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
492
|
+
// Resolver -- the only door.
|
|
493
|
+
try {
|
|
494
|
+
const cmds = require('../workflow/command-resolver.cjs').commandsForFramework(name);
|
|
495
|
+
if (Array.isArray(cmds) && cmds.length > 0) {
|
|
496
|
+
return cmds[0].replace(/^\/mos:/, '');
|
|
497
|
+
}
|
|
498
|
+
} catch (_e) {
|
|
499
|
+
// resolver unavailable -> fall through to the fallback slug
|
|
393
500
|
}
|
|
394
501
|
return FALLBACK_COMMAND_SLUG;
|
|
395
502
|
}
|
|
@@ -400,7 +507,13 @@ module.exports = {
|
|
|
400
507
|
parseFrameworkChainSection: parseFrameworkChainSection,
|
|
401
508
|
detectCompletedFramework: detectCompletedFramework,
|
|
402
509
|
proposeNextFramework: proposeNextFramework,
|
|
403
|
-
|
|
510
|
+
collectForwardChain: collectForwardChain,
|
|
511
|
+
// Exported for back-compat: relies solely on the resolver, then the
|
|
512
|
+
// fallback slug. Phase 122-05 removed the legacy in-module table.
|
|
513
|
+
mapFrameworkToCommandSlug: mapFrameworkToCommandSlug,
|
|
514
|
+
// KNOWN_FRAMEWORKS is a name-recognition bootstrap (NOT the framework-to-
|
|
515
|
+
// command source). FRAMEWORK_TO_COMMAND_SLUG is an EMPTY back-compat export
|
|
516
|
+
// (Phase 122-05) -- the resolver (data/command-registry.json) is the only door.
|
|
404
517
|
KNOWN_FRAMEWORKS: KNOWN_FRAMEWORKS,
|
|
405
518
|
FRAMEWORK_TO_COMMAND_SLUG: FRAMEWORK_TO_COMMAND_SLUG,
|
|
406
519
|
// Constants exposed for invariant tests + downstream callers.
|
|
@@ -242,6 +242,31 @@ function backfillAssumptionsAsGraphNodes(db) {
|
|
|
242
242
|
return db.prepare("SELECT COUNT(*) AS n FROM assumptions").get().n;
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
function dependentSchemaObjects(db) {
|
|
246
|
+
// Enumerate every view and trigger that mentions the legacy `nodes` table in
|
|
247
|
+
// its definition. The SQLite "making other kinds of table schema changes"
|
|
248
|
+
// recipe (the canonical 12-step procedure) requires these to be dropped
|
|
249
|
+
// BEFORE the rename-out-of-existence rebuild and recreated AFTER -- otherwise
|
|
250
|
+
// SQLite re-validates the schema during ALTER TABLE ... RENAME TO, finds the
|
|
251
|
+
// now-dangling view, and throws "error in view <name>: no such table:
|
|
252
|
+
// main.nodes". We do not hardcode rs_discoveries; any future view/trigger on
|
|
253
|
+
// `nodes` is picked up automatically. Drop-then-recreate is idempotent: views
|
|
254
|
+
// whose sql carries IF NOT EXISTS re-exec cleanly; ones without it are simply
|
|
255
|
+
// recreated fresh since we dropped them first.
|
|
256
|
+
const rows = db.prepare(
|
|
257
|
+
"SELECT type, name, sql FROM sqlite_master " +
|
|
258
|
+
"WHERE type IN ('view','trigger') AND sql IS NOT NULL " +
|
|
259
|
+
// \bnodes\b style match: the token "nodes" not immediately followed by an
|
|
260
|
+
// identifier char (so we do not mistakenly catch nodes_new). SQLite LIKE
|
|
261
|
+
// has no word boundaries, so over-match a little and trust the recreate to
|
|
262
|
+
// be a no-op for anything unrelated -- but exclude the obvious nodes_new.
|
|
263
|
+
"AND sql LIKE '%nodes%' AND sql NOT LIKE '%nodes_new%'"
|
|
264
|
+
).all();
|
|
265
|
+
// Defensive: drop NULL/empty sql rows (autogenerated indexes never appear
|
|
266
|
+
// here because we filtered type, but be safe).
|
|
267
|
+
return rows.filter((r) => r && r.name && typeof r.sql === 'string' && r.sql.trim());
|
|
268
|
+
}
|
|
269
|
+
|
|
245
270
|
function tightenSchemaWithCheckConstraints(db) {
|
|
246
271
|
// Step 2: re-create-table-with-NOT-NULL plus CHECK constraints.
|
|
247
272
|
// Canonical SQLite 12-step recipe (foreign_keys disabled for the duration;
|
|
@@ -251,6 +276,19 @@ function tightenSchemaWithCheckConstraints(db) {
|
|
|
251
276
|
// do not flip it here; the BEGIN/COMMIT wrapper guarantees atomicity, and
|
|
252
277
|
// FK behavior is unchanged because the only FK targeting nodes is from edges
|
|
253
278
|
// which we do not drop.
|
|
279
|
+
|
|
280
|
+
// Step 2a: capture and drop every view/trigger that depends on `nodes`. Must
|
|
281
|
+
// happen before DROP TABLE nodes so the schema stays internally consistent
|
|
282
|
+
// through the rename. Recreated verbatim at the end of this function.
|
|
283
|
+
const dependents = dependentSchemaObjects(db);
|
|
284
|
+
for (const obj of dependents) {
|
|
285
|
+
if (obj.type === 'view') {
|
|
286
|
+
db.exec('DROP VIEW IF EXISTS "' + obj.name.replace(/"/g, '""') + '"');
|
|
287
|
+
} else {
|
|
288
|
+
db.exec('DROP TRIGGER IF EXISTS "' + obj.name.replace(/"/g, '""') + '"');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
254
292
|
db.exec(
|
|
255
293
|
"CREATE TABLE nodes_new (" +
|
|
256
294
|
" id TEXT PRIMARY KEY, " +
|
|
@@ -291,6 +329,15 @@ function tightenSchemaWithCheckConstraints(db) {
|
|
|
291
329
|
'CREATE INDEX IF NOT EXISTS idx_nodes_confirmed_by ON nodes(confirmed_by) ' +
|
|
292
330
|
'WHERE confirmed_by IS NOT NULL'
|
|
293
331
|
);
|
|
332
|
+
|
|
333
|
+
// Step 2b: recreate the views/triggers we dropped in Step 2a, now that
|
|
334
|
+
// `nodes` exists again with the tightened schema. The captured `sql` is the
|
|
335
|
+
// exact CREATE statement from sqlite_master; many carry IF NOT EXISTS which
|
|
336
|
+
// keeps the recreate idempotent, and any that do not were dropped above so
|
|
337
|
+
// re-exec is still safe.
|
|
338
|
+
for (const obj of dependents) {
|
|
339
|
+
db.exec(obj.sql);
|
|
340
|
+
}
|
|
294
341
|
}
|
|
295
342
|
|
|
296
343
|
function insertSentinel(db) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
3
|
"canon_parts": [2, "2a", 3, 7],
|
|
4
|
+
"methodology_hooks_note": "informational only; lib/workflow/command-resolver.cjs (reading the generated data/command-registry.json, built from each command's frontmatter -- see docs/COMMAND-FRONTMATTER.md and docs/WORKFLOWS.md) is authoritative for framework-to-command. These hooks are convenience pointers for JTBD-driven routing; they are NOT a source of truth. Larry never names a /mos: from memory.",
|
|
4
5
|
"entries": [
|
|
5
6
|
{
|
|
6
7
|
"id": "decide-pursue",
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
"methodology_hooks": [
|
|
17
18
|
"/mos:diagnose",
|
|
18
19
|
"/mos:build-thesis",
|
|
19
|
-
"/mos:
|
|
20
|
+
"/mos:validate-proposition",
|
|
20
21
|
"/mos:lean-canvas"
|
|
21
22
|
],
|
|
22
23
|
"next_move_verbs": [
|
|
@@ -104,6 +104,17 @@ const SAMPLE_4_LINES = [
|
|
|
104
104
|
'- Jobs-to-be-Done FEEDS_INTO Value Proposition Canvas (confidence: 0.90, phase: pre-opportunity)',
|
|
105
105
|
].join('\n');
|
|
106
106
|
|
|
107
|
+
// Phase 122-04: a chain whose `next` framework IS registered in
|
|
108
|
+
// data/command-registry.json, so proposeNextFramework returns a non-null
|
|
109
|
+
// /mos: command (Tests 16 + 18 need the engine's offer_next_step.command
|
|
110
|
+
// to be a real command string -- only the resolver-registered frameworks
|
|
111
|
+
// yield one; unregistered ones degrade to null per reliability rule 5).
|
|
112
|
+
// 'Business Model Canvas' is in KNOWN_FRAMEWORKS (detectable from a
|
|
113
|
+
// governing thought); 'Lean Canvas' resolves to /mos:lean-canvas.
|
|
114
|
+
const SAMPLE_REGISTERED_CHAIN = [
|
|
115
|
+
'- Business Model Canvas FEEDS_INTO Lean Canvas (confidence: 0.85, phase: thesis-build)',
|
|
116
|
+
].join('\n');
|
|
117
|
+
|
|
107
118
|
// =========================================================
|
|
108
119
|
// Task 1 -- parser + completion detection + proposal (Tests 1..15)
|
|
109
120
|
// =========================================================
|
|
@@ -297,25 +308,43 @@ run('Test 11: proposeNextFramework tie-breaking by confidence desc', () => {
|
|
|
297
308
|
assert.equal(Math.abs(out.confidence - 0.85) < 1e-9, true);
|
|
298
309
|
});
|
|
299
310
|
|
|
300
|
-
run('Test 12: proposeNextFramework
|
|
311
|
+
run('Test 12: proposeNextFramework command resolution via the resolver (Phase 122-04: degrade to null, not fabricate)', () => {
|
|
301
312
|
const { proposeNextFramework } = requireComposer();
|
|
302
|
-
|
|
313
|
+
const resolver = require('../workflow/command-resolver.cjs');
|
|
314
|
+
// 'Lean Canvas' has an existing /mos:lean-canvas command in the registry.
|
|
303
315
|
const knownEdges = [
|
|
304
316
|
{ from: 'Business Model Canvas', to: 'Lean Canvas', confidence: 0.82, phase_indicator: 'thesis' },
|
|
305
317
|
];
|
|
306
318
|
const known = proposeNextFramework('Business Model Canvas', knownEdges);
|
|
307
319
|
assert.equal(known !== null, true);
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
320
|
+
// Phase 122-04: command comes from the resolver (commandsForFramework[0]).
|
|
321
|
+
const expected = resolver.commandsForFramework('Lean Canvas');
|
|
322
|
+
assert.equal(known.command, expected.length > 0 ? expected[0] : null,
|
|
323
|
+
'command must equal commandsForFramework(next)[0] (or null); got: ' + known.command);
|
|
324
|
+
assert.equal(typeof known.command === 'string' && known.command.indexOf('/mos:') === 0, true,
|
|
325
|
+
'Lean Canvas is registered, so command must be a /mos: string; got: ' + known.command);
|
|
326
|
+
// A workflow array (the resolver's composeWorkflow for the chain) is on
|
|
327
|
+
// the proposal as data -- a list of { step, framework, command|null, optional }.
|
|
328
|
+
assert.equal(Array.isArray(known.workflow), true, 'workflow must be a composeWorkflow array');
|
|
329
|
+
assert.equal(known.workflow.length >= 2, true, 'workflow includes [completed, next, ...]');
|
|
330
|
+
assert.equal(known.workflow[0].step, 1);
|
|
331
|
+
for (let i = 0; i < known.workflow.length; i += 1) {
|
|
332
|
+
const s = known.workflow[i];
|
|
333
|
+
assert.equal(s.step, i + 1);
|
|
334
|
+
assert.equal('command' in s && 'optional' in s && 'framework' in s, true);
|
|
335
|
+
assert.equal(s.command === null || (typeof s.command === 'string' && s.command.indexOf('/mos:') === 0), true);
|
|
336
|
+
}
|
|
337
|
+
// A framework name the registry does not know yet -> command is null
|
|
338
|
+
// (degrade, do not fabricate -- WORKFLOW-LAYER-SPEC reliability rule 5).
|
|
313
339
|
const unknownEdges = [
|
|
314
340
|
{ from: 'X', to: 'A Wholly Imaginary Framework', confidence: 0.8, phase_indicator: null },
|
|
315
341
|
];
|
|
316
342
|
const unknown = proposeNextFramework('X', unknownEdges);
|
|
317
343
|
assert.equal(unknown !== null, true);
|
|
318
|
-
assert.equal(unknown.command
|
|
344
|
+
assert.equal(unknown.command, null, 'command must be null for an unregistered framework; got: ' + unknown.command);
|
|
345
|
+
assert.equal(Array.isArray(unknown.workflow), true);
|
|
346
|
+
assert.equal(unknown.workflow.every(function (s) { return s.command === null; }), true,
|
|
347
|
+
'an unregistered chain composes to all-null commands (run manually)');
|
|
319
348
|
});
|
|
320
349
|
|
|
321
350
|
run('Test 13: proposeNextFramework grounding rule (FEEDS_INTO + Brain + confidence in reason)', () => {
|
|
@@ -408,9 +437,13 @@ function makeQuadrupleWithChain(chainBody, governingThought) {
|
|
|
408
437
|
|
|
409
438
|
run('Test 16: decide() with chain + governing_thought -> offer_next_step with FEEDS_INTO reason', () => {
|
|
410
439
|
const { decide } = requireEngine();
|
|
440
|
+
// Phase 122-04: use a chain whose `next` framework is registered, so the
|
|
441
|
+
// resolver yields a real /mos: command (an unregistered `next` would
|
|
442
|
+
// degrade to command:null -- a true statement, but the presenter then
|
|
443
|
+
// treats it as not-an-offer; this test wants the command-carrying path).
|
|
411
444
|
const quadruple = makeQuadrupleWithChain(
|
|
412
|
-
|
|
413
|
-
'After our
|
|
445
|
+
SAMPLE_REGISTERED_CHAIN,
|
|
446
|
+
'After our Business Model Canvas work we mapped the value flows.'
|
|
414
447
|
);
|
|
415
448
|
const out = decide(
|
|
416
449
|
{ sectionPath: '/tmp/fixture', sessionId: 's1' },
|
|
@@ -421,11 +454,11 @@ run('Test 16: decide() with chain + governing_thought -> offer_next_step with FE
|
|
|
421
454
|
intentSignal: null,
|
|
422
455
|
}
|
|
423
456
|
);
|
|
424
|
-
// Engine should compose a chain offer:
|
|
425
|
-
// outgoing edge from SWOT in SAMPLE_4_LINES).
|
|
457
|
+
// Engine should compose a chain offer: Business Model Canvas -> Lean Canvas.
|
|
426
458
|
assert.equal(out.offer_next_step !== null, true,
|
|
427
459
|
'expected non-null offer_next_step; trace: ' + (out.decision_trace.chosen_rationale || ''));
|
|
428
|
-
assert.equal(typeof out.offer_next_step.command, 'string'
|
|
460
|
+
assert.equal(typeof out.offer_next_step.command, 'string',
|
|
461
|
+
'offer command must be a string (Lean Canvas is registered); got: ' + out.offer_next_step.command);
|
|
429
462
|
assert.equal(out.offer_next_step.command.indexOf('/mos:') === 0, true);
|
|
430
463
|
assert.equal(/FEEDS_INTO/.test(out.offer_next_step.reason), true,
|
|
431
464
|
'offer reason must reference FEEDS_INTO; got: ' + out.offer_next_step.reason);
|
|
@@ -460,13 +493,12 @@ run('Test 17: decide() Mode A confidence 0.85 chain -> recommended_eligible flag
|
|
|
460
493
|
|
|
461
494
|
run('Test 18: decide() user override path: turn 1 offer; turn 2 different command -> REJECTED chain trace', () => {
|
|
462
495
|
const { decide } = requireEngine();
|
|
463
|
-
// Turn 1: chain proposes /mos:
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
// chain context.
|
|
496
|
+
// Turn 1: chain proposes /mos:lean-canvas (Business Model Canvas FEEDS_INTO
|
|
497
|
+
// Lean Canvas; Lean Canvas is resolver-registered so the command is real).
|
|
498
|
+
// Engine sets offer_next_step with the chain command + reason.
|
|
467
499
|
const turn1Quad = makeQuadrupleWithChain(
|
|
468
|
-
|
|
469
|
-
'After our
|
|
500
|
+
SAMPLE_REGISTERED_CHAIN,
|
|
501
|
+
'After our Business Model Canvas work we mapped the value flows.'
|
|
470
502
|
);
|
|
471
503
|
const turn1 = decide(
|
|
472
504
|
{ sectionPath: '/tmp/fixture', sessionId: 's1', userText: '' },
|
|
@@ -480,6 +512,8 @@ run('Test 18: decide() user override path: turn 1 offer; turn 2 different comman
|
|
|
480
512
|
assert.equal(turn1.offer_next_step !== null, true,
|
|
481
513
|
'turn 1 must produce a chain offer; trace: ' + turn1.decision_trace.chosen_rationale);
|
|
482
514
|
const turn1Command = turn1.offer_next_step.command;
|
|
515
|
+
assert.equal(typeof turn1Command === 'string' && turn1Command.length > 0, true,
|
|
516
|
+
'turn 1 chain command must be a real /mos: string; got: ' + turn1Command);
|
|
483
517
|
|
|
484
518
|
// Turn 2: user invokes a DIFFERENT /mos: command. Engine should record
|
|
485
519
|
// this as REJECTED chain suggestion in decision_trace.
|
|
@@ -487,7 +521,7 @@ run('Test 18: decide() user override path: turn 1 offer; turn 2 different comman
|
|
|
487
521
|
{
|
|
488
522
|
sectionPath: '/tmp/fixture',
|
|
489
523
|
sessionId: 's1',
|
|
490
|
-
userText: '/mos:
|
|
524
|
+
userText: '/mos:mullins run the 7-domains screen instead',
|
|
491
525
|
},
|
|
492
526
|
{
|
|
493
527
|
quadruple: turn1Quad,
|