docassert 0.9.0__tar.gz → 0.10.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.
Files changed (119) hide show
  1. {docassert-0.9.0/docassert.egg-info → docassert-0.10.1}/PKG-INFO +1 -1
  2. {docassert-0.9.0 → docassert-0.10.1}/docassert/__init__.py +1 -1
  3. {docassert-0.9.0 → docassert-0.10.1}/docassert/bridge/ops.py +22 -3
  4. {docassert-0.9.0 → docassert-0.10.1}/docassert/bridge/plan.py +19 -3
  5. {docassert-0.9.0 → docassert-0.10.1}/docassert/cli.py +11 -0
  6. {docassert-0.9.0 → docassert-0.10.1}/docassert/status/render.py +35 -0
  7. {docassert-0.9.0 → docassert-0.10.1/docassert.egg-info}/PKG-INFO +1 -1
  8. {docassert-0.9.0 → docassert-0.10.1}/tests/test_badge.py +22 -0
  9. {docassert-0.9.0 → docassert-0.10.1}/tests/test_bridge.py +29 -0
  10. {docassert-0.9.0 → docassert-0.10.1}/LICENSE +0 -0
  11. {docassert-0.9.0 → docassert-0.10.1}/NOTICE +0 -0
  12. {docassert-0.9.0 → docassert-0.10.1}/README.md +0 -0
  13. {docassert-0.9.0 → docassert-0.10.1}/docassert/__main__.py +0 -0
  14. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/consistency.yaml +0 -0
  15. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/adr.criteria.yaml +0 -0
  16. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
  17. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/brd.criteria.yaml +0 -0
  18. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
  19. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/charter.criteria.yaml +0 -0
  20. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
  21. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
  22. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
  23. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
  24. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/prd.criteria.yaml +0 -0
  25. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/project.criteria.yaml +0 -0
  26. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
  27. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
  28. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
  29. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
  30. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
  31. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
  32. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
  33. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
  34. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
  35. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/profiles/agile-delivery.yaml +0 -0
  36. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/profiles/lean-startup.yaml +0 -0
  37. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/profiles/regulated-industry.yaml +0 -0
  38. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/adr.schema.json +0 -0
  39. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/benefits-realization.schema.json +0 -0
  40. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/brd.schema.json +0 -0
  41. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/business-case.schema.json +0 -0
  42. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/charter.schema.json +0 -0
  43. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
  44. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/frnfr.schema.json +0 -0
  45. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
  46. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
  47. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/prd.schema.json +0 -0
  48. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/project.schema.json +0 -0
  49. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
  50. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
  51. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
  52. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/risk-register.schema.json +0 -0
  53. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/rollback-plan.schema.json +0 -0
  54. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/runbook.schema.json +0 -0
  55. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/status-report.schema.json +0 -0
  56. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/test-cases.schema.json +0 -0
  57. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/schema/user-story.schema.json +0 -0
  58. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/skills/doc-to-pmo/SKILL.md +0 -0
  59. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/adr.template.md +0 -0
  60. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/benefits-realization.template.md +0 -0
  61. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/brd.template.md +0 -0
  62. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/business-case.template.md +0 -0
  63. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/charter.template.md +0 -0
  64. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/data-migration-plan.template.md +0 -0
  65. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/frnfr.template.md +0 -0
  66. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/hypercare-plan.template.md +0 -0
  67. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/post-implementation-review.template.md +0 -0
  68. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/prd.template.md +0 -0
  69. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/project.template.md +0 -0
  70. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/qa-test-plan.template.md +0 -0
  71. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
  72. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
  73. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/risk-register.template.md +0 -0
  74. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/rollback-plan.template.md +0 -0
  75. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/runbook.template.md +0 -0
  76. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/status-report.template.md +0 -0
  77. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/test-cases.template.md +0 -0
  78. {docassert-0.9.0 → docassert-0.10.1}/docassert/_data/templates/user-story.template.md +0 -0
  79. {docassert-0.9.0 → docassert-0.10.1}/docassert/bridge/__init__.py +0 -0
  80. {docassert-0.9.0 → docassert-0.10.1}/docassert/bridge/board.py +0 -0
  81. {docassert-0.9.0 → docassert-0.10.1}/docassert/bridge/gh.py +0 -0
  82. {docassert-0.9.0 → docassert-0.10.1}/docassert/config.py +0 -0
  83. {docassert-0.9.0 → docassert-0.10.1}/docassert/consistency.py +0 -0
  84. {docassert-0.9.0 → docassert-0.10.1}/docassert/extract.py +0 -0
  85. {docassert-0.9.0 → docassert-0.10.1}/docassert/graph.py +0 -0
  86. {docassert-0.9.0 → docassert-0.10.1}/docassert/loader.py +0 -0
  87. {docassert-0.9.0 → docassert-0.10.1}/docassert/models.py +0 -0
  88. {docassert-0.9.0 → docassert-0.10.1}/docassert/profiles.py +0 -0
  89. {docassert-0.9.0 → docassert-0.10.1}/docassert/projects.py +0 -0
  90. {docassert-0.9.0 → docassert-0.10.1}/docassert/report.py +0 -0
  91. {docassert-0.9.0 → docassert-0.10.1}/docassert/rtm.py +0 -0
  92. {docassert-0.9.0 → docassert-0.10.1}/docassert/scaffold.py +0 -0
  93. {docassert-0.9.0 → docassert-0.10.1}/docassert/semantic.py +0 -0
  94. {docassert-0.9.0 → docassert-0.10.1}/docassert/status/__init__.py +0 -0
  95. {docassert-0.9.0 → docassert-0.10.1}/docassert/status/derive.py +0 -0
  96. {docassert-0.9.0 → docassert-0.10.1}/docassert/structural.py +0 -0
  97. {docassert-0.9.0 → docassert-0.10.1}/docassert.egg-info/SOURCES.txt +0 -0
  98. {docassert-0.9.0 → docassert-0.10.1}/docassert.egg-info/dependency_links.txt +0 -0
  99. {docassert-0.9.0 → docassert-0.10.1}/docassert.egg-info/entry_points.txt +0 -0
  100. {docassert-0.9.0 → docassert-0.10.1}/docassert.egg-info/requires.txt +0 -0
  101. {docassert-0.9.0 → docassert-0.10.1}/docassert.egg-info/top_level.txt +0 -0
  102. {docassert-0.9.0 → docassert-0.10.1}/pyproject.toml +0 -0
  103. {docassert-0.9.0 → docassert-0.10.1}/setup.cfg +0 -0
  104. {docassert-0.9.0 → docassert-0.10.1}/tests/test_config.py +0 -0
  105. {docassert-0.9.0 → docassert-0.10.1}/tests/test_consistency.py +0 -0
  106. {docassert-0.9.0 → docassert-0.10.1}/tests/test_defects.py +0 -0
  107. {docassert-0.9.0 → docassert-0.10.1}/tests/test_draft_relaxation.py +0 -0
  108. {docassert-0.9.0 → docassert-0.10.1}/tests/test_extract.py +0 -0
  109. {docassert-0.9.0 → docassert-0.10.1}/tests/test_graph.py +0 -0
  110. {docassert-0.9.0 → docassert-0.10.1}/tests/test_json_report.py +0 -0
  111. {docassert-0.9.0 → docassert-0.10.1}/tests/test_kinds_delivery.py +0 -0
  112. {docassert-0.9.0 → docassert-0.10.1}/tests/test_kinds_governance.py +0 -0
  113. {docassert-0.9.0 → docassert-0.10.1}/tests/test_kinds_operate.py +0 -0
  114. {docassert-0.9.0 → docassert-0.10.1}/tests/test_kinds_reporting.py +0 -0
  115. {docassert-0.9.0 → docassert-0.10.1}/tests/test_profiles.py +0 -0
  116. {docassert-0.9.0 → docassert-0.10.1}/tests/test_projects.py +0 -0
  117. {docassert-0.9.0 → docassert-0.10.1}/tests/test_scaffold.py +0 -0
  118. {docassert-0.9.0 → docassert-0.10.1}/tests/test_status.py +0 -0
  119. {docassert-0.9.0 → docassert-0.10.1}/tests/test_structural.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docassert
