@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,63 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 121-03 Task 2 -- emit helper for room_receipt_written events (D-09 surface 2).
|
|
5
|
+
*
|
|
6
|
+
* Called from the Phase 119 receipt-write surface (lib/core/room-auto-create.cjs
|
|
7
|
+
* after autoCreatePlaceholderRoom finishes scaffolding the room directory + registry
|
|
8
|
+
* entry + room_auto_created memory_event). One line per receipt-write, routed
|
|
9
|
+
* through writer.emit() (Canon Part 9 chokepoint), validated by Canon Part 8.
|
|
10
|
+
*
|
|
11
|
+
* Per Canon Part 10 sub-claim 3 ("rooms are receipts, not entry points"): every
|
|
12
|
+
* room creation IS a receipt of conversation work. This module emits the
|
|
13
|
+
* trajectory-telemetry counterpart of the room_auto_created memory_event, so
|
|
14
|
+
* SEED-002 consumers can correlate receipt cadence with engagement quality.
|
|
15
|
+
*
|
|
16
|
+
* Non-throwing by contract: validation breaches and disk errors are swallowed
|
|
17
|
+
* so the Phase 119 receipt-write surface is never blocked by telemetry. The
|
|
18
|
+
* pipeline must not crash because telemetry storage is unavailable (mirrors
|
|
19
|
+
* the writer.cjs best-effort silent-swallow policy).
|
|
20
|
+
*
|
|
21
|
+
* Hash discipline:
|
|
22
|
+
* - room_slug_sha256: sha256(slug) -- 64-char hex; if slug falsy, sha256('').
|
|
23
|
+
* - conversation_id_hash: sha256(conversationId) first 16 chars -- shorter hash
|
|
24
|
+
* (matches schema.cjs MAX_CONTEXT_HASH_LEN convention).
|
|
25
|
+
* - generated_at_ts: ISO-8601 timestamp string (Date.toISOString()).
|
|
26
|
+
*
|
|
27
|
+
* Pure CJS, node built-ins only. Zero new runtime deps.
|
|
28
|
+
*/
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
const crypto = require('node:crypto');
|
|
32
|
+
const telemetry = require('./telemetry/writer.cjs');
|
|
33
|
+
|
|
34
|
+
function _sha256(s) {
|
|
35
|
+
return crypto.createHash('sha256').update(String(s || '')).digest('hex');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* emitReceiptWritten(roomSlug, conversationId) -> void
|
|
40
|
+
*
|
|
41
|
+
* Best-effort emit of one room_receipt_written event. Falsy arguments are
|
|
42
|
+
* defensively normalized to empty-string hashes so the Phase 119 wire-in
|
|
43
|
+
* never blocks on missing scaffolding metadata.
|
|
44
|
+
*
|
|
45
|
+
* @param {string|null|undefined} roomSlug placeholder slug (e.g. 'untitled-2026-05-19-1234')
|
|
46
|
+
* @param {string|null|undefined} conversationId Phase 119 source_material_id or session id
|
|
47
|
+
*/
|
|
48
|
+
function emitReceiptWritten(roomSlug, conversationId) {
|
|
49
|
+
try {
|
|
50
|
+
telemetry.emit('room_receipt_written', {
|
|
51
|
+
room_slug_sha256: _sha256(roomSlug),
|
|
52
|
+
conversation_id_hash: _sha256(conversationId).slice(0, 16),
|
|
53
|
+
generated_at_ts: new Date().toISOString(),
|
|
54
|
+
});
|
|
55
|
+
} catch (_e) {
|
|
56
|
+
// Best-effort. Per Canon Part 9 (memory locality) and the writer's
|
|
57
|
+
// silent-swallow policy: telemetry MUST NEVER block the surface it
|
|
58
|
+
// observes. A missing telemetry row is recoverable; a crashed room
|
|
59
|
+
// creation is not.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { emitReceiptWritten: emitReceiptWritten };
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Phase 119-02 -- Room skeleton scaffold generator.
|
|
4
|
+
*
|
|
5
|
+
* Fills a placeholder room (created by Plan 119-00's autoCreatePlaceholderRoom)
|
|
6
|
+
* with the canonical 8-section ICM structure + STATE.md + MINTO.md + USER.md +
|
|
7
|
+
* per-directory ROOM.md identity files (Canon decision 15: ICM Layer 0 everywhere).
|
|
8
|
+
*
|
|
9
|
+
* Idempotent + reentrant: never overwrites human-authored content. When STATE.md
|
|
10
|
+
* already has auto_created:false in its frontmatter (OR the frontmatter lacks the
|
|
11
|
+
* auto_created key entirely -- treated as human-authored), the regenerable
|
|
12
|
+
* STATE.md is skipped while the missing identity files are still created
|
|
13
|
+
* (Canon Part 9 "files preserve meaning" invariant).
|
|
14
|
+
*
|
|
15
|
+
* Atomic writes per Phase 124-02 precedent (tmp + rename).
|
|
16
|
+
*
|
|
17
|
+
* Canon Part 8 invariant: pure-local; no Brain MCP, no fetch, no telemetry.
|
|
18
|
+
* Canon Part 9 invariant: no direct room-db.cjs require (this module touches
|
|
19
|
+
* only files; memory_event emission stays in Plan 119-00 and Plan 119-01).
|
|
20
|
+
* Canon Part 10 sub-claim 3: rooms are receipts -- the receipt's substrate
|
|
21
|
+
* materializes the instant the auto-explore finding lands, never gated on
|
|
22
|
+
* material quality (D-05).
|
|
23
|
+
* Canon decision 15: every directory in the Data Room MUST have ROOM.md;
|
|
24
|
+
* the IDENTITY_DIRECTORIES table covers the 5 non-ICM cases.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
|
|
30
|
+
const TEMPLATES_DIR = path.resolve(__dirname, '..', '..', 'templates', 'room-skeleton');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The 8 ICM sections. Lookup table extracted from commands/new-project.md
|
|
34
|
+
* Section Definitions. Single source of truth here; renames cascade.
|
|
35
|
+
*/
|
|
36
|
+
const SECTION_NAMES = Object.freeze([
|
|
37
|
+
'problem-definition',
|
|
38
|
+
'market-analysis',
|
|
39
|
+
'solution-design',
|
|
40
|
+
'business-model',
|
|
41
|
+
'competitive-analysis',
|
|
42
|
+
'team-execution',
|
|
43
|
+
'legal-ip',
|
|
44
|
+
'financial-model',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const SECTION_METADATA = Object.freeze({
|
|
48
|
+
'problem-definition': { purpose: 'Define the core problem this venture addresses.', stage_relevance: ['Pre-Opportunity', 'Discovery'], default_methodologies: ['domain-explorer', 'beautiful-question', 'trending-to-absurd'] },
|
|
49
|
+
'market-analysis': { purpose: 'Map market size, trends, and customer segments.', stage_relevance: ['Discovery', 'Validation'], default_methodologies: ['domain-explorer', 'scenario-analysis'] },
|
|
50
|
+
'solution-design': { purpose: 'Design the solution, technology, and architecture.', stage_relevance: ['Validation', 'Design'], default_methodologies: ['structure-argument', 'think-hats'] },
|
|
51
|
+
'business-model': { purpose: 'Define revenue model, unit economics, go-to-market.', stage_relevance: ['Design', 'Investment'], default_methodologies: ['structure-argument', 'scenario-analysis'] },
|
|
52
|
+
'competitive-analysis': { purpose: 'Analyze competition, positioning, differentiation.', stage_relevance: ['Discovery', 'Design'], default_methodologies: ['challenge-assumptions', 'find-bottlenecks'] },
|
|
53
|
+
'team-execution': { purpose: 'Document team, advisors, and execution plan.', stage_relevance: ['Validation', 'Design'], default_methodologies: ['think-hats', 'analyze-needs'] },
|
|
54
|
+
'legal-ip': { purpose: 'Track legal structure, agreements, IP protection.', stage_relevance: ['Design', 'Investment'], default_methodologies: ['structure-argument'] },
|
|
55
|
+
'financial-model': { purpose: 'Build financial projections and metrics.', stage_relevance: ['Design', 'Investment'], default_methodologies: ['scenario-analysis', 'build-thesis'] },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Non-ICM directories that still need ROOM.md per Canon decision 15.
|
|
60
|
+
*/
|
|
61
|
+
const IDENTITY_DIRECTORIES = Object.freeze({
|
|
62
|
+
'team': { directory_type: 'team', purpose: 'The people layer of the Data Room. Members, mentors, advisors -- created on demand from meetings or user input.' },
|
|
63
|
+
'assets': { directory_type: 'assets', purpose: 'Binary file storage (PDFs, images, videos) organized by section. Subdirectories appear when scripts/file-asset is invoked.' },
|
|
64
|
+
'.intelligence':{ directory_type: '.intelligence', purpose: 'Sentinel-generated alerts and digests (health checks, deadline reports, competitor watch).' },
|
|
65
|
+
'.snapshots': { directory_type: '.snapshots', purpose: 'Weekly STATE.md copies for drift detection by sentinel-health-check.' },
|
|
66
|
+
'.context': { directory_type: '.context', purpose: 'Per-session conversational state (last-session, rejection-log, methodology-history, weekly-digest).' },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Render a template with {{KEY}} string substitutions. Pure function.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} templateContent the raw template file contents
|
|
73
|
+
* @param {object} substitutions map of KEY -> value (any type coerced via String())
|
|
74
|
+
* @returns {string} the substituted output
|
|
75
|
+
*/
|
|
76
|
+
function renderTemplate(templateContent, substitutions) {
|
|
77
|
+
let rendered = templateContent;
|
|
78
|
+
for (const key of Object.keys(substitutions || {})) {
|
|
79
|
+
const re = new RegExp('\\{\\{' + key + '\\}\\}', 'g');
|
|
80
|
+
rendered = rendered.replace(re, String(substitutions[key]));
|
|
81
|
+
}
|
|
82
|
+
return rendered;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Atomic write: tmp + rename. Returns true on success, false on failure.
|
|
87
|
+
* Mirrors Phase 124-02 timeline-runner.cjs invariant. The tmp file path
|
|
88
|
+
* includes pid + random suffix so concurrent invocations do not collide.
|
|
89
|
+
*/
|
|
90
|
+
function atomicWrite(filePath, content) {
|
|
91
|
+
try {
|
|
92
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o755 });
|
|
93
|
+
const tmpPath = filePath + '.tmp.' + process.pid + '.' + Math.random().toString(36).slice(2, 10);
|
|
94
|
+
fs.writeFileSync(tmpPath, content, 'utf8');
|
|
95
|
+
fs.renameSync(tmpPath, filePath);
|
|
96
|
+
return true;
|
|
97
|
+
} catch (_e) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function readTemplate(name) {
|
|
103
|
+
try {
|
|
104
|
+
return fs.readFileSync(path.join(TEMPLATES_DIR, name), 'utf8');
|
|
105
|
+
} catch (_e) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Detect whether STATE.md has been authored by a human (or by a non-Phase-119
|
|
112
|
+
* source). Returns true if the file:
|
|
113
|
+
* - Exists AND
|
|
114
|
+
* - Has frontmatter AND
|
|
115
|
+
* - Frontmatter contains `auto_created: false` OR lacks the `auto_created` key.
|
|
116
|
+
*
|
|
117
|
+
* Returns false if STATE.md doesn't exist OR has `auto_created: true` (the
|
|
118
|
+
* Phase 119 auto-scaffold signal).
|
|
119
|
+
*/
|
|
120
|
+
function isStateAuthored(roomDir) {
|
|
121
|
+
try {
|
|
122
|
+
const statePath = path.join(roomDir, 'STATE.md');
|
|
123
|
+
if (!fs.existsSync(statePath)) return false;
|
|
124
|
+
const raw = fs.readFileSync(statePath, 'utf8');
|
|
125
|
+
const m = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
126
|
+
if (!m) return false;
|
|
127
|
+
// If frontmatter has auto_created: false, treat as authored.
|
|
128
|
+
if (/^\s*auto_created:\s*false\s*$/m.test(m[1])) return true;
|
|
129
|
+
// If frontmatter LACKS auto_created entirely, treat as authored (human
|
|
130
|
+
// crafted from scratch -- the absence of the key is the signal).
|
|
131
|
+
if (!/^\s*auto_created:\s*/m.test(m[1])) return true;
|
|
132
|
+
return false;
|
|
133
|
+
} catch (_e) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* scaffoldRoomSkeleton(roomDir, opts) -- the entry point.
|
|
140
|
+
*
|
|
141
|
+
* Fills a placeholder room with the canonical skeleton: STATE.md + MINTO.md +
|
|
142
|
+
* USER.md + 8 ICM section folders (each with ROOM.md) + 5 identity directories
|
|
143
|
+
* (each with ROOM.md per Canon decision 15).
|
|
144
|
+
*
|
|
145
|
+
* Idempotent. Reentrancy invariant: existing files are NEVER overwritten;
|
|
146
|
+
* human-authored STATE.md is byte-preserved.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} roomDir absolute path to the placeholder room
|
|
149
|
+
* @param {object} opts
|
|
150
|
+
* @param {string} [opts.placeholder_slug] e.g. 'untitled-2026-05-16-1845'
|
|
151
|
+
* @param {string} [opts.source_material_id] 32-char hex from Phase 117
|
|
152
|
+
* @param {object} [opts.auto_explore_finding] the Phase 117 JSON output (used
|
|
153
|
+
* for the thinness pass-through)
|
|
154
|
+
* @returns {{ok: boolean,
|
|
155
|
+
* sections_created: string[],
|
|
156
|
+
* identity_files_created: string[],
|
|
157
|
+
* state_written: boolean,
|
|
158
|
+
* minto_written: boolean,
|
|
159
|
+
* user_written: boolean,
|
|
160
|
+
* thinness_acknowledged: boolean,
|
|
161
|
+
* errors: string[]}}
|
|
162
|
+
*/
|
|
163
|
+
function scaffoldRoomSkeleton(roomDir, opts) {
|
|
164
|
+
const options = opts || {};
|
|
165
|
+
const result = {
|
|
166
|
+
ok: true,
|
|
167
|
+
sections_created: [],
|
|
168
|
+
identity_files_created: [],
|
|
169
|
+
state_written: false,
|
|
170
|
+
minto_written: false,
|
|
171
|
+
user_written: false,
|
|
172
|
+
thinness_acknowledged: false,
|
|
173
|
+
errors: [],
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (!roomDir || typeof roomDir !== 'string') {
|
|
177
|
+
result.ok = false;
|
|
178
|
+
result.errors.push('invalid_room_dir');
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
if (!fs.existsSync(roomDir)) {
|
|
182
|
+
result.ok = false;
|
|
183
|
+
result.errors.push('room_dir_does_not_exist');
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Substitutions used across templates.
|
|
188
|
+
const subs = {
|
|
189
|
+
AUTO_CREATED_AT_ISO: new Date().toISOString(),
|
|
190
|
+
PLACEHOLDER_SLUG: options.placeholder_slug || path.basename(roomDir),
|
|
191
|
+
SOURCE_MATERIAL_ID: options.source_material_id || 'unknown',
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const authored = isStateAuthored(roomDir);
|
|
195
|
+
|
|
196
|
+
// Render STATE.md (skip if authored by human).
|
|
197
|
+
if (!authored) {
|
|
198
|
+
const stateTpl = readTemplate('STATE.md.tmpl');
|
|
199
|
+
if (stateTpl) {
|
|
200
|
+
const stateContent = renderTemplate(stateTpl, subs);
|
|
201
|
+
if (atomicWrite(path.join(roomDir, 'STATE.md'), stateContent)) {
|
|
202
|
+
result.state_written = true;
|
|
203
|
+
} else {
|
|
204
|
+
result.errors.push('state_write_failed');
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
result.errors.push('state_template_missing');
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
process.stderr.write('[room-skeleton] STATE.md already authored; skipping\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Render MINTO.md (skip if file exists -- the sentinel-bounded content is the contract).
|
|
214
|
+
const mintoPath = path.join(roomDir, 'MINTO.md');
|
|
215
|
+
if (!fs.existsSync(mintoPath)) {
|
|
216
|
+
const mintoTpl = readTemplate('MINTO.md.tmpl');
|
|
217
|
+
if (mintoTpl) {
|
|
218
|
+
if (atomicWrite(mintoPath, mintoTpl)) {
|
|
219
|
+
result.minto_written = true;
|
|
220
|
+
} else {
|
|
221
|
+
result.errors.push('minto_write_failed');
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
result.errors.push('minto_template_missing');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Render USER.md (skip if exists).
|
|
229
|
+
const userPath = path.join(roomDir, 'USER.md');
|
|
230
|
+
if (!fs.existsSync(userPath)) {
|
|
231
|
+
const userTpl = readTemplate('USER.md.tmpl');
|
|
232
|
+
if (userTpl) {
|
|
233
|
+
if (atomicWrite(userPath, userTpl)) {
|
|
234
|
+
result.user_written = true;
|
|
235
|
+
} else {
|
|
236
|
+
result.errors.push('user_write_failed');
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
result.errors.push('user_template_missing');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 8 ICM sections with per-section ROOM.md.
|
|
244
|
+
const sectionTpl = readTemplate('ROOM.md.section.tmpl');
|
|
245
|
+
if (sectionTpl) {
|
|
246
|
+
for (const section of SECTION_NAMES) {
|
|
247
|
+
const sectionDir = path.join(roomDir, section);
|
|
248
|
+
const sectionRoomMd = path.join(sectionDir, 'ROOM.md');
|
|
249
|
+
if (!fs.existsSync(sectionRoomMd)) {
|
|
250
|
+
const meta = SECTION_METADATA[section];
|
|
251
|
+
const titleCase = section.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
252
|
+
const sectionSubs = {
|
|
253
|
+
SECTION_NAME: section,
|
|
254
|
+
SECTION_NAME_TITLE_CASE: titleCase,
|
|
255
|
+
SECTION_PURPOSE: meta.purpose,
|
|
256
|
+
STAGE_RELEVANCE_LIST: meta.stage_relevance.map(s => ' - ' + s).join('\n'),
|
|
257
|
+
DEFAULT_METHODOLOGIES_LIST: meta.default_methodologies.map(m => ' - ' + m).join('\n'),
|
|
258
|
+
};
|
|
259
|
+
const sectionContent = renderTemplate(sectionTpl, sectionSubs);
|
|
260
|
+
if (atomicWrite(sectionRoomMd, sectionContent)) {
|
|
261
|
+
result.sections_created.push(section);
|
|
262
|
+
} else {
|
|
263
|
+
result.errors.push('section_write_failed:' + section);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
result.errors.push('section_template_missing');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Non-ICM identity directories with ROOM.md (Canon decision 15).
|
|
272
|
+
const identityTpl = readTemplate('ROOM.md.identity.tmpl');
|
|
273
|
+
if (identityTpl) {
|
|
274
|
+
for (const dirName of Object.keys(IDENTITY_DIRECTORIES)) {
|
|
275
|
+
const dirPath = path.join(roomDir, dirName);
|
|
276
|
+
const identityRoomMd = path.join(dirPath, 'ROOM.md');
|
|
277
|
+
if (!fs.existsSync(identityRoomMd)) {
|
|
278
|
+
const meta = IDENTITY_DIRECTORIES[dirName];
|
|
279
|
+
const identitySubs = {
|
|
280
|
+
DIRECTORY_TYPE: meta.directory_type,
|
|
281
|
+
DIRECTORY_PURPOSE: meta.purpose,
|
|
282
|
+
};
|
|
283
|
+
const identityContent = renderTemplate(identityTpl, identitySubs);
|
|
284
|
+
if (atomicWrite(identityRoomMd, identityContent)) {
|
|
285
|
+
result.identity_files_created.push(dirName);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
result.errors.push('identity_template_missing');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Thinness pass-through (Plan 119-01 reads result.thinness_acknowledged
|
|
294
|
+
// to decide whether to render the Larry voice line at F.1 selector time).
|
|
295
|
+
try {
|
|
296
|
+
const tha = require('./larry-thinness-acknowledgment.cjs');
|
|
297
|
+
result.thinness_acknowledged = tha.shouldAcknowledgeThinness(options.auto_explore_finding || null);
|
|
298
|
+
} catch (_e) {
|
|
299
|
+
result.thinness_acknowledged = true; // fail-safe: default to acknowledging thinness
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ok is true if no errors OR if at least the regenerable surface (state or
|
|
303
|
+
// sections) made it through. Errors are still recorded for observability.
|
|
304
|
+
result.ok = result.errors.length === 0;
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = {
|
|
309
|
+
SECTION_NAMES: SECTION_NAMES,
|
|
310
|
+
SECTION_METADATA: SECTION_METADATA,
|
|
311
|
+
IDENTITY_DIRECTORIES: IDENTITY_DIRECTORIES,
|
|
312
|
+
scaffoldRoomSkeleton: scaffoldRoomSkeleton,
|
|
313
|
+
renderTemplate: renderTemplate,
|
|
314
|
+
isStateAuthored: isStateAuthored,
|
|
315
|
+
};
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Phase 119-02 Task 2 -- test suite for room-skeleton-scaffold.cjs.
|
|
4
|
+
*
|
|
5
|
+
* Verifies the 14 behaviors from
|
|
6
|
+
* .planning/phases/119-room-as-receipt-invariant/119-02-PLAN.md Task 2.
|
|
7
|
+
*
|
|
8
|
+
* Each test uses an isolated tmp roomDir via os.tmpdir() + crypto-random
|
|
9
|
+
* suffix; tear-down via fs.rmSync(.., {recursive:true, force:true}).
|
|
10
|
+
*
|
|
11
|
+
* Canon Part 8 invariant: zero Brain MCP coupling (Test 12).
|
|
12
|
+
* Canon Part 9 invariant: zero room-db.cjs require (Test 13 -- this module
|
|
13
|
+
* writes files only; memory_event emission stays in Plan 119-00 + 119-01).
|
|
14
|
+
* Em-dash HARD RULE: zero `--` chars in source (Test 14).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const assert = require('node:assert');
|
|
18
|
+
const test = require('node:test');
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const os = require('node:os');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
const crypto = require('node:crypto');
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
scaffoldRoomSkeleton,
|
|
26
|
+
SECTION_NAMES,
|
|
27
|
+
SECTION_METADATA,
|
|
28
|
+
IDENTITY_DIRECTORIES,
|
|
29
|
+
renderTemplate,
|
|
30
|
+
isStateAuthored,
|
|
31
|
+
} = require('./room-skeleton-scaffold.cjs');
|
|
32
|
+
|
|
33
|
+
const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
|
|
34
|
+
|
|
35
|
+
function makeTmpRoom() {
|
|
36
|
+
const dir = path.join(os.tmpdir(), 'phase119-02-' + crypto.randomBytes(4).toString('hex'));
|
|
37
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
38
|
+
fs.mkdirSync(path.join(dir, '.mindrian'), { recursive: true });
|
|
39
|
+
// Mark as placeholder room (matches Plan 119-00 contract).
|
|
40
|
+
fs.writeFileSync(path.join(dir, '.room-root'), '');
|
|
41
|
+
return dir;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function rmTmp(dir) {
|
|
45
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
test('Test 1: 8 ICM sections present after scaffolding', () => {
|
|
49
|
+
const roomDir = makeTmpRoom();
|
|
50
|
+
try {
|
|
51
|
+
const r = scaffoldRoomSkeleton(roomDir, {});
|
|
52
|
+
assert.strictEqual(r.ok, true, 'scaffold did not return ok');
|
|
53
|
+
const expectedSections = [
|
|
54
|
+
'problem-definition', 'market-analysis', 'solution-design', 'business-model',
|
|
55
|
+
'competitive-analysis', 'team-execution', 'legal-ip', 'financial-model',
|
|
56
|
+
];
|
|
57
|
+
for (const section of expectedSections) {
|
|
58
|
+
const sectionDir = path.join(roomDir, section);
|
|
59
|
+
const roomMd = path.join(sectionDir, 'ROOM.md');
|
|
60
|
+
assert.ok(fs.existsSync(sectionDir), `missing section dir: ${section}`);
|
|
61
|
+
assert.ok(fs.existsSync(roomMd), `missing ROOM.md in ${section}/`);
|
|
62
|
+
}
|
|
63
|
+
} finally { rmTmp(roomDir); }
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('Test 2: non-ICM identity directories present with ROOM.md (Canon decision 15)', () => {
|
|
67
|
+
const roomDir = makeTmpRoom();
|
|
68
|
+
try {
|
|
69
|
+
scaffoldRoomSkeleton(roomDir, {});
|
|
70
|
+
const identityDirs = ['team', 'assets', '.intelligence', '.snapshots', '.context'];
|
|
71
|
+
for (const d of identityDirs) {
|
|
72
|
+
const dirPath = path.join(roomDir, d);
|
|
73
|
+
const roomMd = path.join(dirPath, 'ROOM.md');
|
|
74
|
+
assert.ok(fs.existsSync(dirPath), `missing identity dir: ${d}`);
|
|
75
|
+
assert.ok(fs.existsSync(roomMd), `missing ROOM.md in ${d}/`);
|
|
76
|
+
}
|
|
77
|
+
} finally { rmTmp(roomDir); }
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('Test 3: STATE.md rendered with substitutions', () => {
|
|
81
|
+
const roomDir = makeTmpRoom();
|
|
82
|
+
try {
|
|
83
|
+
scaffoldRoomSkeleton(roomDir, {
|
|
84
|
+
placeholder_slug: 'untitled-test-slug',
|
|
85
|
+
source_material_id: 'deadbeefcafef00d',
|
|
86
|
+
});
|
|
87
|
+
const state = fs.readFileSync(path.join(roomDir, 'STATE.md'), 'utf8');
|
|
88
|
+
assert.match(state, /phase: scoping/, 'phase scoping not rendered');
|
|
89
|
+
assert.match(state, /journey_stage: ordinary-world/, 'journey_stage not rendered');
|
|
90
|
+
assert.match(state, /placeholder_slug: 'untitled-test-slug'/, 'placeholder_slug substitution failed');
|
|
91
|
+
assert.match(state, /source_material_id: 'deadbeefcafef00d'/, 'source_material_id substitution failed');
|
|
92
|
+
} finally { rmTmp(roomDir); }
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('Test 4: MINTO.md is sentinel-bounded', () => {
|
|
96
|
+
const roomDir = makeTmpRoom();
|
|
97
|
+
try {
|
|
98
|
+
scaffoldRoomSkeleton(roomDir, {});
|
|
99
|
+
const minto = fs.readFileSync(path.join(roomDir, 'MINTO.md'), 'utf8');
|
|
100
|
+
assert.ok(minto.includes('<!-- mos:minto-begin -->'), 'missing minto-begin sentinel');
|
|
101
|
+
assert.ok(minto.includes('<!-- mos:minto-end -->'), 'missing minto-end sentinel');
|
|
102
|
+
} finally { rmTmp(roomDir); }
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('Test 5: USER.md present with non-zero size', () => {
|
|
106
|
+
const roomDir = makeTmpRoom();
|
|
107
|
+
try {
|
|
108
|
+
scaffoldRoomSkeleton(roomDir, {});
|
|
109
|
+
const userPath = path.join(roomDir, 'USER.md');
|
|
110
|
+
assert.ok(fs.existsSync(userPath), 'USER.md not created');
|
|
111
|
+
const stat = fs.statSync(userPath);
|
|
112
|
+
assert.ok(stat.size > 0, 'USER.md is empty');
|
|
113
|
+
} finally { rmTmp(roomDir); }
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('Test 6: per-section ROOM.md has YAML frontmatter with required fields', () => {
|
|
117
|
+
const roomDir = makeTmpRoom();
|
|
118
|
+
try {
|
|
119
|
+
scaffoldRoomSkeleton(roomDir, {});
|
|
120
|
+
const content = fs.readFileSync(path.join(roomDir, 'problem-definition', 'ROOM.md'), 'utf8');
|
|
121
|
+
const m = content.match(/^---\n([\s\S]*?)\n---/);
|
|
122
|
+
assert.ok(m, 'no frontmatter delimiter');
|
|
123
|
+
const fm = m[1];
|
|
124
|
+
assert.match(fm, /section: problem-definition/, 'section field missing');
|
|
125
|
+
assert.match(fm, /purpose:/, 'purpose field missing');
|
|
126
|
+
assert.match(fm, /stage_relevance:/, 'stage_relevance missing');
|
|
127
|
+
assert.match(fm, /default_methodologies:/, 'default_methodologies missing');
|
|
128
|
+
assert.match(fm, /icm_layer: 0/, 'icm_layer 0 missing');
|
|
129
|
+
assert.match(fm, /auto_scaffolded: true/, 'auto_scaffolded missing');
|
|
130
|
+
} finally { rmTmp(roomDir); }
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('Test 7: each of 8 sections has section-matching ROOM.md identity', () => {
|
|
134
|
+
const roomDir = makeTmpRoom();
|
|
135
|
+
try {
|
|
136
|
+
scaffoldRoomSkeleton(roomDir, {});
|
|
137
|
+
for (const section of SECTION_NAMES) {
|
|
138
|
+
const content = fs.readFileSync(path.join(roomDir, section, 'ROOM.md'), 'utf8');
|
|
139
|
+
assert.match(content, new RegExp('section: ' + section), `wrong section identity in ${section}/ROOM.md`);
|
|
140
|
+
}
|
|
141
|
+
} finally { rmTmp(roomDir); }
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('Test 8: REENTRANCY -- human-authored STATE.md preserved across re-invocation', () => {
|
|
145
|
+
const roomDir = makeTmpRoom();
|
|
146
|
+
try {
|
|
147
|
+
// Pre-author STATE.md with auto_created: false (human signal).
|
|
148
|
+
const humanState = '---\nphase: validation\nauto_created: false\nventure_name: my-venture\n---\n\n# Human STATE\n';
|
|
149
|
+
fs.writeFileSync(path.join(roomDir, 'STATE.md'), humanState);
|
|
150
|
+
const before = fs.readFileSync(path.join(roomDir, 'STATE.md'), 'utf8');
|
|
151
|
+
|
|
152
|
+
// Capture stderr to verify the skip message.
|
|
153
|
+
const origWrite = process.stderr.write.bind(process.stderr);
|
|
154
|
+
let stderrCapture = '';
|
|
155
|
+
process.stderr.write = (chunk) => { stderrCapture += String(chunk); return true; };
|
|
156
|
+
try {
|
|
157
|
+
const r = scaffoldRoomSkeleton(roomDir, {});
|
|
158
|
+
const after = fs.readFileSync(path.join(roomDir, 'STATE.md'), 'utf8');
|
|
159
|
+
assert.strictEqual(after, before, 'human-authored STATE.md was overwritten');
|
|
160
|
+
assert.strictEqual(r.state_written, false, 'state_written should be false on skip path');
|
|
161
|
+
assert.ok(stderrCapture.includes('STATE.md already authored'), 'expected skip message on stderr');
|
|
162
|
+
// Sections + identity files STILL created (idempotent fill).
|
|
163
|
+
assert.ok(fs.existsSync(path.join(roomDir, 'problem-definition', 'ROOM.md')), 'sections not filled on re-entry');
|
|
164
|
+
} finally {
|
|
165
|
+
process.stderr.write = origWrite;
|
|
166
|
+
}
|
|
167
|
+
} finally { rmTmp(roomDir); }
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('Test 9: atomic-write idiom -- tmp file removed on success (no leftover .tmp files)', () => {
|
|
171
|
+
const roomDir = makeTmpRoom();
|
|
172
|
+
try {
|
|
173
|
+
scaffoldRoomSkeleton(roomDir, {});
|
|
174
|
+
// Walk roomDir; no .tmp.* files should remain.
|
|
175
|
+
const walk = (dir) => {
|
|
176
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
177
|
+
const results = [];
|
|
178
|
+
for (const e of entries) {
|
|
179
|
+
const p = path.join(dir, e.name);
|
|
180
|
+
if (e.isDirectory()) results.push(...walk(p));
|
|
181
|
+
else results.push(p);
|
|
182
|
+
}
|
|
183
|
+
return results;
|
|
184
|
+
};
|
|
185
|
+
const files = walk(roomDir);
|
|
186
|
+
const leftovers = files.filter(f => /\.tmp\./.test(f));
|
|
187
|
+
assert.strictEqual(leftovers.length, 0, `leftover tmp files: ${leftovers.join(',')}`);
|
|
188
|
+
} finally { rmTmp(roomDir); }
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('Test 10: return shape has all expected fields', () => {
|
|
192
|
+
const roomDir = makeTmpRoom();
|
|
193
|
+
try {
|
|
194
|
+
const r = scaffoldRoomSkeleton(roomDir, {});
|
|
195
|
+
assert.ok(Object.prototype.hasOwnProperty.call(r, 'ok'));
|
|
196
|
+
assert.ok(Object.prototype.hasOwnProperty.call(r, 'sections_created'));
|
|
197
|
+
assert.ok(Object.prototype.hasOwnProperty.call(r, 'identity_files_created'));
|
|
198
|
+
assert.ok(Object.prototype.hasOwnProperty.call(r, 'state_written'));
|
|
199
|
+
assert.ok(Object.prototype.hasOwnProperty.call(r, 'minto_written'));
|
|
200
|
+
assert.ok(Object.prototype.hasOwnProperty.call(r, 'user_written'));
|
|
201
|
+
assert.ok(Object.prototype.hasOwnProperty.call(r, 'thinness_acknowledged'));
|
|
202
|
+
assert.ok(Array.isArray(r.sections_created));
|
|
203
|
+
assert.ok(Array.isArray(r.identity_files_created));
|
|
204
|
+
assert.strictEqual(typeof r.state_written, 'boolean');
|
|
205
|
+
assert.strictEqual(typeof r.minto_written, 'boolean');
|
|
206
|
+
assert.strictEqual(typeof r.user_written, 'boolean');
|
|
207
|
+
assert.strictEqual(typeof r.thinness_acknowledged, 'boolean');
|
|
208
|
+
} finally { rmTmp(roomDir); }
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('Test 11: thinness pass-through -- null finding => thinness_acknowledged true', () => {
|
|
212
|
+
const roomDir = makeTmpRoom();
|
|
213
|
+
try {
|
|
214
|
+
const r = scaffoldRoomSkeleton(roomDir, { auto_explore_finding: null });
|
|
215
|
+
assert.strictEqual(r.thinness_acknowledged, true);
|
|
216
|
+
} finally { rmTmp(roomDir); }
|
|
217
|
+
|
|
218
|
+
const roomDir2 = makeTmpRoom();
|
|
219
|
+
try {
|
|
220
|
+
const substantive = { findings: [{ source_pipeline: 'whitespace', hsi_score: 0.7 }, { source_pipeline: 'cross-domain', hsi_score: 0.6 }] };
|
|
221
|
+
const r = scaffoldRoomSkeleton(roomDir2, { auto_explore_finding: substantive });
|
|
222
|
+
assert.strictEqual(r.thinness_acknowledged, false);
|
|
223
|
+
} finally { rmTmp(roomDir2); }
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('Test 12: no Brain MCP coupling in scaffold module', () => {
|
|
227
|
+
const src = fs.readFileSync(path.join(PROJECT_ROOT, 'lib', 'core', 'room-skeleton-scaffold.cjs'), 'utf8');
|
|
228
|
+
assert.ok(!/require\([^)]*brain-client/.test(src), 'brain-client require found');
|
|
229
|
+
assert.ok(!/fetch\([^)]*brain\.mindrian/.test(src), 'brain.mindrian fetch found');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('Test 13: no direct room-db.cjs require (Canon Part 9 chokepoint preserved)', () => {
|
|
233
|
+
const src = fs.readFileSync(path.join(PROJECT_ROOT, 'lib', 'core', 'room-skeleton-scaffold.cjs'), 'utf8');
|
|
234
|
+
assert.ok(!/require\([^)]*room-db\.cjs/.test(src), 'room-db.cjs require found');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('Test 14: em-dash invariant -- zero U+2014 in source files', () => {
|
|
238
|
+
// Construct the em-dash via charCode so this test file itself does not
|
|
239
|
+
// contain the literal char (which would self-trip the invariant).
|
|
240
|
+
const EMDASH = String.fromCharCode(0x2014);
|
|
241
|
+
const sources = [
|
|
242
|
+
path.join(PROJECT_ROOT, 'lib', 'core', 'room-skeleton-scaffold.cjs'),
|
|
243
|
+
path.join(PROJECT_ROOT, 'lib', 'core', 'room-skeleton-scaffold.test.cjs'),
|
|
244
|
+
];
|
|
245
|
+
for (const file of sources) {
|
|
246
|
+
const src = fs.readFileSync(file, 'utf8');
|
|
247
|
+
assert.ok(!src.includes(EMDASH), 'em-dash U+2014 found in ' + file);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('Bonus Test 15: SECTION_NAMES has exactly 8 entries; IDENTITY_DIRECTORIES has exactly 5', () => {
|
|
252
|
+
assert.strictEqual(SECTION_NAMES.length, 8, 'SECTION_NAMES not 8');
|
|
253
|
+
assert.strictEqual(Object.keys(IDENTITY_DIRECTORIES).length, 5, 'IDENTITY_DIRECTORIES not 5');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('Bonus Test 16: renderTemplate substitutes {{KEY}} tokens correctly', () => {
|
|
257
|
+
const out = renderTemplate('hello {{NAME}}, you are {{ROLE}}.', { NAME: 'Larry', ROLE: 'pedagogical guide' });
|
|
258
|
+
assert.strictEqual(out, 'hello Larry, you are pedagogical guide.');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('Bonus Test 17: isStateAuthored detects auto_created:false in frontmatter', () => {
|
|
262
|
+
const roomDir = makeTmpRoom();
|
|
263
|
+
try {
|
|
264
|
+
// auto_created:true => not authored (auto-scaffold output)
|
|
265
|
+
fs.writeFileSync(path.join(roomDir, 'STATE.md'), '---\nphase: scoping\nauto_created: true\n---\n');
|
|
266
|
+
assert.strictEqual(isStateAuthored(roomDir), false);
|
|
267
|
+
|
|
268
|
+
// auto_created:false => authored (human override)
|
|
269
|
+
fs.writeFileSync(path.join(roomDir, 'STATE.md'), '---\nphase: validation\nauto_created: false\n---\n');
|
|
270
|
+
assert.strictEqual(isStateAuthored(roomDir), true);
|
|
271
|
+
|
|
272
|
+
// No auto_created key => treat as authored (human authored from scratch)
|
|
273
|
+
fs.writeFileSync(path.join(roomDir, 'STATE.md'), '---\nphase: validation\n---\n');
|
|
274
|
+
assert.strictEqual(isStateAuthored(roomDir), true);
|
|
275
|
+
} finally { rmTmp(roomDir); }
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('Bonus Test 18: invalid roomDir returns ok:false with error', () => {
|
|
279
|
+
const r = scaffoldRoomSkeleton('/this/path/definitely/does/not/exist/xyzpdq', {});
|
|
280
|
+
assert.strictEqual(r.ok, false);
|
|
281
|
+
assert.ok(r.errors.includes('room_dir_does_not_exist'));
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('Bonus Test 19: SECTION_METADATA covers all 8 sections', () => {
|
|
285
|
+
for (const section of SECTION_NAMES) {
|
|
286
|
+
assert.ok(SECTION_METADATA[section], `missing metadata for ${section}`);
|
|
287
|
+
assert.ok(SECTION_METADATA[section].purpose, `missing purpose for ${section}`);
|
|
288
|
+
assert.ok(Array.isArray(SECTION_METADATA[section].stage_relevance), `stage_relevance not array for ${section}`);
|
|
289
|
+
assert.ok(Array.isArray(SECTION_METADATA[section].default_methodologies), `default_methodologies not array for ${section}`);
|
|
290
|
+
}
|
|
291
|
+
});
|