@sushanglewis/lincoln 1.5.0 → 1.5.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 (305) hide show
  1. package/dist/main.d.ts +3 -0
  2. package/dist/main.js +1286 -916
  3. package/dist/main.js.map +1 -1
  4. package/dist/postinstall.d.ts +3 -0
  5. package/dist/postinstall.js +18 -0
  6. package/dist/postinstall.js.map +1 -0
  7. package/framework/.claude/agents/_contract.md +79 -0
  8. package/framework/.claude/agents/architect.md +31 -0
  9. package/framework/.claude/agents/default.md +151 -0
  10. package/framework/.claude/agents/designer.md +33 -0
  11. package/framework/.claude/agents/engineer.md +42 -0
  12. package/framework/.claude/agents/external/NOTICES.md +12 -0
  13. package/framework/.claude/agents/external/everything-claude-code.manifest.yaml +23 -0
  14. package/framework/.claude/agents/external/oh-my-claudecode.manifest.yaml +26 -0
  15. package/framework/.claude/agents/external/wshobson-agents.manifest.yaml +23 -0
  16. package/framework/.claude/agents/frontend-engineer.md +32 -0
  17. package/framework/.claude/agents/knowledge-curator.md +28 -0
  18. package/framework/.claude/agents/pm.md +38 -0
  19. package/framework/.claude/agents/qa.md +28 -0
  20. package/framework/.claude/agents/release-coordinator.md +25 -0
  21. package/framework/.claude/agents/researcher.md +41 -0
  22. package/framework/.claude/harnesses/claude-code.yaml +15 -0
  23. package/framework/.claude/harnesses/codex.yaml +40 -0
  24. package/framework/.claude/harnesses/command-map.yaml +255 -0
  25. package/framework/.claude/harnesses/opencode.yaml +32 -0
  26. package/framework/.claude/harnesses/scenarios.yaml +45 -0
  27. package/framework/.claude/hooks/on-pr-event.sh +69 -0
  28. package/framework/.claude/hooks/on-session-start.sh +427 -0
  29. package/framework/.claude/hooks/on-stop.sh +104 -0
  30. package/framework/.claude/hooks/post-tool-use.sh +161 -0
  31. package/framework/.claude/hooks/pre-tool-use.sh +246 -0
  32. package/framework/.claude/schemas/agent-definition.schema.json +35 -0
  33. package/framework/.claude/schemas/benchmark-report.schema.json +148 -0
  34. package/framework/.claude/schemas/pm-to-ux-handoff.schema.yaml +128 -0
  35. package/framework/.claude/schemas/stage-definition.schema.json +233 -0
  36. package/framework/.claude/schemas/workflow-stage.schema.json +86 -0
  37. package/framework/.claude/schemas/workflow-template.schema.json +88 -0
  38. package/framework/.claude/settings.json +14 -0
  39. package/framework/.claude/skills/build-product-prototype/SKILL.md +35 -0
  40. package/framework/.claude/skills/build-product-prototype/prompts/main.md +78 -0
  41. package/framework/.claude/skills/clarify-requirements/SKILL.md +32 -0
  42. package/framework/.claude/skills/clarify-requirements/prompts/main.md +81 -0
  43. package/framework/.claude/skills/dependencies.yaml +139 -0
  44. package/framework/.claude/skills/draft-product-design/SKILL.md +40 -0
  45. package/framework/.claude/skills/draft-product-design/prompts/main.md +47 -0
  46. package/framework/.claude/skills/lc-analyze-frameworks/SKILL.md +43 -0
  47. package/framework/.claude/skills/lc-analyze-frameworks/prompts/main.md +95 -0
  48. package/framework/.claude/skills/lc-benchmark/SKILL.md +61 -0
  49. package/framework/.claude/skills/lc-build-codebase-knowledge/SKILL.md +38 -0
  50. package/framework/.claude/skills/lc-build-codebase-knowledge/prompts/build-codebase-knowledge.md +61 -0
  51. package/framework/.claude/skills/lc-collect-intelligence/SKILL.md +43 -0
  52. package/framework/.claude/skills/lc-collect-intelligence/prompts/main.md +72 -0
  53. package/framework/.claude/skills/lc-competitive-analysis/SKILL.md +43 -0
  54. package/framework/.claude/skills/lc-competitive-analysis/prompts/main.md +70 -0
  55. package/framework/.claude/skills/lc-explore-opensource/SKILL.md +36 -0
  56. package/framework/.claude/skills/lc-explore-opensource/prompts/explore-opensource.md +77 -0
  57. package/framework/.claude/skills/lc-first-principles/SKILL.md +43 -0
  58. package/framework/.claude/skills/lc-first-principles/prompts/main.md +73 -0
  59. package/framework/.claude/skills/lc-handoff/SKILL.md +34 -0
  60. package/framework/.claude/skills/lc-init-branch/SKILL.md +52 -0
  61. package/framework/.claude/skills/lc-market-research/SKILL.md +43 -0
  62. package/framework/.claude/skills/lc-market-research/prompts/main.md +79 -0
  63. package/framework/.claude/skills/lc-product-research/SKILL.md +43 -0
  64. package/framework/.claude/skills/lc-product-research/prompts/main.md +80 -0
  65. package/framework/.claude/skills/lc-research-report/SKILL.md +43 -0
  66. package/framework/.claude/skills/lc-research-report/prompts/main.md +90 -0
  67. package/framework/.claude/skills/lc-research-scope/SKILL.md +43 -0
  68. package/framework/.claude/skills/lc-research-scope/prompts/main.md +82 -0
  69. package/framework/.claude/skills/lc-setup/SKILL.md +68 -0
  70. package/framework/.claude/skills/lc-setup/prompts/main.md +88 -0
  71. package/framework/.claude/skills/lc-stage/SKILL.md +45 -0
  72. package/framework/.claude/skills/lc-stakeholder-research/SKILL.md +43 -0
  73. package/framework/.claude/skills/lc-stakeholder-research/prompts/main.md +65 -0
  74. package/framework/.claude/skills/lc-status/SKILL.md +25 -0
  75. package/framework/.claude/skills/lc-storytelling/SKILL.md +43 -0
  76. package/framework/.claude/skills/lc-storytelling/prompts/main.md +77 -0
  77. package/framework/.claude/skills/lc-wf/SKILL.md +54 -0
  78. package/framework/.claude/skills/lc-workflow/SKILL.md +48 -0
  79. package/framework/.claude/skills/lc-workflow/validators/validate.py +755 -0
  80. package/framework/.claude/skills/lc-workflow-router/SKILL.md +41 -0
  81. package/framework/.claude/skills/lc-workflow-router/prompts/intake-prompt.md +91 -0
  82. package/framework/.claude/skills/lc-workflow-router/prompts/router-prompt.md +44 -0
  83. package/framework/.claude/skills/plan-tdd-development/SKILL.md +31 -0
  84. package/framework/.claude/skills/plan-tdd-development/prompts/main.md +44 -0
  85. package/framework/.claude/skills/process-interview/SKILL.md +32 -0
  86. package/framework/.claude/skills/process-interview/prompts/main.md +47 -0
  87. package/framework/.claude/skills/propose-with-openspec/SKILL.md +37 -0
  88. package/framework/.claude/skills/propose-with-openspec/prompts/main.md +53 -0
  89. package/framework/.claude/skills/split-to-github/SKILL.md +32 -0
  90. package/framework/.claude/skills/split-to-github/prompts/main.md +42 -0
  91. package/framework/.claude/skills/sync-to-knowledge/SKILL.md +35 -0
  92. package/framework/.claude/skills/sync-to-knowledge/prompts/main.md +54 -0
  93. package/framework/.claude/skills/workflow-continue/SKILL.md +24 -0
  94. package/framework/.claude/skills/workflow-continue/prompts/main.md +25 -0
  95. package/framework/.claude/stages/build-codebase-knowledge.yaml +42 -0
  96. package/framework/.claude/stages/clarify.yaml +59 -0
  97. package/framework/.claude/stages/explore-opensource.yaml +40 -0
  98. package/framework/.claude/stages/implement.yaml +60 -0
  99. package/framework/.claude/stages/ingest.yaml +40 -0
  100. package/framework/.claude/stages/lc-analyze-frameworks.yaml +37 -0
  101. package/framework/.claude/stages/lc-collect-intelligence.yaml +37 -0
  102. package/framework/.claude/stages/lc-competitive-analysis.yaml +37 -0
  103. package/framework/.claude/stages/lc-first-principles.yaml +37 -0
  104. package/framework/.claude/stages/lc-market-research.yaml +37 -0
  105. package/framework/.claude/stages/lc-product-research.yaml +37 -0
  106. package/framework/.claude/stages/lc-research-report.yaml +38 -0
  107. package/framework/.claude/stages/lc-research-scope.yaml +38 -0
  108. package/framework/.claude/stages/lc-stakeholder-research.yaml +37 -0
  109. package/framework/.claude/stages/lc-storytelling.yaml +37 -0
  110. package/framework/.claude/stages/product-design-docs.yaml +66 -0
  111. package/framework/.claude/stages/product-prototype.yaml +53 -0
  112. package/framework/.claude/stages/propose.yaml +45 -0
  113. package/framework/.claude/stages/split.yaml +41 -0
  114. package/framework/.claude/stages/sync-knowledge.yaml +49 -0
  115. package/framework/.claude/stages/tdd-development-plan.yaml +43 -0
  116. package/framework/.claude/stages/workflow-router.yaml +45 -0
  117. package/framework/.claude/templates/solo-workflow-context.yaml +23 -0
  118. package/framework/.claude/workflows/README.md +93 -0
  119. package/framework/.claude/workflows/bug-fix.yaml +81 -0
  120. package/framework/.claude/workflows/design-spike.yaml +55 -0
  121. package/framework/.claude/workflows/existing-project-iteration.yaml +106 -0
  122. package/framework/.claude/workflows/interview-to-knowledge.yaml +189 -0
  123. package/framework/.claude/workflows/oss-first-design.yaml +100 -0
  124. package/framework/.claude/workflows/pm-research.yaml +115 -0
  125. package/framework/.claude-plugin/hooks.json +6 -0
  126. package/framework/.claude-plugin/marketplace.json +32 -0
  127. package/framework/.claude-plugin/plugin.json +57 -0
  128. package/framework/.version-bump.json +43 -0
  129. package/framework/CLAUDE.md +140 -0
  130. package/framework/CONTRIBUTING.md +219 -0
  131. package/framework/LICENSE +21 -0
  132. package/framework/README.en.md +139 -0
  133. package/framework/README.md +139 -0
  134. package/framework/RELEASE.md +334 -0
  135. package/framework/SKILL.md +24 -0
  136. package/framework/USAGE.md +356 -0
  137. package/framework/manifest.json +1214 -0
  138. package/framework/requirements.txt +4 -0
  139. package/framework/scripts/__init__.py +0 -0
  140. package/framework/scripts/build_codebase_knowledge.py +76 -0
  141. package/framework/scripts/bump_version.py +181 -0
  142. package/framework/scripts/check-dependency-drift.py +69 -0
  143. package/framework/scripts/check-harness-drift.sh +57 -0
  144. package/framework/scripts/check-main-merge-hygiene.py +128 -0
  145. package/framework/scripts/check-skill-dependencies.sh +124 -0
  146. package/framework/scripts/init-lincoln-branch.sh +300 -0
  147. package/framework/scripts/init-project.sh +184 -0
  148. package/framework/scripts/lc-benchmark-cli.py +66 -0
  149. package/framework/scripts/lincoln-audit.py +288 -0
  150. package/framework/scripts/lincoln-setup.py +651 -0
  151. package/framework/scripts/lincoln-status.py +448 -0
  152. package/framework/scripts/lincoln_benchmark.py +224 -0
  153. package/framework/scripts/lincoln_benchmark_eval.py +104 -0
  154. package/framework/scripts/lincoln_benchmark_metrics.py +818 -0
  155. package/framework/scripts/lincoln_benchmark_report.py +141 -0
  156. package/framework/scripts/lincoln_command_map.py +289 -0
  157. package/framework/scripts/lincoln_dependency_manager.py +335 -0
  158. package/framework/scripts/lincoln_documents.py +175 -0
  159. package/framework/scripts/lincoln_harness_adapter.py +319 -0
  160. package/framework/scripts/lincoln_index.py +255 -0
  161. package/framework/scripts/lincoln_paths.py +198 -0
  162. package/framework/scripts/lincoln_prd.py +227 -0
  163. package/framework/scripts/lincoln_role.py +46 -0
  164. package/framework/scripts/lincoln_scenario.py +59 -0
  165. package/framework/scripts/lincoln_session_start_metrics.py +71 -0
  166. package/framework/scripts/lincoln_skill_prompt.py +48 -0
  167. package/framework/scripts/lincoln_trace.py +335 -0
  168. package/framework/scripts/lincoln_workflow.py +199 -0
  169. package/framework/scripts/list-active-lincoln-branches.sh +87 -0
  170. package/framework/scripts/package-lincoln-plugin.py +340 -0
  171. package/framework/scripts/run-infrastructure-tests.py +84 -0
  172. package/framework/scripts/stage_loader.py +1071 -0
  173. package/framework/scripts/static-check.sh +356 -0
  174. package/framework/scripts/sync-external-agents.sh +210 -0
  175. package/framework/scripts/sync-framework-package.mjs +218 -0
  176. package/framework/scripts/task_tool_guard.py +213 -0
  177. package/framework/scripts/track-artifacts.py +140 -0
  178. package/framework/scripts/validate_stage.py +350 -0
  179. package/{README.md → framework/tools/lincoln/README.md} +4 -4
  180. package/framework/tools/lincoln/package-lock.json +3349 -0
  181. package/framework/tools/lincoln/package.json +48 -0
  182. package/framework/tools/lincoln/src/cli.tsx +60 -0
  183. package/framework/tools/lincoln/src/components/AudioMeter.tsx +34 -0
  184. package/framework/tools/lincoln/src/components/CancelledScreen.tsx +11 -0
  185. package/framework/tools/lincoln/src/components/DevicesMenu.tsx +79 -0
  186. package/framework/tools/lincoln/src/components/HintBar.tsx +25 -0
  187. package/framework/tools/lincoln/src/components/LogView.tsx +36 -0
  188. package/framework/tools/lincoln/src/components/ModelMenu.tsx +122 -0
  189. package/framework/tools/lincoln/src/components/ReadyScreen.tsx +54 -0
  190. package/framework/tools/lincoln/src/components/RecordingApp.tsx +211 -0
  191. package/framework/tools/lincoln/src/components/RecordingScreen.tsx +41 -0
  192. package/framework/tools/lincoln/src/components/SelectMenu.tsx +53 -0
  193. package/framework/tools/lincoln/src/components/StatusPanel.tsx +52 -0
  194. package/framework/tools/lincoln/src/components/StopConfirmation.tsx +32 -0
  195. package/framework/tools/lincoln/src/config/args.ts +35 -0
  196. package/framework/tools/lincoln/src/config/loadConfig.ts +81 -0
  197. package/framework/tools/lincoln/src/config/resolveConfig.ts +85 -0
  198. package/framework/tools/lincoln/src/devices/listDevices.ts +11 -0
  199. package/framework/tools/lincoln/src/devices/parseDevices.ts +25 -0
  200. package/framework/tools/lincoln/src/hooks/useKeyHandler.ts +63 -0
  201. package/framework/tools/lincoln/src/main.tsx +4 -0
  202. package/framework/tools/lincoln/src/models/models.ts +34 -0
  203. package/framework/tools/lincoln/src/models/runWarmup.ts +46 -0
  204. package/framework/tools/lincoln/src/models/warmupProgress.ts +30 -0
  205. package/framework/tools/lincoln/src/recording/spawnRecorder.ts +127 -0
  206. package/framework/tools/lincoln/src/recording/useRecorder.ts +146 -0
  207. package/framework/tools/lincoln/src/utils/amplitudeToMeter.ts +4 -0
  208. package/framework/tools/lincoln/src/utils/formatDuration.ts +6 -0
  209. package/framework/tools/lincoln/src/utils/sessionId.ts +25 -0
  210. package/framework/tools/lincoln/tests/cli.test.tsx +52 -0
  211. package/framework/tools/lincoln/tests/components/AudioMeter.test.tsx +31 -0
  212. package/framework/tools/lincoln/tests/components/CancelledScreen.test.tsx +12 -0
  213. package/framework/tools/lincoln/tests/components/ReadyScreen.test.tsx +68 -0
  214. package/framework/tools/lincoln/tests/components/RecordingApp.menus.test.tsx +158 -0
  215. package/framework/tools/lincoln/tests/components/RecordingApp.test.tsx +248 -0
  216. package/framework/tools/lincoln/tests/components/RecordingScreen.test.tsx +37 -0
  217. package/framework/tools/lincoln/tests/components/SelectMenu.test.tsx +59 -0
  218. package/framework/tools/lincoln/tests/components/StopConfirmation.test.tsx +38 -0
  219. package/framework/tools/lincoln/tests/config/args.test.ts +49 -0
  220. package/framework/tools/lincoln/tests/config/loadConfig.test.ts +59 -0
  221. package/framework/tools/lincoln/tests/config/resolveConfig.test.ts +72 -0
  222. package/framework/tools/lincoln/tests/devices/parseDevices.test.ts +21 -0
  223. package/framework/tools/lincoln/tests/hooks/useKeyHandler.enabled.test.tsx +44 -0
  224. package/framework/tools/lincoln/tests/hooks/useKeyHandler.test.tsx +130 -0
  225. package/framework/tools/lincoln/tests/models/models.test.ts +27 -0
  226. package/framework/tools/lincoln/tests/models/warmupProgress.test.ts +25 -0
  227. package/framework/tools/lincoln/tests/recording/spawnRecorder.test.ts +85 -0
  228. package/framework/tools/lincoln/tests/recording/useRecorder.test.tsx +172 -0
  229. package/framework/tools/lincoln/tests/utils/amplitudeToMeter.test.ts +22 -0
  230. package/framework/tools/lincoln/tests/utils/formatDuration.test.ts +11 -0
  231. package/framework/tools/lincoln/tests/utils/sessionId.test.ts +30 -0
  232. package/framework/tools/lincoln/tsconfig.json +22 -0
  233. package/framework/tools/lincoln/tsconfig.tests.json +10 -0
  234. package/framework/tools/lincoln/tsup.config.ts +17 -0
  235. package/framework/tools/lincoln/vitest.config.ts +20 -0
  236. package/framework/tools/lincoln-installer/package-lock.json +3611 -0
  237. package/framework/tools/lincoln-installer/package.json +53 -0
  238. package/framework/tools/lincoln-installer/src/cli.tsx +63 -0
  239. package/framework/tools/lincoln-installer/src/components/HarnessSelectScreen.tsx +43 -0
  240. package/framework/tools/lincoln-installer/src/components/InstallerApp.tsx +98 -0
  241. package/framework/tools/lincoln-installer/src/components/OptionsScreen.tsx +56 -0
  242. package/framework/tools/lincoln-installer/src/components/ProgressScreen.tsx +25 -0
  243. package/framework/tools/lincoln-installer/src/components/ResultScreen.tsx +40 -0
  244. package/framework/tools/lincoln-installer/src/components/SelectMenu.tsx +73 -0
  245. package/framework/tools/lincoln-installer/src/components/SummaryScreen.tsx +49 -0
  246. package/framework/tools/lincoln-installer/src/components/UpdateApp.tsx +135 -0
  247. package/framework/tools/lincoln-installer/src/components/WelcomeScreen.tsx +22 -0
  248. package/framework/tools/lincoln-installer/src/config/args.ts +76 -0
  249. package/framework/tools/lincoln-installer/src/config/resolveConfig.ts +33 -0
  250. package/framework/tools/lincoln-installer/src/config/updateArgs.ts +67 -0
  251. package/framework/tools/lincoln-installer/src/hooks/useSetup.ts +149 -0
  252. package/framework/tools/lincoln-installer/src/main.tsx +11 -0
  253. package/framework/tools/lincoln-installer/src/types.ts +19 -0
  254. package/framework/tools/lincoln-installer/src/update-main.tsx +11 -0
  255. package/framework/tools/lincoln-installer/src/update.tsx +90 -0
  256. package/framework/tools/lincoln-installer/src/utils/detectHarness.ts +51 -0
  257. package/framework/tools/lincoln-installer/src/utils/runSetup.ts +62 -0
  258. package/framework/tools/lincoln-installer/src/utils/updateRelease.ts +454 -0
  259. package/framework/tools/lincoln-installer/tests/components/InstallerApp.test.tsx +88 -0
  260. package/framework/tools/lincoln-installer/tests/components/SelectMenu.test.tsx +26 -0
  261. package/framework/tools/lincoln-installer/tests/components/UpdateApp.test.tsx +170 -0
  262. package/framework/tools/lincoln-installer/tests/components/screens.test.tsx +310 -0
  263. package/framework/tools/lincoln-installer/tests/config/args.test.ts +66 -0
  264. package/framework/tools/lincoln-installer/tests/config/resolveConfig.test.ts +75 -0
  265. package/framework/tools/lincoln-installer/tests/config/updateArgs.test.ts +52 -0
  266. package/framework/tools/lincoln-installer/tests/hooks/useSetup.test.tsx +213 -0
  267. package/framework/tools/lincoln-installer/tests/update.test.tsx +108 -0
  268. package/framework/tools/lincoln-installer/tests/utils/detectHarness.test.ts +40 -0
  269. package/framework/tools/lincoln-installer/tests/utils/runSetup.test.ts +45 -0
  270. package/framework/tools/lincoln-installer/tests/utils/updateRelease.test.ts +559 -0
  271. package/framework/tools/lincoln-installer/tsconfig.json +22 -0
  272. package/framework/tools/lincoln-installer/tsconfig.tests.json +8 -0
  273. package/framework/tools/lincoln-installer/tsup.config.ts +17 -0
  274. package/framework/tools/lincoln-installer/vitest.config.ts +20 -0
  275. package/framework/tools/lincoln-record/Cargo.lock +1896 -0
  276. package/framework/tools/lincoln-record/Cargo.toml +24 -0
  277. package/framework/tools/lincoln-record/diarize/diarize.py +117 -0
  278. package/framework/tools/lincoln-record/diarize/merge.py +55 -0
  279. package/framework/tools/lincoln-record/src/audio/capture/microphone.rs +194 -0
  280. package/framework/tools/lincoln-record/src/audio/capture/mod.rs +1 -0
  281. package/framework/tools/lincoln-record/src/audio/devices.rs +20 -0
  282. package/framework/tools/lincoln-record/src/audio/mod.rs +5 -0
  283. package/framework/tools/lincoln-record/src/audio/resampler.rs +44 -0
  284. package/framework/tools/lincoln-record/src/audio/saver.rs +87 -0
  285. package/framework/tools/lincoln-record/src/audio/source.rs +104 -0
  286. package/framework/tools/lincoln-record/src/cli.rs +96 -0
  287. package/framework/tools/lincoln-record/src/config.rs +135 -0
  288. package/framework/tools/lincoln-record/src/lib.rs +6 -0
  289. package/framework/tools/lincoln-record/src/main.rs +242 -0
  290. package/framework/tools/lincoln-record/src/model/mod.rs +248 -0
  291. package/framework/tools/lincoln-record/src/output/metadata.rs +30 -0
  292. package/framework/tools/lincoln-record/src/output/mod.rs +2 -0
  293. package/framework/tools/lincoln-record/src/output/transcript.rs +42 -0
  294. package/framework/tools/lincoln-record/src/transcription/diarization.rs +64 -0
  295. package/framework/tools/lincoln-record/src/transcription/mod.rs +3 -0
  296. package/framework/tools/lincoln-record/src/transcription/provider.rs +43 -0
  297. package/framework/tools/lincoln-record/src/transcription/whisper.rs +176 -0
  298. package/framework/tools/lincoln-record/tests/cli_args.rs +103 -0
  299. package/framework/tools/lincoln-record/tests/config_test.rs +70 -0
  300. package/framework/tools/lincoln-record/tests/devices_test.rs +23 -0
  301. package/framework/tools/lincoln-record/tests/diarization_tests.rs +101 -0
  302. package/framework/tools/lincoln-record/tests/integration_tests.rs +44 -0
  303. package/framework/tools/lincoln-record/tests/model_test.rs +68 -0
  304. package/framework/tools/lincoln-record/tests/transcription_tests.rs +73 -0
  305. package/package.json +19 -13
