docassert 0.19.0__tar.gz → 0.20.1__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.19.0/docassert.egg-info → docassert-0.20.1}/PKG-INFO +1 -1
- {docassert-0.19.0 → docassert-0.20.1}/docassert/__init__.py +1 -1
- {docassert-0.19.0 → docassert-0.20.1}/docassert/consistency.py +32 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/status/derive.py +44 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/status/render.py +109 -71
- {docassert-0.19.0 → docassert-0.20.1/docassert.egg-info}/PKG-INFO +1 -1
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_status.py +78 -0
- {docassert-0.19.0 → docassert-0.20.1}/LICENSE +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/NOTICE +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/README.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/__main__.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/consistency.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/adr.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/brd.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/charter.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/operations.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/prd.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/project.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/profiles/agile-delivery.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/profiles/lean-startup.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/profiles/operations.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/profiles/regulated-industry.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/adr.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/benefits-realization.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/brd.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/business-case.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/charter.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/frnfr.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/operations.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/prd.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/project.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/risk-register.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/rollback-plan.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/runbook.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/status-report.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/test-cases.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/user-story.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/skills/doc-to-pmo/SKILL.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/adr.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/benefits-realization.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/brd.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/business-case.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/charter.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/data-migration-plan.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/frnfr.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/hypercare-plan.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/operations.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/post-implementation-review.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/prd.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/project.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/qa-test-plan.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/risk-register.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/rollback-plan.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/runbook.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/status-report.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/test-cases.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/_data/templates/user-story.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/bridge/__init__.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/bridge/board.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/bridge/gh.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/bridge/ops.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/bridge/plan.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/cli.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/config.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/extract.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/graph.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/loader.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/models.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/profiles.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/projects.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/report.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/rtm.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/scaffold.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/semantic.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/status/__init__.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert/structural.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert.egg-info/SOURCES.txt +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert.egg-info/dependency_links.txt +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert.egg-info/entry_points.txt +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert.egg-info/requires.txt +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/docassert.egg-info/top_level.txt +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/pyproject.toml +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/setup.cfg +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_badge.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_bridge.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_config.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_consistency.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_defects.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_draft_relaxation.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_extract.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_graph.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_json_report.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_kinds_delivery.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_kinds_governance.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_kinds_operate.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_kinds_reporting.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_profiles.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_projects.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_report_extract.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_scaffold.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_semantic.py +0 -0
- {docassert-0.19.0 → docassert-0.20.1}/tests/test_structural.py +0 -0
|
@@ -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
|
|
@@ -245,6 +288,7 @@ def build_status(documents_dir=DOCUMENTS_DIR, project: str | None = None) -> dic
|
|
|
245
288
|
"risks": _risks(graph, code),
|
|
246
289
|
"operations": _operations(docs),
|
|
247
290
|
"milestones": _milestones(docs),
|
|
291
|
+
"features": _features(graph, code),
|
|
248
292
|
"broken_references": _broken_references(graph, code),
|
|
249
293
|
"latest_report": _latest_report(docs),
|
|
250
294
|
"completeness": completeness,
|
|
@@ -142,6 +142,31 @@ def _today():
|
|
|
142
142
|
return datetime.date.today()
|
|
143
143
|
|
|
144
144
|
|
|
145
|
+
def _verdict_causes(model) -> list[str]:
|
|
146
|
+
"""The raw cause strings behind the verdict (also the verdict tooltip)."""
|
|
147
|
+
causes = []
|
|
148
|
+
gaps = [c for c in model["coverage"] if c["gaps"]]
|
|
149
|
+
if gaps:
|
|
150
|
+
causes.append(f"{len(gaps)} coverage chain(s) with gaps")
|
|
151
|
+
failing = [d for d in model["documents"] if not d["passing"]]
|
|
152
|
+
if failing:
|
|
153
|
+
causes.append(f"{len(failing)} document(s) failing audit")
|
|
154
|
+
if model["broken_references"]:
|
|
155
|
+
causes.append(f"{len(model['broken_references'])} broken reference(s)")
|
|
156
|
+
comp = model.get("completeness")
|
|
157
|
+
if comp and comp.get("blocks"):
|
|
158
|
+
causes.append("required documents missing under an enforced profile")
|
|
159
|
+
thr = model.get("risk_amber_score", 6)
|
|
160
|
+
open_r = [r for r in model["risks"] if r.get("disposition", "open") == "open"
|
|
161
|
+
and (thr == 0 or r.get("score", 0) >= thr)]
|
|
162
|
+
if open_r:
|
|
163
|
+
causes.append(f"{len(open_r)} open risk(s) at or above the appetite (score ≥ {thr})")
|
|
164
|
+
stale = [o for o in model.get("operations", []) if not o["fresh"]]
|
|
165
|
+
if stale:
|
|
166
|
+
causes.append(f"operations review overdue since {stale[0]['review_by']}")
|
|
167
|
+
return causes
|
|
168
|
+
|
|
169
|
+
|
|
145
170
|
def _verdict(model) -> str:
|
|
146
171
|
"""One decision-grade sentence composed only from recorded causes."""
|
|
147
172
|
rag = model["rag"]
|
|
@@ -259,8 +284,16 @@ def render_html(model) -> str:
|
|
|
259
284
|
(f"{esc(nxt[1])} · {nxt[2]}" if nxt else "no dated milestone")),
|
|
260
285
|
("DOC SET", docset_v, docset_c, docset_sub),
|
|
261
286
|
]
|
|
287
|
+
stat_tips = {
|
|
288
|
+
"HEALTH": "Derived RAG. Red: failing approved docs, broken references, or enforced profile gaps. Amber: coverage gaps, open risks at or above the risk appetite, stale operations review, or an amber/red status report. Green: none of those.",
|
|
289
|
+
"DOCS APPROVED": "Documents with approved or baselined status, of all documents in this project. The sub-line counts documents passing every blocking structural check.",
|
|
290
|
+
"COVERAGE": "Traceability links present over links required, across every configured chain (BR→PR, PR→AC, AC→TC, and any others).",
|
|
291
|
+
"OPEN RISKS": "Risks with disposition open. Dispositioned risks (mitigated, accepted, closed) stay on the register as record but do not count here.",
|
|
292
|
+
"NEXT MILESTONE": "Days to the nearest upcoming dated marker: a dated charter milestone or the operations review. Temporal fact only.",
|
|
293
|
+
"DOC SET": "Profile completeness: required kinds complete (approved and passing audit) over required kinds expected.",
|
|
294
|
+
}
|
|
262
295
|
stat_html = "".join(
|
|
263
|
-
f'<div style="padding:22px 20px;border-left:1px solid {_C["line"]};">'
|
|
296
|
+
f'<div title="{esc(stat_tips.get(k, ""))}" style="cursor:help;padding:22px 20px;border-left:1px solid {_C["line"]};">'
|
|
264
297
|
f'<div class="chiplabel" style="margin-bottom:14px;">{k}</div>'
|
|
265
298
|
f'<div style="font-family:Archivo,system-ui,sans-serif;font-weight:800;font-size:30px;letter-spacing:-.02em;'
|
|
266
299
|
f'line-height:1;color:{col};font-variant-numeric:tabular-nums;">{esc(v)}</div>'
|
|
@@ -278,8 +311,11 @@ def render_html(model) -> str:
|
|
|
278
311
|
f'{_C["green"] if i < filled else _C["line"]};"></span>'
|
|
279
312
|
for i in range(cells))
|
|
280
313
|
ratio_c = _C["green"] if covered == total else _C["amberT"]
|
|
314
|
+
tip = ("all links traced" if not cov["gaps"] else
|
|
315
|
+
"gaps: " + ", ".join(cov["gaps"]))
|
|
281
316
|
meters.append(
|
|
282
|
-
f'<div
|
|
317
|
+
f'<div title="{esc(tip)}" style="cursor:help;">'
|
|
318
|
+
f'<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:11px;">'
|
|
283
319
|
f'<span style="font-size:12.5px;color:{_C["soft"]};">{esc(cov["label"])}</span>'
|
|
284
320
|
f'<span style="font-family:Archivo,system-ui,sans-serif;font-weight:700;font-size:13px;color:{ratio_c};">'
|
|
285
321
|
f'{covered}/{total}</span></div><div style="display:flex;gap:4px;">{cell_html}</div></div>')
|
|
@@ -342,8 +378,11 @@ def render_html(model) -> str:
|
|
|
342
378
|
if m["when"] == "elapsed":
|
|
343
379
|
return f'color:{_C["muted"]};background:#14181b;'
|
|
344
380
|
return f'color:{_C["muted"]};background:#14181b;'
|
|
381
|
+
ms_tip = ("Temporal fact only: elapsed means the date passed, "
|
|
382
|
+
"not that the milestone completed (spec §13.2).")
|
|
345
383
|
cols = "".join(
|
|
346
|
-
f'<div
|
|
384
|
+
f'<div title="{esc(m["label"])} · {esc(m["date"])} · {esc(ms_tip)}" '
|
|
385
|
+
f'style="cursor:help;display:flex;flex-direction:column;align-items:flex-start;">'
|
|
347
386
|
f'<span style="display:block;width:14px;height:14px;border-radius:50%;'
|
|
348
387
|
f'border:2px solid {_C["bg"]};{_dot(m)}"></span>'
|
|
349
388
|
f'<div style="font-family:Archivo,system-ui,sans-serif;font-weight:700;font-size:13px;color:{_C["text"]};'
|
|
@@ -362,77 +401,67 @@ def render_html(model) -> str:
|
|
|
362
401
|
ex = model.get("execution") or {}
|
|
363
402
|
lanes_html = ""
|
|
364
403
|
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%"
|
|
404
|
+
ex_state = {}
|
|
405
|
+
for f in feats:
|
|
406
|
+
facts = f.get("facts") or {}
|
|
407
|
+
ex_state[f["id"]] = ("blocked" if "blocked" in (facts.get("labels") or [])
|
|
408
|
+
else "done" if f.get("closed") else "open")
|
|
409
|
+
chart = model.get("features") or []
|
|
410
|
+
if chart:
|
|
411
|
+
maxp = max(f["points"] for f in chart) or 1
|
|
412
|
+
maxlayer = max(f["layer"] for f in chart)
|
|
413
|
+
ncols = maxlayer + 1
|
|
414
|
+
colw = 100.0 / ncols
|
|
393
415
|
fills = {"done": f'background:rgba(63,185,80,.78);color:{_C["bg"]};',
|
|
394
416
|
"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
|
-
|
|
406
|
-
|
|
417
|
+
"blocked": f'background:rgba(240,78,78,.82);color:{_C["bg"]};',
|
|
418
|
+
"scoped": 'background:rgba(255,255,255,.05);color:#9BA1A8;border:1px dashed #3a4046;'}
|
|
419
|
+
headers = "".join(
|
|
420
|
+
f'<span style="position:absolute;left:{i * colw:.2f}%;top:0;font-size:9px;'
|
|
421
|
+
f'letter-spacing:.14em;color:{_C["dim"]};padding-left:7px;">SEQ {i + 1}</span>'
|
|
422
|
+
for i in range(ncols))
|
|
423
|
+
gridlines = ", ".join(
|
|
424
|
+
f'transparent {i * colw:.2f}%, #191d21 {i * colw:.2f}%, '
|
|
425
|
+
f'#191d21 calc({i * colw:.2f}% + 1px), transparent calc({i * colw:.2f}% + 1px)'
|
|
426
|
+
for i in range(1, ncols)) or "transparent 0%"
|
|
427
|
+
rows_html = []
|
|
428
|
+
for f in chart:
|
|
429
|
+
state = ex_state.get(f["id"], "scoped")
|
|
430
|
+
size = _tshirt_label(f["points"])
|
|
431
|
+
width = max(6.0, (f["points"] / maxp) * (colw - 3))
|
|
432
|
+
left = f["layer"] * colw + 1
|
|
433
|
+
dep = f' · after {", ".join(esc(d) for d in f["after"])}' if f["after"] else ""
|
|
434
|
+
bar_tip = (f'{f["text"]} — {f["points"]} scope point(s) = '
|
|
435
|
+
f'{f["stories"]} story(ies) + {f["acs"]} acceptance criteria. '
|
|
436
|
+
+ (f'Sequenced after {", ".join(f["after"])}. ' if f["after"] else "")
|
|
437
|
+
+ ("State from the bridge issue." if f["id"] in ex_state
|
|
438
|
+
else "No execution data yet: scoped only."))
|
|
439
|
+
rows_html.append(
|
|
440
|
+
f'<div title="{esc(bar_tip)}" style="cursor:help;display:grid;grid-template-columns:186px 1fr;align-items:center;'
|
|
407
441
|
f'border-bottom:1px solid #14181b;">'
|
|
408
442
|
f'<div style="padding-right:14px;"><div style="font-size:12px;color:{_C["soft"]};'
|
|
409
|
-
f'line-height:1.3;">{
|
|
443
|
+
f'line-height:1.3;">{esc(f["text"])[:52]}</div>'
|
|
410
444
|
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)
|
|
445
|
+
f' · {f["stories"]} st · {f["acs"]} ac{dep}</div></div>'
|
|
446
|
+
f'<div style="position:relative;height:40px;background-image:linear-gradient(90deg, {gridlines});">'
|
|
447
|
+
f'<div style="position:absolute;top:9px;left:{left:.2f}%;width:{width:.2f}%;height:22px;'
|
|
448
|
+
f'border-radius:3px;display:flex;align-items:center;gap:6px;padding:0 8px;font-size:9.5px;'
|
|
449
|
+
f'letter-spacing:.02em;overflow:hidden;white-space:nowrap;{fills[state]}">'
|
|
450
|
+
f'<b>{size}</b> {f["points"]}pt · {state}</div></div></div>')
|
|
423
451
|
lanes_html = (
|
|
424
452
|
f'<div style="margin:0 0 60px;">'
|
|
425
453
|
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>'
|
|
454
|
+
f'<div style="position:relative;height:20px;">{headers}</div></div>'
|
|
455
|
+
f'<div style="position:relative;border-top:1px solid {_C["line"]};">{"".join(rows_html)}</div>'
|
|
430
456
|
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="
|
|
457
|
+
f'<span>position = dependency sequence (`after` layers)</span>'
|
|
458
|
+
f'<span>width = <b>scope points</b> = stories + acceptance criteria (no estimation, no time axis)</span>'
|
|
459
|
+
f'<span>XS=1 · S=2 · M=3–4 · L=5–7 · XL=8+</span>'
|
|
460
|
+
f'<span style="margin-left:auto;display:flex;gap:14px;">'
|
|
461
|
+
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>'
|
|
462
|
+
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>'
|
|
463
|
+
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>'
|
|
464
|
+
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
465
|
|
|
437
466
|
roadmap_html = ""
|
|
438
467
|
if timeline_html or lanes_html:
|
|
@@ -505,6 +534,8 @@ def render_html(model) -> str:
|
|
|
505
534
|
work_aside = (f"{ex.get('stories_closed', 0)}/{ex.get('stories_total', 0)} stories closed"
|
|
506
535
|
if ex else "bridge-managed issues")
|
|
507
536
|
verdict = _verdict(model)
|
|
537
|
+
causes = _verdict_causes(model)
|
|
538
|
+
verdict_tip = ("Causes: " + "; ".join(causes)) if causes else "No amber or red causes recorded."
|
|
508
539
|
payload = _json_embed({"risks": risks_data, "docs": docs_data, "work": work_data,
|
|
509
540
|
"C": _C})
|
|
510
541
|
|
|
@@ -539,7 +570,7 @@ def render_html(model) -> str:
|
|
|
539
570
|
|
|
540
571
|
<div style="display:flex;gap:18px;margin:28px 0 44px;padding:20px 22px;background:rgba(255,178,36,.06);border-left:3px solid {rc};">
|
|
541
572
|
<span style="font-family:Archivo,system-ui,sans-serif;font-weight:900;font-size:12px;letter-spacing:.16em;color:{rt};writing-mode:vertical-rl;transform:rotate(180deg);">VERDICT</span>
|
|
542
|
-
<p style="margin:0;font-family:Archivo,system-ui,sans-serif;font-weight:500;font-size:17.5px;line-height:1.5;color:#DDE1E5;max-width:88ch;">{esc(verdict)}</p>
|
|
573
|
+
<p title="{esc(verdict_tip)}" style="cursor:help;margin:0;font-family:Archivo,system-ui,sans-serif;font-weight:500;font-size:17.5px;line-height:1.5;color:#DDE1E5;max-width:88ch;">{esc(verdict)}</p>
|
|
543
574
|
</div>
|
|
544
575
|
|
|
545
576
|
<div style="display:grid;grid-template-columns:repeat(6,1fr);border-top:1px solid {_C['line']};border-bottom:1px solid {_C['line']};margin-bottom:56px;">{stat_html}</div>
|
|
@@ -591,6 +622,13 @@ def render_html(model) -> str:
|
|
|
591
622
|
"""
|
|
592
623
|
|
|
593
624
|
|
|
625
|
+
def _tshirt_label(points: int) -> str:
|
|
626
|
+
for cap, label in ((1, "XS"), (2, "S"), (4, "M"), (7, "L")):
|
|
627
|
+
if points <= cap:
|
|
628
|
+
return label
|
|
629
|
+
return "XL"
|
|
630
|
+
|
|
631
|
+
|
|
594
632
|
def _section(num, name, aside) -> str:
|
|
595
633
|
return (f'<div class="sect"><span class="num">{num}</span>'
|
|
596
634
|
f'<span class="name">{name}</span><span class="rule"></span>'
|
|
@@ -617,7 +655,7 @@ function matrix(){
|
|
|
617
655
|
probs.forEach(pr => {
|
|
618
656
|
const inCell = DATA.risks.filter(r => r.disposition==='open' && r.prob===pr && (r.impact===im || (r.impact==='critical' && im==='high')));
|
|
619
657
|
const z = zone(PV[pr]*PV[im]); const occ = inCell.length>0;
|
|
620
|
-
const chips = inCell.map(r => `<span style="font-size:11px;font-weight:600;padding:2px 6px;border-radius:3px;border:1px solid ${z.b};color:${S.sel===r.id?C.bg:z.t};background:${S.sel===r.id?z.t:'transparent'};">${esc(r.short)}</span>`).join('');
|
|
658
|
+
const chips = inCell.map(r => `<span title="${esc(r.id)}: ${esc(r.desc)}" style="cursor:help;font-size:11px;font-weight:600;padding:2px 6px;border-radius:3px;border:1px solid ${z.b};color:${S.sel===r.id?C.bg:z.t};background:${S.sel===r.id?z.t:'transparent'};">${esc(r.short)}</span>`).join('');
|
|
621
659
|
h += `<div data-risk="${occ?inCell[0].id:''}" style="min-height:56px;border:1px solid ${occ?z.b:'#191d21'};background:${occ?z.s:z.f};border-radius:3px;display:flex;gap:4px;align-items:center;justify-content:center;flex-wrap:wrap;cursor:${occ?'pointer':'default'};">${chips}</div>`;
|
|
622
660
|
});
|
|
623
661
|
});
|
|
@@ -682,7 +720,7 @@ function docs(){
|
|
|
682
720
|
<span style="font-size:12.5px;color:${C.text};">${esc(d.kind)}</span>
|
|
683
721
|
<span class="mono-tag" style="justify-self:start;">${esc(d.id)}</span>
|
|
684
722
|
<span style="font-size:12px;color:${d.status==='active'?C.amberT:C.muted};">${esc(d.status)}</span>
|
|
685
|
-
<span style="text-align:right;color:${d.passing?C.green:C.red};font-size:14px;">${d.passing?'✓':'✗'}</span></div>`).join('');
|
|
723
|
+
<span title="${d.passing?'passes every blocking structural check':'failing one or more blocking checks'}" style="cursor:help;text-align:right;color:${d.passing?C.green:C.red};font-size:14px;">${d.passing?'✓':'✗'}</span></div>`).join('');
|
|
686
724
|
}
|
|
687
725
|
|
|
688
726
|
function work(){
|
|
@@ -708,7 +746,7 @@ function work(){
|
|
|
708
746
|
<div><div style="font-size:13px;color:${C.text};line-height:1.45;">${w.url?`<a style="text-decoration:none;" href="${esc(w.url)}">${esc(w.title)}</a>`:esc(w.title)}</div>
|
|
709
747
|
<div style="font-size:10.5px;color:${C.dim};margin-top:6px;">linked ${esc(w.link)}</div></div>
|
|
710
748
|
<span style="font-size:11.5px;color:${C.muted};">${esc(w.assignee)}</span>
|
|
711
|
-
<span style="text-align:right;font-size:11.5px;color:${stC(w.state)};">${w.state==='done'?'done ✓':esc(w.state)}</span></div>`).join('');
|
|
749
|
+
<span title="state read from the bridge-managed issue; presentation never alters the derived RAG" style="cursor:help;text-align:right;font-size:11.5px;color:${stC(w.state)};">${w.state==='done'?'done ✓':esc(w.state)}</span></div>`).join('');
|
|
712
750
|
}
|
|
713
751
|
|
|
714
752
|
function render(){ matrix(); riskCtl(); riskTable(); docs(); work(); }
|
|
@@ -730,12 +768,12 @@ def _index_row(p) -> str:
|
|
|
730
768
|
f'<div><div style="font-family:Archivo,system-ui,sans-serif;font-weight:700;font-size:15px;color:#F1F3F5;line-height:1.2;">{esc(p["name"])}</div>'
|
|
731
769
|
f'<div style="font-size:10.5px;color:{_C["dim"]};margin-top:5px;display:flex;gap:10px;">'
|
|
732
770
|
f'<span>{esc(p["id"])}</span><span>·</span><span>{esc(p["sponsor"])}</span></div></div>'
|
|
733
|
-
f'<div style="text-align:center;"><div style="font-size:12.5px;color:{covc};">{cov}%</div>'
|
|
771
|
+
f'<div title="traceability links traced over required, all chains" style="cursor:help;text-align:center;"><div style="font-size:12.5px;color:{covc};">{cov}%</div>'
|
|
734
772
|
f'<div style="height:4px;background:#16191c;margin-top:7px;border-radius:2px;overflow:hidden;">'
|
|
735
773
|
f'<span style="display:block;height:100%;width:{cov}%;background:{covc};"></span></div></div>'
|
|
736
774
|
f'<span style="text-align:center;font-size:12.5px;color:{_C["soft"]};">{esc(p["docs"])}</span>'
|
|
737
775
|
f'<span style="text-align:center;font-size:12px;color:{_C["amberT"] if p["risks"] else _C["faint"]};">{risks}</span>'
|
|
738
|
-
f'<span style="text-align:right;font-size:11.5px;color:{_C["muted"]};">{esc(p["next"])}</span></a>')
|
|
776
|
+
f'<span title="nearest upcoming dated marker (milestone or operations review)" style="cursor:help;text-align:right;font-size:11.5px;color:{_C["muted"]};">{esc(p["next"])}</span></a>')
|
|
739
777
|
|
|
740
778
|
|
|
741
779
|
def render_index_html(index) -> str:
|
|
@@ -301,3 +301,81 @@ def test_charter_target_becomes_implicit_milestone():
|
|
|
301
301
|
m2 = S.build_status(ROOT / "documents", project="PRJ-001-AUR")
|
|
302
302
|
seen = [x["date"] for x in m2["milestones"]]
|
|
303
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
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def test_report_tooltips_present(tmp_path):
|
|
373
|
+
_cd_root()
|
|
374
|
+
m = S.build_status(_seq_tree(tmp_path), project="PRJ-001-SQ")
|
|
375
|
+
out = S.render_html(m)
|
|
376
|
+
assert 'title="Derived RAG.' in out # stat derivation
|
|
377
|
+
assert "not that the milestone completed" in out or "Temporal fact" in out
|
|
378
|
+
assert "scope point(s) = " in out # bar breakdown tooltip
|
|
379
|
+
assert 'title="Causes:' in out or "No amber or red causes" in out
|
|
380
|
+
idx = S.render_index_html(S.build_index(ROOT / "documents"))
|
|
381
|
+
assert "nearest upcoming dated marker" in idx
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.19.0 → docassert-0.20.1}/docassert/_data/criteria/benefits-realization.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.19.0 → docassert-0.20.1}/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.19.0 → docassert-0.20.1}/docassert/_data/criteria/raci-stakeholder.criteria.yaml
RENAMED
|
File without changes
|
{docassert-0.19.0 → docassert-0.20.1}/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.19.0 → docassert-0.20.1}/docassert/_data/schema/benefits-realization.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.19.0 → docassert-0.20.1}/docassert/_data/schema/data-migration-plan.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.19.0 → docassert-0.20.1}/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.19.0 → docassert-0.20.1}/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.19.0 → docassert-0.20.1}/docassert/_data/templates/benefits-realization.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.19.0 → docassert-0.20.1}/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.19.0 → docassert-0.20.1}/docassert/_data/templates/raci-stakeholder.template.md
RENAMED
|
File without changes
|
{docassert-0.19.0 → docassert-0.20.1}/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
|