3
- Version: 0.9.0
3
+ Version: 0.10.1
4
4
  Summary: Unit testing for business documents — validate structured Markdown docs against a configurable audit standard.
5
5
  Author: C4G Enterprises Inc.
6
6
  License: Apache-2.0
@@ -5,4 +5,4 @@ standard: deterministic structural checks that gate a merge, plus optional
5
5
  AI-graded semantic checks that advise.
6
6
  """
7
7
 
8
- __version__ = "0.9.0"
8
+ __version__ = "0.10.1"
@@ -171,9 +171,28 @@ def reconcile(plan: BridgePlan, gh: G.GhRunner, repo: str) -> tuple[list[str], i
171
171
 
172
172
 
173
173
  def status(plan: BridgePlan, gh: G.GhRunner, repo: str) -> dict:
174
- """Read-only: delivery progress per feature from issue states."""
175
- by_key = _index(G.list_issues(gh, repo))
176
- out: dict = {"projects": []}
174
+ """Read-only: delivery progress per feature, plus scope classification.
175
+
176
+ Unlike reconcile, this never labels or comments; it only reports, so it is
177
+ safe to run from the dashboard build.
178
+ """
179
+ issues = G.list_issues(gh, repo)
180
+ by_key = _index(issues)
181
+ managed = plan.managed_ids
182
+ unverified, orphaned = [], []
183
+ for issue in issues:
184
+ if issue.get("state") != "open":
185
+ continue
186
+ key = _marker_of(issue)
187
+ if key == SCOPE_REPORT_KEY:
188
+ continue
189
+ if key is None:
190
+ unverified.append({"number": issue["number"], "title": issue["title"]})
191
+ elif key not in managed:
192
+ orphaned.append({"number": issue["number"], "title": issue["title"],
193
+ "doc": key})
194
+ out: dict = {"repo": repo, "projects": [],
195
+ "scope": {"unverified": unverified, "orphaned": orphaned}}
177
196
  for p in plan.projects:
178
197
  feats = []
179
198
  for f in p["features"]:
@@ -7,6 +7,7 @@ anywhere in the bridge, writes to documents/.
7
7
  """
