@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,477 +0,0 @@
|
|
|
1
|
-
"""Pure Spec Context model: dataclasses, serialization and identity hashing.
|
|
2
|
-
|
|
3
|
-
This module owns the persisted schema and its stable error contract. CLI
|
|
4
|
-
commands, doctor and decision orchestration stay in `cf_spec_context`; lower
|
|
5
|
-
layers (cache, evidence, plan quality) depend on this model instead of the CLI
|
|
6
|
-
entry.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
from dataclasses import dataclass
|
|
12
|
-
from datetime import datetime, timezone
|
|
13
|
-
import hashlib
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import TYPE_CHECKING, Mapping, Optional, Sequence
|
|
16
|
-
|
|
17
|
-
from codeflow.core import cf_fs
|
|
18
|
-
from codeflow.spec.cf_spec_metadata import SpecHashes
|
|
19
|
-
|
|
20
|
-
if TYPE_CHECKING:
|
|
21
|
-
from codeflow.spec.cf_spec_resolver import SpecCandidate
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
CONTEXT_VERSION = 1
|
|
25
|
-
STAGE_STATUSES = frozenset(
|
|
26
|
-
("pending", "applied", "not_applicable", "waived", "verified", "stale", "conflict", "unverified")
|
|
27
|
-
)
|
|
28
|
-
DECISION_KINDS = frozenset(("not_applicable", "waived", "manual_verification"))
|
|
29
|
-
_AGENT_IDENTITIES = frozenset(("agent", "assistant", "codex", "claude", "opencode", "costrict"))
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class ContextError(ValueError):
|
|
33
|
-
"""A stable context error suitable for JSON CLI output."""
|
|
34
|
-
|
|
35
|
-
def __init__(self, code: str, field: str, message: str, path: str = "") -> None:
|
|
36
|
-
self.code = code
|
|
37
|
-
self.field = field
|
|
38
|
-
self.message = message
|
|
39
|
-
self.path = path
|
|
40
|
-
location = f"{path}: " if path else ""
|
|
41
|
-
super().__init__(f"{location}{code} {field}: {message}")
|
|
42
|
-
|
|
43
|
-
def to_dict(self) -> dict[str, str]:
|
|
44
|
-
return {"code": self.code, "field": self.field, "message": self.message, "path": self.path}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def error_payload(exc: Exception) -> Optional[dict]:
|
|
48
|
-
"""Duck-typed ContextError recognition across duplicate module instances.
|
|
49
|
-
|
|
50
|
-
Direct script execution (`python3 cf_spec_context.py`) can make lazy
|
|
51
|
-
`import cf_spec_context` calls inside sibling modules load a second copy,
|
|
52
|
-
so `isinstance(exc, ContextError)` fails even though the error contract is
|
|
53
|
-
identical. Recognize it structurally instead of masking it as
|
|
54
|
-
`internal_error`.
|
|
55
|
-
"""
|
|
56
|
-
to_dict = getattr(exc, "to_dict", None)
|
|
57
|
-
if not callable(to_dict) or not isinstance(getattr(exc, "code", None), str):
|
|
58
|
-
return None
|
|
59
|
-
payload = to_dict()
|
|
60
|
-
return payload if isinstance(payload, dict) else None
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@dataclass(frozen=True)
|
|
64
|
-
class ContextSource:
|
|
65
|
-
type: str
|
|
66
|
-
ref: str
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
@dataclass(frozen=True)
|
|
70
|
-
class Decision:
|
|
71
|
-
kind: str
|
|
72
|
-
reason: str
|
|
73
|
-
confirmed_by: str
|
|
74
|
-
confirmed_at: str
|
|
75
|
-
source: str
|
|
76
|
-
expires_at: Optional[str]
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@dataclass(frozen=True)
|
|
80
|
-
class ArtifactRef:
|
|
81
|
-
artifact: str
|
|
82
|
-
section_id: str
|
|
83
|
-
item_id: str
|
|
84
|
-
artifact_sha256: str
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
@dataclass(frozen=True)
|
|
88
|
-
class RuleStageStatus:
|
|
89
|
-
status: str
|
|
90
|
-
refs: tuple[ArtifactRef, ...]
|
|
91
|
-
decision: Optional[Decision]
|
|
92
|
-
evidence: tuple[Mapping[str, object], ...]
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@dataclass(frozen=True)
|
|
96
|
-
class RuleBinding:
|
|
97
|
-
ref: str
|
|
98
|
-
summary: str
|
|
99
|
-
text_sha256: str
|
|
100
|
-
enforcement: str
|
|
101
|
-
verifier_ref: str
|
|
102
|
-
stage_status: Mapping[str, RuleStageStatus]
|
|
103
|
-
verifier_stage: str = "code"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
@dataclass(frozen=True)
|
|
107
|
-
class SpecBinding:
|
|
108
|
-
spec_id: str
|
|
109
|
-
path: str
|
|
110
|
-
hashes: SpecHashes
|
|
111
|
-
selected_by: str
|
|
112
|
-
reason: str
|
|
113
|
-
enforcement: str
|
|
114
|
-
stages: tuple[str, ...]
|
|
115
|
-
rules: tuple[RuleBinding, ...]
|
|
116
|
-
status: str = "active"
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
@dataclass(frozen=True)
|
|
120
|
-
class SpecContext:
|
|
121
|
-
version: int
|
|
122
|
-
task: str
|
|
123
|
-
enforcement: str
|
|
124
|
-
updated_at: str
|
|
125
|
-
sources: tuple[ContextSource, ...]
|
|
126
|
-
bindings: tuple[SpecBinding, ...]
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
@dataclass(frozen=True)
|
|
130
|
-
class BindingInput:
|
|
131
|
-
candidate: SpecCandidate
|
|
132
|
-
selected_by: str
|
|
133
|
-
reason: str
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
@dataclass(frozen=True)
|
|
137
|
-
class DriftChange:
|
|
138
|
-
kind: str
|
|
139
|
-
spec_id: str
|
|
140
|
-
rule_ref: str
|
|
141
|
-
old_hash: str
|
|
142
|
-
new_hash: str
|
|
143
|
-
stages: tuple[str, ...]
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
@dataclass(frozen=True)
|
|
147
|
-
class DriftResult:
|
|
148
|
-
context: SpecContext
|
|
149
|
-
changes: tuple[DriftChange, ...]
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def now_iso() -> str:
|
|
153
|
-
return datetime.now(timezone.utc).isoformat()
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def atomic_text(target: Path, text: str) -> None:
|
|
157
|
-
"""Atomic write with the Context's stable error code on failure."""
|
|
158
|
-
try:
|
|
159
|
-
cf_fs.atomic_write_text(target, text)
|
|
160
|
-
except OSError as exc:
|
|
161
|
-
raise ContextError("atomic_write_error", "file", str(exc), str(target)) from exc
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def mapping_field(value: object, field: str, path: str) -> Mapping[str, object]:
|
|
165
|
-
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
|
|
166
|
-
raise ContextError("invalid_context", field, "必须是字符串 key mapping", path)
|
|
167
|
-
return value
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def sequence_field(value: object, field: str, path: str) -> Sequence[object]:
|
|
171
|
-
if not isinstance(value, list):
|
|
172
|
-
raise ContextError("invalid_context", field, "必须是列表", path)
|
|
173
|
-
return value
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def string_field(value: object, field: str, path: str) -> str:
|
|
177
|
-
if not isinstance(value, str) or not value.strip():
|
|
178
|
-
raise ContextError("invalid_context", field, "必须是非空字符串", path)
|
|
179
|
-
return value.strip()
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def _decision_string(value: object, field: str, path: str) -> str:
|
|
183
|
-
if not isinstance(value, str) or not value.strip():
|
|
184
|
-
raise ContextError("invalid_decision", field, "必须是非空字符串", path)
|
|
185
|
-
return value.strip()
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def _timestamp(value: str, field: str) -> None:
|
|
189
|
-
try:
|
|
190
|
-
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
191
|
-
except ValueError as exc:
|
|
192
|
-
raise ContextError("invalid_decision", field, "必须是 RFC3339 时间") from exc
|
|
193
|
-
if parsed.tzinfo is None:
|
|
194
|
-
raise ContextError("invalid_decision", field, "必须包含时区")
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def validate_decision(decision: Decision) -> None:
|
|
198
|
-
if decision.kind not in DECISION_KINDS:
|
|
199
|
-
raise ContextError("invalid_decision", "decision.kind", f"不支持 {decision.kind!r}")
|
|
200
|
-
for field in ("reason", "confirmed_by", "confirmed_at", "source"):
|
|
201
|
-
if not getattr(decision, field).strip():
|
|
202
|
-
raise ContextError("invalid_decision", f"decision.{field}", "必须是非空字符串")
|
|
203
|
-
identity = decision.confirmed_by.lower().split(":", 1)[0]
|
|
204
|
-
if identity in _AGENT_IDENTITIES:
|
|
205
|
-
raise ContextError("agent_confirmation_forbidden", "decision.confirmed_by", "Agent 不能自行确认")
|
|
206
|
-
_timestamp(decision.confirmed_at, "decision.confirmed_at")
|
|
207
|
-
if decision.kind == "waived" and not decision.expires_at:
|
|
208
|
-
raise ContextError("invalid_decision", "decision.expires_at", "waived 必须有失效时间")
|
|
209
|
-
if decision.expires_at:
|
|
210
|
-
_timestamp(decision.expires_at, "decision.expires_at")
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def decision_from_data(value: object, path: str) -> Optional[Decision]:
|
|
214
|
-
if value is None:
|
|
215
|
-
return None
|
|
216
|
-
data = mapping_field(value, "decision", path)
|
|
217
|
-
expires = data.get("expires_at")
|
|
218
|
-
if expires is not None and not isinstance(expires, str):
|
|
219
|
-
raise ContextError("invalid_decision", "decision.expires_at", "必须是字符串或 null", path)
|
|
220
|
-
decision = Decision(
|
|
221
|
-
_decision_string(data.get("kind"), "decision.kind", path),
|
|
222
|
-
_decision_string(data.get("reason"), "decision.reason", path),
|
|
223
|
-
_decision_string(data.get("confirmed_by"), "decision.confirmed_by", path),
|
|
224
|
-
_decision_string(data.get("confirmed_at"), "decision.confirmed_at", path),
|
|
225
|
-
_decision_string(data.get("source"), "decision.source", path),
|
|
226
|
-
expires,
|
|
227
|
-
)
|
|
228
|
-
validate_decision(decision)
|
|
229
|
-
return decision
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def _decision_data(decision: Optional[Decision]) -> Optional[dict[str, object]]:
|
|
233
|
-
if decision is None:
|
|
234
|
-
return None
|
|
235
|
-
return {
|
|
236
|
-
"kind": decision.kind,
|
|
237
|
-
"reason": decision.reason,
|
|
238
|
-
"confirmed_by": decision.confirmed_by,
|
|
239
|
-
"confirmed_at": decision.confirmed_at,
|
|
240
|
-
"source": decision.source,
|
|
241
|
-
"expires_at": decision.expires_at,
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def _artifact_from_data(value: object, path: str) -> ArtifactRef:
|
|
246
|
-
data = mapping_field(value, "refs[]", path)
|
|
247
|
-
return ArtifactRef(
|
|
248
|
-
string_field(data.get("artifact"), "refs[].artifact", path),
|
|
249
|
-
string_field(data.get("section_id"), "refs[].section_id", path),
|
|
250
|
-
string_field(data.get("item_id"), "refs[].item_id", path),
|
|
251
|
-
string_field(data.get("artifact_sha256"), "refs[].artifact_sha256", path),
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def _artifact_data(reference: ArtifactRef) -> dict[str, str]:
|
|
256
|
-
return {
|
|
257
|
-
"artifact": reference.artifact,
|
|
258
|
-
"section_id": reference.section_id,
|
|
259
|
-
"item_id": reference.item_id,
|
|
260
|
-
"artifact_sha256": reference.artifact_sha256,
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def _stage_from_data(value: object, field: str, path: str) -> RuleStageStatus:
|
|
265
|
-
data = mapping_field(value, field, path)
|
|
266
|
-
status = string_field(data.get("status"), f"{field}.status", path)
|
|
267
|
-
if status not in STAGE_STATUSES:
|
|
268
|
-
raise ContextError("invalid_context", f"{field}.status", f"不支持 {status!r}", path)
|
|
269
|
-
refs = tuple(_artifact_from_data(item, path) for item in sequence_field(data.get("refs", []), f"{field}.refs", path))
|
|
270
|
-
evidence = tuple(mapping_field(item, f"{field}.evidence[]", path) for item in sequence_field(data.get("evidence", []), f"{field}.evidence", path))
|
|
271
|
-
decision = decision_from_data(data.get("decision"), path)
|
|
272
|
-
if status == "applied" and not refs:
|
|
273
|
-
raise ContextError("invalid_context", f"{field}.refs", "applied 必须有 artifact ref", path)
|
|
274
|
-
if status in ("not_applicable", "waived") and decision is None:
|
|
275
|
-
raise ContextError("invalid_context", f"{field}.decision", f"{status} 必须有确认记录", path)
|
|
276
|
-
return RuleStageStatus(status, refs, decision, evidence)
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
def _stage_data(status: RuleStageStatus) -> dict[str, object]:
|
|
280
|
-
return {
|
|
281
|
-
"status": status.status,
|
|
282
|
-
"refs": [_artifact_data(item) for item in status.refs],
|
|
283
|
-
"decision": _decision_data(status.decision),
|
|
284
|
-
"evidence": [dict(item) for item in status.evidence],
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def _rule_from_data(value: object, path: str) -> RuleBinding:
|
|
289
|
-
data = mapping_field(value, "rules[]", path)
|
|
290
|
-
stages = mapping_field(data.get("stage_status"), "rules[].stage_status", path)
|
|
291
|
-
parsed = {stage: _stage_from_data(item, f"stage_status.{stage}", path) for stage, item in stages.items()}
|
|
292
|
-
return RuleBinding(
|
|
293
|
-
string_field(data.get("ref"), "rules[].ref", path),
|
|
294
|
-
string_field(data.get("summary"), "rules[].summary", path),
|
|
295
|
-
string_field(data.get("text_sha256"), "rules[].text_sha256", path),
|
|
296
|
-
string_field(data.get("enforcement"), "rules[].enforcement", path),
|
|
297
|
-
string_field(data.get("verifier_ref"), "rules[].verifier_ref", path),
|
|
298
|
-
parsed,
|
|
299
|
-
string_field(data.get("verifier_stage", "code"), "rules[].verifier_stage", path),
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
def _rule_data(rule: RuleBinding) -> dict[str, object]:
|
|
304
|
-
return {
|
|
305
|
-
"ref": rule.ref,
|
|
306
|
-
"summary": rule.summary,
|
|
307
|
-
"text_sha256": rule.text_sha256,
|
|
308
|
-
"enforcement": rule.enforcement,
|
|
309
|
-
"verifier_ref": rule.verifier_ref,
|
|
310
|
-
"verifier_stage": rule.verifier_stage,
|
|
311
|
-
"stage_status": {stage: _stage_data(status) for stage, status in rule.stage_status.items()},
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
def _hashes_from_data(value: object, path: str) -> SpecHashes:
|
|
316
|
-
data = mapping_field(value, "hashes", path)
|
|
317
|
-
return SpecHashes(
|
|
318
|
-
string_field(data.get("file_sha256"), "hashes.file_sha256", path),
|
|
319
|
-
string_field(data.get("metadata_sha256"), "hashes.metadata_sha256", path),
|
|
320
|
-
string_field(data.get("rules_sha256"), "hashes.rules_sha256", path),
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
def _binding_from_data(value: object, path: str) -> SpecBinding:
|
|
325
|
-
data = mapping_field(value, "bindings[]", path)
|
|
326
|
-
stages = tuple(string_field(item, "bindings[].stages[]", path) for item in sequence_field(data.get("stages"), "bindings[].stages", path))
|
|
327
|
-
rules = tuple(_rule_from_data(item, path) for item in sequence_field(data.get("rules"), "bindings[].rules", path))
|
|
328
|
-
return SpecBinding(
|
|
329
|
-
string_field(data.get("spec_id"), "bindings[].spec_id", path),
|
|
330
|
-
string_field(data.get("path"), "bindings[].path", path),
|
|
331
|
-
_hashes_from_data(data.get("hashes"), path),
|
|
332
|
-
string_field(data.get("selected_by"), "bindings[].selected_by", path),
|
|
333
|
-
string_field(data.get("reason"), "bindings[].reason", path),
|
|
334
|
-
string_field(data.get("enforcement"), "bindings[].enforcement", path),
|
|
335
|
-
stages,
|
|
336
|
-
rules,
|
|
337
|
-
string_field(data.get("status", "active"), "bindings[].status", path),
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
def _binding_data(binding: SpecBinding) -> dict[str, object]:
|
|
342
|
-
return {
|
|
343
|
-
"spec_id": binding.spec_id,
|
|
344
|
-
"path": binding.path,
|
|
345
|
-
"status": binding.status,
|
|
346
|
-
"hashes": {
|
|
347
|
-
"file_sha256": binding.hashes.file_sha256,
|
|
348
|
-
"metadata_sha256": binding.hashes.metadata_sha256,
|
|
349
|
-
"rules_sha256": binding.hashes.rules_sha256,
|
|
350
|
-
},
|
|
351
|
-
"selected_by": binding.selected_by,
|
|
352
|
-
"reason": binding.reason,
|
|
353
|
-
"enforcement": binding.enforcement,
|
|
354
|
-
"stages": list(binding.stages),
|
|
355
|
-
"rules": [_rule_data(rule) for rule in binding.rules],
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
def context_to_data(context: SpecContext) -> dict[str, object]:
|
|
360
|
-
return {
|
|
361
|
-
"version": context.version,
|
|
362
|
-
"task": context.task,
|
|
363
|
-
"enforcement": context.enforcement,
|
|
364
|
-
"updated_at": context.updated_at,
|
|
365
|
-
"sources": [{"type": item.type, "ref": item.ref} for item in context.sources],
|
|
366
|
-
"bindings": [_binding_data(binding) for binding in context.bindings],
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
def context_to_identity(context: SpecContext) -> dict[str, object]:
|
|
371
|
-
"""Stable identity projection for marker drift detection.
|
|
372
|
-
|
|
373
|
-
Excludes volatile runtime state (`updated_at`, `stage_status` including
|
|
374
|
-
evidence timestamps) so the active marker only invalidates when the bound
|
|
375
|
-
rules themselves change (rule/spec text, bindings, sources).
|
|
376
|
-
"""
|
|
377
|
-
return {
|
|
378
|
-
"version": context.version,
|
|
379
|
-
"task": context.task,
|
|
380
|
-
"enforcement": context.enforcement,
|
|
381
|
-
"sources": [{"type": item.type, "ref": item.ref} for item in context.sources],
|
|
382
|
-
"bindings": [
|
|
383
|
-
{
|
|
384
|
-
"spec_id": binding.spec_id,
|
|
385
|
-
"path": binding.path,
|
|
386
|
-
"status": binding.status,
|
|
387
|
-
"hashes": {
|
|
388
|
-
"file_sha256": binding.hashes.file_sha256,
|
|
389
|
-
"metadata_sha256": binding.hashes.metadata_sha256,
|
|
390
|
-
"rules_sha256": binding.hashes.rules_sha256,
|
|
391
|
-
},
|
|
392
|
-
"selected_by": binding.selected_by,
|
|
393
|
-
"reason": binding.reason,
|
|
394
|
-
"enforcement": binding.enforcement,
|
|
395
|
-
"stages": list(binding.stages),
|
|
396
|
-
"rules": [
|
|
397
|
-
{
|
|
398
|
-
"ref": rule.ref,
|
|
399
|
-
"summary": rule.summary,
|
|
400
|
-
"text_sha256": rule.text_sha256,
|
|
401
|
-
"enforcement": rule.enforcement,
|
|
402
|
-
"verifier_ref": rule.verifier_ref,
|
|
403
|
-
}
|
|
404
|
-
for rule in binding.rules
|
|
405
|
-
],
|
|
406
|
-
}
|
|
407
|
-
for binding in context.bindings
|
|
408
|
-
],
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
def context_sha256(context: SpecContext) -> str:
|
|
413
|
-
"""Deterministic identity hash binding the active marker to the bound rules.
|
|
414
|
-
|
|
415
|
-
Computed from the stable identity projection (never volatile runtime state),
|
|
416
|
-
so evidence timestamps and stage statuses cannot drift the marker.
|
|
417
|
-
"""
|
|
418
|
-
import yaml
|
|
419
|
-
data = yaml.safe_dump(context_to_identity(context), sort_keys=True, allow_unicode=True).encode()
|
|
420
|
-
return hashlib.sha256(data).hexdigest()
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
def new_context(task: str, sources: Sequence[tuple[str, str]]) -> SpecContext:
|
|
424
|
-
if not task.strip():
|
|
425
|
-
raise ContextError("invalid_context", "task", "必须是非空字符串")
|
|
426
|
-
parsed = tuple(ContextSource(string_field(kind, "sources[].type", ""), string_field(ref, "sources[].ref", "")) for kind, ref in sources)
|
|
427
|
-
return SpecContext(CONTEXT_VERSION, task.strip(), "required", now_iso(), parsed, ())
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
def load_context(path: str, *, source: Optional[bytes] = None) -> SpecContext:
|
|
431
|
-
import yaml
|
|
432
|
-
try:
|
|
433
|
-
raw = Path(path).read_bytes() if source is None else source
|
|
434
|
-
loaded = yaml.safe_load(raw.decode("utf-8")) or {}
|
|
435
|
-
except (OSError, UnicodeError, yaml.YAMLError) as exc:
|
|
436
|
-
raise ContextError("context_read_error", "file", str(exc), path) from exc
|
|
437
|
-
return context_from_data(loaded, path)
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
def context_from_data(loaded: object, path: str) -> SpecContext:
|
|
441
|
-
"""Validate a parsed Context through the same schema for YAML and cache reads."""
|
|
442
|
-
data = mapping_field(loaded, "context", path)
|
|
443
|
-
version = data.get("version")
|
|
444
|
-
if version != CONTEXT_VERSION:
|
|
445
|
-
raise ContextError("unsupported_version", "version", f"仅支持 {CONTEXT_VERSION},收到 {version!r}", path)
|
|
446
|
-
source_values = sequence_field(data.get("sources", []), "sources", path)
|
|
447
|
-
sources = tuple(
|
|
448
|
-
ContextSource(
|
|
449
|
-
string_field(mapping_field(item, "sources[]", path).get("type"), "sources[].type", path),
|
|
450
|
-
string_field(mapping_field(item, "sources[]", path).get("ref"), "sources[].ref", path),
|
|
451
|
-
)
|
|
452
|
-
for item in source_values
|
|
453
|
-
)
|
|
454
|
-
bindings = tuple(_binding_from_data(item, path) for item in sequence_field(data.get("bindings", []), "bindings", path))
|
|
455
|
-
return SpecContext(
|
|
456
|
-
CONTEXT_VERSION,
|
|
457
|
-
string_field(data.get("task"), "task", path),
|
|
458
|
-
string_field(data.get("enforcement"), "enforcement", path),
|
|
459
|
-
string_field(data.get("updated_at"), "updated_at", path),
|
|
460
|
-
sources,
|
|
461
|
-
bindings,
|
|
462
|
-
)
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
def serialize_context(context: SpecContext) -> str:
|
|
466
|
-
import yaml
|
|
467
|
-
return yaml.safe_dump(context_to_data(context), sort_keys=False, allow_unicode=True)
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
def save_context(path: str, context: SpecContext) -> None:
|
|
471
|
-
target = Path(path)
|
|
472
|
-
target.parent.mkdir(parents=True, exist_ok=True)
|
|
473
|
-
text = serialize_context(context)
|
|
474
|
-
try:
|
|
475
|
-
atomic_text(target, text)
|
|
476
|
-
except ContextError as exc:
|
|
477
|
-
raise ContextError("context_write_error", "file", str(exc), path) from exc
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
"""Read-only, bounded impact analysis for Spec Contexts and task documents."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import os
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
import re
|
|
7
|
-
from typing import Callable, Iterator
|
|
8
|
-
|
|
9
|
-
from codeflow.spec.cf_spec_context_model import ContextError, load_context
|
|
10
|
-
from codeflow.spec.cf_spec_resolver import resolve_candidates
|
|
11
|
-
from codeflow.task.cf_task_doc import TaskDocumentError, parse_task_document
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _identity(root: str, value: str) -> tuple[str, str]:
|
|
15
|
-
query = value.replace('\\', '/').removeprefix('.code-flow/specs/')
|
|
16
|
-
found: dict[str, str] = {}
|
|
17
|
-
for stage in ('prd', 'design', 'plan', 'code', 'review'):
|
|
18
|
-
for item in resolve_candidates(root, stage, ()):
|
|
19
|
-
if query in (item.spec_id, item.path):
|
|
20
|
-
found[item.spec_id] = item.path
|
|
21
|
-
if len(found) != 1:
|
|
22
|
-
raise ValueError('spec_not_found' if not found else 'spec_identity_ambiguous')
|
|
23
|
-
return next(iter(found.items()))
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _files(root: Path, max_depth: int, onerror: Callable[[OSError], None]) -> Iterator[Path | None]:
|
|
27
|
-
if not root.exists():
|
|
28
|
-
return
|
|
29
|
-
for current, dirs, names in os.walk(root, followlinks=False, onerror=onerror):
|
|
30
|
-
directory = Path(current)
|
|
31
|
-
depth = len(directory.relative_to(root).parts)
|
|
32
|
-
blocked = [name for name in dirs if depth >= max_depth or (directory / name).is_symlink()]
|
|
33
|
-
dirs[:] = sorted(name for name in dirs if name not in blocked)
|
|
34
|
-
if blocked:
|
|
35
|
-
yield None
|
|
36
|
-
for name in sorted(names):
|
|
37
|
-
if name == 'spec-context.yml' or (name.endswith('.md') and not name.endswith(('.prd.md', '.design.md', '.scenarios.md'))):
|
|
38
|
-
yield directory / name
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _context_references(path: Path, root: Path, identities: dict[str, str]) -> list[dict[str, object]]:
|
|
42
|
-
context = load_context(str(path))
|
|
43
|
-
rows: list[dict[str, object]] = []
|
|
44
|
-
for binding in context.bindings:
|
|
45
|
-
if binding.spec_id not in identities:
|
|
46
|
-
continue
|
|
47
|
-
for rule in binding.rules:
|
|
48
|
-
if rule.enforcement != 'required':
|
|
49
|
-
continue
|
|
50
|
-
stage = rule.stage_status.get('code') or rule.stage_status.get('design')
|
|
51
|
-
rows.append({'context_path': path.relative_to(root).as_posix(), 'task_id': context.task,
|
|
52
|
-
'spec_id': binding.spec_id, 'rule_ref': f'{binding.spec_id}#{rule.ref}',
|
|
53
|
-
'rule_status': stage.status if stage else 'unknown',
|
|
54
|
-
'source': 'context', 'archived': 'archived' in path.parts})
|
|
55
|
-
return rows
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _task_references(path: Path, root: Path, identities: dict[str, str]) -> list[dict[str, object]]:
|
|
59
|
-
text = path.read_text(encoding='utf-8')
|
|
60
|
-
candidates = {name: spec_path for name, spec_path in identities.items() if name in text or spec_path in text}
|
|
61
|
-
if not candidates:
|
|
62
|
-
return []
|
|
63
|
-
try:
|
|
64
|
-
document = parse_task_document(text, source=str(path))
|
|
65
|
-
except TaskDocumentError as exc:
|
|
66
|
-
if exc.code == 'task_document_empty':
|
|
67
|
-
return []
|
|
68
|
-
raise
|
|
69
|
-
rows: list[dict[str, object]] = []
|
|
70
|
-
for task in document.tasks:
|
|
71
|
-
for spec_id, spec_path in candidates.items():
|
|
72
|
-
pattern = re.compile(rf'{re.escape(spec_id)}#(RULE-[\w-]+)')
|
|
73
|
-
refs = sorted(set(pattern.findall(task.body)))
|
|
74
|
-
if not refs and spec_path not in task.body:
|
|
75
|
-
continue
|
|
76
|
-
for ref in refs or ['']:
|
|
77
|
-
rows.append({'task_path': path.relative_to(root).as_posix(), 'task_id': task.id,
|
|
78
|
-
'spec_id': spec_id, 'status': task.status,
|
|
79
|
-
'rule_ref': f'{spec_id}#{ref}' if ref else '',
|
|
80
|
-
'source': 'task', 'archived': 'archived' in path.parts})
|
|
81
|
-
return rows
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def scan_impacts(root: str, identities: dict[str, str], max_files: int = 5000,
|
|
85
|
-
max_depth: int = 20) -> dict[str, object]:
|
|
86
|
-
if max_files <= 0 or max_depth <= 0:
|
|
87
|
-
raise ValueError('impact limits must be positive')
|
|
88
|
-
project = Path(root).resolve()
|
|
89
|
-
tasks = project / '.code-flow/tasks'
|
|
90
|
-
rows: list[dict[str, object]] = []
|
|
91
|
-
errors: list[dict[str, str]] = []
|
|
92
|
-
def walk_error(exc: OSError) -> None:
|
|
93
|
-
errors.append({'path': str(exc.filename or tasks), 'message': str(exc)})
|
|
94
|
-
scanned = 0
|
|
95
|
-
truncated = False
|
|
96
|
-
for path in _files(tasks, max_depth, walk_error):
|
|
97
|
-
if path is None:
|
|
98
|
-
truncated = True
|
|
99
|
-
continue
|
|
100
|
-
if scanned >= max_files:
|
|
101
|
-
truncated = True
|
|
102
|
-
break
|
|
103
|
-
scanned += 1
|
|
104
|
-
try:
|
|
105
|
-
if path.is_symlink():
|
|
106
|
-
raise ValueError('symlink task file is not scanned')
|
|
107
|
-
references = (_context_references(path, project, identities) if path.name == 'spec-context.yml'
|
|
108
|
-
else _task_references(path, project, identities))
|
|
109
|
-
rows.extend(references)
|
|
110
|
-
except (OSError, UnicodeError, ValueError, ContextError) as exc:
|
|
111
|
-
errors.append({'path': path.relative_to(project).as_posix(), 'message': str(exc)})
|
|
112
|
-
return {'references': rows, 'scanned_files': scanned, 'truncated': truncated, 'errors': errors}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def inspect_impact(root: str, query: str, max_files: int = 5000, max_depth: int = 20) -> dict[str, object]:
|
|
116
|
-
spec_id, spec_path = _identity(root, query)
|
|
117
|
-
return {'spec_id': spec_id, 'spec_path': spec_path,
|
|
118
|
-
**scan_impacts(root, {spec_id: spec_path}, max_files, max_depth),
|
|
119
|
-
'next': '修改后运行 code-flow spec refresh'}
|