docassert 0.8.1__tar.gz → 0.9.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.
Files changed (119) hide show
  1. {docassert-0.8.1/docassert.egg-info → docassert-0.9.0}/PKG-INFO +1 -1
  2. {docassert-0.8.1 → docassert-0.9.0}/docassert/__init__.py +1 -1
  3. docassert-0.9.0/docassert/bridge/board.py +126 -0
  4. {docassert-0.8.1 → docassert-0.9.0}/docassert/bridge/gh.py +15 -3
  5. {docassert-0.8.1 → docassert-0.9.0}/docassert/bridge/ops.py +30 -2
  6. {docassert-0.8.1 → docassert-0.9.0}/docassert/cli.py +31 -2
  7. {docassert-0.8.1 → docassert-0.9.0/docassert.egg-info}/PKG-INFO +1 -1
  8. {docassert-0.8.1 → docassert-0.9.0}/docassert.egg-info/SOURCES.txt +1 -0
  9. {docassert-0.8.1 → docassert-0.9.0}/tests/test_bridge.py +73 -0
  10. {docassert-0.8.1 → docassert-0.9.0}/LICENSE +0 -0
  11. {docassert-0.8.1 → docassert-0.9.0}/NOTICE +0 -0
  12. {docassert-0.8.1 → docassert-0.9.0}/README.md +0 -0
  13. {docassert-0.8.1 → docassert-0.9.0}/docassert/__main__.py +0 -0
  14. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/consistency.yaml +0 -0
  15. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/adr.criteria.yaml +0 -0
  16. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/benefits-realization.criteria.yaml +0 -0
  17. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/brd.criteria.yaml +0 -0
  18. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/business-case.criteria.yaml +0 -0
  19. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/charter.criteria.yaml +0 -0
  20. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/data-migration-plan.criteria.yaml +0 -0
  21. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/frnfr.criteria.yaml +0 -0
  22. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/hypercare-plan.criteria.yaml +0 -0
  23. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/post-implementation-review.criteria.yaml +0 -0
  24. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/prd.criteria.yaml +0 -0
  25. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/project.criteria.yaml +0 -0
  26. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/qa-test-plan.criteria.yaml +0 -0
  27. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/raci-stakeholder.criteria.yaml +0 -0
  28. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/release-cutover-plan.criteria.yaml +0 -0
  29. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/risk-register.criteria.yaml +0 -0
  30. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/rollback-plan.criteria.yaml +0 -0
  31. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/runbook.criteria.yaml +0 -0
  32. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/status-report.criteria.yaml +0 -0
  33. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/test-cases.criteria.yaml +0 -0
  34. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/criteria/user-story.criteria.yaml +0 -0
  35. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/profiles/agile-delivery.yaml +0 -0
  36. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/profiles/lean-startup.yaml +0 -0
  37. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/profiles/regulated-industry.yaml +0 -0
  38. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/adr.schema.json +0 -0
  39. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/benefits-realization.schema.json +0 -0
  40. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/brd.schema.json +0 -0
  41. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/business-case.schema.json +0 -0
  42. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/charter.schema.json +0 -0
  43. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/data-migration-plan.schema.json +0 -0
  44. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/frnfr.schema.json +0 -0
  45. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/hypercare-plan.schema.json +0 -0
  46. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/post-implementation-review.schema.json +0 -0
  47. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/prd.schema.json +0 -0
  48. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/project.schema.json +0 -0
  49. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/qa-test-plan.schema.json +0 -0
  50. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/raci-stakeholder.schema.json +0 -0
  51. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/release-cutover-plan.schema.json +0 -0
  52. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/risk-register.schema.json +0 -0
  53. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/rollback-plan.schema.json +0 -0
  54. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/runbook.schema.json +0 -0
  55. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/status-report.schema.json +0 -0
  56. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/test-cases.schema.json +0 -0
  57. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/schema/user-story.schema.json +0 -0
  58. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/skills/doc-to-pmo/SKILL.md +0 -0
  59. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/adr.template.md +0 -0
  60. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/benefits-realization.template.md +0 -0
  61. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/brd.template.md +0 -0
  62. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/business-case.template.md +0 -0
  63. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/charter.template.md +0 -0
  64. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/data-migration-plan.template.md +0 -0
  65. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/frnfr.template.md +0 -0
  66. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/hypercare-plan.template.md +0 -0
  67. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/post-implementation-review.template.md +0 -0
  68. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/prd.template.md +0 -0
  69. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/project.template.md +0 -0
  70. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/qa-test-plan.template.md +0 -0
  71. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/raci-stakeholder.template.md +0 -0
  72. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/release-cutover-plan.template.md +0 -0
  73. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/risk-register.template.md +0 -0
  74. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/rollback-plan.template.md +0 -0
  75. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/runbook.template.md +0 -0
  76. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/status-report.template.md +0 -0
  77. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/test-cases.template.md +0 -0
  78. {docassert-0.8.1 → docassert-0.9.0}/docassert/_data/templates/user-story.template.md +0 -0
  79. {docassert-0.8.1 → docassert-0.9.0}/docassert/bridge/__init__.py +0 -0
  80. {docassert-0.8.1 → docassert-0.9.0}/docassert/bridge/plan.py +0 -0
  81. {docassert-0.8.1 → docassert-0.9.0}/docassert/config.py +0 -0
  82. {docassert-0.8.1 → docassert-0.9.0}/docassert/consistency.py +0 -0
  83. {docassert-0.8.1 → docassert-0.9.0}/docassert/extract.py +0 -0
  84. {docassert-0.8.1 → docassert-0.9.0}/docassert/graph.py +0 -0
  85. {docassert-0.8.1 → docassert-0.9.0}/docassert/loader.py +0 -0
  86. {docassert-0.8.1 → docassert-0.9.0}/docassert/models.py +0 -0
  87. {docassert-0.8.1 → docassert-0.9.0}/docassert/profiles.py +0 -0
  88. {docassert-0.8.1 → docassert-0.9.0}/docassert/projects.py +0 -0
  89. {docassert-0.8.1 → docassert-0.9.0}/docassert/report.py +0 -0
  90. {docassert-0.8.1 → docassert-0.9.0}/docassert/rtm.py +0 -0
  91. {docassert-0.8.1 → docassert-0.9.0}/docassert/scaffold.py +0 -0
  92. {docassert-0.8.1 → docassert-0.9.0}/docassert/semantic.py +0 -0
  93. {docassert-0.8.1 → docassert-0.9.0}/docassert/status/__init__.py +0 -0
  94. {docassert-0.8.1 → docassert-0.9.0}/docassert/status/derive.py +0 -0
  95. {docassert-0.8.1 → docassert-0.9.0}/docassert/status/render.py +0 -0
  96. {docassert-0.8.1 → docassert-0.9.0}/docassert/structural.py +0 -0
  97. {docassert-0.8.1 → docassert-0.9.0}/docassert.egg-info/dependency_links.txt +0 -0
  98. {docassert-0.8.1 → docassert-0.9.0}/docassert.egg-info/entry_points.txt +0 -0
  99. {docassert-0.8.1 → docassert-0.9.0}/docassert.egg-info/requires.txt +0 -0
  100. {docassert-0.8.1 → docassert-0.9.0}/docassert.egg-info/top_level.txt +0 -0
  101. {docassert-0.8.1 → docassert-0.9.0}/pyproject.toml +0 -0
  102. {docassert-0.8.1 → docassert-0.9.0}/setup.cfg +0 -0
  103. {docassert-0.8.1 → docassert-0.9.0}/tests/test_badge.py +0 -0
  104. {docassert-0.8.1 → docassert-0.9.0}/tests/test_config.py +0 -0
  105. {docassert-0.8.1 → docassert-0.9.0}/tests/test_consistency.py +0 -0
  106. {docassert-0.8.1 → docassert-0.9.0}/tests/test_defects.py +0 -0
  107. {docassert-0.8.1 → docassert-0.9.0}/tests/test_draft_relaxation.py +0 -0
  108. {docassert-0.8.1 → docassert-0.9.0}/tests/test_extract.py +0 -0
  109. {docassert-0.8.1 → docassert-0.9.0}/tests/test_graph.py +0 -0
  110. {docassert-0.8.1 → docassert-0.9.0}/tests/test_json_report.py +0 -0
  111. {docassert-0.8.1 → docassert-0.9.0}/tests/test_kinds_delivery.py +0 -0
  112. {docassert-0.8.1 → docassert-0.9.0}/tests/test_kinds_governance.py +0 -0
  113. {docassert-0.8.1 → docassert-0.9.0}/tests/test_kinds_operate.py +0 -0
  114. {docassert-0.8.1 → docassert-0.9.0}/tests/test_kinds_reporting.py +0 -0
  115. {docassert-0.8.1 → docassert-0.9.0}/tests/test_profiles.py +0 -0
  116. {docassert-0.8.1 → docassert-0.9.0}/tests/test_projects.py +0 -0
  117. {docassert-0.8.1 → docassert-0.9.0}/tests/test_scaffold.py +0 -0
  118. {docassert-0.8.1 → docassert-0.9.0}/tests/test_status.py +0 -0
  119. {docassert-0.8.1 → docassert-0.9.0}/tests/test_structural.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docassert
