@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,233 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 120-01 Task 1 -- F.7 Breakthrough Surface renderer test suite.
|
|
5
|
+
*
|
|
6
|
+
* Per 120-01-PLAN.md behavior tests (11 tests):
|
|
7
|
+
* T1 F7_VERBS verbatim lock (5 strings, exact order, Object.freeze invariant)
|
|
8
|
+
* T2 F7_DISMISS_INDEX === 3 (zero-indexed position of 'Dismiss')
|
|
9
|
+
* T3 renderShapeF7Breakthrough happy path (contract envelope)
|
|
10
|
+
* T4 Two-row layout (title + verbs)
|
|
11
|
+
* T5 [Dismiss] MANDATORY guard (D-10 -- assertContractInvariants refuses without 'Dismiss')
|
|
12
|
+
* T6 D-20 HARD FLOOR -- empty artifact_ids returns {error: 'provenance_required'}
|
|
13
|
+
* T7 "More breakthroughs (N)" affordance (D-11; only when more_count > 0)
|
|
14
|
+
* T8 Closed-vocab carve-out -- contract.freeTextOffered === false; no Free-Text
|
|
15
|
+
* T9 formatTimeAnchor buckets (in the last hour / today / this week / etc.)
|
|
16
|
+
* T10 Canon Part 8 source-grep (zero brain-client / fetch brain.mindrian / cross-room aggregat*)
|
|
17
|
+
* T11 Em-dash invariant (HARD RULE; zero U+2014 in the renderer source)
|
|
18
|
+
*
|
|
19
|
+
* Pure CJS, node built-ins only, zero deps (Phase 87 invariant).
|
|
20
|
+
*/
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const assert = require('node:assert/strict');
|
|
24
|
+
const test = require('node:test');
|
|
25
|
+
const fs = require('node:fs');
|
|
26
|
+
const path = require('node:path');
|
|
27
|
+
|
|
28
|
+
const RENDERER_PATH = path.resolve(__dirname, 'shape-f7-breakthrough-renderer.cjs');
|
|
29
|
+
const renderer = require('./shape-f7-breakthrough-renderer.cjs');
|
|
30
|
+
const {
|
|
31
|
+
renderShapeF7Breakthrough,
|
|
32
|
+
F7_VERBS,
|
|
33
|
+
F7_DISMISS_INDEX,
|
|
34
|
+
formatTimeAnchor,
|
|
35
|
+
assertContractInvariants,
|
|
36
|
+
KIND_DISPLAY_NAMES,
|
|
37
|
+
} = renderer;
|
|
38
|
+
|
|
39
|
+
// --- T1: F7_VERBS verbatim lock ---
|
|
40
|
+
test('T1: F7_VERBS is the 5 verbs verbatim in exact order', () => {
|
|
41
|
+
assert.deepEqual(F7_VERBS, ['Explore deeper', 'Confirm', 'File as decision', 'Dismiss', 'Back']);
|
|
42
|
+
assert.equal(F7_VERBS.length, 5);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('T1b: F7_VERBS is Object.freeze-d -- mutation does not change the array', () => {
|
|
46
|
+
const before = F7_VERBS.slice();
|
|
47
|
+
// Frozen arrays: push throws in strict mode OR silently no-ops in sloppy mode.
|
|
48
|
+
// Either way, the canonical array contents must be unchanged after the attempt.
|
|
49
|
+
try { F7_VERBS.push('Free-Text'); } catch (_e) { /* expected in strict */ }
|
|
50
|
+
assert.deepEqual(F7_VERBS.slice(0, 5), before);
|
|
51
|
+
assert.equal(F7_VERBS.length, 5, 'mutation must not extend the frozen array');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// --- T2: F7_DISMISS_INDEX ---
|
|
55
|
+
test('T2: F7_DISMISS_INDEX === 3 (zero-indexed slot of Dismiss in F7_VERBS)', () => {
|
|
56
|
+
assert.equal(F7_DISMISS_INDEX, 3);
|
|
57
|
+
assert.equal(F7_VERBS[F7_DISMISS_INDEX], 'Dismiss');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// --- T3: happy path ---
|
|
61
|
+
test('T3: renderShapeF7Breakthrough returns canonical {zones, contract} envelope', () => {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const out = renderShapeF7Breakthrough({
|
|
64
|
+
tier: 1,
|
|
65
|
+
breakthrough: {
|
|
66
|
+
id: 'bk:1',
|
|
67
|
+
kind: 'convergence',
|
|
68
|
+
theme: 'X',
|
|
69
|
+
confidence: 0.55,
|
|
70
|
+
artifact_ids: ['a1', 'a2', 'a3', 'a4'],
|
|
71
|
+
detected_at: now - 3600 * 1000,
|
|
72
|
+
},
|
|
73
|
+
more_count: 2,
|
|
74
|
+
});
|
|
75
|
+
assert.equal(typeof out, 'object');
|
|
76
|
+
assert.equal(out.contract.shape, 'F.7');
|
|
77
|
+
assert.deepEqual(out.contract.verbs, F7_VERBS);
|
|
78
|
+
assert.equal(out.contract.freeTextOffered, false);
|
|
79
|
+
assert.equal(out.contract.mode, 'closed');
|
|
80
|
+
assert.equal(out.contract.recommended, null);
|
|
81
|
+
assert.equal(out.contract.breakthrough_id, 'bk:1');
|
|
82
|
+
assert.equal(out.contract.more_count, 2);
|
|
83
|
+
assert.equal(typeof out.zones.header, 'string');
|
|
84
|
+
assert.equal(typeof out.zones.body, 'string');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// --- T4: two-row layout ---
|
|
88
|
+
test('T4: zones.body contains title row + 5-verb row with [X] brackets', () => {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const out = renderShapeF7Breakthrough({
|
|
91
|
+
tier: 1,
|
|
92
|
+
breakthrough: {
|
|
93
|
+
id: 'bk:1',
|
|
94
|
+
kind: 'convergence',
|
|
95
|
+
theme: 'X',
|
|
96
|
+
artifact_ids: ['a1', 'a2', 'a3'],
|
|
97
|
+
detected_at: now - 1800 * 1000,
|
|
98
|
+
},
|
|
99
|
+
more_count: 0,
|
|
100
|
+
});
|
|
101
|
+
// Row 1: title with kind capitalized + theme + time anchor
|
|
102
|
+
assert.match(out.zones.body, /Convergence: X/);
|
|
103
|
+
assert.match(out.zones.body, /\(in the last hour\)/i);
|
|
104
|
+
// Row 2: 5 verbs in [X] brackets in the locked order
|
|
105
|
+
assert.match(out.zones.body, /\[Explore deeper\]/);
|
|
106
|
+
assert.match(out.zones.body, /\[Confirm\]/);
|
|
107
|
+
assert.match(out.zones.body, /\[File as decision\]/);
|
|
108
|
+
assert.match(out.zones.body, /\[Dismiss\]/);
|
|
109
|
+
assert.match(out.zones.body, /\[Back\]/);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// --- T5: D-10 mandatory Dismiss guard ---
|
|
113
|
+
test('T5: assertContractInvariants refuses a verbs array missing Dismiss (D-10 MANDATORY)', () => {
|
|
114
|
+
// Direct invariant call: a verbs array with the same length but Dismiss replaced.
|
|
115
|
+
const broken = ['Explore deeper', 'Confirm', 'File as decision', 'NotDismiss', 'Back'];
|
|
116
|
+
const result = assertContractInvariants(broken);
|
|
117
|
+
assert.equal(result.ok, false);
|
|
118
|
+
assert.equal(result.reason, 'dismiss_required');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('T5b: assertContractInvariants refuses a wrong-count verbs array', () => {
|
|
122
|
+
const tooShort = ['Explore deeper', 'Confirm', 'File as decision', 'Dismiss'];
|
|
123
|
+
const r1 = assertContractInvariants(tooShort);
|
|
124
|
+
assert.equal(r1.ok, false);
|
|
125
|
+
assert.equal(r1.reason, 'verb_count_mismatch');
|
|
126
|
+
|
|
127
|
+
const wrong = ['Explore deeper', 'wrongverb', 'File as decision', 'Dismiss', 'Back'];
|
|
128
|
+
const r2 = assertContractInvariants(wrong);
|
|
129
|
+
assert.equal(r2.ok, false);
|
|
130
|
+
assert.equal(r2.reason, 'verb_order_mismatch');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// --- T6: D-20 HARD FLOOR -- empty artifact_ids ---
|
|
134
|
+
test('T6: D-20 HARD FLOOR -- empty artifact_ids returns {error: provenance_required}', () => {
|
|
135
|
+
const out = renderShapeF7Breakthrough({
|
|
136
|
+
tier: 1,
|
|
137
|
+
breakthrough: { id: 'bk:1', kind: 'convergence', theme: 'X', artifact_ids: [], detected_at: Date.now() },
|
|
138
|
+
more_count: 0,
|
|
139
|
+
});
|
|
140
|
+
assert.equal(typeof out, 'object');
|
|
141
|
+
assert.equal(out.error, 'provenance_required');
|
|
142
|
+
assert.equal(out.zones, undefined);
|
|
143
|
+
assert.equal(out.contract, undefined);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('T6b: missing breakthrough returns provenance_required', () => {
|
|
147
|
+
const out = renderShapeF7Breakthrough({ tier: 1, more_count: 0 });
|
|
148
|
+
assert.equal(out.error, 'provenance_required');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('T6c: artifact_ids not an array returns provenance_required', () => {
|
|
152
|
+
const out = renderShapeF7Breakthrough({
|
|
153
|
+
tier: 1,
|
|
154
|
+
breakthrough: { id: 'bk:1', kind: 'convergence', theme: 'X', artifact_ids: 'not-array', detected_at: Date.now() },
|
|
155
|
+
more_count: 0,
|
|
156
|
+
});
|
|
157
|
+
assert.equal(out.error, 'provenance_required');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// --- T7: More breakthroughs (N) affordance ---
|
|
161
|
+
test('T7: more_count > 0 renders "More breakthroughs (N)" in zones.footer', () => {
|
|
162
|
+
const out = renderShapeF7Breakthrough({
|
|
163
|
+
tier: 1,
|
|
164
|
+
breakthrough: { id: 'bk:1', kind: 'convergence', theme: 'X', artifact_ids: ['a1'], detected_at: Date.now() },
|
|
165
|
+
more_count: 2,
|
|
166
|
+
});
|
|
167
|
+
assert.match(out.zones.footer, /More breakthroughs \(2\)/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('T7b: more_count === 0 leaves zones.footer empty (no affordance)', () => {
|
|
171
|
+
const out = renderShapeF7Breakthrough({
|
|
172
|
+
tier: 1,
|
|
173
|
+
breakthrough: { id: 'bk:1', kind: 'convergence', theme: 'X', artifact_ids: ['a1'], detected_at: Date.now() },
|
|
174
|
+
more_count: 0,
|
|
175
|
+
});
|
|
176
|
+
assert.equal(out.zones.footer.indexOf('More breakthroughs'), -1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// --- T8: closed-vocab carve-out ---
|
|
180
|
+
test('T8: contract.freeTextOffered === false; verbs does NOT contain Free-Text', () => {
|
|
181
|
+
const out = renderShapeF7Breakthrough({
|
|
182
|
+
tier: 1,
|
|
183
|
+
breakthrough: { id: 'bk:1', kind: 'convergence', theme: 'X', artifact_ids: ['a1'], detected_at: Date.now() },
|
|
184
|
+
more_count: 0,
|
|
185
|
+
});
|
|
186
|
+
assert.equal(out.contract.freeTextOffered, false);
|
|
187
|
+
assert.equal(out.contract.verbs.indexOf('Free-Text'), -1);
|
|
188
|
+
assert.equal(out.contract.verbs.length, 5);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// --- T9: formatTimeAnchor buckets ---
|
|
192
|
+
test('T9: formatTimeAnchor returns the correct bucket string per age', () => {
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
// < 1 hour -> "in the last hour"
|
|
195
|
+
assert.equal(formatTimeAnchor(now - 30 * 60 * 1000), 'in the last hour');
|
|
196
|
+
// 1-8 hours -> "in the last N hours"
|
|
197
|
+
assert.match(formatTimeAnchor(now - 4 * 3600 * 1000), /in the last \d+ hours/);
|
|
198
|
+
// 8-24 hours -> "today"
|
|
199
|
+
assert.equal(formatTimeAnchor(now - 12 * 3600 * 1000), 'today');
|
|
200
|
+
// 1-2 days -> "in the last day"
|
|
201
|
+
assert.equal(formatTimeAnchor(now - 36 * 3600 * 1000), 'in the last day');
|
|
202
|
+
// 2-7 days -> "this week"
|
|
203
|
+
assert.equal(formatTimeAnchor(now - 4 * 24 * 3600 * 1000), 'this week');
|
|
204
|
+
// 7-14 days -> "in the last two weeks"
|
|
205
|
+
assert.equal(formatTimeAnchor(now - 10 * 24 * 3600 * 1000), 'in the last two weeks');
|
|
206
|
+
// > 14 days -> "on YYYY-MM-DD"
|
|
207
|
+
assert.match(formatTimeAnchor(now - 30 * 24 * 3600 * 1000), /^on \d{4}-\d{2}-\d{2}$/);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// --- T10: Canon Part 8 source-grep ---
|
|
211
|
+
test('T10: Canon Part 8 invariant -- zero brain-client / brain.mindrian / cross-room aggregat in renderer source', () => {
|
|
212
|
+
const src = fs.readFileSync(RENDERER_PATH, 'utf8');
|
|
213
|
+
assert.equal(/require\([^)]*brain-client[^)]*\)/.test(src), false, 'must not require brain-client');
|
|
214
|
+
assert.equal(/fetch\([^)]*brain\.mindrian[^)]*\)/.test(src), false, 'must not fetch brain.mindrian');
|
|
215
|
+
assert.equal(/cross.{0,3}room.{0,3}aggregat/i.test(src), false, 'must not mention cross-room aggregat');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// --- T11: em-dash invariant ---
|
|
219
|
+
test('T11: em-dash invariant -- zero U+2014 in renderer source (HARD RULE)', () => {
|
|
220
|
+
const src = fs.readFileSync(RENDERER_PATH, 'utf8');
|
|
221
|
+
// U+2014 is the em-dash. The test file itself avoids the literal char so this
|
|
222
|
+
// suite stays em-dash-free.
|
|
223
|
+
const emDash = String.fromCharCode(0x2014);
|
|
224
|
+
assert.equal(src.indexOf(emDash), -1, 'renderer source must contain zero em-dashes');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// --- Bonus: KIND_DISPLAY_NAMES sanity ---
|
|
228
|
+
test('Bonus: KIND_DISPLAY_NAMES covers the four known detector kinds', () => {
|
|
229
|
+
assert.equal(KIND_DISPLAY_NAMES.convergence, 'Convergence');
|
|
230
|
+
assert.equal(KIND_DISPLAY_NAMES.contradiction_resolved, 'Contradiction resolved');
|
|
231
|
+
assert.equal(KIND_DISPLAY_NAMES.cross_domain_analogy, 'Cross-domain analogy');
|
|
232
|
+
assert.equal(KIND_DISPLAY_NAMES.reverse_salient_closed, 'Reverse salient closed');
|
|
233
|
+
});
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
6
|
+
*
|
|
7
|
+
* Phase 121.5-01 Task 2 -- Body-shape coverage acceptance tests.
|
|
8
|
+
*
|
|
9
|
+
* Verifies scripts/audit-body-shape-coverage.cjs and output-styles/destijl.md.
|
|
10
|
+
* The audit script is the CI tripwire that scans commands/*.md frontmatter for
|
|
11
|
+
* body_shape: presence and a valid value from the locked vocabulary. The
|
|
12
|
+
* output style is the system-prompt-enforced 4-zone De Stijl format that lifts
|
|
13
|
+
* SKILL.md from skill-instruction-suggested to force-for-plugin-enforced.
|
|
14
|
+
*
|
|
15
|
+
* Canon references:
|
|
16
|
+
* Part 3 UI Ruling System -- this test enforces the body_shape contract
|
|
17
|
+
* that the ruling system depends on.
|
|
18
|
+
* Part 7 Reuse Before Build -- no new commands are created; this is a
|
|
19
|
+
* pure compliance + infrastructure pass.
|
|
20
|
+
* Part 8 Graph Boundary -- audit script + output style add zero network
|
|
21
|
+
* surface, zero Brain calls, zero telemetry egress.
|
|
22
|
+
*
|
|
23
|
+
* Test map (7 cases, one-to-one with PLAN Task 2 <behavior>):
|
|
24
|
+
*
|
|
25
|
+
* 1. output-styles/destijl.md exists and parses as valid YAML frontmatter
|
|
26
|
+
* + markdown body.
|
|
27
|
+
* 2. Frontmatter contains exactly `force-for-plugin: true` AND
|
|
28
|
+
* `keep-coding-instructions: true` (literal string match).
|
|
29
|
+
* 3. Body documents the four zones in fixed order matching SKILL.md
|
|
30
|
+
* Section 1: Header Panel / Content Body / Intelligence Strip /
|
|
31
|
+
* Action Footer.
|
|
32
|
+
* 4. Body documents the 5+2 body shapes (A, B, C, D, E + F family)
|
|
33
|
+
* mapping to the SKILL.md Section 2 vocabulary.
|
|
34
|
+
* 5. scripts/audit-body-shape-coverage.cjs exits 0 against the live
|
|
35
|
+
* commands/ directory (post-Task-1 state). Exits 1 against a
|
|
36
|
+
* synthetic fixture with a command missing body_shape:.
|
|
37
|
+
* 6. The audit script reports per-category counts (A / B / C / D / E /
|
|
38
|
+
* F.x / methodology) in --json mode.
|
|
39
|
+
* 7. Every body_shape: value in commands/*.md is one of the locked
|
|
40
|
+
* vocabulary {A, B, C, D, E, F.0..F.6, methodology}. The audit
|
|
41
|
+
* script flags any other value.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const assert = require('node:assert/strict');
|
|
45
|
+
const fs = require('node:fs');
|
|
46
|
+
const os = require('node:os');
|
|
47
|
+
const path = require('node:path');
|
|
48
|
+
const { execFileSync } = require('node:child_process');
|
|
49
|
+
|
|
50
|
+
const REPO = path.resolve(__dirname, '..', '..');
|
|
51
|
+
const DESTIJL_PATH = path.join(REPO, 'output-styles', 'destijl.md');
|
|
52
|
+
const AUDIT_PATH = path.join(REPO, 'scripts', 'audit-body-shape-coverage.cjs');
|
|
53
|
+
const COMMANDS_DIR = path.join(REPO, 'commands');
|
|
54
|
+
|
|
55
|
+
// ---------- Fixture helpers ----------
|
|
56
|
+
|
|
57
|
+
const TMP_ROOTS = [];
|
|
58
|
+
function mkTmp(prefix) {
|
|
59
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
60
|
+
TMP_ROOTS.push(d);
|
|
61
|
+
return d;
|
|
62
|
+
}
|
|
63
|
+
process.on('exit', () => {
|
|
64
|
+
for (const d of TMP_ROOTS) {
|
|
65
|
+
try { fs.rmSync(d, { recursive: true, force: true }); } catch (_) {}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
function readDestijl() {
|
|
70
|
+
return fs.readFileSync(DESTIJL_PATH, 'utf8');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function loadAudit() {
|
|
74
|
+
try { delete require.cache[require.resolve(AUDIT_PATH)]; } catch (_) {}
|
|
75
|
+
return require(AUDIT_PATH);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Run audit script as a subprocess (mirrors how CI will invoke it).
|
|
79
|
+
// Returns { code, stdout, stderr }.
|
|
80
|
+
function runAudit(args, env) {
|
|
81
|
+
const result = { code: 0, stdout: '', stderr: '' };
|
|
82
|
+
try {
|
|
83
|
+
result.stdout = execFileSync('node', [AUDIT_PATH, ...(args || [])], {
|
|
84
|
+
env: Object.assign({}, process.env, env || {}),
|
|
85
|
+
encoding: 'utf8',
|
|
86
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
87
|
+
});
|
|
88
|
+
} catch (e) {
|
|
89
|
+
result.code = (e && typeof e.status === 'number') ? e.status : 1;
|
|
90
|
+
result.stdout = (e && e.stdout) ? e.stdout.toString() : '';
|
|
91
|
+
result.stderr = (e && e.stderr) ? e.stderr.toString() : '';
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------- Tests ----------
|
|
97
|
+
|
|
98
|
+
let passed = 0;
|
|
99
|
+
let failed = 0;
|
|
100
|
+
const failures = [];
|
|
101
|
+
|
|
102
|
+
function test(name, fn) {
|
|
103
|
+
try {
|
|
104
|
+
fn();
|
|
105
|
+
passed++;
|
|
106
|
+
console.log(' ok ' + name);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
failed++;
|
|
109
|
+
failures.push({ name, error: e });
|
|
110
|
+
console.log(' FAIL ' + name);
|
|
111
|
+
console.log(' ' + (e && e.message ? e.message : String(e)));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log('Phase 121.5-01 Task 2 -- body-shape-coverage acceptance suite');
|
|
116
|
+
console.log('');
|
|
117
|
+
|
|
118
|
+
// Test 1: destijl.md exists and parses
|
|
119
|
+
test('Test 1: output-styles/destijl.md exists with frontmatter + body', () => {
|
|
120
|
+
assert.ok(fs.existsSync(DESTIJL_PATH), 'output-styles/destijl.md missing');
|
|
121
|
+
const text = readDestijl();
|
|
122
|
+
const fm = text.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
123
|
+
assert.ok(fm, 'destijl.md does not parse as frontmatter + body');
|
|
124
|
+
assert.ok(fm[1].length > 0, 'frontmatter is empty');
|
|
125
|
+
assert.ok(fm[2].length > 100, 'body is too short (< 100 chars)');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Test 2: force-for-plugin + keep-coding-instructions present
|
|
129
|
+
test('Test 2: frontmatter has force-for-plugin: true AND keep-coding-instructions: true', () => {
|
|
130
|
+
const text = readDestijl();
|
|
131
|
+
const fm = text.match(/^---\n([\s\S]*?)\n---/);
|
|
132
|
+
assert.ok(fm, 'no frontmatter found');
|
|
133
|
+
assert.ok(/^force-for-plugin:\s*true\s*$/m.test(fm[1]),
|
|
134
|
+
'force-for-plugin: true not found in frontmatter');
|
|
135
|
+
assert.ok(/^keep-coding-instructions:\s*true\s*$/m.test(fm[1]),
|
|
136
|
+
'keep-coding-instructions: true not found in frontmatter');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Test 3: 4 zones documented in fixed order
|
|
140
|
+
test('Test 3: body documents 4 zones in SKILL.md Section 1 order', () => {
|
|
141
|
+
const text = readDestijl();
|
|
142
|
+
const idxHeader = text.indexOf('Header Panel');
|
|
143
|
+
const idxBody = text.indexOf('Content Body');
|
|
144
|
+
const idxStrip = text.indexOf('Intelligence Strip');
|
|
145
|
+
const idxFooter = text.indexOf('Action Footer');
|
|
146
|
+
assert.ok(idxHeader > 0, 'Header Panel zone not documented');
|
|
147
|
+
assert.ok(idxBody > idxHeader, 'Content Body must come after Header Panel');
|
|
148
|
+
assert.ok(idxStrip > idxBody, 'Intelligence Strip must come after Content Body');
|
|
149
|
+
assert.ok(idxFooter > idxStrip, 'Action Footer must come after Intelligence Strip');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Test 4: 5+2 body shapes documented
|
|
153
|
+
test('Test 4: body documents shapes A, B, C, D, E + F family + methodology', () => {
|
|
154
|
+
const text = readDestijl();
|
|
155
|
+
// Each shape letter must appear in the shape table or shape documentation.
|
|
156
|
+
// Use regex to find the shape table row pattern: "| <letter> |" or "Shape <letter>".
|
|
157
|
+
for (const shape of ['A', 'B', 'C', 'D', 'E']) {
|
|
158
|
+
const re = new RegExp('(\\|\\s*' + shape + '\\s*\\||Shape\\s+' + shape + '\\b)');
|
|
159
|
+
assert.ok(re.test(text), 'Shape ' + shape + ' not documented in body');
|
|
160
|
+
}
|
|
161
|
+
// F family members
|
|
162
|
+
for (const fSub of ['F.0', 'F.1', 'F.2', 'F.3', 'F.4', 'F.5', 'F.6']) {
|
|
163
|
+
const re = new RegExp('\\b' + fSub.replace('.', '\\.') + '\\b');
|
|
164
|
+
assert.ok(re.test(text), fSub + ' not documented in body');
|
|
165
|
+
}
|
|
166
|
+
assert.ok(/methodology/i.test(text), 'methodology not documented in body');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Test 5a: audit script exits 0 against live commands/ (post-Task-1 state)
|
|
170
|
+
test('Test 5a: audit exits 0 against live commands/ (100% coverage)', () => {
|
|
171
|
+
assert.ok(fs.existsSync(AUDIT_PATH), 'audit script missing');
|
|
172
|
+
const r = runAudit([]);
|
|
173
|
+
if (r.code !== 0) {
|
|
174
|
+
console.log(' stdout: ' + r.stdout);
|
|
175
|
+
console.log(' stderr: ' + r.stderr);
|
|
176
|
+
}
|
|
177
|
+
assert.strictEqual(r.code, 0, 'audit script exited non-zero on live repo');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Test 5b: audit exits 1 on synthetic fixture missing body_shape
|
|
181
|
+
test('Test 5b: audit exits 1 on synthetic command missing body_shape', () => {
|
|
182
|
+
const tmp = mkTmp('body-shape-audit-');
|
|
183
|
+
const cmdsDir = path.join(tmp, 'commands');
|
|
184
|
+
fs.mkdirSync(cmdsDir, { recursive: true });
|
|
185
|
+
// Good file
|
|
186
|
+
fs.writeFileSync(path.join(cmdsDir, 'good.md'),
|
|
187
|
+
'---\nname: good\ndescription: test\nbody_shape: A\n---\n# good\n');
|
|
188
|
+
// Bad file (missing body_shape)
|
|
189
|
+
fs.writeFileSync(path.join(cmdsDir, 'bad.md'),
|
|
190
|
+
'---\nname: bad\ndescription: test\n---\n# bad\n');
|
|
191
|
+
|
|
192
|
+
// Audit accepts a --dir override.
|
|
193
|
+
const r = runAudit(['--dir', cmdsDir]);
|
|
194
|
+
assert.notStrictEqual(r.code, 0,
|
|
195
|
+
'audit must fail when a command is missing body_shape (got exit 0)');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Test 6: --json mode reports per-category counts
|
|
199
|
+
test('Test 6: --json mode reports histogram per category', () => {
|
|
200
|
+
const r = runAudit(['--json']);
|
|
201
|
+
assert.strictEqual(r.code, 0, 'audit --json exited non-zero');
|
|
202
|
+
let parsed;
|
|
203
|
+
try {
|
|
204
|
+
parsed = JSON.parse(r.stdout);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
assert.fail('--json output not valid JSON: ' + e.message);
|
|
207
|
+
}
|
|
208
|
+
assert.ok(parsed && typeof parsed === 'object', '--json must emit object');
|
|
209
|
+
assert.ok(typeof parsed.total === 'number', 'total missing in --json output');
|
|
210
|
+
assert.ok(parsed.histogram && typeof parsed.histogram === 'object',
|
|
211
|
+
'histogram missing in --json output');
|
|
212
|
+
// At least 3 distinct shape categories should appear in a healthy repo
|
|
213
|
+
// (proves the sweep is a spread, not a monoculture).
|
|
214
|
+
const keys = Object.keys(parsed.histogram);
|
|
215
|
+
assert.ok(keys.length >= 3,
|
|
216
|
+
'histogram has < 3 distinct shapes -- monoculture detected: ' + keys.join(','));
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Test 7: all body_shape values in live repo are in the locked vocabulary
|
|
220
|
+
test('Test 7: every body_shape value in commands/*.md is in locked vocab', () => {
|
|
221
|
+
const { VALID_SHAPES } = loadAudit();
|
|
222
|
+
assert.ok(VALID_SHAPES && typeof VALID_SHAPES.has === 'function',
|
|
223
|
+
'audit must export VALID_SHAPES Set');
|
|
224
|
+
// Sanity-check the vocabulary itself
|
|
225
|
+
for (const v of ['A', 'B', 'C', 'D', 'E',
|
|
226
|
+
'F.0', 'F.1', 'F.2', 'F.3', 'F.4', 'F.5', 'F.6',
|
|
227
|
+
'methodology']) {
|
|
228
|
+
assert.ok(VALID_SHAPES.has(v), 'VALID_SHAPES missing required value: ' + v);
|
|
229
|
+
}
|
|
230
|
+
// Now scan live commands and validate each
|
|
231
|
+
const files = fs.readdirSync(COMMANDS_DIR).filter(f => f.endsWith('.md'));
|
|
232
|
+
const offenders = [];
|
|
233
|
+
for (const f of files) {
|
|
234
|
+
const text = fs.readFileSync(path.join(COMMANDS_DIR, f), 'utf8');
|
|
235
|
+
const fm = text.match(/^---\n([\s\S]*?)\n---/);
|
|
236
|
+
if (!fm) { offenders.push(f + ' (no frontmatter)'); continue; }
|
|
237
|
+
const m = fm[1].match(/^body_shape:\s*(.+)$/m);
|
|
238
|
+
if (!m) { offenders.push(f + ' (no body_shape)'); continue; }
|
|
239
|
+
// Strip trailing parenthetical (e.g. "B (Semantic Tree)" -> "B")
|
|
240
|
+
let v = m[1].trim().replace(/\s*\(.*\)\s*$/, '');
|
|
241
|
+
// Strip surrounding quotes if present
|
|
242
|
+
v = v.replace(/^['"]|['"]$/g, '');
|
|
243
|
+
if (!VALID_SHAPES.has(v)) {
|
|
244
|
+
offenders.push(f + ' = "' + m[1].trim() + '" (extracted: "' + v + '")');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (offenders.length) {
|
|
248
|
+
assert.fail('commands with invalid body_shape values:\n ' + offenders.join('\n '));
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ---------- Summary ----------
|
|
253
|
+
|
|
254
|
+
console.log('');
|
|
255
|
+
console.log('========================================');
|
|
256
|
+
console.log('Tests: ' + (passed + failed) + ' total, ' + passed + ' passed, ' + failed + ' failed');
|
|
257
|
+
console.log('========================================');
|
|
258
|
+
|
|
259
|
+
if (failed > 0) {
|
|
260
|
+
console.log('');
|
|
261
|
+
console.log('Failures:');
|
|
262
|
+
for (const f of failures) {
|
|
263
|
+
console.log(' - ' + f.name);
|
|
264
|
+
}
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
process.exit(0);
|