@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,31 @@
|
|
|
1
|
+
"""Programmatic validation entry with complete Git scope and argv execution."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from typing import IO, Optional, Sequence
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from codeflow.task.cf_validation_service import validate_files, validation_scope
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
|
|
14
|
+
parser = argparse.ArgumentParser(prog=os.environ.get("CF_RUNTIME_COMMAND") or None)
|
|
15
|
+
parser.add_argument("--root", default=os.getcwd())
|
|
16
|
+
parser.add_argument("--files", nargs="*", default=[])
|
|
17
|
+
parser.add_argument("--budget", type=float)
|
|
18
|
+
parser.add_argument("--no-heavy", action="store_true", help="Skip heavy validators (task-level finish scope)")
|
|
19
|
+
parser.add_argument("--no-cache", action="store_true", help="Force validators to run; skip content-fingerprint reuse")
|
|
20
|
+
parser.add_argument("--scope-only", action="store_true")
|
|
21
|
+
parser.add_argument("--json", action="store_true")
|
|
22
|
+
args = parser.parse_args(argv)
|
|
23
|
+
try:
|
|
24
|
+
result = ({"files": validation_scope(args.root, args.files or None)} if args.scope_only
|
|
25
|
+
else validate_files(args.root, args.files or None, args.budget, include_heavy=not args.no_heavy,
|
|
26
|
+
use_cache=not args.no_cache))
|
|
27
|
+
stdout.write(json.dumps(result, ensure_ascii=False))
|
|
28
|
+
return 3 if result.get("decision") == "block" else 0
|
|
29
|
+
except (OSError, ValueError) as exc:
|
|
30
|
+
stdout.write(json.dumps({"decision": "block", "error": str(exc)}, ensure_ascii=False))
|
|
31
|
+
return 2
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Rebuildable Context parse cache bound to source bytes and parser version."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
from codeflow.core import cf_fs
|
|
10
|
+
from codeflow.core.cf_log import warn
|
|
11
|
+
import codeflow.spec.cf_spec_context_model as cf_spec_context_model
|
|
12
|
+
from codeflow.spec.cf_spec_context_model import (ContextError, SpecContext, context_from_data,
|
|
13
|
+
context_sha256, context_to_data)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ContextLoader(Protocol):
|
|
17
|
+
def __call__(self, path: str, *, source: bytes | None = None) -> SpecContext: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _digest(value: dict[str, object]) -> str:
|
|
21
|
+
encoded = json.dumps(value, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
22
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _read(path: Path, key: str) -> dict[str, object]:
|
|
26
|
+
try:
|
|
27
|
+
text = cf_fs.read_text(path)
|
|
28
|
+
if text is None:
|
|
29
|
+
return {}
|
|
30
|
+
payload = json.loads(text)
|
|
31
|
+
if not isinstance(payload, dict):
|
|
32
|
+
return {}
|
|
33
|
+
checksum = payload.pop("checksum", None)
|
|
34
|
+
if payload.get("version") == 1 and payload.get("key") == key and checksum == _digest(payload):
|
|
35
|
+
return payload
|
|
36
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
37
|
+
warn(f"Context parse cache unavailable; reloading source: {exc}")
|
|
38
|
+
return {}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _write(path: Path, payload: dict[str, object]) -> None:
|
|
42
|
+
try:
|
|
43
|
+
payload = dict(payload, checksum=_digest(payload))
|
|
44
|
+
cf_fs.atomic_write_text(path, json.dumps(payload, ensure_ascii=False))
|
|
45
|
+
except (OSError, UnicodeError, TypeError) as exc:
|
|
46
|
+
warn(f"Context parse cache cannot be saved: {exc}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_snapshot(root: str, path: str, loader: ContextLoader) -> tuple[SpecContext, str]:
|
|
50
|
+
source = Path(path).read_bytes()
|
|
51
|
+
parser = Path(cf_spec_context_model.__file__).read_bytes()
|
|
52
|
+
key = hashlib.sha256(str(Path(path).resolve()).encode("utf-8") + b"\0"
|
|
53
|
+
+ hashlib.sha256(source).digest() + hashlib.sha256(parser).digest()).hexdigest()
|
|
54
|
+
cache = Path(root) / ".code-flow/.context-state.json"
|
|
55
|
+
payload = _read(cache, key)
|
|
56
|
+
if payload:
|
|
57
|
+
try:
|
|
58
|
+
context = context_from_data(payload.get("context"), path)
|
|
59
|
+
identity = payload.get("identity")
|
|
60
|
+
if isinstance(identity, str) and len(identity) == 64:
|
|
61
|
+
return context, identity
|
|
62
|
+
except ContextError as exc:
|
|
63
|
+
warn(f"Context parse cache invalid; reloading source: {exc}")
|
|
64
|
+
context = loader(path, source=source)
|
|
65
|
+
identity = context_sha256(context)
|
|
66
|
+
_write(cache, {"version": 1, "key": key, "context": context_to_data(context), "identity": identity})
|
|
67
|
+
return context, identity
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
import fnmatch
|
|
3
|
+
import json
|
|
3
4
|
import os
|
|
4
5
|
import re
|
|
5
6
|
import sys
|
|
6
7
|
import time
|
|
7
8
|
|
|
9
|
+
from codeflow.core import cf_log
|
|
10
|
+
from codeflow.core.cf_markdown import parse_frontmatter
|
|
11
|
+
|
|
8
12
|
# --- Config cache (fix #3: avoid re-parsing YAML on every hook call) ---
|
|
9
13
|
|
|
10
14
|
_config_cache: dict = {}
|
|
@@ -13,6 +17,12 @@ _effective_mapping_cache: dict = {}
|
|
|
13
17
|
_ext_set_cache: dict = {}
|
|
14
18
|
|
|
15
19
|
|
|
20
|
+
class ConfigError(ValueError):
|
|
21
|
+
"""An existing project configuration cannot be read or validated."""
|
|
22
|
+
|
|
23
|
+
code = "invalid_config"
|
|
24
|
+
|
|
25
|
+
|
|
16
26
|
def project_instruction_file(project_root: str, platform: str = "") -> str:
|
|
17
27
|
"""Select the installed platform's instruction file, with legacy fallback."""
|
|
18
28
|
names = ("CLAUDE.md", "AGENTS.md") if platform in ("claude", "costrict") else ("AGENTS.md", "CLAUDE.md")
|
|
@@ -24,24 +34,27 @@ def load_config(project_root: str) -> dict:
|
|
|
24
34
|
if not os.path.exists(config_path):
|
|
25
35
|
return {}
|
|
26
36
|
try:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
info = os.stat(config_path)
|
|
38
|
+
signature = (info.st_mtime_ns, info.st_ctime_ns, info.st_size)
|
|
39
|
+
except OSError as exc:
|
|
40
|
+
raise ConfigError(f"{config_path}: {exc}") from exc
|
|
30
41
|
cached = _config_cache.get(config_path)
|
|
31
|
-
if cached and cached["mtime"] ==
|
|
42
|
+
if cached and cached["mtime"] == signature:
|
|
32
43
|
return cached["data"]
|
|
33
44
|
try:
|
|
34
45
|
import yaml
|
|
35
|
-
except
|
|
36
|
-
|
|
46
|
+
except ImportError as exc:
|
|
47
|
+
raise ConfigError("PyYAML is required to read code-flow configuration") from exc
|
|
37
48
|
try:
|
|
38
49
|
with open(config_path, "r", encoding="utf-8") as file:
|
|
39
50
|
data = yaml.safe_load(file)
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
if not isinstance(data, dict):
|
|
52
|
+
raise ConfigError(f"{config_path}: configuration must be a YAML mapping")
|
|
53
|
+
result = data
|
|
54
|
+
_config_cache[config_path] = {"mtime": signature, "data": result}
|
|
42
55
|
return result
|
|
43
|
-
except
|
|
44
|
-
|
|
56
|
+
except (OSError, UnicodeError, yaml.YAMLError) as exc:
|
|
57
|
+
raise ConfigError(f"{config_path}: {exc}") from exc
|
|
45
58
|
|
|
46
59
|
|
|
47
60
|
def estimate_tokens(text: str) -> int:
|
|
@@ -86,9 +99,6 @@ def _spec_path_from_entry(entry) -> str:
|
|
|
86
99
|
|
|
87
100
|
|
|
88
101
|
def discover_spec_domains(project_root: str) -> dict:
|
|
89
|
-
cached = _spec_domains_cache.get(project_root)
|
|
90
|
-
if cached is not None:
|
|
91
|
-
return cached
|
|
92
102
|
specs_root = os.path.join(project_root, ".code-flow", "specs")
|
|
93
103
|
discovered = {}
|
|
94
104
|
if not os.path.isdir(specs_root):
|
|
@@ -122,12 +132,24 @@ def _default_spec_entry(rel: str) -> dict:
|
|
|
122
132
|
return {"path": rel, "tags": ["*"], "tier": tier}
|
|
123
133
|
|
|
124
134
|
|
|
135
|
+
def _mapping_signature(mapping: dict) -> str:
|
|
136
|
+
"""Content signature for the mapping cache.
|
|
137
|
+
|
|
138
|
+
`id(mapping)` keys were reused after GC, potentially serving a mapping
|
|
139
|
+
built from stale config contents. Hash the JSON projection instead.
|
|
140
|
+
"""
|
|
141
|
+
try:
|
|
142
|
+
return json.dumps(mapping or {}, ensure_ascii=False, sort_keys=True, default=str)
|
|
143
|
+
except (TypeError, ValueError):
|
|
144
|
+
return ""
|
|
145
|
+
|
|
146
|
+
|
|
125
147
|
def build_effective_mapping(project_root: str, mapping: dict) -> dict:
|
|
126
|
-
|
|
148
|
+
discovered = discover_spec_domains(project_root)
|
|
149
|
+
cache_key = (project_root, _mapping_signature(mapping), _mapping_signature(discovered))
|
|
127
150
|
cached = _effective_mapping_cache.get(cache_key)
|
|
128
151
|
if cached is not None:
|
|
129
152
|
return cached
|
|
130
|
-
discovered = discover_spec_domains(project_root)
|
|
131
153
|
effective = {}
|
|
132
154
|
|
|
133
155
|
for domain, rel_paths in discovered.items():
|
|
@@ -182,10 +204,12 @@ def is_code_file(rel_path: str, runtime_config: dict) -> bool:
|
|
|
182
204
|
if ext in (runtime_config.get("skip_extensions") or []):
|
|
183
205
|
return False
|
|
184
206
|
code_exts = runtime_config.get("code_extensions") or []
|
|
185
|
-
|
|
207
|
+
# Content key: id() reuse after GC could serve a stale extension set.
|
|
208
|
+
ext_key = tuple(code_exts)
|
|
209
|
+
ext_set = _ext_set_cache.get(ext_key)
|
|
186
210
|
if ext_set is None:
|
|
187
211
|
ext_set = frozenset(code_exts)
|
|
188
|
-
_ext_set_cache[
|
|
212
|
+
_ext_set_cache[ext_key] = ext_set
|
|
189
213
|
return ext in ext_set
|
|
190
214
|
|
|
191
215
|
|
|
@@ -201,119 +225,6 @@ def match_domains(rel_path: str, mapping: dict) -> list:
|
|
|
201
225
|
return domains
|
|
202
226
|
|
|
203
227
|
|
|
204
|
-
# --- Fix #2: safe depluralization with allowlist ---
|
|
205
|
-
|
|
206
|
-
_SAFE_DEPLURALS = {
|
|
207
|
-
"models": "model",
|
|
208
|
-
"services": "service",
|
|
209
|
-
"components": "component",
|
|
210
|
-
"handlers": "handler",
|
|
211
|
-
"controllers": "controller",
|
|
212
|
-
"middlewares": "middleware",
|
|
213
|
-
"validators": "validator",
|
|
214
|
-
"schemas": "schema",
|
|
215
|
-
"repositories": "repository",
|
|
216
|
-
"migrations": "migration",
|
|
217
|
-
"fixtures": "fixture",
|
|
218
|
-
"plugins": "plugin",
|
|
219
|
-
"routes": "route",
|
|
220
|
-
"routers": "router",
|
|
221
|
-
"hooks": "hook",
|
|
222
|
-
"pages": "page",
|
|
223
|
-
"stores": "store",
|
|
224
|
-
"styles": "style",
|
|
225
|
-
"types": "type",
|
|
226
|
-
"configs": "config",
|
|
227
|
-
"scripts": "script",
|
|
228
|
-
"tasks": "task",
|
|
229
|
-
"specs": "spec",
|
|
230
|
-
"tests": "test",
|
|
231
|
-
"utils": "util",
|
|
232
|
-
"helpers": "helper",
|
|
233
|
-
"views": "view",
|
|
234
|
-
"templates": "template",
|
|
235
|
-
"errors": "error",
|
|
236
|
-
"exceptions": "exception",
|
|
237
|
-
"docs": "doc",
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
# --- Fix #1: semantic directory → concept tag mapping ---
|
|
241
|
-
|
|
242
|
-
_DIR_SEMANTIC_TAGS = {
|
|
243
|
-
"handlers": ["api", "error"],
|
|
244
|
-
"controllers": ["api"],
|
|
245
|
-
"middleware": ["api", "config"],
|
|
246
|
-
"middlewares": ["api", "config"],
|
|
247
|
-
"routers": ["api", "route"],
|
|
248
|
-
"routes": ["api", "route"],
|
|
249
|
-
"views": ["ui", "render"],
|
|
250
|
-
"templates": ["ui", "render"],
|
|
251
|
-
"models": ["model", "database", "orm", "schema"],
|
|
252
|
-
"model": ["model", "database", "orm", "schema"],
|
|
253
|
-
"schemas": ["model", "schema", "database"],
|
|
254
|
-
"migrations": ["database", "migration"],
|
|
255
|
-
"repositories": ["database", "query"],
|
|
256
|
-
"dao": ["database", "query"],
|
|
257
|
-
"validators": ["quality", "error"],
|
|
258
|
-
"exceptions": ["error", "exception"],
|
|
259
|
-
"errors": ["error", "exception"],
|
|
260
|
-
"auth": ["api", "config"],
|
|
261
|
-
"config": ["config", "deploy"],
|
|
262
|
-
"configs": ["config", "deploy"],
|
|
263
|
-
"settings": ["config"],
|
|
264
|
-
"tests": ["test", "quality"],
|
|
265
|
-
"test": ["test", "quality"],
|
|
266
|
-
"utils": ["quality"],
|
|
267
|
-
"helpers": ["quality"],
|
|
268
|
-
"lib": ["quality"],
|
|
269
|
-
"common": ["quality"],
|
|
270
|
-
"shared": ["quality"],
|
|
271
|
-
"core": ["quality"],
|
|
272
|
-
"logging": ["log", "logging"],
|
|
273
|
-
"logger": ["log", "logging"],
|
|
274
|
-
"logs": ["log", "logging"],
|
|
275
|
-
"cache": ["cache", "performance"],
|
|
276
|
-
"queue": ["performance"],
|
|
277
|
-
"jobs": ["performance"],
|
|
278
|
-
"workers": ["performance"],
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
def extract_context_tags(rel_path: str) -> set:
|
|
283
|
-
"""Extract context tags from a file path for spec matching.
|
|
284
|
-
|
|
285
|
-
Uses three strategies:
|
|
286
|
-
1. Directory names as tags (with safe depluralization)
|
|
287
|
-
2. Semantic mapping: common directory names → concept tags
|
|
288
|
-
3. Filename stem word splitting
|
|
289
|
-
"""
|
|
290
|
-
rel_path = normalize_path(rel_path)
|
|
291
|
-
tags = set()
|
|
292
|
-
parts = rel_path.split("/")
|
|
293
|
-
|
|
294
|
-
for part in parts[:-1]:
|
|
295
|
-
lower = part.lower()
|
|
296
|
-
tags.add(lower)
|
|
297
|
-
deplural = _SAFE_DEPLURALS.get(lower)
|
|
298
|
-
if deplural:
|
|
299
|
-
tags.add(deplural)
|
|
300
|
-
semantic = _DIR_SEMANTIC_TAGS.get(lower)
|
|
301
|
-
if semantic:
|
|
302
|
-
tags.update(semantic)
|
|
303
|
-
|
|
304
|
-
filename = parts[-1] if parts else ""
|
|
305
|
-
stem = os.path.splitext(filename)[0].lower()
|
|
306
|
-
if stem:
|
|
307
|
-
words = _FILENAME_WORDS_RE.findall(stem.replace("_", " ").replace("-", " "))
|
|
308
|
-
tags.update(words)
|
|
309
|
-
for word in words:
|
|
310
|
-
semantic = _DIR_SEMANTIC_TAGS.get(word)
|
|
311
|
-
if semantic:
|
|
312
|
-
tags.update(semantic)
|
|
313
|
-
|
|
314
|
-
return tags
|
|
315
|
-
|
|
316
|
-
|
|
317
228
|
def normalize_spec_entry(entry) -> dict:
|
|
318
229
|
"""Normalize spec config entry. Supports both old (string) and new (dict) format."""
|
|
319
230
|
if isinstance(entry, str):
|
|
@@ -327,7 +238,6 @@ def normalize_spec_entry(entry) -> dict:
|
|
|
327
238
|
return {}
|
|
328
239
|
|
|
329
240
|
|
|
330
|
-
_FILENAME_WORDS_RE = re.compile(r"[a-z]+")
|
|
331
241
|
_BULLET_PREFIXES = ("- ", "* ", "+ ")
|
|
332
242
|
_COMPRESS_HTML_RE = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
333
243
|
_COMPRESS_TRAILING_WS_RE = re.compile(r"[ \t]+$", re.MULTILINE)
|
|
@@ -376,107 +286,6 @@ def compress_content(text: str) -> str:
|
|
|
376
286
|
return text
|
|
377
287
|
|
|
378
288
|
|
|
379
|
-
def read_matched_specs(
|
|
380
|
-
project_root: str,
|
|
381
|
-
domain: str,
|
|
382
|
-
matched: list,
|
|
383
|
-
compress: bool = True,
|
|
384
|
-
) -> list:
|
|
385
|
-
"""Read matched spec files; optionally apply lossless compression.
|
|
386
|
-
|
|
387
|
-
When ``compress`` is True (default), each spec's content is passed through
|
|
388
|
-
:func:`compress_content` before token estimation so callers can make
|
|
389
|
-
explicit budget decisions from the returned token counts.
|
|
390
|
-
|
|
391
|
-
Returned items carry:
|
|
392
|
-
- ``content``: final content (compressed when ``compress=True``)
|
|
393
|
-
- ``tokens``: token count of final content (drives budget decisions)
|
|
394
|
-
- ``tokens_raw``: token count of uncompressed content (for cf-stats)
|
|
395
|
-
|
|
396
|
-
``CF_DEBUG=1`` emits a per-file ``compress path=... raw=... compressed=...
|
|
397
|
-
saved=...%`` record via :func:`debug_log`.
|
|
398
|
-
"""
|
|
399
|
-
specs_root = os.path.join(project_root, ".code-flow", "specs")
|
|
400
|
-
specs: list = []
|
|
401
|
-
for cfg in matched:
|
|
402
|
-
rel = cfg["path"]
|
|
403
|
-
spec_path = os.path.join(specs_root, rel)
|
|
404
|
-
try:
|
|
405
|
-
with open(spec_path, "r", encoding="utf-8") as f:
|
|
406
|
-
raw_content = f.read().strip()
|
|
407
|
-
# Frontmatter feeds the spec catalog only — never spend injection
|
|
408
|
-
# budget on it. Specs without frontmatter pass through unchanged.
|
|
409
|
-
_, raw_content = parse_spec_frontmatter(raw_content)
|
|
410
|
-
raw_content = raw_content.strip()
|
|
411
|
-
if not raw_content:
|
|
412
|
-
continue
|
|
413
|
-
raw_tokens = estimate_tokens(raw_content)
|
|
414
|
-
if compress:
|
|
415
|
-
try:
|
|
416
|
-
content = compress_content(raw_content)
|
|
417
|
-
except Exception as exc:
|
|
418
|
-
_log(f"read_matched_specs compress failed path={rel}: {exc}")
|
|
419
|
-
content = raw_content
|
|
420
|
-
else:
|
|
421
|
-
content = raw_content
|
|
422
|
-
tokens = estimate_tokens(content)
|
|
423
|
-
if compress and raw_tokens != tokens:
|
|
424
|
-
saved_pct = (
|
|
425
|
-
round((raw_tokens - tokens) * 100 / raw_tokens, 1)
|
|
426
|
-
if raw_tokens
|
|
427
|
-
else 0.0
|
|
428
|
-
)
|
|
429
|
-
debug_log(
|
|
430
|
-
f"compress path={rel} raw={raw_tokens} "
|
|
431
|
-
f"compressed={tokens} saved={saved_pct}%",
|
|
432
|
-
project_root,
|
|
433
|
-
)
|
|
434
|
-
specs.append(
|
|
435
|
-
{
|
|
436
|
-
"path": rel,
|
|
437
|
-
"content": content,
|
|
438
|
-
"tokens": tokens,
|
|
439
|
-
"tokens_raw": raw_tokens,
|
|
440
|
-
"domain": domain,
|
|
441
|
-
"tier": cfg.get("tier", 1),
|
|
442
|
-
}
|
|
443
|
-
)
|
|
444
|
-
except Exception:
|
|
445
|
-
continue
|
|
446
|
-
return specs
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
# --- Kept for backward compatibility with old config format ---
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
def read_specs(project_root: str, domain: str, domain_cfg: dict) -> list:
|
|
453
|
-
specs_root = os.path.join(project_root, ".code-flow", "specs")
|
|
454
|
-
specs = []
|
|
455
|
-
for entry in domain_cfg.get("specs") or []:
|
|
456
|
-
cfg = normalize_spec_entry(entry)
|
|
457
|
-
rel = cfg.get("path", "")
|
|
458
|
-
if not rel:
|
|
459
|
-
continue
|
|
460
|
-
spec_path = os.path.join(specs_root, rel)
|
|
461
|
-
try:
|
|
462
|
-
with open(spec_path, "r", encoding="utf-8") as file:
|
|
463
|
-
content = file.read().strip()
|
|
464
|
-
if not content:
|
|
465
|
-
continue
|
|
466
|
-
specs.append(
|
|
467
|
-
{
|
|
468
|
-
"path": rel,
|
|
469
|
-
"content": content,
|
|
470
|
-
"tokens": estimate_tokens(content),
|
|
471
|
-
"domain": domain,
|
|
472
|
-
"tier": cfg.get("tier", 1),
|
|
473
|
-
}
|
|
474
|
-
)
|
|
475
|
-
except Exception:
|
|
476
|
-
continue
|
|
477
|
-
return specs
|
|
478
|
-
|
|
479
|
-
|
|
480
289
|
def non_injectable_specs(effective_mapping: dict) -> set:
|
|
481
290
|
"""tags: [] 的命令专用模板(如 shared 的 PRD/设计模板)——永不自动注入,
|
|
482
291
|
预算统计(cf-stats/cf-scan)必须排除,避免利用率虚高。"""
|
|
@@ -501,7 +310,9 @@ def resolve_quality_loop(config: dict) -> dict:
|
|
|
501
310
|
return {"enabled": False, "post_check": False,
|
|
502
311
|
"stop_check": False, "finish_check": False,
|
|
503
312
|
"correction_capture": False,
|
|
504
|
-
"compress_reminder": False
|
|
313
|
+
"compress_reminder": False,
|
|
314
|
+
"heavy_at_finish": False,
|
|
315
|
+
"finish_verifier_budget": 300.0}
|
|
505
316
|
cfg = config.get("quality_loop")
|
|
506
317
|
cfg = cfg if isinstance(cfg, dict) else {}
|
|
507
318
|
enabled = cfg.get("enabled") is True
|
|
@@ -509,6 +320,12 @@ def resolve_quality_loop(config: dict) -> dict:
|
|
|
509
320
|
def _sub(key: str) -> bool:
|
|
510
321
|
return enabled and cfg.get(key) is not False
|
|
511
322
|
|
|
323
|
+
# 任务级 verifier 执行预算(秒):超预算的 command/test verifier 延后到
|
|
324
|
+
# 需求级 verify-e2e;0 = 不限制(恢复旧行为)。非法值回落默认 300。
|
|
325
|
+
budget = cfg.get("finish_verifier_budget", 300)
|
|
326
|
+
if isinstance(budget, bool) or not isinstance(budget, (int, float)) or budget < 0 or budget != budget:
|
|
327
|
+
budget = 300.0
|
|
328
|
+
|
|
512
329
|
return {
|
|
513
330
|
"enabled": enabled,
|
|
514
331
|
"post_check": _sub("post_check"),
|
|
@@ -516,6 +333,9 @@ def resolve_quality_loop(config: dict) -> dict:
|
|
|
516
333
|
"finish_check": _sub("finish_check"),
|
|
517
334
|
"correction_capture": _sub("correction_capture"),
|
|
518
335
|
"compress_reminder": _sub("compress_reminder"),
|
|
336
|
+
# heavy validator 默认只在归档 cf_validation / /cf-validate 全量执行
|
|
337
|
+
"heavy_at_finish": enabled and cfg.get("heavy_at_finish") is True,
|
|
338
|
+
"finish_verifier_budget": float(budget),
|
|
519
339
|
}
|
|
520
340
|
|
|
521
341
|
|
|
@@ -540,30 +360,13 @@ def resolve_enforcement(config: dict) -> str:
|
|
|
540
360
|
return "required"
|
|
541
361
|
|
|
542
362
|
|
|
543
|
-
_FRONTMATTER_RE = re.compile(r"\A---[ \t]*\n(.*?)\n---[ \t]*\n?", re.DOTALL)
|
|
544
|
-
|
|
545
|
-
|
|
546
363
|
def parse_spec_frontmatter(content: str) -> tuple:
|
|
547
364
|
"""Split optional YAML frontmatter from spec content → (meta, body).
|
|
548
365
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
frontmatter returns ({}, content) unchanged.
|
|
366
|
+
Flat `key: value` parsing keeps the hook hot path free of pyyaml; content
|
|
367
|
+
without frontmatter returns ({}, content) unchanged.
|
|
552
368
|
"""
|
|
553
|
-
|
|
554
|
-
return {}, content
|
|
555
|
-
match = _FRONTMATTER_RE.match(content)
|
|
556
|
-
if not match:
|
|
557
|
-
return {}, content
|
|
558
|
-
meta: dict = {}
|
|
559
|
-
for line in match.group(1).splitlines():
|
|
560
|
-
key, sep, value = line.partition(":")
|
|
561
|
-
if not sep:
|
|
562
|
-
continue
|
|
563
|
-
key = key.strip()
|
|
564
|
-
if key:
|
|
565
|
-
meta[key] = value.strip().strip('"').strip("'")
|
|
566
|
-
return meta, content[match.end():]
|
|
369
|
+
return parse_frontmatter(content)
|
|
567
370
|
|
|
568
371
|
|
|
569
372
|
def spec_description(content: str) -> str:
|
|
@@ -595,6 +398,14 @@ CATALOG_HEADER = (
|
|
|
595
398
|
)
|
|
596
399
|
|
|
597
400
|
|
|
401
|
+
class CatalogBudgetError(ConfigError):
|
|
402
|
+
"""Catalog cannot fit its mandatory header and complete-list notice."""
|
|
403
|
+
|
|
404
|
+
def __init__(self, minimum: int) -> None:
|
|
405
|
+
self.minimum = minimum
|
|
406
|
+
super().__init__(f"catalog_max is too small; minimum is {minimum} estimated tokens")
|
|
407
|
+
|
|
408
|
+
|
|
598
409
|
def _catalog_entries(project_root: str, effective_mapping: dict) -> list:
|
|
599
410
|
"""Collect injectable specs as (tier, rel_path, abs_path) sorted tier1-first."""
|
|
600
411
|
specs_root = os.path.join(project_root, ".code-flow", "specs")
|
|
@@ -604,7 +415,7 @@ def _catalog_entries(project_root: str, effective_mapping: dict) -> list:
|
|
|
604
415
|
cfg = normalize_spec_entry(entry)
|
|
605
416
|
rel = cfg.get("path", "")
|
|
606
417
|
# tags: [] marks command-only templates — never advertised either
|
|
607
|
-
if not rel or not cfg.get("tags"):
|
|
418
|
+
if not rel or rel.startswith("_session/") or not cfg.get("tags"):
|
|
608
419
|
continue
|
|
609
420
|
entries.append((cfg.get("tier", 1), rel, os.path.join(specs_root, rel)))
|
|
610
421
|
# Tier 1 constraints first so budget truncation drops navigation maps first
|
|
@@ -612,66 +423,77 @@ def _catalog_entries(project_root: str, effective_mapping: dict) -> list:
|
|
|
612
423
|
return entries
|
|
613
424
|
|
|
614
425
|
|
|
615
|
-
def
|
|
616
|
-
project_root: str, effective_mapping: dict, catalog_max: int = 200
|
|
617
|
-
) -> str:
|
|
618
|
-
"""Build the compact Catalog used only when no task or path is active.
|
|
619
|
-
|
|
620
|
-
The model performs the semantic matching itself by reading the catalog and
|
|
621
|
-
pulling relevant specs — replacing keyword-table guessing on prompts with
|
|
622
|
-
no explicit path evidence. Cached per project_root keyed by the mtimes of
|
|
623
|
-
config.yml and every listed spec file.
|
|
624
|
-
"""
|
|
625
|
-
entries = _catalog_entries(project_root, effective_mapping)
|
|
626
|
-
if not entries:
|
|
627
|
-
return ""
|
|
628
|
-
config_path = os.path.join(project_root, ".code-flow", "config.yml")
|
|
629
|
-
signature = []
|
|
630
|
-
for path in [config_path] + [abs_path for _, _, abs_path in entries]:
|
|
631
|
-
try:
|
|
632
|
-
signature.append((path, os.path.getmtime(path)))
|
|
633
|
-
except OSError:
|
|
634
|
-
signature.append((path, 0.0))
|
|
635
|
-
signature = tuple(signature)
|
|
636
|
-
cached = _catalog_cache.get(project_root)
|
|
637
|
-
if cached and cached["signature"] == signature and cached["catalog_max"] == catalog_max:
|
|
638
|
-
return cached["catalog"]
|
|
639
|
-
|
|
426
|
+
def _catalog_lines(entries: list[tuple[int, str, str]]) -> list[str]:
|
|
640
427
|
lines = []
|
|
641
428
|
for tier, rel, abs_path in entries:
|
|
642
429
|
try:
|
|
643
430
|
with open(abs_path, "r", encoding="utf-8") as f:
|
|
644
431
|
desc = spec_description(f.read())
|
|
645
|
-
except
|
|
432
|
+
except (OSError, UnicodeError) as exc:
|
|
433
|
+
_log(f"Spec Catalog read failed: {rel}: {exc}")
|
|
646
434
|
continue
|
|
647
435
|
suffix = "(导航地图)" if tier == 0 else ""
|
|
648
436
|
lines.append(f"- `{rel}` — {desc or '(no description)'}{suffix}")
|
|
649
|
-
|
|
650
|
-
return ""
|
|
437
|
+
return lines
|
|
651
438
|
|
|
439
|
+
|
|
440
|
+
def _bounded_catalog(lines: list[str], maximum: int) -> str:
|
|
652
441
|
catalog = CATALOG_HEADER
|
|
653
442
|
included = 0
|
|
443
|
+
def footer(count: int) -> str:
|
|
444
|
+
if count == len(lines):
|
|
445
|
+
return ""
|
|
446
|
+
return (f"\n已展示 {count}/{len(lines)} 项;其余规范请运行 "
|
|
447
|
+
"`code-flow spec catalog --root . --stage code --json` 获取。")
|
|
654
448
|
for line in lines:
|
|
655
449
|
candidate = catalog + "\n" + line
|
|
656
|
-
if estimate_tokens(candidate) >
|
|
657
|
-
|
|
450
|
+
if estimate_tokens(candidate + footer(included + 1)) > maximum:
|
|
451
|
+
continue
|
|
658
452
|
catalog = candidate
|
|
659
453
|
included += 1
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
454
|
+
text = catalog + footer(included)
|
|
455
|
+
if estimate_tokens(text) > maximum:
|
|
456
|
+
raise CatalogBudgetError(estimate_tokens(text))
|
|
457
|
+
return text
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def build_spec_catalog(project_root: str, effective_mapping: dict, catalog_max: int = 200) -> str:
|
|
461
|
+
"""Publish bounded navigation with an explicit complete-catalog entry point."""
|
|
462
|
+
entries = _catalog_entries(project_root, effective_mapping)
|
|
463
|
+
if not entries:
|
|
666
464
|
return ""
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
465
|
+
signature = []
|
|
466
|
+
for _, _, path in entries:
|
|
467
|
+
try:
|
|
468
|
+
info = os.stat(path)
|
|
469
|
+
signature.append((path, info.st_mtime_ns, info.st_ctime_ns, info.st_size))
|
|
470
|
+
except OSError:
|
|
471
|
+
signature.append((path, 0, 0, 0))
|
|
472
|
+
cached = _catalog_cache.get(project_root)
|
|
473
|
+
if cached and cached["signature"] == signature and cached["catalog_max"] == catalog_max:
|
|
474
|
+
return cached["catalog"]
|
|
475
|
+
lines = _catalog_lines(entries)
|
|
476
|
+
catalog = _bounded_catalog(lines, catalog_max) if lines else ""
|
|
477
|
+
_catalog_cache[project_root] = {"signature": signature, "catalog_max": catalog_max, "catalog": catalog}
|
|
672
478
|
return catalog
|
|
673
479
|
|
|
674
480
|
|
|
481
|
+
def catalog_omissions(project_root: str, effective_mapping: dict, catalog_max: int) -> dict[str, object]:
|
|
482
|
+
"""Compute omissions from the actual bounded catalog without changing its ordering."""
|
|
483
|
+
from collections import Counter
|
|
484
|
+
lines = _catalog_lines(_catalog_entries(project_root, effective_mapping))
|
|
485
|
+
text = build_spec_catalog(project_root, effective_mapping, catalog_max)
|
|
486
|
+
remaining = Counter(line for line in text.splitlines() if line.startswith('- `'))
|
|
487
|
+
dropped: list[str] = []
|
|
488
|
+
for line in lines:
|
|
489
|
+
if remaining[line]:
|
|
490
|
+
remaining[line] -= 1
|
|
491
|
+
else:
|
|
492
|
+
dropped.append(line.split('`', 2)[1])
|
|
493
|
+
return {"dropped_count": len(dropped),
|
|
494
|
+
"dropped_domains": sorted({path.split('/', 1)[0] for path in dropped})}
|
|
495
|
+
|
|
496
|
+
|
|
675
497
|
def assemble_context(specs: list, heading: str) -> str:
|
|
676
498
|
parts = [heading]
|
|
677
499
|
parts.append("以上规范是本次开发的约束条件,生成代码必须遵循。")
|
|
@@ -695,8 +517,8 @@ def assemble_context(specs: list, heading: str) -> str:
|
|
|
695
517
|
|
|
696
518
|
|
|
697
519
|
def _log(msg: str) -> None:
|
|
698
|
-
"""
|
|
699
|
-
|
|
520
|
+
"""Diagnostics go to stderr through the shared L0 writer (fix #9)."""
|
|
521
|
+
cf_log.warn(msg)
|
|
700
522
|
|
|
701
523
|
|
|
702
524
|
_PROCESS_START = time.monotonic()
|