docassert 0.2.0__tar.gz → 0.3.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 (107) hide show
  1. {docassert-0.2.0/docassert.egg-info → docassert-0.3.0}/PKG-INFO +9 -3
  2. {docassert-0.2.0 → docassert-0.3.0}/README.md +8 -2
  3. {docassert-0.2.0 → docassert-0.3.0}/docassert/__init__.py +1 -1
  4. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/consistency.yaml +4 -0
  5. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/adr.template.md +2 -1
  6. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/benefits-realization.template.md +2 -1
  7. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/brd.template.md +2 -1
  8. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/business-case.template.md +2 -1
  9. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/charter.template.md +2 -1
  10. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/data-migration-plan.template.md +2 -1
  11. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/frnfr.template.md +2 -1
  12. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/hypercare-plan.template.md +2 -1
  13. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/post-implementation-review.template.md +2 -1
  14. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/prd.template.md +2 -1
  15. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/qa-test-plan.template.md +2 -1
  16. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/raci-stakeholder.template.md +2 -1
  17. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/release-cutover-plan.template.md +2 -1
  18. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/risk-register.template.md +2 -1
  19. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/rollback-plan.template.md +2 -1
  20. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/runbook.template.md +2 -1
  21. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/status-report.template.md +2 -1
  22. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/test-cases.template.md +2 -1
  23. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/user-story.template.md +2 -1
  24. {docassert-0.2.0 → docassert-0.3.0}/docassert/cli.py +68 -21
  25. {docassert-0.2.0 → docassert-0.3.0}/docassert/config.py +18 -0
  26. {docassert-0.2.0 → docassert-0.3.0}/docassert/consistency.py +22 -2
  27. docassert-0.3.0/docassert/scaffold.py +128 -0
  28. {docassert-0.2.0 → docassert-0.3.0/docassert.egg-info}/PKG-INFO +9 -3
  29. {docassert-0.2.0 → docassert-0.3.0}/docassert.egg-info/SOURCES.txt +3 -0
  30. docassert-0.3.0/tests/test_defects.py +85 -0
  31. docassert-0.3.0/tests/test_scaffold.py +147 -0
  32. {docassert-0.2.0 → docassert-0.3.0}/LICENSE +0 -0
  33. {docassert-0.2.0 → docassert-0.3.0}/NOTICE +0 -0
  34. {docassert-0.2.0 → docassert-0.3.0}/docassert/__main__.py +0 -0
  35. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
  36. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
  37. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
  38. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
  39. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
  40. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
  41. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
  42. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
  43. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
  44. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
  45. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
  46. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
  47. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
  48. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
  49. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
  50. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
  51. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
  52. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
  53. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
  54. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
  55. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
  56. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
  57. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
  58. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/adr.schema.json +0 -0
  59. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
  60. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/brd.schema.json +0 -0
  61. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/business-case.schema.json +0 -0
  62. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/charter.schema.json +0 -0
  63. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
  64. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/frnfr.schema.json +0 -0
  65. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
  66. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
  67. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/prd.schema.json +0 -0
  68. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/project.schema.json +0 -0
  69. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
  70. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
  71. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
  72. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/risk-register.schema.json +0 -0
  73. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
  74. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/runbook.schema.json +0 -0
  75. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/status-report.schema.json +0 -0
  76. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/test-cases.schema.json +0 -0
  77. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/schema/user-story.schema.json +0 -0
  78. {docassert-0.2.0 → docassert-0.3.0}/docassert/_data/templates/project.template.md +0 -0
  79. {docassert-0.2.0 → docassert-0.3.0}/docassert/extract.py +0 -0
  80. {docassert-0.2.0 → docassert-0.3.0}/docassert/graph.py +0 -0
  81. {docassert-0.2.0 → docassert-0.3.0}/docassert/loader.py +0 -0
  82. {docassert-0.2.0 → docassert-0.3.0}/docassert/models.py +0 -0
  83. {docassert-0.2.0 → docassert-0.3.0}/docassert/profiles.py +0 -0
  84. {docassert-0.2.0 → docassert-0.3.0}/docassert/projects.py +0 -0
  85. {docassert-0.2.0 → docassert-0.3.0}/docassert/report.py +0 -0
  86. {docassert-0.2.0 → docassert-0.3.0}/docassert/rtm.py +0 -0
  87. {docassert-0.2.0 → docassert-0.3.0}/docassert/semantic.py +0 -0
  88. {docassert-0.2.0 → docassert-0.3.0}/docassert/status.py +0 -0
  89. {docassert-0.2.0 → docassert-0.3.0}/docassert/structural.py +0 -0
  90. {docassert-0.2.0 → docassert-0.3.0}/docassert.egg-info/dependency_links.txt +0 -0
  91. {docassert-0.2.0 → docassert-0.3.0}/docassert.egg-info/entry_points.txt +0 -0
  92. {docassert-0.2.0 → docassert-0.3.0}/docassert.egg-info/requires.txt +0 -0
  93. {docassert-0.2.0 → docassert-0.3.0}/docassert.egg-info/top_level.txt +0 -0
  94. {docassert-0.2.0 → docassert-0.3.0}/pyproject.toml +0 -0
  95. {docassert-0.2.0 → docassert-0.3.0}/setup.cfg +0 -0
  96. {docassert-0.2.0 → docassert-0.3.0}/tests/test_config.py +0 -0
  97. {docassert-0.2.0 → docassert-0.3.0}/tests/test_consistency.py +0 -0
  98. {docassert-0.2.0 → docassert-0.3.0}/tests/test_extract.py +0 -0
  99. {docassert-0.2.0 → docassert-0.3.0}/tests/test_graph.py +0 -0
  100. {docassert-0.2.0 → docassert-0.3.0}/tests/test_kinds_delivery.py +0 -0
  101. {docassert-0.2.0 → docassert-0.3.0}/tests/test_kinds_governance.py +0 -0
  102. {docassert-0.2.0 → docassert-0.3.0}/tests/test_kinds_operate.py +0 -0
  103. {docassert-0.2.0 → docassert-0.3.0}/tests/test_kinds_reporting.py +0 -0
  104. {docassert-0.2.0 → docassert-0.3.0}/tests/test_profiles.py +0 -0
  105. {docassert-0.2.0 → docassert-0.3.0}/tests/test_projects.py +0 -0
  106. {docassert-0.2.0 → docassert-0.3.0}/tests/test_status.py +0 -0
  107. {docassert-0.2.0 → docassert-0.3.0}/tests/test_structural.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docassert
