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.
Files changed (54) hide show
  1. {agent_code_guard-0.2.0/src/agent_code_guard.egg-info → agent_code_guard-0.3.0}/PKG-INFO +38 -14
  2. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/README.md +37 -13
  3. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/pyproject.toml +1 -1
  4. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/SKILL.md +13 -10
  5. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/references/loc-policy.md +9 -0
  6. agent_code_guard-0.3.0/src/agent_code_guard/analysis/csharp_compat.py +115 -0
  7. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/errors.py +4 -0
  8. agent_code_guard-0.3.0/src/agent_code_guard/analysis/pipeline.py +86 -0
  9. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/provider.py +5 -3
  10. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/regions.py +9 -3
  11. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/code_guard.py +63 -5
  12. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/config_validation.py +1 -0
  13. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/guards/loc.py +19 -0
  14. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/human_output.py +19 -4
  15. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/loc_baseline.py +6 -4
  16. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0/src/agent_code_guard.egg-info}/PKG-INFO +38 -14
  17. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard.egg-info/SOURCES.txt +1 -0
  18. agent_code_guard-0.2.0/src/agent_code_guard/analysis/pipeline.py +0 -37
  19. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/LICENSE +0 -0
  20. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/MANIFEST.in +0 -0
  21. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/setup.cfg +0 -0
  22. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/LICENSE.txt +0 -0
  23. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/agents/openai.yaml +0 -0
  24. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/references/callable-size-policy.md +0 -0
  25. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/references/complexity-policy.md +0 -0
  26. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/references/markdown-size-policy.md +0 -0
  27. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/skills/code-guard/references/nesting-policy.md +0 -0
  28. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/__init__.py +0 -0
  29. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/__init__.py +0 -0
  30. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/adapters.py +0 -0
  31. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/branch_normalization.py +0 -0
  32. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/callable_identity.py +0 -0
  33. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/facts.py +0 -0
  34. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/language_specs.py +0 -0
  35. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/analysis/syntax_nodes.py +0 -0
  36. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/doctor.py +0 -0
  37. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/file_selection.py +0 -0
  38. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/guards/__init__.py +0 -0
  39. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/guards/callable_size.py +0 -0
  40. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/guards/complexity.py +0 -0
  41. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/guards/markdown_document_size.py +0 -0
  42. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/guards/markdown_section_size.py +0 -0
  43. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/guards/nesting.py +0 -0
  44. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/markdown/__init__.py +0 -0
  45. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/markdown/facts.py +0 -0
  46. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/markdown/scanner.py +0 -0
  47. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/path_matching.py +0 -0
  48. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/reporting.py +0 -0
  49. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/result_model.py +0 -0
  50. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard/skill_distribution.py +0 -0
  51. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard.egg-info/dependency_links.txt +0 -0
  52. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard.egg-info/entry_points.txt +0 -0
  53. {agent_code_guard-0.2.0 → agent_code_guard-0.3.0}/src/agent_code_guard.egg-info/requires.txt +0 -0
  54. {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.2.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
- > Consult the official Agent Code Guard repository and documentation. Install
86
- > the published distribution in an isolated supported way, preferably with
87
- > pipx; verify `code-guard --version` and run `code-guard doctor`. Locate the
88
- > installed version-matched skill with `code-guard --skill-path`, and use or
89
- > export only that skill through the documented mechanism. Inspect this
90
- > repository without creating a LOC baseline and use changed-work scope. Ask
91
- > before exporting into a persistent skill directory, changing persistent
92
- > agent or platform configuration, or configuring hooks.
93
- > Never weaken thresholds, exclusions, configuration, or baselines merely to
94
- > silence findings.
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 provider errors exit `3`.
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 the findings or change FAIL and tool-error exits.
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 a fail-closed tool error. See [Language support](https://github.com/stef-k/agent-code-guard/blob/main/docs/language-support.md).
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
- > Consult the official Agent Code Guard repository and documentation. Install
72
- > the published distribution in an isolated supported way, preferably with
73
- > pipx; verify `code-guard --version` and run `code-guard doctor`. Locate the
74
- > installed version-matched skill with `code-guard --skill-path`, and use or
75
- > export only that skill through the documented mechanism. Inspect this
76
- > repository without creating a LOC baseline and use changed-work scope. Ask
77
- > before exporting into a persistent skill directory, changing persistent
78
- > agent or platform configuration, or configuring hooks.
79
- > Never weaken thresholds, exclusions, configuration, or baselines merely to
80
- > silence findings.
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 provider errors exit `3`.
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 the findings or change FAIL and tool-error exits.
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 a fail-closed tool error. See [Language support](https://github.com/stef-k/agent-code-guard/blob/main/docs/language-support.md).
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agent-code-guard"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Deterministic cross-language guardrails for agent-assisted development"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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; failure to load a required provider or
54
- grammar is a deterministic tool error during normal zero-config syntax analysis.
55
- Disabling every syntax guard preserves the lazy no-Tree-sitter path. A strictly
56
- LOC-only result also requires both Markdown guards to be explicitly disabled.
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. When all guards return `PASS`, no detailed
79
- policy file needs to be loaded.
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` and tool errors remain `3`. Outside Git, a hook
107
- must supply the exact edited files. Do not install hooks, export into persistent
108
- skill directories, or modify persistent user or repository configuration
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
 
@@ -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 Exception as exc:
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(f"unable to parse {path}: Vue container syntax tree contains errors")
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(f"unable to analyze {path}: external Vue script regions are unsupported")
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(f"unable to analyze {path}: unsupported Vue script language: {value}")
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
- return {
257
+ data = {
256
258
  "selected": self.selected,
257
259
  "analyzed": self.analyzed,
258
260
  "inapplicable": self.inapplicable,
259
- "excluded": self.excluded,
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": aggregate_state(results),
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
- facts = analysis.analyze_files(scope.files)
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(selected, analyzed, selected - analyzed, len(scope.excluded_files)),
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:
@@ -26,6 +26,7 @@ REVIEW_GUARD_NAMES = (
26
26
  REVIEW_GUARD_KEYS = {"enabled", "reviewAt"}
27
27
  LOC_KEYS = {
28
28
  "enabled",
29
+ "ratchetAt",
29
30
  "warnAt",
30
31
  "failAt",
31
32
  "countBlankLines",
@@ -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
- lines = [
110
- f"{str(data['overall']).upper()}: {scope['selected']} selected; {scope['analyzed']} analyzed; "
111
- f"{scope['inapplicable']} inapplicable; {scope['excluded']} excluded."
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
- _, fail_at, _ = loc.effective_thresholds(relative, config)
130
- if counted > fail_at:
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
- _, fail_at, _ = loc.effective_thresholds(relative, config)
182
- if counted <= fail_at:
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.2.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
- > Consult the official Agent Code Guard repository and documentation. Install
86
- > the published distribution in an isolated supported way, preferably with
87
- > pipx; verify `code-guard --version` and run `code-guard doctor`. Locate the
88
- > installed version-matched skill with `code-guard --skill-path`, and use or
89
- > export only that skill through the documented mechanism. Inspect this
90
- > repository without creating a LOC baseline and use changed-work scope. Ask
91
- > before exporting into a persistent skill directory, changing persistent
92
- > agent or platform configuration, or configuring hooks.
93
- > Never weaken thresholds, exclusions, configuration, or baselines merely to
94
- > silence findings.
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 provider errors exit `3`.
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 the findings or change FAIL and tool-error exits.
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 a fail-closed tool error. See [Language support](https://github.com/stef-k/agent-code-guard/blob/main/docs/language-support.md).
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))