docassert 0.2.1__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.1/docassert.egg-info → docassert-0.3.0}/PKG-INFO +4 -2
  2. {docassert-0.2.1 → docassert-0.3.0}/README.md +3 -1
  3. {docassert-0.2.1 → docassert-0.3.0}/docassert/__init__.py +1 -1
  4. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/adr.template.md +2 -1
  5. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/benefits-realization.template.md +2 -1
  6. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/brd.template.md +2 -1
  7. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/business-case.template.md +2 -1
  8. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/charter.template.md +2 -1
  9. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/data-migration-plan.template.md +2 -1
  10. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/frnfr.template.md +2 -1
  11. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/hypercare-plan.template.md +2 -1
  12. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/post-implementation-review.template.md +2 -1
  13. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/prd.template.md +2 -1
  14. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/qa-test-plan.template.md +2 -1
  15. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/raci-stakeholder.template.md +2 -1
  16. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/release-cutover-plan.template.md +2 -1
  17. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/risk-register.template.md +2 -1
  18. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/rollback-plan.template.md +2 -1
  19. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/runbook.template.md +2 -1
  20. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/status-report.template.md +2 -1
  21. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/test-cases.template.md +2 -1
  22. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/user-story.template.md +2 -1
  23. {docassert-0.2.1 → docassert-0.3.0}/docassert/cli.py +25 -0
  24. {docassert-0.2.1 → docassert-0.3.0}/docassert/config.py +18 -0
  25. docassert-0.3.0/docassert/scaffold.py +128 -0
  26. {docassert-0.2.1 → docassert-0.3.0/docassert.egg-info}/PKG-INFO +4 -2
  27. {docassert-0.2.1 → docassert-0.3.0}/docassert.egg-info/SOURCES.txt +2 -0
  28. docassert-0.3.0/tests/test_scaffold.py +147 -0
  29. {docassert-0.2.1 → docassert-0.3.0}/LICENSE +0 -0
  30. {docassert-0.2.1 → docassert-0.3.0}/NOTICE +0 -0
  31. {docassert-0.2.1 → docassert-0.3.0}/docassert/__main__.py +0 -0
  32. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/consistency.yaml +0 -0
  33. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
  34. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
  35. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
  36. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
  37. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
  38. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
  39. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
  40. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
  41. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
  42. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
  43. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
  44. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
  45. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
  46. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
  47. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
  48. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
  49. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
  50. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
  51. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
  52. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
  53. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
  54. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
  55. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
  56. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/adr.schema.json +0 -0
  57. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
  58. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/brd.schema.json +0 -0
  59. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/business-case.schema.json +0 -0
  60. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/charter.schema.json +0 -0
  61. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
  62. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/frnfr.schema.json +0 -0
  63. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
  64. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
  65. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/prd.schema.json +0 -0
  66. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/project.schema.json +0 -0
  67. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
  68. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
  69. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
  70. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/risk-register.schema.json +0 -0
  71. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
  72. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/runbook.schema.json +0 -0
  73. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/status-report.schema.json +0 -0
  74. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/test-cases.schema.json +0 -0
  75. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/user-story.schema.json +0 -0
  76. {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/project.template.md +0 -0
  77. {docassert-0.2.1 → docassert-0.3.0}/docassert/consistency.py +0 -0
  78. {docassert-0.2.1 → docassert-0.3.0}/docassert/extract.py +0 -0
  79. {docassert-0.2.1 → docassert-0.3.0}/docassert/graph.py +0 -0
  80. {docassert-0.2.1 → docassert-0.3.0}/docassert/loader.py +0 -0
  81. {docassert-0.2.1 → docassert-0.3.0}/docassert/models.py +0 -0
  82. {docassert-0.2.1 → docassert-0.3.0}/docassert/profiles.py +0 -0
  83. {docassert-0.2.1 → docassert-0.3.0}/docassert/projects.py +0 -0
  84. {docassert-0.2.1 → docassert-0.3.0}/docassert/report.py +0 -0
  85. {docassert-0.2.1 → docassert-0.3.0}/docassert/rtm.py +0 -0
  86. {docassert-0.2.1 → docassert-0.3.0}/docassert/semantic.py +0 -0
  87. {docassert-0.2.1 → docassert-0.3.0}/docassert/status.py +0 -0
  88. {docassert-0.2.1 → docassert-0.3.0}/docassert/structural.py +0 -0
  89. {docassert-0.2.1 → docassert-0.3.0}/docassert.egg-info/dependency_links.txt +0 -0
  90. {docassert-0.2.1 → docassert-0.3.0}/docassert.egg-info/entry_points.txt +0 -0
  91. {docassert-0.2.1 → docassert-0.3.0}/docassert.egg-info/requires.txt +0 -0
  92. {docassert-0.2.1 → docassert-0.3.0}/docassert.egg-info/top_level.txt +0 -0
  93. {docassert-0.2.1 → docassert-0.3.0}/pyproject.toml +0 -0
  94. {docassert-0.2.1 → docassert-0.3.0}/setup.cfg +0 -0
  95. {docassert-0.2.1 → docassert-0.3.0}/tests/test_config.py +0 -0
  96. {docassert-0.2.1 → docassert-0.3.0}/tests/test_consistency.py +0 -0
  97. {docassert-0.2.1 → docassert-0.3.0}/tests/test_defects.py +0 -0
  98. {docassert-0.2.1 → docassert-0.3.0}/tests/test_extract.py +0 -0
  99. {docassert-0.2.1 → docassert-0.3.0}/tests/test_graph.py +0 -0
  100. {docassert-0.2.1 → docassert-0.3.0}/tests/test_kinds_delivery.py +0 -0
  101. {docassert-0.2.1 → docassert-0.3.0}/tests/test_kinds_governance.py +0 -0
  102. {docassert-0.2.1 → docassert-0.3.0}/tests/test_kinds_operate.py +0 -0
  103. {docassert-0.2.1 → docassert-0.3.0}/tests/test_kinds_reporting.py +0 -0
  104. {docassert-0.2.1 → docassert-0.3.0}/tests/test_profiles.py +0 -0
  105. {docassert-0.2.1 → docassert-0.3.0}/tests/test_projects.py +0 -0
  106. {docassert-0.2.1 → docassert-0.3.0}/tests/test_status.py +0 -0
  107. {docassert-0.2.1 → 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.1
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
@@ -86,6 +87,7 @@ you can customize them.
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
 
@@ -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
@@ -48,6 +49,7 @@ you can customize them.
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
 
@@ -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.1"
8
+ __version__ = "0.3.0"
@@ -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
@@ -261,6 +261,22 @@ def cmd_extract(args: argparse.Namespace) -> int:
261
261
  return 0
262
262
 
263
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
+
264
280
  def main(argv: list[str] | None = None) -> int:
265
281
  from . import __version__
266
282
  parser = argparse.ArgumentParser(prog="docassert",
@@ -327,6 +343,15 @@ def main(argv: list[str] | None = None) -> int:
327
343
  ex.add_argument("--out", help="Write to this path instead of stdout.")
328
344
  ex.set_defaults(func=cmd_extract)
329
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
+
330
355
  args = parser.parse_args(argv)
331
356
  return args.func(args)
332
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")
@@ -0,0 +1,128 @@
1
+ """Scaffold new documents from the templates (`docassert new`).
2
+
3
+ Fills a kind's template with the right identity — `project:`, a namespaced
4
+ `id:` (AUR-brd), and for project anchors an auto-numbered PRJ-NNN-CODE — and
5
+ suggests the next free item ids so authoring starts from a valid document
6
+ instead of a hand-edited copy.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import datetime as dt
11
+ import re
12
+ from pathlib import Path
13
+
14
+ from . import config
15
+ from .graph import build_graph
16
+ from .projects import load_projects
17
+
18
+ PROJECT_ID_RE = re.compile(r"^PRJ-(?P<seq>\d{3,})-(?P<code>[A-Z]{2,6})$")
19
+ _ITEM_NUM_RE = re.compile(r"-(\d+)$")
20
+
21
+
22
+ def _set_field(text: str, field: str, value: str) -> str:
23
+ """Set a frontmatter field, replacing its line (comments and all) if present,
24
+ else inserting it right after `kind:`."""
25
+ lines = text.split("\n")
26
+ end = lines.index("---", 1)
27
+ for i in range(1, end):
28
+ if re.match(rf"{field}\s*:", lines[i]):
29
+ lines[i] = f"{field}: {value}"
30
+ return "\n".join(lines)
31
+ for i in range(1, end):
32
+ if re.match(r"kind\s*:", lines[i]):
33
+ lines.insert(i + 1, f"{field}: {value}")
34
+ return "\n".join(lines)
35
+ lines.insert(1, f"{field}: {value}")
36
+ return "\n".join(lines)
37
+
38
+
39
+ def _read_template(kind: str) -> str:
40
+ path = config.template_path(kind)
41
+ if path is None:
42
+ kinds = ", ".join(config.available_kinds())
43
+ raise ValueError(f"unknown kind '{kind}' (available: {kinds})")
44
+ return path.read_text(encoding="utf-8")
45
+
46
+
47
+ def _next_project_id(code: str, anchors: list[dict]) -> str:
48
+ seqs = [int(m.group("seq")) for a in anchors
49
+ if (m := PROJECT_ID_RE.match(a["id"]))]
50
+ return f"PRJ-{max(seqs, default=0) + 1:03d}-{code}"
51
+
52
+
53
+ def _item_hints(kind: str, code: str, documents_dir: str | Path) -> list[str]:
54
+ """Next free item id per item type the kind declares (e.g. AUR-BR-003)."""
55
+ specs = config.read_criteria(kind).get("item_sections", []) or []
56
+ if not specs:
57
+ return []
58
+ graph = build_graph(documents_dir)
59
+ hints = []
60
+ for spec in specs:
61
+ type_ = spec["prefix"]
62
+ nums = [int(m.group(1)) for item in graph.by_type.get(type_, [])
63
+ if item.project == code and (m := _ITEM_NUM_RE.search(item.id))]
64
+ hints.append(f"{code}-{type_}-{max(nums, default=0) + 1:03d}")
65
+ return hints
66
+
67
+
68
+ def new_document(kind: str, documents_dir: str | Path = "documents",
69
+ project: str | None = None, code: str | None = None,
70
+ name: str | None = None, out: str | Path | None = None,
71
+ ) -> tuple[Path, list[str]]:
72
+ """Create a document of `kind` from its template. Returns (path, notes)."""
73
+ docs = Path(documents_dir)
74
+ text = _read_template(kind)
75
+ anchors = load_projects(docs) if docs.is_dir() else []
76
+ notes: list[str] = []
77
+
78
+ if kind == "project":
79
+ if project:
80
+ m = PROJECT_ID_RE.match(project)
81
+ if not m:
82
+ raise ValueError(f"project id {project!r} must match PRJ-NNN-CODE (e.g. PRJ-001-AUR)")
83
+ pid, pcode = project, m.group("code")
84
+ elif code:
85
+ if not re.fullmatch(r"[A-Z]{2,6}", code):
86
+ raise ValueError(f"code {code!r} must be 2–6 uppercase letters")
87
+ pid, pcode = _next_project_id(code, anchors), code
88
+ else:
89
+ raise ValueError("new project needs --project PRJ-NNN-CODE or --code CODE")
90
+ clash = [a["id"] for a in anchors if a["id"] == pid or a["code"] == pcode]
91
+ if clash:
92
+ raise ValueError(f"project id/code already taken by {', '.join(clash)}")
93
+ text = _set_field(text, "id", pid)
94
+ text = _set_field(text, "code", pcode)
95
+ if name:
96
+ text = _set_field(text, "name", name)
97
+ dest = Path(out) if out else docs / pid / "project.md"
98
+ else:
99
+ if not project:
100
+ raise ValueError(f"new {kind} needs --project PRJ-NNN-CODE (the owning project)")
101
+ anchor = next((a for a in anchors if a["id"] == project), None)
102
+ if anchor:
103
+ pcode = anchor["code"]
104
+ else:
105
+ m = PROJECT_ID_RE.match(project)
106
+ if not m:
107
+ raise ValueError(f"project id {project!r} must match PRJ-NNN-CODE")
108
+ pcode = m.group("code")
109
+ notes.append(f"no project.md anchor for {project} yet — "
110
+ f"create it with: docassert new project --project {project}")
111
+ today = dt.date.today().isoformat()
112
+ if kind == "status-report":
113
+ doc_id = f"{pcode}-status-{today}"
114
+ text = _set_field(text, "period", today)
115
+ default_dest = docs / project / "status-reports" / f"{today}.md"
116
+ else:
117
+ doc_id = f"{pcode}-{kind}"
118
+ default_dest = docs / project / f"{kind}.md"
119
+ text = _set_field(text, "id", doc_id)
120
+ text = _set_field(text, "project", project)
121
+ dest = Path(out) if out else default_dest
122
+ notes.extend(f"next item id: {h}" for h in _item_hints(kind, pcode, docs))
123
+
124
+ if dest.exists():
125
+ raise FileExistsError(f"{dest} already exists — refusing to overwrite")
126
+ dest.parent.mkdir(parents=True, exist_ok=True)
127
+ dest.write_text(text, encoding="utf-8")
128
+ return dest, notes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docassert
3
- Version: 0.2.1
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
@@ -86,6 +87,7 @@ you can customize them.
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
 
@@ -15,6 +15,7 @@ docassert/profiles.py
15
15
  docassert/projects.py
16
16
  docassert/report.py
17
17
  docassert/rtm.py
18
+ docassert/scaffold.py
18
19
  docassert/semantic.py
19
20
  docassert/status.py
20
21
  docassert/structural.py
@@ -99,5 +100,6 @@ tests/test_kinds_operate.py
99
100
  tests/test_kinds_reporting.py
100
101
  tests/test_profiles.py
101
102
  tests/test_projects.py
103
+ tests/test_scaffold.py
102
104
  tests/test_status.py
103
105
  tests/test_structural.py
@@ -0,0 +1,147 @@
1
+ """Tests for `docassert new` (docassert/scaffold.py)."""
2
+ import datetime as dt
3
+
4
+ import pytest
5
+
6
+ from docassert import scaffold
7
+ from docassert.cli import main
8
+ from docassert.loader import load
9
+
10
+ BRD_MD = """---
11
+ kind: brd
12
+ id: AAA-brd
13
+ project: PRJ-001-AAA
14
+ title: T
15
+ owner: o.owner
16
+ status: draft
17
+ ---
18
+
19
+ ## Purpose
20
+ p.
21
+
22
+ ## Business Requirements
23
+ - **AAA-BR-001**: The business shall do a thing by 2 days.
24
+ - **AAA-BR-002**: The business shall do another thing by 3 days.
25
+
26
+ ## Out of Scope
27
+ n/a
28
+ """
29
+
30
+
31
+ def _anchor(docs, pid="PRJ-001-AAA", code="AAA"):
32
+ d = docs / pid
33
+ d.mkdir(parents=True)
34
+ (d / "project.md").write_text(f"""---
35
+ kind: project
36
+ id: {pid}
37
+ code: {code}
38
+ name: Test
39
+ sponsor: s.person
40
+ status: proposed
41
+ ---
42
+
43
+ ## Overview
44
+ o.
45
+
46
+ ## Scope
47
+ s.
48
+ """, encoding="utf-8")
49
+
50
+
51
+ # ── new <kind> into an anchored project ──────────────────────────────────────
52
+ def test_new_charter_fills_identity(tmp_path):
53
+ docs = tmp_path / "documents"
54
+ _anchor(docs)
55
+ dest, _ = scaffold.new_document("charter", docs, project="PRJ-001-AAA")
56
+ assert dest == docs / "PRJ-001-AAA" / "charter.md"
57
+ doc = load(dest)
58
+ assert doc.frontmatter["kind"] == "charter"
59
+ assert doc.frontmatter["project"] == "PRJ-001-AAA"
60
+ assert doc.frontmatter["id"] == "AAA-charter"
61
+
62
+
63
+ def test_new_status_report_dated(tmp_path):
64
+ docs = tmp_path / "documents"
65
+ _anchor(docs)
66
+ today = dt.date.today().isoformat()
67
+ dest, _ = scaffold.new_document("status-report", docs, project="PRJ-001-AAA")
68
+ assert dest == docs / "PRJ-001-AAA" / "status-reports" / f"{today}.md"
69
+ fm = load(dest).frontmatter
70
+ assert fm["id"] == f"AAA-status-{today}" and str(fm["period"]) == today
71
+
72
+
73
+ def test_new_without_anchor_notes_it(tmp_path):
74
+ docs = tmp_path / "documents"
75
+ docs.mkdir()
76
+ dest, notes = scaffold.new_document("charter", docs, project="PRJ-002-BBB")
77
+ assert load(dest).frontmatter["id"] == "BBB-charter"
78
+ assert any("no project.md anchor" in n for n in notes)
79
+
80
+
81
+ def test_new_suggests_next_item_ids(tmp_path):
82
+ docs = tmp_path / "documents"
83
+ _anchor(docs)
84
+ (docs / "PRJ-001-AAA" / "brd.md").write_text(BRD_MD, encoding="utf-8")
85
+ _, notes = scaffold.new_document("prd", docs, project="PRJ-001-AAA")
86
+ joined = " ".join(notes)
87
+ assert "AAA-PR-001" in joined and "AAA-AC-001" in joined
88
+ # with the brd (and its items) gone, the BR counter resets
89
+ (docs / "PRJ-001-AAA" / "brd.md").unlink()
90
+ _, notes2 = scaffold.new_document("brd", docs, project="PRJ-001-AAA",
91
+ out=tmp_path / "brd2.md")
92
+ assert any("AAA-BR-001" in n for n in notes2)
93
+
94
+
95
+ def test_new_brd_counts_existing_items(tmp_path):
96
+ docs = tmp_path / "documents"
97
+ _anchor(docs)
98
+ (docs / "PRJ-001-AAA" / "brd.md").write_text(BRD_MD, encoding="utf-8")
99
+ _, notes = scaffold.new_document("brd", docs, project="PRJ-001-AAA",
100
+ out=tmp_path / "brd2.md")
101
+ assert any("AAA-BR-003" in n for n in notes)
102
+
103
+
104
+ # ── new project ──────────────────────────────────────────────────────────────
105
+ def test_new_project_auto_numbers(tmp_path):
106
+ docs = tmp_path / "documents"
107
+ _anchor(docs, "PRJ-001-AAA", "AAA")
108
+ dest, _ = scaffold.new_document("project", docs, code="BBB", name="Bravo")
109
+ assert dest == docs / "PRJ-002-BBB" / "project.md"
110
+ fm = load(dest).frontmatter
111
+ assert fm["id"] == "PRJ-002-BBB" and fm["code"] == "BBB" and fm["name"] == "Bravo"
112
+
113
+
114
+ def test_new_project_rejects_duplicate_code(tmp_path):
115
+ docs = tmp_path / "documents"
116
+ _anchor(docs, "PRJ-001-AAA", "AAA")
117
+ with pytest.raises(ValueError, match="already taken"):
118
+ scaffold.new_document("project", docs, code="AAA")
119
+
120
+
121
+ # ── guardrails ───────────────────────────────────────────────────────────────
122
+ def test_unknown_kind_lists_available(tmp_path):
123
+ with pytest.raises(ValueError, match="charter"):
124
+ scaffold.new_document("nope", tmp_path)
125
+
126
+
127
+ def test_refuses_overwrite(tmp_path):
128
+ docs = tmp_path / "documents"
129
+ _anchor(docs)
130
+ scaffold.new_document("charter", docs, project="PRJ-001-AAA")
131
+ with pytest.raises(FileExistsError):
132
+ scaffold.new_document("charter", docs, project="PRJ-001-AAA")
133
+
134
+
135
+ def test_cli_new_missing_project_exits_2(tmp_path, monkeypatch, capsys):
136
+ monkeypatch.chdir(tmp_path)
137
+ assert main(["new", "charter"]) == 2
138
+ assert "needs --project" in capsys.readouterr().err
139
+
140
+
141
+ def test_cli_new_creates_and_hints(tmp_path, monkeypatch, capsys):
142
+ docs = tmp_path / "documents"
143
+ _anchor(docs)
144
+ monkeypatch.chdir(tmp_path)
145
+ assert main(["new", "brd", "--project", "PRJ-001-AAA"]) == 0
146
+ out = capsys.readouterr().out
147
+ assert "created" in out and "AAA-BR-001" in out
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes