@haaaiawd/loom 1.2.1 → 1.3.1

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.
@@ -1,7 +1,7 @@
1
1
  // activate — compile a role- and intent-scoped Context Pack.
2
2
 
3
- import { readFileSync, existsSync } from 'node:fs';
4
- import { join } from 'node:path';
3
+ import { readFileSync, existsSync, readdirSync } from 'node:fs';
4
+ import { join, relative } from 'node:path';
5
5
  import { getLoomRoot } from './shared/paths.js';
6
6
  import { getIntent, getNarrative } from './intent-map.js';
7
7
  import { getIntentDraft } from './intent-draft.js';
@@ -11,13 +11,15 @@ import { extractMdSection } from './shared/md-utils.js';
11
11
  import { compileCapabilityInputs } from './capability-graph.js';
12
12
  import { getAtelierRecord } from './atelier.js';
13
13
  import { formatExpertisePackForPrompt, getExpertisePack, getExpertisePackState } from './expertise-pack.js';
14
+ import { listCapabilityProposals } from './capability-proposals.js';
14
15
 
15
- const VALID_ROLES = ['weaver', 'visionary', 'architect', 'forge', 'keeper'];
16
+ const VALID_ROLES = ['weaver', 'visionary', 'architect', 'impact-reviewer', 'forge', 'keeper'];
16
17
 
17
18
  const ROLE_FILES = {
18
19
  weaver: 'meta/PHILOSOPHY_WEAVER.md',
19
20
  visionary: 'roles/visionary.md',
20
21
  architect: 'roles/architect.md',
22
+ 'impact-reviewer': 'roles/impact-reviewer.md',
21
23
  forge: 'roles/forge.md',
22
24
  keeper: 'roles/keeper.md',
23
25
  };
@@ -25,6 +27,7 @@ const ROLE_FILES = {
25
27
  const ROLE_PHILOSOPHY_FILES = {
26
28
  visionary: ['PRODUCT_PHILOSOPHY.md', 'DECISION_RUBRIC.md'],
27
29
  architect: ['ENGINEERING_CREED.md', 'DECISION_RUBRIC.md'],
30
+ 'impact-reviewer': ['PRODUCT_PHILOSOPHY.md', 'DECISION_RUBRIC.md'],
28
31
  forge: ['ENGINEERING_CREED.md'],
29
32
  keeper: ['PRODUCT_PHILOSOPHY.md', 'DECISION_RUBRIC.md'],
30
33
  };
@@ -79,6 +82,10 @@ function compileEnvelope(role, intentId) {
79
82
  lines.push('- Keeper 必须在新的 Agent thread 中运行;同一会话切换角色不构成独立验证。');
80
83
  lines.push('- 无法获得独立上下文时降低声明,关键判断使用 pending_human。');
81
84
  }
85
+ if (role === 'impact-reviewer') {
86
+ lines.push('- Impact Reviewer 必须在新的 Agent thread 中运行;不得与 Architect 共用本轮推理上下文。');
87
+ lines.push('- 只审能力影响等级与外部获取必要性;不改产品目标、不写 Intent、不实现功能。');
88
+ }
82
89
  return lines.join('\n');
83
90
  }
84
91
 
