engineering-process 2.5.0__tar.gz → 2.6.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 (107) hide show
  1. {engineering_process-2.5.0/engineering_process.egg-info → engineering_process-2.6.0}/PKG-INFO +76 -4
  2. {engineering_process-2.5.0 → engineering_process-2.6.0}/README.md +75 -3
  3. engineering_process-2.6.0/RELEASE_NOTES.md +49 -0
  4. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/__init__.py +1 -1
  5. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/cli.py +54 -1
  6. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/contracts.py +1 -0
  7. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/evidence.py +8 -1
  8. engineering_process-2.6.0/engineering_process/impact.py +231 -0
  9. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/lifecycle.py +448 -25
  10. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/pr_description.py +3 -1
  11. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/project.py +60 -0
  12. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/release_notes.py +52 -5
  13. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/repository.py +38 -0
  14. {engineering_process-2.5.0 → engineering_process-2.6.0/engineering_process.egg-info}/PKG-INFO +76 -4
  15. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process.egg-info/SOURCES.txt +3 -0
  16. {engineering_process-2.5.0 → engineering_process-2.6.0}/process_assets/skills/change-plan/SKILL.md +13 -0
  17. {engineering_process-2.5.0 → engineering_process-2.6.0}/process_assets/skills/change-verify/SKILL.md +60 -7
  18. {engineering_process-2.5.0 → engineering_process-2.6.0}/pyproject.toml +2 -1
  19. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/project.schema.json +90 -0
  20. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/receipt.schema.json +7 -5
  21. engineering_process-2.6.0/schemas/release-change.schema.json +79 -0
  22. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/release-notes-data.schema.json +44 -1
  23. engineering_process-2.6.0/schemas/release.schema.json +105 -0
  24. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/run.schema.json +2 -0
  25. engineering_process-2.6.0/schemas/verification-impact-selection.schema.json +143 -0
  26. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/verification-selection.schema.json +21 -2
  27. {engineering_process-2.5.0 → engineering_process-2.6.0}/tests/test_architecture.py +1 -0
  28. {engineering_process-2.5.0 → engineering_process-2.6.0}/tests/test_cli.py +10 -0
  29. {engineering_process-2.5.0 → engineering_process-2.6.0}/tests/test_commands.py +3 -1
  30. {engineering_process-2.5.0 → engineering_process-2.6.0}/tests/test_contracts.py +46 -0
  31. {engineering_process-2.5.0 → engineering_process-2.6.0}/tests/test_distribution.py +16 -0
  32. engineering_process-2.6.0/tests/test_impact.py +374 -0
  33. {engineering_process-2.5.0 → engineering_process-2.6.0}/tests/test_lifecycle.py +135 -2
  34. {engineering_process-2.5.0 → engineering_process-2.6.0}/tests/test_release.py +168 -16
  35. {engineering_process-2.5.0 → engineering_process-2.6.0}/tests/test_repository.py +27 -2
  36. {engineering_process-2.5.0 → engineering_process-2.6.0}/tests/test_review_contexts.py +124 -23
  37. {engineering_process-2.5.0 → engineering_process-2.6.0}/tests/test_skills.py +10 -1
  38. engineering_process-2.5.0/RELEASE_NOTES.md +0 -17
  39. engineering_process-2.5.0/schemas/release-change.schema.json +0 -20
  40. engineering_process-2.5.0/schemas/release.schema.json +0 -40
  41. {engineering_process-2.5.0 → engineering_process-2.6.0}/LICENSE +0 -0
  42. {engineering_process-2.5.0 → engineering_process-2.6.0}/MANIFEST.in +0 -0
  43. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/__main__.py +0 -0
  44. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/_supervisor_contract.py +0 -0
  45. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/_supervisor_posix.py +0 -0
  46. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/_supervisor_windows.py +0 -0
  47. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/_windows_job.py +0 -0
  48. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/adoption.py +0 -0
  49. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/artifact_standards.py +0 -0
  50. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/automation_name.py +0 -0
  51. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/commands.py +0 -0
  52. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/distribution.py +0 -0
  53. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/helper_launch.py +0 -0
  54. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/issue.py +0 -0
  55. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/production_engineering.py +0 -0
  56. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/publication_compat.py +0 -0
  57. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/release.py +0 -0
  58. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/requirements-dev.txt +0 -0
  59. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/requirements-runtime.txt +0 -0
  60. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/review_contexts.py +0 -0
  61. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/skills.py +0 -0
  62. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/source_publication.py +0 -0
  63. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process/supervision.py +0 -0
  64. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process.egg-info/dependency_links.txt +0 -0
  65. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process.egg-info/entry_points.txt +0 -0
  66. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process.egg-info/requires.txt +0 -0
  67. {engineering_process-2.5.0 → engineering_process-2.6.0}/engineering_process.egg-info/top_level.txt +0 -0
  68. {engineering_process-2.5.0 → engineering_process-2.6.0}/process-graph.json +0 -0
  69. {engineering_process-2.5.0 → engineering_process-2.6.0}/process_assets/skills/change-complete/SKILL.md +0 -0
  70. {engineering_process-2.5.0 → engineering_process-2.6.0}/process_assets/skills/change-implement/SKILL.md +0 -0
  71. {engineering_process-2.5.0 → engineering_process-2.6.0}/process_assets/skills/change-review/SKILL.md +0 -0
  72. {engineering_process-2.5.0 → engineering_process-2.6.0}/process_assets/skills/change-start/SKILL.md +0 -0
  73. {engineering_process-2.5.0 → engineering_process-2.6.0}/process_assets/skills/deliver-change/SKILL.md +0 -0
  74. {engineering_process-2.5.0 → engineering_process-2.6.0}/process_assets/skills/process-improve/SKILL.md +0 -0
  75. {engineering_process-2.5.0 → engineering_process-2.6.0}/process_assets/skills/production-engineering/SKILL.md +0 -0
  76. {engineering_process-2.5.0 → engineering_process-2.6.0}/process_assets/skills/production-engineering/invariants.json +0 -0
  77. {engineering_process-2.5.0 → engineering_process-2.6.0}/process_assets/standards/automation-name.v1.json +0 -0
  78. {engineering_process-2.5.0 → engineering_process-2.6.0}/process_assets/standards/issue.v1.json +0 -0
  79. {engineering_process-2.5.0 → engineering_process-2.6.0}/process_assets/standards/pull-request.v1.json +0 -0
  80. {engineering_process-2.5.0 → engineering_process-2.6.0}/process_assets/standards/release-notes.v1.json +0 -0
  81. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/artifact-selection.schema.json +0 -0
  82. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/artifact-standard.schema.json +0 -0
  83. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/automation-name-data.schema.json +0 -0
  84. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/change.schema.json +0 -0
  85. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/issue-data.schema.json +0 -0
  86. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/plan.schema.json +0 -0
  87. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/pr-description-data.schema.json +0 -0
  88. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/process-graph.schema.json +0 -0
  89. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/process-lock.schema.json +0 -0
  90. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/production-engineering.schema.json +0 -0
  91. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/project-legacy.schema.json +0 -0
  92. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/renovate-preset.schema.json +0 -0
  93. {engineering_process-2.5.0 → engineering_process-2.6.0}/schemas/review.schema.json +0 -0
  94. {engineering_process-2.5.0 → engineering_process-2.6.0}/setup.cfg +0 -0
  95. {engineering_process-2.5.0 → engineering_process-2.6.0}/templates/AGENTS.process.md +0 -0
  96. {engineering_process-2.5.0 → engineering_process-2.6.0}/templates/PULL_REQUEST_TEMPLATE.md +0 -0
  97. {engineering_process-2.5.0 → engineering_process-2.6.0}/templates/adopt-process-windows-job.py +0 -0
  98. {engineering_process-2.5.0 → engineering_process-2.6.0}/templates/adopt-process.py +0 -0
  99. {engineering_process-2.5.0 → engineering_process-2.6.0}/templates/renovate.json +0 -0
  100. {engineering_process-2.5.0 → engineering_process-2.6.0}/tests/test_adoption.py +0 -0
  101. {engineering_process-2.5.0 → engineering_process-2.6.0}/tests/test_artifact_standards.py +0 -0
  102. {engineering_process-2.5.0 → engineering_process-2.6.0}/tests/test_automation.py +0 -0
  103. {engineering_process-2.5.0 → engineering_process-2.6.0}/tests/test_production_engineering.py +0 -0
  104. {engineering_process-2.5.0 → engineering_process-2.6.0}/tests/test_publication_compat.py +0 -0
  105. {engineering_process-2.5.0 → engineering_process-2.6.0}/tests/test_publication_workflow.py +0 -0
  106. {engineering_process-2.5.0 → engineering_process-2.6.0}/tests/test_pypi_cache.py +0 -0
  107. {engineering_process-2.5.0 → engineering_process-2.6.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.5.0
3
+ Version: 2.6.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
@@ -96,6 +96,22 @@ child environment match. Legacy evidence without that identity reruns. This is
96
96
  whole-profile reuse: equal check IDs and ordered side effects are never merged, and
97
97
  the lifecycle receipt still records only actual executions.
98
98
 
99
+ Reuse is stage-aware rather than a blanket cache: the contract and plan travel by
100
+ digest; valid whole-profile verification can satisfy a continuation request; review
101
+ and finish consume the same reports after rechecking the snapshot; and a new
102
+ implementation cycle invalidates verification. Operation-local calculation reuse is
103
+ allowed only before a mutation/concurrency boundary. Impact feedback and generated
104
+ documents remain derived artifacts and never replace final required profiles or
105
+ independent review.
106
+
107
+ Consumers that can prove complete changed-path coverage may opt selected required
108
+ profiles into final impact assurance with `impactProfiles.schemaVersion: 2` and
109
+ `finalProfiles`. Each opted profile must declare an explicit global unit whose paths
110
+ include the universal `**` pattern for cross-cutting reach. The lifecycle then
111
+ records the selected units as final
112
+ verification evidence; missing or unresolved coverage blocks rather than silently
113
+ falling back. Existing version 1 policies remain feedback-only.
114
+
99
115
  `processctl change review replace-reused` repairs only an initial pending assignment
100
116
  proven to reuse another change's agent context, before a submitted review or normal
101
117
  report file exists. It validates a fresh replacement and current verification,
@@ -166,6 +182,33 @@ Python 3.11 or newer and Git are required. A consumer owns .process/project.json
166
182
  "timeoutSeconds": 600
167
183
  }
