engineering-process 2.3.0__tar.gz → 2.4.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-2.3.0/engineering_process.egg-info → engineering_process-2.4.0}/PKG-INFO +40 -10
- {engineering_process-2.3.0 → engineering_process-2.4.0}/README.md +39 -9
- engineering_process-2.4.0/RELEASE_NOTES.md +22 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/__init__.py +1 -1
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/cli.py +5 -1
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/contracts.py +14 -3
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/lifecycle.py +108 -23
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/repository.py +49 -1
- engineering_process-2.4.0/engineering_process/review_contexts.py +291 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0/engineering_process.egg-info}/PKG-INFO +40 -10
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process.egg-info/SOURCES.txt +3 -1
- {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/change-implement/SKILL.md +18 -2
- {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/change-plan/SKILL.md +15 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/change-review/SKILL.md +49 -2
- {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/change-start/SKILL.md +7 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/change-verify/SKILL.md +8 -2
- {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/deliver-change/SKILL.md +3 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/pyproject.toml +1 -1
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/run.schema.json +28 -1
- {engineering_process-2.3.0 → engineering_process-2.4.0}/templates/adopt-process.py +10 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_architecture.py +1 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_automation.py +5 -15
- {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_cli.py +1 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_lifecycle.py +118 -1
- engineering_process-2.4.0/tests/test_pypi_cache.py +184 -0
- engineering_process-2.4.0/tests/test_repository.py +203 -0
- engineering_process-2.4.0/tests/test_review_contexts.py +507 -0
- engineering_process-2.3.0/RELEASE_NOTES.md +0 -23
- engineering_process-2.3.0/tests/test_pypi_cache_horizon.py +0 -81
- engineering_process-2.3.0/tests/test_repository.py +0 -63
- {engineering_process-2.3.0 → engineering_process-2.4.0}/LICENSE +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/MANIFEST.in +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/__main__.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/_supervisor_contract.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/_supervisor_posix.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/_supervisor_windows.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/_windows_job.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/adoption.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/artifact_standards.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/automation_name.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/commands.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/distribution.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/helper_launch.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/issue.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/pr_description.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/production_engineering.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/project.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/publication_compat.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/release.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/release_notes.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/requirements-dev.txt +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/requirements-runtime.txt +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/skills.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/source_publication.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/supervision.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process.egg-info/dependency_links.txt +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process.egg-info/entry_points.txt +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process.egg-info/requires.txt +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process.egg-info/top_level.txt +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/process-graph.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/change-complete/SKILL.md +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/process-improve/SKILL.md +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/production-engineering/SKILL.md +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/production-engineering/invariants.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/standards/automation-name.v1.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/standards/issue.v1.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/standards/pull-request.v1.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/standards/release-notes.v1.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/artifact-selection.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/artifact-standard.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/automation-name-data.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/change.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/issue-data.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/plan.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/pr-description-data.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/process-graph.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/process-lock.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/production-engineering.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/project-legacy.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/project.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/receipt.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/release-change.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/release-notes-data.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/release.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/renovate-preset.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/review.schema.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/setup.cfg +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/templates/AGENTS.process.md +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/templates/PULL_REQUEST_TEMPLATE.md +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/templates/adopt-process-windows-job.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/templates/renovate.json +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_adoption.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_artifact_standards.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_commands.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_contracts.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_distribution.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_production_engineering.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_publication_compat.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_publication_workflow.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_release.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_skills.py +0 -0
- {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_supervisor_posix.py +0 -0
{engineering_process-2.3.0/engineering_process.egg-info → engineering_process-2.4.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: engineering-process
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.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
|
|
@@ -64,19 +64,35 @@ under .process/receipts. Both paths are ignored by Git. Verification evidence is
|
|
|
64
64
|
to HEAD plus a fingerprint of tracked and non-ignored untracked files. Any relevant
|
|
65
65
|
mutation invalidates it.
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
Neither the reviewer actor nor reviewer context may have implemented the current
|
|
68
|
+
cycle. An agent reviewer context also cannot be recorded in another accepted change
|
|
69
|
+
in this repository or its registered worktrees. There is no attestation hierarchy,
|
|
69
70
|
recommendation chain, authority-transition protocol, remote-evidence federation, or
|
|
70
71
|
second handwritten validator.
|
|
71
72
|
|
|
72
|
-
The coordinator hands
|
|
73
|
-
|
|
74
|
-
reviewer continues corrections. The reviewer inspects the change and authors its
|
|
73
|
+
The coordinator hands each new change to a newly spawned reviewer, with the accepted
|
|
74
|
+
source artifacts and no inherited implementation or other-change review conversation.
|
|
75
|
+
The same reviewer continues only that change's corrections. The reviewer inspects the change and authors its
|
|
75
76
|
own report. The runner's existing task/session interaction and returned result make
|
|
76
77
|
that work inspectable. Sharing a provider or account is allowed; this is a
|
|
77
78
|
workflow for independent judgment, not authenticated identity or merge enforcement.
|
|
78
79
|
See [change-review](process_assets/skills/change-review/SKILL.md) for the handoff.
|
|
79
80
|
|
|
81
|
+
Start, submission and finish check schema-valid canonical run history with bounded
|
|
82
|
+
worktree/file reads. A short OS lock in common Git metadata serializes canonical
|
|
83
|
+
history writes and review checks; profiles run outside it and reload current state
|
|
84
|
+
before recording results. The lock stores no identity registry. The check rejects known context reuse,
|
|
85
|
+
but cannot establish native conversation freshness or inspect removed history,
|
|
86
|
+
other clones or other repositories. Keep native creation, non-inherited dispatch
|
|
87
|
+
and effective settings evidence in the existing handoff.
|
|
88
|
+
|
|
89
|
+
`processctl change review replace-reused` repairs only an initial pending assignment
|
|
90
|
+
proven to reuse another change's agent context, before a submitted review or normal
|
|
91
|
+
report file exists. It validates a fresh replacement and current verification,
|
|
92
|
+
preserves the prior assignment and conflict in canonical history, and leaves cycle
|
|
93
|
+
and evidence unchanged. Valid assignments and reviewed correction cycles cannot use
|
|
94
|
+
this operation. It does not provide an unrestricted reviewer reset.
|
|
95
|
+
|
|
80
96
|
Every delegated agent and reviewer follows the active user-selected model and
|
|
81
97
|
reasoning effort under [deliver-change's settings rule](process_assets/skills/deliver-change/SKILL.md#preserve-agent-execution-settings).
|
|
82
98
|
It covers fresh agents and resumed sessions, requires native runtime confirmation,
|
|
@@ -86,10 +102,14 @@ processctl a provider runtime or a model-quality evaluator.
|
|
|
86
102
|
Consumers using the packaged publication compatibility policy can enable
|
|
87
103
|
`lifecycle.publication.required: true` in `.process/project.json` after adopting a
|
|
88
104
|
version that supports it. Start then rejects an invalid branch before creating a
|
|
89
|
-
run.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
105
|
+
run. Before final profiles run and before review assignment, the lifecycle checks
|
|
106
|
+
the current branch, head subject, and nonempty pinned comparison-base-to-head range,
|
|
107
|
+
and requires all candidate changes to be committed. Commit before `change verify`;
|
|
108
|
+
a later commit changes the checkpoint even when its source content is identical.
|
|
109
|
+
Local lifecycle run/receipt files and Git-ignored files are excluded. Finish repeats
|
|
110
|
+
the same checks and writes publication metadata in a version 2 receipt. Version 1
|
|
111
|
+
receipts and consumers without the opt-in remain supported. Missing pinned bases and
|
|
112
|
+
mutations during the check fail without advancing the lifecycle.
|
|
93
113
|
|
|
94
114
|
PR readiness additionally requires the selected ready-state body/title and exact
|
|
95
115
|
head/range checks. Follow the [completion preflight](process_assets/skills/change-complete/SKILL.md)
|
|
@@ -585,6 +605,16 @@ package rule keeps the adoption pull request in draft and runs exactly:
|
|
|
585
605
|
|
|
586
606
|
python .process/adopt-process.py --project-root . --requirements-lock requirements/process.txt
|
|
587
607
|
|
|
608
|
+
The bootstrap refreshes `engineering-process` index metadata using pip's
|
|
609
|
+
`--refresh-package` option, with `--no-cache-dir` for older pip. Exact pins, hashes,
|
|
610
|
+
isolated installation and bounded failures still apply. Consumer CI that installs
|
|
611
|
+
the lock directly must also request fresh metadata: use
|
|
612
|
+
`--refresh-package engineering-process` with pip 26.2 or newer, or the portable
|
|
613
|
+
`--no-cache-dir` option. Keep this setting on the process installation command;
|
|
614
|
+
other dependency caches do not need to be disabled. For non-isolated pip-compile,
|
|
615
|
+
the equivalent `PIP_REFRESH_PACKAGE=engineering-process` environment setting keeps
|
|
616
|
+
fresh release lookup compatible with older pip, which revalidated by default.
|
|
617
|
+
|
|
588
618
|
postUpgradeTasks.fileFilters includes the managed paths, so Renovate commits the new
|
|
589
619
|
hash lock and the fully materialized process in the same pull request. A self-hosted
|
|
590
620
|
Renovate administrator must allow only this anchored command and must keep shell
|
|
@@ -40,19 +40,35 @@ under .process/receipts. Both paths are ignored by Git. Verification evidence is
|
|
|
40
40
|
to HEAD plus a fingerprint of tracked and non-ignored untracked files. Any relevant
|
|
41
41
|
mutation invalidates it.
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
Neither the reviewer actor nor reviewer context may have implemented the current
|
|
44
|
+
cycle. An agent reviewer context also cannot be recorded in another accepted change
|
|
45
|
+
in this repository or its registered worktrees. There is no attestation hierarchy,
|
|
45
46
|
recommendation chain, authority-transition protocol, remote-evidence federation, or
|
|
46
47
|
second handwritten validator.
|
|
47
48
|
|
|
48
|
-
The coordinator hands
|
|
49
|
-
|
|
50
|
-
reviewer continues corrections. The reviewer inspects the change and authors its
|
|
49
|
+
The coordinator hands each new change to a newly spawned reviewer, with the accepted
|
|
50
|
+
source artifacts and no inherited implementation or other-change review conversation.
|
|
51
|
+
The same reviewer continues only that change's corrections. The reviewer inspects the change and authors its
|
|
51
52
|
own report. The runner's existing task/session interaction and returned result make
|
|
52
53
|
that work inspectable. Sharing a provider or account is allowed; this is a
|
|
53
54
|
workflow for independent judgment, not authenticated identity or merge enforcement.
|
|
54
55
|
See [change-review](process_assets/skills/change-review/SKILL.md) for the handoff.
|
|
55
56
|
|
|
57
|
+
Start, submission and finish check schema-valid canonical run history with bounded
|
|
58
|
+
worktree/file reads. A short OS lock in common Git metadata serializes canonical
|
|
59
|
+
history writes and review checks; profiles run outside it and reload current state
|
|
60
|
+
before recording results. The lock stores no identity registry. The check rejects known context reuse,
|
|
61
|
+
but cannot establish native conversation freshness or inspect removed history,
|
|
62
|
+
other clones or other repositories. Keep native creation, non-inherited dispatch
|
|
63
|
+
and effective settings evidence in the existing handoff.
|
|
64
|
+
|
|
65
|
+
`processctl change review replace-reused` repairs only an initial pending assignment
|
|
66
|
+
proven to reuse another change's agent context, before a submitted review or normal
|
|
67
|
+
report file exists. It validates a fresh replacement and current verification,
|
|
68
|
+
preserves the prior assignment and conflict in canonical history, and leaves cycle
|
|
69
|
+
and evidence unchanged. Valid assignments and reviewed correction cycles cannot use
|
|
70
|
+
this operation. It does not provide an unrestricted reviewer reset.
|
|
71
|
+
|
|
56
72
|
Every delegated agent and reviewer follows the active user-selected model and
|
|
57
73
|
reasoning effort under [deliver-change's settings rule](process_assets/skills/deliver-change/SKILL.md#preserve-agent-execution-settings).
|
|
58
74
|
It covers fresh agents and resumed sessions, requires native runtime confirmation,
|
|
@@ -62,10 +78,14 @@ processctl a provider runtime or a model-quality evaluator.
|
|
|
62
78
|
Consumers using the packaged publication compatibility policy can enable
|
|
63
79
|
`lifecycle.publication.required: true` in `.process/project.json` after adopting a
|
|
64
80
|
version that supports it. Start then rejects an invalid branch before creating a
|
|
65
|
-
run.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
run. Before final profiles run and before review assignment, the lifecycle checks
|
|
82
|
+
the current branch, head subject, and nonempty pinned comparison-base-to-head range,
|
|
83
|
+
and requires all candidate changes to be committed. Commit before `change verify`;
|
|
84
|
+
a later commit changes the checkpoint even when its source content is identical.
|
|
85
|
+
Local lifecycle run/receipt files and Git-ignored files are excluded. Finish repeats
|
|
86
|
+
the same checks and writes publication metadata in a version 2 receipt. Version 1
|
|
87
|
+
receipts and consumers without the opt-in remain supported. Missing pinned bases and
|
|
88
|
+
mutations during the check fail without advancing the lifecycle.
|
|
69
89
|
|
|
70
90
|
PR readiness additionally requires the selected ready-state body/title and exact
|
|
71
91
|
head/range checks. Follow the [completion preflight](process_assets/skills/change-complete/SKILL.md)
|
|
@@ -561,6 +581,16 @@ package rule keeps the adoption pull request in draft and runs exactly:
|
|
|
561
581
|
|
|
562
582
|
python .process/adopt-process.py --project-root . --requirements-lock requirements/process.txt
|
|
563
583
|
|
|
584
|
+
The bootstrap refreshes `engineering-process` index metadata using pip's
|
|
585
|
+
`--refresh-package` option, with `--no-cache-dir` for older pip. Exact pins, hashes,
|
|
586
|
+
isolated installation and bounded failures still apply. Consumer CI that installs
|
|
587
|
+
the lock directly must also request fresh metadata: use
|
|
588
|
+
`--refresh-package engineering-process` with pip 26.2 or newer, or the portable
|
|
589
|
+
`--no-cache-dir` option. Keep this setting on the process installation command;
|
|
590
|
+
other dependency caches do not need to be disabled. For non-isolated pip-compile,
|
|
591
|
+
the equivalent `PIP_REFRESH_PACKAGE=engineering-process` environment setting keeps
|
|
592
|
+
fresh release lookup compatible with older pip, which revalidated by default.
|
|
593
|
+
|
|
564
594
|
postUpgradeTasks.fileFilters includes the managed paths, so Renovate commits the new
|
|
565
595
|
hash lock and the fully materialized process in the same pull request. A self-hosted
|
|
566
596
|
Renovate administrator must allow only this anchored command and must keep shell
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Engineering Process v2.4.0
|
|
2
|
+
|
|
3
|
+
Changes since v2.3.1.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Lifecycle guidance helps consumers find authoritative current project knowledge\, maintain only information affected by accepted work or necessary gaps\, and review explanations and useful connections in context\. Existing consumer organization remains valid\; no documentation taxonomy or migration is required\. ([#192](https://github.com/phuongnse/engineering-process/issues/192))
|
|
8
|
+
- Planning\, implementation and review connect tests and other evidence to accepted behavior and concrete risks\, including regression sensitivity and fitting evidence boundaries\. Consumer\-required profiles and testing policies remain authoritative\; no universal test\-first ritual or new test per change is imposed\. ([#193](https://github.com/phuongnse/engineering-process/issues/193))
|
|
9
|
+
|
|
10
|
+
## Fixes
|
|
11
|
+
|
|
12
|
+
- Publication\-required changes must commit the complete candidate and pass the existing branch\, subject and nonempty pinned\-range checks before final verification and review\. This avoids a late commit forcing a repeated verification\/review cycle\; exact\-snapshot and finish checks remain enforced\. ([#191](https://github.com/phuongnse/engineering-process/issues/191))
|
|
13
|
+
|
|
14
|
+
## Upgrade and compatibility
|
|
15
|
+
|
|
16
|
+
Merge the complete hash-locked package/adoption PR, update the local and CI environments to the selected version, and start a fresh agent session.
|
|
17
|
+
|
|
18
|
+
Consumer CI, naming conventions and branch-protection settings remain consumer-owned; adoption does not configure them automatically.
|
|
19
|
+
|
|
20
|
+
See [versioning and compatibility](https://github.com/phuongnse/engineering-process/blob/v2.4.0/VERSIONING.md) and [adoption guidance](https://github.com/phuongnse/engineering-process/blob/v2.4.0/SELF_HOSTING.md).
|
|
21
|
+
|
|
22
|
+
[Full change comparison](https://github.com/phuongnse/engineering-process/compare/v2.3.1...v2.4.0)
|
|
@@ -330,9 +330,10 @@ def command_change_review_start(args: argparse.Namespace) -> Result:
|
|
|
330
330
|
actor_id=args.actor,
|
|
331
331
|
context_id=args.context,
|
|
332
332
|
kind=args.actor_kind,
|
|
333
|
+
replace_reused=args.review_command == "replace-reused",
|
|
333
334
|
)
|
|
334
335
|
return _state_result(
|
|
335
|
-
"change review
|
|
336
|
+
f"change review {args.review_command}",
|
|
336
337
|
state,
|
|
337
338
|
assignment=state["reviewAssignment"],
|
|
338
339
|
processSignals=process_improvement_signals(state),
|
|
@@ -542,6 +543,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
542
543
|
review_start = _leaf(review_commands, "start", command_change_review_start)
|
|
543
544
|
_add_actor(review_start)
|
|
544
545
|
review_start.add_argument("--change-id", required=True)
|
|
546
|
+
review_replace = _leaf(review_commands, "replace-reused", command_change_review_start)
|
|
547
|
+
_add_actor(review_replace)
|
|
548
|
+
review_replace.add_argument("--change-id", required=True)
|
|
545
549
|
review_submit = _leaf(review_commands, "submit", command_change_review_submit)
|
|
546
550
|
review_submit.add_argument("--change-id", required=True)
|
|
547
551
|
review_submit.add_argument("--review", type=Path, required=True)
|
|
@@ -51,9 +51,20 @@ def read_json(path: Path, *, maximum_bytes: int = MAX_JSON_BYTES) -> Any:
|
|
|
51
51
|
if size > maximum_bytes:
|
|
52
52
|
raise ProcessError(f"{path} exceeds {maximum_bytes} bytes")
|
|
53
53
|
try:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
with path.open("rb") as stream:
|
|
55
|
+
content = stream.read(maximum_bytes + 1)
|
|
56
|
+
except OSError as error:
|
|
57
|
+
raise ProcessError(f"cannot read {path}: {error}") from error
|
|
58
|
+
if len(content) > maximum_bytes:
|
|
59
|
+
raise ProcessError(f"{path} exceeds {maximum_bytes} bytes")
|
|
60
|
+
return parse_json_bytes(content, source=str(path))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_json_bytes(content: bytes, *, source: str) -> Any:
|
|
64
|
+
try:
|
|
65
|
+
return json.loads(content.decode("utf-8"))
|
|
66
|
+
except (UnicodeError, json.JSONDecodeError) as error:
|
|
67
|
+
raise ProcessError(f"{source} is not valid UTF-8 JSON: {error}") from error
|
|
57
68
|
|
|
58
69
|
|
|
59
70
|
def canonical_bytes(value: Any) -> bytes:
|
|
@@ -36,7 +36,14 @@ from .production_engineering import (
|
|
|
36
36
|
validate_review_assessments,
|
|
37
37
|
)
|
|
38
38
|
from .source_publication import branch_issues, current_branch, validate_current_source
|
|
39
|
-
from .repository import
|
|
39
|
+
from .repository import (
|
|
40
|
+
repository_snapshot, require_committed_candidate, resolve_commit, same_checkpoint,
|
|
41
|
+
)
|
|
42
|
+
from .review_contexts import (
|
|
43
|
+
recorded_context_conflict,
|
|
44
|
+
require_unreused_context,
|
|
45
|
+
history_transaction,
|
|
46
|
+
)
|
|
40
47
|
|
|
41
48
|
|
|
42
49
|
NEXT_COMMAND = {
|
|
@@ -134,6 +141,7 @@ def _require_phase(state: dict[str, Any], *phases: str) -> None:
|
|
|
134
141
|
)
|
|
135
142
|
|
|
136
143
|
|
|
144
|
+
@history_transaction
|
|
137
145
|
def start_change(
|
|
138
146
|
project_root: Path,
|
|
139
147
|
process_root: Path,
|
|
@@ -215,6 +223,7 @@ def start_change(
|
|
|
215
223
|
return state
|
|
216
224
|
|
|
217
225
|
|
|
226
|
+
@history_transaction
|
|
218
227
|
def register_plan(
|
|
219
228
|
project_root: Path,
|
|
220
229
|
process_root: Path,
|
|
@@ -251,6 +260,7 @@ def register_plan(
|
|
|
251
260
|
return state
|
|
252
261
|
|
|
253
262
|
|
|
263
|
+
@history_transaction
|
|
254
264
|
def begin_implementation(
|
|
255
265
|
project_root: Path,
|
|
256
266
|
process_root: Path,
|
|
@@ -309,6 +319,33 @@ def begin_implementation(
|
|
|
309
319
|
return state
|
|
310
320
|
|
|
311
321
|
|
|
322
|
+
def _publication_preflight(
|
|
323
|
+
project_root: Path,
|
|
324
|
+
project: dict[str, Any],
|
|
325
|
+
state: dict[str, Any],
|
|
326
|
+
checkpoint: dict[str, Any],
|
|
327
|
+
) -> dict[str, Any] | None:
|
|
328
|
+
if not publication_required(project):
|
|
329
|
+
return None
|
|
330
|
+
if not (comparison_base := state.get("comparisonBaseCommit")):
|
|
331
|
+
raise ProcessError("publication requires a pinned comparison base")
|
|
332
|
+
publication = validate_current_source(project_root, comparison_base)
|
|
333
|
+
if publication["range"] != f"{comparison_base}..{checkpoint['head']}":
|
|
334
|
+
raise ProcessError("publication validation does not match the candidate HEAD")
|
|
335
|
+
if publication["issues"]:
|
|
336
|
+
raise ProcessError(
|
|
337
|
+
"publication compatibility checks failed: "
|
|
338
|
+
+ "; ".join(publication["issues"])
|
|
339
|
+
+ "; commit the candidate on a valid publication branch before change verify"
|
|
340
|
+
)
|
|
341
|
+
require_committed_candidate(project_root, checkpoint["head"])
|
|
342
|
+
after = repository_snapshot(project_root)
|
|
343
|
+
if (not same_checkpoint(checkpoint, after)
|
|
344
|
+
or current_branch(project_root) != publication["branch"]):
|
|
345
|
+
raise ProcessError("repository changed while publication validation was running")
|
|
346
|
+
return publication
|
|
347
|
+
|
|
348
|
+
|
|
312
349
|
def verify_change(
|
|
313
350
|
project_root: Path,
|
|
314
351
|
process_root: Path,
|
|
@@ -322,13 +359,35 @@ def verify_change(
|
|
|
322
359
|
raise ProcessError(f"profile {profile} is not required by change {change_id}")
|
|
323
360
|
|
|
324
361
|
before = repository_snapshot(project_root)
|
|
362
|
+
_publication_preflight(project_root, project, state, before)
|
|
363
|
+
report = run_profile(project_root, project, profile)
|
|
364
|
+
return _record_verification(
|
|
365
|
+
project_root, process_root, change_id, state["cycle"], before, report
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@history_transaction
|
|
370
|
+
def _record_verification(
|
|
371
|
+
project_root: Path,
|
|
372
|
+
process_root: Path,
|
|
373
|
+
change_id: str,
|
|
374
|
+
cycle: int,
|
|
375
|
+
before: dict[str, Any],
|
|
376
|
+
report: dict[str, Any],
|
|
377
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
378
|
+
# Profiles run outside the lock. Reload so their publication cannot erase
|
|
379
|
+
# participants or evidence recorded by another command while they ran.
|
|
380
|
+
state = _load_state(project_root, process_root, change_id)
|
|
381
|
+
_require_phase(state, "implementing")
|
|
382
|
+
if state["cycle"] != cycle:
|
|
383
|
+
raise ProcessError("implementation cycle changed while verification was running")
|
|
325
384
|
state["verification"] = {
|
|
326
385
|
name: report
|
|
327
386
|
for name, report in state["verification"].items()
|
|
328
387
|
if same_checkpoint(report["checkpoint"], before)
|
|
329
388
|
}
|
|
330
|
-
report = run_profile(project_root, project, profile)
|
|
331
389
|
after = repository_snapshot(project_root)
|
|
390
|
+
profile = report["profile"]
|
|
332
391
|
report["checkpoint"] = after
|
|
333
392
|
report["recordedAt"] = _now()
|
|
334
393
|
if not same_checkpoint(before, after):
|
|
@@ -356,6 +415,7 @@ def verify_change(
|
|
|
356
415
|
return state, report
|
|
357
416
|
|
|
358
417
|
|
|
418
|
+
@history_transaction
|
|
359
419
|
def start_review(
|
|
360
420
|
project_root: Path,
|
|
361
421
|
process_root: Path,
|
|
@@ -364,9 +424,29 @@ def start_review(
|
|
|
364
424
|
actor_id: str,
|
|
365
425
|
context_id: str,
|
|
366
426
|
kind: str,
|
|
427
|
+
replace_reused: bool = False,
|
|
367
428
|
) -> dict[str, Any]:
|
|
368
429
|
state = _load_state(project_root, process_root, change_id)
|
|
369
|
-
|
|
430
|
+
previous_assignment = conflict = None
|
|
431
|
+
if replace_reused:
|
|
432
|
+
_require_phase(state, "review-pending")
|
|
433
|
+
if (state["reviewAssignment"] is None or state["review"] is not None
|
|
434
|
+
or state["reviewHistory"]
|
|
435
|
+
or any(event["event"] == "review-submitted" for event in state["history"])):
|
|
436
|
+
raise ProcessError("only an unsubmitted initial review assignment can be replaced")
|
|
437
|
+
previous_assignment = deepcopy(state["reviewAssignment"])
|
|
438
|
+
if previous_assignment["reviewer"]["kind"] != "agent":
|
|
439
|
+
raise ProcessError("replace-reused requires a recorded reused agent context")
|
|
440
|
+
report_path = _run_path(project_root, change_id).with_name(f"review-{state['cycle']}.json")
|
|
441
|
+
if report_path.exists() or report_path.is_symlink():
|
|
442
|
+
raise ProcessError("a review report already exists; assignment replacement would discard it")
|
|
443
|
+
conflict = recorded_context_conflict(
|
|
444
|
+
project_root, process_root, state, previous_assignment["reviewer"]["contextId"]
|
|
445
|
+
)
|
|
446
|
+
if conflict is None:
|
|
447
|
+
raise ProcessError("the pending reviewer context has no recorded cross-change reuse")
|
|
448
|
+
else:
|
|
449
|
+
_require_phase(state, "verified")
|
|
370
450
|
reviewer = _actor(actor_id, context_id, kind)
|
|
371
451
|
if state["reviewHistory"]:
|
|
372
452
|
original_reviewer = state["reviewHistory"][0]["document"]["reviewer"]
|
|
@@ -383,6 +463,7 @@ def start_review(
|
|
|
383
463
|
raise ProcessError("reviewer actor must be independent from implementation")
|
|
384
464
|
if any(item["contextId"] == reviewer["contextId"] for item in implementers):
|
|
385
465
|
raise ProcessError("reviewer context must be independent from implementation")
|
|
466
|
+
require_unreused_context(project_root, process_root, state, reviewer)
|
|
386
467
|
|
|
387
468
|
checkpoint = repository_snapshot(project_root)
|
|
388
469
|
required = state["contract"]["document"]["requiredProfiles"]
|
|
@@ -393,6 +474,13 @@ def start_review(
|
|
|
393
474
|
for name in required
|
|
394
475
|
):
|
|
395
476
|
raise ProcessError("verification evidence is stale or incomplete")
|
|
477
|
+
if previous_assignment is not None and not same_checkpoint(
|
|
478
|
+
previous_assignment["checkpoint"], checkpoint
|
|
479
|
+
):
|
|
480
|
+
raise ProcessError("repository changed after the reused review assignment")
|
|
481
|
+
_publication_preflight(
|
|
482
|
+
project_root, load_project(project_root, process_root), state, checkpoint
|
|
483
|
+
)
|
|
396
484
|
state["reviewAssignment"] = {
|
|
397
485
|
"reviewer": reviewer,
|
|
398
486
|
"checkpoint": checkpoint,
|
|
@@ -403,11 +491,21 @@ def start_review(
|
|
|
403
491
|
),
|
|
404
492
|
}
|
|
405
493
|
state["phase"] = "review-pending"
|
|
406
|
-
|
|
494
|
+
if previous_assignment is None:
|
|
495
|
+
_event(state, "review-started", reviewer)
|
|
496
|
+
else:
|
|
497
|
+
_event(
|
|
498
|
+
state, "review-assignment-replaced", reviewer,
|
|
499
|
+
reason="cross-change-context-reuse",
|
|
500
|
+
previousAssignment=previous_assignment,
|
|
501
|
+
replacementAssignment=deepcopy(state["reviewAssignment"]),
|
|
502
|
+
conflict=conflict,
|
|
503
|
+
)
|
|
407
504
|
_save_state(project_root, process_root, state)
|
|
408
505
|
return state
|
|
409
506
|
|
|
410
507
|
|
|
508
|
+
@history_transaction
|
|
411
509
|
def submit_review(
|
|
412
510
|
project_root: Path,
|
|
413
511
|
process_root: Path,
|
|
@@ -428,6 +526,7 @@ def submit_review(
|
|
|
428
526
|
raise ProcessError("review changeId does not match lifecycle state")
|
|
429
527
|
if review["reviewer"] != assignment["reviewer"]:
|
|
430
528
|
raise ProcessError("reviewer does not match the assigned independent identity")
|
|
529
|
+
require_unreused_context(project_root, process_root, state, assignment["reviewer"])
|
|
431
530
|
if not same_checkpoint(review["checkpoint"], assignment["checkpoint"]):
|
|
432
531
|
raise ProcessError("review checkpoint does not match the assignment")
|
|
433
532
|
current = repository_snapshot(project_root)
|
|
@@ -535,6 +634,7 @@ def submit_review(
|
|
|
535
634
|
return state
|
|
536
635
|
|
|
537
636
|
|
|
637
|
+
@history_transaction
|
|
538
638
|
def finish_change(
|
|
539
639
|
project_root: Path,
|
|
540
640
|
process_root: Path,
|
|
@@ -546,28 +646,13 @@ def finish_change(
|
|
|
546
646
|
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
547
647
|
state = _load_state(project_root, process_root, change_id)
|
|
548
648
|
_require_phase(state, "approved")
|
|
649
|
+
require_unreused_context(
|
|
650
|
+
project_root, process_root, state, state["reviewAssignment"]["reviewer"]
|
|
651
|
+
)
|
|
549
652
|
actor = _actor(actor_id, context_id, kind)
|
|
550
653
|
project = load_project(project_root, process_root)
|
|
551
654
|
checkpoint = repository_snapshot(project_root)
|
|
552
|
-
publication =
|
|
553
|
-
if publication_required(project):
|
|
554
|
-
if not (comparison_base := state.get("comparisonBaseCommit")):
|
|
555
|
-
raise ProcessError("publication completion requires a pinned comparison base")
|
|
556
|
-
publication = validate_current_source(
|
|
557
|
-
project_root, comparison_base
|
|
558
|
-
)
|
|
559
|
-
if publication["range"] != f"{comparison_base}..{checkpoint['head']}":
|
|
560
|
-
raise ProcessError("publication validation does not match the reviewed HEAD")
|
|
561
|
-
after_publication = repository_snapshot(project_root)
|
|
562
|
-
if (not same_checkpoint(checkpoint, after_publication)
|
|
563
|
-
or current_branch(project_root) != publication["branch"]):
|
|
564
|
-
raise ProcessError("repository changed while publication validation was running")
|
|
565
|
-
if publication["issues"]:
|
|
566
|
-
raise ProcessError(
|
|
567
|
-
"publication compatibility checks failed: "
|
|
568
|
-
+ "; ".join(publication["issues"])
|
|
569
|
-
)
|
|
570
|
-
checkpoint = after_publication
|
|
655
|
+
publication = _publication_preflight(project_root, project, state, checkpoint)
|
|
571
656
|
if not same_checkpoint(checkpoint, state["reviewAssignment"]["checkpoint"]):
|
|
572
657
|
raise ProcessError("repository changed after approval")
|
|
573
658
|
|
|
@@ -7,6 +7,7 @@ import os
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
import stat
|
|
9
9
|
import subprocess
|
|
10
|
+
import tempfile
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
12
13
|
from .contracts import ProcessError
|
|
@@ -18,11 +19,15 @@ MAX_TOTAL_BYTES = 1_000_000_000
|
|
|
18
19
|
STATE_PREFIXES = (b".process/runs/", b".process/receipts/")
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
def _git(
|
|
22
|
+
def _git(
|
|
23
|
+
root: Path, arguments: list[str], *, check: bool = True,
|
|
24
|
+
env: dict[str, str] | None = None,
|
|
25
|
+
) -> bytes:
|
|
22
26
|
try:
|
|
23
27
|
result = subprocess.run(
|
|
24
28
|
["git", "-c", "core.quotepath=false", *arguments],
|
|
25
29
|
cwd=root,
|
|
30
|
+
env=env,
|
|
26
31
|
stdin=subprocess.DEVNULL,
|
|
27
32
|
stdout=subprocess.PIPE,
|
|
28
33
|
stderr=subprocess.PIPE,
|
|
@@ -56,6 +61,49 @@ def resolve_commit(root: Path, reference: str) -> str:
|
|
|
56
61
|
).decode("ascii").strip()
|
|
57
62
|
|
|
58
63
|
|
|
64
|
+
def require_committed_candidate(root: Path, head: str = "HEAD") -> None:
|
|
65
|
+
root = root.resolve()
|
|
66
|
+
head = resolve_commit(root, head)
|
|
67
|
+
environment = dict(os.environ, GIT_OPTIONAL_LOCKS="0")
|
|
68
|
+
paths = _git(root, [
|
|
69
|
+
"diff", "--cached", "--name-only", "-z", "--no-ext-diff",
|
|
70
|
+
"--ignore-submodules=none", head,
|
|
71
|
+
], env=environment).split(b"\0")
|
|
72
|
+
# Git's skip-worktree contract permits absence, not changed materialized content.
|
|
73
|
+
omitted = {
|
|
74
|
+
record[2:]
|
|
75
|
+
for record in _git(root, ["ls-files", "-t", "-z"], env=environment).split(b"\0")
|
|
76
|
+
if record.startswith(b"S ") and not os.path.lexists(root / os.fsdecode(record[2:]))
|
|
77
|
+
}
|
|
78
|
+
# A fresh HEAD index cannot hide worktree changes behind visibility flags or
|
|
79
|
+
# cached stat data. Keep the consumer's real index and its flags untouched.
|
|
80
|
+
# Git metadata stays outside snapshots even when TMPDIR is inside the checkout.
|
|
81
|
+
git_directory = os.fsdecode(_git(root, ["rev-parse", "--absolute-git-dir"]).rstrip(b"\r\n"))
|
|
82
|
+
with tempfile.TemporaryDirectory(prefix="process-candidate-", dir=git_directory) as directory:
|
|
83
|
+
environment["GIT_INDEX_FILE"] = str(Path(directory) / "index")
|
|
84
|
+
inspection = [
|
|
85
|
+
"-c", "core.fsmonitor=false", "-c", "core.ignoreStat=false",
|
|
86
|
+
"-c", "core.sparseCheckout=false", "-c", "core.splitIndex=false",
|
|
87
|
+
]
|
|
88
|
+
_git(root, [*inspection, "read-tree", head], env=environment)
|
|
89
|
+
records = _git(root, [
|
|
90
|
+
*inspection, "status", "--porcelain=v1", "-z", "--untracked-files=all",
|
|
91
|
+
"--no-renames", "--ignore-submodules=none",
|
|
92
|
+
], env=environment).split(b"\0")
|
|
93
|
+
for record in records:
|
|
94
|
+
if not record:
|
|
95
|
+
continue
|
|
96
|
+
path = record[3:]
|
|
97
|
+
if record.startswith(b" D ") and path in omitted:
|
|
98
|
+
continue
|
|
99
|
+
paths.append(path)
|
|
100
|
+
if any(path and not path.startswith(STATE_PREFIXES) for path in paths):
|
|
101
|
+
raise ProcessError(
|
|
102
|
+
"publication requires committed candidate changes; commit or remove "
|
|
103
|
+
"uncommitted changes before change verify"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
59
107
|
def repository_snapshot(root: Path) -> dict[str, Any]:
|
|
60
108
|
root = root.resolve()
|
|
61
109
|
if not (root / ".git").exists():
|