8
8
  from __future__ import annotations
9
9
 
10
+ import os
10
11
  from dataclasses import dataclass, field
11
12
  from pathlib import Path
12
13
 
@@ -54,6 +55,18 @@ class BridgePlan:
54
55
  return {f.id for f in self.features} | {s.id for s in self.stories}
55
56
 
56
57
 
58
+ def _repo_rel(path: str, documents_dir: Path) -> str:
59
+ """Path relative to the docs repo root (the parent of documents_dir).
60
+
61
+ Issue bodies link Source as {docs_url}/{doc_path}, and docs_url points at
62
+ the docs repository. When CI checks that repository out into a
63
+ subdirectory, load paths carry the checkout prefix; stripping down to the
64
+ repo root keeps the links valid wherever the docs are checked out.
65
+ """
66
+ rel = os.path.relpath(path, documents_dir.parent)
67
+ return path if rel.startswith("..") else Path(rel).as_posix()
68
+
69
+
57
70
  def _title(item_text: str, limit: int = 64) -> str:
58
71
  """First sentence of the item text, trimmed for an issue title."""
59
72
  first = item_text.split(". ")[0].rstrip(".")
@@ -126,14 +139,17 @@ def build_bridge_plan(documents_dir: str | Path = "documents") -> BridgePlan:
126
139
  feat = features.get(parent.id)
127
140
  if feat is None:
128
141
  feat = Feature(id=parent.id, text=parent.text,
129
- doc_path=parent.doc_path, project=proj_id)
142
+ doc_path=_repo_rel(parent.doc_path,
143
+ documents_dir),
144
+ project=proj_id)
130
145
  feat.acceptance = [(a.id, a.text) for a in
131
146
  graph.children(parent.id, "verifies", "AC")]
132
147
  for aid, _ in feat.acceptance:
133
148
  feat.tests += [t.id for t in graph.children(aid, "tests", "TC")]
134
149
  features[parent.id] = feat
135
- feat.stories.append(Story(id=s.id, text=s.text,
136
- feature_id=parent.id, doc_path=s.doc_path))
150
+ feat.stories.append(Story(
151
+ id=s.id, text=s.text, feature_id=parent.id,
152
+ doc_path=_repo_rel(s.doc_path, documents_dir)))
137
153
  if problems:
138
154
  plan.skipped.append({"id": proj_id,
139
155
  "reason": "; ".join(problems)})
@@ -232,9 +232,19 @@ def cmd_pages(args: argparse.Namespace) -> int:
232
232
  status_mod.render_badge_json(index["overall"]["rag"]))
233
233
  (out / "badges").mkdir(exist_ok=True)
234
234
 
