docassert 0.14.0__tar.gz → 0.14.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 (120) hide show
  1. {docassert-0.14.0/docassert.egg-info → docassert-0.14.1}/PKG-INFO +1 -1
  2. {docassert-0.14.0 → docassert-0.14.1}/docassert/__init__.py +1 -1
  3. {docassert-0.14.0 → docassert-0.14.1}/docassert/bridge/board.py +23 -17
  4. {docassert-0.14.0 → docassert-0.14.1}/docassert/bridge/ops.py +29 -0
  5. {docassert-0.14.0 → docassert-0.14.1/docassert.egg-info}/PKG-INFO +1 -1
  6. {docassert-0.14.0 → docassert-0.14.1}/tests/test_bridge.py +29 -0
  7. {docassert-0.14.0 → docassert-0.14.1}/LICENSE +0 -0
  8. {docassert-0.14.0 → docassert-0.14.1}/NOTICE +0 -0
  9. {docassert-0.14.0 → docassert-0.14.1}/README.md +0 -0
  10. {docassert-0.14.0 → docassert-0.14.1}/docassert/__main__.py +0 -0
  11. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/consistency.yaml +0 -0
  12. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/adr.criteria.yaml +0 -0
  13. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
  14. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/brd.criteria.yaml +0 -0
  15. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
  16. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/charter.criteria.yaml +0 -0
  17. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
  18. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
  19. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
  20. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
  21. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/prd.criteria.yaml +0 -0
  22. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/project.criteria.yaml +0 -0
  23. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
  24. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
  25. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
  26. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
  27. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
  28. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
  29. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
  30. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
  31. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
  32. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/profiles/agile-delivery.yaml +0 -0
  33. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/profiles/lean-startup.yaml +0 -0
  34. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/profiles/regulated-industry.yaml +0 -0
  35. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/adr.schema.json +0 -0
  36. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/benefits-realization.schema.json +0 -0
  37. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/brd.schema.json +0 -0
  38. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/business-case.schema.json +0 -0
  39. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/charter.schema.json +0 -0
  40. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
  41. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/frnfr.schema.json +0 -0
  42. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
  43. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
  44. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/prd.schema.json +0 -0
  45. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/project.schema.json +0 -0
  46. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
  47. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
  48. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
  49. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/risk-register.schema.json +0 -0
  50. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/rollback-plan.schema.json +0 -0
  51. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/runbook.schema.json +0 -0
  52. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/status-report.schema.json +0 -0
  53. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/test-cases.schema.json +0 -0
  54. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/schema/user-story.schema.json +0 -0
  55. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/skills/doc-to-pmo/SKILL.md +0 -0
  56. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/adr.template.md +0 -0
  57. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/benefits-realization.template.md +0 -0
  58. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/brd.template.md +0 -0
  59. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/business-case.template.md +0 -0
  60. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/charter.template.md +0 -0
  61. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/data-migration-plan.template.md +0 -0
  62. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/frnfr.template.md +0 -0
  63. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/hypercare-plan.template.md +0 -0
  64. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/post-implementation-review.template.md +0 -0
  65. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/prd.template.md +0 -0
  66. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/project.template.md +0 -0
  67. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/qa-test-plan.template.md +0 -0
  68. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
  69. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
  70. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/risk-register.template.md +0 -0
  71. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/rollback-plan.template.md +0 -0
  72. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/runbook.template.md +0 -0
  73. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/status-report.template.md +0 -0
  74. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/test-cases.template.md +0 -0
  75. {docassert-0.14.0 → docassert-0.14.1}/docassert/_data/templates/user-story.template.md +0 -0
  76. {docassert-0.14.0 → docassert-0.14.1}/docassert/bridge/__init__.py +0 -0
  77. {docassert-0.14.0 → docassert-0.14.1}/docassert/bridge/gh.py +0 -0
  78. {docassert-0.14.0 → docassert-0.14.1}/docassert/bridge/plan.py +0 -0
  79. {docassert-0.14.0 → docassert-0.14.1}/docassert/cli.py +0 -0
  80. {docassert-0.14.0 → docassert-0.14.1}/docassert/config.py +0 -0
  81. {docassert-0.14.0 → docassert-0.14.1}/docassert/consistency.py +0 -0
  82. {docassert-0.14.0 → docassert-0.14.1}/docassert/extract.py +0 -0
  83. {docassert-0.14.0 → docassert-0.14.1}/docassert/graph.py +0 -0
  84. {docassert-0.14.0 → docassert-0.14.1}/docassert/loader.py +0 -0
  85. {docassert-0.14.0 → docassert-0.14.1}/docassert/models.py +0 -0
  86. {docassert-0.14.0 → docassert-0.14.1}/docassert/profiles.py +0 -0
  87. {docassert-0.14.0 → docassert-0.14.1}/docassert/projects.py +0 -0
  88. {docassert-0.14.0 → docassert-0.14.1}/docassert/report.py +0 -0
  89. {docassert-0.14.0 → docassert-0.14.1}/docassert/rtm.py +0 -0
  90. {docassert-0.14.0 → docassert-0.14.1}/docassert/scaffold.py +0 -0
  91. {docassert-0.14.0 → docassert-0.14.1}/docassert/semantic.py +0 -0
  92. {docassert-0.14.0 → docassert-0.14.1}/docassert/status/__init__.py +0 -0
  93. {docassert-0.14.0 → docassert-0.14.1}/docassert/status/derive.py +0 -0
  94. {docassert-0.14.0 → docassert-0.14.1}/docassert/status/render.py +0 -0
  95. {docassert-0.14.0 → docassert-0.14.1}/docassert/structural.py +0 -0
  96. {docassert-0.14.0 → docassert-0.14.1}/docassert.egg-info/SOURCES.txt +0 -0
  97. {docassert-0.14.0 → docassert-0.14.1}/docassert.egg-info/dependency_links.txt +0 -0
  98. {docassert-0.14.0 → docassert-0.14.1}/docassert.egg-info/entry_points.txt +0 -0
  99. {docassert-0.14.0 → docassert-0.14.1}/docassert.egg-info/requires.txt +0 -0
  100. {docassert-0.14.0 → docassert-0.14.1}/docassert.egg-info/top_level.txt +0 -0
  101. {docassert-0.14.0 → docassert-0.14.1}/pyproject.toml +0 -0
  102. {docassert-0.14.0 → docassert-0.14.1}/setup.cfg +0 -0
  103. {docassert-0.14.0 → docassert-0.14.1}/tests/test_badge.py +0 -0
  104. {docassert-0.14.0 → docassert-0.14.1}/tests/test_config.py +0 -0
  105. {docassert-0.14.0 → docassert-0.14.1}/tests/test_consistency.py +0 -0
  106. {docassert-0.14.0 → docassert-0.14.1}/tests/test_defects.py +0 -0
  107. {docassert-0.14.0 → docassert-0.14.1}/tests/test_draft_relaxation.py +0 -0
  108. {docassert-0.14.0 → docassert-0.14.1}/tests/test_extract.py +0 -0
  109. {docassert-0.14.0 → docassert-0.14.1}/tests/test_graph.py +0 -0
  110. {docassert-0.14.0 → docassert-0.14.1}/tests/test_json_report.py +0 -0
  111. {docassert-0.14.0 → docassert-0.14.1}/tests/test_kinds_delivery.py +0 -0
  112. {docassert-0.14.0 → docassert-0.14.1}/tests/test_kinds_governance.py +0 -0
  113. {docassert-0.14.0 → docassert-0.14.1}/tests/test_kinds_operate.py +0 -0
  114. {docassert-0.14.0 → docassert-0.14.1}/tests/test_kinds_reporting.py +0 -0
  115. {docassert-0.14.0 → docassert-0.14.1}/tests/test_profiles.py +0 -0
  116. {docassert-0.14.0 → docassert-0.14.1}/tests/test_projects.py +0 -0
  117. {docassert-0.14.0 → docassert-0.14.1}/tests/test_scaffold.py +0 -0
  118. {docassert-0.14.0 → docassert-0.14.1}/tests/test_semantic.py +0 -0
  119. {docassert-0.14.0 → docassert-0.14.1}/tests/test_status.py +0 -0
  120. {docassert-0.14.0 → docassert-0.14.1}/tests/test_structural.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docassert