@@ -0,0 +1,755 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Lincoln workflow validators.
4
+
5
+ Usage:
6
+ python validate.py --phase entry --check file_exists --args path/to/file
7
+ python validate.py --phase exit --check transcript_has_timestamps --args {process_slug}/interviews/session-id
8
+
9
+ Exit code 0 means pass, 1 means fail.
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import re
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ PROJECT_ROOT = Path(__file__).resolve().parents[4]
20
+ if str(PROJECT_ROOT) not in sys.path:
21
+ sys.path.insert(0, str(PROJECT_ROOT))
22
+
23
+ try:
24
+ from scripts.lincoln_paths import default_process_slug
25
+ except Exception: # pragma: no cover - validator can be copied standalone
26
+ default_process_slug = None
27
+
28
+ try:
29
+ from scripts.lincoln_paths import resolve_state_path, load_yaml, get_process_slug
30
+ except Exception: # pragma: no cover - validator can be copied standalone
31
+ resolve_state_path = None # type: ignore[assignment]
32
+ load_yaml = None # type: ignore[assignment]
33
+ get_process_slug = None # type: ignore[assignment]
34
+
35
+ try:
36
+ from scripts.lincoln_documents import extract_markdown_version
37
+ except Exception: # pragma: no cover - validator can be copied standalone
38
+ extract_markdown_version = None # type: ignore[assignment]
39
+
40
+ try:
41
+ from scripts.lincoln_index import extract_html_markdown
42
+ except Exception: # pragma: no cover - validator can be copied standalone
43
+ extract_html_markdown = None # type: ignore[assignment]
44
+
45
+
46
+ def fail(message: str):
47
+ print(f"FAIL: {message}")
48
+ sys.exit(1)
49
+
50
+
51
+ def pass_check(message: str = ""):
52
+ print(f"PASS{' - ' + message if message else ''}")
53
+ sys.exit(0)
54
+
55
+
56
+ def has_any_heading(content: str, aliases: list[str]) -> bool:
57
+ return any(heading in content for heading in aliases)
58
+
59
+
60
+ def missing_heading_groups(content: str, required_groups: dict[str, list[str]]) -> list[str]:
61
+ return [
62
+ label
63
+ for label, aliases in required_groups.items()
64
+ if not has_any_heading(content, aliases)
65
+ ]
66
+
67
+
68
+ def read_flat_yaml(path: Path) -> dict[str, str]:
69
+ data = {}
70
+ for line in path.read_text(encoding="utf-8").splitlines():
71
+ stripped = line.strip()
72
+ if not stripped or stripped.startswith("#") or ":" not in stripped:
73
+ continue
74
+ key, value = stripped.split(":", 1)
75
+ data[key.strip()] = value.strip().strip("'\"")
76
+ return data
77
+
78
+
79
+ _state_file_override: Path | None = None
80
+ _state_cache: dict[str, Any] | None = None
81
+
82
+
83
+ def set_state_file(path: Path | None) -> None:
84
+ global _state_file_override, _state_cache
85
+ _state_file_override = path
86
+ _state_cache = None
87
+
88
+
89
+ def load_state() -> dict[str, Any] | None:
90
+ global _state_cache
91
+ if _state_cache is not None:
92
+ return _state_cache
93
+ state_file = _state_file_override
94
+ if state_file is None and resolve_state_path is not None:
95
+ state_file = resolve_state_path(None, PROJECT_ROOT)
96
+ if state_file is None or not state_file.exists():
97
+ return None
98
+ if load_yaml is None:
99
+ return None
100
+ try:
101
+ _state_cache = load_yaml(state_file)
102
+ except Exception:
103
+ return None
104
+ return _state_cache
105
+
106
+
107
+ def process_slug() -> str:
108
+ import os
109
+
110
+ env_slug = os.environ.get("LINCOLN_PROCESS_SLUG")
111
+ if env_slug:
112
+ return env_slug
113
+
114
+ state = load_state()
115
+ if state is not None and get_process_slug is not None:
116
+ try:
117
+ return get_process_slug(state, _state_file_override)
118
+ except Exception:
119
+ pass
120
+
121
+ if default_process_slug:
122
+ return default_process_slug(PROJECT_ROOT)
123
+ return "lc-process"
124
+
125
+
126
+ def process_root() -> Path:
127
+ slug = process_slug()
128
+ root = PROJECT_ROOT / slug
129
+ if root.exists():
130
+ return root
131
+ return PROJECT_ROOT
132
+
133
+
134
+ def process_path(*parts: str) -> Path:
135
+ return process_root().joinpath(*parts)
136
+
137
+
138
+ def doc_page(name: str) -> Path:
139
+ """Return the path of an HTML doc page under pages/docs/."""
140
+ return process_path("pages", "docs", f"{name}.html")
141
+
142
+
143
+ def read_document_file(path: Path) -> str:
144
+ """Return document text; HTML doc pages yield their embedded Markdown source."""
145
+ if path.suffix.lower() == ".html" and extract_html_markdown is not None:
146
+ return extract_html_markdown(path)
147
+ return path.read_text(encoding="utf-8")
148
+
149
+
150
+ def design_base(design_id: str) -> Path:
151
+ if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", design_id):
152
+ fail(f"Invalid design_id '{design_id}'. Use kebab-case, e.g. checkout-redesign")
153
+ return process_path("designs", design_id)
154
+
155
+
156
+ def require_nonempty_file(path: Path, label: str):
157
+ if not path.exists() or not path.is_file() or path.stat().st_size == 0:
158
+ fail(f"{label} missing or empty: {path}")
159
+
160
+
161
+ def read_required_file(path: Path, label: str) -> str:
162
+ require_nonempty_file(path, label)
163
+ return read_document_file(path)
164
+
165
+
166
+ def has_approval_marker(content: str, zh_label: str) -> bool:
167
+ return "<!-- status: approved -->" in content or f"[x] PM 已确认{zh_label}" in content
168
+
169
+
170
+ # ---------------------------------------------------------------------------
171
+ # Entry checks
172
+ # ---------------------------------------------------------------------------
173
+
174
+ def check_file_exists(path: str):
175
+ target = PROJECT_ROOT / path
176
+ if not target.exists():
177
+ fail(f"File does not exist: {target}")
178
+ pass_check(str(target))
179
+
180
+
181
+ def check_audio_format_supported(path: str):
182
+ supported = {".mp3", ".m4a", ".wav", ".mp4", ".mov"}
183
+ ext = Path(path).suffix.lower()
184
+ if ext not in supported:
185
+ fail(f"Unsupported audio format: {ext}. Supported: {', '.join(supported)}")
186
+ pass_check(ext)
187
+
188
+
189
+ def check_summary_ready(session_id: str):
190
+ summary = process_path("interviews", session_id, "summary.md")
191
+ if not summary.exists() or summary.stat().st_size == 0:
192
+ fail(f"Summary not ready: {summary}")
193
+ pass_check(str(summary))
194
+
195
+
196
+ def check_requirements_approved(session_id: str):
197
+ req = doc_page("requirements")
198
+ if not req.exists():
199
+ fail(f"Requirements document missing: {req}")
200
+ content = read_document_file(req)
201
+ if "<!-- status: approved -->" not in content and "[x] PM 已确认" not in content:
202
+ fail("Requirements document exists but is not marked as approved")
203
+ pass_check(str(req))
204
+
205
+
206
+ def check_openspec_tasks_ready(change_name: str):
207
+ tasks = process_path("openspec", "changes", change_name, "tasks.md")
208
+ if not tasks.exists() or tasks.stat().st_size == 0:
209
+ fail(f"OpenSpec tasks not ready: {tasks}")
210
+ content = tasks.read_text(encoding="utf-8")
211
+ if not re.search(r"[-*]\s+\[.?\]", content):
212
+ fail("OpenSpec tasks.md does not contain a recognizable task list")
213
+ pass_check(str(tasks))
214
+
215
+
216
+ def check_issues_ready(session_id: str):
217
+ linked = PROJECT_ROOT / ".github" / "linked-issues.yaml"
218
+ if not linked.exists():
219
+ fail("Linked issues file missing; run split-to-github first")
220
+ pass_check(str(linked))
221
+
222
+
223
+ def check_pr_merged(pr_number: str):
224
+ queue_file = PROJECT_ROOT / ".github" / "lincoln-sync-queue" / f"pr-{pr_number}.yaml"
225
+ if not queue_file.exists():
226
+ fail(f"PR {pr_number} sync queue file missing: {queue_file}")
227
+
228
+ data = read_flat_yaml(queue_file)
229
+ required = ["status", "repository", "issue_number", "pr_number", "merged_at"]
230
+ missing = [key for key in required if not data.get(key)]
231
+ if missing:
232
+ fail(f"PR sync queue file missing fields: {', '.join(missing)}")
233
+ if data["pr_number"] != str(pr_number):
234
+ fail(f"PR sync queue file is for PR {data['pr_number']}, expected {pr_number}")
235
+ if data["status"] != "pending":
236
+ fail(f"PR {pr_number} sync status is {data['status']}, expected pending")
237
+ pass_check(str(queue_file))
238
+
239
+
240
+ def check_issue_exists(issue_number: str):
241
+ linked = PROJECT_ROOT / ".github" / "linked-issues.yaml"
242
+ if not linked.exists():
243
+ fail("Linked issues file missing")
244
+ content = linked.read_text(encoding="utf-8")
245
+ if issue_number not in content:
246
+ fail(f"Issue {issue_number} not found in linked issues")
247
+ pass_check(f"Issue {issue_number} linked")
248
+
249
+
250
+ def check_design_docs_ready(design_id: str):
251
+ validate_design_docs_complete(design_id)
252
+ pass_check(f"Design docs ready: {design_id}")
253
+
254
+
255
+ def check_product_design_approved(design_id: str):
256
+ validate_design_docs_complete(design_id)
257
+ content = read_required_file(doc_page("design-review"), "Design review")
258
+ if not has_approval_marker(content, "设计文档"):
259
+ fail("Product design docs are not marked as approved")
260
+ pass_check(f"Product design approved: {design_id}")
261
+
262
+
263
+ def check_prototype_ready(design_id: str):
264
+ validate_prototype_artifact_complete(design_id)
265
+ content = read_required_file(doc_page("ui-spec"), "UI spec")
266
+ if "<!-- prototype-status: approved -->" not in content and "[x] PM 已确认原型" not in content:
267
+ fail("Prototype is not marked as approved")
268
+ pass_check(f"Prototype ready: {design_id}")
269
+
270
+
271
+ def check_tdd_plan_ready(design_id: str):
272
+ validate_tdd_plan_complete(design_id)
273
+ content = read_required_file(doc_page("tdd-plan"), "TDD plan")
274
+ if "<!-- status: ready-for-openspec -->" not in content:
275
+ fail("TDD plan is not marked as ready for OpenSpec")
276
+ pass_check(f"TDD plan ready: {design_id}")
277
+
278
+
279
+ # ---------------------------------------------------------------------------
280
+ # Exit checks
281
+ # ---------------------------------------------------------------------------
282
+
283
+ def check_transcript_has_timestamps(session_id: str):
284
+ transcript = process_path("interviews", session_id, "transcript.md")
285
+ if not transcript.exists():
286
+ fail(f"Transcript missing: {transcript}")
287
+ content = transcript.read_text(encoding="utf-8")
288
+ if not re.search(r"\d{2}:\d{2}:\d{2}", content):
289
+ fail("Transcript does not contain timestamps")
290
+ pass_check()
291
+
292
+
293
+ def check_summary_has_key_topics(session_id: str):
294
+ summary = process_path("interviews", session_id, "summary.md")
295
+ if not summary.exists():
296
+ fail(f"Summary missing: {summary}")
297
+ content = summary.read_text(encoding="utf-8")
298
+ required = {
299
+ "关键主题": ["关键主题", "Key topics", "Key Topics"],
300
+ "决策": ["决策", "Decisions"],
301
+ "行动项": ["行动项", "Action items", "Action Items"],
302
+ }
303
+ missing = missing_heading_groups(content, required)
304
+ if missing:
305
+ fail(f"Summary missing sections: {', '.join(missing)}")
306
+ pass_check()
307
+
308
+
309
+ def check_requirements_has_background_problem_solution_acceptance(session_id: str):
310
+ req = doc_page("requirements")
311
+ if not req.exists():
312
+ fail(f"Requirements missing: {req}")
313
+ content = read_document_file(req)
314
+ required = {
315
+ "背景": ["背景", "Background"],
316
+ "问题": ["问题", "Problem"],
317
+ "用户": ["用户", "Users", "Personas"],
318
+ "方案": ["方案", "Proposed Solution", "Solution"],
319
+ "验收标准": ["验收标准", "Acceptance Criteria"],
320
+ }
321
+ missing = missing_heading_groups(content, required)
322
+ if missing:
323
+ fail(f"Requirements missing sections: {', '.join(missing)}")
324
+ pass_check()
325
+
326
+
327
+ def check_human_approved(session_id: str):
328
+ req = doc_page("requirements")
329
+ if not req.exists():
330
+ fail(f"Requirements missing: {req}")
331
+ content = read_document_file(req)
332
+ if "<!-- status: approved -->" not in content and "[x] PM 已确认" not in content:
333
+ fail("Human PM approval marker not found")
334
+ pass_check()
335
+
336
+
337
+ def check_openspec_artifact_complete(change_name: str, design_id: str = ""):
338
+ base = process_path("openspec", "changes", change_name)
339
+ required_files = ["proposal.md", "design.md", "tasks.md"]
340
+ required_dirs = ["specs"]
341
+ for f in required_files:
342
+ p = base / f
343
+ if not p.exists() or p.stat().st_size == 0:
344
+ fail(f"OpenSpec artifact missing or empty: {p}")
345
+ for d in required_dirs:
346
+ p = base / d
347
+ if not p.exists() or not any(p.iterdir()):
348
+ fail(f"OpenSpec artifact directory missing or empty: {p}")
349
+ if design_id:
350
+ slug = process_slug()
351
+ required_refs = [
352
+ f"{slug}/pages/docs/tdd-plan.html",
353
+ f"{slug}/pages/prototype/",
354
+ f"{slug}/pages/docs/design-review.html",
355
+ ]
356
+ combined = "\n".join((base / f).read_text(encoding="utf-8") for f in required_files)
357
+ missing_refs = [ref for ref in required_refs if ref not in combined]
358
+ if missing_refs:
359
+ fail(f"OpenSpec artifact missing design references: {', '.join(missing_refs)}")
360
+ pass_check()
361
+
362
+
363
+ def check_tasks_extracted(change_name: str):
364
+ tasks = process_path("openspec", "changes", change_name, "tasks.md")
365
+ if not tasks.exists():
366
+ fail(f"Tasks file missing: {tasks}")
367
+ content = tasks.read_text(encoding="utf-8")
368
+ matches = re.findall(r"[-*]\s+\[.?\]\s+(.+)", content)
369
+ if len(matches) < 1:
370
+ fail("No tasks extracted from OpenSpec tasks.md")
371
+ pass_check(f"{len(matches)} tasks found")
372
+
373
+
374
+ def check_issues_created(session_id: str):
375
+ linked = PROJECT_ROOT / ".github" / "linked-issues.yaml"
376
+ if not linked.exists():
377
+ fail("Linked issues file missing")
378
+ content = linked.read_text(encoding="utf-8")
379
+ if not re.search(r"issue_number:\s*\d+", content):
380
+ fail("No issue numbers recorded")
381
+ pass_check()
382
+
383
+
384
+ def check_tasks_link_back_to_issues(session_id: str):
385
+ linked = PROJECT_ROOT / ".github" / "linked-issues.yaml"
386
+ req = doc_page("requirements")
387
+ if not linked.exists():
388
+ fail("Linked issues file missing")
389
+ if not req.exists():
390
+ fail("Requirements file missing")
391
+ req_content = read_document_file(req)
392
+ issues = re.findall(r"issue_number:\s*(\d+)", linked.read_text(encoding="utf-8"))
393
+ missing = [i for i in issues if f"#{i}" not in req_content]
394
+ if missing:
395
+ fail(f"Requirements do not link back to issues: {', '.join(missing)}")
396
+ pass_check()
397
+
398
+
399
+ def check_feature_doc_has_business_and_technical_sections(feature_slug: str):
400
+ doc = PROJECT_ROOT / "docs" / "knowledge" / "03-features" / f"{feature_slug}.md"
401
+ if not doc.exists():
402
+ fail(f"Feature doc missing: {doc}")
403
+ content = doc.read_text(encoding="utf-8")
404
+ required = {
405
+ "业务知识": ["业务知识", "Business Knowledge"],
406
+ "技术知识": ["技术知识", "Technical Knowledge"],
407
+ }
408
+ missing = missing_heading_groups(content, required)
409
+ if missing:
410
+ fail(f"Feature doc missing sections: {', '.join(missing)}")
411
+ pass_check()
412
+
413
+
414
+ def check_feature_doc_has_links(feature_slug: str):
415
+ doc = PROJECT_ROOT / "docs" / "knowledge" / "03-features" / f"{feature_slug}.md"
416
+ if not doc.exists():
417
+ fail(f"Feature doc missing: {doc}")
418
+ content = doc.read_text(encoding="utf-8")
419
+ links = re.findall(r"\[\[([^\]]+)\]\]", content)
420
+ if len(links) < 3:
421
+ fail(f"Feature doc has too few wikilinks: {len(links)}")
422
+ pass_check(f"{len(links)} wikilinks found")
423
+
424
+
425
+ def check_no_conflict_with_existing_knowledge(feature_slug: str):
426
+ # Placeholder for semantic conflict detection.
427
+ # v1: ensure no other feature doc has the same ID in frontmatter.
428
+ doc = PROJECT_ROOT / "docs" / "knowledge" / "03-features" / f"{feature_slug}.md"
429
+ if not doc.exists():
430
+ fail(f"Feature doc missing: {doc}")
431
+ content = doc.read_text(encoding="utf-8")
432
+ id_match = re.search(r"^id:\s*(.+)$", content, re.MULTILINE)
433
+ if not id_match:
434
+ pass_check("No id in frontmatter; skipping conflict check")
435
+ feat_id = id_match.group(1).strip()
436
+ features_dir = PROJECT_ROOT / "docs" / "knowledge" / "03-features"
437
+ conflicts = []
438
+ for other in features_dir.glob("*.md"):
439
+ if other.name == doc.name:
440
+ continue
441
+ other_content = other.read_text(encoding="utf-8")
442
+ other_id_match = re.search(r"^id:\s*(.+)$", other_content, re.MULTILINE)
443
+ if other_id_match and other_id_match.group(1).strip() == feat_id:
444
+ conflicts.append(other.name)
445
+ if conflicts:
446
+ fail(f"Conflicting feature ID found in: {', '.join(conflicts)}")
447
+ pass_check()
448
+
449
+
450
+ def validate_design_docs_complete(design_id: str):
451
+ docs = [
452
+ "design-review.html",
453
+ "scenarios.html",
454
+ "feature-catalog.html",
455
+ "data-model.html",
456
+ "flows.html",
457
+ "feasibility.html",
458
+ ]
459
+ for doc in docs:
460
+ require_nonempty_file(process_path("pages", "docs", doc), doc)
461
+
462
+ review = read_required_file(doc_page("design-review"), "Design review")
463
+ missing_links = [doc for doc in docs[1:] if doc not in review]
464
+ if missing_links:
465
+ fail(f"Design review missing links to: {', '.join(missing_links)}")
466
+
467
+ flows = read_required_file(doc_page("flows"), "Flows")
468
+ if "```mermaid" not in flows:
469
+ fail("flows.html must contain at least one Mermaid diagram")
470
+
471
+ feature_catalog = read_required_file(doc_page("feature-catalog"), "Feature catalog")
472
+ if not has_any_heading(feature_catalog, ["验收", "Acceptance"]):
473
+ fail("feature-catalog.html must map features to acceptance criteria")
474
+
475
+ data_model = read_required_file(doc_page("data-model"), "Data model")
476
+ if not has_any_heading(data_model, ["字段", "Field", "约束", "Constraint"]):
477
+ fail("data-model.html must describe fields or constraints")
478
+
479
+ feasibility = read_required_file(doc_page("feasibility"), "Feasibility")
480
+ required = {
481
+ "业务可行性": ["业务可行性", "Business feasibility"],
482
+ "技术可行性": ["技术可行性", "Technical feasibility"],
483
+ "开源项目": ["开源项目", "Open-source", "Open Source"],
484
+ "技术框架": ["技术框架", "Framework"],
485
+ }
486
+ missing = missing_heading_groups(feasibility, required)
487
+ if missing:
488
+ fail(f"feasibility.html missing sections: {', '.join(missing)}")
489
+
490
+
491
+ def check_design_docs_complete(design_id: str):
492
+ validate_design_docs_complete(design_id)
493
+ pass_check(f"Design docs complete: {design_id}")
494
+
495
+
496
+ def check_design_docs_human_approved(design_id: str):
497
+ validate_design_docs_complete(design_id)
498
+ content = read_required_file(doc_page("design-review"), "Design review")
499
+ if not has_approval_marker(content, "设计文档"):
500
+ fail("Design docs approval marker not found")
501
+ pass_check(f"Design docs approved: {design_id}")
502
+
503
+
504
+ def validate_prototype_artifact_complete(design_id: str):
505
+ fields = read_required_file(doc_page("fields"), "Fields")
506
+ ui_spec = read_required_file(doc_page("ui-spec"), "UI spec")
507
+
508
+ prototype_dir = process_path("pages", "prototype")
509
+ prototype_files = (
510
+ [p for p in prototype_dir.rglob("*") if p.is_file() and p.name != ".gitkeep"]
511
+ if prototype_dir.exists()
512
+ else []
513
+ )
514
+ if not prototype_files:
515
+ fail(f"Prototype artifact missing: no files under {prototype_dir}")
516
+
517
+ html_files = [p for p in prototype_files if p.suffix.lower() == ".html"]
518
+ missing_nav_label = [
519
+ str(p.relative_to(process_root()))
520
+ for p in html_files
521
+ if '<meta name="nav-label"' not in p.read_text(encoding="utf-8")
522
+ ]
523
+ if missing_nav_label:
524
+ fail(f"Prototype pages missing nav-label meta: {', '.join(missing_nav_label)}")
525
+
526
+ required_annotation_metas = [
527
+ ("doc-purpose", 'doc-purpose'),
528
+ ("doc-layout", 'doc-layout'),
529
+ ("doc-fields", 'doc-fields'),
530
+ ("doc-boundaries", 'doc-boundaries'),
531
+ ("doc-exceptions", 'doc-exceptions'),
532
+ ]
533
+ annotation_gaps: dict[str, list[str]] = {}
534
+ for p in html_files:
535
+ text = p.read_text(encoding="utf-8")
536
+ missing = [name for label, name in required_annotation_metas if f'<meta name="{name}"' not in text]
537
+ if missing:
538
+ annotation_gaps[str(p.relative_to(process_root()))] = missing
539
+ if annotation_gaps:
540
+ messages = [f"{path}: missing {', '.join(metas)}" for path, metas in annotation_gaps.items()]
541
+ fail(f"Prototype pages missing required annotation meta tags:\n" + "\n".join(messages))
542
+
543
+ # Anti-pattern: in-app tray redraw. The system tray is portal-level chrome.
544
+ in_app_tray_markers = [
545
+ 'class="menubar"',
546
+ 'class="tray-icon"',
547
+ 'class="tray-panel"',
548
+ "bindTray(",
549
+ "trayMenu(",
550
+ ]
551
+ in_app_tray_offenders = []
552
+ for p in html_files:
553
+ text = p.read_text(encoding="utf-8")
554
+ if any(marker in text for marker in in_app_tray_markers):
555
+ in_app_tray_offenders.append(str(p.relative_to(process_root())))
556
+ if in_app_tray_offenders:
557
+ fail(
558
+ "Prototype pages redraw the system tray inside the app (anti-pattern). "
559
+ "Tray scenarios must be thin controllers that postMessage 'lincoln-tray-state' to the portal. "
560
+ "Offending pages: " + ", ".join(in_app_tray_offenders)
561
+ )
562
+
563
+ app_dir = prototype_dir / "app"
564
+ app_html_files = (
565
+ [p for p in app_dir.rglob("*.html") if p.is_file()]
566
+ if app_dir.exists()
567
+ else []
568
+ )
569
+ if app_html_files:
570
+ tray_dir = app_dir / "tray"
571
+ tray_pages = [p for p in tray_dir.rglob("*.html") if p.is_file()] if tray_dir.exists() else []
572
+ if not tray_pages:
573
+ fail("App-shell prototype detected but no tray scenario page found under pages/prototype/app/tray/")
574
+ for tp in tray_pages:
575
+ text = tp.read_text(encoding="utf-8")
576
+ if 'lincoln-tray-state' not in text:
577
+ fail(
578
+ f"Tray scenario page must postMessage 'lincoln-tray-state' to the portal: "
579
+ f"{tp.relative_to(process_root())}"
580
+ )
581
+
582
+ required_fields = {
583
+ "字段": ["字段", "Field"],
584
+ "校验": ["校验", "Validation"],
585
+ "错误状态": ["错误状态", "Error"],
586
+ }
587
+ missing_fields = missing_heading_groups(fields, required_fields)
588
+ if missing_fields:
589
+ fail(f"fields.html missing sections: {', '.join(missing_fields)}")
590
+
591
+ required_ui = {
592
+ "界面": ["界面", "Screen", "UI"],
593
+ "交互": ["交互", "Interaction"],
594
+ "状态": ["状态", "State"],
595
+ }
596
+ missing_ui = missing_heading_groups(ui_spec, required_ui)
597
+ if missing_ui:
598
+ fail(f"ui-spec.html missing sections: {', '.join(missing_ui)}")
599
+
600
+
601
+ def check_prototype_artifact_complete(design_id: str):
602
+ validate_prototype_artifact_complete(design_id)
603
+ pass_check(f"Prototype artifacts complete: {design_id}")
604
+
605
+
606
+ def validate_tdd_plan_complete(design_id: str):
607
+ content = read_required_file(doc_page("tdd-plan"), "TDD plan")
608
+ required = {
609
+ "验收映射": ["验收映射", "Acceptance mapping", "验收标准映射"],
610
+ "测试场景": ["测试场景", "Test scenarios"],
611
+ "红绿重构": ["红/绿/重构", "红绿重构", "Red/Green/Refactor"],
612
+ "任务切片": ["任务切片", "Task slices"],
613
+ "回归范围": ["回归范围", "Regression"],
614
+ }
615
+ missing = missing_heading_groups(content, required)
616
+ if missing:
617
+ fail(f"tdd-plan.html missing sections: {', '.join(missing)}")
618
+ slug = process_slug()
619
+ required_refs = [
620
+ f"{slug}/pages/docs/requirements.html",
621
+ f"{slug}/pages/docs/design-review.html",
622
+ f"{slug}/pages/docs/fields.html",
623
+ f"{slug}/pages/docs/ui-spec.html",
624
+ f"{slug}/pages/prototype/",
625
+ ]
626
+ missing_refs = [ref for ref in required_refs if ref not in content]
627
+ if missing_refs:
628
+ fail(f"tdd-plan.html missing source references: {', '.join(missing_refs)}")
629
+
630
+
631
+ def check_tdd_plan_complete(design_id: str):
632
+ validate_tdd_plan_complete(design_id)
633
+ pass_check(f"TDD plan complete: {design_id}")
634
+
635
+
636
+ # ---------------------------------------------------------------------------
637
+ # PRD checks
638
+ # ---------------------------------------------------------------------------
639
+
640
+ REQUIRED_PRD_SECTIONS = [
641
+ "## 1. 需求背景",
642
+ "## 2. 用户故事",
643
+ "## 3. 功能拆解",
644
+ "## 4. 业务流程图",
645
+ "## 5. 验收标准",
646
+ "## 6. 业务规则",
647
+ "## 7. 非功能需求",
648
+ "## 8. 关联系统/接口",
649
+ "## 9. 相关产物链接",
650
+ "## 10. 风险与开放问题",
651
+ ]
652
+
653
+
654
+ def check_prd_has_required_sections(path: str):
655
+ target = PROJECT_ROOT / path
656
+ if not target.exists():
657
+ fail(f"PRD missing: {target}")
658
+
659
+ text = read_document_file(target)
660
+ missing = [section for section in REQUIRED_PRD_SECTIONS if section not in text]
661
+ if missing:
662
+ fail(f"PRD missing required sections: {', '.join(missing)}")
663
+
664
+ pass_check("PRD has all required sections")
665
+
666
+
667
+ def check_prd_snapshot_present(path: str):
668
+ target = PROJECT_ROOT / path
669
+ if not target.exists():
670
+ fail(f"PRD missing: {target}")
671
+
672
+ if extract_markdown_version is None:
673
+ fail("markdown version extraction is not available")
674
+
675
+ version = extract_markdown_version(target)
676
+ if not version:
677
+ fail(f"PRD missing version marker: {target}")
678
+
679
+ if target.suffix.lower() == ".html":
680
+ snapshot_path = target.parent / "snapshots" / f"prd-{version}.html"
681
+ else:
682
+ snapshot_path = target.with_name(f"prd-{version}.md")
683
+ if not snapshot_path.exists():
684
+ fail(
685
+ f"PRD snapshot missing: {snapshot_path}. "
686
+ "Run 'python scripts/lincoln_prd.py freeze' after approval."
687
+ )
688
+
689
+ pass_check(f"PRD snapshot present: {snapshot_path}")
690
+
691
+
692
+ # ---------------------------------------------------------------------------
693
+ # Registry
694
+ # ---------------------------------------------------------------------------
695
+
696
+ ENTRY_CHECKS = {
697
+ "file_exists": check_file_exists,
698
+ "audio_format_supported": check_audio_format_supported,
699
+ "summary_ready": check_summary_ready,
700
+ "requirements_approved": check_requirements_approved,
701
+ "openspec_tasks_ready": check_openspec_tasks_ready,
702
+ "issues_ready": check_issues_ready,
703
+ "pr_merged": check_pr_merged,
704
+ "issue_exists": check_issue_exists,
705
+ "design_docs_ready": check_design_docs_ready,
706
+ "product_design_approved": check_product_design_approved,
707
+ "prototype_ready": check_prototype_ready,
708
+ "tdd_plan_ready": check_tdd_plan_ready,
709
+ }
710
+
711
+ EXIT_CHECKS = {
712
+ "transcript_has_timestamps": check_transcript_has_timestamps,
713
+ "summary_has_key_topics": check_summary_has_key_topics,
714
+ "requirements_has_background_problem_solution_acceptance": check_requirements_has_background_problem_solution_acceptance,
715
+ "human_approved": check_human_approved,
716
+ "openspec_artifact_complete": check_openspec_artifact_complete,
717
+ "tasks_extracted": check_tasks_extracted,
718
+ "issues_created": check_issues_created,
719
+ "tasks_link_back_to_issues": check_tasks_link_back_to_issues,
720
+ "feature_doc_has_business_and_technical_sections": check_feature_doc_has_business_and_technical_sections,
721
+ "feature_doc_has_links": check_feature_doc_has_links,
722
+ "no_conflict_with_existing_knowledge": check_no_conflict_with_existing_knowledge,
723
+ "design_docs_complete": check_design_docs_complete,
724
+ "design_docs_human_approved": check_design_docs_human_approved,
725
+ "prototype_artifact_complete": check_prototype_artifact_complete,
726
+ "tdd_plan_complete": check_tdd_plan_complete,
727
+ "prd_has_required_sections": check_prd_has_required_sections,
728
+ "prd_snapshot_present": check_prd_snapshot_present,
729
+ }
730
+
731
+
732
+ def main():
733
+ parser = argparse.ArgumentParser(description="Lincoln workflow validators")
734
+ parser.add_argument("--phase", required=True, choices=["entry", "exit"])
735
+ parser.add_argument("--check", required=True)
736
+ parser.add_argument("--args", default="", help="Comma-separated arguments for the check")
737
+ parser.add_argument("--state-file", type=Path, default=None, help="Path to workflow state file")
738
+ args = parser.parse_args()
739
+
740
+ set_state_file(args.state_file)
741
+
742
+ registry = ENTRY_CHECKS if args.phase == "entry" else EXIT_CHECKS
743
+ check_fn = registry.get(args.check)
744
+ if not check_fn:
745
+ fail(f"Unknown check: {args.check}. Available: {', '.join(registry.keys())}")
746
+
747
+ check_args = [a.strip() for a in args.args.split(",")] if args.args else []
748
+ try:
749
+ check_fn(*check_args)
750
+ except TypeError as e:
751
+ fail(f"Invalid arguments for check '{args.check}': {e}")
752
+
753
+
754
+ if __name__ == "__main__":
755
+ main()