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.
- {modwire-4.0.2 → modwire-5.0.0}/.github/ISSUE_TEMPLATE/bug_report.yml +3 -2
- modwire-5.0.0/.github/ISSUE_TEMPLATE/decision.yml +48 -0
- modwire-5.0.0/.github/ISSUE_TEMPLATE/epic.yml +51 -0
- {modwire-4.0.2 → modwire-5.0.0}/.github/ISSUE_TEMPLATE/feature_request.yml +3 -2
- modwire-5.0.0/.github/ISSUE_TEMPLATE/task.yml +56 -0
- {modwire-4.0.2 → modwire-5.0.0}/.github/modwire-ecosystem.yml +81 -25
- {modwire-4.0.2 → modwire-5.0.0}/.github/scripts/reconcile_ecosystem.py +234 -3
- modwire-5.0.0/.github/workflows/human-approval.yml +133 -0
- modwire-5.0.0/.github/workflows/issue-metadata-check.yml +17 -0
- modwire-5.0.0/.github/workflows/issue-metadata.yml +87 -0
- {modwire-4.0.2 → modwire-5.0.0}/CHANGELOG.md +18 -0
- modwire-5.0.0/MANIFEST.in +2 -0
- {modwire-4.0.2/src/modwire.egg-info → modwire-5.0.0}/PKG-INFO +35 -29
- {modwire-4.0.2 → modwire-5.0.0}/README.md +30 -24
- modwire-5.0.0/docs/decisions/0001-canonical-modwire-repository-shape.md +106 -0
- {modwire-4.0.2 → modwire-5.0.0}/docs/governance/github-ecosystem.md +88 -22
- {modwire-4.0.2 → modwire-5.0.0}/docs/governance/python-workflows.md +3 -3
- modwire-5.0.0/docs/migration-5.md +53 -0
- modwire-5.0.0/docs/scaffolding/golden/cli.yaml +44 -0
- modwire-5.0.0/docs/scaffolding/golden/library.yaml +40 -0
- modwire-5.0.0/docs/scaffolding/repository-contract-v1.yaml +147 -0
- modwire-5.0.0/docs/wiki/Home.md +28 -0
- {modwire-4.0.2 → modwire-5.0.0}/docs/wiki/Reporting-bugs.md +1 -1
- {modwire-4.0.2 → modwire-5.0.0}/docs/wiki/Requesting-features.md +1 -1
- {modwire-4.0.2 → modwire-5.0.0}/pyproject.toml +4 -4
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/__init__.py +2 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/_version.py +3 -3
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/__init__.py +0 -3
- modwire-5.0.0/src/modwire/architecture/app.py +67 -0
- modwire-5.0.0/src/modwire/architecture/boundaries/__init__.py +4 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/analyzer.py +6 -2
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/analyzers/backward.py +2 -0
- modwire-5.0.0/src/modwire/architecture/boundaries/analyzers/module_boundaries.py +146 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/analyzers/no_cycles.py +2 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/analyzers/no_reentry.py +2 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/base.py +4 -0
- modwire-5.0.0/src/modwire/architecture/boundaries/composition.py +22 -0
- modwire-5.0.0/src/modwire/architecture/composition.py +21 -0
- modwire-5.0.0/src/modwire/architecture/insights/__init__.py +4 -0
- modwire-5.0.0/src/modwire/architecture/insights/composition.py +18 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/reporters/coherence.py +2 -7
- modwire-5.0.0/src/modwire/architecture/map/__init__.py +4 -0
- modwire-5.0.0/src/modwire/architecture/map/composition.py +8 -0
- modwire-5.0.0/src/modwire/architecture/shape/__init__.py +4 -0
- modwire-5.0.0/src/modwire/architecture/shape/composition.py +27 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/import_resolver.py +5 -1
- modwire-5.0.0/src/modwire/code/__init__.py +6 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/facade.py +1 -8
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/projects/__init__.py +10 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/projects/ecosystem.py +70 -9
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/base.py +0 -15
- modwire-5.0.0/src/modwire/shared/config/modwire.py +14 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/config/shape.py +2 -2
- {modwire-4.0.2 → modwire-5.0.0/src/modwire.egg-info}/PKG-INFO +35 -29
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire.egg-info/SOURCES.txt +21 -3
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire.egg-info/requires.txt +1 -1
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire.egg-info/scm_file_list.json +21 -3
- modwire-5.0.0/src/modwire.egg-info/scm_version.json +8 -0
- {modwire-4.0.2 → modwire-5.0.0}/tests/test_architecture.py +2 -2
- modwire-5.0.0/tests/test_boundaries.py +192 -0
- {modwire-4.0.2 → modwire-5.0.0}/tests/test_ecosystem_contract.py +39 -13
- modwire-5.0.0/tests/test_import_shape.py +57 -0
- {modwire-4.0.2 → modwire-5.0.0}/tests/test_models.py +9 -12
- modwire-5.0.0/tests/test_repository_shape_contract.py +44 -0
- {modwire-4.0.2 → modwire-5.0.0}/uv.lock +4 -4
- modwire-4.0.2/MANIFEST.in +0 -1
- modwire-4.0.2/docs/wiki/Home.md +0 -27
- modwire-4.0.2/src/modwire/architecture/app.py +0 -137
- modwire-4.0.2/src/modwire/architecture/boundaries/__init__.py +0 -8
- modwire-4.0.2/src/modwire/architecture/insights/__init__.py +0 -8
- modwire-4.0.2/src/modwire/architecture/map/__init__.py +0 -11
- modwire-4.0.2/src/modwire/architecture/shape/__init__.py +0 -8
- modwire-4.0.2/src/modwire/code/__init__.py +0 -10
- modwire-4.0.2/src/modwire/code/reader.py +0 -9
- modwire-4.0.2/src/modwire/code/writer.py +0 -27
- modwire-4.0.2/src/modwire/shared/config/modwire.py +0 -54
- modwire-4.0.2/src/modwire.egg-info/scm_version.json +0 -8
- {modwire-4.0.2 → modwire-5.0.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/.github/workflows/ci.yml +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/.github/workflows/python-package.yml +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/.github/workflows/python-release-assets.yml +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/.github/workflows/python-release-build.yml +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/.github/workflows/release.yml +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/.gitignore +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/.modwire/boundaries.yaml +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/.modwire/shape.yaml +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/CONTRIBUTING.md +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/LICENSE +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/Makefile +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/docs/wiki/Development-checks.md +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/setup.cfg +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/analyzers/__init__.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/analyzers/analyzer_base.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/collector.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/tags/__init__.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/tags/matcher.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/boundaries/tags/tag_map.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/base.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/collector.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/reporters/__init__.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/reporters/base.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/reporters/callables.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/reporters/clusters.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/reporters/exports.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/insights/reporters/hotspots.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/map/base.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/map/collector.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/map/loader.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/base.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/collector.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/__init__.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/abstract_class_resolver.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/callable_resolver.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/class_resolver.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/file_resolver.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/property_resolver.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/signature_resolver.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/architecture/shape/resolvers/symbol_resolver.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/code/package.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/layers/__init__.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/layers/app.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/modules/__init__.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/modules/app.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/projects/app.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/py.typed +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/__init__.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/config/__init__.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/config/architecture.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/config/layers.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/config/modules.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/config/projects.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/report/__init__.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire/shared/report/base.py +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire.egg-info/dependency_links.txt +0 -0
- {modwire-4.0.2 → modwire-5.0.0}/src/modwire.egg-info/top_level.txt +0 -0
|
@@ -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
|
|
@@ -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:
|
|
1
|
+
version: 3
|
|
2
2
|
|
|
3
3
|
packages:
|
|
4
4
|
core:
|
|
5
|
-
repository:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
48
|
-
depends_on: [
|
|
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:
|
|
51
|
+
owner: modwire
|
|
52
52
|
title: Modwire Ecosystem
|
|
53
|
-
number:
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
|
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.
|
|
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"{
|
|
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":
|