3
- Version: 0.2.0
3
+ Version: 0.3.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
@@ -64,7 +64,8 @@ pip install "docassert[ai]"
64
64
  ## Quickstart
65
65
 
66
66
  ```bash
67
- docassert init # scaffold criteria/schema/profiles/templates into your repo
67
+ docassert new project --code AUR --name "Aurora" # anchor a project (auto-numbered id)
68
+ docassert new charter --project PRJ-001-AUR # scaffold a charter into it
68
69
  docassert validate documents/**/*.md # unit-test your documents
69
70
  docassert consistency # cross-document traceability + profile completeness
70
71
  docassert status --index # derived RAG per project
@@ -80,15 +81,20 @@ you can customize them.
80
81
 
81
82
  | Command | What it does |
82
83
  |---|---|
83
- | `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures. |
84
+ | `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). |
84
85
  | `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. |
85
86
  | `docassert rtm [--project ID]` | Requirements traceability matrix (Markdown or CSV). |
86
87
  | `docassert status [--project ID] [--index]` | Derived project status (md / json / html). |
87
88
  | `docassert pages --out DIR` | Build the portfolio site (index + a page per project). |
88
89
  | `docassert projects [--out] [--check]` | Generate / verify the project registry. |
90
+ | `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. |
89
91
  | `docassert init [DIR]` | Scaffold the default config into a repo. |
90
92
  | `docassert extract <file>` | Extract plain text from a source `.docx` / `.pdf` / `.md` / `.txt` (the first step of doc-to-pmo conversion). Needs the `convert` extra: `pip install "docassert[convert]"`. |
91
93
 
94
+ Every document-reading command accepts `--documents-dir` (default `documents/`).
95
+ AI alignment grades at most `alignment_limit` links per run (default 25; set it
96
+ in `consistency.yaml`, `0` = no cap) so API cost stays bounded on large graphs.
97
+
92
98
  ## Document kinds
93
99
 
94
100
  Twenty kinds, each a `templates/<kind>.template.md` + `schema/<kind>.schema.json`
@@ -26,7 +26,8 @@ pip install "docassert[ai]"
26
26
  ## Quickstart
27
27
 
28
28
  ```bash
