attune-verify 0.1.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 (27) hide show
  1. attune_verify-0.1.0/LICENSE +17 -0
  2. attune_verify-0.1.0/PKG-INFO +101 -0
  3. attune_verify-0.1.0/README.md +50 -0
  4. attune_verify-0.1.0/pyproject.toml +70 -0
  5. attune_verify-0.1.0/setup.cfg +4 -0
  6. attune_verify-0.1.0/src/attune_verify/__init__.py +34 -0
  7. attune_verify-0.1.0/src/attune_verify/_extract.py +79 -0
  8. attune_verify-0.1.0/src/attune_verify/_verify.py +142 -0
  9. attune_verify-0.1.0/src/attune_verify/checkers/__init__.py +1 -0
  10. attune_verify-0.1.0/src/attune_verify/checkers/counts.py +68 -0
  11. attune_verify-0.1.0/src/attune_verify/checkers/flags.py +85 -0
  12. attune_verify-0.1.0/src/attune_verify/checkers/imports.py +66 -0
  13. attune_verify-0.1.0/src/attune_verify/checkers/links.py +58 -0
  14. attune_verify-0.1.0/src/attune_verify/context.py +46 -0
  15. attune_verify-0.1.0/src/attune_verify/result.py +59 -0
  16. attune_verify-0.1.0/src/attune_verify/semantic/__init__.py +1 -0
  17. attune_verify-0.1.0/src/attune_verify/semantic/protocol.py +45 -0
  18. attune_verify-0.1.0/src/attune_verify/semantic/rag_adapter.py +61 -0
  19. attune_verify-0.1.0/src/attune_verify.egg-info/PKG-INFO +101 -0
  20. attune_verify-0.1.0/src/attune_verify.egg-info/SOURCES.txt +25 -0
  21. attune_verify-0.1.0/src/attune_verify.egg-info/dependency_links.txt +1 -0
  22. attune_verify-0.1.0/src/attune_verify.egg-info/requires.txt +12 -0
  23. attune_verify-0.1.0/src/attune_verify.egg-info/top_level.txt +1 -0
  24. attune_verify-0.1.0/tests/test_regression_author_351.py +152 -0
  25. attune_verify-0.1.0/tests/test_result.py +43 -0
  26. attune_verify-0.1.0/tests/test_security.py +35 -0
  27. attune_verify-0.1.0/tests/test_semantic.py +45 -0
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 Smart AI Memory
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: attune-verify
3
+ Version: 0.1.0
4
+ Summary: Generation fact-checker for the attune-* family. Verifies named entities in LLM output actually exist — imports import, CLI flags are real, links resolve, counts match source.
5
+ Author-email: Patrick Roebuck <admin@smartaimemory.com>
6
+ License: Apache License
7
+ Version 2.0, January 2004
8
+ http://www.apache.org/licenses/
9
+
10
+ Copyright 2026 Smart AI Memory
11
+
12
+ Licensed under the Apache License, Version 2.0 (the "License");
13
+ you may not use this file except in compliance with the License.
14
+ You may obtain a copy of the License at
15
+
16
+ http://www.apache.org/licenses/LICENSE-2.0
17
+
18
+ Unless required by applicable law or agreed to in writing, software
19
+ distributed under the License is distributed on an "AS IS" BASIS,
20
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
+ See the License for the specific language governing permissions and
22
+ limitations under the License.
23
+
24
+ Project-URL: Homepage, https://github.com/Smart-AI-Memory/attune-verify
25
+ Project-URL: Repository, https://github.com/Smart-AI-Memory/attune-verify
26
+ Keywords: verification,fact-checking,hallucination,llm,attune,grounding,faithfulness
27
+ Classifier: Development Status :: 2 - Pre-Alpha
28
+ Classifier: Intended Audience :: Developers
29
+ Classifier: License :: OSI Approved :: Apache Software License
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: Programming Language :: Python :: 3.10
32
+ Classifier: Programming Language :: Python :: 3.11
33
+ Classifier: Programming Language :: Python :: 3.12
34
+ Classifier: Programming Language :: Python :: 3.13
35
+ Classifier: Topic :: Software Development :: Libraries
36
+ Classifier: Topic :: Software Development :: Quality Assurance
37
+ Requires-Python: >=3.10
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Provides-Extra: rag
41
+ Requires-Dist: attune-rag>=0.2.0; extra == "rag"
42
+ Provides-Extra: dev
43
+ Requires-Dist: pytest>=8.0; extra == "dev"
44
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
45
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
46
+ Requires-Dist: ruff>=0.4.0; extra == "dev"
47
+ Requires-Dist: black>=24.0; extra == "dev"
48
+ Requires-Dist: build>=1.0; extra == "dev"
49
+ Requires-Dist: twine>=5.0; extra == "dev"
50
+ Dynamic: license-file
51
+
52
+ # attune-verify
53
+
54
+ Generation fact-checker for the attune-\* family. Verifies named entities
55
+ in LLM-generated content actually exist — imports import, CLI flags are real,
56
+ links resolve, counts match source — so hallucinations that pass unit tests
57
+ are caught before they reach a reader.
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ pip install attune-verify
63
+ ```
64
+
65
+ With the optional LLM semantic layer (requires attune-rag):
66
+
67
+ ```bash
68
+ pip install 'attune-verify[rag]'
69
+ ```
70
+
71
+ ## Quick start
72
+
73
+ ```python
74
+ from attune_verify import verify, VerifyContext
75
+ from pathlib import Path
76
+
77
+ ctx = VerifyContext(
78
+ project_root=Path("."),
79
+ allowed_help_cmds=frozenset(["attune"]),
80
+ )
81
+ result = verify(generated_content, ctx)
82
+ if not result.ok:
83
+ for f in result.findings:
84
+ print(f"{f.kind}: {f.detail}")
85
+ ```
86
+
87
+ ## Part of the attune family
88
+
89
+ - **attune-rag** grounds generation in accurate retrieved sources (input-side)
90
+ - **attune-verify** checks that named entities in the output actually exist (output-side)
91
+
92
+ Together they bracket generation: rag verifies *"is this claim supported?"*;
93
+ verify checks *"does this named thing exist?"*
94
+
95
+ ## Status
96
+
97
+ Pre-alpha — spec complete, implementation in progress.
98
+
99
+ ## License
100
+
101
+ Apache 2.0
@@ -0,0 +1,50 @@
1
+ # attune-verify
2
+
3
+ Generation fact-checker for the attune-\* family. Verifies named entities
4
+ in LLM-generated content actually exist — imports import, CLI flags are real,
5
+ links resolve, counts match source — so hallucinations that pass unit tests
6
+ are caught before they reach a reader.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install attune-verify
12
+ ```
13
+
14
+ With the optional LLM semantic layer (requires attune-rag):
15
+
16
+ ```bash
17
+ pip install 'attune-verify[rag]'
18
+ ```
19
+
20
+ ## Quick start
21
+
22
+ ```python
23
+ from attune_verify import verify, VerifyContext
24
+ from pathlib import Path
25
+
26
+ ctx = VerifyContext(
27
+ project_root=Path("."),
28
+ allowed_help_cmds=frozenset(["attune"]),
29
+ )
30
+ result = verify(generated_content, ctx)
31
+ if not result.ok:
32
+ for f in result.findings:
33
+ print(f"{f.kind}: {f.detail}")
34
+ ```
35
+
36
+ ## Part of the attune family
37
+
38
+ - **attune-rag** grounds generation in accurate retrieved sources (input-side)
39
+ - **attune-verify** checks that named entities in the output actually exist (output-side)
40
+
41
+ Together they bracket generation: rag verifies *"is this claim supported?"*;
42
+ verify checks *"does this named thing exist?"*
43
+
44
+ ## Status
45
+
46
+ Pre-alpha — spec complete, implementation in progress.
47
+
48
+ ## License
49
+
50
+ Apache 2.0
@@ -0,0 +1,70 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "attune-verify"
7
+ version = "0.1.0"
8
+ description = "Generation fact-checker for the attune-* family. Verifies named entities in LLM output actually exist — imports import, CLI flags are real, links resolve, counts match source."
9
+ readme = {file = "README.md", content-type = "text/markdown"}
10
+ requires-python = ">=3.10"
11
+ license = {file = "LICENSE"}
12
+ authors = [
13
+ {name = "Patrick Roebuck", email = "admin@smartaimemory.com"}
14
+ ]
15
+ keywords = [
16
+ "verification", "fact-checking", "hallucination",
17
+ "llm", "attune", "grounding", "faithfulness",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 2 - Pre-Alpha",
21
+ "Intended Audience :: Developers",
22
+ "License :: OSI Approved :: Apache Software License",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Topic :: Software Development :: Libraries",
29
+ "Topic :: Software Development :: Quality Assurance",
30
+ ]
31
+ dependencies = []
32
+
33
+ [project.optional-dependencies]
34
+ rag = ["attune-rag>=0.2.0"]
35
+ dev = [
36
+ "pytest>=8.0",
37
+ "pytest-asyncio>=0.23",
38
+ "pytest-cov>=4.0",
39
+ "ruff>=0.4.0",
40
+ "black>=24.0",
41
+ "build>=1.0",
42
+ "twine>=5.0",
43
+ ]
44
+
45
+ [project.urls]
46
+ Homepage = "https://github.com/Smart-AI-Memory/attune-verify"
47
+ Repository = "https://github.com/Smart-AI-Memory/attune-verify"
48
+
49
+ [tool.setuptools.packages.find]
50
+ where = ["src"]
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]
54
+ asyncio_mode = "auto"
55
+
56
+ [tool.ruff]
57
+ line-length = 100
58
+ target-version = "py310"
59
+
60
+ [tool.ruff.lint]
61
+ select = ["E", "F", "B", "I"]
62
+ ignore = ["BLE001"]
63
+
64
+ [tool.black]
65
+ line-length = 100
66
+ target-version = ["py310", "py311", "py312", "py313"]
67
+
68
+ [tool.coverage.run]
69
+ source = ["attune_verify"]
70
+ omit = ["tests/*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,34 @@
1
+ """attune-verify — generation fact-checker.
2
+
3
+ Verifies named entities in LLM-generated content actually exist:
4
+ imports import, CLI flags are real, links resolve, counts match source.
5
+
6
+ Public API::
7
+
8
+ from attune_verify import verify, VerifyContext, VerifyResult
9
+ from attune_verify import Finding, FindingKind
10
+ from attune_verify import VerificationError, raise_if_failed
11
+ from attune_verify.semantic.protocol import Judge, SemanticVerdict
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from attune_verify.context import VerifyContext
16
+ from attune_verify.result import (
17
+ Finding,
18
+ FindingKind,
19
+ VerificationError,
20
+ VerifyResult,
21
+ raise_if_failed,
22
+ )
23
+ from attune_verify._verify import verify
24
+
25
+ __version__ = "0.1.0"
26
+ __all__ = [
27
+ "verify",
28
+ "VerifyContext",
29
+ "VerifyResult",
30
+ "Finding",
31
+ "FindingKind",
32
+ "VerificationError",
33
+ "raise_if_failed",
34
+ ]
@@ -0,0 +1,79 @@
1
+ """Shared extraction utilities: pull code fences, links, numeric claims."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from dataclasses import dataclass
6
+ from typing import List, Optional, Tuple
7
+
8
+
9
+ @dataclass
10
+ class CodeFence:
11
+ """A fenced code block extracted from markdown."""
12
+ language: str
13
+ content: str
14
+ line: Optional[int] = None
15
+
16
+
17
+ @dataclass
18
+ class MarkdownLink:
19
+ """A markdown link extracted from content."""
20
+ text: str
21
+ target: str
22
+ line: Optional[int] = None
23
+
24
+
25
+ @dataclass
26
+ class NumericClaim:
27
+ """A numeric claim extracted from content."""
28
+ value: int
29
+ context: str # surrounding text
30
+ line: Optional[int] = None
31
+
32
+
33
+ _FENCE_RE = re.compile(
34
+ r"^```(\w*)\n(.*?)^```",
35
+ re.MULTILINE | re.DOTALL,
36
+ )
37
+ _LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
38
+ _NUM_RE = re.compile(r"\b(\d{2,})\b") # 2+ digit numbers (skip single digits)
39
+
40
+
41
+ def extract_code_fences(content: str) -> List[CodeFence]:
42
+ """Extract all fenced code blocks from markdown content."""
43
+ fences = []
44
+ for match in _FENCE_RE.finditer(content):
45
+ line = content[: match.start()].count("\n") + 1
46
+ fences.append(CodeFence(
47
+ language=match.group(1) or "text",
48
+ content=match.group(2),
49
+ line=line,
50
+ ))
51
+ return fences
52
+
53
+
54
+ def extract_links(content: str) -> List[MarkdownLink]:
55
+ """Extract all markdown links from content."""
56
+ links = []
57
+ for match in _LINK_RE.finditer(content):
58
+ line = content[: match.start()].count("\n") + 1
59
+ links.append(MarkdownLink(
60
+ text=match.group(1),
61
+ target=match.group(2),
62
+ line=line,
63
+ ))
64
+ return links
65
+
66
+
67
+ def extract_numeric_claims(content: str) -> List[NumericClaim]:
68
+ """Extract numeric claims (2+ digit numbers) with surrounding context."""
69
+ claims = []
70
+ for match in _NUM_RE.finditer(content):
71
+ line = content[: match.start()].count("\n") + 1
72
+ start = max(0, match.start() - 40)
73
+ end = min(len(content), match.end() + 40)
74
+ claims.append(NumericClaim(
75
+ value=int(match.group(1)),
76
+ context=content[start:end].replace("\n", " "),
77
+ line=line,
78
+ ))
79
+ return claims
@@ -0,0 +1,142 @@
1
+ """Core verify() orchestration — runs all checkers and the semantic layer."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from typing import List
6
+
7
+ from attune_verify._extract import (
8
+ extract_code_fences,
9
+ extract_links,
10
+ extract_numeric_claims,
11
+ )
12
+ from attune_verify.checkers.counts import check_counts
13
+ from attune_verify.checkers.flags import check_flags
14
+ from attune_verify.checkers.imports import check_imports
15
+ from attune_verify.checkers.links import check_links
16
+ from attune_verify.context import VerifyContext
17
+ from attune_verify.result import Finding, FindingKind, VerifyResult
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def verify(content: str, context: VerifyContext) -> VerifyResult:
23
+ """Run deterministic checkers and optional semantic layer.
24
+
25
+ Deterministic checkers always run and are independent — a failure in
26
+ one does not abort the others. The semantic layer runs only when
27
+ context.semantic is True and a judge is available.
28
+
29
+ Args:
30
+ content: LLM-generated content to verify.
31
+ context: Declared truth boundaries (project root, env, commands,
32
+ count sources, optional judge).
33
+
34
+ Returns:
35
+ VerifyResult with all findings. Never raises on findings — use
36
+ raise_if_failed(result) for a hard gate.
37
+ """
38
+ result = VerifyResult()
39
+
40
+ # --- Deterministic checkers ---
41
+ _run_checker(result, "imports", _check_imports, content, context)
42
+ _run_checker(result, "flags", _check_flags, content, context)
43
+ _run_checker(result, "links", _check_links, content, context)
44
+ _run_checker(result, "counts", _check_counts, content, context)
45
+
46
+ # --- Semantic layer (opt-in) ---
47
+ if context.semantic:
48
+ _run_semantic(result, content, context)
49
+
50
+ return result
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Per-checker wrappers — each catches exceptions and surfaces as a warning
55
+ # ---------------------------------------------------------------------------
56
+
57
+ def _check_imports(content: str, context: VerifyContext) -> List[Finding]:
58
+ fences = extract_code_fences(content)
59
+ return check_imports(fences, env_python=context.env_python)
60
+
61
+
62
+ def _check_flags(content: str, context: VerifyContext) -> List[Finding]:
63
+ return check_flags(
64
+ content,
65
+ help_commands=context.help_commands,
66
+ allowed_help_cmds=context.allowed_help_cmds,
67
+ )
68
+
69
+
70
+ def _check_links(content: str, context: VerifyContext) -> List[Finding]:
71
+ links = extract_links(content)
72
+ return check_links(links, project_root=context.project_root)
73
+
74
+
75
+ def _check_counts(content: str, context: VerifyContext) -> List[Finding]:
76
+ claims = extract_numeric_claims(content)
77
+ return check_counts(claims, count_sources=context.count_sources)
78
+
79
+
80
+ def _run_checker(
81
+ result: VerifyResult,
82
+ name: str,
83
+ fn: object,
84
+ content: str,
85
+ context: VerifyContext,
86
+ ) -> None:
87
+ """Run one checker, surfacing any internal exception as a warning."""
88
+ try:
89
+ findings = fn(content, context) # type: ignore[operator]
90
+ result.findings.extend(findings)
91
+ result.checked.append(name)
92
+ except Exception as exc: # noqa: BLE001
93
+ # INTENTIONAL: individual checker failures must not abort the run.
94
+ logger.exception("checker '%s' raised: %s", name, exc)
95
+ result.findings.append(Finding(
96
+ kind=FindingKind.UNRESOLVED_IMPORT, # closest kind for infra error
97
+ detail=f"Checker '{name}' failed: {exc}",
98
+ evidence="",
99
+ severity="warning",
100
+ ))
101
+
102
+
103
+ def _run_semantic(result: VerifyResult, content: str, context: VerifyContext) -> None:
104
+ """Run the semantic layer if a judge is available."""
105
+ from attune_verify.semantic.protocol import Judge # noqa: PLC0415
106
+
107
+ if context.judge is None or not isinstance(context.judge, Judge):
108
+ result.findings.append(Finding(
109
+ kind=FindingKind.SEMANTIC,
110
+ detail=(
111
+ "Semantic layer requested (context.semantic=True) "
112
+ "but no judge was provided in VerifyContext.judge"
113
+ ),
114
+ evidence="",
115
+ severity="warning",
116
+ ))
117
+ return
118
+
119
+ try:
120
+ verdict = context.judge.score(
121
+ query="Verify this generated content for faithfulness",
122
+ answer=content,
123
+ passages=content,
124
+ )
125
+ result.semantic_ran = True
126
+ if not verdict.faithful:
127
+ for issue in verdict.issues:
128
+ result.findings.append(Finding(
129
+ kind=FindingKind.SEMANTIC,
130
+ detail=issue,
131
+ evidence="",
132
+ severity="error",
133
+ ))
134
+ except Exception as exc: # noqa: BLE001
135
+ # INTENTIONAL: semantic layer is opt-in; failures degrade gracefully.
136
+ logger.exception("semantic judge raised: %s", exc)
137
+ result.findings.append(Finding(
138
+ kind=FindingKind.SEMANTIC,
139
+ detail=f"Semantic judge failed: {exc}",
140
+ evidence="",
141
+ severity="warning",
142
+ ))
@@ -0,0 +1 @@
1
+ """Deterministic entity-resolution checkers for attune-verify."""
@@ -0,0 +1,68 @@
1
+ """Count checker — verifies numeric claims match caller-supplied sources."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Callable, Dict, List, Union
5
+
6
+ from attune_verify._extract import NumericClaim
7
+ from attune_verify.result import Finding, FindingKind
8
+
9
+
10
+ def check_counts(
11
+ claims: List[NumericClaim],
12
+ count_sources: Dict[str, Union[int, Callable[[], int]]],
13
+ ) -> List[Finding]:
14
+ """Verify numeric claims match count_sources values.
15
+
16
+ Counts cannot be inferred — the caller must supply them. Any numeric
17
+ claim in the content is matched against count_sources by value. Claims
18
+ with no matching source entry are flagged as warnings (unverifiable),
19
+ not errors.
20
+
21
+ Args:
22
+ claims: Numeric claims extracted from generated content.
23
+ count_sources: Expected values keyed by label/description.
24
+ Values may be plain ints or zero-argument callables.
25
+
26
+ Returns:
27
+ List of findings for mismatched or unverifiable counts.
28
+ """
29
+ if not count_sources:
30
+ return []
31
+
32
+ resolved_sources: Dict[str, int] = {}
33
+ for label, value in count_sources.items():
34
+ resolved_sources[label] = value() if callable(value) else value
35
+
36
+ expected_values = set(resolved_sources.values())
37
+ findings: List[Finding] = []
38
+
39
+ for claim in claims:
40
+ if claim.value not in expected_values:
41
+ # Check if it could be a mismatch against a named source
42
+ close_label = _find_close_label(claim.context, resolved_sources)
43
+ if close_label is not None:
44
+ expected = resolved_sources[close_label]
45
+ findings.append(Finding(
46
+ kind=FindingKind.COUNT_MISMATCH,
47
+ detail=(
48
+ f"Count {claim.value} doesn't match "
49
+ f"'{close_label}' (expected {expected})"
50
+ ),
51
+ evidence=claim.context,
52
+ location=f"line {claim.line}" if claim.line else None,
53
+ severity="error",
54
+ ))
55
+ return findings
56
+
57
+
58
+ def _find_close_label(
59
+ context: str,
60
+ sources: Dict[str, int],
61
+ ) -> str | None:
62
+ """Find a source label whose keywords appear in the claim's context."""
63
+ context_lower = context.lower()
64
+ for label in sources:
65
+ words = label.lower().split()
66
+ if any(w in context_lower for w in words if len(w) > 3):
67
+ return label
68
+ return None
@@ -0,0 +1,85 @@
1
+ """Flag checker — verifies CLI flags referenced in content exist in --help."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ import subprocess
6
+ from typing import Dict, FrozenSet, List
7
+
8
+ from attune_verify.result import Finding, FindingKind
9
+
10
+ _FLAG_RE = re.compile(r"`(--[\w-]+)`")
11
+
12
+
13
+ def check_flags(
14
+ content: str,
15
+ help_commands: Dict[str, str],
16
+ allowed_help_cmds: FrozenSet[str],
17
+ ) -> List[Finding]:
18
+ """Verify flags referenced in content exist in command --help output.
19
+
20
+ Security: only invokes --help for commands in allowed_help_cmds.
21
+ A flag for an unknown command yields a warning, not a silent pass.
22
+
23
+ Args:
24
+ content: Generated content to scan for flag references.
25
+ help_commands: Pre-captured --help text keyed by command name.
26
+ allowed_help_cmds: Commands safe to invoke at runtime.
27
+
28
+ Returns:
29
+ List of findings for unverifiable or unknown flags.
30
+ """
31
+ findings: List[Finding] = []
32
+ # Find patterns like "`--flag`" or "`command --flag`"
33
+ for match in _FLAG_RE.finditer(content):
34
+ flag = match.group(1)
35
+ surrounding = content[max(0, match.start() - 30): match.start()]
36
+ cmd = _guess_command(surrounding)
37
+ help_text = _get_help(cmd, help_commands, allowed_help_cmds)
38
+ if help_text is None:
39
+ findings.append(Finding(
40
+ kind=FindingKind.UNKNOWN_FLAG,
41
+ detail=(
42
+ f"Flag '{flag}' could not be verified "
43
+ f"(command '{cmd}' not in allowed_help_cmds)"
44
+ ),
45
+ evidence=match.group(0),
46
+ severity="warning",
47
+ ))
48
+ elif flag not in help_text:
49
+ findings.append(Finding(
50
+ kind=FindingKind.UNKNOWN_FLAG,
51
+ detail=f"Flag '{flag}' not found in '{cmd} --help'",
52
+ evidence=match.group(0),
53
+ severity="error",
54
+ ))
55
+ return findings
56
+
57
+
58
+ def _guess_command(preceding: str) -> str:
59
+ """Heuristically extract the command name preceding a flag."""
60
+ words = preceding.strip().split()
61
+ for word in reversed(words):
62
+ cleaned = word.strip("`")
63
+ if cleaned and not cleaned.startswith("-"):
64
+ return cleaned
65
+ return "unknown"
66
+
67
+
68
+ def _get_help(
69
+ cmd: str,
70
+ help_commands: Dict[str, str],
71
+ allowed_help_cmds: FrozenSet[str],
72
+ ) -> str | None:
73
+ """Return help text or None if the command cannot be introspected."""
74
+ if cmd in help_commands:
75
+ return help_commands[cmd]
76
+ if cmd in allowed_help_cmds:
77
+ result = subprocess.run(
78
+ [cmd, "--help"],
79
+ capture_output=True,
80
+ text=True,
81
+ encoding="utf-8",
82
+ timeout=10,
83
+ )
84
+ return result.stdout + result.stderr
85
+ return None