docassert 0.10.1__tar.gz → 0.11.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.
- {docassert-0.10.1/docassert.egg-info → docassert-0.11.0}/PKG-INFO +1 -1
- {docassert-0.10.1 → docassert-0.11.0}/docassert/__init__.py +1 -1
- {docassert-0.10.1 → docassert-0.11.0}/docassert/consistency.py +23 -11
- {docassert-0.10.1 → docassert-0.11.0}/docassert/semantic.py +43 -10
- {docassert-0.10.1 → docassert-0.11.0/docassert.egg-info}/PKG-INFO +1 -1
- {docassert-0.10.1 → docassert-0.11.0}/docassert.egg-info/SOURCES.txt +1 -0
- docassert-0.11.0/tests/test_semantic.py +122 -0
- {docassert-0.10.1 → docassert-0.11.0}/LICENSE +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/NOTICE +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/README.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/__main__.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/consistency.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/adr.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/brd.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/business-case.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/charter.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/frnfr.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/prd.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/project.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/risk-register.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/runbook.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/status-report.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/test-cases.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/user-story.schema.json +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/skills/doc-to-pmo/SKILL.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/adr.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/benefits-realization.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/brd.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/business-case.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/charter.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/data-migration-plan.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/frnfr.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/hypercare-plan.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/post-implementation-review.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/prd.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/project.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/qa-test-plan.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/risk-register.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/rollback-plan.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/runbook.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/status-report.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/test-cases.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/user-story.template.md +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/bridge/__init__.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/bridge/board.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/bridge/gh.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/bridge/ops.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/bridge/plan.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/cli.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/config.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/extract.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/graph.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/loader.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/models.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/profiles.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/projects.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/report.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/rtm.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/scaffold.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/status/__init__.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/status/derive.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/status/render.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert/structural.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert.egg-info/dependency_links.txt +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert.egg-info/entry_points.txt +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert.egg-info/requires.txt +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/docassert.egg-info/top_level.txt +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/pyproject.toml +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/setup.cfg +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_badge.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_bridge.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_config.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_consistency.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_defects.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_draft_relaxation.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_extract.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_graph.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_json_report.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_kinds_delivery.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_kinds_governance.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_kinds_operate.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_kinds_reporting.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_profiles.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_projects.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_scaffold.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_status.py +0 -0
- {docassert-0.10.1 → docassert-0.11.0}/tests/test_structural.py +0 -0
|
@@ -18,6 +18,7 @@ from pathlib import Path
|
|
|
18
18
|
import yaml
|
|
19
19
|
|
|
20
20
|
from . import config as config_mod
|
|
21
|
+
from . import semantic
|
|
21
22
|
from .graph import build_graph
|
|
22
23
|
from .models import CheckResult
|
|
23
24
|
from .semantic import run_alignment
|
|
@@ -134,7 +135,9 @@ def check_profile_completeness(documents_dir: str | Path = "documents") -> Check
|
|
|
134
135
|
# ── semantic (advisory) ────────────────────────────────────────────────────
|
|
135
136
|
# Each alignment edge costs one API call, so a large graph could otherwise run
|
|
136
137
|
# away on cost. Cap per run; tune with `alignment_limit` in consistency.yaml
|
|
137
|
-
# (0 disables the cap).
|
|
138
|
+
# (0 disables the cap). The cap budgets API calls, not links: grades already
|
|
139
|
+
# in the semantic cache replay for free, so successive runs that persist
|
|
140
|
+
# `.docassert-cache` walk the whole graph `alignment_limit` links at a time.
|
|
138
141
|
DEFAULT_ALIGNMENT_LIMIT = 25
|
|
139
142
|
|
|
140
143
|
|
|
@@ -151,20 +154,29 @@ def run_alignment_checks(graph, config) -> list[CheckResult]:
|
|
|
151
154
|
if not edges:
|
|
152
155
|
return []
|
|
153
156
|
|
|
154
|
-
limit = int(config.get("alignment_limit", DEFAULT_ALIGNMENT_LIMIT) or 0)
|
|
155
|
-
note: CheckResult | None = None
|
|
156
|
-
if limit and len(edges) > limit:
|
|
157
|
-
note = CheckResult(
|
|
158
|
-
"alignment-limit", True, False,
|
|
159
|
-
f"graded {limit} of {len(edges)} link(s) — raise `alignment_limit` "
|
|
160
|
-
f"in consistency.yaml to grade more per run",
|
|
161
|
-
kind="semantic", score=None)
|
|
162
|
-
edges = edges[:limit]
|
|
163
|
-
|
|
164
157
|
if not os.environ.get("ANTHROPIC_API_KEY"):
|
|
165
158
|
return [CheckResult("alignment", True, False,
|
|
166
159
|
f"skipped — no ANTHROPIC_API_KEY ({len(edges)} link(s) to grade)",
|
|
167
160
|
kind="semantic", score=None)]
|
|
161
|
+
|
|
162
|
+
limit = int(config.get("alignment_limit", DEFAULT_ALIGNMENT_LIMIT) or 0)
|
|
163
|
+
note: CheckResult | None = None
|
|
164
|
+
if limit:
|
|
165
|
+
# Spend the budget on cache misses; cached grades replay for free.
|
|
166
|
+
fresh = [e for e in edges
|
|
167
|
+
if not semantic.is_cached(e[0], semantic.alignment_content(
|
|
168
|
+
e[1].text, e[2].text))]
|
|
169
|
+
if len(fresh) > limit:
|
|
170
|
+
over_budget = set(id(e) for e in fresh[limit:])
|
|
171
|
+
edges = [e for e in edges if id(e) not in over_budget]
|
|
172
|
+
note = CheckResult(
|
|
173
|
+
"alignment-limit", True, False,
|
|
174
|
+
f"graded {limit} of {len(fresh)} ungraded link(s) "
|
|
175
|
+
f"({len(edges) - limit} from cache) — raise `alignment_limit` in "
|
|
176
|
+
f"consistency.yaml, or persist `.docassert-cache` between runs "
|
|
177
|
+
f"to walk the rest",
|
|
178
|
+
kind="semantic", score=None)
|
|
179
|
+
|
|
168
180
|
results = [run_alignment(f"align:{c.id}-{rel}-{p.id}", prompt, p.text, c.text)
|
|
169
181
|
for prompt, p, c, rel in edges]
|
|
170
182
|
if note is not None:
|
|
@@ -9,12 +9,15 @@ from __future__ import annotations
|
|
|
9
9
|
import hashlib
|
|
10
10
|
import json
|
|
11
11
|
import os
|
|
12
|
+
import re
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
|
|
14
15
|
from .models import CheckResult, Document
|
|
15
16
|
|
|
16
|
-
DEFAULT_MODEL = os.environ.get("
|
|
17
|
-
|
|
17
|
+
DEFAULT_MODEL = (os.environ.get("DOCASSERT_MODEL")
|
|
18
|
+
or os.environ.get("DOCUNIT_MODEL", "claude-sonnet-5"))
|
|
19
|
+
CACHE_DIR = Path(os.environ.get("DOCASSERT_CACHE")
|
|
20
|
+
or os.environ.get("DOCUNIT_CACHE", ".docassert-cache"))
|
|
18
21
|
|
|
19
22
|
_SYSTEM = (
|
|
20
23
|
"You are a meticulous document auditor. You are given one audit criterion "
|
|
@@ -53,6 +56,31 @@ def _cache_put(key: str, value: dict) -> None:
|
|
|
53
56
|
pass # caching is best-effort
|
|
54
57
|
|
|
55
58
|
|
|
59
|
+
def _parse_grade(text: str) -> dict:
|
|
60
|
+
"""Parse the model's JSON grade, tolerating prose wrappers and truncation.
|
|
61
|
+
|
|
62
|
+
Responses put score and pass before the free-text rationale, so a reply
|
|
63
|
+
that ran out of tokens mid-rationale still carries a usable grade.
|
|
64
|
+
"""
|
|
65
|
+
start, end = text.find("{"), text.rfind("}")
|
|
66
|
+
if start != -1 and end > start:
|
|
67
|
+
try:
|
|
68
|
+
return json.loads(text[start:end + 1])
|
|
69
|
+
except json.JSONDecodeError:
|
|
70
|
+
pass
|
|
71
|
+
m = re.search(r'"score"\s*:\s*([0-9.]+)', text)
|
|
72
|
+
if m:
|
|
73
|
+
grade: dict = {"score": float(m.group(1))}
|
|
74
|
+
pm = re.search(r'"pass"\s*:\s*(true|false)', text)
|
|
75
|
+
if pm:
|
|
76
|
+
grade["pass"] = pm.group(1) == "true"
|
|
77
|
+
rm = re.search(r'"rationale"\s*:\s*"(.*)', text, re.DOTALL)
|
|
78
|
+
if rm:
|
|
79
|
+
grade["rationale"] = rm.group(1).rstrip('"').strip() + " [truncated]"
|
|
80
|
+
return grade
|
|
81
|
+
raise ValueError(f"no JSON in model response: {text[:120]!r}")
|
|
82
|
+
|
|
83
|
+
|
|
56
84
|
def _grade(prompt: str, content: str, model: str) -> dict:
|
|
57
85
|
"""Call the Anthropic API and return the parsed JSON grade."""
|
|
58
86
|
import anthropic # imported lazily so structural-only runs need no dependency
|
|
@@ -62,7 +90,7 @@ def _grade(prompt: str, content: str, model: str) -> dict:
|
|
|
62
90
|
# not set it. Grading is kept consistent by the strict JSON rubric instead.
|
|
63
91
|
message = client.messages.create(
|
|
64
92
|
model=model,
|
|
65
|
-
max_tokens=
|
|
93
|
+
max_tokens=800,
|
|
66
94
|
system=_SYSTEM,
|
|
67
95
|
messages=[{
|
|
68
96
|
"role": "user",
|
|
@@ -71,11 +99,7 @@ def _grade(prompt: str, content: str, model: str) -> dict:
|
|
|
71
99
|
)
|
|
72
100
|
text = "".join(getattr(block, "text", "") for block in message.content
|
|
73
101
|
if getattr(block, "type", None) == "text").strip()
|
|
74
|
-
|
|
75
|
-
start, end = text.find("{"), text.rfind("}")
|
|
76
|
-
if start == -1 or end == -1:
|
|
77
|
-
raise ValueError(f"no JSON in model response: {text[:120]!r}")
|
|
78
|
-
return json.loads(text[start:end + 1])
|
|
102
|
+
return _parse_grade(text)
|
|
79
103
|
|
|
80
104
|
|
|
81
105
|
def advisory(check_id: str, prompt: str, content: str,
|
|
@@ -117,8 +141,17 @@ def run_semantic(doc: Document, spec: dict, content: str) -> CheckResult:
|
|
|
117
141
|
)
|
|
118
142
|
|
|
119
143
|
|
|
144
|
+
def alignment_content(parent_text: str, child_text: str) -> str:
|
|
145
|
+
return f"PARENT:\n{parent_text}\n\nCHILD:\n{child_text}"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def is_cached(prompt: str, content: str, model: str = DEFAULT_MODEL) -> bool:
|
|
149
|
+
"""True when this grade can be replayed without an API call."""
|
|
150
|
+
return _cache_get(_cache_key(model, prompt, content)) is not None
|
|
151
|
+
|
|
152
|
+
|
|
120
153
|
def run_alignment(check_id: str, prompt: str, parent_text: str,
|
|
121
154
|
child_text: str, threshold: float = 0.7) -> CheckResult:
|
|
122
155
|
"""Advisory: does the child item genuinely fulfil the parent it links to?"""
|
|
123
|
-
|
|
124
|
-
|
|
156
|
+
return advisory(check_id, prompt, alignment_content(parent_text, child_text),
|
|
157
|
+
threshold=threshold)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Tests for the advisory grading layer: parsing, caching, and the API budget."""
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from docassert import consistency, semantic
|
|
7
|
+
from docassert.models import Item
|
|
8
|
+
from docassert.semantic import _parse_grade
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ── _parse_grade ─────────────────────────────────────────────────────────────
|
|
12
|
+
def test_parse_grade_clean_json():
|
|
13
|
+
g = _parse_grade('{"score": 0.9, "pass": true, "rationale": "solid"}')
|
|
14
|
+
assert g == {"score": 0.9, "pass": True, "rationale": "solid"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_parse_grade_tolerates_prose_and_fences():
|
|
18
|
+
g = _parse_grade('Here you go:\n```json\n{"score": 0.4, "pass": false, "rationale": "weak"}\n```')
|
|
19
|
+
assert g["score"] == 0.4 and g["pass"] is False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_parse_grade_recovers_truncated_response():
|
|
23
|
+
# The exact failure shape seen live: max_tokens cut the rationale short.
|
|
24
|
+
text = '{"score": 0.35, "pass": false, "rationale": "The child only refines the \'2 seconds\' timing aspect (backend fingerprint r'
|
|
25
|
+
g = _parse_grade(text)
|
|
26
|
+
assert g["score"] == 0.35
|
|
27
|
+
assert g["pass"] is False
|
|
28
|
+
assert g["rationale"].endswith("[truncated]")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_parse_grade_truncated_without_pass_leaves_it_to_threshold():
|
|
32
|
+
g = _parse_grade('{"score": 0.8, "ratio')
|
|
33
|
+
assert g["score"] == 0.8 and "pass" not in g
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_parse_grade_raises_on_garbage():
|
|
37
|
+
with pytest.raises(ValueError):
|
|
38
|
+
_parse_grade("I cannot grade this.")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── cache replay ─────────────────────────────────────────────────────────────
|
|
42
|
+
def _use_tmp_cache(tmp_path, monkeypatch):
|
|
43
|
+
monkeypatch.setattr(semantic, "CACHE_DIR", tmp_path / "cache")
|
|
44
|
+
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_advisory_caches_grades(tmp_path, monkeypatch):
|
|
48
|
+
_use_tmp_cache(tmp_path, monkeypatch)
|
|
49
|
+
calls = []
|
|
50
|
+
monkeypatch.setattr(semantic, "_grade", lambda p, c, m: (
|
|
51
|
+
calls.append(1) or {"score": 1.0, "pass": True, "rationale": "ok"}))
|
|
52
|
+
for _ in range(2):
|
|
53
|
+
r = semantic.advisory("chk", "prompt", "content")
|
|
54
|
+
assert r.passed and r.score == 1.0
|
|
55
|
+
assert len(calls) == 1
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_advisory_does_not_cache_failures(tmp_path, monkeypatch):
|
|
59
|
+
_use_tmp_cache(tmp_path, monkeypatch)
|
|
60
|
+
calls = []
|
|
61
|
+
|
|
62
|
+
def boom(p, c, m):
|
|
63
|
+
calls.append(1)
|
|
64
|
+
raise ValueError("no JSON in model response: ''")
|
|
65
|
+
|
|
66
|
+
monkeypatch.setattr(semantic, "_grade", boom)
|
|
67
|
+
for _ in range(2):
|
|
68
|
+
r = semantic.advisory("chk", "prompt", "content")
|
|
69
|
+
assert "unavailable" in r.detail
|
|
70
|
+
assert len(calls) == 2 # transient errors retry on the next run
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── the alignment budget spends on cache misses ──────────────────────────────
|
|
74
|
+
def _alignment_graph(n):
|
|
75
|
+
from docassert.graph import Graph
|
|
76
|
+
g = Graph()
|
|
77
|
+
g.add(Item("TST-PR-001", "TST", "PR", "the parent requirement", {},
|
|
78
|
+
"d.md", "prd", "approved", "S"))
|
|
79
|
+
for i in range(1, n + 1):
|
|
80
|
+
g.add(Item(f"TST-US-{i:03d}", "TST", "US", f"story {i}",
|
|
81
|
+
{"traces": ["TST-PR-001"]}, "d.md", "user-story",
|
|
82
|
+
"approved", "S"))
|
|
83
|
+
return g
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_alignment_budget_walks_the_graph_across_runs(tmp_path, monkeypatch):
|
|
87
|
+
_use_tmp_cache(tmp_path, monkeypatch)
|
|
88
|
+
calls = []
|
|
89
|
+
monkeypatch.setattr(semantic, "_grade", lambda p, c, m: (
|
|
90
|
+
calls.append(c) or {"score": 1.0, "pass": True, "rationale": "ok"}))
|
|
91
|
+
graph = _alignment_graph(5)
|
|
92
|
+
config = {"alignment": [{"relation": "traces", "prompt": "aligned?"}],
|
|
93
|
+
"alignment_limit": 2}
|
|
94
|
+
|
|
95
|
+
first = consistency.run_alignment_checks(graph, config)
|
|
96
|
+
assert len(calls) == 2 # budget spent on misses
|
|
97
|
+
assert any(r.check_id == "alignment-limit" and "2 of 5" in r.detail
|
|
98
|
+
for r in first)
|
|
99
|
+
|
|
100
|
+
second = consistency.run_alignment_checks(graph, config)
|
|
101
|
+
assert len(calls) == 4 # next two misses, not repeats
|
|
102
|
+
assert len(set(map(json.dumps, calls))) == 4
|
|
103
|
+
graded = [r for r in second if r.check_id.startswith("align:")]
|
|
104
|
+
assert len(graded) == 4 # 2 cached replays + 2 new
|
|
105
|
+
|
|
106
|
+
third = consistency.run_alignment_checks(graph, config)
|
|
107
|
+
assert len(calls) == 5 # the last miss
|
|
108
|
+
assert not any(r.check_id == "alignment-limit" for r in third)
|
|
109
|
+
graded = [r for r in third if r.check_id.startswith("align:")]
|
|
110
|
+
assert len(graded) == 5 # full coverage reached
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_alignment_limit_zero_grades_everything(tmp_path, monkeypatch):
|
|
114
|
+
_use_tmp_cache(tmp_path, monkeypatch)
|
|
115
|
+
calls = []
|
|
116
|
+
monkeypatch.setattr(semantic, "_grade", lambda p, c, m: (
|
|
117
|
+
calls.append(1) or {"score": 1.0, "pass": True, "rationale": "ok"}))
|
|
118
|
+
graph = _alignment_graph(4)
|
|
119
|
+
config = {"alignment": [{"relation": "traces", "prompt": "aligned?"}],
|
|
120
|
+
"alignment_limit": 0}
|
|
121
|
+
consistency.run_alignment_checks(graph, config)
|
|
122
|
+
assert len(calls) == 4
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/benefits-realization.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml
RENAMED
|
File without changes
|
{docassert-0.10.1 → docassert-0.11.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/benefits-realization.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/data-migration-plan.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/post-implementation-review.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.10.1 → docassert-0.11.0}/docassert/_data/schema/release-cutover-plan.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/benefits-realization.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/data-migration-plan.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/raci-stakeholder.template.md
RENAMED
|
File without changes
|
{docassert-0.10.1 → docassert-0.11.0}/docassert/_data/templates/release-cutover-plan.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|