docassert 0.1.0__py3-none-any.whl

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.
Files changed (86) hide show
  1. docassert/__init__.py +8 -0
  2. docassert/__main__.py +6 -0
  3. docassert/_data/consistency.yaml +51 -0
  4. docassert/_data/criteria/adr.criteria.yaml +36 -0
  5. docassert/_data/criteria/benefits-realization.criteria.yaml +30 -0
  6. docassert/_data/criteria/brd.criteria.yaml +30 -0
  7. docassert/_data/criteria/business-case.criteria.yaml +23 -0
  8. docassert/_data/criteria/charter.criteria.yaml +73 -0
  9. docassert/_data/criteria/data-migration-plan.criteria.yaml +28 -0
  10. docassert/_data/criteria/frnfr.criteria.yaml +31 -0
  11. docassert/_data/criteria/hypercare-plan.criteria.yaml +27 -0
  12. docassert/_data/criteria/post-implementation-review.criteria.yaml +24 -0
  13. docassert/_data/criteria/prd.criteria.yaml +31 -0
  14. docassert/_data/criteria/project.criteria.yaml +32 -0
  15. docassert/_data/criteria/qa-test-plan.criteria.yaml +27 -0
  16. docassert/_data/criteria/raci-stakeholder.criteria.yaml +24 -0
  17. docassert/_data/criteria/release-cutover-plan.criteria.yaml +30 -0
  18. docassert/_data/criteria/risk-register.criteria.yaml +32 -0
  19. docassert/_data/criteria/rollback-plan.criteria.yaml +29 -0
  20. docassert/_data/criteria/runbook.criteria.yaml +30 -0
  21. docassert/_data/criteria/status-report.criteria.yaml +26 -0
  22. docassert/_data/criteria/test-cases.criteria.yaml +28 -0
  23. docassert/_data/criteria/user-story.criteria.yaml +32 -0
  24. docassert/_data/profiles/agile-delivery.yaml +20 -0
  25. docassert/_data/profiles/lean-startup.yaml +19 -0
  26. docassert/_data/profiles/regulated-industry.yaml +31 -0
  27. docassert/_data/schema/adr.schema.json +45 -0
  28. docassert/_data/schema/benefits-realization.schema.json +45 -0
  29. docassert/_data/schema/brd.schema.json +45 -0
  30. docassert/_data/schema/business-case.schema.json +45 -0
  31. docassert/_data/schema/charter.schema.json +84 -0
  32. docassert/_data/schema/data-migration-plan.schema.json +45 -0
  33. docassert/_data/schema/frnfr.schema.json +45 -0
  34. docassert/_data/schema/hypercare-plan.schema.json +45 -0
  35. docassert/_data/schema/post-implementation-review.schema.json +45 -0
  36. docassert/_data/schema/prd.schema.json +45 -0
  37. docassert/_data/schema/project.schema.json +32 -0
  38. docassert/_data/schema/qa-test-plan.schema.json +45 -0
  39. docassert/_data/schema/raci-stakeholder.schema.json +45 -0
  40. docassert/_data/schema/release-cutover-plan.schema.json +45 -0
  41. docassert/_data/schema/risk-register.schema.json +45 -0
  42. docassert/_data/schema/rollback-plan.schema.json +45 -0
  43. docassert/_data/schema/runbook.schema.json +45 -0
  44. docassert/_data/schema/status-report.schema.json +58 -0
  45. docassert/_data/schema/test-cases.schema.json +45 -0
  46. docassert/_data/schema/user-story.schema.json +45 -0
  47. docassert/_data/templates/adr.template.md +17 -0
  48. docassert/_data/templates/benefits-realization.template.md +25 -0
  49. docassert/_data/templates/brd.template.md +22 -0
  50. docassert/_data/templates/business-case.template.md +27 -0
  51. docassert/_data/templates/charter.template.md +46 -0
  52. docassert/_data/templates/data-migration-plan.template.md +35 -0
  53. docassert/_data/templates/frnfr.template.md +19 -0
  54. docassert/_data/templates/hypercare-plan.template.md +29 -0
  55. docassert/_data/templates/post-implementation-review.template.md +31 -0
  56. docassert/_data/templates/prd.template.md +23 -0
  57. docassert/_data/templates/project.template.md +17 -0
  58. docassert/_data/templates/qa-test-plan.template.md +31 -0
  59. docassert/_data/templates/raci-stakeholder.template.md +21 -0
  60. docassert/_data/templates/release-cutover-plan.template.md +28 -0
  61. docassert/_data/templates/risk-register.template.md +18 -0
  62. docassert/_data/templates/rollback-plan.template.md +24 -0
  63. docassert/_data/templates/runbook.template.md +28 -0
  64. docassert/_data/templates/status-report.template.md +27 -0
  65. docassert/_data/templates/test-cases.template.md +17 -0
  66. docassert/_data/templates/user-story.template.md +17 -0
  67. docassert/cli.py +291 -0
  68. docassert/config.py +104 -0
  69. docassert/consistency.py +167 -0
  70. docassert/graph.py +68 -0
  71. docassert/loader.py +116 -0
  72. docassert/models.py +99 -0
  73. docassert/profiles.py +111 -0
  74. docassert/projects.py +49 -0
  75. docassert/report.py +83 -0
  76. docassert/rtm.py +70 -0
  77. docassert/semantic.py +124 -0
  78. docassert/status.py +538 -0
  79. docassert/structural.py +406 -0
  80. docassert-0.1.0.dist-info/METADATA +125 -0
  81. docassert-0.1.0.dist-info/RECORD +86 -0
  82. docassert-0.1.0.dist-info/WHEEL +5 -0
  83. docassert-0.1.0.dist-info/entry_points.txt +2 -0
  84. docassert-0.1.0.dist-info/licenses/LICENSE +201 -0
  85. docassert-0.1.0.dist-info/licenses/NOTICE +4 -0
  86. docassert-0.1.0.dist-info/top_level.txt +1 -0
