docassert 0.2.0__tar.gz → 0.2.1__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.2.0/docassert.egg-info → docassert-0.2.1}/PKG-INFO +6 -2
- {docassert-0.2.0 → docassert-0.2.1}/README.md +5 -1
- {docassert-0.2.0 → docassert-0.2.1}/docassert/__init__.py +1 -1
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/consistency.yaml +4 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/cli.py +43 -21
- {docassert-0.2.0 → docassert-0.2.1}/docassert/consistency.py +22 -2
- {docassert-0.2.0 → docassert-0.2.1/docassert.egg-info}/PKG-INFO +6 -2
- {docassert-0.2.0 → docassert-0.2.1}/docassert.egg-info/SOURCES.txt +1 -0
- docassert-0.2.1/tests/test_defects.py +85 -0
- {docassert-0.2.0 → docassert-0.2.1}/LICENSE +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/NOTICE +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/__main__.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/adr.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/brd.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/charter.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/prd.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/project.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/profiles/agile-delivery.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/profiles/lean-startup.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/profiles/regulated-industry.yaml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/adr.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/benefits-realization.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/brd.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/business-case.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/charter.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/frnfr.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/prd.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/project.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/risk-register.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/rollback-plan.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/runbook.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/status-report.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/test-cases.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/user-story.schema.json +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/adr.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/benefits-realization.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/brd.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/business-case.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/charter.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/data-migration-plan.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/frnfr.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/hypercare-plan.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/post-implementation-review.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/prd.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/project.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/qa-test-plan.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/risk-register.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/rollback-plan.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/runbook.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/status-report.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/test-cases.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/user-story.template.md +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/config.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/extract.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/graph.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/loader.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/models.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/profiles.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/projects.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/report.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/rtm.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/semantic.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/status.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert/structural.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert.egg-info/dependency_links.txt +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert.egg-info/entry_points.txt +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert.egg-info/requires.txt +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/docassert.egg-info/top_level.txt +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/pyproject.toml +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/setup.cfg +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/tests/test_config.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/tests/test_consistency.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/tests/test_extract.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/tests/test_graph.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/tests/test_kinds_delivery.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/tests/test_kinds_governance.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/tests/test_kinds_operate.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/tests/test_kinds_reporting.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/tests/test_profiles.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/tests/test_projects.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/tests/test_status.py +0 -0
- {docassert-0.2.0 → docassert-0.2.1}/tests/test_structural.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docassert
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Unit testing for business documents — validate structured Markdown docs against a configurable audit standard.
|
|
5
5
|
Author: C4G Enterprises Inc.
|
|
6
6
|
License: Apache-2.0
|
|
@@ -80,7 +80,7 @@ you can customize them.
|
|
|
80
80
|
|
|
81
81
|
| Command | What it does |
|
|
82
82
|
|---|---|
|
|
83
|
-
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures. |
|
|
83
|
+
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). |
|
|
84
84
|
| `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. |
|
|
85
85
|
| `docassert rtm [--project ID]` | Requirements traceability matrix (Markdown or CSV). |
|
|
86
86
|
| `docassert status [--project ID] [--index]` | Derived project status (md / json / html). |
|
|
@@ -89,6 +89,10 @@ you can customize them.
|
|
|
89
89
|
| `docassert init [DIR]` | Scaffold the default config into a repo. |
|
|
90
90
|
| `docassert extract <file>` | Extract plain text from a source `.docx` / `.pdf` / `.md` / `.txt` (the first step of doc-to-pmo conversion). Needs the `convert` extra: `pip install "docassert[convert]"`. |
|
|
91
91
|
|
|
92
|
+
Every document-reading command accepts `--documents-dir` (default `documents/`).
|
|
93
|
+
AI alignment grades at most `alignment_limit` links per run (default 25; set it
|
|
94
|
+
in `consistency.yaml`, `0` = no cap) so API cost stays bounded on large graphs.
|
|
95
|
+
|
|
92
96
|
## Document kinds
|
|
93
97
|
|
|
94
98
|
Twenty kinds, each a `templates/<kind>.template.md` + `schema/<kind>.schema.json`
|
|
@@ -42,7 +42,7 @@ you can customize them.
|
|
|
42
42
|
|
|
43
43
|
| Command | What it does |
|
|
44
44
|
|---|---|
|
|
45
|
-
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures. |
|
|
45
|
+
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). |
|
|
46
46
|
| `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. |
|
|
47
47
|
| `docassert rtm [--project ID]` | Requirements traceability matrix (Markdown or CSV). |
|
|
48
48
|
| `docassert status [--project ID] [--index]` | Derived project status (md / json / html). |
|
|
@@ -51,6 +51,10 @@ you can customize them.
|
|
|
51
51
|
| `docassert init [DIR]` | Scaffold the default config into a repo. |
|
|
52
52
|
| `docassert extract <file>` | Extract plain text from a source `.docx` / `.pdf` / `.md` / `.txt` (the first step of doc-to-pmo conversion). Needs the `convert` extra: `pip install "docassert[convert]"`. |
|
|
53
53
|
|
|
54
|
+
Every document-reading command accepts `--documents-dir` (default `documents/`).
|
|
55
|
+
AI alignment grades at most `alignment_limit` links per run (default 25; set it
|
|
56
|
+
in `consistency.yaml`, `0` = no cap) so API cost stays bounded on large graphs.
|
|
57
|
+
|
|
54
58
|
## Document kinds
|
|
55
59
|
|
|
56
60
|
Twenty kinds, each a `templates/<kind>.template.md` + `schema/<kind>.schema.json`
|
|
@@ -35,6 +35,10 @@ coverage:
|
|
|
35
35
|
|
|
36
36
|
# Advisory AI alignment: for each relation, judge whether the child genuinely
|
|
37
37
|
# fulfils the parent it links to. Never blocks.
|
|
38
|
+
# Each graded link costs one API call; `alignment_limit` caps calls per run
|
|
39
|
+
# (0 = no cap).
|
|
40
|
+
alignment_limit: 25
|
|
41
|
+
|
|
38
42
|
alignment:
|
|
39
43
|
- relation: traces
|
|
40
44
|
prompt: >
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
docassert validate documents/charters/aurora.md
|
|
4
4
|
docassert validate documents/**/*.md --junit out.xml --markdown comment.md
|
|
5
5
|
|
|
6
|
-
Exit code = number of BLOCKING (structural) failures
|
|
7
|
-
|
|
6
|
+
Exit code = number of BLOCKING (structural) failures, capped at 125 so large
|
|
7
|
+
counts can't wrap around the 8-bit exit-status space (256 failures must never
|
|
8
|
+
read as success). Advisory (AI) failures never affect the exit code, so CI is
|
|
9
|
+
gated only by deterministic checks.
|
|
8
10
|
"""
|
|
9
11
|
from __future__ import annotations
|
|
10
12
|
|
|
@@ -23,15 +25,24 @@ from .models import CheckResult
|
|
|
23
25
|
from .semantic import run_semantic
|
|
24
26
|
from .structural import run_structural
|
|
25
27
|
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
|
|
28
|
+
# Default documents location; every document-reading command accepts
|
|
29
|
+
# --documents-dir to override it. Criteria / schema / consistency.yaml /
|
|
30
|
+
# profiles resolve via `config` (local override → packaged default).
|
|
31
|
+
DEFAULT_DOCUMENTS_DIR = "documents"
|
|
29
32
|
|
|
33
|
+
# POSIX exit statuses are 8-bit; 126+ carry shell meanings. Cap so a failure
|
|
34
|
+
# count can never wrap to 0.
|
|
35
|
+
_EXIT_CAP = 125
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
|
|
38
|
+
def _capped(failures: int) -> int:
|
|
39
|
+
return min(failures, _EXIT_CAP)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _build_id_index(documents_dir: Path) -> dict[str, list[str]]:
|
|
43
|
+
"""Map document id -> [paths] across the documents tree, for uniqueness checks."""
|
|
33
44
|
index: dict[str, list[str]] = defaultdict(list)
|
|
34
|
-
for path in
|
|
45
|
+
for path in documents_dir.rglob("*.md"):
|
|
35
46
|
try:
|
|
36
47
|
doc = load(path)
|
|
37
48
|
except ValueError:
|
|
@@ -86,7 +97,7 @@ def cmd_validate(args: argparse.Namespace) -> int:
|
|
|
86
97
|
print("docassert: no markdown documents matched.", file=sys.stderr)
|
|
87
98
|
return 0
|
|
88
99
|
|
|
89
|
-
id_index = _build_id_index()
|
|
100
|
+
id_index = _build_id_index(Path(args.documents_dir))
|
|
90
101
|
results_by_doc: dict[str, list[CheckResult]] = {}
|
|
91
102
|
for path in files:
|
|
92
103
|
try:
|
|
@@ -106,12 +117,12 @@ def cmd_validate(args: argparse.Namespace) -> int:
|
|
|
106
117
|
if args.markdown:
|
|
107
118
|
Path(args.markdown).write_text(report.markdown(results_by_doc))
|
|
108
119
|
|
|
109
|
-
return sum(1 for rs in results_by_doc.values()
|
|
110
|
-
|
|
120
|
+
return _capped(sum(1 for rs in results_by_doc.values()
|
|
121
|
+
for r in rs if r.is_blocking_failure))
|
|
111
122
|
|
|
112
123
|
|
|
113
124
|
def cmd_consistency(args: argparse.Namespace) -> int:
|
|
114
|
-
results = run_consistency(
|
|
125
|
+
results = run_consistency(args.documents_dir, with_semantic=not args.no_semantic)
|
|
115
126
|
results_by_doc = {"consistency (cross-document)": results}
|
|
116
127
|
|
|
117
128
|
print(report.console(results_by_doc))
|
|
@@ -123,7 +134,7 @@ def cmd_consistency(args: argparse.Namespace) -> int:
|
|
|
123
134
|
Path(args.markdown).write_text(
|
|
124
135
|
report.markdown(results_by_doc, title="docassert consistency"))
|
|
125
136
|
|
|
126
|
-
return sum(1 for r in results if r.is_blocking_failure)
|
|
137
|
+
return _capped(sum(1 for r in results if r.is_blocking_failure))
|
|
127
138
|
|
|
128
139
|
|
|
129
140
|
def _project_code(value: str | None) -> str | None:
|
|
@@ -132,7 +143,7 @@ def _project_code(value: str | None) -> str | None:
|
|
|
132
143
|
|
|
133
144
|
|
|
134
145
|
def cmd_rtm(args: argparse.Namespace) -> int:
|
|
135
|
-
graph = build_graph(
|
|
146
|
+
graph = build_graph(args.documents_dir)
|
|
136
147
|
code = _project_code(args.project)
|
|
137
148
|
text = rtm.render_csv(graph, code) if args.csv else rtm.render_markdown(graph, code)
|
|
138
149
|
if args.out:
|
|
@@ -145,7 +156,7 @@ def cmd_rtm(args: argparse.Namespace) -> int:
|
|
|
145
156
|
|
|
146
157
|
def cmd_projects(args: argparse.Namespace) -> int:
|
|
147
158
|
from . import projects as proj
|
|
148
|
-
plist = proj.load_projects(
|
|
159
|
+
plist = proj.load_projects(args.documents_dir)
|
|
149
160
|
issues = proj.registry_issues(plist)
|
|
150
161
|
for issue in issues:
|
|
151
162
|
print(f"docassert: {issue}", file=sys.stderr)
|
|
@@ -172,7 +183,7 @@ def cmd_projects(args: argparse.Namespace) -> int:
|
|
|
172
183
|
def cmd_status(args: argparse.Namespace) -> int:
|
|
173
184
|
from . import status as status_mod
|
|
174
185
|
if args.index:
|
|
175
|
-
index = status_mod.build_index(
|
|
186
|
+
index = status_mod.build_index(args.documents_dir)
|
|
176
187
|
if args.format == "json":
|
|
177
188
|
text = status_mod.render_json(index)
|
|
178
189
|
elif args.format == "html":
|
|
@@ -181,7 +192,7 @@ def cmd_status(args: argparse.Namespace) -> int:
|
|
|
181
192
|
text = status_mod.render_index_markdown(index)
|
|
182
193
|
tag = index["overall"]["rag"]
|
|
183
194
|
else:
|
|
184
|
-
model = status_mod.build_status(
|
|
195
|
+
model = status_mod.build_status(args.documents_dir, project=args.project)
|
|
185
196
|
if args.project and not model["documents"]:
|
|
186
197
|
print(f"docassert: no documents for project {args.project!r}", file=sys.stderr)
|
|
187
198
|
return 2
|
|
@@ -206,16 +217,17 @@ def cmd_pages(args: argparse.Namespace) -> int:
|
|
|
206
217
|
from . import status as status_mod
|
|
207
218
|
out = Path(args.out)
|
|
208
219
|
out.mkdir(parents=True, exist_ok=True)
|
|
220
|
+
docs_dir = args.documents_dir
|
|
209
221
|
|
|
210
|
-
index = status_mod.build_index(
|
|
222
|
+
index = status_mod.build_index(docs_dir)
|
|
211
223
|
(out / "index.html").write_text(status_mod.render_index_html(index))
|
|
212
224
|
|
|
213
|
-
plist = projects_mod.load_projects(
|
|
225
|
+
plist = projects_mod.load_projects(docs_dir)
|
|
214
226
|
for p in plist:
|
|
215
|
-
model = status_mod.build_status(
|
|
227
|
+
model = status_mod.build_status(docs_dir, project=p["id"])
|
|
216
228
|
(out / f"{p['id']}.html").write_text(status_mod.render_html(model))
|
|
217
229
|
|
|
218
|
-
(out / "RTM.md").write_text(rtm.render_markdown(build_graph(
|
|
230
|
+
(out / "RTM.md").write_text(rtm.render_markdown(build_graph(docs_dir)))
|
|
219
231
|
print(f"docassert: wrote {out}/ — index + {len(plist)} project page(s) + RTM.md "
|
|
220
232
|
f"(portfolio: {index['overall']['rag']})")
|
|
221
233
|
return 0
|
|
@@ -256,10 +268,15 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
256
268
|
parser.add_argument("--version", action="version", version=f"docassert {__version__}")
|
|
257
269
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
258
270
|
|
|
271
|
+
def docs_dir_opt(sp: argparse.ArgumentParser) -> None:
|
|
272
|
+
sp.add_argument("--documents-dir", default=DEFAULT_DOCUMENTS_DIR,
|
|
273
|
+
help=f"Documents tree to read (default: {DEFAULT_DOCUMENTS_DIR}/).")
|
|
274
|
+
|
|
259
275
|
v = sub.add_parser("validate", help="Validate documents against their criteria.")
|
|
260
276
|
v.add_argument("paths", nargs="+", help="Markdown files or globs.")
|
|
261
277
|
v.add_argument("--junit", help="Write a JUnit XML report to this path.")
|
|
262
278
|
v.add_argument("--markdown", help="Write a PR-comment markdown report to this path.")
|
|
279
|
+
docs_dir_opt(v)
|
|
263
280
|
v.set_defaults(func=cmd_validate)
|
|
264
281
|
|
|
265
282
|
c = sub.add_parser("consistency", help="Check cross-document traceability.")
|
|
@@ -267,12 +284,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
267
284
|
c.add_argument("--markdown", help="Write a PR-comment markdown report to this path.")
|
|
268
285
|
c.add_argument("--no-semantic", action="store_true",
|
|
269
286
|
help="Skip AI alignment (structural consistency only).")
|
|
287
|
+
docs_dir_opt(c)
|
|
270
288
|
c.set_defaults(func=cmd_consistency)
|
|
271
289
|
|
|
272
290
|
r = sub.add_parser("rtm", help="Generate the requirements traceability matrix.")
|
|
273
291
|
r.add_argument("--out", help="Write to this path instead of stdout.")
|
|
274
292
|
r.add_argument("--csv", action="store_true", help="Emit CSV instead of Markdown.")
|
|
275
293
|
r.add_argument("--project", help="Scope to one project (PRJ-NNN-CODE id or CODE).")
|
|
294
|
+
docs_dir_opt(r)
|
|
276
295
|
r.set_defaults(func=cmd_rtm)
|
|
277
296
|
|
|
278
297
|
s = sub.add_parser("status", help="Derive a project status page from the documents.")
|
|
@@ -284,16 +303,19 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
284
303
|
s.add_argument("--index", action="store_true",
|
|
285
304
|
help="Render the multi-project portfolio index instead of one status.")
|
|
286
305
|
s.add_argument("--out", help="Write to this path instead of stdout.")
|
|
306
|
+
docs_dir_opt(s)
|
|
287
307
|
s.set_defaults(func=cmd_status)
|
|
288
308
|
|
|
289
309
|
pg = sub.add_parser("pages", help="Build the full Pages site (portfolio index + a page per project).")
|
|
290
310
|
pg.add_argument("--out", default="_site", help="Output directory (default: _site).")
|
|
311
|
+
docs_dir_opt(pg)
|
|
291
312
|
pg.set_defaults(func=cmd_pages)
|
|
292
313
|
|
|
293
314
|
p = sub.add_parser("projects", help="Generate the project registry from the project.md anchors.")
|
|
294
315
|
p.add_argument("--out", help="Write to this path instead of stdout (e.g. projects.yaml).")
|
|
295
316
|
p.add_argument("--check", action="store_true",
|
|
296
317
|
help="Exit non-zero if the registry file is stale (CI freshness gate).")
|
|
318
|
+
docs_dir_opt(p)
|
|
297
319
|
p.set_defaults(func=cmd_projects)
|
|
298
320
|
|
|
299
321
|
ini = sub.add_parser("init", help="Scaffold the default criteria/schema/profiles/templates into a repo.")
|
|
@@ -130,6 +130,12 @@ def check_profile_completeness(documents_dir: str | Path = "documents") -> Check
|
|
|
130
130
|
|
|
131
131
|
|
|
132
132
|
# ── semantic (advisory) ────────────────────────────────────────────────────
|
|
133
|
+
# Each alignment edge costs one API call, so a large graph could otherwise run
|
|
134
|
+
# away on cost. Cap per run; tune with `alignment_limit` in consistency.yaml
|
|
135
|
+
# (0 disables the cap).
|
|
136
|
+
DEFAULT_ALIGNMENT_LIMIT = 25
|
|
137
|
+
|
|
138
|
+
|
|
133
139
|
def run_alignment_checks(graph, config) -> list[CheckResult]:
|
|
134
140
|
edges = [] # (prompt, parent, child, relation)
|
|
135
141
|
for rule in config.get("alignment", []):
|
|
@@ -142,12 +148,26 @@ def run_alignment_checks(graph, config) -> list[CheckResult]:
|
|
|
142
148
|
|
|
143
149
|
if not edges:
|
|
144
150
|
return []
|
|
151
|
+
|
|
152
|
+
limit = int(config.get("alignment_limit", DEFAULT_ALIGNMENT_LIMIT) or 0)
|
|
153
|
+
note: CheckResult | None = None
|
|
154
|
+
if limit and len(edges) > limit:
|
|
155
|
+
note = CheckResult(
|
|
156
|
+
"alignment-limit", True, False,
|
|
157
|
+
f"graded {limit} of {len(edges)} link(s) — raise `alignment_limit` "
|
|
158
|
+
f"in consistency.yaml to grade more per run",
|
|
159
|
+
kind="semantic", score=None)
|
|
160
|
+
edges = edges[:limit]
|
|
161
|
+
|
|
145
162
|
if not os.environ.get("ANTHROPIC_API_KEY"):
|
|
146
163
|
return [CheckResult("alignment", True, False,
|
|
147
164
|
f"skipped — no ANTHROPIC_API_KEY ({len(edges)} link(s) to grade)",
|
|
148
165
|
kind="semantic", score=None)]
|
|
149
|
-
|
|
150
|
-
|
|
166
|
+
results = [run_alignment(f"align:{c.id}-{rel}-{p.id}", prompt, p.text, c.text)
|
|
167
|
+
for prompt, p, c, rel in edges]
|
|
168
|
+
if note is not None:
|
|
169
|
+
results.append(note)
|
|
170
|
+
return results
|
|
151
171
|
|
|
152
172
|
|
|
153
173
|
def run_consistency(documents_dir: str | Path = "documents",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docassert
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Unit testing for business documents — validate structured Markdown docs against a configurable audit standard.
|
|
5
5
|
Author: C4G Enterprises Inc.
|
|
6
6
|
License: Apache-2.0
|
|
@@ -80,7 +80,7 @@ you can customize them.
|
|
|
80
80
|
|
|
81
81
|
| Command | What it does |
|
|
82
82
|
|---|---|
|
|
83
|
-
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures. |
|
|
83
|
+
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). |
|
|
84
84
|
| `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. |
|
|
85
85
|
| `docassert rtm [--project ID]` | Requirements traceability matrix (Markdown or CSV). |
|
|
86
86
|
| `docassert status [--project ID] [--index]` | Derived project status (md / json / html). |
|
|
@@ -89,6 +89,10 @@ you can customize them.
|
|
|
89
89
|
| `docassert init [DIR]` | Scaffold the default config into a repo. |
|
|
90
90
|
| `docassert extract <file>` | Extract plain text from a source `.docx` / `.pdf` / `.md` / `.txt` (the first step of doc-to-pmo conversion). Needs the `convert` extra: `pip install "docassert[convert]"`. |
|
|
91
91
|
|
|
92
|
+
Every document-reading command accepts `--documents-dir` (default `documents/`).
|
|
93
|
+
AI alignment grades at most `alignment_limit` links per run (default 25; set it
|
|
94
|
+
in `consistency.yaml`, `0` = no cap) so API cost stays bounded on large graphs.
|
|
95
|
+
|
|
92
96
|
## Document kinds
|
|
93
97
|
|
|
94
98
|
Twenty kinds, each a `templates/<kind>.template.md` + `schema/<kind>.schema.json`
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Tests for the 0.2.1 defect fixes: exit-code cap, --documents-dir, alignment cap."""
|
|
2
|
+
from docassert import consistency as C
|
|
3
|
+
from docassert.cli import _capped, main
|
|
4
|
+
from docassert.graph import Graph
|
|
5
|
+
from docassert.models import CheckResult, Item
|
|
6
|
+
|
|
7
|
+
PROJECT_MD = """---
|
|
8
|
+
kind: project
|
|
9
|
+
id: PRJ-009-TST
|
|
10
|
+
code: TST
|
|
11
|
+
name: Test Project
|
|
12
|
+
sponsor: jane.doe
|
|
13
|
+
status: proposed
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
A test project.
|
|
18
|
+
|
|
19
|
+
## Scope
|
|
20
|
+
Everything.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ── exit-code cap ────────────────────────────────────────────────────────────
|
|
25
|
+
def test_exit_code_capped_below_wraparound():
|
|
26
|
+
assert _capped(0) == 0
|
|
27
|
+
assert _capped(3) == 3
|
|
28
|
+
assert _capped(125) == 125
|
|
29
|
+
assert _capped(256) == 125 # would otherwise wrap to exit status 0
|
|
30
|
+
assert _capped(1000) == 125
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ── --documents-dir ──────────────────────────────────────────────────────────
|
|
34
|
+
def test_projects_reads_documents_dir_flag(tmp_path, monkeypatch, capsys):
|
|
35
|
+
docs = tmp_path / "elsewhere"
|
|
36
|
+
(docs / "PRJ-009-TST").mkdir(parents=True)
|
|
37
|
+
(docs / "PRJ-009-TST" / "project.md").write_text(PROJECT_MD, encoding="utf-8")
|
|
38
|
+
monkeypatch.chdir(tmp_path) # cwd has no documents/ at all
|
|
39
|
+
assert main(["projects", "--documents-dir", str(docs)]) == 0
|
|
40
|
+
assert "PRJ-009-TST" in capsys.readouterr().out
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_status_reads_documents_dir_flag(tmp_path, monkeypatch, capsys):
|
|
44
|
+
docs = tmp_path / "elsewhere"
|
|
45
|
+
(docs / "PRJ-009-TST").mkdir(parents=True)
|
|
46
|
+
(docs / "PRJ-009-TST" / "project.md").write_text(PROJECT_MD, encoding="utf-8")
|
|
47
|
+
monkeypatch.chdir(tmp_path)
|
|
48
|
+
assert main(["status", "--documents-dir", str(docs), "--summary"]) == 0
|
|
49
|
+
assert "Derived from 1 documents" in capsys.readouterr().out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ── alignment call cap ───────────────────────────────────────────────────────
|
|
53
|
+
def _graph_with_edges(n):
|
|
54
|
+
g = Graph()
|
|
55
|
+
g.add(Item("TST-BR-001", "TST", "BR", "parent", {}, "d.md", "k", "approved", "S"))
|
|
56
|
+
for i in range(n):
|
|
57
|
+
g.add(Item(f"TST-PR-{i:03d}", "TST", "PR", "child",
|
|
58
|
+
{"traces": ["TST-BR-001"]}, "d.md", "k", "approved", "S"))
|
|
59
|
+
return g
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _stub_calls(monkeypatch):
|
|
63
|
+
calls = []
|
|
64
|
+
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
|
|
65
|
+
monkeypatch.setattr(C, "run_alignment",
|
|
66
|
+
lambda cid, *a: calls.append(cid) or CheckResult(
|
|
67
|
+
cid, True, False, "ok", kind="semantic", score=1.0))
|
|
68
|
+
return calls
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_alignment_capped(monkeypatch):
|
|
72
|
+
calls = _stub_calls(monkeypatch)
|
|
73
|
+
cfg = {"alignment": [{"relation": "traces", "prompt": "judge"}], "alignment_limit": 2}
|
|
74
|
+
results = C.run_alignment_checks(_graph_with_edges(4), cfg)
|
|
75
|
+
assert len(calls) == 2
|
|
76
|
+
note = next(r for r in results if r.check_id == "alignment-limit")
|
|
77
|
+
assert "graded 2 of 4" in note.detail and not note.blocking
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_alignment_cap_disabled_with_zero(monkeypatch):
|
|
81
|
+
calls = _stub_calls(monkeypatch)
|
|
82
|
+
cfg = {"alignment": [{"relation": "traces", "prompt": "judge"}], "alignment_limit": 0}
|
|
83
|
+
results = C.run_alignment_checks(_graph_with_edges(4), cfg)
|
|
84
|
+
assert len(calls) == 4
|
|
85
|
+
assert not any(r.check_id == "alignment-limit" for r in results)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.2.0 → docassert-0.2.1}/docassert/_data/criteria/benefits-realization.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.2.0 → docassert-0.2.1}/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
|
|
File without changes
|
{docassert-0.2.0 → docassert-0.2.1}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.2.0 → docassert-0.2.1}/docassert/_data/schema/post-implementation-review.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/benefits-realization.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/data-migration-plan.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.2.0 → docassert-0.2.1}/docassert/_data/templates/post-implementation-review.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.2.0 → docassert-0.2.1}/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
|