docassert 0.19.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.19.0/docassert.egg-info → docassert-0.20.0}/PKG-INFO +1 -1
- {docassert-0.19.0 → docassert-0.20.0}/docassert/__init__.py +1 -1
- {docassert-0.19.0 → docassert-0.20.0}/docassert/consistency.py +32 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/status/derive.py +44 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/status/render.py +53 -61
- {docassert-0.19.0 → docassert-0.20.0/docassert.egg-info}/PKG-INFO +1 -1
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_status.py +66 -0
- {docassert-0.19.0 → docassert-0.20.0}/LICENSE +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/NOTICE +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/README.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/__main__.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/consistency.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/operations.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/profiles/operations.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/adr.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/brd.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/business-case.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/charter.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/frnfr.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/operations.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/prd.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/project.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/risk-register.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/runbook.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/status-report.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/test-cases.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/schema/user-story.schema.json +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/skills/doc-to-pmo/SKILL.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/adr.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/benefits-realization.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/brd.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/business-case.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/charter.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/data-migration-plan.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/frnfr.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/hypercare-plan.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/operations.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/post-implementation-review.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/prd.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/project.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/qa-test-plan.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/risk-register.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/rollback-plan.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/runbook.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/status-report.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/test-cases.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/_data/templates/user-story.template.md +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/bridge/__init__.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/bridge/board.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/bridge/gh.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/bridge/ops.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/bridge/plan.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/cli.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/config.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/extract.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/graph.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/loader.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/models.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/profiles.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/projects.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/report.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/rtm.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/scaffold.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/semantic.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/status/__init__.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert/structural.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert.egg-info/SOURCES.txt +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert.egg-info/dependency_links.txt +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert.egg-info/entry_points.txt +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert.egg-info/requires.txt +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/docassert.egg-info/top_level.txt +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/pyproject.toml +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/setup.cfg +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_badge.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_bridge.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_config.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_consistency.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_defects.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_draft_relaxation.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_extract.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_graph.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_json_report.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_kinds_delivery.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_kinds_governance.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_kinds_operate.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_kinds_reporting.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_profiles.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_projects.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_report_extract.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_scaffold.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/tests/test_semantic.py +0 -0
- {docassert-0.19.0 → docassert-0.20.0}/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,
|
|
@@ -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>'
|
|
@@ -301,3 +301,69 @@ 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
|
|
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.0}/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.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.19.0 → docassert-0.20.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml
RENAMED
|
File without changes
|
{docassert-0.19.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.19.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.19.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.19.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.19.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.19.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.19.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.19.0 → docassert-0.20.0}/docassert/_data/templates/raci-stakeholder.template.md
RENAMED
|
File without changes
|
{docassert-0.19.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
|