@sushanglewis/lincoln 1.5.5 → 1.6.2

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 (82) hide show
  1. package/dist/main.js +425 -51
  2. package/dist/main.js.map +1 -1
  3. package/dist/postinstall.d.ts +9 -2
  4. package/dist/postinstall.js +812 -6
  5. package/dist/postinstall.js.map +1 -1
  6. package/framework/.claude/agents/architect.md +15 -15
  7. package/framework/.claude/agents/default.md +118 -104
  8. package/framework/.claude/agents/designer.md +16 -51
  9. package/framework/.claude/agents/engineer.md +16 -25
  10. package/framework/.claude/agents/frontend-engineer.md +15 -16
  11. package/framework/.claude/agents/knowledge-curator.md +18 -15
  12. package/framework/.claude/agents/pm.md +16 -75
  13. package/framework/.claude/agents/qa.md +17 -13
  14. package/framework/.claude/agents/release-coordinator.md +18 -12
  15. package/framework/.claude/agents/researcher.md +16 -26
  16. package/framework/.claude/harnesses/command-map.yaml +13 -6
  17. package/framework/.claude/hooks/on-session-start.sh +0 -1
  18. package/framework/.claude/schemas/agent-definition.schema.json +6 -1
  19. package/framework/.claude/schemas/stage-definition.schema.json +49 -4
  20. package/framework/.claude/skills/build-product-prototype/prompts/main.md +89 -26
  21. package/framework/.claude/skills/clarify-requirements/prompts/main.md +80 -14
  22. package/framework/.claude/skills/draft-product-design/prompts/main.md +24 -12
  23. package/framework/.claude/skills/lc-build-codebase-knowledge/SKILL.md +2 -0
  24. package/framework/.claude/skills/lc-explore-opensource/SKILL.md +2 -0
  25. package/framework/.claude/skills/lc-init-branch/SKILL.md +2 -2
  26. package/framework/.claude/skills/lc-market-research/SKILL.md +1 -1
  27. package/framework/.claude/skills/lc-one-id/SKILL.md +81 -0
  28. package/framework/.claude/skills/lc-one-id/prompts/main.md +35 -0
  29. package/framework/.claude/skills/lc-stakeholder-research/SKILL.md +1 -1
  30. package/framework/.claude/skills/lc-workflow-router/SKILL.md +2 -0
  31. package/framework/.claude/stages/build-codebase-knowledge.yaml +10 -10
  32. package/framework/.claude/stages/clarify.yaml +21 -15
  33. package/framework/.claude/stages/explore-opensource.yaml +13 -10
  34. package/framework/.claude/stages/implement.yaml +11 -11
  35. package/framework/.claude/stages/ingest.yaml +13 -10
  36. package/framework/.claude/stages/lc-analyze-frameworks.yaml +13 -10
  37. package/framework/.claude/stages/lc-collect-intelligence.yaml +13 -10
  38. package/framework/.claude/stages/lc-competitive-analysis.yaml +13 -10
  39. package/framework/.claude/stages/lc-first-principles.yaml +13 -10
  40. package/framework/.claude/stages/lc-market-research.yaml +13 -10
  41. package/framework/.claude/stages/lc-product-research.yaml +13 -10
  42. package/framework/.claude/stages/lc-research-report.yaml +13 -10
  43. package/framework/.claude/stages/lc-research-scope.yaml +13 -10
  44. package/framework/.claude/stages/lc-stakeholder-research.yaml +13 -10
  45. package/framework/.claude/stages/lc-storytelling.yaml +13 -10
  46. package/framework/.claude/stages/product-design-docs.yaml +30 -18
  47. package/framework/.claude/stages/product-prototype.yaml +20 -17
  48. package/framework/.claude/stages/propose.yaml +16 -9
  49. package/framework/.claude/stages/split.yaml +13 -9
  50. package/framework/.claude/stages/sync-knowledge.yaml +20 -9
  51. package/framework/.claude/stages/tdd-development-plan.yaml +17 -9
  52. package/framework/.claude/stages/workflow-router.yaml +17 -13
  53. package/framework/.claude/workflows/bug-fix.yaml +1 -0
  54. package/framework/.claude/workflows/existing-project-iteration.yaml +1 -0
  55. package/framework/.claude/workflows/interview-to-knowledge.yaml +1 -0
  56. package/framework/.claude/workflows/oss-first-design.yaml +1 -0
  57. package/framework/.claude-plugin/marketplace.json +2 -2
  58. package/framework/.claude-plugin/plugin.json +2 -1
  59. package/framework/.version-bump.json +1 -1
  60. package/framework/README.en.md +15 -2
  61. package/framework/README.md +16 -3
  62. package/framework/RELEASE.md +48 -0
  63. package/framework/manifest.json +94 -74
  64. package/framework/scripts/check-prompt-drift.py +132 -1
  65. package/framework/scripts/extract_release_notes.py +79 -0
  66. package/framework/scripts/init-lincoln-branch.sh +6 -0
  67. package/framework/scripts/lincoln_command_map.py +1 -1
  68. package/framework/scripts/lincoln_id.py +333 -0
  69. package/framework/scripts/lincoln_index.py +10 -0
  70. package/framework/scripts/lincoln_prd.py +16 -17
  71. package/framework/scripts/lincoln_render.py +657 -0
  72. package/framework/scripts/run-infrastructure-tests.py +2 -0
  73. package/framework/scripts/stage_loader.py +149 -2
  74. package/framework/scripts/static-check.sh +1 -1
  75. package/framework/scripts/update_readme_version.py +149 -0
  76. package/framework/scripts/validate_stage.py +80 -1
  77. package/framework/tools/lincoln/package-lock.json +2 -2
  78. package/framework/tools/lincoln/package.json +1 -1
  79. package/framework/tools/lincoln-installer/package-lock.json +2 -2
  80. package/framework/tools/lincoln-installer/package.json +1 -1
  81. package/package.json +2 -2
  82. package/framework/.claude/agents/_contract.md +0 -79
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+ """从 RELEASE.md 抽取指定版本的发布说明,供 GitHub Release 等发布流程复用。
3
+
4
+ 匹配的节标题形态:`# Lincoln vX.Y.Z ...`、`# vX.Y.Z ...`、`## vX.Y.Z ...`;
5
+ 抽取范围从节标题起到下一个同级或更高级标题之前。用法:
6
+
7
+ python3 scripts/extract_release_notes.py 1.6.0
8
+ python3 scripts/extract_release_notes.py v1.6.0 --root /path/to/repo
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ ROOT = Path(__file__).resolve().parents[1]
18
+ SEMVER = re.compile(r"^\d+\.\d+\.\d+$")
19
+ HEADING = re.compile(r"^(#{1,6})\s+(.+?)\s*$")
20
+ RELEASE_FILE = "RELEASE.md"
21
+
22
+
23
+ def section_matcher(version: str) -> re.Pattern:
24
+ """匹配节标题文本:`Lincoln ` 前缀可选,版本号后必须是空白或行尾。"""
25
+ return re.compile(rf"^(?:Lincoln\s+)?v{re.escape(version)}(?=\s|$)")
26
+
27
+
28
+ def extract_section(text: str, version: str) -> str | None:
29
+ """抽取 version 对应章节;找不到返回 None。"""
30
+ matcher = section_matcher(version)
31
+ lines = text.splitlines()
32
+ start: int | None = None
33
+ level = 0
34
+ for i, line in enumerate(lines):
35
+ heading = HEADING.match(line)
36
+ if heading and len(heading.group(1)) <= 2 and matcher.match(heading.group(2)):
37
+ start, level = i, len(heading.group(1))
38
+ break
39
+ if start is None:
40
+ return None
41
+ end = len(lines)
42
+ for j in range(start + 1, len(lines)):
43
+ heading = HEADING.match(lines[j])
44
+ if heading and len(heading.group(1)) <= level:
45
+ end = j
46
+ break
47
+ return "\n".join(lines[start:end]).rstrip()
48
+
49
+
50
+ def main(argv: list[str], root: Path = ROOT) -> int:
51
+ args = argv[1:]
52
+ if "--root" in args:
53
+ index = args.index("--root")
54
+ if index + 1 >= len(args):
55
+ print("--root requires a path", file=sys.stderr)
56
+ return 2
57
+ root = Path(args[index + 1])
58
+ del args[index : index + 2]
59
+ if len(args) != 1:
60
+ print(__doc__, file=sys.stderr)
61
+ return 2
62
+ version = args[0].removeprefix("v")
63
+ if not SEMVER.match(version):
64
+ print(f"invalid semver (want X.Y.Z): {args[0]!r}", file=sys.stderr)
65
+ return 2
66
+ release = root / RELEASE_FILE
67
+ if not release.exists():
68
+ print(f"missing release notes: {RELEASE_FILE}", file=sys.stderr)
69
+ return 1
70
+ section = extract_section(release.read_text(encoding="utf-8"), version)
71
+ if section is None:
72
+ print(f"section for v{version} not found in {RELEASE_FILE}", file=sys.stderr)
73
+ return 1
74
+ print(section)
75
+ return 0
76
+
77
+
78
+ if __name__ == "__main__":
79
+ sys.exit(main(sys.argv))
@@ -253,11 +253,17 @@ for dir in \
253
253
  "$PROCESS_ROOT/openspec/changes" \
254
254
  "$PROCESS_ROOT/openspec/specs" \
255
255
  "$PROCESS_ROOT/recordings" \
256
+ "$PROCESS_ROOT/.lincoln/one-id" \
256
257
  ".github/lincoln-sync-queue"; do
257
258
  mkdir -p "$dir"
258
259
  touch "$dir/.gitkeep"
259
260
  done
260
261
 
262
+ # Initialize empty one-id index files for the issue package.
263
+ for idx in docs features stories models apis flows fields pages; do
264
+ echo "{}" > "$PROCESS_ROOT/.lincoln/one-id/${idx}.yaml"
265
+ done
266
+
261
267
  # Copy the HTML portal scaffold from read-only templates.
262
268
  # Templates stay in .claude/templates/issue-package/; only the rendered shell and
263
269
  # shared assets are copied into the package.
@@ -105,7 +105,7 @@ def list_workflows() -> dict[str, dict[str, Any]]:
105
105
 
106
106
  def list_agents() -> dict[str, dict[str, Any]]:
107
107
  agents: dict[str, dict[str, Any]] = {}
108
- excluded = {"default.md", "_contract.md"}
108
+ excluded = {"default.md"}
109
109
  for path in sorted(AGENTS_DIR.glob("*.md")):
110
110
  if path.name in excluded:
111
111
  continue
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env python3
2
+ """One-id index manager for Lincoln issue packages.
3
+
4
+ Provides stable, machine-readable identifiers for PRD themes, features, user
5
+ stories, data models, APIs, flows, fields, and pages. Agents can query by ID
6
+ instead of reading whole documents.
7
+
8
+ Examples:
9
+ python scripts/lincoln_id.py lookup page/checkout-cart
10
+ python scripts/lincoln_id.py list --type page
11
+ python scripts/lincoln_id.py related feature/checkout-redesign
12
+ python scripts/lincoln_id.py create page/checkout-cart \
13
+ --title "购物车" \
14
+ --path "pages/prototype/web/checkout-cart/page.html" \
15
+ --feature feature/checkout-redesign
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import re
22
+ import sys
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
27
+ if str(PROJECT_ROOT) not in sys.path:
28
+ sys.path.insert(0, str(PROJECT_ROOT))
29
+
30
+ import yaml
31
+
32
+ from scripts.lincoln_paths import (
33
+ PROJECT_ROOT as _PROJECT_ROOT,
34
+ get_process_slug,
35
+ load_yaml,
36
+ resolve_state_path,
37
+ )
38
+
39
+ ONE_ID_DIR = ".lincoln/one-id"
40
+
41
+ ID_RE = re.compile(r"^(doc|feature|story|model|api|flow|field|page)/[a-z0-9][a-z0-9-]*$")
42
+
43
+ TYPE_TO_FILE = {
44
+ "doc": "docs.yaml",
45
+ "feature": "features.yaml",
46
+ "story": "stories.yaml",
47
+ "model": "models.yaml",
48
+ "api": "apis.yaml",
49
+ "flow": "flows.yaml",
50
+ "field": "fields.yaml",
51
+ "page": "pages.yaml",
52
+ }
53
+
54
+ FILE_TO_TYPE = {v: k for k, v in TYPE_TO_FILE.items()}
55
+
56
+
57
+ def _load_state() -> dict[str, Any] | None:
58
+ state_path = resolve_state_path(None, PROJECT_ROOT)
59
+ if not state_path.exists():
60
+ return None
61
+ try:
62
+ return load_yaml(state_path)
63
+ except Exception:
64
+ return None
65
+
66
+
67
+ def _process_slug() -> str:
68
+ state = _load_state()
69
+ state_path = resolve_state_path(None, PROJECT_ROOT)
70
+ return get_process_slug(state, state_path)
71
+
72
+
73
+ def _one_id_dir() -> Path:
74
+ return PROJECT_ROOT / _process_slug() / ONE_ID_DIR
75
+
76
+
77
+ def _index_path(id_type: str) -> Path:
78
+ filename = TYPE_TO_FILE[id_type]
79
+ path = _one_id_dir() / filename
80
+ path.parent.mkdir(parents=True, exist_ok=True)
81
+ return path
82
+
83
+
84
+ def _load_index(id_type: str) -> dict[str, Any]:
85
+ path = _index_path(id_type)
86
+ if not path.exists():
87
+ return {}
88
+ try:
89
+ data = load_yaml(path)
90
+ return data if isinstance(data, dict) else {}
91
+ except Exception:
92
+ return {}
93
+
94
+
95
+ def _save_index(id_type: str, data: dict[str, Any]) -> None:
96
+ path = _index_path(id_type)
97
+ path.write_text(yaml.safe_dump(data, allow_unicode=True, sort_keys=True), encoding="utf-8")
98
+
99
+
100
+ def _split_id(full_id: str) -> tuple[str, str]:
101
+ if "/" not in full_id:
102
+ raise ValueError(f"Invalid ID (must contain '/'): {full_id}")
103
+ id_type, name = full_id.split("/", 1)
104
+ if id_type not in TYPE_TO_FILE:
105
+ raise ValueError(f"Unknown ID type '{id_type}': {full_id}")
106
+ if not ID_RE.match(full_id):
107
+ raise ValueError(f"Invalid ID format: {full_id}")
108
+ return id_type, name
109
+
110
+
111
+ def _pretty_print(data: Any, indent: int = 0) -> None:
112
+ prefix = " " * indent
113
+ if isinstance(data, dict):
114
+ for key, value in data.items():
115
+ if isinstance(value, (dict, list)):
116
+ print(f"{prefix}{key}:")
117
+ _pretty_print(value, indent + 1)
118
+ else:
119
+ print(f"{prefix}{key}: {value}")
120
+ elif isinstance(data, list):
121
+ for item in data:
122
+ if isinstance(item, (dict, list)):
123
+ print(f"{prefix}-")
124
+ _pretty_print(item, indent + 1)
125
+ else:
126
+ print(f"{prefix}- {item}")
127
+ else:
128
+ print(f"{prefix}{data}")
129
+
130
+
131
+ def cmd_lookup(full_id: str) -> int:
132
+ """Print the complete entry for an ID, searching across all index files."""
133
+ try:
134
+ id_type, name = _split_id(full_id)
135
+ except ValueError as exc:
136
+ print(f"ERROR: {exc}", file=sys.stderr)
137
+ return 1
138
+
139
+ index = _load_index(id_type)
140
+ entry = index.get(full_id)
141
+ if entry is None:
142
+ print(f"NOT FOUND: {full_id}")
143
+ return 1
144
+
145
+ print(f"{full_id}:")
146
+ _pretty_print(entry, indent=1)
147
+ return 0
148
+
149
+
150
+ def cmd_list(id_type: str | None) -> int:
151
+ """List all IDs, optionally filtered by type."""
152
+ if id_type:
153
+ if id_type not in TYPE_TO_FILE:
154
+ print(f"ERROR: unknown type '{id_type}'", file=sys.stderr)
155
+ return 1
156
+ types = [id_type]
157
+ else:
158
+ types = list(TYPE_TO_FILE.keys())
159
+
160
+ found = False
161
+ for t in types:
162
+ index = _load_index(t)
163
+ for full_id in sorted(index):
164
+ print(full_id)
165
+ found = True
166
+ if not found:
167
+ print("No IDs indexed yet.")
168
+ return 0
169
+
170
+
171
+ def _collect_related(
172
+ full_id: str,
173
+ visited: set[str],
174
+ collected: dict[str, list[dict[str, Any]]],
175
+ ) -> None:
176
+ """Recursively collect entries related to *full_id* via cross-reference fields."""
177
+ if full_id in visited:
178
+ return
179
+ visited.add(full_id)
180
+
181
+ try:
182
+ id_type, _ = _split_id(full_id)
183
+ except ValueError:
184
+ return
185
+
186
+ index = _load_index(id_type)
187
+ entry = index.get(full_id)
188
+ if entry is None:
189
+ return
190
+
191
+ collected.setdefault(id_type, []).append({full_id: entry})
192
+
193
+ relation_fields = (
194
+ "source",
195
+ "pages",
196
+ "flows",
197
+ "stories",
198
+ "fields",
199
+ "apis",
200
+ "models",
201
+ "features",
202
+ "docs",
203
+ )
204
+ for field in relation_fields:
205
+ value = entry.get(field)
206
+ if not value:
207
+ continue
208
+ refs = value if isinstance(value, list) else [value]
209
+ for ref in refs:
210
+ ref_id = str(ref).split("#")[0]
211
+ if "/" not in ref_id:
212
+ continue
213
+ try:
214
+ _split_id(ref_id)
215
+ except ValueError:
216
+ continue
217
+ _collect_related(ref_id, visited, collected)
218
+
219
+
220
+ def cmd_related(full_id: str) -> int:
221
+ """Print all entries reachable from *full_id* through cross-references."""
222
+ try:
223
+ _split_id(full_id)
224
+ except ValueError as exc:
225
+ print(f"ERROR: {exc}", file=sys.stderr)
226
+ return 1
227
+
228
+ collected: dict[str, list[dict[str, Any]]] = {}
229
+ _collect_related(full_id, set(), collected)
230
+
231
+ if not collected:
232
+ print(f"No related entries found for {full_id}")
233
+ return 0
234
+
235
+ for id_type in sorted(collected):
236
+ print(f"## {id_type}")
237
+ for item in collected[id_type]:
238
+ for key, value in item.items():
239
+ print(f"{key}:")
240
+ _pretty_print(value, indent=1)
241
+ print()
242
+ return 0
243
+
244
+
245
+ def cmd_create(
246
+ full_id: str,
247
+ title: str | None,
248
+ path: str | None,
249
+ source: str | None,
250
+ relations: list[str],
251
+ ) -> int:
252
+ """Create or update a one-id entry."""
253
+ try:
254
+ id_type, name = _split_id(full_id)
255
+ except ValueError as exc:
256
+ print(f"ERROR: {exc}", file=sys.stderr)
257
+ return 1
258
+
259
+ index = _load_index(id_type)
260
+ entry: dict[str, Any] = index.get(full_id, {})
261
+
262
+ if title:
263
+ entry["title"] = title
264
+ if path:
265
+ entry["path"] = path
266
+ if source:
267
+ entry["source"] = source
268
+
269
+ # Relations are stored as simple list values under the inferred type key.
270
+ for relation in relations:
271
+ try:
272
+ rel_type, _ = _split_id(relation)
273
+ except ValueError as exc:
274
+ print(f"ERROR: {exc}", file=sys.stderr)
275
+ return 1
276
+ plural = rel_type + "s"
277
+ existing = entry.setdefault(plural, [])
278
+ if relation not in existing:
279
+ existing.append(relation)
280
+
281
+ index[full_id] = entry
282
+ _save_index(id_type, index)
283
+ print(f"Created/updated {full_id}")
284
+ return 0
285
+
286
+
287
+ def _build_argument_parser() -> argparse.ArgumentParser:
288
+ parser = argparse.ArgumentParser(description="Lincoln one-id index manager")
289
+ subparsers = parser.add_subparsers(dest="command", required=True)
290
+
291
+ lookup_parser = subparsers.add_parser("lookup", help="Look up an ID across all indexes")
292
+ lookup_parser.add_argument("id", help="Full ID (e.g. page/checkout-cart)")
293
+
294
+ list_parser = subparsers.add_parser("list", help="List indexed IDs")
295
+ list_parser.add_argument("--type", help=f"Filter by type ({', '.join(TYPE_TO_FILE)})")
296
+
297
+ related_parser = subparsers.add_parser("related", help="Show entries related to an ID")
298
+ related_parser.add_argument("id", help="Full ID (e.g. feature/checkout-redesign)")
299
+
300
+ create_parser = subparsers.add_parser("create", help="Create or update an ID entry")
301
+ create_parser.add_argument("id", help="Full ID (e.g. page/checkout-cart)")
302
+ create_parser.add_argument("--title", help="Human-readable title")
303
+ create_parser.add_argument("--path", help="Artifact path (repo-relative)")
304
+ create_parser.add_argument("--source", help="Source document ID or URL fragment")
305
+ create_parser.add_argument(
306
+ "--relation",
307
+ action="append",
308
+ default=[],
309
+ help="Related full ID (can be repeated)",
310
+ )
311
+
312
+ return parser
313
+
314
+
315
+ def main(argv: list[str] | None = None) -> int:
316
+ parser = _build_argument_parser()
317
+ args = parser.parse_args(argv)
318
+
319
+ if args.command == "lookup":
320
+ return cmd_lookup(args.id)
321
+ if args.command == "list":
322
+ return cmd_list(args.type)
323
+ if args.command == "related":
324
+ return cmd_related(args.id)
325
+ if args.command == "create":
326
+ return cmd_create(args.id, args.title, args.path, args.source, args.relation)
327
+
328
+ parser.print_help()
329
+ return 1
330
+
331
+
332
+ if __name__ == "__main__":
333
+ sys.exit(main())
@@ -175,8 +175,13 @@ def build_package_data(
175
175
 
176
176
  # Build nav groups preserving scan order.
177
177
  groups: dict[str, list[dict[str, Any]]] = {}
178
+ meta_warnings: list[str] = []
178
179
  for rel in sorted(page_data):
179
180
  entry = page_data[rel]
181
+ if not entry.get("nav_label"):
182
+ meta_warnings.append(f"{rel}: missing <meta name=\"nav-label\">")
183
+ if not entry.get("uid"):
184
+ meta_warnings.append(f"{rel}: missing <meta name=\"doc-uid\"> or <meta name=\"page-uid\">")
180
185
  group_name = entry.get("group") or entry.get("nav_group") or "Docs"
181
186
  groups.setdefault(group_name, []).append(
182
187
  {
@@ -200,6 +205,11 @@ def build_package_data(
200
205
  }
201
206
  )
202
207
 
208
+ if meta_warnings:
209
+ print("WARN: pages with incomplete portal meta tags:", file=sys.stderr)
210
+ for warning in meta_warnings:
211
+ print(f" - {warning}", file=sys.stderr)
212
+
203
213
  nav = [{"group": g, "items": items} for g, items in groups.items()]
204
214
 
205
215
  return {
@@ -136,23 +136,22 @@ def current_version(package_root: Path | None = None, *, args: argparse.Namespac
136
136
  return version
137
137
 
138
138
 
139
- def _render_prd_html(title: str, version: str, markdown: str) -> str:
139
+ def _render_prd_html(title: str, version: str, markdown: str, package_root: Path) -> str:
140
140
  """Wrap legacy Markdown PRD content in the page-doc HTML template."""
141
- template_path = PROJECT_ROOT / ".claude" / "templates" / "issue-package" / "page-doc.html.tpl"
142
- if not template_path.exists():
143
- return markdown
144
- template = template_path.read_text(encoding="utf-8")
145
- replacements = {
146
- "{TITLE}": title,
147
- "{NAV_GROUP}": "Docs",
148
- "{VERSION}": version,
149
- "{STAGE}": "clarify",
150
- "{UID}": "",
151
- "{MARKDOWN_SOURCE}": markdown,
152
- }
153
- for placeholder, value in replacements.items():
154
- template = template.replace(placeholder, value)
155
- return template
141
+ from scripts import lincoln_render
142
+
143
+ return lincoln_render.render_page(
144
+ stage_id="clarify",
145
+ target="{process_slug}/pages/docs/prd.html",
146
+ title=title,
147
+ nav_label=title,
148
+ nav_group="Docs",
149
+ version=version,
150
+ uid="prd",
151
+ stage_mark="clarify",
152
+ markdown_source=markdown,
153
+ extra_variables={"process_slug": package_root.name},
154
+ )
156
155
 
157
156
 
158
157
  def migrate(
@@ -187,7 +186,7 @@ def migrate(
187
186
  # Avoid duplicating the version marker once the template injects it.
188
187
  content = VERSION_COMMENT_RE.sub("", content, count=1).strip("\n")
189
188
 
190
- html = _render_prd_html("PRD", version, content)
189
+ html = _render_prd_html("PRD", version, content, root)
191
190
 
192
191
  target_path.parent.mkdir(parents=True, exist_ok=True)
193
192
  atomic_write_text(target_path, html)