agent-code-guard 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {agent_code_guard-0.2.0/src/agent_code_guard.egg-info → agent_code_guard-0.3.0}/PKG-INFO +38 -14
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/README.md +37 -13
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/pyproject.toml +1 -1
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/SKILL.md +13 -10
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/references/loc-policy.md +9 -0
- agent_code_guard-0.3.0/src/agent_code_guard/analysis/csharp_compat.py +115 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/errors.py +4 -0
- agent_code_guard-0.3.0/src/agent_code_guard/analysis/pipeline.py +86 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/provider.py +5 -3
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/regions.py +9 -3
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/code_guard.py +63 -5
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/config_validation.py +1 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/guards/loc.py +19 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/human_output.py +19 -4
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/loc_baseline.py +6 -4
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0/src/agent_code_guard.egg-info}/PKG-INFO +38 -14
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard.egg-info/SOURCES.txt +1 -0
- agent_code_guard-0.2.0/src/agent_code_guard/analysis/pipeline.py +0 -37
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/LICENSE +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/MANIFEST.in +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/setup.cfg +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/LICENSE.txt +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/agents/openai.yaml +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/references/callable-size-policy.md +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/references/complexity-policy.md +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/references/markdown-size-policy.md +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/references/nesting-policy.md +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/__init__.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/__init__.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/adapters.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/branch_normalization.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/callable_identity.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/facts.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/language_specs.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/syntax_nodes.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/doctor.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/file_selection.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/guards/__init__.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/guards/callable_size.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/guards/complexity.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/guards/markdown_document_size.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/guards/markdown_section_size.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/guards/nesting.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/markdown/__init__.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/markdown/facts.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/markdown/scanner.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/path_matching.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/reporting.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/result_model.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/skill_distribution.py +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard.egg-info/dependency_links.txt +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard.egg-info/entry_points.txt +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard.egg-info/requires.txt +0 -0
- {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-code-guard
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Deterministic cross-language guardrails for agent-assisted development
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Source, https://github.com/stef-k/agent-code-guard
|
|
@@ -82,16 +82,28 @@ environment, uv, and developer alternatives.
|
|
|
82
82
|
|
|
83
83
|
Copy this prompt to a coding agent:
|
|
84
84
|
|
|
85
|
-
>
|
|
86
|
-
>
|
|
87
|
-
>
|
|
88
|
-
>
|
|
89
|
-
>
|
|
90
|
-
>
|
|
91
|
-
>
|
|
92
|
-
>
|
|
93
|
-
>
|
|
94
|
-
>
|
|
85
|
+
> Adopt Agent Code Guard for this repository using the official project and
|
|
86
|
+
> documentation at https://github.com/stef-k/agent-code-guard. This prompt
|
|
87
|
+
> authorizes installing the published distribution in an isolated supported
|
|
88
|
+
> way, preferably with `pipx install agent-code-guard`, and persistently
|
|
89
|
+
> activating or exporting its installed, version-matched skill for the current
|
|
90
|
+
> agent environment. Verify `code-guard --version` and run `code-guard doctor`,
|
|
91
|
+
> then locate the skill with `code-guard --skill-path`. Follow the
|
|
92
|
+
> [skill activation guide](https://github.com/stef-k/agent-code-guard/blob/main/docs/skill-distribution.md):
|
|
93
|
+
> activate that installed path when the platform supports it, or export it with
|
|
94
|
+
> `code-guard --export-skill <target-directory>` to the platform's documented
|
|
95
|
+
> skill directory. Do not guess a target or overwrite an existing export; verify
|
|
96
|
+
> that `.agent-code-guard-version` matches the installed distribution. Follow the
|
|
97
|
+
> [agent workflow](https://github.com/stef-k/agent-code-guard/blob/main/docs/agent-workflow.md)
|
|
98
|
+
> without creating a LOC baseline. After meaningful source or Markdown edits, run
|
|
99
|
+
> `code-guard . --changed-only --json --json-mode compact`; outside Git, pass the
|
|
100
|
+
> exact edited files. Inspect REVIEW and FAIL findings, make only genuine
|
|
101
|
+
> improvements, rerun the check, and report the result. Never weaken thresholds,
|
|
102
|
+
> exclusions, configuration, or baselines merely to silence findings. Code Guard
|
|
103
|
+
> does not install or enable hooks by default. Ask before configuring a post-edit
|
|
104
|
+
> or post-turn hook, or making unrelated persistent agent or platform changes.
|
|
105
|
+
> Finish by reporting the installed version, doctor status, activated skill
|
|
106
|
+
> location, first analysis result, and any REVIEW findings you accepted.
|
|
95
107
|
|
|
96
108
|
## Five-minute start
|
|
97
109
|
|
|
@@ -138,12 +150,14 @@ hook-assisted process.
|
|
|
138
150
|
warranted; normally exit `1`.
|
|
139
151
|
- **Completed FAIL** — blocks normal completion until fixed or an explicitly
|
|
140
152
|
authorized exception applies; exit `2` with a completed report on stdout.
|
|
153
|
+
- **INCOMPLETE** — one or more requested syntax analyses are unavailable;
|
|
154
|
+
independent completed findings remain authoritative, but the run exits `3`.
|
|
141
155
|
- An argparse usage or invalid-choice error exits `2`, writes usage/error text
|
|
142
156
|
to stderr, and produces no completed report.
|
|
143
|
-
- Other Code Guard tool, configuration, scope, or
|
|
157
|
+
- Other Code Guard tool, configuration, scope, or unexpected errors exit `3`.
|
|
144
158
|
|
|
145
159
|
`--ci` makes REVIEW nonblocking at the process level by changing its exit to
|
|
146
|
-
`0`; it does not hide
|
|
160
|
+
`0`; it does not hide findings or change FAIL, INCOMPLETE, and tool-error exits.
|
|
147
161
|
|
|
148
162
|
**Never game a metric.** Do not create artificial helpers, files,
|
|
149
163
|
abstractions, formatting, exclusions, or policy changes merely to lower a
|
|
@@ -175,6 +189,12 @@ result, scope, required policies, guards, ordering, and actionable findings.
|
|
|
175
189
|
Named modes require `--json`. See [Usage](https://github.com/stef-k/agent-code-guard/blob/main/docs/usage.md) for the schema and
|
|
176
190
|
option contract.
|
|
177
191
|
|
|
192
|
+
Known per-file syntax or provider failures instead produce `overall:
|
|
193
|
+
"incomplete"`, ordered structured `unavailable` records, and completed guard
|
|
194
|
+
evidence. Syntax guards identify unavailable paths; LOC and Markdown remain
|
|
195
|
+
complete. Full, debug, and compact JSON retain unavailable records unchanged,
|
|
196
|
+
and completed runs retain their existing schema.
|
|
197
|
+
|
|
178
198
|
### Common scope commands
|
|
179
199
|
|
|
180
200
|
```bash
|
|
@@ -200,7 +220,8 @@ Markdown guards apply to `.md` files.
|
|
|
200
220
|
Generic `.h` files are not syntax-dispatched; `.markdown` is not enabled; Vue
|
|
201
221
|
template and style regions are not executable syntax input; and unsupported
|
|
202
222
|
artifacts are inapplicable. Malformed applicable syntax or a required provider
|
|
203
|
-
failure is
|
|
223
|
+
failure is unavailable evidence, distinct from both an inapplicable file and a
|
|
224
|
+
completed FAIL finding. See [Language support](https://github.com/stef-k/agent-code-guard/blob/main/docs/language-support.md).
|
|
204
225
|
|
|
205
226
|
### Skill integration
|
|
206
227
|
|
|
@@ -221,6 +242,9 @@ Built-in defaults require no configuration. Configure a project only for a
|
|
|
221
242
|
concrete policy reason; see the [configuration guide](https://github.com/stef-k/agent-code-guard/blob/main/docs/configuration.md).
|
|
222
243
|
The LOC baseline is an explicit adoption tool for established legacy
|
|
223
244
|
repositories, not an ordinary-use requirement or a way to silence findings.
|
|
245
|
+
Its default `ratchetAt: "fail"` policy freezes files above `failAt`; choose
|
|
246
|
+
`ratchetAt: "review"` only when every established file above `warnAt` must be
|
|
247
|
+
non-increasing while ordinary findings in that range remain REVIEW.
|
|
224
248
|
|
|
225
249
|
## Trust, CI, and platform support
|
|
226
250
|
|
|
@@ -68,16 +68,28 @@ environment, uv, and developer alternatives.
|
|
|
68
68
|
|
|
69
69
|
Copy this prompt to a coding agent:
|
|
70
70
|
|
|
71
|
-
>
|
|
72
|
-
>
|
|
73
|
-
>
|
|
74
|
-
>
|
|
75
|
-
>
|
|
76
|
-
>
|
|
77
|
-
>
|
|
78
|
-
>
|
|
79
|
-
>
|
|
80
|
-
>
|
|
71
|
+
> Adopt Agent Code Guard for this repository using the official project and
|
|
72
|
+
> documentation at https://github.com/stef-k/agent-code-guard. This prompt
|
|
73
|
+
> authorizes installing the published distribution in an isolated supported
|
|
74
|
+
> way, preferably with `pipx install agent-code-guard`, and persistently
|
|
75
|
+
> activating or exporting its installed, version-matched skill for the current
|
|
76
|
+
> agent environment. Verify `code-guard --version` and run `code-guard doctor`,
|
|
77
|
+
> then locate the skill with `code-guard --skill-path`. Follow the
|
|
78
|
+
> [skill activation guide](https://github.com/stef-k/agent-code-guard/blob/main/docs/skill-distribution.md):
|
|
79
|
+
> activate that installed path when the platform supports it, or export it with
|
|
80
|
+
> `code-guard --export-skill <target-directory>` to the platform's documented
|
|
81
|
+
> skill directory. Do not guess a target or overwrite an existing export; verify
|
|
82
|
+
> that `.agent-code-guard-version` matches the installed distribution. Follow the
|
|
83
|
+
> [agent workflow](https://github.com/stef-k/agent-code-guard/blob/main/docs/agent-workflow.md)
|
|
84
|
+
> without creating a LOC baseline. After meaningful source or Markdown edits, run
|
|
85
|
+
> `code-guard . --changed-only --json --json-mode compact`; outside Git, pass the
|
|
86
|
+
> exact edited files. Inspect REVIEW and FAIL findings, make only genuine
|
|
87
|
+
> improvements, rerun the check, and report the result. Never weaken thresholds,
|
|
88
|
+
> exclusions, configuration, or baselines merely to silence findings. Code Guard
|
|
89
|
+
> does not install or enable hooks by default. Ask before configuring a post-edit
|
|
90
|
+
> or post-turn hook, or making unrelated persistent agent or platform changes.
|
|
91
|
+
> Finish by reporting the installed version, doctor status, activated skill
|
|
92
|
+
> location, first analysis result, and any REVIEW findings you accepted.
|
|
81
93
|
|
|
82
94
|
## Five-minute start
|
|
83
95
|
|
|
@@ -124,12 +136,14 @@ hook-assisted process.
|
|
|
124
136
|
warranted; normally exit `1`.
|
|
125
137
|
- **Completed FAIL** — blocks normal completion until fixed or an explicitly
|
|
126
138
|
authorized exception applies; exit `2` with a completed report on stdout.
|
|
139
|
+
- **INCOMPLETE** — one or more requested syntax analyses are unavailable;
|
|
140
|
+
independent completed findings remain authoritative, but the run exits `3`.
|
|
127
141
|
- An argparse usage or invalid-choice error exits `2`, writes usage/error text
|
|
128
142
|
to stderr, and produces no completed report.
|
|
129
|
-
- Other Code Guard tool, configuration, scope, or
|
|
143
|
+
- Other Code Guard tool, configuration, scope, or unexpected errors exit `3`.
|
|
130
144
|
|
|
131
145
|
`--ci` makes REVIEW nonblocking at the process level by changing its exit to
|
|
132
|
-
`0`; it does not hide
|
|
146
|
+
`0`; it does not hide findings or change FAIL, INCOMPLETE, and tool-error exits.
|
|
133
147
|
|
|
134
148
|
**Never game a metric.** Do not create artificial helpers, files,
|
|
135
149
|
abstractions, formatting, exclusions, or policy changes merely to lower a
|
|
@@ -161,6 +175,12 @@ result, scope, required policies, guards, ordering, and actionable findings.
|
|
|
161
175
|
Named modes require `--json`. See [Usage](https://github.com/stef-k/agent-code-guard/blob/main/docs/usage.md) for the schema and
|
|
162
176
|
option contract.
|
|
163
177
|
|
|
178
|
+
Known per-file syntax or provider failures instead produce `overall:
|
|
179
|
+
"incomplete"`, ordered structured `unavailable` records, and completed guard
|
|
180
|
+
evidence. Syntax guards identify unavailable paths; LOC and Markdown remain
|
|
181
|
+
complete. Full, debug, and compact JSON retain unavailable records unchanged,
|
|
182
|
+
and completed runs retain their existing schema.
|
|
183
|
+
|
|
164
184
|
### Common scope commands
|
|
165
185
|
|
|
166
186
|
```bash
|
|
@@ -186,7 +206,8 @@ Markdown guards apply to `.md` files.
|
|
|
186
206
|
Generic `.h` files are not syntax-dispatched; `.markdown` is not enabled; Vue
|
|
187
207
|
template and style regions are not executable syntax input; and unsupported
|
|
188
208
|
artifacts are inapplicable. Malformed applicable syntax or a required provider
|
|
189
|
-
failure is
|
|
209
|
+
failure is unavailable evidence, distinct from both an inapplicable file and a
|
|
210
|
+
completed FAIL finding. See [Language support](https://github.com/stef-k/agent-code-guard/blob/main/docs/language-support.md).
|
|
190
211
|
|
|
191
212
|
### Skill integration
|
|
192
213
|
|
|
@@ -207,6 +228,9 @@ Built-in defaults require no configuration. Configure a project only for a
|
|
|
207
228
|
concrete policy reason; see the [configuration guide](https://github.com/stef-k/agent-code-guard/blob/main/docs/configuration.md).
|
|
208
229
|
The LOC baseline is an explicit adoption tool for established legacy
|
|
209
230
|
repositories, not an ordinary-use requirement or a way to silence findings.
|
|
231
|
+
Its default `ratchetAt: "fail"` policy freezes files above `failAt`; choose
|
|
232
|
+
`ratchetAt: "review"` only when every established file above `warnAt` must be
|
|
233
|
+
non-increasing while ordinary findings in that range remain REVIEW.
|
|
210
234
|
|
|
211
235
|
## Trust, CI, and platform support
|
|
212
236
|
|
|
@@ -15,6 +15,7 @@ Code Guard provides deterministic measurements that act as anchors for agent jud
|
|
|
15
15
|
- `PASS` — no special action is required.
|
|
16
16
|
- `REVIEW` — inspect the finding and either accept it with a meaningful justification or improve the code when doing so improves real clarity, cohesion, or boundaries.
|
|
17
17
|
- `FAIL` — do not declare normal completion until the condition is fixed or an explicitly permitted/user-approved exception applies.
|
|
18
|
+
- `INCOMPLETE` — known per-file syntax or provider evidence is unavailable; retain and report independent completed evidence, but do not declare normal completion.
|
|
18
19
|
|
|
19
20
|
## Universal rules
|
|
20
21
|
|
|
@@ -50,10 +51,11 @@ python3 skills/code-guard/scripts/code_guard.py . --changed-only
|
|
|
50
51
|
```
|
|
51
52
|
|
|
52
53
|
`pyproject.toml` canonically owns the production pins. Tree-sitter remains
|
|
53
|
-
dormant during LOC-only execution
|
|
54
|
-
|
|
55
|
-
Disabling every syntax guard preserves
|
|
56
|
-
LOC-only result also requires both
|
|
54
|
+
dormant during LOC-only execution. A known per-file provider, grammar, or syntax
|
|
55
|
+
failure produces blocking `INCOMPLETE` output while retaining independent LOC,
|
|
56
|
+
Markdown, and unaffected syntax evidence. Disabling every syntax guard preserves
|
|
57
|
+
the lazy no-Tree-sitter path. A strictly LOC-only result also requires both
|
|
58
|
+
Markdown guards to be explicitly disabled.
|
|
57
59
|
|
|
58
60
|
Without Git, pass exactly the files you created or modified. You are
|
|
59
61
|
responsible for supplying the complete edited-file set:
|
|
@@ -75,8 +77,9 @@ project exclusions. LOC `--exclude` remains LOC-specific. Explicit files may
|
|
|
75
77
|
intentionally inspect Git-ignored or built-in-pruned artifacts, unless Code
|
|
76
78
|
Guard `scope.exclude` or `--scope-exclude` removes them.
|
|
77
79
|
|
|
78
|
-
Inspect every REVIEW and FAIL.
|
|
79
|
-
|
|
80
|
+
Inspect every REVIEW and FAIL. Treat INCOMPLETE as blocking, preserve its
|
|
81
|
+
completed findings, and report its unavailable paths and failure context. When
|
|
82
|
+
all guards return `PASS`, no detailed policy file needs to be loaded.
|
|
80
83
|
|
|
81
84
|
When a guard returns `REVIEW` or `FAIL`, read only the policy file named by that finding. The runner returns required policy identifiers/files in both human-readable and JSON output.
|
|
82
85
|
|
|
@@ -103,10 +106,10 @@ code-guard . --changed-only --ci --json --json-mode compact
|
|
|
103
106
|
```
|
|
104
107
|
|
|
105
108
|
`--ci` makes REVIEW exit `0`, but REVIEW remains visible and requires
|
|
106
|
-
judgment; FAIL remains `2
|
|
107
|
-
must supply the exact edited files. Do not install hooks, export
|
|
108
|
-
skill directories, or modify persistent user or repository
|
|
109
|
-
without authorization.
|
|
109
|
+
judgment; FAIL remains `2`, and INCOMPLETE and tool errors remain `3`. Outside
|
|
110
|
+
Git, a hook must supply the exact edited files. Do not install hooks, export
|
|
111
|
+
into persistent skill directories, or modify persistent user or repository
|
|
112
|
+
configuration without authorization.
|
|
110
113
|
|
|
111
114
|
## Scope
|
|
112
115
|
|
{agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/references/loc-policy.md
RENAMED
|
@@ -27,6 +27,15 @@ Existing `allowedLargeFiles` entries may be honored with their configured reason
|
|
|
27
27
|
|
|
28
28
|
Do not infer approval from inconvenience, historical size, a nearby exemption, time pressure, or a request to finish the coding task.
|
|
29
29
|
|
|
30
|
+
## Source-controlled ratchets
|
|
31
|
+
|
|
32
|
+
An established repository may use `guards.loc.ratchetAt: "fail"` (the default)
|
|
33
|
+
to freeze only files already above `failAt`, or `"review"` to freeze every
|
|
34
|
+
recorded file above `warnAt` while retaining ordinary REVIEW semantics within
|
|
35
|
+
its allowance. Growth above a recorded allowance is always FAIL. Never switch
|
|
36
|
+
the policy, raise thresholds, add exclusions or exemptions, or increase/remove
|
|
37
|
+
allowances merely to silence growth.
|
|
38
|
+
|
|
30
39
|
## Do not game LOC
|
|
31
40
|
|
|
32
41
|
Project formatting conventions take priority. Never combine independent statements, compress control flow or expressions unusually, minify handwritten code, remove useful comments/structure, or fight the formatter merely to lower physical LOC.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Narrow compatibility correction for upstream C# contextual-keyword errors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
_CONTEXTUAL_KEYWORD = b"async"
|
|
7
|
+
_NEUTRAL_IDENTIFIER = b"azync"
|
|
8
|
+
_EXPRESSION_CONTAINERS = {
|
|
9
|
+
"argument", "arrow_expression_clause", "expression_statement", "return_statement",
|
|
10
|
+
}
|
|
11
|
+
_NON_EXPRESSION_FIELDS = {"alias", "label", "name", "type"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def corrected_csharp_root(provider, source: bytes, first_tree):
|
|
15
|
+
"""Return one verified corrected root, or ``None`` when correction is unsafe."""
|
|
16
|
+
problems = tuple(_problem_nodes(first_tree.root_node))
|
|
17
|
+
if not problems:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
token_offsets: set[int] = set()
|
|
21
|
+
for problem in problems:
|
|
22
|
+
candidates = _contained_unescaped_tokens(source, problem.start_byte, problem.end_byte)
|
|
23
|
+
if len(candidates) > 1:
|
|
24
|
+
candidates = tuple(
|
|
25
|
+
offset
|
|
26
|
+
for offset in candidates
|
|
27
|
+
if not _is_declaration_identifier(first_tree.root_node, offset)
|
|
28
|
+
)
|
|
29
|
+
if len(candidates) != 1:
|
|
30
|
+
return None
|
|
31
|
+
token_offsets.add(candidates[0])
|
|
32
|
+
|
|
33
|
+
corrected = bytearray(source)
|
|
34
|
+
for offset in token_offsets:
|
|
35
|
+
corrected[offset:offset + len(_CONTEXTUAL_KEYWORD)] = _NEUTRAL_IDENTIFIER
|
|
36
|
+
corrected_source = bytes(corrected)
|
|
37
|
+
if len(corrected_source) != len(source) or _newline_offsets(corrected_source) != _newline_offsets(source):
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
retry_tree = provider.parse("csharp", corrected_source)
|
|
41
|
+
if retry_tree.root_node.has_error or any(_problem_nodes(retry_tree.root_node)):
|
|
42
|
+
return None
|
|
43
|
+
if not all(_has_authorized_role(retry_tree.root_node, offset) for offset in token_offsets):
|
|
44
|
+
return None
|
|
45
|
+
return retry_tree.root_node
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _problem_nodes(node):
|
|
49
|
+
if node.is_error or node.is_missing:
|
|
50
|
+
yield node
|
|
51
|
+
for child in node.children:
|
|
52
|
+
yield from _problem_nodes(child)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _contained_unescaped_tokens(source: bytes, start: int, end: int) -> tuple[int, ...]:
|
|
56
|
+
offsets: list[int] = []
|
|
57
|
+
position = source.find(_CONTEXTUAL_KEYWORD, start, end)
|
|
58
|
+
while position >= 0:
|
|
59
|
+
token_end = position + len(_CONTEXTUAL_KEYWORD)
|
|
60
|
+
if token_end <= end and _is_token_boundary(source, position, token_end):
|
|
61
|
+
offsets.append(position)
|
|
62
|
+
position = source.find(_CONTEXTUAL_KEYWORD, position + 1, end)
|
|
63
|
+
return tuple(offsets)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _is_token_boundary(source: bytes, start: int, end: int) -> bool:
|
|
67
|
+
before = source[start - 1] if start else None
|
|
68
|
+
after = source[end] if end < len(source) else None
|
|
69
|
+
return before != ord("@") and not _identifier_byte(before) and not _identifier_byte(after)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _identifier_byte(value: int | None) -> bool:
|
|
73
|
+
return value is not None and (value >= 0x80 or value == ord("_") or chr(value).isalnum())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _newline_offsets(source: bytes) -> tuple[int, ...]:
|
|
77
|
+
return tuple(index for index, value in enumerate(source) if value == ord("\n"))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _is_declaration_identifier(root, offset: int) -> bool:
|
|
81
|
+
node = root.descendant_for_byte_range(offset, offset + len(_CONTEXTUAL_KEYWORD))
|
|
82
|
+
if (
|
|
83
|
+
node.type != "identifier"
|
|
84
|
+
or node.start_byte != offset
|
|
85
|
+
or node.end_byte != offset + len(_CONTEXTUAL_KEYWORD)
|
|
86
|
+
or node.parent is None
|
|
87
|
+
):
|
|
88
|
+
return False
|
|
89
|
+
field_name = next(
|
|
90
|
+
(
|
|
91
|
+
node.parent.field_name_for_child(index)
|
|
92
|
+
for index, child in enumerate(node.parent.children)
|
|
93
|
+
if child == node
|
|
94
|
+
),
|
|
95
|
+
None,
|
|
96
|
+
)
|
|
97
|
+
return field_name in _NON_EXPRESSION_FIELDS
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _has_authorized_role(root, offset: int) -> bool:
|
|
101
|
+
node = root.descendant_for_byte_range(offset, offset + len(_NEUTRAL_IDENTIFIER))
|
|
102
|
+
if node.type != "identifier" or node.start_byte != offset or node.end_byte != offset + len(_NEUTRAL_IDENTIFIER):
|
|
103
|
+
return False
|
|
104
|
+
parent = node.parent
|
|
105
|
+
if parent is None:
|
|
106
|
+
return False
|
|
107
|
+
if parent.type == "argument" and parent.child_by_field_name("name") == node:
|
|
108
|
+
return True
|
|
109
|
+
field_name = next(
|
|
110
|
+
(parent.field_name_for_child(index) for index, child in enumerate(parent.children) if child == node),
|
|
111
|
+
None,
|
|
112
|
+
)
|
|
113
|
+
return (
|
|
114
|
+
parent.type.endswith("_expression") or parent.type in _EXPRESSION_CONTAINERS
|
|
115
|
+
) and field_name not in _NON_EXPRESSION_FIELDS
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
class AnalysisError(RuntimeError):
|
|
5
5
|
"""Base error suitable for Code Guard's existing exit-3 boundary."""
|
|
6
6
|
|
|
7
|
+
def __init__(self, message: str, *, language: str | None = None) -> None:
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.language = language
|
|
10
|
+
|
|
7
11
|
|
|
8
12
|
class ProviderUnavailableError(AnalysisError):
|
|
9
13
|
"""The configured parser provider or a required grammar is unavailable."""
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Parse applicable selected files once and return reusable immutable facts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .adapters import extract_facts
|
|
9
|
+
from .csharp_compat import corrected_csharp_root
|
|
10
|
+
from .errors import ProviderUnavailableError, SyntaxAnalysisError
|
|
11
|
+
from .facts import AnalysisFacts, FileFacts
|
|
12
|
+
from .provider import ParserProvider, TreeSitterProvider
|
|
13
|
+
from .regions import executable_regions, is_applicable
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def analyze_files(files: tuple[Path, ...] | list[Path], provider: ParserProvider | None = None) -> AnalysisFacts:
|
|
17
|
+
"""Analyze only applicable entries from the already-resolved caller scope."""
|
|
18
|
+
active_provider = provider or TreeSitterProvider()
|
|
19
|
+
results = [_analyze_file(Path(path), active_provider) for path in files if is_applicable(Path(path))]
|
|
20
|
+
return AnalysisFacts(tuple(results))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class UnavailableAnalysis:
|
|
25
|
+
path: Path
|
|
26
|
+
language: str
|
|
27
|
+
kind: str
|
|
28
|
+
message: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class BatchAnalysis:
|
|
33
|
+
facts: AnalysisFacts
|
|
34
|
+
unavailable: tuple[UnavailableAnalysis, ...]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def analyze_files_for_runner(
|
|
38
|
+
files: tuple[Path, ...] | list[Path], provider: ParserProvider | None = None,
|
|
39
|
+
) -> BatchAnalysis:
|
|
40
|
+
"""Analyze selected files independently while retaining only known unavailable evidence."""
|
|
41
|
+
active_provider = provider or TreeSitterProvider()
|
|
42
|
+
results: list[FileFacts] = []
|
|
43
|
+
unavailable: list[UnavailableAnalysis] = []
|
|
44
|
+
for value in files:
|
|
45
|
+
path = Path(value)
|
|
46
|
+
if not is_applicable(path):
|
|
47
|
+
continue
|
|
48
|
+
try:
|
|
49
|
+
results.append(_analyze_file(path, active_provider))
|
|
50
|
+
except (SyntaxAnalysisError, ProviderUnavailableError) as exc:
|
|
51
|
+
if exc.language is None:
|
|
52
|
+
raise
|
|
53
|
+
kind = "syntax" if isinstance(exc, SyntaxAnalysisError) else "provider"
|
|
54
|
+
unavailable.append(UnavailableAnalysis(path, exc.language, kind, str(exc)))
|
|
55
|
+
return BatchAnalysis(AnalysisFacts(tuple(results)), tuple(unavailable))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _analyze_file(path: Path, provider: ParserProvider) -> FileFacts:
|
|
59
|
+
callables = []
|
|
60
|
+
controls = []
|
|
61
|
+
decisions = []
|
|
62
|
+
regions = executable_regions(path, provider)
|
|
63
|
+
for region in regions:
|
|
64
|
+
try:
|
|
65
|
+
tree = provider.parse(region.language, region.source)
|
|
66
|
+
except (SyntaxAnalysisError, ProviderUnavailableError) as exc:
|
|
67
|
+
if exc.language is None:
|
|
68
|
+
raise type(exc)(str(exc), language=region.language) from exc
|
|
69
|
+
raise
|
|
70
|
+
root = tree.root_node
|
|
71
|
+
if tree.root_node.has_error:
|
|
72
|
+
root = (
|
|
73
|
+
corrected_csharp_root(provider, region.source, tree)
|
|
74
|
+
if region.language == "csharp"
|
|
75
|
+
else None
|
|
76
|
+
)
|
|
77
|
+
if root is None:
|
|
78
|
+
raise SyntaxAnalysisError(
|
|
79
|
+
f"unable to parse {path}: embedded {region.language} syntax tree contains errors",
|
|
80
|
+
language=region.language,
|
|
81
|
+
)
|
|
82
|
+
region_callables, region_controls, region_decisions = extract_facts(root, region)
|
|
83
|
+
callables.extend(region_callables)
|
|
84
|
+
controls.extend(region_controls)
|
|
85
|
+
decisions.extend(region_decisions)
|
|
86
|
+
return FileFacts(path, tuple(callables), tuple(controls), tuple(decisions), len(regions))
|
|
@@ -33,13 +33,15 @@ class TreeSitterProvider:
|
|
|
33
33
|
except (ImportError, LookupError, OSError, RuntimeError) as exc:
|
|
34
34
|
raise ProviderUnavailableError(
|
|
35
35
|
f"syntax provider unavailable for supported language {language!r}: {exc}; "
|
|
36
|
-
"reinstall Agent Code Guard"
|
|
36
|
+
"reinstall Agent Code Guard",
|
|
37
|
+
language=language,
|
|
37
38
|
) from exc
|
|
38
39
|
self._parsers[language] = parser
|
|
39
40
|
try:
|
|
40
41
|
return parser.parse(source)
|
|
41
|
-
except
|
|
42
|
+
except RuntimeError as exc:
|
|
42
43
|
raise ProviderUnavailableError(
|
|
43
44
|
f"syntax provider failed for supported language {language!r}: {exc}; "
|
|
44
|
-
"verify the Agent Code Guard installation"
|
|
45
|
+
"verify the Agent Code Guard installation",
|
|
46
|
+
language=language,
|
|
45
47
|
) from exc
|
|
@@ -65,7 +65,9 @@ def executable_regions(path: Path, provider: ParserProvider) -> tuple[Executable
|
|
|
65
65
|
def _vue_regions(path: Path, source: bytes, provider: ParserProvider) -> tuple[ExecutableRegion, ...]:
|
|
66
66
|
root = provider.parse("vue", source).root_node
|
|
67
67
|
if root.has_error:
|
|
68
|
-
raise SyntaxAnalysisError(
|
|
68
|
+
raise SyntaxAnalysisError(
|
|
69
|
+
f"unable to parse {path}: Vue container syntax tree contains errors", language="vue",
|
|
70
|
+
)
|
|
69
71
|
regions: list[ExecutableRegion] = []
|
|
70
72
|
for element in root.named_children:
|
|
71
73
|
if element.type != "script_element":
|
|
@@ -73,7 +75,9 @@ def _vue_regions(path: Path, source: bytes, provider: ParserProvider) -> tuple[E
|
|
|
73
75
|
start_tag = next(child for child in element.named_children if child.type == "start_tag")
|
|
74
76
|
attributes = _attributes(start_tag, source)
|
|
75
77
|
if "src" in attributes:
|
|
76
|
-
raise SyntaxAnalysisError(
|
|
78
|
+
raise SyntaxAnalysisError(
|
|
79
|
+
f"unable to analyze {path}: external Vue script regions are unsupported", language="vue",
|
|
80
|
+
)
|
|
77
81
|
language = _script_language(path, attributes.get("lang"))
|
|
78
82
|
raw_text = next((child for child in element.named_children if child.type == "raw_text"), None)
|
|
79
83
|
if raw_text is not None:
|
|
@@ -106,4 +110,6 @@ def _script_language(path: Path, value: str | None) -> str:
|
|
|
106
110
|
return "javascript"
|
|
107
111
|
if value in {"ts", "typescript"}:
|
|
108
112
|
return "typescript"
|
|
109
|
-
raise SyntaxAnalysisError(
|
|
113
|
+
raise SyntaxAnalysisError(
|
|
114
|
+
f"unable to analyze {path}: unsupported Vue script language: {value}", language="vue",
|
|
115
|
+
)
|
|
@@ -17,6 +17,7 @@ from .guards import callable_size, complexity, loc, markdown_document_size, mark
|
|
|
17
17
|
from .human_output import format_completed_analysis
|
|
18
18
|
from . import loc_baseline
|
|
19
19
|
from .result_model import GuardResult, aggregate_state, required_policies
|
|
20
|
+
from .reporting import reporting_path
|
|
20
21
|
from .skill_distribution import export_skill, skill_path as installed_skill_path
|
|
21
22
|
|
|
22
23
|
DISTRIBUTION_NAME = "agent-code-guard"
|
|
@@ -250,13 +251,31 @@ class ScopeSummary:
|
|
|
250
251
|
analyzed: int
|
|
251
252
|
inapplicable: int
|
|
252
253
|
excluded: int
|
|
254
|
+
unavailable: int | None = None
|
|
253
255
|
|
|
254
256
|
def to_json(self) -> dict[str, int]:
|
|
255
|
-
|
|
257
|
+
data = {
|
|
256
258
|
"selected": self.selected,
|
|
257
259
|
"analyzed": self.analyzed,
|
|
258
260
|
"inapplicable": self.inapplicable,
|
|
259
|
-
|
|
261
|
+
}
|
|
262
|
+
if self.unavailable is not None:
|
|
263
|
+
data["unavailable"] = self.unavailable
|
|
264
|
+
data["excluded"] = self.excluded
|
|
265
|
+
return data
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@dataclass(frozen=True)
|
|
269
|
+
class UnavailableEntry:
|
|
270
|
+
path: str
|
|
271
|
+
language: str
|
|
272
|
+
kind: str
|
|
273
|
+
message: str
|
|
274
|
+
|
|
275
|
+
def to_json(self) -> dict[str, str]:
|
|
276
|
+
return {
|
|
277
|
+
"path": self.path, "language": self.language,
|
|
278
|
+
"kind": self.kind, "message": self.message,
|
|
260
279
|
}
|
|
261
280
|
|
|
262
281
|
|
|
@@ -264,6 +283,8 @@ class ScopeSummary:
|
|
|
264
283
|
class CompletedAnalysis:
|
|
265
284
|
results: list[GuardResult]
|
|
266
285
|
scope: ScopeSummary
|
|
286
|
+
unavailable: tuple[UnavailableEntry, ...] = ()
|
|
287
|
+
incomplete_guard_ids: tuple[str, ...] = ()
|
|
267
288
|
|
|
268
289
|
|
|
269
290
|
def payload(
|
|
@@ -273,12 +294,28 @@ def payload(
|
|
|
273
294
|
if isinstance(analysis, list):
|
|
274
295
|
analysis = CompletedAnalysis(analysis, ScopeSummary(0, 0, 0, 0))
|
|
275
296
|
results = analysis.results
|
|
297
|
+
completed_overall = aggregate_state(results)
|
|
298
|
+
incomplete = bool(analysis.unavailable)
|
|
276
299
|
data = {
|
|
277
|
-
"overall":
|
|
300
|
+
"overall": "incomplete" if incomplete else completed_overall,
|
|
278
301
|
"scope": analysis.scope.to_json(),
|
|
279
302
|
"requiredPolicies": required_policies(results),
|
|
280
303
|
"guards": {result.guard_id: result.to_json() for result in results},
|
|
281
304
|
}
|
|
305
|
+
if incomplete:
|
|
306
|
+
data = {
|
|
307
|
+
"overall": "incomplete",
|
|
308
|
+
"completedOverall": completed_overall,
|
|
309
|
+
"scope": data["scope"],
|
|
310
|
+
"unavailable": [entry.to_json() for entry in analysis.unavailable],
|
|
311
|
+
"requiredPolicies": data["requiredPolicies"],
|
|
312
|
+
"guards": data["guards"],
|
|
313
|
+
}
|
|
314
|
+
unavailable_paths = [entry.path for entry in analysis.unavailable]
|
|
315
|
+
for guard_id, guard in data["guards"].items():
|
|
316
|
+
guard["complete"] = guard_id not in analysis.incomplete_guard_ids
|
|
317
|
+
if not guard["complete"]:
|
|
318
|
+
guard["unavailablePaths"] = unavailable_paths
|
|
282
319
|
if json_mode == "compact":
|
|
283
320
|
for guard in data["guards"].values():
|
|
284
321
|
guard["findings"] = [
|
|
@@ -321,7 +358,8 @@ def run_analysis(
|
|
|
321
358
|
if needs_analysis:
|
|
322
359
|
analysis = import_module("agent_code_guard.analysis.pipeline")
|
|
323
360
|
analyzed_files.update(path for path in scope.files if analysis.is_applicable(path))
|
|
324
|
-
|
|
361
|
+
batch = analysis.analyze_files_for_runner(scope.files)
|
|
362
|
+
facts = batch.facts
|
|
325
363
|
if callable_size_config.enabled:
|
|
326
364
|
results.append(callable_size.run(scope.root, callable_size_config, facts))
|
|
327
365
|
if nesting_config.enabled:
|
|
@@ -345,9 +383,27 @@ def run_analysis(
|
|
|
345
383
|
results.append(markdown_section_size.run(scope.root, markdown_section_config, _empty_markdown_facts()))
|
|
346
384
|
selected = len(scope.files)
|
|
347
385
|
analyzed = len(analyzed_files)
|
|
386
|
+
unavailable = tuple(
|
|
387
|
+
UnavailableEntry(
|
|
388
|
+
reporting_path(item.path, scope.root), item.language, item.kind, item.message,
|
|
389
|
+
)
|
|
390
|
+
for item in (batch.unavailable if needs_analysis else ())
|
|
391
|
+
)
|
|
392
|
+
incomplete_guard_ids = tuple(
|
|
393
|
+
guard_id for guard_id, enabled in (
|
|
394
|
+
("callableSize", callable_size_config.enabled),
|
|
395
|
+
("nesting", nesting_config.enabled),
|
|
396
|
+
("complexity", complexity_config.enabled),
|
|
397
|
+
) if enabled and unavailable
|
|
398
|
+
)
|
|
348
399
|
return CompletedAnalysis(
|
|
349
400
|
results,
|
|
350
|
-
ScopeSummary(
|
|
401
|
+
ScopeSummary(
|
|
402
|
+
selected, analyzed, selected - analyzed, len(scope.excluded_files),
|
|
403
|
+
len({entry.path for entry in unavailable}) if unavailable else None,
|
|
404
|
+
),
|
|
405
|
+
unavailable,
|
|
406
|
+
incomplete_guard_ids,
|
|
351
407
|
)
|
|
352
408
|
|
|
353
409
|
|
|
@@ -362,6 +418,8 @@ def print_text(data: dict[str, object]) -> None:
|
|
|
362
418
|
|
|
363
419
|
|
|
364
420
|
def exit_code(overall: str, ci: bool) -> int:
|
|
421
|
+
if overall == "incomplete":
|
|
422
|
+
return 3
|
|
365
423
|
if overall == "fail":
|
|
366
424
|
return 2
|
|
367
425
|
if overall == "review" and not ci:
|
|
@@ -64,6 +64,7 @@ class Config:
|
|
|
64
64
|
exclude: list[str]
|
|
65
65
|
allowed_large_files: list[AllowedLargeFile]
|
|
66
66
|
overrides: list[ThresholdOverride]
|
|
67
|
+
ratchet_at: str = "fail"
|
|
67
68
|
|
|
68
69
|
|
|
69
70
|
def load_config(args: argparse.Namespace) -> Config:
|
|
@@ -89,6 +90,9 @@ def load_config(args: argparse.Namespace) -> Config:
|
|
|
89
90
|
enabled = data.get("enabled", True)
|
|
90
91
|
if not isinstance(enabled, bool):
|
|
91
92
|
raise ValueError("guards.loc.enabled must be a boolean")
|
|
93
|
+
ratchet_at = data.get("ratchetAt", "fail")
|
|
94
|
+
if not isinstance(ratchet_at, str) or ratchet_at not in {"fail", "review"}:
|
|
95
|
+
raise ValueError("guards.loc.ratchetAt must be 'fail' or 'review'")
|
|
92
96
|
|
|
93
97
|
warn_at = args.warn if args.warn is not None else data.get("warnAt", DEFAULT_WARN_AT)
|
|
94
98
|
fail_at = args.fail if args.fail is not None else data.get("failAt", DEFAULT_FAIL_AT)
|
|
@@ -114,6 +118,7 @@ def load_config(args: argparse.Namespace) -> Config:
|
|
|
114
118
|
False if args.ignore_comment_lines else count_comments, include_extensions, exclude,
|
|
115
119
|
parse_allowed_large_files(data.get("allowedLargeFiles", [])),
|
|
116
120
|
parse_overrides(data.get("overrides", [])),
|
|
121
|
+
ratchet_at,
|
|
117
122
|
)
|
|
118
123
|
|
|
119
124
|
|
|
@@ -201,6 +206,20 @@ def evaluate(
|
|
|
201
206
|
ratchet_status = None
|
|
202
207
|
if allowed and counted > warn_at:
|
|
203
208
|
native_status, state, reason = "exempt", "pass", allowed.reason
|
|
209
|
+
elif config.ratchet_at == "review" and baseline_loc is not None:
|
|
210
|
+
if counted > baseline_loc:
|
|
211
|
+
native_status, state = "ratchetExceeded", "fail"
|
|
212
|
+
reason = f"LOC grew above source-controlled allowance {baseline_loc}."
|
|
213
|
+
ratchet_status = "exceeded"
|
|
214
|
+
elif counted > fail_at:
|
|
215
|
+
native_status, state, reason = "grandfathered", "review", None
|
|
216
|
+
ratchet_status = "within"
|
|
217
|
+
elif counted > warn_at:
|
|
218
|
+
native_status, state, reason = "warn", "review", None
|
|
219
|
+
ratchet_status = "within"
|
|
220
|
+
else:
|
|
221
|
+
native_status, state, reason = "ok", "pass", None
|
|
222
|
+
ratchet_status = "notNeeded"
|
|
204
223
|
elif counted > fail_at:
|
|
205
224
|
if baseline_loc is not None and counted <= baseline_loc:
|
|
206
225
|
native_status, state, reason = "grandfathered", "review", None
|
|
@@ -106,10 +106,25 @@ def _markdown_lines(data: dict[str, object]) -> list[str]:
|
|
|
106
106
|
def format_completed_analysis(data: dict[str, object]) -> str:
|
|
107
107
|
"""Return the complete human report for an existing completed payload."""
|
|
108
108
|
scope = data["scope"]
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
if data["overall"] == "incomplete":
|
|
110
|
+
lines = [
|
|
111
|
+
f"INCOMPLETE: {scope['selected']} selected; {scope['analyzed']} analyzed; "
|
|
112
|
+
f"{scope['inapplicable']} inapplicable; {scope['unavailable']} unavailable; "
|
|
113
|
+
f"{scope['excluded']} excluded. Completed findings: {str(data['completedOverall']).upper()}."
|
|
114
|
+
]
|
|
115
|
+
lines.extend(
|
|
116
|
+
f"UNAVAILABLE: {item['path']} [{item['language']} {item['kind']}] - {item['message']}"
|
|
117
|
+
for item in data["unavailable"]
|
|
118
|
+
)
|
|
119
|
+
incomplete_guards = [
|
|
120
|
+
guard_id for guard_id, result in data["guards"].items() if not result["complete"]
|
|
121
|
+
]
|
|
122
|
+
lines.append(f"Incomplete guards: {', '.join(incomplete_guards)}.")
|
|
123
|
+
else:
|
|
124
|
+
lines = [
|
|
125
|
+
f"{str(data['overall']).upper()}: {scope['selected']} selected; {scope['analyzed']} analyzed; "
|
|
126
|
+
f"{scope['inapplicable']} inapplicable; {scope['excluded']} excluded."
|
|
127
|
+
]
|
|
113
128
|
lines.extend(_loc_lines(data))
|
|
114
129
|
lines.extend(_callable_size_lines(data))
|
|
115
130
|
lines.extend(_nesting_lines(data))
|
|
@@ -126,8 +126,9 @@ def create(root: Path, files: tuple[Path, ...], config: loc.Config) -> int:
|
|
|
126
126
|
if any(matches_path_glob(relative, item.path) for item in config.allowed_large_files):
|
|
127
127
|
continue
|
|
128
128
|
counted = loc.count_loc(path, config)
|
|
129
|
-
|
|
130
|
-
if
|
|
129
|
+
warn_at, fail_at, _ = loc.effective_thresholds(relative, config)
|
|
130
|
+
threshold = warn_at if config.ratchet_at == "review" else fail_at
|
|
131
|
+
if counted > threshold:
|
|
131
132
|
entries[relative] = counted
|
|
132
133
|
content = serialize(entries)
|
|
133
134
|
created_directory = not target.parent.exists()
|
|
@@ -178,8 +179,9 @@ def update(
|
|
|
178
179
|
raise ValueError(
|
|
179
180
|
f"LOC baseline update would increase allowance for {relative}: {allowance} to {counted}"
|
|
180
181
|
)
|
|
181
|
-
|
|
182
|
-
if
|
|
182
|
+
warn_at, fail_at, _ = loc.effective_thresholds(relative, config)
|
|
183
|
+
threshold = warn_at if config.ratchet_at == "review" else fail_at
|
|
184
|
+
if counted <= threshold:
|
|
183
185
|
proposed.pop(relative)
|
|
184
186
|
removed += 1
|
|
185
187
|
elif counted < allowance:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-code-guard
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Deterministic cross-language guardrails for agent-assisted development
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Source, https://github.com/stef-k/agent-code-guard
|
|
@@ -82,16 +82,28 @@ environment, uv, and developer alternatives.
|
|
|
82
82
|
|
|
83
83
|
Copy this prompt to a coding agent:
|
|
84
84
|
|
|
85
|
-
>
|
|
86
|
-
>
|
|
87
|
-
>
|
|
88
|
-
>
|
|
89
|
-
>
|
|
90
|
-
>
|
|
91
|
-
>
|
|
92
|
-
>
|
|
93
|
-
>
|
|
94
|
-
>
|
|
85
|
+
> Adopt Agent Code Guard for this repository using the official project and
|
|
86
|
+
> documentation at https://github.com/stef-k/agent-code-guard. This prompt
|
|
87
|
+
> authorizes installing the published distribution in an isolated supported
|
|
88
|
+
> way, preferably with `pipx install agent-code-guard`, and persistently
|
|
89
|
+
> activating or exporting its installed, version-matched skill for the current
|
|
90
|
+
> agent environment. Verify `code-guard --version` and run `code-guard doctor`,
|
|
91
|
+
> then locate the skill with `code-guard --skill-path`. Follow the
|
|
92
|
+
> [skill activation guide](https://github.com/stef-k/agent-code-guard/blob/main/docs/skill-distribution.md):
|
|
93
|
+
> activate that installed path when the platform supports it, or export it with
|
|
94
|
+
> `code-guard --export-skill <target-directory>` to the platform's documented
|
|
95
|
+
> skill directory. Do not guess a target or overwrite an existing export; verify
|
|
96
|
+
> that `.agent-code-guard-version` matches the installed distribution. Follow the
|
|
97
|
+
> [agent workflow](https://github.com/stef-k/agent-code-guard/blob/main/docs/agent-workflow.md)
|
|
98
|
+
> without creating a LOC baseline. After meaningful source or Markdown edits, run
|
|
99
|
+
> `code-guard . --changed-only --json --json-mode compact`; outside Git, pass the
|
|
100
|
+
> exact edited files. Inspect REVIEW and FAIL findings, make only genuine
|
|
101
|
+
> improvements, rerun the check, and report the result. Never weaken thresholds,
|
|
102
|
+
> exclusions, configuration, or baselines merely to silence findings. Code Guard
|
|
103
|
+
> does not install or enable hooks by default. Ask before configuring a post-edit
|
|
104
|
+
> or post-turn hook, or making unrelated persistent agent or platform changes.
|
|
105
|
+
> Finish by reporting the installed version, doctor status, activated skill
|
|
106
|
+
> location, first analysis result, and any REVIEW findings you accepted.
|
|
95
107
|
|
|
96
108
|
## Five-minute start
|
|
97
109
|
|
|
@@ -138,12 +150,14 @@ hook-assisted process.
|
|
|
138
150
|
warranted; normally exit `1`.
|
|
139
151
|
- **Completed FAIL** — blocks normal completion until fixed or an explicitly
|
|
140
152
|
authorized exception applies; exit `2` with a completed report on stdout.
|
|
153
|
+
- **INCOMPLETE** — one or more requested syntax analyses are unavailable;
|
|
154
|
+
independent completed findings remain authoritative, but the run exits `3`.
|
|
141
155
|
- An argparse usage or invalid-choice error exits `2`, writes usage/error text
|
|
142
156
|
to stderr, and produces no completed report.
|
|
143
|
-
- Other Code Guard tool, configuration, scope, or
|
|
157
|
+
- Other Code Guard tool, configuration, scope, or unexpected errors exit `3`.
|
|
144
158
|
|
|
145
159
|
`--ci` makes REVIEW nonblocking at the process level by changing its exit to
|
|
146
|
-
`0`; it does not hide
|
|
160
|
+
`0`; it does not hide findings or change FAIL, INCOMPLETE, and tool-error exits.
|
|
147
161
|
|
|
148
162
|
**Never game a metric.** Do not create artificial helpers, files,
|
|
149
163
|
abstractions, formatting, exclusions, or policy changes merely to lower a
|
|
@@ -175,6 +189,12 @@ result, scope, required policies, guards, ordering, and actionable findings.
|
|
|
175
189
|
Named modes require `--json`. See [Usage](https://github.com/stef-k/agent-code-guard/blob/main/docs/usage.md) for the schema and
|
|
176
190
|
option contract.
|
|
177
191
|
|
|
192
|
+
Known per-file syntax or provider failures instead produce `overall:
|
|
193
|
+
"incomplete"`, ordered structured `unavailable` records, and completed guard
|
|
194
|
+
evidence. Syntax guards identify unavailable paths; LOC and Markdown remain
|
|
195
|
+
complete. Full, debug, and compact JSON retain unavailable records unchanged,
|
|
196
|
+
and completed runs retain their existing schema.
|
|
197
|
+
|
|
178
198
|
### Common scope commands
|
|
179
199
|
|
|
180
200
|
```bash
|
|
@@ -200,7 +220,8 @@ Markdown guards apply to `.md` files.
|
|
|
200
220
|
Generic `.h` files are not syntax-dispatched; `.markdown` is not enabled; Vue
|
|
201
221
|
template and style regions are not executable syntax input; and unsupported
|
|
202
222
|
artifacts are inapplicable. Malformed applicable syntax or a required provider
|
|
203
|
-
failure is
|
|
223
|
+
failure is unavailable evidence, distinct from both an inapplicable file and a
|
|
224
|
+
completed FAIL finding. See [Language support](https://github.com/stef-k/agent-code-guard/blob/main/docs/language-support.md).
|
|
204
225
|
|
|
205
226
|
### Skill integration
|
|
206
227
|
|
|
@@ -221,6 +242,9 @@ Built-in defaults require no configuration. Configure a project only for a
|
|
|
221
242
|
concrete policy reason; see the [configuration guide](https://github.com/stef-k/agent-code-guard/blob/main/docs/configuration.md).
|
|
222
243
|
The LOC baseline is an explicit adoption tool for established legacy
|
|
223
244
|
repositories, not an ordinary-use requirement or a way to silence findings.
|
|
245
|
+
Its default `ratchetAt: "fail"` policy freezes files above `failAt`; choose
|
|
246
|
+
`ratchetAt: "review"` only when every established file above `warnAt` must be
|
|
247
|
+
non-increasing while ordinary findings in that range remain REVIEW.
|
|
224
248
|
|
|
225
249
|
## Trust, CI, and platform support
|
|
226
250
|
|
|
@@ -31,6 +31,7 @@ src/agent_code_guard/analysis/__init__.py
|
|
|
31
31
|
src/agent_code_guard/analysis/adapters.py
|
|
32
32
|
src/agent_code_guard/analysis/branch_normalization.py
|
|
33
33
|
src/agent_code_guard/analysis/callable_identity.py
|
|
34
|
+
src/agent_code_guard/analysis/csharp_compat.py
|
|
34
35
|
src/agent_code_guard/analysis/errors.py
|
|
35
36
|
src/agent_code_guard/analysis/facts.py
|
|
36
37
|
src/agent_code_guard/analysis/language_specs.py
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
"""Parse applicable selected files once and return reusable immutable facts."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
from .adapters import extract_facts
|
|
8
|
-
from .errors import SyntaxAnalysisError
|
|
9
|
-
from .facts import AnalysisFacts, FileFacts
|
|
10
|
-
from .provider import ParserProvider, TreeSitterProvider
|
|
11
|
-
from .regions import executable_regions, is_applicable
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def analyze_files(files: tuple[Path, ...] | list[Path], provider: ParserProvider | None = None) -> AnalysisFacts:
|
|
15
|
-
"""Analyze only applicable entries from the already-resolved caller scope."""
|
|
16
|
-
active_provider = provider or TreeSitterProvider()
|
|
17
|
-
results: list[FileFacts] = []
|
|
18
|
-
for path in files:
|
|
19
|
-
path = Path(path)
|
|
20
|
-
if not is_applicable(path):
|
|
21
|
-
continue
|
|
22
|
-
callables = []
|
|
23
|
-
controls = []
|
|
24
|
-
decisions = []
|
|
25
|
-
regions = executable_regions(path, active_provider)
|
|
26
|
-
for region in regions:
|
|
27
|
-
tree = active_provider.parse(region.language, region.source)
|
|
28
|
-
if tree.root_node.has_error:
|
|
29
|
-
raise SyntaxAnalysisError(
|
|
30
|
-
f"unable to parse {path}: embedded {region.language} syntax tree contains errors"
|
|
31
|
-
)
|
|
32
|
-
region_callables, region_controls, region_decisions = extract_facts(tree.root_node, region)
|
|
33
|
-
callables.extend(region_callables)
|
|
34
|
-
controls.extend(region_controls)
|
|
35
|
-
decisions.extend(region_decisions)
|
|
36
|
-
results.append(FileFacts(path, tuple(callables), tuple(controls), tuple(decisions), len(regions)))
|
|
37
|
-
return AnalysisFacts(tuple(results))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/references/complexity-policy.md
RENAMED
|
File without changes
|
|
File without changes
|
{agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/references/nesting-policy.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/callable_identity.py
RENAMED
|
File without changes
|
|
File without changes
|
{agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/language_specs.py
RENAMED
|
File without changes
|
{agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/syntax_nodes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/guards/callable_size.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/skill_distribution.py
RENAMED
|
File without changes
|
{agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard.egg-info/requires.txt
RENAMED
|
File without changes
|
{agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard.egg-info/top_level.txt
RENAMED
|
File without changes
|