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