@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,193 +0,0 @@
|
|
|
1
|
-
"""Read-only, input-bound Spec optimization proposals."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import hashlib
|
|
5
|
-
import difflib
|
|
6
|
-
import json
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
import re
|
|
9
|
-
from typing import Sequence
|
|
10
|
-
|
|
11
|
-
from codeflow.core.cf_core import estimate_tokens, load_config
|
|
12
|
-
from codeflow.spec.cf_spec_budget import measure_path_budget
|
|
13
|
-
from codeflow.spec.cf_spec_impact import scan_impacts
|
|
14
|
-
from codeflow.spec.cf_spec_metadata import parse_spec_metadata
|
|
15
|
-
from codeflow.spec.cf_spec_resolver import resolve_candidates
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _selected(root: str, paths: Sequence[str]) -> list:
|
|
19
|
-
all_candidates = list(resolve_candidates(root, 'code', ()))
|
|
20
|
-
if not paths:
|
|
21
|
-
return all_candidates
|
|
22
|
-
normalized = [item.replace('\\', '/').removeprefix('.code-flow/specs/') for item in paths]
|
|
23
|
-
direct = {item.path for item in all_candidates if item.path in normalized}
|
|
24
|
-
code_paths = [path for path in normalized if path not in direct]
|
|
25
|
-
matched = {item.path for item in resolve_candidates(root, 'code', tuple(code_paths), matched_only=True)} if code_paths else set()
|
|
26
|
-
selected = [item for item in all_candidates if item.path in direct | matched]
|
|
27
|
-
if not selected:
|
|
28
|
-
raise ValueError('no matching specs for requested paths')
|
|
29
|
-
return selected
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _section_tokens(text: str, heading: str) -> int:
|
|
33
|
-
match = re.search(rf'(?im)^##\s+{re.escape(heading)}\s*$([\s\S]*?)(?=^##\s|\Z)', text)
|
|
34
|
-
return estimate_tokens(match.group(1)) if match else 0
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _spec_data(root: Path, candidate: object, config: dict) -> dict[str, object]:
|
|
38
|
-
relative = str(candidate.path)
|
|
39
|
-
base = (root / '.code-flow/specs').resolve()
|
|
40
|
-
path = (base / relative).resolve()
|
|
41
|
-
if base not in path.parents or not path.is_file():
|
|
42
|
-
raise ValueError(f'invalid spec path: {relative}')
|
|
43
|
-
raw = path.read_bytes()
|
|
44
|
-
text = raw.decode('utf-8')
|
|
45
|
-
domain = relative.split('/', 1)[0]
|
|
46
|
-
mapping = config.get('path_mapping') or {}
|
|
47
|
-
entry = mapping.get(domain, {}) if isinstance(mapping, dict) else {}
|
|
48
|
-
patterns = entry.get('patterns', []) if isinstance(entry, dict) else []
|
|
49
|
-
rules = [rule for rule in candidate.metadata.rules if rule.ref.startswith('RULE-')]
|
|
50
|
-
tokens = estimate_tokens(text)
|
|
51
|
-
rule_tokens = _section_tokens(text, 'Rules')
|
|
52
|
-
convention_tokens = _section_tokens(text, 'Conventions')
|
|
53
|
-
return {'id': candidate.spec_id, 'path': relative, 'sha256': hashlib.sha256(raw).hexdigest(),
|
|
54
|
-
'tokens': tokens, 'rule_tokens': rule_tokens, 'convention_tokens': convention_tokens,
|
|
55
|
-
'rule_percent': round(100 * rule_tokens / tokens, 2) if tokens else 0,
|
|
56
|
-
'convention_percent': round(100 * convention_tokens / tokens, 2) if tokens else 0,
|
|
57
|
-
'rule_ids': sorted(rule.ref for rule in rules), 'patterns': patterns,
|
|
58
|
-
'verifiers': [{'rule': item.rule, 'type': item.type, 'stage': item.stage, 'config': item.config}
|
|
59
|
-
for item in candidate.metadata.verifiers]}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _suggestions(specs: list[dict[str, object]], limit: int) -> list[dict[str, object]]:
|
|
63
|
-
return [{'path': item['path'], 'kind': 'review_conventions_split',
|
|
64
|
-
'reason': '规范超过单次注入上限;需人工选择目标,规则保持原位'}
|
|
65
|
-
for item in specs if int(item['tokens']) > limit and int(item['convention_tokens']) > 0]
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _overlaps(specs: list[dict[str, object]]) -> list[dict[str, object]]:
|
|
69
|
-
groups: list[dict[str, object]] = []
|
|
70
|
-
for index, first in enumerate(specs):
|
|
71
|
-
for second in specs[index + 1:]:
|
|
72
|
-
shared = sorted(set(first['patterns']) & set(second['patterns']))
|
|
73
|
-
if shared:
|
|
74
|
-
groups.append({'paths': [first['path'], second['path']], 'patterns': shared})
|
|
75
|
-
return groups
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def _target_path(root: Path, target: str) -> Path:
|
|
79
|
-
relative = Path(target)
|
|
80
|
-
if relative.is_absolute() or '..' in relative.parts or len(relative.parts) < 2 or relative.suffix != '.md':
|
|
81
|
-
raise ValueError('target must be a relative spec path inside an existing domain')
|
|
82
|
-
if relative.parts[0] in ('shared', '_session') or relative.parts[0].startswith('.'):
|
|
83
|
-
raise ValueError('target domain must be an injectable spec domain')
|
|
84
|
-
base = (root / '.code-flow/specs').resolve()
|
|
85
|
-
path = base / relative
|
|
86
|
-
if any((base.joinpath(*relative.parts[:i])).is_symlink() for i in range(1, len(relative.parts) + 1)):
|
|
87
|
-
raise ValueError('target path must not contain symlinks')
|
|
88
|
-
if base not in path.resolve().parents or not path.parent.is_dir():
|
|
89
|
-
raise ValueError('target domain directory must exist within specs')
|
|
90
|
-
if path.exists() or path.is_symlink():
|
|
91
|
-
raise ValueError('target already exists')
|
|
92
|
-
return path
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _movement(root: Path, specs: list[dict[str, object]], target: str, config: dict) -> tuple[list[dict], list[dict]]:
|
|
96
|
-
if len(specs) != 1:
|
|
97
|
-
raise ValueError('target requires exactly one source spec')
|
|
98
|
-
destination = _target_path(root, target)
|
|
99
|
-
source = specs[0]
|
|
100
|
-
base = root / '.code-flow/specs'
|
|
101
|
-
relative_source = Path(str(source['path']))
|
|
102
|
-
if any(base.joinpath(*relative_source.parts[:i]).is_symlink() for i in range(1, len(relative_source.parts) + 1)):
|
|
103
|
-
raise ValueError('source path must not contain symlinks')
|
|
104
|
-
text = (base / relative_source).read_text(encoding='utf-8')
|
|
105
|
-
match = re.search(r'(?im)^##\s+Conventions\s*$([\s\S]*?)(?=^##\s|\Z)', text)
|
|
106
|
-
if not match or 'RULE-' in match.group(0):
|
|
107
|
-
raise ValueError('source conventions missing or contain rule references')
|
|
108
|
-
new_id = str(source['id']) + '-conventions'
|
|
109
|
-
for stage in ('prd', 'design', 'plan', 'code', 'review'):
|
|
110
|
-
if any(item.spec_id == new_id for item in resolve_candidates(str(root), stage, ())):
|
|
111
|
-
raise ValueError('target spec id already exists')
|
|
112
|
-
relative = destination.relative_to(root / '.code-flow/specs').as_posix()
|
|
113
|
-
operation = {'kind': 'move_conventions', 'source': source['path'], 'target': relative, 'target_id': new_id}
|
|
114
|
-
source_after, target_after = render_movement(root, operation)
|
|
115
|
-
operation.update({'source_after_sha256': hashlib.sha256(source_after).hexdigest(),
|
|
116
|
-
'target_after_sha256': hashlib.sha256(target_after).hexdigest(),
|
|
117
|
-
'source_after_tokens': estimate_tokens(source_after.decode('utf-8')),
|
|
118
|
-
'target_after_tokens': estimate_tokens(target_after.decode('utf-8')),
|
|
119
|
-
'source_diff': ''.join(difflib.unified_diff(text.splitlines(keepends=True),
|
|
120
|
-
source_after.decode('utf-8').splitlines(keepends=True), fromfile=str(source['path']),
|
|
121
|
-
tofile=str(source['path']))),
|
|
122
|
-
'target_diff': ''.join(difflib.unified_diff([], target_after.decode('utf-8').splitlines(keepends=True),
|
|
123
|
-
fromfile='/dev/null', tofile=relative))})
|
|
124
|
-
domain = relative.split('/')[0]
|
|
125
|
-
mapping = config.get('path_mapping') or {}
|
|
126
|
-
entry = mapping.get(domain, {}) if isinstance(mapping, dict) else {}
|
|
127
|
-
patterns = entry.get('patterns', []) if isinstance(entry, dict) else []
|
|
128
|
-
return [operation], [{'kind': 'auto_discovery_add', 'domain': domain, 'path': relative,
|
|
129
|
-
'source_patterns': source['patterns'], 'target_patterns': patterns}]
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def render_movement(root: Path, operation: dict) -> tuple[bytes, bytes]:
|
|
133
|
-
source = root / '.code-flow/specs' / str(operation['source'])
|
|
134
|
-
text = source.read_text(encoding='utf-8')
|
|
135
|
-
match = re.search(r'(?im)^##\s+Conventions\s*$([\s\S]*?)(?=^##\s|\Z)', text)
|
|
136
|
-
if not match or 'RULE-' in match.group(0):
|
|
137
|
-
raise ValueError('rule guard: invalid conventions section')
|
|
138
|
-
updated = text[:match.start()] + text[match.end():]
|
|
139
|
-
before = parse_spec_metadata(text, path=str(source))
|
|
140
|
-
target = ('---\nid: ' + str(operation['target_id']) + '\ndescription: Migrated conventions\n'
|
|
141
|
-
'stages: ' + json.dumps(list(before.stages)) + '\nenforcement: advisory\n---\n\n'
|
|
142
|
-
'# Conventions\n\n' + match.group(0) + '\n')
|
|
143
|
-
after = parse_spec_metadata(updated, path=str(source))
|
|
144
|
-
target_meta = parse_spec_metadata(target, path=str(root / '.code-flow/specs' / str(operation['target'])))
|
|
145
|
-
signature = lambda meta: [(rule.ref, rule.text_sha256, rule.section, rule.enforcement) for rule in meta.rules]
|
|
146
|
-
if signature(before) != signature(after) or before.verifiers != after.verifiers or target_meta.verifiers:
|
|
147
|
-
raise ValueError('rule guard: rule or verifier changed')
|
|
148
|
-
if any(item.ref.startswith('RULE-') for item in target_meta.rules):
|
|
149
|
-
raise ValueError('rule guard: target contains rules')
|
|
150
|
-
return updated.encode('utf-8'), target.encode('utf-8')
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def build_proposal(root: str, paths: Sequence[str], target: str = '') -> dict[str, object]:
|
|
154
|
-
project = Path(root).resolve()
|
|
155
|
-
config = load_config(root)
|
|
156
|
-
candidates = _selected(root, paths)
|
|
157
|
-
specs = [_spec_data(project, candidate, config) for candidate in candidates]
|
|
158
|
-
operations, mapping_diff = _movement(project, specs, target, config) if target else ([], [])
|
|
159
|
-
identities = {str(item['id']): str(item['path']) for item in specs}
|
|
160
|
-
for operation in operations:
|
|
161
|
-
identities[str(operation['target_id'])] = str(operation['target'])
|
|
162
|
-
impact = scan_impacts(root, identities)
|
|
163
|
-
if impact['truncated'] or impact['errors']:
|
|
164
|
-
raise ValueError('impact scan incomplete; proposal cannot assert complete scope')
|
|
165
|
-
budget = config.get('budget') or {}
|
|
166
|
-
if not isinstance(budget, dict):
|
|
167
|
-
raise ValueError('budget must be a mapping')
|
|
168
|
-
limit = min(int(budget.get('total', 2500)), int(budget.get('l1_max', 1700)))
|
|
169
|
-
if limit <= 0:
|
|
170
|
-
raise ValueError('budget limit must be positive')
|
|
171
|
-
config_hash = hashlib.sha256((project / '.code-flow/config.yml').read_bytes()).hexdigest()
|
|
172
|
-
code_paths = [path for path in paths if not any(path in (item['path'], '.code-flow/specs/' + str(item['path'])) for item in specs)]
|
|
173
|
-
measured = measure_path_budget(root, code_paths) if code_paths else None
|
|
174
|
-
raw_before = sum(int(item['tokens']) for item in specs)
|
|
175
|
-
raw_after = (raw_before - int(specs[0]['tokens']) + int(operations[0]['source_after_tokens'])
|
|
176
|
-
+ int(operations[0]['target_after_tokens'])) if operations else raw_before
|
|
177
|
-
affected_files = {str(row.get('context_path') or row.get('task_path')) for row in impact['references']}
|
|
178
|
-
affected_hashes = {path: hashlib.sha256((project / path).read_bytes()).hexdigest() for path in sorted(affected_files)}
|
|
179
|
-
overlap = _overlaps(specs)
|
|
180
|
-
suggestions = _suggestions(specs, limit)
|
|
181
|
-
suggestions.extend({'paths': group['paths'], 'kind': 'review_overlap',
|
|
182
|
-
'reason': '相同路径模式命中多份规范;人工评估是否合并 Conventions'} for group in overlap)
|
|
183
|
-
proposal = {'schema': 1, 'config_sha256': config_hash, 'specs': specs,
|
|
184
|
-
'budget': {'before': {'raw_spec_tokens': raw_before, 'path_measurement': measured},
|
|
185
|
-
'after': {'raw_spec_tokens': raw_after, 'path_measurement': None if operations else measured},
|
|
186
|
-
'limit': limit, 'note': '变更后单次注入需在应用后重新测量'},
|
|
187
|
-
'mapping_diff': mapping_diff, 'affected_contexts': impact['references'],
|
|
188
|
-
'affected_file_hashes': affected_hashes,
|
|
189
|
-
'scanned_files': impact['scanned_files'], 'path_overlaps': overlap, 'suggestions': suggestions,
|
|
190
|
-
'operations': operations, 'note': ('请审核提案并交互确认应用。' if target
|
|
191
|
-
else '未指定迁移目标,提案没有可应用的文件变更。')}
|
|
192
|
-
proposal['proposal_id'] = hashlib.sha256(json.dumps(proposal, ensure_ascii=False, sort_keys=True).encode()).hexdigest()
|
|
193
|
-
return proposal
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
"""Guarded, recoverable application of a reviewed conventions proposal."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import hashlib
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
import shutil
|
|
9
|
-
from typing import Mapping
|
|
10
|
-
|
|
11
|
-
from codeflow.spec.cf_spec_optimize import build_proposal, render_movement
|
|
12
|
-
from codeflow.task.cf_task_doc import TaskDocumentError, parse_task_document
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _digest(data: bytes) -> str:
|
|
16
|
-
return hashlib.sha256(data).hexdigest()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _proposal_hash(proposal: Mapping[str, object]) -> str:
|
|
20
|
-
unsigned = {key: value for key, value in proposal.items() if key != 'proposal_id'}
|
|
21
|
-
return _digest(json.dumps(unsigned, ensure_ascii=False, sort_keys=True).encode())
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _check_context_tasks(root: Path, directory: str) -> None:
|
|
25
|
-
task_directory = root / directory
|
|
26
|
-
statuses: list[str] = []
|
|
27
|
-
for path in sorted(task_directory.glob('*.md')):
|
|
28
|
-
if path.name.endswith(('.prd.md', '.design.md', '.scenarios.md')):
|
|
29
|
-
continue
|
|
30
|
-
try:
|
|
31
|
-
document = parse_task_document(path.read_text(encoding='utf-8'), source=str(path))
|
|
32
|
-
except TaskDocumentError as exc:
|
|
33
|
-
if exc.code == 'task_document_empty':
|
|
34
|
-
continue
|
|
35
|
-
raise
|
|
36
|
-
statuses.extend(task.status for task in document.tasks)
|
|
37
|
-
if not statuses or any(status not in ('done', 'verified') for status in statuses):
|
|
38
|
-
raise ValueError('inflight_context: affected Context has incomplete or unproven tasks')
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _check_inflight(root: str, proposal: Mapping[str, object]) -> None:
|
|
42
|
-
refs = proposal.get('affected_contexts')
|
|
43
|
-
if not isinstance(refs, list):
|
|
44
|
-
raise ValueError('proposal impact evidence missing')
|
|
45
|
-
tasks = [row for row in refs if isinstance(row, dict) and row.get('source') == 'task' and not row.get('archived')]
|
|
46
|
-
contexts = [row for row in refs if isinstance(row, dict) and row.get('source') == 'context' and not row.get('archived')]
|
|
47
|
-
if any(row.get('status') not in ('done', 'verified') for row in tasks):
|
|
48
|
-
raise ValueError('inflight_context: affected task is not complete')
|
|
49
|
-
directories = {str(Path(str(row.get('context_path', ''))).parent) for row in contexts}
|
|
50
|
-
for directory in sorted(directories):
|
|
51
|
-
_check_context_tasks(Path(root), directory)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _validate(root: str, proposal: Mapping[str, object]) -> dict[str, object]:
|
|
55
|
-
if proposal.get('schema') != 1 or proposal.get('proposal_id') != _proposal_hash(proposal):
|
|
56
|
-
raise ValueError('proposal guard: invalid proposal identity')
|
|
57
|
-
operations = proposal.get('operations')
|
|
58
|
-
if not isinstance(operations, list) or len(operations) != 1 or not isinstance(operations[0], dict):
|
|
59
|
-
raise ValueError('proposal guard: exactly one explicit operation is required')
|
|
60
|
-
operation = operations[0]
|
|
61
|
-
if operation.get('kind') != 'move_conventions' or not isinstance(operation.get('source'), str) or not isinstance(operation.get('target'), str):
|
|
62
|
-
raise ValueError('proposal guard: invalid operation')
|
|
63
|
-
try:
|
|
64
|
-
current = build_proposal(root, (operation['source'],), str(operation['target']))
|
|
65
|
-
except (OSError, ValueError) as exc:
|
|
66
|
-
raise ValueError(f'proposal_stale: {exc}') from exc
|
|
67
|
-
if current != proposal:
|
|
68
|
-
raise ValueError('proposal_stale: input, impact, or operation changed')
|
|
69
|
-
_check_inflight(root, proposal)
|
|
70
|
-
return operation
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _write(path: Path, data: bytes, mode: int = 0o600) -> None:
|
|
74
|
-
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, mode)
|
|
75
|
-
with os.fdopen(descriptor, 'wb') as handle:
|
|
76
|
-
os.fchmod(handle.fileno(), mode)
|
|
77
|
-
handle.write(data)
|
|
78
|
-
handle.flush()
|
|
79
|
-
os.fsync(handle.fileno())
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _journal(directory: Path, data: dict[str, object]) -> None:
|
|
83
|
-
temporary = directory / 'journal.tmp'
|
|
84
|
-
if temporary.exists():
|
|
85
|
-
raise ValueError('transaction journal temporary file already exists')
|
|
86
|
-
_write(temporary, (json.dumps(data, ensure_ascii=False, sort_keys=True) + '\n').encode())
|
|
87
|
-
os.replace(temporary, directory / 'journal.json')
|
|
88
|
-
descriptor = os.open(directory, os.O_RDONLY)
|
|
89
|
-
try:
|
|
90
|
-
os.fsync(descriptor)
|
|
91
|
-
finally:
|
|
92
|
-
os.close(descriptor)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def commit_replace(staged: Path, destination: Path) -> None:
|
|
96
|
-
"""Replace the source; create a new target exclusively without overwriting."""
|
|
97
|
-
if staged.name == 'target.staged':
|
|
98
|
-
os.link(staged, destination)
|
|
99
|
-
else:
|
|
100
|
-
os.replace(staged, destination)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _rollback(source: Path, target: Path, backup: Path, original_hash: str,
|
|
104
|
-
source_after_hash: str, target_after_hash: str, target_created: bool) -> None:
|
|
105
|
-
if target_created:
|
|
106
|
-
if not target.is_file() or _digest(target.read_bytes()) != target_after_hash:
|
|
107
|
-
raise ValueError('recovery_required: target changed during rollback')
|
|
108
|
-
target.unlink()
|
|
109
|
-
current = _digest(source.read_bytes()) if source.is_file() else ''
|
|
110
|
-
if current == original_hash:
|
|
111
|
-
return
|
|
112
|
-
if current != source_after_hash or _digest(backup.read_bytes()) != original_hash:
|
|
113
|
-
raise ValueError('recovery_required: source changed during rollback')
|
|
114
|
-
os.replace(backup, source)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def _remove_transaction(directory: Path, parent_existed: bool) -> None:
|
|
118
|
-
parent = directory.parent
|
|
119
|
-
shutil.rmtree(directory)
|
|
120
|
-
if not parent_existed:
|
|
121
|
-
parent.rmdir()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def _transaction(root: Path, proposal: Mapping[str, object], operation: Mapping[str, object]) -> dict[str, object]:
|
|
125
|
-
base = root / '.code-flow/specs'
|
|
126
|
-
source = base / str(operation['source'])
|
|
127
|
-
target = base / str(operation['target'])
|
|
128
|
-
original = source.read_bytes()
|
|
129
|
-
after_source, after_target = render_movement(root, dict(operation))
|
|
130
|
-
specs = proposal.get('specs')
|
|
131
|
-
if (not isinstance(specs, list) or len(specs) != 1 or not isinstance(specs[0], dict)
|
|
132
|
-
or _digest(original) != specs[0].get('sha256')
|
|
133
|
-
or _digest(after_source) != operation.get('source_after_sha256')
|
|
134
|
-
or _digest(after_target) != operation.get('target_after_sha256')):
|
|
135
|
-
raise ValueError('proposal_stale: source or proposed result changed')
|
|
136
|
-
parent = base / '.optimize-transactions'
|
|
137
|
-
if parent.is_symlink() or (parent.exists() and (not parent.is_dir() or any(parent.iterdir()))):
|
|
138
|
-
raise ValueError('recovery_required: unfinished optimization transaction')
|
|
139
|
-
parent_existed = parent.exists()
|
|
140
|
-
parent.mkdir(exist_ok=True)
|
|
141
|
-
directory = parent / str(proposal['proposal_id'])
|
|
142
|
-
directory.mkdir()
|
|
143
|
-
backup, source_stage, target_stage = directory / 'source.backup', directory / 'source.staged', directory / 'target.staged'
|
|
144
|
-
target_created = False
|
|
145
|
-
state = {'schema': 1, 'proposal_id': proposal['proposal_id'], 'source': operation['source'],
|
|
146
|
-
'target': operation['target'], 'original_sha256': _digest(original),
|
|
147
|
-
'source_after_sha256': _digest(after_source), 'target_after_sha256': _digest(after_target), 'state': 'prepared'}
|
|
148
|
-
try:
|
|
149
|
-
_write(backup, original, source.stat().st_mode & 0o777)
|
|
150
|
-
_write(source_stage, after_source, source.stat().st_mode & 0o777)
|
|
151
|
-
_write(target_stage, after_target, 0o644)
|
|
152
|
-
_journal(directory, state)
|
|
153
|
-
state['state'] = 'committing'
|
|
154
|
-
_journal(directory, state)
|
|
155
|
-
if _digest(source.read_bytes()) != state['original_sha256']:
|
|
156
|
-
raise ValueError('proposal_stale: source changed before commit')
|
|
157
|
-
commit_replace(source_stage, source)
|
|
158
|
-
commit_replace(target_stage, target)
|
|
159
|
-
target_created = True
|
|
160
|
-
if _digest(source.read_bytes()) != state['source_after_sha256'] or _digest(target.read_bytes()) != state['target_after_sha256']:
|
|
161
|
-
raise ValueError('post-write guard: content differs from approved proposal')
|
|
162
|
-
state['state'] = 'committed'
|
|
163
|
-
_journal(directory, state)
|
|
164
|
-
except (OSError, ValueError):
|
|
165
|
-
_rollback(source, target, backup, _digest(original), _digest(after_source), _digest(after_target), target_created)
|
|
166
|
-
_remove_transaction(directory, parent_existed)
|
|
167
|
-
raise
|
|
168
|
-
_remove_transaction(directory, parent_existed)
|
|
169
|
-
return {'decision': 'pass', 'proposal_id': proposal['proposal_id'],
|
|
170
|
-
'changed': [str(operation['source']), str(operation['target'])]}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def apply_proposal(root: str, proposal: Mapping[str, object], *, confirmed: bool) -> dict[str, object]:
|
|
174
|
-
if not confirmed:
|
|
175
|
-
raise ValueError('explicit confirmation required')
|
|
176
|
-
project = Path(root).resolve()
|
|
177
|
-
lock = project / '.code-flow/specs/.optimize.lock'
|
|
178
|
-
try:
|
|
179
|
-
descriptor = os.open(lock, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
180
|
-
except FileExistsError as exc:
|
|
181
|
-
raise ValueError('recovery_required: optimization lock exists') from exc
|
|
182
|
-
os.close(descriptor)
|
|
183
|
-
try:
|
|
184
|
-
operation = _validate(root, proposal)
|
|
185
|
-
return _transaction(project, proposal, operation)
|
|
186
|
-
finally:
|
|
187
|
-
lock.unlink()
|