@michaelxwb/code-flow 0.6.5 → 0.8.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/README.md +47 -3
- package/docs/USAGE.md +1529 -0
- package/docs/acceptance-contract.md +217 -0
- package/docs/ci/acceptance-evidence.md +77 -0
- package/docs/migrations/runtime-0.7.0.md +41 -0
- package/docs/migrations/runtime-0.8.0.md +154 -0
- package/docs/migrations/runtime-0.9.0.md +40 -0
- package/docs/spec-injection-hardening.md +54 -0
- package/docs/spec-injection-performance.json +73 -0
- package/docs/spec-optimization.md +12 -0
- package/package.json +18 -3
- package/src/adapters/claude/CLAUDE.md +1 -1
- package/src/adapters/claude/commands/cf-init.md +1 -3
- package/src/adapters/claude/commands/cf-spec.md +23 -3
- package/src/adapters/claude/commands/cf-stats.md +11 -1
- package/src/adapters/claude/commands/cf-sync.md +20 -6
- package/src/adapters/claude/commands/cf-task/align.md +25 -3
- package/src/adapters/claude/commands/cf-task/archive.md +36 -42
- package/src/adapters/claude/commands/cf-task/block.md +15 -2
- package/src/adapters/claude/commands/cf-task/graph.md +13 -1
- package/src/adapters/claude/commands/cf-task/note.md +13 -1
- package/src/adapters/claude/commands/cf-task/plan.md +137 -23
- package/src/adapters/claude/commands/cf-task/prd.md +13 -2
- package/src/adapters/claude/commands/cf-task/start.md +52 -39
- package/src/adapters/claude/commands/cf-task/status.md +12 -0
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/claude/commands/cf-validate.md +30 -5
- package/src/adapters/codex/AGENTS.md +1 -1
- package/src/adapters/codex/hooks.json +14 -10
- package/src/adapters/codex/skills/cf-init/SKILL.md +4 -4
- package/src/adapters/codex/skills/cf-spec/SKILL.md +34 -6
- package/src/adapters/codex/skills/cf-stats/SKILL.md +11 -1
- package/src/adapters/codex/skills/cf-sync/SKILL.md +21 -6
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +30 -8
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +36 -42
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -2
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +175 -21
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +13 -2
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +52 -39
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +12 -0
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +69 -35
- package/src/adapters/codex/skills/cf-validate/SKILL.md +31 -5
- package/src/adapters/commands.json +115 -0
- package/src/adapters/costrict/CLAUDE.md +1 -1
- package/src/adapters/costrict/commands/cf-init.md +1 -3
- package/src/adapters/costrict/commands/cf-spec.md +23 -3
- package/src/adapters/costrict/commands/cf-stats.md +11 -1
- package/src/adapters/costrict/commands/cf-sync.md +20 -6
- package/src/adapters/costrict/commands/cf-task/align.md +25 -3
- package/src/adapters/costrict/commands/cf-task/archive.md +36 -42
- package/src/adapters/costrict/commands/cf-task/block.md +15 -2
- package/src/adapters/costrict/commands/cf-task/graph.md +13 -1
- package/src/adapters/costrict/commands/cf-task/note.md +13 -1
- package/src/adapters/costrict/commands/cf-task/plan.md +137 -23
- package/src/adapters/costrict/commands/cf-task/prd.md +13 -2
- package/src/adapters/costrict/commands/cf-task/start.md +52 -39
- package/src/adapters/costrict/commands/cf-task/status.md +12 -0
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/costrict/commands/cf-validate.md +30 -5
- package/src/adapters/install-plan.json +100 -0
- package/src/adapters/opencode/AGENTS.md +1 -1
- package/src/adapters/opencode/commands/cf-init.md +7 -7
- package/src/adapters/opencode/commands/cf-learn.md +6 -6
- package/src/adapters/opencode/commands/cf-spec.md +34 -2
- package/src/adapters/opencode/commands/cf-stats.md +17 -7
- package/src/adapters/opencode/commands/cf-sync.md +21 -6
- package/src/adapters/opencode/commands/cf-task/align.md +25 -8
- package/src/adapters/opencode/commands/cf-task/archive.md +36 -42
- package/src/adapters/opencode/commands/cf-task/block.md +16 -2
- package/src/adapters/opencode/commands/cf-task/graph.md +14 -1
- package/src/adapters/opencode/commands/cf-task/note.md +14 -1
- package/src/adapters/opencode/commands/cf-task/plan.md +137 -23
- package/src/adapters/opencode/commands/cf-task/prd.md +13 -2
- package/src/adapters/opencode/commands/cf-task/start.md +53 -45
- package/src/adapters/opencode/commands/cf-task/status.md +13 -0
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/opencode/commands/cf-validate.md +35 -9
- package/src/adapters/opencode/plugins/code-flow/index.js +128 -179
- package/src/adapters/platform-bindings.json +370 -0
- package/src/cli.js +369 -316
- package/src/core/code-flow/config.yml +4 -1
- package/src/core/code-flow/runtime-commands.json +259 -0
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +5 -289
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +5 -187
- package/src/core/code-flow/scripts/cf_audit.py +10 -0
- package/src/core/code-flow/scripts/cf_codex_hook.py +10 -0
- package/src/core/code-flow/scripts/cf_design_lint.py +10 -0
- package/src/core/code-flow/scripts/cf_evidence_import.py +10 -0
- package/src/core/code-flow/scripts/cf_feedback.py +5 -51
- package/src/core/code-flow/scripts/cf_opencode_event.py +10 -0
- package/src/core/code-flow/scripts/cf_post_hook.py +6 -191
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +6 -99
- package/src/core/code-flow/scripts/cf_scenarios.py +10 -0
- package/src/core/code-flow/scripts/cf_spec_context.py +5 -1741
- package/src/core/code-flow/scripts/cf_spec_gate.py +5 -259
- package/src/core/code-flow/scripts/cf_spec_session.py +5 -114
- package/src/core/code-flow/scripts/cf_stats.py +5 -501
- package/src/core/code-flow/scripts/cf_stop_hook.py +6 -376
- package/src/core/code-flow/scripts/cf_sync.py +5 -135
- package/src/core/code-flow/scripts/cf_task_index.py +5 -143
- package/src/core/code-flow/scripts/cf_task_parallel.py +5 -389
- package/src/core/code-flow/scripts/cf_task_workflow.py +6 -201
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +6 -173
- package/src/core/code-flow/scripts/cf_validation.py +6 -77
- 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 +347 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +660 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +202 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +60 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +73 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +282 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +266 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +172 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +326 -0
- package/src/core/code-flow/scripts/{cf_checks.py → codeflow/acceptance/cf_checks.py} +8 -11
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +197 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +174 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +300 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +502 -0
- package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +347 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +1490 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +533 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +172 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +607 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +53 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +58 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +1175 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +60 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +46 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +540 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +144 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +177 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +1142 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +742 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +31 -0
- package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +67 -0
- package/src/core/code-flow/scripts/{cf_core.py → codeflow/core/cf_core.py} +119 -297
- package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +325 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +57 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_git.py +92 -0
- package/src/core/code-flow/scripts/{cf_log.py → codeflow/core/cf_log.py} +13 -7
- package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +77 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +26 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +33 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +32 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +53 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +9 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +408 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +253 -0
- package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +527 -0
- package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +95 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +98 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +30 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +41 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +292 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +161 -0
- package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +412 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +301 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +149 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +111 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +114 -0
- package/src/core/code-flow/scripts/{cf_scan.py → codeflow/spec/cf_scan.py} +93 -21
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +55 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +477 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +119 -0
- package/src/core/code-flow/scripts/{cf_spec_metadata.py → codeflow/spec/cf_spec_metadata.py} +5 -2
- package/src/core/code-flow/scripts/{cf_spec_migrate.py → codeflow/spec/cf_spec_migrate.py} +27 -10
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +193 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +187 -0
- package/src/core/code-flow/scripts/{cf_spec_resolver.py → codeflow/spec/cf_spec_resolver.py} +39 -17
- package/src/core/code-flow/scripts/{cf_spec_router.py → codeflow/spec/cf_spec_router.py} +96 -36
- package/src/core/code-flow/scripts/{cf_spec_verify.py → codeflow/spec/cf_spec_verify.py} +163 -35
- package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +330 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +83 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +755 -0
- package/src/core/code-flow/scripts/{cf_workflow_transaction.py → codeflow/spec/cf_workflow_transaction.py} +29 -15
- package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +867 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +178 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +635 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +251 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +570 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +73 -0
- package/src/core/code-flow/scripts/runtime-resources.json +29 -0
- package/src/core/code-flow/specs/shared/design/design-frontend.md +18 -15
- package/src/core/code-flow/specs/shared/design/design-full.md +33 -23
- package/src/core/code-flow/specs/shared/design/design-lite.md +23 -13
- package/src/core/code-flow/specs/shared/scenarios-template.md +55 -0
- package/src/core/code-flow/validation.yml +9 -3
- package/src/migrate/runtime.js +213 -0
- package/src/migrate/spec-workflow.js +16 -1
- package/src/runtime.js +219 -0
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
- package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
- package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
- package/src/core/code-flow/scripts/cf_task_state.py +0 -3
- package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
- /package/src/core/code-flow/scripts/{cf_session_state.py → codeflow/core/cf_session_state.py} +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""CLI wrapper for the fail-closed Spec Context Stage Gate."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from typing import IO, Optional, Sequence
|
|
12
|
+
|
|
13
|
+
from codeflow.core.cf_core import phase_timing
|
|
14
|
+
from codeflow.spec.cf_spec_context_model import load_context
|
|
15
|
+
from codeflow.spec.cf_stage_gate import (
|
|
16
|
+
GateIssue, GateResult, evidence_deferral_kind, result_to_data, validate_plan_coverage, validate_stage,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _merge_results(first: GateResult, second: GateResult) -> GateResult:
|
|
21
|
+
errors = (*first.errors, *second.errors)
|
|
22
|
+
warnings = (*first.warnings, *second.warnings)
|
|
23
|
+
refs = tuple(sorted(set((*first.affected_refs, *second.affected_refs))))
|
|
24
|
+
return GateResult("block" if errors else "pass", errors, warnings, refs)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _parser() -> argparse.ArgumentParser:
|
|
28
|
+
parser = argparse.ArgumentParser(prog=os.environ.get("CF_RUNTIME_COMMAND", "cf_spec_gate.py"))
|
|
29
|
+
parser.add_argument("--task-dir", required=True)
|
|
30
|
+
parser.add_argument("--stage", required=True)
|
|
31
|
+
parser.add_argument("--task", default="")
|
|
32
|
+
parser.add_argument("--artifact", default="")
|
|
33
|
+
parser.add_argument("--json", action="store_true")
|
|
34
|
+
return parser
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
|
|
38
|
+
started = time.monotonic()
|
|
39
|
+
try:
|
|
40
|
+
args = _parser().parse_args(argv)
|
|
41
|
+
phase_started = time.monotonic()
|
|
42
|
+
context = load_context(str(Path(args.task_dir) / "spec-context.yml"))
|
|
43
|
+
phase_timing("gate.load_context", phase_started)
|
|
44
|
+
phase_started = time.monotonic()
|
|
45
|
+
directory = Path(args.task_dir).resolve()
|
|
46
|
+
root = next((parent.parent for parent in (directory, *directory.parents)
|
|
47
|
+
if parent.name == ".code-flow"), directory)
|
|
48
|
+
result = validate_stage(context, args.stage, task_id=args.task, root=str(root), task_dir=args.task_dir)
|
|
49
|
+
phase_timing("gate.validate_stage", phase_started)
|
|
50
|
+
if args.stage == "plan" and args.artifact:
|
|
51
|
+
phase_started = time.monotonic()
|
|
52
|
+
result = _merge_results(result, validate_plan_coverage(context, args.artifact))
|
|
53
|
+
phase_timing("gate.validate_plan", phase_started)
|
|
54
|
+
stdout.write(json.dumps(result_to_data(result), ensure_ascii=False))
|
|
55
|
+
phase_timing("gate.total", started)
|
|
56
|
+
return 0 if result.decision == "pass" else 3
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
sys.stderr.write(f"cf_spec_gate error: {exc}\n")
|
|
59
|
+
stdout.write(json.dumps({"decision": "block", "errors": [{"code": "gate_error"}]}, ensure_ascii=False))
|
|
60
|
+
return 2
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""CLI wrapper for TASK-bound Spec Context projection."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import sys
|
|
10
|
+
from typing import IO, Optional, Sequence
|
|
11
|
+
|
|
12
|
+
from codeflow.spec.cf_spec_context_model import context_sha256, load_context
|
|
13
|
+
from codeflow.spec.cf_task_projection import SessionProjection, project_task_session
|
|
14
|
+
from codeflow.task.cf_task_doc import task_section, task_spec_refs
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _task_section(text: str, task_id: str) -> str:
|
|
18
|
+
return task_section(text, task_id).body
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _refs(section: str) -> tuple[str, ...]:
|
|
22
|
+
return task_spec_refs(section)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parser() -> argparse.ArgumentParser:
|
|
26
|
+
parser = argparse.ArgumentParser(prog=os.environ.get("CF_RUNTIME_COMMAND", "cf_spec_session.py"))
|
|
27
|
+
parser.add_argument("--task-dir", required=True)
|
|
28
|
+
parser.add_argument("--task-file", required=True)
|
|
29
|
+
parser.add_argument("--task", required=True)
|
|
30
|
+
parser.add_argument("--output", required=True)
|
|
31
|
+
parser.add_argument("--budget", type=int, default=12000)
|
|
32
|
+
parser.add_argument("--json", action="store_true")
|
|
33
|
+
return parser
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
|
|
37
|
+
try:
|
|
38
|
+
args = _parser().parse_args(argv)
|
|
39
|
+
context = load_context(str(Path(args.task_dir) / "spec-context.yml"))
|
|
40
|
+
result = project_task_session(context, args.task_file, args.task, args.budget)
|
|
41
|
+
Path(args.output).write_text(result.text, encoding="utf-8")
|
|
42
|
+
stdout.write(json.dumps(result.__dict__, ensure_ascii=False))
|
|
43
|
+
return 0
|
|
44
|
+
except Exception as exc:
|
|
45
|
+
stdout.write(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False))
|
|
46
|
+
return 3
|
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from codeflow.core import cf_log
|
|
10
|
+
from codeflow.acceptance.cf_checks import load_check_state
|
|
11
|
+
from codeflow.core.cf_core import (
|
|
12
|
+
build_effective_mapping,
|
|
13
|
+
build_spec_catalog,
|
|
14
|
+
compress_content,
|
|
15
|
+
estimate_tokens,
|
|
16
|
+
load_config,
|
|
17
|
+
project_instruction_file,
|
|
18
|
+
non_injectable_specs,
|
|
19
|
+
resolve_quality_loop,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _violation_fixed(index: int, events: list) -> bool:
|
|
24
|
+
"""修正口径:违规后同会话同文件有后续编辑,且其后无同 check 再违规。
|
|
25
|
+
|
|
26
|
+
用日志追加顺序判先后(ts 仅秒级精度,同秒事件无法靠时间戳排序)。
|
|
27
|
+
保留单点查询语义;批量聚合请用 violation_fixed_batch(一次线性扫描)。
|
|
28
|
+
"""
|
|
29
|
+
violation = events[index]
|
|
30
|
+
v_data = violation.get("data") or {}
|
|
31
|
+
later_edit = False
|
|
32
|
+
for event in events[index + 1:]:
|
|
33
|
+
if event.get("sid") != violation.get("sid"):
|
|
34
|
+
continue
|
|
35
|
+
data = event.get("data") or {}
|
|
36
|
+
if event.get("event") == "edit" and data.get("file") == v_data.get("file"):
|
|
37
|
+
later_edit = True
|
|
38
|
+
if (
|
|
39
|
+
later_edit
|
|
40
|
+
and event.get("event") == "violation"
|
|
41
|
+
and data.get("check_id") == v_data.get("check_id")
|
|
42
|
+
and data.get("file") == v_data.get("file")
|
|
43
|
+
):
|
|
44
|
+
return False
|
|
45
|
+
return later_edit
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def violation_fixed_batch(events: list) -> list[bool]:
|
|
49
|
+
"""Batch twin of _violation_fixed: one reverse pass, identical semantics.
|
|
50
|
+
|
|
51
|
+
Segments the timeline per (session, file) at edits: a violation is fixed
|
|
52
|
+
iff an edit exists after it and no same-check violation exists in a later
|
|
53
|
+
segment. No slicing, no per-violation rescan: O(n) time.
|
|
54
|
+
"""
|
|
55
|
+
fixed: dict[int, bool] = {}
|
|
56
|
+
edit_right: set[tuple[str, str]] = set()
|
|
57
|
+
current: dict[tuple[str, str], set[str]] = {}
|
|
58
|
+
later: dict[tuple[str, str], set[str]] = {}
|
|
59
|
+
for index in range(len(events) - 1, -1, -1):
|
|
60
|
+
event = events[index]
|
|
61
|
+
if not isinstance(event, dict):
|
|
62
|
+
continue
|
|
63
|
+
kind = event.get("event")
|
|
64
|
+
data = event.get("data")
|
|
65
|
+
if not isinstance(data, dict):
|
|
66
|
+
data = {}
|
|
67
|
+
key = (str(event.get("sid")), str(data.get("file")))
|
|
68
|
+
if kind == "edit":
|
|
69
|
+
later.setdefault(key, set()).update(current.get(key, ()))
|
|
70
|
+
current[key] = set()
|
|
71
|
+
edit_right.add(key)
|
|
72
|
+
elif kind == "violation":
|
|
73
|
+
check = str(data.get("check_id"))
|
|
74
|
+
fixed[index] = key in edit_right and check not in later.get(key, ())
|
|
75
|
+
current.setdefault(key, set()).add(check)
|
|
76
|
+
return [fixed[index] for index, event in enumerate(events) if isinstance(event, dict) and event.get("event") == "violation"]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def quality_loop_summary(project_root: str, config: dict) -> dict:
|
|
80
|
+
"""FEAT-05/02 度量聚合:Top 违规榜 / 修正率 / 误报与停用 / 降级组件。"""
|
|
81
|
+
switches = resolve_quality_loop(config)
|
|
82
|
+
events = cf_log.read_events(project_root, days=30)
|
|
83
|
+
summary = {"switches": switches, "window_days": 30}
|
|
84
|
+
if not events:
|
|
85
|
+
summary["note"] = "暂无数据"
|
|
86
|
+
return summary
|
|
87
|
+
|
|
88
|
+
violations = [e for e in events if e.get("event") == "violation"]
|
|
89
|
+
counts: dict = {}
|
|
90
|
+
for v in violations:
|
|
91
|
+
data = v.get("data") or {}
|
|
92
|
+
key = f"{data.get('spec', '?')}#{data.get('check_id', '?')}"
|
|
93
|
+
counts[key] = counts.get(key, 0) + 1
|
|
94
|
+
summary["top_violations"] = sorted(
|
|
95
|
+
({"rule": k, "count": n} for k, n in counts.items()),
|
|
96
|
+
key=lambda item: -item["count"],
|
|
97
|
+
)[:10]
|
|
98
|
+
|
|
99
|
+
fixed = violation_fixed_batch(events)
|
|
100
|
+
summary["violation_total"] = len(violations)
|
|
101
|
+
summary["fix_rate"] = (
|
|
102
|
+
f"{round(sum(fixed) * 100 / len(violations))}%" if violations else "n/a"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
state = load_check_state(project_root)
|
|
106
|
+
summary["checks"] = {
|
|
107
|
+
cid: {
|
|
108
|
+
"hit_count": entry.get("hit_count", 0),
|
|
109
|
+
"fp_count": entry.get("fp_count", 0),
|
|
110
|
+
"disabled": bool(entry.get("disabled")),
|
|
111
|
+
"disabled_reason": entry.get("disabled_reason", ""),
|
|
112
|
+
}
|
|
113
|
+
for cid, entry in state.items()
|
|
114
|
+
if isinstance(entry, dict) and not cid.startswith("_")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
degraded: dict = {}
|
|
118
|
+
for event in events:
|
|
119
|
+
if event.get("event") != "degrade":
|
|
120
|
+
continue
|
|
121
|
+
data = event.get("data") or {}
|
|
122
|
+
component = data.get("component", "?")
|
|
123
|
+
item = degraded.setdefault(component, {"count": 0, "last_error": ""})
|
|
124
|
+
item["count"] += 1
|
|
125
|
+
item["last_error"] = data.get("error", "")
|
|
126
|
+
summary["degraded"] = degraded
|
|
127
|
+
return summary
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _percent(numerator: int, denominator: int) -> str:
|
|
131
|
+
return f"{round(numerator * 100 / denominator)}%" if denominator else "n/a"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def spec_workflow_summary(project_root: str) -> dict:
|
|
135
|
+
events = cf_log.read_events(project_root, days=30)
|
|
136
|
+
candidates = {
|
|
137
|
+
(str((event.get("data") or {}).get("task")), str((event.get("data") or {}).get("rule")))
|
|
138
|
+
for event in events if event.get("event") == "spec_candidate"
|
|
139
|
+
}
|
|
140
|
+
bound = {
|
|
141
|
+
(str((event.get("data") or {}).get("task")), str((event.get("data") or {}).get("rule")))
|
|
142
|
+
for event in events if event.get("event") == "spec_bound"
|
|
143
|
+
}
|
|
144
|
+
gates = [event for event in events if event.get("event") == "spec_gate"]
|
|
145
|
+
first_pass = sum((event.get("data") or {}).get("first_pass") is True for event in gates)
|
|
146
|
+
late = sum(event.get("event") == "late_violation" for event in events)
|
|
147
|
+
statuses = {"stale": 0, "conflict": 0, "not_applicable": 0}
|
|
148
|
+
for event in events:
|
|
149
|
+
status = (event.get("data") or {}).get("status")
|
|
150
|
+
if status in statuses:
|
|
151
|
+
statuses[status] += 1
|
|
152
|
+
degraded = sum(event.get("event") == "metrics_degraded" for event in events)
|
|
153
|
+
return {
|
|
154
|
+
"window_days": 30,
|
|
155
|
+
"coverage": _percent(len(bound & candidates), len(candidates)),
|
|
156
|
+
"first_compliance_rate": _percent(first_pass, len(gates)),
|
|
157
|
+
"late_violation_rate": _percent(late, len(bound)),
|
|
158
|
+
"statuses": statuses,
|
|
159
|
+
"data_complete": degraded == 0,
|
|
160
|
+
"degraded_events": degraded,
|
|
161
|
+
"read_note": "read 仅表示读取,不代表理解",
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def read_text(path: str) -> str:
|
|
166
|
+
try:
|
|
167
|
+
with open(path, "r", encoding="utf-8") as file:
|
|
168
|
+
return file.read().strip()
|
|
169
|
+
except Exception:
|
|
170
|
+
return ""
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def normalize_rel_path(path: str) -> str:
|
|
174
|
+
return path.replace(os.sep, "/")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def extract_spec_path(spec_entry) -> str:
|
|
178
|
+
if isinstance(spec_entry, dict):
|
|
179
|
+
return normalize_rel_path(spec_entry.get("path", ""))
|
|
180
|
+
if isinstance(spec_entry, str):
|
|
181
|
+
return normalize_rel_path(spec_entry)
|
|
182
|
+
return ""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def discover_specs(specs_root: str) -> dict:
|
|
186
|
+
discovered = {}
|
|
187
|
+
if not os.path.isdir(specs_root):
|
|
188
|
+
return discovered
|
|
189
|
+
|
|
190
|
+
for root, _, files in os.walk(specs_root):
|
|
191
|
+
for filename in files:
|
|
192
|
+
if not filename.endswith(".md"):
|
|
193
|
+
continue
|
|
194
|
+
full_path = os.path.join(root, filename)
|
|
195
|
+
rel = normalize_rel_path(os.path.relpath(full_path, specs_root))
|
|
196
|
+
parts = rel.split("/", 1)
|
|
197
|
+
if len(parts) < 2:
|
|
198
|
+
continue
|
|
199
|
+
domain = parts[0]
|
|
200
|
+
discovered.setdefault(domain, []).append(rel)
|
|
201
|
+
|
|
202
|
+
for domain in discovered:
|
|
203
|
+
discovered[domain] = sorted(set(discovered[domain]))
|
|
204
|
+
return discovered
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def configured_specs(config: dict, domain: str) -> list:
|
|
208
|
+
mapping = (config.get("path_mapping") or {}).get(domain) or {}
|
|
209
|
+
specs_config = mapping.get("specs") or []
|
|
210
|
+
result = []
|
|
211
|
+
for spec_entry in specs_config:
|
|
212
|
+
rel = extract_spec_path(spec_entry)
|
|
213
|
+
if rel:
|
|
214
|
+
result.append(rel)
|
|
215
|
+
return result
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def resolve_domains(config: dict, discovered: dict, domain_filter: Optional[str]) -> list:
|
|
219
|
+
if domain_filter:
|
|
220
|
+
if domain_filter in discovered:
|
|
221
|
+
return [domain_filter]
|
|
222
|
+
if domain_filter in (config.get("path_mapping") or {}):
|
|
223
|
+
return [domain_filter]
|
|
224
|
+
return []
|
|
225
|
+
|
|
226
|
+
if discovered:
|
|
227
|
+
return sorted(discovered.keys())
|
|
228
|
+
return sorted((config.get("path_mapping") or {}).keys())
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _build_item(rel: str, raw_content: str) -> dict:
|
|
232
|
+
raw_tokens = estimate_tokens(raw_content)
|
|
233
|
+
compressed_tokens = estimate_tokens(compress_content(raw_content))
|
|
234
|
+
saved_pct = (
|
|
235
|
+
round((raw_tokens - compressed_tokens) * 100 / raw_tokens, 1)
|
|
236
|
+
if raw_tokens
|
|
237
|
+
else 0.0
|
|
238
|
+
)
|
|
239
|
+
return {
|
|
240
|
+
"path": rel,
|
|
241
|
+
"tokens": compressed_tokens,
|
|
242
|
+
"tokens_raw": raw_tokens,
|
|
243
|
+
"tokens_compressed": compressed_tokens,
|
|
244
|
+
"saved_pct": saved_pct,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def collect_domain_items(
|
|
249
|
+
specs_root: str,
|
|
250
|
+
domain: str,
|
|
251
|
+
configured: list,
|
|
252
|
+
discovered: list,
|
|
253
|
+
) -> tuple:
|
|
254
|
+
items = []
|
|
255
|
+
missing = []
|
|
256
|
+
seen = set()
|
|
257
|
+
|
|
258
|
+
for rel in configured:
|
|
259
|
+
if rel in seen:
|
|
260
|
+
continue
|
|
261
|
+
seen.add(rel)
|
|
262
|
+
full_path = os.path.join(specs_root, rel)
|
|
263
|
+
if not os.path.exists(full_path):
|
|
264
|
+
missing.append({"domain": domain, "path": rel})
|
|
265
|
+
continue
|
|
266
|
+
content = read_text(full_path)
|
|
267
|
+
if content:
|
|
268
|
+
items.append(_build_item(rel, content))
|
|
269
|
+
|
|
270
|
+
for rel in discovered:
|
|
271
|
+
if rel in seen:
|
|
272
|
+
continue
|
|
273
|
+
seen.add(rel)
|
|
274
|
+
full_path = os.path.join(specs_root, rel)
|
|
275
|
+
content = read_text(full_path)
|
|
276
|
+
if content:
|
|
277
|
+
items.append(_build_item(rel, content))
|
|
278
|
+
|
|
279
|
+
return items, missing
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _arguments() -> argparse.Namespace:
|
|
283
|
+
parser = argparse.ArgumentParser(prog=os.environ.get("CF_RUNTIME_COMMAND", "cf_stats.py"))
|
|
284
|
+
formats = parser.add_mutually_exclusive_group()
|
|
285
|
+
formats.add_argument("--human", action="store_true")
|
|
286
|
+
formats.add_argument("--json", action="store_true")
|
|
287
|
+
parser.add_argument("--audit", action="store_true")
|
|
288
|
+
parser.add_argument("--domain", default=None)
|
|
289
|
+
parser.add_argument("--platform", choices=("claude", "codex", "costrict", "opencode"), default="")
|
|
290
|
+
return parser.parse_args()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _budget_number(config: dict[str, object], key: str, default: int) -> int:
|
|
294
|
+
try:
|
|
295
|
+
return int(config.get(key, default))
|
|
296
|
+
except (TypeError, ValueError) as exc:
|
|
297
|
+
sys.stderr.write(f"cf-stats invalid {key}: {exc}; using {default}\n")
|
|
298
|
+
return default
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _domain_usage(root: str, config: dict[str, object], domain_filter: Optional[str]) -> dict[str, object]:
|
|
302
|
+
specs_root = os.path.join(root, ".code-flow/specs")
|
|
303
|
+
discovered = discover_specs(specs_root)
|
|
304
|
+
mapping = build_effective_mapping(root, config.get("path_mapping") or {})
|
|
305
|
+
excluded = non_injectable_specs(mapping)
|
|
306
|
+
l1, spec_domains, missing_specs, empty_domains = {}, {}, [], []
|
|
307
|
+
for domain in resolve_domains(config, discovered, domain_filter):
|
|
308
|
+
configured = configured_specs(config, domain)
|
|
309
|
+
discovered_paths = discovered.get(domain, [])
|
|
310
|
+
items, missing = collect_domain_items(specs_root, domain, configured, discovered_paths)
|
|
311
|
+
for relative in configured + discovered_paths:
|
|
312
|
+
if relative:
|
|
313
|
+
spec_domains[relative] = domain
|
|
314
|
+
missing_specs.extend(missing)
|
|
315
|
+
for item in items:
|
|
316
|
+
item["injectable"] = item["path"] not in excluded
|
|
317
|
+
if items:
|
|
318
|
+
l1[domain] = items
|
|
319
|
+
elif configured and (not discovered or discovered_paths):
|
|
320
|
+
empty_domains.append(domain)
|
|
321
|
+
tokens = sum(item["tokens"] for items in l1.values() for item in items if item["injectable"])
|
|
322
|
+
templates = sum(item["tokens"] for items in l1.values() for item in items if not item["injectable"])
|
|
323
|
+
return {"l1": l1, "tokens": tokens, "template_tokens": templates, "excluded": sorted(excluded),
|
|
324
|
+
"missing_specs": missing_specs, "empty_domains": empty_domains, "spec_domain_map": spec_domains}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _compression_usage(l1: dict[str, list[dict[str, object]]]) -> dict[str, object]:
|
|
328
|
+
items = [item for values in l1.values() for item in values if item.get("injectable", True)]
|
|
329
|
+
raw = sum(item.get("tokens_raw", item["tokens"]) for item in items)
|
|
330
|
+
compressed = sum(item.get("tokens_compressed", item["tokens"]) for item in items)
|
|
331
|
+
return {"total_raw": raw, "total_compressed": compressed,
|
|
332
|
+
"total_saved_pct": round((raw - compressed) * 100 / raw, 1) if raw else 0.0}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _usage_warnings(l0_tokens: int, l0_budget: int, l1_budget: int, total: int,
|
|
336
|
+
total_budget: int, usage: dict[str, object]) -> list[str]:
|
|
337
|
+
warnings = []
|
|
338
|
+
for exceeded, message in ((l0_tokens > l0_budget, "L0 超出预算"),
|
|
339
|
+
(total - l0_tokens > min(total_budget, l1_budget), "L1 全量聚合超过单次注入上限(不代表单次编辑超限)"),
|
|
340
|
+
(total > total_budget, "总预算超出")):
|
|
341
|
+
if exceeded:
|
|
342
|
+
warnings.append(message)
|
|
343
|
+
if usage["missing_specs"]:
|
|
344
|
+
warnings.append(f"配置的 spec 文件缺失: {len(usage['missing_specs'])} 个")
|
|
345
|
+
if usage["empty_domains"]:
|
|
346
|
+
domains = ", ".join(sorted(set(usage["empty_domains"])))
|
|
347
|
+
warnings.append(f"以下域未加载到任何 L1 spec: {domains}")
|
|
348
|
+
return warnings
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _catalog_usage(root: str, config: dict[str, object], budget: dict[str, object]) -> dict[str, object]:
|
|
352
|
+
maximum = _budget_number(budget, "catalog_max", 200)
|
|
353
|
+
mapping = build_effective_mapping(root, config.get("path_mapping") or {})
|
|
354
|
+
text = build_spec_catalog(root, mapping, maximum)
|
|
355
|
+
return {"mode": "context_first", "tokens": estimate_tokens(text), "budget": maximum,
|
|
356
|
+
"entries": text.count("\n- `")}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _runtime_manifest_counts(manifest: Path) -> dict[str, int]:
|
|
360
|
+
from codeflow.acceptance.cf_acceptance_risk import risk_counts
|
|
361
|
+
from codeflow.acceptance.cf_acceptance_schema import scenario_check_ids
|
|
362
|
+
if not manifest.is_file():
|
|
363
|
+
return {}
|
|
364
|
+
try:
|
|
365
|
+
data = json.loads(manifest.read_text(encoding="utf-8"))
|
|
366
|
+
if not isinstance(data, dict):
|
|
367
|
+
raise ValueError("manifest must be an object")
|
|
368
|
+
except (OSError, ValueError) as exc:
|
|
369
|
+
sys.stderr.write(f"cf_stats: cannot read {manifest}: {exc}\n")
|
|
370
|
+
return {}
|
|
371
|
+
stale: set[str] = set()
|
|
372
|
+
for row in data.get("scenarios", []):
|
|
373
|
+
if isinstance(row, dict) and row.get("status") == "stale":
|
|
374
|
+
ids = row.get("stale_checks") or scenario_check_ids(row) or [str(row.get("id"))]
|
|
375
|
+
stale.update(str(value) for value in ids)
|
|
376
|
+
return {"stale_checks": len(stale), **risk_counts(data)}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _deferred_rule_count(context: Path) -> int:
|
|
380
|
+
from codeflow.cli.cf_spec_context import ContextError, load_context
|
|
381
|
+
from codeflow.spec.cf_stage_gate import evidence_deferral_kind
|
|
382
|
+
if not context.is_file():
|
|
383
|
+
return 0
|
|
384
|
+
try:
|
|
385
|
+
data = load_context(str(context))
|
|
386
|
+
except (OSError, ValueError, ContextError) as exc:
|
|
387
|
+
sys.stderr.write(f"cf_stats: cannot read {context}: {exc}\n")
|
|
388
|
+
return 0
|
|
389
|
+
return sum(any(
|
|
390
|
+
status.status == "unverified" and bool(status.evidence)
|
|
391
|
+
and evidence_deferral_kind(status.evidence[-1]) in ("review_stage", "heavy")
|
|
392
|
+
for status in rule.stage_status.values()
|
|
393
|
+
) for binding in data.bindings for rule in binding.rules)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _runtime_usage(root: str) -> dict[str, object]:
|
|
397
|
+
"""Counts describe unique checks, live risk acceptances and deferred rules."""
|
|
398
|
+
from codeflow.evidence.cf_evidence_store import EvidenceStore, requirement_key
|
|
399
|
+
marker = Path(root) / ".code-flow/.active-task.json"
|
|
400
|
+
task_dir = ""
|
|
401
|
+
if marker.is_file():
|
|
402
|
+
try:
|
|
403
|
+
data = json.loads(marker.read_text(encoding="utf-8"))
|
|
404
|
+
task_dir = str(data.get("task_dir") or "") if isinstance(data, dict) else ""
|
|
405
|
+
except (OSError, ValueError) as exc:
|
|
406
|
+
sys.stderr.write(f"cf_stats: cannot read {marker}: {exc}\n")
|
|
407
|
+
runtime: dict[str, object] = {
|
|
408
|
+
"task_dir": task_dir, "context_bytes": 0, "evidence_runs": 0,
|
|
409
|
+
"deferred_rules": 0, "stale_checks": 0, "accepted_risks": 0,
|
|
410
|
+
"recorded_risks": 0, "inactive_risks": 0,
|
|
411
|
+
}
|
|
412
|
+
if not task_dir:
|
|
413
|
+
return runtime
|
|
414
|
+
directory = (Path(root) / task_dir).resolve()
|
|
415
|
+
context = directory / "spec-context.yml"
|
|
416
|
+
if context.is_file():
|
|
417
|
+
runtime["context_bytes"] = context.stat().st_size
|
|
418
|
+
try:
|
|
419
|
+
store = EvidenceStore(root, requirement=requirement_key(directory))
|
|
420
|
+
runtime["evidence_runs"] = sum(
|
|
421
|
+
1 for path in store.directory.rglob("*.json")
|
|
422
|
+
if path.name != "latest.json" and "_context" not in path.parts
|
|
423
|
+
)
|
|
424
|
+
except (OSError, ValueError) as exc:
|
|
425
|
+
sys.stderr.write(f"cf_stats: cannot count evidence: {exc}\n")
|
|
426
|
+
runtime.update(_runtime_manifest_counts(directory / ".acceptance-manifest.json"))
|
|
427
|
+
runtime["deferred_rules"] = _deferred_rule_count(context)
|
|
428
|
+
return runtime
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _stats_report(root: str, args: argparse.Namespace) -> dict[str, object]:
|
|
432
|
+
config = load_config(root)
|
|
433
|
+
budget = config.get("budget") or {}
|
|
434
|
+
l0_budget = _budget_number(budget, "l0_max", 800)
|
|
435
|
+
l1_budget = _budget_number(budget, "l1_max", 1700)
|
|
436
|
+
total_budget = _budget_number(budget, "total", l0_budget + l1_budget)
|
|
437
|
+
instruction = project_instruction_file(root, args.platform)
|
|
438
|
+
instruction_path = os.path.join(root, instruction)
|
|
439
|
+
l0_tokens = estimate_tokens(read_text(instruction_path)) if os.path.exists(instruction_path) else 0
|
|
440
|
+
usage = _domain_usage(root, config, args.domain)
|
|
441
|
+
total = l0_tokens + usage["tokens"]
|
|
442
|
+
report = {
|
|
443
|
+
"l0": {"file": instruction, "tokens": l0_tokens, "budget": l0_budget},
|
|
444
|
+
"l1": usage["l1"], "total_tokens": total, "total_budget": total_budget,
|
|
445
|
+
"budget_summary": {"aggregate_total": usage["tokens"], "single_edit_limit": min(total_budget, l1_budget)},
|
|
446
|
+
"utilization": f"{round(total * 100 / total_budget)}%" if total_budget else "0%",
|
|
447
|
+
"warnings": _usage_warnings(l0_tokens, l0_budget, l1_budget, total, total_budget, usage),
|
|
448
|
+
"spec_domain_map": usage["spec_domain_map"], "missing_specs": usage["missing_specs"],
|
|
449
|
+
"compression_summary": _compression_usage(usage["l1"]), "catalog": _catalog_usage(root, config, budget),
|
|
450
|
+
"quality_loop": quality_loop_summary(root, config), "spec_workflow": spec_workflow_summary(root),
|
|
451
|
+
"templates": {"tokens": usage["template_tokens"], "files": usage["excluded"],
|
|
452
|
+
"note": "tags:[] 命令专用模板,永不自动注入,不计预算"},
|
|
453
|
+
"runtime": _runtime_usage(root),
|
|
454
|
+
}
|
|
455
|
+
if args.audit:
|
|
456
|
+
from codeflow.spec.cf_scan import build_report
|
|
457
|
+
scan = build_report(root, args.platform)
|
|
458
|
+
report["audit"] = {"files": [entry for entry in scan["files"] if entry.get("issues")],
|
|
459
|
+
"review": scan["review"], "warnings": scan["audit_warnings"],
|
|
460
|
+
"path_resolutions": [{"path": entry["path"], "resolved_paths": entry["resolved_paths"]}
|
|
461
|
+
for entry in scan["files"] if entry.get("resolved_paths")]}
|
|
462
|
+
return report
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _print_domains(report: dict[str, object]) -> None:
|
|
466
|
+
for domain, items in report["l1"].items():
|
|
467
|
+
print(f"L1 {domain}:", sum(item["tokens"] for item in items if item.get("injectable", True)))
|
|
468
|
+
for item in items:
|
|
469
|
+
raw, compressed = item.get("tokens_raw", item["tokens"]), item.get("tokens_compressed", item["tokens"])
|
|
470
|
+
suffix = "" if item.get("injectable", True) else "(模板,不计预算)"
|
|
471
|
+
print(" -", item["path"], item["tokens"], f"(raw={raw}→compressed={compressed}, -{item.get('saved_pct', 0.0)}%)" + suffix)
|
|
472
|
+
if report["templates"]["tokens"]:
|
|
473
|
+
print("TEMPLATES (非注入):", f"{report['templates']['tokens']} tokens,不计预算")
|
|
474
|
+
if report["missing_specs"]:
|
|
475
|
+
print("MISSING SPECS:")
|
|
476
|
+
for item in report["missing_specs"]:
|
|
477
|
+
print(" -", item["domain"], item["path"])
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _print_quality(summary: dict[str, object]) -> None:
|
|
481
|
+
print("QUALITY-LOOP:", "enabled" if summary["switches"]["enabled"] else "disabled")
|
|
482
|
+
if summary.get("note"):
|
|
483
|
+
print(" -", summary["note"])
|
|
484
|
+
return
|
|
485
|
+
print(" - violations:", summary.get("violation_total", 0), "| fix_rate:", summary.get("fix_rate", "n/a"))
|
|
486
|
+
for item in summary.get("top_violations", [])[:5]:
|
|
487
|
+
print(" ·", item["rule"], item["count"])
|
|
488
|
+
for check_id, info in summary.get("checks", {}).items():
|
|
489
|
+
if info["disabled"] or info["fp_count"]:
|
|
490
|
+
suffix = f" DISABLED({info['disabled_reason']})" if info["disabled"] else ""
|
|
491
|
+
print(f" · {check_id}: hits={info['hit_count']} fp={info['fp_count']}" + suffix)
|
|
492
|
+
for component, info in summary.get("degraded", {}).items():
|
|
493
|
+
print(f" · degraded {component}: {info['count']} ({info['last_error']})")
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _print_audit(audit: Optional[dict[str, object]]) -> None:
|
|
497
|
+
if audit is None:
|
|
498
|
+
print("AUDIT: 运行 code-flow stats --audit 查看规范质量问题与待复审清单")
|
|
499
|
+
return
|
|
500
|
+
print("AUDIT (规范质量):")
|
|
501
|
+
if not audit["files"] and not audit["review"] and not audit.get("warnings"):
|
|
502
|
+
print(" - 无问题")
|
|
503
|
+
for warning in audit.get("warnings", []):
|
|
504
|
+
print(f" - 无效审计路径根: {warning['path']}")
|
|
505
|
+
for entry in audit["files"]:
|
|
506
|
+
print(" -", entry["path"], "|", " / ".join(entry["issues"]))
|
|
507
|
+
if audit["review"]:
|
|
508
|
+
print(" REVIEW (待复审):")
|
|
509
|
+
for item in audit["review"]:
|
|
510
|
+
print(f" · {item['item']}: {item['reason']}")
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _print_stats(report: dict[str, object]) -> None:
|
|
514
|
+
l0, compression, catalog = report["l0"], report["compression_summary"], report["catalog"]
|
|
515
|
+
print(f"L0 ({l0['file']}):", f"{l0['tokens']} / {l0['budget']}")
|
|
516
|
+
_print_domains(report)
|
|
517
|
+
print("TOTAL:", f"{report['total_tokens']} / {report['total_budget']}")
|
|
518
|
+
print("UTILIZATION:", report["utilization"])
|
|
519
|
+
print("COMPRESSION:", f"{compression['total_raw']} → {compression['total_compressed']} (-{compression['total_saved_pct']}%)")
|
|
520
|
+
print("CATALOG:", "mode=context_first,", f"{catalog['tokens']} / {catalog['budget']} tokens,", f"{catalog['entries']} entries")
|
|
521
|
+
_print_quality(report["quality_loop"])
|
|
522
|
+
runtime = report.get("runtime") or {}
|
|
523
|
+
if runtime.get("task_dir"):
|
|
524
|
+
print("RUNTIME:", f"context={runtime.get('context_bytes', 0)}B,",
|
|
525
|
+
f"evidence_runs={runtime.get('evidence_runs', 0)},",
|
|
526
|
+
f"deferred_rules={runtime.get('deferred_rules', 0)},",
|
|
527
|
+
f"stale_checks={runtime.get('stale_checks', 0)},",
|
|
528
|
+
f"accepted_risks={runtime.get('accepted_risks', 0)}")
|
|
529
|
+
if report["warnings"]:
|
|
530
|
+
print("WARNINGS:", "; ".join(report["warnings"]))
|
|
531
|
+
_print_audit(report.get("audit"))
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def main() -> None:
|
|
535
|
+
args = _arguments()
|
|
536
|
+
report = _stats_report(os.getcwd(), args)
|
|
537
|
+
if args.human:
|
|
538
|
+
_print_stats(report)
|
|
539
|
+
else:
|
|
540
|
+
sys.stdout.write(json.dumps(report, ensure_ascii=False) + "\n")
|