29
- docassert init # scaffold criteria/schema/profiles/templates into your repo
29
+ docassert new project --code AUR --name "Aurora" # anchor a project (auto-numbered id)
30
+ docassert new charter --project PRJ-001-AUR # scaffold a charter into it
30
31
  docassert validate documents/**/*.md # unit-test your documents
31
32
  docassert consistency # cross-document traceability + profile completeness
32
33
  docassert status --index # derived RAG per project
@@ -42,15 +43,20 @@ you can customize them.
42
43
 
43
44
  | Command | What it does |
44
45
  |---|---|
45
- | `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures. |
46
+ | `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures (capped at 125). |
46
47
  | `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. |
47
48
  | `docassert rtm [--project ID]` | Requirements traceability matrix (Markdown or CSV). |
48
49
  | `docassert status [--project ID] [--index]` | Derived project status (md / json / html). |
49
50
  | `docassert pages --out DIR` | Build the portfolio site (index + a page per project). |
50
51
  | `docassert projects [--out] [--check]` | Generate / verify the project registry. |
52
+ | `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. |
51
53
  | `docassert init [DIR]` | Scaffold the default config into a repo. |
52
54
  | `docassert extract <file>` | Extract plain text from a source `.docx` / `.pdf` / `.md` / `.txt` (the first step of doc-to-pmo conversion). Needs the `convert` extra: `pip install "docassert[convert]"`. |
53
55
 
56
+ Every document-reading command accepts `--documents-dir` (default `documents/`).
57
+ AI alignment grades at most `alignment_limit` links per run (default 25; set it
58
+ in `consistency.yaml`, `0` = no cap) so API cost stays bounded on large graphs.
59
+
54
60
  ## Document kinds
55
61
 
56
62
  Twenty kinds, each a `templates/<kind>.template.md` + `schema/<kind>.schema.json`
@@ -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.2.0"
8
+ __version__ = "0.3.0"
@@ -35,6 +35,10 @@ coverage:
35
35
 
36
36
  # Advisory AI alignment: for each relation, judge whether the child genuinely
37
37
  # fulfils the parent it links to. Never blocks.
38
+ # Each graded link costs one API call; `alignment_limit` caps calls per run
39
+ # (0 = no cap).
40
+ alignment_limit: 25
41
+
38
42
  alignment:
39
43
  - relation: traces
40
44
  prompt: >
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: adr
3
- id: my-adr-log
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-adr # <CODE>-<slug>; the project code namespaces it
4
5
  title: My Architecture Decision Log
5
6
  owner: jane.doe
6
7
  status: draft
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: benefits-realization
3
- id: my-benefits
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-benefits-realization # <CODE>-<slug>; the project code namespaces it
4
5
  title: My Benefits Realization Plan
5
6
  owner: jane.doe
6
7
  status: draft
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: brd
3
- id: my-brd
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-brd # <CODE>-<slug>; the project code namespaces it
4
5
  title: My Business Requirements Document
5
6
  owner: jane.doe
6
7
  status: draft # draft | proposed | approved | baselined
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: business-case
3
- id: my-business-case
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-business-case # <CODE>-<slug>; the project code namespaces it
4
5
  title: My Business Case
