docassert 0.18.0__tar.gz → 0.20.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.18.0/docassert.egg-info → docassert-0.20.0}/PKG-INFO +1 -1
- {docassert-0.18.0 → docassert-0.20.0}/docassert/__init__.py +1 -1
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/consistency.yaml +6 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/consistency.py +32 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/status/derive.py +69 -7
- {docassert-0.18.0 → docassert-0.20.0}/docassert/status/render.py +53 -61
- {docassert-0.18.0 → docassert-0.20.0/docassert.egg-info}/PKG-INFO +1 -1
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_status.py +95 -3
- {docassert-0.18.0 → docassert-0.20.0}/LICENSE +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/NOTICE +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/README.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/__main__.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/operations.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/profiles/operations.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/adr.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/brd.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/business-case.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/charter.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/frnfr.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/operations.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/prd.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/project.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/risk-register.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/runbook.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/status-report.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/test-cases.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/user-story.schema.json +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/skills/doc-to-pmo/SKILL.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/adr.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/benefits-realization.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/brd.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/business-case.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/charter.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/data-migration-plan.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/frnfr.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/hypercare-plan.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/operations.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/post-implementation-review.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/prd.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/project.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/qa-test-plan.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/risk-register.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/rollback-plan.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/runbook.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/status-report.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/test-cases.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/user-story.template.md +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/bridge/__init__.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/bridge/board.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/bridge/gh.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/bridge/ops.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/bridge/plan.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/cli.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/config.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/extract.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/graph.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/loader.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/models.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/profiles.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/projects.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/report.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/rtm.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/scaffold.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/semantic.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/status/__init__.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert/structural.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert.egg-info/SOURCES.txt +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert.egg-info/dependency_links.txt +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert.egg-info/entry_points.txt +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert.egg-info/requires.txt +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/docassert.egg-info/top_level.txt +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/pyproject.toml +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/setup.cfg +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_badge.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_bridge.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_config.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_consistency.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_defects.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_draft_relaxation.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_extract.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_graph.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_json_report.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_kinds_delivery.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_kinds_governance.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_kinds_operate.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_kinds_reporting.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_profiles.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_projects.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_report_extract.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_scaffold.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_semantic.py +0 -0
- {docassert-0.18.0 → docassert-0.20.0}/tests/test_structural.py +0 -0
|
@@ -39,6 +39,12 @@ coverage:
|
|
|
39
39
|
# (0 = no cap).
|
|
40
40
|
alignment_limit: 25
|
|
41
41
|
|
|
42
|
+
# Open risks move derived status to amber only at or above this
|
|
43
|
+
# probability x impact score (low=1 medium=2 high/critical=3). 0 = any open
|
|
44
|
+
# risk ambers (the strict pre-0.19 behavior). Exposure below the threshold
|
|
45
|
+
# stays fully reported; it just doesn't flatten the headline.
|
|
46
|
+
risk_amber_score: 6
|
|
47
|
+
|
|
42
48
|
# Rules match in order; the first matching rule grades a link. Typed rules
|
|
43
49
|
# (child_type/parent_type) come before the generic ones.
|
|
44
50
|
alignment:
|
|
@@ -49,6 +49,37 @@ def check_unique_item_ids(graph) -> CheckResult:
|
|
|
49
49
|
f"Duplicate item IDs: {detail}")
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
def check_sequence_acyclic(graph) -> CheckResult:
|
|
53
|
+
"""The `after` digraph contains no cycle (a cycle is broken sequencing)."""
|
|
54
|
+
edges: dict[str, list[str]] = {}
|
|
55
|
+
for item in graph.all_items():
|
|
56
|
+
for target in item.links.get("after", []):
|
|
57
|
+
edges.setdefault(item.id, []).append(target)
|
|
58
|
+
state: dict[str, int] = {} # 0 visiting, 1 done
|
|
59
|
+
|
|
60
|
+
def visit(node, path):
|
|
61
|
+
if state.get(node) == 1:
|
|
62
|
+
return None
|
|
63
|
+
if state.get(node) == 0:
|
|
64
|
+
return path[path.index(node):] + [node]
|
|
65
|
+
state[node] = 0
|
|
66
|
+
for nxt in edges.get(node, []):
|
|
67
|
+
cycle = visit(nxt, path + [node])
|
|
68
|
+
if cycle:
|
|
69
|
+
return cycle
|
|
70
|
+
state[node] = 1
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
for start in list(edges):
|
|
74
|
+
cycle = visit(start, [])
|
|
75
|
+
if cycle:
|
|
76
|
+
return CheckResult("sequence-acyclic", False, True,
|
|
77
|
+
"after-cycle: " + " → ".join(cycle))
|
|
78
|
+
n = sum(len(v) for v in edges.values())
|
|
79
|
+
return CheckResult("sequence-acyclic", True, True,
|
|
80
|
+
f"{n} after link(s), no cycles.")
|
|
81
|
+
|
|
82
|
+
|
|
52
83
|
def check_referential_integrity(graph) -> CheckResult:
|
|
53
84
|
broken = []
|
|
54
85
|
for item in graph.all_items():
|
|
@@ -208,6 +239,7 @@ def run_consistency(documents_dir: str | Path = "documents",
|
|
|
208
239
|
results = [
|
|
209
240
|
check_unique_item_ids(graph),
|
|
210
241
|
check_referential_integrity(graph),
|
|
242
|
+
check_sequence_acyclic(graph),
|
|
211
243
|
check_required_links(graph, cfg),
|
|
212
244
|
check_coverage(graph, cfg),
|
|
213
245
|
check_profile_completeness(documents_dir),
|
|
@@ -116,6 +116,49 @@ def _broken_references(graph, code=None):
|
|
|
116
116
|
_MS_RE = re.compile(r"^(?P<label>.+?)[:\u2014\u2013-]\s*(?P<date>\d{4}-\d{2}-\d{2})\.?\s*$")
|
|
117
117
|
|
|
118
118
|
|
|
119
|
+
_TSHIRT = [(1, "XS"), (2, "S"), (4, "M"), (7, "L")]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _tshirt(points: int) -> str:
|
|
123
|
+
for cap, label in _TSHIRT:
|
|
124
|
+
if points <= cap:
|
|
125
|
+
return label
|
|
126
|
+
return "XL"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _features(graph, code):
|
|
130
|
+
"""Every product requirement as a chartable feature: scope points =
|
|
131
|
+
traced stories + verifying acceptance criteria (document arithmetic, no
|
|
132
|
+
estimation), plus `after` sequencing resolved into topological layers."""
|
|
133
|
+
prs = [i for i in graph.by_type.get("PR", []) if not code or i.project == code]
|
|
134
|
+
feats = {}
|
|
135
|
+
for pr in prs:
|
|
136
|
+
stories = graph.children(pr.id, "traces", "US")
|
|
137
|
+
acs = graph.children(pr.id, "verifies", "AC")
|
|
138
|
+
feats[pr.id] = {
|
|
139
|
+
"id": pr.id,
|
|
140
|
+
"text": pr.text.split(". ")[0][:80],
|
|
141
|
+
"stories": len(stories),
|
|
142
|
+
"acs": len(acs),
|
|
143
|
+
"points": len(stories) + len(acs),
|
|
144
|
+
"after": [t for t in pr.links.get("after", []) if any(
|
|
145
|
+
p.id == t for p in prs)],
|
|
146
|
+
"layer": 0,
|
|
147
|
+
}
|
|
148
|
+
# Kahn layering; a cycle (gated by sequence-acyclic) degrades to layer 0
|
|
149
|
+
for _ in range(len(feats)):
|
|
150
|
+
changed = False
|
|
151
|
+
for f in feats.values():
|
|
152
|
+
want = max((feats[d]["layer"] + 1 for d in f["after"]
|
|
153
|
+
if d in feats), default=0)
|
|
154
|
+
if want > f["layer"]:
|
|
155
|
+
f["layer"] = want
|
|
156
|
+
changed = True
|
|
157
|
+
if not changed:
|
|
158
|
+
break
|
|
159
|
+
return sorted(feats.values(), key=lambda f: (f["layer"], -f["points"], f["id"]))
|
|
160
|
+
|
|
161
|
+
|
|
119
162
|
def _milestones(docs):
|
|
120
163
|
"""Dated charter milestones as temporal facts (never completion claims)."""
|
|
121
164
|
import datetime
|
|
@@ -135,15 +178,29 @@ def _milestones(docs):
|
|
|
135
178
|
due = datetime.date.fromisoformat(m.group("date"))
|
|
136
179
|
except ValueError:
|
|
137
180
|
continue
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
181
|
+
out.append(_ms_entry(m.group("label").strip(), due, today))
|
|
182
|
+
for d in docs:
|
|
183
|
+
if d.kind != "charter":
|
|
184
|
+
continue
|
|
185
|
+
target = (d.frontmatter.get("dates") or {}).get("target")
|
|
186
|
+
if target:
|
|
187
|
+
try:
|
|
188
|
+
due = (target if isinstance(target, datetime.date)
|
|
189
|
+
else datetime.date.fromisoformat(str(target)))
|
|
190
|
+
if not any(x["date"] == due.isoformat() for x in out):
|
|
191
|
+
out.append(_ms_entry("Charter target", due, today))
|
|
192
|
+
except (TypeError, ValueError):
|
|
193
|
+
pass
|
|
144
194
|
return sorted(out, key=lambda x: x["date"])
|
|
145
195
|
|
|
146
196
|
|
|
197
|
+
def _ms_entry(label, due, today):
|
|
198
|
+
delta = (due - today).days
|
|
199
|
+
return {"label": label, "date": due.isoformat(), "days": delta,
|
|
200
|
+
"when": ("today" if delta == 0 else
|
|
201
|
+
"elapsed" if delta < 0 else "upcoming")}
|
|
202
|
+
|
|
203
|
+
|
|
147
204
|
def _operations(docs):
|
|
148
205
|
"""Operations documents with their review state."""
|
|
149
206
|
import datetime
|
|
@@ -231,9 +288,11 @@ def build_status(documents_dir=DOCUMENTS_DIR, project: str | None = None) -> dic
|
|
|
231
288
|
"risks": _risks(graph, code),
|
|
232
289
|
"operations": _operations(docs),
|
|
233
290
|
"milestones": _milestones(docs),
|
|
291
|
+
"features": _features(graph, code),
|
|
234
292
|
"broken_references": _broken_references(graph, code),
|
|
235
293
|
"latest_report": _latest_report(docs),
|
|
236
294
|
"completeness": completeness,
|
|
295
|
+
"risk_amber_score": int(cfg.get("risk_amber_score", 6) or 0),
|
|
237
296
|
}
|
|
238
297
|
model["rag"] = derive_rag(model)
|
|
239
298
|
return model
|
|
@@ -313,7 +372,10 @@ def derive_rag(model) -> str:
|
|
|
313
372
|
reported = (model["latest_report"] or {}).get("rag")
|
|
314
373
|
completeness_gap = bool(comp and (comp["missing_required"] or comp["incomplete_required"]
|
|
315
374
|
or comp["recommended_gaps"] or comp.get("unknown")))
|
|
316
|
-
|
|
375
|
+
threshold = model.get("risk_amber_score", 6)
|
|
376
|
+
open_risks = [r for r in model["risks"]
|
|
377
|
+
if r.get("disposition", "open") == "open"
|
|
378
|
+
and (threshold == 0 or r.get("score", 0) >= threshold)]
|
|
317
379
|
stale_ops = [o for o in model.get("operations", []) if not o["fresh"]]
|
|
318
380
|
if (coverage_gap or open_risks or stale_ops
|
|
319
381
|
or reported in {"amber", "red"} or completeness_gap):
|
|
@@ -362,77 +362,62 @@ def render_html(model) -> str:
|
|
|
362
362
|
ex = model.get("execution") or {}
|
|
363
363
|
lanes_html = ""
|
|
364
364
|
feats = ex.get("features", [])
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
w0 = bounds[0]
|
|
377
|
-
span = max((bounds[1] - w0).days + 7, 14)
|
|
378
|
-
# month gridlines
|
|
379
|
-
months = []
|
|
380
|
-
cur = datetime.date(w0.year, w0.month, 1)
|
|
381
|
-
while cur <= bounds[1]:
|
|
382
|
-
if cur >= w0:
|
|
383
|
-
months.append(cur)
|
|
384
|
-
cur = datetime.date(cur.year + (cur.month == 12), (cur.month % 12) + 1, 1)
|
|
385
|
-
month_html = "".join(
|
|
386
|
-
f'<span style="position:absolute;left:{_pct(m.isoformat(), w0, span):.2f}%;top:0;'
|
|
387
|
-
f'font-size:9px;letter-spacing:.14em;color:{_C["dim"]};padding-left:7px;">'
|
|
388
|
-
f'{m.strftime("%b").upper()}</span>' for m in months)
|
|
389
|
-
grid = ", ".join(
|
|
390
|
-
f'transparent {_pct(m.isoformat(), w0, span):.2f}%, #191d21 {_pct(m.isoformat(), w0, span):.2f}%, '
|
|
391
|
-
f'#191d21 calc({_pct(m.isoformat(), w0, span):.2f}% + 1px), transparent calc({_pct(m.isoformat(), w0, span):.2f}% + 1px)'
|
|
392
|
-
for m in months) or "transparent 0%"
|
|
365
|
+
ex_state = {}
|
|
366
|
+
for f in feats:
|
|
367
|
+
facts = f.get("facts") or {}
|
|
368
|
+
ex_state[f["id"]] = ("blocked" if "blocked" in (facts.get("labels") or [])
|
|
369
|
+
else "done" if f.get("closed") else "open")
|
|
370
|
+
chart = model.get("features") or []
|
|
371
|
+
if chart:
|
|
372
|
+
maxp = max(f["points"] for f in chart) or 1
|
|
373
|
+
maxlayer = max(f["layer"] for f in chart)
|
|
374
|
+
ncols = maxlayer + 1
|
|
375
|
+
colw = 100.0 / ncols
|
|
393
376
|
fills = {"done": f'background:rgba(63,185,80,.78);color:{_C["bg"]};',
|
|
394
377
|
"open": f'background:rgba(255,178,36,.85);color:{_C["bg"]};',
|
|
395
|
-
"blocked": f'background:rgba(240,78,78,.82);color:{_C["bg"]};'
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
378
|
+
"blocked": f'background:rgba(240,78,78,.82);color:{_C["bg"]};',
|
|
379
|
+
"scoped": 'background:rgba(255,255,255,.05);color:#9BA1A8;border:1px dashed #3a4046;'}
|
|
380
|
+
headers = "".join(
|
|
381
|
+
f'<span style="position:absolute;left:{i * colw:.2f}%;top:0;font-size:9px;'
|
|
382
|
+
f'letter-spacing:.14em;color:{_C["dim"]};padding-left:7px;">SEQ {i + 1}</span>'
|
|
383
|
+
for i in range(ncols))
|
|
384
|
+
gridlines = ", ".join(
|
|
385
|
+
f'transparent {i * colw:.2f}%, #191d21 {i * colw:.2f}%, '
|
|
386
|
+
f'#191d21 calc({i * colw:.2f}% + 1px), transparent calc({i * colw:.2f}% + 1px)'
|
|
387
|
+
for i in range(1, ncols)) or "transparent 0%"
|
|
388
|
+
rows_html = []
|
|
389
|
+
for f in chart:
|
|
390
|
+
state = ex_state.get(f["id"], "scoped")
|
|
391
|
+
size = _tshirt_label(f["points"])
|
|
392
|
+
width = max(6.0, (f["points"] / maxp) * (colw - 3))
|
|
393
|
+
left = f["layer"] * colw + 1
|
|
394
|
+
dep = f' · after {", ".join(esc(d) for d in f["after"])}' if f["after"] else ""
|
|
395
|
+
rows_html.append(
|
|
406
396
|
f'<div style="display:grid;grid-template-columns:186px 1fr;align-items:center;'
|
|
407
397
|
f'border-bottom:1px solid #14181b;">'
|
|
408
398
|
f'<div style="padding-right:14px;"><div style="font-size:12px;color:{_C["soft"]};'
|
|
409
|
-
f'line-height:1.3;">{
|
|
399
|
+
f'line-height:1.3;">{esc(f["text"])[:52]}</div>'
|
|
410
400
|
f'<div style="font-size:9.5px;color:{_C["dim"]};margin-top:3px;">{esc(f["id"])}'
|
|
411
|
-
f' · {f["
|
|
412
|
-
f'<div style="position:relative;height:40px;background-image:linear-gradient(90deg, {
|
|
413
|
-
f'<div style="position:absolute;top:9px;left:{left:.2f}%;width:{
|
|
414
|
-
f'border-radius:3px;display:flex;align-items:center;padding:0 8px;font-size:9.5px;'
|
|
415
|
-
f'letter-spacing:.02em;overflow:hidden;white-space:nowrap;{fills[state]}">
|
|
416
|
-
|
|
417
|
-
f'top:0;bottom:0;width:1px;background:rgba(255,178,36,.45);"></div>')
|
|
418
|
-
review_marks = "".join(
|
|
419
|
-
f'<div style="position:absolute;left:{_pct(o["review_by"], w0, span):.2f}%;top:50%;'
|
|
420
|
-
f'width:10px;height:10px;background:{_C["amber"]};transform:translate(-50%,-50%) rotate(45deg);'
|
|
421
|
-
f'border:2px solid {_C["bg"]};"></div>'
|
|
422
|
-
for o in model.get("operations", []) if len(o.get("review_by", "")) == 10)
|
|
401
|
+
f' · {f["stories"]} st · {f["acs"]} ac{dep}</div></div>'
|
|
402
|
+
f'<div style="position:relative;height:40px;background-image:linear-gradient(90deg, {gridlines});">'
|
|
403
|
+
f'<div style="position:absolute;top:9px;left:{left:.2f}%;width:{width:.2f}%;height:22px;'
|
|
404
|
+
f'border-radius:3px;display:flex;align-items:center;gap:6px;padding:0 8px;font-size:9.5px;'
|
|
405
|
+
f'letter-spacing:.02em;overflow:hidden;white-space:nowrap;{fills[state]}">'
|
|
406
|
+
f'<b>{size}</b> {f["points"]}pt · {state}</div></div></div>')
|
|
423
407
|
lanes_html = (
|
|
424
408
|
f'<div style="margin:0 0 60px;">'
|
|
425
409
|
f'<div style="display:grid;grid-template-columns:186px 1fr;"><span></span>'
|
|
426
|
-
f'<div style="position:relative;height:20px;">{
|
|
427
|
-
f'<div style="position:relative;border-top:1px solid {_C["line"]};">'
|
|
428
|
-
f'<div style="position:absolute;left:186px;right:0;top:0;bottom:0;pointer-events:none;">'
|
|
429
|
-
f'{today_line}{review_marks}</div>{"".join(lane_rows)}</div>'
|
|
410
|
+
f'<div style="position:relative;height:20px;">{headers}</div></div>'
|
|
411
|
+
f'<div style="position:relative;border-top:1px solid {_C["line"]};">{"".join(rows_html)}</div>'
|
|
430
412
|
f'<div style="display:flex;gap:18px;margin-top:18px;font-size:10.5px;color:{_C["muted"]};flex-wrap:wrap;align-items:center;">'
|
|
431
|
-
f'<span
|
|
432
|
-
f'<span
|
|
433
|
-
f'<span
|
|
434
|
-
f'<span style="margin-left:auto;display:flex;
|
|
435
|
-
f'<span style="
|
|
413
|
+
f'<span>position = dependency sequence (`after` layers)</span>'
|
|
414
|
+
f'<span>width = <b>scope points</b> = stories + acceptance criteria (no estimation, no time axis)</span>'
|
|
415
|
+
f'<span>XS=1 · S=2 · M=3–4 · L=5–7 · XL=8+</span>'
|
|
416
|
+
f'<span style="margin-left:auto;display:flex;gap:14px;">'
|
|
417
|
+
f'<span style="display:flex;align-items:center;gap:6px;"><span style="width:11px;height:11px;background:rgba(63,185,80,.78);border-radius:2px;display:inline-block;"></span>done</span>'
|
|
418
|
+
f'<span style="display:flex;align-items:center;gap:6px;"><span style="width:11px;height:11px;background:rgba(255,178,36,.85);border-radius:2px;display:inline-block;"></span>open</span>'
|
|
419
|
+
f'<span style="display:flex;align-items:center;gap:6px;"><span style="width:11px;height:11px;background:rgba(240,78,78,.82);border-radius:2px;display:inline-block;"></span>blocked</span>'
|
|
420
|
+
f'<span style="display:flex;align-items:center;gap:6px;"><span style="width:11px;height:11px;background:rgba(255,255,255,.05);border:1px dashed #3a4046;border-radius:2px;display:inline-block;"></span>scoped</span></span></div></div>')
|
|
436
421
|
|
|
437
422
|
roadmap_html = ""
|
|
438
423
|
if timeline_html or lanes_html:
|
|
@@ -591,6 +576,13 @@ def render_html(model) -> str:
|
|
|
591
576
|
"""
|
|
592
577
|
|
|
593
578
|
|
|
579
|
+
def _tshirt_label(points: int) -> str:
|
|
580
|
+
for cap, label in ((1, "XS"), (2, "S"), (4, "M"), (7, "L")):
|
|
581
|
+
if points <= cap:
|
|
582
|
+
return label
|
|
583
|
+
return "XL"
|
|
584
|
+
|
|
585
|
+
|
|
594
586
|
def _section(num, name, aside) -> str:
|
|
595
587
|
return (f'<div class="sect"><span class="num">{num}</span>'
|
|
596
588
|
f'<span class="name">{name}</span><span class="rule"></span>'
|
|
@@ -53,7 +53,9 @@ def test_rag_red_on_failing_approved_doc():
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def test_rag_amber_on_open_risk():
|
|
56
|
-
|
|
56
|
+
# at or above the appetite (default 6) ambers; below stays green
|
|
57
|
+
assert S.derive_rag(_model(risks=[{"id": "RISK-1", "score": 6}])) == "amber"
|
|
58
|
+
assert S.derive_rag(_model(risks=[{"id": "RISK-1", "score": 4}])) == "green"
|
|
57
59
|
|
|
58
60
|
|
|
59
61
|
def test_rag_amber_on_coverage_gap():
|
|
@@ -189,8 +191,8 @@ def test_disposition_parsed_and_defaulted():
|
|
|
189
191
|
def test_rag_green_when_all_risks_dispositioned():
|
|
190
192
|
base = {"documents": [], "coverage": [], "broken_references": [],
|
|
191
193
|
"latest_report": None, "completeness": None}
|
|
192
|
-
open_r = dict(base, risks=[{"id": "R", "score":
|
|
193
|
-
done_r = dict(base, risks=[{"id": "R", "score":
|
|
194
|
+
open_r = dict(base, risks=[{"id": "R", "score": 6, "disposition": "open"}])
|
|
195
|
+
done_r = dict(base, risks=[{"id": "R", "score": 6, "disposition": "accepted"}])
|
|
194
196
|
assert S.derive_rag(open_r) == "amber"
|
|
195
197
|
assert S.derive_rag(done_r) == "green"
|
|
196
198
|
|
|
@@ -275,3 +277,93 @@ def test_svc_and_freshness_checks(tmp_path):
|
|
|
275
277
|
assert ok
|
|
276
278
|
fresh_ok, detail = check_ops_review_fresh(doc, ctx)
|
|
277
279
|
assert not fresh_ok and "overdue" in detail
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ── risk appetite (spec 0.7.0) ───────────────────────────────────────────────
|
|
283
|
+
def test_low_grade_open_risks_do_not_amber():
|
|
284
|
+
base = {"documents": [], "coverage": [], "broken_references": [],
|
|
285
|
+
"latest_report": None, "completeness": None, "operations": [],
|
|
286
|
+
"risk_amber_score": 6}
|
|
287
|
+
low = dict(base, risks=[{"id": "R", "score": 4, "disposition": "open"}])
|
|
288
|
+
hi = dict(base, risks=[{"id": "R", "score": 6, "disposition": "open"}])
|
|
289
|
+
strict = dict(low, risk_amber_score=0)
|
|
290
|
+
assert S.derive_rag(low) == "green"
|
|
291
|
+
assert S.derive_rag(hi) == "amber"
|
|
292
|
+
assert S.derive_rag(strict) == "amber"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_charter_target_becomes_implicit_milestone():
|
|
296
|
+
_cd_root()
|
|
297
|
+
m = S.build_status(ROOT / "documents", project="PRJ-002-ATL")
|
|
298
|
+
dates = [x["label"] for x in m["milestones"]]
|
|
299
|
+
assert any("Charter target" in lbl or lbl for lbl in dates) or m["milestones"] == []
|
|
300
|
+
# Aurora has explicit dated milestones AND a target; no duplicates by date
|
|
301
|
+
m2 = S.build_status(ROOT / "documents", project="PRJ-001-AUR")
|
|
302
|
+
seen = [x["date"] for x in m2["milestones"]]
|
|
303
|
+
assert len(seen) == len(set(seen))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ── sequence-and-size charts (sponsor direction, spec 0.8.0) ─────────────────
|
|
307
|
+
def _seq_tree(tmp_path, cyclic=False):
|
|
308
|
+
d = tmp_path / "documents" / "PRJ-001-SQ"
|
|
309
|
+
d.mkdir(parents=True)
|
|
310
|
+
(d / "project.md").write_text(
|
|
311
|
+
"---\nkind: project\nid: PRJ-001-SQ\ncode: SQ\nname: Seq Fixture\n"
|
|
312
|
+
"sponsor: sp.owner\nstatus: active\n---\n\n## Overview\nx\n\n## Scope\nx\n",
|
|
313
|
+
encoding="utf-8")
|
|
314
|
+
first_after = "; after: SQ-PR-003" if cyclic else ""
|
|
315
|
+
(d / "prd.md").write_text(
|
|
316
|
+
"---\nkind: prd\nid: SQ-prd\nproject: PRJ-001-SQ\ntitle: T\n"
|
|
317
|
+
"owner: ow.ner\nstatus: approved\n---\n\n## Overview\nx\n\n"
|
|
318
|
+
"## Product Requirements\n\n"
|
|
319
|
+
f"- **SQ-PR-001** (traces: SQ-BR-001{first_after}): First thing.\n"
|
|
320
|
+
"- **SQ-PR-002** (traces: SQ-BR-001; after: SQ-PR-001): Second thing.\n"
|
|
321
|
+
"- **SQ-PR-003** (traces: SQ-BR-001; after: SQ-PR-002): Third thing.\n\n"
|
|
322
|
+
"## Acceptance Criteria\n\n"
|
|
323
|
+
"- **SQ-AC-001** (verifies: SQ-PR-001): Given a, when b, then c.\n"
|
|
324
|
+
"- **SQ-AC-002** (verifies: SQ-PR-001): Given d, when e, then f.\n"
|
|
325
|
+
"- **SQ-AC-003** (verifies: SQ-PR-002): Given g, when h, then i.\n",
|
|
326
|
+
encoding="utf-8")
|
|
327
|
+
(d / "brd.md").write_text(
|
|
328
|
+
"---\nkind: brd\nid: SQ-brd\nproject: PRJ-001-SQ\ntitle: T\n"
|
|
329
|
+
"owner: ow.ner\nstatus: approved\n---\n\n## Purpose\nx\n\n"
|
|
330
|
+
"## Business Requirements\n\n"
|
|
331
|
+
"- **SQ-BR-001**: The business shall reach 100% of the metric by 2026-12-31.\n\n"
|
|
332
|
+
"## Out of Scope\nx\n", encoding="utf-8")
|
|
333
|
+
(d / "user-story.md").write_text(
|
|
334
|
+
"---\nkind: user-story\nid: SQ-user-story\nproject: PRJ-001-SQ\ntitle: T\n"
|
|
335
|
+
"owner: ow.ner\nstatus: approved\n---\n\n## Overview\nx\n\n"
|
|
336
|
+
"## User Stories\n\n"
|
|
337
|
+
"- **SQ-US-001** (traces: SQ-PR-001): As a user, I want one so that value.\n"
|
|
338
|
+
"- **SQ-US-002** (traces: SQ-PR-003): As a user, I want three so that value.\n",
|
|
339
|
+
encoding="utf-8")
|
|
340
|
+
return tmp_path / "documents"
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def test_features_points_layers_and_tshirt(tmp_path):
|
|
344
|
+
_cd_root()
|
|
345
|
+
m = S.build_status(_seq_tree(tmp_path), project="PRJ-001-SQ")
|
|
346
|
+
by = {f["id"]: f for f in m["features"]}
|
|
347
|
+
assert by["SQ-PR-001"]["points"] == 3 and by["SQ-PR-001"]["layer"] == 0
|
|
348
|
+
assert by["SQ-PR-002"]["points"] == 1 and by["SQ-PR-002"]["layer"] == 1
|
|
349
|
+
assert by["SQ-PR-003"]["points"] == 1 and by["SQ-PR-003"]["layer"] == 2
|
|
350
|
+
from docassert.status.derive import _tshirt
|
|
351
|
+
assert [_tshirt(n) for n in (1, 2, 3, 5, 9)] == ["XS", "S", "M", "L", "XL"]
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def test_sequence_chart_renders_without_time_axis(tmp_path):
|
|
355
|
+
_cd_root()
|
|
356
|
+
m = S.build_status(_seq_tree(tmp_path), project="PRJ-001-SQ")
|
|
357
|
+
out = S.render_html(m)
|
|
358
|
+
assert "SEQ 1" in out and "scope points" in out
|
|
359
|
+
assert "XS=1" in out and "scoped" in out
|
|
360
|
+
assert "created→closed" not in out and "JUL" not in out
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def test_after_cycle_blocks(tmp_path):
|
|
364
|
+
from docassert.consistency import check_sequence_acyclic
|
|
365
|
+
from docassert.graph import build_graph
|
|
366
|
+
_cd_root()
|
|
367
|
+
g = build_graph(_seq_tree(tmp_path, cyclic=True))
|
|
368
|
+
r = check_sequence_acyclic(g)
|
|
369
|
+
assert not r.passed and r.blocking and "after-cycle" in r.detail
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/benefits-realization.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml
RENAMED
|
File without changes
|
{docassert-0.18.0 → docassert-0.20.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/benefits-realization.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.18.0 → docassert-0.20.0}/docassert/_data/schema/data-migration-plan.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.18.0 → docassert-0.20.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.18.0 → docassert-0.20.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.18.0 → docassert-0.20.0}/docassert/_data/templates/benefits-realization.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.18.0 → docassert-0.20.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
|
|
File without changes
|
{docassert-0.18.0 → docassert-0.20.0}/docassert/_data/templates/raci-stakeholder.template.md
RENAMED
|
File without changes
|
{docassert-0.18.0 → docassert-0.20.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|