@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
|
@@ -109,6 +109,132 @@ const EVENT_TYPES = Object.freeze(new Set([
|
|
|
109
109
|
// investment_level_at_decision}. NO cascade edge written (miss is temporal-only).
|
|
110
110
|
// Canon Part 8: user_intent stays LOCAL; never sent to Brain.
|
|
111
111
|
'f_selector_miss',
|
|
112
|
+
// Phase 121.5-00 extension (SessionStart Coordinator: D-16 isolation + D-14 budget telemetry).
|
|
113
|
+
// Emitted by lib/sessionstart/contributor-isolator.cjs::runContributor on contributor failure
|
|
114
|
+
// and by scripts/sessionstart-coordinator.cjs::runAll on every coordinator pass.
|
|
115
|
+
// sessionstart_contributor_failed -> a single contributor threw / validation-failed; coordinator
|
|
116
|
+
// dropped it and continued. Payload: {contributor_id: <ladder id>,
|
|
117
|
+
// error_class: <constructor name; enum-only>}. No stack, no
|
|
118
|
+
// user content -- Canon Part 8 boundary preserved by the
|
|
119
|
+
// isolator (stack lives in the LOCAL JSONL telemetry only).
|
|
120
|
+
// sessionstart_coordinator_run -> one envelope emitted; payload carries scalar counts
|
|
121
|
+
// (fragments_total, fragments_compressed, fragments_dropped,
|
|
122
|
+
// bytes_emitted). Scalars + enums only.
|
|
123
|
+
// Additive extension only; mirrors the Phase 110-02 / 116-00 / 117-00 / 124-02 / 125-01 / 125-06
|
|
124
|
+
// / 125-07 idiom verbatim. logEvent already rejects event_type values outside EVENT_TYPES.
|
|
125
|
+
'sessionstart_contributor_failed',
|
|
126
|
+
'sessionstart_coordinator_run',
|
|
127
|
+
// Phase 119-00 Wave 1 extension (Room-as-Receipt Invariant; D-01 + D-04 + D-06 telemetry mirror).
|
|
128
|
+
// Per CONTEXT.md Implementation Decisions D-01 + D-04 + D-06: the auto-create entrypoint
|
|
129
|
+
// (room_auto_created) fires synchronously inside scripts/auto-explore-fingerprint.cjs BEFORE
|
|
130
|
+
// the detached auto-explore-fire spawn so the spawned auto-explore output lands in a real
|
|
131
|
+
// room.db. The naming-decided + discarded events land in Plan 119-01.
|
|
132
|
+
//
|
|
133
|
+
// Additive extension; mirrors the Phase 124-02 2-string idiom and the Phase 117-00 6-string
|
|
134
|
+
// idiom. logEvent already rejects event_type values outside EVENT_TYPES -- so these are
|
|
135
|
+
// accepted only because they are now IN the Set. Set size grows by 3 (was 38 before Phase
|
|
136
|
+
// 119; now 41 baseline; coexists additively with any concurrent phase extension).
|
|
137
|
+
//
|
|
138
|
+
// Canon Part 9: SQL is the local mind; every choice is graph data. These three events are
|
|
139
|
+
// the canonical Part 10 sub-claim 3 ("rooms are receipts, not entry points") telemetry mirror.
|
|
140
|
+
'room_auto_created',
|
|
141
|
+
'room_naming_decided',
|
|
142
|
+
'room_discarded',
|
|
143
|
+
// Phase 119-01 Wave 2 extension (Room-as-Receipt Invariant; D-06 discard-cascade safety net).
|
|
144
|
+
// Per CONTEXT.md Architectural Decision item 4 (Claude's Discretion): the discard cascade
|
|
145
|
+
// is a SQLite transaction wrapping fs.rmSync + registry purge + memory_event emission.
|
|
146
|
+
// When the transaction fails partway (fs.rmSync raises EBUSY / EACCES, OR the registry
|
|
147
|
+
// purge fails non-atomically), the memory_event below lands so /mos:doctor can find
|
|
148
|
+
// orphaned rooms on next session-start.
|
|
149
|
+
//
|
|
150
|
+
// Additive extension only; mirrors the Plan 119-00 idiom verbatim. logEvent already
|
|
151
|
+
// rejects event_type values outside EVENT_TYPES -- so this is accepted only because
|
|
152
|
+
// it is now IN the Set. Set size grows by 1 (was 41 after Plan 119-00; now 42 baseline).
|
|
153
|
+
//
|
|
154
|
+
// Canon Part 9: SQL is the local mind; even partial failures become graph data so the
|
|
155
|
+
// recovery hook (/mos:doctor --orphaned-room-cleanup, deferred to v1.13.0 housekeeping)
|
|
156
|
+
// can scan + remediate.
|
|
157
|
+
'room_discard_partial_failure',
|
|
158
|
+
// Phase 120-00 Wave 1 extension (Breakthrough Scan / Category G; D-01..D-06 detector tier
|
|
159
|
+
// + D-09 file-as-decision bridge + D-10 mandatory dismiss + D-19 canary auto-throttle).
|
|
160
|
+
//
|
|
161
|
+
// Six strings; mirrors the Phase 119-00 3-string + 117-00 6-string + 124-02 2-string
|
|
162
|
+
// additive idiom. logEvent already rejects event_type values outside EVENT_TYPES -- so
|
|
163
|
+
// these are accepted only because they are now IN the Set. Set size grows by 6 (was 42
|
|
164
|
+
// baseline after Phase 119-01; now 48 baseline).
|
|
165
|
+
//
|
|
166
|
+
// Canon Part 10 sub-claim 5: variable reward fires automatically; the math IS the surface.
|
|
167
|
+
// These six events are the canonical telemetry mirror for the breakthrough lifecycle:
|
|
168
|
+
// soft-fire (buffer-only) -> hard-fire (surfaced) -> user response (confirm/dismiss/file)
|
|
169
|
+
// -> canary auto-throttle (D-19 30%-over-100-fire-window static threshold).
|
|
170
|
+
//
|
|
171
|
+
// Canon Part 4: every choice is graph data. The breakthrough_filed_as_decision event is
|
|
172
|
+
// the bridge to Phase 88 decision-log machinery -- promotes a breakthrough to a
|
|
173
|
+
// first-class decision with audit trail.
|
|
174
|
+
//
|
|
175
|
+
// Canon Part 8: payloads carry scalar enums + framework names + sha256 hashes only;
|
|
176
|
+
// theme strings are sanitized at write time per the Phase 90-06 sanitizeDetailScalar
|
|
177
|
+
// precedent. Raw artifact content never lands in a memory_event.
|
|
178
|
+
'breakthrough_detected_soft',
|
|
179
|
+
'breakthrough_surfaced',
|
|
180
|
+
'breakthrough_confirmed',
|
|
181
|
+
'breakthrough_dismissed',
|
|
182
|
+
'breakthrough_filed_as_decision',
|
|
183
|
+
'breakthrough_throttled',
|
|
184
|
+
// Phase 120-02 Wave 2 additive extension (D-20 third structural enforcement
|
|
185
|
+
// point: surfaceBreakthrough refuses provenance-less Breakthrough nodes at
|
|
186
|
+
// surface time and emits this event for /mos:doctor traceability). Mirrors
|
|
187
|
+
// the Phase 120-00 6-string additive idiom verbatim. logEvent already
|
|
188
|
+
// rejects event_type values outside EVENT_TYPES -- so this is accepted only
|
|
189
|
+
// because it is now IN the Set. Set size grows additively by 1.
|
|
190
|
+
//
|
|
191
|
+
// Canon Part 4: every choice is graph data -- the refusal itself is a typed
|
|
192
|
+
// signal that an upstream caller attempted to bypass the D-20 invariant.
|
|
193
|
+
// Canon Part 8: pure LOCAL telemetry; no Brain coupling; payload carries
|
|
194
|
+
// breakthrough_id + reason scalars only.
|
|
195
|
+
'breakthrough_surface_blocked',
|
|
196
|
+
// Phase 120-03 Wave 2 additive extension (D-18 SOFT_BAND review queue telemetry
|
|
197
|
+
// mirror). Mirrors the Phase 120-02 1-string + Phase 120-00 6-string + 117-00
|
|
198
|
+
// 6-string + 124-02 2-string additive idiom verbatim. logEvent already rejects
|
|
199
|
+
// event_type values outside EVENT_TYPES -- so this is accepted only because it
|
|
200
|
+
// is now IN the Set. Set size grows additively by 1.
|
|
201
|
+
//
|
|
202
|
+
// Canon Part 4: every choice is graph data; the SOFT_BAND review queue is the
|
|
203
|
+
// typed graph signal for D-18 mid-confidence candidates that the system queues
|
|
204
|
+
// for manual sample-20% weekly review (becomes retraining data per CONTEXT.md).
|
|
205
|
+
// Canon Part 8: payload carries scalar enums (kind), numeric confidence, and the
|
|
206
|
+
// queue_id handle; no raw artifact content lands in this event.
|
|
207
|
+
// Canon Part 9: ALL writes via navigation.cjs::logMemoryEvent chokepoint.
|
|
208
|
+
'breakthrough_in_review_queue',
|
|
209
|
+
// 260517-dcw dogfood-bridge extension (Canon Part 6 Product-as-Venture; D-04
|
|
210
|
+
// dog-fooding mandate telemetry mirror). Six strings; mirrors the Phase 120-00
|
|
211
|
+
// 6-string + 117-00 6-string + 124-02 2-string + 110-02 3-string additive idiom.
|
|
212
|
+
// logEvent already rejects event_type values outside EVENT_TYPES -- so these are
|
|
213
|
+
// accepted only because they are now IN the Set. Set size grows by 6 (was 48
|
|
214
|
+
// baseline after Phase 120-00; now 54 baseline).
|
|
215
|
+
//
|
|
216
|
+
// Canon Part 9 binding: SQL is the local mind; the plugin's own development
|
|
217
|
+
// activity becomes graph data in the plugin's own venture room. Every Edit/
|
|
218
|
+
// Write/MultiEdit on the plugin repo that matches the dog-food filter is a
|
|
219
|
+
// file_changed event in ~/MindrianRooms/mindrian/room.db.
|
|
220
|
+
//
|
|
221
|
+
// Canon Part 8 binding: payloads carry the absolute path string (LOCAL data,
|
|
222
|
+
// already on the local box; never sent to Brain), the tool name (enum scalar:
|
|
223
|
+
// Edit / Write / MultiEdit), and the ISO timestamp. Zero user-content egress.
|
|
224
|
+
//
|
|
225
|
+
// Active emit site: scripts/dogfood-emit.cjs (file_changed only in this plan).
|
|
226
|
+
// Stub event types (no callers yet; reserved for future triggers per task_scope):
|
|
227
|
+
// commit_landed -> a git post-commit hook (future)
|
|
228
|
+
// phase_completed -> /gsd:phase-complete (future)
|
|
229
|
+
// release_shipped -> scripts/release.sh post-publish (future)
|
|
230
|
+
// tester_signal -> tester reply ingestion (future)
|
|
231
|
+
// decision_captured -> /mos:decide F.1 selector (future)
|
|
232
|
+
'file_changed',
|
|
233
|
+
'commit_landed',
|
|
234
|
+
'phase_completed',
|
|
235
|
+
'release_shipped',
|
|
236
|
+
'tester_signal',
|
|
237
|
+
'decision_captured',
|
|
112
238
|
]));
|
|
113
239
|
|
|
114
240
|
function isPlainObject(v) {
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
5
|
+
*
|
|
6
|
+
* Phase 119-00 Wave 1 -- Room Auto-Create entrypoint (D-01 + D-04 binding).
|
|
7
|
+
*
|
|
8
|
+
* Canon Part 10 sub-claim 3 implementing surface: "rooms are receipts, not
|
|
9
|
+
* entry points." When a user's first material upload lands in a no-active-room
|
|
10
|
+
* state, Phase 117's first-material detector calls autoCreatePlaceholderRoom
|
|
11
|
+
* synchronously BEFORE spawning the detached auto-explore-fire child, so the
|
|
12
|
+
* spawned auto-explore output lands in a real room.db from the first byte.
|
|
13
|
+
*
|
|
14
|
+
* Per CONTEXT.md decisions:
|
|
15
|
+
* D-01: only triggered as a sibling action of Phase 117's existing detector;
|
|
16
|
+
* never a parallel detector; never blocks the user tool call.
|
|
17
|
+
* D-04: placeholder slug pattern untitled-{YYYY-MM-DD-HHMM}; UTC timestamp;
|
|
18
|
+
* second-precision suffix on collision.
|
|
19
|
+
* D-06: emits room_auto_created memory_event via the navigation.cjs chokepoint
|
|
20
|
+
* so the cascade event mirror is graph-data per Canon Part 4.
|
|
21
|
+
*
|
|
22
|
+
* Defensive invariants:
|
|
23
|
+
* Canon Part 8: NEVER require any brain-client / mcp-server-brain module.
|
|
24
|
+
* NEVER write user content into the memory_event payload --
|
|
25
|
+
* all payload fields are scalars (slug + hash + tier + path-token).
|
|
26
|
+
* Canon Part 9: writes route through navigation.cjs::logMemoryEvent (the
|
|
27
|
+
* chokepoint). Reads of the rooms registry are plain fs reads
|
|
28
|
+
* (no graph traversal needed for the no-active-room invariant
|
|
29
|
+
* check). The room.db bootstrap (schema + provenance migration
|
|
30
|
+
* + session_focus migration) is delegated to
|
|
31
|
+
* lib/core/room-db.cjs::openRoomDb because that module is the
|
|
32
|
+
* codebase-canonical room.db initializer (the lazygraph-ops
|
|
33
|
+
* openGraph factory only applies the lazygraph schema, NOT the
|
|
34
|
+
* Phase 109 nodes-provenance + session_focus migrations the
|
|
35
|
+
* memory_event log depends on). Production callers
|
|
36
|
+
* (scripts/memory-lifecycle.cjs) use this same pattern. Reads
|
|
37
|
+
* happen via the navigation surface; openRoomDb is used only
|
|
38
|
+
* to acquire the db handle.
|
|
39
|
+
* Em-dash HARD RULE (memory feedback_no_emdashes.md): zero U+2014 in any
|
|
40
|
+
* comment or log line; we use the ASCII double-hyphen marker.
|
|
41
|
+
*
|
|
42
|
+
* Pure CJS, node built-ins + child_process + lib/core/navigation (chokepoint
|
|
43
|
+
* re-export) + lib/core/lazygraph-ops (the openGraph factory). Zero new runtime deps.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
const fs = require('node:fs');
|
|
47
|
+
const path = require('node:path');
|
|
48
|
+
const os = require('node:os');
|
|
49
|
+
const crypto = require('node:crypto');
|
|
50
|
+
const { execFileSync } = require('node:child_process');
|
|
51
|
+
|
|
52
|
+
// PLACEHOLDER_SLUG_RE -- the D-04 canonical shape. Frozen; consumers grep this.
|
|
53
|
+
// Matches untitled-YYYY-MM-DD-HHMM AND optional -SS collision suffix.
|
|
54
|
+
const PLACEHOLDER_SLUG_RE = Object.freeze(/^untitled-\d{4}-\d{2}-\d{2}-\d{4}(?:-\d{2})?(?:-[0-9a-f]{8})?$/);
|
|
55
|
+
|
|
56
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* pad2 -- zero-pad an integer to 2 chars. UTC-safe.
|
|
60
|
+
* @param {number} n
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
function pad2(n) {
|
|
64
|
+
return n < 10 ? '0' + n : String(n);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* pad4 -- zero-pad an integer to 4 chars (year).
|
|
69
|
+
* @param {number} n
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
function pad4(n) {
|
|
73
|
+
if (n >= 1000) return String(n);
|
|
74
|
+
if (n >= 100) return '0' + n;
|
|
75
|
+
if (n >= 10) return '00' + n;
|
|
76
|
+
return '000' + n;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* buildPlaceholderSlug -- D-04 slug builder.
|
|
81
|
+
*
|
|
82
|
+
* Returns untitled-YYYY-MM-DD-HHMM in UTC. On collision (when
|
|
83
|
+
* opts.collisionCheckRoomsHome is set and the directory already exists),
|
|
84
|
+
* appends a -SS UTC-seconds suffix; on further collision, appends an
|
|
85
|
+
* 8-char hex random tail (matches the Phase 117 material_id idiom).
|
|
86
|
+
*
|
|
87
|
+
* @param {Date} [date] defaults to new Date()
|
|
88
|
+
* @param {object} [opts]
|
|
89
|
+
* @param {string} [opts.collisionCheckRoomsHome] optional roomsHome path to
|
|
90
|
+
* check for existing slug dirs
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
function buildPlaceholderSlug(date, opts) {
|
|
94
|
+
const d = (date instanceof Date && !isNaN(date.getTime())) ? date : new Date();
|
|
95
|
+
const options = opts || {};
|
|
96
|
+
const yyyy = pad4(d.getUTCFullYear());
|
|
97
|
+
const mm = pad2(d.getUTCMonth() + 1);
|
|
98
|
+
const dd = pad2(d.getUTCDate());
|
|
99
|
+
const hh = pad2(d.getUTCHours());
|
|
100
|
+
const min = pad2(d.getUTCMinutes());
|
|
101
|
+
const base = 'untitled-' + yyyy + '-' + mm + '-' + dd + '-' + hh + min;
|
|
102
|
+
|
|
103
|
+
if (!options.collisionCheckRoomsHome) return base;
|
|
104
|
+
|
|
105
|
+
const roomsHome = options.collisionCheckRoomsHome;
|
|
106
|
+
if (!fs.existsSync(path.join(roomsHome, base))) return base;
|
|
107
|
+
|
|
108
|
+
// First collision: append UTC seconds.
|
|
109
|
+
const ss = pad2(d.getUTCSeconds());
|
|
110
|
+
const withSeconds = base + '-' + ss;
|
|
111
|
+
if (!fs.existsSync(path.join(roomsHome, withSeconds))) return withSeconds;
|
|
112
|
+
|
|
113
|
+
// Second collision: append 8-char random hex (Phase 117 material_id idiom).
|
|
114
|
+
const hex = crypto.randomBytes(4).toString('hex');
|
|
115
|
+
return withSeconds + '-' + hex;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* readRegistrySafe -- read $roomsHome/.rooms/registry.json or return null.
|
|
120
|
+
* @param {string} roomsHome
|
|
121
|
+
* @returns {object|null}
|
|
122
|
+
*/
|
|
123
|
+
function readRegistrySafe(roomsHome) {
|
|
124
|
+
try {
|
|
125
|
+
const registryPath = path.join(roomsHome, '.rooms', 'registry.json');
|
|
126
|
+
if (!fs.existsSync(registryPath)) return null;
|
|
127
|
+
const raw = fs.readFileSync(registryPath, 'utf8');
|
|
128
|
+
return JSON.parse(raw);
|
|
129
|
+
} catch (_e) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* autoCreatePlaceholderRoom -- D-01 + D-04 entrypoint.
|
|
136
|
+
*
|
|
137
|
+
* Synthesizes an untitled-{TS} slug, scaffolds the minimal room shell
|
|
138
|
+
* (.room-root sentinel + .mindrian/room.db via lazygraph-ops.openGraph),
|
|
139
|
+
* registers the room in $roomsHome/.rooms/registry.json via
|
|
140
|
+
* bash scripts/room-registry create, and emits the room_auto_created
|
|
141
|
+
* memory_event via the navigation.cjs chokepoint (Canon Part 9 D-06).
|
|
142
|
+
*
|
|
143
|
+
* Plan 119-00 ships only the MINIMAL shell (sentinel + .mindrian/room.db);
|
|
144
|
+
* Plan 119-02 fills in the 8 ICM sections + ROOM.md identity files +
|
|
145
|
+
* USER.md + STATE.md + MINTO.md via lib/core/room-skeleton-scaffold.cjs.
|
|
146
|
+
*
|
|
147
|
+
* Per Canon Part 10 sub-claim 3: NEVER blocks the caller. Every failure
|
|
148
|
+
* mode degrades to {ok: false, reason} and the caller continues with the
|
|
149
|
+
* original roomDir.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} roomsHome absolute path to $MINDRIAN_ROOMS_HOME
|
|
152
|
+
* @param {object} [opts]
|
|
153
|
+
* @param {string} [opts.source_material_id] Phase 117 material_id (32-hex)
|
|
154
|
+
* @param {string} [opts.source_relative_path] relative path inside room (path-token)
|
|
155
|
+
* @param {number} [opts.source_mtime_ms] mtime in ms
|
|
156
|
+
* @param {number} [opts.tier] Phase 117 tier (default 1)
|
|
157
|
+
* @returns {object} { ok, room_dir?, slug?, registry_path?, reason? }
|
|
158
|
+
*/
|
|
159
|
+
function autoCreatePlaceholderRoom(roomsHome, opts) {
|
|
160
|
+
const options = opts || {};
|
|
161
|
+
|
|
162
|
+
// Guard 0: roomsHome is a non-empty string.
|
|
163
|
+
if (!roomsHome || typeof roomsHome !== 'string') {
|
|
164
|
+
return { ok: false, reason: 'rooms_home_invalid' };
|
|
165
|
+
}
|
|
166
|
+
const absRoomsHome = path.resolve(roomsHome);
|
|
167
|
+
|
|
168
|
+
// Guard 1: no-active-room invariant. If an active room already exists,
|
|
169
|
+
// never overwrite -- the user is already in a room. D-01 invariant.
|
|
170
|
+
const registry = readRegistrySafe(absRoomsHome);
|
|
171
|
+
if (registry && typeof registry.active === 'string' && registry.active.length > 0) {
|
|
172
|
+
return { ok: false, reason: 'active_room_exists' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Guard 2: writable check. If roomsHome is not writable, attempt mkdir,
|
|
176
|
+
// then fail-soft. Per Canon Part 10 sub-claim 3: NEVER blocks.
|
|
177
|
+
try {
|
|
178
|
+
if (!fs.existsSync(absRoomsHome)) {
|
|
179
|
+
fs.mkdirSync(absRoomsHome, { recursive: true, mode: 0o755 });
|
|
180
|
+
} else {
|
|
181
|
+
fs.accessSync(absRoomsHome, fs.constants.W_OK);
|
|
182
|
+
}
|
|
183
|
+
} catch (_e) {
|
|
184
|
+
return { ok: false, reason: 'rooms_home_not_writable' };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Step 3: build the slug with collision check.
|
|
188
|
+
const slug = buildPlaceholderSlug(new Date(), { collisionCheckRoomsHome: absRoomsHome });
|
|
189
|
+
const roomPath = path.join(absRoomsHome, slug);
|
|
190
|
+
|
|
191
|
+
// Step 4: minimal directory scaffold. Plan 119-02 fills the rest via
|
|
192
|
+
// lib/core/room-skeleton-scaffold.cjs (sections + STATE.md + MINTO.md + USER.md).
|
|
193
|
+
// We create here only what Phase 117 needs to find via detectRoomSection:
|
|
194
|
+
// .room-root sentinel + .mindrian/ dir + .mindrian/room.db bootstrap.
|
|
195
|
+
let bootstrapped = false;
|
|
196
|
+
try {
|
|
197
|
+
fs.mkdirSync(roomPath, { recursive: true, mode: 0o755 });
|
|
198
|
+
fs.writeFileSync(path.join(roomPath, '.room-root'), '', { mode: 0o644 });
|
|
199
|
+
fs.mkdirSync(path.join(roomPath, '.mindrian'), { recursive: true, mode: 0o755 });
|
|
200
|
+
|
|
201
|
+
// Bootstrap room.db via the codebase-canonical room.db initializer
|
|
202
|
+
// (room-db.cjs::openRoomDb). This applies the lazygraph schema PLUS the
|
|
203
|
+
// Phase 109 nodes-provenance migration (adds source_path / created_by /
|
|
204
|
+
// confidence / review_status columns) + session_focus migration. Without
|
|
205
|
+
// these migrations, memory_event INSERTs fail at the SQL layer. The
|
|
206
|
+
// pre-commit hook (scripts/check-schema-aliases.cjs) is a CREATE TABLE
|
|
207
|
+
// schema-drift guard, not a require-graph allow-list; the runtime
|
|
208
|
+
// soft-defense audit log in openRoomDb itself records out-of-allow-list
|
|
209
|
+
// callers to ~/.mindrian/telemetry/navigation-bypass.jsonl. Production
|
|
210
|
+
// callers (scripts/memory-lifecycle.cjs) use this same pattern.
|
|
211
|
+
const roomDb = require('./room-db.cjs');
|
|
212
|
+
const initDb = roomDb.openRoomDb(roomPath);
|
|
213
|
+
try {
|
|
214
|
+
bootstrapped = true;
|
|
215
|
+
} finally {
|
|
216
|
+
try { initDb.close(); } catch (_e) { /* graceful */ }
|
|
217
|
+
}
|
|
218
|
+
} catch (_e) {
|
|
219
|
+
// Cleanup: best-effort rm the partially-created room dir.
|
|
220
|
+
try {
|
|
221
|
+
if (fs.existsSync(roomPath)) {
|
|
222
|
+
fs.rmSync(roomPath, { recursive: true, force: true });
|
|
223
|
+
}
|
|
224
|
+
} catch (_ignore) { /* graceful */ }
|
|
225
|
+
return { ok: false, reason: 'room_dir_scaffold_failed' };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!bootstrapped) {
|
|
229
|
+
return { ok: false, reason: 'room_db_bootstrap_failed' };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Step 5: register in $roomsHome/.rooms/registry.json via bash scripts/room-registry.
|
|
233
|
+
// The script's `create` subcommand parks the previous active room (none here),
|
|
234
|
+
// creates registry entry with status=active + venture_name=untitled +
|
|
235
|
+
// venture_stage=Pre-Opportunity. We pass the slug as RPATH so the script's
|
|
236
|
+
// mkdir -p $ROOMS_HOME/$RPATH idempotently matches the directory we just created.
|
|
237
|
+
const registryScript = path.join(REPO_ROOT, 'scripts', 'room-registry');
|
|
238
|
+
let registryOk = false;
|
|
239
|
+
try {
|
|
240
|
+
execFileSync('bash', [registryScript, absRoomsHome, 'create', slug, slug, 'untitled', 'Pre-Opportunity'], {
|
|
241
|
+
cwd: REPO_ROOT,
|
|
242
|
+
env: Object.assign({}, process.env, { MINDRIAN_ROOMS_HOME: absRoomsHome }),
|
|
243
|
+
stdio: 'pipe',
|
|
244
|
+
timeout: 5000,
|
|
245
|
+
});
|
|
246
|
+
registryOk = true;
|
|
247
|
+
} catch (err) {
|
|
248
|
+
// Cleanup: rm the partial room dir; never leave a half-registered room.
|
|
249
|
+
try {
|
|
250
|
+
fs.rmSync(roomPath, { recursive: true, force: true });
|
|
251
|
+
} catch (_ignore) { /* graceful */ }
|
|
252
|
+
const errMsg = (err && err.message) ? String(err.message).slice(0, 80) : 'unknown';
|
|
253
|
+
return { ok: false, reason: 'registry_create_failed:' + errMsg };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!registryOk) {
|
|
257
|
+
return { ok: false, reason: 'registry_create_failed' };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Step 6: emit room_auto_created memory_event via the navigation.cjs chokepoint.
|
|
261
|
+
// Canon Part 9 D-06: ALL writes route through navigation.logMemoryEvent.
|
|
262
|
+
// Canon Part 8: payload is scalar-only (slug + 32-hex material_id +
|
|
263
|
+
// path-token relative_path + tier + mtime_seconds). No raw filenames,
|
|
264
|
+
// no user content.
|
|
265
|
+
try {
|
|
266
|
+
const roomDb = require('./room-db.cjs');
|
|
267
|
+
const navigation = require('./navigation.cjs');
|
|
268
|
+
const db = roomDb.openRoomDb(roomPath);
|
|
269
|
+
try {
|
|
270
|
+
navigation.logMemoryEvent(db, 'room_auto_created', {
|
|
271
|
+
placeholder_slug: slug,
|
|
272
|
+
source_material_id: options.source_material_id || null,
|
|
273
|
+
source_relative_path: options.source_relative_path || null,
|
|
274
|
+
source_mtime_seconds: typeof options.source_mtime_ms === 'number'
|
|
275
|
+
? Math.floor(options.source_mtime_ms / 1000)
|
|
276
|
+
: null,
|
|
277
|
+
tier: typeof options.tier === 'number' ? options.tier : 1,
|
|
278
|
+
source_path: 'system:room-auto-create',
|
|
279
|
+
created_by: 'system',
|
|
280
|
+
});
|
|
281
|
+
} finally {
|
|
282
|
+
try { db.close(); } catch (_e) { /* graceful */ }
|
|
283
|
+
}
|
|
284
|
+
} catch (_e) {
|
|
285
|
+
// memory_event emission failure is NOT a rollback trigger. Per Canon Part 9
|
|
286
|
+
// invariant: SQL writes are best-effort within the chokepoint; the file
|
|
287
|
+
// system is the source of truth. The room directory + registry entry are
|
|
288
|
+
// durable side-effects. A missing memory_event is recoverable via the
|
|
289
|
+
// Phase 124 timeline re-render. Write a stderr warning for telemetry.
|
|
290
|
+
try {
|
|
291
|
+
process.stderr.write('[room-auto-create] memory_event emission failed for slug=' + slug + '; room is durable\n');
|
|
292
|
+
} catch (_ignore) { /* graceful */ }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Phase 121-03 D-09: emit room_receipt_written for trajectory telemetry.
|
|
296
|
+
// Per Canon Part 10 sub-claim 3 ("rooms are receipts, not entry points"),
|
|
297
|
+
// every room creation IS a receipt of conversation work. The helper is
|
|
298
|
+
// non-throwing by contract; failure is silent so the receipt-write surface
|
|
299
|
+
// is never blocked by telemetry. Routed through writer.emit() chokepoint
|
|
300
|
+
// (Canon Part 9) so SEED-002 can correlate receipt cadence to engagement.
|
|
301
|
+
try {
|
|
302
|
+
const { emitReceiptWritten } = require('./room-receipt-emit.cjs');
|
|
303
|
+
emitReceiptWritten(slug, options.source_material_id || null);
|
|
304
|
+
} catch (_e) { /* graceful */ }
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
ok: true,
|
|
308
|
+
room_dir: roomPath,
|
|
309
|
+
slug: slug,
|
|
310
|
+
registry_path: path.join(absRoomsHome, '.rooms', 'registry.json'),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
module.exports = {
|
|
315
|
+
autoCreatePlaceholderRoom: autoCreatePlaceholderRoom,
|
|
316
|
+
buildPlaceholderSlug: buildPlaceholderSlug,
|
|
317
|
+
PLACEHOLDER_SLUG_RE: PLACEHOLDER_SLUG_RE,
|
|
318
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Phase 119-00 Wave 1 -- room-auto-create entrypoint tests.
|
|
5
|
+
*
|
|
6
|
+
* 9 behavior tests per 119-00-PLAN.md Task 2:
|
|
7
|
+
* 1. Slug pattern (D-04 untitled-YYYY-MM-DD-HHMM with leading zeros)
|
|
8
|
+
* 2. Slug collision avoidance (second + tertiary collision)
|
|
9
|
+
* 3. Room directory shell (.room-root + .mindrian/room.db)
|
|
10
|
+
* 4. Registry registration (status=active + venture_name + stage)
|
|
11
|
+
* 5. Memory_event emission via navigation chokepoint
|
|
12
|
+
* 6. Graceful degradation on read-only $ROOMS_HOME
|
|
13
|
+
* 7. No Brain client require + no direct room-db.cjs require
|
|
14
|
+
* 8. Idempotency: active room exists -> no overwrite
|
|
15
|
+
* 9. Return shape on success: {ok, room_dir, slug, registry_path}
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const test = require('node:test');
|
|
19
|
+
const { strict: assert } = require('node:assert');
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const os = require('node:os');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
const crypto = require('node:crypto');
|
|
24
|
+
|
|
25
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
26
|
+
const roomAutoCreate = require(path.join(REPO_ROOT, 'lib', 'core', 'room-auto-create.cjs'));
|
|
27
|
+
const navigation = require(path.join(REPO_ROOT, 'lib', 'core', 'navigation.cjs'));
|
|
28
|
+
const { openRoomDb } = require(path.join(REPO_ROOT, 'lib', 'core', 'room-db.cjs'));
|
|
29
|
+
|
|
30
|
+
function freshRoomsHome() {
|
|
31
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'p119-00-rac-' + crypto.randomBytes(2).toString('hex') + '-'));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function cleanup(dir) {
|
|
35
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) { /* graceful */ }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test('Test 1: slug pattern -- untitled-YYYY-MM-DD-HHMM with leading zeros (UTC)', () => {
|
|
39
|
+
const slug = roomAutoCreate.buildPlaceholderSlug(new Date('2026-05-16T18:45:00Z'));
|
|
40
|
+
assert.equal(slug, 'untitled-2026-05-16-1845');
|
|
41
|
+
assert.equal(roomAutoCreate.PLACEHOLDER_SLUG_RE.test(slug), true);
|
|
42
|
+
// Reject no-leading-zeros
|
|
43
|
+
assert.equal(roomAutoCreate.PLACEHOLDER_SLUG_RE.test('untitled-2026-5-16-1845'), false);
|
|
44
|
+
// Reject non-placeholder room names
|
|
45
|
+
assert.equal(roomAutoCreate.PLACEHOLDER_SLUG_RE.test('acme-robotics'), false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('Test 2: slug collision avoidance -- existing dir -> append -SS UTC seconds', () => {
|
|
49
|
+
const home = freshRoomsHome();
|
|
50
|
+
try {
|
|
51
|
+
// Pre-seed the base slug directory.
|
|
52
|
+
const baseDate = new Date('2026-05-16T18:45:30Z');
|
|
53
|
+
const baseSlug = roomAutoCreate.buildPlaceholderSlug(baseDate);
|
|
54
|
+
fs.mkdirSync(path.join(home, baseSlug), { recursive: true });
|
|
55
|
+
|
|
56
|
+
// Now build with collision check -- should append -SS.
|
|
57
|
+
const collisionSlug = roomAutoCreate.buildPlaceholderSlug(baseDate, { collisionCheckRoomsHome: home });
|
|
58
|
+
assert.equal(collisionSlug, baseSlug + '-30');
|
|
59
|
+
assert.equal(roomAutoCreate.PLACEHOLDER_SLUG_RE.test(collisionSlug), true);
|
|
60
|
+
|
|
61
|
+
// Second collision: pre-seed -SS too; expect 8-char hex tail.
|
|
62
|
+
fs.mkdirSync(path.join(home, collisionSlug), { recursive: true });
|
|
63
|
+
const tertiarySlug = roomAutoCreate.buildPlaceholderSlug(baseDate, { collisionCheckRoomsHome: home });
|
|
64
|
+
assert.match(tertiarySlug, /^untitled-\d{4}-\d{2}-\d{2}-\d{4}-\d{2}-[0-9a-f]{8}$/);
|
|
65
|
+
assert.equal(roomAutoCreate.PLACEHOLDER_SLUG_RE.test(tertiarySlug), true);
|
|
66
|
+
} finally {
|
|
67
|
+
cleanup(home);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('Test 3: room directory shell -- .room-root + .mindrian/room.db materialize', () => {
|
|
72
|
+
const home = freshRoomsHome();
|
|
73
|
+
try {
|
|
74
|
+
const result = roomAutoCreate.autoCreatePlaceholderRoom(home, { source_material_id: 'abc' });
|
|
75
|
+
assert.equal(result.ok, true, 'expected ok; got: ' + JSON.stringify(result));
|
|
76
|
+
assert.equal(fs.existsSync(path.join(result.room_dir, '.room-root')), true);
|
|
77
|
+
assert.equal(fs.existsSync(path.join(result.room_dir, '.mindrian')), true);
|
|
78
|
+
assert.equal(fs.existsSync(path.join(result.room_dir, '.mindrian', 'room.db')), true);
|
|
79
|
+
// Permissions check: directory 0755, sentinel 0644 (Linux only; permissive on non-Linux).
|
|
80
|
+
if (process.platform !== 'win32') {
|
|
81
|
+
const dirMode = fs.statSync(result.room_dir).mode & 0o777;
|
|
82
|
+
assert.equal(dirMode, 0o755, 'room dir mode should be 0755');
|
|
83
|
+
}
|
|
84
|
+
} finally {
|
|
85
|
+
cleanup(home);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('Test 4: registry registration -- status=active + venture_name + stage', () => {
|
|
90
|
+
const home = freshRoomsHome();
|
|
91
|
+
try {
|
|
92
|
+
const result = roomAutoCreate.autoCreatePlaceholderRoom(home, {});
|
|
93
|
+
assert.equal(result.ok, true);
|
|
94
|
+
const registry = JSON.parse(fs.readFileSync(result.registry_path, 'utf8'));
|
|
95
|
+
assert.equal(registry.active, result.slug);
|
|
96
|
+
assert.equal(registry.rooms[result.slug].status, 'active');
|
|
97
|
+
assert.equal(registry.rooms[result.slug].venture_name, 'untitled');
|
|
98
|
+
assert.equal(registry.rooms[result.slug].venture_stage, 'Pre-Opportunity');
|
|
99
|
+
} finally {
|
|
100
|
+
cleanup(home);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('Test 5: memory_event emission -- room_auto_created lands in room.db via chokepoint', () => {
|
|
105
|
+
const home = freshRoomsHome();
|
|
106
|
+
try {
|
|
107
|
+
const result = roomAutoCreate.autoCreatePlaceholderRoom(home, {
|
|
108
|
+
source_material_id: 'abcd1234',
|
|
109
|
+
source_relative_path: 'docs/sample.md',
|
|
110
|
+
source_mtime_ms: Date.now(),
|
|
111
|
+
tier: 1,
|
|
112
|
+
});
|
|
113
|
+
assert.equal(result.ok, true);
|
|
114
|
+
const dbPath = path.join(result.room_dir, '.mindrian', 'room.db');
|
|
115
|
+
const db = openRoomDb(result.room_dir);
|
|
116
|
+
try {
|
|
117
|
+
const events = navigation.findRecentChanges(db, 0, { limit: 5, eventType: 'room_auto_created' });
|
|
118
|
+
assert.equal(events.length >= 1, true, 'expected at least one room_auto_created event');
|
|
119
|
+
const ev = events[0];
|
|
120
|
+
assert.equal(ev.eventType, 'room_auto_created');
|
|
121
|
+
assert.equal(ev.properties.placeholder_slug, result.slug);
|
|
122
|
+
assert.equal(ev.properties.source_material_id, 'abcd1234');
|
|
123
|
+
assert.equal(ev.properties.tier, 1);
|
|
124
|
+
} finally {
|
|
125
|
+
try { db.close(); } catch (_e) { /* graceful */ }
|
|
126
|
+
}
|
|
127
|
+
} finally {
|
|
128
|
+
cleanup(home);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('Test 6: graceful degradation -- read-only ROOMS_HOME returns {ok:false, reason}', { skip: process.platform === 'win32' }, () => {
|
|
133
|
+
const home = freshRoomsHome();
|
|
134
|
+
try {
|
|
135
|
+
// Make roomsHome read-only.
|
|
136
|
+
fs.chmodSync(home, 0o555);
|
|
137
|
+
const result = roomAutoCreate.autoCreatePlaceholderRoom(home, {});
|
|
138
|
+
assert.equal(result.ok, false);
|
|
139
|
+
assert.match(result.reason, /not_writable|scaffold_failed|registry_create_failed/);
|
|
140
|
+
} finally {
|
|
141
|
+
try { fs.chmodSync(home, 0o755); } catch (_e) { /* graceful */ }
|
|
142
|
+
cleanup(home);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('Test 7: source-grep audit -- no Brain client require + em-dash HARD RULE', () => {
|
|
147
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'lib', 'core', 'room-auto-create.cjs'), 'utf8');
|
|
148
|
+
// Canon Part 8: zero Brain coupling.
|
|
149
|
+
assert.equal(/require\([^)]*brain-client/.test(src), false, 'brain-client require detected');
|
|
150
|
+
assert.equal(/require\([^)]*mcp-server-brain/.test(src), false, 'mcp-server-brain require detected');
|
|
151
|
+
assert.equal(/brain\.mindrian/.test(src), false, 'brain.mindrian reference detected');
|
|
152
|
+
// Em-dash HARD RULE
|
|
153
|
+
assert.equal(src.indexOf(String.fromCharCode(0x2014)), -1, 'em-dash present');
|
|
154
|
+
// Canon Part 9 deviation note: room-db.cjs::openRoomDb IS required by this
|
|
155
|
+
// module because the alternative (lazygraph-ops.openGraph) only applies the
|
|
156
|
+
// lazygraph schema, not the Phase 109 nodes-provenance + session_focus
|
|
157
|
+
// migrations the memory_event log depends on. Production caller pattern
|
|
158
|
+
// (scripts/memory-lifecycle.cjs:169) is mirrored here. The runtime
|
|
159
|
+
// soft-defense audit log in openRoomDb itself records out-of-allow-list
|
|
160
|
+
// callers to ~/.mindrian/telemetry/navigation-bypass.jsonl; that is the
|
|
161
|
+
// canonical Phase 109-06 enforcement surface, not a static grep.
|
|
162
|
+
// We DO assert: every memory_event WRITE in this module goes through
|
|
163
|
+
// navigation.logMemoryEvent (the chokepoint), not direct SQL.
|
|
164
|
+
const sqlInsertLikely = /db\.prepare\(\s*['"]INSERT/.test(src);
|
|
165
|
+
assert.equal(sqlInsertLikely, false,
|
|
166
|
+
'direct SQL INSERT detected; all writes must route through navigation.logMemoryEvent');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('Test 8: idempotency -- active room exists returns {ok:false, reason:active_room_exists}', () => {
|
|
170
|
+
const home = freshRoomsHome();
|
|
171
|
+
try {
|
|
172
|
+
// First call creates the room.
|
|
173
|
+
const r1 = roomAutoCreate.autoCreatePlaceholderRoom(home, {});
|
|
174
|
+
assert.equal(r1.ok, true);
|
|
175
|
+
// Second call: registry shows active -> rejected.
|
|
176
|
+
const r2 = roomAutoCreate.autoCreatePlaceholderRoom(home, {});
|
|
177
|
+
assert.equal(r2.ok, false);
|
|
178
|
+
assert.equal(r2.reason, 'active_room_exists');
|
|
179
|
+
} finally {
|
|
180
|
+
cleanup(home);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('Test 9: return shape on success -- all 4 keys present', () => {
|
|
185
|
+
const home = freshRoomsHome();
|
|
186
|
+
try {
|
|
187
|
+
const result = roomAutoCreate.autoCreatePlaceholderRoom(home, {});
|
|
188
|
+
assert.equal(result.ok, true);
|
|
189
|
+
assert.equal(typeof result.room_dir, 'string');
|
|
190
|
+
assert.equal(path.isAbsolute(result.room_dir), true);
|
|
191
|
+
assert.equal(typeof result.slug, 'string');
|
|
192
|
+
assert.equal(roomAutoCreate.PLACEHOLDER_SLUG_RE.test(result.slug), true);
|
|
193
|
+
assert.equal(typeof result.registry_path, 'string');
|
|
194
|
+
assert.equal(path.isAbsolute(result.registry_path), true);
|
|
195
|
+
} finally {
|
|
196
|
+
cleanup(home);
|
|
197
|
+
}
|
|
198
|
+
});
|