@seanyao/roll 2026.521.2 → 2026.522.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.
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ US-DECK-002: deck.md schema + grounding validator.
4
+
5
+ Reads a `deck.md` file, parses it with the same parser used by the renderer
6
+ (`lib/slides-render.py`), and verifies:
7
+
8
+ 1. Required frontmatter fields are present:
9
+ template, slug, title_en, title_zh, total_slides, created
10
+ 2. frontmatter.total_slides matches the actual `## Slide N` section count.
11
+ 3. Each slide has non-empty title_en / title_zh / body_en / body_zh.
12
+ 4. Grounding threshold: at least ceil(N/3) evidence citations across all
13
+ slides (i.e. >= 1 per 3 slides). If the deck has fewer, the validator
14
+ exits non-zero with a ⚠️ grounding warning so callers (e.g.
15
+ `roll slides build`) can flag it.
16
+
17
+ Usage:
18
+ python3 slides-validate.py <deck.md>
19
+
20
+ Exit codes:
21
+ 0 valid (schema OK + grounding threshold met)
22
+ 1 schema error (missing field, mismatch, missing slide body, etc.)
23
+ 2 grounding warning (schema OK but evidence below threshold)
24
+ 3 file not found / unreadable / parse error
25
+
26
+ Error messages are written to stderr in English + Chinese (Roll bilingual
27
+ convention).
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import importlib.util
33
+ import math
34
+ import sys
35
+ from pathlib import Path
36
+
37
+
38
+ REQUIRED_FRONTMATTER = (
39
+ "template",
40
+ "slug",
41
+ "title_en",
42
+ "title_zh",
43
+ "total_slides",
44
+ "created",
45
+ )
46
+ REQUIRED_SLIDE_KEYS = ("title_en", "title_zh", "body_en", "body_zh")
47
+
48
+
49
+ def _load_renderer():
50
+ """Import lib/slides-render.py as a module (hyphenated filename, so we
51
+ can't `import slides_render` directly)."""
52
+ here = Path(__file__).resolve().parent
53
+ spec = importlib.util.spec_from_file_location(
54
+ "slides_render", str(here / "slides-render.py")
55
+ )
56
+ if spec is None or spec.loader is None:
57
+ raise ImportError("could not load slides-render.py")
58
+ mod = importlib.util.module_from_spec(spec)
59
+ spec.loader.exec_module(mod)
60
+ return mod
61
+
62
+
63
+ def err(msg_en: str, msg_zh: str = "") -> None:
64
+ print(f"[slides-validate] {msg_en}", file=sys.stderr)
65
+ if msg_zh:
66
+ print(f"[slides-validate] {msg_zh}", file=sys.stderr)
67
+
68
+
69
+ def validate_frontmatter(fm: dict) -> list[str]:
70
+ errors: list[str] = []
71
+ for key in REQUIRED_FRONTMATTER:
72
+ if key not in fm or fm[key] == "" or fm[key] is None:
73
+ errors.append(f"missing required frontmatter field: {key}")
74
+ if "total_slides" in fm and not isinstance(fm["total_slides"], int):
75
+ errors.append(
76
+ f"total_slides must be an integer, got "
77
+ f"{type(fm['total_slides']).__name__}: {fm['total_slides']!r}"
78
+ )
79
+ return errors
80
+
81
+
82
+ def validate_slides(fm: dict, slides: list[dict]) -> list[str]:
83
+ errors: list[str] = []
84
+ actual = len(slides)
85
+ declared = fm.get("total_slides")
86
+ if isinstance(declared, int) and declared != actual:
87
+ errors.append(
88
+ f"total_slides mismatch: frontmatter declares {declared} but "
89
+ f"found {actual} `## Slide N` sections"
90
+ )
91
+ for slide in slides:
92
+ n = slide.get("number", "?")
93
+ for key in REQUIRED_SLIDE_KEYS:
94
+ v = slide.get(key)
95
+ if v is None or (isinstance(v, str) and v.strip() == ""):
96
+ errors.append(f"slide {n}: missing or empty {key}")
97
+ return errors
98
+
99
+
100
+ def evaluate_grounding(slides: list[dict]) -> tuple[int, int, bool]:
101
+ """
102
+ Return (citations, threshold, meets_threshold).
103
+
104
+ The threshold is `ceil(len(slides) / 3)` — i.e. at least one evidence
105
+ citation per three slides. An empty deck trivially meets the threshold
106
+ (threshold = 0).
107
+ """
108
+ citations = 0
109
+ for slide in slides:
110
+ ev = slide.get("evidence")
111
+ if isinstance(ev, list):
112
+ citations += len(ev)
113
+ threshold = math.ceil(len(slides) / 3) if slides else 0
114
+ return citations, threshold, citations >= threshold
115
+
116
+
117
+ def main(argv: list[str]) -> int:
118
+ if len(argv) < 2:
119
+ err(
120
+ "usage: slides-validate.py <deck.md>",
121
+ "用法: slides-validate.py <deck.md>",
122
+ )
123
+ return 3
124
+
125
+ path = Path(argv[1])
126
+ if not path.is_file():
127
+ err(f"deck file not found: {path}", f"未找到 deck 文件:{path}")
128
+ return 3
129
+
130
+ try:
131
+ renderer = _load_renderer()
132
+ except Exception as e:
133
+ err(f"could not load renderer module: {e}")
134
+ return 3
135
+
136
+ try:
137
+ src = path.read_text(encoding="utf-8")
138
+ fm, body = renderer.parse_frontmatter(src)
139
+ slides = renderer.parse_slides(body)
140
+ except (ValueError, OSError) as e:
141
+ err(f"failed to parse deck.md: {e}", "解析 deck.md 失败")
142
+ return 3
143
+
144
+ schema_errors: list[str] = []
145
+ schema_errors += validate_frontmatter(fm)
146
+ schema_errors += validate_slides(fm, slides)
147
+
148
+ if schema_errors:
149
+ for e in schema_errors:
150
+ err(e)
151
+ return 1
152
+
153
+ citations, threshold, ok = evaluate_grounding(slides)
154
+ if not ok:
155
+ err(
156
+ f"⚠️ grounding below threshold: {citations} evidence citation(s) "
157
+ f"for {len(slides)} slides (need >= {threshold}). "
158
+ f"Each slide group of 3 must include at least one evidence entry.",
159
+ f"⚠️ 证据引用不足:{len(slides)} 张幻灯片仅有 {citations} 条 evidence,"
160
+ f"至少需要 {threshold} 条(每 3 张 ≥ 1 条)。",
161
+ )
162
+ return 2
163
+
164
+ # Valid — silent success.
165
+ return 0
166
+
167
+
168
+ if __name__ == "__main__":
169
+ sys.exit(main(sys.argv))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanyao/roll",
3
- "version": "2026.521.2",
3
+ "version": "2026.522.1",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "scripts": {
6
6
  "test": "bash tests/run.sh"
@@ -19,7 +19,7 @@ After successful Build & Deploy, extracts completed Stories from .roll/backlog.m
19
19
 
20
20
  - Generating commit messages or PR descriptions — this skill only runs post-deploy
21
21
  - Recording dev diary / moments (use `$roll-notes`)
22
- - Bumping package version (use the project's release script, e.g. `scripts/release.sh`)
22
+ - Bumping package version (use the project's release script Roll's lives in `roll-meta/ops/release.sh`)
23
23
 
24
24
  ## Workflow
25
25
 
@@ -59,7 +59,7 @@ CHANGELOG 只取破折号前的用户症状,丢弃破折号后的修复手段
59
59
  - 测试基建(teardown 清理、test isolation、bats helper、CI 时序)
60
60
  - prompt / SKILL.md 内部契约(schema 锁定、enum 强制、contract test)
61
61
  - 内部重构(提取函数、变量改名、目录调整)
62
- - 只有开发者会遇到的 bug(release.sh 自身逻辑、TCR 节奏调整)
62
+ - 只有开发者会遇到的 bug(发版脚本自身逻辑、TCR 节奏调整)
63
63
  - 任何"用户体验不变"的改动
64
64
 
65
65
  判断准则:**如果用户读了这条记录,他不会改变使用方式,就别写。**
@@ -124,9 +124,10 @@ Fix 类句式参考:`<命令/功能> 不再 <之前的坏现象>`,或 `<命
124
124
 
125
125
  ### 4. Section Header — Always `## Unreleased`
126
126
 
127
- **⚠️ do NOT guess version numbers.** Only `scripts/release.sh` assigns concrete
128
- versions, and it only does so at the moment of a real release. Until then,
129
- every new bullet goes under `## Unreleased` at the top of CHANGELOG.md.
127
+ **⚠️ do NOT guess version numbers.** Only the release script (maintainer-private
128
+ in `roll-meta/ops/release.sh`) assigns concrete versions, and it only does so at
129
+ the moment of a real release. Until then, every new bullet goes under
130
+ `## Unreleased` at the top of CHANGELOG.md.
130
131
 
131
132
  ```
132
133
  ## Unreleased
@@ -134,8 +135,8 @@ every new bullet goes under `## Unreleased` at the top of CHANGELOG.md.
134
135
  - **Fixed**: ...
135
136
  ```
136
137
 
137
- When `release.sh` runs, it renames `## Unreleased` to `## v{N}` (where N is
138
- computed from git tags) — that's the single moment a version label gets
138
+ When the release script runs, it renames `## Unreleased` to `## v{N}` (where N
139
+ is computed from git tags) — that's the single moment a version label gets
139
140
  assigned.
140
141
 
141
142
  Do NOT read `package.json` version, do NOT call `git describe`, do NOT invent
@@ -291,8 +292,8 @@ After successful deploy in `$roll-build` / `$roll-fix`:
291
292
 
292
293
  ## 7. Release Notes 生成模式(GitHub Release 正文)
293
294
 
294
- `CHANGELOG.md` 里的 `## Unreleased` 条目是**原始数据**,每条对应一个故事或修复。
295
- 发版时(`release.sh` 打 tag 前,或手动 `roll release notes`),把这些散装 bullet 整理成**给人读的 Release 正文**。
295
+ `CHANGELOG.md` 里的 `## Unreleased` 条目是**原始数据**,每条对应一个故事或修复。
296
+ 发版时(release script 打 tag 前,或手动 `roll release notes`),把这些散装 bullet 整理成**给人读的 Release 正文**。
296
297
 
297
298
  这是两个不同的产物:
298
299
  - `CHANGELOG.md` — 机器写、给 `roll update` 之后展示用,条目可以多
@@ -300,7 +301,7 @@ After successful deploy in `$roll-build` / `$roll-fix`:
300
301
 
301
302
  ### 7.1 何时触发
302
303
 
303
- - `release.sh` commit 之前调用本 skill 并传入 `--release-notes` flag
304
+ - 发版脚本在 commit 之前调用本 skill 并传入 `--release-notes` flag
304
305
  - 或用户手动说"帮我整理 Release Notes"
305
306
 
306
307
  ### 7.2 分组规则
@@ -360,13 +361,14 @@ After successful deploy in `$roll-build` / `$roll-fix`:
360
361
 
361
362
  ## 8. features.md 重写模式(产品 SOT)
362
363
 
363
- US-DOC-008 — `scripts/release.sh` 在 changelog/release-notes 生成完后会再
364
- 调一次本 skill,请求"整体重写 `.roll/features.md`"。这次调用的语义和上面
365
- 两种完全不同:**不是基于本版 Story 增量**,而是基于**项目整体当前状态**。
364
+ US-DOC-008 — 发版脚本(maintainer-private,位于 `roll-meta/ops/release.sh`)
365
+ changelog/release-notes 生成完后会再调一次本 skill,请求"整体重写
366
+ `.roll/features.md`"。这次调用的语义和上面两种完全不同:**不是基于本版
367
+ Story 增量**,而是基于**项目整体当前状态**。
366
368
 
367
369
  ### 8.1 何时触发
368
370
 
369
- release.sh 完成 changelog/release-notes 写盘后,喂一段以
371
+ 发版脚本完成 changelog/release-notes 写盘后,喂一段以
370
372
  `## 当前任务:重写 .roll/features.md(Section 8)` 开头的 prompt。
371
373
 
372
374
  ### 8.2 输入
@@ -420,9 +422,9 @@ prompt 会包含:
420
422
  - 该 Feature 下**所有** Story 均为 `📋 Todo` → 在描述末尾追加 `*(规划中)*`
421
423
  - 只要有 **≥1 个** `✅ Done` Story → 正常展示,**不加**任何标记
422
424
  - 一眼可见:规划中的 Feature 在每个 Epic 分组的末尾列出
423
- - **FIX-051 兜底**:`scripts/release.sh` AI 重写后会跑机械校验
425
+ - **FIX-051 兜底**:发版脚本在 AI 重写后会跑机械校验
424
426
  `_enforce_planning_markers`,即使本规则被 AI 漏掉也会自动补 `*(规划中)*`;
425
- 规则的权威实现是 release.sh 里的纯 shell 函数,prompt 这条只是软提示
427
+ 规则的权威实现是发版脚本里的纯 shell 函数,prompt 这条只是软提示
426
428
  - 描述写 1 句话 **产品视角**:用户能用它做什么,避免实现细节
427
429
  - **语言:单一中文**。Feature 名(如 `roll-loop` / `Cross-Agent Peer Review`)和命令、环境变量等术语保留英文原样;描述句一律中文。**不要**在条目下追加英文翻译行(早期 v517.x 双语混排版式已废弃,理由:扫读困难、维护翻倍、与 `guide/en|zh/` 平行目录约定不一致;英文受众走 site 端 i18n)
428
430
  - 分组用 BACKLOG 的 Epic 名,原序,不重排
@@ -435,5 +437,5 @@ prompt 会包含:
435
437
  ### 8.5 失败安全
436
438
 
437
439
  如果 prompt 信息不足(BACKLOG 解析失败等),**不要部分写入** —— 输出原
438
- 文件内容即可。release.sh 会捕获 stdout 后比较:内容未变就不 stage。
440
+ 文件内容即可。发版脚本会捕获 stdout 后比较:内容未变就不 stage。
439
441
 
@@ -137,7 +137,7 @@ Parse .roll/backlog.md for all `### Feature: <name>` groups that contain ≥1
137
137
  | REFACTOR-XXX | features.md 功能目录落后于 BACKLOG,N 个已完成功能区未收录,用户无法通过产品目录发现这些功能 — flagged by dream YYYY-MM-DD | 📋 Todo |
138
138
  ```
139
139
 
140
- The catalog is auto-updated by `scripts/release.sh` at release time (Section 8 of roll-.changelog). Between releases, this check surfaces the coverage gap so it isn't silently skipped.
140
+ The catalog is auto-updated by the release script (maintainer-private at `roll-meta/ops/release.sh`) at release time (Section 8 of roll-.changelog). Between releases, this check surfaces the coverage gap so it isn't silently skipped.
141
141
 
142
142
  **REFACTOR entry format for doc findings:**
143
143
 
@@ -0,0 +1,136 @@
1
+ ---
2
+ name: roll-deck
3
+ license: MIT
4
+ allowed-tools: "Read, Edit, Write, Glob, Grep, Bash(git:*)"
5
+ description: "Generate a bilingual 18-slide deck.md from a topic. Reads the current project (README, AGENTS.md, backlog, features), discusses outline with the user when needed, and writes one file: .roll/slides/<slug>/deck.md. Each slide carries title_en/title_zh + body_en/body_zh + evidence (file:line). HTML rendering is a separate bash step (roll slides build)."
6
+ ---
7
+
8
+ # roll-deck
9
+
10
+ > Topic in, deck.md out. AI authoring layer of the slide-deck pipeline.
11
+ > 主题进,deck.md 出。幻灯片管线的 AI 创作层。
12
+
13
+ ## Trigger
14
+
15
+ User invokes `roll-deck` from the CLI:
16
+
17
+ ```
18
+ $roll-deck "Introducing Roll Loop"
19
+ $roll-deck "How TCR keeps us honest"
20
+ ```
21
+
22
+ `roll slides new "<topic>"` shells out to the selected agent with this skill loaded.
23
+ `roll slides new "<topic>"` 会通过所选 agent 加载本 skill。
24
+
25
+ ## When Not to Use
26
+
27
+ - Rendering an existing `deck.md` to HTML → use `roll slides build <slug>` (bash, no AI).
28
+ - Listing or previewing decks → use `roll slides list` / `roll slides preview <slug>`.
29
+ - Authoring backlog stories → use `$roll-design` / `$roll-idea`.
30
+
31
+ ## Hard Constraints 硬约束
32
+
33
+ - **You may write exactly one file**: `.roll/slides/<slug>/deck.md`.
34
+ - You MUST NOT edit any other file: no README, no AGENTS.md, no backlog, no source code, no `.roll/slides/*.html`.
35
+ - HTML rendering is `roll slides build <slug>` — never produce HTML here.
36
+ - If `.roll/slides/<slug>/deck.md` already exists, ask the user before overwriting.
37
+
38
+ ## Inputs
39
+
40
+ - `<topic>`: a free-text topic string (required).
41
+ - `<template>`: template name, default `introduction-v3`.
42
+ - `<slug>`: kebab-case slug derived from the topic by the CLI; you receive it.
43
+
44
+ ## Workflow
45
+
46
+ ### 1. Read the project 阅读项目
47
+
48
+ Read in this order, skipping files that don't exist:
49
+
50
+ 1. `README.md` — top-level pitch and entry points.
51
+ 2. `AGENTS.md` — communication style, conventions, where to look.
52
+ 3. `.roll/backlog.md` — recently Done Stories and the top of the Todo queue.
53
+ 4. `.roll/features/**/*.md` — feature specs relevant to the topic.
54
+ 5. Targeted `Grep` for the topic keywords across the repo (≤ 30 hits).
55
+
56
+ Stop reading when you have enough to write 18 slides with concrete evidence.
57
+ 拿到足够 18 张 slide 的证据就停。
58
+
59
+ ### 2. Outline check outline 校验
60
+
61
+ - If the topic is unambiguous and the project gives clear evidence (high confidence), proceed directly.
62
+ - If the topic is vague (multiple valid framings), restate the proposed 18-slide outline in 3–5 lines and ask the user to confirm or adjust before writing. Wait for confirmation.
63
+ - Never ask more than one round of questions — pick the strongest framing and proceed.
64
+
65
+ ### 3. Generate 18 slides 生成 18 张 slide
66
+
67
+ Default count is `18` (or the count the template prescribes). Each slide MUST contain:
68
+
69
+ - `title_en` — short English title (≤ 8 words).
70
+ - `title_zh` — short Chinese title (≤ 16 chars).
71
+ - `body_en` — markdown body, concise, ≤ 200 words.
72
+ - `body_zh` — Chinese body, concise, ≤ 200 chars.
73
+ - `evidence` — list of `<path>:<line>` references that ground the claim.
74
+
75
+ **Grounding threshold**: every 3 consecutive slides MUST contain at least 1 evidence citation. If you cannot ground a slide, label it `⚠️ unverified` in the body and explain why in one line.
76
+ **Grounding 阈值**:每 3 张 slide 至少 1 条 evidence。无法取证时打 `⚠️ unverified` 并一行说明。
77
+
78
+ Bilingual rule: English and Chinese MUST be on separate lines in the rendered body — never inline on the same line.
79
+
80
+ ### 4. Write deck.md 写文件
81
+
82
+ Write to `.roll/slides/<slug>/deck.md` with this structure:
83
+
84
+ ```markdown
85
+ ---
86
+ template: <template>
87
+ slug: <slug>
88
+ title_en: "<topic in English>"
89
+ title_zh: "<topic in Chinese>"
90
+ total_slides: 18
91
+ created: <YYYY-MM-DD>
92
+ ---
93
+
94
+ ## Slide 1
95
+ title_en: "..."
96
+ title_zh: "..."
97
+ body_en: |
98
+ ...
99
+ body_zh: |
100
+ ...
101
+ evidence:
102
+ - README.md:12
103
+ - .roll/backlog.md:34
104
+
105
+ ## Slide 2
106
+ ...
107
+ ```
108
+
109
+ `total_slides` MUST match the number of `## Slide N` blocks. The validator (run by `roll slides build`) will reject mismatches.
110
+
111
+ ### 5. Prompt the user 提示用户
112
+
113
+ After writing, print a single bilingual block:
114
+
115
+ ```
116
+ deck.md written → .roll/slides/<slug>/deck.md
117
+ deck.md 已写入 → .roll/slides/<slug>/deck.md
118
+
119
+ Next: roll slides build <slug>
120
+ 下一步:roll slides build <slug>
121
+ ```
122
+
123
+ Do not run `roll slides build` yourself — that is a deliberate human gate.
124
+
125
+ ## Output format expectations
126
+
127
+ - One file written: `.roll/slides/<slug>/deck.md`.
128
+ - Final agent message ends with the bilingual "Next" hint above.
129
+ - No HTML, no edits to any other file.
130
+
131
+ ## Rules
132
+
133
+ - Honour the hard constraints above. The CLI will trust you not to scribble outside the deck file.
134
+ - If `.roll/` does not exist, create it first (with `mkdir -p .roll/slides/<slug>`).
135
+ - If the project has no `README.md` or `AGENTS.md`, proceed using what is available and flag affected slides as `⚠️ unverified`.
136
+ - Keep slides terse — this is a deck, not a doc.