modwire 4.0.2__tar.gz → 5.0.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 (136) hide show
  1. {modwire-4.0.2 → modwire-5.0.0}/.github/ISSUE_TEMPLATE/bug_report.yml +3 -2
  2. modwire-5.0.0/.github/ISSUE_TEMPLATE/decision.yml +48 -0
  3. modwire-5.0.0/.github/ISSUE_TEMPLATE/epic.yml +51 -0
  4. {modwire-4.0.2 → modwire-5.0.0}/.github/ISSUE_TEMPLATE/feature_request.yml +3 -2
  5. modwire-5.0.0/.github/ISSUE_TEMPLATE/task.yml +56 -0
  6. {modwire-4.0.2 → modwire-5.0.0}/.github/modwire-ecosystem.yml +81 -25
  7. {modwire-4.0.2 → modwire-5.0.0}/.github/scripts/reconcile_ecosystem.py +234 -3
  8. modwire-5.0.0/.github/workflows/human-approval.yml +133 -0
  9. modwire-5.0.0/.github/workflows/issue-metadata-check.yml +17 -0
  10. modwire-5.0.0/.github/workflows/issue-metadata.yml +87 -0
  11. {modwire-4.0.2 → modwire-5.0.0}/CHANGELOG.md +18 -0
  12. modwire-5.0.0/MANIFEST.in +2 -0
  13. {modwire-4.0.2/src/modwire.egg-info → modwire-5.0.0}/PKG-INFO +35 -29
  14. {modwire-4.0.2 → modwire-5.0.0}/README.md +30 -24
  15. modwire-5.0.0/docs/decisions/0001-canonical-modwire-repository-shape.md +106 -0
  16. {modwire-4.0.2 → modwire-5.0.0}/docs/governance/github-ecosystem.md +88 -22
  17. {modwire-4.0.2 → modwire-5.0.0}/docs/governance/python-workflows.md +3 -3
  18. modwire-5.0.0/docs/migration-5.md +53 -0
  19. modwire-5.0.0/docs/scaffolding/golden/cli.yaml +44 -0
  20. modwire-5.0.0/docs/scaffolding/golden/library.yaml +40 -0
  21. modwire-5.0.0/docs/scaffolding/repository-contract-v1.yaml +147 -0
  22. modwire-5.0.0/docs/wiki/Home.md +28 -0
  23. {modwire-4.0.2 → modwire-5.0.0}/docs/wiki/Reporting-bugs.md +1 -1
  24. {modwire-4.0.2 → modwire-5.0.0}/docs/wiki/Requesting-features.md +1 -1
  25. {modwire-4.0.2 → modwire-5.0.0}/pyproject.toml +4 -4
  26. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/__init__.py +2 -0
  27. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/_version.py +3 -3
  28. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/__init__.py +0 -3
  29. modwire-5.0.0/src/modwire/architecture/app.py +67 -0
  30. modwire-5.0.0/src/modwire/architecture/boundaries/__init__.py +4 -0
  31. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/analyzer.py +6 -2
  32. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/analyzers/backward.py +2 -0
  33. modwire-5.0.0/src/modwire/architecture/boundaries/analyzers/module_boundaries.py +146 -0
  34. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/analyzers/no_cycles.py +2 -0
  35. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/analyzers/no_reentry.py +2 -0
  36. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/base.py +4 -0
  37. modwire-5.0.0/src/modwire/architecture/boundaries/composition.py +22 -0
  38. modwire-5.0.0/src/modwire/architecture/composition.py +21 -0
  39. modwire-5.0.0/src/modwire/architecture/insights/__init__.py +4 -0
  40. modwire-5.0.0/src/modwire/architecture/insights/composition.py +18 -0
  41. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/reporters/coherence.py +2 -7
  42. modwire-5.0.0/src/modwire/architecture/map/__init__.py +4 -0
  43. modwire-5.0.0/src/modwire/architecture/map/composition.py +8 -0
  44. modwire-5.0.0/src/modwire/architecture/shape/__init__.py +4 -0
  45. modwire-5.0.0/src/modwire/architecture/shape/composition.py +27 -0
  46. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/import_resolver.py +5 -1
  47. modwire-5.0.0/src/modwire/code/__init__.py +6 -0
  48. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/facade.py +1 -8
  49. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/projects/__init__.py +10 -0
  50. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/projects/ecosystem.py +70 -9
  51. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/base.py +0 -15
  52. modwire-5.0.0/src/modwire/shared/config/modwire.py +14 -0
  53. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/config/shape.py +2 -2
  54. {modwire-4.0.2 → modwire-5.0.0/src/modwire.egg-info}/PKG-INFO +35 -29
  55. {modwire-4.0.2 → modwire-5.0.0}/src/modwire.egg-info/SOURCES.txt +21 -3
  56. {modwire-4.0.2 → modwire-5.0.0}/src/modwire.egg-info/requires.txt +1 -1
  57. {modwire-4.0.2 → modwire-5.0.0}/src/modwire.egg-info/scm_file_list.json +21 -3
  58. modwire-5.0.0/src/modwire.egg-info/scm_version.json +8 -0
  59. {modwire-4.0.2 → modwire-5.0.0}/tests/test_architecture.py +2 -2
  60. modwire-5.0.0/tests/test_boundaries.py +192 -0
  61. {modwire-4.0.2 → modwire-5.0.0}/tests/test_ecosystem_contract.py +39 -13
  62. modwire-5.0.0/tests/test_import_shape.py +57 -0
  63. {modwire-4.0.2 → modwire-5.0.0}/tests/test_models.py +9 -12
  64. modwire-5.0.0/tests/test_repository_shape_contract.py +44 -0
  65. {modwire-4.0.2 → modwire-5.0.0}/uv.lock +4 -4
  66. modwire-4.0.2/MANIFEST.in +0 -1
  67. modwire-4.0.2/docs/wiki/Home.md +0 -27
  68. modwire-4.0.2/src/modwire/architecture/app.py +0 -137
  69. modwire-4.0.2/src/modwire/architecture/boundaries/__init__.py +0 -8
  70. modwire-4.0.2/src/modwire/architecture/insights/__init__.py +0 -8
  71. modwire-4.0.2/src/modwire/architecture/map/__init__.py +0 -11
  72. modwire-4.0.2/src/modwire/architecture/shape/__init__.py +0 -8
  73. modwire-4.0.2/src/modwire/code/__init__.py +0 -10
  74. modwire-4.0.2/src/modwire/code/reader.py +0 -9
  75. modwire-4.0.2/src/modwire/code/writer.py +0 -27
  76. modwire-4.0.2/src/modwire/shared/config/modwire.py +0 -54
  77. modwire-4.0.2/src/modwire.egg-info/scm_version.json +0 -8
  78. {modwire-4.0.2 → modwire-5.0.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  79. {modwire-4.0.2 → modwire-5.0.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  80. {modwire-4.0.2 → modwire-5.0.0}/.github/workflows/ci.yml +0 -0
  81. {modwire-4.0.2 → modwire-5.0.0}/.github/workflows/python-package.yml +0 -0
  82. {modwire-4.0.2 → modwire-5.0.0}/.github/workflows/python-release-assets.yml +0 -0
  83. {modwire-4.0.2 → modwire-5.0.0}/.github/workflows/python-release-build.yml +0 -0
  84. {modwire-4.0.2 → modwire-5.0.0}/.github/workflows/release.yml +0 -0
  85. {modwire-4.0.2 → modwire-5.0.0}/.gitignore +0 -0
  86. {modwire-4.0.2 → modwire-5.0.0}/.modwire/boundaries.yaml +0 -0
  87. {modwire-4.0.2 → modwire-5.0.0}/.modwire/shape.yaml +0 -0
  88. {modwire-4.0.2 → modwire-5.0.0}/CONTRIBUTING.md +0 -0
  89. {modwire-4.0.2 → modwire-5.0.0}/LICENSE +0 -0
  90. {modwire-4.0.2 → modwire-5.0.0}/Makefile +0 -0
  91. {modwire-4.0.2 → modwire-5.0.0}/docs/wiki/Development-checks.md +0 -0
  92. {modwire-4.0.2 → modwire-5.0.0}/setup.cfg +0 -0
  93. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/analyzers/__init__.py +0 -0
  94. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/analyzers/analyzer_base.py +0 -0
  95. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/collector.py +0 -0
  96. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/tags/__init__.py +0 -0
  97. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/tags/matcher.py +0 -0
  98. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/tags/tag_map.py +0 -0
  99. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/base.py +0 -0
  100. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/collector.py +0 -0
  101. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/reporters/__init__.py +0 -0
  102. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/reporters/base.py +0 -0
  103. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/reporters/callables.py +0 -0
  104. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/reporters/clusters.py +0 -0
  105. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/reporters/exports.py +0 -0
  106. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/reporters/hotspots.py +0 -0
  107. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/map/base.py +0 -0
  108. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/map/collector.py +0 -0
  109. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/map/loader.py +0 -0
  110. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/base.py +0 -0
  111. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/collector.py +0 -0
  112. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/__init__.py +0 -0
  113. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/abstract_class_resolver.py +0 -0
  114. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/callable_resolver.py +0 -0
  115. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/class_resolver.py +0 -0
  116. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/file_resolver.py +0 -0
  117. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/property_resolver.py +0 -0
  118. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/signature_resolver.py +0 -0
  119. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/symbol_resolver.py +0 -0
  120. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/code/package.py +0 -0
  121. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/layers/__init__.py +0 -0
  122. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/layers/app.py +0 -0
  123. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/modules/__init__.py +0 -0
  124. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/modules/app.py +0 -0
  125. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/projects/app.py +0 -0
  126. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/py.typed +0 -0
  127. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/__init__.py +0 -0
  128. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/config/__init__.py +0 -0
  129. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/config/architecture.py +0 -0
  130. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/config/layers.py +0 -0
  131. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/config/modules.py +0 -0
  132. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/config/projects.py +0 -0
  133. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/report/__init__.py +0 -0
  134. {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/report/base.py +0 -0
  135. {modwire-4.0.2 → modwire-5.0.0}/src/modwire.egg-info/dependency_links.txt +0 -0
  136. {modwire-4.0.2 → modwire-5.0.0}/src/modwire.egg-info/top_level.txt +0 -0
@@ -1,8 +1,9 @@
1
1
  name: Bug report
2
2
  description: Report incorrect architecture analysis, graph output, package metadata, or report output.
3
3
  title: "Bug: "
4
- labels:
5
- - bug
4
+ type: Bug
5
+ projects:
6
+ - modwire/1
6
7
  body:
7
8
  - type: markdown
8
9
  attributes:
@@ -0,0 +1,48 @@
1
+ name: Architecture or governance decision
2
+ description: Record a decision that changes contracts, compatibility, architecture, or governance.
3
+ title: "Decision: "
4
+ type: Decision
5
+ labels:
6
+ - contract
7
+ projects:
8
+ - modwire/1
9
+ body:
10
+ - type: textarea
11
+ id: context
12
+ attributes:
13
+ label: Context
14
+ description: Explain the forces, constraints, and decision deadline.
15
+ validations:
16
+ required: true
17
+
18
+ - type: textarea
19
+ id: options
20
+ attributes:
21
+ label: Options considered
22
+ description: Describe credible alternatives and their tradeoffs.
23
+ validations:
24
+ required: true
25
+
26
+ - type: textarea
27
+ id: decision
28
+ attributes:
29
+ label: Proposed decision
30
+ description: State the proposed choice and why it best fits the constraints.
31
+ validations:
32
+ required: true
33
+
34
+ - type: textarea
35
+ id: consequences
36
+ attributes:
37
+ label: Consequences and acceptance criteria
38
+ description: Describe positive and negative consequences plus the evidence needed to consider the decision implemented.
39
+ validations:
40
+ required: true
41
+
42
+ - type: input
43
+ id: relationships
44
+ attributes:
45
+ label: Related issues and decisions
46
+ description: Link superseded, blocking, blocked, parent, or implementation issues.
47
+ validations:
48
+ required: false
@@ -0,0 +1,51 @@
1
+ name: Ecosystem epic
2
+ description: Coordinate an outcome delivered by multiple repository-owned sub-issues.
3
+ title: "Epic: "
4
+ type: Epic
5
+ labels:
6
+ - ecosystem
7
+ projects:
8
+ - modwire/1
9
+ body:
10
+ - type: textarea
11
+ id: outcome
12
+ attributes:
13
+ label: Ecosystem outcome
14
+ description: Describe the user-visible or compatibility result across packages.
15
+ validations:
16
+ required: true
17
+
18
+ - type: textarea
19
+ id: repositories
20
+ attributes:
21
+ label: Affected repositories
22
+ description: List every repository expected to own a sub-issue.
23
+ placeholder: |
24
+ - modwire/modwire
25
+ - modwire/modwire-cli
26
+ validations:
27
+ required: true
28
+
29
+ - type: textarea
30
+ id: acceptance
31
+ attributes:
32
+ label: Acceptance criteria
33
+ description: Include integration verification and release-readiness conditions.
34
+ validations:
35
+ required: true
36
+
37
+ - type: textarea
38
+ id: rollout
39
+ attributes:
40
+ label: Rollout and rollback
41
+ description: Define compatibility promises, rollout order, and rollback strategy.
42
+ validations:
43
+ required: true
44
+
45
+ - type: input
46
+ id: relationships
47
+ attributes:
48
+ label: Initial sub-issues and dependencies
49
+ description: Link existing issues. Create native sub-issue and dependency relationships after submission.
50
+ validations:
51
+ required: false
@@ -1,8 +1,9 @@
1
1
  name: Feature request
2
2
  description: Suggest a new architecture, graph, shape, callable, export, or integration capability.
3
3
  title: "Feature: "
4
- labels:
5
- - enhancement
4
+ type: Feature
5
+ projects:
6
+ - modwire/1
6
7
  body:
7
8
  - type: markdown
8
9
  attributes:
@@ -0,0 +1,56 @@
1
+ name: Task
2
+ description: Propose concrete implementation, maintenance, documentation, or release work.
3
+ title: "Task: "
4
+ type: Task
5
+ projects:
6
+ - modwire/1
7
+ body:
8
+ - type: markdown
9
+ attributes:
10
+ value: |
11
+ Select every pinned issue field before submission. Use native dependencies and sub-issues for relationships; do not encode them only in prose.
12
+
13
+ - type: textarea
14
+ id: outcome
15
+ attributes:
16
+ label: Outcome
17
+ description: State the observable result, not only the activity.
18
+ placeholder: After this work...
19
+ validations:
20
+ required: true
21
+
22
+ - type: textarea
23
+ id: scope
24
+ attributes:
25
+ label: Scope
26
+ description: Describe the intended implementation boundary and important exclusions.
27
+ validations:
28
+ required: true
29
+
30
+ - type: textarea
31
+ id: acceptance
32
+ attributes:
33
+ label: Acceptance criteria
34
+ description: List objective conditions that prove the outcome is complete.
35
+ placeholder: |
36
+ - [ ] Observable behavior is implemented.
37
+ - [ ] Relevant tests and documentation are updated.
38
+ validations:
39
+ required: true
40
+
41
+ - type: textarea
42
+ id: rollout
43
+ attributes:
44
+ label: Rollout and rollback
45
+ description: Explain compatibility, migration, rollout, or rollback considerations; write "Not applicable" when none exist.
46
+ validations:
47
+ required: true
48
+
49
+ - type: input
50
+ id: relationships
51
+ attributes:
52
+ label: Related issues
53
+ description: Link candidate parents, sub-issues, blockers, or blocked work. A maintainer will create the native relationships.
54
+ placeholder: https://github.com/modwire/...
55
+ validations:
56
+ required: false
@@ -1,8 +1,8 @@
1
- version: 2
1
+ version: 3
2
2
 
3
3
  packages:
4
4
  core:
5
- repository: 9orky/modwire
5
+ repository: modwire/modwire
6
6
  distribution: modwire
7
7
  component: Core
8
8
  role: coordinator
@@ -10,7 +10,7 @@ packages:
10
10
  summary: Typed coordination library and ecosystem integration gate.
11
11
  depends_on: [extraction, mermaid, siren]
12
12
  cli:
13
- repository: 9orky/modwire-cli
13
+ repository: modwire/modwire-cli
14
14
  distribution: modwire-cli
15
15
  component: CLI
16
16
  role: runtime
@@ -18,43 +18,43 @@ packages:
18
18
  summary: Command-line runtime and user-facing workflows.
19
19
  depends_on: [core]
20
20
  extraction:
21
- repository: 9orky/modwire-extraction
21
+ repository: modwire/modwire-extraction
22
22
  distribution: modwire-extraction
23
23
  component: Extraction
24
24
  role: building_block
25
25
  lifecycle: active
26
26
  summary: Canonical source extraction and normalization.
27
27
  mermaid:
28
- repository: 9orky/modwire-mermaid
28
+ repository: modwire/modwire-mermaid
29
29
  distribution: modwire-mermaid
30
30
  component: Mermaid
31
31
  role: building_block
32
32
  lifecycle: active
33
33
  summary: Typed, deterministic Mermaid source generation.
34
34
  siren:
35
- repository: 9orky/modwire-siren
35
+ repository: modwire/modwire-siren
36
36
  distribution: modwire-siren
37
37
  component: Siren
38
38
  role: building_block
39
39
  lifecycle: active
40
40
  summary: Typed Siren hypermedia and OpenAPI integration.
41
41
  mcp:
42
- repository: 9orky/modwire-mcp
42
+ repository: modwire/modwire-mcp
43
43
  distribution: modwire-mcp
44
44
  component: MCP
45
45
  role: integration
46
46
  lifecycle: incubating
47
- summary: MCP surface and future home of scaffolding operations.
48
- depends_on: [core]
47
+ summary: Planned MCP integration, activated after CLI exposes filesystem, project, and execution capabilities.
48
+ depends_on: [cli]
49
49
 
50
50
  project:
51
- owner: 9orky
51
+ owner: modwire
52
52
  title: Modwire Ecosystem
53
- number: 4
53
+ number: 1
54
54
  visibility: public
55
55
  default_package: core
56
56
  description: Cross-repository roadmap and release coordination for the Modwire ecosystem.
57
- governance_url: https://github.com/9orky/modwire/blob/main/docs/governance/github-ecosystem.md
57
+ governance_url: https://github.com/modwire/modwire/blob/main/docs/governance/github-ecosystem.md
58
58
 
59
59
  workflows:
60
60
  contract_ref: workflows-v1
@@ -75,26 +75,85 @@ workflows:
75
75
  download_artifact: actions/download-artifact@v8
76
76
  publish_pypi: pypa/gh-action-pypi-publish@release/v1
77
77
 
78
+ issue_types:
79
+ Task:
80
+ description: A concrete unit of implementation, maintenance, or documentation work.
81
+ color: yellow
82
+ Bug:
83
+ description: Incorrect or unexpected existing behavior.
84
+ color: red
85
+ Feature:
86
+ description: A new user-facing capability or behavior.
87
+ color: blue
88
+ Epic:
89
+ description: A coordinated outcome delivered through multiple sub-issues.
90
+ color: purple
91
+ Decision:
92
+ description: An architecture, compatibility, or governance decision.
93
+ color: green
94
+
78
95
  fields:
79
96
  Status:
97
+ owner: project
80
98
  type: single_select
81
99
  options: [Inbox, Ready, In progress, Blocked, Review, Done]
82
100
  Horizon:
101
+ owner: issue
83
102
  type: single_select
84
- options: [Now, Next, Later]
103
+ description: Planning horizon; Unscheduled means accepted but not placed on the roadmap.
104
+ visibility: public
105
+ options: [Now, Next, Later, Unscheduled]
85
106
  Priority:
107
+ owner: issue
86
108
  type: single_select
109
+ description: Relative delivery priority, from critical P0 to opportunistic P3.
110
+ visibility: public
87
111
  options: [P0, P1, P2, P3]
88
112
  Component:
113
+ owner: issue
89
114
  type: single_select
115
+ description: Modwire package primarily responsible for delivery.
116
+ visibility: public
90
117
  source: packages
91
118
  Release train:
119
+ owner: issue
92
120
  type: text
121
+ description: Coordinated delivery train, or Unscheduled when none applies.
122
+ visibility: public
93
123
  Target date:
124
+ owner: issue
94
125
  type: date
126
+ description: Expected completion date for committed work.
127
+ visibility: public
128
+ Start date:
129
+ owner: issue
130
+ type: date
131
+ description: Date on which active implementation begins.
132
+ visibility: public
95
133
  Risk:
134
+ owner: issue
96
135
  type: single_select
136
+ description: Delivery, compatibility, or operational risk if the issue changes or slips.
137
+ visibility: public
97
138
  options: [Low, Medium, High]
139
+ Effort:
140
+ owner: issue
141
+ type: single_select
142
+ description: Relative implementation effort using T-shirt sizing.
143
+ visibility: public
144
+ options: [XS, S, M, L, XL]
145
+
146
+ issue_intake:
147
+ project: modwire/1
148
+ required_fields: [Horizon, Priority, Risk, Component, Release train, Effort]
149
+ ready_requires: [assignee, acceptance_criteria]
150
+ incomplete_label: needs-metadata
151
+ agent_must_not_guess: true
152
+
153
+ relationships:
154
+ dependencies: native
155
+ hierarchy: native_sub_issues
156
+ cross_repository_parent: modwire/modwire
98
157
 
99
158
  views:
100
159
  - name: Roadmap
@@ -122,16 +181,10 @@ views:
122
181
  filter: "Status:Inbox"
123
182
 
124
183
  labels:
125
- "type:bug":
126
- color: "d73a4a"
127
- description: Incorrect existing behavior
128
- "type:feature":
129
- color: "0e8a16"
130
- description: New user-facing capability
131
- "type:maintenance":
184
+ maintenance:
132
185
  color: "c5def5"
133
186
  description: Internal maintenance
134
- "type:docs":
187
+ documentation:
135
188
  color: "0075ca"
136
189
  description: Documentation
137
190
  ecosystem:
@@ -143,19 +196,22 @@ labels:
143
196
  breaking:
144
197
  color: "b60205"
145
198
  description: Backward-incompatible change
146
- blocked:
147
- color: "e99695"
148
- description: Blocked by another decision or delivery
149
199
  release-gate:
150
200
  color: "fbca04"
151
201
  description: Must complete before a release
202
+ needs-metadata:
203
+ color: "b60205"
204
+ description: Missing required issue intake metadata
205
+ requires-human-approval:
206
+ color: "d93f0b"
207
+ description: Explicitly require a human to approve the pull request
152
208
 
153
209
  automation:
154
210
  auto_add_filter: "is:open"
155
211
  added_status: Inbox
156
212
  closed_status: Done
157
213
  merged_status: Done
158
- archive_filter: "Status:Done updated:<@today-30d"
214
+ archive_filter: "is:closed updated:<@today-30d"
159
215
 
160
216
  milestones:
161
217
  scope: repository
@@ -8,7 +8,13 @@ import subprocess
8
8
  from pathlib import Path
9
9
  from typing import Any
10
10
 
11
- from modwire.projects import EcosystemContract, EcosystemDrift, ProjectFieldType
11
+ from modwire.projects import (
12
+ EcosystemContract,
13
+ EcosystemDrift,
14
+ FieldOwner,
15
+ FieldVisibility,
16
+ ProjectFieldType,
17
+ )
12
18
 
13
19
 
14
20
  ROOT = Path(__file__).resolve().parents[2]
@@ -29,8 +35,222 @@ def gh_json(*arguments: str) -> Any:
29
35
  return json.loads(gh(*arguments))
30
36
 
31
37
 
38
+ def gh_api_json(method: str, path: str, payload: dict[str, Any]) -> Any:
39
+ result = subprocess.run(
40
+ [
41
+ "gh",
42
+ "api",
43
+ "--method",
44
+ method,
45
+ "-H",
46
+ "X-GitHub-Api-Version: 2026-03-10",
47
+ "--input",
48
+ "-",
49
+ path,
50
+ ],
51
+ check=True,
52
+ capture_output=True,
53
+ input=json.dumps(payload),
54
+ text=True,
55
+ )
56
+ return json.loads(result.stdout) if result.stdout else None
57
+
58
+
59
+ def gh_graphql(query: str, variables: dict[str, Any]) -> Any:
60
+ return gh_api_json(
61
+ "POST",
62
+ "graphql",
63
+ {"query": query, "variables": variables},
64
+ )
65
+
66
+
32
67
  def load_contract(path: Path) -> EcosystemContract:
33
- return EcosystemContract.load_yaml(path)
68
+ return EcosystemContract.from_yaml(path.read_text(encoding="utf-8"))
69
+
70
+
71
+ def organization_issue_types(contract: EcosystemContract) -> dict[str, dict[str, Any]]:
72
+ values = gh_json(
73
+ "api",
74
+ "-H",
75
+ "X-GitHub-Api-Version: 2026-03-10",
76
+ f"orgs/{contract.project.owner}/issue-types",
77
+ )
78
+ return {value["name"]: value for value in values}
79
+
80
+
81
+ def organization_issue_fields(contract: EcosystemContract) -> dict[str, dict[str, Any]]:
82
+ values = gh_json(
83
+ "api",
84
+ "-H",
85
+ "X-GitHub-Api-Version: 2026-03-10",
86
+ f"orgs/{contract.project.owner}/issue-fields",
87
+ )
88
+ return {value["name"]: value for value in values}
89
+
90
+
91
+ def option_color(field_name: str, option_name: str, position: int) -> str:
92
+ semantic = {
93
+ "P0": "red",
94
+ "P1": "orange",
95
+ "P2": "yellow",
96
+ "P3": "green",
97
+ "High": "red",
98
+ "Medium": "yellow",
99
+ "Low": "green",
100
+ "Now": "red",
101
+ "Next": "yellow",
102
+ "Later": "blue",
103
+ "Unscheduled": "gray",
104
+ "XS": "green",
105
+ "S": "blue",
106
+ "M": "yellow",
107
+ "L": "orange",
108
+ "XL": "red",
109
+ }
110
+ if option_name in semantic:
111
+ return semantic[option_name]
112
+ component_colors = ("blue", "green", "purple", "orange", "pink", "yellow")
113
+ return component_colors[position % len(component_colors)]
114
+
115
+
116
+ def issue_governance_drift(contract: EcosystemContract) -> list[str]:
117
+ errors: list[str] = []
118
+ actual_types = organization_issue_types(contract)
119
+ for name, expected in contract.issue_types.items():
120
+ actual = actual_types.get(name)
121
+ if actual is None:
122
+ errors.append(f"organization is missing issue type {name}")
123
+ continue
124
+ if not actual.get("is_enabled", True):
125
+ errors.append(f"organization issue type {name} is disabled")
126
+ if actual.get("description") != expected.description:
127
+ errors.append(f"organization issue type {name} description differs")
128
+ if actual.get("color") != expected.color:
129
+ errors.append(f"organization issue type {name} color differs")
130
+
131
+ actual_fields = organization_issue_fields(contract)
132
+ for name, expected in contract.fields.items():
133
+ if expected.owner is not FieldOwner.ISSUE:
134
+ continue
135
+ actual = actual_fields.get(name)
136
+ if actual is None:
137
+ errors.append(f"organization is missing issue field {name}")
138
+ continue
139
+ if actual.get("data_type") != expected.type.value:
140
+ errors.append(f"organization issue field {name} type differs")
141
+ expected_visibility = (
142
+ "all"
143
+ if expected.visibility is FieldVisibility.PUBLIC
144
+ else "organization_members_only"
145
+ )
146
+ if actual.get("visibility") != expected_visibility:
147
+ errors.append(f"organization issue field {name} visibility differs")
148
+ if actual.get("description") != expected.description:
149
+ errors.append(f"organization issue field {name} description differs")
150
+ if expected.type in {
151
+ ProjectFieldType.SINGLE_SELECT,
152
+ ProjectFieldType.MULTI_SELECT,
153
+ }:
154
+ actual_options = tuple(
155
+ option["name"]
156
+ for option in sorted(
157
+ actual.get("options", ()),
158
+ key=lambda option: option["priority"],
159
+ )
160
+ )
161
+ if actual_options != contract.field_options(name):
162
+ errors.append(f"organization issue field {name} options differ")
163
+ return errors
164
+
165
+
166
+ def desired_issue_field_options(
167
+ contract: EcosystemContract,
168
+ name: str,
169
+ actual: dict[str, Any] | None,
170
+ ) -> list[dict[str, Any]]:
171
+ existing = {option["name"]: option for option in (actual or {}).get("options", ())}
172
+ aliases = {
173
+ "Priority": {"P0": "Urgent", "P1": "High", "P2": "Medium", "P3": "Low"},
174
+ "Effort": {"S": "Low", "M": "Medium", "L": "High"},
175
+ }
176
+ result: list[dict[str, Any]] = []
177
+ for position, option_name in enumerate(contract.field_options(name), start=1):
178
+ current = existing.get(option_name)
179
+ if current is None:
180
+ current = existing.get(aliases.get(name, {}).get(option_name, ""))
181
+ option: dict[str, Any] = {
182
+ "name": option_name,
183
+ "description": None,
184
+ "color": option_color(name, option_name, position - 1),
185
+ "priority": position,
186
+ }
187
+ if current is not None:
188
+ option["id"] = current["id"]
189
+ result.append(option)
190
+ return result
191
+
192
+
193
+ def apply_issue_governance(contract: EcosystemContract) -> None:
194
+ owner = contract.project.owner
195
+ actual_types = organization_issue_types(contract)
196
+ for name, expected in contract.issue_types.items():
197
+ payload = {
198
+ "name": name,
199
+ "description": expected.description,
200
+ "is_enabled": True,
201
+ "color": expected.color,
202
+ }
203
+ actual = actual_types.get(name)
204
+ if actual is None:
205
+ gh_api_json("POST", f"orgs/{owner}/issue-types", payload)
206
+ else:
207
+ query = """
208
+ mutation($input: UpdateIssueTypeInput!) {
209
+ updateIssueType(input: $input) { issueType { id } }
210
+ }
211
+ """
212
+ gh_graphql(
213
+ query,
214
+ {
215
+ "input": {
216
+ "issueTypeId": actual["node_id"],
217
+ "name": name,
218
+ "description": expected.description,
219
+ "isEnabled": True,
220
+ "color": expected.color.upper(),
221
+ }
222
+ },
223
+ )
224
+
225
+ actual_fields = organization_issue_fields(contract)
226
+ for name, expected in contract.fields.items():
227
+ if expected.owner is not FieldOwner.ISSUE:
228
+ continue
229
+ actual = actual_fields.get(name)
230
+ if actual is not None and actual.get("data_type") != expected.type.value:
231
+ raise RuntimeError(
232
+ f"cannot change organization issue field {name} from "
233
+ f"{actual.get('data_type')} to {expected.type.value}"
234
+ )
235
+ payload: dict[str, Any] = {
236
+ "name": name,
237
+ "description": expected.description,
238
+ "visibility": (
239
+ "all"
240
+ if expected.visibility is FieldVisibility.PUBLIC
241
+ else "organization_members_only"
242
+ ),
243
+ }
244
+ if expected.type in {
245
+ ProjectFieldType.SINGLE_SELECT,
246
+ ProjectFieldType.MULTI_SELECT,
247
+ }:
248
+ payload["options"] = desired_issue_field_options(contract, name, actual)
249
+ if actual is None:
250
+ payload["data_type"] = expected.type.value
251
+ gh_api_json("POST", f"orgs/{owner}/issue-fields", payload)
252
+ else:
253
+ gh_api_json("PATCH", f"orgs/{owner}/issue-fields/{actual['id']}", payload)
34
254
 
35
255
 
36
256
  def workflow_drift(contract: EcosystemContract) -> list[str]:
@@ -258,6 +478,7 @@ def label_drift(contract: EcosystemContract) -> list[str]:
258
478
 
259
479
  def inspect_live(contract: EcosystemContract) -> EcosystemDrift:
260
480
  errors = workflow_drift(contract)
481
+ errors.extend(issue_governance_drift(contract))
261
482
  project = gh_json(
262
483
  "project",
263
484
  "view",
@@ -290,6 +511,10 @@ def inspect_live(contract: EcosystemContract) -> EcosystemDrift:
290
511
  if actual is None:
291
512
  errors.append(f"project is missing field {name}")
292
513
  continue
514
+ if expected.owner is FieldOwner.ISSUE:
515
+ # The organization field is validated through the issue-field API.
516
+ # Project GraphQL exposes the column but not its option definitions.
517
+ continue
293
518
  if expected.type is ProjectFieldType.SINGLE_SELECT:
294
519
  options = tuple(option["name"] for option in actual.get("options", ()))
295
520
  if options != contract.field_options(name):
@@ -306,11 +531,14 @@ def inspect_live(contract: EcosystemContract) -> EcosystemDrift:
306
531
  manual_controls=(
307
532
  "Saved Project views must be compared in the GitHub UI.",
308
533
  "Project auto-add, close, merge, and archive workflows must be compared in the GitHub UI.",
534
+ "Issue fields must be pinned and ordered for every issue type in the organization UI.",
535
+ "Legacy Project-only field values must be migrated before deleting duplicate fields.",
309
536
  ),
310
537
  )
311
538
 
312
539
 
313
540
  def apply_live(contract: EcosystemContract) -> None:
541
+ apply_issue_governance(contract)
314
542
  gh(
315
543
  "project",
316
544
  "edit",
@@ -390,7 +618,10 @@ def main() -> int:
390
618
  return 1
391
619
  print(
392
620
  f"valid ecosystem contract: {len(contract.packages)} packages, "
393
- f"{len(contract.fields)} project fields"
621
+ f"{sum(field.owner is FieldOwner.PROJECT for field in contract.fields.values())} "
622
+ "Project-owned field, "
623
+ f"{sum(field.owner is FieldOwner.ISSUE for field in contract.fields.values())} "
624
+ "issue-owned fields"
394
625
  )
395
626
  return 0
396
627
  if arguments.command == "apply-live":