168
184
  ]
185
+ },
186
+ "impactProfiles": {
187
+ "schemaVersion": 1,
188
+ "profiles": {
189
+ "development": [
190
+ {
191
+ "id": "unit-tests",
192
+ "run": ["python", "-m", "unittest", "tests.test_orders"],
193
+ "timeoutSeconds": 600,
194
+ "paths": ["src/orders/**", "tests/test_orders.py"]
195
+ },
196
+ {
197
+ "id": "cross-cutting",
198
+ "run": ["python", "-m", "unittest"],
199
+ "timeoutSeconds": 900,
200
+ "paths": ["**/process-policy.json"]
201
+ }
202
+ ],
203
+ "review": [
204
+ {
205
+ "id": "package",
206
+ "run": ["python", "-m", "build"],
207
+ "timeoutSeconds": 600,
208
+ "paths": ["src/**", "pyproject.toml"]
209
+ }
210
+ ]
211
+ }
169
212
  }
170
213
  }
171
214
 
@@ -520,6 +563,33 @@ When continuing an incomplete verification, inspect and execute the necessary se
520
563
  The explicit `--profile` form remains a refresh and is never silently converted to
521
564
  reuse.
522
565
 
566
+ For fast feedback, consumers may declare a versioned `impactProfiles` policy in
567
+ `.process/project.json` and run only units related to the candidate paths:
568
+
569
+ processctl change explain --change-id change-123 --impact
570
+ processctl change verify --change-id change-123 --affected --affected-profile development
571
+
572
+ Each impact unit owns an exact command and path-pattern coverage. Matching is
573
+ explicit and deterministic; overlapping units all run. A unit with
574
+ `scope: "global"` may subsume narrower units only when its declared paths include
575
+ the universal `**` pattern; a narrower pattern never covers unrelated paths. Every
576
+ changed path must resolve for
577
+ each requested profile. Missing policy, unsupported policy, or an unmapped path is
578
+ `unresolved`/`unavailable`: the process launches nothing and returns the action
579
+ `inspect-diff-and-update-impact-policy`. The agent must inspect the diff and
580
+ dependency reach, update the consumer-owned mapping or obtain an owner decision,
581
+ then repeat the explanation. It must not use a full profile as a fallback.
582
+
583
+ Affected execution under a version 1 policy is feedback evidence only and never
584
+ advances the lifecycle or satisfies a required profile. A consumer that can prove
585
+ complete coverage may use schema version 2 with `finalProfiles`; `--remaining` then
586
+ records the selected units as `impact-assurance` evidence. Each opted profile needs
587
+ an explicit global unit with the universal `**` pattern for cross-cutting reach, and
588
+ unresolved final coverage
589
+ blocks rather than falling back silently. Explicit `--profile` remains the full
590
+ refresh. Consumers that do not adopt the opt-in retain the existing final boundary;
591
+ the affected command does not guess a policy for them.
592
+
523
593
  Assign an independent reviewer and submit its report:
