@haaaiawd/loom 1.2.2 → 2.0.0
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/CHANGELOG.md +11 -58
- package/CONTRIBUTING.md +37 -0
- package/EVIL_EVAL.md +112 -0
- package/README.md +193 -446
- package/README.zh-CN.md +174 -0
- package/SECURITY.md +11 -0
- package/cli/bin/loom.js +170 -995
- package/cli/src/protocol.js +367 -0
- package/cli/src/store.js +626 -0
- package/design.md +194 -0
- package/docs/PROMPT_CATALOG.md +99 -0
- package/docs/RELEASE_CHECKLIST.md +53 -0
- package/docs/UX_FLOW.md +171 -0
- package/docs/brand/loom-mark.svg +18 -0
- package/docs/brand/loom-readme-header.svg +34 -0
- package/docs/brand/loom-readme-header.zh-CN.svg +29 -0
- package/docs/loom-eval-loop.drawio +21 -0
- package/docs/loom-eval-loop.svg +56 -0
- package/docs/loom-production-loop.drawio +41 -0
- package/docs/loom-production-loop.svg +92 -0
- package/package.json +43 -40
- package/EXTERNAL_ACQUISITION_DESIGN.md +0 -143
- package/cli/help/asset.md +0 -36
- package/cli/help/atelier.md +0 -37
- package/cli/help/capability.md +0 -73
- package/cli/help/concepts.md +0 -103
- package/cli/help/doctor.md +0 -73
- package/cli/help/expertise.md +0 -51
- package/cli/help/loop.md +0 -134
- package/cli/help/patch.md +0 -33
- package/cli/help/preview.md +0 -60
- package/cli/help/proposals.md +0 -21
- package/cli/help/version.md +0 -136
- package/cli/help/workflow.md +0 -114
- package/cli/src/activate.js +0 -473
- package/cli/src/asset-library.js +0 -384
- package/cli/src/atelier.js +0 -331
- package/cli/src/auto.js +0 -116
- package/cli/src/capability-graph.js +0 -430
- package/cli/src/capability-proposals.js +0 -225
- package/cli/src/diagnostics.js +0 -766
- package/cli/src/expertise-pack.js +0 -336
- package/cli/src/guide.js +0 -495
- package/cli/src/help.js +0 -41
- package/cli/src/init.js +0 -187
- package/cli/src/intent-draft.js +0 -303
- package/cli/src/intent-map.js +0 -747
- package/cli/src/patch.js +0 -214
- package/cli/src/philosophy.js +0 -331
- package/cli/src/preview-prompt.md +0 -337
- package/cli/src/preview.js +0 -73
- package/cli/src/shared/intent-ref.js +0 -38
- package/cli/src/shared/md-utils.js +0 -125
- package/cli/src/shared/paths.js +0 -73
- package/cli/src/shared/proof-reference.js +0 -19
- package/cli/src/shared/verification-method.js +0 -32
- package/cli/src/verify.js +0 -394
- package/cli/src/version.js +0 -134
- package/dimensions/AUTHORSHIP.md +0 -45
- package/dimensions/PART_DECOMPOSITION.md +0 -42
- package/dimensions/SEARCH_METHODOLOGY.md +0 -101
- package/dimensions/examples/AGENT_SYSTEM/README.md +0 -219
- package/dimensions/examples/CLI_TOOL/README.md +0 -163
- package/dimensions/universal/COLLABORATION_PHILOSOPHY.md +0 -28
- package/dimensions/universal/ENGINEERING_CREED.md +0 -30
- package/dimensions/universal/PRODUCT_PHILOSOPHY.md +0 -32
- package/meta/BASELINE.md +0 -91
- package/meta/INTENT_LOOP.md +0 -296
- package/meta/PHILOSOPHY_WEAVER.md +0 -110
- package/meta/ROLE_ACTIVATION.md +0 -114
- package/roles/architect.md +0 -86
- package/roles/forge.md +0 -112
- package/roles/keeper.md +0 -113
- package/roles/visionary.md +0 -57
- package/templates/ASSET_LIBRARY_MANIFEST_TEMPLATE.json +0 -10
- package/templates/ATELIER_RECORD_TEMPLATE.json +0 -48
- package/templates/CAPABILITY_BRIEF_TEMPLATE.md +0 -35
- package/templates/CAPABILITY_GRAPH_TEMPLATE.json +0 -11
- package/templates/EXPERTISE_PACK_TEMPLATE.json +0 -22
- package/templates/INTENT_MAP_TEMPLATE.json +0 -85
- package/templates/PHILOSOPHY_TEMPLATE.md +0 -44
- package/templates/VISION_TEMPLATE.md +0 -44
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
# LOOM Preview 生成提示词
|
|
2
|
-
|
|
3
|
-
你的任务:读 `.loom/` 目录下当前版本的所有文件,把信息**拆解成关键要点**,用最合适的视觉和交互手段呈现成一份 HTML 页面,写到 `loom-preview.html`。
|
|
4
|
-
|
|
5
|
-
## 核心原则:信息密度靠视觉手段,不靠文字堆砌
|
|
6
|
-
|
|
7
|
-
类比 PPT——PPT 不是把报告文字排成幻灯片,是用图表、图示、对比、交互把信息密度压缩到人类一眼能消化的程度。能用图就不表,能交互就不静态,能一句话就不写一段。
|
|
8
|
-
|
|
9
|
-
### 错误 vs 正确
|
|
10
|
-
|
|
11
|
-
| 错误(文字堆砌) | 正确(视觉手段) |
|
|
12
|
-
|---|---|
|
|
13
|
-
| 哲学全文渲染成 HTML | 北极星一句话 + 信念用图标网格 + 反模式用红色警告卡 |
|
|
14
|
-
| Intent 列表表格 | SVG 依赖图,节点颜色=状态,一眼看出阻塞链 |
|
|
15
|
-
| 验证记录 JSON 列表 | 时间轴,通过绿色偏离红色,点击节点看证据 |
|
|
16
|
-
| 进度用文字描述 | 环形进度图 + 大数字仪表盘 |
|
|
17
|
-
| 状态分布用表格 | 堆叠条形图,一段横条按状态颜色分段 |
|
|
18
|
-
| 决策原则用列表 | 对比表,两列并排突出差异 |
|
|
19
|
-
| 意图叙事全文展示 | 卡片网格,每张卡片一句话,点击展开详情 |
|
|
20
|
-
|
|
21
|
-
### 拆解粒度
|
|
22
|
-
|
|
23
|
-
每个信息块回答一个问题,人类 3 秒内能消化:
|
|
24
|
-
- 产品为什么存在?→ 北极星一句话
|
|
25
|
-
- 产品信什么?→ 3-5 个图标 + 关键句
|
|
26
|
-
- 什么绝对不做?→ 红色警告卡,每张一句
|
|
27
|
-
- 项目推进到哪了?→ 环形进度图 + 数字仪表盘
|
|
28
|
-
- 谁阻塞谁?→ SVG 依赖图
|
|
29
|
-
- 这个 Intent 为什么存在?→ 一句话叙事
|
|
30
|
-
- 验证历史怎么样?→ 时间轴
|
|
31
|
-
|
|
32
|
-
如果一个块需要读超过 3 行文字才能理解,它太长了——换一种视觉形式。
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## 去读哪些文件
|
|
37
|
-
|
|
38
|
-
当前版本目录在 `.loom/current` 指针里(通常是 `.loom/v1/`)。读:
|
|
39
|
-
|
|
40
|
-
| 文件 | 拆解出什么 |
|
|
41
|
-
|---|---|
|
|
42
|
-
| `00_PHILOSOPHY/*.md` | 北极星、核心信念、反模式、决策原则 |
|
|
43
|
-
| `01_VISION.md` | 北极星、问题空间、意图叙事、不做什么 |
|
|
44
|
-
| `02_ARCHITECTURE.md` | 关键决策、trade-offs |
|
|
45
|
-
| `04_INTENT_MAP.json` | 依赖关系、状态、完成/质量契约、专业能力需求 |
|
|
46
|
-
| `verifications/*.json` | 验证时间轴、基础维度、按需的质量维度与 Quality Proof |
|
|
47
|
-
| `06_CHANGELOG.json` | Patch 历史(唯一权威来源);不要从生成的 Markdown 反推数据 |
|
|
48
|
-
| `06_CHANGELOG.md` | JSON 的确定性只读投影,仅用于交叉检查 |
|
|
49
|
-
|
|
50
|
-
文件还是模板(含 `<!-- LOOM_TEMPLATE -->` 标记)时,对应区域显示提示:"等待 {角色} 填充"。
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
## 一、视觉手段库——什么信息用什么呈现
|
|
55
|
-
|
|
56
|
-
不同信息适合不同视觉手段。不要什么都用卡片或列表——选最能压缩信息密度的方式。
|
|
57
|
-
|
|
58
|
-
### 图表类(数据可视化)
|
|
59
|
-
|
|
60
|
-
**环形进度图** — 适合:整体完成度
|
|
61
|
-
一个圆环,填充比例 = completed/total。中心显示百分比。比横条进度条更紧凑、更视觉化。纯 SVG 实现。
|
|
62
|
-
|
|
63
|
-
**堆叠条形图** — 适合:状态分布
|
|
64
|
-
一段横条,按状态颜色分段(完成绿/进行中黄/待执行灰/阻塞红)。一段条形展示所有状态比例,比 4 个数字卡片更紧凑。
|
|
65
|
-
|
|
66
|
-
**柱状图** — 适合:各 Intent 的验证轮数、各系统的 Intent 数量
|
|
67
|
-
SVG 矩形,高度=数值。适合比较多个项的数量。
|
|
68
|
-
|
|
69
|
-
**燃尽图风格折线** — 适合:剩余 Intent 随时间变化(如果有历史数据)
|
|
70
|
-
SVG 折线,横轴时间,纵轴剩余数。
|
|
71
|
-
|
|
72
|
-
### 图示类(关系和结构)
|
|
73
|
-
|
|
74
|
-
**SVG 依赖图** — 适合:Intent 之间的依赖关系
|
|
75
|
-
节点是 Intent,边是 depends_on。拓扑排序布局,无依赖的在左,依赖链向右。节点颜色按状态编码。节点可点击,点击后侧边面板显示详情。
|
|
76
|
-
|
|
77
|
-
**热力图网格** — 适合:Intent × 验证轮次的通过/偏离矩阵
|
|
78
|
-
行是 Intent,列是验证轮次,格子颜色=通过(绿)/偏离(红)/未验证(灰)。一眼看出哪个 Intent 反复偏离。
|
|
79
|
-
|
|
80
|
-
**树状图** — 适合:架构的系统层级、Intent 的依赖树
|
|
81
|
-
展开/折叠的树形结构,点击节点展开子项。
|
|
82
|
-
|
|
83
|
-
### 交互类(探索和筛选)
|
|
84
|
-
|
|
85
|
-
**过滤面板** — 适合:Intent 列表、验证记录
|
|
86
|
-
复选框按状态过滤(全部/待执行/进行中/完成/阻塞),输入框模糊搜索。过滤后实时更新视图。
|
|
87
|
-
|
|
88
|
-
**点击下钻** — 适合:从概览到详情
|
|
89
|
-
概览页点击某个 Intent → 展开详情面板或跳到详情区。不在一页摊开所有详情,按需展开。
|
|
90
|
-
|
|
91
|
-
**Hover 工具提示** — 适合:图表上的额外信息
|
|
92
|
-
鼠标悬停在依赖图节点上 → 显示 tooltip(ID + 状态 + 一句话叙事)。不点击就能预览。
|
|
93
|
-
|
|
94
|
-
**Tab 切换** — 适合:大板块切换
|
|
95
|
-
概览 / Intent Map / 哲学 / 愿景 / 架构。所有内容先渲染堆叠,JS 切换显示。
|
|
96
|
-
|
|
97
|
-
**可折叠面板** — 适合:分层信息
|
|
98
|
-
默认折叠显示标题,点击展开内容。用 `max-height` 过渡,不要 `display: none`。
|
|
99
|
-
|
|
100
|
-
### 布局类(组织和对比)
|
|
101
|
-
|
|
102
|
-
**仪表盘** — 适合:关键指标概览
|
|
103
|
-
大数字 + 颜色编码。数字 24-32px,标签 12-13px。一眼看到状态。
|
|
104
|
-
|
|
105
|
-
**对比表** — 适合:trade-offs、做什么 vs 不做什么
|
|
106
|
-
两列或三列并排,突出差异。每列一个方向,行是对比维度。
|
|
107
|
-
|
|
108
|
-
**卡片网格** — 适合:核心信念、意图叙事
|
|
109
|
-
每张卡片一个要点,标题 + 一句话。网格排列,大小一致。卡片内不超过 3 行。
|
|
110
|
-
|
|
111
|
-
**时间轴** — 适合:验证历史、项目演进
|
|
112
|
-
横向或纵向,每个节点一个事件。通过绿色偏离红色,附证据摘要。
|
|
113
|
-
|
|
114
|
-
**看板列** — 适合:按状态分组的 Intent
|
|
115
|
-
四列:待执行 / 进行中 / 完成 / 阻塞。每列下面是该状态的 Intent 卡片。一眼看出分布。
|
|
116
|
-
|
|
117
|
-
**清单** — 适合:反模式、不做什么、决策原则
|
|
118
|
-
每条一行 + 一句话理由。反模式用红色警告样式。
|
|
119
|
-
|
|
120
|
-
### 标记类(状态和强调)
|
|
121
|
-
|
|
122
|
-
**状态徽章** — 适合:Intent 状态
|
|
123
|
-
颜色 + 图标 + 文字(不只靠颜色,色盲友好):
|
|
124
|
-
- 完成 = 绿色 + ✓
|
|
125
|
-
- 进行中 = 黄色 + ◐
|
|
126
|
-
- 待执行 = 灰色 + ○
|
|
127
|
-
- 阻塞 = 红色 + ✕
|
|
128
|
-
- 需复审 = 蓝色 + ?
|
|
129
|
-
|
|
130
|
-
**高亮引用** — 适合:北极星、关键宣言
|
|
131
|
-
大字号 + 左侧色条 + 引用样式。视觉上从周围内容跳出来。
|
|
132
|
-
|
|
133
|
-
---
|
|
134
|
-
|
|
135
|
-
## 二、页面结构
|
|
136
|
-
|
|
137
|
-
### 顶部:一屏概览
|
|
138
|
-
|
|
139
|
-
人类打开第一眼看到全局:
|
|
140
|
-
|
|
141
|
-
- 项目名 + 版本号
|
|
142
|
-
- 北极星一句话(高亮引用样式,最醒目)
|
|
143
|
-
- **环形进度图**:完成 X/Y(百分比)
|
|
144
|
-
- **堆叠条形图**:状态分布(一段条形展示所有状态比例)
|
|
145
|
-
- **数字仪表盘**:进行中 N、阻塞 M(大数字 + 颜色)
|
|
146
|
-
|
|
147
|
-
### Intent Map 区域
|
|
148
|
-
|
|
149
|
-
项目推进到哪了——用多种手段呈现不同维度:
|
|
150
|
-
|
|
151
|
-
- **SVG 依赖图**:依赖链和阻塞点
|
|
152
|
-
- **看板列**:按状态分组的 Intent 卡片(待执行/进行中/完成/阻塞)
|
|
153
|
-
- **过滤面板**:状态过滤 + 模糊搜索(Intent 多时)
|
|
154
|
-
- **点击下钻**:点击节点或卡片 → 侧边详情面板(叙事、完成/质量契约、能力需求、验证历史)
|
|
155
|
-
|
|
156
|
-
### 哲学区域
|
|
157
|
-
|
|
158
|
-
"我们信什么"——拆解成视觉块:
|
|
159
|
-
|
|
160
|
-
- **北极星**:高亮引用,一句话
|
|
161
|
-
- **核心信念**:卡片网格,每张一个图标 + 关键句
|
|
162
|
-
- **不可妥协的价值**:清单,每条一句话 + 一句话理由
|
|
163
|
-
- **反模式**:红色警告卡,每张一句
|
|
164
|
-
- **决策原则**:对比表或清单,遇到冲突时怎么取舍
|
|
165
|
-
- **灵感来源**:哲学文档里的"灵感来源"章节——展示参考的艺术家、机构、流派、著作。这是 LOOM 哲学织造的核心机制:从真实思想体系萃取核心思想。每个来源用卡片或标签展示:名字(如 Dieter Rams / Stripe / Unix Philosophy / Anthropic)+ 一句话说明"从这个体系萃取了什么原则" + 来源引用(著作/URL)。这些名字是"思想压缩包的索引"——人类看到名字就能激活对这套哲学的理解。不要把灵感来源藏在正文里,要作为独立视觉块展示。
|
|
166
|
-
|
|
167
|
-
### 愿景区域
|
|
168
|
-
|
|
169
|
-
"我们要去哪"——拆解:
|
|
170
|
-
|
|
171
|
-
- **北极星**:和哲学呼应
|
|
172
|
-
- **问题空间**:一段话
|
|
173
|
-
- **意图叙事**:卡片网格,每个意图一张卡片,一句话"为什么存在",点击展开详情
|
|
174
|
-
- **不做什么**:清单,和"做什么"同样醒目
|
|
175
|
-
|
|
176
|
-
### 架构区域(如果有)
|
|
177
|
-
|
|
178
|
-
"怎么实现"——拆解:
|
|
179
|
-
|
|
180
|
-
- **关键决策对比表**:每个决策两列对比(选了什么 / 放弃了什么)
|
|
181
|
-
- **trade-offs 清单**:每条一句话
|
|
182
|
-
|
|
183
|
-
### 验证历史区域(如果有)
|
|
184
|
-
|
|
185
|
-
"质量怎么样"——拆解:
|
|
186
|
-
|
|
187
|
-
- **时间轴**:每次验证一个节点,通过/偏离用颜色区分,点击看证据
|
|
188
|
-
- **热力图网格**(Intent 多时):Intent × 验证轮次矩阵,一眼看出哪个反复偏离
|
|
189
|
-
- **偏离清单**:哪些 Intent 偏离了,证据是什么
|
|
190
|
-
|
|
191
|
-
### Patch 历史区域(如果有)
|
|
192
|
-
|
|
193
|
-
- 从 `06_CHANGELOG.json` 展示 Patch 时间轴:摘要、原因、影响的 Intent、文件和已记录的验证结果
|
|
194
|
-
- 明确区分 Patch 与 Intent 验证;`06_CHANGELOG.md` 是生成文件,不是另一份真相
|
|
195
|
-
|
|
196
|
-
### 底部
|
|
197
|
-
|
|
198
|
-
- 提醒:这是只读投影,修改请编辑源文件后运行 `loom preview` 重新生成
|
|
199
|
-
- 可选:一个"复制项目状态"按钮
|
|
200
|
-
|
|
201
|
-
---
|
|
202
|
-
|
|
203
|
-
## 三、符合人类直觉
|
|
204
|
-
|
|
205
|
-
### 视觉权重
|
|
206
|
-
|
|
207
|
-
最重要的信息最大、最醒目。次要信息小、灰、退后:
|
|
208
|
-
- 北极星 → 全页最大字号
|
|
209
|
-
- 进度数字 → 大数字 + 颜色
|
|
210
|
-
- Intent ID → 中等字号,等宽字体
|
|
211
|
-
- 完成契约正文 → 小字,可展开才显示;质量契约与 Quality Proof 使用相邻但可区分的区域
|
|
212
|
-
- 辅助说明 → 最小字,灰色
|
|
213
|
-
|
|
214
|
-
不要所有文字一样大。人类靠大小判断重要性。
|
|
215
|
-
|
|
216
|
-
### 渐进披露
|
|
217
|
-
|
|
218
|
-
不要一次把所有信息摊开。先给摘要,想看详情再展开:
|
|
219
|
-
- Intent 卡片默认折叠,只显示 ID + 状态 + 一句话
|
|
220
|
-
- 点击展开后显示:意图叙事、验收契约、哲学锚点、验证历史
|
|
221
|
-
- 图表上 Hover 显示 tooltip 预览,点击才展开完整详情
|
|
222
|
-
|
|
223
|
-
### 信息分组
|
|
224
|
-
|
|
225
|
-
相关信息放一起,不相关的用空间分隔:
|
|
226
|
-
- 进度概览自成一个区块
|
|
227
|
-
- Intent 依赖图 + 详情面板自成一个区块
|
|
228
|
-
- 哲学、愿景、架构各自独立区块
|
|
229
|
-
- 区块之间用足够留白分隔(32-48px),不要挤
|
|
230
|
-
|
|
231
|
-
---
|
|
232
|
-
|
|
233
|
-
## 四、视觉设计原则
|
|
234
|
-
|
|
235
|
-
参考 Anthropic 和 CopilotKit 的 Generative UI 设计系统:
|
|
236
|
-
|
|
237
|
-
### 平面克制
|
|
238
|
-
|
|
239
|
-
- **无渐变**:纯色背景
|
|
240
|
-
- **无阴影**:用边框区分层次
|
|
241
|
-
- **无模糊**:不要 filter: blur
|
|
242
|
-
- **无发光**:不要 text-shadow 或 glow
|
|
243
|
-
- **无装饰性 emoji**:用 CSS 形状或 SVG 路径做图标
|
|
244
|
-
|
|
245
|
-
### 字体克制
|
|
246
|
-
|
|
247
|
-
- 只用两种字重:400(正文)和 500(标题/强调)
|
|
248
|
-
- 不要 600、700——太重,破坏平面感
|
|
249
|
-
- 标题用句首大写,不要全大写
|
|
250
|
-
- 正文不要句中加粗,用 `code style` 标记实体名
|
|
251
|
-
- 最小字号 12px
|
|
252
|
-
|
|
253
|
-
### 边框和间距
|
|
254
|
-
|
|
255
|
-
- 边框 `1px solid`
|
|
256
|
-
- 卡片圆角 8-12px
|
|
257
|
-
- 卡片内边距 16-20px
|
|
258
|
-
- 卡片之间间距 16-24px
|
|
259
|
-
- 区块之间间距 32-48px——大间距区分大板块
|
|
260
|
-
|
|
261
|
-
### CSS 变量
|
|
262
|
-
|
|
263
|
-
```css
|
|
264
|
-
:root {
|
|
265
|
-
--bg: ...;
|
|
266
|
-
--surface: ...;
|
|
267
|
-
--border: ...;
|
|
268
|
-
--text: ...;
|
|
269
|
-
--text-muted: ...;
|
|
270
|
-
--accent: ...;
|
|
271
|
-
--success: ...;
|
|
272
|
-
--warning: ...;
|
|
273
|
-
--danger: ...;
|
|
274
|
-
}
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
不要在 HTML 里硬编码颜色。所有颜色引用变量。
|
|
278
|
-
|
|
279
|
-
### 配色自定
|
|
280
|
-
|
|
281
|
-
不要求暗色也不要求亮色。选你觉得让信息最易读的方案。关键是:
|
|
282
|
-
- 背景和文字对比度足够(WCAG AA 标准)
|
|
283
|
-
- 状态颜色区分明显
|
|
284
|
-
- 不要花哨——信息是主角,视觉是配角
|
|
285
|
-
|
|
286
|
-
---
|
|
287
|
-
|
|
288
|
-
## 五、技术约束
|
|
289
|
-
|
|
290
|
-
- **单文件**:所有 HTML/CSS/JS 内联,零外部依赖
|
|
291
|
-
- **零构建**:双击即可在浏览器打开
|
|
292
|
-
- **内联 SVG**:图表和依赖图直接写 `<svg>`,不用 `<img>` 或外部库
|
|
293
|
-
- **语义化 HTML**:用 `<section>` `<nav>` `<article>` `<button>`
|
|
294
|
-
- **响应式**:手机宽度下布局不崩(`@media (max-width: 768px)`)
|
|
295
|
-
|
|
296
|
-
---
|
|
297
|
-
|
|
298
|
-
## 六、质量检查清单
|
|
299
|
-
|
|
300
|
-
生成前逐项确认:
|
|
301
|
-
|
|
302
|
-
**信息密度**
|
|
303
|
-
- [ ] 每个信息块用最合适的视觉手段,不是什么都用卡片或列表
|
|
304
|
-
- [ ] 能用图表展示的数据不用表格
|
|
305
|
-
- [ ] 能用图示展示的关系不用文字描述
|
|
306
|
-
- [ ] 每个块 3 秒内能消化,不超过 3 行文字
|
|
307
|
-
|
|
308
|
-
**功能**
|
|
309
|
-
- [ ] 双击能在浏览器打开,不需要任何外部资源
|
|
310
|
-
- [ ] SVG 依赖图节点颜色按状态编码
|
|
311
|
-
- [ ] 点击 Intent 能展开详情
|
|
312
|
-
- [ ] Tab 切换正常
|
|
313
|
-
- [ ] 图表 Hover 有 tooltip(如果适用)
|
|
314
|
-
|
|
315
|
-
**视觉**
|
|
316
|
-
- [ ] 无渐变、无阴影、无模糊、无发光
|
|
317
|
-
- [ ] 字重只有 400 和 500
|
|
318
|
-
- [ ] 所有颜色用 CSS 变量
|
|
319
|
-
- [ ] 边框统一 1px
|
|
320
|
-
- [ ] 信息层次清晰(大字重要,小字次要)
|
|
321
|
-
|
|
322
|
-
**可访问性**
|
|
323
|
-
- [ ] `@media (prefers-reduced-motion: reduce)` 禁用动画
|
|
324
|
-
- [ ] 对比度足够
|
|
325
|
-
- [ ] 点击区域 ≥ 44px
|
|
326
|
-
- [ ] 不仅靠颜色传达信息(颜色 + 图标 + 文字)
|
|
327
|
-
|
|
328
|
-
**响应式**
|
|
329
|
-
- [ ] 手机宽度下布局不崩
|
|
330
|
-
- [ ] SVG 依赖图横向滚动
|
|
331
|
-
- [ ] Tab 导航横向滚动
|
|
332
|
-
|
|
333
|
-
---
|
|
334
|
-
|
|
335
|
-
## 输出
|
|
336
|
-
|
|
337
|
-
把 HTML 写到 `loom-preview.html`。完成后告诉用户文件路径。
|
package/cli/src/preview.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
// preview — 输出提示词,让 AI 读 .loom/ 文件并生成 HTML,并检查投影新鲜度。
|
|
2
|
-
// CLI 不生成 HTML。AI 自己读文件、重组信息、生成 HTML。
|
|
3
|
-
|
|
4
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
5
|
-
import { join, relative } from 'node:path';
|
|
6
|
-
import { readCurrentPointer } from './shared/paths.js';
|
|
7
|
-
|
|
8
|
-
const PROMPT_PATH = new URL('./preview-prompt.md', import.meta.url);
|
|
9
|
-
const SOURCE_FILE_NAMES = new Set([
|
|
10
|
-
'01_VISION.md',
|
|
11
|
-
'02_ARCHITECTURE.md',
|
|
12
|
-
'04_INTENT_MAP.json',
|
|
13
|
-
'05_VERIFICATION.md',
|
|
14
|
-
'06_CHANGELOG.json',
|
|
15
|
-
'06_CHANGELOG.md',
|
|
16
|
-
]);
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 输出 preview 提示词。
|
|
20
|
-
* @returns {string}
|
|
21
|
-
*/
|
|
22
|
-
export function generatePreviewPrompt() {
|
|
23
|
-
return readFileSync(PROMPT_PATH, 'utf-8');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function shouldIncludeSource(filePath) {
|
|
27
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
28
|
-
const fileName = normalized.split('/').pop();
|
|
29
|
-
if (SOURCE_FILE_NAMES.has(fileName)) return true;
|
|
30
|
-
return normalized.includes('/00_PHILOSOPHY/')
|
|
31
|
-
|| normalized.includes('/03_DECISIONS/')
|
|
32
|
-
|| normalized.includes('/verifications/');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function collectSourceFiles(dir, files = []) {
|
|
36
|
-
if (!existsSync(dir)) return files;
|
|
37
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
38
|
-
const fullPath = join(dir, entry.name);
|
|
39
|
-
if (entry.isDirectory()) {
|
|
40
|
-
collectSourceFiles(fullPath, files);
|
|
41
|
-
} else if (shouldIncludeSource(fullPath)) {
|
|
42
|
-
files.push(fullPath);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return files;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function getPreviewStatus(projectDir) {
|
|
49
|
-
const previewPath = join(projectDir, 'loom-preview.html');
|
|
50
|
-
const loomRoot = join(projectDir, '.loom');
|
|
51
|
-
const current = readCurrentPointer(loomRoot);
|
|
52
|
-
const versionDir = current ? join(loomRoot, current) : null;
|
|
53
|
-
const sourceFiles = versionDir ? collectSourceFiles(versionDir) : [];
|
|
54
|
-
const latestSource = sourceFiles
|
|
55
|
-
.map((filePath) => ({ filePath, mtimeMs: statSync(filePath).mtimeMs }))
|
|
56
|
-
.sort((a, b) => b.mtimeMs - a.mtimeMs)[0] || null;
|
|
57
|
-
|
|
58
|
-
const exists = existsSync(previewPath);
|
|
59
|
-
const previewMtimeMs = exists ? statSync(previewPath).mtimeMs : null;
|
|
60
|
-
const sourceLatestMtimeMs = latestSource?.mtimeMs ?? null;
|
|
61
|
-
const fresh = exists && sourceLatestMtimeMs !== null && previewMtimeMs >= sourceLatestMtimeMs;
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
exists,
|
|
65
|
-
fresh,
|
|
66
|
-
version: current,
|
|
67
|
-
preview_path: previewPath,
|
|
68
|
-
preview_mtime: previewMtimeMs ? new Date(previewMtimeMs).toISOString() : null,
|
|
69
|
-
source_latest_mtime: sourceLatestMtimeMs ? new Date(sourceLatestMtimeMs).toISOString() : null,
|
|
70
|
-
latest_source_file: latestSource ? relative(projectDir, latestSource.filePath).replace(/\\/g, '/') : null,
|
|
71
|
-
next_command: fresh ? 'loom preview' : 'loom preview --regen',
|
|
72
|
-
};
|
|
73
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
// intent-ref.js — Resolve current and version-qualified Intent read references.
|
|
2
|
-
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
4
|
-
import { basename, dirname, join } from 'node:path';
|
|
5
|
-
|
|
6
|
-
export const VERSION_PATTERN = /^v\d+$/;
|
|
7
|
-
export const INTENT_ID_PATTERN = /^INT-\d+$/;
|
|
8
|
-
|
|
9
|
-
export function formatIntentRef(version, intentId) {
|
|
10
|
-
return `${version}:${intentId}`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Resolve INT-003 against the current version or v1:INT-003 against .loom/v1. */
|
|
14
|
-
export function resolveIntentRef(currentVersionDir, input) {
|
|
15
|
-
const currentVersion = basename(currentVersionDir);
|
|
16
|
-
if (!VERSION_PATTERN.test(currentVersion)) {
|
|
17
|
-
throw new Error(`无法从目录确定当前 LOOM 版本: ${currentVersionDir}`);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const parts = String(input || '').split(':');
|
|
21
|
-
if (parts.length > 2) throw new Error(`Intent 引用格式非法: ${input}`);
|
|
22
|
-
const qualified = parts.length === 2;
|
|
23
|
-
const version = qualified ? parts[0] : currentVersion;
|
|
24
|
-
const intentId = qualified ? parts[1] : parts[0];
|
|
25
|
-
if (!VERSION_PATTERN.test(version) || !INTENT_ID_PATTERN.test(intentId)) {
|
|
26
|
-
throw new Error(`Intent 引用格式非法: ${input}(应为 INT-003 或 v1:INT-003)`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const versionDir = qualified ? join(dirname(currentVersionDir), version) : currentVersionDir;
|
|
30
|
-
if (!existsSync(versionDir)) throw new Error(`版本不存在: ${version}`);
|
|
31
|
-
return {
|
|
32
|
-
ref: formatIntentRef(version, intentId),
|
|
33
|
-
version,
|
|
34
|
-
intentId,
|
|
35
|
-
versionDir,
|
|
36
|
-
historical: version !== currentVersion,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
// shared/md-utils.js — MD 章节解析的公共工具
|
|
2
|
-
// 提取自 philosophy.js / intent-map.js / verify.js 三处重复实现。
|
|
3
|
-
// 统一 slugify + extractMdSection + 显式锚点逻辑,修中文标题 bug。
|
|
4
|
-
|
|
5
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 从 heading 文本中提取显式锚点。
|
|
9
|
-
* 支持 Pandoc/MDX 风格语法: "## 核心信念 {#core-belief}"
|
|
10
|
-
* @param {string} headingText — heading 文本(不含 # 前缀)
|
|
11
|
-
* @returns {string|null} 显式锚点 slug,或 null(无显式锚点时)
|
|
12
|
-
*/
|
|
13
|
-
export function extractExplicitAnchor(headingText) {
|
|
14
|
-
const match = headingText.match(/\{#([\w-]+)\}\s*$/);
|
|
15
|
-
return match ? match[1] : null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 从 heading 文本生成 slug(fallback,无显式锚点时用)。
|
|
20
|
-
* 规则:小写、去除 \r、空格转连字符、去除非 [a-z0-9_-] 字符。
|
|
21
|
-
*
|
|
22
|
-
* 中文标题处理:\w 不匹配中文,所以纯中文标题 slugify 后是空字符串。
|
|
23
|
-
* 这是设计约束——中文标题必须用显式锚点 {#anchor} 标注。
|
|
24
|
-
* slugify 返回空字符串时,调用方应给出明确错误提示。
|
|
25
|
-
*
|
|
26
|
-
* @param {string} text — heading 文本
|
|
27
|
-
* @returns {string} slug(可能为空字符串——纯中文标题无显式锚点时)
|
|
28
|
-
*/
|
|
29
|
-
export function slugify(text) {
|
|
30
|
-
return text
|
|
31
|
-
.replace(/\r/g, '') // strip CRLF 的 \r
|
|
32
|
-
.replace(/\{#[\w-]+\}\s*$/, '') // 去掉显式锚点标记
|
|
33
|
-
.toLowerCase()
|
|
34
|
-
.replace(/[^\w\s-]/g, '') // \w = [a-zA-Z0-9_]
|
|
35
|
-
.replace(/\s+/g, '-')
|
|
36
|
-
.replace(/-+/g, '-')
|
|
37
|
-
.replace(/^-|-$/g, '')
|
|
38
|
-
.trim();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 从 MD 内容中提取指定 section 的内容(到下一个同级或更高级 heading 为止)。
|
|
43
|
-
* 如果 sectionSlug 为 null/空,返回整个文件。
|
|
44
|
-
* 支持显式锚点 {#slug} 和自动 slugify 两种方式匹配。
|
|
45
|
-
*
|
|
46
|
-
* @param {string} content — MD 文件内容
|
|
47
|
-
* @param {string} sectionSlug — 目标 section 的 slug
|
|
48
|
-
* @param {string} contextLabel — 错误信息里的上下文标签(如 "意图叙事"、"验证契约")
|
|
49
|
-
* @returns {string} 提取的章节内容
|
|
50
|
-
* @throws {Error} 章节未找到时抛错,错误信息包含 contextLabel 和 slug
|
|
51
|
-
*/
|
|
52
|
-
export function extractMdSection(content, sectionSlug, contextLabel = '章节') {
|
|
53
|
-
if (!sectionSlug) return content;
|
|
54
|
-
|
|
55
|
-
const lines = content.split('\n');
|
|
56
|
-
let capturing = false;
|
|
57
|
-
let targetLevel = 0;
|
|
58
|
-
const captured = [];
|
|
59
|
-
|
|
60
|
-
for (const line of lines) {
|
|
61
|
-
// strip \r 以兼容 Windows CRLF
|
|
62
|
-
const cleanLine = line.replace(/\r$/, '');
|
|
63
|
-
const headingMatch = cleanLine.match(/^(#{1,6})\s+(.+)$/);
|
|
64
|
-
if (headingMatch) {
|
|
65
|
-
const level = headingMatch[1].length;
|
|
66
|
-
const headingText = headingMatch[2];
|
|
67
|
-
// 优先用显式锚点,没有再 fallback 到 slugify
|
|
68
|
-
const slug = extractExplicitAnchor(headingText) || slugify(headingText);
|
|
69
|
-
|
|
70
|
-
if (capturing && level <= targetLevel) {
|
|
71
|
-
break;
|
|
72
|
-
}
|
|
73
|
-
if (slug === sectionSlug) {
|
|
74
|
-
capturing = true;
|
|
75
|
-
targetLevel = level;
|
|
76
|
-
captured.push(cleanLine);
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (capturing) {
|
|
81
|
-
captured.push(cleanLine);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (captured.length === 0) {
|
|
86
|
-
throw new Error(
|
|
87
|
-
`${contextLabel}章节未找到: #${sectionSlug}\n` +
|
|
88
|
-
`可能原因:\n` +
|
|
89
|
-
` 1. 章节标题用了中文但没加显式锚点 {#anchor}\n` +
|
|
90
|
-
` 2. 锚点 slug 拼写错误\n` +
|
|
91
|
-
` 3. 引用的文件不存在该章节`
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
return captured.join('\n').trim();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* 安全读取并解析 JSON 文件。
|
|
99
|
-
* 统一错误处理——文件不存在、JSON 解析失败时给出明确错误信息。
|
|
100
|
-
*
|
|
101
|
-
* @param {string} filePath — JSON 文件绝对路径
|
|
102
|
-
* @param {string} contextLabel — 错误信息里的上下文标签(如 "Intent Map"、"验证记录")
|
|
103
|
-
* @returns {object} 解析后的 JSON 对象
|
|
104
|
-
* @throws {Error} 文件不存在或 JSON 解析失败时抛错,错误信息包含文件路径和原因
|
|
105
|
-
*/
|
|
106
|
-
export function readJsonFile(filePath, contextLabel = 'JSON 文件') {
|
|
107
|
-
if (!existsSync(filePath)) {
|
|
108
|
-
throw new Error(`${contextLabel}文件不存在: ${filePath}`);
|
|
109
|
-
}
|
|
110
|
-
let raw;
|
|
111
|
-
try {
|
|
112
|
-
raw = readFileSync(filePath, 'utf-8');
|
|
113
|
-
} catch (e) {
|
|
114
|
-
throw new Error(`${contextLabel}文件读取失败: ${filePath}\n原因: ${e.message}`);
|
|
115
|
-
}
|
|
116
|
-
try {
|
|
117
|
-
return JSON.parse(raw);
|
|
118
|
-
} catch (e) {
|
|
119
|
-
throw new Error(
|
|
120
|
-
`${contextLabel}文件 JSON 解析失败: ${filePath}\n` +
|
|
121
|
-
`原因: ${e.message}\n` +
|
|
122
|
-
`请检查文件内容是否为合法 JSON(多余逗号、缺少引号等)。`
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
}
|
package/cli/src/shared/paths.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
// shared/paths.js — LOOM 路径解析的公共工具
|
|
2
|
-
// 统一 getLoomRoot / findLoomRoot / findVersionDir 的命名和实现。
|
|
3
|
-
|
|
4
|
-
import { resolve, join, dirname } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
7
|
-
import { argv, cwd } from 'node:process';
|
|
8
|
-
|
|
9
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* 获取 LOOM 框架根目录(cli/src/shared 的上上上级)。
|
|
13
|
-
* cli/src/shared/paths.js -> cli/src/shared -> cli/src -> cli -> LOOM root
|
|
14
|
-
* @returns {string} LOOM 框架根目录绝对路径
|
|
15
|
-
*/
|
|
16
|
-
export function getLoomRoot() {
|
|
17
|
-
return resolve(__dirname, '..', '..', '..');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* 从命令行参数或 cwd 推断 .loom 目录路径。
|
|
22
|
-
* 优先级:--loom-dir 参数(指向版本目录,反推 .loom root)> cwd/.loom
|
|
23
|
-
* @returns {string} .loom 目录绝对路径
|
|
24
|
-
*/
|
|
25
|
-
export function findLoomRoot() {
|
|
26
|
-
const flagIdx = argv.indexOf('--loom-dir');
|
|
27
|
-
if (flagIdx !== -1 && argv[flagIdx + 1]) {
|
|
28
|
-
// --loom-dir 直接指向版本目录,反推 .loom root
|
|
29
|
-
const dir = resolve(argv[flagIdx + 1]);
|
|
30
|
-
return resolve(dir, '..');
|
|
31
|
-
}
|
|
32
|
-
return join(cwd(), '.loom');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 从命令行参数或 .loom/current 指针推断当前版本目录。
|
|
37
|
-
* @returns {string} .loom/v{N} 目录绝对路径
|
|
38
|
-
* @throws {Error} .loom 不存在或没有版本目录时抛错
|
|
39
|
-
*/
|
|
40
|
-
export function findVersionDir() {
|
|
41
|
-
const flagIdx = argv.indexOf('--loom-dir');
|
|
42
|
-
if (flagIdx !== -1 && argv[flagIdx + 1]) {
|
|
43
|
-
return resolve(argv[flagIdx + 1]);
|
|
44
|
-
}
|
|
45
|
-
const loomRoot = join(cwd(), '.loom');
|
|
46
|
-
if (!existsSync(loomRoot)) {
|
|
47
|
-
throw new Error(`找不到 .loom 目录: ${loomRoot}`);
|
|
48
|
-
}
|
|
49
|
-
const current = readCurrentPointer(loomRoot);
|
|
50
|
-
if (!current) {
|
|
51
|
-
throw new Error(`.loom 下没有版本目录 (v1, v2, ...)`);
|
|
52
|
-
}
|
|
53
|
-
return join(loomRoot, current);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* 读取当前版本指针。
|
|
58
|
-
* 优先读 .loom/current 文件;不存在则回退到自动探测最新版本。
|
|
59
|
-
* @param {string} loomRoot — .loom 目录路径
|
|
60
|
-
* @returns {string|null} 版本号如 'v1',或 null
|
|
61
|
-
*/
|
|
62
|
-
export function readCurrentPointer(loomRoot) {
|
|
63
|
-
const pointerPath = join(loomRoot, 'current');
|
|
64
|
-
if (existsSync(pointerPath)) {
|
|
65
|
-
const v = readFileSync(pointerPath, 'utf-8').trim();
|
|
66
|
-
if (/^v\d+$/.test(v) && existsSync(join(loomRoot, v))) return v;
|
|
67
|
-
}
|
|
68
|
-
if (!existsSync(loomRoot)) return null;
|
|
69
|
-
const versions = readdirSync(loomRoot)
|
|
70
|
-
.filter((d) => /^v\d+$/.test(d) && statSync(join(loomRoot, d)).isDirectory())
|
|
71
|
-
.sort((a, b) => parseInt(b.slice(1)) - parseInt(a.slice(1)));
|
|
72
|
-
return versions[0] ?? null;
|
|
73
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
-
import { extractMdSection } from './md-utils.js';
|
|
4
|
-
|
|
5
|
-
/** Resolve a root-relative Markdown proof reference without allowing path escape. */
|
|
6
|
-
export function resolveQualityProofReference(versionDir, ref) {
|
|
7
|
-
if (typeof ref !== 'string' || !ref.trim()) throw new Error('quality_proof_ref 必须是非空字符串');
|
|
8
|
-
const match = ref.trim().match(/^([^#]+)#([\w-]+)$/);
|
|
9
|
-
if (!match) throw new Error('quality_proof_ref 必须是项目相对路径加 Markdown 锚点,例如 verifications/INT-001-quality-proof.md#int-001');
|
|
10
|
-
const [, file, anchor] = match;
|
|
11
|
-
if (isAbsolute(file)) throw new Error('quality_proof_ref 不得使用绝对路径');
|
|
12
|
-
const projectDir = resolve(versionDir, '..', '..');
|
|
13
|
-
const filePath = resolve(projectDir, file);
|
|
14
|
-
const relation = relative(projectDir, filePath);
|
|
15
|
-
if (relation.startsWith('..') || isAbsolute(relation)) throw new Error('quality_proof_ref 不得越出项目目录');
|
|
16
|
-
if (!existsSync(filePath)) throw new Error(`quality_proof_ref 指向的文件不存在: ${file}`);
|
|
17
|
-
extractMdSection(readFileSync(filePath, 'utf-8'), anchor, 'Quality Proof');
|
|
18
|
-
return { filePath, anchor, ref: ref.trim() };
|
|
19
|
-
}
|