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.
- docassert/__init__.py +8 -0
- docassert/__main__.py +6 -0
- docassert/_data/consistency.yaml +51 -0
- docassert/_data/criteria/adr.criteria.yaml +36 -0
- docassert/_data/criteria/benefits-realization.criteria.yaml +30 -0
- docassert/_data/criteria/brd.criteria.yaml +30 -0
- docassert/_data/criteria/business-case.criteria.yaml +23 -0
- docassert/_data/criteria/charter.criteria.yaml +73 -0
- docassert/_data/criteria/data-migration-plan.criteria.yaml +28 -0
- docassert/_data/criteria/frnfr.criteria.yaml +31 -0
- docassert/_data/criteria/hypercare-plan.criteria.yaml +27 -0
- docassert/_data/criteria/post-implementation-review.criteria.yaml +24 -0
- docassert/_data/criteria/prd.criteria.yaml +31 -0
- docassert/_data/criteria/project.criteria.yaml +32 -0
- docassert/_data/criteria/qa-test-plan.criteria.yaml +27 -0
- docassert/_data/criteria/raci-stakeholder.criteria.yaml +24 -0
- docassert/_data/criteria/release-cutover-plan.criteria.yaml +30 -0
- docassert/_data/criteria/risk-register.criteria.yaml +32 -0
- docassert/_data/criteria/rollback-plan.criteria.yaml +29 -0
- docassert/_data/criteria/runbook.criteria.yaml +30 -0
- docassert/_data/criteria/status-report.criteria.yaml +26 -0
- docassert/_data/criteria/test-cases.criteria.yaml +28 -0
- docassert/_data/criteria/user-story.criteria.yaml +32 -0
- docassert/_data/profiles/agile-delivery.yaml +20 -0
- docassert/_data/profiles/lean-startup.yaml +19 -0
- docassert/_data/profiles/regulated-industry.yaml +31 -0
- docassert/_data/schema/adr.schema.json +45 -0
- docassert/_data/schema/benefits-realization.schema.json +45 -0
- docassert/_data/schema/brd.schema.json +45 -0
- docassert/_data/schema/business-case.schema.json +45 -0
- docassert/_data/schema/charter.schema.json +84 -0
- docassert/_data/schema/data-migration-plan.schema.json +45 -0
- docassert/_data/schema/frnfr.schema.json +45 -0
- docassert/_data/schema/hypercare-plan.schema.json +45 -0
- docassert/_data/schema/post-implementation-review.schema.json +45 -0
- docassert/_data/schema/prd.schema.json +45 -0
- docassert/_data/schema/project.schema.json +32 -0
- docassert/_data/schema/qa-test-plan.schema.json +45 -0
- docassert/_data/schema/raci-stakeholder.schema.json +45 -0
- docassert/_data/schema/release-cutover-plan.schema.json +45 -0
- docassert/_data/schema/risk-register.schema.json +45 -0
- docassert/_data/schema/rollback-plan.schema.json +45 -0
- docassert/_data/schema/runbook.schema.json +45 -0
- docassert/_data/schema/status-report.schema.json +58 -0
- docassert/_data/schema/test-cases.schema.json +45 -0
- docassert/_data/schema/user-story.schema.json +45 -0
- docassert/_data/templates/adr.template.md +17 -0
- docassert/_data/templates/benefits-realization.template.md +25 -0
- docassert/_data/templates/brd.template.md +22 -0
- docassert/_data/templates/business-case.template.md +27 -0
- docassert/_data/templates/charter.template.md +46 -0
- docassert/_data/templates/data-migration-plan.template.md +35 -0
- docassert/_data/templates/frnfr.template.md +19 -0
- docassert/_data/templates/hypercare-plan.template.md +29 -0
- docassert/_data/templates/post-implementation-review.template.md +31 -0
- docassert/_data/templates/prd.template.md +23 -0
- docassert/_data/templates/project.template.md +17 -0
- docassert/_data/templates/qa-test-plan.template.md +31 -0
- docassert/_data/templates/raci-stakeholder.template.md +21 -0
- docassert/_data/templates/release-cutover-plan.template.md +28 -0
- docassert/_data/templates/risk-register.template.md +18 -0
- docassert/_data/templates/rollback-plan.template.md +24 -0
- docassert/_data/templates/runbook.template.md +28 -0
- docassert/_data/templates/status-report.template.md +27 -0
- docassert/_data/templates/test-cases.template.md +17 -0
- docassert/_data/templates/user-story.template.md +17 -0
- docassert/cli.py +291 -0
- docassert/config.py +104 -0
- docassert/consistency.py +167 -0
- docassert/graph.py +68 -0
- docassert/loader.py +116 -0
- docassert/models.py +99 -0
- docassert/profiles.py +111 -0
- docassert/projects.py +49 -0
- docassert/report.py +83 -0
- docassert/rtm.py +70 -0
- docassert/semantic.py +124 -0
- docassert/status.py +538 -0
- docassert/structural.py +406 -0
- docassert-0.1.0.dist-info/METADATA +125 -0
- docassert-0.1.0.dist-info/RECORD +86 -0
- docassert-0.1.0.dist-info/WHEEL +5 -0
- docassert-0.1.0.dist-info/entry_points.txt +2 -0
- docassert-0.1.0.dist-info/licenses/LICENSE +201 -0
- docassert-0.1.0.dist-info/licenses/NOTICE +4 -0
- docassert-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: qa-test-plan
|
|
3
|
+
id: my-test-plan
|
|
4
|
+
title: My QA / Test Plan
|
|
5
|
+
owner: jane.doe
|
|
6
|
+
status: draft
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Scope
|
|
10
|
+
|
|
11
|
+
<!-- What is being tested (and what isn't). -->
|
|
12
|
+
|
|
13
|
+
## Test Approach
|
|
14
|
+
|
|
15
|
+
<!-- Types of testing, tooling, and how test cases map to requirements. -->
|
|
16
|
+
|
|
17
|
+
## Environments
|
|
18
|
+
|
|
19
|
+
<!-- The environments used and their configuration. -->
|
|
20
|
+
|
|
21
|
+
## Entry Criteria
|
|
22
|
+
|
|
23
|
+
<!-- What must be true before testing starts. -->
|
|
24
|
+
|
|
25
|
+
- <!-- e.g. every acceptance criterion has at least one test case -->
|
|
26
|
+
|
|
27
|
+
## Exit Criteria
|
|
28
|
+
|
|
29
|
+
<!-- Each exit criterion must state a measurable threshold. -->
|
|
30
|
+
|
|
31
|
+
- <!-- e.g. at least 95% of test cases pass -->
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: raci-stakeholder
|
|
3
|
+
id: my-raci
|
|
4
|
+
title: My RACI / Stakeholder Register
|
|
5
|
+
owner: jane.doe
|
|
6
|
+
status: draft
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Stakeholders
|
|
10
|
+
|
|
11
|
+
<!-- One bullet per stakeholder and their interest. -->
|
|
12
|
+
|
|
13
|
+
- <!-- Sponsor — name (accountable for benefits) -->
|
|
14
|
+
|
|
15
|
+
## RACI Matrix
|
|
16
|
+
|
|
17
|
+
<!-- Exactly one Accountable (A) per activity row. R/A/C/I. -->
|
|
18
|
+
|
|
19
|
+
| Activity | Sponsor | Delivery Lead | QA Lead |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| Approve charter | A | C | I |
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: release-cutover-plan
|
|
3
|
+
id: my-cutover-plan
|
|
4
|
+
title: My Release / Cutover Plan
|
|
5
|
+
owner: jane.doe
|
|
6
|
+
status: draft
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
<!-- What is being released and when. -->
|
|
12
|
+
|
|
13
|
+
## Pre-Cutover Checklist
|
|
14
|
+
|
|
15
|
+
- <!-- e.g. all tests green; backups taken -->
|
|
16
|
+
|
|
17
|
+
## Cutover Steps
|
|
18
|
+
|
|
19
|
+
1. <!-- first step -->
|
|
20
|
+
2. <!-- second step -->
|
|
21
|
+
|
|
22
|
+
## Verification
|
|
23
|
+
|
|
24
|
+
<!-- How you confirm the release succeeded. -->
|
|
25
|
+
|
|
26
|
+
## Rollback Trigger
|
|
27
|
+
|
|
28
|
+
<!-- The conditions under which you invoke the rollback plan. -->
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: risk-register
|
|
3
|
+
id: my-risk-register
|
|
4
|
+
title: My Risk Register
|
|
5
|
+
owner: jane.doe
|
|
6
|
+
status: draft
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
<!-- What this register covers. -->
|
|
12
|
+
|
|
13
|
+
## Risks
|
|
14
|
+
|
|
15
|
+
<!-- Each risk optionally threatens a requirement or milestone, and MUST state
|
|
16
|
+
Probability, Impact, Owner, and Response. -->
|
|
17
|
+
|
|
18
|
+
- **RISK-001** (threatens: BR-001): <!-- description -->. Probability: Medium. Impact: High. Owner: jane.doe. Response: <!-- mitigation --> .
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: rollback-plan
|
|
3
|
+
id: my-rollback-plan
|
|
4
|
+
title: My Rollback Plan
|
|
5
|
+
owner: jane.doe
|
|
6
|
+
status: draft
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
<!-- What this rolls back and to what state. -->
|
|
12
|
+
|
|
13
|
+
## Trigger Conditions
|
|
14
|
+
|
|
15
|
+
<!-- The conditions that cause a rollback. -->
|
|
16
|
+
|
|
17
|
+
## Rollback Steps
|
|
18
|
+
|
|
19
|
+
1. <!-- first step -->
|
|
20
|
+
2. <!-- second step -->
|
|
21
|
+
|
|
22
|
+
## Verification
|
|
23
|
+
|
|
24
|
+
<!-- How you confirm the rollback restored a good state. -->
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: runbook
|
|
3
|
+
id: my-runbook
|
|
4
|
+
title: My Runbook
|
|
5
|
+
owner: jane.doe
|
|
6
|
+
status: draft
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
<!-- What this runbook operates. -->
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
<!-- Access, tools, and preconditions. -->
|
|
16
|
+
|
|
17
|
+
## Procedures
|
|
18
|
+
|
|
19
|
+
1. <!-- first operational step -->
|
|
20
|
+
2. <!-- second operational step -->
|
|
21
|
+
|
|
22
|
+
## Monitoring
|
|
23
|
+
|
|
24
|
+
<!-- Dashboards, alerts, and what "healthy" looks like. -->
|
|
25
|
+
|
|
26
|
+
## Escalation
|
|
27
|
+
|
|
28
|
+
<!-- Who to contact when a procedure fails. -->
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: status-report
|
|
3
|
+
id: my-status-2026-07-01
|
|
4
|
+
title: My Project — Status Report
|
|
5
|
+
owner: jane.doe
|
|
6
|
+
period: 2026-07-01
|
|
7
|
+
rag: green # green | amber | red
|
|
8
|
+
status: draft
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Summary
|
|
12
|
+
|
|
13
|
+
<!-- One paragraph: overall health and the headline. -->
|
|
14
|
+
|
|
15
|
+
## Progress
|
|
16
|
+
|
|
17
|
+
<!-- What moved since the last report. -->
|
|
18
|
+
|
|
19
|
+
## Risks & Issues
|
|
20
|
+
|
|
21
|
+
<!-- Cite risks by their register ID, e.g. RISK-001. -->
|
|
22
|
+
|
|
23
|
+
- <!-- RISK-001: current state and any change -->
|
|
24
|
+
|
|
25
|
+
## Next Steps
|
|
26
|
+
|
|
27
|
+
<!-- What happens before the next report. -->
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: test-cases
|
|
3
|
+
id: my-test-cases
|
|
4
|
+
title: My Test Cases
|
|
5
|
+
owner: jane.doe
|
|
6
|
+
status: draft
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
<!-- What these test cases cover. -->
|
|
12
|
+
|
|
13
|
+
## Test Cases
|
|
14
|
+
|
|
15
|
+
<!-- Each test case tests one or more acceptance criteria. -->
|
|
16
|
+
|
|
17
|
+
- **TC-001** (tests: AC-001): <!-- Steps: … Expected: … -->
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: user-story
|
|
3
|
+
id: my-user-stories
|
|
4
|
+
title: My User Stories
|
|
5
|
+
owner: jane.doe
|
|
6
|
+
status: draft
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
<!-- What area these stories cover. -->
|
|
12
|
+
|
|
13
|
+
## User Stories
|
|
14
|
+
|
|
15
|
+
<!-- Each story traces to a product requirement and follows the standard form. -->
|
|
16
|
+
|
|
17
|
+
- **US-001** (traces: PR-001): As a <role>, I want <goal>, so that <benefit>.
|
docassert/cli.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""docassert command-line interface.
|
|
2
|
+
|
|
3
|
+
docassert validate documents/charters/aurora.md
|
|
4
|
+
docassert validate documents/**/*.md --junit out.xml --markdown comment.md
|
|
5
|
+
|
|
6
|
+
Exit code = number of BLOCKING (structural) failures. Advisory (AI) failures
|
|
7
|
+
never affect the exit code, so CI is gated only by deterministic checks.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import glob
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from collections import defaultdict
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from . import config, report, rtm
|
|
19
|
+
from .consistency import run_consistency
|
|
20
|
+
from .graph import build_graph
|
|
21
|
+
from .loader import load
|
|
22
|
+
from .models import CheckResult
|
|
23
|
+
from .semantic import run_semantic
|
|
24
|
+
from .structural import run_structural
|
|
25
|
+
|
|
26
|
+
# The user's documents live here; criteria / schema / consistency.yaml / profiles
|
|
27
|
+
# resolve via `config` (local override → packaged default).
|
|
28
|
+
DOCUMENTS_DIR = Path("documents")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _build_id_index() -> dict[str, list[str]]:
|
|
32
|
+
"""Map document id -> [paths] across all documents/, for uniqueness checks."""
|
|
33
|
+
index: dict[str, list[str]] = defaultdict(list)
|
|
34
|
+
for path in DOCUMENTS_DIR.rglob("*.md"):
|
|
35
|
+
try:
|
|
36
|
+
doc = load(path)
|
|
37
|
+
except ValueError:
|
|
38
|
+
continue
|
|
39
|
+
if doc.id:
|
|
40
|
+
index[doc.id].append(str(path))
|
|
41
|
+
return index
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _validate_one(path: str, id_index: dict) -> list[CheckResult]:
|
|
45
|
+
doc = load(path)
|
|
46
|
+
kind = doc.kind or "charter"
|
|
47
|
+
criteria = config.read_criteria(kind)
|
|
48
|
+
schema = config.read_schema(kind)
|
|
49
|
+
|
|
50
|
+
ctx = {
|
|
51
|
+
"schema": schema,
|
|
52
|
+
"required_sections": criteria.get("required_sections", []),
|
|
53
|
+
"item_sections": criteria.get("item_sections", []),
|
|
54
|
+
"steps_sections": criteria.get("steps_sections", []),
|
|
55
|
+
"measurable_sections": criteria.get("measurable_sections", []),
|
|
56
|
+
"id_index": id_index,
|
|
57
|
+
}
|
|
58
|
+
content = Path(path).read_text(encoding="utf-8")
|
|
59
|
+
|
|
60
|
+
results: list[CheckResult] = []
|
|
61
|
+
for spec in criteria.get("checks", []):
|
|
62
|
+
if spec.get("type") == "structural":
|
|
63
|
+
results.append(run_structural(doc, spec, ctx))
|
|
64
|
+
elif spec.get("type") == "semantic":
|
|
65
|
+
results.append(run_semantic(doc, spec, content))
|
|
66
|
+
return results
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _expand(paths: list[str]) -> list[str]:
|
|
70
|
+
files: list[str] = []
|
|
71
|
+
for p in paths:
|
|
72
|
+
matched = glob.glob(p, recursive=True)
|
|
73
|
+
files.extend(matched if matched else [p])
|
|
74
|
+
# de-dupe; keep only markdown docs that still exist (skip files a PR deleted)
|
|
75
|
+
seen, out = set(), []
|
|
76
|
+
for f in files:
|
|
77
|
+
if f.endswith(".md") and f not in seen and os.path.isfile(f):
|
|
78
|
+
seen.add(f)
|
|
79
|
+
out.append(f)
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def cmd_validate(args: argparse.Namespace) -> int:
|
|
84
|
+
files = _expand(args.paths)
|
|
85
|
+
if not files:
|
|
86
|
+
print("docassert: no markdown documents matched.", file=sys.stderr)
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
id_index = _build_id_index()
|
|
90
|
+
results_by_doc: dict[str, list[CheckResult]] = {}
|
|
91
|
+
for path in files:
|
|
92
|
+
try:
|
|
93
|
+
results_by_doc[path] = _validate_one(path, id_index)
|
|
94
|
+
except FileNotFoundError as exc:
|
|
95
|
+
print(f"docassert: {exc}", file=sys.stderr)
|
|
96
|
+
return 2
|
|
97
|
+
except ValueError as exc: # malformed frontmatter → a real, blocking failure
|
|
98
|
+
results_by_doc[path] = [CheckResult(
|
|
99
|
+
"parse", False, True, str(exc), kind="structural")]
|
|
100
|
+
|
|
101
|
+
print(report.console(results_by_doc))
|
|
102
|
+
print("\n" + report.summary_line(results_by_doc))
|
|
103
|
+
|
|
104
|
+
if args.junit:
|
|
105
|
+
Path(args.junit).write_text(report.junit(results_by_doc))
|
|
106
|
+
if args.markdown:
|
|
107
|
+
Path(args.markdown).write_text(report.markdown(results_by_doc))
|
|
108
|
+
|
|
109
|
+
return sum(1 for rs in results_by_doc.values()
|
|
110
|
+
for r in rs if r.is_blocking_failure)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def cmd_consistency(args: argparse.Namespace) -> int:
|
|
114
|
+
results = run_consistency(DOCUMENTS_DIR, with_semantic=not args.no_semantic)
|
|
115
|
+
results_by_doc = {"consistency (cross-document)": results}
|
|
116
|
+
|
|
117
|
+
print(report.console(results_by_doc))
|
|
118
|
+
print("\n" + report.summary_line(results_by_doc))
|
|
119
|
+
|
|
120
|
+
if args.junit:
|
|
121
|
+
Path(args.junit).write_text(report.junit(results_by_doc))
|
|
122
|
+
if args.markdown:
|
|
123
|
+
Path(args.markdown).write_text(
|
|
124
|
+
report.markdown(results_by_doc, title="docassert consistency"))
|
|
125
|
+
|
|
126
|
+
return sum(1 for r in results if r.is_blocking_failure)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _project_code(value: str | None) -> str | None:
|
|
130
|
+
"""Accept either a PRJ-NNN-CODE id or a bare CODE; return the CODE."""
|
|
131
|
+
return value.split("-")[-1] if value else None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def cmd_rtm(args: argparse.Namespace) -> int:
|
|
135
|
+
graph = build_graph(DOCUMENTS_DIR)
|
|
136
|
+
code = _project_code(args.project)
|
|
137
|
+
text = rtm.render_csv(graph, code) if args.csv else rtm.render_markdown(graph, code)
|
|
138
|
+
if args.out:
|
|
139
|
+
Path(args.out).write_text(text)
|
|
140
|
+
print(f"docassert: wrote {args.out}")
|
|
141
|
+
else:
|
|
142
|
+
sys.stdout.write(text)
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def cmd_projects(args: argparse.Namespace) -> int:
|
|
147
|
+
from . import projects as proj
|
|
148
|
+
plist = proj.load_projects(DOCUMENTS_DIR)
|
|
149
|
+
issues = proj.registry_issues(plist)
|
|
150
|
+
for issue in issues:
|
|
151
|
+
print(f"docassert: {issue}", file=sys.stderr)
|
|
152
|
+
text = proj.render_yaml(plist)
|
|
153
|
+
|
|
154
|
+
if args.check:
|
|
155
|
+
current = Path(args.out or "projects.yaml")
|
|
156
|
+
existing = current.read_text() if current.is_file() else ""
|
|
157
|
+
if existing != text:
|
|
158
|
+
print(f"docassert: {current} is stale — run `docassert projects --out {current}`",
|
|
159
|
+
file=sys.stderr)
|
|
160
|
+
return 1
|
|
161
|
+
print(f"docassert: {current} is up to date ({len(plist)} projects).")
|
|
162
|
+
return 1 if issues else 0
|
|
163
|
+
|
|
164
|
+
if args.out:
|
|
165
|
+
Path(args.out).write_text(text)
|
|
166
|
+
print(f"docassert: wrote {args.out} ({len(plist)} projects)")
|
|
167
|
+
else:
|
|
168
|
+
sys.stdout.write(text)
|
|
169
|
+
return 1 if issues else 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def cmd_status(args: argparse.Namespace) -> int:
|
|
173
|
+
from . import status as status_mod
|
|
174
|
+
if args.index:
|
|
175
|
+
index = status_mod.build_index(DOCUMENTS_DIR)
|
|
176
|
+
if args.format == "json":
|
|
177
|
+
text = status_mod.render_json(index)
|
|
178
|
+
elif args.format == "html":
|
|
179
|
+
text = status_mod.render_index_html(index)
|
|
180
|
+
else:
|
|
181
|
+
text = status_mod.render_index_markdown(index)
|
|
182
|
+
tag = index["overall"]["rag"]
|
|
183
|
+
else:
|
|
184
|
+
model = status_mod.build_status(DOCUMENTS_DIR, project=args.project)
|
|
185
|
+
if args.project and not model["documents"]:
|
|
186
|
+
print(f"docassert: no documents for project {args.project!r}", file=sys.stderr)
|
|
187
|
+
return 2
|
|
188
|
+
if args.format == "json":
|
|
189
|
+
text = status_mod.render_json(model)
|
|
190
|
+
elif args.format == "html":
|
|
191
|
+
text = status_mod.render_html(model)
|
|
192
|
+
else:
|
|
193
|
+
text = status_mod.render_markdown(model, summary=args.summary)
|
|
194
|
+
tag = model["rag"]
|
|
195
|
+
if args.out:
|
|
196
|
+
Path(args.out).write_text(text)
|
|
197
|
+
print(f"docassert: wrote {args.out} (status: {tag})")
|
|
198
|
+
else:
|
|
199
|
+
sys.stdout.write(text)
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def cmd_pages(args: argparse.Namespace) -> int:
|
|
204
|
+
"""Build the whole Pages site: a portfolio index plus one page per project."""
|
|
205
|
+
from . import projects as projects_mod
|
|
206
|
+
from . import status as status_mod
|
|
207
|
+
out = Path(args.out)
|
|
208
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
209
|
+
|
|
210
|
+
index = status_mod.build_index(DOCUMENTS_DIR)
|
|
211
|
+
(out / "index.html").write_text(status_mod.render_index_html(index))
|
|
212
|
+
|
|
213
|
+
plist = projects_mod.load_projects(DOCUMENTS_DIR)
|
|
214
|
+
for p in plist:
|
|
215
|
+
model = status_mod.build_status(DOCUMENTS_DIR, project=p["id"])
|
|
216
|
+
(out / f"{p['id']}.html").write_text(status_mod.render_html(model))
|
|
217
|
+
|
|
218
|
+
(out / "RTM.md").write_text(rtm.render_markdown(build_graph(DOCUMENTS_DIR)))
|
|
219
|
+
print(f"docassert: wrote {out}/ — index + {len(plist)} project page(s) + RTM.md "
|
|
220
|
+
f"(portfolio: {index['overall']['rag']})")
|
|
221
|
+
return 0
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
225
|
+
"""Scaffold a repo with the default criteria, schema, profiles, templates,
|
|
226
|
+
and consistency.yaml so a team can customize the standard."""
|
|
227
|
+
created = config.init(args.dir)
|
|
228
|
+
if created:
|
|
229
|
+
print(f"docassert: scaffolded {', '.join(created)} in {args.dir}/")
|
|
230
|
+
else:
|
|
231
|
+
print(f"docassert: nothing to do — {args.dir}/ already has the config files")
|
|
232
|
+
return 0
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def main(argv: list[str] | None = None) -> int:
|
|
236
|
+
from . import __version__
|
|
237
|
+
parser = argparse.ArgumentParser(prog="docassert",
|
|
238
|
+
description="Unit testing for business documents.")
|
|
239
|
+
parser.add_argument("--version", action="version", version=f"docassert {__version__}")
|
|
240
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
241
|
+
|
|
242
|
+
v = sub.add_parser("validate", help="Validate documents against their criteria.")
|
|
243
|
+
v.add_argument("paths", nargs="+", help="Markdown files or globs.")
|
|
244
|
+
v.add_argument("--junit", help="Write a JUnit XML report to this path.")
|
|
245
|
+
v.add_argument("--markdown", help="Write a PR-comment markdown report to this path.")
|
|
246
|
+
v.set_defaults(func=cmd_validate)
|
|
247
|
+
|
|
248
|
+
c = sub.add_parser("consistency", help="Check cross-document traceability.")
|
|
249
|
+
c.add_argument("--junit", help="Write a JUnit XML report to this path.")
|
|
250
|
+
c.add_argument("--markdown", help="Write a PR-comment markdown report to this path.")
|
|
251
|
+
c.add_argument("--no-semantic", action="store_true",
|
|
252
|
+
help="Skip AI alignment (structural consistency only).")
|
|
253
|
+
c.set_defaults(func=cmd_consistency)
|
|
254
|
+
|
|
255
|
+
r = sub.add_parser("rtm", help="Generate the requirements traceability matrix.")
|
|
256
|
+
r.add_argument("--out", help="Write to this path instead of stdout.")
|
|
257
|
+
r.add_argument("--csv", action="store_true", help="Emit CSV instead of Markdown.")
|
|
258
|
+
r.add_argument("--project", help="Scope to one project (PRJ-NNN-CODE id or CODE).")
|
|
259
|
+
r.set_defaults(func=cmd_rtm)
|
|
260
|
+
|
|
261
|
+
s = sub.add_parser("status", help="Derive a project status page from the documents.")
|
|
262
|
+
s.add_argument("--format", choices=["md", "json", "html"], default="md",
|
|
263
|
+
help="Output format (default: md).")
|
|
264
|
+
s.add_argument("--summary", action="store_true",
|
|
265
|
+
help="Condensed markdown (RAG + signals, no inventory table).")
|
|
266
|
+
s.add_argument("--project", help="Scope the status to one project (its PRJ-NNN-CODE id).")
|
|
267
|
+
s.add_argument("--index", action="store_true",
|
|
268
|
+
help="Render the multi-project portfolio index instead of one status.")
|
|
269
|
+
s.add_argument("--out", help="Write to this path instead of stdout.")
|
|
270
|
+
s.set_defaults(func=cmd_status)
|
|
271
|
+
|
|
272
|
+
pg = sub.add_parser("pages", help="Build the full Pages site (portfolio index + a page per project).")
|
|
273
|
+
pg.add_argument("--out", default="_site", help="Output directory (default: _site).")
|
|
274
|
+
pg.set_defaults(func=cmd_pages)
|
|
275
|
+
|
|
276
|
+
p = sub.add_parser("projects", help="Generate the project registry from the project.md anchors.")
|
|
277
|
+
p.add_argument("--out", help="Write to this path instead of stdout (e.g. projects.yaml).")
|
|
278
|
+
p.add_argument("--check", action="store_true",
|
|
279
|
+
help="Exit non-zero if the registry file is stale (CI freshness gate).")
|
|
280
|
+
p.set_defaults(func=cmd_projects)
|
|
281
|
+
|
|
282
|
+
ini = sub.add_parser("init", help="Scaffold the default criteria/schema/profiles/templates into a repo.")
|
|
283
|
+
ini.add_argument("dir", nargs="?", default=".", help="Target directory (default: current).")
|
|
284
|
+
ini.set_defaults(func=cmd_init)
|
|
285
|
+
|
|
286
|
+
args = parser.parse_args(argv)
|
|
287
|
+
return args.func(args)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
if __name__ == "__main__":
|
|
291
|
+
sys.exit(main())
|
docassert/config.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Configuration resolution: local repo override → packaged default.
|
|
2
|
+
|
|
3
|
+
docassert ships default criteria, schemas, profiles, templates, and
|
|
4
|
+
consistency.yaml as package data under ``docassert/_data/``. At runtime each is
|
|
5
|
+
resolved from the current working directory first (so a repo can define its own
|
|
6
|
+
standard), falling back to the packaged default — which is what makes
|
|
7
|
+
``docassert`` usable in a repo that hasn't set up its own config.
|
|
8
|
+
|
|
9
|
+
``docassert init`` copies the packaged defaults into a repo so they can be edited.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import shutil
|
|
15
|
+
from importlib.resources import files
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
# A concrete filesystem path to the bundled defaults (docassert is installed
|
|
21
|
+
# unzipped, so this resolves to a real directory for both wheel and editable installs).
|
|
22
|
+
DATA_DIR = Path(str(files("docassert"))) / "_data"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _resolve(local: Path, packaged_rel: str) -> Path | None:
|
|
26
|
+
"""The local file if it exists, else the packaged default, else None."""
|
|
27
|
+
if local.is_file():
|
|
28
|
+
return local
|
|
29
|
+
packaged = DATA_DIR / packaged_rel
|
|
30
|
+
return packaged if packaged.is_file() else None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _read_yaml(path: Path) -> dict:
|
|
34
|
+
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── criteria ────────────────────────────────────────────────────────────────
|
|
38
|
+
def criteria_path(kind: str) -> Path | None:
|
|
39
|
+
return _resolve(Path("criteria") / f"{kind}.criteria.yaml",
|
|
40
|
+
f"criteria/{kind}.criteria.yaml")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def criteria_exists(kind: str) -> bool:
|
|
44
|
+
return criteria_path(kind) is not None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def read_criteria(kind: str) -> dict:
|
|
48
|
+
path = criteria_path(kind)
|
|
49
|
+
if path is None:
|
|
50
|
+
raise FileNotFoundError(
|
|
51
|
+
f"no criteria for kind '{kind}' (looked in ./criteria and packaged defaults)")
|
|
52
|
+
return _read_yaml(path)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ── schema ──────────────────────────────────────────────────────────────────
|
|
56
|
+
def read_schema(kind: str) -> dict:
|
|
57
|
+
path = _resolve(Path("schema") / f"{kind}.schema.json", f"schema/{kind}.schema.json")
|
|
58
|
+
if path is None:
|
|
59
|
+
raise FileNotFoundError(f"no schema for kind '{kind}'")
|
|
60
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ── consistency config ──────────────────────────────────────────────────────
|
|
64
|
+
def read_consistency_config() -> dict:
|
|
65
|
+
path = _resolve(Path("consistency.yaml"), "consistency.yaml")
|
|
66
|
+
return _read_yaml(path) if path is not None else {}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ── profiles ────────────────────────────────────────────────────────────────
|
|
70
|
+
def profile_path(name: str) -> Path | None:
|
|
71
|
+
return _resolve(Path("profiles") / f"{name}.yaml", f"profiles/{name}.yaml")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def available_profiles() -> list[str]:
|
|
75
|
+
names: set[str] = set()
|
|
76
|
+
local = Path("profiles")
|
|
77
|
+
if local.is_dir():
|
|
78
|
+
names |= {p.stem for p in local.glob("*.yaml")}
|
|
79
|
+
packaged = DATA_DIR / "profiles"
|
|
80
|
+
if packaged.is_dir():
|
|
81
|
+
names |= {p.stem for p in packaged.glob("*.yaml")}
|
|
82
|
+
return sorted(names)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ── scaffolding (docassert init) ────────────────────────────────────────────
|
|
86
|
+
_INIT_TREE = ["criteria", "schema", "profiles", "templates", "consistency.yaml"]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def init(dest: str | Path = ".") -> list[str]:
|
|
90
|
+
"""Copy the packaged defaults into `dest`, skipping anything already present.
|
|
91
|
+
Returns the top-level names that were created."""
|
|
92
|
+
dest = Path(dest)
|
|
93
|
+
created: list[str] = []
|
|
94
|
+
for name in _INIT_TREE:
|
|
95
|
+
target = dest / name
|
|
96
|
+
if target.exists():
|
|
97
|
+
continue
|
|
98
|
+
src = DATA_DIR / name
|
|
99
|
+
if src.is_dir():
|
|
100
|
+
shutil.copytree(src, target)
|
|
101
|
+
else:
|
|
102
|
+
target.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
|
|
103
|
+
created.append(name)
|
|
104
|
+
return created
|