524
594
 
525
595
  processctl change review start \
@@ -606,9 +676,11 @@ At any point:
606
676
  ## Release to consumer PR
607
677
 
608
678
  Each release includes [reviewed release contents](RELEASE_NOTES.md) generated from
609
- the canonical manifest: shipped features/fixes, their source issues or changes, and
610
- upgrade guidance. The release PR reviews this file; the GitHub Release publishes the
611
- same contents. See [the release procedure](RELEASING.md) for authoring and checks.
679
+ the canonical manifest: one record per shipped feature/fix, its source issue, the
680
+ problem and affected paths, the application action, compatibility impact, important
681
+ notes, and upgrade guidance. The release PR reviews this file; the GitHub Release
682
+ publishes the same contents. See [the release procedure](RELEASING.md) for authoring
683
+ and checks.
612
684
 
613
685
  This producer's release identity inputs and text assets declared by
614
686
  `tool.setuptools.data-files` use UTF-8 without BOM and LF, matching `.gitattributes`.
@@ -72,6 +72,22 @@ child environment match. Legacy evidence without that identity reruns. This is
72
72
  whole-profile reuse: equal check IDs and ordered side effects are never merged, and
73
73
  the lifecycle receipt still records only actual executions.
74
74
 
75
+ Reuse is stage-aware rather than a blanket cache: the contract and plan travel by
76
+ digest; valid whole-profile verification can satisfy a continuation request; review
77
+ and finish consume the same reports after rechecking the snapshot; and a new
78
+ implementation cycle invalidates verification. Operation-local calculation reuse is
79
+ allowed only before a mutation/concurrency boundary. Impact feedback and generated
80
+ documents remain derived artifacts and never replace final required profiles or
81
+ independent review.
82
+
83
+ Consumers that can prove complete changed-path coverage may opt selected required
84
+ profiles into final impact assurance with `impactProfiles.schemaVersion: 2` and
85
+ `finalProfiles`. Each opted profile must declare an explicit global unit whose paths
86
+ include the universal `**` pattern for cross-cutting reach. The lifecycle then
87
+ records the selected units as final
88
+ verification evidence; missing or unresolved coverage blocks rather than silently
89
+ falling back. Existing version 1 policies remain feedback-only.
90
+
75
91
  `processctl change review replace-reused` repairs only an initial pending assignment
76
92
  proven to reuse another change's agent context, before a submitted review or normal
77
93
  report file exists. It validates a fresh replacement and current verification,
@@ -142,6 +158,33 @@ Python 3.11 or newer and Git are required. A consumer owns .process/project.json
142
158
  "timeoutSeconds": 600
143
159
  }
144
160
  ]
