claude-dev-env 2.4.0 → 2.5.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/CLAUDE.md +53 -49
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +2 -1
- package/agents/caveman.md +0 -1
- package/agents/clasp-deployment-orchestrator.md +0 -1
- package/agents/clean-coder.md +0 -1
- package/agents/code-advisor.md +0 -1
- package/agents/code-quality-agent.md +1 -2
- package/agents/code-verifier.md +0 -1
- package/agents/deep-research.md +0 -1
- package/agents/docs-agent.md +0 -1
- package/agents/git-commit-crafter.md +0 -1
- package/agents/issue-tracker.md +42 -0
- package/agents/plan-packet-validator.md +0 -1
- package/agents/pr-description-writer.md +0 -1
- package/agents/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
- package/bin/CLAUDE.md +68 -5
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +88 -0
- package/bin/install.mjs +1138 -114
- package/bin/install.prune.test.mjs +869 -19
- package/bin/install.test.mjs +906 -2
- package/commands/implement.md +1 -1
- package/commands/right-size.md +1 -1
- package/docs/CLAUDE.md +1 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/references/CLAUDE.md +4 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +10 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/hooks/blocking/CLAUDE.md +1 -0
- package/hooks/blocking/code_review_pr_create_gate.py +7 -3
- package/hooks/blocking/code_review_push_gate.py +9 -4
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +4 -1
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -159
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
- package/hooks/blocking/test_code_review_push_gate.py +16 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
- package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
- package/hooks/blocking/test_verification_verdict_store.py +54 -0
- package/hooks/blocking/test_verified_commit_gate.py +581 -581
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
- package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1036
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/git-hooks/test_pre_push.py +25 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/CLAUDE.md +2 -1
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
- package/hooks/pyproject.toml +1 -0
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/config/__init__.py +0 -0
- package/hooks/validators/config/directory_exemption_constants.py +183 -0
- package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
- package/hooks/validators/conftest.py +4 -0
- package/hooks/validators/ruff_integration.py +49 -5
- package/hooks/validators/run_all_validators.py +206 -9
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +92 -1
- package/hooks/validators/test_run_all_validators.py +115 -68
- package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
- package/package.json +10 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +45 -44
- package/rules/state-what-is.md +25 -0
- package/rules/verified-commit-gate-skip.md +1 -1
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Capture-PoolHealth.ps1 +410 -0
- package/scripts/_code_review_test_support.py +404 -0
- package/scripts/claude_chain_runner.py +141 -1
- package/scripts/conftest.py +16 -1
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
- package/scripts/resolve_worker_spawn.py +626 -626
- package/scripts/spawn_grok_batch.py +672 -672
- package/scripts/test_claude_chain_runner.py +131 -0
- package/scripts/test_invoke_code_review_chain.py +70 -0
- package/scripts/test_invoke_code_review_cli.py +192 -0
- package/scripts/test_invoke_code_review_contract.py +256 -0
- package/scripts/test_invoke_code_review_git.py +123 -0
- package/scripts/test_invoke_code_review_mode.py +99 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -1014
- package/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -427
- package/skills/autoconverge/reference/convergence.md +24 -3
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
- package/skills/autoconverge/workflow/converge.mjs +167 -0
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
- package/skills/closeout/SKILL.md +33 -50
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
- package/skills/codex-review/scripts/run_codex_review.py +233 -1
- package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
- package/skills/condensing-instructions/SKILL.md +81 -0
- package/skills/copilot-review/SKILL.md +119 -119
- package/skills/e-code-review/SKILL.md +52 -0
- package/skills/e-code-review/reference/fix.md +54 -0
- package/skills/e-code-review/reference/loop.md +43 -0
- package/skills/e-code-review/reference/low.md +57 -0
- package/skills/e-code-review/reference/medium.md +153 -0
- package/skills/e-code-review/reference/xhigh.md +182 -0
- package/skills/e-simplify/SKILL.md +97 -0
- package/skills/issue-tracker/SKILL.md +92 -0
- package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
- package/skills/issue-tracker/reference/handoff-schema.md +64 -0
- package/skills/issue-tracker/reference/operation-matrix.md +41 -0
- package/skills/orchestrator/SKILL.md +162 -21
- package/skills/orchestrator/scripts/status_gate.py +625 -0
- package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
- package/skills/orchestrator/scripts/test_status_gate.py +439 -0
- package/skills/orchestrator-refresh/SKILL.md +110 -35
- package/skills/plan-to-pr/SKILL.md +155 -0
- package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
- package/skills/plan-to-pr/reference/model-routing.md +36 -0
- package/skills/plan-to-pr/reference/packet-contract.md +43 -0
- package/skills/plan-to-pr/reference/packet-schema.json +57 -0
- package/skills/plan-to-pr/reference/process-inventory.md +22 -0
- package/skills/plan-to-pr/reference/review-loop.md +33 -0
- package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
- package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
- package/skills/plan-to-pr/reference/task-seeds.md +14 -0
- package/skills/plan-to-pr/reference/task-ticket.md +38 -0
- package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
- package/skills/plan-to-pr/scripts/config/constants.py +193 -0
- package/skills/plan-to-pr/scripts/create_packet.py +173 -0
- package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
- package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
- package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
- package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
- package/skills/plan-to-pr/scripts/validate_run.py +173 -0
- package/skills/plan-to-pr/test_skill_contract.py +207 -0
- package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
- package/skills/pr-converge/SKILL.md +472 -469
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +7 -4
- package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/pr-fix-protocol/SKILL.md +1 -1
- package/skills/privacy-hygiene/SKILL.md +68 -68
- package/skills/prototype/workflows/promotion.md +1 -1
- package/skills/release-notes-html/SKILL.md +164 -0
- package/skills/task-build/CLAUDE.md +8 -7
- package/skills/task-build/SKILL.md +16 -8
- package/skills/task-build/reference/tool-routing.md +19 -0
- package/scripts/test_invoke_code_review.py +0 -966
- package/skills/closeout/reference/issue-body-templates.md +0 -108
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
"""Validate one skill protocol task-run record."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import hashlib
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Mapping, Sequence
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from config.constants import (
|
|
14
|
+
ALL_ALLOWED_RECORD_FIELDS,
|
|
15
|
+
ALL_RECORD_FIELDS,
|
|
16
|
+
ALL_REPAIR_FIELDS,
|
|
17
|
+
ALL_REPAIR_TEXT_FIELDS,
|
|
18
|
+
ALL_REVIEW_FIELDS,
|
|
19
|
+
ALL_REVIEW_TEXT_FIELDS,
|
|
20
|
+
ALL_TOP_LEVEL_TEXT_FIELDS,
|
|
21
|
+
ALL_SUPPORTED_SCHEMA_TYPES,
|
|
22
|
+
ALL_VERIFICATION_FIELDS,
|
|
23
|
+
ALLOWED_REVIEW_MODEL,
|
|
24
|
+
ALLOWED_WORKER_EFFORT,
|
|
25
|
+
ALLOWED_WORKER_ROLE,
|
|
26
|
+
ARGUMENT_COUNT_REQUIRED,
|
|
27
|
+
ARGUMENT_COUNT_WITH_WORKTREE,
|
|
28
|
+
OPTION_ARGUMENT_INDEX,
|
|
29
|
+
WORKTREE_OPTION,
|
|
30
|
+
WORKTREE_PATH_ARGUMENT_INDEX,
|
|
31
|
+
COMMIT_HASH_ERROR_TEMPLATE,
|
|
32
|
+
COMMIT_HASH_PATTERN,
|
|
33
|
+
EXIT_CODE_INVALID_RECORD,
|
|
34
|
+
EXIT_CODE_VALID_RECORD,
|
|
35
|
+
JSON_ENCODING,
|
|
36
|
+
MISSING_FIELD_SEPARATOR,
|
|
37
|
+
MAXIMUM_COMMIT_HASH_LENGTH,
|
|
38
|
+
MINIMUM_COMMIT_HASH_LENGTH,
|
|
39
|
+
PROTOCOL_VALIDATION_PASSED,
|
|
40
|
+
RECORD_SCHEMA_FILENAME,
|
|
41
|
+
DOCS_PLANS_PATH_PATTERN,
|
|
42
|
+
FINDING_STATUS_CONFIRMED,
|
|
43
|
+
FINDING_STATUS_DISMISSED,
|
|
44
|
+
NATIVE_REVIEW_COMMAND,
|
|
45
|
+
PASSED_EVIDENCE_PREFIX,
|
|
46
|
+
REPAIR_STATUS_COMPLETE,
|
|
47
|
+
REPAIR_STATUS_NOT_REQUIRED,
|
|
48
|
+
SURFACE_HASH_PATTERN,
|
|
49
|
+
PATH_SEPARATOR_PATTERN,
|
|
50
|
+
SHA256_ALGORITHM,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ProtocolValidationError(ValueError):
|
|
55
|
+
"""Identify a task-run record contract violation."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _load_schema_object(schema_path: Path) -> Mapping[str, object]:
|
|
59
|
+
"""Load one JSON schema object."""
|
|
60
|
+
try:
|
|
61
|
+
parsed_schema = json.loads(schema_path.read_text(encoding=JSON_ENCODING))
|
|
62
|
+
except json.JSONDecodeError as error:
|
|
63
|
+
raise ProtocolValidationError("record schema is invalid JSON") from error
|
|
64
|
+
except OSError as error:
|
|
65
|
+
raise ProtocolValidationError(f"cannot read record schema: {error}") from error
|
|
66
|
+
if not isinstance(parsed_schema, dict):
|
|
67
|
+
raise ProtocolValidationError("record schema must be a JSON object")
|
|
68
|
+
return parsed_schema
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _resolve_schema_reference(
|
|
72
|
+
all_schema_node: Mapping[str, object], all_schema_root: Mapping[str, object]
|
|
73
|
+
) -> Mapping[str, object]:
|
|
74
|
+
"""Resolve a local `$defs` schema reference."""
|
|
75
|
+
schema_reference = all_schema_node.get("$ref")
|
|
76
|
+
if schema_reference is None:
|
|
77
|
+
return all_schema_node
|
|
78
|
+
if not isinstance(schema_reference, str) or not schema_reference.startswith(
|
|
79
|
+
"#/$defs/"
|
|
80
|
+
):
|
|
81
|
+
raise ProtocolValidationError("record schema has an unsupported reference")
|
|
82
|
+
definitions = all_schema_root.get("$defs")
|
|
83
|
+
if not isinstance(definitions, dict):
|
|
84
|
+
raise ProtocolValidationError("record schema has no definitions")
|
|
85
|
+
definition_name = schema_reference.removeprefix("#/$defs/")
|
|
86
|
+
definition = definitions.get(definition_name)
|
|
87
|
+
if not isinstance(definition, dict):
|
|
88
|
+
raise ProtocolValidationError(
|
|
89
|
+
f"record schema definition is missing: {definition_name}"
|
|
90
|
+
)
|
|
91
|
+
return definition
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _validate_schema_node(
|
|
95
|
+
candidate: object,
|
|
96
|
+
all_schema_node: Mapping[str, object],
|
|
97
|
+
all_schema_root: Mapping[str, object],
|
|
98
|
+
field_path: str,
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Validate one record value against the supported schema keywords."""
|
|
101
|
+
resolved_schema = _resolve_schema_reference(all_schema_node, all_schema_root)
|
|
102
|
+
if "const" in resolved_schema and candidate != resolved_schema["const"]:
|
|
103
|
+
raise ProtocolValidationError(
|
|
104
|
+
f"{field_path} does not match its schema constant"
|
|
105
|
+
)
|
|
106
|
+
schema_type = resolved_schema.get("type")
|
|
107
|
+
if schema_type == "object":
|
|
108
|
+
_validate_schema_object(candidate, resolved_schema, all_schema_root, field_path)
|
|
109
|
+
return
|
|
110
|
+
if schema_type == "array":
|
|
111
|
+
_validate_schema_array(candidate, resolved_schema, all_schema_root, field_path)
|
|
112
|
+
return
|
|
113
|
+
if schema_type == "string":
|
|
114
|
+
_validate_schema_string(candidate, resolved_schema, field_path)
|
|
115
|
+
return
|
|
116
|
+
if schema_type == "boolean" and not isinstance(candidate, bool):
|
|
117
|
+
raise ProtocolValidationError(f"{field_path} must be a boolean")
|
|
118
|
+
if schema_type not in (*ALL_SUPPORTED_SCHEMA_TYPES, None):
|
|
119
|
+
raise ProtocolValidationError(f"{field_path} has an unsupported schema type")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _validate_schema_object(
|
|
123
|
+
candidate: object,
|
|
124
|
+
all_schema_node: Mapping[str, object],
|
|
125
|
+
all_schema_root: Mapping[str, object],
|
|
126
|
+
field_path: str,
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Validate an object schema node."""
|
|
129
|
+
if not isinstance(candidate, dict):
|
|
130
|
+
raise ProtocolValidationError(f"{field_path} must be an object")
|
|
131
|
+
required_fields = all_schema_node.get("required", [])
|
|
132
|
+
if not isinstance(required_fields, list):
|
|
133
|
+
raise ProtocolValidationError(f"{field_path} has invalid required fields")
|
|
134
|
+
for each_field_name in required_fields:
|
|
135
|
+
if isinstance(each_field_name, str) and each_field_name not in candidate:
|
|
136
|
+
raise ProtocolValidationError(f"{field_path} missing: {each_field_name}")
|
|
137
|
+
properties = all_schema_node.get("properties", {})
|
|
138
|
+
if not isinstance(properties, dict):
|
|
139
|
+
raise ProtocolValidationError(f"{field_path} has invalid properties")
|
|
140
|
+
if all_schema_node.get("additionalProperties") is False:
|
|
141
|
+
additional_fields = sorted(
|
|
142
|
+
each_field_name
|
|
143
|
+
for each_field_name in candidate
|
|
144
|
+
if each_field_name not in properties
|
|
145
|
+
)
|
|
146
|
+
if additional_fields:
|
|
147
|
+
raise ProtocolValidationError(
|
|
148
|
+
f"{field_path} has additional fields: "
|
|
149
|
+
f"{MISSING_FIELD_SEPARATOR.join(additional_fields)}"
|
|
150
|
+
)
|
|
151
|
+
for each_field_name, each_schema_node in properties.items():
|
|
152
|
+
if each_field_name in candidate and isinstance(each_schema_node, dict):
|
|
153
|
+
_validate_schema_node(
|
|
154
|
+
candidate[each_field_name],
|
|
155
|
+
each_schema_node,
|
|
156
|
+
all_schema_root,
|
|
157
|
+
f"{field_path}.{each_field_name}",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _validate_schema_array(
|
|
162
|
+
candidate: object,
|
|
163
|
+
all_schema_node: Mapping[str, object],
|
|
164
|
+
all_schema_root: Mapping[str, object],
|
|
165
|
+
field_path: str,
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Validate an array schema node."""
|
|
168
|
+
if not isinstance(candidate, list):
|
|
169
|
+
raise ProtocolValidationError(f"{field_path} must be an array")
|
|
170
|
+
minimum_array_entries = all_schema_node.get("minItems", 0)
|
|
171
|
+
if (
|
|
172
|
+
isinstance(minimum_array_entries, int)
|
|
173
|
+
and len(candidate) < minimum_array_entries
|
|
174
|
+
):
|
|
175
|
+
raise ProtocolValidationError(
|
|
176
|
+
f"{field_path} must contain at least {minimum_array_entries} items"
|
|
177
|
+
)
|
|
178
|
+
array_entry_schema = all_schema_node.get("items")
|
|
179
|
+
if isinstance(array_entry_schema, dict):
|
|
180
|
+
for each_index, each_candidate in enumerate(candidate):
|
|
181
|
+
_validate_schema_node(
|
|
182
|
+
each_candidate,
|
|
183
|
+
array_entry_schema,
|
|
184
|
+
all_schema_root,
|
|
185
|
+
f"{field_path}[{each_index}]",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _validate_schema_string(
|
|
190
|
+
candidate: object, all_schema_node: Mapping[str, object], field_path: str
|
|
191
|
+
) -> None:
|
|
192
|
+
"""Validate a string schema node."""
|
|
193
|
+
if not isinstance(candidate, str):
|
|
194
|
+
raise ProtocolValidationError(f"{field_path} must be a string")
|
|
195
|
+
minimum_length = all_schema_node.get("minLength", 0)
|
|
196
|
+
if isinstance(minimum_length, int) and len(candidate) < minimum_length:
|
|
197
|
+
raise ProtocolValidationError(f"{field_path} must not be empty")
|
|
198
|
+
pattern = all_schema_node.get("pattern")
|
|
199
|
+
if isinstance(pattern, str) and re.search(pattern, candidate) is None:
|
|
200
|
+
raise ProtocolValidationError(f"{field_path} does not match its schema pattern")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _load_json_object(task_run_path: Path) -> Mapping[str, object]:
|
|
204
|
+
"""Load one task-run object from JSON.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
task_run_path: JSON file containing one task-run record.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
The decoded task-run mapping.
|
|
211
|
+
|
|
212
|
+
Raises:
|
|
213
|
+
ProtocolValidationError: If the file cannot be read or decoded as an object.
|
|
214
|
+
"""
|
|
215
|
+
try:
|
|
216
|
+
parsed_task_run = json.loads(task_run_path.read_text(encoding=JSON_ENCODING))
|
|
217
|
+
except json.JSONDecodeError as error:
|
|
218
|
+
raise ProtocolValidationError("invalid JSON") from error
|
|
219
|
+
except UnicodeDecodeError as error:
|
|
220
|
+
raise ProtocolValidationError("invalid UTF-8") from error
|
|
221
|
+
except OSError as error:
|
|
222
|
+
raise ProtocolValidationError(
|
|
223
|
+
f"cannot read task-run record: {error}"
|
|
224
|
+
) from error
|
|
225
|
+
if not isinstance(parsed_task_run, dict):
|
|
226
|
+
raise ProtocolValidationError("task-run record must be a JSON object")
|
|
227
|
+
return parsed_task_run
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _require_fields(
|
|
231
|
+
all_task_run_record: Mapping[str, object],
|
|
232
|
+
all_required_field_names: Sequence[str],
|
|
233
|
+
record_name: str,
|
|
234
|
+
) -> None:
|
|
235
|
+
"""Require named fields in a task-run mapping.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
all_task_run_record: Mapping to inspect.
|
|
239
|
+
all_required_field_names: Field names that must be present.
|
|
240
|
+
record_name: Display name used in validation errors.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
None.
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
ProtocolValidationError: If a required field is absent.
|
|
247
|
+
"""
|
|
248
|
+
missing_field_names = [
|
|
249
|
+
each_field_name
|
|
250
|
+
for each_field_name in all_required_field_names
|
|
251
|
+
if each_field_name not in all_task_run_record
|
|
252
|
+
]
|
|
253
|
+
if missing_field_names:
|
|
254
|
+
raise ProtocolValidationError(
|
|
255
|
+
f"{record_name} missing: "
|
|
256
|
+
f"{MISSING_FIELD_SEPARATOR.join(missing_field_names)}"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _reject_additional_fields(
|
|
261
|
+
all_task_run_record: Mapping[str, object],
|
|
262
|
+
all_allowed_field_names: Sequence[str],
|
|
263
|
+
record_name: str,
|
|
264
|
+
) -> None:
|
|
265
|
+
"""Reject fields that are not named by the record schema.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
all_task_run_record: Mapping to inspect.
|
|
269
|
+
all_allowed_field_names: Field names accepted by the schema.
|
|
270
|
+
record_name: Display name used in validation errors.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
None.
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
ProtocolValidationError: If an unknown field is present.
|
|
277
|
+
"""
|
|
278
|
+
allowed_field_names = set(all_allowed_field_names)
|
|
279
|
+
additional_field_names = sorted(
|
|
280
|
+
each_field_name
|
|
281
|
+
for each_field_name in all_task_run_record
|
|
282
|
+
if each_field_name not in allowed_field_names
|
|
283
|
+
)
|
|
284
|
+
if additional_field_names:
|
|
285
|
+
raise ProtocolValidationError(
|
|
286
|
+
f"{record_name} has additional fields: "
|
|
287
|
+
f"{MISSING_FIELD_SEPARATOR.join(additional_field_names)}"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _require_text(
|
|
292
|
+
all_task_run_record: Mapping[str, object], field_name: str, record_name: str
|
|
293
|
+
) -> None:
|
|
294
|
+
"""Require a non-empty text field in a task-run mapping.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
all_task_run_record: Mapping containing the field.
|
|
298
|
+
field_name: Name of the field to validate.
|
|
299
|
+
record_name: Display name used in validation errors.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
None.
|
|
303
|
+
|
|
304
|
+
Raises:
|
|
305
|
+
ProtocolValidationError: If the field is absent, non-text, or empty.
|
|
306
|
+
"""
|
|
307
|
+
field_text = all_task_run_record[field_name]
|
|
308
|
+
if not isinstance(field_text, str) or not field_text:
|
|
309
|
+
raise ProtocolValidationError(
|
|
310
|
+
f"{record_name}.{field_name} must be a non-empty string"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _require_string_list(
|
|
315
|
+
all_task_run_record: Mapping[str, object], field_name: str, record_name: str
|
|
316
|
+
) -> list[str]:
|
|
317
|
+
"""Require a list containing only non-empty strings.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
all_task_run_record: Mapping containing the field.
|
|
321
|
+
field_name: Name of the field to validate.
|
|
322
|
+
record_name: Display name used in validation errors.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
None.
|
|
326
|
+
|
|
327
|
+
Raises:
|
|
328
|
+
ProtocolValidationError: If the field is not a string list.
|
|
329
|
+
"""
|
|
330
|
+
all_field_entries = all_task_run_record[field_name]
|
|
331
|
+
if not isinstance(all_field_entries, list) or any(
|
|
332
|
+
not isinstance(each_entry, str) or not each_entry
|
|
333
|
+
for each_entry in all_field_entries
|
|
334
|
+
):
|
|
335
|
+
raise ProtocolValidationError(
|
|
336
|
+
f"{record_name}.{field_name} must be a string list"
|
|
337
|
+
)
|
|
338
|
+
return all_field_entries
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _require_non_empty_string_list(
|
|
342
|
+
all_task_run_record: Mapping[str, object], field_name: str, record_name: str
|
|
343
|
+
) -> None:
|
|
344
|
+
"""Require a non-empty list containing only non-empty strings.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
all_task_run_record: Mapping containing the field.
|
|
348
|
+
field_name: Name of the field to validate.
|
|
349
|
+
record_name: Display name used in validation errors.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
None.
|
|
353
|
+
|
|
354
|
+
Raises:
|
|
355
|
+
ProtocolValidationError: If the field is not a non-empty string list.
|
|
356
|
+
"""
|
|
357
|
+
all_field_entries = _require_string_list(
|
|
358
|
+
all_task_run_record, field_name, record_name
|
|
359
|
+
)
|
|
360
|
+
if not all_field_entries:
|
|
361
|
+
raise ProtocolValidationError(f"{record_name}.{field_name} must not be empty")
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _require_record(
|
|
365
|
+
all_task_run_record: Mapping[str, object], field_name: str
|
|
366
|
+
) -> Mapping[str, object]:
|
|
367
|
+
"""Return a nested task-run mapping.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
all_task_run_record: Mapping containing the nested record.
|
|
371
|
+
field_name: Name of the nested record field.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
The nested task-run mapping.
|
|
375
|
+
|
|
376
|
+
Raises:
|
|
377
|
+
ProtocolValidationError: If the nested field is not an object.
|
|
378
|
+
"""
|
|
379
|
+
nested_task_run_record = all_task_run_record[field_name]
|
|
380
|
+
if not isinstance(nested_task_run_record, dict):
|
|
381
|
+
raise ProtocolValidationError(f"record.{field_name} must be an object")
|
|
382
|
+
return nested_task_run_record
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _validate_review_record(all_review_record: Mapping[str, object]) -> None:
|
|
386
|
+
"""Validate the findings-only review section.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
all_review_record: Review mapping from the task-run record.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
None.
|
|
393
|
+
|
|
394
|
+
Raises:
|
|
395
|
+
ProtocolValidationError: If the review section violates its contract.
|
|
396
|
+
"""
|
|
397
|
+
_reject_additional_fields(all_review_record, ALL_REVIEW_FIELDS, "review_record")
|
|
398
|
+
_require_fields(all_review_record, ALL_REVIEW_FIELDS, "review_record")
|
|
399
|
+
for each_field_name in ALL_REVIEW_TEXT_FIELDS:
|
|
400
|
+
_require_text(all_review_record, each_field_name, "review_record")
|
|
401
|
+
findings = all_review_record["findings"]
|
|
402
|
+
if not isinstance(findings, list) or any(
|
|
403
|
+
not isinstance(each_finding, dict)
|
|
404
|
+
or not isinstance(each_finding.get("finding"), str)
|
|
405
|
+
or each_finding.get("disposition") not in {
|
|
406
|
+
FINDING_STATUS_CONFIRMED,
|
|
407
|
+
FINDING_STATUS_DISMISSED,
|
|
408
|
+
}
|
|
409
|
+
for each_finding in findings
|
|
410
|
+
):
|
|
411
|
+
raise ProtocolValidationError("review_record.findings must be classified")
|
|
412
|
+
if all_review_record["findings_only"] is not True:
|
|
413
|
+
raise ProtocolValidationError("review_record.findings_only must be true")
|
|
414
|
+
if all_review_record["has_repair_flag"] is not False:
|
|
415
|
+
raise ProtocolValidationError("review_record.has_repair_flag must be false")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _validate_repair_record(all_repair_record: Mapping[str, object]) -> None:
|
|
419
|
+
"""Validate the confirmed-findings repair section.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
all_repair_record: Repair mapping from the task-run record.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
None.
|
|
426
|
+
|
|
427
|
+
Raises:
|
|
428
|
+
ProtocolValidationError: If the repair section violates its contract.
|
|
429
|
+
"""
|
|
430
|
+
_reject_additional_fields(all_repair_record, ALL_REPAIR_FIELDS, "repair_record")
|
|
431
|
+
_require_fields(all_repair_record, ALL_REPAIR_FIELDS, "repair_record")
|
|
432
|
+
for each_field_name in ALL_REPAIR_TEXT_FIELDS:
|
|
433
|
+
_require_text(all_repair_record, each_field_name, "repair_record")
|
|
434
|
+
_require_string_list(all_repair_record, "confirmed_findings", "repair_record")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _validate_verification_record(
|
|
438
|
+
all_verification_record: Mapping[str, object], record_name: str
|
|
439
|
+
) -> None:
|
|
440
|
+
"""Validate one verification evidence section.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
all_verification_record: Verification mapping from the task-run record.
|
|
444
|
+
record_name: Display name used in validation errors.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
None.
|
|
448
|
+
|
|
449
|
+
Raises:
|
|
450
|
+
ProtocolValidationError: If required evidence is missing or empty.
|
|
451
|
+
"""
|
|
452
|
+
_reject_additional_fields(
|
|
453
|
+
all_verification_record, ALL_VERIFICATION_FIELDS, record_name
|
|
454
|
+
)
|
|
455
|
+
_require_fields(all_verification_record, ALL_VERIFICATION_FIELDS, record_name)
|
|
456
|
+
for each_field_name in ALL_VERIFICATION_FIELDS:
|
|
457
|
+
_require_text(all_verification_record, each_field_name, record_name)
|
|
458
|
+
field_text = all_verification_record[each_field_name]
|
|
459
|
+
if (
|
|
460
|
+
each_field_name != "surface_hash"
|
|
461
|
+
and isinstance(field_text, str)
|
|
462
|
+
and not field_text.lower().startswith(PASSED_EVIDENCE_PREFIX)
|
|
463
|
+
):
|
|
464
|
+
raise ProtocolValidationError(
|
|
465
|
+
f"{record_name}.{each_field_name} must be explicit passed evidence"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _normalize_allowed_path(raw_path: str) -> str:
|
|
470
|
+
normalized_path = PATH_SEPARATOR_PATTERN.sub("/", raw_path.strip())
|
|
471
|
+
normalized_path = normalized_path.removeprefix("./")
|
|
472
|
+
if not normalized_path or normalized_path.startswith("../") or "/../" in normalized_path:
|
|
473
|
+
raise ProtocolValidationError("allowed_files contains an unsafe path")
|
|
474
|
+
if normalized_path.startswith("/") or (
|
|
475
|
+
normalized_path[1:].startswith(":/")
|
|
476
|
+
):
|
|
477
|
+
raise ProtocolValidationError("allowed_files contains an absolute path")
|
|
478
|
+
if DOCS_PLANS_PATH_PATTERN.match(normalized_path.lower()):
|
|
479
|
+
raise ProtocolValidationError("allowed_files cannot contain docs/plans paths")
|
|
480
|
+
return normalized_path
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _validate_semantics(all_task_run_record: Mapping[str, object]) -> None:
|
|
484
|
+
worker_route = all_task_run_record["worker_route"]
|
|
485
|
+
if worker_route != f"{ALLOWED_WORKER_ROLE}; effort={ALLOWED_WORKER_EFFORT}":
|
|
486
|
+
raise ProtocolValidationError("worker_route must be the fast low-effort Luna contract")
|
|
487
|
+
all_allowed_files = all_task_run_record["allowed_files"]
|
|
488
|
+
assert isinstance(all_allowed_files, list)
|
|
489
|
+
normalized_files = [_normalize_allowed_path(each_path) for each_path in all_allowed_files if isinstance(each_path, str)]
|
|
490
|
+
if len(normalized_files) != len(set(normalized_files)):
|
|
491
|
+
raise ProtocolValidationError("allowed_files contains duplicate paths")
|
|
492
|
+
review_record = _require_record(all_task_run_record, "review_record")
|
|
493
|
+
if review_record["resolved_model"] != ALLOWED_REVIEW_MODEL or review_record["effort"] != ALLOWED_WORKER_EFFORT:
|
|
494
|
+
raise ProtocolValidationError("review_record must use fast low-effort Luna")
|
|
495
|
+
command = review_record["command"]
|
|
496
|
+
if not isinstance(command, str):
|
|
497
|
+
raise ProtocolValidationError("review_record.command must be a string")
|
|
498
|
+
if command != NATIVE_REVIEW_COMMAND or "--fix" in command:
|
|
499
|
+
raise ProtocolValidationError("review_record.command must be exactly /e-code-review low")
|
|
500
|
+
findings = review_record["findings"]
|
|
501
|
+
assert isinstance(findings, list)
|
|
502
|
+
confirmed_findings = [each_finding["finding"] for each_finding in findings if each_finding["disposition"] == FINDING_STATUS_CONFIRMED]
|
|
503
|
+
repair_record = _require_record(all_task_run_record, "repair_record")
|
|
504
|
+
if repair_record["confirmed_findings"] != confirmed_findings:
|
|
505
|
+
raise ProtocolValidationError("repair_record must list exactly the confirmed findings")
|
|
506
|
+
if confirmed_findings and repair_record["repair_status"] != REPAIR_STATUS_COMPLETE:
|
|
507
|
+
raise ProtocolValidationError("confirmed findings require a completed repair")
|
|
508
|
+
if not confirmed_findings and repair_record["repair_status"] != REPAIR_STATUS_NOT_REQUIRED:
|
|
509
|
+
raise ProtocolValidationError("repair is permitted only when no findings are confirmed")
|
|
510
|
+
hashes = [
|
|
511
|
+
review_record["surface_hash"], repair_record["surface_hash"],
|
|
512
|
+
_require_record(all_task_run_record, "reverification_record")["surface_hash"],
|
|
513
|
+
_require_record(all_task_run_record, "verification_record")["surface_hash"],
|
|
514
|
+
]
|
|
515
|
+
if any(not isinstance(each_hash, str) or SURFACE_HASH_PATTERN.fullmatch(each_hash) is None for each_hash in hashes) or len(set(hashes)) != 1:
|
|
516
|
+
raise ProtocolValidationError("review and verification surface hashes must match")
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _committed_surface_hash(worktree: Path, commit_hash: str, all_allowed_files: list[str]) -> str:
|
|
520
|
+
try:
|
|
521
|
+
completed = subprocess.run(
|
|
522
|
+
["git", "diff", f"{commit_hash}^", commit_hash, "--", *all_allowed_files],
|
|
523
|
+
cwd=worktree, check=True, capture_output=True,
|
|
524
|
+
)
|
|
525
|
+
except (OSError, subprocess.CalledProcessError) as error:
|
|
526
|
+
raise ProtocolValidationError("cannot compute committed task surface") from error
|
|
527
|
+
return hashlib.new(SHA256_ALGORITHM, completed.stdout).hexdigest()
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def validate_record(
|
|
531
|
+
all_task_run_record: Mapping[str, object], schema_path: Path
|
|
532
|
+
) -> None:
|
|
533
|
+
"""Validate a complete task-run record against its JSON schema file.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
all_task_run_record: Task-run mapping to validate.
|
|
537
|
+
schema_path: Path to the task-run schema JSON file.
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
None.
|
|
541
|
+
|
|
542
|
+
Raises:
|
|
543
|
+
ProtocolValidationError: If the schema or task-run record is invalid.
|
|
544
|
+
"""
|
|
545
|
+
schema_object = _load_schema_object(schema_path)
|
|
546
|
+
_validate_schema_node(all_task_run_record, schema_object, schema_object, "record")
|
|
547
|
+
_reject_additional_fields(all_task_run_record, ALL_ALLOWED_RECORD_FIELDS, "record")
|
|
548
|
+
_require_fields(all_task_run_record, ALL_RECORD_FIELDS, "record")
|
|
549
|
+
for each_field_name in ALL_TOP_LEVEL_TEXT_FIELDS:
|
|
550
|
+
_require_text(all_task_run_record, each_field_name, "record")
|
|
551
|
+
_require_non_empty_string_list(all_task_run_record, "allowed_files", "record")
|
|
552
|
+
commit_hash = all_task_run_record["commit"]
|
|
553
|
+
if (
|
|
554
|
+
not isinstance(commit_hash, str)
|
|
555
|
+
or not MINIMUM_COMMIT_HASH_LENGTH
|
|
556
|
+
<= len(commit_hash)
|
|
557
|
+
<= MAXIMUM_COMMIT_HASH_LENGTH
|
|
558
|
+
or COMMIT_HASH_PATTERN.fullmatch(commit_hash) is None
|
|
559
|
+
):
|
|
560
|
+
raise ProtocolValidationError(
|
|
561
|
+
COMMIT_HASH_ERROR_TEMPLATE
|
|
562
|
+
% (MINIMUM_COMMIT_HASH_LENGTH, MAXIMUM_COMMIT_HASH_LENGTH)
|
|
563
|
+
)
|
|
564
|
+
_validate_review_record(_require_record(all_task_run_record, "review_record"))
|
|
565
|
+
_validate_repair_record(_require_record(all_task_run_record, "repair_record"))
|
|
566
|
+
_validate_verification_record(
|
|
567
|
+
_require_record(all_task_run_record, "reverification_record"),
|
|
568
|
+
"reverification_record",
|
|
569
|
+
)
|
|
570
|
+
_validate_verification_record(
|
|
571
|
+
_require_record(all_task_run_record, "verification_record"),
|
|
572
|
+
"verification_record",
|
|
573
|
+
)
|
|
574
|
+
_validate_semantics(all_task_run_record)
|
|
575
|
+
worktree = all_task_run_record.get("worktree")
|
|
576
|
+
if isinstance(worktree, str):
|
|
577
|
+
all_allowed_files = [
|
|
578
|
+
_normalize_allowed_path(each_path)
|
|
579
|
+
for each_path in _require_string_list(
|
|
580
|
+
all_task_run_record, "allowed_files", "record"
|
|
581
|
+
)
|
|
582
|
+
]
|
|
583
|
+
committed_hash = _committed_surface_hash(Path(worktree), str(all_task_run_record["commit"]), all_allowed_files)
|
|
584
|
+
recorded_hash = _require_record(all_task_run_record, "verification_record")["surface_hash"]
|
|
585
|
+
if committed_hash != recorded_hash:
|
|
586
|
+
raise ProtocolValidationError("verification surface hash does not match the committed diff")
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def main(all_cli_arguments: Sequence[str]) -> int:
|
|
590
|
+
"""Validate a task-run record from command-line arguments.
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
all_cli_arguments: Program name followed by one record path.
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
Exit code `0` for a valid record or `2` for invalid input.
|
|
597
|
+
"""
|
|
598
|
+
if len(all_cli_arguments) not in {
|
|
599
|
+
ARGUMENT_COUNT_REQUIRED,
|
|
600
|
+
ARGUMENT_COUNT_WITH_WORKTREE,
|
|
601
|
+
} or (
|
|
602
|
+
len(all_cli_arguments) == ARGUMENT_COUNT_WITH_WORKTREE
|
|
603
|
+
and all_cli_arguments[OPTION_ARGUMENT_INDEX] != WORKTREE_OPTION
|
|
604
|
+
):
|
|
605
|
+
print("usage: validate_protocol.py <record.json> [--worktree PATH]", file=sys.stderr)
|
|
606
|
+
return EXIT_CODE_INVALID_RECORD
|
|
607
|
+
schema_path = Path(__file__).parent.parent / "reference" / RECORD_SCHEMA_FILENAME
|
|
608
|
+
try:
|
|
609
|
+
record = _load_json_object(Path(all_cli_arguments[1]))
|
|
610
|
+
if len(all_cli_arguments) == ARGUMENT_COUNT_WITH_WORKTREE:
|
|
611
|
+
record = dict(record)
|
|
612
|
+
record["worktree"] = all_cli_arguments[WORKTREE_PATH_ARGUMENT_INDEX]
|
|
613
|
+
validate_record(record, schema_path)
|
|
614
|
+
except ProtocolValidationError as error:
|
|
615
|
+
print(f"protocol validation failed: {error}", file=sys.stderr)
|
|
616
|
+
return EXIT_CODE_INVALID_RECORD
|
|
617
|
+
print(PROTOCOL_VALIDATION_PASSED)
|
|
618
|
+
return EXIT_CODE_VALID_RECORD
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
if __name__ == "__main__":
|
|
622
|
+
raise SystemExit(main(sys.argv))
|