235
+ execution = {}
236
+ if getattr(args, "execution", None):
237
+ import json as _json
238
+ data = _json.loads(Path(args.execution).read_text())
239
+ for proj in data.get("projects", []):
240
+ execution[proj["id"]] = {**proj, "scope": data.get("scope"),
241
+ "repo": data.get("repo")}
242
+
235
243
  plist = projects_mod.load_projects(docs_dir)
236
244
  for p in plist:
237
245
  model = status_mod.build_status(docs_dir, project=p["id"])
246
+ if p["id"] in execution:
247
+ model["execution"] = execution[p["id"]]
238
248
  (out / f"{p['id']}.html").write_text(status_mod.render_html(model))
239
249
  (out / "badges" / f"{p['id']}.json").write_text(
240
250
  status_mod.render_badge_json(model["rag"], label=p["code"].lower()))
@@ -398,6 +408,7 @@ def main(argv: list[str] | None = None) -> int:
398
408
 
399
409
  pg = sub.add_parser("pages", help="Build the full Pages site (portfolio index + a page per project).")
400
410
  pg.add_argument("--out", default="_site", help="Output directory (default: _site).")
411
+ pg.add_argument("--execution", help="Optional bridge-status JSON; adds Delivery and Scope panels to project pages.")
401
412
  docs_dir_opt(pg)
402
413
  pg.set_defaults(func=cmd_pages)
403
414
 
@@ -205,6 +205,40 @@ def render_html(model) -> str:
205
205
  f'{note}<table><thead><tr><th>Kind</th><th>State</th></tr></thead>'
206
206
  f'<tbody>{req_rows}{sep}{rec_rows}</tbody></table></section>')
207
207
 
