docassert 0.14.0__tar.gz → 0.15.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.14.0/docassert.egg-info → docassert-0.15.0}/PKG-INFO +1 -1
- {docassert-0.14.0 → docassert-0.15.0}/docassert/__init__.py +1 -1
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/risk-register.criteria.yaml +5 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/bridge/board.py +23 -17
- {docassert-0.14.0 → docassert-0.15.0}/docassert/bridge/ops.py +29 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/status/derive.py +10 -3
- {docassert-0.14.0 → docassert-0.15.0}/docassert/status/render.py +16 -5
- {docassert-0.14.0 → docassert-0.15.0}/docassert/structural.py +16 -0
- {docassert-0.14.0 → docassert-0.15.0/docassert.egg-info}/PKG-INFO +1 -1
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_bridge.py +29 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_status.py +52 -0
- {docassert-0.14.0 → docassert-0.15.0}/LICENSE +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/NOTICE +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/README.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/__main__.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/consistency.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/adr.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/brd.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/business-case.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/charter.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/frnfr.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/prd.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/project.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/risk-register.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/runbook.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/status-report.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/test-cases.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/user-story.schema.json +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/skills/doc-to-pmo/SKILL.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/adr.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/benefits-realization.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/brd.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/business-case.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/charter.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/data-migration-plan.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/frnfr.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/hypercare-plan.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/post-implementation-review.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/prd.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/project.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/qa-test-plan.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/risk-register.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/rollback-plan.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/runbook.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/status-report.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/test-cases.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/user-story.template.md +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/bridge/__init__.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/bridge/gh.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/bridge/plan.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/cli.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/config.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/consistency.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/extract.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/graph.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/loader.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/models.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/profiles.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/projects.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/report.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/rtm.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/scaffold.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/semantic.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert/status/__init__.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert.egg-info/SOURCES.txt +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert.egg-info/dependency_links.txt +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert.egg-info/entry_points.txt +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert.egg-info/requires.txt +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/docassert.egg-info/top_level.txt +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/pyproject.toml +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/setup.cfg +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_badge.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_config.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_consistency.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_defects.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_draft_relaxation.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_extract.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_graph.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_json_report.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_kinds_delivery.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_kinds_governance.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_kinds_operate.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_kinds_reporting.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_profiles.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_projects.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_scaffold.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_semantic.py +0 -0
- {docassert-0.14.0 → docassert-0.15.0}/tests/test_structural.py +0 -0
|
@@ -30,6 +30,11 @@ checks:
|
|
|
30
30
|
type: structural
|
|
31
31
|
blocking: once-proposed
|
|
32
32
|
description: Every risk states a Probability, Impact, Owner, and Response.
|
|
33
|
+
- id: risk-disposition-valid
|
|
34
|
+
type: structural
|
|
35
|
+
blocking: true
|
|
36
|
+
description: Any Status field on a RISK item names a valid disposition (open/mitigated/accepted/closed).
|
|
37
|
+
|
|
33
38
|
- id: unique-id
|
|
34
39
|
type: structural
|
|
35
40
|
blocking: true
|
|
@@ -7,7 +7,7 @@ writes converge on the same values.
|
|
|
7
7
|
"""
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
from .gh import GhRunner
|
|
10
|
+
from .gh import GhError, GhRunner
|
|
11
11
|
|
|
12
12
|
FIELD_SPECS: dict[str, dict] = {
|
|
13
13
|
"Type": {"dataType": "SINGLE_SELECT", "options": ["Feature", "Story"]},
|
|
@@ -67,22 +67,28 @@ def ensure_fields(gh: GhRunner, owner: str, number: int) -> dict:
|
|
|
67
67
|
for name, spec in FIELD_SPECS.items():
|
|
68
68
|
if name in project["fields"]:
|
|
69
69
|
continue
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
70
|
+
try:
|
|
71
|
+
if spec["dataType"] == "SINGLE_SELECT":
|
|
72
|
+
opts = ", ".join(
|
|
73
|
+
f'{{name: "{o}", color: GRAY, description: ""}}'
|
|
74
|
+
for o in spec["options"])
|
|
75
|
+
gh.graphql(
|
|
76
|
+
"mutation($p: ID!, $n: String!) { createProjectV2Field(input: {"
|
|
77
|
+
"projectId: $p, dataType: SINGLE_SELECT, name: $n, "
|
|
78
|
+
f"singleSelectOptions: [{opts}]"
|
|
79
|
+
"}) { projectV2Field { ... on ProjectV2FieldCommon { id } } } }",
|
|
80
|
+
p=project["id"], n=name)
|
|
81
|
+
else:
|
|
82
|
+
gh.graphql(
|
|
83
|
+
"mutation($p: ID!, $n: String!) { createProjectV2Field(input: {"
|
|
84
|
+
"projectId: $p, dataType: TEXT, name: $n"
|
|
85
|
+
"}) { projectV2Field { ... on ProjectV2FieldCommon { id } } } }",
|
|
86
|
+
p=project["id"], n=name)
|
|
87
|
+
except GhError as exc:
|
|
88
|
+
# A concurrent sync created it between our read and our write;
|
|
89
|
+
# an existing field is the outcome we wanted.
|
|
90
|
+
if "already been taken" not in str(exc).lower():
|
|
91
|
+
raise
|
|
86
92
|
return get_project(gh, owner, number)
|
|
87
93
|
|
|
88
94
|
|
|
@@ -45,6 +45,33 @@ def _index(issues: list[dict]) -> dict[str, dict]:
|
|
|
45
45
|
return out
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def _converge_duplicates(gh, repo: str, issues: list[dict]) -> tuple[list[dict], list[str]]:
|
|
49
|
+
"""One open issue per bridge marker: racing scaffolds may have created
|
|
50
|
+
duplicates, so close every open duplicate except the lowest-numbered one
|
|
51
|
+
and drop the closed ones from the working set. First writer wins."""
|
|
52
|
+
from collections import defaultdict
|
|
53
|
+
by_marker: dict[str, list[dict]] = defaultdict(list)
|
|
54
|
+
for issue in issues:
|
|
55
|
+
m = MARKER_RE.search(issue.get("body") or "")
|
|
56
|
+
if m and issue.get("state") == "open":
|
|
57
|
+
by_marker[m.group(1)].append(issue)
|
|
58
|
+
actions: list[str] = []
|
|
59
|
+
losers: set[int] = set()
|
|
60
|
+
for marker, dupes in by_marker.items():
|
|
61
|
+
if len(dupes) < 2:
|
|
62
|
+
continue
|
|
63
|
+
dupes.sort(key=lambda i: i["number"])
|
|
64
|
+
for extra in dupes[1:]:
|
|
65
|
+
G.update_issue(gh, repo, extra["number"], state="closed")
|
|
66
|
+
G.comment(gh, repo, extra["number"],
|
|
67
|
+
f"Duplicate of #{dupes[0]['number']} (bridge marker "
|
|
68
|
+
f"{marker}); racing scaffold runs converge on the "
|
|
69
|
+
"lowest-numbered issue.")
|
|
70
|
+
losers.add(extra["number"])
|
|
71
|
+
actions.append(f"converged duplicate #{extra['number']} -> #{dupes[0]['number']}")
|
|
72
|
+
return [i for i in issues if i["number"] not in losers], actions
|
|
73
|
+
|
|
74
|
+
|
|
48
75
|
def scaffold(plan: BridgePlan, gh: G.GhRunner, repo: str,
|
|
49
76
|
docs_url: str | None = None,
|
|
50
77
|
board_cfg: dict | None = None) -> list[str]:
|
|
@@ -59,6 +86,8 @@ def scaffold(plan: BridgePlan, gh: G.GhRunner, repo: str,
|
|
|
59
86
|
G.ensure_label(gh, repo, name, color, desc)
|
|
60
87
|
|
|
61
88
|
issues = G.list_issues(gh, repo)
|
|
89
|
+
issues, converge_actions = _converge_duplicates(gh, repo, issues)
|
|
90
|
+
actions.extend(converge_actions)
|
|
62
91
|
by_key = _index(issues)
|
|
63
92
|
|
|
64
93
|
def ensure(key: str, title: str, body: str, label: str) -> dict:
|
|
@@ -82,6 +82,9 @@ def _risks(graph, code=None):
|
|
|
82
82
|
description = (item.text[:m.start()] if m else item.text).strip()
|
|
83
83
|
rm = re.search(r"Response\s*:\s*(.+)$", item.text,
|
|
84
84
|
re.IGNORECASE | re.DOTALL)
|
|
85
|
+
raw = (_field_value(item.text, "status") or "open").lower()
|
|
86
|
+
disposition = raw if raw in {"open", "mitigated", "accepted",
|
|
87
|
+
"closed"} else "open"
|
|
85
88
|
risks.append({
|
|
86
89
|
"id": item.id,
|
|
87
90
|
"description": description,
|
|
@@ -90,9 +93,12 @@ def _risks(graph, code=None):
|
|
|
90
93
|
"impact": impact or "?",
|
|
91
94
|
"owner": _field_value(item.text, "owner") or "?",
|
|
92
95
|
"response": rm.group(1).strip() if rm else "",
|
|
96
|
+
"disposition": disposition,
|
|
93
97
|
"score": _SEVERITY.get(prob, 0) * _SEVERITY.get(impact, 0),
|
|
94
98
|
})
|
|
95
|
-
|
|
99
|
+
# open risks first, then by severity; dispositioned risks stay as record
|
|
100
|
+
return sorted(risks, key=lambda r: (r["disposition"] != "open",
|
|
101
|
+
-r["score"]))
|
|
96
102
|
|
|
97
103
|
|
|
98
104
|
def _broken_references(graph, code=None):
|
|
@@ -218,7 +224,7 @@ def build_index(documents_dir=DOCUMENTS_DIR) -> dict:
|
|
|
218
224
|
"rag": m["rag"],
|
|
219
225
|
"total": m["counts"]["total"],
|
|
220
226
|
"failing": m["counts"]["failing"],
|
|
221
|
-
"risks": len(m["risks"]),
|
|
227
|
+
"risks": len([r for r in m["risks"] if r.get("disposition", "open") == "open"]),
|
|
222
228
|
"coverage_gaps": sum(len(c["gaps"]) for c in m["coverage"]),
|
|
223
229
|
"broken": len(m["broken_references"]),
|
|
224
230
|
"completeness": m.get("completeness"),
|
|
@@ -238,7 +244,8 @@ def derive_rag(model) -> str:
|
|
|
238
244
|
reported = (model["latest_report"] or {}).get("rag")
|
|
239
245
|
completeness_gap = bool(comp and (comp["missing_required"] or comp["incomplete_required"]
|
|
240
246
|
or comp["recommended_gaps"] or comp.get("unknown")))
|
|
241
|
-
|
|
247
|
+
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:
|
|
242
249
|
return "amber"
|
|
243
250
|
return "green"
|
|
244
251
|
|
|
@@ -55,8 +55,12 @@ def render_markdown(model, summary: bool = False) -> str:
|
|
|
55
55
|
out.append(f"- 🔴 **Documents failing audit**: {', '.join(failing)}")
|
|
56
56
|
|
|
57
57
|
if model["risks"]:
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
open_r = [r for r in model["risks"] if r.get("disposition", "open") == "open"]
|
|
59
|
+
rest = len(model["risks"]) - len(open_r)
|
|
60
|
+
mark = "🟠" if open_r else "🟢"
|
|
61
|
+
out.append(f"- {mark} **Open risks**: {len(open_r)}"
|
|
62
|
+
+ (f" ({rest} dispositioned)" if rest else ""))
|
|
63
|
+
for r in open_r[:5]:
|
|
60
64
|
out.append(f" - `{r['id']}` — {r['probability']}/{r['impact']} · owner: {r['owner']}")
|
|
61
65
|
|
|
62
66
|
lr = model["latest_report"]
|
|
@@ -149,6 +153,12 @@ def render_html(model) -> str:
|
|
|
149
153
|
value, "var(--muted)")
|
|
150
154
|
return f'<td style="color:{color};font-weight:600;">{esc(value)}</td>'
|
|
151
155
|
|
|
156
|
+
def disp(value):
|
|
157
|
+
color = {"open": "var(--amber)", "mitigated": "var(--ok)",
|
|
158
|
+
"accepted": "var(--muted)", "closed": "var(--muted)"}.get(
|
|
159
|
+
value, "var(--amber)")
|
|
160
|
+
return f'<td style="color:{color};font-weight:600;">{esc(value)}</td>'
|
|
161
|
+
|
|
152
162
|
if model["risks"]:
|
|
153
163
|
risk_rows = "".join(
|
|
154
164
|
f'<tr><td><code>{esc(r["id"])}</code></td>'
|
|
@@ -156,11 +166,12 @@ def render_html(model) -> str:
|
|
|
156
166
|
f'<td>{", ".join(f"<code>{esc(b)}</code>" for b in r["threatens"]) or "—"}</td>'
|
|
157
167
|
+ sev(r["probability"]) + sev(r["impact"])
|
|
158
168
|
+ f'<td>{esc(r["owner"])}</td>'
|
|
159
|
-
|
|
169
|
+
+ disp(r.get("disposition", "open"))
|
|
170
|
+
+ f'<td>{esc(r["response"]) or "—"}</td></tr>'
|
|
160
171
|
for r in model["risks"])
|
|
161
172
|
risks = ('<table><thead><tr><th>ID</th><th>Risk</th><th>Threatens</th>'
|
|
162
173
|
'<th>Probability</th><th>Impact</th><th>Owner</th>'
|
|
163
|
-
'<th>Response</th></tr></thead>'
|
|
174
|
+
'<th>Status</th><th>Response</th></tr></thead>'
|
|
164
175
|
f'<tbody>{risk_rows}</tbody></table>')
|
|
165
176
|
else:
|
|
166
177
|
risks = "<p>None open.</p>"
|
|
@@ -280,7 +291,7 @@ def render_html(model) -> str:
|
|
|
280
291
|
{document_set}
|
|
281
292
|
{execution_html}
|
|
282
293
|
{problems}
|
|
283
|
-
<section><h2>
|
|
294
|
+
<section><h2>Risks ({len([r for r in model['risks'] if r.get('disposition', 'open') == 'open'])} open of {len(model['risks'])})</h2>{risks}</section>
|
|
284
295
|
<section><h2>Latest status report</h2><p>{report}</p></section>
|
|
285
296
|
<section><h2>Documents ({c['total']})</h2>
|
|
286
297
|
<table><thead><tr><th>Kind</th><th>ID</th><th>Status</th><th>Audit</th></tr></thead>
|
|
@@ -226,6 +226,7 @@ def check_project_id_format(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
|
226
226
|
|
|
227
227
|
|
|
228
228
|
_RISK_FIELDS = ["Probability", "Impact", "Owner", "Response"]
|
|
229
|
+
_DISPOSITIONS = {"open", "mitigated", "accepted", "closed"}
|
|
229
230
|
_ADR_STATES = {"proposed", "accepted", "superseded", "deprecated", "rejected"}
|
|
230
231
|
|
|
231
232
|
|
|
@@ -255,6 +256,20 @@ def check_risk_items_complete(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
|
255
256
|
return True, f"All {total} risk(s) state probability, impact, owner, and response."
|
|
256
257
|
|
|
257
258
|
|
|
259
|
+
def check_risk_disposition_valid(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
260
|
+
"""Where a RISK item yields Status, it names a valid disposition."""
|
|
261
|
+
bad, total = [], 0
|
|
262
|
+
for iid, text in _items_of_prefix(doc, ctx, "RISK"):
|
|
263
|
+
total += 1
|
|
264
|
+
value = _field_value(text, "Status")
|
|
265
|
+
if value is not None and value.lower() not in _DISPOSITIONS:
|
|
266
|
+
bad.append(f"{iid}: {value!r}")
|
|
267
|
+
if bad:
|
|
268
|
+
return False, ("invalid disposition (open|mitigated|accepted|closed): "
|
|
269
|
+
+ "; ".join(bad))
|
|
270
|
+
return True, f"All {total} risk disposition(s) valid (absent means open)."
|
|
271
|
+
|
|
272
|
+
|
|
258
273
|
def check_adr_items_have_status(doc: Document, ctx: dict) -> tuple[bool, str]:
|
|
259
274
|
"""Every ADR item declares a valid Status."""
|
|
260
275
|
bad, total = [], 0
|
|
@@ -402,6 +417,7 @@ CHECKS: dict[str, Callable[[Document, dict], tuple[bool, str]]] = {
|
|
|
402
417
|
"unique-id": check_unique_id,
|
|
403
418
|
"items-well-formed": check_items_well_formed,
|
|
404
419
|
"risk-items-complete": check_risk_items_complete,
|
|
420
|
+
"risk-disposition-valid": check_risk_disposition_valid,
|
|
405
421
|
"adr-items-have-status": check_adr_items_have_status,
|
|
406
422
|
"raci-one-accountable": check_raci_one_accountable,
|
|
407
423
|
"story-format": check_story_format,
|
|
@@ -441,3 +441,32 @@ def test_anchor_without_repo_keeps_single_repo_mode_working(tmp_path):
|
|
|
441
441
|
gh = FakeGh()
|
|
442
442
|
ops.scaffold(plan, gh, "o/r", docs_url=None)
|
|
443
443
|
assert any("issues" in " ".join(c) for c in gh.calls)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
# ── concurrency safety (ENG-PR-006) ──────────────────────────────────────────
|
|
447
|
+
def test_scaffold_converges_racing_duplicates(tmp_path):
|
|
448
|
+
plan = build_bridge_plan(_tree(tmp_path))
|
|
449
|
+
fid = plan.features[0].id
|
|
450
|
+
dupes = [_mk_issue(5, fid), _mk_issue(9, fid)]
|
|
451
|
+
gh = FakeGh(issues=dupes)
|
|
452
|
+
actions = ops.scaffold(plan, gh, "o/r", docs_url=None)
|
|
453
|
+
assert any("converged duplicate #9" in a for a in actions)
|
|
454
|
+
patches = [c for c in gh.calls if "repos/o/r/issues/9" in " ".join(c)
|
|
455
|
+
and "state=closed" in " ".join(c)]
|
|
456
|
+
assert patches, "the higher-numbered duplicate is closed"
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def test_ensure_fields_tolerates_existing_field():
|
|
460
|
+
gh = BoardGh()
|
|
461
|
+
original = gh.graphql
|
|
462
|
+
|
|
463
|
+
def racing(query, **kw):
|
|
464
|
+
if "createProjectV2Field" in query:
|
|
465
|
+
from docassert.bridge.gh import GhError
|
|
466
|
+
raise GhError('[{"message": "Name has already been taken"}]')
|
|
467
|
+
return original(query, **kw)
|
|
468
|
+
|
|
469
|
+
gh.graphql = racing
|
|
470
|
+
from docassert.bridge import board
|
|
471
|
+
project = board.ensure_fields(gh, "o", 1) # must not raise
|
|
472
|
+
assert project["id"]
|
|
@@ -162,3 +162,55 @@ def test_render_html_risk_table():
|
|
|
162
162
|
# severity cells are colored
|
|
163
163
|
assert 'style="color:var(--bad);font-weight:600;">high</td>' in out \
|
|
164
164
|
or 'style="color:var(--amber);font-weight:600;">medium</td>' in out
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ── risk disposition (ENG-PR-002) ────────────────────────────────────────────
|
|
168
|
+
def _risk_graph(*status_clauses):
|
|
169
|
+
from docassert.graph import Graph
|
|
170
|
+
from docassert.models import Item
|
|
171
|
+
g = Graph()
|
|
172
|
+
for i, clause in enumerate(status_clauses, 1):
|
|
173
|
+
text = (f"Risk {i} description. Probability: low. Impact: high. "
|
|
174
|
+
f"Owner: o.{i}.{clause} Response: watch.")
|
|
175
|
+
g.add(Item(f"XX-RISK-{i:03d}", "XX", "RISK", text, {}, "d.md",
|
|
176
|
+
"risk-register", "approved", "S"))
|
|
177
|
+
return g
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_disposition_parsed_and_defaulted():
|
|
181
|
+
from docassert.status.derive import _risks
|
|
182
|
+
g = _risk_graph(" Status: Mitigated.", " Status: accepted.", "")
|
|
183
|
+
rs = _risks(g)
|
|
184
|
+
assert [r["disposition"] for r in rs] == ["open", "mitigated", "accepted"]
|
|
185
|
+
assert rs[0]["id"] == "XX-RISK-003" # open risks sort first
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_rag_green_when_all_risks_dispositioned():
|
|
189
|
+
base = {"documents": [], "coverage": [], "broken_references": [],
|
|
190
|
+
"latest_report": None, "completeness": None}
|
|
191
|
+
open_r = dict(base, risks=[{"id": "R", "score": 2, "disposition": "open"}])
|
|
192
|
+
done_r = dict(base, risks=[{"id": "R", "score": 2, "disposition": "accepted"}])
|
|
193
|
+
assert S.derive_rag(open_r) == "amber"
|
|
194
|
+
assert S.derive_rag(done_r) == "green"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_disposition_check_blocks_invalid(tmp_path):
|
|
198
|
+
from docassert.loader import load
|
|
199
|
+
from docassert.structural import check_risk_disposition_valid
|
|
200
|
+
p = tmp_path / "r.md"
|
|
201
|
+
p.write_text(
|
|
202
|
+
"---\nkind: risk-register\nid: XX-risk-register\n"
|
|
203
|
+
"project: PRJ-001-XX\ntitle: T\nowner: o\nstatus: approved\n---\n\n"
|
|
204
|
+
"## Overview\nx\n\n## Risks\n\n"
|
|
205
|
+
"- **XX-RISK-001**: Bad. Probability: low. Impact: low. Owner: o. "
|
|
206
|
+
"Status: wontfix. Response: r.\n", encoding="utf-8")
|
|
207
|
+
doc = load(p)
|
|
208
|
+
ok, detail = check_risk_disposition_valid(
|
|
209
|
+
doc, {"item_sections": [{"section": "Risks", "prefix": "RISK"}]})
|
|
210
|
+
assert not ok and "wontfix" in detail
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_render_html_shows_disposition_column():
|
|
214
|
+
_cd_root()
|
|
215
|
+
out = S.render_html(S.build_status(ROOT / "documents"))
|
|
216
|
+
assert "<th>Status</th>" in out and "open of" in out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/benefits-realization.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml
RENAMED
|
File without changes
|
{docassert-0.14.0 → docassert-0.15.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/benefits-realization.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/data-migration-plan.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/post-implementation-review.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.0 → docassert-0.15.0}/docassert/_data/schema/release-cutover-plan.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/benefits-realization.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/data-migration-plan.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/raci-stakeholder.template.md
RENAMED
|
File without changes
|
{docassert-0.14.0 → docassert-0.15.0}/docassert/_data/templates/release-cutover-plan.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|