5
6
  sponsor: jane.doe
6
7
  status: draft
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: charter
3
- id: my-project # lowercase, hyphenated, unique across documents/
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-charter # <CODE>-<slug>; the project code namespaces it
4
5
  title: My Project Charter
5
6
  sponsor: jane.doe # the accountable individual, not a team
6
7
  budget:
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: data-migration-plan
3
- id: my-data-migration
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-data-migration-plan # <CODE>-<slug>; the project code namespaces it
4
5
  title: My Data Migration Plan
5
6
  owner: jane.doe
6
7
  status: draft
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: frnfr
3
- id: my-frnfr
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-frnfr # <CODE>-<slug>; the project code namespaces it
4
5
  title: My Functional & Non-Functional Requirements
5
6
  owner: jane.doe
6
7
  status: draft
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: hypercare-plan
3
- id: my-hypercare-plan
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-hypercare-plan # <CODE>-<slug>; the project code namespaces it
4
5
  title: My Hypercare Plan
5
6
  owner: jane.doe
6
7
  status: draft
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: post-implementation-review
3
- id: my-pir
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-post-implementation-review # <CODE>-<slug>; the project code namespaces it
4
5
  title: My Post-Implementation Review
5
6
  owner: jane.doe
6
7
  status: draft
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: prd
3
- id: my-prd
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-prd # <CODE>-<slug>; the project code namespaces it
4
5
  title: My Product Requirements Document
5
6
  owner: jane.doe
6
7
  status: draft
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: qa-test-plan
3
- id: my-test-plan
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-qa-test-plan # <CODE>-<slug>; the project code namespaces it
4
5
  title: My QA / Test Plan
5
6
  owner: jane.doe
6
7
  status: draft
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: raci-stakeholder
3
- id: my-raci
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-raci-stakeholder # <CODE>-<slug>; the project code namespaces it
4
5
  title: My RACI / Stakeholder Register
5
6
  owner: jane.doe
6
7
  status: draft
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: release-cutover-plan
3
- id: my-cutover-plan
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-release-cutover-plan # <CODE>-<slug>; the project code namespaces it
4
5
  title: My Release / Cutover Plan
5
6
  owner: jane.doe
6
7
  status: draft
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: risk-register
3
- id: my-risk-register
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-risk-register # <CODE>-<slug>; the project code namespaces it
4
5
  title: My Risk Register
5
6
  owner: jane.doe
6
7
  status: draft
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: rollback-plan
3
- id: my-rollback-plan
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-rollback-plan # <CODE>-<slug>; the project code namespaces it
4
5
  title: My Rollback Plan
5
6
  owner: jane.doe
6
7
  status: draft
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: runbook
3
- id: my-runbook
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-runbook # <CODE>-<slug>; the project code namespaces it
4
5
  title: My Runbook
5
6
  owner: jane.doe
6
7
  status: draft
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: status-report
3
- id: my-status-2026-07-01
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-status-2026-07-01 # <CODE>-<slug>; the project code namespaces it
4
5
  title: My Project — Status Report
5
6
  owner: jane.doe
6
7
  period: 2026-07-01
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: test-cases
3
- id: my-test-cases
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-test-cases # <CODE>-<slug>; the project code namespaces it
4
5
  title: My Test Cases
5
6
  owner: jane.doe
6
7
  status: draft
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  kind: user-story
3
- id: my-user-stories
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-user-story # <CODE>-<slug>; the project code namespaces it
4
5
  title: My User Stories
5
6
  owner: jane.doe
6
7
  status: draft
@@ -3,8 +3,10 @@
3
3
  docassert validate documents/charters/aurora.md
4
4
  docassert validate documents/**/*.md --junit out.xml --markdown comment.md
5
5
 