3
- Version: 0.14.0
3
+ Version: 0.14.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.14.0"
8
+ __version__ = "0.14.1"
@@ -7,7 +7,7 @@ writes converge on the same values.
7
7
  """
8
8
  from __future__ import annotations
9
9
 
10
- from .gh import GhRunner
10
+ from .gh import GhError, GhRunner
11
11
 
12
12
  FIELD_SPECS: dict[str, dict] = {
13
13
  "Type": {"dataType": "SINGLE_SELECT", "options": ["Feature", "Story"]},
@@ -67,22 +67,28 @@ def ensure_fields(gh: GhRunner, owner: str, number: int) -> dict:
67
67
  for name, spec in FIELD_SPECS.items():
68
68
  if name in project["fields"]:
69
69
  continue
70
- if spec["dataType"] == "SINGLE_SELECT":
71
- opts = ", ".join(
72
- f'{{name: "{o}", color: GRAY, description: ""}}'
73
- for o in spec["options"])
74
- gh.graphql(
75
- "mutation($p: ID!, $n: String!) { createProjectV2Field(input: {"
76
- "projectId: $p, dataType: SINGLE_SELECT, name: $n, "
77
- f"singleSelectOptions: [{opts}]"
78
- "}) { projectV2Field { ... on ProjectV2FieldCommon { id } } } }",
79
- p=project["id"], n=name)
80
- else:
81
- gh.graphql(
82
- "mutation($p: ID!, $n: String!) { createProjectV2Field(input: {"
83
- "projectId: $p, dataType: TEXT, name: $n"
84
- "}) { projectV2Field { ... on ProjectV2FieldCommon { id } } } }",
85
- p=project["id"], n=name)
70
+ try:
71
+ if spec["dataType"] == "SINGLE_SELECT":
72
+ opts = ", ".join(
73
+ f'{{name: "{o}", color: GRAY, description: ""}}'
74
+ for o in spec["options"])
75
+ gh.graphql(
76
+ "mutation($p: ID!, $n: String!) { createProjectV2Field(input: {"
77
+ "projectId: $p, dataType: SINGLE_SELECT, name: $n, "
78
+ f"singleSelectOptions: [{opts}]"
79
+ "}) { projectV2Field { ... on ProjectV2FieldCommon { id } } } }",
80
+ p=project["id"], n=name)
81
+ else:
82
+ gh.graphql(
83
+ "mutation($p: ID!, $n: String!) { createProjectV2Field(input: {"
84
+ "projectId: $p, dataType: TEXT, name: $n"
85
+ "}) { projectV2Field { ... on ProjectV2FieldCommon { id } } } }",
86
+ p=project["id"], n=name)
87
+ except GhError as exc:
88
+ # A concurrent sync created it between our read and our write;
89
+ # an existing field is the outcome we wanted.
90
+ if "already been taken" not in str(exc).lower():
91
+ raise
86
92
  return get_project(gh, owner, number)
87
93
 
88
94
 
@@ -45,6 +45,33 @@ def _index(issues: list[dict]) -> dict[str, dict]:
45
45
  return out
46
46
 
47
47
 
48
+ def _converge_duplicates(gh, repo: str, issues: list[dict]) -> tuple[list[dict], list[str]]:
49
+ """One open issue per bridge marker: racing scaffolds may have created
50
+ duplicates, so close every open duplicate except the lowest-numbered one
51
+ and drop the closed ones from the working set. First writer wins."""
52
+ from collections import defaultdict
53
+ by_marker: dict[str, list[dict]] = defaultdict(list)
54
+ for issue in issues:
55
+ m = MARKER_RE.search(issue.get("body") or "")
56
+ if m and issue.get("state") == "open":
57
+ by_marker[m.group(1)].append(issue)
58
+ actions: list[str] = []
59
+ losers: set[int] = set()
60
+ for marker, dupes in by_marker.items():
61
+ if len(dupes) < 2:
62
+ continue
63
+ dupes.sort(key=lambda i: i["number"])
64
+ for extra in dupes[1:]:
65
+ G.update_issue(gh, repo, extra["number"], state="closed")
66
+ G.comment(gh, repo, extra["number"],
67
+ f"Duplicate of #{dupes[0]['number']} (bridge marker "
68
+ f"{marker}); racing scaffold runs converge on the "
69
+ "lowest-numbered issue.")
70
+ losers.add(extra["number"])
71
+ actions.append(f"converged duplicate #{extra['number']} -> #{dupes[0]['number']}")
72
+ return [i for i in issues if i["number"] not in losers], actions
73
+
74
+
48
75
  def scaffold(plan: BridgePlan, gh: G.GhRunner, repo: str,
49
76
  docs_url: str | None = None,
50
77
  board_cfg: dict | None = None) -> list[str]:
@@ -59,6 +86,8 @@ def scaffold(plan: BridgePlan, gh: G.GhRunner, repo: str,
59
86
  G.ensure_label(gh, repo, name, color, desc)
60
87
 
61
88
  issues = G.list_issues(gh, repo)
89
+ issues, converge_actions = _converge_duplicates(gh, repo, issues)
90
+ actions.extend(converge_actions)
62
91
  by_key = _index(issues)
63
92
 
64
93
  def ensure(key: str, title: str, body: str, label: str) -> dict:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docassert
3
- Version: 0.14.0
3
+ Version: 0.14.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
@@ -441,3 +441,32 @@ def test_anchor_without_repo_keeps_single_repo_mode_working(tmp_path):
441
441
  gh = FakeGh()
442
442
  ops.scaffold(plan, gh, "o/r", docs_url=None)
443
443
  assert any("issues" in " ".join(c) for c in gh.calls)
444
+
445
+
446
+ # ── concurrency safety (ENG-PR-006) ──────────────────────────────────────────
447
+ def test_scaffold_converges_racing_duplicates(tmp_path):
448
+ plan = build_bridge_plan(_tree(tmp_path))
449
+ fid = plan.features[0].id
450
+ dupes = [_mk_issue(5, fid), _mk_issue(9, fid)]
451
+ gh = FakeGh(issues=dupes)
452
+ actions = ops.scaffold(plan, gh, "o/r", docs_url=None)
453
+ assert any("converged duplicate #9" in a for a in actions)
454
+ patches = [c for c in gh.calls if "repos/o/r/issues/9" in " ".join(c)
455
+ and "state=closed" in " ".join(c)]
456
+ assert patches, "the higher-numbered duplicate is closed"
457
+
458
+
459
+ def test_ensure_fields_tolerates_existing_field():
460
+ gh = BoardGh()
461
+ original = gh.graphql
462
+
463
+ def racing(query, **kw):
464
+ if "createProjectV2Field" in query:
465
+ from docassert.bridge.gh import GhError
466
+ raise GhError('[{"message": "Name has already been taken"}]')
467
+ return original(query, **kw)
468
+
469
+ gh.graphql = racing
470
+ from docassert.bridge import board
471
+ project = board.ensure_fields(gh, "o", 1) # must not raise
472
+ assert project["id"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes