agent-code-guard 0.2.0__tar.gz → 0.3.1__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 (55) hide show
  1. {agent_code_guard-0.2.0/src/agent_code_guard.egg-info → agent_code_guard-0.3.1}/PKG-INFO +38 -14
  2. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/README.md +37 -13
  3. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/pyproject.toml +1 -1
  4. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/skills/code-guard/SKILL.md +13 -10
  5. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/skills/code-guard/references/loc-policy.md +9 -0
  6. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/analysis/adapters.py +4 -4
  7. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/analysis/callable_identity.py +1 -2
  8. agent_code_guard-0.3.1/src/agent_code_guard/analysis/csharp_compat.py +115 -0
  9. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/analysis/errors.py +4 -0
  10. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/analysis/facts.py +21 -1
  11. agent_code_guard-0.3.1/src/agent_code_guard/analysis/pipeline.py +92 -0
  12. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/analysis/provider.py +5 -3
  13. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/analysis/regions.py +33 -17
  14. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/code_guard.py +127 -36
  15. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/config_validation.py +18 -19
  16. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/file_selection.py +42 -17
  17. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/guards/callable_size.py +14 -20
  18. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/guards/complexity.py +14 -19
  19. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/guards/loc.py +46 -36
  20. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/guards/markdown_document_size.py +16 -18
  21. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/guards/markdown_section_size.py +16 -18
  22. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/guards/nesting.py +14 -19
  23. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/human_output.py +19 -4
  24. agent_code_guard-0.3.1/src/agent_code_guard/invocation.py +54 -0
  25. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/loc_baseline.py +16 -7
  26. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/markdown/facts.py +1 -0
  27. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/markdown/scanner.py +15 -6
  28. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1/src/agent_code_guard.egg-info}/PKG-INFO +38 -14
  29. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard.egg-info/SOURCES.txt +2 -0
  30. agent_code_guard-0.2.0/src/agent_code_guard/analysis/pipeline.py +0 -37
  31. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/LICENSE +0 -0
  32. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/MANIFEST.in +0 -0
  33. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/setup.cfg +0 -0
  34. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/skills/code-guard/LICENSE.txt +0 -0
  35. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/skills/code-guard/agents/openai.yaml +0 -0
  36. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/skills/code-guard/references/callable-size-policy.md +0 -0
  37. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/skills/code-guard/references/complexity-policy.md +0 -0
  38. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/skills/code-guard/references/markdown-size-policy.md +0 -0
  39. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/skills/code-guard/references/nesting-policy.md +0 -0
  40. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/__init__.py +0 -0
  41. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/analysis/__init__.py +0 -0
  42. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/analysis/branch_normalization.py +0 -0
  43. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/analysis/language_specs.py +0 -0
  44. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/analysis/syntax_nodes.py +0 -0
  45. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/doctor.py +0 -0
  46. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/guards/__init__.py +0 -0
  47. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/markdown/__init__.py +0 -0
  48. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/path_matching.py +0 -0
  49. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/reporting.py +0 -0
  50. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/result_model.py +0 -0
  51. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard/skill_distribution.py +0 -0
  52. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard.egg-info/dependency_links.txt +0 -0
  53. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard.egg-info/entry_points.txt +0 -0
  54. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/src/agent_code_guard.egg-info/requires.txt +0 -0
  55. {agent_code_guard-0.2.0 → agent_code_guard-0.3.1}/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.1
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.1"
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.
@@ -116,11 +116,11 @@ def _range_end_node(node, language: str):
116
116
 
117
117
  def _callable_range(node, region: ExecutableRegion) -> SourceRange:
118
118
  """Snapshot provider points once before mapping them to original source."""
119
- start_row, start_column = callable_source_start(node, region.language).start_point
120
- end_row, end_column = _range_end_node(node, region.language).end_point
119
+ start = callable_source_start(node, region.language)
120
+ end = _range_end_node(node, region.language)
121
121
  return SourceRange(
122
- region.original_point(start_row, start_column),
123
- region.original_point(end_row, end_column),
122
+ region.original_point_at_byte(start.start_byte),
123
+ region.original_point_at_byte(end.end_byte),
124
124
  )
125
125
 
126
126
 
@@ -272,8 +272,7 @@ def _javascript_lexical_name(node, source: bytes) -> str | None:
272
272
 
273
273
 
274
274
  def _callback_name(node, region: ExecutableRegion) -> str:
275
- row, column = node.start_point
276
- point = region.original_point(row, column)
275
+ point = region.original_point_at_byte(node.start_byte)
277
276
  return f"<callback@{point.line}:{point.byte_column}>"
278
277
 
279
278
 
