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.
- {docassert-0.2.1/docassert.egg-info → docassert-0.3.0}/PKG-INFO +4 -2
- {docassert-0.2.1 → docassert-0.3.0}/README.md +3 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/__init__.py +1 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/adr.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/benefits-realization.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/brd.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/business-case.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/charter.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/data-migration-plan.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/frnfr.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/hypercare-plan.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/post-implementation-review.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/prd.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/qa-test-plan.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/raci-stakeholder.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/release-cutover-plan.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/risk-register.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/rollback-plan.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/runbook.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/status-report.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/test-cases.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/user-story.template.md +2 -1
- {docassert-0.2.1 → docassert-0.3.0}/docassert/cli.py +25 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/config.py +18 -0
- docassert-0.3.0/docassert/scaffold.py +128 -0
- {docassert-0.2.1 → docassert-0.3.0/docassert.egg-info}/PKG-INFO +4 -2
- {docassert-0.2.1 → docassert-0.3.0}/docassert.egg-info/SOURCES.txt +2 -0
- docassert-0.3.0/tests/test_scaffold.py +147 -0
- {docassert-0.2.1 → docassert-0.3.0}/LICENSE +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/NOTICE +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/__main__.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/consistency.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/adr.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/brd.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/business-case.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/charter.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/frnfr.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/prd.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/project.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/risk-register.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/runbook.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/status-report.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/test-cases.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/schema/user-story.schema.json +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/_data/templates/project.template.md +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/consistency.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/extract.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/graph.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/loader.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/models.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/profiles.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/projects.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/report.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/rtm.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/semantic.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/status.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert/structural.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert.egg-info/dependency_links.txt +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert.egg-info/entry_points.txt +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert.egg-info/requires.txt +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/docassert.egg-info/top_level.txt +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/pyproject.toml +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/setup.cfg +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/tests/test_config.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/tests/test_consistency.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/tests/test_defects.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/tests/test_extract.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/tests/test_graph.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/tests/test_kinds_delivery.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/tests/test_kinds_governance.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/tests/test_kinds_operate.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/tests/test_kinds_reporting.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/tests/test_profiles.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/tests/test_projects.py +0 -0
- {docassert-0.2.1 → docassert-0.3.0}/tests/test_status.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
|
|
@@ -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")
|
|
@@ -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.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
|
|
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
|
{docassert-0.2.1 → docassert-0.3.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.3.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.3.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.3.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
|
|
File without changes
|