docassert 0.15.0__tar.gz → 0.16.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. {docassert-0.15.0/docassert.egg-info → docassert-0.16.0}/PKG-INFO +1 -1
  2. {docassert-0.15.0 → docassert-0.16.0}/docassert/__init__.py +1 -1
  3. docassert-0.16.0/docassert/_data/criteria/operations.criteria.yaml +40 -0
  4. docassert-0.16.0/docassert/_data/profiles/operations.yaml +18 -0
  5. docassert-0.16.0/docassert/_data/schema/operations.schema.json +50 -0
  6. docassert-0.16.0/docassert/_data/templates/operations.template.md +17 -0
  7. {docassert-0.15.0 → docassert-0.16.0}/docassert/status/derive.py +23 -1
  8. {docassert-0.15.0 → docassert-0.16.0}/docassert/status/render.py +13 -1
  9. {docassert-0.15.0 → docassert-0.16.0}/docassert/structural.py +31 -0
  10. {docassert-0.15.0 → docassert-0.16.0/docassert.egg-info}/PKG-INFO +1 -1
  11. {docassert-0.15.0 → docassert-0.16.0}/docassert.egg-info/SOURCES.txt +4 -0
  12. {docassert-0.15.0 → docassert-0.16.0}/tests/test_profiles.py +3 -2
  13. {docassert-0.15.0 → docassert-0.16.0}/tests/test_status.py +59 -0
  14. {docassert-0.15.0 → docassert-0.16.0}/LICENSE +0 -0
  15. {docassert-0.15.0 → docassert-0.16.0}/NOTICE +0 -0
  16. {docassert-0.15.0 → docassert-0.16.0}/README.md +0 -0
  17. {docassert-0.15.0 → docassert-0.16.0}/docassert/__main__.py +0 -0
  18. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/consistency.yaml +0 -0
  19. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
  20. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
  21. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
  22. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
  23. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
  24. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
  25. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
  26. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
  27. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
  28. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
  29. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
  30. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
  31. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
  32. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
  33. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
  34. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
  35. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
  36. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
  37. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
  38. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
  39. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
  40. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
  41. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
  42. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/adr.schema.json +0 -0
  43. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
  44. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/brd.schema.json +0 -0
  45. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/business-case.schema.json +0 -0
  46. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/charter.schema.json +0 -0
  47. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
  48. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/frnfr.schema.json +0 -0
  49. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
  50. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
  51. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/prd.schema.json +0 -0
  52. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/project.schema.json +0 -0
  53. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
  54. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
  55. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
  56. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/risk-register.schema.json +0 -0
  57. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
  58. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/runbook.schema.json +0 -0
  59. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/status-report.schema.json +0 -0
  60. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/test-cases.schema.json +0 -0
  61. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/schema/user-story.schema.json +0 -0
  62. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/skills/doc-to-pmo/SKILL.md +0 -0
  63. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/adr.template.md +0 -0
  64. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/benefits-realization.template.md +0 -0
  65. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/brd.template.md +0 -0
  66. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/business-case.template.md +0 -0
  67. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/charter.template.md +0 -0
  68. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/data-migration-plan.template.md +0 -0
  69. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/frnfr.template.md +0 -0
  70. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/hypercare-plan.template.md +0 -0
  71. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/post-implementation-review.template.md +0 -0
  72. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/prd.template.md +0 -0
  73. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/project.template.md +0 -0
  74. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/qa-test-plan.template.md +0 -0
  75. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
  76. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
  77. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/risk-register.template.md +0 -0
  78. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/rollback-plan.template.md +0 -0
  79. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/runbook.template.md +0 -0
  80. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/status-report.template.md +0 -0
  81. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/test-cases.template.md +0 -0
  82. {docassert-0.15.0 → docassert-0.16.0}/docassert/_data/templates/user-story.template.md +0 -0
  83. {docassert-0.15.0 → docassert-0.16.0}/docassert/bridge/__init__.py +0 -0
  84. {docassert-0.15.0 → docassert-0.16.0}/docassert/bridge/board.py +0 -0
  85. {docassert-0.15.0 → docassert-0.16.0}/docassert/bridge/gh.py +0 -0
  86. {docassert-0.15.0 → docassert-0.16.0}/docassert/bridge/ops.py +0 -0
  87. {docassert-0.15.0 → docassert-0.16.0}/docassert/bridge/plan.py +0 -0
  88. {docassert-0.15.0 → docassert-0.16.0}/docassert/cli.py +0 -0
  89. {docassert-0.15.0 → docassert-0.16.0}/docassert/config.py +0 -0
  90. {docassert-0.15.0 → docassert-0.16.0}/docassert/consistency.py +0 -0
  91. {docassert-0.15.0 → docassert-0.16.0}/docassert/extract.py +0 -0
  92. {docassert-0.15.0 → docassert-0.16.0}/docassert/graph.py +0 -0
  93. {docassert-0.15.0 → docassert-0.16.0}/docassert/loader.py +0 -0
  94. {docassert-0.15.0 → docassert-0.16.0}/docassert/models.py +0 -0
  95. {docassert-0.15.0 → docassert-0.16.0}/docassert/profiles.py +0 -0
  96. {docassert-0.15.0 → docassert-0.16.0}/docassert/projects.py +0 -0
  97. {docassert-0.15.0 → docassert-0.16.0}/docassert/report.py +0 -0
  98. {docassert-0.15.0 → docassert-0.16.0}/docassert/rtm.py +0 -0
  99. {docassert-0.15.0 → docassert-0.16.0}/docassert/scaffold.py +0 -0
  100. {docassert-0.15.0 → docassert-0.16.0}/docassert/semantic.py +0 -0
  101. {docassert-0.15.0 → docassert-0.16.0}/docassert/status/__init__.py +0 -0
  102. {docassert-0.15.0 → docassert-0.16.0}/docassert.egg-info/dependency_links.txt +0 -0
  103. {docassert-0.15.0 → docassert-0.16.0}/docassert.egg-info/entry_points.txt +0 -0
  104. {docassert-0.15.0 → docassert-0.16.0}/docassert.egg-info/requires.txt +0 -0
  105. {docassert-0.15.0 → docassert-0.16.0}/docassert.egg-info/top_level.txt +0 -0
  106. {docassert-0.15.0 → docassert-0.16.0}/pyproject.toml +0 -0
  107. {docassert-0.15.0 → docassert-0.16.0}/setup.cfg +0 -0
  108. {docassert-0.15.0 → docassert-0.16.0}/tests/test_badge.py +0 -0
  109. {docassert-0.15.0 → docassert-0.16.0}/tests/test_bridge.py +0 -0
  110. {docassert-0.15.0 → docassert-0.16.0}/tests/test_config.py +0 -0
  111. {docassert-0.15.0 → docassert-0.16.0}/tests/test_consistency.py +0 -0
  112. {docassert-0.15.0 → docassert-0.16.0}/tests/test_defects.py +0 -0
  113. {docassert-0.15.0 → docassert-0.16.0}/tests/test_draft_relaxation.py +0 -0
  114. {docassert-0.15.0 → docassert-0.16.0}/tests/test_extract.py +0 -0
  115. {docassert-0.15.0 → docassert-0.16.0}/tests/test_graph.py +0 -0
  116. {docassert-0.15.0 → docassert-0.16.0}/tests/test_json_report.py +0 -0
  117. {docassert-0.15.0 → docassert-0.16.0}/tests/test_kinds_delivery.py +0 -0
  118. {docassert-0.15.0 → docassert-0.16.0}/tests/test_kinds_governance.py +0 -0
  119. {docassert-0.15.0 → docassert-0.16.0}/tests/test_kinds_operate.py +0 -0
  120. {docassert-0.15.0 → docassert-0.16.0}/tests/test_kinds_reporting.py +0 -0
  121. {docassert-0.15.0 → docassert-0.16.0}/tests/test_projects.py +0 -0
  122. {docassert-0.15.0 → docassert-0.16.0}/tests/test_scaffold.py +0 -0
  123. {docassert-0.15.0 → docassert-0.16.0}/tests/test_semantic.py +0 -0
  124. {docassert-0.15.0 → docassert-0.16.0}/tests/test_structural.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docassert
