@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.
- package/dist/main.js +425 -51
- package/dist/main.js.map +1 -1
- package/dist/postinstall.d.ts +9 -2
- package/dist/postinstall.js +812 -6
- package/dist/postinstall.js.map +1 -1
- package/framework/.claude/agents/architect.md +15 -15
- package/framework/.claude/agents/default.md +118 -104
- package/framework/.claude/agents/designer.md +16 -51
- package/framework/.claude/agents/engineer.md +16 -25
- package/framework/.claude/agents/frontend-engineer.md +15 -16
- package/framework/.claude/agents/knowledge-curator.md +18 -15
- package/framework/.claude/agents/pm.md +16 -75
- package/framework/.claude/agents/qa.md +17 -13
- package/framework/.claude/agents/release-coordinator.md +18 -12
- package/framework/.claude/agents/researcher.md +16 -26
- package/framework/.claude/harnesses/command-map.yaml +13 -6
- package/framework/.claude/hooks/on-session-start.sh +0 -1
- package/framework/.claude/schemas/agent-definition.schema.json +6 -1
- package/framework/.claude/schemas/stage-definition.schema.json +49 -4
- package/framework/.claude/skills/build-product-prototype/prompts/main.md +89 -26
- package/framework/.claude/skills/clarify-requirements/prompts/main.md +80 -14
- package/framework/.claude/skills/draft-product-design/prompts/main.md +24 -12
- package/framework/.claude/skills/lc-build-codebase-knowledge/SKILL.md +2 -0
- package/framework/.claude/skills/lc-explore-opensource/SKILL.md +2 -0
- package/framework/.claude/skills/lc-init-branch/SKILL.md +2 -2
- package/framework/.claude/skills/lc-market-research/SKILL.md +1 -1
- package/framework/.claude/skills/lc-one-id/SKILL.md +81 -0
- package/framework/.claude/skills/lc-one-id/prompts/main.md +35 -0
- package/framework/.claude/skills/lc-stakeholder-research/SKILL.md +1 -1
- package/framework/.claude/skills/lc-workflow-router/SKILL.md +2 -0
- package/framework/.claude/stages/build-codebase-knowledge.yaml +10 -10
- package/framework/.claude/stages/clarify.yaml +21 -15
- package/framework/.claude/stages/explore-opensource.yaml +13 -10
- package/framework/.claude/stages/implement.yaml +11 -11
- package/framework/.claude/stages/ingest.yaml +13 -10
- package/framework/.claude/stages/lc-analyze-frameworks.yaml +13 -10
- package/framework/.claude/stages/lc-collect-intelligence.yaml +13 -10
- package/framework/.claude/stages/lc-competitive-analysis.yaml +13 -10
- package/framework/.claude/stages/lc-first-principles.yaml +13 -10
- package/framework/.claude/stages/lc-market-research.yaml +13 -10
- package/framework/.claude/stages/lc-product-research.yaml +13 -10
- package/framework/.claude/stages/lc-research-report.yaml +13 -10
- package/framework/.claude/stages/lc-research-scope.yaml +13 -10
- package/framework/.claude/stages/lc-stakeholder-research.yaml +13 -10
- package/framework/.claude/stages/lc-storytelling.yaml +13 -10
- package/framework/.claude/stages/product-design-docs.yaml +30 -18
- package/framework/.claude/stages/product-prototype.yaml +20 -17
- package/framework/.claude/stages/propose.yaml +16 -9
- package/framework/.claude/stages/split.yaml +13 -9
- package/framework/.claude/stages/sync-knowledge.yaml +20 -9
- package/framework/.claude/stages/tdd-development-plan.yaml +17 -9
- package/framework/.claude/stages/workflow-router.yaml +17 -13
- package/framework/.claude/workflows/bug-fix.yaml +1 -0
- package/framework/.claude/workflows/existing-project-iteration.yaml +1 -0
- package/framework/.claude/workflows/interview-to-knowledge.yaml +1 -0
- package/framework/.claude/workflows/oss-first-design.yaml +1 -0
- package/framework/.claude-plugin/marketplace.json +2 -2
- package/framework/.claude-plugin/plugin.json +2 -1
- package/framework/.version-bump.json +1 -1
- package/framework/README.en.md +15 -2
- package/framework/README.md +16 -3
- package/framework/RELEASE.md +48 -0
- package/framework/manifest.json +94 -74
- package/framework/scripts/check-prompt-drift.py +132 -1
- package/framework/scripts/extract_release_notes.py +79 -0
- package/framework/scripts/init-lincoln-branch.sh +6 -0
- package/framework/scripts/lincoln_command_map.py +1 -1
- package/framework/scripts/lincoln_id.py +333 -0
- package/framework/scripts/lincoln_index.py +10 -0
- package/framework/scripts/lincoln_prd.py +16 -17
- package/framework/scripts/lincoln_render.py +657 -0
- package/framework/scripts/run-infrastructure-tests.py +2 -0
- package/framework/scripts/stage_loader.py +149 -2
- package/framework/scripts/static-check.sh +1 -1
- package/framework/scripts/update_readme_version.py +149 -0
- package/framework/scripts/validate_stage.py +80 -1
- package/framework/tools/lincoln/package-lock.json +2 -2
- package/framework/tools/lincoln/package.json +1 -1
- package/framework/tools/lincoln-installer/package-lock.json +2 -2
- package/framework/tools/lincoln-installer/package.json +1 -1
- package/package.json +2 -2
- 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"
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
"
|
|
149
|
-
|
|
150
|
-
"
|
|
151
|
-
"
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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)
|