@sushanglewis/lincoln 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.d.ts +3 -0
- package/dist/main.js +1231 -906
- package/dist/main.js.map +1 -1
- package/dist/postinstall.d.ts +3 -0
- package/dist/postinstall.js +18 -0
- package/dist/postinstall.js.map +1 -0
- package/framework/.claude/agents/_contract.md +79 -0
- package/framework/.claude/agents/architect.md +31 -0
- package/framework/.claude/agents/default.md +151 -0
- package/framework/.claude/agents/designer.md +33 -0
- package/framework/.claude/agents/engineer.md +42 -0
- package/framework/.claude/agents/external/NOTICES.md +12 -0
- package/framework/.claude/agents/external/everything-claude-code.manifest.yaml +23 -0
- package/framework/.claude/agents/external/oh-my-claudecode.manifest.yaml +26 -0
- package/framework/.claude/agents/external/wshobson-agents.manifest.yaml +23 -0
- package/framework/.claude/agents/frontend-engineer.md +32 -0
- package/framework/.claude/agents/knowledge-curator.md +28 -0
- package/framework/.claude/agents/pm.md +38 -0
- package/framework/.claude/agents/qa.md +28 -0
- package/framework/.claude/agents/release-coordinator.md +25 -0
- package/framework/.claude/agents/researcher.md +41 -0
- package/framework/.claude/harnesses/claude-code.yaml +15 -0
- package/framework/.claude/harnesses/codex.yaml +40 -0
- package/framework/.claude/harnesses/command-map.yaml +255 -0
- package/framework/.claude/harnesses/opencode.yaml +32 -0
- package/framework/.claude/harnesses/scenarios.yaml +45 -0
- package/framework/.claude/hooks/on-pr-event.sh +69 -0
- package/framework/.claude/hooks/on-session-start.sh +427 -0
- package/framework/.claude/hooks/on-stop.sh +104 -0
- package/framework/.claude/hooks/post-tool-use.sh +161 -0
- package/framework/.claude/hooks/pre-tool-use.sh +246 -0
- package/framework/.claude/schemas/agent-definition.schema.json +35 -0
- package/framework/.claude/schemas/benchmark-report.schema.json +148 -0
- package/framework/.claude/schemas/pm-to-ux-handoff.schema.yaml +128 -0
- package/framework/.claude/schemas/stage-definition.schema.json +233 -0
- package/framework/.claude/schemas/workflow-stage.schema.json +86 -0
- package/framework/.claude/schemas/workflow-template.schema.json +88 -0
- package/framework/.claude/settings.json +14 -0
- package/framework/.claude/skills/build-product-prototype/SKILL.md +35 -0
- package/framework/.claude/skills/build-product-prototype/prompts/main.md +78 -0
- package/framework/.claude/skills/clarify-requirements/SKILL.md +32 -0
- package/framework/.claude/skills/clarify-requirements/prompts/main.md +81 -0
- package/framework/.claude/skills/dependencies.yaml +139 -0
- package/framework/.claude/skills/draft-product-design/SKILL.md +40 -0
- package/framework/.claude/skills/draft-product-design/prompts/main.md +47 -0
- package/framework/.claude/skills/lc-analyze-frameworks/SKILL.md +43 -0
- package/framework/.claude/skills/lc-analyze-frameworks/prompts/main.md +95 -0
- package/framework/.claude/skills/lc-benchmark/SKILL.md +61 -0
- package/framework/.claude/skills/lc-build-codebase-knowledge/SKILL.md +38 -0
- package/framework/.claude/skills/lc-build-codebase-knowledge/prompts/build-codebase-knowledge.md +61 -0
- package/framework/.claude/skills/lc-collect-intelligence/SKILL.md +43 -0
- package/framework/.claude/skills/lc-collect-intelligence/prompts/main.md +72 -0
- package/framework/.claude/skills/lc-competitive-analysis/SKILL.md +43 -0
- package/framework/.claude/skills/lc-competitive-analysis/prompts/main.md +70 -0
- package/framework/.claude/skills/lc-explore-opensource/SKILL.md +36 -0
- package/framework/.claude/skills/lc-explore-opensource/prompts/explore-opensource.md +77 -0
- package/framework/.claude/skills/lc-first-principles/SKILL.md +43 -0
- package/framework/.claude/skills/lc-first-principles/prompts/main.md +73 -0
- package/framework/.claude/skills/lc-handoff/SKILL.md +34 -0
- package/framework/.claude/skills/lc-init-branch/SKILL.md +52 -0
- package/framework/.claude/skills/lc-market-research/SKILL.md +43 -0
- package/framework/.claude/skills/lc-market-research/prompts/main.md +79 -0
- package/framework/.claude/skills/lc-product-research/SKILL.md +43 -0
- package/framework/.claude/skills/lc-product-research/prompts/main.md +80 -0
- package/framework/.claude/skills/lc-research-report/SKILL.md +43 -0
- package/framework/.claude/skills/lc-research-report/prompts/main.md +90 -0
- package/framework/.claude/skills/lc-research-scope/SKILL.md +43 -0
- package/framework/.claude/skills/lc-research-scope/prompts/main.md +82 -0
- package/framework/.claude/skills/lc-setup/SKILL.md +68 -0
- package/framework/.claude/skills/lc-setup/prompts/main.md +88 -0
- package/framework/.claude/skills/lc-stage/SKILL.md +45 -0
- package/framework/.claude/skills/lc-stakeholder-research/SKILL.md +43 -0
- package/framework/.claude/skills/lc-stakeholder-research/prompts/main.md +65 -0
- package/framework/.claude/skills/lc-status/SKILL.md +25 -0
- package/framework/.claude/skills/lc-storytelling/SKILL.md +43 -0
- package/framework/.claude/skills/lc-storytelling/prompts/main.md +77 -0
- package/framework/.claude/skills/lc-wf/SKILL.md +54 -0
- package/framework/.claude/skills/lc-workflow/SKILL.md +48 -0
- package/framework/.claude/skills/lc-workflow/validators/validate.py +755 -0
- package/framework/.claude/skills/lc-workflow-router/SKILL.md +41 -0
- package/framework/.claude/skills/lc-workflow-router/prompts/intake-prompt.md +91 -0
- package/framework/.claude/skills/lc-workflow-router/prompts/router-prompt.md +44 -0
- package/framework/.claude/skills/plan-tdd-development/SKILL.md +31 -0
- package/framework/.claude/skills/plan-tdd-development/prompts/main.md +44 -0
- package/framework/.claude/skills/process-interview/SKILL.md +32 -0
- package/framework/.claude/skills/process-interview/prompts/main.md +47 -0
- package/framework/.claude/skills/propose-with-openspec/SKILL.md +37 -0
- package/framework/.claude/skills/propose-with-openspec/prompts/main.md +53 -0
- package/framework/.claude/skills/split-to-github/SKILL.md +32 -0
- package/framework/.claude/skills/split-to-github/prompts/main.md +42 -0
- package/framework/.claude/skills/sync-to-knowledge/SKILL.md +35 -0
- package/framework/.claude/skills/sync-to-knowledge/prompts/main.md +54 -0
- package/framework/.claude/skills/workflow-continue/SKILL.md +24 -0
- package/framework/.claude/skills/workflow-continue/prompts/main.md +25 -0
- package/framework/.claude/stages/build-codebase-knowledge.yaml +42 -0
- package/framework/.claude/stages/clarify.yaml +59 -0
- package/framework/.claude/stages/explore-opensource.yaml +40 -0
- package/framework/.claude/stages/implement.yaml +60 -0
- package/framework/.claude/stages/ingest.yaml +40 -0
- package/framework/.claude/stages/lc-analyze-frameworks.yaml +37 -0
- package/framework/.claude/stages/lc-collect-intelligence.yaml +37 -0
- package/framework/.claude/stages/lc-competitive-analysis.yaml +37 -0
- package/framework/.claude/stages/lc-first-principles.yaml +37 -0
- package/framework/.claude/stages/lc-market-research.yaml +37 -0
- package/framework/.claude/stages/lc-product-research.yaml +37 -0
- package/framework/.claude/stages/lc-research-report.yaml +38 -0
- package/framework/.claude/stages/lc-research-scope.yaml +38 -0
- package/framework/.claude/stages/lc-stakeholder-research.yaml +37 -0
- package/framework/.claude/stages/lc-storytelling.yaml +37 -0
- package/framework/.claude/stages/product-design-docs.yaml +66 -0
- package/framework/.claude/stages/product-prototype.yaml +53 -0
- package/framework/.claude/stages/propose.yaml +45 -0
- package/framework/.claude/stages/split.yaml +41 -0
- package/framework/.claude/stages/sync-knowledge.yaml +49 -0
- package/framework/.claude/stages/tdd-development-plan.yaml +43 -0
- package/framework/.claude/stages/workflow-router.yaml +45 -0
- package/framework/.claude/templates/solo-workflow-context.yaml +23 -0
- package/framework/.claude/workflows/README.md +93 -0
- package/framework/.claude/workflows/bug-fix.yaml +81 -0
- package/framework/.claude/workflows/design-spike.yaml +55 -0
- package/framework/.claude/workflows/existing-project-iteration.yaml +106 -0
- package/framework/.claude/workflows/interview-to-knowledge.yaml +189 -0
- package/framework/.claude/workflows/oss-first-design.yaml +100 -0
- package/framework/.claude/workflows/pm-research.yaml +115 -0
- package/framework/.claude-plugin/hooks.json +6 -0
- package/framework/.claude-plugin/marketplace.json +32 -0
- package/framework/.claude-plugin/plugin.json +57 -0
- package/framework/.version-bump.json +43 -0
- package/framework/CLAUDE.md +140 -0
- package/framework/CONTRIBUTING.md +219 -0
- package/framework/LICENSE +21 -0
- package/framework/README.en.md +139 -0
- package/framework/README.md +139 -0
- package/framework/RELEASE.md +334 -0
- package/framework/SKILL.md +24 -0
- package/framework/USAGE.md +356 -0
- package/framework/manifest.json +1214 -0
- package/framework/requirements.txt +4 -0
- package/framework/scripts/__init__.py +0 -0
- package/framework/scripts/build_codebase_knowledge.py +76 -0
- package/framework/scripts/bump_version.py +181 -0
- package/framework/scripts/check-dependency-drift.py +69 -0
- package/framework/scripts/check-harness-drift.sh +57 -0
- package/framework/scripts/check-main-merge-hygiene.py +128 -0
- package/framework/scripts/check-skill-dependencies.sh +124 -0
- package/framework/scripts/init-lincoln-branch.sh +300 -0
- package/framework/scripts/init-project.sh +184 -0
- package/framework/scripts/lc-benchmark-cli.py +66 -0
- package/framework/scripts/lincoln-audit.py +288 -0
- package/framework/scripts/lincoln-setup.py +651 -0
- package/framework/scripts/lincoln-status.py +448 -0
- package/framework/scripts/lincoln_benchmark.py +224 -0
- package/framework/scripts/lincoln_benchmark_eval.py +104 -0
- package/framework/scripts/lincoln_benchmark_metrics.py +818 -0
- package/framework/scripts/lincoln_benchmark_report.py +141 -0
- package/framework/scripts/lincoln_command_map.py +289 -0
- package/framework/scripts/lincoln_dependency_manager.py +335 -0
- package/framework/scripts/lincoln_documents.py +175 -0
- package/framework/scripts/lincoln_harness_adapter.py +319 -0
- package/framework/scripts/lincoln_index.py +255 -0
- package/framework/scripts/lincoln_paths.py +198 -0
- package/framework/scripts/lincoln_prd.py +227 -0
- package/framework/scripts/lincoln_role.py +46 -0
- package/framework/scripts/lincoln_scenario.py +59 -0
- package/framework/scripts/lincoln_session_start_metrics.py +71 -0
- package/framework/scripts/lincoln_skill_prompt.py +48 -0
- package/framework/scripts/lincoln_trace.py +335 -0
- package/framework/scripts/lincoln_workflow.py +199 -0
- package/framework/scripts/list-active-lincoln-branches.sh +87 -0
- package/framework/scripts/package-lincoln-plugin.py +340 -0
- package/framework/scripts/run-infrastructure-tests.py +84 -0
- package/framework/scripts/stage_loader.py +1071 -0
- package/framework/scripts/static-check.sh +356 -0
- package/framework/scripts/sync-external-agents.sh +210 -0
- package/framework/scripts/sync-framework-package.mjs +218 -0
- package/framework/scripts/task_tool_guard.py +213 -0
- package/framework/scripts/track-artifacts.py +140 -0
- package/framework/scripts/validate_stage.py +350 -0
- package/{README.md → framework/tools/lincoln/README.md} +4 -4
- package/framework/tools/lincoln/package-lock.json +3349 -0
- package/framework/tools/lincoln/package.json +48 -0
- package/framework/tools/lincoln/src/cli.tsx +60 -0
- package/framework/tools/lincoln/src/components/AudioMeter.tsx +34 -0
- package/framework/tools/lincoln/src/components/CancelledScreen.tsx +11 -0
- package/framework/tools/lincoln/src/components/DevicesMenu.tsx +79 -0
- package/framework/tools/lincoln/src/components/HintBar.tsx +25 -0
- package/framework/tools/lincoln/src/components/LogView.tsx +36 -0
- package/framework/tools/lincoln/src/components/ModelMenu.tsx +122 -0
- package/framework/tools/lincoln/src/components/ReadyScreen.tsx +54 -0
- package/framework/tools/lincoln/src/components/RecordingApp.tsx +211 -0
- package/framework/tools/lincoln/src/components/RecordingScreen.tsx +41 -0
- package/framework/tools/lincoln/src/components/SelectMenu.tsx +53 -0
- package/framework/tools/lincoln/src/components/StatusPanel.tsx +52 -0
- package/framework/tools/lincoln/src/components/StopConfirmation.tsx +32 -0
- package/framework/tools/lincoln/src/config/args.ts +35 -0
- package/framework/tools/lincoln/src/config/loadConfig.ts +81 -0
- package/framework/tools/lincoln/src/config/resolveConfig.ts +85 -0
- package/framework/tools/lincoln/src/devices/listDevices.ts +11 -0
- package/framework/tools/lincoln/src/devices/parseDevices.ts +25 -0
- package/framework/tools/lincoln/src/hooks/useKeyHandler.ts +63 -0
- package/framework/tools/lincoln/src/main.tsx +4 -0
- package/framework/tools/lincoln/src/models/models.ts +34 -0
- package/framework/tools/lincoln/src/models/runWarmup.ts +46 -0
- package/framework/tools/lincoln/src/models/warmupProgress.ts +30 -0
- package/framework/tools/lincoln/src/recording/spawnRecorder.ts +127 -0
- package/framework/tools/lincoln/src/recording/useRecorder.ts +146 -0
- package/framework/tools/lincoln/src/utils/amplitudeToMeter.ts +4 -0
- package/framework/tools/lincoln/src/utils/formatDuration.ts +6 -0
- package/framework/tools/lincoln/src/utils/sessionId.ts +25 -0
- package/framework/tools/lincoln/tests/cli.test.tsx +52 -0
- package/framework/tools/lincoln/tests/components/AudioMeter.test.tsx +31 -0
- package/framework/tools/lincoln/tests/components/CancelledScreen.test.tsx +12 -0
- package/framework/tools/lincoln/tests/components/ReadyScreen.test.tsx +68 -0
- package/framework/tools/lincoln/tests/components/RecordingApp.menus.test.tsx +158 -0
- package/framework/tools/lincoln/tests/components/RecordingApp.test.tsx +248 -0
- package/framework/tools/lincoln/tests/components/RecordingScreen.test.tsx +37 -0
- package/framework/tools/lincoln/tests/components/SelectMenu.test.tsx +59 -0
- package/framework/tools/lincoln/tests/components/StopConfirmation.test.tsx +38 -0
- package/framework/tools/lincoln/tests/config/args.test.ts +49 -0
- package/framework/tools/lincoln/tests/config/loadConfig.test.ts +59 -0
- package/framework/tools/lincoln/tests/config/resolveConfig.test.ts +72 -0
- package/framework/tools/lincoln/tests/devices/parseDevices.test.ts +21 -0
- package/framework/tools/lincoln/tests/hooks/useKeyHandler.enabled.test.tsx +44 -0
- package/framework/tools/lincoln/tests/hooks/useKeyHandler.test.tsx +130 -0
- package/framework/tools/lincoln/tests/models/models.test.ts +27 -0
- package/framework/tools/lincoln/tests/models/warmupProgress.test.ts +25 -0
- package/framework/tools/lincoln/tests/recording/spawnRecorder.test.ts +85 -0
- package/framework/tools/lincoln/tests/recording/useRecorder.test.tsx +172 -0
- package/framework/tools/lincoln/tests/utils/amplitudeToMeter.test.ts +22 -0
- package/framework/tools/lincoln/tests/utils/formatDuration.test.ts +11 -0
- package/framework/tools/lincoln/tests/utils/sessionId.test.ts +30 -0
- package/framework/tools/lincoln/tsconfig.json +22 -0
- package/framework/tools/lincoln/tsconfig.tests.json +10 -0
- package/framework/tools/lincoln/tsup.config.ts +17 -0
- package/framework/tools/lincoln/vitest.config.ts +20 -0
- package/framework/tools/lincoln-installer/package-lock.json +3611 -0
- package/framework/tools/lincoln-installer/package.json +53 -0
- package/framework/tools/lincoln-installer/src/cli.tsx +63 -0
- package/framework/tools/lincoln-installer/src/components/HarnessSelectScreen.tsx +43 -0
- package/framework/tools/lincoln-installer/src/components/InstallerApp.tsx +98 -0
- package/framework/tools/lincoln-installer/src/components/OptionsScreen.tsx +56 -0
- package/framework/tools/lincoln-installer/src/components/ProgressScreen.tsx +25 -0
- package/framework/tools/lincoln-installer/src/components/ResultScreen.tsx +40 -0
- package/framework/tools/lincoln-installer/src/components/SelectMenu.tsx +73 -0
- package/framework/tools/lincoln-installer/src/components/SummaryScreen.tsx +49 -0
- package/framework/tools/lincoln-installer/src/components/UpdateApp.tsx +135 -0
- package/framework/tools/lincoln-installer/src/components/WelcomeScreen.tsx +22 -0
- package/framework/tools/lincoln-installer/src/config/args.ts +76 -0
- package/framework/tools/lincoln-installer/src/config/resolveConfig.ts +33 -0
- package/framework/tools/lincoln-installer/src/config/updateArgs.ts +67 -0
- package/framework/tools/lincoln-installer/src/hooks/useSetup.ts +149 -0
- package/framework/tools/lincoln-installer/src/main.tsx +11 -0
- package/framework/tools/lincoln-installer/src/types.ts +19 -0
- package/framework/tools/lincoln-installer/src/update-main.tsx +11 -0
- package/framework/tools/lincoln-installer/src/update.tsx +90 -0
- package/framework/tools/lincoln-installer/src/utils/detectHarness.ts +51 -0
- package/framework/tools/lincoln-installer/src/utils/runSetup.ts +62 -0
- package/framework/tools/lincoln-installer/src/utils/updateRelease.ts +454 -0
- package/framework/tools/lincoln-installer/tests/components/InstallerApp.test.tsx +88 -0
- package/framework/tools/lincoln-installer/tests/components/SelectMenu.test.tsx +26 -0
- package/framework/tools/lincoln-installer/tests/components/UpdateApp.test.tsx +170 -0
- package/framework/tools/lincoln-installer/tests/components/screens.test.tsx +310 -0
- package/framework/tools/lincoln-installer/tests/config/args.test.ts +66 -0
- package/framework/tools/lincoln-installer/tests/config/resolveConfig.test.ts +75 -0
- package/framework/tools/lincoln-installer/tests/config/updateArgs.test.ts +52 -0
- package/framework/tools/lincoln-installer/tests/hooks/useSetup.test.tsx +213 -0
- package/framework/tools/lincoln-installer/tests/update.test.tsx +108 -0
- package/framework/tools/lincoln-installer/tests/utils/detectHarness.test.ts +40 -0
- package/framework/tools/lincoln-installer/tests/utils/runSetup.test.ts +45 -0
- package/framework/tools/lincoln-installer/tests/utils/updateRelease.test.ts +559 -0
- package/framework/tools/lincoln-installer/tsconfig.json +22 -0
- package/framework/tools/lincoln-installer/tsconfig.tests.json +8 -0
- package/framework/tools/lincoln-installer/tsup.config.ts +17 -0
- package/framework/tools/lincoln-installer/vitest.config.ts +20 -0
- package/framework/tools/lincoln-record/Cargo.lock +1896 -0
- package/framework/tools/lincoln-record/Cargo.toml +24 -0
- package/framework/tools/lincoln-record/diarize/diarize.py +117 -0
- package/framework/tools/lincoln-record/diarize/merge.py +55 -0
- package/framework/tools/lincoln-record/src/audio/capture/microphone.rs +194 -0
- package/framework/tools/lincoln-record/src/audio/capture/mod.rs +1 -0
- package/framework/tools/lincoln-record/src/audio/devices.rs +20 -0
- package/framework/tools/lincoln-record/src/audio/mod.rs +5 -0
- package/framework/tools/lincoln-record/src/audio/resampler.rs +44 -0
- package/framework/tools/lincoln-record/src/audio/saver.rs +87 -0
- package/framework/tools/lincoln-record/src/audio/source.rs +104 -0
- package/framework/tools/lincoln-record/src/cli.rs +96 -0
- package/framework/tools/lincoln-record/src/config.rs +135 -0
- package/framework/tools/lincoln-record/src/lib.rs +6 -0
- package/framework/tools/lincoln-record/src/main.rs +242 -0
- package/framework/tools/lincoln-record/src/model/mod.rs +248 -0
- package/framework/tools/lincoln-record/src/output/metadata.rs +30 -0
- package/framework/tools/lincoln-record/src/output/mod.rs +2 -0
- package/framework/tools/lincoln-record/src/output/transcript.rs +42 -0
- package/framework/tools/lincoln-record/src/transcription/diarization.rs +64 -0
- package/framework/tools/lincoln-record/src/transcription/mod.rs +3 -0
- package/framework/tools/lincoln-record/src/transcription/provider.rs +43 -0
- package/framework/tools/lincoln-record/src/transcription/whisper.rs +176 -0
- package/framework/tools/lincoln-record/tests/cli_args.rs +103 -0
- package/framework/tools/lincoln-record/tests/config_test.rs +70 -0
- package/framework/tools/lincoln-record/tests/devices_test.rs +23 -0
- package/framework/tools/lincoln-record/tests/diarization_tests.rs +101 -0
- package/framework/tools/lincoln-record/tests/integration_tests.rs +44 -0
- package/framework/tools/lincoln-record/tests/model_test.rs +68 -0
- package/framework/tools/lincoln-record/tests/transcription_tests.rs +73 -0
- 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()
|