@@ -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."""
@@ -2,8 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from dataclasses import dataclass
5
+ from dataclasses import dataclass, field
6
6
  from pathlib import Path
7
+ from types import MappingProxyType
8
+ from typing import Mapping
7
9
 
8
10
 
9
11
  @dataclass(frozen=True, order=True)
@@ -82,11 +84,20 @@ class FileFacts:
82
84
  controls: tuple[ControlFlowFact, ...]
83
85
  decisions: tuple[DecisionFact, ...]
84
86
  region_count: int
87
+ reporting_path: str | None = None
85
88
 
86
89
 
87
90
  @dataclass(frozen=True)
88
91
  class AnalysisFacts:
89
92
  files: tuple[FileFacts, ...]
93
+ _reporting_paths: Mapping[Path, str] = field(init=False, repr=False, compare=False)
94
+
95
+ def __post_init__(self) -> None:
96
+ object.__setattr__(self, "_reporting_paths", MappingProxyType({
97
+ file.path: file.reporting_path
98
+ for file in self.files
99
+ if file.reporting_path is not None
100
+ }))
90
101
 
91
102
  @property
92
103
  def callables(self) -> tuple[CallableFact, ...]:
@@ -99,3 +110,12 @@ class AnalysisFacts:
99
110
  @property
100
111
  def decisions(self) -> tuple[DecisionFact, ...]:
101
112
  return tuple(fact for file in self.files for fact in file.decisions)
113
+
114
+ def reporting_path_for(self, path: Path, root: Path | None = None) -> str:
115
+ stored = self._reporting_paths.get(path)
116
+ if stored is not None:
117
+ return stored
118
+ try:
119
+ return path.relative_to(root).as_posix() if root is not None else path.as_posix()
120
+ except ValueError:
121
+ return path.as_posix()
@@ -0,0 +1,92 @@
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 ..invocation import SelectedFile
9
+
10
+ from .adapters import extract_facts
11
+ from .csharp_compat import corrected_csharp_root
12
+ from .errors import ProviderUnavailableError, SyntaxAnalysisError
13
+ from .facts import AnalysisFacts, FileFacts
14
+ from .provider import ParserProvider, TreeSitterProvider
15
+ from .regions import executable_regions, is_applicable
16
+
17
+
18
+ def analyze_files(files: tuple[SelectedFile, ...] | list[SelectedFile], provider: ParserProvider | None = None) -> AnalysisFacts:
19
+ """Analyze only applicable entries from the already-resolved caller scope."""
20
+ active_provider = provider or TreeSitterProvider()
21
+ results = [
22
+ _analyze_file(selected.physical_path, active_provider, selected.reporting_path)
23
+ for selected in files if is_applicable(selected.physical_path)
24
+ ]
25
+ return AnalysisFacts(tuple(results))
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class UnavailableAnalysis:
30
+ path: Path
31
+ reporting_path: str
32
+ language: str
33
+ kind: str
34
+ message: str
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class BatchAnalysis:
39
+ facts: AnalysisFacts
40
+ unavailable: tuple[UnavailableAnalysis, ...]
41
+
42
+
43
+ def analyze_files_for_runner(
44
+ files: tuple[SelectedFile, ...], provider: ParserProvider | None = None,
45
+ ) -> BatchAnalysis:
46
+ """Analyze selected files independently while retaining only known unavailable evidence."""
47
+ active_provider = provider or TreeSitterProvider()
48
+ results: list[FileFacts] = []
49
+ unavailable: list[UnavailableAnalysis] = []
50
+ for selected in files:
51
+ path = selected.physical_path
52
+ if not is_applicable(path):
53
+ continue
54
+ try:
55
+ results.append(_analyze_file(path, active_provider, selected.reporting_path))
56
+ except (SyntaxAnalysisError, ProviderUnavailableError) as exc:
57
+ if exc.language is None:
58
+ raise
59
+ kind = "syntax" if isinstance(exc, SyntaxAnalysisError) else "provider"
60
+ unavailable.append(UnavailableAnalysis(path, selected.reporting_path, exc.language, kind, str(exc)))
61
+ return BatchAnalysis(AnalysisFacts(tuple(results)), tuple(unavailable))
62
+
63
+
64
+ def _analyze_file(path: Path, provider: ParserProvider, reporting_path: str | None = None) -> FileFacts:
65
+ callables = []
66
+ controls = []
67
+ decisions = []
68
+ regions = executable_regions(path, provider)
69
+ for region in regions:
70
+ try:
71
+ tree = provider.parse(region.language, region.source)
72
+ except (SyntaxAnalysisError, ProviderUnavailableError) as exc:
73
+ if exc.language is None:
74
+ raise type(exc)(str(exc), language=region.language) from exc
75
+ raise
76
+ root = tree.root_node
77
+ if tree.root_node.has_error:
78
+ root = (
79
+ corrected_csharp_root(provider, region.source, tree)
80
+ if region.language == "csharp"
81
+ else None
82
+ )
83
+ if root is None:
84
+ raise SyntaxAnalysisError(
85
+ f"unable to parse {path}: embedded {region.language} syntax tree contains errors",
86
+ language=region.language,
87
+ )
88
+ region_callables, region_controls, region_decisions = extract_facts(root, region)
89
+ callables.extend(region_callables)
90
+ controls.extend(region_controls)
91
+ decisions.extend(region_decisions)
92
+ return FileFacts(path, tuple(callables), tuple(controls), tuple(decisions), len(regions), reporting_path)
@@ -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