3
- Version: 0.15.0
3
+ Version: 0.16.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
@@ -5,4 +5,4 @@ standard: deterministic structural checks that gate a merge, plus optional
5
5
  AI-graded semantic checks that advise.
6
6
  """
7
7
 
8
- __version__ = "0.15.0"
8
+ __version__ = "0.16.0"
@@ -0,0 +1,40 @@
1
+ # Audit criteria for the Operations kind (spec §13.21).
2
+ kind: operations
3
+
4
+ required_sections:
5
+ - Overview
6
+ - Services
7
+
8
+ item_sections:
9
+ - section: Services
10
+ prefix: SVC
11
+
12
+ checks:
13
+ - id: frontmatter-schema
14
+ type: structural
15
+ blocking: true
16
+ description: Frontmatter is valid against schema/operations.schema.json.
17
+ - id: frontmatter-complete
18
+ type: structural
19
+ blocking: once-proposed
20
+ description: Every schema-required frontmatter field is present (advisory while draft).
21
+ - id: required-sections
22
+ type: structural
23
+ blocking: once-proposed
24
+ description: Every required section is present and non-empty.
25
+ - id: items-well-formed
26
+ type: structural
27
+ blocking: true
28
+ description: Every SVC item follows the item grammar in its declared section.
29
+ - id: svc-items-complete
30
+ type: structural
31
+ blocking: once-proposed
32
+ description: Every SVC item states a Level (the commitment) and a Measure (how attainment is read).
33
+ - id: ops-review-fresh
34
+ type: structural
35
+ blocking: false
36
+ description: review_by is today or later; a past date is an amber input to derived status, never a merge blocker.
37
+ - id: unique-id
38
+ type: structural
39
+ blocking: true
40
+ description: The document id is unique across the repository.
@@ -0,0 +1,18 @@
1
+ # Profile: operations
2
+ #
3
+ # For a repository (or project) whose work is a standing service catalog
4
+ # rather than a delivery arc. The catalog itself is the one required kind;
5
+ # the register and runbook are strongly recommended companions.
6
+ name: operations
7
+ description: >
8
+ A governed service catalog: the operations document is required, with the
9
+ risk register and runbook recommended alongside it.
10
+
11
+ enforce_when: active
12
+
13
+ expects:
14
+ required:
15
+ - operations
16
+ recommended:
17
+ - risk-register
18
+ - runbook
@@ -0,0 +1,50 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Operations frontmatter",
4
+ "type": "object",
5
+ "required": [
6
+ "kind",
7
+ "id",
8
+ "project",
9
+ "title",
10
+ "owner",
11
+ "status",
12
+ "review_by"
13
+ ],
14
+ "additionalProperties": true,
15
+ "properties": {
16
+ "kind": {
17
+ "const": "operations"
18
+ },
19
+ "id": {
20
+ "type": "string",
21
+ "pattern": "^[A-Z]{2,6}-[a-z0-9][a-z0-9-]*$",
22
+ "description": "Stable id, <CODE>-<slug>. Unique across documents/."
23
+ },
24
+ "title": {
25
+ "type": "string",
26
+ "minLength": 3
27
+ },
28
+ "owner": {
29
+ "type": "string",
30
+ "minLength": 2
31
+ },
32
+ "status": {
33
+ "enum": [
34
+ "draft",
35
+ "proposed",
36
+ "approved",
37
+ "baselined"
38
+ ]
39
+ },
40
+ "project": {
41
+ "type": "string",
42
+ "pattern": "^PRJ-\\d{3,}-[A-Z]{2,6}$"
43
+ },
44
+ "review_by": {
45
+ "type": "string",
46
+ "format": "date",
47
+ "description": "The next operations review; a past date is an amber input to derived status."
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,17 @@
1
+ ---
2
+ kind: operations
3
+ project: PRJ-000-XXX # the owning project's id
4
+ id: XXX-operations # <CODE>-<slug>; the project code namespaces it
5
+ title: My Operations
6
+ owner: jane.doe
7
+ status: draft
8
+ review_by: 2026-12-31 # the next operations review; stale = amber
9
+ ---
10
+
11
+ ## Overview
12
+
13
+ <!-- The operating period, the envelope, and what this catalog covers. -->
14
+
15
+ ## Services
16
+
17
+ <!-- - **XXX-SVC-001**: What the service is. Level: the commitment, measurably stated. Measure: how attainment is read. -->
@@ -113,6 +113,25 @@ def _broken_references(graph, code=None):
113
113
  return broken
114
114
 
115
115
 
116
+ def _operations(docs):
117
+ """Operations documents with their review state."""
118
+ import datetime
119
+ out = []
120
+ for d in docs:
121
+ if d.kind != "operations":
122
+ continue
123
+ raw = d.frontmatter.get("review_by")
124
+ try:
125
+ due = (raw if isinstance(raw, datetime.date)
126
+ else datetime.date.fromisoformat(str(raw)))
127
+ fresh = due >= datetime.date.today()
128
+ due_s = due.isoformat()
129
+ except (TypeError, ValueError):
130
+ fresh, due_s = False, str(raw)
131
+ out.append({"id": d.id, "review_by": due_s, "fresh": fresh})
132
+ return out
133
+
134
+
116
135
  def _latest_report(docs):
117
136
  reports = [d for d in docs if d.kind == "status-report"]
118
137
  if not reports:
@@ -179,6 +198,7 @@ def build_status(documents_dir=DOCUMENTS_DIR, project: str | None = None) -> dic
179
198
  },
180
199
  "coverage": _coverage(graph, cfg, code),
181
200
  "risks": _risks(graph, code),
201
+ "operations": _operations(docs),
182
202
  "broken_references": _broken_references(graph, code),
183
203
  "latest_report": _latest_report(docs),
184
204
  "completeness": completeness,
@@ -245,7 +265,9 @@ def derive_rag(model) -> str:
245
265
  completeness_gap = bool(comp and (comp["missing_required"] or comp["incomplete_required"]
246
266
  or comp["recommended_gaps"] or comp.get("unknown")))
247
267
  open_risks = [r for r in model["risks"] if r.get("disposition", "open") == "open"]
248
- if coverage_gap or open_risks or reported in {"amber", "red"} or completeness_gap:
268
+ stale_ops = [o for o in model.get("operations", []) if not o["fresh"]]
269
+ if (coverage_gap or open_risks or stale_ops
270
+ or reported in {"amber", "red"} or completeness_gap):
249
271
  return "amber"
250
272
  return "green"
251
273
 
@@ -54,6 +54,10 @@ def render_markdown(model, summary: bool = False) -> str:
54
54
  if failing:
55
55
  out.append(f"- 🔴 **Documents failing audit**: {', '.join(failing)}")
56
56
 
57
+ for o in model.get("operations", []):
58
+ mark = "🟢" if o["fresh"] else "🟠"
59
+ verb = "next" if o["fresh"] else "OVERDUE since"
60
+ out.append(f"- {mark} **Operations review** (`{o['id']}`): {verb} {o['review_by']}")
57
61
  if model["risks"]:
58
62
  open_r = [r for r in model["risks"] if r.get("disposition", "open") == "open"]
59
63
  rest = len(model["risks"]) - len(open_r)
@@ -176,6 +180,14 @@ def render_html(model) -> str:
176
180
  else:
177
181
  risks = "<p>None open.</p>"
178
182
 
183
+ ops_html = "".join(
184
+ f'<div class="sig"><div class="sig-h"><span>Operations review · '
185
+ f'<code>{esc(o["id"])}</code></span><span style="color:'
186
+ + ("var(--ok)" if o["fresh"] else "var(--amber)") + ';font-weight:600;">'
187
+ + ("next " if o["fresh"] else "overdue since ") + f'{esc(o["review_by"])}'
188
+ '</span></div></div>'
189
+ for o in model.get("operations", []))
190
+
179
191
  problems = ""
180
192
  if model["broken_references"]:
181
193
  problems += ("<section><h2>Broken references</h2><ul>"
@@ -287,7 +299,7 @@ def render_html(model) -> str:
287
299
  </header>
288
300
  <p class="meta">Derived from {c['total']} documents · {c['kinds']} kinds ·
289
301
  {c['approved']} approved{scope} · generated {gen}. Do not edit — regenerated from the documents.</p>
290
- <section><h2>Traceability coverage</h2>{coverage}</section>
302
+ <section><h2>Traceability coverage</h2>{coverage}{ops_html}</section>
291
303
  {document_set}
292
304
  {execution_html}
293
305
  {problems}
@@ -270,6 +270,35 @@ def check_risk_disposition_valid(doc: Document, ctx: dict) -> tuple[bool, str]:
270
270
  return True, f"All {total} risk disposition(s) valid (absent means open)."
271
271
 
272
272
 
273
+ def check_svc_items_complete(doc: Document, ctx: dict) -> tuple[bool, str]:
274
+ """Every SVC item states a Level and a Measure."""
275
+ incomplete, total = [], 0
276
+ for iid, text in _items_of_prefix(doc, ctx, "SVC"):
277
+ total += 1
278
+ missing = [f for f in ("Level", "Measure") if _field_value(text, f) is None]
279
+ if missing:
280
+ incomplete.append(f'{iid} missing {", ".join(missing)}')
281
+ if incomplete:
282
+ return False, "; ".join(incomplete)
283
+ return True, f"All {total} service(s) state a level and a measure."
284
+
285
+
286
+ def check_ops_review_fresh(doc: Document, ctx: dict) -> tuple[bool, str]:
287
+ """review_by is today or later (advisory; staleness ambers derived status)."""
288
+ import datetime
289
+ raw = doc.frontmatter.get("review_by")
290
+ if raw is None:
291
+ return False, "No review_by date."
292
+ try:
293
+ due = raw if isinstance(raw, datetime.date) else datetime.date.fromisoformat(str(raw))
294
+ except ValueError:
295
+ return False, f"review_by is not a date: {raw!r}"
296
+ today = datetime.date.today()
297
+ if due < today:
298
+ return False, f"operations review overdue since {due.isoformat()}"
299
+ return True, f"Next operations review {due.isoformat()}."
300
+
301
+
273
302
  def check_adr_items_have_status(doc: Document, ctx: dict) -> tuple[bool, str]:
274
303
  """Every ADR item declares a valid Status."""
275
304
  bad, total = [], 0
@@ -418,6 +447,8 @@ CHECKS: dict[str, Callable[[Document, dict], tuple[bool, str]]] = {
418
447
  "items-well-formed": check_items_well_formed,
419
448
  "risk-items-complete": check_risk_items_complete,
420
449
  "risk-disposition-valid": check_risk_disposition_valid,
450
+ "svc-items-complete": check_svc_items_complete,
451
+ "ops-review-fresh": check_ops_review_fresh,
421
452
  "adr-items-have-status": check_adr_items_have_status,
422
453
  "raci-one-accountable": check_raci_one_accountable,
423
454
  "story-format": check_story_format,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docassert
3
- Version: 0.15.0
3
+ Version: 0.16.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
@@ -33,6 +33,7 @@ docassert/_data/criteria/charter.criteria.yaml
33
33
  docassert/_data/criteria/data-migration-plan.criteria.yaml
34
34
  docassert/_data/criteria/frnfr.criteria.yaml
35
35
  docassert/_data/criteria/hypercare-plan.criteria.yaml
36
+ docassert/_data/criteria/operations.criteria.yaml
36
37
  docassert/_data/criteria/post-implementation-review.criteria.yaml
37
38
  docassert/_data/criteria/prd.criteria.yaml
38
39
  docassert/_data/criteria/project.criteria.yaml
@@ -47,6 +48,7 @@ docassert/_data/criteria/test-cases.criteria.yaml
47
48
  docassert/_data/criteria/user-story.criteria.yaml
48
49
  docassert/_data/profiles/agile-delivery.yaml
49
50
  docassert/_data/profiles/lean-startup.yaml
51
+ docassert/_data/profiles/operations.yaml
50
52
  docassert/_data/profiles/regulated-industry.yaml
51
53
  docassert/_data/schema/adr.schema.json
52
54
  docassert/_data/schema/benefits-realization.schema.json
@@ -56,6 +58,7 @@ docassert/_data/schema/charter.schema.json
56
58
  docassert/_data/schema/data-migration-plan.schema.json
57
59
  docassert/_data/schema/frnfr.schema.json
58
60
  docassert/_data/schema/hypercare-plan.schema.json
61
+ docassert/_data/schema/operations.schema.json
59
62
  docassert/_data/schema/post-implementation-review.schema.json
60
63
  docassert/_data/schema/prd.schema.json
61
64
  docassert/_data/schema/project.schema.json
@@ -77,6 +80,7 @@ docassert/_data/templates/charter.template.md
77
80
  docassert/_data/templates/data-migration-plan.template.md
78
81
  docassert/_data/templates/frnfr.template.md
79
82
  docassert/_data/templates/hypercare-plan.template.md
83
+ docassert/_data/templates/operations.template.md
80
84
  docassert/_data/templates/post-implementation-review.template.md
81
85
  docassert/_data/templates/prd.template.md
82
86
  docassert/_data/templates/project.template.md
@@ -60,8 +60,9 @@ def test_load_profile_and_unknown():
60
60
  assert P.load_profile("does-not-exist", PROFILES) is None
61
61
 
62
62
 
63
- def test_available_lists_the_three_profiles():
64
- assert set(P.available(PROFILES)) == {"regulated-industry", "lean-startup", "agile-delivery"}
63
+ def test_available_lists_the_shipped_profiles():
64
+ assert set(P.available(PROFILES)) == {"regulated-industry", "lean-startup",
65
+ "agile-delivery", "operations"}
65
66
 
66
67
 
67
68
  # ── integration with the real repo ──────────────────────────────────────────
@@ -214,3 +214,62 @@ def test_render_html_shows_disposition_column():
214
214
  _cd_root()
215
215
  out = S.render_html(S.build_status(ROOT / "documents"))
216
216
  assert "<th>Status</th>" in out and "open of" in out
217
+
218
+
219
+ # ── operations kind (ENG-PR-003) ─────────────────────────────────────────────
220
+ OPS_DOC = """---
221
+ kind: operations
222
+ id: XX-operations
223
+ project: PRJ-001-XX
224
+ title: Ops fixture
225
+ owner: ops.owner
226
+ status: approved
227
+ review_by: {due}
228
+ ---
229
+
230
+ ## Overview
231
+ x
232
+
233
+ ## Services
234
+
235
+ - **XX-SVC-001**: Triage. Level: within 14 days. Measure: monthly report.
236
+ """
237
+
238
+
239
+ def _ops_tree(tmp_path, due):
240
+ d = tmp_path / "documents" / "PRJ-001-XX"
241
+ d.mkdir(parents=True)
242
+ (d / "project.md").write_text(
243
+ "---\nkind: project\nid: PRJ-001-XX\ncode: XX\nname: Xy Ops\n"
244
+ "sponsor: sp.owner\nstatus: active\n---\n\n## Overview\nx\n\n## Scope\nx\n",
245
+ encoding="utf-8")
246
+ (d / "operations.md").write_text(OPS_DOC.format(due=due), encoding="utf-8")
247
+ return tmp_path / "documents"
248
+
249
+
250
+ def test_stale_operations_review_ambers_status(tmp_path):
251
+ _cd_root()
252
+ m = S.build_status(_ops_tree(tmp_path, "2020-01-01"), project="PRJ-001-XX")
253
+ assert m["operations"][0]["fresh"] is False
254
+ assert m["rag"] == "amber"
255
+ assert "OVERDUE" in S.render_markdown(m)
256
+
257
+
258
+ def test_fresh_operations_review_stays_green(tmp_path):
259
+ _cd_root()
260
+ m = S.build_status(_ops_tree(tmp_path, "2099-12-31"), project="PRJ-001-XX")
261
+ assert m["operations"][0]["fresh"] is True
262
+ assert m["rag"] == "green"
263
+ assert "Operations review" in S.render_html(m)
264
+
265
+
266
+ def test_svc_and_freshness_checks(tmp_path):
267
+ from docassert.loader import load
268
+ from docassert.structural import check_ops_review_fresh, check_svc_items_complete
269
+ docs = _ops_tree(tmp_path, "2020-01-01")
270
+ doc = load(docs / "PRJ-001-XX" / "operations.md")
271
+ ctx = {"item_sections": [{"section": "Services", "prefix": "SVC"}]}
272
+ ok, _ = check_svc_items_complete(doc, ctx)
273
+ assert ok
274
+ fresh_ok, detail = check_ops_review_fresh(doc, ctx)
275
+ assert not fresh_ok and "overdue" in detail
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes