@openprd/cli 0.1.1 → 0.1.8

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.
Files changed (137) hide show
  1. package/.openprd/README.md +43 -69
  2. package/.openprd/README_EN.md +84 -0
  3. package/.openprd/benchmarks/index.md +7 -0
  4. package/.openprd/benchmarks/sources.yaml +25 -3
  5. package/.openprd/discovery/config.json +16 -2
  6. package/.openprd/engagements/active/flows.md +19 -14
  7. package/.openprd/engagements/active/handoff.md +11 -4
  8. package/.openprd/engagements/active/prd.md +99 -71
  9. package/.openprd/engagements/active/review.html +4 -4
  10. package/.openprd/engagements/active/roles.md +9 -8
  11. package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +4 -4
  12. package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +18 -0
  13. package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +18 -0
  14. package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +18 -0
  15. package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +18 -0
  16. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +78 -0
  17. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +129 -0
  18. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +41 -0
  19. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +14 -0
  20. package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +49 -0
  21. package/.openprd/knowledge/index.json +44 -4
  22. package/.openprd/reviews/v0001.html +195 -129
  23. package/.openprd/reviews/v0002.html +1150 -0
  24. package/.openprd/reviews/v0003.html +1150 -0
  25. package/.openprd/reviews/v0004.html +1150 -0
  26. package/.openprd/reviews/v0005.html +1150 -0
  27. package/.openprd/standards/config.json +12 -9
  28. package/.openprd/state/changes.json +17 -2
  29. package/.openprd/state/current.json +399 -63
  30. package/.openprd/state/release-ledger.json +344 -0
  31. package/.openprd/state/version-index.json +52 -0
  32. package/.openprd/state/versions/v0002.json +264 -0
  33. package/.openprd/state/versions/v0002.md +183 -0
  34. package/.openprd/state/versions/v0003.json +269 -0
  35. package/.openprd/state/versions/v0003.md +188 -0
  36. package/.openprd/state/versions/v0004.json +274 -0
  37. package/.openprd/state/versions/v0004.md +193 -0
  38. package/.openprd/state/versions/v0005.json +299 -0
  39. package/.openprd/state/versions/v0005.md +189 -0
  40. package/.openprd/templates/agent/intake.md +5 -4
  41. package/.openprd/templates/b2b/intake.md +5 -4
  42. package/.openprd/templates/base/intake.md +10 -4
  43. package/.openprd/templates/company/README.md +9 -7
  44. package/.openprd/templates/company/README_EN.md +12 -0
  45. package/.openprd/templates/consumer/intake.md +5 -4
  46. package/.openprd/templates/industry/README.md +12 -10
  47. package/.openprd/templates/industry/README_EN.md +18 -0
  48. package/.openprd/templates/project/README.md +11 -9
  49. package/.openprd/templates/project/README_EN.md +16 -0
  50. package/.openprd/templates/session/README.md +11 -9
  51. package/.openprd/templates/session/README_EN.md +16 -0
  52. package/AGENTS.md +12 -8
  53. package/README.md +399 -438
  54. package/README_CN.md +4 -578
  55. package/README_EN.md +850 -0
  56. package/docs/assets/openprd-requirement-routing-en.png +0 -0
  57. package/docs/assets/openprd-requirement-routing-en.svg +102 -0
  58. package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
  59. package/docs/assets/openprd-requirement-routing-zh.png +0 -0
  60. package/docs/assets/openprd-requirement-routing-zh.svg +102 -0
  61. package/package.json +6 -2
  62. package/scripts/dev-check-wrapup-copy.mjs +110 -0
  63. package/scripts/openprd-github-release-notes.mjs +99 -0
  64. package/scripts/quality-perf-check.mjs +203 -0
  65. package/skills/openprd-benchmark-router/SKILL.md +1 -0
  66. package/skills/openprd-benchmark-router/references/benchmark-sources.md +1 -0
  67. package/skills/openprd-benchmark-router/references/source-policy.md +2 -0
  68. package/skills/openprd-discovery-loop/SKILL.md +2 -2
  69. package/skills/openprd-harness/SKILL.md +46 -24
  70. package/skills/openprd-harness/references/workflow-gates.md +15 -0
  71. package/skills/openprd-quality/SKILL.md +10 -4
  72. package/skills/openprd-requirement-intake/SKILL.md +31 -20
  73. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -6
  74. package/skills/openprd-requirement-intake/references/routing-rubric.md +10 -2
  75. package/skills/openprd-router/SKILL.md +2 -2
  76. package/skills/openprd-shared/SKILL.md +51 -23
  77. package/skills/openprd-standards/SKILL.md +2 -1
  78. package/src/agent-integration.js +265 -65
  79. package/src/benchmark/constants.js +107 -0
  80. package/src/benchmark/operations.js +235 -0
  81. package/src/benchmark/registry.js +64 -0
  82. package/src/benchmark/render.js +115 -0
  83. package/src/benchmark/source.js +617 -0
  84. package/src/benchmark/storage.js +121 -0
  85. package/src/benchmark/verify.js +235 -0
  86. package/src/benchmark.js +50 -851
  87. package/src/change-summary.js +339 -0
  88. package/src/cli/args.js +67 -6
  89. package/src/cli/basic-print.js +365 -0
  90. package/src/cli/benchmark-print.js +91 -0
  91. package/src/cli/change-print.js +221 -0
  92. package/src/cli/doctor-print.js +268 -0
  93. package/src/cli/growth-print.js +176 -0
  94. package/src/cli/print.js +73 -1384
  95. package/src/cli/quality-print.js +284 -0
  96. package/src/cli/run-print.js +297 -0
  97. package/src/cli/shared-print.js +127 -0
  98. package/src/cli/workflow-print.js +195 -0
  99. package/src/codex-hook-runner-template.mjs +639 -117
  100. package/src/codex-runtime.js +324 -0
  101. package/src/dev-standards.js +178 -5
  102. package/src/diagram-core.js +5 -5
  103. package/src/discovery.js +2 -1
  104. package/src/execution-strategy.js +369 -0
  105. package/src/fleet.js +4 -0
  106. package/src/github-release.js +156 -0
  107. package/src/growth.js +311 -13
  108. package/src/html-artifact-utils.js +25 -0
  109. package/src/html-artifacts.js +157 -1596
  110. package/src/knowledge.js +1176 -75
  111. package/src/language-policy.js +2 -112
  112. package/src/learning-html-artifact.js +1031 -0
  113. package/src/learning-review.js +3 -2
  114. package/src/loop.js +280 -9
  115. package/src/openprd.js +341 -38
  116. package/src/openspec/change-validate.js +0 -9
  117. package/src/openspec/execute.js +79 -3
  118. package/src/openspec/generate.js +33 -20
  119. package/src/openspec/tasks.js +33 -2
  120. package/src/prd-core.js +10 -9
  121. package/src/product-type-copy.js +69 -0
  122. package/src/quality-html-artifact.js +108 -9
  123. package/src/quality-learning.js +30 -0
  124. package/src/quality-visual-review.js +237 -0
  125. package/src/quality.js +329 -43
  126. package/src/registry-hygiene.js +54 -0
  127. package/src/release-ledger.js +413 -0
  128. package/src/review-presentation.js +12 -6
  129. package/src/run-harness.js +722 -48
  130. package/src/session-binding.js +40 -3
  131. package/src/session-registry.js +159 -0
  132. package/src/standards.js +5 -3
  133. package/src/test-strategy.js +386 -0
  134. package/src/visual-compare.js +915 -34
  135. package/src/work-unit-migration.js +5 -1
  136. package/src/workspace-core.js +343 -19
  137. package/src/workspace-workflow.js +538 -134
@@ -0,0 +1,1031 @@
1
+ import path from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { cjoin } from './fs-utils.js';
4
+ import { escapeHtml, listMarkup, slugify } from './html-artifact-utils.js';
5
+
6
+ function learningSourceAnchor(sourceId) {
7
+ return `source-${slugify(sourceId, 'source')}`;
8
+ }
9
+
10
+ function learningAssetUrl(rawPath) {
11
+ const value = String(rawPath ?? '').trim();
12
+ if (!value) return null;
13
+ if (/^(?:https?:|data:|file:)/i.test(value)) return value;
14
+ if (path.isAbsolute(value)) return pathToFileURL(value).href;
15
+ return encodeURI(value.split(path.sep).join('/'));
16
+ }
17
+
18
+ function formatLearningParagraphs(paragraphs) {
19
+ const list = Array.isArray(paragraphs) ? paragraphs.filter(Boolean) : [];
20
+ return list.map((paragraph) => `<p>${escapeHtml(paragraph)}</p>`).join('\n');
21
+ }
22
+
23
+ function formatLearningRetrievalBlocks(blocks, chapterId) {
24
+ const list = Array.isArray(blocks) ? blocks.filter(Boolean) : [];
25
+ if (list.length === 0) return '';
26
+ return `
27
+ <section class="learning-block retrieval" id="${escapeHtml(chapterId)}-retrieval">
28
+ <h4>检索练习</h4>
29
+ ${list.map((block, index) => `
30
+ <details class="retrieval-item" id="${escapeHtml(chapterId)}-retrieval-${index + 1}">
31
+ <summary><span>R${index + 1}</span>${escapeHtml(block.prompt)}</summary>
32
+ ${block.hint ? `<div class="retrieval-hint">提示: ${escapeHtml(block.hint)}</div>` : ''}
33
+ <div class="retrieval-answer">参考答案: ${escapeHtml(block.answer)}</div>
34
+ </details>
35
+ `).join('\n')}
36
+ </section>
37
+ `;
38
+ }
39
+
40
+ function formatLearningWorkedExamples(examples, chapterId) {
41
+ const list = Array.isArray(examples) ? examples.filter(Boolean) : [];
42
+ if (list.length === 0) return '';
43
+ return `
44
+ <section class="learning-block worked" id="${escapeHtml(chapterId)}-worked">
45
+ <h4>工作示例</h4>
46
+ ${list.map((example, index) => `
47
+ <div class="worked-item" id="${escapeHtml(chapterId)}-worked-${index + 1}">
48
+ <div class="worked-title">${escapeHtml(example.title)}</div>
49
+ <p>${escapeHtml(example.scenario)}</p>
50
+ <ol>${listMarkup(example.steps, '暂无步骤')}</ol>
51
+ ${example.principle ? `<div class="worked-principle">原则: ${escapeHtml(example.principle)}</div>` : ''}
52
+ </div>
53
+ `).join('\n')}
54
+ </section>
55
+ `;
56
+ }
57
+
58
+ function formatLearningVisualExplainer(explainer, chapterId) {
59
+ if (!explainer || typeof explainer !== 'object') return '';
60
+ const takeaways = Array.isArray(explainer.takeaways) ? explainer.takeaways.filter(Boolean) : [];
61
+ const imageUrl = learningAssetUrl(explainer.image?.path);
62
+ const hasImage = Boolean(imageUrl);
63
+ return `
64
+ <section class="learning-block visual" id="${escapeHtml(chapterId)}-visual">
65
+ <div class="visual-header">
66
+ <div class="visual-kicker">一眼看懂</div>
67
+ <h4>${escapeHtml(explainer.title ?? '图文解释')}</h4>
68
+ </div>
69
+ <div class="visual-grid${hasImage ? ' has-image' : ''}">
70
+ ${hasImage ? `
71
+ <figure class="visual-figure">
72
+ <img src="${escapeHtml(imageUrl)}" alt="${escapeHtml(explainer.image?.alt ?? explainer.title ?? 'visual explainer')}" loading="lazy" />
73
+ ${explainer.image?.caption ? `<figcaption>${escapeHtml(explainer.image.caption)}</figcaption>` : ''}
74
+ </figure>
75
+ ` : ''}
76
+ <div class="visual-copy">
77
+ <div class="visual-note">
78
+ <div class="visual-label">比喻</div>
79
+ <p>${escapeHtml(explainer.analogy ?? '')}</p>
80
+ </div>
81
+ <div class="visual-note">
82
+ <div class="visual-label">场景</div>
83
+ <p>${escapeHtml(explainer.scene ?? '')}</p>
84
+ </div>
85
+ <div class="visual-note">
86
+ <div class="visual-label">为什么这张图有用</div>
87
+ <p>${escapeHtml(explainer.whyItMatters ?? '')}</p>
88
+ </div>
89
+ ${takeaways.length > 0 ? `
90
+ <div class="visual-note">
91
+ <div class="visual-label">看图重点</div>
92
+ <ul class="visual-takeaways">${listMarkup(takeaways, '暂无重点')}</ul>
93
+ </div>
94
+ ` : ''}
95
+ </div>
96
+ </div>
97
+ </section>
98
+ `;
99
+ }
100
+
101
+ function formatLearningEvidenceDetails(chapter, sourcesById) {
102
+ const ids = Array.isArray(chapter.evidenceIds) ? chapter.evidenceIds.filter(Boolean) : [];
103
+ if (ids.length === 0) return '';
104
+ return `
105
+ <details class="chapter-evidence" id="${escapeHtml(chapter.id)}-evidence">
106
+ <summary>
107
+ <span class="evidence-summary-title">本章出处</span>
108
+ <span class="evidence-summary-count">${ids.length} 个来源</span>
109
+ </summary>
110
+ <div class="evidence-mini-list">
111
+ ${ids.map((id) => {
112
+ const source = sourcesById.get(id);
113
+ return `
114
+ <div class="evidence-mini-card">
115
+ <strong>${escapeHtml(source?.title ?? id)}</strong>
116
+ <span>${escapeHtml(source?.relativePath ?? source?.path ?? id)}</span>
117
+ ${source?.summary ? `<p>${escapeHtml(source.summary)}</p>` : ''}
118
+ </div>
119
+ `;
120
+ }).join('\n')}
121
+ </div>
122
+ </details>
123
+ `;
124
+ }
125
+
126
+ function formatLearningChapter(chapter, index, sourcesById) {
127
+ return `
128
+ <section class="chapter${index === 0 ? ' active' : ''}" id="${escapeHtml(chapter.id)}" data-chapter-index="${index}"${index === 0 ? '' : ' hidden'}>
129
+ <div class="chapter-kicker" id="${escapeHtml(chapter.id)}-reading">第 ${index + 1} 章 · ${escapeHtml(chapter.label)}</div>
130
+ <h2>${escapeHtml(chapter.semanticTitle)}</h2>
131
+ <p class="chapter-summary">${escapeHtml(chapter.summary)}</p>
132
+ ${formatLearningVisualExplainer(chapter.visualExplainer, chapter.id)}
133
+ ${formatLearningParagraphs(chapter.paragraphs)}
134
+ ${formatLearningRetrievalBlocks(chapter.retrievalBlocks, chapter.id)}
135
+ ${formatLearningWorkedExamples(chapter.workedExamples, chapter.id)}
136
+ ${formatLearningEvidenceDetails(chapter, sourcesById)}
137
+ </section>
138
+ `;
139
+ }
140
+
141
+ function formatLearningOutlineNode(node, indexPath = '1', activeChapterId = null) {
142
+ const hasChildren = Array.isArray(node.children) && node.children.length > 0;
143
+ const label = `
144
+ <span class="outline-jump depth-${escapeHtml(node.depth ?? 1)}${node.id === activeChapterId ? ' active' : ''}" data-target-id="${escapeHtml(node.id)}">
145
+ <span class="outline-number">${escapeHtml(indexPath)}</span>
146
+ <span class="outline-copy">
147
+ <strong>${escapeHtml(node.title)}</strong>
148
+ ${node.subtitle ? `<small>${escapeHtml(node.subtitle)}</small>` : ''}
149
+ </span>
150
+ </span>
151
+ `;
152
+ if (!hasChildren) return `<li>${label}</li>`;
153
+ return `
154
+ <li>
155
+ <details class="outline-branch" open>
156
+ <summary>${label}</summary>
157
+ <ol>
158
+ ${node.children.map((child, childIndex) => formatLearningOutlineNode(child, `${indexPath}.${childIndex + 1}`, activeChapterId)).join('\n')}
159
+ </ol>
160
+ </details>
161
+ </li>
162
+ `;
163
+ }
164
+
165
+ function formatLearningEmptyState(content, packageMeta, evidenceManifest) {
166
+ const promptPath = content.agentPromptPath ?? packageMeta?.paths?.agentPrompt ?? null;
167
+ const contextPath = content.agentContextPath ?? packageMeta?.paths?.agentContext ?? null;
168
+ const contentPath = content.packagePaths?.contentJson ?? packageMeta?.paths?.contentJson ?? null;
169
+ const assetsDir = content.packagePaths?.assetsDir ?? packageMeta?.paths?.assetsDir ?? null;
170
+ const renderCommand = contentPath ? `openprd learn . --content-json ${contentPath} --open` : null;
171
+ const sourceCount = evidenceManifest?.sourceCount ?? (evidenceManifest?.sources?.length ?? 0);
172
+ const claimCount = evidenceManifest?.claimCount ?? (evidenceManifest?.claims?.length ?? 0);
173
+ const gapCount = Array.isArray(evidenceManifest?.gaps) ? evidenceManifest.gaps.length : 0;
174
+ return `
175
+ <section class="empty-reader" id="agent-authoring">
176
+ <p class="chapter-kicker">证据包待写作</p>
177
+ <h2>还没有生成可阅读正文</h2>
178
+ <p>这一步只完成了学习包归档和证据收集。真正给人阅读的标题、大纲、章节、检索练习和工作示例,还需要由 Agent 根据证据写入内容 JSON 后再渲染。</p>
179
+ <div class="stat-grid">
180
+ <div class="stat"><div class="stat-value">${sourceCount}</div><div class="stat-label">份证据来源</div></div>
181
+ <div class="stat"><div class="stat-value">${claimCount}</div><div class="stat-label">条结构化判断</div></div>
182
+ <div class="stat"><div class="stat-value">${gapCount}</div><div class="stat-label">个待补缺口</div></div>
183
+ </div>
184
+ <ol class="empty-steps">
185
+ <li>让 Agent 读取写作提示、上下文和证据清单。</li>
186
+ <li>由 Agent 把标题、目录、章节正文、检索练习、工作示例和需要的 visualExplainer 写进 <code>learning-content.json</code>。</li>
187
+ <li>写完后重新执行渲染命令,再打开阅读器查看成品。</li>
188
+ </ol>
189
+ <div class="empty-paths">
190
+ ${promptPath ? `<div><strong>写作提示</strong><span>${escapeHtml(promptPath)}</span></div>` : ''}
191
+ ${contextPath ? `<div><strong>上下文</strong><span>${escapeHtml(contextPath)}</span></div>` : ''}
192
+ ${contentPath ? `<div><strong>内容 JSON</strong><span>${escapeHtml(contentPath)}</span></div>` : ''}
193
+ ${assetsDir ? `<div><strong>图片素材目录</strong><span>${escapeHtml(assetsDir)}</span></div>` : ''}
194
+ ${renderCommand ? `<div><strong>重渲染命令</strong><span>${escapeHtml(renderCommand)}</span></div>` : ''}
195
+ </div>
196
+ </section>
197
+ `;
198
+ }
199
+
200
+ export function renderLearningArtifact({ packageMeta, content, evidenceManifest }) {
201
+ const chapters = Array.isArray(content.chapters) ? content.chapters : [];
202
+ const sources = Array.isArray(evidenceManifest.sources) ? evidenceManifest.sources : [];
203
+ const title = content.title || packageMeta?.title || 'OpenPrd 复盘学习包';
204
+ const outline = Array.isArray(content.outline) && content.outline.length > 0
205
+ ? content.outline
206
+ : chapters.map((chapter, index) => ({
207
+ id: chapter.id,
208
+ depth: 1,
209
+ title: `第 ${index + 1} 章 · ${chapter.label}`,
210
+ subtitle: chapter.semanticTitle,
211
+ children: [],
212
+ }));
213
+ const sourcesById = new Map(sources.map((source) => [source.id, source]));
214
+ const initialChapterId = chapters[0]?.id ?? outline[0]?.id ?? null;
215
+ const initialProgressPercent = chapters.length > 0 ? String((1 / chapters.length) * 100) : '0';
216
+
217
+ return `<!DOCTYPE html>
218
+ <html lang="zh-CN">
219
+ <head>
220
+ <meta charset="UTF-8" />
221
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
222
+ <title>${escapeHtml(title)}</title>
223
+ <style>
224
+ :root {
225
+ color-scheme: light;
226
+ --bg: #f6fbff;
227
+ --bg-deep: #eef6ff;
228
+ --paper: #ffffff;
229
+ --panel: rgba(255, 255, 255, 0.96);
230
+ --ink: #171411;
231
+ --text: #1f2b3d;
232
+ --muted: #66758b;
233
+ --line: rgba(121, 151, 194, 0.28);
234
+ --line-strong: rgba(91, 126, 177, 0.32);
235
+ --accent: #ef7b43;
236
+ --accent-deep: #d95f26;
237
+ --accent-soft: #fff2e8;
238
+ --amber: #8a5a2b;
239
+ --amber-soft: #f6e7d4;
240
+ --jade: #ef7b43;
241
+ --wash: #f5f9ff;
242
+ --danger-soft: rgba(220,38,38,0.08);
243
+ --reader-scale: 1;
244
+ --mono: "JetBrains Mono","SFMono-Regular",Menlo,monospace;
245
+ --serif: "Songti SC","Noto Serif CJK SC","Iowan Old Style","Palatino Linotype",serif;
246
+ --ui: "Avenir Next","Gill Sans","Trebuchet MS",sans-serif;
247
+ }
248
+ * { box-sizing: border-box; }
249
+ html { scroll-behavior: smooth; }
250
+ body {
251
+ margin: 0;
252
+ background:
253
+ linear-gradient(90deg, rgba(95, 129, 181, 0.07) 0 1px, transparent 1px 100%),
254
+ linear-gradient(rgba(95, 129, 181, 0.07) 0 1px, transparent 1px 100%),
255
+ radial-gradient(circle at top, rgba(255,255,255,0.82), transparent 30%),
256
+ linear-gradient(180deg, #fbfdff 0%, var(--bg) 50%, var(--bg-deep) 100%);
257
+ background-size: 56px 56px, 56px 56px, auto, auto;
258
+ color: var(--text);
259
+ font-family: var(--ui);
260
+ overflow: hidden;
261
+ }
262
+ .shell {
263
+ display: grid;
264
+ grid-template-columns: minmax(280px, 330px) minmax(0, 980px);
265
+ gap: 18px;
266
+ max-width: 1340px;
267
+ height: 100vh;
268
+ margin: 0 auto;
269
+ padding: 18px;
270
+ }
271
+ .side-panel,
272
+ .reader {
273
+ border: 1px solid var(--line);
274
+ border-radius: 18px;
275
+ background: var(--panel);
276
+ box-shadow: 0 20px 50px rgba(92, 122, 168, 0.14);
277
+ }
278
+ .side-panel {
279
+ position: sticky;
280
+ top: 18px;
281
+ align-self: start;
282
+ max-height: calc(100vh - 36px);
283
+ overflow: auto;
284
+ padding: 18px;
285
+ background:
286
+ linear-gradient(180deg, rgba(255,255,255,0.985), rgba(252,254,255,0.985)),
287
+ var(--panel);
288
+ }
289
+ .reader {
290
+ min-width: 0;
291
+ background: var(--paper);
292
+ overflow: hidden;
293
+ position: relative;
294
+ display: grid;
295
+ grid-template-rows: auto minmax(0, 1fr);
296
+ height: calc(100vh - 36px);
297
+ }
298
+ .reader-header {
299
+ border-bottom: 1px solid var(--line);
300
+ background:
301
+ linear-gradient(135deg, rgba(255,255,255,0.995), rgba(249,252,255,0.98)),
302
+ var(--paper);
303
+ padding: 16px 30px 10px;
304
+ }
305
+ .reader-scroll {
306
+ min-height: 0;
307
+ overflow-y: auto;
308
+ overscroll-behavior: contain;
309
+ scrollbar-gutter: stable;
310
+ scroll-padding-top: 24px;
311
+ }
312
+ .eyebrow {
313
+ margin: 0 0 8px;
314
+ color: var(--accent);
315
+ font-size: 13px;
316
+ font-weight: 800;
317
+ text-transform: uppercase;
318
+ letter-spacing: 0;
319
+ }
320
+ h1 {
321
+ margin: 0;
322
+ font-family: var(--serif);
323
+ font-size: clamp(27px, 3.2vw, 36px);
324
+ line-height: 1.14;
325
+ font-weight: 700;
326
+ letter-spacing: 0.01em;
327
+ color: var(--ink);
328
+ }
329
+ .subtitle {
330
+ margin: 10px 0 0;
331
+ color: var(--muted);
332
+ line-height: 1.55;
333
+ font-size: 15px;
334
+ }
335
+ .meta-row,
336
+ .controls,
337
+ .chapter-evidence {
338
+ display: flex;
339
+ flex-wrap: wrap;
340
+ gap: 8px;
341
+ align-items: center;
342
+ }
343
+ .meta-row { margin-top: 8px; }
344
+ .meta-details {
345
+ margin-top: 10px;
346
+ color: var(--muted);
347
+ font-size: 12px;
348
+ }
349
+ .meta-details summary,
350
+ .retrieval-item summary,
351
+ .chapter-evidence summary {
352
+ list-style: none;
353
+ }
354
+ .meta-details summary::-webkit-details-marker,
355
+ .retrieval-item summary::-webkit-details-marker,
356
+ .chapter-evidence summary::-webkit-details-marker {
357
+ display: none;
358
+ }
359
+ .meta-details summary {
360
+ width: fit-content;
361
+ cursor: pointer;
362
+ color: var(--accent-deep);
363
+ font-weight: 650;
364
+ line-height: 1.4;
365
+ display: inline-flex;
366
+ align-items: center;
367
+ gap: 8px;
368
+ }
369
+ .meta-details summary::before,
370
+ .retrieval-item summary::before,
371
+ .chapter-evidence summary::before {
372
+ content: "▸";
373
+ display: inline-flex;
374
+ align-items: center;
375
+ justify-content: center;
376
+ width: 12px;
377
+ color: var(--accent-deep);
378
+ font-size: 11px;
379
+ transform-origin: 50% 50%;
380
+ transition: transform 120ms ease;
381
+ }
382
+ .meta-details[open] summary::before,
383
+ .retrieval-item[open] summary::before,
384
+ .chapter-evidence[open] summary::before {
385
+ transform: rotate(90deg);
386
+ }
387
+ .meta-pill,
388
+ .evidence-chip {
389
+ display: inline-flex;
390
+ width: fit-content;
391
+ border: 1px solid var(--line);
392
+ border-radius: 999px;
393
+ padding: 4px 8px;
394
+ background: rgba(255,255,255,0.86);
395
+ color: var(--muted);
396
+ font-size: 10.5px;
397
+ text-decoration: none;
398
+ }
399
+ .evidence-chip {
400
+ color: var(--accent);
401
+ background: var(--accent-soft);
402
+ border-color: rgba(239,123,67,0.22);
403
+ }
404
+ .evidence-chip.muted {
405
+ color: var(--muted);
406
+ background: #f8fafc;
407
+ }
408
+ .controls {
409
+ justify-content: space-between;
410
+ margin-top: 9px;
411
+ border-top: 1px solid var(--line);
412
+ padding-top: 9px;
413
+ background: transparent;
414
+ gap: 14px;
415
+ }
416
+ .button-row { display: flex; gap: 8px; flex-wrap: wrap; }
417
+ button {
418
+ border: 1px solid var(--line);
419
+ border-radius: 999px;
420
+ padding: 7px 10px;
421
+ background: rgba(255, 255, 255, 0.96);
422
+ color: var(--text);
423
+ font: inherit;
424
+ font-size: 14px;
425
+ cursor: pointer;
426
+ }
427
+ button:hover { border-color: var(--accent); }
428
+ button:disabled { color: var(--muted); cursor: not-allowed; opacity: 0.58; }
429
+ .progress-wrap {
430
+ min-width: 180px;
431
+ flex: 1;
432
+ }
433
+ .progress-meta {
434
+ display: flex;
435
+ justify-content: space-between;
436
+ gap: 10px;
437
+ color: var(--muted);
438
+ font-size: 12px;
439
+ margin-bottom: 6px;
440
+ }
441
+ .progress-track {
442
+ height: 7px;
443
+ border-radius: 999px;
444
+ background: #e5dfd4;
445
+ overflow: hidden;
446
+ }
447
+ .progress-bar {
448
+ height: 100%;
449
+ width: 0%;
450
+ border-radius: inherit;
451
+ background: var(--accent);
452
+ transition: width 180ms ease;
453
+ }
454
+ .toc-title {
455
+ margin: 0 0 12px;
456
+ font-size: 14px;
457
+ font-weight: 800;
458
+ color: var(--accent-deep);
459
+ }
460
+ .toc-subtitle {
461
+ margin: -4px 0 16px;
462
+ color: var(--muted);
463
+ line-height: 1.6;
464
+ font-size: 13px;
465
+ }
466
+ .outline-list,
467
+ .outline-list ol {
468
+ list-style: none;
469
+ margin: 0;
470
+ padding: 0;
471
+ }
472
+ .outline-list ol {
473
+ margin-left: 12px;
474
+ padding-left: 12px;
475
+ border-left: 1px solid var(--line);
476
+ }
477
+ .outline-branch summary {
478
+ list-style: none;
479
+ }
480
+ .outline-branch summary::-webkit-details-marker { display: none; }
481
+ .outline-jump {
482
+ display: grid;
483
+ grid-template-columns: 42px 1fr;
484
+ gap: 10px;
485
+ width: 100%;
486
+ text-align: left;
487
+ border-color: transparent;
488
+ background: transparent;
489
+ color: var(--text);
490
+ line-height: 1.45;
491
+ padding: 9px 8px;
492
+ border-radius: 12px;
493
+ border: 1px solid transparent;
494
+ cursor: pointer;
495
+ }
496
+ .outline-jump:hover {
497
+ border-color: rgba(239,123,67,0.18);
498
+ background: rgba(255, 246, 239, 0.78);
499
+ color: var(--accent-deep);
500
+ }
501
+ .outline-jump.active {
502
+ border-color: rgba(239,123,67,0.24);
503
+ background: linear-gradient(180deg, rgba(255,246,239,0.96), rgba(255,250,245,0.98));
504
+ color: var(--accent-deep);
505
+ }
506
+ .outline-jump.active .outline-number,
507
+ .outline-jump.active .outline-copy strong {
508
+ color: var(--accent-deep);
509
+ }
510
+ .outline-jump.active .outline-copy small {
511
+ color: #b27044;
512
+ }
513
+ .outline-number {
514
+ color: var(--amber);
515
+ font-family: var(--serif);
516
+ font-weight: 800;
517
+ }
518
+ .outline-copy strong,
519
+ .outline-copy small {
520
+ display: block;
521
+ }
522
+ .outline-copy strong {
523
+ font-weight: 700;
524
+ }
525
+ .outline-copy small {
526
+ margin-top: 3px;
527
+ color: var(--muted);
528
+ font-size: 12px;
529
+ }
530
+ .stat-grid {
531
+ display: grid;
532
+ grid-template-columns: 1fr;
533
+ gap: 8px;
534
+ margin-top: 16px;
535
+ }
536
+ .stat {
537
+ border: 1px solid var(--line);
538
+ border-radius: 14px;
539
+ padding: 10px;
540
+ background: var(--wash);
541
+ }
542
+ .stat-value {
543
+ font-family: var(--serif);
544
+ font-size: 26px;
545
+ font-weight: 700;
546
+ }
547
+ .stat-label {
548
+ color: var(--muted);
549
+ font-size: 12px;
550
+ margin-top: 2px;
551
+ }
552
+ .chapter {
553
+ padding: 38px 52px 54px;
554
+ min-height: 100%;
555
+ }
556
+ .chapter[hidden] { display: none; }
557
+ .chapter-kicker {
558
+ color: var(--accent-deep);
559
+ font-size: 13px;
560
+ font-weight: 700;
561
+ letter-spacing: 0.04em;
562
+ margin-bottom: 8px;
563
+ }
564
+ .chapter h2 {
565
+ margin: 0 0 12px;
566
+ font-family: var(--serif);
567
+ font-size: 38px;
568
+ line-height: 1.24;
569
+ font-weight: 600;
570
+ letter-spacing: 0.01em;
571
+ }
572
+ .chapter-summary {
573
+ margin: 0 0 20px;
574
+ color: var(--muted);
575
+ font-size: 15px;
576
+ line-height: 1.8;
577
+ max-width: 36em;
578
+ }
579
+ .chapter > p {
580
+ max-width: 42em;
581
+ }
582
+ .chapter p,
583
+ .learning-block p {
584
+ font-size: calc(17px * var(--reader-scale));
585
+ line-height: 1.85;
586
+ }
587
+ .learning-block {
588
+ margin: 34px 0 0;
589
+ border: 0;
590
+ border-top: 1px solid var(--line);
591
+ border-radius: 0;
592
+ padding: 22px 0 0;
593
+ background: transparent;
594
+ }
595
+ .learning-block h4 {
596
+ margin: 0 0 14px;
597
+ font-family: var(--serif);
598
+ font-size: 24px;
599
+ line-height: 1.3;
600
+ font-weight: 600;
601
+ letter-spacing: 0.01em;
602
+ }
603
+ .learning-block.retrieval,
604
+ .learning-block.worked,
605
+ .learning-block.visual {
606
+ border-top-color: rgba(239,123,67,0.2);
607
+ }
608
+ .learning-block.visual {
609
+ padding-top: 26px;
610
+ }
611
+ .visual-header {
612
+ display: grid;
613
+ gap: 6px;
614
+ margin-bottom: 18px;
615
+ }
616
+ .visual-kicker {
617
+ color: var(--accent-deep);
618
+ font-size: 12px;
619
+ font-weight: 800;
620
+ letter-spacing: 0.12em;
621
+ }
622
+ .visual-header h4 {
623
+ margin: 0;
624
+ font-size: 30px;
625
+ line-height: 1.28;
626
+ font-weight: 600;
627
+ }
628
+ .visual-grid {
629
+ display: grid;
630
+ gap: 26px;
631
+ }
632
+ .visual-grid.has-image {
633
+ grid-template-columns: minmax(0, 1.28fr) minmax(240px, 320px);
634
+ align-items: start;
635
+ }
636
+ .visual-copy {
637
+ display: grid;
638
+ gap: 0;
639
+ border-left: 1px solid var(--line);
640
+ padding-left: 22px;
641
+ }
642
+ .visual-note {
643
+ border: 0;
644
+ border-radius: 0;
645
+ background: transparent;
646
+ padding: 0 0 16px;
647
+ }
648
+ .visual-note + .visual-note {
649
+ border-top: 1px solid rgba(121, 151, 194, 0.22);
650
+ padding-top: 16px;
651
+ }
652
+ .visual-label {
653
+ color: var(--accent-deep);
654
+ font-size: 11px;
655
+ font-weight: 700;
656
+ letter-spacing: 0.12em;
657
+ margin-bottom: 8px;
658
+ }
659
+ .visual-note p {
660
+ margin: 0;
661
+ }
662
+ .visual-takeaways {
663
+ margin: 0;
664
+ padding-left: 20px;
665
+ }
666
+ .visual-figure {
667
+ margin: 0;
668
+ border: 1px solid rgba(121, 151, 194, 0.2);
669
+ border-radius: 16px;
670
+ overflow: hidden;
671
+ background: rgba(255,255,255,0.98);
672
+ box-shadow: 0 18px 42px rgba(91, 126, 177, 0.08);
673
+ }
674
+ .visual-figure img {
675
+ display: block;
676
+ width: 100%;
677
+ height: auto;
678
+ background:
679
+ linear-gradient(90deg, rgba(95, 129, 181, 0.07) 0 1px, transparent 1px 100%),
680
+ linear-gradient(rgba(95, 129, 181, 0.07) 0 1px, transparent 1px 100%),
681
+ #f8fbff;
682
+ background-size: 24px 24px, 24px 24px, auto;
683
+ }
684
+ .visual-figure figcaption {
685
+ padding: 12px 14px 14px;
686
+ border-top: 1px solid rgba(121, 151, 194, 0.16);
687
+ color: var(--muted);
688
+ font-size: 12px;
689
+ line-height: 1.6;
690
+ }
691
+ .retrieval-item {
692
+ border-top: 1px solid var(--line);
693
+ padding: 16px 0;
694
+ }
695
+ .retrieval-item:first-of-type { border-top: 0; }
696
+ .retrieval-item summary {
697
+ cursor: pointer;
698
+ font-weight: 650;
699
+ line-height: 1.6;
700
+ display: flex;
701
+ gap: 10px;
702
+ align-items: flex-start;
703
+ }
704
+ .retrieval-item summary span {
705
+ display: inline-flex;
706
+ color: var(--accent);
707
+ font-family: var(--mono);
708
+ font-size: 11px;
709
+ min-width: 24px;
710
+ padding-top: 2px;
711
+ }
712
+ .retrieval-hint,
713
+ .retrieval-answer {
714
+ color: var(--muted);
715
+ line-height: 1.7;
716
+ margin-top: 8px;
717
+ margin-left: 34px;
718
+ }
719
+ .worked-item {
720
+ padding: 18px 0;
721
+ border-top: 1px solid var(--line);
722
+ }
723
+ .worked-item:first-of-type {
724
+ padding-top: 6px;
725
+ border-top: 0;
726
+ }
727
+ .worked-title {
728
+ font-family: var(--serif);
729
+ font-size: 24px;
730
+ font-weight: 600;
731
+ line-height: 1.35;
732
+ }
733
+ .worked-principle {
734
+ color: var(--muted);
735
+ line-height: 1.7;
736
+ margin-top: 12px;
737
+ padding-top: 12px;
738
+ border-top: 1px solid rgba(239,123,67,0.16);
739
+ }
740
+ ol,
741
+ ul {
742
+ margin: 10px 0 0;
743
+ padding-left: 20px;
744
+ line-height: 1.75;
745
+ }
746
+ .chapter-evidence {
747
+ display: block;
748
+ margin-top: 24px;
749
+ padding-top: 16px;
750
+ border-top: 1px solid var(--line);
751
+ }
752
+ .chapter-evidence summary {
753
+ cursor: pointer;
754
+ display: flex;
755
+ flex-wrap: wrap;
756
+ align-items: center;
757
+ gap: 10px;
758
+ color: var(--muted);
759
+ }
760
+ .evidence-summary-title {
761
+ color: var(--accent-deep);
762
+ font-size: 13px;
763
+ font-weight: 700;
764
+ letter-spacing: 0.04em;
765
+ }
766
+ .evidence-summary-count {
767
+ color: var(--muted);
768
+ font-size: 12px;
769
+ }
770
+ .evidence-mini-list {
771
+ display: grid;
772
+ gap: 0;
773
+ margin-top: 12px;
774
+ }
775
+ .evidence-mini-card {
776
+ border: 0;
777
+ border-top: 1px solid rgba(121, 151, 194, 0.16);
778
+ border-radius: 0;
779
+ background: transparent;
780
+ padding: 12px 0;
781
+ }
782
+ .evidence-mini-card:first-child {
783
+ border-top: 0;
784
+ padding-top: 0;
785
+ }
786
+ .evidence-mini-card strong,
787
+ .evidence-mini-card span {
788
+ display: block;
789
+ }
790
+ .evidence-mini-card strong {
791
+ font-weight: 650;
792
+ font-size: 14px;
793
+ line-height: 1.5;
794
+ }
795
+ .evidence-mini-card span {
796
+ color: var(--muted);
797
+ font-family: var(--mono);
798
+ font-size: 11px;
799
+ letter-spacing: 0.02em;
800
+ margin-top: 4px;
801
+ }
802
+ .evidence-mini-card p {
803
+ margin: 6px 0 0;
804
+ color: var(--muted);
805
+ font-size: 13px;
806
+ line-height: 1.6;
807
+ }
808
+ .empty-reader {
809
+ margin: 38px 52px 54px;
810
+ padding: 28px;
811
+ border: 1px dashed var(--line-strong);
812
+ border-radius: 16px;
813
+ background: var(--wash);
814
+ }
815
+ .empty-reader h2 {
816
+ margin: 0 0 12px;
817
+ font-family: var(--serif);
818
+ font-size: 34px;
819
+ line-height: 1.2;
820
+ }
821
+ .empty-reader p {
822
+ margin: 0;
823
+ color: var(--muted);
824
+ font-size: 17px;
825
+ line-height: 1.8;
826
+ }
827
+ .empty-steps {
828
+ margin: 18px 0 0;
829
+ padding-left: 22px;
830
+ color: var(--muted);
831
+ line-height: 1.8;
832
+ }
833
+ .empty-steps li + li {
834
+ margin-top: 6px;
835
+ }
836
+ .empty-paths {
837
+ display: grid;
838
+ gap: 10px;
839
+ margin-top: 18px;
840
+ }
841
+ .empty-paths div {
842
+ display: grid;
843
+ gap: 4px;
844
+ border: 1px solid var(--line);
845
+ border-radius: 12px;
846
+ padding: 10px 12px;
847
+ background: #fffefa;
848
+ }
849
+ .empty-paths strong {
850
+ color: var(--accent-deep);
851
+ font-size: 13px;
852
+ }
853
+ .empty-paths span {
854
+ color: var(--muted);
855
+ font-family: var(--mono);
856
+ font-size: 12px;
857
+ overflow-wrap: anywhere;
858
+ }
859
+ @media (max-width: 1120px) {
860
+ body { overflow: auto; }
861
+ .shell { grid-template-columns: 1fr; height: auto; min-height: 100vh; padding: 12px; }
862
+ .side-panel {
863
+ position: static;
864
+ max-height: none;
865
+ }
866
+ .reader { height: auto; }
867
+ .reader-scroll { height: auto; overflow: visible; }
868
+ .chapter { min-height: auto; padding: 24px 20px 30px; }
869
+ .visual-grid.has-image { grid-template-columns: 1fr; }
870
+ .visual-copy {
871
+ border-left: 0;
872
+ border-top: 1px solid var(--line);
873
+ padding-left: 0;
874
+ padding-top: 18px;
875
+ }
876
+ }
877
+ @media (max-width: 700px) {
878
+ .reader-header { padding: 18px 20px 12px; }
879
+ h1 { font-size: 30px; }
880
+ .chapter h2 { font-size: 28px; }
881
+ .learning-block h4,
882
+ .visual-header h4,
883
+ .worked-title {
884
+ font-size: 24px;
885
+ }
886
+ .stat-grid { grid-template-columns: 1fr; }
887
+ .controls { display: grid; gap: 12px; }
888
+ .chapter { padding: 30px 22px 38px; }
889
+ }
890
+ </style>
891
+ </head>
892
+ <body>
893
+ <main class="shell">
894
+ <aside class="side-panel">
895
+ <p class="toc-title">书籍大纲</p>
896
+ <p class="toc-subtitle">最多三层展开。先读章名,再进入心法、练习与示例。</p>
897
+ <ol class="outline-list">
898
+ ${outline.length > 0 ? outline.map((node, index) => formatLearningOutlineNode(node, `${index + 1}`, initialChapterId)).join('\n') : '<li><span class="outline-jump"><span class="outline-number">0</span><span class="outline-copy"><strong>证据包待写作</strong><small>正文完成后显示目录</small></span></span></li>'}
899
+ </ol>
900
+ </aside>
901
+
902
+ <article class="reader">
903
+ <header class="reader-header">
904
+ <p class="eyebrow">OpenPrd 复盘学习 · ${escapeHtml(content.genre?.label ?? '默认题材')}</p>
905
+ <h1>${escapeHtml(title)}</h1>
906
+ <p class="subtitle">${escapeHtml(content.subtitle ?? '')}</p>
907
+ <details class="meta-details">
908
+ <summary>生成信息</summary>
909
+ <div class="meta-row">
910
+ <span class="meta-pill">topic: ${escapeHtml(content.topic ?? '未指定')}</span>
911
+ <span class="meta-pill">genre: ${escapeHtml(content.genre?.id ?? 'unknown')}</span>
912
+ <span class="meta-pill">风格: ${escapeHtml(content.stylePromptPack?.styleId ?? packageMeta?.styleId ?? 'default')}</span>
913
+ <span class="meta-pill">trigger: ${escapeHtml(packageMeta?.trigger ?? content.trigger ?? 'manual')}</span>
914
+ </div>
915
+ </details>
916
+ <div class="controls">
917
+ <div class="button-row">
918
+ <button type="button" id="prevChapter" disabled>上一章</button>
919
+ <button type="button" id="nextChapter"${chapters.length <= 1 ? ' disabled' : ''}>下一章</button>
920
+ <button type="button" id="smallerText">A-</button>
921
+ <button type="button" id="largerText">A+</button>
922
+ </div>
923
+ <div class="progress-wrap">
924
+ <div class="progress-meta">
925
+ <span id="progressTitle">阅读进度</span>
926
+ <span id="progressText">${chapters.length > 0 ? `1/${chapters.length}` : '0/0'}</span>
927
+ </div>
928
+ <div class="progress-track"><div class="progress-bar" id="progressBar" style="width: ${initialProgressPercent}%"></div></div>
929
+ </div>
930
+ </div>
931
+ </header>
932
+ <div class="reader-scroll" tabindex="0" aria-label="OpenPrd 复盘学习阅读器 · 当前章节正文">
933
+ ${chapters.length > 0 ? chapters.map((chapter, index) => formatLearningChapter(chapter, index, sourcesById)).join('\n') : formatLearningEmptyState(content, packageMeta, evidenceManifest)}
934
+ </div>
935
+ </article>
936
+ </main>
937
+ <script>
938
+ const scrollRoot = document.querySelector('.reader-scroll');
939
+ const chapters = Array.from(document.querySelectorAll('.chapter'));
940
+ const outlineItems = Array.from(document.querySelectorAll('[data-target-id]'));
941
+ const prevButton = document.getElementById('prevChapter');
942
+ const nextButton = document.getElementById('nextChapter');
943
+ const progressBar = document.getElementById('progressBar');
944
+ const progressText = document.getElementById('progressText');
945
+ let activeIndex = 0;
946
+ let fontScale = Number(localStorage.getItem('openprd-learning-font-scale') || '1');
947
+
948
+ function clamp(value, min, max) {
949
+ return Math.max(min, Math.min(max, value));
950
+ }
951
+
952
+ function applyFontScale() {
953
+ fontScale = clamp(fontScale, 0.9, 1.25);
954
+ document.documentElement.style.setProperty('--reader-scale', String(fontScale));
955
+ localStorage.setItem('openprd-learning-font-scale', String(fontScale));
956
+ }
957
+
958
+ function setActive(index, shouldScroll = false) {
959
+ if (chapters.length === 0) return;
960
+ activeIndex = clamp(index, 0, chapters.length - 1);
961
+ chapters.forEach((chapter, chapterIndex) => {
962
+ const isActive = chapterIndex === activeIndex;
963
+ chapter.hidden = !isActive;
964
+ chapter.classList.toggle('active', isActive);
965
+ });
966
+ const activeChapterId = chapters[activeIndex].id;
967
+ outlineItems.forEach((item) => item.classList.toggle('active', item.dataset.targetId === activeChapterId));
968
+ prevButton.disabled = activeIndex === 0;
969
+ nextButton.disabled = activeIndex === chapters.length - 1;
970
+ progressText.textContent = String(activeIndex + 1) + '/' + String(chapters.length);
971
+ progressBar.style.width = String(((activeIndex + 1) / chapters.length) * 100) + '%';
972
+ if (shouldScroll) {
973
+ scrollRoot?.scrollTo({ top: 0, behavior: 'smooth' });
974
+ }
975
+ }
976
+
977
+ function scrollToReaderTarget(target) {
978
+ if (!target || !scrollRoot) return;
979
+ const rootTop = scrollRoot.getBoundingClientRect().top;
980
+ const targetTop = target.getBoundingClientRect().top;
981
+ scrollRoot.scrollTo({
982
+ top: scrollRoot.scrollTop + targetTop - rootTop - 18,
983
+ behavior: 'smooth',
984
+ });
985
+ }
986
+
987
+ outlineItems.forEach((item) => {
988
+ item.addEventListener('click', () => {
989
+ const target = document.getElementById(item.dataset.targetId);
990
+ if (!target) return;
991
+ const chapterIndex = chapters.findIndex((chapter) => chapter.id === target.id || chapter.contains(target));
992
+ if (chapterIndex >= 0) setActive(chapterIndex, false);
993
+ scrollToReaderTarget(target);
994
+ });
995
+ });
996
+ prevButton.addEventListener('click', () => setActive(activeIndex - 1, true));
997
+ nextButton.addEventListener('click', () => setActive(activeIndex + 1, true));
998
+ document.getElementById('smallerText').addEventListener('click', () => {
999
+ fontScale -= 0.05;
1000
+ applyFontScale();
1001
+ });
1002
+ document.getElementById('largerText').addEventListener('click', () => {
1003
+ fontScale += 0.05;
1004
+ applyFontScale();
1005
+ });
1006
+ document.addEventListener('keydown', (event) => {
1007
+ if (event.key === 'ArrowRight' || event.key === 'PageDown') setActive(activeIndex + 1, true);
1008
+ if (event.key === 'ArrowLeft' || event.key === 'PageUp') setActive(activeIndex - 1, true);
1009
+ });
1010
+
1011
+ applyFontScale();
1012
+ setActive(0, false);
1013
+ </script>
1014
+ </body>
1015
+ </html>`;
1016
+ }
1017
+
1018
+ export function learningPackagePaths(ws, packageId) {
1019
+ const dir = cjoin(ws.paths.learningArchiveDir, slugify(packageId, 'learning-package'));
1020
+ return {
1021
+ dir,
1022
+ readerHtml: cjoin(dir, 'reader.html'),
1023
+ assetsDir: cjoin(dir, 'assets'),
1024
+ packageJson: cjoin(dir, 'learning-package.json'),
1025
+ contentJson: cjoin(dir, 'learning-content.json'),
1026
+ contentMarkdown: cjoin(dir, 'learning-content.md'),
1027
+ evidenceManifest: cjoin(dir, 'evidence-manifest.json'),
1028
+ agentContext: cjoin(dir, 'agent-context.json'),
1029
+ agentPrompt: cjoin(dir, 'agent-prompt.md'),
1030
+ };
1031
+ }