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.
Files changed (110) hide show
  1. {docassert-0.4.0/docassert.egg-info → docassert-0.6.0}/PKG-INFO +19 -5
  2. {docassert-0.4.0 → docassert-0.6.0}/README.md +16 -4
  3. {docassert-0.4.0 → docassert-0.6.0}/docassert/__init__.py +1 -1
  4. {docassert-0.4.0 → docassert-0.6.0}/docassert/cli.py +15 -1
  5. {docassert-0.4.0 → docassert-0.6.0}/docassert/consistency.py +4 -2
  6. {docassert-0.4.0 → docassert-0.6.0}/docassert/profiles.py +1 -1
  7. {docassert-0.4.0 → docassert-0.6.0}/docassert/report.py +27 -1
  8. {docassert-0.4.0 → docassert-0.6.0}/docassert/semantic.py +1 -1
  9. {docassert-0.4.0 → docassert-0.6.0}/docassert/status.py +10 -2
  10. {docassert-0.4.0 → docassert-0.6.0/docassert.egg-info}/PKG-INFO +19 -5
  11. {docassert-0.4.0 → docassert-0.6.0}/docassert.egg-info/SOURCES.txt +2 -0
  12. {docassert-0.4.0 → docassert-0.6.0}/docassert.egg-info/requires.txt +2 -0
  13. {docassert-0.4.0 → docassert-0.6.0}/pyproject.toml +18 -1
  14. docassert-0.6.0/tests/test_badge.py +26 -0
  15. {docassert-0.4.0 → docassert-0.6.0}/tests/test_extract.py +32 -0
  16. docassert-0.6.0/tests/test_json_report.py +43 -0
  17. {docassert-0.4.0 → docassert-0.6.0}/LICENSE +0 -0
  18. {docassert-0.4.0 → docassert-0.6.0}/NOTICE +0 -0
  19. {docassert-0.4.0 → docassert-0.6.0}/docassert/__main__.py +0 -0
  20. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/consistency.yaml +0 -0
  21. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
  22. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
  23. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
  24. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
  25. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
  26. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
  27. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
  28. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
  29. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
  30. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
  31. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
  32. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
  33. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
  34. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
  35. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
  36. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
  37. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
  38. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
  39. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
  40. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
  41. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
  42. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
  43. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
  44. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/adr.schema.json +0 -0
  45. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
  46. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/brd.schema.json +0 -0
  47. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/business-case.schema.json +0 -0
  48. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/charter.schema.json +0 -0
  49. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
  50. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/frnfr.schema.json +0 -0
  51. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
  52. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
  53. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/prd.schema.json +0 -0
  54. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/project.schema.json +0 -0
  55. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
  56. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
  57. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
  58. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/risk-register.schema.json +0 -0
  59. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
  60. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/runbook.schema.json +0 -0
  61. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/status-report.schema.json +0 -0
  62. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/test-cases.schema.json +0 -0
  63. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/schema/user-story.schema.json +0 -0
  64. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/skills/doc-to-pmo/SKILL.md +0 -0
  65. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/adr.template.md +0 -0
  66. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/benefits-realization.template.md +0 -0
  67. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/brd.template.md +0 -0
  68. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/business-case.template.md +0 -0
  69. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/charter.template.md +0 -0
  70. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/data-migration-plan.template.md +0 -0
  71. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/frnfr.template.md +0 -0
  72. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/hypercare-plan.template.md +0 -0
  73. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/post-implementation-review.template.md +0 -0
  74. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/prd.template.md +0 -0
  75. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/project.template.md +0 -0
  76. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/qa-test-plan.template.md +0 -0
  77. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
  78. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
  79. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/risk-register.template.md +0 -0
  80. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/rollback-plan.template.md +0 -0
  81. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/runbook.template.md +0 -0
  82. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/status-report.template.md +0 -0
  83. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/test-cases.template.md +0 -0
  84. {docassert-0.4.0 → docassert-0.6.0}/docassert/_data/templates/user-story.template.md +0 -0
  85. {docassert-0.4.0 → docassert-0.6.0}/docassert/config.py +0 -0
  86. {docassert-0.4.0 → docassert-0.6.0}/docassert/extract.py +0 -0
  87. {docassert-0.4.0 → docassert-0.6.0}/docassert/graph.py +0 -0
  88. {docassert-0.4.0 → docassert-0.6.0}/docassert/loader.py +0 -0
  89. {docassert-0.4.0 → docassert-0.6.0}/docassert/models.py +0 -0
  90. {docassert-0.4.0 → docassert-0.6.0}/docassert/projects.py +0 -0
  91. {docassert-0.4.0 → docassert-0.6.0}/docassert/rtm.py +0 -0
  92. {docassert-0.4.0 → docassert-0.6.0}/docassert/scaffold.py +0 -0
  93. {docassert-0.4.0 → docassert-0.6.0}/docassert/structural.py +0 -0
  94. {docassert-0.4.0 → docassert-0.6.0}/docassert.egg-info/dependency_links.txt +0 -0
  95. {docassert-0.4.0 → docassert-0.6.0}/docassert.egg-info/entry_points.txt +0 -0
  96. {docassert-0.4.0 → docassert-0.6.0}/docassert.egg-info/top_level.txt +0 -0
  97. {docassert-0.4.0 → docassert-0.6.0}/setup.cfg +0 -0
  98. {docassert-0.4.0 → docassert-0.6.0}/tests/test_config.py +0 -0
  99. {docassert-0.4.0 → docassert-0.6.0}/tests/test_consistency.py +0 -0
  100. {docassert-0.4.0 → docassert-0.6.0}/tests/test_defects.py +0 -0
  101. {docassert-0.4.0 → docassert-0.6.0}/tests/test_graph.py +0 -0
  102. {docassert-0.4.0 → docassert-0.6.0}/tests/test_kinds_delivery.py +0 -0
  103. {docassert-0.4.0 → docassert-0.6.0}/tests/test_kinds_governance.py +0 -0
  104. {docassert-0.4.0 → docassert-0.6.0}/tests/test_kinds_operate.py +0 -0
  105. {docassert-0.4.0 → docassert-0.6.0}/tests/test_kinds_reporting.py +0 -0
  106. {docassert-0.4.0 → docassert-0.6.0}/tests/test_profiles.py +0 -0
  107. {docassert-0.4.0 → docassert-0.6.0}/tests/test_projects.py +0 -0
  108. {docassert-0.4.0 → docassert-0.6.0}/tests/test_scaffold.py +0 -0
  109. {docassert-0.4.0 → docassert-0.6.0}/tests/test_status.py +0 -0
  110. {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.4.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 files.
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 files.
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
@@ -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.4.0"
8
+ __version__ = "0.6.0"
@@ -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, draft_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, draft_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, 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."""
@@ -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.text for block in message.content
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.4.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 files.
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
@@ -11,4 +11,6 @@ pypdf>=4.0
11
11
 
12
12
  [dev]
13
13
  pytest>=8.0
14
+ pytest-cov>=5.0
14
15
  ruff>=0.6
16
+ mypy>=1.10
@@ -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
File without changes
File without changes
File without changes
File without changes