161
+ },
162
+ "impactProfiles": {
163
+ "schemaVersion": 1,
164
+ "profiles": {
165
+ "development": [
166
+ {
167
+ "id": "unit-tests",
168
+ "run": ["python", "-m", "unittest", "tests.test_orders"],
169
+ "timeoutSeconds": 600,
170
+ "paths": ["src/orders/**", "tests/test_orders.py"]
171
+ },
172
+ {
173
+ "id": "cross-cutting",
174
+ "run": ["python", "-m", "unittest"],
175
+ "timeoutSeconds": 900,
176
+ "paths": ["**/process-policy.json"]
177
+ }
178
+ ],
179
+ "review": [
180
+ {
181
+ "id": "package",
182
+ "run": ["python", "-m", "build"],
183
+ "timeoutSeconds": 600,
184
+ "paths": ["src/**", "pyproject.toml"]
185
+ }
186
+ ]
187
+ }
145
188
  }
146
189
  }
147
190
 
@@ -496,6 +539,33 @@ When continuing an incomplete verification, inspect and execute the necessary se
496
539
  The explicit `--profile` form remains a refresh and is never silently converted to
497
540
  reuse.
498
541
 
542
+ For fast feedback, consumers may declare a versioned `impactProfiles` policy in
543
+ `.process/project.json` and run only units related to the candidate paths:
544
+
545
+ processctl change explain --change-id change-123 --impact
546
+ processctl change verify --change-id change-123 --affected --affected-profile development
547
+
548
+ Each impact unit owns an exact command and path-pattern coverage. Matching is
549
+ explicit and deterministic; overlapping units all run. A unit with
550
+ `scope: "global"` may subsume narrower units only when its declared paths include
551
+ the universal `**` pattern; a narrower pattern never covers unrelated paths. Every
552
+ changed path must resolve for
553
+ each requested profile. Missing policy, unsupported policy, or an unmapped path is
554
+ `unresolved`/`unavailable`: the process launches nothing and returns the action
555
+ `inspect-diff-and-update-impact-policy`. The agent must inspect the diff and
556
+ dependency reach, update the consumer-owned mapping or obtain an owner decision,
557
+ then repeat the explanation. It must not use a full profile as a fallback.
558
+
559
+ Affected execution under a version 1 policy is feedback evidence only and never
560
+ advances the lifecycle or satisfies a required profile. A consumer that can prove
561
+ complete coverage may use schema version 2 with `finalProfiles`; `--remaining` then
562
+ records the selected units as `impact-assurance` evidence. Each opted profile needs
563
+ an explicit global unit with the universal `**` pattern for cross-cutting reach, and
564
+ unresolved final coverage
565
+ blocks rather than falling back silently. Explicit `--profile` remains the full
566
+ refresh. Consumers that do not adopt the opt-in retain the existing final boundary;
567
+ the affected command does not guess a policy for them.
568
+
499
569
  Assign an independent reviewer and submit its report:
500
570
 
501
571
  processctl change review start \
@@ -582,9 +652,11 @@ At any point:
582
652
  ## Release to consumer PR
583
653
 
584
654
  Each release includes [reviewed release contents](RELEASE_NOTES.md) generated from
585
- the canonical manifest: shipped features/fixes, their source issues or changes, and
586
- upgrade guidance. The release PR reviews this file; the GitHub Release publishes the
587
- same contents. See [the release procedure](RELEASING.md) for authoring and checks.
655
+ the canonical manifest: one record per shipped feature/fix, its source issue, the
656
+ problem and affected paths, the application action, compatibility impact, important
657
+ notes, and upgrade guidance. The release PR reviews this file; the GitHub Release
658
+ publishes the same contents. See [the release procedure](RELEASING.md) for authoring
659
+ and checks.
588
660
 
589
661
  This producer's release identity inputs and text assets declared by
590
662
  `tool.setuptools.data-files` use UTF-8 without BOM and LF, matching `.gitattributes`.
