docassert 0.4.0__tar.gz → 0.6.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.4.0/docassert.egg-info → docassert-0.6.0}/PKG-INFO +19 -5
- {docassert-0.4.0 → docassert-0.6.0}/README.md +16 -4
- {docassert-0.4.0 → docassert-0.6.0}/docassert/__init__.py +1 -1
- {docassert-0.4.0 → docassert-0.6.0}/docassert/cli.py +15 -1
- {docassert-0.4.0 → docassert-0.6.0}/docassert/consistency.py +4 -2
- {docassert-0.4.0 → docassert-0.6.0}/docassert/profiles.py +1 -1
- {docassert-0.4.0 → docassert-0.6.0}/docassert/report.py +27 -1
- {docassert-0.4.0 → docassert-0.6.0}/docassert/semantic.py +1 -1
- {docassert-0.4.0 → docassert-0.6.0}/docassert/status.py +10 -2
- {docassert-0.4.0 → docassert-0.6.0/docassert.egg-info}/PKG-INFO +19 -5
- {docassert-0.4.0 → docassert-0.6.0}/docassert.egg-info/SOURCES.txt +2 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert.egg-info/requires.txt +2 -0
- {docassert-0.4.0 → docassert-0.6.0}/pyproject.toml +18 -1
- docassert-0.6.0/tests/test_badge.py +26 -0
- {docassert-0.4.0 → docassert-0.6.0}/tests/test_extract.py +32 -0
- docassert-0.6.0/tests/test_json_report.py +43 -0
- {docassert-0.4.0 → docassert-0.6.0}/LICENSE +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/NOTICE +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/__main__.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/consistency.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/adr.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/brd.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/business-case.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/charter.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/frnfr.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/prd.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/project.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/risk-register.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/runbook.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/status-report.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/test-cases.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/user-story.schema.json +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/skills/doc-to-pmo/SKILL.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/adr.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/benefits-realization.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/brd.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/business-case.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/charter.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/data-migration-plan.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/frnfr.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/hypercare-plan.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/post-implementation-review.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/prd.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/project.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/qa-test-plan.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/risk-register.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/rollback-plan.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/runbook.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/status-report.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/test-cases.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/user-story.template.md +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/config.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/extract.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/graph.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/loader.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/models.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/projects.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/rtm.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/scaffold.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert/structural.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert.egg-info/dependency_links.txt +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert.egg-info/entry_points.txt +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/docassert.egg-info/top_level.txt +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/setup.cfg +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/tests/test_config.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/tests/test_consistency.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/tests/test_defects.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/tests/test_graph.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/tests/test_kinds_delivery.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/tests/test_kinds_governance.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/tests/test_kinds_operate.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/tests/test_kinds_reporting.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/tests/test_profiles.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/tests/test_projects.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/tests/test_scaffold.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/tests/test_status.py +0 -0
- {docassert-0.4.0 → docassert-0.6.0}/tests/test_structural.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docassert
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
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
|
|
@@ -33,7 +33,9 @@ Requires-Dist: python-docx>=1.1; extra == "convert"
|
|
|
33
33
|
Requires-Dist: pypdf>=4.0; extra == "convert"
|
|
34
34
|
Provides-Extra: dev
|
|
35
35
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
36
37
|
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
38
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
37
39
|
Dynamic: license-file
|
|
38
40
|
|
|
39
41
|
# docassert
|
|
@@ -49,7 +51,8 @@ semantic checks that advise. Requirements trace end to end, and project status i
|
|
|
49
51
|
derived from the documents rather than self-reported.
|
|
50
52
|
|
|
51
53
|
docassert is the reference implementation of **[PMO as Code](https://c4g-john.github.io/pmo-as-code/)** —
|
|
52
|
-
a vendor-neutral standard for running a PMO from version-controlled, declarative
|
|
54
|
+
a vendor-neutral standard for running a PMO from version-controlled, declarative
|
|
55
|
+
files. It implements the [PMO as Code specification](https://github.com/c4g-john/pmo-as-code-spec) **v0.1**.
|
|
53
56
|
|
|
54
57
|
## Install
|
|
55
58
|
|
|
@@ -85,11 +88,11 @@ flagged as TODOs, never invented). The skill's source is
|
|
|
85
88
|
|
|
86
89
|
| Command | What it does |
|
|
87
90
|
|---|---|
|
|
88
|
-
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). |
|
|
89
|
-
| `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. |
|
|
91
|
+
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). Reports: `--junit` / `--markdown` / `--json`. |
|
|
92
|
+
| `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. Reports: `--junit` / `--markdown` / `--json`. |
|
|
90
93
|
| `docassert rtm [--project ID]` | Requirements traceability matrix (Markdown or CSV). |
|
|
91
94
|
| `docassert status [--project ID] [--index]` | Derived project status (md / json / html). |
|
|
92
|
-
| `docassert pages --out DIR` | Build the portfolio site (index + a page per project). |
|
|
95
|
+
| `docassert pages --out DIR` | Build the portfolio site (index + a page per project + shields.io badge endpoints `badge.json` / `badges/<ID>.json`). |
|
|
93
96
|
| `docassert projects [--out] [--check]` | Generate / verify the project registry. |
|
|
94
97
|
| `docassert new <kind> --project ID` | Scaffold a document from its template with identity filled in (`new project --code XYZ` auto-numbers the id); suggests the next free item ids. |
|
|
95
98
|
| `docassert init [DIR]` | Scaffold the default config into a repo. |
|
|
@@ -117,6 +120,17 @@ kind is adding a trio — no code for the common cases.
|
|
|
117
120
|
- **Semantic — AI-graded, advisory.** Scored via the Anthropic API and posted to
|
|
118
121
|
the PR — never blocking. Set `ANTHROPIC_API_KEY` to enable; skipped otherwise.
|
|
119
122
|
|
|
123
|
+
## Privacy
|
|
124
|
+
|
|
125
|
+
Structural checks run **entirely locally** — no document content leaves your
|
|
126
|
+
machine or CI runner. Semantic checks are the one exception: when
|
|
127
|
+
`ANTHROPIC_API_KEY` is set, the graded excerpts (section text, linked item
|
|
128
|
+
text) are sent to the **Anthropic API** for scoring. Without the key, semantic
|
|
129
|
+
checks are skipped and nothing is sent anywhere. Alignment grading is capped at
|
|
130
|
+
`alignment_limit` links per run (default 25). If your documents are
|
|
131
|
+
confidential, run without the key or review [Anthropic's data-usage
|
|
132
|
+
policies](https://www.anthropic.com/legal/commercial-terms) first.
|
|
133
|
+
|
|
120
134
|
## Development
|
|
121
135
|
|
|
122
136
|
```bash
|
|
@@ -11,7 +11,8 @@ semantic checks that advise. Requirements trace end to end, and project status i
|
|
|
11
11
|
derived from the documents rather than self-reported.
|
|
12
12
|
|
|
13
13
|
docassert is the reference implementation of **[PMO as Code](https://c4g-john.github.io/pmo-as-code/)** —
|
|
14
|
-
a vendor-neutral standard for running a PMO from version-controlled, declarative
|
|
14
|
+
a vendor-neutral standard for running a PMO from version-controlled, declarative
|
|
15
|
+
files. It implements the [PMO as Code specification](https://github.com/c4g-john/pmo-as-code-spec) **v0.1**.
|
|
15
16
|
|
|
16
17
|
## Install
|
|
17
18
|
|
|
@@ -47,11 +48,11 @@ flagged as TODOs, never invented). The skill's source is
|
|
|
47
48
|
|
|
48
49
|
| Command | What it does |
|
|
49
50
|
|---|---|
|
|
50
|
-
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). |
|
|
51
|
-
| `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. |
|
|
51
|
+
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). Reports: `--junit` / `--markdown` / `--json`. |
|
|
52
|
+
| `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. Reports: `--junit` / `--markdown` / `--json`. |
|
|
52
53
|
| `docassert rtm [--project ID]` | Requirements traceability matrix (Markdown or CSV). |
|
|
53
54
|
| `docassert status [--project ID] [--index]` | Derived project status (md / json / html). |
|
|
54
|
-
| `docassert pages --out DIR` | Build the portfolio site (index + a page per project). |
|
|
55
|
+
| `docassert pages --out DIR` | Build the portfolio site (index + a page per project + shields.io badge endpoints `badge.json` / `badges/<ID>.json`). |
|
|
55
56
|
| `docassert projects [--out] [--check]` | Generate / verify the project registry. |
|
|
56
57
|
| `docassert new <kind> --project ID` | Scaffold a document from its template with identity filled in (`new project --code XYZ` auto-numbers the id); suggests the next free item ids. |
|
|
57
58
|
| `docassert init [DIR]` | Scaffold the default config into a repo. |
|
|
@@ -79,6 +80,17 @@ kind is adding a trio — no code for the common cases.
|
|
|
79
80
|
- **Semantic — AI-graded, advisory.** Scored via the Anthropic API and posted to
|
|
80
81
|
the PR — never blocking. Set `ANTHROPIC_API_KEY` to enable; skipped otherwise.
|
|
81
82
|
|
|
83
|
+
## Privacy
|
|
84
|
+
|
|
85
|
+
Structural checks run **entirely locally** — no document content leaves your
|
|
86
|
+
machine or CI runner. Semantic checks are the one exception: when
|
|
87
|
+
`ANTHROPIC_API_KEY` is set, the graded excerpts (section text, linked item
|
|
88
|
+
text) are sent to the **Anthropic API** for scoring. Without the key, semantic
|
|
89
|
+
checks are skipped and nothing is sent anywhere. Alignment grading is capped at
|
|
90
|
+
`alignment_limit` links per run (default 25). If your documents are
|
|
91
|
+
confidential, run without the key or review [Anthropic's data-usage
|
|
92
|
+
policies](https://www.anthropic.com/legal/commercial-terms) first.
|
|
93
|
+
|
|
82
94
|
## Development
|
|
83
95
|
|
|
84
96
|
```bash
|
|
@@ -116,6 +116,8 @@ def cmd_validate(args: argparse.Namespace) -> int:
|
|
|
116
116
|
Path(args.junit).write_text(report.junit(results_by_doc))
|
|
117
117
|
if args.markdown:
|
|
118
118
|
Path(args.markdown).write_text(report.markdown(results_by_doc))
|
|
119
|
+
if args.json:
|
|
120
|
+
Path(args.json).write_text(report.json_report(results_by_doc))
|
|
119
121
|
|
|
120
122
|
return _capped(sum(1 for rs in results_by_doc.values()
|
|
121
123
|
for r in rs if r.is_blocking_failure))
|
|
@@ -133,6 +135,8 @@ def cmd_consistency(args: argparse.Namespace) -> int:
|
|
|
133
135
|
if args.markdown:
|
|
134
136
|
Path(args.markdown).write_text(
|
|
135
137
|
report.markdown(results_by_doc, title="docassert consistency"))
|
|
138
|
+
if args.json:
|
|
139
|
+
Path(args.json).write_text(report.json_report(results_by_doc))
|
|
136
140
|
|
|
137
141
|
return _capped(sum(1 for r in results if r.is_blocking_failure))
|
|
138
142
|
|
|
@@ -222,13 +226,21 @@ def cmd_pages(args: argparse.Namespace) -> int:
|
|
|
222
226
|
index = status_mod.build_index(docs_dir)
|
|
223
227
|
(out / "index.html").write_text(status_mod.render_index_html(index))
|
|
224
228
|
|
|
229
|
+
# shields.io endpoint badges: one for the portfolio, one per project
|
|
230
|
+
# (https://img.shields.io/endpoint?url=<site>/badge.json)
|
|
231
|
+
(out / "badge.json").write_text(
|
|
232
|
+
status_mod.render_badge_json(index["overall"]["rag"]))
|
|
233
|
+
(out / "badges").mkdir(exist_ok=True)
|
|
234
|
+
|
|
225
235
|
plist = projects_mod.load_projects(docs_dir)
|
|
226
236
|
for p in plist:
|
|
227
237
|
model = status_mod.build_status(docs_dir, project=p["id"])
|
|
228
238
|
(out / f"{p['id']}.html").write_text(status_mod.render_html(model))
|
|
239
|
+
(out / "badges" / f"{p['id']}.json").write_text(
|
|
240
|
+
status_mod.render_badge_json(model["rag"], label=p["code"].lower()))
|
|
229
241
|
|
|
230
242
|
(out / "RTM.md").write_text(rtm.render_markdown(build_graph(docs_dir)))
|
|
231
|
-
print(f"docassert: wrote {out}/ — index + {len(plist)} project page(s) + RTM.md "
|
|
243
|
+
print(f"docassert: wrote {out}/ — index + {len(plist)} project page(s) + badges + RTM.md "
|
|
232
244
|
f"(portfolio: {index['overall']['rag']})")
|
|
233
245
|
return 0
|
|
234
246
|
|
|
@@ -292,12 +304,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
292
304
|
v.add_argument("paths", nargs="+", help="Markdown files or globs.")
|
|
293
305
|
v.add_argument("--junit", help="Write a JUnit XML report to this path.")
|
|
294
306
|
v.add_argument("--markdown", help="Write a PR-comment markdown report to this path.")
|
|
307
|
+
v.add_argument("--json", help="Write a machine-readable JSON report to this path.")
|
|
295
308
|
docs_dir_opt(v)
|
|
296
309
|
v.set_defaults(func=cmd_validate)
|
|
297
310
|
|
|
298
311
|
c = sub.add_parser("consistency", help="Check cross-document traceability.")
|
|
299
312
|
c.add_argument("--junit", help="Write a JUnit XML report to this path.")
|
|
300
313
|
c.add_argument("--markdown", help="Write a PR-comment markdown report to this path.")
|
|
314
|
+
c.add_argument("--json", help="Write a machine-readable JSON report to this path.")
|
|
301
315
|
c.add_argument("--no-semantic", action="store_true",
|
|
302
316
|
help="Skip AI alignment (structural consistency only).")
|
|
303
317
|
docs_dir_opt(c)
|
|
@@ -63,7 +63,8 @@ def check_referential_integrity(graph) -> CheckResult:
|
|
|
63
63
|
|
|
64
64
|
def check_required_links(graph, config) -> CheckResult:
|
|
65
65
|
required = config.get("required_links", {})
|
|
66
|
-
approved_orphans
|
|
66
|
+
approved_orphans: list[str] = []
|
|
67
|
+
draft_orphans: list[str] = []
|
|
67
68
|
for item in graph.all_items():
|
|
68
69
|
relation = required.get(item.type)
|
|
69
70
|
if relation and not item.targets(relation):
|
|
@@ -79,7 +80,8 @@ def check_required_links(graph, config) -> CheckResult:
|
|
|
79
80
|
|
|
80
81
|
|
|
81
82
|
def check_coverage(graph, config) -> CheckResult:
|
|
82
|
-
approved_gaps
|
|
83
|
+
approved_gaps: list[str] = []
|
|
84
|
+
draft_gaps: list[str] = []
|
|
83
85
|
for rule in config.get("coverage", []):
|
|
84
86
|
parent_prefix, relation = rule["parent"], rule["relation"]
|
|
85
87
|
by_prefix = rule.get("by_prefix")
|
|
@@ -73,7 +73,7 @@ def completeness(profile: dict, documents: list[dict], project_status: str) -> d
|
|
|
73
73
|
"""
|
|
74
74
|
by_kind: dict[str, list[dict]] = {}
|
|
75
75
|
for d in documents:
|
|
76
|
-
by_kind.setdefault(d.get("kind"), []).append(d)
|
|
76
|
+
by_kind.setdefault(str(d.get("kind") or ""), []).append(d)
|
|
77
77
|
|
|
78
78
|
required = [{"kind": k, "state": _kind_state(k, by_kind)} for k in profile["required"]]
|
|
79
79
|
recommended = [{"kind": k, "state": _kind_state(k, by_kind)} for k in profile["recommended"]]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
"""Render check results as console text, PR-comment markdown,
|
|
1
|
+
"""Render check results as console text, PR-comment markdown, JUnit XML, or JSON."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import json as _json
|
|
4
5
|
import xml.etree.ElementTree as ET
|
|
5
6
|
from xml.dom import minidom
|
|
6
7
|
|
|
@@ -37,6 +38,31 @@ def summary_line(results_by_doc: dict[str, list[CheckResult]]) -> str:
|
|
|
37
38
|
return f"{_TICK} All structural checks passed across {docs} document(s) {_DASH} clear to merge."
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
def json_report(results_by_doc: dict[str, list[CheckResult]]) -> str:
|
|
42
|
+
"""Machine-readable results: one entry per document, plus a summary."""
|
|
43
|
+
documents = {
|
|
44
|
+
path: [{
|
|
45
|
+
"check_id": r.check_id,
|
|
46
|
+
"passed": r.passed,
|
|
47
|
+
"blocking": r.blocking,
|
|
48
|
+
"kind": r.kind,
|
|
49
|
+
"score": r.score,
|
|
50
|
+
"detail": r.detail,
|
|
51
|
+
} for r in results]
|
|
52
|
+
for path, results in results_by_doc.items()
|
|
53
|
+
}
|
|
54
|
+
all_results = [r for rs in results_by_doc.values() for r in rs]
|
|
55
|
+
summary = {
|
|
56
|
+
"documents": len(results_by_doc),
|
|
57
|
+
"checks": len(all_results),
|
|
58
|
+
"blocking_failures": sum(1 for r in all_results if r.is_blocking_failure),
|
|
59
|
+
"advisory_failures": sum(1 for r in all_results
|
|
60
|
+
if not r.passed and not r.blocking),
|
|
61
|
+
"passed": not any(r.is_blocking_failure for r in all_results),
|
|
62
|
+
}
|
|
63
|
+
return _json.dumps({"summary": summary, "documents": documents}, indent=2) + "\n"
|
|
64
|
+
|
|
65
|
+
|
|
40
66
|
def markdown(results_by_doc: dict[str, list[CheckResult]],
|
|
41
67
|
title: str = "docassert audit") -> str:
|
|
42
68
|
"""PR-comment body."""
|
|
@@ -69,7 +69,7 @@ def _grade(prompt: str, content: str, model: str) -> dict:
|
|
|
69
69
|
"content": f"AUDIT CRITERION:\n{prompt}\n\nDOCUMENT:\n{content}",
|
|
70
70
|
}],
|
|
71
71
|
)
|
|
72
|
-
text = "".join(block
|
|
72
|
+
text = "".join(getattr(block, "text", "") for block in message.content
|
|
73
73
|
if getattr(block, "type", None) == "text").strip()
|
|
74
74
|
# tolerate models that wrap JSON in prose or fences
|
|
75
75
|
start, end = text.find("{"), text.rfind("}")
|
|
@@ -128,9 +128,9 @@ def build_status(documents_dir=DOCUMENTS_DIR, project: str | None = None) -> dic
|
|
|
128
128
|
else:
|
|
129
129
|
docs = all_docs
|
|
130
130
|
|
|
131
|
-
id_index = {}
|
|
131
|
+
id_index: dict[str, list[str]] = {}
|
|
132
132
|
for d in all_docs: # uniqueness is always global
|
|
133
|
-
id_index.setdefault(d.id, []).append(d.path)
|
|
133
|
+
id_index.setdefault(d.id or "", []).append(d.path)
|
|
134
134
|
|
|
135
135
|
documents = [{
|
|
136
136
|
"kind": d.kind,
|
|
@@ -460,6 +460,14 @@ def render_html(model) -> str:
|
|
|
460
460
|
|
|
461
461
|
|
|
462
462
|
_RAG_COLOR = {"green": "var(--ok)", "amber": "var(--amber)", "red": "var(--bad)"}
|
|
463
|
+
_BADGE_COLOR = {"green": "brightgreen", "amber": "orange", "red": "red"}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def render_badge_json(rag: str, label: str = "pmo docs") -> str:
|
|
467
|
+
"""A shields.io endpoint payload (https://shields.io/badges/endpoint-badge),
|
|
468
|
+
so a README can carry a live derived-status badge."""
|
|
469
|
+
return json.dumps({"schemaVersion": 1, "label": label,
|
|
470
|
+
"message": rag, "color": _BADGE_COLOR[rag]}) + "\n"
|
|
463
471
|
|
|
464
472
|
|
|
465
473
|
def _index_card(p, esc) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docassert
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
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
|
|
@@ -33,7 +33,9 @@ Requires-Dist: python-docx>=1.1; extra == "convert"
|
|
|
33
33
|
Requires-Dist: pypdf>=4.0; extra == "convert"
|
|
34
34
|
Provides-Extra: dev
|
|
35
35
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
36
37
|
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
38
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
37
39
|
Dynamic: license-file
|
|
38
40
|
|
|
39
41
|
# docassert
|
|
@@ -49,7 +51,8 @@ semantic checks that advise. Requirements trace end to end, and project status i
|
|
|
49
51
|
derived from the documents rather than self-reported.
|
|
50
52
|
|
|
51
53
|
docassert is the reference implementation of **[PMO as Code](https://c4g-john.github.io/pmo-as-code/)** —
|
|
52
|
-
a vendor-neutral standard for running a PMO from version-controlled, declarative
|
|
54
|
+
a vendor-neutral standard for running a PMO from version-controlled, declarative
|
|
55
|
+
files. It implements the [PMO as Code specification](https://github.com/c4g-john/pmo-as-code-spec) **v0.1**.
|
|
53
56
|
|
|
54
57
|
## Install
|
|
55
58
|
|
|
@@ -85,11 +88,11 @@ flagged as TODOs, never invented). The skill's source is
|
|
|
85
88
|
|
|
86
89
|
| Command | What it does |
|
|
87
90
|
|---|---|
|
|
88
|
-
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). |
|
|
89
|
-
| `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. |
|
|
91
|
+
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). Reports: `--junit` / `--markdown` / `--json`. |
|
|
92
|
+
| `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. Reports: `--junit` / `--markdown` / `--json`. |
|
|
90
93
|
| `docassert rtm [--project ID]` | Requirements traceability matrix (Markdown or CSV). |
|
|
91
94
|
| `docassert status [--project ID] [--index]` | Derived project status (md / json / html). |
|
|
92
|
-
| `docassert pages --out DIR` | Build the portfolio site (index + a page per project). |
|
|
95
|
+
| `docassert pages --out DIR` | Build the portfolio site (index + a page per project + shields.io badge endpoints `badge.json` / `badges/<ID>.json`). |
|
|
93
96
|
| `docassert projects [--out] [--check]` | Generate / verify the project registry. |
|
|
94
97
|
| `docassert new <kind> --project ID` | Scaffold a document from its template with identity filled in (`new project --code XYZ` auto-numbers the id); suggests the next free item ids. |
|
|
95
98
|
| `docassert init [DIR]` | Scaffold the default config into a repo. |
|
|
@@ -117,6 +120,17 @@ kind is adding a trio — no code for the common cases.
|
|
|
117
120
|
- **Semantic — AI-graded, advisory.** Scored via the Anthropic API and posted to
|
|
118
121
|
the PR — never blocking. Set `ANTHROPIC_API_KEY` to enable; skipped otherwise.
|
|
119
122
|
|
|
123
|
+
## Privacy
|
|
124
|
+
|
|
125
|
+
Structural checks run **entirely locally** — no document content leaves your
|
|
126
|
+
machine or CI runner. Semantic checks are the one exception: when
|
|
127
|
+
`ANTHROPIC_API_KEY` is set, the graded excerpts (section text, linked item
|
|
128
|
+
text) are sent to the **Anthropic API** for scoring. Without the key, semantic
|
|
129
|
+
checks are skipped and nothing is sent anywhere. Alignment grading is capped at
|
|
130
|
+
`alignment_limit` links per run (default 25). If your documents are
|
|
131
|
+
confidential, run without the key or review [Anthropic's data-usage
|
|
132
|
+
policies](https://www.anthropic.com/legal/commercial-terms) first.
|
|
133
|
+
|
|
120
134
|
## Development
|
|
121
135
|
|
|
122
136
|
```bash
|
|
@@ -90,11 +90,13 @@ docassert/_data/templates/runbook.template.md
|
|
|
90
90
|
docassert/_data/templates/status-report.template.md
|
|
91
91
|
docassert/_data/templates/test-cases.template.md
|
|
92
92
|
docassert/_data/templates/user-story.template.md
|
|
93
|
+
tests/test_badge.py
|
|
93
94
|
tests/test_config.py
|
|
94
95
|
tests/test_consistency.py
|
|
95
96
|
tests/test_defects.py
|
|
96
97
|
tests/test_extract.py
|
|
97
98
|
tests/test_graph.py
|
|
99
|
+
tests/test_json_report.py
|
|
98
100
|
tests/test_kinds_delivery.py
|
|
99
101
|
tests/test_kinds_governance.py
|
|
100
102
|
tests/test_kinds_operate.py
|
|
@@ -29,7 +29,7 @@ dependencies = [
|
|
|
29
29
|
[project.optional-dependencies]
|
|
30
30
|
ai = ["anthropic>=0.40"]
|
|
31
31
|
convert = ["python-docx>=1.1", "pypdf>=4.0"] # source extraction for doc-to-pmo
|
|
32
|
-
dev = ["pytest>=8.0", "ruff>=0.6"]
|
|
32
|
+
dev = ["pytest>=8.0", "pytest-cov>=5.0", "ruff>=0.6", "mypy>=1.10"]
|
|
33
33
|
|
|
34
34
|
[project.urls]
|
|
35
35
|
Homepage = "https://docassert.com"
|
|
@@ -69,3 +69,20 @@ target-version = "py310"
|
|
|
69
69
|
select = ["E", "F", "I", "W", "B", "UP"]
|
|
70
70
|
# Long lines are common in the descriptive check messages and sample content.
|
|
71
71
|
ignore = ["E501"]
|
|
72
|
+
|
|
73
|
+
[tool.mypy]
|
|
74
|
+
files = ["docassert"]
|
|
75
|
+
python_version = "3.10"
|
|
76
|
+
warn_unused_ignores = true
|
|
77
|
+
warn_redundant_casts = true
|
|
78
|
+
|
|
79
|
+
[[tool.mypy.overrides]]
|
|
80
|
+
module = ["frontmatter", "docx", "pypdf", "yaml", "jsonschema", "anthropic"]
|
|
81
|
+
ignore_missing_imports = true
|
|
82
|
+
|
|
83
|
+
[tool.coverage.run]
|
|
84
|
+
source = ["docassert"]
|
|
85
|
+
|
|
86
|
+
[tool.coverage.report]
|
|
87
|
+
# Keep the gate honest: fail CI if coverage drops below the floor.
|
|
88
|
+
fail_under = 80
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Tests for the shields.io status-badge endpoint output."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from docassert import status as S
|
|
6
|
+
from docassert.cli import main
|
|
7
|
+
|
|
8
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_badge_payload_shape_and_colors():
|
|
12
|
+
for rag, color in (("green", "brightgreen"), ("amber", "orange"), ("red", "red")):
|
|
13
|
+
data = json.loads(S.render_badge_json(rag))
|
|
14
|
+
assert data == {"schemaVersion": 1, "label": "pmo docs",
|
|
15
|
+
"message": rag, "color": color}
|
|
16
|
+
assert json.loads(S.render_badge_json("green", label="aur"))["label"] == "aur"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_pages_emits_badges(tmp_path, monkeypatch):
|
|
20
|
+
monkeypatch.chdir(ROOT)
|
|
21
|
+
out = tmp_path / "site"
|
|
22
|
+
assert main(["pages", "--out", str(out)]) == 0
|
|
23
|
+
overall = json.loads((out / "badge.json").read_text())
|
|
24
|
+
assert overall["schemaVersion"] == 1 and overall["message"] in {"green", "amber", "red"}
|
|
25
|
+
aur = json.loads((out / "badges" / "PRJ-001-AUR.json").read_text())
|
|
26
|
+
assert aur["label"] == "aur" and aur["message"] in {"green", "amber", "red"}
|
|
@@ -30,6 +30,38 @@ def test_unsupported_type_raises(tmp_path):
|
|
|
30
30
|
E.extract(f)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def _minimal_pdf(text: str) -> bytes:
|
|
34
|
+
"""Assemble a one-page PDF with `text` in a content stream, xref included."""
|
|
35
|
+
stream = f"BT /F1 24 Tf 72 720 Td ({text}) Tj ET".encode()
|
|
36
|
+
objects = [
|
|
37
|
+
b"<< /Type /Catalog /Pages 2 0 R >>",
|
|
38
|
+
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
|
|
39
|
+
(b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] "
|
|
40
|
+
b"/Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>"),
|
|
41
|
+
b"<< /Length " + str(len(stream)).encode() + b" >>\nstream\n" + stream + b"\nendstream",
|
|
42
|
+
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
|
|
43
|
+
]
|
|
44
|
+
out = b"%PDF-1.4\n"
|
|
45
|
+
offsets = []
|
|
46
|
+
for i, body in enumerate(objects, start=1):
|
|
47
|
+
offsets.append(len(out))
|
|
48
|
+
out += f"{i} 0 obj\n".encode() + body + b"\nendobj\n"
|
|
49
|
+
xref_at = len(out)
|
|
50
|
+
out += f"xref\n0 {len(objects) + 1}\n0000000000 65535 f \n".encode()
|
|
51
|
+
for off in offsets:
|
|
52
|
+
out += f"{off:010d} 00000 n \n".encode()
|
|
53
|
+
out += (f"trailer\n<< /Size {len(objects) + 1} /Root 1 0 R >>\n"
|
|
54
|
+
f"startxref\n{xref_at}\n%%EOF\n").encode()
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_extract_pdf(tmp_path):
|
|
59
|
+
pytest.importorskip("pypdf") # needs the 'convert' extra
|
|
60
|
+
path = tmp_path / "s.pdf"
|
|
61
|
+
path.write_bytes(_minimal_pdf("Hello docassert PDF"))
|
|
62
|
+
assert "Hello docassert PDF" in E.extract(path)
|
|
63
|
+
|
|
64
|
+
|
|
33
65
|
def test_extract_docx_paragraphs_and_tables(tmp_path):
|
|
34
66
|
docx = pytest.importorskip("docx") # needs the 'convert' extra
|
|
35
67
|
d = docx.Document()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Tests for the machine-readable JSON report (`--json`)."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from docassert import report
|
|
6
|
+
from docassert.cli import main
|
|
7
|
+
from docassert.models import CheckResult
|
|
8
|
+
|
|
9
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_json_report_shape():
|
|
13
|
+
results = {
|
|
14
|
+
"a.md": [CheckResult("c1", True, True, "ok"),
|
|
15
|
+
CheckResult("c2", False, True, "bad")],
|
|
16
|
+
"b.md": [CheckResult("c3", False, False, "meh", kind="semantic", score=0.4)],
|
|
17
|
+
}
|
|
18
|
+
data = json.loads(report.json_report(results))
|
|
19
|
+
assert data["summary"] == {"documents": 2, "checks": 3, "blocking_failures": 1,
|
|
20
|
+
"advisory_failures": 1, "passed": False}
|
|
21
|
+
assert data["documents"]["a.md"][1]["check_id"] == "c2"
|
|
22
|
+
assert data["documents"]["b.md"][0]["score"] == 0.4
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_cli_validate_writes_json(tmp_path, monkeypatch):
|
|
26
|
+
monkeypatch.chdir(ROOT) # criteria/schema resolve; sample documents exist
|
|
27
|
+
out = tmp_path / "r.json"
|
|
28
|
+
code = main(["validate", "documents/PRJ-001-AUR/charter.md", "--json", str(out)])
|
|
29
|
+
assert code == 0
|
|
30
|
+
data = json.loads(out.read_text())
|
|
31
|
+
assert data["summary"]["passed"] is True
|
|
32
|
+
assert "documents/PRJ-001-AUR/charter.md" in data["documents"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_cli_consistency_writes_json(tmp_path, monkeypatch):
|
|
36
|
+
monkeypatch.chdir(ROOT)
|
|
37
|
+
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
|
38
|
+
out = tmp_path / "c.json"
|
|
39
|
+
code = main(["consistency", "--no-semantic", "--json", str(out)])
|
|
40
|
+
assert code == 0
|
|
41
|
+
data = json.loads(out.read_text())
|
|
42
|
+
checks = {c["check_id"] for c in data["documents"]["consistency (cross-document)"]}
|
|
43
|
+
assert {"item-id-uniqueness", "referential-integrity", "coverage"} <= checks
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/benefits-realization.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.4.0 → docassert-0.6.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
|
|
File without changes
|
{docassert-0.4.0 → docassert-0.6.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.4.0 → docassert-0.6.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
|
|
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.4.0 → docassert-0.6.0}/docassert/_data/templates/benefits-realization.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/data-migration-plan.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.4.0 → docassert-0.6.0}/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.4.0 → docassert-0.6.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
|