@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
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Phase 121.5-08 Plan Task 2 -- doctor class I (deprecated-usage surface)
|
|
4
|
+
* + F.1 selector wiring on onboard Step 6 + diagnose recommendation.
|
|
5
|
+
*
|
|
6
|
+
* Behavior contract (per plan):
|
|
7
|
+
* T1: scripts/doctor.cjs --deprecated-usage detects synthetic usage in temp
|
|
8
|
+
* transcript fixtures (a temp ~/.claude/projects/.../session.jsonl-like
|
|
9
|
+
* fixture mentioning "/mos:heal" -- the class I detector picks it up).
|
|
10
|
+
* T2: Class I returns {class: 'I', status, action, deprecated_uses: []}.
|
|
11
|
+
* T3: --deprecated-usage exits 0 when no deprecated commands used; exits 1
|
|
12
|
+
* when violations found.
|
|
13
|
+
* T4: doctor --all includes class I in the cascade.
|
|
14
|
+
* T5: commands/onboard.md Step 6 body contains the canonical F.1 selector
|
|
15
|
+
* vocabulary (Run Methodology / Defer / Free-Text).
|
|
16
|
+
* T6: commands/diagnose.md recommendation block contains the canonical F.1
|
|
17
|
+
* selector vocabulary.
|
|
18
|
+
* T7: CHANGELOG.md has an Unreleased / next-beta entry listing all 5
|
|
19
|
+
* renames + the /mos:mos creation + the F.1 wiring + the class I add.
|
|
20
|
+
*
|
|
21
|
+
* Hermetic: uses fs.mkdtempSync for synthetic transcript fixture (T1). The
|
|
22
|
+
* doctor invocation spawns scripts/doctor.cjs as a subprocess with the
|
|
23
|
+
* fixture dir bound via MINDRIAN_DOCTOR_TRANSCRIPTS_DIR.
|
|
24
|
+
*/
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const os = require('os');
|
|
30
|
+
const cp = require('child_process');
|
|
31
|
+
|
|
32
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
33
|
+
const DOCTOR_CJS = path.join(REPO_ROOT, 'scripts', 'doctor.cjs');
|
|
34
|
+
|
|
35
|
+
let pass = 0;
|
|
36
|
+
let fail = 0;
|
|
37
|
+
const failures = [];
|
|
38
|
+
|
|
39
|
+
function assert(cond, name, detail) {
|
|
40
|
+
if (cond) {
|
|
41
|
+
pass++;
|
|
42
|
+
console.log('PASS ' + name);
|
|
43
|
+
} else {
|
|
44
|
+
fail++;
|
|
45
|
+
failures.push(name + (detail ? ' -- ' + detail : ''));
|
|
46
|
+
console.log('FAIL ' + name + (detail ? ' -- ' + detail : ''));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- T1 + T2 + T3 (positive): synthesize a fixture transcript dir with
|
|
51
|
+
// "/mos:heal" usage; --deprecated-usage detects + exits 1 + returns class I.
|
|
52
|
+
{
|
|
53
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-deprec-'));
|
|
54
|
+
const sessionDir = path.join(tmpRoot, 'sessions', 'fake-project-hash');
|
|
55
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
56
|
+
const jsonlPath = path.join(sessionDir, 'session-abc.jsonl');
|
|
57
|
+
// Recent mtime (now). Per implementation contract: scan last 7 days.
|
|
58
|
+
const fixture = JSON.stringify({ role: 'user', content: 'let me run /mos:heal here' }) + '\n'
|
|
59
|
+
+ JSON.stringify({ role: 'user', content: 'now /mos:query something' }) + '\n';
|
|
60
|
+
fs.writeFileSync(jsonlPath, fixture);
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
fs.utimesSync(jsonlPath, new Date(now), new Date(now));
|
|
63
|
+
|
|
64
|
+
const r = cp.spawnSync('node', [DOCTOR_CJS, '--deprecated-usage', '--json'], {
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
env: Object.assign({}, process.env, { MINDRIAN_DOCTOR_TRANSCRIPTS_DIR: tmpRoot }),
|
|
67
|
+
timeout: 30000,
|
|
68
|
+
});
|
|
69
|
+
// Parse JSON envelope
|
|
70
|
+
let envelope = null;
|
|
71
|
+
if (r.stdout) {
|
|
72
|
+
const start = r.stdout.indexOf('{');
|
|
73
|
+
if (start >= 0) {
|
|
74
|
+
try { envelope = JSON.parse(r.stdout.slice(start)); } catch (_) { envelope = null; }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
assert(envelope !== null, 'T1: --deprecated-usage emits parseable JSON', (r.stdout || '').slice(0, 300));
|
|
78
|
+
const cls = envelope && envelope.checks && envelope.checks['deprecated-usage'];
|
|
79
|
+
assert(cls !== undefined && cls !== null, 'T1: report.checks["deprecated-usage"] exists', JSON.stringify(envelope && envelope.checks));
|
|
80
|
+
if (cls) {
|
|
81
|
+
assert(cls.class === 'L', 'T2: class==="L"', cls.class);
|
|
82
|
+
assert(typeof cls.status === 'string', 'T2: status is a string', cls.status);
|
|
83
|
+
assert(typeof cls.action === 'string', 'T2: action is a string', cls.action);
|
|
84
|
+
assert(Array.isArray(cls.deprecated_uses), 'T2: deprecated_uses is array', JSON.stringify(cls.deprecated_uses));
|
|
85
|
+
assert(cls.deprecated_uses.length >= 2, 'T1: detected at least 2 deprecated uses (heal + query)', JSON.stringify(cls.deprecated_uses));
|
|
86
|
+
const found = cls.deprecated_uses.map(function (x) { return x.deprecated; }).sort();
|
|
87
|
+
assert(found.indexOf('/mos:heal') >= 0, 'T1: /mos:heal detected', JSON.stringify(found));
|
|
88
|
+
assert(found.indexOf('/mos:query') >= 0, 'T1: /mos:query detected', JSON.stringify(found));
|
|
89
|
+
assert(cls.status === 'DEPRECATED_USAGE' || cls.status === 'warn', 'T2: status flags violation', cls.status);
|
|
90
|
+
}
|
|
91
|
+
// Cleanup
|
|
92
|
+
try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch (_) {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- T3 negative: no transcripts -> --deprecated-usage clean.
|
|
96
|
+
{
|
|
97
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-deprec-clean-'));
|
|
98
|
+
const r = cp.spawnSync('node', [DOCTOR_CJS, '--deprecated-usage', '--json'], {
|
|
99
|
+
encoding: 'utf8',
|
|
100
|
+
env: Object.assign({}, process.env, { MINDRIAN_DOCTOR_TRANSCRIPTS_DIR: tmpRoot }),
|
|
101
|
+
timeout: 30000,
|
|
102
|
+
});
|
|
103
|
+
let envelope = null;
|
|
104
|
+
if (r.stdout) {
|
|
105
|
+
const start = r.stdout.indexOf('{');
|
|
106
|
+
if (start >= 0) { try { envelope = JSON.parse(r.stdout.slice(start)); } catch (_) {} }
|
|
107
|
+
}
|
|
108
|
+
const cls = envelope && envelope.checks && envelope.checks['deprecated-usage'];
|
|
109
|
+
assert(cls && cls.status === 'OK', 'T3: status === OK with no deprecated usage', cls && cls.status);
|
|
110
|
+
assert(cls && cls.deprecated_uses && cls.deprecated_uses.length === 0, 'T3: deprecated_uses is empty array');
|
|
111
|
+
// Per spec: --deprecated-usage exits 0 when clean.
|
|
112
|
+
assert(r.status === 0, 'T3: exit 0 when clean', String(r.status));
|
|
113
|
+
try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch (_) {}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- T4: --all activates --deprecated-usage in the cascade.
|
|
117
|
+
{
|
|
118
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-deprec-all-'));
|
|
119
|
+
const r = cp.spawnSync('node', [DOCTOR_CJS, '--all', '--json'], {
|
|
120
|
+
encoding: 'utf8',
|
|
121
|
+
env: Object.assign({}, process.env, { MINDRIAN_DOCTOR_TRANSCRIPTS_DIR: tmpRoot }),
|
|
122
|
+
timeout: 60000,
|
|
123
|
+
});
|
|
124
|
+
let envelope = null;
|
|
125
|
+
if (r.stdout) {
|
|
126
|
+
const start = r.stdout.indexOf('{');
|
|
127
|
+
if (start >= 0) { try { envelope = JSON.parse(r.stdout.slice(start)); } catch (_) {} }
|
|
128
|
+
}
|
|
129
|
+
const cls = envelope && envelope.checks && envelope.checks['deprecated-usage'];
|
|
130
|
+
assert(cls !== undefined && cls !== null, 'T4: --all includes deprecated-usage in cascade', JSON.stringify(envelope && envelope.checks ? Object.keys(envelope.checks) : null));
|
|
131
|
+
try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch (_) {}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// --- T5: commands/onboard.md Step 6 has F.1 selector vocabulary.
|
|
135
|
+
{
|
|
136
|
+
const onboardPath = path.join(REPO_ROOT, 'commands', 'onboard.md');
|
|
137
|
+
const body = fs.readFileSync(onboardPath, 'utf8');
|
|
138
|
+
// Locate Step 6 region for verification (the test asserts the F.1 lives
|
|
139
|
+
// inside or in the vicinity of Step 6, not just anywhere in the file).
|
|
140
|
+
const step6Idx = body.indexOf('## Step 6');
|
|
141
|
+
assert(step6Idx > 0, 'T5: Step 6 section exists', String(step6Idx));
|
|
142
|
+
// The whole step-6 region (from Step 6 to end of file or next ##)
|
|
143
|
+
const rest = body.slice(step6Idx);
|
|
144
|
+
const nextStep = rest.indexOf('\n## ', 5);
|
|
145
|
+
const step6Region = nextStep > 0 ? rest.slice(0, nextStep) : rest;
|
|
146
|
+
assert(/F\.1/.test(step6Region), 'T5: Step 6 mentions F.1', step6Region.slice(0, 400));
|
|
147
|
+
assert(/Run Methodology/.test(step6Region), 'T5: Step 6 has "Run Methodology" verb');
|
|
148
|
+
assert(/Defer/.test(step6Region), 'T5: Step 6 has "Defer" verb');
|
|
149
|
+
assert(/Free-Text/.test(step6Region), 'T5: Step 6 has "Free-Text" verb');
|
|
150
|
+
assert(step6Region.indexOf('—') < 0, 'T5: Step 6 region has no em-dash');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --- T6: commands/diagnose.md recommendation has F.1 selector vocabulary.
|
|
154
|
+
{
|
|
155
|
+
const diagPath = path.join(REPO_ROOT, 'commands', 'diagnose.md');
|
|
156
|
+
const body = fs.readFileSync(diagPath, 'utf8');
|
|
157
|
+
assert(/F\.1/.test(body), 'T6: diagnose.md mentions F.1');
|
|
158
|
+
assert(/Run Methodology/.test(body), 'T6: diagnose.md has "Run Methodology" verb');
|
|
159
|
+
assert(/Defer/.test(body), 'T6: diagnose.md has "Defer" verb');
|
|
160
|
+
assert(/Free-Text/.test(body), 'T6: diagnose.md has "Free-Text" verb');
|
|
161
|
+
assert(body.indexOf('—') < 0, 'T6: diagnose.md has no em-dash');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- T7: CHANGELOG.md entry for Phase 121.5-08 covers all 5 renames + /mos:mos + F.1.
|
|
165
|
+
{
|
|
166
|
+
const cl = fs.readFileSync(path.join(REPO_ROOT, 'CHANGELOG.md'), 'utf8');
|
|
167
|
+
assert(/Phase 121\.5-08/.test(cl), 'T7: CHANGELOG mentions Phase 121.5-08');
|
|
168
|
+
assert(/heal/i.test(cl), 'T7: CHANGELOG mentions heal rename');
|
|
169
|
+
assert(/query/i.test(cl), 'T7: CHANGELOG mentions query rename');
|
|
170
|
+
assert(/organize/i.test(cl), 'T7: CHANGELOG mentions organize rename');
|
|
171
|
+
assert(/hmi-status/i.test(cl), 'T7: CHANGELOG mentions hmi-status rename');
|
|
172
|
+
assert(/visualize/i.test(cl), 'T7: CHANGELOG mentions visualize rename');
|
|
173
|
+
assert(/\/mos:mos/.test(cl), 'T7: CHANGELOG mentions /mos:mos creation');
|
|
174
|
+
// F.1 selector wiring announcement
|
|
175
|
+
assert(/F\.1/.test(cl) || /F.1 selector/.test(cl), 'T7: CHANGELOG mentions F.1 selector wiring');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log('');
|
|
179
|
+
console.log('doctor-deprecation-surface.test.cjs: ' + pass + ' passed, ' + fail + ' failed');
|
|
180
|
+
if (fail > 0) {
|
|
181
|
+
console.log('FAILURES:');
|
|
182
|
+
for (const f of failures) console.log(' - ' + f);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
process.exit(0);
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
4
|
+
*
|
|
5
|
+
* lib/memory/first-touch-version.test.cjs
|
|
6
|
+
* ---------------------------------------
|
|
7
|
+
* Phase 121.5-05 Sub-plan F Task 1 acceptance tests.
|
|
8
|
+
*
|
|
9
|
+
* 8 tests covering the first-touch-version-stamper module:
|
|
10
|
+
*
|
|
11
|
+
* 1. stampVersion('banner', '1.13.0-beta.15') contains "MindrianOS v1.13.0-beta.15"
|
|
12
|
+
* 2. stampVersion('splash', '1.13.0') starts with "MindrianOS v1.13.0"
|
|
13
|
+
* 3. stampVersion('onboard', '1.14.0') embeds "MindrianOS v1.14.0"
|
|
14
|
+
* 4. stampVersion(<unknown_surface>, version) throws Error('unknown_surface: ...')
|
|
15
|
+
* 5. stampVersion called without version arg auto-resolves from plugin.json
|
|
16
|
+
* 6. data/first-touch-surfaces.json parses; surfaces[] length >= 6
|
|
17
|
+
* 7. hasEmDash(text) returns true on U+2014, false otherwise
|
|
18
|
+
* 8. scripts/banner output contains "MindrianOS v" + live plugin.json version
|
|
19
|
+
*
|
|
20
|
+
* Pure CJS, node built-ins only. No emoji. No em-dashes.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const os = require('os');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const { execSync } = require('child_process');
|
|
29
|
+
|
|
30
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
31
|
+
const stamperPath = path.join(REPO_ROOT, 'lib', 'core', 'first-touch-version-stamper.cjs');
|
|
32
|
+
const stamper = require(stamperPath);
|
|
33
|
+
|
|
34
|
+
let passed = 0;
|
|
35
|
+
let failed = 0;
|
|
36
|
+
const failures = [];
|
|
37
|
+
|
|
38
|
+
function assert(name, cond, detail) {
|
|
39
|
+
if (cond) {
|
|
40
|
+
passed++;
|
|
41
|
+
console.log(' PASS ' + name);
|
|
42
|
+
} else {
|
|
43
|
+
failed++;
|
|
44
|
+
failures.push({ name, detail });
|
|
45
|
+
console.log(' FAIL ' + name + (detail ? ' :: ' + detail : ''));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- Test 1: banner stamp contains MindrianOS v<version> ----------------
|
|
50
|
+
{
|
|
51
|
+
const out = stamper.stampVersion('banner', '1.13.0-beta.15');
|
|
52
|
+
assert(
|
|
53
|
+
'Test 1: stampVersion(banner, "1.13.0-beta.15") contains "MindrianOS v1.13.0-beta.15"',
|
|
54
|
+
out.indexOf('MindrianOS v1.13.0-beta.15') >= 0,
|
|
55
|
+
'got: ' + out,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- Test 2: splash stamp starts with MindrianOS v<version> -------------
|
|
60
|
+
{
|
|
61
|
+
const out = stamper.stampVersion('splash', '1.13.0');
|
|
62
|
+
assert(
|
|
63
|
+
'Test 2: stampVersion(splash, "1.13.0") starts with "MindrianOS v1.13.0"',
|
|
64
|
+
out.indexOf('MindrianOS v1.13.0') === 0,
|
|
65
|
+
'got: ' + out,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- Test 3: onboard stamp embeds MindrianOS v<version> -----------------
|
|
70
|
+
{
|
|
71
|
+
const out = stamper.stampVersion('onboard', '1.14.0');
|
|
72
|
+
assert(
|
|
73
|
+
'Test 3: stampVersion(onboard, "1.14.0") embeds "MindrianOS v1.14.0"',
|
|
74
|
+
out.indexOf('MindrianOS v1.14.0') >= 0,
|
|
75
|
+
'got: ' + out,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --- Test 4: unknown surface throws -------------------------------------
|
|
80
|
+
{
|
|
81
|
+
let threw = false;
|
|
82
|
+
let msg = '';
|
|
83
|
+
try {
|
|
84
|
+
stamper.stampVersion('not-a-real-surface', '1.0.0');
|
|
85
|
+
} catch (e) {
|
|
86
|
+
threw = true;
|
|
87
|
+
msg = e.message;
|
|
88
|
+
}
|
|
89
|
+
assert(
|
|
90
|
+
'Test 4: stampVersion(unknown_surface) throws Error(unknown_surface: ...)',
|
|
91
|
+
threw && /^unknown_surface:/.test(msg),
|
|
92
|
+
'threw=' + threw + ' msg=' + msg,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- Test 5: no version arg -> auto-resolve from plugin.json ------------
|
|
97
|
+
{
|
|
98
|
+
const pluginJsonPath = path.join(REPO_ROOT, '.claude-plugin', 'plugin.json');
|
|
99
|
+
const liveVersion = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8')).version;
|
|
100
|
+
const out = stamper.stampVersion('banner');
|
|
101
|
+
assert(
|
|
102
|
+
'Test 5: stampVersion(banner) auto-resolves version from plugin.json (' + liveVersion + ')',
|
|
103
|
+
out.indexOf(liveVersion) >= 0,
|
|
104
|
+
'got: ' + out + ' live=' + liveVersion,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- Test 6: surfaces JSON parses + has >= 6 entries --------------------
|
|
109
|
+
{
|
|
110
|
+
const surfaces = stamper.readSurfaces();
|
|
111
|
+
assert(
|
|
112
|
+
'Test 6: data/first-touch-surfaces.json parses; surfaces[].length >= 6',
|
|
113
|
+
Array.isArray(surfaces.surfaces) && surfaces.surfaces.length >= 6,
|
|
114
|
+
'length=' + (surfaces.surfaces ? surfaces.surfaces.length : 'undefined'),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Sanity: each surface has the required keys.
|
|
118
|
+
const requiredKeys = ['id', 'file', 'kind', 'version_stamp_required', 'em_dash_check'];
|
|
119
|
+
let allOk = true;
|
|
120
|
+
let missingDetail = '';
|
|
121
|
+
for (const s of surfaces.surfaces) {
|
|
122
|
+
for (const k of requiredKeys) {
|
|
123
|
+
if (!(k in s)) {
|
|
124
|
+
allOk = false;
|
|
125
|
+
missingDetail = 'surface ' + (s.id || '?') + ' missing key ' + k;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!allOk) break;
|
|
130
|
+
}
|
|
131
|
+
assert(
|
|
132
|
+
'Test 6b: every surface has id / file / kind / version_stamp_required / em_dash_check',
|
|
133
|
+
allOk,
|
|
134
|
+
missingDetail,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- Test 7: hasEmDash detects U+2014 -----------------------------------
|
|
139
|
+
{
|
|
140
|
+
// U+2014 EM DASH, written via escape so this source stays ASCII-clean.
|
|
141
|
+
const withDash = 'this has an \u2014 em dash';
|
|
142
|
+
const withoutDash = 'this has a - hyphen';
|
|
143
|
+
assert(
|
|
144
|
+
'Test 7a: hasEmDash returns true on U+2014',
|
|
145
|
+
stamper.hasEmDash(withDash) === true,
|
|
146
|
+
);
|
|
147
|
+
assert(
|
|
148
|
+
'Test 7b: hasEmDash returns false on plain hyphen',
|
|
149
|
+
stamper.hasEmDash(withoutDash) === false,
|
|
150
|
+
);
|
|
151
|
+
assert(
|
|
152
|
+
'Test 7c: hasEmDash returns false on non-string',
|
|
153
|
+
stamper.hasEmDash(null) === false && stamper.hasEmDash(42) === false,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- Test 8: scripts/banner emits "MindrianOS v" + live version ---------
|
|
158
|
+
{
|
|
159
|
+
const bannerPath = path.join(REPO_ROOT, 'scripts', 'banner');
|
|
160
|
+
let bannerOut = '';
|
|
161
|
+
let bannerErr = null;
|
|
162
|
+
try {
|
|
163
|
+
bannerOut = execSync('bash ' + JSON.stringify(bannerPath), {
|
|
164
|
+
cwd: REPO_ROOT,
|
|
165
|
+
encoding: 'utf8',
|
|
166
|
+
env: Object.assign({}, process.env, { COLUMNS: '120' }),
|
|
167
|
+
});
|
|
168
|
+
} catch (e) {
|
|
169
|
+
bannerErr = e.message;
|
|
170
|
+
}
|
|
171
|
+
const liveVersion = stamper.resolveCurrentVersion();
|
|
172
|
+
assert(
|
|
173
|
+
'Test 8a: scripts/banner runs without error',
|
|
174
|
+
bannerErr === null,
|
|
175
|
+
bannerErr || '',
|
|
176
|
+
);
|
|
177
|
+
assert(
|
|
178
|
+
'Test 8b: banner output contains "MindrianOS v"',
|
|
179
|
+
bannerOut.indexOf('MindrianOS v') >= 0,
|
|
180
|
+
'first 200 chars: ' + bannerOut.slice(0, 200),
|
|
181
|
+
);
|
|
182
|
+
assert(
|
|
183
|
+
'Test 8c: banner output contains the live plugin.json version (' + liveVersion + ')',
|
|
184
|
+
bannerOut.indexOf(liveVersion) >= 0,
|
|
185
|
+
'live=' + liveVersion,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --- Summary ------------------------------------------------------------
|
|
190
|
+
console.log('');
|
|
191
|
+
console.log('first-touch-version.test.cjs: ' + passed + ' passed, ' + failed + ' failed');
|
|
192
|
+
if (failed > 0) {
|
|
193
|
+
for (const f of failures) {
|
|
194
|
+
console.log(' - ' + f.name + (f.detail ? ' :: ' + f.detail : ''));
|
|
195
|
+
}
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
process.exit(0);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
6
|
+
*
|
|
7
|
+
* Phase 121.5-07 Task 2 -- help-coverage CI guard tests.
|
|
8
|
+
*
|
|
9
|
+
* Test map (4 cases, PLAN Task 2 <behavior> Tests 10-13):
|
|
10
|
+
*
|
|
11
|
+
* 10. scripts/check-help-coverage.cjs exits 0 against live repo.
|
|
12
|
+
* 11. Synthetic fixture: temp command without help_jtbd -> exit 1.
|
|
13
|
+
* 12. Synthetic fixture: command file exists but NOT in help-groups.json
|
|
14
|
+
* (and not admin) -> exit 1.
|
|
15
|
+
* 13. Infrastructure group contains "doctor" (closes Cluster 5 audit
|
|
16
|
+
* finding -- doctor was previously silently absent from help.md).
|
|
17
|
+
*
|
|
18
|
+
* Canon Part 3 (UI Ruling System), Part 7 (Reuse Before Build), Part 8
|
|
19
|
+
* (Graph Boundary).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const assert = require('node:assert/strict');
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const os = require('node:os');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
const { spawnSync } = require('node:child_process');
|
|
27
|
+
|
|
28
|
+
const REPO = path.resolve(__dirname, '..', '..');
|
|
29
|
+
const CHECKER = path.join(REPO, 'scripts', 'check-help-coverage.cjs');
|
|
30
|
+
const GROUPS_PATH = path.join(REPO, 'data', 'help-groups.json');
|
|
31
|
+
const COMMANDS_DIR = path.join(REPO, 'commands');
|
|
32
|
+
|
|
33
|
+
let passed = 0;
|
|
34
|
+
let failed = 0;
|
|
35
|
+
const failures = [];
|
|
36
|
+
|
|
37
|
+
function run(name, fn) {
|
|
38
|
+
try {
|
|
39
|
+
fn();
|
|
40
|
+
console.log(' PASS ' + name);
|
|
41
|
+
passed++;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error(' FAIL ' + name);
|
|
44
|
+
console.error(' ' + (err.message || err));
|
|
45
|
+
failed++;
|
|
46
|
+
failures.push(name);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- Test 10: live repo passes ---
|
|
51
|
+
run('Test 10: check-help-coverage.cjs exits 0 against live repo', () => {
|
|
52
|
+
assert.ok(fs.existsSync(CHECKER), 'scripts/check-help-coverage.cjs must exist');
|
|
53
|
+
const r = spawnSync('node', [CHECKER], { encoding: 'utf8' });
|
|
54
|
+
assert.equal(r.status, 0, 'expected exit 0, got ' + r.status + '\nstdout:' + r.stdout + '\nstderr:' + r.stderr);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// --- Test 11: synthetic fixture without help_jtbd -> exit 1 ---
|
|
58
|
+
run('Test 11: command without help_jtbd causes exit 1', () => {
|
|
59
|
+
// Create a temp commands/foo.md without help_jtbd; the checker exits 1.
|
|
60
|
+
const tmpName = '___coverage_tmp_no_jtbd.md';
|
|
61
|
+
const tmpPath = path.join(COMMANDS_DIR, tmpName);
|
|
62
|
+
const content = '---\nname: tmp-no-jtbd\ndescription: temp fixture\n---\n# tmp\n';
|
|
63
|
+
fs.writeFileSync(tmpPath, content);
|
|
64
|
+
try {
|
|
65
|
+
const r = spawnSync('node', [CHECKER], { encoding: 'utf8' });
|
|
66
|
+
assert.notEqual(r.status, 0, 'expected non-zero exit (missing help_jtbd)');
|
|
67
|
+
assert.ok(/help_jtbd/i.test(r.stdout + r.stderr), 'expected diagnostic mentioning help_jtbd');
|
|
68
|
+
} finally {
|
|
69
|
+
fs.unlinkSync(tmpPath);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// --- Test 12: synthetic fixture file present but not in groups -> exit 1 ---
|
|
74
|
+
run('Test 12: command file not in help-groups.json causes exit 1', () => {
|
|
75
|
+
const tmpName = '___coverage_tmp_no_group.md';
|
|
76
|
+
const tmpPath = path.join(COMMANDS_DIR, tmpName);
|
|
77
|
+
// Has help_jtbd but is NOT in data/help-groups.json AND not admin.
|
|
78
|
+
const content =
|
|
79
|
+
'---\nname: tmp-no-group\ndescription: temp fixture\nhelp_jtbd: "Test fixture command without a group."\n---\n# tmp\n';
|
|
80
|
+
fs.writeFileSync(tmpPath, content);
|
|
81
|
+
try {
|
|
82
|
+
const r = spawnSync('node', [CHECKER], { encoding: 'utf8' });
|
|
83
|
+
assert.notEqual(r.status, 0, 'expected non-zero exit (missing from groups)');
|
|
84
|
+
assert.ok(
|
|
85
|
+
/help-groups\.json|MISSING from help-groups/i.test(r.stdout + r.stderr),
|
|
86
|
+
'expected diagnostic mentioning help-groups.json'
|
|
87
|
+
);
|
|
88
|
+
} finally {
|
|
89
|
+
fs.unlinkSync(tmpPath);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// --- Test 13: Infrastructure group contains doctor ---
|
|
94
|
+
run('Test 13: Infrastructure group contains doctor (Cluster 5 audit closed)', () => {
|
|
95
|
+
const groups = JSON.parse(fs.readFileSync(GROUPS_PATH, 'utf8'));
|
|
96
|
+
const infra = groups.groups.find((g) => g.id === 'infrastructure');
|
|
97
|
+
assert.ok(infra, 'infrastructure group must exist');
|
|
98
|
+
assert.ok(infra.commands.includes('doctor'), 'Infrastructure must contain doctor');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log('passed=' + passed + ' failed=' + failed);
|
|
103
|
+
if (failed > 0) {
|
|
104
|
+
console.error('Failed tests:');
|
|
105
|
+
for (const t of failures) console.error(' - ' + t);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
process.exit(0);
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
6
|
+
*
|
|
7
|
+
* Phase 121.5-07 Task 2 -- help-renderer + help-groups.json tests.
|
|
8
|
+
*
|
|
9
|
+
* Test map (6 cases, one-to-one with PLAN Task 2 <behavior> Tests 1-6):
|
|
10
|
+
*
|
|
11
|
+
* 1. data/help-groups.json parses; groups[] has 11 entries.
|
|
12
|
+
* 2. Each group entry has {id, label, glyph, commands: []}.
|
|
13
|
+
* 3. Every commands/*.md (non-admin) appears in EXACTLY one group's commands[].
|
|
14
|
+
* 4. renderHelp({capability: 'truecolor'}) emits 24-bit ANSI escapes.
|
|
15
|
+
* 5. renderHelp({capability: 'ascii'}) emits zero \x1b escape sequences.
|
|
16
|
+
* 6. renderHelp({capability: 'ascii'}) contains sentinel glyphs for each
|
|
17
|
+
* of the 11 groups (proves all groups render in ASCII mode).
|
|
18
|
+
*
|
|
19
|
+
* Canon Part 3 (UI Ruling System), Part 7 (Reuse Before Build), Part 8
|
|
20
|
+
* (Graph Boundary -- filesystem only, no network, no Brain, no telemetry).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const assert = require('node:assert/strict');
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
|
|
27
|
+
const REPO = path.resolve(__dirname, '..', '..');
|
|
28
|
+
const GROUPS_PATH = path.join(REPO, 'data', 'help-groups.json');
|
|
29
|
+
const RENDERER_PATH = path.join(REPO, 'scripts', 'help-renderer.cjs');
|
|
30
|
+
const COMMANDS_DIR = path.join(REPO, 'commands');
|
|
31
|
+
|
|
32
|
+
let passed = 0;
|
|
33
|
+
let failed = 0;
|
|
34
|
+
const failures = [];
|
|
35
|
+
|
|
36
|
+
function run(name, fn) {
|
|
37
|
+
try {
|
|
38
|
+
fn();
|
|
39
|
+
console.log(' PASS ' + name);
|
|
40
|
+
passed++;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(' FAIL ' + name);
|
|
43
|
+
console.error(' ' + (err.message || err));
|
|
44
|
+
failed++;
|
|
45
|
+
failures.push(name);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- Test 1: data/help-groups.json parses + 11 groups ---
|
|
50
|
+
run('Test 1: data/help-groups.json parses; groups[] has 11 entries', () => {
|
|
51
|
+
assert.ok(fs.existsSync(GROUPS_PATH), 'data/help-groups.json must exist');
|
|
52
|
+
const groups = JSON.parse(fs.readFileSync(GROUPS_PATH, 'utf8'));
|
|
53
|
+
assert.ok(Array.isArray(groups.groups), 'groups must be array');
|
|
54
|
+
assert.equal(groups.groups.length, 11, 'expected 11 groups, got ' + groups.groups.length);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// --- Test 2: each group has required keys ---
|
|
58
|
+
run('Test 2: each group has {id, label, glyph, commands: []}', () => {
|
|
59
|
+
const groups = JSON.parse(fs.readFileSync(GROUPS_PATH, 'utf8'));
|
|
60
|
+
for (const g of groups.groups) {
|
|
61
|
+
assert.ok(typeof g.id === 'string' && g.id.length > 0, 'group.id missing: ' + JSON.stringify(g));
|
|
62
|
+
assert.ok(typeof g.label === 'string' && g.label.length > 0, 'group.label missing: ' + g.id);
|
|
63
|
+
assert.ok(typeof g.glyph === 'string' && g.glyph.length > 0, 'group.glyph missing: ' + g.id);
|
|
64
|
+
assert.ok(Array.isArray(g.commands), 'group.commands not array: ' + g.id);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// --- Test 3: every non-admin command appears in exactly one group ---
|
|
69
|
+
run('Test 3: every non-admin command appears in exactly one group', () => {
|
|
70
|
+
const groups = JSON.parse(fs.readFileSync(GROUPS_PATH, 'utf8'));
|
|
71
|
+
const deprecated = new Set(
|
|
72
|
+
Object.keys(groups.deprecated_aliases || {}).filter((k) => !k.startsWith('_'))
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Build seen-count map.
|
|
76
|
+
const seen = new Map();
|
|
77
|
+
for (const g of groups.groups) {
|
|
78
|
+
for (const c of g.commands) {
|
|
79
|
+
seen.set(c, (seen.get(c) || 0) + 1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Every command file (non-admin) must be in exactly one group OR deprecated.
|
|
84
|
+
const files = fs.readdirSync(COMMANDS_DIR).filter((f) => f.endsWith('.md'));
|
|
85
|
+
const errors = [];
|
|
86
|
+
for (const f of files) {
|
|
87
|
+
const name = f.replace(/\.md$/, '');
|
|
88
|
+
const text = fs.readFileSync(path.join(COMMANDS_DIR, f), 'utf8');
|
|
89
|
+
const fm = text.match(/^---\n([\s\S]*?)\n---/);
|
|
90
|
+
let visibility = 'user';
|
|
91
|
+
if (fm) {
|
|
92
|
+
const vm = fm[1].match(/^visibility:\s*(\S+)/m);
|
|
93
|
+
if (vm) visibility = vm[1];
|
|
94
|
+
}
|
|
95
|
+
if (visibility === 'admin') {
|
|
96
|
+
// admin commands may be in Infrastructure group or absent; allowed either way.
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (deprecated.has(name)) continue;
|
|
100
|
+
const count = seen.get(name) || 0;
|
|
101
|
+
if (count === 0) errors.push(name + ' (missing from all groups)');
|
|
102
|
+
if (count > 1) errors.push(name + ' (appears in ' + count + ' groups)');
|
|
103
|
+
}
|
|
104
|
+
assert.deepEqual(errors, [], 'command-group violations: ' + errors.join(', '));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// --- Test 4: truecolor render emits ANSI ---
|
|
108
|
+
run('Test 4: renderHelp({capability: truecolor}) emits 24-bit ANSI', () => {
|
|
109
|
+
assert.ok(fs.existsSync(RENDERER_PATH), 'scripts/help-renderer.cjs must exist');
|
|
110
|
+
const { renderHelp } = require(RENDERER_PATH);
|
|
111
|
+
const out = renderHelp({ capability: 'truecolor' });
|
|
112
|
+
assert.ok(out.length > 100, 'output too short');
|
|
113
|
+
// Look for 24-bit truecolor escape pattern: ESC[38;2;R;G;Bm
|
|
114
|
+
assert.ok(/\x1b\[38;2;\d+;\d+;\d+m/.test(out), 'no 24-bit ANSI escape found');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// --- Test 5: ascii render has zero ANSI ---
|
|
118
|
+
run('Test 5: renderHelp({capability: ascii}) emits zero \\x1b escapes', () => {
|
|
119
|
+
const { renderHelp } = require(RENDERER_PATH);
|
|
120
|
+
const out = renderHelp({ capability: 'ascii' });
|
|
121
|
+
assert.ok(out.length > 100, 'output too short');
|
|
122
|
+
assert.ok(!/\x1b/.test(out), 'ASCII output contains \\x1b escape');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// --- Test 6: ASCII output renders all 11 group labels ---
|
|
126
|
+
run('Test 6: ASCII render contains all 11 group labels', () => {
|
|
127
|
+
const { renderHelp, loadGroups } = require(RENDERER_PATH);
|
|
128
|
+
const groups = loadGroups();
|
|
129
|
+
const out = renderHelp({ capability: 'ascii', groups });
|
|
130
|
+
for (const g of groups.groups) {
|
|
131
|
+
assert.ok(
|
|
132
|
+
out.includes(g.label),
|
|
133
|
+
'ASCII output missing group label "' + g.label + '"'
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
console.log('');
|
|
139
|
+
console.log('passed=' + passed + ' failed=' + failed);
|
|
140
|
+
if (failed > 0) {
|
|
141
|
+
console.error('Failed tests:');
|
|
142
|
+
for (const t of failures) console.error(' - ' + t);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
process.exit(0);
|