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.
Files changed (102) hide show
  1. {engineering_process-2.3.0/engineering_process.egg-info → engineering_process-2.4.0}/PKG-INFO +40 -10
  2. {engineering_process-2.3.0 → engineering_process-2.4.0}/README.md +39 -9
  3. engineering_process-2.4.0/RELEASE_NOTES.md +22 -0
  4. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/__init__.py +1 -1
  5. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/cli.py +5 -1
  6. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/contracts.py +14 -3
  7. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/lifecycle.py +108 -23
  8. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/repository.py +49 -1
  9. engineering_process-2.4.0/engineering_process/review_contexts.py +291 -0
  10. {engineering_process-2.3.0 → engineering_process-2.4.0/engineering_process.egg-info}/PKG-INFO +40 -10
  11. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process.egg-info/SOURCES.txt +3 -1
  12. {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/change-implement/SKILL.md +18 -2
  13. {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/change-plan/SKILL.md +15 -0
  14. {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/change-review/SKILL.md +49 -2
  15. {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/change-start/SKILL.md +7 -0
  16. {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/change-verify/SKILL.md +8 -2
  17. {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/deliver-change/SKILL.md +3 -0
  18. {engineering_process-2.3.0 → engineering_process-2.4.0}/pyproject.toml +1 -1
  19. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/run.schema.json +28 -1
  20. {engineering_process-2.3.0 → engineering_process-2.4.0}/templates/adopt-process.py +10 -0
  21. {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_architecture.py +1 -0
  22. {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_automation.py +5 -15
  23. {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_cli.py +1 -0
  24. {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_lifecycle.py +118 -1
  25. engineering_process-2.4.0/tests/test_pypi_cache.py +184 -0
  26. engineering_process-2.4.0/tests/test_repository.py +203 -0
  27. engineering_process-2.4.0/tests/test_review_contexts.py +507 -0
  28. engineering_process-2.3.0/RELEASE_NOTES.md +0 -23
  29. engineering_process-2.3.0/tests/test_pypi_cache_horizon.py +0 -81
  30. engineering_process-2.3.0/tests/test_repository.py +0 -63
  31. {engineering_process-2.3.0 → engineering_process-2.4.0}/LICENSE +0 -0
  32. {engineering_process-2.3.0 → engineering_process-2.4.0}/MANIFEST.in +0 -0
  33. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/__main__.py +0 -0
  34. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/_supervisor_contract.py +0 -0
  35. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/_supervisor_posix.py +0 -0
  36. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/_supervisor_windows.py +0 -0
  37. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/_windows_job.py +0 -0
  38. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/adoption.py +0 -0
  39. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/artifact_standards.py +0 -0
  40. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/automation_name.py +0 -0
  41. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/commands.py +0 -0
  42. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/distribution.py +0 -0
  43. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/helper_launch.py +0 -0
  44. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/issue.py +0 -0
  45. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/pr_description.py +0 -0
  46. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/production_engineering.py +0 -0
  47. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/project.py +0 -0
  48. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/publication_compat.py +0 -0
  49. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/release.py +0 -0
  50. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/release_notes.py +0 -0
  51. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/requirements-dev.txt +0 -0
  52. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/requirements-runtime.txt +0 -0
  53. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/skills.py +0 -0
  54. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/source_publication.py +0 -0
  55. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process/supervision.py +0 -0
  56. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process.egg-info/dependency_links.txt +0 -0
  57. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process.egg-info/entry_points.txt +0 -0
  58. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process.egg-info/requires.txt +0 -0
  59. {engineering_process-2.3.0 → engineering_process-2.4.0}/engineering_process.egg-info/top_level.txt +0 -0
  60. {engineering_process-2.3.0 → engineering_process-2.4.0}/process-graph.json +0 -0
  61. {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/change-complete/SKILL.md +0 -0
  62. {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/process-improve/SKILL.md +0 -0
  63. {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/production-engineering/SKILL.md +0 -0
  64. {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/skills/production-engineering/invariants.json +0 -0
  65. {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/standards/automation-name.v1.json +0 -0
  66. {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/standards/issue.v1.json +0 -0
  67. {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/standards/pull-request.v1.json +0 -0
  68. {engineering_process-2.3.0 → engineering_process-2.4.0}/process_assets/standards/release-notes.v1.json +0 -0
  69. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/artifact-selection.schema.json +0 -0
  70. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/artifact-standard.schema.json +0 -0
  71. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/automation-name-data.schema.json +0 -0
  72. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/change.schema.json +0 -0
  73. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/issue-data.schema.json +0 -0
  74. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/plan.schema.json +0 -0
  75. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/pr-description-data.schema.json +0 -0
  76. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/process-graph.schema.json +0 -0
  77. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/process-lock.schema.json +0 -0
  78. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/production-engineering.schema.json +0 -0
  79. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/project-legacy.schema.json +0 -0
  80. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/project.schema.json +0 -0
  81. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/receipt.schema.json +0 -0
  82. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/release-change.schema.json +0 -0
  83. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/release-notes-data.schema.json +0 -0
  84. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/release.schema.json +0 -0
  85. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/renovate-preset.schema.json +0 -0
  86. {engineering_process-2.3.0 → engineering_process-2.4.0}/schemas/review.schema.json +0 -0
  87. {engineering_process-2.3.0 → engineering_process-2.4.0}/setup.cfg +0 -0
  88. {engineering_process-2.3.0 → engineering_process-2.4.0}/templates/AGENTS.process.md +0 -0
  89. {engineering_process-2.3.0 → engineering_process-2.4.0}/templates/PULL_REQUEST_TEMPLATE.md +0 -0
  90. {engineering_process-2.3.0 → engineering_process-2.4.0}/templates/adopt-process-windows-job.py +0 -0
  91. {engineering_process-2.3.0 → engineering_process-2.4.0}/templates/renovate.json +0 -0
  92. {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_adoption.py +0 -0
  93. {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_artifact_standards.py +0 -0
  94. {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_commands.py +0 -0
  95. {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_contracts.py +0 -0
  96. {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_distribution.py +0 -0
  97. {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_production_engineering.py +0 -0
  98. {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_publication_compat.py +0 -0
  99. {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_publication_workflow.py +0 -0
  100. {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_release.py +0 -0
  101. {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_skills.py +0 -0
  102. {engineering_process-2.3.0 → engineering_process-2.4.0}/tests/test_supervisor_posix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-process
3
- Version: 2.3.0
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
- Independent review is one direct rule: neither the reviewer actor nor reviewer context
68
- may have implemented the current cycle. There is no attestation hierarchy,
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 review to an actual reviewer. A new agent review starts in a
73
- fresh context with the accepted source artifacts and no suggested verdict; the same
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. Finish checks the current branch, head subject, and pinned comparison-base-to-head
90
- range, and writes publication metadata in a version 2 receipt. Version 1 receipts and
91
- consumers without the opt-in remain supported. Missing pinned bases and mutations
92
- during the check fail completion.
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
- Independent review is one direct rule: neither the reviewer actor nor reviewer context
44
- may have implemented the current cycle. There is no attestation hierarchy,
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 review to an actual reviewer. A new agent review starts in a
49
- fresh context with the accepted source artifacts and no suggested verdict; the same
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. Finish checks the current branch, head subject, and pinned comparison-base-to-head
66
- range, and writes publication metadata in a version 2 receipt. Version 1 receipts and
67
- consumers without the opt-in remain supported. Missing pinned bases and mutations
68
- during the check fail completion.
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)
@@ -1,3 +1,3 @@
1
1
  """Agent-neutral engineering process."""
2
2
 
3
- VERSION = "2.3.0"
3
+ VERSION = "2.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 start",
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
- return json.loads(path.read_text(encoding="utf-8"))
55
- except (OSError, UnicodeError, json.JSONDecodeError) as error:
56
- raise ProcessError(f"{path} is not valid UTF-8 JSON: {error}") from error
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 repository_snapshot, resolve_commit, same_checkpoint
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
- _require_phase(state, "verified")
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
- _event(state, "review-started", reviewer)
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 = None
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(root: Path, arguments: list[str], *, check: bool = True) -> bytes:
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():