@@ -0,0 +1,49 @@
1
+ # Engineering Process v2.6.0
2
+
3
+ Changes since v2.5.0.
4
+
5
+ ## Features
6
+
7
+ - **Select and assure verification units from explicit change impact.** ([#205](https://github.com/phuongnse/engineering-process/issues/205))
8
+ - **Problem:** A consumer could only choose a whole profile or reuse a whole-profile result, so a small change lacked a standard way to run related verification units. When impact was unclear, an automatic full-suite fallback could hide that dependency reach had not been resolved, while explicit coverage could not yet satisfy a final required profile.
9
+ - **What changed:** Add versioned consumer-owned impactProfiles, deterministic changed-path resolution, change explain --impact, and change verify --affected. Every path must resolve to matching units or the command stops with an explicit re-analysis action; an explicit global pattern is the only broad selection rule. Add a schema-version 2 finalProfiles opt-in so complete explicit coverage can run selected units as final impact-assurance evidence. Policy or candidate changes, missing coverage and unresolved paths fail closed; schema-version 1 policies remain feedback-only.
10
+ - **Where:** `schemas/project.schema.json`, `schemas/verification-impact-selection.schema.json`, `schemas/verification-selection.schema.json`, `schemas/run.schema.json`, `schemas/receipt.schema.json`, `engineering_process/impact.py`, `engineering_process/repository.py`, `engineering_process/lifecycle.py`, `engineering_process/cli.py`, `engineering_process/project.py`, `process_assets/skills/change-verify/SKILL.md`, `process_assets/skills/change-plan/SKILL.md`, `SELF_HOSTING.md`, `README.md`, `tests/test_contracts.py`, `tests/test_impact.py`, `tests/test_lifecycle.py`, `tests/test_skills.py`
11
+ - **Apply:** Adopt the package release, add impactProfiles to the consumer project policy, map every changed path to one or more independently executable commands, and run change explain --impact before change verify --affected. Opt selected required profiles into schema version 2 finalProfiles only after reviewing complete path coverage and global cross-cutting units; then run change verify --remaining. Resolve every unresolved path by inspecting the diff/dependency reach and updating the consumer-owned mapping or recording an owner decision.
12
+ - **Compatibility:** Backward-compatible opt-in. Existing project files, explicit profile verification, whole-profile reuse, required lifecycle profiles, and final review/release evidence remain supported. Consumers without impactProfiles receive a clear unavailable result, and schema-version 1 policies remain feedback-only.
13
+ - **Notes:** Path patterns are a versioned consumer policy, not a process guess. Overlapping units all run; a global unit may subsume narrower units only when its declared paths include the explicit universal \*\* pattern, while narrower patterns never cover unrelated paths. Affected feedback does not advance the lifecycle or prove final assurance. Final assurance records execution mode and selection identity in the existing lifecycle evidence; it does not create a second lifecycle or infer coverage from filenames, labels, commands, or diagnostics.
14
+
15
+ ## Fixes
16
+
17
+ - **Render release records as readable Markdown without gratuitous escapes.** ([#171](https://github.com/phuongnse/engineering-process/issues/171))
18
+ - **Problem:** Generated release notes escaped every punctuation mark and padded ordinary code references, making otherwise complete issue-level records difficult to read.
19
+ - **What changed:** Escape only Markdown and HTML-sensitive metadata syntax, keep ordinary punctuation readable, and render simple owned references as normal inline code while retaining safe fencing for embedded backticks.
20
+ - **Where:** `engineering_process/release_notes.py`, `tests/test_release.py`, `release-changes/README.md`, `RELEASING.md`, `ARTIFACT_STANDARDS.md`, `RELEASE_NOTES.md`
21
+ - **Apply:** Use the normal prepare-release workflow and review the generated RELEASE\_NOTES.md against release.json before publication.
22
+ - **Compatibility:** No breaking change. Schema-v5 manifests and legacy release-note data remain readable; the selected release-notes@1 structure and published v2.5.0 body are not rewritten.
23
+ - **Notes:** This correction applies to subsequently generated release bodies. Published release artifacts remain immutable and must be corrected only through the owner's release policy.
24
+ - **Batch reusable verification evidence across lifecycle stages.** ([#198](https://github.com/phuongnse/engineering-process/issues/198))
25
+ - **Problem:** Continuation verification could make repeated reuse decisions and authority calculations one profile at a time even when several reports already covered the same unchanged candidate.
26
+ - **What changed:** Batch valid whole-profile reuse decisions into one canonical state write and reuse one process-authority digest across stable evidence comparisons. Preserve the selected runtime path spelling and virtual-environment symlinks across POSIX and Windows while retaining fresh candidate snapshots, runtime identity checks, explicit refresh semantics, and correction-cycle invalidation.
27
+ - **Where:** `engineering_process/evidence.py`, `engineering_process/lifecycle.py`, `engineering_process/pr_description.py`, `process_assets/skills/change-verify/SKILL.md`, `README.md`, `tests/test_lifecycle.py`, `tests/test_skills.py`
28
+ - **Apply:** Use change verify --remaining for continuation work. The process reuses only exact valid reports and records reuse without relaunching their child commands.
29
+ - **Compatibility:** No breaking change. Explicit profile verification remains an unconditional refresh; stale, legacy, or mismatched evidence reruns; final review and finish still require exact-snapshot evidence.
30
+ - **Notes:** The optimization is operation-scoped and does not add a persistent success cache or reuse evidence across a changed candidate, mutation boundary, or incompatible runtime. Runtime path normalization no longer dereferences the selected executable before constructing the bounded child PATH.
31
+ - **Make the full verification suite deterministic and expose bounded timing diagnostics.** ([#201](https://github.com/phuongnse/engineering-process/issues/201))
32
+ - **Problem:** The repository-owned full verification suite took about six minutes on Windows and review-context tests incorrectly reported stale evidence when run after the complete suite, while local output did not identify the slowest tests.
33
+ - **What changed:** Give review-context tests an isolated fixture owner, seed their verified states once per test class, use the same bounded child environment as the verification boundary, and report aggregate, slow-test, fixture-setup, and profile timing without changing profile exit semantics or deleting behavioral coverage. Lifecycle fixtures clone an immutable Git snapshot instead of copying a live .git directory, avoiding races with Git maintenance locks.
34
+ - **Where:** `tests/test_review_contexts.py`, `tests/test_lifecycle.py`, `verification/run_test_suite.py`
35
+ - **Apply:** Run the normal development and review profiles; use the bounded suite timing and slow-test summary to diagnose future cost, and do not replace final verification with a focused subset.
36
+ - **Compatibility:** No breaking change. Existing profile commands, required checks, skip behavior, process/Git/filesystem boundaries, and stored evidence semantics remain supported.
37
+ - **Notes:** The reviewed change evidence records repeated comparable self-consumer measurements and the bounded improvement method; it does not establish a universal latency target. Shared mutable checkouts and blanket mocks remain out of scope.
38
+
39
+ ## Upgrade and compatibility
40
+
41
+ Merge the complete hash-locked package/adoption PR, update the local and CI environments to the selected version, and start a fresh agent session.
42
+
43
+ Consumer CI, naming conventions and branch-protection settings remain consumer-owned; adoption does not configure them automatically.
44
+
45
+ No breaking changes are included. Read each change's Apply, Compatibility and Notes entry before adopting.
46
+
47
+ See [versioning and compatibility](https://github.com/phuongnse/engineering-process/blob/v2.6.0/VERSIONING.md) and [adoption guidance](https://github.com/phuongnse/engineering-process/blob/v2.6.0/SELF_HOSTING.md).
48
+
49
+ [Full change comparison](https://github.com/phuongnse/engineering-process/compare/v2.5.0...v2.6.0)
@@ -1,3 +1,3 @@
1
1
  """Agent-neutral engineering process."""
2
2
 
3
- VERSION = "2.5.0"
3
+ VERSION = "2.6.0"
@@ -38,12 +38,14 @@ from .lifecycle import (
38
38
  start_change,
39
39
  start_review,
40
40
  submit_review,
41
+ resolve_impact_work,
41
42
  resolve_verification_work,
43
+ verify_affected,
42
44
  verify_remaining,
43
45
  verify_change,
44
46
  )
45
47
  from .issue import render_issue
46
- from .project import load_project, readiness_summary
48
+ from .project import impact_profiles, load_project, readiness_summary
47
49
  from .production_engineering import (
48
50
  validate_plan_assessments,
49
51
  validate_review_assessments,
@@ -135,6 +137,7 @@ def command_project_validate(args: argparse.Namespace) -> Result:
135
137
  "project validate",
136
138
  project=project["project"],
137
139
  profiles=sorted(project["profiles"]),
140
+ impactProfiles=sorted(impact_profiles(project)),
138
141
  requiredProfiles=project["lifecycle"]["requiredProfiles"],
139
142
  readiness=readiness_summary(project),
140
143
  ), 0
@@ -326,6 +329,34 @@ def command_change_implement(args: argparse.Namespace) -> Result:
326
329
 
327
330
  def command_change_verify(args: argparse.Namespace) -> Result:
328
331
  process_root = _process_root(args)
332
+ if getattr(args, "affected", False):
333
+ selected_profiles = tuple(getattr(args, "affected_profile", []) or []) or None
334
+ state, selection, executions = verify_affected(
335
+ args.project_root,
336
+ process_root,
337
+ {},
338
+ args.change_id,
339
+ profiles=selected_profiles,
340
+ )
341
+ execution_status = executions[0]["status"] if executions else None
342
+ status = (
343
+ "passed"
344
+ if selection["status"] == "ready" and execution_status == "passed"
345
+ else selection["status"]
346
+ if selection["status"] != "ready"
347
+ else "failed"
348
+ )
349
+ return _state_result(
350
+ "change verify",
351
+ state,
352
+ status=status,
353
+ selection=selection,
354
+ execution="affected",
355
+ evidence="feedback-only; does not satisfy a required full profile",
356
+ executions=executions,
357
+ ), (0 if status == "passed" else 1)
358
+ if getattr(args, "affected_profile", []):
359
+ raise ProcessError("--affected-profile requires --affected")
329
360
  project = load_project(args.project_root, process_root)
330
361
  if args.remaining:
331
362
  state, selection = verify_remaining(
@@ -345,6 +376,7 @@ def command_change_verify(args: argparse.Namespace) -> Result:
345
376
  {
346
377
  "profile": profile,
347
378
  "status": report["status"],
379
+ "executionMode": report.get("executionMode", "full"),
348
380
  "launchCount": len(report["checks"]),
349
381
  "durationMs": report.get("durationMs", check_duration),
350
382
  "checkDurationMs": check_duration,
@@ -386,6 +418,18 @@ def command_change_verify(args: argparse.Namespace) -> Result:
386
418
 
387
419
  def command_change_explain(args: argparse.Namespace) -> Result:
388
420
  process_root = _process_root(args)
421
+ if getattr(args, "impact", False):
422
+ profiles = (args.profile,) if args.profile else None
423
+ selection = resolve_impact_work(
424
+ args.project_root,
425
+ process_root,
426
+ {},
427
+ args.change_id,
428
+ profiles=profiles,
429
+ )
430
+ return _result("change explain", selection=selection), 0
431
+ if getattr(args, "profile", None):
432
+ raise ProcessError("--profile for change explain requires --impact")
389
433
  project = load_project(args.project_root, process_root)
390
434
  selection = resolve_verification_work(
391
435
  args.project_root, process_root, project, args.change_id
@@ -667,9 +711,18 @@ def build_parser() -> argparse.ArgumentParser:
667
711
  verify_scope = change_verify.add_mutually_exclusive_group(required=True)
668
712
  verify_scope.add_argument("--profile")
669
713
  verify_scope.add_argument("--remaining", action="store_true")
714
+ verify_scope.add_argument("--affected", action="store_true")
715
+ change_verify.add_argument(
716
+ "--affected-profile",
717
+ action="append",
718
+ default=[],
719
+ help="limit --affected to one or more accepted profiles",
720
+ )
670
721
 
671
722
  change_explain = _leaf(change_commands, "explain", command_change_explain)
672
723
  change_explain.add_argument("--change-id", required=True)
724
+ change_explain.add_argument("--impact", action="store_true")
725
+ change_explain.add_argument("--profile")
673
726
 
674
727
  review = change_commands.add_parser("review")
675
728
  review_commands = review.add_subparsers(dest="review_command", required=True)
@@ -36,6 +36,7 @@ CONTRACT_KINDS = (
36
36
  "renovate-preset",
37
37
  "review",
38
38
  "run",
39
+ "verification-impact-selection",
39
40
  "verification-selection",
40
41
  )
41
42
 
@@ -110,6 +110,7 @@ def verification_input_digest(
110
110
  profile: str,
111
111
  *,
112
112
  runtime: dict[str, Any] | None = None,
113
+ authority_digest: str | None = None,
113
114
  ) -> str | None:
114
115
  """Bind reusable evidence to every input controlled by this process."""
115
116
  runtime = runtime if runtime is not None else execution_identity()
@@ -119,7 +120,11 @@ def verification_input_digest(
119
120
  return digest_json({
120
121
  "authority": {
121
122
  "version": VERSION,
122
- "distribution": distribution_digest(process_root),
123
+ "distribution": (
124
+ authority_digest
125
+ if authority_digest is not None
126
+ else distribution_digest(process_root)
127
+ ),
123
128
  },
124
129
  "project": project,
125
130
  "contractDigest": state["contract"]["digest"],
@@ -145,6 +150,7 @@ def verification_report_matches_inputs(
145
150
  require_input: bool,
146
151
  require_explicit_scope: bool = False,
147
152
  runtime: dict[str, Any] | None = None,
153
+ authority_digest: str | None = None,
148
154
  ) -> bool:
149
155
  """Return whether one report is reusable for the supplied inputs."""
150
156
  scope = report.get("scope")
@@ -169,5 +175,6 @@ def verification_report_matches_inputs(
169
175
  state,
170
176
  profile,
171
177
  runtime=runtime,
178
+ authority_digest=authority_digest,
172
179
  )
173
180
  return current is not None and recorded == current
@@ -0,0 +1,231 @@
1
+ """Strict, consumer-owned change-impact resolution for fast feedback."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fnmatch import fnmatchcase
6
+ from pathlib import Path
7
+ from typing import Any, Iterable
8
+
9
+ from .contracts import ProcessError, digest_json, validate_document
10
+ from .distribution import schemas_root
11
+ from .project import impact_assurance_profiles, impact_profiles
12
+ from .repository import changed_paths
13
+
14
+
15
+ def _matches(path: str, patterns: Iterable[str]) -> bool:
16
+ return any(fnmatchcase(path, pattern) for pattern in patterns)
17
+
18
+
19
+ def resolve_impact_selection(
20
+ project_root: Path,
21
+ process_root: Path,
22
+ project: dict[str, Any],
23
+ state: dict[str, Any],
24
+ *,
25
+ profiles: Iterable[str] | None = None,
26
+ ) -> dict[str, Any]:
27
+ """Resolve every changed path to explicit consumer verification units.
28
+
29
+ This function is intentionally fail-closed. It never converts an unknown
30
+ path into a full profile request.
31
+ """
32
+ required = tuple(state["contract"]["document"]["requiredProfiles"])
33
+ requested = tuple(required if profiles is None else profiles)
34
+ invalid = sorted(set(requested) - set(required))
35
+ if invalid:
36
+ raise ProcessError(
37
+ "impact selection may request only accepted required profiles: "
38
+ + ", ".join(invalid)
39
+ )
40
+
41
+ base = state.get("comparisonBaseCommit")
42
+ if not isinstance(base, str) or not base:
43
+ raise ProcessError("impact selection requires a pinned comparison base")
44
+ paths = changed_paths(project_root, base)
45
+ policy = impact_profiles(project)
46
+ assurance_profiles = tuple(impact_assurance_profiles(project))
47
+ policy_digest = digest_json(project.get("impactProfiles", {}))
48
+ selected: list[dict[str, Any]] = []
49
+ requirements: list[dict[str, Any]] = []
50
+ unresolved: list[dict[str, str]] = []
51
+
52
+ for profile in requested:
53
+ units = policy.get(profile) if isinstance(policy, dict) else None
54
+ if not isinstance(units, list) or not units:
55
+ requirements.append(
56
+ {
57
+ "profile": profile,
58
+ "status": "unavailable",
59
+ "selectedUnits": [],
60
+ "unresolvedPaths": [],
61
+ "reason": "the consumer has not declared an impact policy for this profile",
62
+ }
63
+ )
64
+ unresolved.extend({"profile": profile, "path": path} for path in paths)
65
+ continue
66
+
67
+ matched: dict[str, set[str]] = {unit["id"]: set() for unit in units}
68
+ missing: list[str] = []
69
+ for path in paths:
70
+ matches = [unit for unit in units if _matches(path, unit["paths"])]
71
+ if not matches:
72
+ missing.append(path)
73
+ unresolved.append({"profile": profile, "path": path})
74
+ continue
75
+ for unit in matches:
76
+ matched[unit["id"]].add(path)
77
+
78
+ # Only an explicit universal pattern can subsume narrower units. A
79
+ # partial global pattern remains subject to the ordinary unresolved-
80
+ # path boundary.
81
+ global_ids = {
82
+ unit["id"]
83
+ for unit in units
84
+ if (
85
+ unit.get("scope", "matched") == "global"
86
+ and "**" in unit["paths"]
87
+ and matched[unit["id"]]
88
+ )
89
+ }
90
+
91
+ selected_for_profile = [
92
+ {
93
+ "profile": profile,
94
+ "id": unit["id"],
95
+ "matchedPaths": sorted(matched[unit["id"]]),
96
+ }
97
+ for unit in units
98
+ if matched[unit["id"]]
99
+ and (not global_ids or unit["id"] in global_ids)
100
+ ]
101
+ selected.extend(selected_for_profile)
102
+ requirements.append(
103
+ {
104
+ "profile": profile,
105
+ "status": "unresolved" if missing else "ready",
106
+ "selectedUnits": selected_for_profile,
107
+ "unresolvedPaths": sorted(missing),
108
+ **(
109
+ {
110
+ "reason": "one or more changed paths have no declared verification unit"
111
+ }
112
+ if missing
113
+ else {}
114
+ ),
115
+ }
116
+ )
117
+
118
+ unresolved = sorted(
119
+ unresolved,
120
+ key=lambda item: (item["profile"], item["path"]),
121
+ )
122
+ status = "ready"
123
+ if unresolved or not paths:
124
+ status = "unresolved" if policy else "unavailable"
125
+ selection: dict[str, Any] = {
126
+ "schemaVersion": 2 if assurance_profiles else 1,
127
+ "changeId": state["changeId"],
128
+ "status": status,
129
+ "comparisonBaseCommit": base,
130
+ "policyDigest": policy_digest,
131
+ "assuranceProfiles": [
132
+ profile for profile in requested if profile in assurance_profiles
133
+ ],
134
+ "changedPaths": list(paths),
135
+ "requirements": requirements,
136
+ "selectedUnits": selected,
137
+ "unresolvedPaths": unresolved,
138
+ }
139
+ if status != "ready":
140
+ if not paths:
141
+ reason = "the candidate has no changed path from the pinned comparison base"
142
+ elif not policy:
143
+ reason = "the consumer has not declared a versioned impact policy"
144
+ else:
145
+ reason = "every requested profile must resolve every changed path before affected execution"
146
+ selection["resolution"] = {
147
+ "action": "inspect-diff-and-update-impact-policy",
148
+ "reason": reason,
149
+ "paths": list(paths),
150
+ }
151
+ return validate_document(
152
+ selection,
153
+ "verification-impact-selection",
154
+ schema_root=schemas_root(process_root),
155
+ source="verification impact selection",
156
+ )
157
+
158
+
159
+ def unavailable_impact_selection(
160
+ project_root: Path,
161
+ process_root: Path,
162
+ state: dict[str, Any],
163
+ reason: str,
164
+ *,
165
+ profiles: Iterable[str] | None = None,
166
+ ) -> dict[str, Any]:
167
+ """Build an actionable unavailable result when policy loading fails."""
168
+ required = tuple(state["contract"]["document"]["requiredProfiles"])
169
+ requested = tuple(required if profiles is None else profiles)
170
+ invalid = sorted(set(requested) - set(required))
171
+ if invalid:
172
+ raise ProcessError(
173
+ "impact selection may request only accepted required profiles: "
174
+ + ", ".join(invalid)
175
+ )
176
+ paths = changed_paths(project_root, state["comparisonBaseCommit"])
177
+ unresolved = [
178
+ {"profile": profile, "path": path}
179
+ for profile in requested
180
+ for path in paths
181
+ ]
182
+ selection = {
183
+ "schemaVersion": 1,
184
+ "changeId": state["changeId"],
185
+ "status": "unavailable",
186
+ "comparisonBaseCommit": state["comparisonBaseCommit"],
187
+ "changedPaths": list(paths),
188
+ "requirements": [
189
+ {
190
+ "profile": profile,
191
+ "status": "unavailable",
192
+ "selectedUnits": [],
193
+ "unresolvedPaths": list(paths),
194
+ "reason": "the consumer impact policy could not be loaded",
195
+ }
196
+ for profile in requested
197
+ ],
198
+ "selectedUnits": [],
199
+ "unresolvedPaths": unresolved,
200
+ "resolution": {
201
+ "action": "inspect-diff-and-update-impact-policy",
202
+ "reason": f"the consumer impact policy is invalid or unsupported: {reason}"[:2000],
203
+ "paths": list(paths),
204
+ },
205
+ }
206
+ return validate_document(
207
+ selection,
208
+ "verification-impact-selection",
209
+ schema_root=schemas_root(process_root),
210
+ source="verification impact selection",
211
+ )
212
+
213
+
214
+ def impact_unit_lookup(
215
+ project: dict[str, Any], selection: dict[str, Any]
216
+ ) -> dict[tuple[str, str], dict[str, Any]]:
217
+ """Resolve selected output ids back to the consumer-owned command objects."""
218
+ policies = impact_profiles(project)
219
+ lookup: dict[tuple[str, str], dict[str, Any]] = {}
220
+ for profile, units in policies.items():
221
+ for unit in units:
222
+ lookup[(profile, unit["id"])] = unit
223
+ result: dict[tuple[str, str], dict[str, Any]] = {}
224
+ for selected in selection["selectedUnits"]:
225
+ key = (selected["profile"], selected["id"])
226
+ if key not in lookup:
227
+ raise ProcessError(
228
+ f"impact policy changed while resolving unit {key[0]}:{key[1]}"
229
+ )
230
+ result[key] = lookup[key]
231
+ return result