208
+ execution_html = ""
209
+ ex = model.get("execution")
210
+ if ex:
211
+ feat_rows: list[str] = []
212
+ for f in ex.get("features", []):
213
+ total = f.get("stories_total", 0) or 0
214
+ done = f.get("stories_closed", 0) or 0
215
+ pct = (100 * done // total) if total else 0
216
+ mark = "✓" if f.get("closed") else ""
217
+ issue = f' · <a href="https://github.com/{esc(ex.get("repo", ""))}/issues/{f["issue"]}">#{f["issue"]}</a>' if f.get("issue") else ""
218
+ feat_rows.append(
219
+ f'<div class="sig"><div class="sig-h"><span>{esc(f["id"])} {mark}</span>'
220
+ f'<span>{done}/{total} stories{issue}</span></div>'
221
+ f'<div class="bar"><div class="bar-f" style="width:{pct}%;background:var(--ok)"></div></div></div>')
222
+ overall_t = ex.get("stories_total", 0) or 0
223
+ overall_d = ex.get("stories_closed", 0) or 0
224
+ execution_html = (
225
+ f'<section><h2>Delivery · {overall_d}/{overall_t} stories closed</h2>'
226
+ '<p style="color:var(--muted);font-size:13px;margin:0 0 10px">'
227
+ "Read from the bridge-managed issues. Scope lives in these documents; "
228
+ "delivery lives on the board.</p>" + "".join(feat_rows) + "</section>")
229
+ scope = ex.get("scope") or {}
230
+ bad = scope.get("unverified", []) + scope.get("orphaned", [])
231
+ if bad:
232
+ items = "".join(
233
+ f'<li><a href="https://github.com/{esc(ex.get("repo", ""))}/issues/{i["number"]}">#{i["number"]}</a> {esc(i["title"])}</li>'
234
+ for i in bad)
235
+ execution_html += (
236
+ f'<section><h2 style="color:var(--bad)">Scope · {len(bad)} unmatched item(s)</h2>'
237
+ f'<ul>{items}</ul></section>')
238
+ else:
239
+ execution_html += ('<section><h2>Scope</h2><p style="color:var(--ok)">'
240
+ "Every open issue matches an approved item in the documents.</p></section>")
241
+
208
242
  title = esc(model.get("title", "Project Status"))
209
243
  pid = model.get("project")
210
244
  back = '<a class="back" href="index.html">← all projects</a>' if pid else ""
@@ -226,6 +260,7 @@ def render_html(model) -> str:
226
260
  {c['approved']} approved{scope} · generated {gen}. Do not edit — regenerated from the documents.</p>
227
261
  <section><h2>Traceability coverage</h2>{coverage}</section>
228
262
  {document_set}
263
+ {execution_html}
229
264
  {problems}
230
265
  <section><h2>Open risks ({len(model['risks'])})</h2><ul>{risks}</ul></section>
231
266
  <section><h2>Latest status report</h2><p>{report}</p></section>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docassert
3
- Version: 0.9.0
3
+ Version: 0.10.1
4
4
  Summary: Unit testing for business documents — validate structured Markdown docs against a configurable audit standard.
5
5
  Author: C4G Enterprises Inc.
6
6
  License: Apache-2.0
@@ -24,3 +24,25 @@ def test_pages_emits_badges(tmp_path, monkeypatch):
24
24
  assert overall["schemaVersion"] == 1 and overall["message"] in {"green", "amber", "red"}
25
25
  aur = json.loads((out / "badges" / "PRJ-001-AUR.json").read_text())
26
26
  assert aur["label"] == "aur" and aur["message"] in {"green", "amber", "red"}
27
+
28
+
29
+ def test_pages_execution_panels(tmp_path, monkeypatch):
30
+ import json
31
+ monkeypatch.chdir(ROOT)
32
+ exec_file = tmp_path / "execution.json"
33
+ exec_file.write_text(json.dumps({
34
+ "repo": "o/r",
35
+ "projects": [{"id": "PRJ-001-AUR",
36
+ "features": [{"id": "AUR-PR-014", "issue": 5,
37
+ "stories_total": 2, "stories_closed": 1,
38
+ "closed": False}],
39
+ "stories_closed": 1, "stories_total": 2}],
40
+ "scope": {"unverified": [], "orphaned": []},
41
+ }))
42
+ out = tmp_path / "site"
43
+ assert main(["pages", "--out", str(out), "--execution", str(exec_file)]) == 0
44
+ html = (out / "PRJ-001-AUR.html").read_text()
45
+ assert "Delivery · 1/2 stories closed" in html
46
+ assert "AUR-PR-014" in html and "Every open issue matches" in html
47
+ other = (out / "PRJ-002-ATL.html").read_text()
48
+ assert "Delivery ·" not in other # no execution data, no panel
@@ -113,6 +113,20 @@ def test_bodies_carry_marker_and_authority_note(tmp_path):
113
113
  assert f"<!-- docassert-bridge: {s.id} -->" in story_body(s, None)
114
114
 
115
115
 
116
+ def test_doc_paths_relative_to_docs_repo_root(tmp_path):
117
+ """CI checks the docs repo out into a subdirectory; Source links must not
118
+ carry that checkout prefix (they resolve against the docs repo root)."""
119
+ docs = _tree(tmp_path / "pmo")
120
+ plan = build_bridge_plan(docs)
121
+ f = plan.features[0]
122
+ assert f.doc_path == "documents/PRJ-001-TST/prd.md"
123
+ assert f.stories[0].doc_path == "documents/PRJ-001-TST/user-story.md"
124
+ body = feature_body(f, "https://github.com/o/r/blob/main")
125
+ assert ("**Source:** https://github.com/o/r/blob/main/"
126
+ "documents/PRJ-001-TST/prd.md") in body
127
+ assert "/pmo/" not in body
128
+
129
+
116
130
  # ── ops against a fake runner ────────────────────────────────────────────────
117
131
  class FakeGh:
118
132
  """Records mutations; serves canned issue lists."""
@@ -320,3 +334,18 @@ def test_scaffold_syncs_board(tmp_path):
320
334
  assert any("5 item(s) synced" in a for a in actions)
321
335
  adds = [q for q in bgh.gql if "addProjectV2ItemById" in q]
322
336
  assert len(adds) == 5
337
+
338
+
339
+ def test_status_includes_scope_classification(tmp_path):
340
+ plan = build_bridge_plan(_tree(tmp_path))
341
+ issues = [
342
+ _mk_issue(1, "TST-US-001"),
343
+ {"number": 2, "node_id": "N2", "state": "open", "title": "rogue",
344
+ "body": "no marker", "labels": []},
345
+ _mk_issue(3, "TST-US-999"),
346
+ _mk_issue(4, "TST-US-002", state="closed"), # closed: not scope-checked
347
+ ]
348
+ data = ops.status(plan, FakeGh(issues), "o/r")
349
+ assert data["repo"] == "o/r"
350
+ assert [i["number"] for i in data["scope"]["unverified"]] == [2]
351
+ assert [i["doc"] for i in data["scope"]["orphaned"]] == ["TST-US-999"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes