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.
- {docassert-0.2.1/docassert.egg-info → docassert-0.4.0}/PKG-INFO +9 -3
- {docassert-0.2.1 → docassert-0.4.0}/README.md +8 -2
- {docassert-0.2.1 → docassert-0.4.0}/docassert/__init__.py +1 -1
- docassert-0.4.0/docassert/_data/skills/doc-to-pmo/SKILL.md +100 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/adr.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/benefits-realization.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/brd.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/business-case.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/charter.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/data-migration-plan.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/frnfr.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/hypercare-plan.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/post-implementation-review.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/prd.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/qa-test-plan.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/raci-stakeholder.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/release-cutover-plan.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/risk-register.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/rollback-plan.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/runbook.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/status-report.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/test-cases.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/user-story.template.md +2 -1
- {docassert-0.2.1 → docassert-0.4.0}/docassert/cli.py +25 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/config.py +34 -6
- docassert-0.4.0/docassert/scaffold.py +128 -0
- {docassert-0.2.1 → docassert-0.4.0/docassert.egg-info}/PKG-INFO +9 -3
- {docassert-0.2.1 → docassert-0.4.0}/docassert.egg-info/SOURCES.txt +3 -0
- {docassert-0.2.1 → docassert-0.4.0}/tests/test_config.py +15 -1
- docassert-0.4.0/tests/test_scaffold.py +147 -0
- {docassert-0.2.1 → docassert-0.4.0}/LICENSE +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/NOTICE +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/__main__.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/consistency.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/adr.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/brd.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/business-case.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/charter.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/frnfr.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/prd.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/project.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/risk-register.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/runbook.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/status-report.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/test-cases.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/user-story.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/_data/templates/project.template.md +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/consistency.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/extract.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/graph.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/loader.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/models.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/profiles.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/projects.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/report.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/rtm.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/semantic.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/status.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert/structural.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert.egg-info/dependency_links.txt +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert.egg-info/entry_points.txt +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert.egg-info/requires.txt +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/docassert.egg-info/top_level.txt +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/pyproject.toml +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/setup.cfg +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/tests/test_consistency.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/tests/test_defects.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/tests/test_extract.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/tests/test_graph.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/tests/test_kinds_delivery.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/tests/test_kinds_governance.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/tests/test_kinds_operate.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/tests/test_kinds_reporting.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/tests/test_profiles.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/tests/test_projects.py +0 -0
- {docassert-0.2.1 → docassert-0.4.0}/tests/test_status.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
|
|
@@ -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: charter
|
|
3
|
-
|
|
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:
|
|
@@ -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
|
-
|
|
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
|
|
118
|
+
Returns the destination names that were created."""
|
|
92
119
|
dest = Path(dest)
|
|
93
120
|
created: list[str] = []
|
|
94
|
-
for
|
|
95
|
-
target = dest /
|
|
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 /
|
|
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(
|
|
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.
|
|
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
|
|
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",
|
|
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
|
{docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/benefits-realization.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.2.1 → docassert-0.4.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.2.1 → docassert-0.4.0}/docassert/_data/schema/post-implementation-review.schema.json
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|