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.
@@ -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
+ ![intent-verify preview](assets/preview.png)
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
+ ![intent-verify preview](assets/preview.png)
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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from .report import CheckResult, ItemResult, Verdict, run_check
2
+
3
+ __all__ = ["CheckResult", "ItemResult", "Verdict", "run_check"]
@@ -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
+ ![intent-verify preview](assets/preview.png)
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,2 @@
1
+ [console_scripts]
2
+ intent-verify = intent_verify.cli:main
@@ -0,0 +1,4 @@
1
+
2
+ [dev]
3
+ pytest>=8.0
4
+ ruff>=0.5
@@ -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
@@ -0,0 +1,10 @@
1
+ from intent_verify.tokenizer import tokenize
2
+
3
+
4
+ def test_tokenize_dedupes_and_drops_stop_words():
5
+ assert tokenize("Accepts uploaded PDF invoices and invoices") == [
6
+ "accepts",
7
+ "uploaded",
8
+ "pdf",
9
+ "invoices",
10
+ ]