engineering-process 1.0.0__tar.gz → 1.1.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.
- {engineering_process-1.0.0/engineering_process.egg-info → engineering_process-1.1.0}/PKG-INFO +67 -4
- {engineering_process-1.0.0 → engineering_process-1.1.0}/README.md +66 -3
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/__init__.py +1 -1
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/_supervisor_posix.py +24 -25
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/cli.py +55 -111
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/project.py +58 -1
- {engineering_process-1.0.0 → engineering_process-1.1.0/engineering_process.egg-info}/PKG-INFO +67 -4
- {engineering_process-1.0.0 → engineering_process-1.1.0}/process_assets/skills/finish-change/SKILL.md +5 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/process_assets/skills/implement-change/SKILL.md +6 -0
- engineering_process-1.1.0/process_assets/skills/improve-process/SKILL.md +75 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/process_assets/skills/plan-change/SKILL.md +6 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/process_assets/skills/review-change/SKILL.md +7 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/process_assets/skills/run-change/SKILL.md +10 -2
- {engineering_process-1.0.0 → engineering_process-1.1.0}/process_assets/skills/start-change/SKILL.md +8 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/process_assets/skills/verify-change/SKILL.md +6 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/pyproject.toml +1 -1
- {engineering_process-1.0.0 → engineering_process-1.1.0}/schemas/project.schema.json +58 -1
- {engineering_process-1.0.0 → engineering_process-1.1.0}/tests/test_architecture.py +1 -1
- {engineering_process-1.0.0 → engineering_process-1.1.0}/tests/test_automation.py +83 -3
- {engineering_process-1.0.0 → engineering_process-1.1.0}/tests/test_cli.py +22 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/tests/test_commands.py +28 -0
- engineering_process-1.1.0/tests/test_contracts.py +232 -0
- engineering_process-1.1.0/tests/test_skills.py +88 -0
- engineering_process-1.0.0/process_assets/skills/improve-process/SKILL.md +0 -22
- engineering_process-1.0.0/tests/test_contracts.py +0 -67
- engineering_process-1.0.0/tests/test_skills.py +0 -53
- {engineering_process-1.0.0 → engineering_process-1.1.0}/LICENSE +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/MANIFEST.in +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/__main__.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/_supervisor_windows.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/_windows_job.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/adoption.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/commands.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/contracts.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/distribution.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/helper_launch.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/lifecycle.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/publication_compat.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/release.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/repository.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/requirements-dev.txt +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/requirements-runtime.txt +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/skills.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/supervision.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process.egg-info/SOURCES.txt +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process.egg-info/dependency_links.txt +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process.egg-info/entry_points.txt +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process.egg-info/requires.txt +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process.egg-info/top_level.txt +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/process-graph.json +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/schemas/change.schema.json +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/schemas/plan.schema.json +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/schemas/process-graph.schema.json +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/schemas/process-lock.schema.json +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/schemas/project-legacy.schema.json +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/schemas/receipt.schema.json +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/schemas/release-change.schema.json +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/schemas/release.schema.json +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/schemas/review.schema.json +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/schemas/run.schema.json +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/setup.cfg +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/templates/AGENTS.process.md +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/templates/PULL_REQUEST_TEMPLATE.md +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/templates/adopt-process-windows-job.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/templates/adopt-process.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/tests/test_adoption.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/tests/test_lifecycle.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/tests/test_publication_compat.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/tests/test_release.py +0 -0
- {engineering_process-1.0.0 → engineering_process-1.1.0}/tests/test_repository.py +0 -0
{engineering_process-1.0.0/engineering_process.egg-info → engineering_process-1.1.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: engineering-process
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: A small agent-neutral engineering lifecycle with managed adoption
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/phuongnse/engineering-process
|
|
@@ -92,10 +92,72 @@ Commands are argument arrays, never shell strings. Each command has a finite tim
|
|
|
92
92
|
Output has a hard aggregate budget; evidence stores byte counts and hashes, never raw
|
|
93
93
|
stdout or stderr that could contain secrets.
|
|
94
94
|
|
|
95
|
+
### Production readiness
|
|
96
|
+
|
|
97
|
+
A consumer declares `.process/readiness.json` with production as its direction, its
|
|
98
|
+
current stage, immutable pack versions, and the state of every required capability.
|
|
99
|
+
An enforced capability maps to project-required profiles; a planned capability names
|
|
100
|
+
the concrete gap without pretending to have evidence:
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
"target": "production",
|
|
104
|
+
"stage": "production",
|
|
105
|
+
"packs": [{"id": "library-cli", "version": 1}],
|
|
106
|
+
"capabilities": [
|
|
107
|
+
{"id": "correctness", "state": "enforced", "evidenceProfiles": ["development"]},
|
|
108
|
+
{"id": "runtime-safety", "state": "enforced", "evidenceProfiles": ["development"]},
|
|
109
|
+
{"id": "compatibility", "state": "enforced", "evidenceProfiles": ["development"]},
|
|
110
|
+
{"id": "portability", "state": "enforced", "evidenceProfiles": ["development", "review"]},
|
|
111
|
+
{"id": "installability", "state": "enforced", "evidenceProfiles": ["review"]},
|
|
112
|
+
{"id": "distribution-integrity", "state": "enforced", "evidenceProfiles": ["review"]},
|
|
113
|
+
{"id": "adoption-integrity", "state": "enforced", "evidenceProfiles": ["development", "review"]}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
`project validate` and `doctor` resolve that declaration to the exact checks owned by
|
|
118
|
+
the consumer. The declaration does not make a weak command sufficient: normal CI and
|
|
119
|
+
independent review still judge whether those commands prove the named capability. A
|
|
120
|
+
building consumer may keep planned gaps while ordinary development continues. A
|
|
121
|
+
production-stage declaration fails closed if any capability remains planned.
|
|
122
|
+
|
|
123
|
+
The sidecar is a deliberate self-hosting boundary. Public authority N continues to
|
|
124
|
+
validate the unchanged strict `.process/project.json` while source N+1 validates and
|
|
125
|
+
self-applies the new readiness contract. Adoption leaves the consumer-owned sidecar
|
|
126
|
+
in place, so every later authority can repeat the same forward-compatible sequence.
|
|
127
|
+
Pack versions are also immutable: a process update must keep `operations@1` working
|
|
128
|
+
even after `operations@2` exists. Process adoption and pack upgrades are separate
|
|
129
|
+
consumer-owned changes, preventing a new standard from deadlocking authority adoption.
|
|
130
|
+
|
|
131
|
+
`library-cli@1` was derived from this repository as a real producer and self-consumer.
|
|
132
|
+
`operations@1` was then derived from renovate-ops and requires auditability, automation
|
|
133
|
+
correctness, bounded execution, least privilege, policy integrity, recovery, and
|
|
134
|
+
target-selection integrity. `desktop-media@1` is derived from LyricRail and keeps its
|
|
135
|
+
existing correctness, input, source-portability, audit, media, package, and recovery-
|
|
136
|
+
mechanism evidence enforced. Stable dependency/recovery claims, signing, key custody,
|
|
137
|
+
runtime/license delivery, Linux advisory resolution, real-host workspace security,
|
|
138
|
+
updater, incident recovery, and independent security review remain planned.
|
|
139
|
+
Consumers without readiness remain compatible during that evidence-backed rollout.
|
|
140
|
+
|
|
141
|
+
For each ordinary change, `run-change` first surfaces this readiness view. The accepted
|
|
142
|
+
request and consumer rules determine which capabilities are affected. Every change
|
|
143
|
+
retains the project's baseline `requiredProfiles`; start and plan add any conditional
|
|
144
|
+
evidence profiles needed by affected capabilities and include a planned gap only when
|
|
145
|
+
the accepted request explicitly selects it. Implement and review protect the enforced
|
|
146
|
+
floor. A planned-to-enforced promotion is a reviewed consumer source diff with fresh
|
|
147
|
+
evidence. Unrelated planned gaps remain visible but do not block development, and no
|
|
148
|
+
skill chooses product priorities or changes readiness automatically.
|
|
149
|
+
|
|
150
|
+
When a consumer incident exposes a reusable process gap, `improve-process` first keeps
|
|
151
|
+
the consumer safe, then prepares a sanitized GitHub issue draft from that checkout.
|
|
152
|
+
It deduplicates by consumer/process-version/invariant, requires owner authorization
|
|
153
|
+
before `gh issue create`, and uses an accepted issue as the later process change source
|
|
154
|
+
and `consumerEvidence`. No producer clone, consumer-CI write token, automatic process
|
|
155
|
+
mutation, or wait for a process release is required to continue consumer development.
|
|
156
|
+
|
|
95
157
|
Pin the process in requirements/process.in:
|
|
96
158
|
|
|
97
159
|
--only-binary :all:
|
|
98
|
-
engineering-process==0.
|
|
160
|
+
engineering-process==1.0.1
|
|
99
161
|
|
|
100
162
|
Generate requirements/process.txt with hashes, install that lock, then run:
|
|
101
163
|
|
|
@@ -232,7 +294,7 @@ At any point:
|
|
|
232
294
|
## Release to consumer PR
|
|
233
295
|
|
|
234
296
|
Every opted-in consumer uses Renovate's pip-compile manager. Its engineering-process
|
|
235
|
-
package rule
|
|
297
|
+
package rule keeps the adoption pull request in draft and runs exactly:
|
|
236
298
|
|
|
237
299
|
python .process/adopt-process.py --project-root . --requirements-lock requirements/process.txt
|
|
238
300
|
|
|
@@ -247,7 +309,8 @@ The release workflow publishes exact wheel and sdist bytes to PyPI, verifies the
|
|
|
247
309
|
registry hashes, creates the immutable GitHub release, and sends one authenticated
|
|
248
310
|
engineering-process-published event to renovate-ops. That control plane runs Renovate
|
|
249
311
|
for each repository whose protected config explicitly opts in. Each consumer's normal
|
|
250
|
-
CI and independent review decide whether its PR can merge
|
|
312
|
+
CI and independent review decide whether its draft PR can merge; the consumer owner
|
|
313
|
+
authorizes that merge.
|
|
251
314
|
|
|
252
315
|
This repository opts in through .github/renovate.json, so it receives the same
|
|
253
316
|
adoption PR as every other consumer. See SELF_HOSTING.md and RELEASING.md.
|
|
@@ -68,10 +68,72 @@ Commands are argument arrays, never shell strings. Each command has a finite tim
|
|
|
68
68
|
Output has a hard aggregate budget; evidence stores byte counts and hashes, never raw
|
|
69
69
|
stdout or stderr that could contain secrets.
|
|
70
70
|
|
|
71
|
+
### Production readiness
|
|
72
|
+
|
|
73
|
+
A consumer declares `.process/readiness.json` with production as its direction, its
|
|
74
|
+
current stage, immutable pack versions, and the state of every required capability.
|
|
75
|
+
An enforced capability maps to project-required profiles; a planned capability names
|
|
76
|
+
the concrete gap without pretending to have evidence:
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
"target": "production",
|
|
80
|
+
"stage": "production",
|
|
81
|
+
"packs": [{"id": "library-cli", "version": 1}],
|
|
82
|
+
"capabilities": [
|
|
83
|
+
{"id": "correctness", "state": "enforced", "evidenceProfiles": ["development"]},
|
|
84
|
+
{"id": "runtime-safety", "state": "enforced", "evidenceProfiles": ["development"]},
|
|
85
|
+
{"id": "compatibility", "state": "enforced", "evidenceProfiles": ["development"]},
|
|
86
|
+
{"id": "portability", "state": "enforced", "evidenceProfiles": ["development", "review"]},
|
|
87
|
+
{"id": "installability", "state": "enforced", "evidenceProfiles": ["review"]},
|
|
88
|
+
{"id": "distribution-integrity", "state": "enforced", "evidenceProfiles": ["review"]},
|
|
89
|
+
{"id": "adoption-integrity", "state": "enforced", "evidenceProfiles": ["development", "review"]}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
`project validate` and `doctor` resolve that declaration to the exact checks owned by
|
|
94
|
+
the consumer. The declaration does not make a weak command sufficient: normal CI and
|
|
95
|
+
independent review still judge whether those commands prove the named capability. A
|
|
96
|
+
building consumer may keep planned gaps while ordinary development continues. A
|
|
97
|
+
production-stage declaration fails closed if any capability remains planned.
|
|
98
|
+
|
|
99
|
+
The sidecar is a deliberate self-hosting boundary. Public authority N continues to
|
|
100
|
+
validate the unchanged strict `.process/project.json` while source N+1 validates and
|
|
101
|
+
self-applies the new readiness contract. Adoption leaves the consumer-owned sidecar
|
|
102
|
+
in place, so every later authority can repeat the same forward-compatible sequence.
|
|
103
|
+
Pack versions are also immutable: a process update must keep `operations@1` working
|
|
104
|
+
even after `operations@2` exists. Process adoption and pack upgrades are separate
|
|
105
|
+
consumer-owned changes, preventing a new standard from deadlocking authority adoption.
|
|
106
|
+
|
|
107
|
+
`library-cli@1` was derived from this repository as a real producer and self-consumer.
|
|
108
|
+
`operations@1` was then derived from renovate-ops and requires auditability, automation
|
|
109
|
+
correctness, bounded execution, least privilege, policy integrity, recovery, and
|
|
110
|
+
target-selection integrity. `desktop-media@1` is derived from LyricRail and keeps its
|
|
111
|
+
existing correctness, input, source-portability, audit, media, package, and recovery-
|
|
112
|
+
mechanism evidence enforced. Stable dependency/recovery claims, signing, key custody,
|
|
113
|
+
runtime/license delivery, Linux advisory resolution, real-host workspace security,
|
|
114
|
+
updater, incident recovery, and independent security review remain planned.
|
|
115
|
+
Consumers without readiness remain compatible during that evidence-backed rollout.
|
|
116
|
+
|
|
117
|
+
For each ordinary change, `run-change` first surfaces this readiness view. The accepted
|
|
118
|
+
request and consumer rules determine which capabilities are affected. Every change
|
|
119
|
+
retains the project's baseline `requiredProfiles`; start and plan add any conditional
|
|
120
|
+
evidence profiles needed by affected capabilities and include a planned gap only when
|
|
121
|
+
the accepted request explicitly selects it. Implement and review protect the enforced
|
|
122
|
+
floor. A planned-to-enforced promotion is a reviewed consumer source diff with fresh
|
|
123
|
+
evidence. Unrelated planned gaps remain visible but do not block development, and no
|
|
124
|
+
skill chooses product priorities or changes readiness automatically.
|
|
125
|
+
|
|
126
|
+
When a consumer incident exposes a reusable process gap, `improve-process` first keeps
|
|
127
|
+
the consumer safe, then prepares a sanitized GitHub issue draft from that checkout.
|
|
128
|
+
It deduplicates by consumer/process-version/invariant, requires owner authorization
|
|
129
|
+
before `gh issue create`, and uses an accepted issue as the later process change source
|
|
130
|
+
and `consumerEvidence`. No producer clone, consumer-CI write token, automatic process
|
|
131
|
+
mutation, or wait for a process release is required to continue consumer development.
|
|
132
|
+
|
|
71
133
|
Pin the process in requirements/process.in:
|
|
72
134
|
|
|
73
135
|
--only-binary :all:
|
|
74
|
-
engineering-process==0.
|
|
136
|
+
engineering-process==1.0.1
|
|
75
137
|
|
|
76
138
|
Generate requirements/process.txt with hashes, install that lock, then run:
|
|
77
139
|
|
|
@@ -208,7 +270,7 @@ At any point:
|
|
|
208
270
|
## Release to consumer PR
|
|
209
271
|
|
|
210
272
|
Every opted-in consumer uses Renovate's pip-compile manager. Its engineering-process
|
|
211
|
-
package rule
|
|
273
|
+
package rule keeps the adoption pull request in draft and runs exactly:
|
|
212
274
|
|
|
213
275
|
python .process/adopt-process.py --project-root . --requirements-lock requirements/process.txt
|
|
214
276
|
|
|
@@ -223,7 +285,8 @@ The release workflow publishes exact wheel and sdist bytes to PyPI, verifies the
|
|
|
223
285
|
registry hashes, creates the immutable GitHub release, and sends one authenticated
|
|
224
286
|
engineering-process-published event to renovate-ops. That control plane runs Renovate
|
|
225
287
|
for each repository whose protected config explicitly opts in. Each consumer's normal
|
|
226
|
-
CI and independent review decide whether its PR can merge
|
|
288
|
+
CI and independent review decide whether its draft PR can merge; the consumer owner
|
|
289
|
+
authorizes that merge.
|
|
227
290
|
|
|
228
291
|
This repository opts in through .github/renovate.json, so it receives the same
|
|
229
292
|
adoption PR as every other consumer. See SELF_HOSTING.md and RELEASING.md.
|
{engineering_process-1.0.0 → engineering_process-1.1.0}/engineering_process/_supervisor_posix.py
RENAMED
|
@@ -98,13 +98,24 @@ def _pid_alive(pid: int) -> bool:
|
|
|
98
98
|
status = Path(f"/proc/{pid}/stat")
|
|
99
99
|
if status.is_file():
|
|
100
100
|
try:
|
|
101
|
-
|
|
101
|
+
state = status.read_text(encoding="utf-8").rsplit(")", 1)[1].split()[0]
|
|
102
|
+
if state == "Z":
|
|
103
|
+
try:
|
|
104
|
+
os.waitpid(pid, os.WNOHANG)
|
|
105
|
+
except OSError:
|
|
106
|
+
pass
|
|
107
|
+
return False
|
|
102
108
|
except (OSError, IndexError):
|
|
103
109
|
pass
|
|
104
110
|
return True
|
|
105
111
|
|
|
106
112
|
|
|
107
113
|
def _process_group_exists(process_group: int) -> bool:
|
|
114
|
+
try:
|
|
115
|
+
while os.waitpid(-process_group, os.WNOHANG)[0]:
|
|
116
|
+
pass
|
|
117
|
+
except OSError:
|
|
118
|
+
pass
|
|
108
119
|
try:
|
|
109
120
|
os.killpg(process_group, 0)
|
|
110
121
|
except ProcessLookupError:
|
|
@@ -114,6 +125,14 @@ def _process_group_exists(process_group: int) -> bool:
|
|
|
114
125
|
return True
|
|
115
126
|
|
|
116
127
|
|
|
128
|
+
def _signal_processes(processes: set[int], signal_number: int) -> None:
|
|
129
|
+
for pid in processes:
|
|
130
|
+
try:
|
|
131
|
+
os.kill(pid, signal_number)
|
|
132
|
+
except OSError:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
|
|
117
136
|
def _wait_for_process_group(process_group: int, timeout: float) -> bool:
|
|
118
137
|
deadline = time.monotonic() + timeout
|
|
119
138
|
while time.monotonic() < deadline:
|
|
@@ -144,11 +163,7 @@ def _terminate_group(process_group: int, grace_seconds: float) -> CleanupOutcome
|
|
|
144
163
|
return CleanupOutcome(
|
|
145
164
|
bounded=bounded,
|
|
146
165
|
descendants_found=True,
|
|
147
|
-
error=
|
|
148
|
-
None
|
|
149
|
-
if bounded
|
|
150
|
-
else "command process group could not be terminated within the bounded grace period"
|
|
151
|
-
),
|
|
166
|
+
error=None if bounded else "command process group exceeded its termination grace period",
|
|
152
167
|
)
|
|
153
168
|
|
|
154
169
|
|
|
@@ -250,24 +265,12 @@ class PosixProcessSupervisor:
|
|
|
250
265
|
live = {pid for pid in candidates if _pid_alive(pid)}
|
|
251
266
|
if not live:
|
|
252
267
|
return CleanupOutcome(bounded=True)
|
|
253
|
-
|
|
254
|
-
try:
|
|
255
|
-
os.kill(pid, signal.SIGTERM)
|
|
256
|
-
except ProcessLookupError:
|
|
257
|
-
pass
|
|
258
|
-
except OSError:
|
|
259
|
-
pass
|
|
268
|
+
_signal_processes(live, signal.SIGTERM)
|
|
260
269
|
deadline = time.monotonic() + grace_seconds
|
|
261
270
|
while time.monotonic() < deadline and any(_pid_alive(pid) for pid in live):
|
|
262
271
|
time.sleep(0.02)
|
|
263
272
|
survivors = {pid for pid in live if _pid_alive(pid)}
|
|
264
|
-
|
|
265
|
-
try:
|
|
266
|
-
os.kill(pid, signal.SIGKILL)
|
|
267
|
-
except ProcessLookupError:
|
|
268
|
-
pass
|
|
269
|
-
except OSError:
|
|
270
|
-
pass
|
|
273
|
+
_signal_processes(survivors, signal.SIGKILL)
|
|
271
274
|
deadline = time.monotonic() + grace_seconds
|
|
272
275
|
while time.monotonic() < deadline and any(
|
|
273
276
|
_pid_alive(pid) for pid in survivors
|
|
@@ -282,11 +285,7 @@ class PosixProcessSupervisor:
|
|
|
282
285
|
return CleanupOutcome(
|
|
283
286
|
bounded=bounded,
|
|
284
287
|
descendants_found=True,
|
|
285
|
-
error=
|
|
286
|
-
None
|
|
287
|
-
if bounded
|
|
288
|
-
else "detached descendants survived bounded termination"
|
|
289
|
-
),
|
|
288
|
+
error=None if bounded else "detached descendants survived bounded termination",
|
|
290
289
|
)
|
|
291
290
|
|
|
292
291
|
def terminate(
|
|
@@ -34,7 +34,7 @@ from .lifecycle import (
|
|
|
34
34
|
submit_review,
|
|
35
35
|
verify_change,
|
|
36
36
|
)
|
|
37
|
-
from .project import load_project
|
|
37
|
+
from .project import load_project, readiness_summary
|
|
38
38
|
from .publication_compat import (
|
|
39
39
|
branch_issues,
|
|
40
40
|
commit_issues,
|
|
@@ -97,6 +97,7 @@ def command_project_validate(args: argparse.Namespace) -> Result:
|
|
|
97
97
|
project=project["project"],
|
|
98
98
|
profiles=sorted(project["profiles"]),
|
|
99
99
|
requiredProfiles=project["lifecycle"]["requiredProfiles"],
|
|
100
|
+
readiness=readiness_summary(project),
|
|
100
101
|
), 0
|
|
101
102
|
|
|
102
103
|
|
|
@@ -155,6 +156,7 @@ def command_doctor(args: argparse.Namespace) -> Result:
|
|
|
155
156
|
project=project["project"],
|
|
156
157
|
processVersion=VERSION,
|
|
157
158
|
profiles=sorted(project["profiles"]),
|
|
159
|
+
readiness=readiness_summary(project),
|
|
158
160
|
issues=issues,
|
|
159
161
|
), (0 if not issues else 1)
|
|
160
162
|
|
|
@@ -341,47 +343,27 @@ def command_change_status(args: argparse.Namespace) -> Result:
|
|
|
341
343
|
|
|
342
344
|
|
|
343
345
|
def command_release_validate(args: argparse.Namespace) -> Result:
|
|
344
|
-
details = validate_release(
|
|
345
|
-
args.project_root, _process_root(args), tag=args.tag
|
|
346
|
-
)
|
|
346
|
+
details = validate_release(args.project_root, _process_root(args), tag=args.tag)
|
|
347
347
|
return _result("release validate", **details), 0
|
|
348
348
|
|
|
349
349
|
|
|
350
|
-
def
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
def command_publication_commit(args: argparse.Namespace) -> Result:
|
|
363
|
-
return _publication_result(
|
|
364
|
-
"publication validate-commit", {"issues": commit_issues(args.subject)}
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
def command_publication_range(args: argparse.Namespace) -> Result:
|
|
369
|
-
return _publication_result(
|
|
370
|
-
"publication validate-range",
|
|
371
|
-
validate_range(args.project_root, args.branch, args.range_spec),
|
|
372
|
-
)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
def command_publication_pr(args: argparse.Namespace) -> Result:
|
|
376
|
-
return _publication_result(
|
|
377
|
-
"publication validate-pr",
|
|
378
|
-
validate_pull_request(
|
|
350
|
+
def command_publication(args: argparse.Namespace) -> Result:
|
|
351
|
+
name = args.publication_command
|
|
352
|
+
if name == "validate-branch":
|
|
353
|
+
details = {"issues": branch_issues(args.branch)}
|
|
354
|
+
elif name == "validate-commit":
|
|
355
|
+
details = {"issues": commit_issues(args.subject)}
|
|
356
|
+
elif name == "validate-range":
|
|
357
|
+
details = validate_range(args.project_root, args.branch, args.range_spec)
|
|
358
|
+
else:
|
|
359
|
+
details = validate_pull_request(
|
|
379
360
|
title=args.title,
|
|
380
361
|
branch=args.branch,
|
|
381
362
|
state=args.state,
|
|
382
363
|
body_path=args.body_file,
|
|
383
|
-
)
|
|
384
|
-
|
|
364
|
+
)
|
|
365
|
+
status = "passed" if not details["issues"] else "failed"
|
|
366
|
+
return _result(f"publication {name}", status=status, **details), (0 if status == "passed" else 1)
|
|
385
367
|
|
|
386
368
|
|
|
387
369
|
def _add_common(parser: argparse.ArgumentParser, *, project: bool = True) -> None:
|
|
@@ -397,151 +379,113 @@ def _add_actor(parser: argparse.ArgumentParser) -> None:
|
|
|
397
379
|
parser.add_argument("--actor-kind", choices=("agent", "human"), default="agent")
|
|
398
380
|
|
|
399
381
|
|
|
382
|
+
def _leaf(subparsers: Any, name: str, handler: Callable[..., Result], *, project: bool = True, help: str | None = None) -> argparse.ArgumentParser:
|
|
383
|
+
parser = subparsers.add_parser(name, help=help)
|
|
384
|
+
_add_common(parser, project=project)
|
|
385
|
+
parser.set_defaults(handler=handler)
|
|
386
|
+
return parser
|
|
387
|
+
|
|
388
|
+
|
|
400
389
|
def build_parser() -> argparse.ArgumentParser:
|
|
401
390
|
parser = argparse.ArgumentParser(
|
|
402
391
|
prog="processctl", description="Small, enforceable engineering process"
|
|
403
392
|
)
|
|
404
393
|
parser.add_argument("--version", action="version", version=VERSION)
|
|
405
394
|
commands = parser.add_subparsers(dest="command", required=True)
|
|
406
|
-
|
|
407
395
|
project = commands.add_parser("project", help="Validate consumer configuration")
|
|
408
396
|
project_commands = project.add_subparsers(dest="project_command", required=True)
|
|
409
|
-
|
|
410
|
-
_add_common(project_validate)
|
|
411
|
-
project_validate.set_defaults(handler=command_project_validate)
|
|
412
|
-
|
|
397
|
+
_leaf(project_commands, "validate", command_project_validate)
|
|
413
398
|
lock = commands.add_parser("lock", help="Validate the adopted process lock")
|
|
414
399
|
lock_commands = lock.add_subparsers(dest="lock_command", required=True)
|
|
415
|
-
|
|
416
|
-
_add_common(lock_validate)
|
|
417
|
-
lock_validate.set_defaults(handler=command_lock_validate)
|
|
418
|
-
|
|
400
|
+
_leaf(lock_commands, "validate", command_lock_validate)
|
|
419
401
|
contract = commands.add_parser("contract", help="Validate a JSON contract")
|
|
420
402
|
contract_commands = contract.add_subparsers(dest="contract_command", required=True)
|
|
421
|
-
contract_validate =
|
|
403
|
+
contract_validate = _leaf(
|
|
404
|
+
contract_commands, "validate", command_contract_validate, project=False
|
|
405
|
+
)
|
|
422
406
|
contract_validate.add_argument("--kind", choices=CONTRACT_KINDS, required=True)
|
|
423
407
|
contract_validate.add_argument("path", type=Path)
|
|
424
|
-
_add_common(contract_validate, project=False)
|
|
425
|
-
contract_validate.set_defaults(handler=command_contract_validate)
|
|
426
|
-
|
|
427
408
|
skills = commands.add_parser("skills", help="Validate the complete skill graph")
|
|
428
409
|
skills_commands = skills.add_subparsers(dest="skills_command", required=True)
|
|
429
|
-
skills_validate =
|
|
410
|
+
skills_validate = _leaf(
|
|
411
|
+
skills_commands, "validate", command_skills_validate, project=False
|
|
412
|
+
)
|
|
430
413
|
skills_validate.add_argument("--root", type=Path)
|
|
431
|
-
|
|
432
|
-
skills_validate.set_defaults(handler=command_skills_validate)
|
|
433
|
-
|
|
434
|
-
doctor = commands.add_parser("doctor", help="Validate one consumer integration")
|
|
435
|
-
_add_common(doctor)
|
|
414
|
+
doctor = _leaf(commands, "doctor", command_doctor, help="Validate one consumer integration")
|
|
436
415
|
doctor.add_argument("--profile")
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
setup = commands.add_parser("setup", help=argparse.SUPPRESS)
|
|
440
|
-
_add_common(setup)
|
|
416
|
+
setup = _leaf(commands, "setup", command_setup, help=argparse.SUPPRESS)
|
|
441
417
|
setup.add_argument("--profile", required=True)
|
|
442
418
|
setup.add_argument("--apply", action="store_true")
|
|
443
419
|
setup.add_argument("--allow", action="append", default=[])
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
verify = commands.add_parser("verify", help="Run a project verification profile")
|
|
447
|
-
_add_common(verify)
|
|
420
|
+
verify = _leaf(commands, "verify", command_verify, help="Run a project verification profile")
|
|
448
421
|
verify.add_argument("--profile", required=True)
|
|
449
|
-
verify.set_defaults(handler=command_verify)
|
|
450
|
-
|
|
451
422
|
adoption = commands.add_parser("adoption", help="Apply or check managed adoption")
|
|
452
423
|
adoption_commands = adoption.add_subparsers(dest="adoption_command", required=True)
|
|
453
424
|
for name, handler in (("apply", command_adoption_apply), ("check", command_adoption_check)):
|
|
454
|
-
adoption_command = adoption_commands
|
|
455
|
-
_add_common(adoption_command)
|
|
425
|
+
adoption_command = _leaf(adoption_commands, name, handler)
|
|
456
426
|
adoption_command.add_argument("--requirements-lock", type=Path, required=True)
|
|
457
427
|
if name == "apply":
|
|
458
428
|
adoption_command.add_argument("--requirements-source", type=Path)
|
|
459
429
|
adoption_command.add_argument("--expected-requirements-digest")
|
|
460
|
-
adoption_command.set_defaults(handler=handler)
|
|
461
|
-
|
|
462
430
|
change = commands.add_parser("change", help="Run the six-phase change lifecycle")
|
|
463
431
|
change_commands = change.add_subparsers(dest="change_command", required=True)
|
|
464
|
-
|
|
465
|
-
change_start = change_commands.add_parser("start")
|
|
466
|
-
_add_common(change_start)
|
|
432
|
+
change_start = _leaf(change_commands, "start", command_change_start)
|
|
467
433
|
_add_actor(change_start)
|
|
468
434
|
change_start.add_argument("--contract", type=Path, required=True)
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
change_plan = change_commands.add_parser("plan")
|
|
472
|
-
_add_common(change_plan)
|
|
435
|
+
change_plan = _leaf(change_commands, "plan", command_change_plan)
|
|
473
436
|
_add_actor(change_plan)
|
|
474
437
|
change_plan.add_argument("--change-id", required=True)
|
|
475
438
|
change_plan.add_argument("--plan", type=Path, required=True)
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
change_implement = change_commands.add_parser("implement")
|
|
479
|
-
_add_common(change_implement)
|
|
439
|
+
change_implement = _leaf(change_commands, "implement", command_change_implement)
|
|
480
440
|
_add_actor(change_implement)
|
|
481
441
|
change_implement.add_argument("--change-id", required=True)
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
change_verify = change_commands.add_parser("verify")
|
|
485
|
-
_add_common(change_verify)
|
|
442
|
+
change_verify = _leaf(change_commands, "verify", command_change_verify)
|
|
486
443
|
change_verify.add_argument("--change-id", required=True)
|
|
487
444
|
change_verify.add_argument("--profile", required=True)
|
|
488
|
-
change_verify.set_defaults(handler=command_change_verify)
|
|
489
445
|
|
|
490
446
|
review = change_commands.add_parser("review")
|
|
491
447
|
review_commands = review.add_subparsers(dest="review_command", required=True)
|
|
492
|
-
review_start = review_commands
|
|
493
|
-
_add_common(review_start)
|
|
448
|
+
review_start = _leaf(review_commands, "start", command_change_review_start)
|
|
494
449
|
_add_actor(review_start)
|
|
495
450
|
review_start.add_argument("--change-id", required=True)
|
|
496
|
-
|
|
497
|
-
review_submit = review_commands.add_parser("submit")
|
|
498
|
-
_add_common(review_submit)
|
|
451
|
+
review_submit = _leaf(review_commands, "submit", command_change_review_submit)
|
|
499
452
|
review_submit.add_argument("--change-id", required=True)
|
|
500
453
|
review_submit.add_argument("--review", type=Path, required=True)
|
|
501
|
-
review_submit.set_defaults(handler=command_change_review_submit)
|
|
502
454
|
|
|
503
|
-
change_finish = change_commands
|
|
504
|
-
_add_common(change_finish)
|
|
455
|
+
change_finish = _leaf(change_commands, "finish", command_change_finish)
|
|
505
456
|
_add_actor(change_finish)
|
|
506
457
|
change_finish.add_argument("--change-id", required=True)
|
|
507
|
-
change_finish.set_defaults(handler=command_change_finish)
|
|
508
458
|
|
|
509
|
-
change_status = change_commands
|
|
510
|
-
_add_common(change_status)
|
|
459
|
+
change_status = _leaf(change_commands, "status", command_change_status)
|
|
511
460
|
change_status.add_argument("--change-id", required=True)
|
|
512
|
-
change_status.set_defaults(handler=command_change_status)
|
|
513
461
|
|
|
514
462
|
release = commands.add_parser("release", help="Validate release identity")
|
|
515
463
|
release_commands = release.add_subparsers(dest="release_command", required=True)
|
|
516
|
-
release_validate = release_commands
|
|
517
|
-
_add_common(release_validate)
|
|
464
|
+
release_validate = _leaf(release_commands, "validate", command_release_validate)
|
|
518
465
|
release_validate.add_argument("--tag")
|
|
519
|
-
release_validate.set_defaults(handler=command_release_validate)
|
|
520
466
|
|
|
521
467
|
publication = commands.add_parser("publication", help=argparse.SUPPRESS)
|
|
522
468
|
publication_commands = publication.add_subparsers(
|
|
523
469
|
dest="publication_command", required=True
|
|
524
470
|
)
|
|
525
|
-
publication_branch =
|
|
471
|
+
publication_branch = _leaf(
|
|
472
|
+
publication_commands, "validate-branch", command_publication, project=False
|
|
473
|
+
)
|
|
526
474
|
publication_branch.add_argument("--branch", required=True)
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
475
|
+
publication_commit = _leaf(
|
|
476
|
+
publication_commands, "validate-commit", command_publication, project=False
|
|
477
|
+
)
|
|
530
478
|
publication_commit.add_argument("--subject", required=True)
|
|
531
|
-
|
|
532
|
-
publication_commit.set_defaults(handler=command_publication_commit)
|
|
533
|
-
publication_range = publication_commands.add_parser("validate-range")
|
|
534
|
-
_add_common(publication_range)
|
|
479
|
+
publication_range = _leaf(publication_commands, "validate-range", command_publication)
|
|
535
480
|
publication_range.add_argument("--branch", required=True)
|
|
536
481
|
publication_range.add_argument("--range", dest="range_spec", required=True)
|
|
537
|
-
|
|
538
|
-
|
|
482
|
+
publication_pr = _leaf(
|
|
483
|
+
publication_commands, "validate-pr", command_publication, project=False
|
|
484
|
+
)
|
|
539
485
|
publication_pr.add_argument("--title", required=True)
|
|
540
486
|
publication_pr.add_argument("--branch", required=True)
|
|
541
487
|
publication_pr.add_argument("--state", required=True)
|
|
542
488
|
publication_pr.add_argument("--body-file", type=Path)
|
|
543
|
-
_add_common(publication_pr, project=False)
|
|
544
|
-
publication_pr.set_defaults(handler=command_publication_pr)
|
|
545
489
|
return parser
|
|
546
490
|
|
|
547
491
|
|
|
@@ -10,6 +10,11 @@ from .distribution import schemas_root
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
PROJECT_SCHEMA_VERSION = 5
|
|
13
|
+
PACK_CAPABILITIES = {
|
|
14
|
+
("desktop-media", 1): set("application-correctness authoritative-input-integrity cross-platform-portability dependency-audit dependency-security incident-recovery independent-security-review key-custody linux-release-security media-pipeline-integrity package-security recovery-integrity recovery-mechanism-integrity release-integrity runtime-delivery-integrity update-integrity workspace-security".split()),
|
|
15
|
+
("library-cli", 1): set("adoption-integrity compatibility correctness distribution-integrity installability portability runtime-safety".split()),
|
|
16
|
+
("operations", 1): set("auditability automation-correctness bounded-execution least-privilege policy-integrity recovery target-selection-integrity".split()),
|
|
17
|
+
}
|
|
13
18
|
|
|
14
19
|
|
|
15
20
|
def project_path(project_root: Path) -> Path:
|
|
@@ -17,7 +22,16 @@ def project_path(project_root: Path) -> Path:
|
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
def load_project(project_root: Path, process_root: Path) -> dict[str, Any]:
|
|
20
|
-
|
|
25
|
+
project = normalize_project(read_json(project_path(project_root)), process_root)
|
|
26
|
+
path = project_root / ".process" / "readiness.json"
|
|
27
|
+
if not path.is_file():
|
|
28
|
+
return project
|
|
29
|
+
if "readiness" in project:
|
|
30
|
+
raise ProcessError("readiness must use either project.json or readiness.json, not both")
|
|
31
|
+
project["readiness"] = read_json(path)
|
|
32
|
+
validate_document(project, "project", schema_root=schemas_root(process_root), source=str(path))
|
|
33
|
+
readiness_summary(project)
|
|
34
|
+
return project
|
|
21
35
|
|
|
22
36
|
|
|
23
37
|
def normalize_project(value: Any, process_root: Path) -> dict[str, Any]:
|
|
@@ -103,9 +117,52 @@ def normalize_project(value: Any, process_root: Path) -> dict[str, Any]:
|
|
|
103
117
|
schema_root=schemas_root(process_root),
|
|
104
118
|
source="normalized project configuration",
|
|
105
119
|
)
|
|
120
|
+
missing = sorted(set(required_profiles(normalized)) - set(normalized["profiles"]))
|
|
121
|
+
if missing:
|
|
122
|
+
raise ProcessError("project requires unknown profiles: " + ", ".join(missing))
|
|
123
|
+
readiness_summary(normalized)
|
|
106
124
|
return normalized
|
|
107
125
|
|
|
108
126
|
|
|
127
|
+
def readiness_summary(project: dict[str, Any]) -> dict[str, Any] | None:
|
|
128
|
+
readiness = project.get("readiness")
|
|
129
|
+
if readiness is None:
|
|
130
|
+
return None
|
|
131
|
+
entries = readiness["capabilities"]
|
|
132
|
+
capabilities = {item["id"]: item for item in entries}
|
|
133
|
+
if len(capabilities) != len(entries):
|
|
134
|
+
raise ProcessError("readiness capability ids must be unique")
|
|
135
|
+
packs = [(item["id"], item["version"]) for item in readiness["packs"]]
|
|
136
|
+
unsupported = [f"{name}@{version}" for name, version in packs if (name, version) not in PACK_CAPABILITIES]
|
|
137
|
+
if unsupported:
|
|
138
|
+
raise ProcessError("unsupported readiness pack versions: " + ", ".join(unsupported))
|
|
139
|
+
required = set().union(*(PACK_CAPABILITIES[pack] for pack in packs))
|
|
140
|
+
missing = sorted(required - capabilities.keys())
|
|
141
|
+
if missing:
|
|
142
|
+
raise ProcessError("readiness packs require missing capabilities: " + ", ".join(missing))
|
|
143
|
+
available = set(project["profiles"])
|
|
144
|
+
mandatory_profiles = set(required_profiles(project))
|
|
145
|
+
coverage: dict[str, Any] = {}
|
|
146
|
+
planned: list[str] = []
|
|
147
|
+
for capability_id, item in sorted(capabilities.items()):
|
|
148
|
+
if item["state"] == "planned":
|
|
149
|
+
coverage[capability_id] = {"state": "planned", "gap": item["gap"]}
|
|
150
|
+
planned.append(capability_id)
|
|
151
|
+
continue
|
|
152
|
+
evidence = set(item["evidenceProfiles"])
|
|
153
|
+
unknown = sorted(evidence - available)
|
|
154
|
+
if unknown:
|
|
155
|
+
raise ProcessError(f"readiness capability {capability_id} references unknown profiles: " + ", ".join(unknown))
|
|
156
|
+
if evidence.isdisjoint(mandatory_profiles):
|
|
157
|
+
raise ProcessError(f"readiness capability {capability_id} relies only on optional profiles: " + ", ".join(sorted(evidence)))
|
|
158
|
+
coverage[capability_id] = {"state": "enforced", "evidence": {
|
|
159
|
+
name: [check["id"] for check in project["profiles"][name]] for name in item["evidenceProfiles"]
|
|
160
|
+
}}
|
|
161
|
+
if readiness["stage"] == "production" and planned:
|
|
162
|
+
raise ProcessError("production readiness cannot contain planned capabilities: " + ", ".join(planned))
|
|
163
|
+
return {"target": readiness["target"], "stage": readiness["stage"], "packs": readiness["packs"], "capabilities": coverage, "plannedCapabilities": planned}
|
|
164
|
+
|
|
165
|
+
|
|
109
166
|
def required_profiles(project: dict[str, Any]) -> tuple[str, ...]:
|
|
110
167
|
return tuple(project["lifecycle"]["requiredProfiles"])
|
|
111
168
|
|