intent-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.
- intent_verify-0.1.0/LICENSE +21 -0
- intent_verify-0.1.0/PKG-INFO +155 -0
- intent_verify-0.1.0/README.md +133 -0
- intent_verify-0.1.0/pyproject.toml +46 -0
- intent_verify-0.1.0/setup.cfg +4 -0
- intent_verify-0.1.0/src/intent_verify/__init__.py +3 -0
- intent_verify-0.1.0/src/intent_verify/cli.py +87 -0
- intent_verify-0.1.0/src/intent_verify/models.py +30 -0
- intent_verify-0.1.0/src/intent_verify/parser.py +55 -0
- intent_verify-0.1.0/src/intent_verify/report.py +125 -0
- intent_verify-0.1.0/src/intent_verify/scanner.py +80 -0
- intent_verify-0.1.0/src/intent_verify/tokenizer.py +50 -0
- intent_verify-0.1.0/src/intent_verify.egg-info/PKG-INFO +155 -0
- intent_verify-0.1.0/src/intent_verify.egg-info/SOURCES.txt +20 -0
- intent_verify-0.1.0/src/intent_verify.egg-info/dependency_links.txt +1 -0
- intent_verify-0.1.0/src/intent_verify.egg-info/entry_points.txt +2 -0
- intent_verify-0.1.0/src/intent_verify.egg-info/requires.txt +4 -0
- intent_verify-0.1.0/src/intent_verify.egg-info/top_level.txt +1 -0
- intent_verify-0.1.0/tests/test_cli.py +46 -0
- intent_verify-0.1.0/tests/test_parser.py +25 -0
- intent_verify-0.1.0/tests/test_scanner.py +15 -0
- intent_verify-0.1.0/tests/test_tokenizer.py +10 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rolando Bosch
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: intent-verify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Repo intent verification and spec drift checks for markdown specs, handoffs, and codebases.
|
|
5
|
+
Author: Rolando Bosch
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: spec drift,repo intent verification,handoff verification,acceptance criteria,ci lint
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
20
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# intent-verify: repo intent verification and spec drift checks
|
|
24
|
+
|
|
25
|
+
Find spec drift fast when your repo has an `INTENT.md`, `SPEC.md`, or handoff doc but nobody knows if the code still matches it.
|
|
26
|
+
|
|
27
|
+
`intent-verify` checks a markdown spec against a repo and returns `verified`, `partial`, or `missing` so you can catch repo intent drift before review, release, or handoff.
|
|
28
|
+
|
|
29
|
+
- "My repo has an `INTENT.md` but nobody knows if the code still matches it."
|
|
30
|
+
- "Reviews catch scope drift too late."
|
|
31
|
+
- "A handoff doc says one thing and the implementation says another."
|
|
32
|
+
- "I want a cheap CI check for spec drift before merge."
|
|
33
|
+
- "We need repo intent verification without inventing another full compliance system."
|
|
34
|
+
|
|
35
|
+
Fastest install:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install intent-verify
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Fastest real usage:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
intent-verify check --spec INTENT.md --repo .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Exact outcome:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
intent-verify: INTENT.md vs . (12 files)
|
|
51
|
+
[OK 100%] uploads PDF invoices
|
|
52
|
+
[PART 50%] retries provider timeout
|
|
53
|
+
[LOW 20%] writes audit log for rejected invoices
|
|
54
|
+
intent-verify: MISSING — 1/3 items below 30% (avg 57%)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
This is a guardrail, not proof of correctness. It answers “does the implementation visibly cover the stated scope?” not “is the software correct?”
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install intent-verify
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
For local development:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install -e ".[dev]"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Common search-intent use cases
|
|
74
|
+
|
|
75
|
+
- repo intent verification
|
|
76
|
+
- spec drift check
|
|
77
|
+
- handoff verification
|
|
78
|
+
- acceptance criteria drift detection
|
|
79
|
+
- CI check for markdown spec vs code
|
|
80
|
+
|
|
81
|
+
## Usage
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
intent-verify check --spec INTENT.md --repo .
|
|
85
|
+
intent-verify check --spec SPEC.md --repo . --json
|
|
86
|
+
intent-verify check --spec docs/handoff.md --repo src --min-verified 0.75 --min-item 0.35
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## What it parses
|
|
90
|
+
|
|
91
|
+
By default it extracts items from:
|
|
92
|
+
|
|
93
|
+
- inline lines such as `Accepts: upload PDF invoices, retry on timeout`
|
|
94
|
+
- markdown sections such as `## Accepts` with bullet items
|
|
95
|
+
|
|
96
|
+
It also supports custom headings:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
intent-verify check --spec SPEC.md --section "Requirements"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Output
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
intent-verify: INTENT.md vs . (12 files)
|
|
106
|
+
[OK 100%] uploads PDF invoices
|
|
107
|
+
[PART 50%] retries provider timeout
|
|
108
|
+
[LOW 20%] writes audit log for rejected invoices
|
|
109
|
+
intent-verify: MISSING — 1/3 items below 35% (avg 57%)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
JSON mode:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
intent-verify check --spec INTENT.md --repo . --json
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Limitations
|
|
119
|
+
|
|
120
|
+
- Lexical, not semantic.
|
|
121
|
+
- Can over-credit token overlap.
|
|
122
|
+
- Can under-credit implementations expressed with different vocabulary.
|
|
123
|
+
- Best used as a CI guardrail or review hint, not as a substitute for tests and code review.
|
|
124
|
+
|
|
125
|
+
## When To Use It
|
|
126
|
+
|
|
127
|
+
- You keep project intent in markdown.
|
|
128
|
+
- You want a lightweight repo intent verification step in CI.
|
|
129
|
+
- You need a handoff verification check before merging or releasing.
|
|
130
|
+
|
|
131
|
+
## When Not To Use It
|
|
132
|
+
|
|
133
|
+
- You need semantic verification of behavior.
|
|
134
|
+
- You do not have any human-readable spec, intent, or requirements file.
|
|
135
|
+
- You want proof of correctness instead of a fast drift signal.
|
|
136
|
+
|
|
137
|
+
## More From Hermes Labs
|
|
138
|
+
|
|
139
|
+
- [csv-quality-gate](https://github.com/roli-lpci/csv-quality-gate): CSV preflight validation and batch CSV quality checks
|
|
140
|
+
|
|
141
|
+
## Development
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
ruff check .
|
|
145
|
+
python3 -m pytest -q
|
|
146
|
+
python3 -m py_compile src/intent_verify/*.py
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Repository layout
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
src/intent_verify/
|
|
153
|
+
tests/
|
|
154
|
+
examples/
|
|
155
|
+
```
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# intent-verify: repo intent verification and spec drift checks
|
|
2
|
+
|
|
3
|
+
Find spec drift fast when your repo has an `INTENT.md`, `SPEC.md`, or handoff doc but nobody knows if the code still matches it.
|
|
4
|
+
|
|
5
|
+
`intent-verify` checks a markdown spec against a repo and returns `verified`, `partial`, or `missing` so you can catch repo intent drift before review, release, or handoff.
|
|
6
|
+
|
|
7
|
+
- "My repo has an `INTENT.md` but nobody knows if the code still matches it."
|
|
8
|
+
- "Reviews catch scope drift too late."
|
|
9
|
+
- "A handoff doc says one thing and the implementation says another."
|
|
10
|
+
- "I want a cheap CI check for spec drift before merge."
|
|
11
|
+
- "We need repo intent verification without inventing another full compliance system."
|
|
12
|
+
|
|
13
|
+
Fastest install:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install intent-verify
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Fastest real usage:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
intent-verify check --spec INTENT.md --repo .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Exact outcome:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
intent-verify: INTENT.md vs . (12 files)
|
|
29
|
+
[OK 100%] uploads PDF invoices
|
|
30
|
+
[PART 50%] retries provider timeout
|
|
31
|
+
[LOW 20%] writes audit log for rejected invoices
|
|
32
|
+
intent-verify: MISSING — 1/3 items below 30% (avg 57%)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
This is a guardrail, not proof of correctness. It answers “does the implementation visibly cover the stated scope?” not “is the software correct?”
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install intent-verify
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For local development:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install -e ".[dev]"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Common search-intent use cases
|
|
52
|
+
|
|
53
|
+
- repo intent verification
|
|
54
|
+
- spec drift check
|
|
55
|
+
- handoff verification
|
|
56
|
+
- acceptance criteria drift detection
|
|
57
|
+
- CI check for markdown spec vs code
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
intent-verify check --spec INTENT.md --repo .
|
|
63
|
+
intent-verify check --spec SPEC.md --repo . --json
|
|
64
|
+
intent-verify check --spec docs/handoff.md --repo src --min-verified 0.75 --min-item 0.35
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## What it parses
|
|
68
|
+
|
|
69
|
+
By default it extracts items from:
|
|
70
|
+
|
|
71
|
+
- inline lines such as `Accepts: upload PDF invoices, retry on timeout`
|
|
72
|
+
- markdown sections such as `## Accepts` with bullet items
|
|
73
|
+
|
|
74
|
+
It also supports custom headings:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
intent-verify check --spec SPEC.md --section "Requirements"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Output
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
intent-verify: INTENT.md vs . (12 files)
|
|
84
|
+
[OK 100%] uploads PDF invoices
|
|
85
|
+
[PART 50%] retries provider timeout
|
|
86
|
+
[LOW 20%] writes audit log for rejected invoices
|
|
87
|
+
intent-verify: MISSING — 1/3 items below 35% (avg 57%)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
JSON mode:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
intent-verify check --spec INTENT.md --repo . --json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Limitations
|
|
97
|
+
|
|
98
|
+
- Lexical, not semantic.
|
|
99
|
+
- Can over-credit token overlap.
|
|
100
|
+
- Can under-credit implementations expressed with different vocabulary.
|
|
101
|
+
- Best used as a CI guardrail or review hint, not as a substitute for tests and code review.
|
|
102
|
+
|
|
103
|
+
## When To Use It
|
|
104
|
+
|
|
105
|
+
- You keep project intent in markdown.
|
|
106
|
+
- You want a lightweight repo intent verification step in CI.
|
|
107
|
+
- You need a handoff verification check before merging or releasing.
|
|
108
|
+
|
|
109
|
+
## When Not To Use It
|
|
110
|
+
|
|
111
|
+
- You need semantic verification of behavior.
|
|
112
|
+
- You do not have any human-readable spec, intent, or requirements file.
|
|
113
|
+
- You want proof of correctness instead of a fast drift signal.
|
|
114
|
+
|
|
115
|
+
## More From Hermes Labs
|
|
116
|
+
|
|
117
|
+
- [csv-quality-gate](https://github.com/roli-lpci/csv-quality-gate): CSV preflight validation and batch CSV quality checks
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
ruff check .
|
|
123
|
+
python3 -m pytest -q
|
|
124
|
+
python3 -m py_compile src/intent_verify/*.py
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Repository layout
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
src/intent_verify/
|
|
131
|
+
tests/
|
|
132
|
+
examples/
|
|
133
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "intent-verify"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Repo intent verification and spec drift checks for markdown specs, handoffs, and codebases."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Rolando Bosch"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["spec drift", "repo intent verification", "handoff verification", "acceptance criteria", "ci lint"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
intent-verify = "intent_verify.cli:main"
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = ["pytest>=8.0", "ruff>=0.5"]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools]
|
|
33
|
+
package-dir = {"" = "src"}
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["src"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
line-length = 100
|
|
43
|
+
target-version = "py310"
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .models import Verdict
|
|
8
|
+
from .report import run_check, to_json, to_text
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
prog="intent-verify",
|
|
14
|
+
description="Check a markdown spec or handoff doc against a repo to catch spec drift.",
|
|
15
|
+
)
|
|
16
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
17
|
+
|
|
18
|
+
check = subparsers.add_parser(
|
|
19
|
+
"check",
|
|
20
|
+
help="run repo intent verification against a markdown spec",
|
|
21
|
+
)
|
|
22
|
+
check.add_argument(
|
|
23
|
+
"--spec",
|
|
24
|
+
required=True,
|
|
25
|
+
help="path to markdown spec, intent, or handoff file",
|
|
26
|
+
)
|
|
27
|
+
check.add_argument("--repo", required=True, help="path to repo or source tree to scan")
|
|
28
|
+
check.add_argument(
|
|
29
|
+
"--section",
|
|
30
|
+
help="optional markdown heading to target, for example Requirements",
|
|
31
|
+
)
|
|
32
|
+
check.add_argument("--json", action="store_true", help="emit machine-readable JSON output")
|
|
33
|
+
check.add_argument(
|
|
34
|
+
"--min-verified",
|
|
35
|
+
type=float,
|
|
36
|
+
default=0.7,
|
|
37
|
+
help="coverage threshold an item must clear to count as verified",
|
|
38
|
+
)
|
|
39
|
+
check.add_argument(
|
|
40
|
+
"--min-item",
|
|
41
|
+
type=float,
|
|
42
|
+
default=0.3,
|
|
43
|
+
help="minimum per-item threshold before the result becomes missing",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return parser
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main(argv: list[str] | None = None) -> int:
|
|
50
|
+
parser = build_parser()
|
|
51
|
+
args = parser.parse_args(argv)
|
|
52
|
+
|
|
53
|
+
if args.command != "check":
|
|
54
|
+
parser.error("unsupported command")
|
|
55
|
+
|
|
56
|
+
spec_path = Path(args.spec)
|
|
57
|
+
repo_path = Path(args.repo)
|
|
58
|
+
if not spec_path.exists():
|
|
59
|
+
print(f"intent-verify: spec not found: {spec_path}", file=sys.stderr)
|
|
60
|
+
return 2
|
|
61
|
+
if not repo_path.exists() or not repo_path.is_dir():
|
|
62
|
+
print(f"intent-verify: repo path is not a directory: {repo_path}", file=sys.stderr)
|
|
63
|
+
return 2
|
|
64
|
+
if not 0 < args.min_item <= args.min_verified <= 1:
|
|
65
|
+
print(
|
|
66
|
+
"intent-verify: thresholds must satisfy 0 < min-item <= min-verified <= 1",
|
|
67
|
+
file=sys.stderr,
|
|
68
|
+
)
|
|
69
|
+
return 2
|
|
70
|
+
|
|
71
|
+
result = run_check(
|
|
72
|
+
spec_path,
|
|
73
|
+
repo_path,
|
|
74
|
+
section=args.section,
|
|
75
|
+
min_verified=args.min_verified,
|
|
76
|
+
min_item=args.min_item,
|
|
77
|
+
)
|
|
78
|
+
print(to_json(result) if args.json else to_text(result))
|
|
79
|
+
return {
|
|
80
|
+
Verdict.VERIFIED: 0,
|
|
81
|
+
Verdict.PARTIAL: 1,
|
|
82
|
+
Verdict.MISSING: 2,
|
|
83
|
+
}[result.verdict]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Verdict(str, Enum):
|
|
8
|
+
VERIFIED = "verified"
|
|
9
|
+
PARTIAL = "partial"
|
|
10
|
+
MISSING = "missing"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class ItemResult:
|
|
15
|
+
text: str
|
|
16
|
+
tokens: list[str]
|
|
17
|
+
coverage: float
|
|
18
|
+
verdict: Verdict
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class CheckResult:
|
|
23
|
+
spec_path: str
|
|
24
|
+
repo_path: str
|
|
25
|
+
files_scanned: int
|
|
26
|
+
items: list[ItemResult]
|
|
27
|
+
average_coverage: float
|
|
28
|
+
verdict: Verdict
|
|
29
|
+
min_verified: float
|
|
30
|
+
min_item: float
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
INLINE_RE = re.compile(r"(?:^|\n)\s*(Accepts?|Requirements?|Scope):\s*([^\n]+)", re.IGNORECASE)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_spec_items(text: str, section: str | None = None) -> list[str]:
|
|
9
|
+
items: list[str] = []
|
|
10
|
+
items.extend(_parse_inline_items(text))
|
|
11
|
+
items.extend(_parse_section_items(text, section))
|
|
12
|
+
|
|
13
|
+
deduped: list[str] = []
|
|
14
|
+
seen: set[str] = set()
|
|
15
|
+
for item in items:
|
|
16
|
+
cleaned = item.strip().strip(".")
|
|
17
|
+
key = cleaned.casefold()
|
|
18
|
+
if cleaned and key not in seen:
|
|
19
|
+
seen.add(key)
|
|
20
|
+
deduped.append(cleaned)
|
|
21
|
+
return deduped
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse_inline_items(text: str) -> list[str]:
|
|
25
|
+
items: list[str] = []
|
|
26
|
+
for match in INLINE_RE.finditer(text):
|
|
27
|
+
raw = match.group(2).strip()
|
|
28
|
+
parts = [part.strip(" .\t") for part in re.split(r",\s*|\s*;\s*", raw) if part.strip()]
|
|
29
|
+
items.extend(parts)
|
|
30
|
+
return items
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _parse_section_items(text: str, section: str | None) -> list[str]:
|
|
34
|
+
target_names = ["Accepts", "Requirements", "Scope"]
|
|
35
|
+
if section:
|
|
36
|
+
target_names = [section]
|
|
37
|
+
pattern = "|".join(re.escape(name) for name in target_names)
|
|
38
|
+
header_re = re.compile(
|
|
39
|
+
rf"(?:^|\n)#+\s*({pattern})\s*\n(.+?)(?=\n#|\Z)",
|
|
40
|
+
re.IGNORECASE | re.DOTALL,
|
|
41
|
+
)
|
|
42
|
+
items: list[str] = []
|
|
43
|
+
for match in header_re.finditer(text):
|
|
44
|
+
body = match.group(2)
|
|
45
|
+
for line in body.splitlines():
|
|
46
|
+
stripped = line.strip()
|
|
47
|
+
if not stripped:
|
|
48
|
+
continue
|
|
49
|
+
bullet = re.match(r"^[-*+]\s+(.+)$", stripped)
|
|
50
|
+
ordered = re.match(r"^\d+\.\s+(.+)$", stripped)
|
|
51
|
+
if bullet:
|
|
52
|
+
items.append(bullet.group(1).strip())
|
|
53
|
+
elif ordered:
|
|
54
|
+
items.append(ordered.group(1).strip())
|
|
55
|
+
return items
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .models import CheckResult, ItemResult, Verdict
|
|
7
|
+
from .parser import parse_spec_items
|
|
8
|
+
from .scanner import coverage_for_tokens, load_repo_blobs
|
|
9
|
+
from .tokenizer import tokenize
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def coverage_verdict(coverage: float, min_verified: float, min_item: float) -> Verdict:
|
|
13
|
+
if coverage >= min_verified:
|
|
14
|
+
return Verdict.VERIFIED
|
|
15
|
+
if coverage >= min_item:
|
|
16
|
+
return Verdict.PARTIAL
|
|
17
|
+
return Verdict.MISSING
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def aggregate_verdict(items: list[ItemResult], min_verified: float, min_item: float) -> Verdict:
|
|
21
|
+
if not items:
|
|
22
|
+
return Verdict.MISSING
|
|
23
|
+
if any(item.coverage < min_item for item in items):
|
|
24
|
+
return Verdict.MISSING
|
|
25
|
+
if any(item.coverage < min_verified for item in items):
|
|
26
|
+
return Verdict.PARTIAL
|
|
27
|
+
average = sum(item.coverage for item in items) / len(items)
|
|
28
|
+
if average < min_verified:
|
|
29
|
+
return Verdict.PARTIAL
|
|
30
|
+
return Verdict.VERIFIED
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run_check(
|
|
34
|
+
spec_path: Path,
|
|
35
|
+
repo_path: Path,
|
|
36
|
+
*,
|
|
37
|
+
section: str | None = None,
|
|
38
|
+
min_verified: float = 0.7,
|
|
39
|
+
min_item: float = 0.3,
|
|
40
|
+
) -> CheckResult:
|
|
41
|
+
text = spec_path.read_text(encoding="utf-8")
|
|
42
|
+
items = parse_spec_items(text, section=section)
|
|
43
|
+
blobs = load_repo_blobs(repo_path, exclude_paths={spec_path})
|
|
44
|
+
|
|
45
|
+
item_results: list[ItemResult] = []
|
|
46
|
+
for item in items:
|
|
47
|
+
tokens = tokenize(item)
|
|
48
|
+
coverage = coverage_for_tokens(tokens, blobs)
|
|
49
|
+
item_results.append(
|
|
50
|
+
ItemResult(
|
|
51
|
+
text=item,
|
|
52
|
+
tokens=tokens,
|
|
53
|
+
coverage=coverage,
|
|
54
|
+
verdict=coverage_verdict(coverage, min_verified, min_item),
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
average = (
|
|
59
|
+
sum(item.coverage for item in item_results) / len(item_results)
|
|
60
|
+
if item_results
|
|
61
|
+
else 0.0
|
|
62
|
+
)
|
|
63
|
+
verdict = aggregate_verdict(item_results, min_verified, min_item)
|
|
64
|
+
return CheckResult(
|
|
65
|
+
spec_path=str(spec_path),
|
|
66
|
+
repo_path=str(repo_path),
|
|
67
|
+
files_scanned=len(blobs),
|
|
68
|
+
items=item_results,
|
|
69
|
+
average_coverage=average,
|
|
70
|
+
verdict=verdict,
|
|
71
|
+
min_verified=min_verified,
|
|
72
|
+
min_item=min_item,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def to_json(result: CheckResult) -> str:
|
|
77
|
+
return json.dumps(
|
|
78
|
+
{
|
|
79
|
+
"spec_path": result.spec_path,
|
|
80
|
+
"repo_path": result.repo_path,
|
|
81
|
+
"files_scanned": result.files_scanned,
|
|
82
|
+
"average_coverage": round(result.average_coverage, 3),
|
|
83
|
+
"verdict": result.verdict.value,
|
|
84
|
+
"thresholds": {
|
|
85
|
+
"min_verified": result.min_verified,
|
|
86
|
+
"min_item": result.min_item,
|
|
87
|
+
},
|
|
88
|
+
"items": [
|
|
89
|
+
{
|
|
90
|
+
"text": item.text,
|
|
91
|
+
"tokens": item.tokens,
|
|
92
|
+
"coverage": round(item.coverage, 3),
|
|
93
|
+
"verdict": item.verdict.value,
|
|
94
|
+
}
|
|
95
|
+
for item in result.items
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
indent=2,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def to_text(result: CheckResult) -> str:
|
|
103
|
+
lines = [
|
|
104
|
+
f"intent-verify: {Path(result.spec_path).name} vs "
|
|
105
|
+
f"{result.repo_path} ({result.files_scanned} files)"
|
|
106
|
+
]
|
|
107
|
+
for item in result.items:
|
|
108
|
+
label = {
|
|
109
|
+
Verdict.VERIFIED: "OK ",
|
|
110
|
+
Verdict.PARTIAL: "PART ",
|
|
111
|
+
Verdict.MISSING: "LOW ",
|
|
112
|
+
}[item.verdict]
|
|
113
|
+
lines.append(f" [{label}{item.coverage:.0%}] {item.text}")
|
|
114
|
+
if result.verdict is Verdict.MISSING:
|
|
115
|
+
low_count = sum(1 for item in result.items if item.coverage < result.min_item)
|
|
116
|
+
lines.append(
|
|
117
|
+
f"intent-verify: MISSING — {low_count}/{len(result.items)} "
|
|
118
|
+
f"items below {result.min_item:.0%} "
|
|
119
|
+
f"(avg {result.average_coverage:.0%})"
|
|
120
|
+
)
|
|
121
|
+
elif result.verdict is Verdict.PARTIAL:
|
|
122
|
+
lines.append(f"intent-verify: PARTIAL — avg coverage {result.average_coverage:.0%}")
|
|
123
|
+
else:
|
|
124
|
+
lines.append(f"intent-verify: VERIFIED — avg coverage {result.average_coverage:.0%}")
|
|
125
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
DEFAULT_EXTENSIONS = {
|
|
7
|
+
".cfg",
|
|
8
|
+
".go",
|
|
9
|
+
".ini",
|
|
10
|
+
".js",
|
|
11
|
+
".json",
|
|
12
|
+
".jsx",
|
|
13
|
+
".md",
|
|
14
|
+
".py",
|
|
15
|
+
".rs",
|
|
16
|
+
".sh",
|
|
17
|
+
".toml",
|
|
18
|
+
".ts",
|
|
19
|
+
".tsx",
|
|
20
|
+
".yaml",
|
|
21
|
+
".yml",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
DEFAULT_SKIP_DIRS = {
|
|
25
|
+
".git",
|
|
26
|
+
".mypy_cache",
|
|
27
|
+
".next",
|
|
28
|
+
".pytest_cache",
|
|
29
|
+
".ruff_cache",
|
|
30
|
+
".venv",
|
|
31
|
+
"__pycache__",
|
|
32
|
+
"build",
|
|
33
|
+
"dist",
|
|
34
|
+
"node_modules",
|
|
35
|
+
"target",
|
|
36
|
+
"venv",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_repo_blobs(
|
|
41
|
+
repo_path: Path,
|
|
42
|
+
extensions: set[str] | None = None,
|
|
43
|
+
exclude_paths: set[Path] | None = None,
|
|
44
|
+
) -> list[tuple[str, str]]:
|
|
45
|
+
exts = extensions or DEFAULT_EXTENSIONS
|
|
46
|
+
excluded = {path.resolve() for path in (exclude_paths or set())}
|
|
47
|
+
blobs: list[tuple[str, str]] = []
|
|
48
|
+
for root, dirs, files in os.walk(repo_path):
|
|
49
|
+
dirs[:] = [
|
|
50
|
+
name
|
|
51
|
+
for name in dirs
|
|
52
|
+
if name not in DEFAULT_SKIP_DIRS and not name.startswith(".")
|
|
53
|
+
]
|
|
54
|
+
for name in files:
|
|
55
|
+
suffix = Path(name).suffix.lower()
|
|
56
|
+
if suffix not in exts:
|
|
57
|
+
continue
|
|
58
|
+
path = Path(root) / name
|
|
59
|
+
if path.resolve() in excluded:
|
|
60
|
+
continue
|
|
61
|
+
try:
|
|
62
|
+
blobs.append((str(path), path.read_text(encoding="utf-8", errors="ignore").lower()))
|
|
63
|
+
except OSError:
|
|
64
|
+
continue
|
|
65
|
+
return blobs
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def coverage_for_tokens(tokens: list[str], blobs: list[tuple[str, str]]) -> float:
|
|
69
|
+
if not tokens:
|
|
70
|
+
return 0.0
|
|
71
|
+
found: set[str] = set()
|
|
72
|
+
for _, blob in blobs:
|
|
73
|
+
for token in tokens:
|
|
74
|
+
if token in found:
|
|
75
|
+
continue
|
|
76
|
+
if token in blob:
|
|
77
|
+
found.add(token)
|
|
78
|
+
if len(found) == len(tokens):
|
|
79
|
+
break
|
|
80
|
+
return len(found) / len(tokens)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
STOP_WORDS = {
|
|
6
|
+
"a",
|
|
7
|
+
"an",
|
|
8
|
+
"and",
|
|
9
|
+
"as",
|
|
10
|
+
"at",
|
|
11
|
+
"be",
|
|
12
|
+
"but",
|
|
13
|
+
"by",
|
|
14
|
+
"for",
|
|
15
|
+
"from",
|
|
16
|
+
"has",
|
|
17
|
+
"have",
|
|
18
|
+
"in",
|
|
19
|
+
"into",
|
|
20
|
+
"is",
|
|
21
|
+
"it",
|
|
22
|
+
"its",
|
|
23
|
+
"of",
|
|
24
|
+
"on",
|
|
25
|
+
"or",
|
|
26
|
+
"that",
|
|
27
|
+
"the",
|
|
28
|
+
"this",
|
|
29
|
+
"to",
|
|
30
|
+
"via",
|
|
31
|
+
"was",
|
|
32
|
+
"were",
|
|
33
|
+
"will",
|
|
34
|
+
"with",
|
|
35
|
+
"without",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
TOKEN_RE = re.compile(r"[a-zA-Z][a-zA-Z0-9_-]{2,}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def tokenize(text: str) -> list[str]:
|
|
42
|
+
words = [word.lower() for word in TOKEN_RE.findall(text)]
|
|
43
|
+
seen: set[str] = set()
|
|
44
|
+
result: list[str] = []
|
|
45
|
+
for word in words:
|
|
46
|
+
if word in STOP_WORDS or word in seen:
|
|
47
|
+
continue
|
|
48
|
+
seen.add(word)
|
|
49
|
+
result.append(word)
|
|
50
|
+
return result
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: intent-verify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Repo intent verification and spec drift checks for markdown specs, handoffs, and codebases.
|
|
5
|
+
Author: Rolando Bosch
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: spec drift,repo intent verification,handoff verification,acceptance criteria,ci lint
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
20
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# intent-verify: repo intent verification and spec drift checks
|
|
24
|
+
|
|
25
|
+
Find spec drift fast when your repo has an `INTENT.md`, `SPEC.md`, or handoff doc but nobody knows if the code still matches it.
|
|
26
|
+
|
|
27
|
+
`intent-verify` checks a markdown spec against a repo and returns `verified`, `partial`, or `missing` so you can catch repo intent drift before review, release, or handoff.
|
|
28
|
+
|
|
29
|
+
- "My repo has an `INTENT.md` but nobody knows if the code still matches it."
|
|
30
|
+
- "Reviews catch scope drift too late."
|
|
31
|
+
- "A handoff doc says one thing and the implementation says another."
|
|
32
|
+
- "I want a cheap CI check for spec drift before merge."
|
|
33
|
+
- "We need repo intent verification without inventing another full compliance system."
|
|
34
|
+
|
|
35
|
+
Fastest install:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install intent-verify
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Fastest real usage:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
intent-verify check --spec INTENT.md --repo .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Exact outcome:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
intent-verify: INTENT.md vs . (12 files)
|
|
51
|
+
[OK 100%] uploads PDF invoices
|
|
52
|
+
[PART 50%] retries provider timeout
|
|
53
|
+
[LOW 20%] writes audit log for rejected invoices
|
|
54
|
+
intent-verify: MISSING — 1/3 items below 30% (avg 57%)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
This is a guardrail, not proof of correctness. It answers “does the implementation visibly cover the stated scope?” not “is the software correct?”
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install intent-verify
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
For local development:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install -e ".[dev]"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Common search-intent use cases
|
|
74
|
+
|
|
75
|
+
- repo intent verification
|
|
76
|
+
- spec drift check
|
|
77
|
+
- handoff verification
|
|
78
|
+
- acceptance criteria drift detection
|
|
79
|
+
- CI check for markdown spec vs code
|
|
80
|
+
|
|
81
|
+
## Usage
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
intent-verify check --spec INTENT.md --repo .
|
|
85
|
+
intent-verify check --spec SPEC.md --repo . --json
|
|
86
|
+
intent-verify check --spec docs/handoff.md --repo src --min-verified 0.75 --min-item 0.35
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## What it parses
|
|
90
|
+
|
|
91
|
+
By default it extracts items from:
|
|
92
|
+
|
|
93
|
+
- inline lines such as `Accepts: upload PDF invoices, retry on timeout`
|
|
94
|
+
- markdown sections such as `## Accepts` with bullet items
|
|
95
|
+
|
|
96
|
+
It also supports custom headings:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
intent-verify check --spec SPEC.md --section "Requirements"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Output
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
intent-verify: INTENT.md vs . (12 files)
|
|
106
|
+
[OK 100%] uploads PDF invoices
|
|
107
|
+
[PART 50%] retries provider timeout
|
|
108
|
+
[LOW 20%] writes audit log for rejected invoices
|
|
109
|
+
intent-verify: MISSING — 1/3 items below 35% (avg 57%)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
JSON mode:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
intent-verify check --spec INTENT.md --repo . --json
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Limitations
|
|
119
|
+
|
|
120
|
+
- Lexical, not semantic.
|
|
121
|
+
- Can over-credit token overlap.
|
|
122
|
+
- Can under-credit implementations expressed with different vocabulary.
|
|
123
|
+
- Best used as a CI guardrail or review hint, not as a substitute for tests and code review.
|
|
124
|
+
|
|
125
|
+
## When To Use It
|
|
126
|
+
|
|
127
|
+
- You keep project intent in markdown.
|
|
128
|
+
- You want a lightweight repo intent verification step in CI.
|
|
129
|
+
- You need a handoff verification check before merging or releasing.
|
|
130
|
+
|
|
131
|
+
## When Not To Use It
|
|
132
|
+
|
|
133
|
+
- You need semantic verification of behavior.
|
|
134
|
+
- You do not have any human-readable spec, intent, or requirements file.
|
|
135
|
+
- You want proof of correctness instead of a fast drift signal.
|
|
136
|
+
|
|
137
|
+
## More From Hermes Labs
|
|
138
|
+
|
|
139
|
+
- [csv-quality-gate](https://github.com/roli-lpci/csv-quality-gate): CSV preflight validation and batch CSV quality checks
|
|
140
|
+
|
|
141
|
+
## Development
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
ruff check .
|
|
145
|
+
python3 -m pytest -q
|
|
146
|
+
python3 -m py_compile src/intent_verify/*.py
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Repository layout
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
src/intent_verify/
|
|
153
|
+
tests/
|
|
154
|
+
examples/
|
|
155
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/intent_verify/__init__.py
|
|
5
|
+
src/intent_verify/cli.py
|
|
6
|
+
src/intent_verify/models.py
|
|
7
|
+
src/intent_verify/parser.py
|
|
8
|
+
src/intent_verify/report.py
|
|
9
|
+
src/intent_verify/scanner.py
|
|
10
|
+
src/intent_verify/tokenizer.py
|
|
11
|
+
src/intent_verify.egg-info/PKG-INFO
|
|
12
|
+
src/intent_verify.egg-info/SOURCES.txt
|
|
13
|
+
src/intent_verify.egg-info/dependency_links.txt
|
|
14
|
+
src/intent_verify.egg-info/entry_points.txt
|
|
15
|
+
src/intent_verify.egg-info/requires.txt
|
|
16
|
+
src/intent_verify.egg-info/top_level.txt
|
|
17
|
+
tests/test_cli.py
|
|
18
|
+
tests/test_parser.py
|
|
19
|
+
tests/test_scanner.py
|
|
20
|
+
tests/test_tokenizer.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
intent_verify
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run_cli(*args: str) -> subprocess.CompletedProcess[str]:
|
|
10
|
+
return subprocess.run(
|
|
11
|
+
[sys.executable, "-m", "intent_verify.cli", *args],
|
|
12
|
+
cwd=ROOT,
|
|
13
|
+
env={"PYTHONPATH": str(ROOT / "src")},
|
|
14
|
+
capture_output=True,
|
|
15
|
+
text=True,
|
|
16
|
+
check=False,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_cli_verified_fixture():
|
|
21
|
+
fixture = ROOT / "tests" / "fixtures" / "repo_ok"
|
|
22
|
+
result = run_cli("check", "--spec", str(fixture / "INTENT.md"), "--repo", str(fixture))
|
|
23
|
+
assert result.returncode == 0
|
|
24
|
+
assert "VERIFIED" in result.stdout
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_cli_partial_fixture_json():
|
|
28
|
+
fixture = ROOT / "tests" / "fixtures" / "repo_partial"
|
|
29
|
+
result = run_cli(
|
|
30
|
+
"check",
|
|
31
|
+
"--spec",
|
|
32
|
+
str(fixture / "INTENT.md"),
|
|
33
|
+
"--repo",
|
|
34
|
+
str(fixture),
|
|
35
|
+
"--json",
|
|
36
|
+
)
|
|
37
|
+
assert result.returncode == 1
|
|
38
|
+
payload = json.loads(result.stdout)
|
|
39
|
+
assert payload["verdict"] == "partial"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_cli_missing_fixture():
|
|
43
|
+
fixture = ROOT / "tests" / "fixtures" / "repo_missing"
|
|
44
|
+
result = run_cli("check", "--spec", str(fixture / "INTENT.md"), "--repo", str(fixture))
|
|
45
|
+
assert result.returncode == 2
|
|
46
|
+
assert "MISSING" in result.stdout
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from intent_verify.parser import parse_spec_items
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_parse_inline_items():
|
|
5
|
+
text = "Accepts: upload PDF invoices, retry provider timeout"
|
|
6
|
+
assert parse_spec_items(text) == ["upload PDF invoices", "retry provider timeout"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_parse_section_items():
|
|
10
|
+
text = """
|
|
11
|
+
# Spec
|
|
12
|
+
|
|
13
|
+
## Accepts
|
|
14
|
+
- uploads PDF invoices
|
|
15
|
+
- retries provider timeout
|
|
16
|
+
"""
|
|
17
|
+
assert parse_spec_items(text) == ["uploads PDF invoices", "retries provider timeout"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_parse_custom_section():
|
|
21
|
+
text = """
|
|
22
|
+
## Requirements
|
|
23
|
+
- writes audit log
|
|
24
|
+
"""
|
|
25
|
+
assert parse_spec_items(text, section="Requirements") == ["writes audit log"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from intent_verify.scanner import coverage_for_tokens, load_repo_blobs
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_load_repo_blobs_skips_non_source_files():
|
|
7
|
+
fixture = Path(__file__).parent / "fixtures" / "repo_ok"
|
|
8
|
+
blobs = load_repo_blobs(fixture)
|
|
9
|
+
assert any(path.endswith("service.py") for path, _ in blobs)
|
|
10
|
+
assert all(not path.endswith(".png") for path, _ in blobs)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_coverage_for_tokens():
|
|
14
|
+
blobs = [("a.py", "uploads pdf invoices and retries timeout")]
|
|
15
|
+
assert coverage_for_tokens(["uploads", "pdf", "timeout"], blobs) == 1.0
|