docassert/report.py ADDED
@@ -0,0 +1,83 @@
1
+ """Render check results as console text, PR-comment markdown, or JUnit XML."""
2
+ from __future__ import annotations
3
+
4
+ import xml.etree.ElementTree as ET
5
+ from xml.dom import minidom
6
+
7
+ from .models import CheckResult
8
+
9
+ _TICK = "✓"
10
+ _CROSS = "✗"
11
+ _DASH = "—"
12
+
13
+
14
+ def _mark(r: CheckResult) -> str:
15
+ if r.kind == "semantic" and r.score is None:
16
+ return "○" # ○ skipped/unavailable
17
+ return _TICK if r.passed else _CROSS
18
+
19
+
20
+ def console(results_by_doc: dict[str, list[CheckResult]]) -> str:
21
+ lines: list[str] = []
22
+ for path, results in results_by_doc.items():
23
+ lines.append(f"\n{path}")
24
+ for r in results:
25
+ tier = "structural" if r.kind == "structural" else "advisory "
26
+ gate = "BLOCK" if r.is_blocking_failure else " "
27
+ score = f" [{r.score:.2f}]" if r.score is not None else ""
28
+ lines.append(f" {_mark(r)} {gate} {tier} {r.check_id}{score}: {r.detail}")
29
+ return "\n".join(lines)
30
+
31
+
32
+ def summary_line(results_by_doc: dict[str, list[CheckResult]]) -> str:
33
+ blocking = sum(1 for rs in results_by_doc.values() for r in rs if r.is_blocking_failure)
34
+ docs = len(results_by_doc)
35
+ if blocking:
36
+ return f"{_CROSS} {blocking} blocking failure(s) across {docs} document(s) {_DASH} merge blocked."
37
+ return f"{_TICK} All structural checks passed across {docs} document(s) {_DASH} clear to merge."
38
+
39
+
40
+ def markdown(results_by_doc: dict[str, list[CheckResult]],
41
+ title: str = "docassert audit") -> str:
42
+ """PR-comment body."""
43
+ out = [f"## {title}", "", summary_line(results_by_doc), ""]
44
+ for path, results in results_by_doc.items():
45
+ out.append(f"### `{path}`")
46
+ out.append("")
47
+ out.append("| | Check | Tier | Result |")
48
+ out.append("|---|---|---|---|")
49
+ for r in results:
50
+ tier = "structural (blocking)" if r.kind == "structural" else "AI advisory"
51
+ score = f" · score {r.score:.2f}" if r.score is not None else ""
52
+ if r.kind == "semantic" and r.score is None:
53
+ emoji = "⚪" # advisory check skipped/unavailable
54
+ elif r.passed:
55
+ emoji = "🟢"
56
+ else:
57
+ emoji = "🔴"
58
+ out.append(f"| {emoji} | `{r.check_id}` | {tier} | {r.detail}{score} |")
59
+ out.append("")
60
+ out.append("<sub>Structural checks block the merge. AI advisory checks inform "
61
+ "reviewers but never block. Configure criteria in `criteria/`.</sub>")
62
+ return "\n".join(out)
63
+
64
+
65
+ def junit(results_by_doc: dict[str, list[CheckResult]]) -> str:
66
+ total = sum(len(rs) for rs in results_by_doc.values())
67
+ failures = sum(1 for rs in results_by_doc.values()
68
+ for r in rs if r.is_blocking_failure)
69
+ suites = ET.Element("testsuites", tests=str(total), failures=str(failures))
70
+ for path, results in results_by_doc.items():
71
+ suite = ET.SubElement(suites, "testsuite", name=path,
72
+ tests=str(len(results)),
73
+ failures=str(sum(1 for r in results if r.is_blocking_failure)))
74
+ for r in results:
75
+ case = ET.SubElement(suite, "testcase",
76
+ classname=f"{path}:{r.kind}", name=r.check_id)
77
+ if r.is_blocking_failure:
78
+ ET.SubElement(case, "failure", message=r.detail).text = r.detail
79
+ elif r.kind == "semantic" and not r.passed:
80
+ # advisory failures surface as skipped, never as build failures
81
+ ET.SubElement(case, "skipped", message=r.detail)
82
+ xml = ET.tostring(suites, encoding="unicode")
83
+ return minidom.parseString(xml).toprettyxml(indent=" ")
docassert/rtm.py ADDED
@@ -0,0 +1,70 @@
1
+ """Generate a Requirements Traceability Matrix from the item graph.
2
+
3
+ Derived, never authored: it walks BR → PR → FR/NFR → AC → TC and renders the
4
+ coverage as a table, so gaps are visible at a glance.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import csv
9
+ import io
10
+
11
+
12
+ def _cell(ids: set[str]) -> str:
13
+ return ", ".join(sorted(ids)) if ids else "—"
14
+
15
+
16
+ def build_rows(graph, project: str | None = None) -> list[dict]:
17
+ rows: list[dict] = []
18
+ brs = graph.by_type.get("BR", [])
19
+ if project:
20
+ brs = [b for b in brs if b.project == project]
21
+ for br in sorted(brs, key=lambda i: i.id):
22
+ prs = graph.children(br.id, "traces", "PR")
23
+ fr_ids, ac_ids, tc_ids = set(), set(), set()
24
+ for pr in prs:
25
+ for src in graph.children(pr.id, "traces"):
26
+ if src.type in {"FR", "NFR"}:
27
+ fr_ids.add(src.id)
28
+ for ac in graph.children(pr.id, "verifies", "AC"):
29
+ ac_ids.add(ac.id)
30
+ for tc in graph.children(ac.id, "tests", "TC"):
31
+ tc_ids.add(tc.id)
32
+ rows.append({
33
+ "BR": br.id,
34
+ "PR": {p.id for p in prs},
35
+ "FR/NFR": fr_ids,
36
+ "AC": ac_ids,
37
+ "TC": tc_ids,
38
+ })
39
+ return rows
40
+
41
+
42
+ _COLS = ["BR", "PR", "FR/NFR", "AC", "TC"]
43
+ _HEADERS = ["Business Req", "Product Req", "Functional / NFR",
44
+ "Acceptance Criteria", "Test Cases"]
45
+
46
+
47
+ def render_markdown(graph, project: str | None = None) -> str:
48
+ rows = build_rows(graph, project)
49
+ title = f"# Requirements Traceability Matrix — {project}" if project else "# Requirements Traceability Matrix"
50
+ out = [title, ""]
51
+ out.append("| " + " | ".join(_HEADERS) + " |")
52
+ out.append("|" + "|".join(["---"] * len(_HEADERS)) + "|")
53
+ for r in rows:
54
+ out.append("| " + " | ".join(
55
+ [r["BR"]] + [_cell(r[c]) for c in _COLS[1:]]) + " |")
56
+ uncovered = [r["BR"] for r in rows if not r["PR"]]
57
+ out += ["",
58
+ f"_{len(rows)} business requirement(s); "
59
+ f"{len(uncovered)} with no product requirement"
60
+ + (": " + ", ".join(uncovered) if uncovered else "") + "._"]
61
+ return "\n".join(out) + "\n"
62
+
63
+
64
+ def render_csv(graph, project: str | None = None) -> str:
65
+ buf = io.StringIO()
66
+ writer = csv.writer(buf)
67
+ writer.writerow(_HEADERS)
68
+ for r in build_rows(graph, project):
69
+ writer.writerow([r["BR"]] + [_cell(r[c]) for c in _COLS[1:]])
70
+ return buf.getvalue()
docassert/semantic.py ADDED
@@ -0,0 +1,124 @@
1
+ """AI-graded semantic checks. These are ADVISORY — they never block a merge.
2
+
3
+ Each check asks a model to score one rubric criterion against the document and
4
+ return structured JSON: {score: 0..1, pass: bool, rationale: str}. Results are
5
+ cached by content hash so unchanged documents are not re-billed.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+
14
+ from .models import CheckResult, Document
15
+
16
+ DEFAULT_MODEL = os.environ.get("DOCUNIT_MODEL", "claude-sonnet-5")
17
+ CACHE_DIR = Path(os.environ.get("DOCUNIT_CACHE", ".docassert-cache"))
18
+
19
+ _SYSTEM = (
20
+ "You are a meticulous document auditor. You are given one audit criterion "
21
+ "and a business document. Judge only that criterion. Respond with a single "
22
+ "JSON object and nothing else: "
23
+ '{"score": <number 0..1>, "pass": <true|false>, "rationale": "<one or two '
24
+ 'sentences citing specifics from the document>"}.'
25
+ )
26
+
27
+
28
+ def _cache_key(model: str, prompt: str, content: str) -> str:
29
+ h = hashlib.sha256()
30
+ h.update(model.encode())
31
+ h.update(b"\x00")
32
+ h.update(prompt.encode())
33
+ h.update(b"\x00")
34
+ h.update(content.encode())
35
+ return h.hexdigest()
36
+
37
+
38
+ def _cache_get(key: str) -> dict | None:
39
+ f = CACHE_DIR / f"{key}.json"
40
+ if f.exists():
41
+ try:
42
+ return json.loads(f.read_text())
43
+ except (OSError, json.JSONDecodeError):
44
+ return None
45
+ return None
46
+
47
+
48
+ def _cache_put(key: str, value: dict) -> None:
49
+ try:
50
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
51
+ (CACHE_DIR / f"{key}.json").write_text(json.dumps(value))
52
+ except OSError:
53
+ pass # caching is best-effort
54
+
55
+
56
+ def _grade(prompt: str, content: str, model: str) -> dict:
57
+ """Call the Anthropic API and return the parsed JSON grade."""
58
+ import anthropic # imported lazily so structural-only runs need no dependency
59
+
60
+ client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
61
+ # Note: newer Claude models deprecate the `temperature` parameter, so we do
62
+ # not set it. Grading is kept consistent by the strict JSON rubric instead.
63
+ message = client.messages.create(
64
+ model=model,
65
+ max_tokens=400,
66
+ system=_SYSTEM,
67
+ messages=[{
68
+ "role": "user",
69
+ "content": f"AUDIT CRITERION:\n{prompt}\n\nDOCUMENT:\n{content}",
70
+ }],
71
+ )
72
+ text = "".join(block.text for block in message.content
73
+ if getattr(block, "type", None) == "text").strip()
74
+ # tolerate models that wrap JSON in prose or fences
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])
79
+
80
+
81
+ def advisory(check_id: str, prompt: str, content: str,
82
+ threshold: float = 0.7, model: str = DEFAULT_MODEL) -> CheckResult:
83
+ """Grade one rubric prompt against `content`. Always advisory (non-blocking).
84
+
85
+ Fails safe: if the key is missing or the model errors, returns a skipped/
86
+ unavailable result rather than raising, so it can never block a merge.
87
+ """
88
+ if not os.environ.get("ANTHROPIC_API_KEY"):
89
+ return CheckResult(check_id, True, False,
90
+ "skipped — no ANTHROPIC_API_KEY (advisory only)",
91
+ kind="semantic", score=None)
92
+
93
+ key = _cache_key(model, prompt, content)
94
+ grade = _cache_get(key)
95
+ if grade is None:
96
+ try:
97
+ grade = _grade(prompt, content, model)
98
+ _cache_put(key, grade)
99
+ except Exception as exc:
100
+ return CheckResult(check_id, True, False,
101
+ f"advisory check unavailable: {exc}",
102
+ kind="semantic", score=None)
103
+
104
+ score = float(grade.get("score", 0.0))
105
+ passed = bool(grade.get("pass", score >= threshold))
106
+ rationale = str(grade.get("rationale", "")).strip()
107
+ return CheckResult(check_id, passed, False,
108
+ rationale or f"score {score:.2f} (threshold {threshold:.2f})",
109
+ kind="semantic", score=score)
110
+
111
+
112
+ def run_semantic(doc: Document, spec: dict, content: str) -> CheckResult:
113
+ return advisory(
114
+ spec["id"], spec.get("prompt", "").strip(), content,
115
+ threshold=float(spec.get("pass_threshold", 0.7)),
116
+ model=spec.get("model", DEFAULT_MODEL),
117
+ )
118
+
119
+
120
+ def run_alignment(check_id: str, prompt: str, parent_text: str,
121
+ child_text: str, threshold: float = 0.7) -> CheckResult:
122
+ """Advisory: does the child item genuinely fulfil the parent it links to?"""
123
+ content = f"PARENT:\n{parent_text}\n\nCHILD:\n{child_text}"
124
+ return advisory(check_id, prompt, content, threshold=threshold)