@michaelxwb/code-flow 0.8.1 → 1.0.0
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/README.md +73 -152
- package/bin/code-flow.js +12 -0
- package/docs/README.md +19 -0
- package/docs/USAGE.md +156 -1417
- package/docs/v1-design.md +1858 -0
- package/docs/v1-implementation.md +186 -0
- package/lib/adapters.js +126 -0
- package/lib/cli.js +258 -0
- package/lib/config.js +85 -0
- package/lib/doctor.js +283 -0
- package/lib/frontmatter.js +42 -0
- package/lib/fs.js +141 -0
- package/lib/glob.js +23 -0
- package/lib/hook.js +99 -0
- package/lib/init.js +132 -0
- package/lib/learn.js +170 -0
- package/lib/locks.js +52 -0
- package/lib/markdown.js +13 -0
- package/lib/process.js +116 -0
- package/lib/render.js +115 -0
- package/lib/repo.js +48 -0
- package/lib/runs.js +155 -0
- package/lib/session.js +40 -0
- package/lib/spec.js +80 -0
- package/lib/task.js +258 -0
- package/lib/taskdoc.js +308 -0
- package/lib/toml.js +140 -0
- package/lib/verify.js +168 -0
- package/lib/worktree.js +930 -0
- package/package.json +18 -21
- package/templates/adapters/opencode.mjs +65 -0
- package/templates/config.json +39 -0
- package/templates/instructions.md +13 -0
- package/templates/launcher/bridge.cjs +295 -0
- package/templates/launcher/hook.cmd +15 -0
- package/templates/launcher/hook.ps1 +10 -0
- package/templates/launcher/hook.sh +20 -0
- package/templates/skills/cf-check.md +19 -0
- package/templates/skills/cf-init.md +16 -0
- package/templates/skills/cf-spec.md +20 -0
- package/templates/skills/cf-task-align.md +19 -0
- package/templates/skills/cf-task-archive.md +20 -0
- package/templates/skills/cf-task-plan.md +31 -0
- package/templates/skills/cf-task-prd.md +14 -0
- package/templates/skills/cf-task-start.md +41 -0
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-frontend.md +12 -12
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +17 -19
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +12 -12
- package/{src/core/code-flow/specs/shared/prd-template.md → templates/workflow/prd.md} +1 -1
- package/templates/workflow/project-guidelines.md +48 -0
- package/templates/workflow/scenarios.md +67 -0
- package/templates/workflow/task.md +56 -0
- package/docs/acceptance-contract.md +0 -217
- package/docs/ci/acceptance-evidence.md +0 -77
- package/docs/migrations/runtime-0.7.0.md +0 -41
- package/docs/migrations/runtime-0.8.0.md +0 -154
- package/docs/migrations/runtime-0.9.0.md +0 -40
- package/docs/migrations/spec-workflow-0.6.0.md +0 -42
- package/docs/spec-injection-hardening.md +0 -54
- package/docs/spec-injection-performance.json +0 -73
- package/docs/spec-optimization.md +0 -12
- package/src/adapters/claude/CLAUDE.md +0 -45
- package/src/adapters/claude/commands/cf-init.md +0 -262
- package/src/adapters/claude/commands/cf-learn.md +0 -291
- package/src/adapters/claude/commands/cf-spec.md +0 -87
- package/src/adapters/claude/commands/cf-stats.md +0 -96
- package/src/adapters/claude/commands/cf-sync.md +0 -39
- package/src/adapters/claude/commands/cf-task/align.md +0 -331
- package/src/adapters/claude/commands/cf-task/archive.md +0 -155
- package/src/adapters/claude/commands/cf-task/block.md +0 -49
- package/src/adapters/claude/commands/cf-task/graph.md +0 -92
- package/src/adapters/claude/commands/cf-task/note.md +0 -248
- package/src/adapters/claude/commands/cf-task/plan.md +0 -477
- package/src/adapters/claude/commands/cf-task/prd.md +0 -173
- package/src/adapters/claude/commands/cf-task/start.md +0 -317
- package/src/adapters/claude/commands/cf-task/status.md +0 -71
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -148
- package/src/adapters/claude/commands/cf-validate.md +0 -100
- package/src/adapters/claude/settings.local.json +0 -50
- package/src/adapters/codex/AGENTS.md +0 -45
- package/src/adapters/codex/config.toml +0 -3
- package/src/adapters/codex/hooks.json +0 -54
- package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
- package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
- package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -88
- package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -97
- package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -45
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -332
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -156
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -50
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -93
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -245
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -478
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -174
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -318
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -72
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -145
- package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -102
- package/src/adapters/commands.json +0 -115
- package/src/adapters/costrict/CLAUDE.md +0 -45
- package/src/adapters/costrict/commands/cf-init.md +0 -262
- package/src/adapters/costrict/commands/cf-learn.md +0 -291
- package/src/adapters/costrict/commands/cf-spec.md +0 -87
- package/src/adapters/costrict/commands/cf-stats.md +0 -96
- package/src/adapters/costrict/commands/cf-sync.md +0 -39
- package/src/adapters/costrict/commands/cf-task/align.md +0 -331
- package/src/adapters/costrict/commands/cf-task/archive.md +0 -155
- package/src/adapters/costrict/commands/cf-task/block.md +0 -49
- package/src/adapters/costrict/commands/cf-task/graph.md +0 -92
- package/src/adapters/costrict/commands/cf-task/note.md +0 -248
- package/src/adapters/costrict/commands/cf-task/plan.md +0 -477
- package/src/adapters/costrict/commands/cf-task/prd.md +0 -173
- package/src/adapters/costrict/commands/cf-task/start.md +0 -317
- package/src/adapters/costrict/commands/cf-task/status.md +0 -71
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -148
- package/src/adapters/costrict/commands/cf-validate.md +0 -100
- package/src/adapters/costrict/settings.local.json +0 -50
- package/src/adapters/install-plan.json +0 -100
- package/src/adapters/opencode/AGENTS.md +0 -45
- package/src/adapters/opencode/commands/cf-init.md +0 -281
- package/src/adapters/opencode/commands/cf-learn.md +0 -295
- package/src/adapters/opencode/commands/cf-spec.md +0 -91
- package/src/adapters/opencode/commands/cf-stats.md +0 -100
- package/src/adapters/opencode/commands/cf-sync.md +0 -44
- package/src/adapters/opencode/commands/cf-task/align.md +0 -331
- package/src/adapters/opencode/commands/cf-task/archive.md +0 -160
- package/src/adapters/opencode/commands/cf-task/block.md +0 -54
- package/src/adapters/opencode/commands/cf-task/graph.md +0 -97
- package/src/adapters/opencode/commands/cf-task/note.md +0 -253
- package/src/adapters/opencode/commands/cf-task/plan.md +0 -477
- package/src/adapters/opencode/commands/cf-task/prd.md +0 -178
- package/src/adapters/opencode/commands/cf-task/start.md +0 -317
- package/src/adapters/opencode/commands/cf-task/status.md +0 -76
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -148
- package/src/adapters/opencode/commands/cf-validate.md +0 -105
- package/src/adapters/opencode/opencode.json +0 -3
- package/src/adapters/opencode/plugins/code-flow/index.js +0 -162
- package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
- package/src/adapters/platform-bindings.json +0 -370
- package/src/cli.js +0 -1076
- package/src/core/code-flow/config.yml +0 -161
- package/src/core/code-flow/runtime-commands.json +0 -259
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -10
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -10
- package/src/core/code-flow/scripts/cf_audit.py +0 -10
- package/src/core/code-flow/scripts/cf_codex_hook.py +0 -10
- package/src/core/code-flow/scripts/cf_design_lint.py +0 -10
- package/src/core/code-flow/scripts/cf_evidence_import.py +0 -10
- package/src/core/code-flow/scripts/cf_feedback.py +0 -10
- package/src/core/code-flow/scripts/cf_opencode_event.py +0 -10
- package/src/core/code-flow/scripts/cf_post_hook.py +0 -11
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -11
- package/src/core/code-flow/scripts/cf_scenarios.py +0 -10
- package/src/core/code-flow/scripts/cf_spec_context.py +0 -10
- package/src/core/code-flow/scripts/cf_spec_gate.py +0 -10
- package/src/core/code-flow/scripts/cf_spec_session.py +0 -10
- package/src/core/code-flow/scripts/cf_stats.py +0 -10
- package/src/core/code-flow/scripts/cf_stop_hook.py +0 -11
- package/src/core/code-flow/scripts/cf_sync.py +0 -10
- package/src/core/code-flow/scripts/cf_task_index.py +0 -10
- package/src/core/code-flow/scripts/cf_task_parallel.py +0 -10
- package/src/core/code-flow/scripts/cf_task_workflow.py +0 -10
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -11
- package/src/core/code-flow/scripts/cf_validation.py +0 -10
- package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +0 -347
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +0 -660
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +0 -202
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +0 -60
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +0 -73
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +0 -282
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +0 -266
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +0 -172
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +0 -326
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_checks.py +0 -333
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +0 -197
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +0 -174
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +0 -300
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +0 -502
- package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +0 -347
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +0 -1490
- package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +0 -533
- package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +0 -172
- package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +0 -607
- package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +0 -53
- package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +0 -58
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +0 -1175
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +0 -60
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +0 -46
- package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +0 -540
- package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +0 -144
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +0 -177
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +0 -1142
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +0 -742
- package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +0 -31
- package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +0 -67
- package/src/core/code-flow/scripts/codeflow/core/cf_core.py +0 -609
- package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +0 -325
- package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +0 -57
- package/src/core/code-flow/scripts/codeflow/core/cf_git.py +0 -92
- package/src/core/code-flow/scripts/codeflow/core/cf_log.py +0 -160
- package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +0 -77
- package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +0 -26
- package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +0 -33
- package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +0 -32
- package/src/core/code-flow/scripts/codeflow/core/cf_session_state.py +0 -37
- package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +0 -53
- package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +0 -9
- package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +0 -408
- package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +0 -253
- package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +0 -527
- package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +0 -95
- package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +0 -98
- package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +0 -30
- package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +0 -41
- package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +0 -292
- package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +0 -161
- package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +0 -412
- package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +0 -301
- package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +0 -149
- package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +0 -111
- package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +0 -114
- package/src/core/code-flow/scripts/codeflow/spec/cf_scan.py +0 -367
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +0 -55
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +0 -477
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +0 -119
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_metadata.py +0 -387
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_migrate.py +0 -529
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +0 -193
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +0 -187
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_resolver.py +0 -369
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_router.py +0 -304
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_verify.py +0 -687
- package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +0 -330
- package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +0 -83
- package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +0 -755
- package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_transaction.py +0 -110
- package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +0 -867
- package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +0 -178
- package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +0 -635
- package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +0 -251
- package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +0 -570
- package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +0 -73
- package/src/core/code-flow/scripts/runtime-resources.json +0 -29
- package/src/core/code-flow/specs/backend/_map.md +0 -56
- package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
- package/src/core/code-flow/specs/backend/database.md +0 -51
- package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
- package/src/core/code-flow/specs/backend/logging.md +0 -50
- package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
- package/src/core/code-flow/specs/frontend/_map.md +0 -52
- package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
- package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
- package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
- package/src/core/code-flow/specs/shared/_map.md +0 -57
- package/src/core/code-flow/specs/shared/scenarios-template.md +0 -55
- package/src/core/code-flow/tasks/.gitkeep +0 -0
- package/src/core/code-flow/validation.yml +0 -59
- package/src/migrate/runtime.js +0 -213
- package/src/migrate/spec-workflow.js +0 -253
- package/src/runtime.js +0 -219
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
import json
|
|
3
|
-
import glob
|
|
4
|
-
import os
|
|
5
|
-
import re
|
|
6
|
-
import sys
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
from codeflow.core import cf_log
|
|
10
|
-
from codeflow.acceptance.cf_checks import load_check_state, parse_spec_checks
|
|
11
|
-
from codeflow.core.cf_core import (
|
|
12
|
-
build_effective_mapping,
|
|
13
|
-
build_spec_catalog,
|
|
14
|
-
estimate_tokens,
|
|
15
|
-
load_config,
|
|
16
|
-
project_instruction_file,
|
|
17
|
-
non_injectable_specs,
|
|
18
|
-
parse_spec_frontmatter,
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
REVIEW_MIN_COVERAGE_DAYS = 7
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _log_coverage_sufficient(events: list) -> bool:
|
|
25
|
-
""""未命中"信号需要足够的日志覆盖期——刚部署一天的日志没资格断言
|
|
26
|
-
"30 天未命中"(实测误伤:单条事件就把全部 spec 标为待复审)。"""
|
|
27
|
-
from datetime import datetime, timedelta
|
|
28
|
-
earliest = min((str(e.get("ts", "")) for e in events), default="")
|
|
29
|
-
try:
|
|
30
|
-
first = datetime.fromisoformat(earliest)
|
|
31
|
-
except ValueError:
|
|
32
|
-
return False
|
|
33
|
-
return first <= datetime.now() - timedelta(days=REVIEW_MIN_COVERAGE_DAYS)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def build_review_list(project_root: str, specs: list) -> list:
|
|
37
|
-
"""待复审清单(FEAT-06):未命中注入 / 已停用 / 反复误报。
|
|
38
|
-
|
|
39
|
-
豁免:check-state `_review_exempt` 列表中的条目不再提示(S-09 处置闭环)。
|
|
40
|
-
日志覆盖不足 REVIEW_MIN_COVERAGE_DAYS 天时跳过"未命中"信号,
|
|
41
|
-
避免误伤全新安装与刚部署项目。
|
|
42
|
-
"""
|
|
43
|
-
state = load_check_state(project_root)
|
|
44
|
-
exempt = set(state.get("_review_exempt") or [])
|
|
45
|
-
review: list = []
|
|
46
|
-
|
|
47
|
-
events = cf_log.read_events(project_root, days=30)
|
|
48
|
-
if events and _log_coverage_sufficient(events):
|
|
49
|
-
engaged: set = set()
|
|
50
|
-
for event in events:
|
|
51
|
-
data = event.get("data") or {}
|
|
52
|
-
if event.get("event") == "inject":
|
|
53
|
-
engaged.update(data.get("specs") or [])
|
|
54
|
-
elif event.get("event") == "violation" and data.get("spec"):
|
|
55
|
-
engaged.add(data["spec"])
|
|
56
|
-
for spec in specs:
|
|
57
|
-
rel = spec.get("rel", "")
|
|
58
|
-
if rel.endswith("_map.md") or rel.startswith("shared/"):
|
|
59
|
-
continue
|
|
60
|
-
if rel not in engaged and rel not in exempt:
|
|
61
|
-
review.append({"item": rel, "reason": "30 天未命中注入/未触发检查"})
|
|
62
|
-
|
|
63
|
-
for cid, entry in state.items():
|
|
64
|
-
if cid.startswith("_") or not isinstance(entry, dict) or cid in exempt:
|
|
65
|
-
continue
|
|
66
|
-
if entry.get("disabled"):
|
|
67
|
-
review.append({
|
|
68
|
-
"item": cid,
|
|
69
|
-
"reason": f"已自动停用({entry.get('disabled_reason', '')}),待改写或删除",
|
|
70
|
-
})
|
|
71
|
-
elif int(entry.get("fp_count", 0)) >= 2:
|
|
72
|
-
review.append({"item": cid, "reason": f"误报 {entry['fp_count']} 次"})
|
|
73
|
-
return review
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
PATH_PATTERN = re.compile(r"(?:[\w./-]+/)+[\w.-]+\.[A-Za-z0-9]+")
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def read_text(path: str) -> str:
|
|
80
|
-
try:
|
|
81
|
-
with open(path, "r", encoding="utf-8") as file:
|
|
82
|
-
return file.read().strip()
|
|
83
|
-
except Exception:
|
|
84
|
-
return ""
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def normalize_line(line: str) -> str:
|
|
88
|
-
return " ".join(line.strip().split())
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
_YAML_STRUCTURE = re.compile(
|
|
92
|
-
r"^(?:stages:.*|enforcement:.*|verifiers:.*|tags:.*|tier:.*|"
|
|
93
|
-
r"- (?:design|plan|code|review)|-?\s*type:\s*(?:command|test))$"
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def _audit_body(content: str) -> str:
|
|
98
|
-
"""Remove only a complete frontmatter block at the start of the file."""
|
|
99
|
-
lines = content.splitlines()
|
|
100
|
-
if lines and lines[0].strip() == "---":
|
|
101
|
-
end = next((i for i in range(1, len(lines)) if lines[i].strip() == "---"), None)
|
|
102
|
-
if end is not None:
|
|
103
|
-
return "\n".join(lines[end + 1:])
|
|
104
|
-
return content
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def find_redundant_lines(specs: list) -> dict:
|
|
108
|
-
line_map: dict = {}
|
|
109
|
-
for spec in specs:
|
|
110
|
-
for raw in _audit_body(spec["content"]).splitlines():
|
|
111
|
-
line = normalize_line(raw)
|
|
112
|
-
if not line:
|
|
113
|
-
continue
|
|
114
|
-
if line.startswith("#") or line == "---" or _YAML_STRUCTURE.fullmatch(line):
|
|
115
|
-
continue
|
|
116
|
-
if len(line) < 6:
|
|
117
|
-
continue
|
|
118
|
-
line_map.setdefault(line, set()).add(spec["path"])
|
|
119
|
-
redundant = {line: paths for line, paths in line_map.items() if len(paths) >= 3}
|
|
120
|
-
return redundant
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def _audit_options(config: dict) -> tuple[list[str], int]:
|
|
124
|
-
audit = config.get("audit", {})
|
|
125
|
-
if not isinstance(audit, dict):
|
|
126
|
-
raise ValueError("audit must be a mapping")
|
|
127
|
-
roots = audit.get("path_roots", [])
|
|
128
|
-
if not isinstance(roots, list) or any(not isinstance(root, str) or not root for root in roots):
|
|
129
|
-
raise ValueError("audit.path_roots must be a list of paths")
|
|
130
|
-
maximum = audit.get("long_spec_tokens", 500)
|
|
131
|
-
if type(maximum) is not int or maximum <= 0:
|
|
132
|
-
raise ValueError("audit.long_spec_tokens must be a positive integer")
|
|
133
|
-
return roots, maximum
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _audit_roots(project_root: str, configured: list[str]) -> tuple[list[str], list[dict[str, str]]]:
|
|
137
|
-
project = Path(project_root).resolve()
|
|
138
|
-
roots: list[str] = []
|
|
139
|
-
warnings: list[dict[str, str]] = []
|
|
140
|
-
discovered = [path for pattern in ("apps/*/src/*", "packages/*/src/*", "lib/*/src/*")
|
|
141
|
-
for path in sorted(glob.glob(str(project / pattern)))]
|
|
142
|
-
for value in [*configured, *discovered]:
|
|
143
|
-
candidate = (project / value).resolve()
|
|
144
|
-
if (candidate != project and project not in candidate.parents) or not candidate.is_dir() or not os.access(candidate, os.R_OK | os.X_OK):
|
|
145
|
-
if value in configured:
|
|
146
|
-
warnings.append({"code": "invalid_path_root", "path": value})
|
|
147
|
-
continue
|
|
148
|
-
resolved = str(candidate)
|
|
149
|
-
if resolved not in roots:
|
|
150
|
-
roots.append(resolved)
|
|
151
|
-
return roots, warnings
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def _path_resolution(text: str, project_root: str, base_dir: str,
|
|
155
|
-
roots: list[str]) -> tuple[list[str], dict[str, str]]:
|
|
156
|
-
missing: list[str] = []
|
|
157
|
-
resolved: dict[str, str] = {}
|
|
158
|
-
project = Path(project_root).resolve()
|
|
159
|
-
for token in _path_tokens(text):
|
|
160
|
-
candidates = [str(project), base_dir, *roots]
|
|
161
|
-
if os.path.isabs(token):
|
|
162
|
-
candidates = [""]
|
|
163
|
-
for root in candidates:
|
|
164
|
-
if root and not Path(root).is_dir():
|
|
165
|
-
continue
|
|
166
|
-
target = Path(os.path.join(root, token)).resolve()
|
|
167
|
-
if not os.path.isabs(token) and target != project and project not in target.parents:
|
|
168
|
-
continue
|
|
169
|
-
if target.exists():
|
|
170
|
-
resolved[token] = os.path.relpath(root, project) if root else "/"
|
|
171
|
-
break
|
|
172
|
-
else:
|
|
173
|
-
missing.append(token)
|
|
174
|
-
return missing, resolved
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def _path_tokens(text: str) -> list[str]:
|
|
178
|
-
tokens: set[str] = set()
|
|
179
|
-
for word in re.findall(r"[^\s`<>()\"']+", text):
|
|
180
|
-
if "://" in word or any(char in word for char in "*?{}[]"):
|
|
181
|
-
continue
|
|
182
|
-
tokens.update(PATH_PATTERN.findall(word))
|
|
183
|
-
return sorted(tokens)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def find_missing_paths(text: str, project_root: str, base_dir: str = "") -> list:
|
|
187
|
-
"""路径引用检查:项目根解析失败时,再按 spec 自身目录二次解析——
|
|
188
|
-
_map.md 常用相对本域目录的引用(如 design/design-lite.md)。"""
|
|
189
|
-
return _path_resolution(text, project_root, base_dir, [])[0]
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def build_report(project_root: str, platform: str = "") -> dict:
|
|
193
|
-
"""规范质量审计数据(cf-scan 引擎,亦供 cf-stats --audit 复用)。
|
|
194
|
-
|
|
195
|
-
files 含 issues / template 标记,review 为待复审清单;展示层负责过滤与渲染。"""
|
|
196
|
-
files = []
|
|
197
|
-
total_tokens = 0
|
|
198
|
-
|
|
199
|
-
instruction_file = project_instruction_file(project_root, platform)
|
|
200
|
-
claude_path = os.path.join(project_root, instruction_file)
|
|
201
|
-
if os.path.exists(claude_path):
|
|
202
|
-
content = read_text(claude_path)
|
|
203
|
-
tokens = estimate_tokens(content)
|
|
204
|
-
total_tokens += tokens
|
|
205
|
-
files.append({"path": instruction_file, "tokens": tokens, "issues": []})
|
|
206
|
-
|
|
207
|
-
specs_root = os.path.join(project_root, ".code-flow", "specs")
|
|
208
|
-
specs = []
|
|
209
|
-
if os.path.isdir(specs_root):
|
|
210
|
-
for root, _, filenames in os.walk(specs_root):
|
|
211
|
-
for name in filenames:
|
|
212
|
-
if not name.endswith(".md"):
|
|
213
|
-
continue
|
|
214
|
-
full_path = os.path.join(root, name)
|
|
215
|
-
rel = os.path.relpath(full_path, specs_root)
|
|
216
|
-
if rel.replace(os.sep, "/").startswith("_session/"):
|
|
217
|
-
continue
|
|
218
|
-
content = read_text(full_path)
|
|
219
|
-
if not content:
|
|
220
|
-
continue
|
|
221
|
-
tokens = estimate_tokens(content)
|
|
222
|
-
total_tokens += tokens
|
|
223
|
-
rel_path = os.path.join("specs", rel).replace(os.sep, "/")
|
|
224
|
-
spec_entry = {
|
|
225
|
-
"path": rel_path,
|
|
226
|
-
"rel": rel.replace(os.sep, "/"),
|
|
227
|
-
"tokens": tokens,
|
|
228
|
-
"issues": [],
|
|
229
|
-
"content": content,
|
|
230
|
-
}
|
|
231
|
-
specs.append(spec_entry)
|
|
232
|
-
|
|
233
|
-
# 模板标记前置:模板(tags:[])不参与冗长/冗余审计,也不把
|
|
234
|
-
# 模板间共享的文档头计入其他文件的冗余计数
|
|
235
|
-
config = load_config(project_root)
|
|
236
|
-
configured_roots, long_spec_tokens = _audit_options(config)
|
|
237
|
-
roots, audit_warnings = _audit_roots(project_root, configured_roots)
|
|
238
|
-
effective_mapping = build_effective_mapping(
|
|
239
|
-
project_root, config.get("path_mapping") or {}
|
|
240
|
-
)
|
|
241
|
-
excluded_specs = non_injectable_specs(effective_mapping)
|
|
242
|
-
for spec in specs:
|
|
243
|
-
spec["template"] = spec.get("rel", "") in excluded_specs
|
|
244
|
-
|
|
245
|
-
redundant_map = find_redundant_lines([s for s in specs if not s["template"]])
|
|
246
|
-
|
|
247
|
-
for spec in specs:
|
|
248
|
-
issues = []
|
|
249
|
-
if not spec["template"] and spec["tokens"] > long_spec_tokens:
|
|
250
|
-
issues.append(f"冗长: {spec['tokens']} tokens")
|
|
251
|
-
|
|
252
|
-
spec_dir = os.path.join(specs_root, os.path.dirname(spec.get("rel", "")))
|
|
253
|
-
missing_paths, resolved_paths = _path_resolution(spec["content"], project_root, spec_dir, roots)
|
|
254
|
-
for path in missing_paths[:3]:
|
|
255
|
-
issues.append(f"过时: 路径不存在 {path}")
|
|
256
|
-
|
|
257
|
-
redundant_lines = [line for line, paths in redundant_map.items() if spec["path"] in paths]
|
|
258
|
-
for line in redundant_lines[:3]:
|
|
259
|
-
issues.append(f"冗余: '{line}' 出现于 {len(redundant_map[line])} 个文件")
|
|
260
|
-
|
|
261
|
-
# Constraint specs feed the catalog one line each — flag missing
|
|
262
|
-
# frontmatter description (catalog falls back to blockquote/H1).
|
|
263
|
-
rel = spec.get("rel", "")
|
|
264
|
-
is_constraint = not rel.endswith("_map.md") and not rel.startswith("shared/")
|
|
265
|
-
if is_constraint:
|
|
266
|
-
meta, _ = parse_spec_frontmatter(spec["content"])
|
|
267
|
-
if not (meta.get("description") or "").strip():
|
|
268
|
-
issues.append("缺描述: 建议加 frontmatter description(catalog 适用场景一句话)")
|
|
269
|
-
# checks 标注校验(E-01 / B-04 的离线出口)
|
|
270
|
-
_, check_errors = parse_spec_checks(spec["content"])
|
|
271
|
-
for error in check_errors[:3]:
|
|
272
|
-
issues.append(f"checks: {error}")
|
|
273
|
-
|
|
274
|
-
files.append({
|
|
275
|
-
"path": spec["path"],
|
|
276
|
-
"tokens": spec["tokens"],
|
|
277
|
-
"issues": issues,
|
|
278
|
-
"template": spec["template"],
|
|
279
|
-
"resolved_paths": resolved_paths,
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
for entry in files:
|
|
283
|
-
if total_tokens <= 0:
|
|
284
|
-
entry["percent"] = "0%"
|
|
285
|
-
else:
|
|
286
|
-
entry["percent"] = f"{round(entry['tokens'] * 100 / total_tokens)}%"
|
|
287
|
-
|
|
288
|
-
budget = (config.get("budget") or {}).get("total", 2500)
|
|
289
|
-
try:
|
|
290
|
-
budget = int(budget)
|
|
291
|
-
except Exception:
|
|
292
|
-
budget = 2500
|
|
293
|
-
|
|
294
|
-
try:
|
|
295
|
-
catalog_max = int((config.get("budget") or {}).get("catalog_max", 200))
|
|
296
|
-
except Exception:
|
|
297
|
-
catalog_max = 200
|
|
298
|
-
catalog = build_spec_catalog(project_root, effective_mapping, catalog_max)
|
|
299
|
-
catalog_info = {
|
|
300
|
-
"tokens": estimate_tokens(catalog),
|
|
301
|
-
"max": catalog_max,
|
|
302
|
-
"entries": catalog.count("\n- `"),
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
review = build_review_list(project_root, specs)
|
|
306
|
-
|
|
307
|
-
# 预算口径与 cf-stats 一致:tags:[] 命令模板不计入注入预算
|
|
308
|
-
templates_tokens = sum(e["tokens"] for e in files if e.get("template"))
|
|
309
|
-
injectable_tokens = total_tokens - templates_tokens
|
|
310
|
-
|
|
311
|
-
return {
|
|
312
|
-
"files": files,
|
|
313
|
-
"total_tokens": injectable_tokens,
|
|
314
|
-
"templates_tokens": templates_tokens,
|
|
315
|
-
"budget": budget,
|
|
316
|
-
"catalog": catalog_info,
|
|
317
|
-
"review": review,
|
|
318
|
-
"warnings": [],
|
|
319
|
-
"audit_warnings": audit_warnings,
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
def main() -> None:
|
|
324
|
-
json_output = "--json" in sys.argv
|
|
325
|
-
only_issues = "--only-issues" in sys.argv
|
|
326
|
-
limit = None
|
|
327
|
-
for arg in sys.argv[1:]:
|
|
328
|
-
if arg.startswith("--limit="):
|
|
329
|
-
try:
|
|
330
|
-
limit = int(arg.split("=", 1)[1])
|
|
331
|
-
except Exception:
|
|
332
|
-
limit = None
|
|
333
|
-
|
|
334
|
-
report = build_report(os.getcwd())
|
|
335
|
-
files = report["files"]
|
|
336
|
-
if limit is not None:
|
|
337
|
-
files = files[:limit]
|
|
338
|
-
if only_issues:
|
|
339
|
-
files = [entry for entry in files if entry.get("issues")]
|
|
340
|
-
|
|
341
|
-
if json_output:
|
|
342
|
-
print(json.dumps(dict(report, files=files), ensure_ascii=False))
|
|
343
|
-
return
|
|
344
|
-
|
|
345
|
-
catalog_info = report["catalog"]
|
|
346
|
-
print("FILE | TOKENS | PERCENT | ISSUES")
|
|
347
|
-
for entry in files:
|
|
348
|
-
issues = entry.get("issues") or []
|
|
349
|
-
issue_text = " / ".join(issues) if issues else "-"
|
|
350
|
-
marker = " [模板]" if entry.get("template") else ""
|
|
351
|
-
print(f"{entry['path']}{marker} | {entry['tokens']} | {entry['percent']} | {issue_text}")
|
|
352
|
-
print("TOTAL:", f"{report['total_tokens']} / {report['budget']}")
|
|
353
|
-
if report["templates_tokens"]:
|
|
354
|
-
print("TEMPLATES (非注入):", f"{report['templates_tokens']} tokens,不计预算")
|
|
355
|
-
print(
|
|
356
|
-
"CATALOG:",
|
|
357
|
-
f"{catalog_info['tokens']} / {catalog_info['max']} tokens,",
|
|
358
|
-
f"{catalog_info['entries']} entries",
|
|
359
|
-
)
|
|
360
|
-
if report["review"]:
|
|
361
|
-
print("REVIEW (待复审):")
|
|
362
|
-
for item in report["review"]:
|
|
363
|
-
print(f" - {item['item']}: {item['reason']}")
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if __name__ == "__main__":
|
|
367
|
-
main()
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"""Shared path-projection inputs and budget accounting."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Sequence
|
|
6
|
-
|
|
7
|
-
from codeflow.core.cf_core import assemble_context, compress_content, estimate_tokens, load_config, parse_spec_frontmatter, resolve_compress
|
|
8
|
-
from codeflow.spec.cf_spec_resolver import resolve_candidates
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class SpecBudgetError(ValueError):
|
|
12
|
-
def __init__(self, code: str, message: str) -> None:
|
|
13
|
-
self.code = code
|
|
14
|
-
super().__init__(message)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def path_specs(root: str, paths: Sequence[str], compress: bool = True,
|
|
18
|
-
matched_only: bool = True) -> list[dict[str, object]]:
|
|
19
|
-
try:
|
|
20
|
-
candidates = resolve_candidates(root, 'code', tuple(paths), matched_only=matched_only)
|
|
21
|
-
except ValueError as exc:
|
|
22
|
-
raise SpecBudgetError('invalid_spec_metadata', str(exc)) from exc
|
|
23
|
-
specs: list[dict[str, object]] = []
|
|
24
|
-
for candidate in candidates:
|
|
25
|
-
if matched_only and candidate.scope == 'unmatched':
|
|
26
|
-
continue
|
|
27
|
-
target = Path(root) / '.code-flow/specs' / candidate.path
|
|
28
|
-
try:
|
|
29
|
-
raw = target.read_text(encoding='utf-8')
|
|
30
|
-
except OSError as exc:
|
|
31
|
-
raise SpecBudgetError('spec_read_failed', f'{candidate.path}: {exc}') from exc
|
|
32
|
-
_, content = parse_spec_frontmatter(raw)
|
|
33
|
-
final = compress_content(content.strip()) if compress else content.strip()
|
|
34
|
-
specs.append({'path': candidate.path, 'content': final, 'tier': 1})
|
|
35
|
-
return specs
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def measure_path_budget(root: str, paths: Sequence[str]) -> dict[str, object]:
|
|
39
|
-
config = load_config(root)
|
|
40
|
-
quality = config.get('quality_loop')
|
|
41
|
-
compress = resolve_compress(quality if isinstance(quality, dict) else {})
|
|
42
|
-
matched = path_specs(root, paths, compress) if paths else []
|
|
43
|
-
aggregate = path_specs(root, (), compress, matched_only=False)
|
|
44
|
-
text = assemble_context(matched, '## Active Specs (path-mapped)') if matched else ''
|
|
45
|
-
budget = config.get('budget', {})
|
|
46
|
-
if not isinstance(budget, dict):
|
|
47
|
-
raise SpecBudgetError('invalid_budget', 'budget must be a mapping')
|
|
48
|
-
limit = min(int(budget.get('total', 2500)), int(budget.get('l1_max', 1700)))
|
|
49
|
-
if limit <= 0:
|
|
50
|
-
raise SpecBudgetError('invalid_budget', 'budget limit must be positive')
|
|
51
|
-
total = estimate_tokens(text)
|
|
52
|
-
return {'matched_specs': [{'path': item['path'], 'tokens': estimate_tokens(item['content'])}
|
|
53
|
-
for item in matched], 'matched_total': total,
|
|
54
|
-
'aggregate_total': sum(estimate_tokens(item['content']) for item in aggregate),
|
|
55
|
-
'limit': limit, 'over_limit': total > limit}
|