6
- Exit code = number of BLOCKING (structural) failures. Advisory (AI) failures
7
- never affect the exit code, so CI is gated only by deterministic checks.
6
+ Exit code = number of BLOCKING (structural) failures, capped at 125 so large
7
+ counts can't wrap around the 8-bit exit-status space (256 failures must never
8
+ read as success). Advisory (AI) failures never affect the exit code, so CI is
9
+ gated only by deterministic checks.
8
10
  """
9
11
  from __future__ import annotations
10
12
 
@@ -23,15 +25,24 @@ from .models import CheckResult
23
25
  from .semantic import run_semantic
24
26
  from .structural import run_structural
25
27
 
26
- # The user's documents live here; criteria / schema / consistency.yaml / profiles
27
- # resolve via `config` (local override packaged default).
28
- DOCUMENTS_DIR = Path("documents")
28
+ # Default documents location; every document-reading command accepts
29
+ # --documents-dir to override it. Criteria / schema / consistency.yaml /
30
+ # profiles resolve via `config` (local override → packaged default).
31
+ DEFAULT_DOCUMENTS_DIR = "documents"
29
32
 
33
+ # POSIX exit statuses are 8-bit; 126+ carry shell meanings. Cap so a failure
34
+ # count can never wrap to 0.
35
+ _EXIT_CAP = 125
30
36
 
31
- def _build_id_index() -> dict[str, list[str]]:
32
- """Map document id -> [paths] across all documents/, for uniqueness checks."""
37
+
38
+ def _capped(failures: int) -> int:
39
+ return min(failures, _EXIT_CAP)
40
+
41
+
42
+ def _build_id_index(documents_dir: Path) -> dict[str, list[str]]:
43
+ """Map document id -> [paths] across the documents tree, for uniqueness checks."""
33
44
  index: dict[str, list[str]] = defaultdict(list)
34
- for path in DOCUMENTS_DIR.rglob("*.md"):
45
+ for path in documents_dir.rglob("*.md"):
35
46
  try:
36
47
  doc = load(path)
37
48
  except ValueError:
@@ -86,7 +97,7 @@ def cmd_validate(args: argparse.Namespace) -> int:
86
97
  print("docassert: no markdown documents matched.", file=sys.stderr)
87
98
  return 0
88
99
 
89
- id_index = _build_id_index()
100
+ id_index = _build_id_index(Path(args.documents_dir))
90
101
  results_by_doc: dict[str, list[CheckResult]] = {}
91
102
  for path in files:
92
103
  try:
@@ -106,12 +117,12 @@ def cmd_validate(args: argparse.Namespace) -> int:
106
117
  if args.markdown:
107
118
  Path(args.markdown).write_text(report.markdown(results_by_doc))
108
119
 
109
- return sum(1 for rs in results_by_doc.values()
110
- for r in rs if r.is_blocking_failure)
120
+ return _capped(sum(1 for rs in results_by_doc.values()
121
+ for r in rs if r.is_blocking_failure))
111
122
 
112
123
 
113
124
  def cmd_consistency(args: argparse.Namespace) -> int:
114
- results = run_consistency(DOCUMENTS_DIR, with_semantic=not args.no_semantic)
125
+ results = run_consistency(args.documents_dir, with_semantic=not args.no_semantic)
115
126
  results_by_doc = {"consistency (cross-document)": results}
116
127
 
117
128
  print(report.console(results_by_doc))
@@ -123,7 +134,7 @@ def cmd_consistency(args: argparse.Namespace) -> int:
123
134
  Path(args.markdown).write_text(
124
135
  report.markdown(results_by_doc, title="docassert consistency"))
125
136
 
126
- return sum(1 for r in results if r.is_blocking_failure)
137
+ return _capped(sum(1 for r in results if r.is_blocking_failure))
127
138
 
128
139
 
129
140
  def _project_code(value: str | None) -> str | None:
@@ -132,7 +143,7 @@ def _project_code(value: str | None) -> str | None:
132
143
 
133
144
 
134
145
  def cmd_rtm(args: argparse.Namespace) -> int:
135
- graph = build_graph(DOCUMENTS_DIR)
146
+ graph = build_graph(args.documents_dir)
136
147
  code = _project_code(args.project)
137
148
  text = rtm.render_csv(graph, code) if args.csv else rtm.render_markdown(graph, code)
138
149
  if args.out:
@@ -145,7 +156,7 @@ def cmd_rtm(args: argparse.Namespace) -> int:
145
156
 
146
157
  def cmd_projects(args: argparse.Namespace) -> int:
147
158
  from . import projects as proj
148
- plist = proj.load_projects(DOCUMENTS_DIR)
159
+ plist = proj.load_projects(args.documents_dir)
149
160
  issues = proj.registry_issues(plist)
150
161
  for issue in issues:
151
162
  print(f"docassert: {issue}", file=sys.stderr)
@@ -172,7 +183,7 @@ def cmd_projects(args: argparse.Namespace) -> int:
172
183
  def cmd_status(args: argparse.Namespace) -> int:
173
184
  from . import status as status_mod
174
185
  if args.index:
175
- index = status_mod.build_index(DOCUMENTS_DIR)
186
+ index = status_mod.build_index(args.documents_dir)
176
187
  if args.format == "json":
177
188
  text = status_mod.render_json(index)
178
189
  elif args.format == "html":
@@ -181,7 +192,7 @@ def cmd_status(args: argparse.Namespace) -> int:
181
192
  text = status_mod.render_index_markdown(index)
182
193
  tag = index["overall"]["rag"]
183
194
  else:
184
- model = status_mod.build_status(DOCUMENTS_DIR, project=args.project)
195
+ model = status_mod.build_status(args.documents_dir, project=args.project)
185
196
  if args.project and not model["documents"]:
186
197
  print(f"docassert: no documents for project {args.project!r}", file=sys.stderr)
187
198
  return 2
@@ -206,16 +217,17 @@ def cmd_pages(args: argparse.Namespace) -> int:
206
217
  from . import status as status_mod
207
218
  out = Path(args.out)
208
219
  out.mkdir(parents=True, exist_ok=True)
220
+ docs_dir = args.documents_dir
209
221
 
210
- index = status_mod.build_index(DOCUMENTS_DIR)
222
+ index = status_mod.build_index(docs_dir)
211
223
  (out / "index.html").write_text(status_mod.render_index_html(index))
212
224
 
213
- plist = projects_mod.load_projects(DOCUMENTS_DIR)
225
+ plist = projects_mod.load_projects(docs_dir)
214
226
  for p in plist:
215
- model = status_mod.build_status(DOCUMENTS_DIR, project=p["id"])
227
+ model = status_mod.build_status(docs_dir, project=p["id"])
216
228
  (out / f"{p['id']}.html").write_text(status_mod.render_html(model))
217
229
 
218
- (out / "RTM.md").write_text(rtm.render_markdown(build_graph(DOCUMENTS_DIR)))
230
+ (out / "RTM.md").write_text(rtm.render_markdown(build_graph(docs_dir)))
219
231
  print(f"docassert: wrote {out}/ — index + {len(plist)} project page(s) + RTM.md "
220
232
  f"(portfolio: {index['overall']['rag']})")
221
233
  return 0
@@ -249,6 +261,22 @@ def cmd_extract(args: argparse.Namespace) -> int:
249
261
  return 0
250
262
 
251
263
 
264
+ def cmd_new(args: argparse.Namespace) -> int:
265
+ """Scaffold a document of a kind from its template, with identity filled in."""
266
+ from . import scaffold
267
+ try:
268
+ dest, notes = scaffold.new_document(
269
+ args.kind, documents_dir=args.documents_dir, project=args.project,
270
+ code=args.code, name=args.name, out=args.out)
271
+ except (ValueError, FileExistsError) as exc:
272
+ print(f"docassert: {exc}", file=sys.stderr)
273
+ return 2
274
+ print(f"docassert: created {dest}")
275
+ for note in notes:
276
+ print(f"docassert: {note}")
277
+ return 0
278
+
279
+
252
280
  def main(argv: list[str] | None = None) -> int:
253
281
  from . import __version__
254
282
  parser = argparse.ArgumentParser(prog="docassert",
@@ -256,10 +284,15 @@ def main(argv: list[str] | None = None) -> int:
256
284
  parser.add_argument("--version", action="version", version=f"docassert {__version__}")
257
285
  sub = parser.add_subparsers(dest="command", required=True)
258
286
 
287
+ def docs_dir_opt(sp: argparse.ArgumentParser) -> None:
288
+ sp.add_argument("--documents-dir", default=DEFAULT_DOCUMENTS_DIR,
289
+ help=f"Documents tree to read (default: {DEFAULT_DOCUMENTS_DIR}/).")
290
+
259
291
  v = sub.add_parser("validate", help="Validate documents against their criteria.")
260
292
  v.add_argument("paths", nargs="+", help="Markdown files or globs.")
261
293
  v.add_argument("--junit", help="Write a JUnit XML report to this path.")
262
294
  v.add_argument("--markdown", help="Write a PR-comment markdown report to this path.")
295
+ docs_dir_opt(v)
263
296
  v.set_defaults(func=cmd_validate)
264
297
 
265
298
  c = sub.add_parser("consistency", help="Check cross-document traceability.")
@@ -267,12 +300,14 @@ def main(argv: list[str] | None = None) -> int:
267
300
  c.add_argument("--markdown", help="Write a PR-comment markdown report to this path.")
268
301
  c.add_argument("--no-semantic", action="store_true",
269
302
  help="Skip AI alignment (structural consistency only).")
303
+ docs_dir_opt(c)
270
304
  c.set_defaults(func=cmd_consistency)
271
305
 
272
306
  r = sub.add_parser("rtm", help="Generate the requirements traceability matrix.")
273
307
  r.add_argument("--out", help="Write to this path instead of stdout.")
274
308
  r.add_argument("--csv", action="store_true", help="Emit CSV instead of Markdown.")
275
309
  r.add_argument("--project", help="Scope to one project (PRJ-NNN-CODE id or CODE).")
310
+ docs_dir_opt(r)
276
311
  r.set_defaults(func=cmd_rtm)
277
312
 
278
313
  s = sub.add_parser("status", help="Derive a project status page from the documents.")
@@ -284,16 +319,19 @@ def main(argv: list[str] | None = None) -> int:
284
319
  s.add_argument("--index", action="store_true",
285
320
  help="Render the multi-project portfolio index instead of one status.")
286
321
  s.add_argument("--out", help="Write to this path instead of stdout.")
322
+ docs_dir_opt(s)
287
323
  s.set_defaults(func=cmd_status)
288
324
 
289
325
  pg = sub.add_parser("pages", help="Build the full Pages site (portfolio index + a page per project).")
290
326
  pg.add_argument("--out", default="_site", help="Output directory (default: _site).")
327
+ docs_dir_opt(pg)
291
328
  pg.set_defaults(func=cmd_pages)
292
329
 
293
330
  p = sub.add_parser("projects", help="Generate the project registry from the project.md anchors.")
294
331
  p.add_argument("--out", help="Write to this path instead of stdout (e.g. projects.yaml).")
295
332
  p.add_argument("--check", action="store_true",
296
333
  help="Exit non-zero if the registry file is stale (CI freshness gate).")
334
+ docs_dir_opt(p)
297
335
  p.set_defaults(func=cmd_projects)
298
336
 
299
337
  ini = sub.add_parser("init", help="Scaffold the default criteria/schema/profiles/templates into a repo.")
@@ -305,6 +343,15 @@ def main(argv: list[str] | None = None) -> int:
305
343
  ex.add_argument("--out", help="Write to this path instead of stdout.")
306
344
  ex.set_defaults(func=cmd_extract)
307
345
 
346
+ n = sub.add_parser("new", help="Scaffold a document of a kind from its template, identity filled in.")
347
+ n.add_argument("kind", help="Document kind (e.g. charter, brd, project).")
348
+ n.add_argument("--project", help="Owning project id, PRJ-NNN-CODE (for `new project`: the id to create).")
349
+ n.add_argument("--code", help="For `new project`: 2–6 letter code; the sequence number is auto-picked.")
350
+ n.add_argument("--name", help="For `new project`: the project name.")
351
+ n.add_argument("--out", help="Write to this path instead of the default location.")
352
+ docs_dir_opt(n)
353
+ n.set_defaults(func=cmd_new)
354
+
308
355
  args = parser.parse_args(argv)
309
356
  return args.func(args)
310
357
 
@@ -66,6 +66,24 @@ def read_consistency_config() -> dict:
66
66
  return _read_yaml(path) if path is not None else {}
67
67
 
68
68
 
69
+ # ── templates ───────────────────────────────────────────────────────────────
70
+ def template_path(kind: str) -> Path | None:
71
+ return _resolve(Path("templates") / f"{kind}.template.md",
72
+ f"templates/{kind}.template.md")
73
+
74
+
75
+ def available_kinds() -> list[str]:
76
+ """Every kind with criteria, local and packaged."""
77
+ names: set[str] = set()
78
+ local = Path("criteria")
79
+ if local.is_dir():
80
+ names |= {p.name.removesuffix(".criteria.yaml") for p in local.glob("*.criteria.yaml")}
81
+ packaged = DATA_DIR / "criteria"
82
+ if packaged.is_dir():
83
+ names |= {p.name.removesuffix(".criteria.yaml") for p in packaged.glob("*.criteria.yaml")}
84
+ return sorted(names)
85
+
86
+
69
87
  # ── profiles ────────────────────────────────────────────────────────────────
70
88
  def profile_path(name: str) -> Path | None:
71
89
  return _resolve(Path("profiles") / f"{name}.yaml", f"profiles/{name}.yaml")
@@ -130,6 +130,12 @@ def check_profile_completeness(documents_dir: str | Path = "documents") -> Check
130
130
 
131
131
 
132
132
  # ── semantic (advisory) ────────────────────────────────────────────────────
133
+ # Each alignment edge costs one API call, so a large graph could otherwise run
134
+ # away on cost. Cap per run; tune with `alignment_limit` in consistency.yaml
135
+ # (0 disables the cap).
136
+ DEFAULT_ALIGNMENT_LIMIT = 25
137
+
138
+
133
139
  def run_alignment_checks(graph, config) -> list[CheckResult]:
134
140
  edges = [] # (prompt, parent, child, relation)
135
141
  for rule in config.get("alignment", []):
@@ -142,12 +148,26 @@ def run_alignment_checks(graph, config) -> list[CheckResult]:
142
148
 
143
149
  if not edges:
144
150
  return []
151
+
152
+ limit = int(config.get("alignment_limit", DEFAULT_ALIGNMENT_LIMIT) or 0)
153
+ note: CheckResult | None = None
154
+ if limit and len(edges) > limit:
155
+ note = CheckResult(
156
+ "alignment-limit", True, False,
157
+ f"graded {limit} of {len(edges)} link(s) — raise `alignment_limit` "
158
+ f"in consistency.yaml to grade more per run",
159
+ kind="semantic", score=None)
160
+ edges = edges[:limit]
161
+
145
162
  if not os.environ.get("ANTHROPIC_API_KEY"):
146
163
  return [CheckResult("alignment", True, False,
147
164
  f"skipped — no ANTHROPIC_API_KEY ({len(edges)} link(s) to grade)",
148
165
  kind="semantic", score=None)]
149
- return [run_alignment(f"align:{c.id}-{rel}-{p.id}", prompt, p.text, c.text)
150
- for prompt, p, c, rel in edges]
166
+ results = [run_alignment(f"align:{c.id}-{rel}-{p.id}", prompt, p.text, c.text)
167
+ for prompt, p, c, rel in edges]
168
+ if note is not None:
169
+ results.append(note)
170
+ return results
151
171
 
152
172
 
153
173
  def run_consistency(documents_dir: str | Path = "documents",