@@ -275,6 +282,18 @@ function compileExpertiseInputs(role, versionDir, objective) {
275
282
  for (const node of compiled.nodes) {
276
283
  lines.push(` - ${node.id} [${node.kind}/${node.impact}] ${node.title}${node.question ? ` — ${node.question}` : ''}`);
277
284
  }
285
+ if (compiled.lenses?.length) {
286
+ lines.push('- Lens Contract: 以下透镜是本 Intent 不可忽略的结果维度;它们不是额外任务,也不能被“功能能跑”替代。');
287
+ for (const lens of compiled.lenses) {
288
+ lines.push(` - ${lens.id} / ${lens.title}: ${lens.question}(节点: ${lens.node_refs.join(', ')})`);
289
+ }
290
+ }
291
+ if (compiled.capability_domains?.length) {
292
+ lines.push('- Capability Domains: 以下是会改变本 Intent 方案或验证方法的专业领域;它们不是贴在结果上的专家标签。');
293
+ for (const domain of compiled.capability_domains) {
294
+ lines.push(` - ${domain.id} / ${domain.title}: ${domain.question} 为什么现在需要:${domain.why_now}(能力: ${domain.node_refs.join(', ')})`);
295
+ }
296
+ }
278
297
  for (const brief of compiled.briefs) {
279
298
  lines.push(`\n### Capability Brief: ${brief.node_id}\n\n${brief.content.trim()}`);
280
299
  }
@@ -354,6 +373,106 @@ function compileWorkingFacts(versionDir, objective) {
354
373
  return refs.join('\n');
355
374
  }
356
375
 
376
+ function readStageFile(path, label) {
377
+ if (!existsSync(path)) return `> ${label} 当前不存在;不要猜测其内容。按角色契约创建或回流。`;
378
+ return readFileSync(path, 'utf-8').trim();
379
+ }
380
+
381
+ function listMarkdownFiles(root, current = root) {
382
+ if (!existsSync(current)) return [];
383
+ return readdirSync(current, { withFileTypes: true })
384
+ .sort((a, b) => a.name.localeCompare(b.name))
385
+ .flatMap((entry) => {
386
+ const path = join(current, entry.name);
387
+ if (entry.isDirectory()) return listMarkdownFiles(root, path);
388
+ return entry.isFile() && entry.name.endsWith('.md')
389
+ ? [relative(root, path).replaceAll('\\', '/')]
390
+ : [];
391
+ });
392
+ }
393
+
394
+ /**
395
+ * Compile the stage inputs that used to be merely named by `loom guide`.
396
+ * These are intentionally separate from Working Facts: they are the source
397
+ * material for a role's immediate decision, not a request to hunt for paths.
398
+ */
399
+ function compileStageInputs(role, versionDir, objective) {
400
+ if (role === 'weaver') {
401
+ const dimensionsRoot = join(getLoomRoot(), 'dimensions');
402
+ const catalog = listMarkdownFiles(dimensionsRoot)
403
+ .filter((path) => path !== 'SEARCH_METHODOLOGY.md')
404
+ .map((path) => `- dimensions/${path}`);
405
+ return [
406
+ '本节由 `loom activate weaver` 直接装配。不要为了取得这些输入而搜索 CLI 安装目录。',
407
+ '',
408
+ '### Search Methodology',
409
+ '',
410
+ readStageFile(join(dimensionsRoot, 'SEARCH_METHODOLOGY.md'), 'SEARCH_METHODOLOGY.md'),
411
+ '',
412
+ '### Available Dimension Catalog',
413
+ '',
414
+ catalog.length
415
+ ? `${catalog.join('\n')}\n\n只打开会实质改变当前项目原则或取舍的维度;目录本身不是要求逐个阅读。`
416
+ : '> 当前没有可用的 dimensions 文档。',
417
+ ].join('\n');
418
+ }
419
+
420
+ if (role === 'architect' && versionDir && !objective.intent && !objective.draft) {
421
+ let proposals = [];
422
+ let proposalsError = null;
423
+ try {
424
+ proposals = listCapabilityProposals(versionDir, { unresolvedOnly: true });
425
+ } catch (error) {
426
+ proposalsError = error.message;
427
+ }
428
+ return [
429
+ '本节由 `loom activate architect` 直接装配。以下是当前设计决策所需的版本化输入,不要先手动查找路径。',
430
+ '',
431
+ '### Vision Input (01_VISION.md)',
432
+ '',
433
+ readStageFile(join(versionDir, '01_VISION.md'), '01_VISION.md'),
434
+ '',
435
+ '### Capability Graph Input (07_CAPABILITY_GRAPH.json)',
436
+ '',
437
+ readStageFile(join(versionDir, '07_CAPABILITY_GRAPH.json'), '07_CAPABILITY_GRAPH.json'),
438
+ '',
439
+ '### Intent Map Input (04_INTENT_MAP.json)',
440
+ '',
441
+ readStageFile(join(versionDir, '04_INTENT_MAP.json'), '04_INTENT_MAP.json'),
442
+ '',
443
+ '### Open Capability Graph Proposals',
444
+ '',
445
+ proposalsError
446
+ ? `> Proposal 记录无法读取:${proposalsError}。先修复这份记录,不要猜测其状态。`
447
+ : proposals.length
448
+ ? `\`\`\`json\n${JSON.stringify(proposals, null, 2)}\n\`\`\``
449
+ : '无。',
450
+ ].join('\n');
451
+ }
452
+
453
+ if (role === 'impact-reviewer' && versionDir && !objective.intent && !objective.draft) {
454
+ return [
455
+ '本节由 `loom activate impact-reviewer` 直接装配。以下是独立判断 Impact Gate 所需的最小版本化输入;不要先手动查找路径。',
456
+ '',
457
+ '### Vision Input (01_VISION.md)',
458
+ '',
459
+ readStageFile(join(versionDir, '01_VISION.md'), '01_VISION.md'),
460
+ '',
461
+ '### Proposed Capability Graph (07_CAPABILITY_GRAPH.json)',
462
+ '',
463
+ readStageFile(join(versionDir, '07_CAPABILITY_GRAPH.json'), '07_CAPABILITY_GRAPH.json'),
464
+ ].join('\n');
465
+ }
466
+
467
+ if (role === 'visionary') {
468
+ return '产品哲学与决策准则已在上方 Project Judgment 中装配;不要重新搜索 `.loom` 目录。';
469
+ }
470
+ if (role === 'forge' || role === 'keeper') {
471
+ return '当前 Intent 的叙事、契约、图谱路由与已就绪的 Expertise 输入已在上方装配;只有产物本身需要按 Working Facts 检查。';
472
+ }
473
+ return '当前角色没有额外的阶段输入。';
474
+ }
475
+
357
476
  /**
358
477
  * Compile a role-scoped Context Pack.
359
478
  * @param {string} role
@@ -377,9 +496,10 @@ export function activateRole(role, versionDir, intentId = null) {
377
496
  section('3. Hard Invariants', compileInvariants(role, versionDir)),
378
497
  section('4. Success Contracts', compileContracts(role, versionDir, objective)),
379
498
  section('5. Project Judgment', compileProjectJudgment(role, versionDir, objective)),
380
- section('6. Expertise Inputs', compileExpertiseInputs(role, versionDir, objective)),
381
- section('7. Working Facts', compileWorkingFacts(versionDir, objective)),
382
- section('8. Role Contract / Output / Reflow / Stop', readRole(role)),
499
+ section('6. Stage Inputs (command-assembled)', compileStageInputs(role, versionDir, objective)),
500
+ section('7. Expertise Inputs', compileExpertiseInputs(role, versionDir, objective)),
501
+ section('8. Working Facts', compileWorkingFacts(versionDir, objective)),
502
+ section('9. Role Contract / Output / Reflow / Stop', readRole(role)),
383
503
  ];
384
504
  return `${parts.join('\n\n---\n\n')}\n`;
385
505
  }
@@ -0,0 +1,282 @@
1
+ // atlas — deterministic decision-document model and H5 delivery contract.
2
+ // Atlas deliberately excludes live progress, verification history and patch ledgers:
3
+ // it explains the current version's architecture and decision structure.
4
+
5
+ import { createHash } from 'node:crypto';
6
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
7
+ import { join, relative } from 'node:path';
8
+ import { loadIntentMap } from './intent-map.js';
9
+ import { getCapabilityCoverage, loadCapabilityGraph } from './capability-graph.js';
10
+
11
+ const ATLAS_DIR = '11_DECISION_ATLAS';
12
+ const MODEL_FILE = 'atlas-model.json';
13
+ const ATLAS_FILE = 'loom-atlas.html';
14
+ const ATLAS_TEMPLATE_PATH = new URL('../../templates/ATLAS_TEMPLATE.html', import.meta.url);
15
+ const REQUIRED_SECTIONS = ['atlas-origin', 'atlas-structure', 'atlas-capabilities', 'atlas-decisions', 'atlas-review'];
16
+ const REQUIRED_EXPERIENCE_HOOKS = [
17
+ ['data-atlas-experience', 'deck-map-v1', 'loom-atlas.html 缺少 deck-map 体验标记'],
18
+ ['data-atlas-view', 'deck', 'loom-atlas.html 缺少“放映”读法入口'],
19
+ ['data-atlas-view', 'map', 'loom-atlas.html 缺少“审查地图”读法入口'],
20
+ ['data-atlas-control', 'previous', 'loom-atlas.html 缺少上一章操作'],
21
+ ['data-atlas-control', 'next', 'loom-atlas.html 缺少下一章操作'],
22
+ ['id', 'atlas-audit-map', 'loom-atlas.html 缺少可审查的关系地图'],
23
+ ];
24
+ const ROOT_DOCUMENTS = ['01_VISION.md', '02_ARCHITECTURE.md', '05_VERIFICATION.md'];
25
+ const DOCUMENT_DIRECTORIES = ['00_PHILOSOPHY', '03_DECISIONS', '07_CAPABILITY_BRIEFS'];
26
+
27
+ export function getAtlasModelPath(versionDir) {
28
+ return join(versionDir, ATLAS_DIR, MODEL_FILE);
29
+ }
30
+
31
+ export function getAtlasHtmlPath(projectDir) {
32
+ return join(projectDir, ATLAS_FILE);
33
+ }
34
+
35
+ function normalizePath(root, path) {
36
+ return relative(root, path).replaceAll('\\', '/');
37
+ }
38
+
39
+ function digest(value) {
40
+ return createHash('sha256').update(value).digest('hex');
41
+ }
42
+
43
+ function listFiles(root, result = []) {
44
+ if (!existsSync(root)) return result;
45
+ for (const entry of readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
46
+ const path = join(root, entry.name);
47
+ if (entry.isDirectory()) listFiles(path, result);
48
+ else if (entry.isFile()) result.push(path);
49
+ }
50
+ return result;
51
+ }
52
+
53
+ function collectAtlasSourceFiles(versionDir) {
54
+ const files = ROOT_DOCUMENTS
55
+ .map((file) => join(versionDir, file))
56
+ .filter(existsSync);
57
+ for (const directory of DOCUMENT_DIRECTORIES) listFiles(join(versionDir, directory), files);
58
+ const graph = join(versionDir, '07_CAPABILITY_GRAPH.json');
59
+ if (existsSync(graph)) files.push(graph);
60
+ return [...new Set(files)].sort((a, b) => normalizePath(versionDir, a).localeCompare(normalizePath(versionDir, b)));
61
+ }
62
+
63
+ function atlasRelevantBytes(path) {
64
+ if (path.endsWith('04_INTENT_MAP.json')) {
65
+ const map = JSON.parse(readFileSync(path, 'utf-8'));
66
+ for (const intent of Object.values(map.intents || {})) delete intent.status;
67
+ return Buffer.from(JSON.stringify(map));
68
+ }
69
+ return readFileSync(path);
70
+ }
71
+
72
+ function sourceManifest(versionDir) {
73
+ return collectAtlasSourceFiles(versionDir).map((path) => ({
74
+ path: normalizePath(versionDir, path),
75
+ sha256: digest(atlasRelevantBytes(path)),
76
+ }));
77
+ }
78
+
79
+ function readText(versionDir, reference) {
80
+ const path = join(versionDir, reference);
81
+ return existsSync(path) ? readFileSync(path, 'utf-8') : null;
82
+ }
83
+
84
+ function readDirectoryDocuments(versionDir, directory) {
85
+ return listFiles(join(versionDir, directory))
86
+ .map((path) => ({ path: normalizePath(versionDir, path), content: readFileSync(path, 'utf-8') }));
87
+ }
88
+
89
+ function projectIntentStructure(versionDir) {
90
+ const map = loadIntentMap(versionDir);
91
+ return Object.values(map.intents).map((intent) => ({
92
+ id: intent.id,
93
+ revision: intent.revision ?? 1,
94
+ title: intent.title,
95
+ narrative_ref: intent.narrative_ref,
96
+ depends_on: intent.depends_on,
97
+ acceptance: intent.acceptance,
98
+ quality_contract: intent.quality_contract || null,
99
+ semantic_guard: intent.semantic_guard || null,
100
+ philosophy_anchors: intent.philosophy_anchors,
101
+ capability_needs: intent.capability_needs || [],
102
+ continuity_required: intent.continuity_required === true,
103
+ creative_scope: intent.creative_scope || null,
104
+ }));
105
+ }
106
+
107
+ function buildStructuralReview(versionDir) {
108
+ const missing_documents = ROOT_DOCUMENTS
109
+ .filter((file) => !existsSync(join(versionDir, file)))
110
+ .map((file) => ({ kind: 'missing_document', source_refs: [file] }));
111
+ try {
112
+ const coverage = getCapabilityCoverage(versionDir);
113
+ return {
114
+ missing_documents,
115
+ capability_graph: {
116
+ high_unrouted: coverage.high_unrouted,
117
+ capabilities_without_plan: coverage.capabilities_without_plan,
118
+ routing_gaps: coverage.routing_gaps,
119
+ outcomes_without_concern: coverage.outcomes_without_concern,
120
+ high_outcomes_without_observable_evidence: coverage.high_outcomes_without_observable_evidence,
121
+ lens_contract_gaps: coverage.lens_contract_gaps || [],
122
+ capability_domain_gaps: coverage.capability_domain_gaps || [],
123
+ unmapped_intents: coverage.unmapped_intents,
124
+ },
125
+ };
126
+ } catch (error) {
127
+ return { missing_documents, capability_graph_error: error.message };
128
+ }
129
+ }
130
+
131
+ export function buildAtlasModel(versionDir) {
132
+ const sources = sourceManifest(versionDir);
133
+ const sourceDigest = digest(JSON.stringify(sources));
134
+ let graph = null;
135
+ try { graph = loadCapabilityGraph(versionDir); } catch { graph = null; }
136
+ return {
137
+ _meta: {
138
+ schema_version: '1.0',
139
+ kind: 'loom-decision-atlas',
140
+ version: normalizePath(join(versionDir, '..'), versionDir),
141
+ source_digest: sourceDigest,
142
+ source_count: sources.length,
143
+ scope: 'architecture_and_decision_structure_only',
144
+ digest_scope: 'source files projected without live intent status',
145
+ excluded: ['intent status', 'progress metrics', 'verification history', 'patch history'],
146
+ },
147
+ sources,
148
+ origin: readDirectoryDocuments(versionDir, '00_PHILOSOPHY'),
149
+ vision: readText(versionDir, '01_VISION.md'),
150
+ architecture: readText(versionDir, '02_ARCHITECTURE.md'),
151
+ capability_graph: graph,
152
+ capability_briefs: readDirectoryDocuments(versionDir, '07_CAPABILITY_BRIEFS'),
153
+ intent_structure: projectIntentStructure(versionDir),
154
+ verification_design: readText(versionDir, '05_VERIFICATION.md'),
155
+ decision_records: readDirectoryDocuments(versionDir, '03_DECISIONS'),
156
+ structural_review: buildStructuralReview(versionDir),
157
+ };
158
+ }
159
+
160
+ export function writeAtlasModel(versionDir) {
161
+ const model = buildAtlasModel(versionDir);
162
+ const path = getAtlasModelPath(versionDir);
163
+ mkdirSync(join(versionDir, ATLAS_DIR), { recursive: true });
164
+ writeFileSync(path, `${JSON.stringify(model, null, 2)}\n`, 'utf-8');
165
+ return { path, source_digest: model._meta.source_digest, source_count: model.sources.length };
166
+ }
167
+
168
+ function readAtlasModel(versionDir) {
169
+ const path = getAtlasModelPath(versionDir);
170
+ if (!existsSync(path)) return null;
171
+ try { return JSON.parse(readFileSync(path, 'utf-8')); } catch { return null; }
172
+ }
173
+
174
+ function readAtlasMeta(html) {
175
+ const match = html.match(/<meta\s+name=["']loom-atlas-source-digest["']\s+content=["']([^"']+)["']\s*\/?>/i);
176
+ return match?.[1] || null;
177
+ }
178
+
179
+ function hasAttributeValue(html, attribute, value) {
180
+ return new RegExp(`${attribute}=["']${value}["']`, 'i').test(html);
181
+ }
182
+
183
+ export function getAtlasStatus(projectDir, versionDir) {
184
+ const model = readAtlasModel(versionDir);
185
+ const current = buildAtlasModel(versionDir);
186
+ const atlasPath = getAtlasHtmlPath(projectDir);
187
+ const html = existsSync(atlasPath) ? readFileSync(atlasPath, 'utf-8') : null;
188
+ const htmlDigest = html ? readAtlasMeta(html) : null;
189
+ const modelFresh = model?._meta?.source_digest === current._meta.source_digest;
190
+ const htmlFresh = Boolean(html && htmlDigest === current._meta.source_digest);
191
+ return {
192
+ exists: Boolean(html),
193
+ model_exists: Boolean(model),
194
+ fresh: modelFresh && htmlFresh,
195
+ model_fresh: modelFresh,
196
+ html_fresh: htmlFresh,
197
+ atlas_path: atlasPath,
198
+ model_path: getAtlasModelPath(versionDir),
199
+ source_digest: current._meta.source_digest,
200
+ source_count: current.sources.length,
201
+ };
202
+ }
203
+
204
+ export function validateAtlas(projectDir, versionDir) {
205
+ const status = getAtlasStatus(projectDir, versionDir);
206
+ const errors = [];
207
+ if (!status.model_exists) errors.push('缺少 Atlas model;先运行 loom atlas build');
208
+ else if (!status.model_fresh) errors.push('Atlas model 已过期;先运行 loom atlas build');
209
+ if (!status.exists) errors.push('缺少 loom-atlas.html;运行 loom atlas --regen 获取 Composer Pack 后生成');
210
+ else {
211
+ const html = readFileSync(status.atlas_path, 'utf-8');
212
+ if (!status.html_fresh) errors.push('loom-atlas.html 未绑定当前 Atlas source digest;重新运行 loom atlas --regen 并基于新模型生成');
213
+ for (const id of REQUIRED_SECTIONS) {
214
+ if (!new RegExp(`id=["']${id}["']`, 'i').test(html)) errors.push(`loom-atlas.html 缺少必需章节 #${id}`);
215
+ }
216
+ for (const [attribute, value, error] of REQUIRED_EXPERIENCE_HOOKS) {
217
+ if (!hasAttributeValue(html, attribute, value)) errors.push(error);
218
+ }
219
+ }
220
+ return { valid: errors.length === 0, errors, ...status };
221
+ }
222
+
223
+ export function generateAtlasComposerPack(versionDir) {
224
+ const model = buildAtlasModel(versionDir);
225
+ const shell = readFileSync(ATLAS_TEMPLATE_PATH, 'utf-8').replace('{{SOURCE_DIGEST}}', model._meta.source_digest);
226
+ return [
227
+ '# LOOM Atlas Composer Pack',
228
+ '',
229
+ '你正在生成当前版本的必交付“决策图谱”。它解释文档架构与设计结构,不是进度仪表盘。',
230
+ '只使用下面 Atlas Model 的事实;不得编造决策、证据、完成状态、用户反馈或来源。',
231
+ '',
232
+ '## Fixed Content Contract',
233
+ '',
234
+ '- 必须有五个可定位章节:#atlas-origin、#atlas-structure、#atlas-capabilities、#atlas-decisions、#atlas-review。',
235
+ '- #atlas-origin:为什么存在、原则与非目标;#atlas-structure:愿景、架构与 Intent 结构;#atlas-capabilities:Outcome/Concern/Capability、Lens Contract 与 Capability Domains;#atlas-decisions:关键取舍和 ADR;#atlas-review:结构性缺口、未决问题与每项的 source_refs。',
236
+ '- 不展示 Intent 状态、完成百分比、看板、燃尽图、验证轮次或 Patch 时间线。',
237
+ '- 页面必须有“放映 / 审查地图”两种读法:放映用章节叙事,地图用目标→能力→领域→Intent→验证设计的可点击关系图。',
238
+ '- 每个可点击决策都显示来源路径;事实、推断、待确认项使用可见标签区分。',
239
+ '- 放映是五张横向章节幻灯片,不是纵向长文:桌面支持左右箭头和键盘 ←/→,移动端支持明确的前后按钮与横向滑动;切换只服务于章节理解,不做自动轮播。',
240
+ '- 审查地图不是缩略目录:用可点击的 SVG / CSS 关系图呈现关键链路。选中节点后,在同屏的“检查台”展示它的简短解释、相连对象与 source_refs;不得把原始 JSON 或长段文字直接堆上去。',
241
+ '- 每章至少有一个由 Atlas Model 派生的视觉结构:原则的取舍构图、架构层/Intent 依赖、能力链、决策分叉、结构缺口或待确认项。图形的每个标签必须来自 Model;SVG 必须带 title/desc,颜色不能是唯一编码。',
242
+ '- 保留这些可验证的语义钩子:body data-atlas-experience="deck-map-v1";data-atlas-view="deck" 与 "map";data-atlas-control="previous" 与 "next";#atlas-audit-map。',
243
+ '',
244
+ '## Visual Freedom',
245
+ '',
246
+ '你可独立选择品牌气质、排版、色彩、图形和细腻的章节转场。默认把它读成一份可翻阅的“决策日报”,而不是 SaaS 仪表盘:暖米色纸张、墨黑文字、砖红作为唯一强调色,标题采用报纸式层级,细线分栏,图表与边注共同承载信息。不要复刻任何真实报纸的报头、标识或报道内容。除非 Atlas Model 提供品牌依据,不要改成深色荧光控制台、紫色渐变或玻璃拟态。',
247
+ '',
248
+ '## SVG Composition Playbook',
249
+ '',
250
+ '- 把 SVG 当作“可审查的论证图”,不是图标拼贴或背景花纹。每一张图只回答一个问题,并让节点、边、标签、选中态都来自 Atlas Model。没有对应事实时,留空并明确它是待确认项,不要画假数据。',
251
+ '- 用少量有结构的原语构图:圆/环表示范围或关注点,线/箭头表示依赖、影响或证据路径,分叉表示取舍,断线表示结构缺口,层叠矩形表示架构边界。避免装饰性 3D、无意义粒子和难读的“科技网格”。',
252
+ '- 推荐每章一个不同的图形语法:起点可用“原则-北极星-非目标”的张力图;结构可用架构分层叠图和 Intent DAG;能力可用 Outcome→Concern→Capability→Domain→Evidence 链;决策可用 ADR 的双路分叉;审查可用断线、孤点和回到 source_refs 的追溯链。不要五章都画成同一种卡片或同一种圆点网络。',
253
+ '- 关系图中的可点击节点必须可键盘访问,并在同屏检查台更新:节点名称、它解决的具体问题、连接对象和 source_refs。hover 只能是额外提示,不能是唯一入口。',
254
+ '- 每个 SVG 必须有 viewBox、title、desc 或等价可读描述;文字标签直接写出实体名称;线条样式、形状与文字共同表达意义,不能只靠颜色。响应式缩放时保留标签空间,减少动态效果时取消路径动画但保留全部关系。',
255
+ '- 只为“关系出现、章节切换、节点选中”做 transform/opacity/path 的有限动效,不自动轮播、不做持续闪烁。复杂图先让静态首帧完整可读,再增加交互。',
256
+ '',
257
+ '## Visual Quality Loop',
258
+ '',
259
+ '- 先从 Model 中选定一个能服务“普通人审查复杂决策”的视觉母题,并用一句内部设计判断约束全页。随后直接做成一个方向,不要交付三个半成品版本,也不要套用通用 SaaS dashboard、五张相同卡片或紫色发光渐变。',
260
+ '- 放映的五章必须各有不同的信息构图:开篇建立张力,结构解释边界,能力展示网络,决策展示分叉,审查暴露缺口。它们可共享报纸栏网、纸张底色和砖红强调,但不能只是把标题、段落和圆角容器重复五次。',
261
+ '- 将 Atlas 做成“可翻阅的图文决策日报”,不是稀疏展览海报:每页至少同时呈现一个关键结论、三到五条从 Model 提炼的事实/取舍、一个有标签的主图、两条以内的读图/边注和来源入口。桌面优先采用主报道区 + 侧栏图表/边注的非对称编排;窄屏按“标题 → 事实栏 → 图表 → 读图注 → 来源”重排,页面本身可纵向阅读,前后切章控件始终可达。信息密度来自结构化事实与图形,不来自缩小字体或塞入长段文字。',
262
+ '- 文案采用“短句 + 图形 + 可展开来源”的节奏。首屏让人立刻明白这个版本试图守住什么;每页只留足够支持图形理解的文字。长内容进入点击后的检查台或来源回链,绝不把 Model JSON、完整 Markdown 或十几条项目符号直接铺在页面上。',
263
+ '- 交付前实际检查 390px 触屏宽度和 1440px 桌面宽度:前后切章、横向滑动、←/→ 键、章节定位、放映/地图切换、地图节点选择、来源显示与 prefers-reduced-motion 都必须可用且不遮挡文字。若运行环境无法做视觉检查,应诚实说明未验证项,不能把“理论上响应式”写成已验证。',
264
+ '- 将明显问题当作返工信号:首屏没有信息图形;只有一个标题和一个孤立图形而没有可读事实;一屏超过约 60 个汉字的连续正文;五章同构;图形只有装饰没有事实;只能 hover 才能读到关键内容;手机上需要双指缩放;对比度不足;或动效让审查者错过关系。修复后再交付。',
265
+ '',
266
+ '## Delivery Contract',
267
+ '',
268
+ `写单文件 ${ATLAS_FILE} 到项目根目录。零外部依赖、内联 CSS/JS、响应式。head 中必须包含:`,
269
+ `<meta name="loom-atlas-source-digest" content="${model._meta.source_digest}">`,
270
+ '- 以下模板是固定结构基线。可重写视觉语言和内部构图,但保留 digest 与五个 section id。',
271
+ '',
272
+ '```html',
273
+ shell,
274
+ '```',
275
+ '',
276
+ '## Atlas Model (command-assembled)',
277
+ '',
278
+ '```json',
279
+ JSON.stringify(model, null, 2),
280
+ '```',
281
+ ].join('\n');
282
+ }