134 lines
6.3 KiB
Python
134 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
|
# #region Tooling.SpecsFilePolicy [C:3] [TYPE Module] [SEMANTICS specs,git,policy,admission]
|
|
# @BRIEF Admit changed spec paths from exact committed trees against the immutable policy introduction.
|
|
# @INVARIANT Unchanged historical files are untouched; deleted paths are allowed; no worktree bytes establish admission.
|
|
# @RATIONALE Existing prototypes predate this forward policy; only reviewed schema JSON may be newly changed in specs.
|
|
# @REJECTED A full-tree extension ban would silently retire existing tooling; inferred latest/HEAD bases could hide additions.
|
|
import argparse
|
|
import json
|
|
from pathlib import Path, PurePosixPath
|
|
import re
|
|
import subprocess
|
|
|
|
POLICY_BASE = "733d632d124ff4c3b264d7ffb21c0089e5f9ddd9"
|
|
SCHEMA_PATHS = frozenset({
|
|
"specs/037-superset-baseline-engine/contracts/baseline-catalog.schema.json",
|
|
"specs/037-superset-baseline-engine/contracts/baseline-pin.schema.json",
|
|
"specs/037-superset-baseline-engine/contracts/catalog-revision.schema.json",
|
|
"specs/037-superset-baseline-engine/contracts/comparison-types.schema.json",
|
|
"specs/037-superset-baseline-engine/contracts/reference-url.schema.json",
|
|
"specs/038-dashboard-scenario-model/contracts/agent-evaluation-spec.schema.json",
|
|
"specs/038-dashboard-scenario-model/contracts/agent-evaluation.schema.json",
|
|
"specs/038-dashboard-scenario-model/contracts/capture-profile.schema.json",
|
|
"specs/038-dashboard-scenario-model/contracts/dashboard-test-scenario-v2.schema.json",
|
|
"specs/038-dashboard-scenario-model/contracts/dashboard-test-scenario.schema.json",
|
|
"specs/038-dashboard-scenario-model/contracts/decision-policy.schema.json",
|
|
"specs/044-dashboard-scenario-execution/contracts/result-evidence.schema.json",
|
|
"specs/050-mcp-interface/contracts/tool-contracts.schema.json",
|
|
})
|
|
FORBIDDEN_SUFFIXES = frozenset({".png", ".log", ".json", ".txt", ".py"})
|
|
|
|
|
|
# #region Tooling.SpecsFilePolicy.Refusal [C:1] [TYPE Class]
|
|
class PolicyRefusal(ValueError):
|
|
def __init__(self, code, paths=()):
|
|
self.code, self.paths = code, tuple(paths)
|
|
super().__init__(code)
|
|
# #endregion Tooling.SpecsFilePolicy.Refusal
|
|
|
|
|
|
# #region Tooling.SpecsFilePolicy.Git [C:2] [TYPE Function]
|
|
def git(repo, *arguments):
|
|
result = subprocess.run(["git", "-C", str(repo), *arguments], capture_output=True)
|
|
if result.returncode:
|
|
raise PolicyRefusal("SPEC_POLICY_GIT_OBJECT_UNAVAILABLE")
|
|
return result.stdout
|
|
# #endregion Tooling.SpecsFilePolicy.Git
|
|
|
|
|
|
# #region Tooling.SpecsFilePolicy.Commits [C:3] [TYPE Function]
|
|
# @POST Both identities are exact commit objects and the fixed introduction is an ancestor of the candidate.
|
|
def require_commits(repo, base, commit):
|
|
if base != POLICY_BASE:
|
|
raise PolicyRefusal("SPEC_POLICY_BASE_INVALID")
|
|
for value in (base, commit):
|
|
if not isinstance(value, str) or re.fullmatch(r"[a-f0-9]{40}", value) is None:
|
|
raise PolicyRefusal("SPEC_POLICY_COMMIT_INVALID")
|
|
if git(repo, "cat-file", "-t", value).strip() != b"commit":
|
|
raise PolicyRefusal("SPEC_POLICY_COMMIT_INVALID")
|
|
result = subprocess.run(["git", "-C", str(repo), "merge-base", "--is-ancestor", base, commit], capture_output=True)
|
|
if result.returncode:
|
|
raise PolicyRefusal("SPEC_POLICY_BASE_NOT_ANCESTOR")
|
|
# #endregion Tooling.SpecsFilePolicy.Commits
|
|
|
|
|
|
# #region Tooling.SpecsFilePolicy.Destinations [C:3] [TYPE Function]
|
|
# @POST Copies and renames validate their destination, never merely the old allowed path.
|
|
def changed_destinations(repo, base, commit):
|
|
fields = git(repo, "diff", "--name-status", "-z", "--find-renames", "--find-copies-harder",
|
|
"--diff-filter=AMCRT", base, commit, "--", "specs").split(b"\0")
|
|
paths, index = [], 0
|
|
while fields[index]:
|
|
status = fields[index].decode("ascii")
|
|
index += 1
|
|
if status[0] in "RC":
|
|
index += 1
|
|
paths.append(fields[index].decode("utf-8"))
|
|
index += 1
|
|
return paths
|
|
# #endregion Tooling.SpecsFilePolicy.Destinations
|
|
|
|
|
|
# #region Tooling.SpecsFilePolicy.Schema [C:3] [TYPE Function]
|
|
# @POST Exact allowlisted path also contains a real JSON Schema object, rather than arbitrary fixture data.
|
|
def require_schema(repo, commit, path):
|
|
mode = git(repo, "ls-tree", commit, "--", path).split(b" ", 1)[0]
|
|
if mode != b"100644":
|
|
raise PolicyRefusal("SPEC_POLICY_SCHEMA_INVALID", (path,))
|
|
try:
|
|
value = json.loads(git(repo, "show", f"{commit}:{path}"))
|
|
except (ValueError, UnicodeDecodeError):
|
|
raise PolicyRefusal("SPEC_POLICY_SCHEMA_INVALID", (path,)) from None
|
|
if (not isinstance(value, dict) or not isinstance(value.get("$schema"), str) or not value["$schema"].strip()
|
|
or not isinstance(value.get("$id"), str) or not value["$id"].strip()
|
|
or not any(isinstance(value.get(key), kind) for key, kind in (
|
|
("properties", dict), ("$defs", dict), ("definitions", dict),
|
|
("allOf", list), ("oneOf", list), ("anyOf", list)))):
|
|
raise PolicyRefusal("SPEC_POLICY_SCHEMA_INVALID", (path,))
|
|
# #endregion Tooling.SpecsFilePolicy.Schema
|
|
|
|
|
|
# #region Tooling.SpecsFilePolicy.Check [C:3] [TYPE Function]
|
|
def check(repo, base, commit):
|
|
require_commits(repo, base, commit)
|
|
paths = changed_destinations(repo, base, commit)
|
|
for path in paths:
|
|
if PurePosixPath(path).suffix.lower() not in FORBIDDEN_SUFFIXES:
|
|
continue
|
|
if path not in SCHEMA_PATHS:
|
|
raise PolicyRefusal("SPEC_POLICY_PATH_FORBIDDEN", (path,))
|
|
require_schema(repo, commit, path)
|
|
return {"status": "PASS", "base": base, "commit": commit, "changed_spec_paths": len(paths)}
|
|
# #endregion Tooling.SpecsFilePolicy.Check
|
|
|
|
|
|
# #region Tooling.SpecsFilePolicy.Main [C:3] [TYPE Function]
|
|
def main(argv=None):
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--base", required=True)
|
|
parser.add_argument("--commit", required=True)
|
|
args = parser.parse_args(argv)
|
|
try:
|
|
result = check(Path(__file__).resolve().parents[1], args.base, args.commit)
|
|
except PolicyRefusal as exc:
|
|
print(json.dumps({"status": "REFUSED", "error_code": exc.code, "paths": exc.paths}))
|
|
return 2
|
|
print(json.dumps(result))
|
|
return 0
|
|
# #endregion Tooling.SpecsFilePolicy.Main
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|
|
# #endregion Tooling.SpecsFilePolicy
|