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.
Files changed (109) hide show
  1. {docassert-0.3.0/docassert.egg-info → docassert-0.5.0}/PKG-INFO +19 -4
  2. {docassert-0.3.0 → docassert-0.5.0}/README.md +18 -3
  3. {docassert-0.3.0 → docassert-0.5.0}/docassert/__init__.py +1 -1
  4. docassert-0.5.0/docassert/_data/skills/doc-to-pmo/SKILL.md +100 -0
  5. {docassert-0.3.0 → docassert-0.5.0}/docassert/cli.py +6 -0
  6. {docassert-0.3.0 → docassert-0.5.0}/docassert/config.py +16 -6
  7. {docassert-0.3.0 → docassert-0.5.0}/docassert/report.py +27 -1
  8. {docassert-0.3.0 → docassert-0.5.0/docassert.egg-info}/PKG-INFO +19 -4
  9. {docassert-0.3.0 → docassert-0.5.0}/docassert.egg-info/SOURCES.txt +2 -0
  10. {docassert-0.3.0 → docassert-0.5.0}/tests/test_config.py +15 -1
  11. {docassert-0.3.0 → docassert-0.5.0}/tests/test_extract.py +32 -0
  12. docassert-0.5.0/tests/test_json_report.py +43 -0
  13. {docassert-0.3.0 → docassert-0.5.0}/LICENSE +0 -0
  14. {docassert-0.3.0 → docassert-0.5.0}/NOTICE +0 -0
  15. {docassert-0.3.0 → docassert-0.5.0}/docassert/__main__.py +0 -0
  16. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/consistency.yaml +0 -0
  17. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
  18. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
  19. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
  20. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
  21. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
  22. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
  23. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
  24. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
  25. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
  26. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
  27. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
  28. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
  29. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
  30. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
  31. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
  32. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
  33. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
  34. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
  35. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
  36. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
  37. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
  38. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
  39. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
  40. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/adr.schema.json +0 -0
  41. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
  42. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/brd.schema.json +0 -0
  43. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/business-case.schema.json +0 -0
  44. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/charter.schema.json +0 -0
  45. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
  46. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/frnfr.schema.json +0 -0
  47. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
  48. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
  49. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/prd.schema.json +0 -0
  50. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/project.schema.json +0 -0
  51. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
  52. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
  53. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
  54. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/risk-register.schema.json +0 -0
  55. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
  56. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/runbook.schema.json +0 -0
  57. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/status-report.schema.json +0 -0
  58. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/test-cases.schema.json +0 -0
  59. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/schema/user-story.schema.json +0 -0
  60. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/adr.template.md +0 -0
  61. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/benefits-realization.template.md +0 -0
  62. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/brd.template.md +0 -0
  63. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/business-case.template.md +0 -0
  64. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/charter.template.md +0 -0
  65. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/data-migration-plan.template.md +0 -0
  66. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/frnfr.template.md +0 -0
  67. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/hypercare-plan.template.md +0 -0
  68. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/post-implementation-review.template.md +0 -0
  69. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/prd.template.md +0 -0
  70. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/project.template.md +0 -0
  71. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/qa-test-plan.template.md +0 -0
  72. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
  73. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
  74. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/risk-register.template.md +0 -0
  75. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/rollback-plan.template.md +0 -0
  76. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/runbook.template.md +0 -0
  77. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/status-report.template.md +0 -0
  78. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/test-cases.template.md +0 -0
  79. {docassert-0.3.0 → docassert-0.5.0}/docassert/_data/templates/user-story.template.md +0 -0
  80. {docassert-0.3.0 → docassert-0.5.0}/docassert/consistency.py +0 -0
  81. {docassert-0.3.0 → docassert-0.5.0}/docassert/extract.py +0 -0
  82. {docassert-0.3.0 → docassert-0.5.0}/docassert/graph.py +0 -0
  83. {docassert-0.3.0 → docassert-0.5.0}/docassert/loader.py +0 -0
  84. {docassert-0.3.0 → docassert-0.5.0}/docassert/models.py +0 -0
  85. {docassert-0.3.0 → docassert-0.5.0}/docassert/profiles.py +0 -0
  86. {docassert-0.3.0 → docassert-0.5.0}/docassert/projects.py +0 -0
  87. {docassert-0.3.0 → docassert-0.5.0}/docassert/rtm.py +0 -0
  88. {docassert-0.3.0 → docassert-0.5.0}/docassert/scaffold.py +0 -0
  89. {docassert-0.3.0 → docassert-0.5.0}/docassert/semantic.py +0 -0
  90. {docassert-0.3.0 → docassert-0.5.0}/docassert/status.py +0 -0
  91. {docassert-0.3.0 → docassert-0.5.0}/docassert/structural.py +0 -0
  92. {docassert-0.3.0 → docassert-0.5.0}/docassert.egg-info/dependency_links.txt +0 -0
  93. {docassert-0.3.0 → docassert-0.5.0}/docassert.egg-info/entry_points.txt +0 -0
  94. {docassert-0.3.0 → docassert-0.5.0}/docassert.egg-info/requires.txt +0 -0
  95. {docassert-0.3.0 → docassert-0.5.0}/docassert.egg-info/top_level.txt +0 -0
  96. {docassert-0.3.0 → docassert-0.5.0}/pyproject.toml +0 -0
  97. {docassert-0.3.0 → docassert-0.5.0}/setup.cfg +0 -0
  98. {docassert-0.3.0 → docassert-0.5.0}/tests/test_consistency.py +0 -0
  99. {docassert-0.3.0 → docassert-0.5.0}/tests/test_defects.py +0 -0
  100. {docassert-0.3.0 → docassert-0.5.0}/tests/test_graph.py +0 -0
  101. {docassert-0.3.0 → docassert-0.5.0}/tests/test_kinds_delivery.py +0 -0
  102. {docassert-0.3.0 → docassert-0.5.0}/tests/test_kinds_governance.py +0 -0
  103. {docassert-0.3.0 → docassert-0.5.0}/tests/test_kinds_operate.py +0 -0
  104. {docassert-0.3.0 → docassert-0.5.0}/tests/test_kinds_reporting.py +0 -0
  105. {docassert-0.3.0 → docassert-0.5.0}/tests/test_profiles.py +0 -0
  106. {docassert-0.3.0 → docassert-0.5.0}/tests/test_projects.py +0 -0
  107. {docassert-0.3.0 → docassert-0.5.0}/tests/test_scaffold.py +0 -0
  108. {docassert-0.3.0 → docassert-0.5.0}/tests/test_status.py +0 -0
  109. {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.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
@@ -5,4 +5,4 @@ standard: deterministic structural checks that gate a merge, plus optional
5
5
  AI-graded semantic checks that advise.
6
6
  """
7
7
 
8
- __version__ = "0.3.0"
8
+ __version__ = "0.5.0"
@@ -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
- _INIT_TREE = ["criteria", "schema", "profiles", "templates", "consistency.yaml"]
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 top-level names that were created."""
118
+ Returns the destination names that were created."""
110
119
  dest = Path(dest)
111
120
  created: list[str] = []
112
- for name in _INIT_TREE:
113
- target = dest / name
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 / name
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(name)
131
+ created.append(dest_name)
122
132
  return created
@@ -1,6 +1,7 @@
1
- """Render check results as console text, PR-comment markdown, or JUnit XML."""
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.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", "consistency.yaml"}
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
File without changes
File without changes
File without changes
File without changes
File without changes