@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,225 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Phase 119-01 -- Transactionally-safe placeholder room discard cascade.
|
|
4
|
+
*
|
|
5
|
+
* Per CONTEXT.md D-06 + Architectural Decision item 4: when the user picks
|
|
6
|
+
* [discard room] in the F.1 selector, the entire placeholder is removed:
|
|
7
|
+
* room.db rows + STATE.md + MINTO.md + section folders + ROOM.md identity
|
|
8
|
+
* files + registry entry. Wrapped in a SQLite transaction + ordered fs ops
|
|
9
|
+
* so a partial failure is recoverable.
|
|
10
|
+
*
|
|
11
|
+
* Cascade ORDER (matters; the reverse-order rollback is unwinding):
|
|
12
|
+
* 1. Open room.db handle (room-db.cjs::openRoomDb -- the canonical opener
|
|
13
|
+
* that applies the Phase 109 nodes-provenance + session_focus migrations
|
|
14
|
+
* the memory_event log depends on).
|
|
15
|
+
* 2. BEGIN sqlite transaction.
|
|
16
|
+
* 3. emit room_discarded memory_event INSIDE the transaction (via navigation.cjs).
|
|
17
|
+
* 4. COMMIT transaction.
|
|
18
|
+
* 5. Close room.db handle (handle MUST close before file removal).
|
|
19
|
+
* 6. execFileSync bash scripts/room-registry archive <slug> + direct
|
|
20
|
+
* registry-key removal (the archive subcommand soft-deletes; we want hard
|
|
21
|
+
* removal because the user picked [discard room]).
|
|
22
|
+
* 7. fs.rmSync($roomsHome/$slug, {recursive: true, force: true}) (LAST -- the
|
|
23
|
+
* fs removal is final; the registry archive is rollback-able by a
|
|
24
|
+
* recovery script; the memory_event is durable in the rooms-meta.db
|
|
25
|
+
* fallback).
|
|
26
|
+
*
|
|
27
|
+
* Partial-failure recovery: if step 7 fails, emit a room_discard_partial_failure
|
|
28
|
+
* memory_event to a SEPARATE .rooms/rooms-meta.db at the rooms-home level (the
|
|
29
|
+
* room.db is either gone or in an inconsistent state). The rooms-meta.db is a
|
|
30
|
+
* Phase 119-01 introduced artifact for the discard recovery signal ONLY; future
|
|
31
|
+
* phases may consolidate this into the central registry-host db.
|
|
32
|
+
*
|
|
33
|
+
* Guard: only PLACEHOLDER slugs (matching PLACEHOLDER_SLUG_RE from Plan 119-00)
|
|
34
|
+
* are discardable via this cascade. Non-placeholder rooms MUST go through
|
|
35
|
+
* /mos:rooms archive.
|
|
36
|
+
*
|
|
37
|
+
* Canon Part 8: pure-local; no Brain MCP coupling.
|
|
38
|
+
* Canon Part 9: ALL memory_event writes route through navigation.cjs::logMemoryEvent.
|
|
39
|
+
* Canon Part 10 sub-claim 3: discard is a typed graph event (the placeholder
|
|
40
|
+
* becomes graph data even on departure).
|
|
41
|
+
* Em-dash discipline: uses `--` never the U+2014 character per HARD RULE.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const fs = require('node:fs');
|
|
45
|
+
const path = require('node:path');
|
|
46
|
+
const child_process = require('node:child_process');
|
|
47
|
+
|
|
48
|
+
// Cross-import the Plan 119-00 placeholder pattern (the cascade refuses any
|
|
49
|
+
// slug that does not match this regex).
|
|
50
|
+
const { PLACEHOLDER_SLUG_RE } = require('./room-auto-create.cjs');
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* discardPlaceholderRoom(roomsHome, slug, opts) -> {ok, slug, rolled_back_*, ...}
|
|
54
|
+
*
|
|
55
|
+
* @param {string} roomsHome
|
|
56
|
+
* @param {string} slug must match PLACEHOLDER_SLUG_RE
|
|
57
|
+
* @param {object} opts
|
|
58
|
+
* @param {string} [opts.decided_by] collaborator identity for the memory_event payload
|
|
59
|
+
* @returns {{ok, slug, rolled_back_db, rolled_back_fs, rolled_back_registry, partial_failure_event_id?, reason?}}
|
|
60
|
+
*/
|
|
61
|
+
function discardPlaceholderRoom(roomsHome, slug, opts) {
|
|
62
|
+
const options = opts || {};
|
|
63
|
+
const result = {
|
|
64
|
+
ok: false,
|
|
65
|
+
slug: slug,
|
|
66
|
+
rolled_back_db: false,
|
|
67
|
+
rolled_back_fs: false,
|
|
68
|
+
rolled_back_registry: false,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Guard 1: placeholder slug invariant.
|
|
72
|
+
if (typeof slug !== 'string' || !PLACEHOLDER_SLUG_RE.test(slug)) {
|
|
73
|
+
result.reason = 'not_a_placeholder_slug';
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const roomDir = path.join(roomsHome, slug);
|
|
78
|
+
const dbPath = path.join(roomDir, '.mindrian', 'room.db');
|
|
79
|
+
|
|
80
|
+
// Guard 2: roomDir must exist.
|
|
81
|
+
if (!fs.existsSync(roomDir)) {
|
|
82
|
+
result.reason = 'room_dir_not_found';
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Steps 1-5: room_discarded memory_event INSIDE a sqlite transaction.
|
|
87
|
+
let dbHandle = null;
|
|
88
|
+
try {
|
|
89
|
+
if (fs.existsSync(dbPath)) {
|
|
90
|
+
// Use room-db.cjs::openRoomDb (canonical opener; applies Phase 109
|
|
91
|
+
// migrations) per the Plan 119-00 Rule 3 deviation. The plan invoked
|
|
92
|
+
// lazygraph-ops.openRoomDb which does not exist; this is the canonical
|
|
93
|
+
// surface.
|
|
94
|
+
const { openRoomDb, closeRoomDb } = require('./room-db.cjs');
|
|
95
|
+
dbHandle = openRoomDb(roomDir);
|
|
96
|
+
if (dbHandle) {
|
|
97
|
+
const nav = require('./navigation.cjs');
|
|
98
|
+
try {
|
|
99
|
+
dbHandle.exec('BEGIN');
|
|
100
|
+
const eventResult = nav.logMemoryEvent(dbHandle, 'room_discarded', {
|
|
101
|
+
previous_slug: slug,
|
|
102
|
+
decided_by: options.decided_by || 'anonymous',
|
|
103
|
+
source_path: 'system:room-discard-cascade',
|
|
104
|
+
created_by: 'system',
|
|
105
|
+
});
|
|
106
|
+
if (eventResult && eventResult.ok) {
|
|
107
|
+
dbHandle.exec('COMMIT');
|
|
108
|
+
result.rolled_back_db = true;
|
|
109
|
+
} else {
|
|
110
|
+
try { dbHandle.exec('ROLLBACK'); } catch (_e2) { /* swallow */ }
|
|
111
|
+
}
|
|
112
|
+
} catch (_e) {
|
|
113
|
+
try { dbHandle.exec('ROLLBACK'); } catch (_e2) { /* swallow */ }
|
|
114
|
+
} finally {
|
|
115
|
+
try { closeRoomDb(dbHandle); } catch (_e2) { /* swallow */ }
|
|
116
|
+
dbHandle = null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (_e) {
|
|
121
|
+
// room-db unavailable -- skip the db step; the registry purge + fs.rmSync
|
|
122
|
+
// still proceed because the durable graph signal is best-effort.
|
|
123
|
+
if (dbHandle) {
|
|
124
|
+
try { dbHandle.close(); } catch (_e2) { /* swallow */ }
|
|
125
|
+
dbHandle = null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Step 6: registry archive + direct key removal.
|
|
130
|
+
try {
|
|
131
|
+
const registryScript = path.join(__dirname, '..', '..', 'scripts', 'room-registry');
|
|
132
|
+
if (fs.existsSync(registryScript)) {
|
|
133
|
+
try {
|
|
134
|
+
child_process.execFileSync('bash', [registryScript, 'archive', slug], {
|
|
135
|
+
cwd: process.cwd(),
|
|
136
|
+
env: Object.assign({}, process.env, { MINDRIAN_ROOMS_HOME: roomsHome }),
|
|
137
|
+
stdio: 'pipe',
|
|
138
|
+
timeout: 5000,
|
|
139
|
+
});
|
|
140
|
+
} catch (_archiveErr) {
|
|
141
|
+
// archive may fail if entry doesn't exist; continue to direct mutation.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Direct registry mutation: remove the key entirely (the archive subcommand
|
|
145
|
+
// sets status=archived; the discard cascade goes further by deleting the key).
|
|
146
|
+
const registryPath = path.join(roomsHome, '.rooms', 'registry.json');
|
|
147
|
+
if (fs.existsSync(registryPath)) {
|
|
148
|
+
const raw = fs.readFileSync(registryPath, 'utf8');
|
|
149
|
+
const reg = JSON.parse(raw);
|
|
150
|
+
if (reg && typeof reg.rooms === 'object' && reg.rooms[slug]) {
|
|
151
|
+
delete reg.rooms[slug];
|
|
152
|
+
if (reg.active === slug) reg.active = '';
|
|
153
|
+
const tmpPath = registryPath + '.tmp.' + process.pid + '.' + Math.random().toString(36).slice(2, 8);
|
|
154
|
+
fs.writeFileSync(tmpPath, JSON.stringify(reg, null, 2), 'utf8');
|
|
155
|
+
fs.renameSync(tmpPath, registryPath);
|
|
156
|
+
result.rolled_back_registry = true;
|
|
157
|
+
} else if (reg && typeof reg.rooms === 'object') {
|
|
158
|
+
// Entry already absent (idempotent path); mark as registry purged.
|
|
159
|
+
result.rolled_back_registry = true;
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
// No registry to purge; treat as nothing-to-do success for this step.
|
|
163
|
+
result.rolled_back_registry = true;
|
|
164
|
+
}
|
|
165
|
+
} catch (registryErr) {
|
|
166
|
+
// Registry update failed -- record partial failure and bail out before fs.rmSync.
|
|
167
|
+
_emitPartialFailure(roomsHome, slug, result, registryErr);
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Step 7: filesystem rmSync (LAST -- final, irreversible). On failure, emit the
|
|
172
|
+
// partial-failure memory_event to a fallback rooms-meta.db so /mos:doctor can
|
|
173
|
+
// find the orphaned directory + the archived-but-still-present registry footprint.
|
|
174
|
+
try {
|
|
175
|
+
fs.rmSync(roomDir, { recursive: true, force: true });
|
|
176
|
+
if (!fs.existsSync(roomDir)) {
|
|
177
|
+
result.rolled_back_fs = true;
|
|
178
|
+
}
|
|
179
|
+
} catch (fsErr) {
|
|
180
|
+
_emitPartialFailure(roomsHome, slug, result, fsErr);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
result.ok = result.rolled_back_fs && result.rolled_back_registry;
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Internal: emit the room_discard_partial_failure memory_event to a fallback
|
|
190
|
+
* .rooms-meta.db at the rooms-home level. This is the recovery hook /mos:doctor
|
|
191
|
+
* --orphaned-room-cleanup uses to find orphaned placeholder rooms (the v1.13.0
|
|
192
|
+
* housekeeping pass; THIS plan only ships the recovery signal).
|
|
193
|
+
*/
|
|
194
|
+
function _emitPartialFailure(roomsHome, slug, result, err) {
|
|
195
|
+
const error_short = String(err && err.message || err).slice(0, 60);
|
|
196
|
+
result.reason = error_short;
|
|
197
|
+
try {
|
|
198
|
+
const metaRoomDir = path.join(roomsHome, '.rooms', '_meta');
|
|
199
|
+
// _meta is a synthetic "room dir" containing a real room.db; we use the
|
|
200
|
+
// canonical openRoomDb opener with the standard .mindrian/room.db nesting.
|
|
201
|
+
fs.mkdirSync(path.join(metaRoomDir, '.mindrian'), { recursive: true, mode: 0o755 });
|
|
202
|
+
const { openRoomDb, closeRoomDb } = require('./room-db.cjs');
|
|
203
|
+
const handle = openRoomDb(metaRoomDir);
|
|
204
|
+
if (handle) {
|
|
205
|
+
const nav = require('./navigation.cjs');
|
|
206
|
+
const eventResult = nav.logMemoryEvent(handle, 'room_discard_partial_failure', {
|
|
207
|
+
previous_slug: slug,
|
|
208
|
+
partial_state: {
|
|
209
|
+
fs_removed: result.rolled_back_fs,
|
|
210
|
+
registry_purged: result.rolled_back_registry,
|
|
211
|
+
db_dropped: result.rolled_back_db,
|
|
212
|
+
},
|
|
213
|
+
error_short: error_short,
|
|
214
|
+
source_path: 'system:room-discard-cascade',
|
|
215
|
+
created_by: 'system',
|
|
216
|
+
});
|
|
217
|
+
if (eventResult && eventResult.ok) {
|
|
218
|
+
result.partial_failure_event_id = eventResult.eventId;
|
|
219
|
+
}
|
|
220
|
+
try { closeRoomDb(handle); } catch (_e2) { /* swallow */ }
|
|
221
|
+
}
|
|
222
|
+
} catch (_e) { /* if even the partial-failure emission fails, swallow */ }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = { discardPlaceholderRoom: discardPlaceholderRoom };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 119-01 Task 2 tests for lib/core/room-discard-cascade.cjs.
|
|
3
|
+
// Validates the happy-path cascade, the guard for non-placeholder slugs,
|
|
4
|
+
// the partial-failure recovery via rooms-meta.db, and the Canon Part 8/9
|
|
5
|
+
// invariants (no Brain coupling; writes through navigation.cjs chokepoint).
|
|
6
|
+
|
|
7
|
+
const test = require('node:test');
|
|
8
|
+
const assert = require('node:assert');
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
|
|
13
|
+
const { discardPlaceholderRoom } = require('./room-discard-cascade.cjs');
|
|
14
|
+
const { openRoomDb, closeRoomDb } = require('./room-db.cjs');
|
|
15
|
+
|
|
16
|
+
function _mkPlaceholderRoom(slug) {
|
|
17
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'discard-cascade-test-'));
|
|
18
|
+
fs.mkdirSync(path.join(tmp, '.rooms'), { recursive: true });
|
|
19
|
+
const roomDir = path.join(tmp, slug);
|
|
20
|
+
fs.mkdirSync(roomDir, { recursive: true });
|
|
21
|
+
fs.writeFileSync(path.join(roomDir, '.room-root'), '');
|
|
22
|
+
// Bootstrap a real room.db (the canonical opener auto-creates the file).
|
|
23
|
+
const db = openRoomDb(roomDir);
|
|
24
|
+
closeRoomDb(db);
|
|
25
|
+
// Seed registry.
|
|
26
|
+
const reg = {
|
|
27
|
+
version: 1,
|
|
28
|
+
active: slug,
|
|
29
|
+
rooms: {
|
|
30
|
+
[slug]: {
|
|
31
|
+
path: roomDir,
|
|
32
|
+
venture_name: 'untitled',
|
|
33
|
+
venture_stage: 'Pre-Opportunity',
|
|
34
|
+
status: 'active',
|
|
35
|
+
last_opened: Date.now(),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
fs.writeFileSync(path.join(tmp, '.rooms', 'registry.json'), JSON.stringify(reg, null, 2), 'utf8');
|
|
40
|
+
return { roomsHome: tmp, roomDir, slug };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function _cleanup(roomsHome) {
|
|
44
|
+
try { fs.rmSync(roomsHome, { recursive: true, force: true }); } catch (_e) {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test('Test 10: discardPlaceholderRoom happy path -- transactional rollback flags all true', function () {
|
|
48
|
+
const { roomsHome, roomDir, slug } = _mkPlaceholderRoom('untitled-2026-05-16-1845');
|
|
49
|
+
try {
|
|
50
|
+
const r = discardPlaceholderRoom(roomsHome, slug, { decided_by: 'jsagi' });
|
|
51
|
+
assert.strictEqual(r.ok, true, 'cascade must succeed; got ' + JSON.stringify(r));
|
|
52
|
+
assert.strictEqual(r.slug, slug);
|
|
53
|
+
assert.strictEqual(r.rolled_back_fs, true);
|
|
54
|
+
assert.strictEqual(r.rolled_back_registry, true);
|
|
55
|
+
assert.strictEqual(r.rolled_back_db, true, 'memory_event must land inside transaction');
|
|
56
|
+
// Filesystem assertion: directory no longer exists.
|
|
57
|
+
assert.ok(!fs.existsSync(roomDir), 'roomDir must be removed');
|
|
58
|
+
// Registry assertion: slug removed from rooms map.
|
|
59
|
+
const reg = JSON.parse(fs.readFileSync(path.join(roomsHome, '.rooms', 'registry.json'), 'utf8'));
|
|
60
|
+
assert.ok(!reg.rooms[slug], 'registry entry must be purged');
|
|
61
|
+
assert.strictEqual(reg.active, '', 'active must be cleared when active room is discarded');
|
|
62
|
+
} finally { _cleanup(roomsHome); }
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('Test 11: discardPlaceholderRoom on fs.rmSync failure -> partial-failure recovery via rooms-meta.db', function () {
|
|
66
|
+
const { roomsHome, roomDir, slug } = _mkPlaceholderRoom('untitled-2026-05-16-1900');
|
|
67
|
+
try {
|
|
68
|
+
// Monkey-patch fs.rmSync to throw EACCES the FIRST time it's called on
|
|
69
|
+
// roomDir (the discard cascade calls it on the target room). Other paths
|
|
70
|
+
// (tmp files etc.) must still work.
|
|
71
|
+
const origRmSync = fs.rmSync;
|
|
72
|
+
let tripped = false;
|
|
73
|
+
fs.rmSync = function (target, opts) {
|
|
74
|
+
if (!tripped && typeof target === 'string' && target === roomDir) {
|
|
75
|
+
tripped = true;
|
|
76
|
+
const err = new Error('EACCES: permission denied');
|
|
77
|
+
err.code = 'EACCES';
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
return origRmSync.call(fs, target, opts);
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
const r = discardPlaceholderRoom(roomsHome, slug, { decided_by: 'jsagi' });
|
|
84
|
+
assert.strictEqual(r.ok, false, 'cascade must fail when fs.rmSync throws');
|
|
85
|
+
assert.strictEqual(r.rolled_back_fs, false, 'fs MUST remain not-removed on EACCES');
|
|
86
|
+
// Directory must still exist (the cascade bounded the failure).
|
|
87
|
+
assert.ok(fs.existsSync(roomDir), 'roomDir must still exist after partial failure');
|
|
88
|
+
// partial_failure_event_id should be populated.
|
|
89
|
+
assert.ok(typeof r.partial_failure_event_id === 'string' && r.partial_failure_event_id.length > 0,
|
|
90
|
+
'partial_failure_event_id must be set; got ' + JSON.stringify(r));
|
|
91
|
+
// rooms-meta.db file must exist on disk.
|
|
92
|
+
const metaDbPath = path.join(roomsHome, '.rooms', '_meta', '.mindrian', 'room.db');
|
|
93
|
+
assert.ok(fs.existsSync(metaDbPath), 'rooms-meta.db must materialize for recovery');
|
|
94
|
+
} finally {
|
|
95
|
+
fs.rmSync = origRmSync;
|
|
96
|
+
}
|
|
97
|
+
} finally { _cleanup(roomsHome); }
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('Test 12: guard -- non-placeholder slug is refused (must go through /mos:rooms archive)', function () {
|
|
101
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'discard-guard-test-'));
|
|
102
|
+
try {
|
|
103
|
+
const r = discardPlaceholderRoom(tmp, 'acme-robotics', {});
|
|
104
|
+
assert.strictEqual(r.ok, false);
|
|
105
|
+
assert.strictEqual(r.reason, 'not_a_placeholder_slug');
|
|
106
|
+
assert.strictEqual(r.rolled_back_db, false);
|
|
107
|
+
assert.strictEqual(r.rolled_back_fs, false);
|
|
108
|
+
assert.strictEqual(r.rolled_back_registry, false);
|
|
109
|
+
} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (_e) {} }
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('Test 13: Canon Part 9 chokepoint -- writes route through navigation.cjs::logMemoryEvent', function () {
|
|
113
|
+
const src = fs.readFileSync(require.resolve('./room-discard-cascade.cjs'), 'utf8');
|
|
114
|
+
// No direct SQL INSERT into nodes (the chokepoint owns INSERTs).
|
|
115
|
+
assert.ok(!/db\.prepare\(\s*['\"]INSERT/.test(src),
|
|
116
|
+
'direct SQL INSERT detected -- writes MUST go through navigation.cjs::logMemoryEvent');
|
|
117
|
+
// Must call logMemoryEvent at least twice (room_discarded + room_discard_partial_failure).
|
|
118
|
+
const matches = src.match(/logMemoryEvent\(/g) || [];
|
|
119
|
+
assert.ok(matches.length >= 2, 'expected >= 2 logMemoryEvent calls; got ' + matches.length);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('Test 14: Canon Part 8 -- no Brain coupling', function () {
|
|
123
|
+
const src = fs.readFileSync(require.resolve('./room-discard-cascade.cjs'), 'utf8');
|
|
124
|
+
assert.ok(src.indexOf('brain.mindrian') === -1, 'brain.mindrian substring present (Canon Part 8 breach)');
|
|
125
|
+
assert.ok(!/require\([^)]*brain-client[^)]*\)/.test(src), 'brain-client require (Canon Part 8 breach)');
|
|
126
|
+
assert.ok(!/fetch\([^)]*['\"][^'\"]*brain[^'\"]*['\"]/.test(src), 'fetch to brain.* host (Canon Part 8 breach)');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('Test 15: em-dash invariant', function () {
|
|
130
|
+
const EMDASH = String.fromCharCode(0x2014);
|
|
131
|
+
const srcCascade = fs.readFileSync(require.resolve('./room-discard-cascade.cjs'), 'utf8');
|
|
132
|
+
const srcValidator = fs.readFileSync(require.resolve('./room-name-validator.cjs'), 'utf8');
|
|
133
|
+
assert.ok(srcCascade.indexOf(EMDASH) === -1, 'em-dash present in room-discard-cascade.cjs (HARD RULE)');
|
|
134
|
+
assert.ok(srcValidator.indexOf(EMDASH) === -1, 'em-dash present in room-name-validator.cjs (HARD RULE)');
|
|
135
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Phase 119-01 -- Room name validator (four rejection classes).
|
|
4
|
+
*
|
|
5
|
+
* Per CONTEXT.md Architectural Decision item 5: when the user picks
|
|
6
|
+
* [type your own name] in the F.1 selector, this validator runs.
|
|
7
|
+
* Four rejection classes: collision, fs-unsafe chars, reserved untitled-
|
|
8
|
+
* prefix, empty / whitespace. On any failure, the F.1 selector re-prompts
|
|
9
|
+
* inline with the rejection reason; it MUST NOT fall back to a different
|
|
10
|
+
* F.1 option silently.
|
|
11
|
+
*
|
|
12
|
+
* Defense-in-depth: a fifth class (Windows-reserved device names) is also
|
|
13
|
+
* surfaced because rooms may sync across CLI / Desktop / Cowork surfaces per
|
|
14
|
+
* CLAUDE.md tri-polar HARD RULE; a Linux/macOS-named room that collides with
|
|
15
|
+
* a Windows reserved device name (CON, PRN, LPT1...) breaks the user's room
|
|
16
|
+
* the moment it syncs to a Windows machine.
|
|
17
|
+
*
|
|
18
|
+
* Canon Part 8 invariant: pure-local; no Brain MCP, no fetch, no telemetry.
|
|
19
|
+
* Em-dash discipline: uses `--` never the U+2014 character per HARD RULE.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
|
|
25
|
+
// Canonical slug pattern: lowercase letters, digits, hyphens.
|
|
26
|
+
// Minimum 3 chars to avoid single-letter directory names that clash with
|
|
27
|
+
// common filesystem temp patterns. Maximum 64 chars per /mos:rooms list
|
|
28
|
+
// rendering budget (the registry render uses fixed-column alignment).
|
|
29
|
+
const FS_SAFE_SLUG_RE = Object.freeze(/^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/);
|
|
30
|
+
|
|
31
|
+
// Reserved-prefix namespaces that the user MUST NOT manually claim.
|
|
32
|
+
// 'untitled-' is reserved by the Phase 119-00 placeholder pattern.
|
|
33
|
+
const RESERVED_PREFIXES = Object.freeze(['untitled-']);
|
|
34
|
+
|
|
35
|
+
// Windows reserved device names (the validator surfaces these even on Linux/
|
|
36
|
+
// macOS because rooms may sync across surfaces per CLAUDE.md tri-polar rule).
|
|
37
|
+
const WINDOWS_RESERVED = Object.freeze(new Set([
|
|
38
|
+
'con', 'prn', 'aux', 'nul',
|
|
39
|
+
'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9',
|
|
40
|
+
'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9',
|
|
41
|
+
]));
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* validateRoomName(candidate, opts) -> {ok, normalized_slug, reasons}
|
|
45
|
+
*
|
|
46
|
+
* @param {string} candidate user input (free-text, untrusted)
|
|
47
|
+
* @param {object} opts
|
|
48
|
+
* @param {string} [opts.roomsHome] absolute path to $ROOMS_HOME for collision check
|
|
49
|
+
* @returns {{ok: boolean, normalized_slug: string, reasons: string[]}}
|
|
50
|
+
*/
|
|
51
|
+
function validateRoomName(candidate, opts) {
|
|
52
|
+
const options = opts || {};
|
|
53
|
+
const reasons = [];
|
|
54
|
+
|
|
55
|
+
// Rejection class 4: empty / whitespace.
|
|
56
|
+
if (typeof candidate !== 'string') {
|
|
57
|
+
return { ok: false, normalized_slug: '', reasons: ['empty'] };
|
|
58
|
+
}
|
|
59
|
+
const trimmed = candidate.trim();
|
|
60
|
+
if (trimmed.length === 0) {
|
|
61
|
+
return { ok: false, normalized_slug: '', reasons: ['empty'] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Normalize: lowercase + trim. We do NOT silently substitute unsafe chars;
|
|
65
|
+
// unsafe input surfaces the fs_unsafe_chars rejection so the user can fix it.
|
|
66
|
+
const normalized = trimmed.toLowerCase();
|
|
67
|
+
|
|
68
|
+
// Rejection class 3: reserved prefix. Match the canonical hyphenated form
|
|
69
|
+
// (RESERVED_PREFIXES e.g. 'untitled-') AND the broader 'untitled' family
|
|
70
|
+
// head (bare 'untitled' OR 'untitled' followed by any non-alphanumeric
|
|
71
|
+
// separator). This closes the "untitled/with-slash" / "untitled.foo"
|
|
72
|
+
// namespace-escape vectors while still allowing names like 'untitledly'
|
|
73
|
+
// to pass the reserved-prefix gate (they will still hit fs_unsafe_chars
|
|
74
|
+
// if non-canonical).
|
|
75
|
+
for (const prefix of RESERVED_PREFIXES) {
|
|
76
|
+
if (normalized.indexOf(prefix) === 0) {
|
|
77
|
+
reasons.push('reserved_prefix:' + prefix);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Bare 'untitled' OR 'untitled' followed by a non-alphanumeric separator
|
|
82
|
+
// (slash, dot, space, etc.) is also reserved.
|
|
83
|
+
if (reasons.indexOf('reserved_prefix:untitled-') === -1) {
|
|
84
|
+
if (normalized === 'untitled' || /^untitled[^a-z0-9]/.test(normalized)) {
|
|
85
|
+
reasons.push('reserved_prefix:untitled-');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Rejection class 2: fs-unsafe characters / shape.
|
|
90
|
+
if (!FS_SAFE_SLUG_RE.test(normalized)) {
|
|
91
|
+
reasons.push('fs_unsafe_chars');
|
|
92
|
+
}
|
|
93
|
+
// Defense-in-depth: Windows reserved device names (regardless of platform).
|
|
94
|
+
if (WINDOWS_RESERVED.has(normalized)) {
|
|
95
|
+
reasons.push('windows_reserved');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Rejection class 1: collision check (registry + on-disk).
|
|
99
|
+
if (options.roomsHome && typeof options.roomsHome === 'string') {
|
|
100
|
+
const registryPath = path.join(options.roomsHome, '.rooms', 'registry.json');
|
|
101
|
+
try {
|
|
102
|
+
if (fs.existsSync(registryPath)) {
|
|
103
|
+
const raw = fs.readFileSync(registryPath, 'utf8');
|
|
104
|
+
const reg = JSON.parse(raw);
|
|
105
|
+
const rooms = (reg && typeof reg.rooms === 'object') ? reg.rooms : {};
|
|
106
|
+
if (Object.prototype.hasOwnProperty.call(rooms, normalized)) {
|
|
107
|
+
reasons.push('collision');
|
|
108
|
+
}
|
|
109
|
+
// Defense-in-depth: registry may be out of sync with disk.
|
|
110
|
+
const candidateDir = path.join(options.roomsHome, normalized);
|
|
111
|
+
if (fs.existsSync(candidateDir) && reasons.indexOf('collision') === -1) {
|
|
112
|
+
reasons.push('collision');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch (_e) {
|
|
116
|
+
// Registry unreadable -- skip collision check; degrade open.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
ok: reasons.length === 0,
|
|
122
|
+
normalized_slug: normalized,
|
|
123
|
+
reasons: reasons,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
validateRoomName: validateRoomName,
|
|
129
|
+
FS_SAFE_SLUG_RE: FS_SAFE_SLUG_RE,
|
|
130
|
+
RESERVED_PREFIXES: RESERVED_PREFIXES,
|
|
131
|
+
WINDOWS_RESERVED: WINDOWS_RESERVED,
|
|
132
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 119-01 Task 2 tests for lib/core/room-name-validator.cjs.
|
|
3
|
+
// Validates FS_SAFE_SLUG_RE shape + 4 rejection classes + Windows-reserved
|
|
4
|
+
// defense-in-depth + normalization + multi-rejection compounding.
|
|
5
|
+
|
|
6
|
+
const test = require('node:test');
|
|
7
|
+
const assert = require('node:assert');
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const os = require('node:os');
|
|
11
|
+
|
|
12
|
+
const { validateRoomName, FS_SAFE_SLUG_RE, RESERVED_PREFIXES, WINDOWS_RESERVED } = require('./room-name-validator.cjs');
|
|
13
|
+
|
|
14
|
+
function _mkTmpRoomsHome() {
|
|
15
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'room-validator-test-'));
|
|
16
|
+
fs.mkdirSync(path.join(tmp, '.rooms'), { recursive: true });
|
|
17
|
+
return tmp;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function _seedRegistry(roomsHome, rooms) {
|
|
21
|
+
const reg = { version: 1, active: '', rooms: rooms || {} };
|
|
22
|
+
fs.writeFileSync(path.join(roomsHome, '.rooms', 'registry.json'), JSON.stringify(reg, null, 2), 'utf8');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('Test 1: FS_SAFE_SLUG_RE accepts canonical slugs', function () {
|
|
26
|
+
assert.ok(FS_SAFE_SLUG_RE.test('acme-robotics'));
|
|
27
|
+
assert.ok(FS_SAFE_SLUG_RE.test('biotech-imaging-v2'));
|
|
28
|
+
assert.ok(FS_SAFE_SLUG_RE.test('a-b'));
|
|
29
|
+
assert.ok(FS_SAFE_SLUG_RE.test('abc'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('Test 2: FS_SAFE_SLUG_RE rejects unsafe chars', function () {
|
|
33
|
+
assert.ok(!FS_SAFE_SLUG_RE.test('acme/robotics'));
|
|
34
|
+
assert.ok(!FS_SAFE_SLUG_RE.test('acme robotics'));
|
|
35
|
+
assert.ok(!FS_SAFE_SLUG_RE.test('acme.robotics'));
|
|
36
|
+
assert.ok(!FS_SAFE_SLUG_RE.test('../etc'));
|
|
37
|
+
assert.ok(!FS_SAFE_SLUG_RE.test('.hidden'));
|
|
38
|
+
// Single char and leading-hyphen
|
|
39
|
+
assert.ok(!FS_SAFE_SLUG_RE.test('a'));
|
|
40
|
+
assert.ok(!FS_SAFE_SLUG_RE.test('-ab'));
|
|
41
|
+
assert.ok(!FS_SAFE_SLUG_RE.test('ab-'));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('Test 3: collision rejection -- registry has an entry with the same slug', function () {
|
|
45
|
+
const roomsHome = _mkTmpRoomsHome();
|
|
46
|
+
try {
|
|
47
|
+
_seedRegistry(roomsHome, { 'acme-robotics': { path: '/tmp/acme', venture_name: 'acme', status: 'active' } });
|
|
48
|
+
const r = validateRoomName('acme-robotics', { roomsHome });
|
|
49
|
+
assert.strictEqual(r.ok, false);
|
|
50
|
+
assert.strictEqual(r.normalized_slug, 'acme-robotics');
|
|
51
|
+
assert.ok(r.reasons.indexOf('collision') !== -1, 'expected collision reason; got ' + JSON.stringify(r.reasons));
|
|
52
|
+
} finally {
|
|
53
|
+
try { fs.rmSync(roomsHome, { recursive: true, force: true }); } catch (_e) {}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('Test 4: fs-unsafe rejection', function () {
|
|
58
|
+
const roomsHome = _mkTmpRoomsHome();
|
|
59
|
+
try {
|
|
60
|
+
_seedRegistry(roomsHome, {});
|
|
61
|
+
const r = validateRoomName('acme/robotics', { roomsHome });
|
|
62
|
+
assert.strictEqual(r.ok, false);
|
|
63
|
+
assert.ok(r.reasons.indexOf('fs_unsafe_chars') !== -1, 'expected fs_unsafe_chars; got ' + JSON.stringify(r.reasons));
|
|
64
|
+
} finally {
|
|
65
|
+
try { fs.rmSync(roomsHome, { recursive: true, force: true }); } catch (_e) {}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('Test 5: reserved-prefix rejection (untitled-* AND bare untitled)', function () {
|
|
70
|
+
const roomsHome = _mkTmpRoomsHome();
|
|
71
|
+
try {
|
|
72
|
+
_seedRegistry(roomsHome, {});
|
|
73
|
+
const r1 = validateRoomName('untitled-mything', { roomsHome });
|
|
74
|
+
assert.strictEqual(r1.ok, false);
|
|
75
|
+
assert.ok(r1.reasons.indexOf('reserved_prefix:untitled-') !== -1, 'expected reserved_prefix; got ' + JSON.stringify(r1.reasons));
|
|
76
|
+
|
|
77
|
+
const r2 = validateRoomName('untitled', { roomsHome });
|
|
78
|
+
assert.strictEqual(r2.ok, false);
|
|
79
|
+
assert.ok(r2.reasons.indexOf('reserved_prefix:untitled-') !== -1, 'expected reserved_prefix on bare untitled; got ' + JSON.stringify(r2.reasons));
|
|
80
|
+
} finally {
|
|
81
|
+
try { fs.rmSync(roomsHome, { recursive: true, force: true }); } catch (_e) {}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('Test 6: empty / whitespace-only rejection', function () {
|
|
86
|
+
const r1 = validateRoomName('', {});
|
|
87
|
+
assert.strictEqual(r1.ok, false);
|
|
88
|
+
assert.deepStrictEqual(r1.reasons, ['empty']);
|
|
89
|
+
|
|
90
|
+
const r2 = validateRoomName(' ', {});
|
|
91
|
+
assert.strictEqual(r2.ok, false);
|
|
92
|
+
assert.deepStrictEqual(r2.reasons, ['empty']);
|
|
93
|
+
|
|
94
|
+
const r3 = validateRoomName('\t\n', {});
|
|
95
|
+
assert.strictEqual(r3.ok, false);
|
|
96
|
+
assert.deepStrictEqual(r3.reasons, ['empty']);
|
|
97
|
+
|
|
98
|
+
const r4 = validateRoomName(null, {});
|
|
99
|
+
assert.strictEqual(r4.ok, false);
|
|
100
|
+
assert.deepStrictEqual(r4.reasons, ['empty']);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('Test 7: multi-rejection compounding (fs_unsafe + reserved_prefix)', function () {
|
|
104
|
+
const roomsHome = _mkTmpRoomsHome();
|
|
105
|
+
try {
|
|
106
|
+
_seedRegistry(roomsHome, {});
|
|
107
|
+
const r = validateRoomName('untitled/with-slash', { roomsHome });
|
|
108
|
+
assert.strictEqual(r.ok, false);
|
|
109
|
+
assert.ok(r.reasons.indexOf('fs_unsafe_chars') !== -1, 'expected fs_unsafe_chars in reasons');
|
|
110
|
+
assert.ok(r.reasons.indexOf('reserved_prefix:untitled-') !== -1, 'expected reserved_prefix in reasons');
|
|
111
|
+
} finally {
|
|
112
|
+
try { fs.rmSync(roomsHome, { recursive: true, force: true }); } catch (_e) {}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('Test 8: happy path -- valid slug + empty registry', function () {
|
|
117
|
+
const roomsHome = _mkTmpRoomsHome();
|
|
118
|
+
try {
|
|
119
|
+
_seedRegistry(roomsHome, {});
|
|
120
|
+
const r = validateRoomName('acme-robotics', { roomsHome });
|
|
121
|
+
assert.strictEqual(r.ok, true);
|
|
122
|
+
assert.strictEqual(r.normalized_slug, 'acme-robotics');
|
|
123
|
+
assert.deepStrictEqual(r.reasons, []);
|
|
124
|
+
} finally {
|
|
125
|
+
try { fs.rmSync(roomsHome, { recursive: true, force: true }); } catch (_e) {}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('Test 9: normalization -- trim + lowercase', function () {
|
|
130
|
+
const roomsHome = _mkTmpRoomsHome();
|
|
131
|
+
try {
|
|
132
|
+
_seedRegistry(roomsHome, {});
|
|
133
|
+
const r = validateRoomName(' Acme-Robotics ', { roomsHome });
|
|
134
|
+
assert.strictEqual(r.ok, true);
|
|
135
|
+
assert.strictEqual(r.normalized_slug, 'acme-robotics');
|
|
136
|
+
assert.deepStrictEqual(r.reasons, []);
|
|
137
|
+
} finally {
|
|
138
|
+
try { fs.rmSync(roomsHome, { recursive: true, force: true }); } catch (_e) {}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('Test 10: Windows-reserved defense-in-depth (CON / PRN / LPT1)', function () {
|
|
143
|
+
for (const name of ['con', 'CON', 'prn', 'lpt1', 'aux']) {
|
|
144
|
+
const r = validateRoomName(name, {});
|
|
145
|
+
assert.strictEqual(r.ok, false, 'expected ok=false for ' + name);
|
|
146
|
+
assert.ok(r.reasons.indexOf('windows_reserved') !== -1, 'expected windows_reserved for ' + name + '; got ' + JSON.stringify(r.reasons));
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('Test 11: source-grep -- no Brain coupling + no em-dash', function () {
|
|
151
|
+
const src = fs.readFileSync(require.resolve('./room-name-validator.cjs'), 'utf8');
|
|
152
|
+
assert.ok(src.indexOf('brain.mindrian') === -1, 'brain.mindrian substring present (Canon Part 8 breach)');
|
|
153
|
+
assert.ok(!/require\([^)]*brain-client[^)]*\)/.test(src), 'brain-client require (Canon Part 8 breach)');
|
|
154
|
+
const EMDASH = String.fromCharCode(0x2014);
|
|
155
|
+
assert.ok(src.indexOf(EMDASH) === -1, 'em-dash present (HARD RULE)');
|
|
156
|
+
});
|