docassert 0.10.0__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.0/docassert.egg-info → docassert-0.11.0}/PKG-INFO +1 -1
- {docassert-0.10.0 → docassert-0.11.0}/docassert/__init__.py +1 -1
- {docassert-0.10.0 → docassert-0.11.0}/docassert/bridge/plan.py +19 -3
- {docassert-0.10.0 → docassert-0.11.0}/docassert/consistency.py +23 -11
- {docassert-0.10.0 → docassert-0.11.0}/docassert/semantic.py +43 -10
- {docassert-0.10.0 → docassert-0.11.0/docassert.egg-info}/PKG-INFO +1 -1
- {docassert-0.10.0 → docassert-0.11.0}/docassert.egg-info/SOURCES.txt +1 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_bridge.py +14 -0
- docassert-0.11.0/tests/test_semantic.py +122 -0
- {docassert-0.10.0 → docassert-0.11.0}/LICENSE +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/NOTICE +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/README.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/__main__.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/consistency.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/adr.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/brd.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/business-case.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/charter.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/frnfr.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/prd.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/project.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/risk-register.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/runbook.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/status-report.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/test-cases.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/schema/user-story.schema.json +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/skills/doc-to-pmo/SKILL.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/adr.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/benefits-realization.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/brd.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/business-case.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/charter.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/data-migration-plan.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/frnfr.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/hypercare-plan.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/post-implementation-review.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/prd.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/project.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/qa-test-plan.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/risk-register.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/rollback-plan.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/runbook.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/status-report.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/test-cases.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/_data/templates/user-story.template.md +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/bridge/__init__.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/bridge/board.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/bridge/gh.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/bridge/ops.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/cli.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/config.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/extract.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/graph.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/loader.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/models.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/profiles.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/projects.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/report.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/rtm.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/scaffold.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/status/__init__.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/status/derive.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/status/render.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert/structural.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert.egg-info/dependency_links.txt +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert.egg-info/entry_points.txt +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert.egg-info/requires.txt +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/docassert.egg-info/top_level.txt +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/pyproject.toml +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/setup.cfg +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_badge.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_config.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_consistency.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_defects.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_draft_relaxation.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_extract.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_graph.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_json_report.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_kinds_delivery.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_kinds_governance.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_kinds_operate.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_kinds_reporting.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_profiles.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_projects.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_scaffold.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_status.py +0 -0
- {docassert-0.10.0 → docassert-0.11.0}/tests/test_structural.py +0 -0
|
@@ -7,6 +7,7 @@ anywhere in the bridge, writes to documents/.
|
|
|
7
7
|
"""
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import os
|
|
10
11
|
from dataclasses import dataclass, field
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
|
|
@@ -54,6 +55,18 @@ class BridgePlan:
|
|
|
54
55
|
return {f.id for f in self.features} | {s.id for s in self.stories}
|
|
55
56
|
|
|
56
57
|
|
|
58
|
+
def _repo_rel(path: str, documents_dir: Path) -> str:
|
|
59
|
+
"""Path relative to the docs repo root (the parent of documents_dir).
|
|
60
|
+
|
|
61
|
+
Issue bodies link Source as {docs_url}/{doc_path}, and docs_url points at
|
|
62
|
+
the docs repository. When CI checks that repository out into a
|
|
63
|
+
subdirectory, load paths carry the checkout prefix; stripping down to the
|
|
64
|
+
repo root keeps the links valid wherever the docs are checked out.
|
|
65
|
+
"""
|
|
66
|
+
rel = os.path.relpath(path, documents_dir.parent)
|
|
67
|
+
return path if rel.startswith("..") else Path(rel).as_posix()
|
|
68
|
+
|
|
69
|
+
|
|
57
70
|
def _title(item_text: str, limit: int = 64) -> str:
|
|
58
71
|
"""First sentence of the item text, trimmed for an issue title."""
|
|
59
72
|
first = item_text.split(". ")[0].rstrip(".")
|
|
@@ -126,14 +139,17 @@ def build_bridge_plan(documents_dir: str | Path = "documents") -> BridgePlan:
|
|
|
126
139
|
feat = features.get(parent.id)
|
|
127
140
|
if feat is None:
|
|
128
141
|
feat = Feature(id=parent.id, text=parent.text,
|
|
129
|
-
doc_path=parent.doc_path,
|
|
142
|
+
doc_path=_repo_rel(parent.doc_path,
|
|
143
|
+
documents_dir),
|
|
144
|
+
project=proj_id)
|
|
130
145
|
feat.acceptance = [(a.id, a.text) for a in
|
|
131
146
|
graph.children(parent.id, "verifies", "AC")]
|
|
132
147
|
for aid, _ in feat.acceptance:
|
|
133
148
|
feat.tests += [t.id for t in graph.children(aid, "tests", "TC")]
|
|
134
149
|
features[parent.id] = feat
|
|
135
|
-
feat.stories.append(Story(
|
|
136
|
-
|
|
150
|
+
feat.stories.append(Story(
|
|
151
|
+
id=s.id, text=s.text, feature_id=parent.id,
|
|
152
|
+
doc_path=_repo_rel(s.doc_path, documents_dir)))
|
|
137
153
|
if problems:
|
|
138
154
|
plan.skipped.append({"id": proj_id,
|
|
139
155
|
"reason": "; ".join(problems)})
|
|
@@ -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)
|
|
@@ -113,6 +113,20 @@ def test_bodies_carry_marker_and_authority_note(tmp_path):
|
|
|
113
113
|
assert f"<!-- docassert-bridge: {s.id} -->" in story_body(s, None)
|
|
114
114
|
|
|
115
115
|
|
|
116
|
+
def test_doc_paths_relative_to_docs_repo_root(tmp_path):
|
|
117
|
+
"""CI checks the docs repo out into a subdirectory; Source links must not
|
|
118
|
+
carry that checkout prefix (they resolve against the docs repo root)."""
|
|
119
|
+
docs = _tree(tmp_path / "pmo")
|
|
120
|
+
plan = build_bridge_plan(docs)
|
|
121
|
+
f = plan.features[0]
|
|
122
|
+
assert f.doc_path == "documents/PRJ-001-TST/prd.md"
|
|
123
|
+
assert f.stories[0].doc_path == "documents/PRJ-001-TST/user-story.md"
|
|
124
|
+
body = feature_body(f, "https://github.com/o/r/blob/main")
|
|
125
|
+
assert ("**Source:** https://github.com/o/r/blob/main/"
|
|
126
|
+
"documents/PRJ-001-TST/prd.md") in body
|
|
127
|
+
assert "/pmo/" not in body
|
|
128
|
+
|
|
129
|
+
|
|
116
130
|
# ── ops against a fake runner ────────────────────────────────────────────────
|
|
117
131
|
class FakeGh:
|
|
118
132
|
"""Records mutations; serves canned issue lists."""
|
|
@@ -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.0 → 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.0 → 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.0 → docassert-0.11.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml
RENAMED
|
File without changes
|
{docassert-0.10.0 → 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.0 → 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.0 → 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.0 → 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.0 → 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.0 → 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.0 → 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.0 → docassert-0.11.0}/docassert/_data/templates/raci-stakeholder.template.md
RENAMED
|
File without changes
|
{docassert-0.10.0 → 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
|