@mindrian_os/install 1.13.0-beta.16 → 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 +36 -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 +4 -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 +4 -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 +58 -0
- package/commands/mva-option.md +91 -0
- package/commands/new-project.md +4 -0
- package/commands/onboard.md +22 -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 +31 -88
- package/lib/agents/auto-explore-agent.cjs +82 -0
- package/lib/agents/mva/brain-classic-traps.cjs +77 -0
- package/lib/agents/mva/brain-cross-domain.cjs +79 -0
- package/lib/agents/mva/brain-similar-ventures.cjs +93 -0
- package/lib/agents/mva/dashboard-graph-neighborhood.cjs +72 -0
- package/lib/agents/mva/index.cjs +42 -0
- package/lib/agents/mva/six-hats-red-black.cjs +137 -0
- package/lib/agents/mva/tavily-funding-scan.cjs +147 -0
- package/lib/agents/mva/test-all-six-agents.cjs +467 -0
- package/lib/conversation/operator.cjs +64 -0
- package/lib/conversation/operator.test.cjs +160 -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-agent-contract.cjs +170 -0
- package/lib/core/mva-agent-contract.test.cjs +169 -0
- package/lib/core/mva-budget.cjs +75 -0
- package/lib/core/mva-budget.test.cjs +68 -0
- package/lib/core/mva-classifier.cjs +370 -0
- package/lib/core/mva-classifier.test.cjs +248 -0
- package/lib/core/mva-deck-builder.cjs +452 -0
- package/lib/core/mva-deck-builder.test.cjs +287 -0
- package/lib/core/mva-detect.smoke.test.cjs +197 -0
- package/lib/core/mva-dispatcher.cjs +110 -0
- package/lib/core/mva-dispatcher.test.cjs +216 -0
- package/lib/core/mva-option-router.cjs +292 -0
- package/lib/core/mva-option-router.test.cjs +483 -0
- package/lib/core/mva-orchestrator.cjs +365 -0
- package/lib/core/mva-orchestrator.test.cjs +908 -0
- package/lib/core/mva-progressive-renderer.cjs +194 -0
- package/lib/core/mva-progressive-renderer.test.cjs +157 -0
- package/lib/core/mva-rule-linter.cjs +213 -0
- package/lib/core/mva-rule-linter.test.cjs +336 -0
- package/lib/core/mva-state.cjs +159 -0
- package/lib/core/mva-telemetry.cjs +58 -0
- package/lib/core/mva-telemetry.test.cjs +196 -0
- package/lib/core/mva-vercel-deploy.cjs +168 -0
- package/lib/core/mva-vercel-deploy.test.cjs +239 -0
- package/lib/core/navigation/dashboard-helpers.cjs +145 -0
- package/lib/core/navigation/edges.cjs +35 -0
- package/lib/core/navigation/memory-events.cjs +126 -0
- package/lib/core/navigation.cjs +11 -0
- package/lib/core/resolve-vercel-key.cjs +107 -0
- package/lib/core/resolve-vercel-key.test.cjs +137 -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 +240 -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/mva-pipeline/SKILL.md +129 -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,161 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Phase 119-00 Wave 1 -- venture-shape-nudge module tests.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the lib/memory/selector-decisions.test.cjs fixture pattern
|
|
7
|
+
* (fs.mkdtempSync + openRoomDb). Tests are allowed to require room-db.cjs
|
|
8
|
+
* directly per the Phase 109-06 pre-commit chokepoint allow-list for tests/.
|
|
9
|
+
*
|
|
10
|
+
* Covers the 7 behavior axes from 119-00-PLAN.md Task 1:
|
|
11
|
+
* Test 1 -- cold-start floor (Test 5 in plan: no room.db -> surface false)
|
|
12
|
+
* Test 2 -- venture-shape accumulation to threshold (Test 6)
|
|
13
|
+
* Test 3 -- D-01 invariant: auto_explore_fired short-circuits (Test 7)
|
|
14
|
+
* Test 4 -- venture_classification_unavailable safe-default (Test 7b)
|
|
15
|
+
* Test 5 -- venture_classified present happy-path (Test 7c)
|
|
16
|
+
* Test 6 -- no Brain client require (Test 8; source-grep audit)
|
|
17
|
+
* Test 7 -- no-room-dir guard
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { test } = require('node:test');
|
|
21
|
+
const { strict: assert } = require('node:assert');
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const os = require('node:os');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
|
|
26
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
27
|
+
const { openRoomDb } = require(path.join(REPO_ROOT, 'lib', 'core', 'room-db.cjs'));
|
|
28
|
+
const navigation = require(path.join(REPO_ROOT, 'lib', 'core', 'navigation.cjs'));
|
|
29
|
+
const ventureShapeNudge = require(path.join(REPO_ROOT, 'lib', 'core', 'venture-shape-nudge.cjs'));
|
|
30
|
+
|
|
31
|
+
function freshRoom() {
|
|
32
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'p119-00-nudge-'));
|
|
33
|
+
fs.mkdirSync(path.join(dir, '.mindrian'), { recursive: true });
|
|
34
|
+
const db = openRoomDb(dir);
|
|
35
|
+
return { dir, db };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function closeQuiet(db) {
|
|
39
|
+
try { db.close(); } catch (_e) { /* graceful */ }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('Test 1: cold-start floor -- no events in db returns surface:false with venture_classification_unavailable', () => {
|
|
43
|
+
const { dir, db } = freshRoom();
|
|
44
|
+
closeQuiet(db);
|
|
45
|
+
const r = ventureShapeNudge.shouldSurfaceNudge(dir, {});
|
|
46
|
+
assert.equal(r.surface, false);
|
|
47
|
+
assert.equal(r.turn_count, 0);
|
|
48
|
+
assert.equal(r.threshold, 3);
|
|
49
|
+
assert.equal(r.skip_reason, 'venture_classification_unavailable');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('Test 2: 3 venture_classified=true events reach threshold (surface=true)', () => {
|
|
53
|
+
const { dir, db } = freshRoom();
|
|
54
|
+
for (let i = 0; i < 3; i++) {
|
|
55
|
+
const result = navigation.logMemoryEvent(db, 'f_selector_decision', {
|
|
56
|
+
venture_classified: true,
|
|
57
|
+
classification_source: 'haiku-4-5',
|
|
58
|
+
source_path: 'test:venture-shape-nudge',
|
|
59
|
+
created_by: 'system',
|
|
60
|
+
});
|
|
61
|
+
assert.equal(result.ok, true, 'logMemoryEvent should accept f_selector_decision; got reason=' + (result.reason || ''));
|
|
62
|
+
}
|
|
63
|
+
closeQuiet(db);
|
|
64
|
+
const r = ventureShapeNudge.shouldSurfaceNudge(dir, {});
|
|
65
|
+
assert.equal(r.surface, true);
|
|
66
|
+
assert.equal(r.turn_count, 3);
|
|
67
|
+
assert.equal(r.threshold, 3);
|
|
68
|
+
assert.equal(r.skip_reason, undefined);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('Test 3: D-01 invariant -- auto_explore_fired short-circuits regardless of turn count', () => {
|
|
72
|
+
const { dir, db } = freshRoom();
|
|
73
|
+
// Seed 5 venture-shaped turns AND one auto_explore_fired event.
|
|
74
|
+
for (let i = 0; i < 5; i++) {
|
|
75
|
+
navigation.logMemoryEvent(db, 'f_selector_decision', {
|
|
76
|
+
venture_classified: true,
|
|
77
|
+
classification_source: 'haiku-4-5',
|
|
78
|
+
source_path: 'test:venture-shape-nudge',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
navigation.logMemoryEvent(db, 'auto_explore_fired', {
|
|
82
|
+
material_id: 'abcd1234',
|
|
83
|
+
relative_file_path: 'docs/sample.md',
|
|
84
|
+
room_slug: 'test-room',
|
|
85
|
+
tier: 1,
|
|
86
|
+
surfacing_count: 0,
|
|
87
|
+
brain_baseline_present: false,
|
|
88
|
+
source_path: 'test:venture-shape-nudge',
|
|
89
|
+
});
|
|
90
|
+
closeQuiet(db);
|
|
91
|
+
const r = ventureShapeNudge.shouldSurfaceNudge(dir, {});
|
|
92
|
+
assert.equal(r.surface, false);
|
|
93
|
+
assert.equal(r.skip_reason, 'upload_path_active');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('Test 4: venture_classification_unavailable safe-default when field absent', () => {
|
|
97
|
+
const { dir, db } = freshRoom();
|
|
98
|
+
// Seed events without venture_classified field at all.
|
|
99
|
+
for (let i = 0; i < 5; i++) {
|
|
100
|
+
navigation.logMemoryEvent(db, 'f_selector_decision', {
|
|
101
|
+
command: 'mos:test',
|
|
102
|
+
framework: 'test-framework',
|
|
103
|
+
source_path: 'test:venture-shape-nudge',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
closeQuiet(db);
|
|
107
|
+
const r = ventureShapeNudge.shouldSurfaceNudge(dir, {});
|
|
108
|
+
assert.equal(r.surface, false);
|
|
109
|
+
assert.equal(r.skip_reason, 'venture_classification_unavailable');
|
|
110
|
+
assert.equal(r.turn_count, 0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('Test 5: mixed venture_classified true/false -- only true counts toward threshold', () => {
|
|
114
|
+
const { dir, db } = freshRoom();
|
|
115
|
+
// 3 true + 1 false; threshold=3 met.
|
|
116
|
+
for (let i = 0; i < 3; i++) {
|
|
117
|
+
navigation.logMemoryEvent(db, 'f_selector_decision', {
|
|
118
|
+
venture_classified: true,
|
|
119
|
+
classification_source: 'haiku-4-5',
|
|
120
|
+
source_path: 'test:venture-shape-nudge',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
navigation.logMemoryEvent(db, 'f_selector_decision', {
|
|
124
|
+
venture_classified: false,
|
|
125
|
+
classification_source: 'haiku-4-5',
|
|
126
|
+
source_path: 'test:venture-shape-nudge',
|
|
127
|
+
});
|
|
128
|
+
closeQuiet(db);
|
|
129
|
+
const r = ventureShapeNudge.shouldSurfaceNudge(dir, {});
|
|
130
|
+
assert.equal(r.surface, true);
|
|
131
|
+
assert.equal(r.turn_count, 3);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('Test 6: no Brain client require -- source-grep audit', () => {
|
|
135
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'lib', 'core', 'venture-shape-nudge.cjs'), 'utf8');
|
|
136
|
+
// Canon Part 8: zero brain-client require, zero mcp-server-brain require,
|
|
137
|
+
// zero fetch to brain.mindrian.
|
|
138
|
+
assert.equal(/require\([^)]*brain-client/.test(src), false, 'brain-client require detected');
|
|
139
|
+
assert.equal(/require\([^)]*mcp-server-brain/.test(src), false, 'mcp-server-brain require detected');
|
|
140
|
+
assert.equal(/brain\.mindrian/.test(src), false, 'brain.mindrian fetch reference detected');
|
|
141
|
+
// Canon Part 9: zero direct room-db.cjs require.
|
|
142
|
+
assert.equal(/require\([^)]*room-db\.cjs/.test(src), false, 'direct room-db.cjs require detected');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('Test 7: no-room-dir guard returns safe default', () => {
|
|
146
|
+
const r = ventureShapeNudge.shouldSurfaceNudge('', {});
|
|
147
|
+
assert.equal(r.surface, false);
|
|
148
|
+
assert.equal(r.skip_reason, 'no_room_dir');
|
|
149
|
+
|
|
150
|
+
const r2 = ventureShapeNudge.shouldSurfaceNudge(null, {});
|
|
151
|
+
assert.equal(r2.surface, false);
|
|
152
|
+
|
|
153
|
+
// Non-existent path -> no_room_db.
|
|
154
|
+
const r3 = ventureShapeNudge.shouldSurfaceNudge('/tmp/p119-nonexistent-' + Date.now(), {});
|
|
155
|
+
assert.equal(r3.surface, false);
|
|
156
|
+
assert.equal(r3.skip_reason, 'no_room_db');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('Test 8: VENTURE_NUDGE_THRESHOLD constant exported and equals 3', () => {
|
|
160
|
+
assert.equal(ventureShapeNudge.VENTURE_NUDGE_THRESHOLD, 3);
|
|
161
|
+
});
|
package/lib/core/visual-ops.cjs
CHANGED
|
@@ -173,8 +173,19 @@ function formatSectionHeader(sectionName, entryCount, stage) {
|
|
|
173
173
|
// MERMAID DIAGRAM GENERATORS
|
|
174
174
|
// ═══════════════════════════════════════════════════════════════
|
|
175
175
|
|
|
176
|
-
// De Stijl hex colors for Mermaid style directives (no ANSI in diagrams)
|
|
177
|
-
|
|
176
|
+
// De Stijl hex colors for Mermaid style directives (no ANSI in diagrams).
|
|
177
|
+
//
|
|
178
|
+
// Phase 121.5-03: DS_HEX is now SOURCED FROM references/visual/palette.json
|
|
179
|
+
// (the canonical De Stijl source-of-truth per ODD 3 resolution + Canon Part 7).
|
|
180
|
+
// The literal fallback below is graceful degradation only -- if palette.json
|
|
181
|
+
// fails to load (unlikely in a properly-installed plugin), the shipping
|
|
182
|
+
// values are frozen here verbatim from the pre-121.5 hardcoded DS_HEX so
|
|
183
|
+
// downstream Mermaid + dashboard rendering keeps working.
|
|
184
|
+
//
|
|
185
|
+
// Test seam: MINDRIAN_PALETTE_PATH env var overrides the default palette
|
|
186
|
+
// path. Used by lib/memory/statusline-two-row.test.cjs (test 11) to verify
|
|
187
|
+
// the wiring without touching the live palette.json.
|
|
188
|
+
const FALLBACK_DS_HEX = {
|
|
178
189
|
red: '#A63D2F',
|
|
179
190
|
blue: '#1E3A6E',
|
|
180
191
|
yellow: '#C8A43C',
|
|
@@ -186,6 +197,63 @@ const DS_HEX = {
|
|
|
186
197
|
bg: '#0D0D0D'
|
|
187
198
|
};
|
|
188
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Compose DS_HEX from references/visual/palette.json. Preserves the legacy
|
|
202
|
+
* key names (red / blue / yellow / green / sienna / amethyst / cream / muted
|
|
203
|
+
* / bg) so existing consumers (Mermaid generators, dashboard exporters) keep
|
|
204
|
+
* working byte-identical. Also exposes the palette tier keys via dot
|
|
205
|
+
* extension for new consumers (e.g. mondrian_red, muted_green, saturated_blue).
|
|
206
|
+
*/
|
|
207
|
+
function loadPaletteDsHex() {
|
|
208
|
+
const path = require('path');
|
|
209
|
+
const fs = require('fs');
|
|
210
|
+
const candidate = process.env.MINDRIAN_PALETTE_PATH ||
|
|
211
|
+
path.join(__dirname, '..', '..', 'references', 'visual', 'palette.json');
|
|
212
|
+
try {
|
|
213
|
+
const palette = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
214
|
+
const base = palette.base || {};
|
|
215
|
+
const a = palette.palette_a_discovery || {};
|
|
216
|
+
const b = palette.palette_b_build || {};
|
|
217
|
+
const ext = palette.extended || {};
|
|
218
|
+
const composed = {
|
|
219
|
+
// Legacy DS_HEX keys (preserve backward compat for Mermaid + dashboard)
|
|
220
|
+
red: base.mondrian_red || FALLBACK_DS_HEX.red,
|
|
221
|
+
blue: base.mondrian_blue || FALLBACK_DS_HEX.blue,
|
|
222
|
+
yellow: base.mondrian_yellow || FALLBACK_DS_HEX.yellow,
|
|
223
|
+
green: base.success_green || FALLBACK_DS_HEX.green,
|
|
224
|
+
sienna: ext.sienna || FALLBACK_DS_HEX.sienna,
|
|
225
|
+
amethyst: base.amethyst || FALLBACK_DS_HEX.amethyst,
|
|
226
|
+
cream: base.cream || FALLBACK_DS_HEX.cream,
|
|
227
|
+
muted: base.gray_meta || FALLBACK_DS_HEX.muted,
|
|
228
|
+
bg: ext.bg || base.mondrian_black || FALLBACK_DS_HEX.bg,
|
|
229
|
+
// Tier-keyed mirrors (new consumers can use the explicit names)
|
|
230
|
+
mondrian_red: base.mondrian_red,
|
|
231
|
+
mondrian_blue: base.mondrian_blue,
|
|
232
|
+
mondrian_yellow: base.mondrian_yellow,
|
|
233
|
+
mondrian_black: base.mondrian_black,
|
|
234
|
+
mondrian_white: base.mondrian_white,
|
|
235
|
+
gray_meta: base.gray_meta,
|
|
236
|
+
success_green: base.success_green,
|
|
237
|
+
muted_red: a.muted_red,
|
|
238
|
+
muted_green: a.muted_green,
|
|
239
|
+
teal_accent: a.teal_accent,
|
|
240
|
+
saturated_red: b.saturated_red,
|
|
241
|
+
saturated_blue: b.saturated_blue,
|
|
242
|
+
saturated_yellow: b.saturated_yellow,
|
|
243
|
+
};
|
|
244
|
+
return composed;
|
|
245
|
+
} catch (e) {
|
|
246
|
+
// Graceful degradation per Canon Part 7. One warning to stderr, then
|
|
247
|
+
// fall back to the frozen literal.
|
|
248
|
+
if (process.env.MINDRIAN_PALETTE_DEBUG) {
|
|
249
|
+
try { process.stderr.write('[visual-ops] palette.json load failed: ' + e.message + '\n'); } catch (_) {}
|
|
250
|
+
}
|
|
251
|
+
return Object.assign({}, FALLBACK_DS_HEX);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const DS_HEX = loadPaletteDsHex();
|
|
256
|
+
|
|
189
257
|
// Map stage names to hex colors for Mermaid
|
|
190
258
|
const STAGE_HEX = {
|
|
191
259
|
preopportunity: DS_HEX.muted,
|
|
@@ -70,6 +70,14 @@
|
|
|
70
70
|
|
|
71
71
|
'use strict';
|
|
72
72
|
|
|
73
|
+
// Phase 121-02 D-04: unified-stream selector_pick emit chokepoint.
|
|
74
|
+
// require()-loaded lazily inside emitSelectorPickUnified() so a missing writer
|
|
75
|
+
// module (or a stripped-down install) cannot crash the dispatcher at load.
|
|
76
|
+
const crypto = require('node:crypto');
|
|
77
|
+
function _sha256(s) {
|
|
78
|
+
return crypto.createHash('sha256').update(String(s || '')).digest('hex');
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
const FREE_TEXT = 'Free-Text';
|
|
74
82
|
const MODE_B_ZONE1_PREFIX = '⚠ Brain unreachable; running on local graph only.';
|
|
75
83
|
|
|
@@ -79,7 +87,12 @@ const MODE_B_ZONE1_PREFIX = '⚠ Brain unreachable; running on local graph only.
|
|
|
79
87
|
// collision-safe lib/hmi/shape-f6-plan-review-renderer.cjs path; the umbrella 'F'
|
|
80
88
|
// branch continues to route to Phase 101-01's lib/hmi/shape-f6-renderer.cjs via
|
|
81
89
|
// JTBD logic preserved byte-stable per R1 invariant).
|
|
82
|
-
|
|
90
|
+
// Phase 120-01: 'F.7' Breakthrough Surface appended (CONTEXT.md D-07..D-10 + D-20). Closed-vocab
|
|
91
|
+
// 5 verbs verbatim [Explore deeper]/[Confirm]/[File as decision]/[Dismiss]/[Back]; mandatory
|
|
92
|
+
// [Dismiss] guard (D-10). Routes to lib/hmi/shape-f7-breakthrough-renderer.cjs via the
|
|
93
|
+
// explicit requestedShape:'F.7' branch in dispatchShapeFSubShape. The umbrella 'F' branch
|
|
94
|
+
// does NOT resolve to F.7 -- breakthrough surfacing is opt-in via explicit dispatch.
|
|
95
|
+
const F_SUBSHAPES = ['F.0', 'F.1', 'F.2', 'F.3', 'F.4', 'F.5', 'F.6', 'F.7'];
|
|
83
96
|
const F_UMBRELLA = 'F';
|
|
84
97
|
const JUST_TALK = 'JUST_TALK';
|
|
85
98
|
const COMPACTION_VIOLATION_CODE = 'render_v2_compaction_violation';
|
|
@@ -175,6 +188,58 @@ function justTalkRefuse(requestedShape) {
|
|
|
175
188
|
};
|
|
176
189
|
}
|
|
177
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Phase 121-02 D-04: emit selector_pick event into the unified
|
|
193
|
+
* ~/.mindrian/telemetry/v1.13/events-YYYY-WNN.jsonl stream (Plan 121-00
|
|
194
|
+
* writer chokepoint). Wrapped in try/catch -- telemetry NEVER fails Larry's
|
|
195
|
+
* turn. Canon Part 8: room slug is sha256-hashed; payload fields are scalar
|
|
196
|
+
* enums / numbers / booleans only. The emit is gated by validateEventPayload
|
|
197
|
+
* inside writer.emit() so adversarial fixtures (Cypher in verb_chosen, etc.)
|
|
198
|
+
* cannot leak. Non-blocking: any thrown error is swallowed so the pickShape
|
|
199
|
+
* dispatch resolution still returns to the caller.
|
|
200
|
+
*/
|
|
201
|
+
function emitSelectorPickUnified(roomDir, subShape, rendered, payloadIn) {
|
|
202
|
+
try {
|
|
203
|
+
let writer;
|
|
204
|
+
try {
|
|
205
|
+
writer = require('../core/telemetry/writer.cjs');
|
|
206
|
+
} catch (_e) {
|
|
207
|
+
return; // missing writer module -- soft skip, do not crash dispatch
|
|
208
|
+
}
|
|
209
|
+
if (!writer || typeof writer.emit !== 'function') return;
|
|
210
|
+
const contract = (rendered && rendered.contract) ? rendered.contract : {};
|
|
211
|
+
const verbs = Array.isArray(contract.verbs) ? contract.verbs : [];
|
|
212
|
+
const mode = (typeof contract.mode === 'string') ? contract.mode : 'A';
|
|
213
|
+
const recommended = contract.recommended;
|
|
214
|
+
const payload = (payloadIn && typeof payloadIn === 'object') ? payloadIn : {};
|
|
215
|
+
const rankerConfidence = (typeof payload.rankerConfidence === 'number')
|
|
216
|
+
? payload.rankerConfidence
|
|
217
|
+
: 0;
|
|
218
|
+
const recommendedRendered = (recommended !== null && recommended !== undefined)
|
|
219
|
+
? true
|
|
220
|
+
: Boolean(payload.recommendedRendered);
|
|
221
|
+
const verbChosen = (typeof payload.verb_chosen === 'string' && payload.verb_chosen.length > 0)
|
|
222
|
+
? payload.verb_chosen
|
|
223
|
+
: (typeof contract.recommended === 'string' ? contract.recommended : '');
|
|
224
|
+
const slugSource = (typeof roomDir === 'string' && roomDir.length > 0)
|
|
225
|
+
? String(roomDir).split('/').filter(Boolean).pop() || roomDir
|
|
226
|
+
: (String(process.cwd()).split('/').filter(Boolean).pop() || 'default-room');
|
|
227
|
+
writer.emit('selector_pick', {
|
|
228
|
+
sub_shape: String(subShape || '').slice(0, 64),
|
|
229
|
+
mode: String(mode || 'A').slice(0, 64),
|
|
230
|
+
ranker_confidence: rankerConfidence,
|
|
231
|
+
recommended_rendered: Boolean(recommendedRendered),
|
|
232
|
+
options_count: Number.isInteger(verbs.length) ? verbs.length : 0,
|
|
233
|
+
room_slug_sha256: _sha256(slugSource),
|
|
234
|
+
verb_chosen: String(verbChosen || '').slice(0, 64),
|
|
235
|
+
});
|
|
236
|
+
} catch (_e) {
|
|
237
|
+
// Canon Part 8 validation failure or any other emit failure: swallow.
|
|
238
|
+
// The dispatch resolution must still return cleanly per non-blocking
|
|
239
|
+
// contract (selector-pick-capture test 3 verifies this).
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
178
243
|
/**
|
|
179
244
|
* Phase 88.2-04: emit selector presentation telemetry. Wrapped in try/catch --
|
|
180
245
|
* telemetry NEVER fails Larry's turn. Canon Part 8: LOCAL ledger only, room
|
|
@@ -354,6 +419,25 @@ function dispatchShapeFSubShape(requestedShape, args) {
|
|
|
354
419
|
personaContext: (typeof payloadObj.personaContext === 'string' && payloadObj.personaContext.length > 0)
|
|
355
420
|
? payloadObj.personaContext : undefined,
|
|
356
421
|
};
|
|
422
|
+
} else if (requestedShape === 'F.7') {
|
|
423
|
+
// Phase 120-01: F.7 Breakthrough Surface (closed-vocab; 5 verbs verbatim).
|
|
424
|
+
// Per CONTEXT.md D-07: built NEW (not a reuse of F.4).
|
|
425
|
+
// Per CONTEXT.md D-08: 5 verbs locked verbatim [Explore deeper]/[Confirm]/
|
|
426
|
+
// [File as decision]/[Dismiss]/[Back].
|
|
427
|
+
// Per CONTEXT.md D-10: [Dismiss] is MANDATORY (renderer asserts presence on
|
|
428
|
+
// every render).
|
|
429
|
+
// Per CONTEXT.md D-20: payload.breakthrough carries provenance.artifact_ids[] --
|
|
430
|
+
// F.7 renderer refuses to render if artifact_ids.length === 0 (HARD FLOOR
|
|
431
|
+
// structural enforcement at the surface layer; defense in depth on top of
|
|
432
|
+
// writeBreakthrough's validateProvenance in lib/core/breakthrough/schema.cjs).
|
|
433
|
+
mod = safeRequire('./shape-f7-breakthrough-renderer.cjs');
|
|
434
|
+
fnName = 'renderShapeF7Breakthrough';
|
|
435
|
+
inputArgs = {
|
|
436
|
+
tier: tier,
|
|
437
|
+
header: callerHeader,
|
|
438
|
+
breakthrough: (payloadObj.breakthrough && typeof payloadObj.breakthrough === 'object') ? payloadObj.breakthrough : null,
|
|
439
|
+
more_count: (Number.isInteger(payloadObj.more_count) && payloadObj.more_count >= 0) ? payloadObj.more_count : 0,
|
|
440
|
+
};
|
|
357
441
|
}
|
|
358
442
|
|
|
359
443
|
if (!mod || typeof mod[fnName] !== 'function') {
|
|
@@ -496,6 +580,11 @@ function pickShape(args) {
|
|
|
496
580
|
const payloadObj = (opts.payload && typeof opts.payload === 'object') ? opts.payload : {};
|
|
497
581
|
if (payloadObj.emitTelemetry === true) {
|
|
498
582
|
emitPresentationTelemetry(opts.roomDir, result.shape, result.rendered, operator);
|
|
583
|
+
// Phase 121-02 D-04: capture selector pick (88.2/125 ranker) into
|
|
584
|
+
// the unified ~/.mindrian/telemetry/v1.13/events-YYYY-WNN.jsonl
|
|
585
|
+
// stream (Plan 121-00 writer chokepoint). Non-blocking; never
|
|
586
|
+
// throws back to caller.
|
|
587
|
+
emitSelectorPickUnified(opts.roomDir, result.shape, result.rendered, payloadObj);
|
|
499
588
|
}
|
|
500
589
|
}
|
|
501
590
|
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 120-01 -- Shape F.7 Breakthrough Surface renderer.
|
|
5
|
+
*
|
|
6
|
+
* Per CONTEXT.md D-07..D-10 (the 5 verbs LOCKED VERBATIM + closed-vocab + mandatory dismiss):
|
|
7
|
+
* - Built NEW (D-07): F.4 maps content-to-user-actionable-observation;
|
|
8
|
+
* F.7 maps user-actions-to-recognition-of-user-own-work. Different speech acts.
|
|
9
|
+
* - 5 verbs LOCKED VERBATIM (D-08): [Explore deeper] / [Confirm] / [File as decision] /
|
|
10
|
+
* [Dismiss] / [Back]. Order is structural, not stylistic.
|
|
11
|
+
* - [File as decision] (D-09) emits breakthrough_filed_as_decision via the Plan 120-02
|
|
12
|
+
* scanner consumer; F.7 only renders the verb -- it does NOT itself emit events.
|
|
13
|
+
* - [Dismiss] (D-10) is MANDATORY -- the renderer asserts its presence; refuses to
|
|
14
|
+
* render a contract without it (defense in depth against accidental drift).
|
|
15
|
+
*
|
|
16
|
+
* Per CONTEXT.md D-11..D-12 (top-1 with "More breakthroughs (N)" affordance):
|
|
17
|
+
* - Top-1 surface; "More breakthroughs (N)" affordance for queued candidates.
|
|
18
|
+
* - more_count is rendered in zones.footer IFF more_count > 0.
|
|
19
|
+
*
|
|
20
|
+
* Per CONTEXT.md D-20 HARD FLOOR (Cypher-provable):
|
|
21
|
+
* - breakthrough.artifact_ids[] must be non-empty AT RENDER TIME. The renderer is
|
|
22
|
+
* the second structural enforcement point (the first is writeBreakthrough in
|
|
23
|
+
* Plan 120-00 schema.cjs). Defense in depth: if a caller somehow bypasses
|
|
24
|
+
* writeBreakthrough's validateProvenance, F.7 refuses to render the surfacing.
|
|
25
|
+
*
|
|
26
|
+
* Per CONTEXT.md D-17 voice scaffold rule 3 (time anchor):
|
|
27
|
+
* - The title line includes a human-readable time anchor (e.g., "in the last 8 hours")
|
|
28
|
+
* derived from breakthrough.detected_at via formatTimeAnchor.
|
|
29
|
+
*
|
|
30
|
+
* Canon Part 3: F.7 IS a Decision Gate primitive instance. LOCAL context only
|
|
31
|
+
* (the breakthrough artifact_ids + theme); BRAIN context is none (D-20 makes
|
|
32
|
+
* breakthroughs locally Cypher-provable); SIGNAL context is none (cross-room
|
|
33
|
+
* forbidden per Part 8).
|
|
34
|
+
*
|
|
35
|
+
* Canon Part 4: every choice is graph data. Each of the 5 verbs maps to a typed
|
|
36
|
+
* event the Plan 120-02 scanner consumer emits at verb-dispatch time.
|
|
37
|
+
*
|
|
38
|
+
* Canon Part 8: zero Brain coupling. The renderer is pure: no FS reads, no DB
|
|
39
|
+
* writes, no fetch calls. Mirrors the F.4 / F.6 pattern byte-for-byte.
|
|
40
|
+
*
|
|
41
|
+
* Pure CJS, node built-ins only, zero deps (Phase 87 invariant).
|
|
42
|
+
*/
|
|
43
|
+
'use strict';
|
|
44
|
+
|
|
45
|
+
// D-08: the 5 verbs LOCKED VERBATIM. Object.freeze prevents accidental mutation
|
|
46
|
+
// of the canonical array; downstream code that needs a mutable copy must call
|
|
47
|
+
// F7_VERBS.slice() (the dispatcher does this in inputArgs).
|
|
48
|
+
const F7_VERBS = Object.freeze([
|
|
49
|
+
'Explore deeper',
|
|
50
|
+
'Confirm',
|
|
51
|
+
'File as decision',
|
|
52
|
+
'Dismiss',
|
|
53
|
+
'Back',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
// D-10: structural index of 'Dismiss' in F7_VERBS. assertContractInvariants uses
|
|
57
|
+
// this to surface a precise 'dismiss_required' reason when the contract drifts.
|
|
58
|
+
const F7_DISMISS_INDEX = 3;
|
|
59
|
+
|
|
60
|
+
// Map the four detector kinds Plan 120-00 emits to their canonical display
|
|
61
|
+
// strings. Unknown kinds fall back to the raw kind string (and finally to
|
|
62
|
+
// 'Pattern' if kind is falsy).
|
|
63
|
+
const KIND_DISPLAY_NAMES = Object.freeze({
|
|
64
|
+
convergence: 'Convergence',
|
|
65
|
+
contradiction_resolved: 'Contradiction resolved',
|
|
66
|
+
cross_domain_analogy: 'Cross-domain analogy',
|
|
67
|
+
reverse_salient_closed: 'Reverse salient closed',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// D-17 rule 3: human-readable time anchor with bucket boundaries chosen to
|
|
71
|
+
// match the spec examples (in the last hour / in the last N hours / today /
|
|
72
|
+
// in the last day / this week / in the last two weeks / on YYYY-MM-DD).
|
|
73
|
+
// D-06 ethical fence: ages > 14 days surface the calendar date verbatim so
|
|
74
|
+
// the user can see exactly how stale the recognition is.
|
|
75
|
+
function formatTimeAnchor(detected_at) {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
const ts = (typeof detected_at === 'number' && Number.isFinite(detected_at)) ? detected_at : now;
|
|
78
|
+
const ageMs = Math.max(0, now - ts);
|
|
79
|
+
const ageHours = ageMs / (3600 * 1000);
|
|
80
|
+
const ageDays = ageHours / 24;
|
|
81
|
+
if (ageHours < 1) return 'in the last hour';
|
|
82
|
+
if (ageHours < 8) return 'in the last ' + Math.floor(ageHours) + ' hours';
|
|
83
|
+
if (ageHours < 24) return 'today';
|
|
84
|
+
if (ageDays < 2) return 'in the last day';
|
|
85
|
+
if (ageDays < 7) return 'this week';
|
|
86
|
+
if (ageDays < 14) return 'in the last two weeks';
|
|
87
|
+
// Older than the 14-day ethical fence (D-06) -- include calendar date for transparency.
|
|
88
|
+
const d = new Date(ts);
|
|
89
|
+
const yyyy = d.getUTCFullYear();
|
|
90
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
91
|
+
const dd = String(d.getUTCDate()).padStart(2, '0');
|
|
92
|
+
return 'on ' + yyyy + '-' + mm + '-' + dd;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// D-10 MANDATORY-Dismiss structural guard. Returns {ok:true} when the verbs
|
|
96
|
+
// array deep-equals F7_VERBS; otherwise returns {ok:false, reason} with a
|
|
97
|
+
// precise reason code so consumers can surface a meaningful error envelope.
|
|
98
|
+
//
|
|
99
|
+
// Reason codes:
|
|
100
|
+
// verb_count_mismatch -- verbs.length !== 5
|
|
101
|
+
// dismiss_required -- the slot at F7_DISMISS_INDEX is not literally 'Dismiss'
|
|
102
|
+
// verb_order_mismatch -- some other slot diverges from the canonical array
|
|
103
|
+
function assertContractInvariants(verbs) {
|
|
104
|
+
if (!Array.isArray(verbs) || verbs.length !== F7_VERBS.length) {
|
|
105
|
+
return { ok: false, reason: 'verb_count_mismatch' };
|
|
106
|
+
}
|
|
107
|
+
for (let i = 0; i < F7_VERBS.length; i++) {
|
|
108
|
+
if (verbs[i] !== F7_VERBS[i]) {
|
|
109
|
+
return { ok: false, reason: i === F7_DISMISS_INDEX ? 'dismiss_required' : 'verb_order_mismatch' };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { ok: true };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Render the F.7 Breakthrough Surface.
|
|
117
|
+
*
|
|
118
|
+
* @param {object} args
|
|
119
|
+
* @param {number} [args.tier] tier integer; informational only -- F.7 is closed-vocab
|
|
120
|
+
* in every tier (no recommended marker; no mode branch)
|
|
121
|
+
* @param {string} [args.header] caller-supplied header line; default 'Breakthrough'
|
|
122
|
+
* @param {object} args.breakthrough the breakthrough candidate (must carry artifact_ids[])
|
|
123
|
+
* @param {number} [args.more_count] non-negative integer; number of queued candidates
|
|
124
|
+
* surfaced via the "More breakthroughs (N)" footer
|
|
125
|
+
* @param {string} [args.voice_line] Phase 120-03 D-17 additive slot. When present + non-empty,
|
|
126
|
+
* prepended as the first line of zones.body above the title.
|
|
127
|
+
* Caller (Plan 120-03 scanner.surfaceBreakthrough) audits
|
|
128
|
+
* the line via voice-scaffold.auditVoiceLine before passing;
|
|
129
|
+
* this renderer does NOT re-audit (single source of truth).
|
|
130
|
+
*
|
|
131
|
+
* @returns {object} { zones: {header, body, footer}, contract: {...} } OR
|
|
132
|
+
* { error: 'provenance_required' | 'dismiss_required' | 'verb_count_mismatch' | 'verb_order_mismatch' }
|
|
133
|
+
*/
|
|
134
|
+
function renderShapeF7Breakthrough(args) {
|
|
135
|
+
const a = (args && typeof args === 'object') ? args : {};
|
|
136
|
+
const more_count = (Number.isInteger(a.more_count) && a.more_count >= 0) ? a.more_count : 0;
|
|
137
|
+
const bk = a.breakthrough;
|
|
138
|
+
|
|
139
|
+
// D-20 HARD FLOOR defense in depth -- refuse to render a provenance-less
|
|
140
|
+
// breakthrough. The Plan 120-00 schema layer rejects writes with empty
|
|
141
|
+
// artifact_ids; this is the SECOND structural enforcement point at the
|
|
142
|
+
// surface layer (in case writeBreakthrough is somehow bypassed).
|
|
143
|
+
if (!bk || typeof bk !== 'object' || !Array.isArray(bk.artifact_ids) || bk.artifact_ids.length === 0) {
|
|
144
|
+
return { error: 'provenance_required' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Build the locked-verbatim verbs array and assert the D-10 invariants.
|
|
148
|
+
// Defense in depth: the source-of-truth F7_VERBS is Object.freeze-d above;
|
|
149
|
+
// assertContractInvariants on the mutable copy gives a final structural
|
|
150
|
+
// assertion before composition.
|
|
151
|
+
const verbs = F7_VERBS.slice();
|
|
152
|
+
const check = assertContractInvariants(verbs);
|
|
153
|
+
if (!check.ok) {
|
|
154
|
+
return { error: check.reason };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Row 1 (title): kind capitalized + theme + time anchor.
|
|
158
|
+
// Theme is sliced to 120 chars at the surface layer (defense in depth on top
|
|
159
|
+
// of the schema-layer 200-char sanitizer in Plan 120-00).
|
|
160
|
+
const kindKey = (typeof bk.kind === 'string') ? bk.kind : '';
|
|
161
|
+
const kindDisplay = KIND_DISPLAY_NAMES[kindKey] || kindKey || 'Pattern';
|
|
162
|
+
const themeRaw = (typeof bk.theme === 'string') ? bk.theme : '';
|
|
163
|
+
const theme = themeRaw.slice(0, 120);
|
|
164
|
+
const timeAnchor = formatTimeAnchor(bk.detected_at || Date.now());
|
|
165
|
+
const titleLine = kindDisplay + (theme ? ': ' + theme : '') + ' (' + timeAnchor + ')';
|
|
166
|
+
|
|
167
|
+
// Row 2 (verbs): the 5 verbs in [X] brackets, separated by double-space.
|
|
168
|
+
// Bracket style matches the F.4 / F.6 closed-vocab convention.
|
|
169
|
+
const verbsLine = verbs.map(function (v) { return '[' + v + ']'; }).join(' ');
|
|
170
|
+
|
|
171
|
+
// Phase 120-03 D-17 additive: optional voice_line prepend.
|
|
172
|
+
//
|
|
173
|
+
// When args.voice_line is a non-empty string, it is prepended as the FIRST line
|
|
174
|
+
// of zones.body, above the title line. This is the conversational opener
|
|
175
|
+
// composed by Plan 120-03 voice-scaffold.composeBreakthroughVoiceLine. The
|
|
176
|
+
// caller (scanner.surfaceBreakthrough) audits the line via auditVoiceLine
|
|
177
|
+
// before passing; this renderer does NOT re-audit (single source of truth, no
|
|
178
|
+
// duplicated regex logic at the surface layer).
|
|
179
|
+
//
|
|
180
|
+
// The contract.verbs / freeTextOffered / recommended / breakthrough_id /
|
|
181
|
+
// more_count fields are PRESERVED byte-stably across both branches (with-line
|
|
182
|
+
// and without-line). Plan 120-01 11-test byte-stability invariant is preserved
|
|
183
|
+
// when voice_line is absent (the without-line branch is the legacy code path).
|
|
184
|
+
const voiceLineRaw = (typeof a.voice_line === 'string') ? a.voice_line : '';
|
|
185
|
+
const voiceLine = voiceLineRaw.length > 0 ? voiceLineRaw : '';
|
|
186
|
+
const body = voiceLine
|
|
187
|
+
? (voiceLine + '\n' + titleLine + '\n' + verbsLine)
|
|
188
|
+
: (titleLine + '\n' + verbsLine);
|
|
189
|
+
|
|
190
|
+
const header = (typeof a.header === 'string' && a.header.length > 0) ? a.header : 'Breakthrough';
|
|
191
|
+
// D-11 affordance: only surface "More breakthroughs (N)" when N > 0; empty
|
|
192
|
+
// footer otherwise so the renderer never lies about queued candidates.
|
|
193
|
+
const footer = (more_count > 0) ? ('More breakthroughs (' + more_count + ')') : '';
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
zones: {
|
|
197
|
+
header: header,
|
|
198
|
+
body: body,
|
|
199
|
+
signals: '',
|
|
200
|
+
footer: footer,
|
|
201
|
+
},
|
|
202
|
+
contract: {
|
|
203
|
+
shape: 'F.7',
|
|
204
|
+
keyboard: 'askuserquestion',
|
|
205
|
+
verbs: verbs,
|
|
206
|
+
freeTextOffered: false, // closed-vocab; mirrors F.3 / F.4 / F.6 plan-review
|
|
207
|
+
mode: 'closed',
|
|
208
|
+
recommended: null, // closed-vocab does not mark a recommended verb
|
|
209
|
+
breakthrough_id: bk.id,
|
|
210
|
+
more_count: more_count,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = {
|
|
216
|
+
renderShapeF7Breakthrough: renderShapeF7Breakthrough,
|
|
217
|
+
F7_VERBS: F7_VERBS,
|
|
218
|
+
F7_DISMISS_INDEX: F7_DISMISS_INDEX,
|
|
219
|
+
formatTimeAnchor: formatTimeAnchor,
|
|
220
|
+
assertContractInvariants: assertContractInvariants,
|
|
221
|
+
KIND_DISPLAY_NAMES: KIND_DISPLAY_NAMES,
|
|
222
|
+
};
|