docassert 0.13.0__tar.gz → 0.14.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.13.0/docassert.egg-info → docassert-0.14.0}/PKG-INFO +1 -1
- {docassert-0.13.0 → docassert-0.14.0}/docassert/__init__.py +1 -1
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/project.schema.json +5 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/bridge/plan.py +33 -1
- {docassert-0.13.0 → docassert-0.14.0}/docassert/cli.py +53 -15
- {docassert-0.13.0 → docassert-0.14.0}/docassert/status/render.py +1 -1
- {docassert-0.13.0 → docassert-0.14.0/docassert.egg-info}/PKG-INFO +1 -1
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_bridge.py +92 -0
- {docassert-0.13.0 → docassert-0.14.0}/LICENSE +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/NOTICE +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/README.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/__main__.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/consistency.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/adr.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/brd.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/business-case.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/charter.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/frnfr.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/prd.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/risk-register.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/runbook.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/status-report.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/test-cases.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/user-story.schema.json +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/skills/doc-to-pmo/SKILL.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/adr.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/benefits-realization.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/brd.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/business-case.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/charter.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/data-migration-plan.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/frnfr.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/hypercare-plan.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/post-implementation-review.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/prd.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/project.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/qa-test-plan.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/risk-register.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/rollback-plan.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/runbook.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/status-report.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/test-cases.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/user-story.template.md +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/bridge/__init__.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/bridge/board.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/bridge/gh.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/bridge/ops.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/config.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/consistency.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/extract.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/graph.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/loader.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/models.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/profiles.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/projects.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/report.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/rtm.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/scaffold.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/semantic.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/status/__init__.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/status/derive.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert/structural.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert.egg-info/SOURCES.txt +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert.egg-info/dependency_links.txt +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert.egg-info/entry_points.txt +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert.egg-info/requires.txt +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/docassert.egg-info/top_level.txt +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/pyproject.toml +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/setup.cfg +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_badge.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_config.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_consistency.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_defects.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_draft_relaxation.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_extract.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_graph.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_json_report.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_kinds_delivery.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_kinds_governance.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_kinds_operate.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_kinds_reporting.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_profiles.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_projects.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_scaffold.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_semantic.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_status.py +0 -0
- {docassert-0.13.0 → docassert-0.14.0}/tests/test_structural.py +0 -0
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
"additionalProperties": true,
|
|
8
8
|
"properties": {
|
|
9
9
|
"kind": { "const": "project" },
|
|
10
|
+
"repo": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$",
|
|
13
|
+
"description": "Optional OWNER/NAME of the code repository this project's execution bridge targets."
|
|
14
|
+
},
|
|
10
15
|
"id": {
|
|
11
16
|
"type": "string",
|
|
12
17
|
"pattern": "^PRJ-\\d{3,}-[A-Z]{2,6}$",
|
|
@@ -83,7 +83,8 @@ def _anchors(documents_dir: Path) -> dict[str, dict]:
|
|
|
83
83
|
continue
|
|
84
84
|
if doc.kind == "project" and doc.id:
|
|
85
85
|
out[doc.id] = {"code": str(doc.frontmatter.get("code", "")),
|
|
86
|
-
"name": str(doc.frontmatter.get("name", ""))
|
|
86
|
+
"name": str(doc.frontmatter.get("name", "")),
|
|
87
|
+
"repo": str(doc.frontmatter.get("repo", "") or "")}
|
|
87
88
|
return out
|
|
88
89
|
|
|
89
90
|
|
|
@@ -155,10 +156,41 @@ def build_bridge_plan(documents_dir: str | Path = "documents") -> BridgePlan:
|
|
|
155
156
|
"reason": "; ".join(problems)})
|
|
156
157
|
continue
|
|
157
158
|
plan.projects.append({"id": proj_id, "code": code, "name": meta["name"],
|
|
159
|
+
"repo": meta.get("repo", ""),
|
|
158
160
|
"features": list(features.values())})
|
|
159
161
|
return plan
|
|
160
162
|
|
|
161
163
|
|
|
164
|
+
def filter_plan(plan: BridgePlan, project: str) -> BridgePlan:
|
|
165
|
+
"""A plan reduced to one project (matched by id or code). Unknown ids
|
|
166
|
+
yield an empty plan whose skipped list explains why."""
|
|
167
|
+
key = project.strip()
|
|
168
|
+
keep = [p for p in plan.projects
|
|
169
|
+
if p["id"] == key or p["code"] == key]
|
|
170
|
+
out = BridgePlan(projects=keep,
|
|
171
|
+
skipped=[s for s in plan.skipped if s["id"] == key])
|
|
172
|
+
if not keep and not out.skipped:
|
|
173
|
+
out.skipped.append({"id": key, "reason": "no such project in the plan"})
|
|
174
|
+
return out
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def plans_by_repo(plan: BridgePlan) -> dict[str, BridgePlan]:
|
|
178
|
+
"""Group a plan by each project's mapped repository (the `repo` field on
|
|
179
|
+
the project anchor). Projects sharing a repo share one sub-plan, so
|
|
180
|
+
reconciliation in that repo sees the union of their managed ids.
|
|
181
|
+
Raises ValueError naming any project without a mapping."""
|
|
182
|
+
unmapped = [p["id"] for p in plan.projects if not p.get("repo")]
|
|
183
|
+
if unmapped:
|
|
184
|
+
raise ValueError(
|
|
185
|
+
"no repo mapping on project anchor(s): " + ", ".join(unmapped)
|
|
186
|
+
+ " (add `repo: OWNER/NAME` to project.md or pass --repo)")
|
|
187
|
+
groups: dict[str, BridgePlan] = {}
|
|
188
|
+
for proj in plan.projects:
|
|
189
|
+
groups.setdefault(proj["repo"],
|
|
190
|
+
BridgePlan(skipped=plan.skipped)).projects.append(proj)
|
|
191
|
+
return groups
|
|
192
|
+
|
|
193
|
+
|
|
162
194
|
def feature_title(f: Feature) -> str:
|
|
163
195
|
return f"[{f.id}] {_title(f.text)}"
|
|
164
196
|
|
|
@@ -238,7 +238,7 @@ def cmd_pages(args: argparse.Namespace) -> int:
|
|
|
238
238
|
data = _json.loads(Path(args.execution).read_text())
|
|
239
239
|
for proj in data.get("projects", []):
|
|
240
240
|
execution[proj["id"]] = {**proj, "scope": data.get("scope"),
|
|
241
|
-
"repo": data.get("repo")}
|
|
241
|
+
"repo": proj.get("repo") or data.get("repo")}
|
|
242
242
|
|
|
243
243
|
plist = projects_mod.load_projects(docs_dir)
|
|
244
244
|
for p in plist:
|
|
@@ -306,10 +306,30 @@ def cmd_bridge(args: argparse.Namespace) -> int:
|
|
|
306
306
|
Scope flows documents -> GitHub only; the bridge never edits documents."""
|
|
307
307
|
from .bridge import build_bridge_plan, ops
|
|
308
308
|
from .bridge import gh as ghmod
|
|
309
|
+
from .bridge.plan import filter_plan, plans_by_repo
|
|
309
310
|
|
|
310
311
|
plan = build_bridge_plan(args.documents_dir)
|
|
312
|
+
if getattr(args, "project", None):
|
|
313
|
+
plan = filter_plan(plan, args.project)
|
|
311
314
|
gh = ghmod.DryRunner() if getattr(args, "dry_run", False) else ghmod.GhRunner()
|
|
312
315
|
|
|
316
|
+
# Repo resolution: an explicit --repo applies the whole plan to one
|
|
317
|
+
# repository (single-repo portfolios, back-compatible); without it, each
|
|
318
|
+
# project routes to the repo mapped on its anchor.
|
|
319
|
+
if args.action != "create-board":
|
|
320
|
+
if args.repo:
|
|
321
|
+
repo_plans = {args.repo: plan}
|
|
322
|
+
else:
|
|
323
|
+
try:
|
|
324
|
+
repo_plans = plans_by_repo(plan)
|
|
325
|
+
except ValueError as exc:
|
|
326
|
+
print(f"docassert bridge: {exc}", file=sys.stderr)
|
|
327
|
+
return 2
|
|
328
|
+
if not repo_plans and plan.skipped:
|
|
329
|
+
for s in plan.skipped:
|
|
330
|
+
print(f"docassert bridge: skipped {s['id']}: {s['reason']}")
|
|
331
|
+
return 0
|
|
332
|
+
|
|
313
333
|
if args.action == "create-board":
|
|
314
334
|
from .bridge import board as board_mod
|
|
315
335
|
token = os.environ.get(args.board_token_env, "")
|
|
@@ -329,30 +349,46 @@ def cmd_bridge(args: argparse.Namespace) -> int:
|
|
|
329
349
|
"(needed for --project-number)", file=sys.stderr)
|
|
330
350
|
return 2
|
|
331
351
|
bgh = gh if isinstance(gh, ghmod.DryRunner) else ghmod.GhRunner(token=token)
|
|
332
|
-
|
|
352
|
+
owner = args.project_owner or next(iter(repo_plans)).split("/")[0]
|
|
353
|
+
board_cfg = {"gh": bgh, "owner": owner,
|
|
333
354
|
"number": args.project_number, "init": args.init_board}
|
|
334
355
|
|
|
335
356
|
if args.action == "scaffold":
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
357
|
+
for repo, sub in repo_plans.items():
|
|
358
|
+
actions = ops.scaffold(sub, gh, repo, docs_url=args.docs_url,
|
|
359
|
+
board_cfg=board_cfg)
|
|
360
|
+
for a in actions:
|
|
361
|
+
print(f"docassert bridge: [{repo}] {a}")
|
|
340
362
|
if isinstance(gh, ghmod.DryRunner):
|
|
341
363
|
print(f"docassert bridge: dry run — {len(gh.planned)} mutation(s) planned")
|
|
342
364
|
for m in gh.planned:
|
|
343
365
|
print(f" {m}")
|
|
344
366
|
return 0
|
|
345
367
|
if args.action == "reconcile":
|
|
346
|
-
|
|
347
|
-
for
|
|
348
|
-
|
|
349
|
-
|
|
368
|
+
worst = 0
|
|
369
|
+
for repo, sub in repo_plans.items():
|
|
370
|
+
lines, code = ops.reconcile(sub, gh, repo)
|
|
371
|
+
for line in lines:
|
|
372
|
+
print(f"docassert bridge: [{repo}] {line}")
|
|
373
|
+
worst = max(worst, code)
|
|
374
|
+
return worst
|
|
350
375
|
if args.action == "status":
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
376
|
+
merged: dict = {"repo": None, "projects": [], "scope": {"unverified": [], "orphaned": []}}
|
|
377
|
+
for repo, sub in repo_plans.items():
|
|
378
|
+
data = ops.status(sub, gh, repo)
|
|
379
|
+
for proj in data.get("projects", []):
|
|
380
|
+
proj["repo"] = repo
|
|
381
|
+
merged["projects"].append(proj)
|
|
382
|
+
for key in ("unverified", "orphaned"):
|
|
383
|
+
for item in data.get("scope", {}).get(key, []):
|
|
384
|
+
item["repo"] = repo
|
|
385
|
+
merged["scope"][key].append(item)
|
|
386
|
+
if len(repo_plans) == 1:
|
|
387
|
+
merged["repo"] = next(iter(repo_plans))
|
|
388
|
+
sys.stdout.write(ops.to_json(merged) if args.json else
|
|
389
|
+
ops.render_status(merged) + "\n")
|
|
354
390
|
if args.json and args.out:
|
|
355
|
-
Path(args.out).write_text(ops.to_json(
|
|
391
|
+
Path(args.out).write_text(ops.to_json(merged))
|
|
356
392
|
return 0
|
|
357
393
|
print(f"docassert bridge: unknown action {args.action!r}", file=sys.stderr)
|
|
358
394
|
return 2
|
|
@@ -441,7 +477,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
441
477
|
b = sub.add_parser("bridge", help="Execution bridge: scaffold and police the GitHub board from approved stories.")
|
|
442
478
|
b.add_argument("action", choices=["scaffold", "reconcile", "status", "create-board"],
|
|
443
479
|
help="scaffold: docs -> issues/board · reconcile: police the board · status: delivery figures.")
|
|
444
|
-
b.add_argument("--repo",
|
|
480
|
+
b.add_argument("--repo", help="Target GitHub repo (OWNER/NAME) for the whole plan; "
|
|
481
|
+
"omit to route each project to the `repo:` on its anchor.")
|
|
482
|
+
b.add_argument("--project", help="Scope to one project (PRJ-NNN-CODE id or CODE).")
|
|
445
483
|
b.add_argument("--docs-url", help="Base URL of the documents repo, for source links in issue bodies.")
|
|
446
484
|
b.add_argument("--dry-run", action="store_true", help="Print planned mutations without executing them.")
|
|
447
485
|
b.add_argument("--json", action="store_true", help="status: emit JSON.")
|
|
@@ -248,7 +248,7 @@ def render_html(model) -> str:
|
|
|
248
248
|
bad = scope.get("unverified", []) + scope.get("orphaned", [])
|
|
249
249
|
if bad:
|
|
250
250
|
items = "".join(
|
|
251
|
-
f'<li><a href="https://github.com/{esc(ex.get("repo"
|
|
251
|
+
f'<li><a href="https://github.com/{esc(i.get("repo") or ex.get("repo") or "")}/issues/{i["number"]}">#{i["number"]}</a> {esc(i["title"])}</li>'
|
|
252
252
|
for i in bad)
|
|
253
253
|
execution_html += (
|
|
254
254
|
f'<section><h2 style="color:var(--bad)">Scope · {len(bad)} unmatched item(s)</h2>'
|
|
@@ -349,3 +349,95 @@ def test_status_includes_scope_classification(tmp_path):
|
|
|
349
349
|
assert data["repo"] == "o/r"
|
|
350
350
|
assert [i["number"] for i in data["scope"]["unverified"]] == [2]
|
|
351
351
|
assert [i["doc"] for i in data["scope"]["orphaned"]] == ["TST-US-999"]
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# ── project filter and per-repo mapping (ENG-US-001 / ENG-US-002) ────────────
|
|
355
|
+
from docassert.bridge.plan import filter_plan, plans_by_repo # noqa: E402
|
|
356
|
+
|
|
357
|
+
ANCHOR2 = """---
|
|
358
|
+
kind: project
|
|
359
|
+
id: PRJ-002-TWO
|
|
360
|
+
code: TWO
|
|
361
|
+
name: Second Project
|
|
362
|
+
sponsor: S
|
|
363
|
+
status: active
|
|
364
|
+
repo: owner/two-repo
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Overview
|
|
368
|
+
|
|
369
|
+
x
|
|
370
|
+
|
|
371
|
+
## Scope
|
|
372
|
+
|
|
373
|
+
x
|
|
374
|
+
"""
|
|
375
|
+
|
|
376
|
+
PRD2 = PRD.replace("PRJ-001-TST", "PRJ-002-TWO").replace("TST", "TWO")
|
|
377
|
+
STORIES2 = STORIES.replace("PRJ-001-TST", "PRJ-002-TWO").replace("TST", "TWO")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _two_project_tree(tmp_path, repo_one="owner/one-repo"):
|
|
381
|
+
docs = _tree(tmp_path)
|
|
382
|
+
if repo_one:
|
|
383
|
+
anchor = docs / "PRJ-001-TST" / "project.md"
|
|
384
|
+
anchor.write_text(anchor.read_text().replace(
|
|
385
|
+
"status: active", f"status: active\nrepo: {repo_one}"), encoding="utf-8")
|
|
386
|
+
d = docs / "PRJ-002-TWO"
|
|
387
|
+
d.mkdir()
|
|
388
|
+
(d / "project.md").write_text(ANCHOR2, encoding="utf-8")
|
|
389
|
+
(d / "prd.md").write_text(PRD2, encoding="utf-8")
|
|
390
|
+
(d / "user-story.md").write_text(STORIES2.format(status="approved"), encoding="utf-8")
|
|
391
|
+
return docs
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def test_filter_plan_by_id_and_code(tmp_path):
|
|
395
|
+
plan = build_bridge_plan(_two_project_tree(tmp_path))
|
|
396
|
+
assert len(plan.projects) == 2
|
|
397
|
+
only = filter_plan(plan, "PRJ-002-TWO")
|
|
398
|
+
assert [p["id"] for p in only.projects] == ["PRJ-002-TWO"]
|
|
399
|
+
by_code = filter_plan(plan, "TST")
|
|
400
|
+
assert [p["code"] for p in by_code.projects] == ["TST"]
|
|
401
|
+
missing = filter_plan(plan, "PRJ-999-NOPE")
|
|
402
|
+
assert not missing.projects and missing.skipped
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def test_plans_by_repo_routes_each_project(tmp_path):
|
|
406
|
+
plan = build_bridge_plan(_two_project_tree(tmp_path))
|
|
407
|
+
groups = plans_by_repo(plan)
|
|
408
|
+
assert set(groups) == {"owner/one-repo", "owner/two-repo"}
|
|
409
|
+
assert [p["id"] for p in groups["owner/one-repo"].projects] == ["PRJ-001-TST"]
|
|
410
|
+
assert [p["id"] for p in groups["owner/two-repo"].projects] == ["PRJ-002-TWO"]
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def test_plans_by_repo_shares_a_repo_as_a_union(tmp_path):
|
|
414
|
+
docs = _two_project_tree(tmp_path)
|
|
415
|
+
anchor = docs / "PRJ-002-TWO" / "project.md"
|
|
416
|
+
anchor.write_text(anchor.read_text().replace("owner/two-repo", "owner/one-repo"),
|
|
417
|
+
encoding="utf-8")
|
|
418
|
+
plan = build_bridge_plan(docs)
|
|
419
|
+
groups = plans_by_repo(plan)
|
|
420
|
+
assert set(groups) == {"owner/one-repo"}
|
|
421
|
+
union = groups["owner/one-repo"]
|
|
422
|
+
assert len(union.projects) == 2
|
|
423
|
+
# both projects' ids are managed, so neither would be orphaned in reconcile
|
|
424
|
+
assert any(i.startswith("TST-") for i in union.managed_ids)
|
|
425
|
+
assert any(i.startswith("TWO-") for i in union.managed_ids)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def test_plans_by_repo_names_unmapped_projects(tmp_path):
|
|
429
|
+
plan = build_bridge_plan(_two_project_tree(tmp_path, repo_one=None))
|
|
430
|
+
try:
|
|
431
|
+
plans_by_repo(plan)
|
|
432
|
+
raise AssertionError("expected ValueError")
|
|
433
|
+
except ValueError as exc:
|
|
434
|
+
assert "PRJ-001-TST" in str(exc) and "--repo" in str(exc)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def test_anchor_without_repo_keeps_single_repo_mode_working(tmp_path):
|
|
438
|
+
# the original single-repo path: no repo fields, explicit --repo, unchanged
|
|
439
|
+
plan = build_bridge_plan(_tree(tmp_path))
|
|
440
|
+
assert plan.projects[0].get("repo", "") == ""
|
|
441
|
+
gh = FakeGh()
|
|
442
|
+
ops.scaffold(plan, gh, "o/r", docs_url=None)
|
|
443
|
+
assert any("issues" in " ".join(c) for c in gh.calls)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/benefits-realization.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.13.0 → docassert-0.14.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml
RENAMED
|
File without changes
|
{docassert-0.13.0 → docassert-0.14.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
|
{docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/benefits-realization.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/data-migration-plan.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.13.0 → docassert-0.14.0}/docassert/_data/schema/post-implementation-review.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.13.0 → docassert-0.14.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.13.0 → docassert-0.14.0}/docassert/_data/templates/benefits-realization.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/data-migration-plan.template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docassert-0.13.0 → docassert-0.14.0}/docassert/_data/templates/raci-stakeholder.template.md
RENAMED
|
File without changes
|
{docassert-0.13.0 → docassert-0.14.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
|