@mindrian_os/install 1.13.0-beta.17 → 1.13.0-beta.19
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 -0
- package/commands/act.md +1 -0
- package/commands/admin.md +1 -0
- package/commands/analyze-needs.md +2 -0
- package/commands/analyze-systems.md +2 -0
- package/commands/analyze-timing.md +2 -0
- package/commands/auto-explore.md +2 -0
- package/commands/beautiful-question.md +2 -0
- package/commands/brain-derive.md +2 -0
- package/commands/build-knowledge.md +2 -0
- package/commands/build-thesis.md +2 -0
- package/commands/causal.md +2 -0
- package/commands/challenge-assumptions.md +2 -0
- package/commands/compare-ventures.md +2 -0
- package/commands/dashboard.md +2 -1
- package/commands/deep-grade.md +2 -0
- package/commands/diagnose.md +21 -1
- package/commands/diagnostics.md +14 -3
- package/commands/doctor.md +4 -1
- package/commands/dogfood-flush.md +92 -0
- package/commands/dominant-designs.md +2 -0
- package/commands/explain-decision.md +2 -0
- package/commands/explore-domains.md +2 -0
- package/commands/explore-futures.md +2 -0
- package/commands/explore-trends.md +2 -0
- package/commands/export.md +1 -0
- package/commands/feynman-timeline-refresh.md +2 -0
- package/commands/file-meeting.md +2 -0
- package/commands/find-analogies.md +1 -0
- package/commands/find-bottlenecks.md +2 -0
- package/commands/find-connections.md +2 -0
- package/commands/funding.md +1 -0
- package/commands/grade.md +2 -0
- package/commands/graph.md +1 -0
- package/commands/hat-briefing.md +1 -0
- package/commands/heal.md +22 -170
- package/commands/help.md +54 -334
- package/commands/hmi-status.md +23 -144
- package/commands/jtbd.md +1 -0
- package/commands/leadership.md +2 -0
- package/commands/lean-canvas.md +2 -0
- package/commands/macro-trends.md +2 -0
- package/commands/map-unknowns.md +2 -0
- package/commands/memory.md +1 -0
- package/commands/models.md +1 -0
- package/commands/mos-reason.md +2 -0
- package/commands/mos.md +139 -0
- package/commands/mullins.md +2 -0
- package/commands/mva-brief.md +2 -0
- package/commands/mva-option.md +2 -0
- package/commands/new-project.md +2 -0
- package/commands/onboard.md +20 -7
- package/commands/operator.md +1 -0
- package/commands/opportunities.md +1 -0
- package/commands/organize.md +22 -469
- package/commands/persona.md +1 -0
- package/commands/pipeline.md +2 -0
- package/commands/present.md +1 -0
- package/commands/publish.md +2 -0
- package/commands/query.md +24 -102
- package/commands/radar.md +2 -0
- package/commands/reanalyze.md +1 -0
- package/commands/research.md +2 -0
- package/commands/room.md +2 -0
- package/commands/rooms.md +1 -0
- package/commands/root-cause.md +2 -0
- package/commands/rs-experts.md +1 -0
- package/commands/rs-explain.md +1 -0
- package/commands/rs-fetch.md +1 -0
- package/commands/rs-thesis.md +1 -0
- package/commands/scenario-plan.md +2 -0
- package/commands/scheduled-tasks.md +1 -0
- package/commands/score-innovation.md +2 -0
- package/commands/scout.md +1 -0
- package/commands/setup.md +2 -0
- package/commands/snapshot.md +2 -0
- package/commands/speakers.md +1 -0
- package/commands/splash.md +5 -2
- package/commands/status.md +1 -0
- package/commands/structure-argument.md +2 -0
- package/commands/suggest-next.md +2 -0
- package/commands/systems-thinking.md +2 -0
- package/commands/think-hats.md +2 -0
- package/commands/update.md +2 -0
- package/commands/user-needs.md +2 -0
- package/commands/validate.md +2 -0
- package/commands/value-proposition.md +2 -0
- package/commands/vault.md +2 -0
- package/commands/visualize.md +24 -29
- package/commands/whitespace.md +2 -1
- package/commands/wiki.md +1 -0
- package/hooks/hooks.json +22 -88
- package/lib/agents/auto-explore-agent.cjs +82 -0
- package/lib/core/breakthrough/canary.cjs +134 -0
- package/lib/core/breakthrough/canary.test.cjs +136 -0
- package/lib/core/breakthrough/detectors.cjs +359 -0
- package/lib/core/breakthrough/detectors.test.cjs +333 -0
- package/lib/core/breakthrough/ethics-fence.cjs +127 -0
- package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
- package/lib/core/breakthrough/resurfacing.cjs +150 -0
- package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
- package/lib/core/breakthrough/review-queue.cjs +154 -0
- package/lib/core/breakthrough/review-queue.test.cjs +160 -0
- package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
- package/lib/core/breakthrough/scanner.cjs +426 -0
- package/lib/core/breakthrough/scanner.test.cjs +267 -0
- package/lib/core/breakthrough/schema.cjs +164 -0
- package/lib/core/breakthrough/schema.test.cjs +256 -0
- package/lib/core/breakthrough/scoring.cjs +293 -0
- package/lib/core/breakthrough/scoring.test.cjs +423 -0
- package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
- package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
- package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
- package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
- package/lib/core/first-touch-version-stamper.cjs +113 -0
- package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
- package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
- package/lib/core/llm-name-suggester.cjs +194 -0
- package/lib/core/llm-name-suggester.test.cjs +132 -0
- package/lib/core/mva-orchestrator.cjs +41 -0
- package/lib/core/mva-telemetry.cjs +31 -143
- package/lib/core/navigation/edges.cjs +35 -0
- package/lib/core/navigation/memory-events.cjs +126 -0
- package/lib/core/room-auto-create.cjs +318 -0
- package/lib/core/room-auto-create.test.cjs +198 -0
- package/lib/core/room-discard-cascade.cjs +225 -0
- package/lib/core/room-discard-cascade.test.cjs +135 -0
- package/lib/core/room-name-validator.cjs +132 -0
- package/lib/core/room-name-validator.test.cjs +156 -0
- package/lib/core/room-naming-selector.cjs +357 -0
- package/lib/core/room-naming-selector.test.cjs +277 -0
- package/lib/core/room-receipt-emit.cjs +63 -0
- package/lib/core/room-skeleton-scaffold.cjs +315 -0
- package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
- package/lib/core/stale-copy-scanner.cjs +190 -0
- package/lib/core/state-aware-router.cjs +78 -0
- package/lib/core/telemetry/schema.cjs +168 -0
- package/lib/core/telemetry/schema.test.cjs +124 -0
- package/lib/core/telemetry/validator.cjs +197 -0
- package/lib/core/telemetry/validator.test.cjs +188 -0
- package/lib/core/telemetry/writer.cjs +141 -0
- package/lib/core/telemetry/writer.test.cjs +331 -0
- package/lib/core/terminal-capability.cjs +88 -0
- package/lib/core/venture-shape-nudge.cjs +163 -0
- package/lib/core/venture-shape-nudge.test.cjs +161 -0
- package/lib/core/visual-ops.cjs +70 -2
- package/lib/hmi/selector-dispatcher.cjs +90 -1
- package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
- package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
- package/lib/memory/body-shape-coverage.test.cjs +268 -0
- package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
- package/lib/memory/first-touch-version.test.cjs +198 -0
- package/lib/memory/help-coverage.test.cjs +108 -0
- package/lib/memory/help-renderer.test.cjs +145 -0
- package/lib/memory/palette-consistency.test.cjs +127 -0
- package/lib/memory/pending-tension-store.cjs +80 -0
- package/lib/memory/render-v2-disposition.test.cjs +199 -0
- package/lib/memory/run-feynman-tests.cjs +213 -0
- package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
- package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
- package/lib/memory/soft-alias.test.cjs +144 -0
- package/lib/memory/stale-copy-scanner.test.cjs +291 -0
- package/lib/memory/state-aware-router.test.cjs +90 -0
- package/lib/memory/statusline-two-row.test.cjs +338 -0
- package/lib/memory/terminal-capability.test.cjs +155 -0
- package/lib/render/ROOM.md +74 -22
- package/lib/sessionstart/budget-compressor.cjs +130 -0
- package/lib/sessionstart/contributor-interface.cjs +134 -0
- package/lib/sessionstart/contributor-isolator.cjs +128 -0
- package/lib/sessionstart/precedence-ladder.cjs +47 -0
- package/lib/statusline/governing-thought-truncator.cjs +45 -0
- package/lib/statusline/two-row-renderer.cjs +186 -0
- package/lib/statusline/version-resolver.cjs +81 -0
- package/package.json +1 -1
- package/references/visual/ROOM.md +55 -0
- package/references/visual/palette.json +54 -0
- package/skills/larry-personality/SKILL.md +34 -0
- package/skills/ui-system/SKILL.md +109 -1
- package/skills/ui-system/rules/dual-palette.md +156 -0
- package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
- package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Phase 120-02 Wave 2 Task 2 -- verb-dispatch unit tests (D-08 + D-09 + D-10).
|
|
5
|
+
*
|
|
6
|
+
* Tests 1-10:
|
|
7
|
+
* 1. VERB_TO_EVENT lock verbatim (5 entries; 3 emit events; 2 navigational)
|
|
8
|
+
* 2. dispatchVerb 'Confirm' emits breakthrough_confirmed
|
|
9
|
+
* 3. dispatchVerb 'Dismiss' captures artifact_ids_at_dismiss (D-13 baseline)
|
|
10
|
+
* 4. dispatchVerb 'File as decision' emits event AND writes FILED_AS_DECISION edge
|
|
11
|
+
* 5. dispatchVerb 'Explore deeper' navigational; no event
|
|
12
|
+
* 6. dispatchVerb 'Back' escape; no event
|
|
13
|
+
* 7. dispatchVerb unknown verb -> {ok:false, reason:'unknown_verb'}
|
|
14
|
+
* 8. dispatchVerb flips Breakthrough node properties handled_at + handled_verb
|
|
15
|
+
* 9. Canon Part 8 source-grep
|
|
16
|
+
* 10. em-dash HARD RULE
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const test = require('node:test');
|
|
20
|
+
const { strict: assert } = require('node:assert');
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const os = require('node:os');
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
|
|
25
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
26
|
+
const verbDispatch = require(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'verb-dispatch.cjs'));
|
|
27
|
+
const schema = require(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'schema.cjs'));
|
|
28
|
+
const navigation = require(path.join(REPO_ROOT, 'lib', 'core', 'navigation.cjs'));
|
|
29
|
+
const { openRoomDb } = require(path.join(REPO_ROOT, 'lib', 'core', 'room-db.cjs'));
|
|
30
|
+
|
|
31
|
+
function makeTmpDb(prefix) {
|
|
32
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
33
|
+
const db = openRoomDb(dir);
|
|
34
|
+
return { dir, db };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function seedArtifact(db, id) {
|
|
38
|
+
const nowMs = Date.now();
|
|
39
|
+
db.prepare(
|
|
40
|
+
"INSERT INTO nodes (id, type, properties, source_path, created_by, review_status, created_at, last_seen_at) " +
|
|
41
|
+
"VALUES (?, 'artifact', '{}', 'test', 'system', 'confirmed', ?, ?)"
|
|
42
|
+
).run(id, nowMs, nowMs);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function seedBreakthrough(db, id, kind, artifactIds, theme) {
|
|
46
|
+
for (const aid of artifactIds) seedArtifact(db, aid);
|
|
47
|
+
const r = schema.writeBreakthrough(db, {
|
|
48
|
+
id: id,
|
|
49
|
+
kind: kind || 'convergence',
|
|
50
|
+
confidence: 0.55,
|
|
51
|
+
artifact_ids: artifactIds,
|
|
52
|
+
theme: theme || 'test-theme',
|
|
53
|
+
differential: 0.5,
|
|
54
|
+
cross_section_linked: false,
|
|
55
|
+
});
|
|
56
|
+
assert.equal(r.ok, true, 'seedBreakthrough writeBreakthrough should succeed');
|
|
57
|
+
return r;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test('120-02 Task 2 Test 1: VERB_TO_EVENT lock verbatim', () => {
|
|
61
|
+
const m = verbDispatch.VERB_TO_EVENT;
|
|
62
|
+
assert.equal(m['Explore deeper'], null);
|
|
63
|
+
assert.equal(m['Confirm'], 'breakthrough_confirmed');
|
|
64
|
+
assert.equal(m['File as decision'], 'breakthrough_filed_as_decision');
|
|
65
|
+
assert.equal(m['Dismiss'], 'breakthrough_dismissed');
|
|
66
|
+
assert.equal(m['Back'], null);
|
|
67
|
+
// Object.freeze invariant: cannot mutate keys.
|
|
68
|
+
assert.throws(function () {
|
|
69
|
+
'use strict';
|
|
70
|
+
m['Confirm'] = 'breakthrough_other';
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('120-02 Task 2 Test 2: dispatchVerb Confirm emits breakthrough_confirmed', () => {
|
|
75
|
+
const { dir, db } = makeTmpDb('p120-02-t2-t2-');
|
|
76
|
+
seedBreakthrough(db, 'bk:1', 'convergence', ['a1', 'a2']);
|
|
77
|
+
const r = verbDispatch.dispatchVerb('Confirm', 'bk:1', { db: db });
|
|
78
|
+
assert.equal(r.ok, true);
|
|
79
|
+
assert.equal(r.navigational, false);
|
|
80
|
+
// Verify event landed with breakthrough_id 'bk:1'.
|
|
81
|
+
const events = navigation.findRecentChanges(db, 0, { eventType: 'breakthrough_confirmed', limit: 10 });
|
|
82
|
+
assert.equal(events.length, 1);
|
|
83
|
+
assert.equal(events[0].properties.breakthrough_id, 'bk:1');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('120-02 Task 2 Test 3: dispatchVerb Dismiss captures artifact_ids_at_dismiss (D-13 baseline)', () => {
|
|
87
|
+
const { dir, db } = makeTmpDb('p120-02-t2-t3-');
|
|
88
|
+
seedBreakthrough(db, 'bk:dis', 'convergence', ['a1', 'a2', 'a3']);
|
|
89
|
+
const r = verbDispatch.dispatchVerb('Dismiss', 'bk:dis', { db: db });
|
|
90
|
+
assert.equal(r.ok, true);
|
|
91
|
+
const events = navigation.findRecentChanges(db, 0, { eventType: 'breakthrough_dismissed', limit: 10 });
|
|
92
|
+
assert.equal(events.length, 1);
|
|
93
|
+
// The artifact_ids_at_dismiss MUST carry the DERIVED_FROM edge targets at dismiss time.
|
|
94
|
+
assert.ok(Array.isArray(events[0].properties.artifact_ids_at_dismiss));
|
|
95
|
+
assert.equal(events[0].properties.artifact_ids_at_dismiss.length, 3);
|
|
96
|
+
// Set equality on the ids (order may vary depending on edge query).
|
|
97
|
+
const got = new Set(events[0].properties.artifact_ids_at_dismiss);
|
|
98
|
+
assert.ok(got.has('a1'));
|
|
99
|
+
assert.ok(got.has('a2'));
|
|
100
|
+
assert.ok(got.has('a3'));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('120-02 Task 2 Test 4: dispatchVerb File as decision emits event AND writes FILED_AS_DECISION edge', () => {
|
|
104
|
+
const { dir, db } = makeTmpDb('p120-02-t2-t4-');
|
|
105
|
+
seedBreakthrough(db, 'bk:file', 'convergence', ['a1', 'a2']);
|
|
106
|
+
// Seed the target Decision node so the FK in the FILED_AS_DECISION edge passes.
|
|
107
|
+
const nowMs = Date.now();
|
|
108
|
+
db.prepare(
|
|
109
|
+
"INSERT INTO nodes (id, type, properties, source_path, created_by, review_status, created_at, last_seen_at) " +
|
|
110
|
+
"VALUES (?, 'decision', '{}', 'test', 'system', 'proposed', ?, ?)"
|
|
111
|
+
).run('decision:bk:file', nowMs, nowMs);
|
|
112
|
+
const r = verbDispatch.dispatchVerb('File as decision', 'bk:file', { db: db });
|
|
113
|
+
assert.equal(r.ok, true);
|
|
114
|
+
// (a) Event landed.
|
|
115
|
+
const events = navigation.findRecentChanges(db, 0, { eventType: 'breakthrough_filed_as_decision', limit: 10 });
|
|
116
|
+
assert.equal(events.length, 1);
|
|
117
|
+
assert.equal(events[0].properties.breakthrough_id, 'bk:file');
|
|
118
|
+
// (b) FILED_AS_DECISION edge landed.
|
|
119
|
+
const edges = db.prepare("SELECT source, target, type FROM edges WHERE source = ? AND type = 'FILED_AS_DECISION'").all('bk:file');
|
|
120
|
+
assert.equal(edges.length, 1);
|
|
121
|
+
assert.equal(edges[0].target, 'decision:bk:file');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('120-02 Task 2 Test 5: dispatchVerb Explore deeper navigational; no event', () => {
|
|
125
|
+
const { dir, db } = makeTmpDb('p120-02-t2-t5-');
|
|
126
|
+
seedBreakthrough(db, 'bk:ex', 'convergence', ['a1', 'a2']);
|
|
127
|
+
const r = verbDispatch.dispatchVerb('Explore deeper', 'bk:ex', { db: db });
|
|
128
|
+
assert.equal(r.ok, true);
|
|
129
|
+
assert.equal(r.navigational, true);
|
|
130
|
+
assert.equal(r.drill_target, 'bk:ex');
|
|
131
|
+
// No memory_event for navigational verbs.
|
|
132
|
+
const conf = navigation.findRecentChanges(db, 0, { eventType: 'breakthrough_confirmed', limit: 10 });
|
|
133
|
+
const dism = navigation.findRecentChanges(db, 0, { eventType: 'breakthrough_dismissed', limit: 10 });
|
|
134
|
+
const file = navigation.findRecentChanges(db, 0, { eventType: 'breakthrough_filed_as_decision', limit: 10 });
|
|
135
|
+
assert.equal(conf.length, 0);
|
|
136
|
+
assert.equal(dism.length, 0);
|
|
137
|
+
assert.equal(file.length, 0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('120-02 Task 2 Test 6: dispatchVerb Back escape; no event', () => {
|
|
141
|
+
const { dir, db } = makeTmpDb('p120-02-t2-t6-');
|
|
142
|
+
seedBreakthrough(db, 'bk:bk', 'convergence', ['a1', 'a2']);
|
|
143
|
+
const r = verbDispatch.dispatchVerb('Back', 'bk:bk', { db: db });
|
|
144
|
+
assert.equal(r.ok, true);
|
|
145
|
+
assert.equal(r.navigational, true);
|
|
146
|
+
assert.equal(r.escape, true);
|
|
147
|
+
const conf = navigation.findRecentChanges(db, 0, { eventType: 'breakthrough_confirmed', limit: 10 });
|
|
148
|
+
assert.equal(conf.length, 0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('120-02 Task 2 Test 7: dispatchVerb unknown verb -> {ok:false, reason:"unknown_verb"}', () => {
|
|
152
|
+
const { dir, db } = makeTmpDb('p120-02-t2-t7-');
|
|
153
|
+
const r = verbDispatch.dispatchVerb('foo', 'bk:x', { db: db });
|
|
154
|
+
assert.equal(r.ok, false);
|
|
155
|
+
assert.equal(r.reason, 'unknown_verb');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('120-02 Task 2 Test 8: dispatchVerb Confirm flips Breakthrough node handled_at + handled_verb', () => {
|
|
159
|
+
const { dir, db } = makeTmpDb('p120-02-t2-t8-');
|
|
160
|
+
seedBreakthrough(db, 'bk:h', 'convergence', ['a1', 'a2']);
|
|
161
|
+
verbDispatch.dispatchVerb('Confirm', 'bk:h', { db: db });
|
|
162
|
+
const row = db.prepare("SELECT properties FROM nodes WHERE id = ? AND type = 'breakthrough'").get('bk:h');
|
|
163
|
+
assert.ok(row, 'breakthrough node should exist after dispatch');
|
|
164
|
+
const props = JSON.parse(row.properties);
|
|
165
|
+
assert.equal(props.handled_verb, 'Confirm');
|
|
166
|
+
assert.ok(typeof props.handled_at === 'number');
|
|
167
|
+
assert.ok(props.handled_at > 0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('120-02 Task 2 Test 9: Canon Part 8 source-grep -- zero Brain coupling in verb-dispatch.cjs', () => {
|
|
171
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'verb-dispatch.cjs'), 'utf8');
|
|
172
|
+
assert.equal(/require\s*\(\s*['"][^'"]*brain-client[^'"]*['"]\s*\)/.test(src), false,
|
|
173
|
+
'verb-dispatch.cjs must not require brain-client');
|
|
174
|
+
assert.equal(/fetch\s*\(\s*['"][^'"]*brain\.mindrian/.test(src), false,
|
|
175
|
+
'verb-dispatch.cjs must not fetch brain.mindrian.*');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('120-02 Task 2 Test 10: em-dash HARD RULE -- zero U+2014 in verb-dispatch.cjs', () => {
|
|
179
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'lib', 'core', 'breakthrough', 'verb-dispatch.cjs'), 'utf8');
|
|
180
|
+
let count = 0;
|
|
181
|
+
for (const ch of src) {
|
|
182
|
+
if (ch.charCodeAt(0) === 0x2014) count++;
|
|
183
|
+
}
|
|
184
|
+
assert.equal(count, 0, 'verb-dispatch.cjs must contain zero U+2014 em-dash characters');
|
|
185
|
+
});
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 120-03 Wave 2 Task 1 -- D-17 4-rule voice scaffold + auditor.
|
|
3
|
+
//
|
|
4
|
+
// Per CONTEXT.md D-17 LOCKED VERBATIM (the 4 rules):
|
|
5
|
+
// Rule 1: Evidence requirement. Cite artifact ids OR graph edges. Pattern matches:
|
|
6
|
+
// "(artifacts #N, #M)" OR "[[artifact-name]]" OR
|
|
7
|
+
// "(see edges X, Y)".
|
|
8
|
+
// Rule 2: Mechanism clause. "by Y" where Y is a user action.
|
|
9
|
+
// Rule 3: Time anchor. One of: "in the last N hours/days", "across this
|
|
10
|
+
// week", "since YYYY-MM-DD" or specific weekday, or
|
|
11
|
+
// "today/this morning".
|
|
12
|
+
// Rule 4: No unbacked superlatives. 'breakthrough', 'biggest', 'first', 'unprecedented',
|
|
13
|
+
// 'major', 'massive' MUST NOT appear UNLESS adjacent to
|
|
14
|
+
// a numeric backing. Frequency words 'consistent',
|
|
15
|
+
// 'repeated', 'always' MUST be accompanied by a count.
|
|
16
|
+
//
|
|
17
|
+
// The auditor IS the structural enforcement (D-20 meta-principle: every voice claim
|
|
18
|
+
// must be testable from graph state). Plan 120-03 scanner.cjs integration calls
|
|
19
|
+
// auditVoiceLine BEFORE passing the line into the F.7 surface; failed-audit lines are
|
|
20
|
+
// replaced with the structural default which is always auditor-safe.
|
|
21
|
+
//
|
|
22
|
+
// Canon Part 5: evidence is graded by context; the D-17 evidence requirement (rule 1)
|
|
23
|
+
// maps to the four evidence tiers Academic / Operational / Practitioner / None via
|
|
24
|
+
// the cite pattern (the cite IS the evidence anchor at this layer).
|
|
25
|
+
// Canon Part 8: pure LOCAL; no Brain coupling; no cross-room aggregation. Pure
|
|
26
|
+
// function over the breakthrough record + an optional roomState scalar.
|
|
27
|
+
// Canon Part 10 sub-claim 5: the math IS the surface; the voice IS the math's
|
|
28
|
+
// translation; honesty requires backing.
|
|
29
|
+
//
|
|
30
|
+
// Em-dash HARD RULE (CLAUDE.md feedback_no_emdashes): zero U+2014 in source -- use
|
|
31
|
+
// the ASCII double-hyphen "--" sequence everywhere.
|
|
32
|
+
|
|
33
|
+
const VOICE_RULE_NAMES = Object.freeze([
|
|
34
|
+
'evidence_requirement',
|
|
35
|
+
'mechanism_clause',
|
|
36
|
+
'time_anchor',
|
|
37
|
+
'no_unbacked_superlatives',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const FORBIDDEN_SUPERLATIVES = Object.freeze([
|
|
41
|
+
'breakthrough', 'biggest', 'first', 'unprecedented', 'major', 'massive',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const FREQUENCY_WORDS = Object.freeze([
|
|
45
|
+
'consistent', 'repeated', 'always',
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
// Display names for the 4 detector kinds emitted by Plan 120-00 detectors.cjs.
|
|
49
|
+
// Kept aligned with shape-f7-breakthrough-renderer.cjs::KIND_DISPLAY_NAMES at the
|
|
50
|
+
// human-readable surface (this module uses lowercase phrases since the voice line
|
|
51
|
+
// embeds the kind mid-sentence; the renderer uses TitleCase for the title row).
|
|
52
|
+
const KIND_DISPLAY = Object.freeze({
|
|
53
|
+
convergence: 'convergence',
|
|
54
|
+
contradiction_resolved: 'contradiction resolved',
|
|
55
|
+
cross_domain_analogy: 'cross-domain analogy',
|
|
56
|
+
reverse_salient_closed: 'closure on a lagging area',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// formatTimeAnchor mirrors the shape-f7-breakthrough-renderer.cjs bucket logic for
|
|
60
|
+
// the title line, but with phrasing tuned for the voice line (mid-sentence). Plan
|
|
61
|
+
// 120-01 renderer uses "in the last hour" / "today" / "this week"; this module
|
|
62
|
+
// matches those for consistency at the audit-pattern level (D-17 rule 3 regex
|
|
63
|
+
// patterns accept the same strings).
|
|
64
|
+
//
|
|
65
|
+
// Buckets (D-06 ethical fence: ages > 14 days surface the date):
|
|
66
|
+
// ageHours < 1 -> "in the last hour"
|
|
67
|
+
// ageHours < 8 -> "in the last N hours"
|
|
68
|
+
// ageHours < 24 -> "today"
|
|
69
|
+
// ageDays < 2 -> "in the last day"
|
|
70
|
+
// ageDays < 3 -> "in the last two days"
|
|
71
|
+
// ageDays < 7 -> "this week"
|
|
72
|
+
// ageDays < 14 -> "in the last two weeks"
|
|
73
|
+
// else -> "since YYYY-MM-DD"
|
|
74
|
+
function formatTimeAnchor(detected_at, nowMs) {
|
|
75
|
+
const now = (typeof nowMs === 'number' && Number.isFinite(nowMs)) ? nowMs : Date.now();
|
|
76
|
+
const ts = (typeof detected_at === 'number' && Number.isFinite(detected_at)) ? detected_at : now;
|
|
77
|
+
const ageMs = Math.max(0, now - ts);
|
|
78
|
+
const ageHours = ageMs / 3600000;
|
|
79
|
+
const ageDays = ageHours / 24;
|
|
80
|
+
if (ageHours < 1) return 'in the last hour';
|
|
81
|
+
if (ageHours < 8) return 'in the last ' + Math.floor(ageHours) + ' hours';
|
|
82
|
+
if (ageHours < 24) return 'today';
|
|
83
|
+
if (ageDays < 2) return 'in the last day';
|
|
84
|
+
if (ageDays < 3) return 'in the last two days';
|
|
85
|
+
if (ageDays < 7) return 'this week';
|
|
86
|
+
if (ageDays < 14) return 'in the last two weeks';
|
|
87
|
+
const d = new Date(ts);
|
|
88
|
+
const yyyy = d.getUTCFullYear();
|
|
89
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
90
|
+
const dd = String(d.getUTCDate()).padStart(2, '0');
|
|
91
|
+
return 'since ' + yyyy + '-' + mm + '-' + dd;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Structural default mechanism (D-17 rule 2 satisfaction without LLM/Brain).
|
|
95
|
+
// Honest fallback: cites the user action without inferring intent. Per Canon
|
|
96
|
+
// Part 8: no inferring; only counting.
|
|
97
|
+
function buildDefaultMechanism(artifactCount, _kind) {
|
|
98
|
+
return 'by adding ' + artifactCount + ' artifacts to this section';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// composeBreakthroughVoiceLine -- THE composer. Pure function over the breakthrough
|
|
102
|
+
// record + an optional roomState (which may carry a roomState.mechanism_phrase
|
|
103
|
+
// that Larry has co-authored with the user, e.g. via a /mos:* command). Returns
|
|
104
|
+
// a single-line string in the canonical D-17 format:
|
|
105
|
+
// "You're seeing a {kind} on {theme} (artifacts {ids}) -- {mechanism} -- {time_anchor}."
|
|
106
|
+
//
|
|
107
|
+
// Inputs:
|
|
108
|
+
// breakthrough.kind -> one of the 4 DETECTOR_TYPES (Plan 120-00)
|
|
109
|
+
// breakthrough.theme -> string (sliced to 120 chars at surface layer)
|
|
110
|
+
// breakthrough.artifact_ids -> string[] (non-empty; D-20 HARD FLOOR upstream)
|
|
111
|
+
// breakthrough.detected_at -> number (epoch ms)
|
|
112
|
+
// roomState -> {mechanism_phrase?: string} | null
|
|
113
|
+
// nowMs -> number (epoch ms; for test determinism)
|
|
114
|
+
function composeBreakthroughVoiceLine(breakthrough, roomState, nowMs) {
|
|
115
|
+
const bk = breakthrough || {};
|
|
116
|
+
const ids = Array.isArray(bk.artifact_ids)
|
|
117
|
+
? bk.artifact_ids.filter((s) => typeof s === 'string' && s.length > 0)
|
|
118
|
+
: [];
|
|
119
|
+
const kindKey = (typeof bk.kind === 'string') ? bk.kind : '';
|
|
120
|
+
const kindDisplay = KIND_DISPLAY[kindKey] || kindKey || 'pattern';
|
|
121
|
+
const themeRaw = (typeof bk.theme === 'string') ? bk.theme : '';
|
|
122
|
+
const theme = themeRaw.slice(0, 120);
|
|
123
|
+
const evidenceCite = '(artifacts ' + ids.join(', ') + ')';
|
|
124
|
+
const mechanism = (roomState && typeof roomState.mechanism_phrase === 'string' && roomState.mechanism_phrase.length > 0)
|
|
125
|
+
? roomState.mechanism_phrase
|
|
126
|
+
: buildDefaultMechanism(ids.length, kindKey);
|
|
127
|
+
const timeAnchor = formatTimeAnchor(bk.detected_at || Date.now(), nowMs);
|
|
128
|
+
const themePart = theme ? ' on ' + theme : '';
|
|
129
|
+
return "You're seeing a " + kindDisplay + themePart + ' ' + evidenceCite + ' -- ' + mechanism + ' -- ' + timeAnchor + '.';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// --- The 4 rule predicates ---
|
|
133
|
+
// Each is a pure function (string) -> boolean. Together they constitute the
|
|
134
|
+
// auditor's structural enforcement of D-17.
|
|
135
|
+
|
|
136
|
+
function citeEvidence(line) {
|
|
137
|
+
// Rule 1: line MUST contain one of:
|
|
138
|
+
// (artifacts ...) -- artifact-id cite
|
|
139
|
+
// [[...]] -- wiki-link cite
|
|
140
|
+
// (see edges X, Y) -- graph-edge cite
|
|
141
|
+
if (typeof line !== 'string') return false;
|
|
142
|
+
if (/\(artifacts\s+[^)]+\)/i.test(line)) return true;
|
|
143
|
+
if (/\[\[[^\]]+\]\]/.test(line)) return true;
|
|
144
|
+
if (/\(see\s+edges?\s+[^)]+\)/i.test(line)) return true;
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function includeMechanism(line) {
|
|
149
|
+
// Rule 2: line MUST contain "by " followed by a verb phrase. Heuristic: "by "
|
|
150
|
+
// immediately followed by a word character; rules out "by." standalone.
|
|
151
|
+
if (typeof line !== 'string') return false;
|
|
152
|
+
return /\bby\s+\w+/i.test(line);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function anchorTime(line) {
|
|
156
|
+
// Rule 3: line MUST contain a time-anchor pattern.
|
|
157
|
+
if (typeof line !== 'string') return false;
|
|
158
|
+
const patterns = [
|
|
159
|
+
/in the last\s+\d+\s+(hour|hours|day|days|week|weeks)/i,
|
|
160
|
+
/in the last\s+(hour|day|two days|week|two weeks)/i,
|
|
161
|
+
/this (week|morning|month)/i,
|
|
162
|
+
/\btoday\b|\byesterday\b|\btonight\b/i,
|
|
163
|
+
/across this week/i,
|
|
164
|
+
/since\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday|\d{4}-\d{2}-\d{2})/i,
|
|
165
|
+
];
|
|
166
|
+
return patterns.some((p) => p.test(line));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// hasNumericBacking -- a digit alone is too lax (artifact ids like "a1, a2"
|
|
170
|
+
// satisfy a naive /\d/ check while not actually backing the claim). The spec
|
|
171
|
+
// example "the highest differential score (0.78)" shows the intent: a real
|
|
172
|
+
// numeric measurement, typically a decimal/integer in parens, OR a multi-digit
|
|
173
|
+
// number, OR a number followed by a unit/score-word. We accept any of:
|
|
174
|
+
// (N) or (N.N) -- parenthesized number (the canonical D-17 form)
|
|
175
|
+
// N.NN -- decimal number (e.g., 0.78)
|
|
176
|
+
// \d{2,} -- multi-digit integer (count/score)
|
|
177
|
+
// \d+\s*(score|count|sessions|times|days|hours|sigma|sd|%) -- numeric + unit
|
|
178
|
+
function hasNumericBacking(window) {
|
|
179
|
+
if (/\(\d+(?:\.\d+)?\)/.test(window)) return true;
|
|
180
|
+
if (/\d+\.\d+/.test(window)) return true;
|
|
181
|
+
if (/\b\d{2,}\b/.test(window)) return true;
|
|
182
|
+
if (/\b\d+\s*(score|count|sessions|times|days|hours|sigma|sd|%|x)\b/i.test(window)) return true;
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function noUnbackedSuperlatives(line) {
|
|
187
|
+
// Rule 4: forbidden superlatives MUST NOT appear unless adjacent to numeric
|
|
188
|
+
// backing (within 60-char window). Frequency words need a count within 30 chars.
|
|
189
|
+
// "Numeric backing" is stricter than "contains a digit" -- see hasNumericBacking.
|
|
190
|
+
if (typeof line !== 'string') return true;
|
|
191
|
+
const lower = line.toLowerCase();
|
|
192
|
+
for (const word of FORBIDDEN_SUPERLATIVES) {
|
|
193
|
+
let searchFrom = 0;
|
|
194
|
+
while (true) {
|
|
195
|
+
const idx = lower.indexOf(word, searchFrom);
|
|
196
|
+
if (idx < 0) break;
|
|
197
|
+
const windowStart = Math.max(0, idx - 60);
|
|
198
|
+
const windowEnd = Math.min(lower.length, idx + word.length + 60);
|
|
199
|
+
const window = lower.slice(windowStart, windowEnd);
|
|
200
|
+
if (!hasNumericBacking(window)) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
searchFrom = idx + word.length;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const word of FREQUENCY_WORDS) {
|
|
207
|
+
let searchFrom = 0;
|
|
208
|
+
while (true) {
|
|
209
|
+
const idx = lower.indexOf(word, searchFrom);
|
|
210
|
+
if (idx < 0) break;
|
|
211
|
+
const windowStart = Math.max(0, idx - 30);
|
|
212
|
+
const windowEnd = Math.min(lower.length, idx + word.length + 30);
|
|
213
|
+
const window = lower.slice(windowStart, windowEnd);
|
|
214
|
+
if (!hasNumericBacking(window)) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
searchFrom = idx + word.length;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// auditVoiceLine -- the auditor. Pure function (string) -> {ok, violations}.
|
|
224
|
+
// Returns {ok: true, violations: []} when ALL 4 rules pass.
|
|
225
|
+
// Returns {ok: false, violations: [...rule names...]} when one or more fail.
|
|
226
|
+
function auditVoiceLine(line) {
|
|
227
|
+
const violations = [];
|
|
228
|
+
if (!citeEvidence(line)) violations.push('evidence_requirement');
|
|
229
|
+
if (!includeMechanism(line)) violations.push('mechanism_clause');
|
|
230
|
+
if (!anchorTime(line)) violations.push('time_anchor');
|
|
231
|
+
if (!noUnbackedSuperlatives(line)) violations.push('no_unbacked_superlatives');
|
|
232
|
+
return { ok: violations.length === 0, violations: violations };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
composeBreakthroughVoiceLine,
|
|
237
|
+
auditVoiceLine,
|
|
238
|
+
citeEvidence,
|
|
239
|
+
includeMechanism,
|
|
240
|
+
anchorTime,
|
|
241
|
+
noUnbackedSuperlatives,
|
|
242
|
+
formatTimeAnchor,
|
|
243
|
+
VOICE_RULE_NAMES,
|
|
244
|
+
FORBIDDEN_SUPERLATIVES,
|
|
245
|
+
FREQUENCY_WORDS,
|
|
246
|
+
KIND_DISPLAY,
|
|
247
|
+
};
|