@arbiterforge/ca-pi 0.6.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/LICENSE +661 -0
- package/README.md +558 -0
- package/package.json +35 -0
- package/plugins/ca-pi/CHANGELOG.md +1030 -0
- package/plugins/ca-pi/COMMANDS.md +90 -0
- package/plugins/ca-pi/ORCHESTRATOR.md +159 -0
- package/plugins/ca-pi/SKILLS.md +47 -0
- package/plugins/ca-pi/SPRINT.md +142 -0
- package/plugins/ca-pi/agents/INDEX.md +31 -0
- package/plugins/ca-pi/agents/architecture-drift-reviewer.md +86 -0
- package/plugins/ca-pi/agents/auth-crypto-reviewer.md +60 -0
- package/plugins/ca-pi/agents/backend-author.md +60 -0
- package/plugins/ca-pi/agents/checkpoint-aggregator.md +111 -0
- package/plugins/ca-pi/agents/coverage-auditor.md +71 -0
- package/plugins/ca-pi/agents/decision-challenger.md +116 -0
- package/plugins/ca-pi/agents/dependency-reviewer.md +79 -0
- package/plugins/ca-pi/agents/design-quality-reviewer.md +80 -0
- package/plugins/ca-pi/agents/finding-triage.md +86 -0
- package/plugins/ca-pi/agents/frontend-author.md +64 -0
- package/plugins/ca-pi/agents/grader.md +173 -0
- package/plugins/ca-pi/agents/infra-author.md +64 -0
- package/plugins/ca-pi/agents/map-deps.md +35 -0
- package/plugins/ca-pi/agents/map-structure.md +37 -0
- package/plugins/ca-pi/agents/migration-reviewer.md +65 -0
- package/plugins/ca-pi/agents/scout.md +127 -0
- package/plugins/ca-pi/agents/security-reviewer.md +72 -0
- package/plugins/ca-pi/agents/tribunal-lens-reviewer.md +65 -0
- package/plugins/ca-pi/extensions/codearbiter-child.js +1885 -0
- package/plugins/ca-pi/extensions/codearbiter.js +9802 -0
- package/plugins/ca-pi/generated/command-catalog.json +197 -0
- package/plugins/ca-pi/generated/roles.json +213 -0
- package/plugins/ca-pi/helpers/windows-supervisor.js +205 -0
- package/plugins/ca-pi/hooks/_activationlib.py +196 -0
- package/plugins/ca-pi/hooks/_arbiterstatelib.py +208 -0
- package/plugins/ca-pi/hooks/_babysitlib.py +76 -0
- package/plugins/ca-pi/hooks/_bashguardlib.py +1667 -0
- package/plugins/ca-pi/hooks/_boxlib.py +131 -0
- package/plugins/ca-pi/hooks/_colorlib.py +304 -0
- package/plugins/ca-pi/hooks/_durabilitylib.py +186 -0
- package/plugins/ca-pi/hooks/_entrylib.py +41 -0
- package/plugins/ca-pi/hooks/_fmtlib.py +161 -0
- package/plugins/ca-pi/hooks/_gitexec.py +45 -0
- package/plugins/ca-pi/hooks/_githooks.py +920 -0
- package/plugins/ca-pi/hooks/_gitlib.py +110 -0
- package/plugins/ca-pi/hooks/_hooklib.py +595 -0
- package/plugins/ca-pi/hooks/_host.py +115 -0
- package/plugins/ca-pi/hooks/_intentlib.py +242 -0
- package/plugins/ca-pi/hooks/_ledgerlib.py +1035 -0
- package/plugins/ca-pi/hooks/_metricslib.py +709 -0
- package/plugins/ca-pi/hooks/_pathnorm.py +74 -0
- package/plugins/ca-pi/hooks/_planfilelib.py +664 -0
- package/plugins/ca-pi/hooks/_previewlib.py +193 -0
- package/plugins/ca-pi/hooks/_protectedlib.py +312 -0
- package/plugins/ca-pi/hooks/_protectedstatelib.py +411 -0
- package/plugins/ca-pi/hooks/_provenancelib.py +971 -0
- package/plugins/ca-pi/hooks/_prunelib.py +1398 -0
- package/plugins/ca-pi/hooks/_prunepolicy.py +235 -0
- package/plugins/ca-pi/hooks/_readinjectlib.py +1080 -0
- package/plugins/ca-pi/hooks/_releaselib.py +2657 -0
- package/plugins/ca-pi/hooks/_scopelib.py +262 -0
- package/plugins/ca-pi/hooks/_segmentslib.py +278 -0
- package/plugins/ca-pi/hooks/_sensitivelib.py +270 -0
- package/plugins/ca-pi/hooks/_sessionlib.py +78 -0
- package/plugins/ca-pi/hooks/_sloplib.py +244 -0
- package/plugins/ca-pi/hooks/_standuplib.py +214 -0
- package/plugins/ca-pi/hooks/_subagentslib.py +219 -0
- package/plugins/ca-pi/hooks/_taskboardlib.py +1088 -0
- package/plugins/ca-pi/hooks/_updatelib.py +278 -0
- package/plugins/ca-pi/hooks/babysit.py +47 -0
- package/plugins/ca-pi/hooks/boardsync.py +129 -0
- package/plugins/ca-pi/hooks/doctor.py +420 -0
- package/plugins/ca-pi/hooks/git-enforce.py +325 -0
- package/plugins/ca-pi/hooks/hostapi.py +460 -0
- package/plugins/ca-pi/hooks/init-codearbiter.py +225 -0
- package/plugins/ca-pi/hooks/metrics.py +62 -0
- package/plugins/ca-pi/hooks/migration-pass.py +129 -0
- package/plugins/ca-pi/hooks/pi-bridge.py +543 -0
- package/plugins/ca-pi/hooks/post-write-edit.py +231 -0
- package/plugins/ca-pi/hooks/pre-bash.py +90 -0
- package/plugins/ca-pi/hooks/pre-edit.py +284 -0
- package/plugins/ca-pi/hooks/pre-read.py +81 -0
- package/plugins/ca-pi/hooks/pre-write.py +217 -0
- package/plugins/ca-pi/hooks/preview.py +69 -0
- package/plugins/ca-pi/hooks/prune-transcript.py +232 -0
- package/plugins/ca-pi/hooks/releasehash.py +216 -0
- package/plugins/ca-pi/hooks/security-pass.py +139 -0
- package/plugins/ca-pi/hooks/session-start.py +1218 -0
- package/plugins/ca-pi/hooks/statusline.py +736 -0
- package/plugins/ca-pi/hooks/taskwrite.py +351 -0
- package/plugins/ca-pi/hooks/update-refresh.py +51 -0
- package/plugins/ca-pi/hooks/wire-statusline.py +435 -0
- package/plugins/ca-pi/includes/anti-slop-design/INDEX.md +55 -0
- package/plugins/ca-pi/includes/anti-slop-design/color.md +43 -0
- package/plugins/ca-pi/includes/anti-slop-design/core.md +244 -0
- package/plugins/ca-pi/includes/anti-slop-design/images.md +32 -0
- package/plugins/ca-pi/includes/anti-slop-design/layout.md +45 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-cli.md +39 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-dataviz.md +43 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-diagram.md +35 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-documents.md +70 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-slides.md +30 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-web.md +39 -0
- package/plugins/ca-pi/includes/anti-slop-design/typography.md +51 -0
- package/plugins/ca-pi/includes/author-tdd-workflow.md +14 -0
- package/plugins/ca-pi/includes/compaction-charter.md +16 -0
- package/plugins/ca-pi/includes/cut-docs.md +16 -0
- package/plugins/ca-pi/includes/dev-mode.md +30 -0
- package/plugins/ca-pi/includes/farm.md +237 -0
- package/plugins/ca-pi/includes/fresh-verification.md +14 -0
- package/plugins/ca-pi/includes/harvest.md +69 -0
- package/plugins/ca-pi/includes/maturity-coverage.md +102 -0
- package/plugins/ca-pi/includes/pi-host-notes.md +69 -0
- package/plugins/ca-pi/includes/redirect.md +69 -0
- package/plugins/ca-pi/includes/reference-map.md +22 -0
- package/plugins/ca-pi/includes/review-matrix.md +14 -0
- package/plugins/ca-pi/includes/reviewer-contract.md +53 -0
- package/plugins/ca-pi/includes/routing-table.md +47 -0
- package/plugins/ca-pi/includes/security-gate-record.md +22 -0
- package/plugins/ca-pi/includes/smarts/core.md +90 -0
- package/plugins/ca-pi/includes/smarts/decision-log-format.md +56 -0
- package/plugins/ca-pi/routines/INDEX.md +32 -0
- package/plugins/ca-pi/routines/brainstorming/SKILL.md +122 -0
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +151 -0
- package/plugins/ca-pi/routines/context-check/SKILL.md +85 -0
- package/plugins/ca-pi/routines/context-creation/SKILL.md +171 -0
- package/plugins/ca-pi/routines/crypto-compliance/SKILL.md +41 -0
- package/plugins/ca-pi/routines/debug/SKILL.md +99 -0
- package/plugins/ca-pi/routines/decision-lifecycle/SKILL.md +104 -0
- package/plugins/ca-pi/routines/decision-lifecycle/references/adr-template.md +74 -0
- package/plugins/ca-pi/routines/decision-variance/SKILL.md +147 -0
- package/plugins/ca-pi/routines/decompose/SKILL.md +168 -0
- package/plugins/ca-pi/routines/dispatching-parallel-agents/SKILL.md +76 -0
- package/plugins/ca-pi/routines/executing-plans/SKILL.md +83 -0
- package/plugins/ca-pi/routines/finishing-a-development-branch/SKILL.md +91 -0
- package/plugins/ca-pi/routines/post-merge-cleanup/SKILL.md +233 -0
- package/plugins/ca-pi/routines/refactor/SKILL.md +91 -0
- package/plugins/ca-pi/routines/release/SKILL.md +315 -0
- package/plugins/ca-pi/routines/secret-handling/SKILL.md +67 -0
- package/plugins/ca-pi/routines/security-architecture/SKILL.md +63 -0
- package/plugins/ca-pi/routines/skill-author/SKILL.md +108 -0
- package/plugins/ca-pi/routines/skill-author/references/skill-template.md +58 -0
- package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +149 -0
- package/plugins/ca-pi/routines/subagent-driven-development/references/farm-dispatch.md +145 -0
- package/plugins/ca-pi/routines/tdd/SKILL.md +139 -0
- package/plugins/ca-pi/routines/tribunal/SKILL.md +109 -0
- package/plugins/ca-pi/routines/tribunal/references/ai-markers.md +29 -0
- package/plugins/ca-pi/routines/tribunal/references/cost-and-models.md +64 -0
- package/plugins/ca-pi/routines/tribunal/references/finding-record.md +27 -0
- package/plugins/ca-pi/routines/tribunal/references/issue-filing.md +47 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/appsec.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/architecture.md +23 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/coverage.md +20 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/infra.md +24 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/migration.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/observability.md +21 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/performance.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/reliability.md +23 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/secrets-supply.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/test-fidelity.md +24 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/typesafety.md +21 -0
- package/plugins/ca-pi/routines/tribunal/references/report.md +19 -0
- package/plugins/ca-pi/routines/tribunal/references/schemas.md +58 -0
- package/plugins/ca-pi/routines/tribunal/references/telemetry.md +28 -0
- package/plugins/ca-pi/routines/tribunal/references/triage.md +53 -0
- package/plugins/ca-pi/routines/using-git-worktrees/SKILL.md +85 -0
- package/plugins/ca-pi/routines/writing-plans/SKILL.md +129 -0
- package/plugins/ca-pi/routines/writing-plans/references/farm-plan.md +50 -0
- package/plugins/ca-pi/skills/ca-add-dep/SKILL.md +88 -0
- package/plugins/ca-pi/skills/ca-adr/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-adr-status/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-arbiter/SKILL.md +36 -0
- package/plugins/ca-pi/skills/ca-audit/SKILL.md +51 -0
- package/plugins/ca-pi/skills/ca-btw/SKILL.md +23 -0
- package/plugins/ca-pi/skills/ca-checkpoint/SKILL.md +50 -0
- package/plugins/ca-pi/skills/ca-chore/SKILL.md +58 -0
- package/plugins/ca-pi/skills/ca-cleanup/SKILL.md +55 -0
- package/plugins/ca-pi/skills/ca-commands/SKILL.md +21 -0
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +27 -0
- package/plugins/ca-pi/skills/ca-conflict/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-context-check/SKILL.md +32 -0
- package/plugins/ca-pi/skills/ca-create-context/SKILL.md +32 -0
- package/plugins/ca-pi/skills/ca-debug/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-decompose/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-dev/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-doctor/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-feature/SKILL.md +105 -0
- package/plugins/ca-pi/skills/ca-fix/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-init/SKILL.md +56 -0
- package/plugins/ca-pi/skills/ca-metrics/SKILL.md +80 -0
- package/plugins/ca-pi/skills/ca-new-skill/SKILL.md +34 -0
- package/plugins/ca-pi/skills/ca-override/SKILL.md +72 -0
- package/plugins/ca-pi/skills/ca-pr/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-preview/SKILL.md +86 -0
- package/plugins/ca-pi/skills/ca-prune/SKILL.md +100 -0
- package/plugins/ca-pi/skills/ca-reconcile/SKILL.md +43 -0
- package/plugins/ca-pi/skills/ca-refactor/SKILL.md +43 -0
- package/plugins/ca-pi/skills/ca-release/SKILL.md +57 -0
- package/plugins/ca-pi/skills/ca-review/SKILL.md +63 -0
- package/plugins/ca-pi/skills/ca-spike/SKILL.md +41 -0
- package/plugins/ca-pi/skills/ca-sprint/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-standup/SKILL.md +112 -0
- package/plugins/ca-pi/skills/ca-status/SKILL.md +56 -0
- package/plugins/ca-pi/skills/ca-task/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-threat-model/SKILL.md +45 -0
- package/plugins/ca-pi/skills/ca-tribunal/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-watch/SKILL.md +78 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# codeArbiter — preview helpers (issue #81, /ca:preview).
|
|
3
|
+
#
|
|
4
|
+
# A library beside _hooklib.py, NOT a hook: it exposes building blocks the
|
|
5
|
+
# /ca:preview command composes (T-03 here covers diff collection only; the
|
|
6
|
+
# secret scan and test-gap detection are later tasks). Same house style as
|
|
7
|
+
# _hooklib.py: stdlib only (ADR-0004), no network, and every git invocation
|
|
8
|
+
# goes through one capture_output/text/utf-8/timeout subprocess.run wrapper so
|
|
9
|
+
# Windows pipe encoding never surprises us.
|
|
10
|
+
#
|
|
11
|
+
# collect_diff is strictly READ-ONLY: it runs only inspecting git commands
|
|
12
|
+
# (rev-parse, diff --name-only, ls-files) and writes nothing to the worktree,
|
|
13
|
+
# the index, or disk.
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import subprocess
|
|
17
|
+
|
|
18
|
+
from _gitexec import git_executable
|
|
19
|
+
import sys
|
|
20
|
+
from collections import namedtuple
|
|
21
|
+
|
|
22
|
+
# Reuse the ONE secret pattern defined in _hooklib.py — never a second copy, so
|
|
23
|
+
# the preview scan and the commit-time gate (H-10b) can never drift apart.
|
|
24
|
+
# _hooklib sits beside this file; ensure that dir is importable the same way the
|
|
25
|
+
# test harness mounts it (sys.path.insert of the hooks dir), then import the
|
|
26
|
+
# regex by reference.
|
|
27
|
+
_HOOKS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
28
|
+
if _HOOKS_DIR not in sys.path:
|
|
29
|
+
sys.path.insert(0, _HOOKS_DIR)
|
|
30
|
+
from _hooklib import SECRET_RE # noqa: E402 — needs the sys.path mount above
|
|
31
|
+
|
|
32
|
+
# One changed file. `path` is repo-relative with forward slashes (so later
|
|
33
|
+
# content scans and reporting are platform-stable). `kinds` is the set of
|
|
34
|
+
# change categories the file appears under — a file can be both staged and
|
|
35
|
+
# modified-unstaged at once, and we keep all of them rather than collapsing.
|
|
36
|
+
ChangedFile = namedtuple("ChangedFile", ["path", "kinds"])
|
|
37
|
+
|
|
38
|
+
# One secret hit: `path` is the repo-relative (forward-slash) changed file,
|
|
39
|
+
# `line_no` is 1-based, `snippet` is the matching line with the credential
|
|
40
|
+
# VALUE masked (keyword/operator/quoting preserved for context) and trailing
|
|
41
|
+
# whitespace stripped so reporting is stable across CRLF/LF worktrees. The
|
|
42
|
+
# snippet never carries the plaintext secret value.
|
|
43
|
+
SecretFinding = namedtuple("SecretFinding", ["path", "line_no", "snippet"])
|
|
44
|
+
|
|
45
|
+
# Change categories used in ChangedFile.kinds.
|
|
46
|
+
KIND_UNSTAGED = "unstaged" # tracked, differs from HEAD in the worktree
|
|
47
|
+
KIND_STAGED = "staged" # staged in the index vs HEAD
|
|
48
|
+
KIND_UNTRACKED = "untracked" # new file git is not yet tracking
|
|
49
|
+
|
|
50
|
+
# Skip files larger than this before reading them. Kept in lockstep with
|
|
51
|
+
# security-pass.py's MAX_UNTRACKED_BYTES (1_000_000): a blob bigger than this is
|
|
52
|
+
# not reviewable prose, and a preview must not slurp a giant file into memory.
|
|
53
|
+
# Oversize files are skipped exactly like binary/unreadable ones — no raise.
|
|
54
|
+
MAX_SCAN_BYTES = 1_000_000
|
|
55
|
+
|
|
56
|
+
# Replace every complete SECRET_RE match before a snippet leaves scan_secrets.
|
|
57
|
+
# Using the detector itself avoids a narrower redaction regex drifting behind
|
|
58
|
+
# new assignment shapes or high-entropy token prefixes.
|
|
59
|
+
_SECRET_MASK = "****"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _redact_secret(line):
|
|
63
|
+
"""Mask every region recognized by the shared commit-time secret gate."""
|
|
64
|
+
return SECRET_RE.sub(_SECRET_MASK, line)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _git(args, root):
|
|
68
|
+
"""Run a read-only git command in `root`. Returns the CompletedProcess, or
|
|
69
|
+
None if git could not be invoked at all (missing binary, timeout). Never
|
|
70
|
+
raises — callers branch on returncode/None, matching the fail-without-stack
|
|
71
|
+
posture the non-repo edge requires."""
|
|
72
|
+
try:
|
|
73
|
+
return subprocess.run(
|
|
74
|
+
[git_executable()] + args, cwd=root,
|
|
75
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
76
|
+
timeout=15,
|
|
77
|
+
)
|
|
78
|
+
except Exception: # noqa: BLE001 — missing git, timeout, etc.
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _is_repo(root):
|
|
83
|
+
"""True iff `root` is inside a git work tree. Detected by the exit code of
|
|
84
|
+
`git rev-parse --is-inside-work-tree` (non-zero -> not a repo), NOT by
|
|
85
|
+
catching a stack trace from a later command."""
|
|
86
|
+
r = _git(["rev-parse", "--is-inside-work-tree"], root)
|
|
87
|
+
return bool(r) and r.returncode == 0 and r.stdout.strip() == "true"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _name_lines(result):
|
|
91
|
+
"""Non-empty, slash-normalized paths from a `--name-only` style output."""
|
|
92
|
+
if not result or result.returncode != 0:
|
|
93
|
+
return []
|
|
94
|
+
return [
|
|
95
|
+
ln.strip().replace("\\", "/")
|
|
96
|
+
for ln in result.stdout.splitlines()
|
|
97
|
+
if ln.strip()
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def collect_diff(root=None):
|
|
102
|
+
"""Collect the changed files in a working tree, unioned across three kinds.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
root: directory to inspect; defaults to the current working directory.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
dict mapping each changed repo-relative path (forward slashes) to a
|
|
109
|
+
ChangedFile(path, kinds), where `kinds` is the set of categories the
|
|
110
|
+
file falls under (any of KIND_UNSTAGED / KIND_STAGED / KIND_UNTRACKED).
|
|
111
|
+
A file changed in more than one way (e.g. staged AND further modified)
|
|
112
|
+
carries all matching kinds. Empty dict when `root` is not a git repo or
|
|
113
|
+
the tree is clean — never raises for those edges.
|
|
114
|
+
|
|
115
|
+
Read-only: runs only `git rev-parse`, `git diff --name-only`, and
|
|
116
|
+
`git ls-files`; writes nothing.
|
|
117
|
+
"""
|
|
118
|
+
if root is None:
|
|
119
|
+
root = os.getcwd()
|
|
120
|
+
|
|
121
|
+
if not _is_repo(root):
|
|
122
|
+
return {}
|
|
123
|
+
|
|
124
|
+
sources = (
|
|
125
|
+
# tracked changes vs HEAD, present in the worktree but not staged
|
|
126
|
+
(KIND_UNSTAGED, ["diff", "--name-only", "HEAD"]),
|
|
127
|
+
# tracked changes staged in the index vs HEAD
|
|
128
|
+
(KIND_STAGED, ["diff", "--cached", "--name-only"]),
|
|
129
|
+
# files git is not tracking yet, honouring .gitignore
|
|
130
|
+
(KIND_UNTRACKED, ["ls-files", "--others", "--exclude-standard"]),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
found = {}
|
|
134
|
+
for kind, args in sources:
|
|
135
|
+
for path in _name_lines(_git(args, root)):
|
|
136
|
+
entry = found.get(path)
|
|
137
|
+
if entry is None:
|
|
138
|
+
found[path] = ChangedFile(path=path, kinds={kind})
|
|
139
|
+
else:
|
|
140
|
+
entry.kinds.add(kind)
|
|
141
|
+
return found
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def scan_secrets(root=None):
|
|
145
|
+
"""Scan every changed file's current worktree content for credential lines.
|
|
146
|
+
|
|
147
|
+
For each path from collect_diff(root), read the file as text (read-only) and
|
|
148
|
+
report every line matching the shared _hooklib.SECRET_RE.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
root: directory to inspect; defaults to the current working directory.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
list of SecretFinding(path, line_no, snippet). `path` is repo-relative
|
|
155
|
+
with forward slashes (as collect_diff yields it); `line_no` is 1-based.
|
|
156
|
+
Empty list when nothing matches, when `root` is not a repo, or when the
|
|
157
|
+
tree is clean.
|
|
158
|
+
|
|
159
|
+
Strictly READ-ONLY: opens files for reading only and writes nothing. Files
|
|
160
|
+
that cannot be read as text (binary, deleted, permission-denied) are skipped
|
|
161
|
+
silently rather than raising — a preview must never crash on the worktree it
|
|
162
|
+
is merely describing.
|
|
163
|
+
"""
|
|
164
|
+
if root is None:
|
|
165
|
+
root = os.getcwd()
|
|
166
|
+
|
|
167
|
+
findings = []
|
|
168
|
+
for path in collect_diff(root):
|
|
169
|
+
abspath = os.path.join(root, path)
|
|
170
|
+
try:
|
|
171
|
+
# Size cap first, so an oversize blob is skipped before we read it
|
|
172
|
+
# (consistent with security-pass.py's MAX_UNTRACKED_BYTES). Treated
|
|
173
|
+
# like binary/unreadable files: skipped, never raised.
|
|
174
|
+
if os.path.getsize(abspath) > MAX_SCAN_BYTES:
|
|
175
|
+
continue
|
|
176
|
+
with open(abspath, "r", encoding="utf-8", errors="strict") as f:
|
|
177
|
+
lines = f.read().splitlines()
|
|
178
|
+
except (OSError, UnicodeDecodeError):
|
|
179
|
+
# Deleted/unreadable (OSError) or binary/non-UTF-8 (UnicodeDecodeError):
|
|
180
|
+
# skip without raising. errors="strict" makes binary blobs raise here
|
|
181
|
+
# rather than yielding mojibake we might then false-positive on.
|
|
182
|
+
continue
|
|
183
|
+
for i, line in enumerate(lines, start=1):
|
|
184
|
+
if SECRET_RE.search(line):
|
|
185
|
+
# Mask the credential value before it leaves the function: the
|
|
186
|
+
# snippet keeps keyword/quoting context but never the plaintext.
|
|
187
|
+
findings.append(
|
|
188
|
+
SecretFinding(
|
|
189
|
+
path=path, line_no=i,
|
|
190
|
+
snippet=_redact_secret(line).rstrip(),
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
return findings
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# codeArbiter - the protected-path classifiers: which repo paths are append-only
|
|
3
|
+
# audit logs (H-05), ADR decision files (H-11), the activation manifest, and the
|
|
4
|
+
# gate-marker directory.
|
|
5
|
+
#
|
|
6
|
+
# Extracted from _hooklib (issue #321, architecture-002) as slice 2. Measured the
|
|
7
|
+
# same way slice 1 was: the cluster referenced exactly ONE symbol from the rest
|
|
8
|
+
# of _hooklib (repo_rel, which moved to the _pathnorm floor because it
|
|
9
|
+
# references no module symbol at all and the remaining slices need it too), and
|
|
10
|
+
# NOTHING in the rest of _hooklib referenced the cluster. A one-way edge with no
|
|
11
|
+
# back-reference is what makes a slice safe to move without touching a consumer.
|
|
12
|
+
#
|
|
13
|
+
# WHY THESE BELONG TOGETHER: they answer one question - "what class of protected
|
|
14
|
+
# thing is this path?" - and the guards branch on the answer as a set.
|
|
15
|
+
# classify_protected returns EVERY class a path hits, which is load-bearing:
|
|
16
|
+
# #528/#529 showed that adding decision-log.md to the audit set while leaving it
|
|
17
|
+
# in the decisions set still blocked, because pre-write.py checks the classes
|
|
18
|
+
# independently. Splitting these across modules would let one classification
|
|
19
|
+
# move without the others and reopen exactly that.
|
|
20
|
+
#
|
|
21
|
+
# _hooklib re-exports every name below, so no consumer changed and the
|
|
22
|
+
# pre-existing hook suites prove parity without moving.
|
|
23
|
+
#
|
|
24
|
+
# T-05a (#564): a FIFTH class, "state", joins the four above — a path
|
|
25
|
+
# registered in the protected-state registry (_protectedstatelib.py, B1: a
|
|
26
|
+
# per-entry policy of marker-gated/helper-only/append-only). It is evaluated
|
|
27
|
+
# strictly AFTER the four legacy classes (a documented ordering, not just an
|
|
28
|
+
# implementation detail — see classify_protected's own docstring) and
|
|
29
|
+
# changes nothing about the RETURN CONTRACT: still a bare set of class-name
|
|
30
|
+
# strings, exactly as before. Resolving the registered path's POLICY is left
|
|
31
|
+
# entirely to whichever flank branch consumes the "state" tag
|
|
32
|
+
# (_protectedstatelib.lookup_policy / resolve_registered_path) — never done
|
|
33
|
+
# here, so this module gains no dependency on a POLICY concept it has no
|
|
34
|
+
# other reason to know about, and the four pre-existing consumers of
|
|
35
|
+
# classify_protected see zero contract change.
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import re
|
|
40
|
+
|
|
41
|
+
from _pathnorm import norm_path, raw_repo_rel, repo_rel
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Append-only audit logs (H-05) and ADR-decisions paths (H-11) — centralized
|
|
45
|
+
# here (architecture-004) so the three pre-* hooks import ONE definition instead
|
|
46
|
+
# of re-encoding the regex inline (the exact drift this module exists to
|
|
47
|
+
# prevent: adding sprint-log.md once meant hand-editing every copy). Same home,
|
|
48
|
+
# same rationale, as CRYPTO_RE/SECRET_RE/MIGRATION_DEFAULT_GLOBS.
|
|
49
|
+
#
|
|
50
|
+
# AUDIT_LOG_NAMES is the bare filename alternation; pre-bash.py composes its
|
|
51
|
+
# shell LOG_NAMES from it, and AUDIT_LOG_RE anchors it under .codearbiter/ for
|
|
52
|
+
# the Write/Edit file-path guards. DECISIONS_DIR_RE is the separator-tolerant
|
|
53
|
+
# decisions directory token; pre-bash.py composes its shell DECISIONS from it,
|
|
54
|
+
# and DECISIONS_PATH_RE extends it to a full ADR file path. `[\\/]+` matches the
|
|
55
|
+
# norm_path'd `/` as well as a raw backslash, so both the file-path and shell
|
|
56
|
+
# flanks derive from one source.
|
|
57
|
+
#
|
|
58
|
+
# gate-events.log (observability-001, #186) joins this set: it is the durable,
|
|
59
|
+
# mechanical BLOCK/REMIND/WARN sink block()/remind()/warn() append to below —
|
|
60
|
+
# an append-only audit artifact exactly like the other three, so it gets the
|
|
61
|
+
# SAME H-05 tool-call protection (Write/Edit + shell) for free via this one
|
|
62
|
+
# alternation, with no separate guard to maintain. Note this protects it only
|
|
63
|
+
# from Write/Edit/Bash TOOL CALLS; the hooks' own os-level `open(..., "a")`
|
|
64
|
+
# append (below) is plain file I/O, never a tool call, so H-05 never gates it.
|
|
65
|
+
#
|
|
66
|
+
# AUDIT_LOG_BASENAMES is the single authoritative list of bare filenames — the
|
|
67
|
+
# ONE place a new audit log gets added. pre-bash.py's H-05 shell guard needs
|
|
68
|
+
# these as plain strings too (a cheap `n in cmd` substring pre-filter before
|
|
69
|
+
# running the regexes below), so it imports this tuple directly instead of
|
|
70
|
+
# re-deriving/hand-copying the name set (the exact drift this centralization
|
|
71
|
+
# exists to prevent — a filter that silently skips a future audit log because
|
|
72
|
+
# its literal name was never added to a second, hand-maintained copy).
|
|
73
|
+
# AUDIT_LOG_NAMES is built FROM this tuple (re.escape'd, alternated) — behavior
|
|
74
|
+
# is unchanged from the prior hand-written pattern (same four literal
|
|
75
|
+
# filenames, same (?:...) grouping), only the source of truth moved.
|
|
76
|
+
AUDIT_LOG_FLAT_BASENAMES = ("overrides.log", "triage.log", "gate-events.log", "sprint-log.md")
|
|
77
|
+
# #528: the SMARTS arbitration log is an append-only audit artifact that happens
|
|
78
|
+
# to sit under decisions/ for filing reasons. It is NOT an ADR, and governing it
|
|
79
|
+
# as one was a live deadlock: `decision-variance` Phase 4 is REQUIRED to append
|
|
80
|
+
# to it, H-11 refused every write without the /adr authoring marker, and only
|
|
81
|
+
# decision-lifecycle arms that marker. So a SMARTS arbitration outside an /adr
|
|
82
|
+
# session made a decision it could not record. Its own format doc states H-05's
|
|
83
|
+
# rule verbatim — "strictly append-only … to supersede, append a new entry" — so
|
|
84
|
+
# H-05 is the correct guard: append freely, never rewrite.
|
|
85
|
+
#
|
|
86
|
+
# It is listed separately from AUDIT_LOG_BASENAMES because those are anchored
|
|
87
|
+
# directly under .codearbiter/ and this one is nested a level deeper. Both halves
|
|
88
|
+
# of the reclassification are load-bearing: adding it here WITHOUT removing it
|
|
89
|
+
# from the H-11 set below leaves the append blocked, because classify_protected
|
|
90
|
+
# reports every class a path hits and pre-write checks them independently.
|
|
91
|
+
DECISION_LOG_BASENAME = "decision-log.md"
|
|
92
|
+
DECISION_LOG_RE = re.compile(
|
|
93
|
+
r"\.codearbiter[\\/]+decisions[\\/]+" + re.escape(DECISION_LOG_BASENAME) + r"$"
|
|
94
|
+
)
|
|
95
|
+
# AUDIT_LOG_BASENAMES stays the SINGLE AUTHORITATIVE BASENAME LIST, and the
|
|
96
|
+
# arbitration log is in it. _bashguardlib's H-05 shell check pre-filters with
|
|
97
|
+
# `any(n in cmd for n in AUDIT_LOG_BASENAMES)` precisely so a newly added audit
|
|
98
|
+
# log cannot silently skip the shell flank — adding the name only to the regex
|
|
99
|
+
# alternation below would sail past that pre-filter and leave the log deletable
|
|
100
|
+
# from the shell. (Caught by test_hook_guards.py, which the comment on that
|
|
101
|
+
# pre-filter predicted verbatim.)
|
|
102
|
+
AUDIT_LOG_BASENAMES = AUDIT_LOG_FLAT_BASENAMES + (DECISION_LOG_BASENAME,)
|
|
103
|
+
AUDIT_LOG_NAMES = "(?:" + "|".join(re.escape(n) for n in AUDIT_LOG_BASENAMES) + ")"
|
|
104
|
+
# The path anchor stays scoped to the FLAT logs — those sit directly under
|
|
105
|
+
# .codearbiter/, the arbitration log one level deeper — so is_audit_log() tests
|
|
106
|
+
# both patterns rather than loosening this one into matching any nesting.
|
|
107
|
+
AUDIT_LOG_RE = re.compile(
|
|
108
|
+
r"\.codearbiter/" + "(?:" + "|".join(re.escape(n) for n in AUDIT_LOG_FLAT_BASENAMES) + ")" + r"$"
|
|
109
|
+
)
|
|
110
|
+
DECISIONS_DIR_RE = r"\.codearbiter[\\/]+decisions"
|
|
111
|
+
DECISIONS_PATH_RE = re.compile(DECISIONS_DIR_RE + r"[\\/]+.+\.md$")
|
|
112
|
+
|
|
113
|
+
# The activation file (#159) and the gate-marker store (#160). CONTEXT.md is the
|
|
114
|
+
# master switch every hook gates on via arbiter_active(); .markers/ holds the
|
|
115
|
+
# gate-pass tokens (security-gate-passed, migration-gate-passed,
|
|
116
|
+
# adr-authoring-active). Both were writable project state with no Write/Edit
|
|
117
|
+
# guard — the token strings are centralized here beside the audit-log/decisions
|
|
118
|
+
# sets so the pre-* hooks import ONE definition (same anti-drift rationale).
|
|
119
|
+
CONTEXT_MD_RE = re.compile(r"\.codearbiter/CONTEXT\.md$")
|
|
120
|
+
MARKERS_RE = re.compile(r"\.codearbiter/\.markers(?:/|$)")
|
|
121
|
+
# The two load-bearing gate-pass markers a commit gate consumes (H-09b/H-10b,
|
|
122
|
+
# H-14). Their bare filenames feed pre-bash.py's shell flank — these are NEVER
|
|
123
|
+
# legitimately shell-written (the sanctioned producers are the python
|
|
124
|
+
# security-pass.py / migration-pass.py helpers), unlike adr-authoring-active
|
|
125
|
+
# which /adr legitimately `touch`es.
|
|
126
|
+
#
|
|
127
|
+
# T-08a / #564: this set enumerates markers whose PRESENCE converts a
|
|
128
|
+
# hard-gate BLOCK into an ALLOW (H-09b/H-10b's security-gate-passed, H-14's
|
|
129
|
+
# migration-gate-passed) — that conversion is the membership criterion, not
|
|
130
|
+
# "is this a marker under .markers/". A protected-state AUTHORING marker
|
|
131
|
+
# (e.g. release-targets-authoring, and the `<stem>-authoring` shape every
|
|
132
|
+
# future marker-gated consumer follows — _protectedstatelib.marker_name_for)
|
|
133
|
+
# does NOT belong here even though it also gates a write: it authorizes
|
|
134
|
+
# nothing on its own, the content it gates still passes through ordinary
|
|
135
|
+
# review, and it is deliberately self-mintable by an agent (a plain `touch`)
|
|
136
|
+
# under ADR-0010 — same as adr-authoring-active above. Its purpose is audit
|
|
137
|
+
# friction, not authorization.
|
|
138
|
+
#
|
|
139
|
+
# The risk here runs opposite to intuition: a future "hardening" pass that
|
|
140
|
+
# widens this blocklist to cover every registered marker would brick every
|
|
141
|
+
# legitimate authoring lane (a marker an agent can never touch admits
|
|
142
|
+
# nothing, ever) while stopping no non-cooperative agent (who can bypass
|
|
143
|
+
# this guard's own enforcement the same way they could bypass any other
|
|
144
|
+
# lexical shell check). See test_marker_touch_allowed (T-08a) for the pin.
|
|
145
|
+
GATE_MARKER_NAMES = r"(?:security-gate-passed|migration-gate-passed)"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def is_audit_log(rel):
|
|
149
|
+
"""True iff `rel` is one of the append-only .codearbiter audit logs
|
|
150
|
+
(overrides.log, triage.log, sprint-log.md, gate-events.log) or the SMARTS
|
|
151
|
+
arbitration log decisions/decision-log.md (#528) — the H-05 guard set."""
|
|
152
|
+
n = norm_path(rel)
|
|
153
|
+
return bool(AUDIT_LOG_RE.search(n) or DECISION_LOG_RE.search(n))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def is_tail_append(current, old, new):
|
|
157
|
+
"""True iff an Edit's (old_string, new_string) pair is a verifiable,
|
|
158
|
+
TAIL-ANCHORED pure append against `current` (the file's REAL on-disk
|
|
159
|
+
content) — the H-05 guard (reliability-003, #172).
|
|
160
|
+
|
|
161
|
+
`new.startswith(old)` alone is not sufficient: `old` could be any interior
|
|
162
|
+
line that happens to be a prefix of `new`, which inserts content BETWEEN
|
|
163
|
+
existing lines rather than appending at the end. This requires TWO things:
|
|
164
|
+
`current` must literally END with `old` (old_string is the file's actual
|
|
165
|
+
trailing content, not just some substring elsewhere), and `new` must
|
|
166
|
+
extend `old`. An empty `old` is never a valid append — every string
|
|
167
|
+
"ends with" the empty string, so the tail-anchor check would trivially
|
|
168
|
+
pass and reopen the migration-003 empty-old_string hole this closes.
|
|
169
|
+
|
|
170
|
+
`old` must also occur EXACTLY ONCE in `current`: a non-unique old_string
|
|
171
|
+
that happens to also match the tail is not self-evidently an append — this
|
|
172
|
+
keeps the guard correct on its own terms rather than depending on the Edit
|
|
173
|
+
tool's own (client-side, not re-verified here) uniqueness enforcement for
|
|
174
|
+
a non-replace_all Edit."""
|
|
175
|
+
if not old:
|
|
176
|
+
return False
|
|
177
|
+
if current.count(old) != 1:
|
|
178
|
+
return False
|
|
179
|
+
return current.endswith(old) and new.startswith(old)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def is_decisions_path(rel):
|
|
183
|
+
"""True iff `rel` is a `.md` ADR anywhere under .codearbiter/decisions/ —
|
|
184
|
+
the H-11 guard set (a non-numbered draft or a nested path still counts).
|
|
185
|
+
|
|
186
|
+
decisions/decision-log.md is the ONE exception (#528): it is the append-only
|
|
187
|
+
arbitration log, not immutable ADR history, and is governed by H-05 instead.
|
|
188
|
+
The carve-out is exactly one path wide and anchored — `old-decision-log.md`
|
|
189
|
+
and a nested `sub/decision-log.md` remain ADRs, so a near-miss filename
|
|
190
|
+
cannot launder itself out of the marker gate. (`decision-log.md.bak` is in
|
|
191
|
+
NEITHER set: it does not end in `.md`, so it was never an H-11 path either.)"""
|
|
192
|
+
n = norm_path(rel)
|
|
193
|
+
if DECISION_LOG_RE.search(n):
|
|
194
|
+
return False
|
|
195
|
+
return bool(DECISIONS_PATH_RE.search(n))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def is_context_md(rel):
|
|
199
|
+
"""True iff `rel` is the .codearbiter/CONTEXT.md activation file (#159) —
|
|
200
|
+
the master switch arbiter_active() reads. Guarded so it can't be flipped to
|
|
201
|
+
`arbiter: disabled` (or corrupted) to make every enforcement hook dormant."""
|
|
202
|
+
return bool(CONTEXT_MD_RE.search(norm_path(rel)))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def is_marker_path(rel):
|
|
206
|
+
"""True iff `rel` is anywhere under .codearbiter/.markers/ (#160) — the
|
|
207
|
+
gate-pass token store. Load-bearing markers turn a BLOCK into an allow, so a
|
|
208
|
+
hand-written marker must not be admitted by the Write/Edit tools."""
|
|
209
|
+
return bool(MARKERS_RE.search(norm_path(rel)))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def is_registered_state_path(rel):
|
|
213
|
+
"""True iff `rel` carries a policy in the protected-state registry
|
|
214
|
+
(`_protectedstatelib.lookup_policy`, B1/#564) — the fifth protected class
|
|
215
|
+
`classify_protected` recognizes (T-05a), evaluated strictly AFTER the
|
|
216
|
+
other four.
|
|
217
|
+
|
|
218
|
+
The `_protectedstatelib` import is deliberately LAZY — done here, inside
|
|
219
|
+
the function body, rather than at this module's top level.
|
|
220
|
+
`_protectedstatelib` imports `marker_fresh` from `_hooklib`, and
|
|
221
|
+
`_hooklib` imports `classify_protected` (and every other name in this
|
|
222
|
+
file) FROM here — a module-level `import _protectedstatelib` at the top
|
|
223
|
+
of `_protectedlib.py` would be a genuine three-module import cycle
|
|
224
|
+
(`_protectedlib` -> `_protectedstatelib` -> `_hooklib` -> `_protectedlib`)
|
|
225
|
+
that Python cannot resolve at module-load time. Deferring the import to
|
|
226
|
+
CALL time breaks the cycle without restructuring the dependency graph:
|
|
227
|
+
every real caller reaches this function only after `_hooklib` (and
|
|
228
|
+
therefore this module) has already finished its own top-level execution,
|
|
229
|
+
so by the time the deferred import statement actually runs,
|
|
230
|
+
`_protectedstatelib`'s own `from _hooklib import marker_fresh` resolves
|
|
231
|
+
against an already-complete `_hooklib` module.
|
|
232
|
+
|
|
233
|
+
Correction (finding F11, #564 follow-up): this is NOT the same pattern
|
|
234
|
+
`_babysitlib.babysit_config` or `statusline.py` use — neither of those
|
|
235
|
+
actually breaks an import CYCLE. `statusline.py`'s function-level
|
|
236
|
+
imports are a startup-latency lazy-load with no cyclic partner at all,
|
|
237
|
+
and `_babysitlib.py`'s deferred import sits inside a lazy default
|
|
238
|
+
injection where `_hooklib` never imports `_babysitlib` back (no cycle
|
|
239
|
+
to break). The deferral here IS still correct and necessary — it is
|
|
240
|
+
the only thing that breaks the genuine three-module cycle described
|
|
241
|
+
above — the prior comment simply cited the wrong precedent; corrected
|
|
242
|
+
so the comment matches the record.
|
|
243
|
+
|
|
244
|
+
It is a local import, not file I/O or a git call, so it does not violate
|
|
245
|
+
this module's zero-side-effects-at-import invariant."""
|
|
246
|
+
import _protectedstatelib
|
|
247
|
+
return _protectedstatelib.lookup_policy(rel) is not None
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def classify_protected(fpath, root):
|
|
251
|
+
"""The set of protected classes a Write/Edit `fpath` targets, resolving
|
|
252
|
+
symlinks (#162). Each classifier runs against BOTH the raw normalized path
|
|
253
|
+
AND the realpath-resolved repo-relative form: a symlink alias whose visible
|
|
254
|
+
path lacks `.codearbiter/` still realpaths back inside the repo, so an alias
|
|
255
|
+
can no longer launder a write past the guard. Centralized so pre-write.py and
|
|
256
|
+
pre-edit.py apply the identical symlink-safe check to every class (H-05,
|
|
257
|
+
H-11, #159 CONTEXT.md, #160 markers, #564 protected-state) instead of
|
|
258
|
+
re-encoding it twice.
|
|
259
|
+
|
|
260
|
+
Classes: "audit", "decisions", "context", "marker", "state". repo_rel()
|
|
261
|
+
returns "" for a target outside the repo (which cannot be a
|
|
262
|
+
`.codearbiter` path), so that flank is simply skipped.
|
|
263
|
+
|
|
264
|
+
"state" (T-05a, #564) is resolved in a SEPARATE loop below, strictly
|
|
265
|
+
AFTER the four legacy classes above — not interleaved into their loop.
|
|
266
|
+
That split is about incremental extension safety (a class added later
|
|
267
|
+
stays visibly separate from the four it was added beside, per the
|
|
268
|
+
module-header rationale above), NOT about the hits it produces:
|
|
269
|
+
`hits` is a `set`, so the SECOND loop's position relative to the first
|
|
270
|
+
has no observable effect on `classify_protected`'s own return value —
|
|
271
|
+
no test (mutant or otherwise) can distinguish "state resolved in loop 2"
|
|
272
|
+
from "state resolved inside loop 1" from the returned set alone
|
|
273
|
+
(finding F12, #564 follow-up correcting an earlier, overselling version
|
|
274
|
+
of this paragraph). The ordering that DOES matter or a real class/tag
|
|
275
|
+
conflict lives elsewhere and IS enforced in code: the textual branch
|
|
276
|
+
order of the `if "..." in classes:` checks in pre-write.py/pre-edit.py,
|
|
277
|
+
and pre-edit.py's own `_CLASS_TAG` priority tuple, which decide which
|
|
278
|
+
ONE message a caller emits when a path hits more than one class.
|
|
279
|
+
|
|
280
|
+
The "state" loop tries BOTH a raw (symlink-unresolved) repo-relative
|
|
281
|
+
form (`raw_repo_rel`) and the realpath-resolved repo-relative form
|
|
282
|
+
(`repo_rel`) — the SAME #162 two-form symlink-safety property the four
|
|
283
|
+
legacy classes get for free from `.search()`-based regex matching over
|
|
284
|
+
the raw path text, restored here explicitly (finding F3) because an
|
|
285
|
+
EQUALITY-based lookup (`_protectedstatelib.lookup_policy`) does not get
|
|
286
|
+
that coverage for free from a bare `norm_path(fpath)` — see
|
|
287
|
+
`_protectedstatelib.resolve_registered_path`'s docstring for the full
|
|
288
|
+
account of the symlink-inversion bug this closes. The return contract is
|
|
289
|
+
unchanged by this addition: still a bare set of strings. A caller that
|
|
290
|
+
needs the registered path's POLICY (marker-gated/helper-only/
|
|
291
|
+
append-only) resolves it separately, via `_protectedstatelib` — this
|
|
292
|
+
function only ever reports CLASS membership, exactly as it always has,
|
|
293
|
+
so its four pre-existing consumers see no contract change."""
|
|
294
|
+
hits = set()
|
|
295
|
+
for p in (norm_path(fpath), repo_rel(fpath, root)):
|
|
296
|
+
if not p:
|
|
297
|
+
continue
|
|
298
|
+
if is_audit_log(p):
|
|
299
|
+
hits.add("audit")
|
|
300
|
+
if is_decisions_path(p):
|
|
301
|
+
hits.add("decisions")
|
|
302
|
+
if is_context_md(p):
|
|
303
|
+
hits.add("context")
|
|
304
|
+
if is_marker_path(p):
|
|
305
|
+
hits.add("marker")
|
|
306
|
+
for p in (raw_repo_rel(fpath, root), repo_rel(fpath, root)):
|
|
307
|
+
if not p:
|
|
308
|
+
continue
|
|
309
|
+
if is_registered_state_path(p):
|
|
310
|
+
hits.add("state")
|
|
311
|
+
break
|
|
312
|
+
return hits
|