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