docassert 0.1.0__py3-none-any.whl
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/__init__.py +8 -0
- docassert/__main__.py +6 -0
- docassert/_data/consistency.yaml +51 -0
- docassert/_data/criteria/adr.criteria.yaml +36 -0
- docassert/_data/criteria/benefits-realization.criteria.yaml +30 -0
- docassert/_data/criteria/brd.criteria.yaml +30 -0
- docassert/_data/criteria/business-case.criteria.yaml +23 -0
- docassert/_data/criteria/charter.criteria.yaml +73 -0
- docassert/_data/criteria/data-migration-plan.criteria.yaml +28 -0
- docassert/_data/criteria/frnfr.criteria.yaml +31 -0
- docassert/_data/criteria/hypercare-plan.criteria.yaml +27 -0
- docassert/_data/criteria/post-implementation-review.criteria.yaml +24 -0
- docassert/_data/criteria/prd.criteria.yaml +31 -0
- docassert/_data/criteria/project.criteria.yaml +32 -0
- docassert/_data/criteria/qa-test-plan.criteria.yaml +27 -0
- docassert/_data/criteria/raci-stakeholder.criteria.yaml +24 -0
- docassert/_data/criteria/release-cutover-plan.criteria.yaml +30 -0
- docassert/_data/criteria/risk-register.criteria.yaml +32 -0
- docassert/_data/criteria/rollback-plan.criteria.yaml +29 -0
- docassert/_data/criteria/runbook.criteria.yaml +30 -0
- docassert/_data/criteria/status-report.criteria.yaml +26 -0
- docassert/_data/criteria/test-cases.criteria.yaml +28 -0
- docassert/_data/criteria/user-story.criteria.yaml +32 -0
- docassert/_data/profiles/agile-delivery.yaml +20 -0
- docassert/_data/profiles/lean-startup.yaml +19 -0
- docassert/_data/profiles/regulated-industry.yaml +31 -0
- docassert/_data/schema/adr.schema.json +45 -0
- docassert/_data/schema/benefits-realization.schema.json +45 -0
- docassert/_data/schema/brd.schema.json +45 -0
- docassert/_data/schema/business-case.schema.json +45 -0
- docassert/_data/schema/charter.schema.json +84 -0
- docassert/_data/schema/data-migration-plan.schema.json +45 -0
- docassert/_data/schema/frnfr.schema.json +45 -0
- docassert/_data/schema/hypercare-plan.schema.json +45 -0
- docassert/_data/schema/post-implementation-review.schema.json +45 -0
- docassert/_data/schema/prd.schema.json +45 -0
- docassert/_data/schema/project.schema.json +32 -0
- docassert/_data/schema/qa-test-plan.schema.json +45 -0
- docassert/_data/schema/raci-stakeholder.schema.json +45 -0
- docassert/_data/schema/release-cutover-plan.schema.json +45 -0
- docassert/_data/schema/risk-register.schema.json +45 -0
- docassert/_data/schema/rollback-plan.schema.json +45 -0
- docassert/_data/schema/runbook.schema.json +45 -0
- docassert/_data/schema/status-report.schema.json +58 -0
- docassert/_data/schema/test-cases.schema.json +45 -0
- docassert/_data/schema/user-story.schema.json +45 -0
- docassert/_data/templates/adr.template.md +17 -0
- docassert/_data/templates/benefits-realization.template.md +25 -0
- docassert/_data/templates/brd.template.md +22 -0
- docassert/_data/templates/business-case.template.md +27 -0
- docassert/_data/templates/charter.template.md +46 -0
- docassert/_data/templates/data-migration-plan.template.md +35 -0
- docassert/_data/templates/frnfr.template.md +19 -0
- docassert/_data/templates/hypercare-plan.template.md +29 -0
- docassert/_data/templates/post-implementation-review.template.md +31 -0
- docassert/_data/templates/prd.template.md +23 -0
- docassert/_data/templates/project.template.md +17 -0
- docassert/_data/templates/qa-test-plan.template.md +31 -0
- docassert/_data/templates/raci-stakeholder.template.md +21 -0
- docassert/_data/templates/release-cutover-plan.template.md +28 -0
- docassert/_data/templates/risk-register.template.md +18 -0
- docassert/_data/templates/rollback-plan.template.md +24 -0
- docassert/_data/templates/runbook.template.md +28 -0
- docassert/_data/templates/status-report.template.md +27 -0
- docassert/_data/templates/test-cases.template.md +17 -0
- docassert/_data/templates/user-story.template.md +17 -0
- docassert/cli.py +291 -0
- docassert/config.py +104 -0
- docassert/consistency.py +167 -0
- docassert/graph.py +68 -0
- docassert/loader.py +116 -0
- docassert/models.py +99 -0
- docassert/profiles.py +111 -0
- docassert/projects.py +49 -0
- docassert/report.py +83 -0
- docassert/rtm.py +70 -0
- docassert/semantic.py +124 -0
- docassert/status.py +538 -0
- docassert/structural.py +406 -0
- docassert-0.1.0.dist-info/METADATA +125 -0
- docassert-0.1.0.dist-info/RECORD +86 -0
- docassert-0.1.0.dist-info/WHEEL +5 -0
- docassert-0.1.0.dist-info/entry_points.txt +2 -0
- docassert-0.1.0.dist-info/licenses/LICENSE +201 -0
- docassert-0.1.0.dist-info/licenses/NOTICE +4 -0
- docassert-0.1.0.dist-info/top_level.txt +1 -0
docassert/structural.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""Deterministic structural checks. These are reliable and BLOCKING."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import datetime as dt
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
|
|
8
|
+
from jsonschema import Draft7Validator, FormatChecker
|
|
9
|
+
|
|
10
|
+
from .loader import iter_item_lines
|
|
11
|
+
from .models import CheckResult, Document
|
|
12
|
+
|
|
13
|
+
# ── measurability heuristic ────────────────────────────────────────────────
|
|
14
|
+
# A success criterion is "measurable" if it contains a number AND either a
|
|
15
|
+
# comparator or a unit/symbol/date — enough to make pass/fail unambiguous.
|
|
16
|
+
_NUMBER = re.compile(r"\d")
|
|
17
|
+
_COMPARATOR = re.compile(
|
|
18
|
+
r"[<>]=?|\b(?:below|under|above|over|at least|at most|no more than|"
|
|
19
|
+
r"no less than|fewer than|more than|greater than|less than|within|by|"
|
|
20
|
+
r"reach|reduce|increase|decrease|drop|rise|cut|from|to)\b",
|
|
21
|
+
re.IGNORECASE,
|
|
22
|
+
)
|
|
23
|
+
_UNIT_OR_SYMBOL = re.compile(
|
|
24
|
+
r"%|[$€£]|/\s*\d|\d{4}-\d{2}-\d{2}|"
|
|
25
|
+
r"\b(?:hours?|hrs?|days?|weeks?|months?|minutes?|mins?|seconds?|secs?|"
|
|
26
|
+
r"USD|EUR|GBP|k|m|bn|pts?|points?|x)\b",
|
|
27
|
+
re.IGNORECASE,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _is_measurable(text: str) -> bool:
|
|
32
|
+
if not _NUMBER.search(text):
|
|
33
|
+
return False
|
|
34
|
+
return bool(_COMPARATOR.search(text) or _UNIT_OR_SYMBOL.search(text))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _field_value(item: str, field: str) -> str | None:
|
|
38
|
+
"""Return the text after `field:` (up to a sentence/clause break), or None.
|
|
39
|
+
|
|
40
|
+
Stops at a semicolon or a period that ends a clause (period + space, or
|
|
41
|
+
period at end) so dotted handles like ``alex.kim`` are not truncated.
|
|
42
|
+
"""
|
|
43
|
+
m = re.search(rf"{field}\s*:\s*(.+?)(?:;|\.\s|\.$|$)", item, re.IGNORECASE)
|
|
44
|
+
if not m:
|
|
45
|
+
return None
|
|
46
|
+
val = m.group(1).strip()
|
|
47
|
+
return val if len(val) >= 2 else None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _jsonify(value):
|
|
51
|
+
"""Convert YAML date/datetime objects to ISO strings so a JSON Schema with
|
|
52
|
+
`type: string, format: date` validates correctly."""
|
|
53
|
+
if isinstance(value, dict):
|
|
54
|
+
return {k: _jsonify(v) for k, v in value.items()}
|
|
55
|
+
if isinstance(value, list):
|
|
56
|
+
return [_jsonify(v) for v in value]
|
|
57
|
+
if isinstance(value, (dt.date, dt.datetime)):
|
|
58
|
+
return value.isoformat()
|
|
59
|
+
return value
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _as_date(value) -> dt.date | None:
|
|
63
|
+
if isinstance(value, dt.datetime):
|
|
64
|
+
return value.date()
|
|
65
|
+
if isinstance(value, dt.date):
|
|
66
|
+
return value
|
|
67
|
+
if isinstance(value, str):
|
|
68
|
+
try:
|
|
69
|
+
return dt.date.fromisoformat(value.strip())
|
|
70
|
+
except ValueError:
|
|
71
|
+
return None
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ── individual checks ──────────────────────────────────────────────────────
|
|
76
|
+
def check_frontmatter_schema(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
77
|
+
schema = ctx["schema"]
|
|
78
|
+
validator = Draft7Validator(schema, format_checker=FormatChecker())
|
|
79
|
+
errors = sorted(validator.iter_errors(_jsonify(doc.frontmatter)), key=str)
|
|
80
|
+
if not errors:
|
|
81
|
+
return True, "Frontmatter is valid against the schema."
|
|
82
|
+
msgs = "; ".join(f"{'/'.join(str(p) for p in e.path) or '(root)'}: {e.message}"
|
|
83
|
+
for e in errors)
|
|
84
|
+
return False, f"Frontmatter schema errors: {msgs}"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def check_required_sections(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
88
|
+
required = ctx["required_sections"]
|
|
89
|
+
missing = [s for s in required if s not in doc.sections]
|
|
90
|
+
empty = [s for s in required if s in doc.sections and doc.sections[s].is_empty]
|
|
91
|
+
problems = []
|
|
92
|
+
if missing:
|
|
93
|
+
problems.append(f"missing: {', '.join(missing)}")
|
|
94
|
+
if empty:
|
|
95
|
+
problems.append(f"empty: {', '.join(empty)}")
|
|
96
|
+
if problems:
|
|
97
|
+
return False, "Required sections — " + "; ".join(problems)
|
|
98
|
+
return True, f"All {len(required)} required sections present and non-empty."
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def check_measurable_success_criteria(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
102
|
+
section = doc.section("Success Criteria")
|
|
103
|
+
if section is None:
|
|
104
|
+
return False, "No Success Criteria section."
|
|
105
|
+
items = section.list_items
|
|
106
|
+
if not items:
|
|
107
|
+
return False, "Success Criteria has no bulleted criteria."
|
|
108
|
+
unmeasurable = [it for it in items if not _is_measurable(it)]
|
|
109
|
+
if unmeasurable:
|
|
110
|
+
preview = "; ".join(f'“{u[:60]}”' for u in unmeasurable)
|
|
111
|
+
return False, f"{len(unmeasurable)}/{len(items)} criteria lack a measurable threshold: {preview}"
|
|
112
|
+
return True, f"All {len(items)} success criteria state a measurable threshold."
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def check_risks_owner_mitigation(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
116
|
+
section = doc.section("Risks")
|
|
117
|
+
if section is None:
|
|
118
|
+
return False, "No Risks section."
|
|
119
|
+
items = section.list_items
|
|
120
|
+
if not items:
|
|
121
|
+
return False, "Risks has no bulleted risks."
|
|
122
|
+
bad = []
|
|
123
|
+
for it in items:
|
|
124
|
+
if _field_value(it, "owner") is None or _field_value(it, "mitigation") is None:
|
|
125
|
+
bad.append(it)
|
|
126
|
+
if bad:
|
|
127
|
+
preview = "; ".join(f'“{b[:60]}”' for b in bad)
|
|
128
|
+
return False, f"{len(bad)}/{len(items)} risks miss an Owner and/or Mitigation: {preview}"
|
|
129
|
+
return True, f"All {len(items)} risks name an owner and a mitigation."
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def check_dates_consistent(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
133
|
+
dates = doc.frontmatter.get("dates") or {}
|
|
134
|
+
created = _as_date(dates.get("created"))
|
|
135
|
+
target = _as_date(dates.get("target"))
|
|
136
|
+
if created is None or target is None:
|
|
137
|
+
return False, "dates.created and dates.target must be valid ISO dates."
|
|
138
|
+
if target < created:
|
|
139
|
+
return False, f"target ({target}) is before created ({created})."
|
|
140
|
+
return True, f"Dates consistent (created {created} → target {target})."
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def check_unique_id(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
144
|
+
if not doc.id:
|
|
145
|
+
return False, "Document has no id."
|
|
146
|
+
others = [p for p in ctx.get("id_index", {}).get(doc.id, []) if p != doc.path]
|
|
147
|
+
if others:
|
|
148
|
+
return False, f"id '{doc.id}' also used by: {', '.join(others)}"
|
|
149
|
+
return True, f"id '{doc.id}' is unique."
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _project_code(doc: Document) -> str:
|
|
153
|
+
"""The project code for a document, e.g. 'AUR' from project: PRJ-001-AUR."""
|
|
154
|
+
return str(doc.frontmatter.get("project", "")).split("-")[-1]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def check_items_well_formed(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
158
|
+
"""Every bullet in a declared item-section is a valid item
|
|
159
|
+
``**<CODE>-<TYPE>-<NNN>**``: the type matches the section, and the project
|
|
160
|
+
code matches the document's project."""
|
|
161
|
+
specs = ctx.get("item_sections") or []
|
|
162
|
+
if not specs:
|
|
163
|
+
return True, "No item sections for this kind."
|
|
164
|
+
code = _project_code(doc)
|
|
165
|
+
problems: list[str] = []
|
|
166
|
+
total = 0
|
|
167
|
+
for spec in specs:
|
|
168
|
+
section = doc.section(spec["section"])
|
|
169
|
+
if section is None:
|
|
170
|
+
continue # a missing section is the required-sections check's job
|
|
171
|
+
for raw, m in iter_item_lines(section):
|
|
172
|
+
total += 1
|
|
173
|
+
if not m:
|
|
174
|
+
problems.append(f'malformed item in "{spec["section"]}": “{raw[:50]}”')
|
|
175
|
+
continue
|
|
176
|
+
if m.group("type") != spec["prefix"]:
|
|
177
|
+
problems.append(
|
|
178
|
+
f'“{m.group("id")}” in "{spec["section"]}" should be type '
|
|
179
|
+
f'{spec["prefix"]} ({code}-{spec["prefix"]}-NNN)')
|
|
180
|
+
if code and m.group("project") != code:
|
|
181
|
+
problems.append(
|
|
182
|
+
f'“{m.group("id")}” uses project {m.group("project")} but the '
|
|
183
|
+
f'document belongs to {code}')
|
|
184
|
+
if problems:
|
|
185
|
+
return False, "; ".join(problems)
|
|
186
|
+
return True, f"All {total} item(s) well-formed."
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
_PROJECT_ID_RE = re.compile(r"^PRJ-\d{3,}-(?P<code>[A-Z]{2,6})$")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def check_project_id_format(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
193
|
+
"""A project.md declares a well-formed id (PRJ-NNN-CODE) whose tail matches
|
|
194
|
+
its `code` field."""
|
|
195
|
+
pid = str(doc.frontmatter.get("id", ""))
|
|
196
|
+
code = str(doc.frontmatter.get("code", ""))
|
|
197
|
+
m = _PROJECT_ID_RE.match(pid)
|
|
198
|
+
if not m:
|
|
199
|
+
return False, f"project id {pid!r} must match PRJ-NNN-CODE (e.g. PRJ-001-AUR)."
|
|
200
|
+
if m.group("code") != code:
|
|
201
|
+
return False, f"project id tail ({m.group('code')}) must equal code ({code!r})."
|
|
202
|
+
return True, f"Project id {pid} is well-formed."
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
_RISK_FIELDS = ["Probability", "Impact", "Owner", "Response"]
|
|
206
|
+
_ADR_STATES = {"proposed", "accepted", "superseded", "deprecated", "rejected"}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _items_of_prefix(doc: Document, ctx: dict, prefix: str):
|
|
210
|
+
"""Yield (id, text) for every well-formed item of `prefix` in the doc."""
|
|
211
|
+
for spec in (ctx.get("item_sections") or []):
|
|
212
|
+
if spec.get("prefix") != prefix:
|
|
213
|
+
continue
|
|
214
|
+
section = doc.section(spec["section"])
|
|
215
|
+
if section is None:
|
|
216
|
+
continue
|
|
217
|
+
for _raw, m in iter_item_lines(section):
|
|
218
|
+
if m:
|
|
219
|
+
yield m.group("id"), m.group("text")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def check_risk_items_complete(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
223
|
+
"""Every RISK item names a Probability, Impact, Owner, and Response."""
|
|
224
|
+
incomplete, total = [], 0
|
|
225
|
+
for iid, text in _items_of_prefix(doc, ctx, "RISK"):
|
|
226
|
+
total += 1
|
|
227
|
+
missing = [f for f in _RISK_FIELDS if _field_value(text, f) is None]
|
|
228
|
+
if missing:
|
|
229
|
+
incomplete.append(f'{iid} missing {", ".join(missing)}')
|
|
230
|
+
if incomplete:
|
|
231
|
+
return False, "; ".join(incomplete)
|
|
232
|
+
return True, f"All {total} risk(s) state probability, impact, owner, and response."
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def check_adr_items_have_status(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
236
|
+
"""Every ADR item declares a valid Status."""
|
|
237
|
+
bad, total = [], 0
|
|
238
|
+
for iid, text in _items_of_prefix(doc, ctx, "ADR"):
|
|
239
|
+
total += 1
|
|
240
|
+
status = _field_value(text, "status")
|
|
241
|
+
if status is None or status.lower() not in _ADR_STATES:
|
|
242
|
+
bad.append(f"{iid} status {status!r}")
|
|
243
|
+
if bad:
|
|
244
|
+
return False, ("; ".join(bad)
|
|
245
|
+
+ f" (expected one of: {', '.join(sorted(_ADR_STATES))})")
|
|
246
|
+
return True, f"All {total} decision(s) have a valid status."
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def check_raci_one_accountable(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
250
|
+
"""The RACI Matrix table has exactly one Accountable (A) per activity."""
|
|
251
|
+
section = doc.section("RACI Matrix")
|
|
252
|
+
if section is None:
|
|
253
|
+
return False, "No RACI Matrix section."
|
|
254
|
+
rows = [ln.strip() for ln in section.body.splitlines() if ln.strip().startswith("|")]
|
|
255
|
+
data = [r for r in rows if "---" not in r][1:] # drop header + separator
|
|
256
|
+
if not data:
|
|
257
|
+
return False, "RACI Matrix has no activity rows."
|
|
258
|
+
problems = []
|
|
259
|
+
for row in data:
|
|
260
|
+
cells = [c.strip() for c in row.strip("|").split("|")]
|
|
261
|
+
activity = cells[0] if cells else "?"
|
|
262
|
+
a_count = sum(1 for c in cells[1:] if c.upper() == "A")
|
|
263
|
+
if a_count != 1:
|
|
264
|
+
problems.append(f'"{activity}" has {a_count} Accountable (need exactly 1)')
|
|
265
|
+
if problems:
|
|
266
|
+
return False, "; ".join(problems)
|
|
267
|
+
return True, f"All {len(data)} activities have exactly one Accountable role."
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def check_story_format(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
271
|
+
"""Every US item follows 'As a … I want … so that …'."""
|
|
272
|
+
bad, total = [], 0
|
|
273
|
+
for iid, text in _items_of_prefix(doc, ctx, "US"):
|
|
274
|
+
total += 1
|
|
275
|
+
low = text.lower()
|
|
276
|
+
if not (("as a " in low or "as an " in low) and "i want" in low):
|
|
277
|
+
bad.append(iid)
|
|
278
|
+
if bad:
|
|
279
|
+
return False, "stories not in 'As a … I want …' form: " + ", ".join(bad)
|
|
280
|
+
return True, f"All {total} user story(ies) follow the standard form."
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def check_measurable_exit_criteria(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
284
|
+
"""Every Exit Criteria bullet states a measurable threshold."""
|
|
285
|
+
section = doc.section("Exit Criteria")
|
|
286
|
+
if section is None:
|
|
287
|
+
return False, "No Exit Criteria section."
|
|
288
|
+
items = section.list_items
|
|
289
|
+
if not items:
|
|
290
|
+
return False, "Exit Criteria has no bulleted criteria."
|
|
291
|
+
unmeasurable = [it for it in items if not _is_measurable(it)]
|
|
292
|
+
if unmeasurable:
|
|
293
|
+
preview = "; ".join(f'“{u[:50]}”' for u in unmeasurable)
|
|
294
|
+
return False, f"{len(unmeasurable)}/{len(items)} exit criteria lack a measurable threshold: {preview}"
|
|
295
|
+
return True, f"All {len(items)} exit criteria state a measurable threshold."
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def check_has_mapping_table(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
299
|
+
"""The Field Mapping section contains a table with at least one row."""
|
|
300
|
+
section = doc.section("Field Mapping")
|
|
301
|
+
if section is None:
|
|
302
|
+
return False, "No Field Mapping section."
|
|
303
|
+
rows = [ln.strip() for ln in section.body.splitlines() if ln.strip().startswith("|")]
|
|
304
|
+
data = [r for r in rows if "---" not in r][1:]
|
|
305
|
+
if not data:
|
|
306
|
+
return False, "Field Mapping has no mapping table rows."
|
|
307
|
+
return True, f"Field Mapping table has {len(data)} row(s)."
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
_STEP_RE = re.compile(r"^\s*\d+\.\s")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def check_numbered_steps(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
314
|
+
"""Each section named in `steps_sections` has an ordered (numbered) list of
|
|
315
|
+
at least two steps."""
|
|
316
|
+
specs = ctx.get("steps_sections") or []
|
|
317
|
+
if not specs:
|
|
318
|
+
return True, "No step sections for this kind."
|
|
319
|
+
problems, total = [], 0
|
|
320
|
+
for name in specs:
|
|
321
|
+
section = doc.section(name)
|
|
322
|
+
if section is None:
|
|
323
|
+
continue
|
|
324
|
+
n = sum(1 for ln in section.body.splitlines() if _STEP_RE.match(ln))
|
|
325
|
+
total += n
|
|
326
|
+
if n < 2:
|
|
327
|
+
problems.append(f'"{name}" needs at least 2 numbered steps (found {n})')
|
|
328
|
+
if problems:
|
|
329
|
+
return False, "; ".join(problems)
|
|
330
|
+
return True, f"Step sections have {total} numbered step(s)."
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def check_measurable_items(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
334
|
+
"""Every bullet in each section named in `measurable_sections` states a
|
|
335
|
+
measurable threshold."""
|
|
336
|
+
specs = ctx.get("measurable_sections") or []
|
|
337
|
+
if not specs:
|
|
338
|
+
return True, "No measurable sections for this kind."
|
|
339
|
+
problems, total = [], 0
|
|
340
|
+
for name in specs:
|
|
341
|
+
section = doc.section(name)
|
|
342
|
+
if section is None:
|
|
343
|
+
continue
|
|
344
|
+
items = section.list_items
|
|
345
|
+
if not items:
|
|
346
|
+
problems.append(f'"{name}" has no bulleted items')
|
|
347
|
+
continue
|
|
348
|
+
total += len(items)
|
|
349
|
+
bad = [it for it in items if not _is_measurable(it)]
|
|
350
|
+
if bad:
|
|
351
|
+
problems.append(f'{len(bad)}/{len(items)} in "{name}" lack a measurable '
|
|
352
|
+
f'threshold: ' + "; ".join(f'“{b[:40]}”' for b in bad))
|
|
353
|
+
if problems:
|
|
354
|
+
return False, "; ".join(problems)
|
|
355
|
+
return True, f"All {total} item(s) in measurable sections state a threshold."
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
_RISK_REF_RE = re.compile(r"\b[A-Z]{2,6}-RISK-\d+\b")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def check_references_risk(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
362
|
+
"""The 'Risks & Issues' section cites at least one <CODE>-RISK-### from the register."""
|
|
363
|
+
section = doc.section("Risks & Issues")
|
|
364
|
+
if section is None:
|
|
365
|
+
return False, "No Risks & Issues section."
|
|
366
|
+
refs = _RISK_REF_RE.findall(section.body)
|
|
367
|
+
if not refs:
|
|
368
|
+
return False, "Risks & Issues cites no <CODE>-RISK-### from the register."
|
|
369
|
+
return True, f"Cites {len(set(refs))} risk(s) from the register: {', '.join(sorted(set(refs)))}."
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
CHECKS: dict[str, Callable[[Document, dict], tuple[bool, str]]] = {
|
|
373
|
+
"frontmatter-schema": check_frontmatter_schema,
|
|
374
|
+
"required-sections": check_required_sections,
|
|
375
|
+
"measurable-success-criteria": check_measurable_success_criteria,
|
|
376
|
+
"risks-have-owner-and-mitigation": check_risks_owner_mitigation,
|
|
377
|
+
"dates-consistent": check_dates_consistent,
|
|
378
|
+
"unique-id": check_unique_id,
|
|
379
|
+
"items-well-formed": check_items_well_formed,
|
|
380
|
+
"risk-items-complete": check_risk_items_complete,
|
|
381
|
+
"adr-items-have-status": check_adr_items_have_status,
|
|
382
|
+
"raci-one-accountable": check_raci_one_accountable,
|
|
383
|
+
"story-format": check_story_format,
|
|
384
|
+
"measurable-exit-criteria": check_measurable_exit_criteria,
|
|
385
|
+
"mapping-table": check_has_mapping_table,
|
|
386
|
+
"numbered-steps": check_numbered_steps,
|
|
387
|
+
"measurable-items": check_measurable_items,
|
|
388
|
+
"references-risk": check_references_risk,
|
|
389
|
+
"project-id-format": check_project_id_format,
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def run_structural(doc: Document, spec: dict, ctx: dict) -> CheckResult:
|
|
394
|
+
"""Run one structural check described by a criteria `spec` dict."""
|
|
395
|
+
check_id = spec["id"]
|
|
396
|
+
fn = CHECKS.get(check_id)
|
|
397
|
+
blocking = bool(spec.get("blocking", True))
|
|
398
|
+
if fn is None:
|
|
399
|
+
return CheckResult(check_id, False, blocking,
|
|
400
|
+
f"Unknown structural check '{check_id}'.", kind="structural")
|
|
401
|
+
try:
|
|
402
|
+
passed, detail = fn(doc, ctx)
|
|
403
|
+
except Exception as exc: # a check crash is a failure, never a silent pass
|
|
404
|
+
return CheckResult(check_id, False, blocking,
|
|
405
|
+
f"Check errored: {exc}", kind="structural")
|
|
406
|
+
return CheckResult(check_id, passed, blocking, detail, kind="structural")
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: docassert
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unit testing for business documents — validate structured Markdown docs against a configurable audit standard.
|
|
5
|
+
Author: C4G Enterprises Inc.
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://docassert.com
|
|
8
|
+
Project-URL: Repository, https://github.com/c4g-john/docassert
|
|
9
|
+
Project-URL: Issues, https://github.com/c4g-john/docassert/issues
|
|
10
|
+
Keywords: pmo,documentation,validation,governance,markdown,audit,traceability,ci
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
21
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
License-File: NOTICE
|
|
26
|
+
Requires-Dist: python-frontmatter>=1.1
|
|
27
|
+
Requires-Dist: PyYAML>=6.0
|
|
28
|
+
Requires-Dist: jsonschema>=4.0
|
|
29
|
+
Provides-Extra: ai
|
|
30
|
+
Requires-Dist: anthropic>=0.40; extra == "ai"
|
|
31
|
+
Provides-Extra: convert
|
|
32
|
+
Requires-Dist: python-docx>=1.1; extra == "convert"
|
|
33
|
+
Requires-Dist: pypdf>=4.0; extra == "convert"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
36
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# docassert
|
|
40
|
+
|
|
41
|
+
**Unit testing for business documents.** Validate structured Markdown documents
|
|
42
|
+
(charters, BRDs, PRDs, risk registers, …) against a configurable audit standard:
|
|
43
|
+
deterministic structural checks that gate a merge, plus optional AI-graded
|
|
44
|
+
semantic checks that advise. Requirements trace end to end, and project status is
|
|
45
|
+
derived from the documents rather than self-reported.
|
|
46
|
+
|
|
47
|
+
docassert is the reference implementation of **[PMO as Code](https://c4g-john.github.io/pmo-as-code/)** —
|
|
48
|
+
a vendor-neutral standard for running a PMO from version-controlled, declarative files.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install "docassert @ git+https://github.com/c4g-john/docassert" # PyPI release coming
|
|
54
|
+
# with the AI advisory extra:
|
|
55
|
+
pip install "docassert[ai] @ git+https://github.com/c4g-john/docassert"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quickstart
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
docassert init # scaffold criteria/schema/profiles/templates into your repo
|
|
62
|
+
docassert validate documents/**/*.md # unit-test your documents
|
|
63
|
+
docassert consistency # cross-document traceability + profile completeness
|
|
64
|
+
docassert status --index # derived RAG per project
|
|
65
|
+
docassert pages --out _site # a portfolio dashboard + a page per project
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Config resolves **local override → packaged default**: docassert ships sensible
|
|
69
|
+
defaults, and your repo's own `criteria/` (or `schema/`, `profiles/`,
|
|
70
|
+
`consistency.yaml`) wins when present. `docassert init` copies the defaults in so
|
|
71
|
+
you can customize them.
|
|
72
|
+
|
|
73
|
+
## Commands
|
|
74
|
+
|
|
75
|
+
| Command | What it does |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `docassert validate <globs>` | Validate documents against their kind's criteria. Exit code = number of blocking failures. |
|
|
78
|
+
| `docassert consistency` | Cross-document checks: referential integrity, coverage, required links, profile completeness. |
|
|
79
|
+
| `docassert rtm [--project ID]` | Requirements traceability matrix (Markdown or CSV). |
|
|
80
|
+
| `docassert status [--project ID] [--index]` | Derived project status (md / json / html). |
|
|
81
|
+
| `docassert pages --out DIR` | Build the portfolio site (index + a page per project). |
|
|
82
|
+
| `docassert projects [--out] [--check]` | Generate / verify the project registry. |
|
|
83
|
+
| `docassert init [DIR]` | Scaffold the default config into a repo. |
|
|
84
|
+
|
|
85
|
+
## Document kinds
|
|
86
|
+
|
|
87
|
+
Twenty kinds, each a `templates/<kind>.template.md` + `schema/<kind>.schema.json`
|
|
88
|
+
+ `criteria/<kind>.criteria.yaml` trio: `project`, `charter`, `business-case`,
|
|
89
|
+
`brd`, `prd`, `frnfr`, `user-story`, `test-cases`, `adr`, `risk-register`,
|
|
90
|
+
`raci-stakeholder`, `qa-test-plan`, `data-migration-plan`,
|
|
91
|
+
`release-cutover-plan`, `rollback-plan`, `hypercare-plan`, `runbook`,
|
|
92
|
+
`status-report`, `post-implementation-review`, `benefits-realization`. Adding a
|
|
93
|
+
kind is adding a trio — no code for the common cases.
|
|
94
|
+
|
|
95
|
+
## Two tiers of checks
|
|
96
|
+
|
|
97
|
+
- **Structural — deterministic, blocking.** Required fields and sections,
|
|
98
|
+
measurable success criteria, risks with owner + mitigation, resolving
|
|
99
|
+
references, unique ids. Plain Python, reliable enough to gate a merge.
|
|
100
|
+
- **Semantic — AI-graded, advisory.** Scored via the Anthropic API and posted to
|
|
101
|
+
the PR — never blocking. Set `ANTHROPIC_API_KEY` to enable; skipped otherwise.
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
107
|
+
pip install -e ".[dev]"
|
|
108
|
+
pytest
|
|
109
|
+
ruff check .
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This repo ships example `documents/` (four sample projects) that the test suite
|
|
113
|
+
validates against.
|
|
114
|
+
|
|
115
|
+
## The reference deployment
|
|
116
|
+
|
|
117
|
+
[**pmo-as-code-pipeline**](https://github.com/c4g-john/pmo-as-code-pipeline) is a
|
|
118
|
+
living example — sample projects, the gate on every pull request, and a published
|
|
119
|
+
dashboard at
|
|
120
|
+
[c4g-john.github.io/pmo-as-code-pipeline](https://c4g-john.github.io/pmo-as-code-pipeline/).
|
|
121
|
+
The standard's site is [c4g-john.github.io/pmo-as-code](https://c4g-john.github.io/pmo-as-code/).
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
Apache-2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE). © 2026 C4G Enterprises Inc.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
docassert/__init__.py,sha256=r3cAnfv14fB1O1GUSxbmeJO-I6iAqcraldvzs-hCW98,273
|
|
2
|
+
docassert/__main__.py,sha256=4JMK66Wj4uLZTKbF-sT3LAxOsr6buig77PmOkJCRRxw,83
|
|
3
|
+
docassert/cli.py,sha256=3yKxLNx3Z0cvBbOySmc23Il0tG1Ksm4cjKnuQHLiYTA,11452
|
|
4
|
+
docassert/config.py,sha256=kRzBvqKIbC3nsviUf6uo9IMP4DXzrEhB0ebJ_5CjjRM,4299
|
|
5
|
+
docassert/consistency.py,sha256=nRbavINfL3A_ZRZb4SP74s7Ub706mTHaHDDArULVO3g,7531
|
|
6
|
+
docassert/graph.py,sha256=JXQyRmBU3bqAcquFK9T2G49ZOp8i9I9or0oV5qc1FGA,2647
|
|
7
|
+
docassert/loader.py,sha256=04rtye1e95sN6-brPiCNHZzPqZu9c1rntJTP1gLpbhA,4000
|
|
8
|
+
docassert/models.py,sha256=xUz3-Ke-UiSf9oWx3j3MtJsXj6kwTLpEcWd2IUopd5s,3148
|
|
9
|
+
docassert/profiles.py,sha256=Wav3XU4znkLXOOuO674JJgAmaOccPSLCMKpzQJQ6N7c,4477
|
|
10
|
+
docassert/projects.py,sha256=wbN7ZPXUSbiz8uMRQHbj04iLavFzdbvrPuuz84mDA9I,1751
|
|
11
|
+
docassert/report.py,sha256=6klvTyAS2HF-XAXDAH8m48s0jQYZvUAAmHwdGF798nQ,3679
|
|
12
|
+
docassert/rtm.py,sha256=yj_OsON0sdez_qvxEEQT36FTHToopbxWIca60wTkkxk,2474
|
|
13
|
+
docassert/semantic.py,sha256=bPGnT1ascg6sTKBRZqd7pABKZO0rEyUftH1gF9z6swA,4720
|
|
14
|
+
docassert/status.py,sha256=plpo6ofS1QVRp0ol-IZX0Zty0Iq05hEvUXqlqbecs7s,24464
|
|
15
|
+
docassert/structural.py,sha256=ryUy8xFG0BQmEBl731xtDwN8fEuAYfmjkEn7CffUkyY,16979
|
|
16
|
+
docassert/_data/consistency.yaml,sha256=YSe8zaebPVoZLcjfUbLPrPl0JE2gwz1hg2nUQXkz59s,2130
|
|
17
|
+
docassert/_data/criteria/adr.criteria.yaml,sha256=2tTiw4A1EUM2dTTkFKaeIWCSi8RyJYpygDb2APEKo6c,1073
|
|
18
|
+
docassert/_data/criteria/benefits-realization.criteria.yaml,sha256=fwHyKLN6YfHH4DU-20_qcmH_udt6xu96nfmJ4c_PbKs,797
|
|
19
|
+
docassert/_data/criteria/brd.criteria.yaml,sha256=5enPIUR9F7BAOemK1KYHmDu9nmQOshXusz5a77Cw95s,801
|
|
20
|
+
docassert/_data/criteria/business-case.criteria.yaml,sha256=PdxKSELE6L1rfXNVUwbq1SLNlLbGn9YVm4kjUoHCEjY,580
|
|
21
|
+
docassert/_data/criteria/charter.criteria.yaml,sha256=3JpgfHO8qXRiFvSuRZUKwX3SwbgVt_T-kCsTZFp-XdM,2468
|
|
22
|
+
docassert/_data/criteria/data-migration-plan.criteria.yaml,sha256=RyeQUfkR2Tf7DfGxkflDsFbwu9DO3TCf0gayZinFXVw,729
|
|
23
|
+
docassert/_data/criteria/frnfr.criteria.yaml,sha256=w6q8wQmr54fWMfPe48qoG7QvOhYNmawQssb_Kxc5epM,857
|
|
24
|
+
docassert/_data/criteria/hypercare-plan.criteria.yaml,sha256=hYsWDmsw4G2aOAsBElzQMvn_h4dZ6rtmBHbOFtDlxz8,733
|
|
25
|
+
docassert/_data/criteria/post-implementation-review.criteria.yaml,sha256=Kx2vbm4SchJ-xJ50C6XQG9RSx4FqDKrOTH_hwcS5RlM,655
|
|
26
|
+
docassert/_data/criteria/prd.criteria.yaml,sha256=n7wAYWGrlwpO_cvY3ZNYnOjwKQQiJw0D-JMN1elVYjg,838
|
|
27
|
+
docassert/_data/criteria/project.criteria.yaml,sha256=Ix-D1VPbFzPBZZHb2b8fpc7z_0pTNW_qvEnkb-WA2JQ,883
|
|
28
|
+
docassert/_data/criteria/qa-test-plan.criteria.yaml,sha256=mGlbwekp1DBwG0cEy_WxiGm5YhiKVX1KbG9w9jUe2aY,716
|
|
29
|
+
docassert/_data/criteria/raci-stakeholder.criteria.yaml,sha256=TKmB-1ExX2RQvbXNmAVgYL4GBwjqIf8M8XKy1joZpNg,704
|
|
30
|
+
docassert/_data/criteria/release-cutover-plan.criteria.yaml,sha256=tOSCtZG0sH_irYUOQVFqPZhk5NJUkpIioAFq9vYfsrQ,791
|
|
31
|
+
docassert/_data/criteria/risk-register.criteria.yaml,sha256=FQZ5-PnYrSsWCgjuZgEVNEq9t8ueMJ5nEJ7avQ19FJA,839
|
|
32
|
+
docassert/_data/criteria/rollback-plan.criteria.yaml,sha256=UNcYLcS1ydE4GTbGwzVC5jGFpoS8C20IikiB-DEMNQU,747
|
|
33
|
+
docassert/_data/criteria/runbook.criteria.yaml,sha256=BRziVcsrxJz3YfrpJ4cJ4WSvPaIVvfWb2YIgynybPtg,725
|
|
34
|
+
docassert/_data/criteria/status-report.criteria.yaml,sha256=BxSEA_zK6_Y6ZcDMPdry-4NeDNUawQGnhvgBJ8LT4_k,717
|
|
35
|
+
docassert/_data/criteria/test-cases.criteria.yaml,sha256=9R-9bBW0Tnyh4DxN2BYl035C_YZnWpr3nCWb0_NrM_c,729
|
|
36
|
+
docassert/_data/criteria/user-story.criteria.yaml,sha256=Z_CZpb1F4S_X2zQt7mV7y3DjI9gZIs_jQKyUhTK6u1Y,829
|
|
37
|
+
docassert/_data/profiles/agile-delivery.yaml,sha256=BKELKUX2288YJ8jXtFiXOoX-VdcYmDq8PPI_SSnTmTc,486
|
|
38
|
+
docassert/_data/profiles/lean-startup.yaml,sha256=uxHvGvb-aLE97SbdpGKubOAbe5mrR04-LYiDlUKZyTE,449
|
|
39
|
+
docassert/_data/profiles/regulated-industry.yaml,sha256=rId3JCU9uMiSseP9eDz-4xQMDN5jkjVAWZKzBh2J4Lw,948
|
|
40
|
+
docassert/_data/schema/adr.schema.json,sha256=EpBNzWNDX-pcVtfLmz0O_91mUmnWrFgeDw-sxxWCeNk,1002
|
|
41
|
+
docassert/_data/schema/benefits-realization.schema.json,sha256=CnLIl26L88mjZEsQH0rIYB8zxJCQACSddnvQGSsxaCI,1005
|
|
42
|
+
docassert/_data/schema/brd.schema.json,sha256=FZBUFu_V7l7x5LkAjkmQTWZBYSQU4PsH5Oafod2OzhU,998
|
|
43
|
+
docassert/_data/schema/business-case.schema.json,sha256=yEu7tzw-kqioNZDi4InrBfQK0JArGXox6LMHYgFjTdg,995
|
|
44
|
+
docassert/_data/schema/charter.schema.json,sha256=Sf_KV1cWqudAC6qTSaHgWc-ZhKjCUthg1cBHjcTf4k0,1853
|
|
45
|
+
docassert/_data/schema/data-migration-plan.schema.json,sha256=KHPLI541f9LmXoGYBxS51Wa_O7Ub5nhrzaEtPR4ZOCc,1003
|
|
46
|
+
docassert/_data/schema/frnfr.schema.json,sha256=u2B8camWpNKeIph-kFzNkUQvX6xPA_HVI8MOTuA4VAA,1010
|
|
47
|
+
docassert/_data/schema/hypercare-plan.schema.json,sha256=dmAGt9esQwnNs1cMwhStOXf1zRH8xuWXLS21p3iouBo,993
|
|
48
|
+
docassert/_data/schema/post-implementation-review.schema.json,sha256=LC35mO2FuWmbnUfLseb2T4XnIvsnk3-m-94m3paFut8,1017
|
|
49
|
+
docassert/_data/schema/prd.schema.json,sha256=ib_znW0Z8wfbe5_x2o0eQZUynJxvTedL_MXYpmEUWCA,997
|
|
50
|
+
docassert/_data/schema/project.schema.json,sha256=pHByCi1h8H8OS6REOBXMPxqmlSrAHfgZ975cfT1wYYU,1198
|
|
51
|
+
docassert/_data/schema/qa-test-plan.schema.json,sha256=0V7BLmfQcI4ggvEdNJ6uOuYmPzc2AwCvZstu9OBNCBE,991
|
|
52
|
+
docassert/_data/schema/raci-stakeholder.schema.json,sha256=6vyXjl4OJTSik5h8p6dSFjt8-bgzsY2TdvzhvzwazI4,1008
|
|
53
|
+
docassert/_data/schema/release-cutover-plan.schema.json,sha256=PScrP5QqpKs37AVk_vQGjt5uEToZRnf3AjBSrBkSe50,1007
|
|
54
|
+
docassert/_data/schema/risk-register.schema.json,sha256=XmMkK88km78fZuHx53tCTIpY-5X3hmaoogQU4iulTv0,991
|
|
55
|
+
docassert/_data/schema/rollback-plan.schema.json,sha256=a29Se0qm656XkP4d3eEdVLT68ieU0bO4GpZ-_zxa-3A,991
|
|
56
|
+
docassert/_data/schema/runbook.schema.json,sha256=6OE1P3DfWNDQ_W8YGLyqz9Zd4V1jP4PS7Uv3N6pY9gg,979
|
|
57
|
+
docassert/_data/schema/status-report.schema.json,sha256=LfrEpGyrahn4TybEdBCdf2cgeLEB_TGYYwFsh0mKH5U,1178
|
|
58
|
+
docassert/_data/schema/test-cases.schema.json,sha256=Lawn-Zi2t4vBf3P4SRSfrwHrdtaV_Mj0lkqdDSkbYxs,985
|
|
59
|
+
docassert/_data/schema/user-story.schema.json,sha256=NL5TtXVtmeK6ysO2viwVaobImwtBghK_t2ga9a9PNxU,987
|
|
60
|
+
docassert/_data/templates/adr.template.md,sha256=36Jh7najTUM8vSKqnkceIW26RnTWZLLiFnZULvWkH3A,396
|
|
61
|
+
docassert/_data/templates/benefits-realization.template.md,sha256=liJ4cLJRkVX4mMK_JQDh_nJMIAf_6rFnx_NzOehLhkk,498
|
|
62
|
+
docassert/_data/templates/brd.template.md,sha256=9Z6YN9hqfxfaAyt4Ub9achrcLdolkWlCTSRrtQSH9sw,439
|
|
63
|
+
docassert/_data/templates/business-case.template.md,sha256=Zk4ijavPO6btlpUlbedGy-NrRrDbbUWbPP-Xmvu9ROY,457
|
|
64
|
+
docassert/_data/templates/charter.template.md,sha256=qdLVQicWGxSt9HO6LDMjAoiF_w_ld4sMoOkCQQ8BlGQ,1335
|
|
65
|
+
docassert/_data/templates/data-migration-plan.template.md,sha256=mFl0nijXISoEQtnaxyYKra6d-rFGyQaIU2P_RQTwAO4,603
|
|
66
|
+
docassert/_data/templates/frnfr.template.md,sha256=kvg47hrpNdLWWA9wc7-D3q6_R1ISjr1aUQi1aUOcGmY,383
|
|
67
|
+
docassert/_data/templates/hypercare-plan.template.md,sha256=UOk0FWlKoZA4Uu0xE44nJ4SnnlSIWcc1z_e71yo3U0s,524
|
|
68
|
+
docassert/_data/templates/post-implementation-review.template.md,sha256=F3BWgMdEFJuxx9vbebTEFfmuMA20lCiSuFZf5-HWA00,475
|
|
69
|
+
docassert/_data/templates/prd.template.md,sha256=GgaMCNZjGSEIQ8fKgSH04oILpAZMW6xtGX-3vFsWZHQ,472
|
|
70
|
+
docassert/_data/templates/project.template.md,sha256=oqYclBtMRSyvn-Jc3wHyElM1in7Dj9xDfhBm6xHvEgI,539
|
|
71
|
+
docassert/_data/templates/qa-test-plan.template.md,sha256=btpDuJ7cyEvO19SOSSZQaetKZ7zV1ctnifxQjDDCyu4,600
|
|
72
|
+
docassert/_data/templates/raci-stakeholder.template.md,sha256=lzgKvXz7G-VKIePlji6J4uYQs_WDIF_JPrKbVFeeDlI,422
|
|
73
|
+
docassert/_data/templates/release-cutover-plan.template.md,sha256=KD-uwP7q8cPX9befWgzRjGV6WtUfHOVz2O_4kOG7d7Q,468
|
|
74
|
+
docassert/_data/templates/risk-register.template.md,sha256=qB8AGkgA_zevbUfZFuCvb1YZy0u7Wx47NHYeuZJhUCA,437
|
|
75
|
+
docassert/_data/templates/rollback-plan.template.md,sha256=TitKTHUQ54oUr2WTbEhJWfdmznMuhwaB9LkRWRWgrRo,383
|
|
76
|
+
docassert/_data/templates/runbook.template.md,sha256=duHGiIvueDNpk1HX5ZJlkJFdPNjLzA8BtstDxx-9Cco,425
|
|
77
|
+
docassert/_data/templates/status-report.template.md,sha256=y_08jiUg1yCt2GvvBY5YfwyXZg_riO3ixlFMh6MiVbE,495
|
|
78
|
+
docassert/_data/templates/test-cases.template.md,sha256=0iZXwA-VLEcbTdW-RGL7B7W46s7hav0NyqP_8a2_HDw,290
|
|
79
|
+
docassert/_data/templates/user-story.template.md,sha256=KKQrxQrBqMTAwv6LUDG1ktkGcMdjhO5qj-kOutkJ_Vg,332
|
|
80
|
+
docassert-0.1.0.dist-info/licenses/LICENSE,sha256=xBe1Yr3cUoPAUIQEuhY3lYqIl9g9Sz2-7TFyPJrEyIk,11350
|
|
81
|
+
docassert-0.1.0.dist-info/licenses/NOTICE,sha256=X68MqGpL0ZAaP0yxmH4R8v5pZOcU0w5chz4USnTp44M,125
|
|
82
|
+
docassert-0.1.0.dist-info/METADATA,sha256=VQvQkpYGr652Y7XSoUfQMZY14oKy_P-kW0fWuyvDL08,5458
|
|
83
|
+
docassert-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
84
|
+
docassert-0.1.0.dist-info/entry_points.txt,sha256=TMhMaSwJcQWPMydjO1CWK0IuLQTBO_b4655glFXsBms,49
|
|
85
|
+
docassert-0.1.0.dist-info/top_level.txt,sha256=JrVdJYhp8XhcWeuXFoV7FZmI9PZ6TmcaTukUKKsVePU,10
|
|
86
|
+
docassert-0.1.0.dist-info/RECORD,,
|