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.
- attune_verify-0.1.0/LICENSE +17 -0
- attune_verify-0.1.0/PKG-INFO +101 -0
- attune_verify-0.1.0/README.md +50 -0
- attune_verify-0.1.0/pyproject.toml +70 -0
- attune_verify-0.1.0/setup.cfg +4 -0
- attune_verify-0.1.0/src/attune_verify/__init__.py +34 -0
- attune_verify-0.1.0/src/attune_verify/_extract.py +79 -0
- attune_verify-0.1.0/src/attune_verify/_verify.py +142 -0
- attune_verify-0.1.0/src/attune_verify/checkers/__init__.py +1 -0
- attune_verify-0.1.0/src/attune_verify/checkers/counts.py +68 -0
- attune_verify-0.1.0/src/attune_verify/checkers/flags.py +85 -0
- attune_verify-0.1.0/src/attune_verify/checkers/imports.py +66 -0
- attune_verify-0.1.0/src/attune_verify/checkers/links.py +58 -0
- attune_verify-0.1.0/src/attune_verify/context.py +46 -0
- attune_verify-0.1.0/src/attune_verify/result.py +59 -0
- attune_verify-0.1.0/src/attune_verify/semantic/__init__.py +1 -0
- attune_verify-0.1.0/src/attune_verify/semantic/protocol.py +45 -0
- attune_verify-0.1.0/src/attune_verify/semantic/rag_adapter.py +61 -0
- attune_verify-0.1.0/src/attune_verify.egg-info/PKG-INFO +101 -0
- attune_verify-0.1.0/src/attune_verify.egg-info/SOURCES.txt +25 -0
- attune_verify-0.1.0/src/attune_verify.egg-info/dependency_links.txt +1 -0
- attune_verify-0.1.0/src/attune_verify.egg-info/requires.txt +12 -0
- attune_verify-0.1.0/src/attune_verify.egg-info/top_level.txt +1 -0
- attune_verify-0.1.0/tests/test_regression_author_351.py +152 -0
- attune_verify-0.1.0/tests/test_result.py +43 -0
- attune_verify-0.1.0/tests/test_security.py +35 -0
- 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,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
|