docassert 0.3.0__tar.gz → 0.5.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.3.0/docassert.egg-info → docassert-0.5.0}/PKG-INFO +19 -4
- {docassert-0.3.0 → docassert-0.5.0}/README.md +18 -3
- {docassert-0.3.0 → docassert-0.5.0}/docassert/__init__.py +1 -1
- docassert-0.5.0/docassert/_data/skills/doc-to-pmo/SKILL.md +100 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/cli.py +6 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/config.py +16 -6
- {docassert-0.3.0 → docassert-0.5.0}/docassert/report.py +27 -1
- {docassert-0.3.0 → docassert-0.5.0/docassert.egg-info}/PKG-INFO +19 -4
- {docassert-0.3.0 → docassert-0.5.0}/docassert.egg-info/SOURCES.txt +2 -0
- {docassert-0.3.0 → docassert-0.5.0}/tests/test_config.py +15 -1
- {docassert-0.3.0 → docassert-0.5.0}/tests/test_extract.py +32 -0
- docassert-0.5.0/tests/test_json_report.py +43 -0
- {docassert-0.3.0 → docassert-0.5.0}/LICENSE +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/NOTICE +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/__main__.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/consistency.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/adr.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/brd.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/business-case.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/charter.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/frnfr.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/prd.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/project.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/risk-register.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/runbook.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/status-report.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/test-cases.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/user-story.schema.json +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/adr.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/benefits-realization.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/brd.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/business-case.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/charter.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/data-migration-plan.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/frnfr.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/hypercare-plan.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/post-implementation-review.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/prd.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/project.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/qa-test-plan.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/risk-register.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/rollback-plan.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/runbook.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/status-report.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/test-cases.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/user-story.template.md +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/consistency.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/extract.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/graph.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/loader.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/models.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/profiles.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/projects.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/rtm.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/scaffold.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/semantic.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/status.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert/structural.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert.egg-info/dependency_links.txt +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert.egg-info/entry_points.txt +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert.egg-info/requires.txt +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/docassert.egg-info/top_level.txt +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/pyproject.toml +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/setup.cfg +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/tests/test_consistency.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/tests/test_defects.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/tests/test_graph.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/tests/test_kinds_delivery.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/tests/test_kinds_governance.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/tests/test_kinds_operate.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/tests/test_kinds_reporting.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/tests/test_profiles.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/tests/test_projects.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/tests/test_scaffold.py +0 -0
- {docassert-0.3.0 → docassert-0.5.0}/tests/test_status.py +0 -0
- {docassert-0.3.0 → docassert-0.5.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.5.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
|
|
@@ -75,14 +75,18 @@ docassert pages --out _site # a portfolio dashboard + a page per proje
|
|
|
75
75
|
Config resolves **local override → packaged default**: docassert ships sensible
|
|
76
76
|
defaults, and your repo's own `criteria/` (or `schema/`, `profiles/`,
|
|
77
77
|
`consistency.yaml`) wins when present. `docassert init` copies the defaults in so
|
|
78
|
-
you can customize them
|
|
78
|
+
you can customize them — including the **doc-to-pmo Claude skill** into
|
|
79
|
+
`.claude/skills/`, so Claude Code in your repo knows how to convert existing
|
|
80
|
+
Word/PDF documents into testable docassert documents (faithfully — gaps are
|
|
81
|
+
flagged as TODOs, never invented). The skill's source is
|
|
82
|
+
[`skills/doc-to-pmo/SKILL.md`](skills/doc-to-pmo/SKILL.md).
|
|
79
83
|
|
|
80
84
|
## Commands
|
|
81
85
|
|
|
82
86
|
| Command | What it does |
|
|
83
87
|
|---|---|
|
|
84
|
-
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). |
|
|
85
|
-
| `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. |
|
|
88
|
+
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). Reports: `--junit` / `--markdown` / `--json`. |
|
|
89
|
+
| `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. Reports: `--junit` / `--markdown` / `--json`. |
|
|
86
90
|
| `docassert rtm [--project ID]` | Requirements traceability matrix (Markdown or CSV). |
|
|
87
91
|
| `docassert status [--project ID] [--index]` | Derived project status (md / json / html). |
|
|
88
92
|
| `docassert pages --out DIR` | Build the portfolio site (index + a page per project). |
|
|
@@ -113,6 +117,17 @@ kind is adding a trio — no code for the common cases.
|
|
|
113
117
|
- **Semantic — AI-graded, advisory.** Scored via the Anthropic API and posted to
|
|
114
118
|
the PR — never blocking. Set `ANTHROPIC_API_KEY` to enable; skipped otherwise.
|
|
115
119
|
|
|
120
|
+
## Privacy
|
|
121
|
+
|
|
122
|
+
Structural checks run **entirely locally** — no document content leaves your
|
|
123
|
+
machine or CI runner. Semantic checks are the one exception: when
|
|
124
|
+
`ANTHROPIC_API_KEY` is set, the graded excerpts (section text, linked item
|
|
125
|
+
text) are sent to the **Anthropic API** for scoring. Without the key, semantic
|
|
126
|
+
checks are skipped and nothing is sent anywhere. Alignment grading is capped at
|
|
127
|
+
`alignment_limit` links per run (default 25). If your documents are
|
|
128
|
+
confidential, run without the key or review [Anthropic's data-usage
|
|
129
|
+
policies](https://www.anthropic.com/legal/commercial-terms) first.
|
|
130
|
+
|
|
116
131
|
## Development
|
|
117
132
|
|
|
118
133
|
```bash
|
|
@@ -37,14 +37,18 @@ docassert pages --out _site # a portfolio dashboard + a page per proje
|
|
|
37
37
|
Config resolves **local override → packaged default**: docassert ships sensible
|
|
38
38
|
defaults, and your repo's own `criteria/` (or `schema/`, `profiles/`,
|
|
39
39
|
`consistency.yaml`) wins when present. `docassert init` copies the defaults in so
|
|
40
|
-
you can customize them
|
|
40
|
+
you can customize them — including the **doc-to-pmo Claude skill** into
|
|
41
|
+
`.claude/skills/`, so Claude Code in your repo knows how to convert existing
|
|
42
|
+
Word/PDF documents into testable docassert documents (faithfully — gaps are
|
|
43
|
+
flagged as TODOs, never invented). The skill's source is
|
|
44
|
+
[`skills/doc-to-pmo/SKILL.md`](skills/doc-to-pmo/SKILL.md).
|
|
41
45
|
|
|
42
46
|
## Commands
|
|
43
47
|
|
|
44
48
|
| Command | What it does |
|
|
45
49
|
|---|---|
|
|
46
|
-
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). |
|
|
47
|
-
| `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. |
|
|
50
|
+
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). Reports: `--junit` / `--markdown` / `--json`. |
|
|
51
|
+
| `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. Reports: `--junit` / `--markdown` / `--json`. |
|
|
48
52
|
| `docassert rtm [--project ID]` | Requirements traceability matrix (Markdown or CSV). |
|
|
49
53
|
| `docassert status [--project ID] [--index]` | Derived project status (md / json / html). |
|
|
50
54
|
| `docassert pages --out DIR` | Build the portfolio site (index + a page per project). |
|
|
@@ -75,6 +79,17 @@ kind is adding a trio — no code for the common cases.
|
|
|
75
79
|
- **Semantic — AI-graded, advisory.** Scored via the Anthropic API and posted to
|
|
76
80
|
the PR — never blocking. Set `ANTHROPIC_API_KEY` to enable; skipped otherwise.
|
|
77
81
|
|
|
82
|
+
## Privacy
|
|
83
|
+
|
|
84
|
+
Structural checks run **entirely locally** — no document content leaves your
|
|
85
|
+
machine or CI runner. Semantic checks are the one exception: when
|
|
86
|
+
`ANTHROPIC_API_KEY` is set, the graded excerpts (section text, linked item
|
|
87
|
+
text) are sent to the **Anthropic API** for scoring. Without the key, semantic
|
|
88
|
+
checks are skipped and nothing is sent anywhere. Alignment grading is capped at
|
|
89
|
+
`alignment_limit` links per run (default 25). If your documents are
|
|
90
|
+
confidential, run without the key or review [Anthropic's data-usage
|
|
91
|
+
policies](https://www.anthropic.com/legal/commercial-terms) first.
|
|
92
|
+
|
|
78
93
|
## Development
|
|
79
94
|
|
|
80
95
|
```bash
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doc-to-pmo
|
|
3
|
+
description: Convert an existing business document (Word .docx, PDF, or pasted text) into a docassert Markdown document so it can be unit-tested and gated in CI. Use when someone wants to bring an existing charter, BRD, PRD, risk register, or any other supported kind into a PMO-as-Code repo.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# doc-to-pmo
|
|
7
|
+
|
|
8
|
+
Convert a source document into a standard docassert document. **Map the content
|
|
9
|
+
faithfully — never invent missing facts to make the audit pass.** A document
|
|
10
|
+
that lacks measurable success criteria *should* fail the audit; your job is to
|
|
11
|
+
surface that, not hide it.
|
|
12
|
+
|
|
13
|
+
Requires docassert (`pipx install docassert`; add `"docassert[convert]"` for
|
|
14
|
+
.docx/.pdf extraction).
|
|
15
|
+
|
|
16
|
+
## Steps
|
|
17
|
+
|
|
18
|
+
1. **Identify the kind.** Supported kinds: `charter`, `business-case`, `brd`,
|
|
19
|
+
`prd`, `frnfr`, `user-story`, `test-cases`, `adr`, `risk-register`,
|
|
20
|
+
`raci-stakeholder`, `qa-test-plan`, `data-migration-plan`,
|
|
21
|
+
`release-cutover-plan`, `rollback-plan`, `hypercare-plan`, `runbook`,
|
|
22
|
+
`status-report`, `post-implementation-review`, `benefits-realization`.
|
|
23
|
+
One source file may yield several documents (a "BRD" is often really a
|
|
24
|
+
BRD + PRD + NFRs + risks). Propose the split to the user before writing.
|
|
25
|
+
|
|
26
|
+
2. **Anchor the project.** Every document belongs to a project
|
|
27
|
+
(`PRJ-NNN-CODE`). Check `projects.yaml` or `documents/` for an existing
|
|
28
|
+
anchor; if there is none, create one (ask the user for the code, name, and
|
|
29
|
+
sponsor — don't guess):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
docassert new project --code AUR --name "Aurora — Customer Onboarding"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
3. **Extract the source text** (`.docx`, `.pdf`, `.md`, `.txt`):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
docassert extract path/to/source.docx
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For pasted text or Google Docs, ask the user to paste the content or export
|
|
42
|
+
to `.docx`/`.pdf` first.
|
|
43
|
+
|
|
44
|
+
4. **Scaffold, then fill.** Let docassert create the file with the identity
|
|
45
|
+
(frontmatter `project:`, namespaced `id:`) already correct, then map the
|
|
46
|
+
source content into its sections:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
docassert new brd --project PRJ-001-AUR
|
|
50
|
+
# docassert: created documents/PRJ-001-AUR/brd.md
|
|
51
|
+
# docassert: next item id: AUR-BR-001
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Keep `status: draft` unless the source clearly states an approval.
|
|
55
|
+
|
|
56
|
+
5. **Author traceable items where the kind defines them.** Requirements,
|
|
57
|
+
criteria, tests, risks, and decisions are bullets with a stable namespaced
|
|
58
|
+
id and typed links — use the `next item id` hints for numbering:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
- **AUR-BR-001**: The business shall reduce onboarding time to under 2 days.
|
|
62
|
+
- **AUR-PR-001** (traces: AUR-BR-001): The product shall provide a self-serve flow.
|
|
63
|
+
- **AUR-RISK-001** (threatens: AUR-BR-001): Migration may slip. Probability: High.
|
|
64
|
+
Impact: High. Owner: alex.kim. Response: dual-run for two weeks.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Relations: `traces` (child → parent requirement), `verifies` (AC → PR/FR),
|
|
68
|
+
`tests` (TC → AC), `threatens` (RISK → BR/PR), `affects` (ADR → FR/NFR).
|
|
69
|
+
Only link items that genuinely relate in the source.
|
|
70
|
+
|
|
71
|
+
6. **Flag gaps honestly.** Wherever the source does not supply required
|
|
72
|
+
information, insert a bullet or note beginning with `TODO:` describing what
|
|
73
|
+
is missing (e.g. `- TODO: no measurable success criteria found in the
|
|
74
|
+
source — add a metric and threshold.`). Do **not** fabricate a number, an
|
|
75
|
+
owner, a date, or a mitigation.
|
|
76
|
+
|
|
77
|
+
7. **Validate and report.**
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
docassert validate documents/PRJ-001-AUR/*.md
|
|
81
|
+
docassert consistency
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Summarise what passed, what blocks, and exactly which TODOs the user must
|
|
85
|
+
resolve before the document can merge.
|
|
86
|
+
|
|
87
|
+
8. **Hand off.** Tell the user to review the generated files, resolve the
|
|
88
|
+
TODOs, then commit and open a pull request — CI re-runs the same audit and
|
|
89
|
+
gates the merge.
|
|
90
|
+
|
|
91
|
+
## Guardrails
|
|
92
|
+
|
|
93
|
+
- **Faithful over passing**: it is correct and expected for a weak source to
|
|
94
|
+
produce a document that fails the audit. That is the pipeline working.
|
|
95
|
+
- Never invent approvals, owners, budgets, dates, or measurable thresholds.
|
|
96
|
+
- Never mark a document `approved`; that is a human decision made in review.
|
|
97
|
+
- If you are unsure whether something in the source is a real measurable
|
|
98
|
+
criterion, leave it as written and let the audit judge it.
|
|
99
|
+
- Confidential sources stay local: don't push converted content to a public
|
|
100
|
+
repo without the user's say-so.
|
|
@@ -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
|
|
|
@@ -292,12 +296,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
292
296
|
v.add_argument("paths", nargs="+", help="Markdown files or globs.")
|
|
293
297
|
v.add_argument("--junit", help="Write a JUnit XML report to this path.")
|
|
294
298
|
v.add_argument("--markdown", help="Write a PR-comment markdown report to this path.")
|
|
299
|
+
v.add_argument("--json", help="Write a machine-readable JSON report to this path.")
|
|
295
300
|
docs_dir_opt(v)
|
|
296
301
|
v.set_defaults(func=cmd_validate)
|
|
297
302
|
|
|
298
303
|
c = sub.add_parser("consistency", help="Check cross-document traceability.")
|
|
299
304
|
c.add_argument("--junit", help="Write a JUnit XML report to this path.")
|
|
300
305
|
c.add_argument("--markdown", help="Write a PR-comment markdown report to this path.")
|
|
306
|
+
c.add_argument("--json", help="Write a machine-readable JSON report to this path.")
|
|
301
307
|
c.add_argument("--no-semantic", action="store_true",
|
|
302
308
|
help="Skip AI alignment (structural consistency only).")
|
|
303
309
|
docs_dir_opt(c)
|
|
@@ -101,22 +101,32 @@ def available_profiles() -> list[str]:
|
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
# ── scaffolding (docassert init) ────────────────────────────────────────────
|
|
104
|
-
|
|
104
|
+
# (packaged source, destination in the repo). The doc-to-pmo Claude skill lands
|
|
105
|
+
# under .claude/skills/ so Claude Code discovers it in the user's repo.
|
|
106
|
+
_INIT_TREE = [
|
|
107
|
+
("criteria", "criteria"),
|
|
108
|
+
("schema", "schema"),
|
|
109
|
+
("profiles", "profiles"),
|
|
110
|
+
("templates", "templates"),
|
|
111
|
+
("consistency.yaml", "consistency.yaml"),
|
|
112
|
+
("skills", ".claude/skills"),
|
|
113
|
+
]
|
|
105
114
|
|
|
106
115
|
|
|
107
116
|
def init(dest: str | Path = ".") -> list[str]:
|
|
108
117
|
"""Copy the packaged defaults into `dest`, skipping anything already present.
|
|
109
|
-
Returns the
|
|
118
|
+
Returns the destination names that were created."""
|
|
110
119
|
dest = Path(dest)
|
|
111
120
|
created: list[str] = []
|
|
112
|
-
for
|
|
113
|
-
target = dest /
|
|
121
|
+
for src_name, dest_name in _INIT_TREE:
|
|
122
|
+
target = dest / dest_name
|
|
114
123
|
if target.exists():
|
|
115
124
|
continue
|
|
116
|
-
src = DATA_DIR /
|
|
125
|
+
src = DATA_DIR / src_name
|
|
117
126
|
if src.is_dir():
|
|
118
127
|
shutil.copytree(src, target)
|
|
119
128
|
else:
|
|
129
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
120
130
|
target.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
|
|
121
|
-
created.append(
|
|
131
|
+
created.append(dest_name)
|
|
122
132
|
return created
|
|
@@ -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."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docassert
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -75,14 +75,18 @@ docassert pages --out _site # a portfolio dashboard + a page per proje
|
|
|
75
75
|
Config resolves **local override → packaged default**: docassert ships sensible
|
|
76
76
|
defaults, and your repo's own `criteria/` (or `schema/`, `profiles/`,
|
|
77
77
|
`consistency.yaml`) wins when present. `docassert init` copies the defaults in so
|
|
78
|
-
you can customize them
|
|
78
|
+
you can customize them — including the **doc-to-pmo Claude skill** into
|
|
79
|
+
`.claude/skills/`, so Claude Code in your repo knows how to convert existing
|
|
80
|
+
Word/PDF documents into testable docassert documents (faithfully — gaps are
|
|
81
|
+
flagged as TODOs, never invented). The skill's source is
|
|
82
|
+
[`skills/doc-to-pmo/SKILL.md`](skills/doc-to-pmo/SKILL.md).
|
|
79
83
|
|
|
80
84
|
## Commands
|
|
81
85
|
|
|
82
86
|
| Command | What it does |
|
|
83
87
|
|---|---|
|
|
84
|
-
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). |
|
|
85
|
-
| `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. |
|
|
88
|
+
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). Reports: `--junit` / `--markdown` / `--json`. |
|
|
89
|
+
| `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. Reports: `--junit` / `--markdown` / `--json`. |
|
|
86
90
|
| `docassert rtm [--project ID]` | Requirements traceability matrix (Markdown or CSV). |
|
|
87
91
|
| `docassert status [--project ID] [--index]` | Derived project status (md / json / html). |
|
|
88
92
|
| `docassert pages --out DIR` | Build the portfolio site (index + a page per project). |
|
|
@@ -113,6 +117,17 @@ kind is adding a trio — no code for the common cases.
|
|
|
113
117
|
- **Semantic — AI-graded, advisory.** Scored via the Anthropic API and posted to
|
|
114
118
|
the PR — never blocking. Set `ANTHROPIC_API_KEY` to enable; skipped otherwise.
|
|
115
119
|
|
|
120
|
+
## Privacy
|
|
121
|
+
|
|
122
|
+
Structural checks run **entirely locally** — no document content leaves your
|
|
123
|
+
machine or CI runner. Semantic checks are the one exception: when
|
|
124
|
+
`ANTHROPIC_API_KEY` is set, the graded excerpts (section text, linked item
|
|
125
|
+
text) are sent to the **Anthropic API** for scoring. Without the key, semantic
|
|
126
|
+
checks are skipped and nothing is sent anywhere. Alignment grading is capped at
|
|
127
|
+
`alignment_limit` links per run (default 25). If your documents are
|
|
128
|
+
confidential, run without the key or review [Anthropic's data-usage
|
|
129
|
+
policies](https://www.anthropic.com/legal/commercial-terms) first.
|
|
130
|
+
|
|
116
131
|
## Development
|
|
117
132
|
|
|
118
133
|
```bash
|
|
@@ -69,6 +69,7 @@ docassert/_data/schema/runbook.schema.json
|
|
|
69
69
|
docassert/_data/schema/status-report.schema.json
|
|
70
70
|
docassert/_data/schema/test-cases.schema.json
|
|
71
71
|
docassert/_data/schema/user-story.schema.json
|
|
72
|
+
docassert/_data/skills/doc-to-pmo/SKILL.md
|
|
72
73
|
docassert/_data/templates/adr.template.md
|
|
73
74
|
docassert/_data/templates/benefits-realization.template.md
|
|
74
75
|
docassert/_data/templates/brd.template.md
|
|
@@ -94,6 +95,7 @@ tests/test_consistency.py
|
|
|
94
95
|
tests/test_defects.py
|
|
95
96
|
tests/test_extract.py
|
|
96
97
|
tests/test_graph.py
|
|
98
|
+
tests/test_json_report.py
|
|
97
99
|
tests/test_kinds_delivery.py
|
|
98
100
|
tests/test_kinds_governance.py
|
|
99
101
|
tests/test_kinds_operate.py
|
|
@@ -23,6 +23,9 @@ def test_packaged_data_mirrors_root_config():
|
|
|
23
23
|
f"{name}/{fn} drifted between the repo root and packaged _data"
|
|
24
24
|
assert filecmp.cmp(ROOT / "consistency.yaml",
|
|
25
25
|
config.DATA_DIR / "consistency.yaml", shallow=False)
|
|
26
|
+
assert filecmp.cmp(ROOT / "skills" / "doc-to-pmo" / "SKILL.md",
|
|
27
|
+
config.DATA_DIR / "skills" / "doc-to-pmo" / "SKILL.md",
|
|
28
|
+
shallow=False), "doc-to-pmo skill drifted between root and packaged copy"
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
# ── resolution: packaged default when there's no local config ───────────────
|
|
@@ -52,8 +55,19 @@ def test_unknown_kind_raises(monkeypatch, tmp_path):
|
|
|
52
55
|
# ── init scaffolds, and is idempotent ──────────────────────────────────────
|
|
53
56
|
def test_init_scaffolds_defaults(tmp_path):
|
|
54
57
|
created = config.init(tmp_path)
|
|
55
|
-
assert set(created) == {"criteria", "schema", "profiles", "templates",
|
|
58
|
+
assert set(created) == {"criteria", "schema", "profiles", "templates",
|
|
59
|
+
"consistency.yaml", ".claude/skills"}
|
|
56
60
|
assert (tmp_path / "criteria" / "charter.criteria.yaml").is_file()
|
|
57
61
|
assert (tmp_path / "schema" / "project.schema.json").is_file()
|
|
58
62
|
assert (tmp_path / "consistency.yaml").is_file()
|
|
63
|
+
assert (tmp_path / ".claude" / "skills" / "doc-to-pmo" / "SKILL.md").is_file()
|
|
59
64
|
assert config.init(tmp_path) == [] # second run creates nothing
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_init_keeps_existing_skills(tmp_path):
|
|
68
|
+
mine = tmp_path / ".claude" / "skills"
|
|
69
|
+
mine.mkdir(parents=True)
|
|
70
|
+
(mine / "my-skill.md").write_text("mine", encoding="utf-8")
|
|
71
|
+
created = config.init(tmp_path)
|
|
72
|
+
assert ".claude/skills" not in created # existing dir left alone
|
|
73
|
+
assert (mine / "my-skill.md").read_text() == "mine"
|
|
@@ -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.3.0 → docassert-0.5.0}/docassert/_data/criteria/benefits-realization.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.3.0 → docassert-0.5.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.3.0 → docassert-0.5.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.3.0 → docassert-0.5.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
|
{docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/benefits-realization.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/data-migration-plan.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.3.0 → docassert-0.5.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.3.0 → docassert-0.5.0}/docassert/_data/templates/release-cutover-plan.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|