3
- Version: 0.8.1
3
+ Version: 0.9.0
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.8.1"
8
+ __version__ = "0.9.0"
@@ -0,0 +1,126 @@
1
+ """Projects v2 board operations (require a token with the `project` scope).
2
+
3
+ The board is a *view* over the bridge-managed issues: every Feature and Story
4
+ issue becomes a project item carrying Type, Doc, and PMO Project fields. All
5
+ mutations are idempotent: re-adding an existing item returns it, and field
6
+ writes converge on the same values.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from .gh import GhRunner
11
+
12
+ FIELD_SPECS: dict[str, dict] = {
13
+ "Type": {"dataType": "SINGLE_SELECT", "options": ["Feature", "Story"]},
14
+ "Doc": {"dataType": "TEXT"},
15
+ "PMO Project": {"dataType": "TEXT"},
16
+ }
17
+
18
+ _FIELDS_QUERY = """
19
+ query($owner: String!, $number: Int!) {
20
+ user(login: $owner) {
21
+ projectV2(number: $number) {
22
+ id
23
+ title
24
+ fields(first: 30) {
25
+ nodes {
26
+ ... on ProjectV2FieldCommon { id name dataType }
27
+ ... on ProjectV2SingleSelectField { id name dataType options { id name } }
28
+ }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ """
34
+
35
+
36
+ def create_project(gh: GhRunner, title: str) -> dict:
37
+ viewer = gh.graphql("query { viewer { id login } }")["viewer"]
38
+ data = gh.graphql(
39
+ """
40
+ mutation($owner: ID!, $title: String!) {
41
+ createProjectV2(input: {ownerId: $owner, title: $title}) {
42
+ projectV2 { id number title }
43
+ }
44
+ }
45
+ """,
46
+ owner=viewer["id"], title=title)
47
+ return data["createProjectV2"]["projectV2"]
48
+
49
+
50
+ def get_project(gh: GhRunner, owner: str, number: int) -> dict:
51
+ proj = gh.graphql(_FIELDS_QUERY, owner=owner, number=number)["user"]["projectV2"]
52
+ fields = {}
53
+ for node in proj["fields"]["nodes"]:
54
+ if not node:
55
+ continue
56
+ fields[node["name"]] = {
57
+ "id": node["id"],
58
+ "dataType": node.get("dataType"),
59
+ "options": {o["name"]: o["id"] for o in node.get("options", [])},
60
+ }
61
+ return {"id": proj["id"], "title": proj["title"], "fields": fields}
62
+
63
+
64
+ def ensure_fields(gh: GhRunner, owner: str, number: int) -> dict:
65
+ """Create any missing bridge fields, then return the refreshed project."""
66
+ project = get_project(gh, owner, number)
67
+ for name, spec in FIELD_SPECS.items():
68
+ if name in project["fields"]:
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)
86
+ return get_project(gh, owner, number)
87
+
88
+
89
+ def add_item(gh: GhRunner, project_id: str, content_id: str) -> str:
90
+ data = gh.graphql(
91
+ "mutation($p: ID!, $c: ID!) { addProjectV2ItemById(input: {"
92
+ "projectId: $p, contentId: $c}) { item { id } } }",
93
+ p=project_id, c=content_id)
94
+ return data["addProjectV2ItemById"]["item"]["id"]
95
+
96
+
97
+ def set_text(gh: GhRunner, project_id: str, item_id: str, field_id: str,
98
+ value: str) -> None:
99
+ gh.graphql(
100
+ "mutation($p: ID!, $i: ID!, $f: ID!, $v: String!) {"
101
+ "updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, "
102
+ "fieldId: $f, value: {text: $v}}) { projectV2Item { id } } }",
103
+ p=project_id, i=item_id, f=field_id, v=value)
104
+
105
+
106
+ def set_option(gh: GhRunner, project_id: str, item_id: str, field_id: str,
107
+ option_id: str) -> None:
108
+ gh.graphql(
109
+ "mutation($p: ID!, $i: ID!, $f: ID!, $o: String!) {"
110
+ "updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, "
111
+ "fieldId: $f, value: {singleSelectOptionId: $o}}) "
112
+ "{ projectV2Item { id } } }",
113
+ p=project_id, i=item_id, f=field_id, o=option_id)
114
+
115
+
116
+ def sync_items(gh: GhRunner, project: dict, entries: list[dict]) -> int:
117
+ """entries: [{node_id, doc_id, type ('Feature'|'Story'), pmo_project}]."""
118
+ f = project["fields"]
119
+ type_field, doc_field, pmo_field = f["Type"], f["Doc"], f["PMO Project"]
120
+ for e in entries:
121
+ item = add_item(gh, project["id"], e["node_id"])
122
+ set_option(gh, project["id"], item, type_field["id"],
123
+ type_field["options"][e["type"]])
124
+ set_text(gh, project["id"], item, doc_field["id"], e["doc_id"])
125
+ set_text(gh, project["id"], item, pmo_field["id"], e["pmo_project"])
126
+ return len(entries)
@@ -15,11 +15,22 @@ class GhError(RuntimeError):
15
15
 
16
16
 
17
17
  class GhRunner:
18
- """Executes gh commands. Tests substitute a fake with the same interface."""
18
+ """Executes gh commands. Tests substitute a fake with the same interface.
19
+
20
+ `token` overrides GH_TOKEN for this runner only, so board operations can
21
+ use the project-scoped credential while issue operations keep the
22
+ default one.
23
+ """
24
+
25
+ def __init__(self, token: str | None = None) -> None:
26
+ self._env: dict[str, str] | None = None
27
+ if token:
28
+ import os
29
+ self._env = dict(os.environ, GH_TOKEN=token)
19
30
 
20
31
  def run(self, args: list[str], input_: str | None = None) -> str:
21
32
  proc = subprocess.run(["gh", *args], capture_output=True, text=True,
22
- input=input_)
33
+ input=input_, env=self._env)
23
34
  if proc.returncode != 0:
24
35
  raise GhError(proc.stderr.strip() or f"gh {' '.join(args)} failed")
25
36
  return proc.stdout
@@ -44,7 +55,8 @@ class DryRunner(GhRunner):
44
55
 
45
56
  MUTATING = ("-X POST", "-X PATCH", "-X PUT", "-X DELETE", "mutation")
46
57
 
47
- def __init__(self) -> None:
58
+ def __init__(self, token: str | None = None) -> None:
59
+ super().__init__(token)
48
60
  self.planned: list[str] = []
49
61
 
50
62
  def run(self, args: list[str], input_: str | None = None) -> str:
@@ -46,8 +46,14 @@ def _index(issues: list[dict]) -> dict[str, dict]:
46
46
 
47
47
 
48
48
  def scaffold(plan: BridgePlan, gh: G.GhRunner, repo: str,
49
- docs_url: str | None = None) -> list[str]:
50
- """Make the repo's issues match the plan. Returns human-readable actions."""
49
+ docs_url: str | None = None,
50
+ board_cfg: dict | None = None) -> list[str]:
51
+ """Make the repo's issues match the plan. Returns human-readable actions.
52
+
53
+ board_cfg (optional): {"gh": GhRunner with project scope, "owner": login,
54
+ "number": int, "init": bool} — mirrors every managed issue onto the
55
+ Projects v2 board with Type/Doc/PMO Project fields.
56
+ """
51
57
  actions: list[str] = []
52
58
  for name, (color, desc) in LABELS.items():
53
59
  G.ensure_label(gh, repo, name, color, desc)
@@ -80,6 +86,28 @@ def scaffold(plan: BridgePlan, gh: G.GhRunner, repo: str,
80
86
  if pnode and cnode:
81
87
  G.add_sub_issue(gh, pnode, cnode)
82
88
 
89
+ if board_cfg:
90
+ from . import board as B
91
+ bgh = board_cfg["gh"]
92
+ if board_cfg.get("init"):
93
+ project = B.ensure_fields(bgh, board_cfg["owner"], board_cfg["number"])
94
+ actions.append(f"board: fields ensured on '{project['title']}'")
95
+ else:
96
+ project = B.get_project(bgh, board_cfg["owner"], board_cfg["number"])
97
+ entries = []
98
+ for f in plan.features:
99
+ issue = by_key.get(f.id)
100
+ if issue and issue.get("node_id"):
101
+ entries.append({"node_id": issue["node_id"], "doc_id": f.id,
102
+ "type": "Feature", "pmo_project": f.project})
103
+ for s in f.stories:
104
+ sissue = by_key.get(s.id)
105
+ if sissue and sissue.get("node_id"):
106
+ entries.append({"node_id": sissue["node_id"], "doc_id": s.id,
107
+ "type": "Story", "pmo_project": f.project})
108
+ n = B.sync_items(bgh, project, entries)
109
+ actions.append(f"board: {n} item(s) synced to '{project['title']}'")
110
+
83
111
  for sk in plan.skipped:
84
112
  actions.append(f"skipped {sk['id']}: {sk['reason']}")
85
113
  return actions
@@ -300,8 +300,31 @@ def cmd_bridge(args: argparse.Namespace) -> int:
300
300
  plan = build_bridge_plan(args.documents_dir)
301
301
  gh = ghmod.DryRunner() if getattr(args, "dry_run", False) else ghmod.GhRunner()
302
302
 
303
+ if args.action == "create-board":
304
+ from .bridge import board as board_mod
305
+ token = os.environ.get(args.board_token_env, "")
306
+ if not token and not getattr(args, "dry_run", False):
307
+ print(f"docassert bridge: no token in ${args.board_token_env}", file=sys.stderr)
308
+ return 2
309
+ bgh = gh if isinstance(gh, ghmod.DryRunner) else ghmod.GhRunner(token=token)
310
+ proj = board_mod.create_project(bgh, args.title or "PMO board")
311
+ print(f"docassert bridge: created project #{proj.get('number')} '{proj.get('title')}'")
312
+ return 0
313
+
314
+ board_cfg = None
315
+ if args.project_number:
316
+ token = os.environ.get(args.board_token_env, "")
317
+ if not token and not getattr(args, "dry_run", False):
318
+ print(f"docassert bridge: no token in ${args.board_token_env} "
319
+ "(needed for --project-number)", file=sys.stderr)
320
+ return 2
321
+ bgh = gh if isinstance(gh, ghmod.DryRunner) else ghmod.GhRunner(token=token)
322
+ board_cfg = {"gh": bgh, "owner": args.project_owner or args.repo.split("/")[0],
323
+ "number": args.project_number, "init": args.init_board}
324
+
303
325
  if args.action == "scaffold":
304
- actions = ops.scaffold(plan, gh, args.repo, docs_url=args.docs_url)
326
+ actions = ops.scaffold(plan, gh, args.repo, docs_url=args.docs_url,
327
+ board_cfg=board_cfg)
305
328
  for a in actions:
306
329
  print(f"docassert bridge: {a}")
307
330
  if isinstance(gh, ghmod.DryRunner):
@@ -405,13 +428,19 @@ def main(argv: list[str] | None = None) -> int:
405
428
 
406
429
 
407
430
  b = sub.add_parser("bridge", help="Execution bridge: scaffold and police the GitHub board from approved stories.")
408
- b.add_argument("action", choices=["scaffold", "reconcile", "status"],
431
+ b.add_argument("action", choices=["scaffold", "reconcile", "status", "create-board"],
409
432
  help="scaffold: docs -> issues/board · reconcile: police the board · status: delivery figures.")
410
433
  b.add_argument("--repo", required=True, help="Target GitHub repo (OWNER/NAME) holding the issues.")
411
434
  b.add_argument("--docs-url", help="Base URL of the documents repo, for source links in issue bodies.")
412
435
  b.add_argument("--dry-run", action="store_true", help="Print planned mutations without executing them.")
413
436
  b.add_argument("--json", action="store_true", help="status: emit JSON.")
414
437
  b.add_argument("--out", help="status --json: also write to this path.")
438
+ b.add_argument("--title", help="create-board: the new project's title.")
439
+ b.add_argument("--project-owner", help="Board owner login (default: the repo owner).")
440
+ b.add_argument("--project-number", type=int, help="Projects v2 board number to sync items onto.")
441
+ b.add_argument("--init-board", action="store_true", help="Ensure the Type/Doc/PMO Project fields exist.")
442
+ b.add_argument("--board-token-env", default="PROJECTS_TOKEN",
443
+ help="Env var holding the project-scoped token (default: PROJECTS_TOKEN).")
415
444
  docs_dir_opt(b)
416
445
  b.set_defaults(func=cmd_bridge)
417
446
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docassert
3
- Version: 0.8.1
3
+ Version: 0.9.0
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
@@ -90,6 +90,7 @@ docassert/_data/templates/status-report.template.md
90
90
  docassert/_data/templates/test-cases.template.md
91
91
  docassert/_data/templates/user-story.template.md
92
92
  docassert/bridge/__init__.py
93
+ docassert/bridge/board.py
93
94
  docassert/bridge/gh.py
94
95
  docassert/bridge/ops.py
95
96
  docassert/bridge/plan.py
@@ -247,3 +247,76 @@ def test_add_sub_issue_still_raises_on_real_errors(tmp_path):
247
247
  import pytest
248
248
  with pytest.raises(GhError):
249
249
  add_sub_issue(BoomGh(), "N1", "N2")
250
+
251
+
252
+ # ── board ops ────────────────────────────────────────────────────────────────
253
+ class BoardGh(FakeGh):
254
+ """GraphQL router for Projects v2 shapes."""
255
+
256
+ def __init__(self, existing_fields=()):
257
+ super().__init__()
258
+ self.gql: list[str] = []
259
+ self._fields = list(existing_fields)
260
+
261
+ def graphql(self, query, **vars_):
262
+ self.gql.append(query)
263
+ if "viewer" in query:
264
+ return {"viewer": {"id": "U1", "login": "tester"}}
265
+ if "createProjectV2(" in query:
266
+ return {"createProjectV2": {"projectV2": {"id": "P1", "number": 7,
267
+ "title": vars_.get("title")}}}
268
+ if "projectV2(number" in query:
269
+ nodes = [{"id": f"F-{n}", "name": n, "dataType": "TEXT"}
270
+ for n in self._fields]
271
+ if "Type" in self._fields:
272
+ for node in nodes:
273
+ if node["name"] == "Type":
274
+ node["dataType"] = "SINGLE_SELECT"
275
+ node["options"] = [{"id": "O1", "name": "Feature"},
276
+ {"id": "O2", "name": "Story"}]
277
+ return {"user": {"projectV2": {"id": "P1", "title": "Board",
278
+ "fields": {"nodes": nodes}}}}
279
+ if "createProjectV2Field" in query:
280
+ self._fields.append(vars_["n"])
281
+ return {"createProjectV2Field": {"projectV2Field": {"id": "F-new"}}}
282
+ if "addProjectV2ItemById" in query:
283
+ return {"addProjectV2ItemById": {"item": {"id": "I1"}}}
284
+ if "updateProjectV2ItemFieldValue" in query:
285
+ return {"updateProjectV2ItemFieldValue": {"projectV2Item": {"id": "I1"}}}
286
+ return {}
287
+
288
+
289
+ def test_create_project():
290
+ from docassert.bridge import board
291
+ gh = BoardGh()
292
+ proj = board.create_project(gh, "Refuge for Humans")
293
+ assert proj["number"] == 7 and proj["title"] == "Refuge for Humans"
294
+
295
+
296
+ def test_ensure_fields_creates_missing():
297
+ from docassert.bridge import board
298
+ gh = BoardGh(existing_fields=["Status"])
299
+ project = board.ensure_fields(gh, "tester", 7)
300
+ assert set(project["fields"]) >= {"Type", "Doc", "PMO Project"}
301
+
302
+
303
+ def test_scaffold_syncs_board(tmp_path):
304
+ plan = build_bridge_plan(_tree(tmp_path))
305
+ existing = []
306
+ for i, f in enumerate(plan.features):
307
+ e = _mk_issue(200 + i, f.id, feature_title(f))
308
+ e["body"] = feature_body(f, None)
309
+ existing.append(e)
310
+ for j, s in enumerate(f.stories):
311
+ from docassert.bridge.plan import story_title
312
+ e2 = _mk_issue(300 + i * 10 + j, s.id, story_title(s))
313
+ e2["body"] = story_body(s, None)
314
+ existing.append(e2)
315
+ gh = FakeGh(existing)
316
+ bgh = BoardGh(existing_fields=["Type", "Doc", "PMO Project"])
317
+ actions = ops.scaffold(plan, gh, "o/r",
318
+ board_cfg={"gh": bgh, "owner": "tester", "number": 7,
319
+ "init": False})
320
+ assert any("5 item(s) synced" in a for a in actions)
321
+ adds = [q for q in bgh.gql if "addProjectV2ItemById" in q]
322
+ assert len(adds) == 5
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