engineering-process 2.3.1__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 (101) hide show
  1. {engineering_process-2.3.1/engineering_process.egg-info → engineering_process-2.4.0}/PKG-INFO +9 -5
  2. {engineering_process-2.3.1 → engineering_process-2.4.0}/README.md +8 -4
  3. engineering_process-2.4.0/RELEASE_NOTES.md +22 -0
  4. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/__init__.py +1 -1
  5. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/lifecycle.py +35 -20
  6. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/repository.py +49 -1
  7. {engineering_process-2.3.1 → engineering_process-2.4.0/engineering_process.egg-info}/PKG-INFO +9 -5
  8. {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/change-implement/SKILL.md +18 -2
  9. {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/change-plan/SKILL.md +15 -0
  10. {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/change-review/SKILL.md +18 -0
  11. {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/change-start/SKILL.md +7 -0
  12. {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/change-verify/SKILL.md +8 -2
  13. {engineering_process-2.3.1 → engineering_process-2.4.0}/pyproject.toml +1 -1
  14. {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_lifecycle.py +118 -1
  15. engineering_process-2.4.0/tests/test_repository.py +203 -0
  16. engineering_process-2.3.1/RELEASE_NOTES.md +0 -18
  17. engineering_process-2.3.1/tests/test_repository.py +0 -63
  18. {engineering_process-2.3.1 → engineering_process-2.4.0}/LICENSE +0 -0
  19. {engineering_process-2.3.1 → engineering_process-2.4.0}/MANIFEST.in +0 -0
  20. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/__main__.py +0 -0
  21. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/_supervisor_contract.py +0 -0
  22. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/_supervisor_posix.py +0 -0
  23. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/_supervisor_windows.py +0 -0
  24. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/_windows_job.py +0 -0
  25. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/adoption.py +0 -0
  26. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/artifact_standards.py +0 -0
  27. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/automation_name.py +0 -0
  28. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/cli.py +0 -0
  29. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/commands.py +0 -0
  30. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/contracts.py +0 -0
  31. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/distribution.py +0 -0
  32. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/helper_launch.py +0 -0
  33. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/issue.py +0 -0
  34. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/pr_description.py +0 -0
  35. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/production_engineering.py +0 -0
  36. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/project.py +0 -0
  37. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/publication_compat.py +0 -0
  38. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/release.py +0 -0
  39. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/release_notes.py +0 -0
  40. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/requirements-dev.txt +0 -0
  41. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/requirements-runtime.txt +0 -0
  42. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/review_contexts.py +0 -0
  43. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/skills.py +0 -0
  44. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/source_publication.py +0 -0
  45. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/supervision.py +0 -0
  46. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process.egg-info/SOURCES.txt +0 -0
  47. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process.egg-info/dependency_links.txt +0 -0
  48. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process.egg-info/entry_points.txt +0 -0
  49. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process.egg-info/requires.txt +0 -0
  50. {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process.egg-info/top_level.txt +0 -0
  51. {engineering_process-2.3.1 → engineering_process-2.4.0}/process-graph.json +0 -0
  52. {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/change-complete/SKILL.md +0 -0
  53. {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/deliver-change/SKILL.md +0 -0
  54. {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/process-improve/SKILL.md +0 -0
  55. {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/production-engineering/SKILL.md +0 -0
  56. {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/production-engineering/invariants.json +0 -0
  57. {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/standards/automation-name.v1.json +0 -0
  58. {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/standards/issue.v1.json +0 -0
  59. {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/standards/pull-request.v1.json +0 -0
  60. {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/standards/release-notes.v1.json +0 -0
  61. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/artifact-selection.schema.json +0 -0
  62. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/artifact-standard.schema.json +0 -0
  63. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/automation-name-data.schema.json +0 -0
  64. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/change.schema.json +0 -0
  65. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/issue-data.schema.json +0 -0
  66. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/plan.schema.json +0 -0
  67. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/pr-description-data.schema.json +0 -0
  68. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/process-graph.schema.json +0 -0
  69. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/process-lock.schema.json +0 -0
  70. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/production-engineering.schema.json +0 -0
  71. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/project-legacy.schema.json +0 -0
  72. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/project.schema.json +0 -0
  73. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/receipt.schema.json +0 -0
  74. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/release-change.schema.json +0 -0
  75. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/release-notes-data.schema.json +0 -0
  76. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/release.schema.json +0 -0
  77. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/renovate-preset.schema.json +0 -0
  78. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/review.schema.json +0 -0
  79. {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/run.schema.json +0 -0
  80. {engineering_process-2.3.1 → engineering_process-2.4.0}/setup.cfg +0 -0
  81. {engineering_process-2.3.1 → engineering_process-2.4.0}/templates/AGENTS.process.md +0 -0
  82. {engineering_process-2.3.1 → engineering_process-2.4.0}/templates/PULL_REQUEST_TEMPLATE.md +0 -0
  83. {engineering_process-2.3.1 → engineering_process-2.4.0}/templates/adopt-process-windows-job.py +0 -0
  84. {engineering_process-2.3.1 → engineering_process-2.4.0}/templates/adopt-process.py +0 -0
  85. {engineering_process-2.3.1 → engineering_process-2.4.0}/templates/renovate.json +0 -0
  86. {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_adoption.py +0 -0
  87. {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_architecture.py +0 -0
  88. {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_artifact_standards.py +0 -0
  89. {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_automation.py +0 -0
  90. {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_cli.py +0 -0
  91. {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_commands.py +0 -0
  92. {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_contracts.py +0 -0
  93. {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_distribution.py +0 -0
  94. {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_production_engineering.py +0 -0
  95. {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_publication_compat.py +0 -0
  96. {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_publication_workflow.py +0 -0
  97. {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_pypi_cache.py +0 -0
  98. {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_release.py +0 -0
  99. {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_review_contexts.py +0 -0
  100. {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_skills.py +0 -0
  101. {engineering_process-2.3.1 → 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.1
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
@@ -102,10 +102,14 @@ processctl a provider runtime or a model-quality evaluator.
102
102
  Consumers using the packaged publication compatibility policy can enable
103
103
  `lifecycle.publication.required: true` in `.process/project.json` after adopting a
104
104
  version that supports it. Start then rejects an invalid branch before creating a
105
- run. Finish checks the current branch, head subject, and pinned comparison-base-to-head
106
- range, and writes publication metadata in a version 2 receipt. Version 1 receipts and
107
- consumers without the opt-in remain supported. Missing pinned bases and mutations
108
- 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.
109
113
 
110
114
  PR readiness additionally requires the selected ready-state body/title and exact
111
115
  head/range checks. Follow the [completion preflight](process_assets/skills/change-complete/SKILL.md)
@@ -78,10 +78,14 @@ processctl a provider runtime or a model-quality evaluator.
78
78
  Consumers using the packaged publication compatibility policy can enable
79
79
  `lifecycle.publication.required: true` in `.process/project.json` after adopting a
80
80
  version that supports it. Start then rejects an invalid branch before creating a
81
- run. Finish checks the current branch, head subject, and pinned comparison-base-to-head
82
- range, and writes publication metadata in a version 2 receipt. Version 1 receipts and
83
- consumers without the opt-in remain supported. Missing pinned bases and mutations
84
- 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.
85
89
 
86
90
  PR readiness additionally requires the selected ready-state body/title and exact
87
91
  head/range checks. Follow the [completion preflight](process_assets/skills/change-complete/SKILL.md)
@@ -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.1"
3
+ VERSION = "2.4.0"
@@ -36,7 +36,9 @@ 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
+ )
40
42
  from .review_contexts import (
41
43
  recorded_context_conflict,
42
44
  require_unreused_context,
@@ -317,6 +319,33 @@ def begin_implementation(
317
319
  return state
318
320
 
319
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
+
320
349
  def verify_change(
321
350
  project_root: Path,
322
351
  process_root: Path,
@@ -330,6 +359,7 @@ def verify_change(
330
359
  raise ProcessError(f"profile {profile} is not required by change {change_id}")
331
360
 
332
361
  before = repository_snapshot(project_root)
362
+ _publication_preflight(project_root, project, state, before)
333
363
  report = run_profile(project_root, project, profile)
334
364
  return _record_verification(
335
365
  project_root, process_root, change_id, state["cycle"], before, report
@@ -448,6 +478,9 @@ def start_review(
448
478
  previous_assignment["checkpoint"], checkpoint
449
479
  ):
450
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
+ )
451
484
  state["reviewAssignment"] = {
452
485
  "reviewer": reviewer,
453
486
  "checkpoint": checkpoint,
@@ -619,25 +652,7 @@ def finish_change(
619
652
  actor = _actor(actor_id, context_id, kind)
620
653
  project = load_project(project_root, process_root)
621
654
  checkpoint = repository_snapshot(project_root)
622
- publication = None
623
- if publication_required(project):
624
- if not (comparison_base := state.get("comparisonBaseCommit")):
625
- raise ProcessError("publication completion requires a pinned comparison base")
626
- publication = validate_current_source(
627
- project_root, comparison_base
628
- )
629
- if publication["range"] != f"{comparison_base}..{checkpoint['head']}":
630
- raise ProcessError("publication validation does not match the reviewed HEAD")
631
- after_publication = repository_snapshot(project_root)
632
- if (not same_checkpoint(checkpoint, after_publication)
633
- or current_branch(project_root) != publication["branch"]):
634
- raise ProcessError("repository changed while publication validation was running")
635
- if publication["issues"]:
636
- raise ProcessError(
637
- "publication compatibility checks failed: "
638
- + "; ".join(publication["issues"])
639
- )
640
- checkpoint = after_publication
655
+ publication = _publication_preflight(project_root, project, state, checkpoint)
641
656
  if not same_checkpoint(checkpoint, state["reviewAssignment"]["checkpoint"]):
642
657
  raise ProcessError("repository changed after approval")
643
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():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-process
3
- Version: 2.3.1
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
@@ -102,10 +102,14 @@ processctl a provider runtime or a model-quality evaluator.
102
102
  Consumers using the packaged publication compatibility policy can enable
103
103
  `lifecycle.publication.required: true` in `.process/project.json` after adopting a
104
104
  version that supports it. Start then rejects an invalid branch before creating a
105
- run. Finish checks the current branch, head subject, and pinned comparison-base-to-head
106
- range, and writes publication metadata in a version 2 receipt. Version 1 receipts and
107
- consumers without the opt-in remain supported. Missing pinned bases and mutations
108
- 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.
109
113
 
110
114
  PR readiness additionally requires the selected ready-state body/title and exact
111
115
  head/range checks. Follow the [completion preflight](process_assets/skills/change-complete/SKILL.md)
@@ -13,8 +13,24 @@ Every delegated actor/context that may mutate the candidate must run the same co
13
13
  before its first edit. During an active cycle the command appends that participant
14
14
  without starting another cycle; review checks all registered participants.
15
15
 
16
- Implement only the current plan. Add focused regression coverage for deterministic
17
- defects and keep consumer-owned policy in the consumer. If review requested changes,
16
+ Implement only the current plan and keep consumer-owned policy in the consumer.
17
+ For a deterministic defect, add focused regression evidence that distinguishes the
18
+ reported faulty behavior from the correction. When a known-faulty state is safely
19
+ available, demonstrate that distinction; no proof of test-authoring or execution
20
+ order is required. Each added or materially changed test or check must protect accepted
21
+ behavior, a contract boundary, or a concrete risk, rather than merely execute changed
22
+ code or mirror its structure. Retain consumer-mandated checks; do not manufacture a
23
+ new automated test when existing evidence suffices or other evidence better fits the
24
+ change. Focused checks support diagnosis and implementation; required full profiles
25
+ still run on the final unchanged candidate.
26
+
27
+ Update, supplement, or create only the project information needed for the planned
28
+ knowledge repairs, using inspected behavior, accepted requirements, and recorded
29
+ decisions. Replaced material is updated, removed, or visibly marked historical or
30
+ superseded when retaining it is useful. Explain enough for the intended reader and
31
+ accepted work, in a logical order, with references that clarify relevant relationships.
32
+
33
+ If review requested changes,
18
34
  resolve every blocking finding in the next cycle without renaming or dropping it.
19
35
  Follow every applicable **production-engineering** assessment and make its named work
20
36
  items produce the planned objective evidence. If implementation proves an
@@ -18,6 +18,21 @@ maintenance change, accounting for indirection and coupling. Bind needed design
18
18
  to existing work-item outcomes. Keep reasoning proportional; retaining a clear
19
19
  existing structure is a valid choice.
20
20
 
21
+ Identify project knowledge this change would make misleading, incomplete, or obsolete,
22
+ and concrete information gaps obstructing the accepted work. Plan only the necessary
23
+ updates or additions in consumer-owned sources. Any proposed cleanup names the
24
+ obstacle, smallest useful repair, and expected benefit; unrelated gaps remain
25
+ non-blocking proposals.
26
+
27
+ Explain a material or non-obvious evidence strategy in the existing `approach`, tied
28
+ to accepted behavior, contract boundaries, and concrete risks. Derive expectations
29
+ for new deterministic behavior from acceptance criteria and important cases; preserve
30
+ behavioral evidence for refactors, and select an integration boundary capable of
31
+ observing the relevant contract or failure. Documentation, configuration, visual, or
32
+ exploratory work may use suitable checks or contextual review. These are defaults;
33
+ preserve consumer-mandated testing policies. An obvious existing verification path
34
+ needs no repeated rationale or additional record.
35
+
21
36
  Read **production-engineering** and add one `productionEngineering` assessment for
22
37
  each canonical invariant in its defined order. Decide applicability from the stated
23
38
  trigger, give a concrete rationale, and bind every applicable invariant to the work
@@ -102,6 +102,24 @@ Apply the existing blocking rules to demonstrated violations even when tests pas
102
102
  preference for a pattern, shorter code, or a different valid structure is insufficient.
103
103
  Do not retrofit new design criteria into the frozen contract.
104
104
 
105
+ Assess relevant project knowledge against implementation and accepted requirements:
106
+ can the intended reader find the authoritative current source, understand sufficient
107
+ explanations in a logical order, and use meaningful connections where helpful? A
108
+ finding names a concrete error, necessary missing explanation, confusing organization,
109
+ or missing/misleading connection and its consequence for the work. A different folder,
110
+ heading, or style preference is insufficient. Consumer checks can prove properties
111
+ such as valid links, runnable examples, parseable structure, or rendering; presence,
112
+ length, formatting, and resolving links do not establish semantic usefulness or truth.
113
+
114
+ Assess whether material test expectations follow the accepted contract, important
115
+ claimed risks are actually exercised, and the execution boundary could detect the
116
+ claimed failure. Reject assertions overfitted to implementation details when they do
117
+ not protect observable behavior. Findings identify an accepted behavior or applicable
118
+ contract and a concrete consequence; tests must not introduce new product behavior.
119
+ Test counts, coverage percentages, or green profiles alone do not establish adequacy.
120
+ No newly added test is required when existing evidence protects the behavior or an
121
+ automated behavioral check is not meaningful; preserve consumer-mandated policies.
122
+
105
123
  Read **production-engineering** and independently reassess every canonical invariant.
106
124
  Use the report's `productionEngineering` entries to record `satisfied`,
107
125
  `not-applicable`, or `violated`; cite concrete snapshot evidence for each satisfied
@@ -10,6 +10,13 @@ behavior. Write a change contract containing the source request, comparison base
10
10
  risk, affected projects, observable acceptance criteria, and required verification
11
11
  profiles. Do not decide unresolved product behavior silently.
12
12
 
13
+ Use a clear consumer-owned reading entry point to locate the project knowledge needed
14
+ for this work. Reuse coherent existing sources; consumers choose their organization
15
+ and document types, without placeholders for irrelevant material. Where sources could
16
+ conflict, establish which is authoritative and which is current or superseded. A
17
+ missing entry point or necessary explanation calls for a focused repair, not a broad
18
+ documentation migration.
19
+
13
20
  When the work produces an issue record, PR description, release notes or an automation name, inspect the consumer's
14
21
  selected artifact standard and existing publication checks. The consumer may override
15
22
  the packaged defaults. Keep document-format choices separate from lifecycle approval.
@@ -5,8 +5,14 @@ description: Run the project-owned verification profiles on one unchanged reposi
5
5
 
6
6
  # Verify a change
7
7
 
8
- Read the registered acceptance criteria and .process/project.json. Run every required
9
- profile through the lifecycle:
8
+ Read the registered acceptance criteria and .process/project.json. When publication
9
+ is required, commit the complete candidate on a valid publication branch before final
10
+ verification. The lifecycle rejects uncommitted candidate changes or an invalid or
11
+ empty pinned-base-to-HEAD range before running a profile, and rechecks before review.
12
+ Lifecycle run/receipt files remain local. A later commit, even with identical source
13
+ content, changes the checkpoint and requires fresh verification and review.
14
+
15
+ Run every required profile through the lifecycle:
10
16
 
11
17
  processctl change verify --change-id ID --profile PROFILE
12
18
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "engineering-process"
7
- version = "2.3.1"
7
+ version = "2.4.0"
8
8
  description = "A small agent-neutral engineering lifecycle with managed adoption"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -9,6 +9,7 @@ import tempfile
9
9
  import unittest
10
10
  from unittest.mock import patch
11
11
 
12
+ from engineering_process.commands import run_profile
12
13
  from engineering_process.contracts import ProcessError, digest_json
13
14
  from engineering_process.artifact_standards import resolve_standard
14
15
  from engineering_process.lifecycle import (
@@ -365,6 +366,122 @@ class LifecycleTests(unittest.TestCase):
365
366
  (self.root / ".process" / "receipts" / "sample-change.json").exists()
366
367
  )
367
368
 
369
+ def test_publication_rejects_empty_range_before_profiles_then_committed_candidate_finishes(self) -> None:
370
+ self.prepare_publication_candidate()
371
+ self.contract["comparisonBase"] = "HEAD"
372
+ self.plan["contractDigest"] = digest_json(self.contract)
373
+ write_json(self.contract_path, self.contract)
374
+ write_json(self.plan_path, self.plan)
375
+ git(self.root, "add", ".")
376
+ git(self.root, "commit", "-qm", "fix: select initial boundary")
377
+ self.begin()
378
+ (self.root / "product.txt").write_text("corrected\n", encoding="utf-8")
379
+ state_path = self.root / ".process/runs/sample-change/run.json"
380
+ before = state_path.read_bytes()
381
+ with patch("engineering_process.lifecycle.run_profile", wraps=run_profile) as runner:
382
+ with self.assertRaisesRegex(ProcessError, "at least one commit.*before change verify"):
383
+ verify_change(self.root, PROCESS_ROOT, self.project, "sample-change", "development")
384
+ runner.assert_not_called()
385
+ self.assertEqual(before, state_path.read_bytes())
386
+
387
+ git(self.root, "add", "product.txt")
388
+ git(self.root, "commit", "-qm", "fix: commit corrected candidate")
389
+ self.verify_all()
390
+ start_review(self.root, PROCESS_ROOT, "sample-change",
391
+ actor_id="reviewer", context_id="review-context", kind="agent")
392
+ review_path = self.root / ".process/runs/review-input.json"
393
+ write_json(review_path, self.review_document("approved"))
394
+ submit_review(self.root, PROCESS_ROOT, "sample-change", review_path)
395
+ state, receipt = finish_change(self.root, PROCESS_ROOT, "sample-change",
396
+ actor_id="coordinator", context_id="finish-context", kind="agent")
397
+ self.assertEqual(1, state["cycle"])
398
+ self.assertEqual("completed", state["phase"])
399
+ self.assertEqual("fix: commit corrected candidate", receipt["publication"]["subject"])
400
+
401
+ def test_publication_verify_rejects_dirty_candidate_before_profiles(self) -> None:
402
+ self.prepare_publication_candidate()
403
+ self.begin()
404
+ (self.root / "product.txt").write_text("not committed\n", encoding="utf-8")
405
+ state_path = self.root / ".process/runs/sample-change/run.json"
406
+ before = state_path.read_bytes()
407
+ with patch("engineering_process.lifecycle.run_profile", wraps=run_profile) as runner:
408
+ with self.assertRaisesRegex(ProcessError, "committed candidate changes"):
409
+ verify_change(self.root, PROCESS_ROOT, self.project, "sample-change", "development")
410
+ runner.assert_not_called()
411
+ self.assertEqual(before, state_path.read_bytes())
412
+
413
+ def test_publication_review_rechecks_branch_without_assigning_reviewer(self) -> None:
414
+ self.prepare_publication_candidate()
415
+ self.begin()
416
+ self.verify_all()
417
+ state_path = self.root / ".process/runs/sample-change/run.json"
418
+ before = state_path.read_bytes()
419
+ git(self.root, "branch", "-M", "codex/invalid")
420
+ with self.assertRaisesRegex(ProcessError, "publication compatibility checks failed"):
421
+ start_review(self.root, PROCESS_ROOT, "sample-change",
422
+ actor_id="reviewer", context_id="review-context", kind="agent")
423
+ self.assertEqual(before, state_path.read_bytes())
424
+
425
+ def test_publication_verify_rejects_changes_hidden_by_index_flags(self) -> None:
426
+ self.prepare_publication_candidate()
427
+ self.begin()
428
+ state_path = self.root / ".process/runs/sample-change/run.json"
429
+ before = state_path.read_bytes()
430
+ for flag in ("--assume-unchanged", "--skip-worktree"):
431
+ with self.subTest(flag=flag):
432
+ git(self.root, "update-index", flag, "product.txt")
433
+ (self.root / "product.txt").write_text("hidden change\n", encoding="utf-8")
434
+ with patch("engineering_process.lifecycle.run_profile", wraps=run_profile) as runner:
435
+ with self.assertRaisesRegex(ProcessError, "committed candidate changes"):
436
+ verify_change(self.root, PROCESS_ROOT, self.project, "sample-change", "development")
437
+ runner.assert_not_called()
438
+ self.assertEqual(before, state_path.read_bytes())
439
+ git(self.root, "update-index", "--no-assume-unchanged", "product.txt")
440
+ git(self.root, "update-index", "--no-skip-worktree", "product.txt")
441
+
442
+ def test_publication_verify_rejects_head_mutation_during_preflight(self) -> None:
443
+ self.prepare_publication_candidate()
444
+ self.begin()
445
+ state_path = self.root / ".process/runs/sample-change/run.json"
446
+ before = state_path.read_bytes()
447
+
448
+ def change_head(root: Path, base: str) -> dict:
449
+ result = validate_current_source(root, base)
450
+ git(root, "commit", "--allow-empty", "-qm", "fix: concurrent commit")
451
+ return result
452
+
453
+ with patch("engineering_process.lifecycle.validate_current_source", side_effect=change_head):
454
+ with patch("engineering_process.lifecycle.run_profile", wraps=run_profile) as runner:
455
+ with self.assertRaisesRegex(ProcessError, "repository changed while publication"):
456
+ verify_change(self.root, PROCESS_ROOT, self.project, "sample-change", "development")
457
+ runner.assert_not_called()
458
+ self.assertEqual(before, state_path.read_bytes())
459
+
460
+ def test_publication_review_rejects_legacy_dirty_verification(self) -> None:
461
+ self.prepare_publication_candidate()
462
+ self.begin()
463
+ (self.root / "product.txt").write_text("legacy uncommitted candidate\n", encoding="utf-8")
464
+ # Reproduce evidence recorded by a process without the early preflight.
465
+ with patch("engineering_process.lifecycle._publication_preflight"):
466
+ self.verify_all()
467
+ state_path = self.root / ".process/runs/sample-change/run.json"
468
+ before = state_path.read_bytes()
469
+ with self.assertRaisesRegex(ProcessError, "committed candidate changes"):
470
+ start_review(self.root, PROCESS_ROOT, "sample-change",
471
+ actor_id="reviewer", context_id="review-context", kind="agent")
472
+ self.assertEqual(before, state_path.read_bytes())
473
+
474
+ def test_metadata_only_commit_still_invalidates_verification(self) -> None:
475
+ self.prepare_publication_candidate()
476
+ self.begin()
477
+ self.verify_all()
478
+ before = repository_snapshot(self.root)
479
+ git(self.root, "commit", "--allow-empty", "-qm", "fix: metadata-only change")
480
+ self.assertEqual(before["fingerprint"], repository_snapshot(self.root)["fingerprint"])
481
+ with self.assertRaisesRegex(ProcessError, "verification evidence is stale or incomplete"):
482
+ start_review(self.root, PROCESS_ROOT, "sample-change",
483
+ actor_id="reviewer", context_id="review-context", kind="agent")
484
+
368
485
  def test_publication_finish_records_validated_source_metadata(self) -> None:
369
486
  base = self.approve_publication_candidate(moving_base=True)
370
487
  head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=self.root, text=True, timeout=30).strip()
@@ -432,7 +549,7 @@ class LifecycleTests(unittest.TestCase):
432
549
  git(root, "update-ref", "HEAD", original)
433
550
 
434
551
  with patch("engineering_process.lifecycle.validate_current_source", side_effect=validate_other_head):
435
- with self.assertRaisesRegex(ProcessError, "does not match the reviewed HEAD"):
552
+ with self.assertRaisesRegex(ProcessError, "does not match the candidate HEAD"):
436
553
  finish_change(self.root, PROCESS_ROOT, "sample-change",
437
554
  actor_id="coordinator", context_id="finish-context", kind="agent")
438
555
  self.assertEqual(original, repository_snapshot(self.root)["head"])
@@ -0,0 +1,203 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ import subprocess
5
+ import tempfile
6
+ import unittest
7
+ from unittest.mock import patch
8
+
9
+ from engineering_process.contracts import ProcessError
10
+ from engineering_process import repository
11
+ from engineering_process.repository import (
12
+ repository_snapshot, require_committed_candidate, resolve_commit, same_checkpoint,
13
+ )
14
+
15
+
16
+ def git(root: Path, *arguments: str) -> None:
17
+ subprocess.run(["git", *arguments], cwd=root, check=True, capture_output=True)
18
+
19
+
20
+ class RepositorySnapshotTests(unittest.TestCase):
21
+ def make_repository(self, root: Path) -> None:
22
+ git(root, "init", "-q")
23
+ git(root, "config", "user.email", "tests@example.invalid")
24
+ git(root, "config", "user.name", "Tests")
25
+ (root / ".gitignore").write_text("/.process/runs/\n/.process/receipts/\n", encoding="utf-8")
26
+ (root / "tracked.txt").write_text("one\n", encoding="utf-8")
27
+ git(root, "add", ".")
28
+ git(root, "commit", "-qm", "initial")
29
+
30
+ def test_tracked_and_untracked_content_change_fingerprint(self) -> None:
31
+ with tempfile.TemporaryDirectory() as directory:
32
+ root = Path(directory)
33
+ self.make_repository(root)
34
+ initial = repository_snapshot(root)
35
+ (root / "tracked.txt").write_text("two\n", encoding="utf-8")
36
+ tracked = repository_snapshot(root)
37
+ self.assertFalse(same_checkpoint(initial, tracked))
38
+ (root / "new.txt").write_text("new\n", encoding="utf-8")
39
+ untracked = repository_snapshot(root)
40
+ self.assertFalse(same_checkpoint(tracked, untracked))
41
+
42
+ def test_resolve_commit_accepts_branches_and_peels_annotated_tags(self) -> None:
43
+ with tempfile.TemporaryDirectory() as directory:
44
+ root = Path(directory)
45
+ self.make_repository(root)
46
+ git(root, "branch", "review-base")
47
+ git(root, "tag", "-a", "review-tag", "-m", "Review boundary")
48
+ expected = repository_snapshot(root)["head"]
49
+ for reference in ("HEAD", "review-base", "review-tag"):
50
+ with self.subTest(reference=reference):
51
+ self.assertEqual(expected, resolve_commit(root, reference))
52
+
53
+ def test_lifecycle_state_does_not_invalidate_its_own_checkpoint(self) -> None:
54
+ with tempfile.TemporaryDirectory() as directory:
55
+ root = Path(directory)
56
+ self.make_repository(root)
57
+ initial = repository_snapshot(root)
58
+ state = root / ".process" / "runs" / "change" / "run.json"
59
+ state.parent.mkdir(parents=True)
60
+ state.write_text("{}\n", encoding="utf-8")
61
+ receipt = root / ".process" / "receipts" / "change.json"
62
+ receipt.parent.mkdir(parents=True)
63
+ receipt.write_text("{}\n", encoding="utf-8")
64
+ self.assertTrue(same_checkpoint(initial, repository_snapshot(root)))
65
+
66
+ def test_committed_candidate_ignores_only_lifecycle_state_and_ignored_files(self) -> None:
67
+ with tempfile.TemporaryDirectory() as directory:
68
+ root = Path(directory)
69
+ self.make_repository(root)
70
+ # State remains excluded even in consumers without matching ignore rules.
71
+ (root / ".gitignore").write_text("ignored.txt\n", encoding="utf-8")
72
+ git(root, "add", ".gitignore")
73
+ git(root, "commit", "-qm", "fix: consumer ignore rules")
74
+ for relative in (".process/runs/sample/run.json", ".process/receipts/sample.json", "ignored.txt"):
75
+ path = root / relative
76
+ path.parent.mkdir(parents=True, exist_ok=True)
77
+ path.write_text("local\n", encoding="utf-8")
78
+ require_committed_candidate(root)
79
+ (root / ".process/project.json").write_text("{}\n", encoding="utf-8")
80
+ with self.assertRaisesRegex(ProcessError, "committed candidate changes"):
81
+ require_committed_candidate(root)
82
+
83
+ def test_committed_candidate_rejects_worktree_index_and_untracked_changes(self) -> None:
84
+ for change in ("unstaged", "staged", "index-only", "untracked", "deleted", "renamed"):
85
+ with self.subTest(change=change), tempfile.TemporaryDirectory() as directory:
86
+ root = Path(directory)
87
+ self.make_repository(root)
88
+ tracked = root / "tracked.txt"
89
+ if change == "untracked":
90
+ (root / "new name-é.txt").write_text("new\n", encoding="utf-8")
91
+ elif change == "deleted":
92
+ tracked.unlink()
93
+ elif change == "renamed":
94
+ git(root, "mv", "tracked.txt", "renamed.txt")
95
+ else:
96
+ tracked.write_text("two\n", encoding="utf-8")
97
+ if change in ("staged", "index-only"):
98
+ git(root, "add", "tracked.txt")
99
+ if change == "index-only":
100
+ tracked.write_text("one\n", encoding="utf-8")
101
+ with self.assertRaisesRegex(ProcessError, "committed candidate changes.*before change verify"):
102
+ require_committed_candidate(root)
103
+
104
+ def test_committed_candidate_inspects_hidden_files_without_changing_index_flags(self) -> None:
105
+ for flag in ("--assume-unchanged", "--skip-worktree"):
106
+ with self.subTest(flag=flag), tempfile.TemporaryDirectory() as directory:
107
+ root = Path(directory)
108
+ self.make_repository(root)
109
+ git(root, "update-index", flag, "tracked.txt")
110
+ index = root / ".git/index"
111
+ before = index.read_bytes()
112
+ require_committed_candidate(root)
113
+ self.assertEqual(before, index.read_bytes())
114
+ (root / "tracked.txt").write_text("two\n", encoding="utf-8")
115
+ with self.assertRaisesRegex(ProcessError, "committed candidate changes"):
116
+ require_committed_candidate(root)
117
+ self.assertEqual(before, index.read_bytes())
118
+
119
+ def test_committed_candidate_does_not_follow_a_transient_head(self) -> None:
120
+ with tempfile.TemporaryDirectory() as directory:
121
+ root = Path(directory)
122
+ self.make_repository(root)
123
+ original = resolve_commit(root, "HEAD")
124
+ (root / "tracked.txt").write_text("two\n", encoding="utf-8")
125
+ git(root, "add", "tracked.txt")
126
+ git(root, "commit", "-qm", "fix: alternate content")
127
+ transient = resolve_commit(root, "HEAD")
128
+ git(root, "reset", "--mixed", original)
129
+ before = repository_snapshot(root)
130
+ inspect = repository._git
131
+
132
+ def change_head(path: Path, arguments: list[str], **options) -> bytes:
133
+ if "read-tree" in arguments:
134
+ git(root, "update-ref", "HEAD", transient)
135
+ try:
136
+ return inspect(path, arguments, **options)
137
+ finally:
138
+ if "status" in arguments:
139
+ git(root, "update-ref", "HEAD", original)
140
+
141
+ try:
142
+ with patch("engineering_process.repository._git", side_effect=change_head):
143
+ with self.assertRaisesRegex(ProcessError, "committed candidate changes"):
144
+ require_committed_candidate(root)
145
+ finally:
146
+ git(root, "update-ref", "HEAD", original)
147
+ self.assertTrue(same_checkpoint(before, repository_snapshot(root)))
148
+
149
+ def test_committed_candidate_preserves_sparse_omissions_but_inspects_present_files(self) -> None:
150
+ for mode in ("--no-sparse-index", "--sparse-index"):
151
+ with self.subTest(mode=mode), tempfile.TemporaryDirectory() as directory:
152
+ root = Path(directory)
153
+ self.make_repository(root)
154
+ for relative in ("selected/kept.txt", "omitted/absent.txt"):
155
+ path = root / relative
156
+ path.parent.mkdir(parents=True)
157
+ path.write_text("committed\n", encoding="utf-8")
158
+ git(root, "add", ".")
159
+ git(root, "commit", "-qm", "fix: sparse fixture")
160
+ git(root, "sparse-checkout", "init", "--cone", mode)
161
+ git(root, "sparse-checkout", "set", "selected")
162
+ absent = root / "omitted/absent.txt"
163
+ self.assertFalse(absent.exists())
164
+ index = root / ".git/index"
165
+ before = index.read_bytes()
166
+ require_committed_candidate(root)
167
+ self.assertEqual(before, index.read_bytes())
168
+ absent.parent.mkdir(exist_ok=True)
169
+ absent.write_text("uncommitted\n", encoding="utf-8")
170
+ with self.assertRaisesRegex(ProcessError, "committed candidate changes"):
171
+ require_committed_candidate(root)
172
+ self.assertEqual(before, index.read_bytes())
173
+
174
+ def test_repository_local_temporary_root_does_not_join_the_candidate(self) -> None:
175
+ with tempfile.TemporaryDirectory() as directory:
176
+ root = Path(directory)
177
+ self.make_repository(root)
178
+ before = repository_snapshot(root)
179
+ index = (root / ".git/index").read_bytes()
180
+ with patch("engineering_process.repository.tempfile.tempdir", str(root)):
181
+ require_committed_candidate(root)
182
+ self.assertTrue(same_checkpoint(before, repository_snapshot(root)))
183
+ self.assertEqual(index, (root / ".git/index").read_bytes())
184
+ self.assertEqual([], list((root / ".git").glob("process-candidate-*")))
185
+
186
+ def test_committed_candidate_supports_linked_worktrees(self) -> None:
187
+ with tempfile.TemporaryDirectory() as directory:
188
+ root = Path(directory) / "source"
189
+ root.mkdir()
190
+ self.make_repository(root)
191
+ linked = Path(directory) / "linked"
192
+ git(root, "worktree", "add", "--detach", str(linked), "HEAD")
193
+ before = repository_snapshot(linked)
194
+ with patch("engineering_process.repository.tempfile.tempdir", str(linked)):
195
+ require_committed_candidate(linked)
196
+ self.assertTrue(same_checkpoint(before, repository_snapshot(linked)))
197
+ (linked / "tracked.txt").write_text("uncommitted\n", encoding="utf-8")
198
+ with self.assertRaisesRegex(ProcessError, "committed candidate changes"):
199
+ require_committed_candidate(linked)
200
+
201
+
202
+ if __name__ == "__main__":
203
+ unittest.main()
@@ -1,18 +0,0 @@
1
- # Engineering Process v2.3.1
2
-
3
- Changes since v2.3.0.
4
-
5
- ## Fixes
6
-
7
- - Refresh package metadata when installing the hash\-locked process so newly published releases remain visible through warm pip caches\; remove the publication\-time cache expiry wait\. Consumer CI using pip 26\.2 or newer should pass \-\-refresh\-package engineering\-process\, or use \-\-no\-cache\-dir with older pip\. ([#134](https://github.com/phuongnse/engineering-process/issues/134))
8
- - Reject recorded agent reviewer\-context reuse across changes in the same repository and its worktrees\. Preserve same\-change correction reviews\, and provide an audited replacement only for a reused initial assignment with no submitted review\. Native fresh\-session and unchanged model\/effort evidence remain required from the host\. ([#182](https://github.com/phuongnse/engineering-process/issues/182))
9
-
10
- ## Upgrade and compatibility
11
-
12
- Merge the complete hash-locked package/adoption PR, update the local and CI environments to the selected version, and start a fresh agent session.
13
-
14
- Consumer CI, naming conventions and branch-protection settings remain consumer-owned; adoption does not configure them automatically.
15
-
16
- See [versioning and compatibility](https://github.com/phuongnse/engineering-process/blob/v2.3.1/VERSIONING.md) and [adoption guidance](https://github.com/phuongnse/engineering-process/blob/v2.3.1/SELF_HOSTING.md).
17
-
18
- [Full change comparison](https://github.com/phuongnse/engineering-process/compare/v2.3.0...v2.3.1)
@@ -1,63 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
- import subprocess
5
- import tempfile
6
- import unittest
7
-
8
- from engineering_process.repository import repository_snapshot, resolve_commit, same_checkpoint
9
-
10
-
11
- def git(root: Path, *arguments: str) -> None:
12
- subprocess.run(["git", *arguments], cwd=root, check=True, capture_output=True)
13
-
14
-
15
- class RepositorySnapshotTests(unittest.TestCase):
16
- def make_repository(self, root: Path) -> None:
17
- git(root, "init", "-q")
18
- git(root, "config", "user.email", "tests@example.invalid")
19
- git(root, "config", "user.name", "Tests")
20
- (root / ".gitignore").write_text("/.process/runs/\n/.process/receipts/\n", encoding="utf-8")
21
- (root / "tracked.txt").write_text("one\n", encoding="utf-8")
22
- git(root, "add", ".")
23
- git(root, "commit", "-qm", "initial")
24
-
25
- def test_tracked_and_untracked_content_change_fingerprint(self) -> None:
26
- with tempfile.TemporaryDirectory() as directory:
27
- root = Path(directory)
28
- self.make_repository(root)
29
- initial = repository_snapshot(root)
30
- (root / "tracked.txt").write_text("two\n", encoding="utf-8")
31
- tracked = repository_snapshot(root)
32
- self.assertFalse(same_checkpoint(initial, tracked))
33
- (root / "new.txt").write_text("new\n", encoding="utf-8")
34
- untracked = repository_snapshot(root)
35
- self.assertFalse(same_checkpoint(tracked, untracked))
36
-
37
- def test_resolve_commit_accepts_branches_and_peels_annotated_tags(self) -> None:
38
- with tempfile.TemporaryDirectory() as directory:
39
- root = Path(directory)
40
- self.make_repository(root)
41
- git(root, "branch", "review-base")
42
- git(root, "tag", "-a", "review-tag", "-m", "Review boundary")
43
- expected = repository_snapshot(root)["head"]
44
- for reference in ("HEAD", "review-base", "review-tag"):
45
- with self.subTest(reference=reference):
46
- self.assertEqual(expected, resolve_commit(root, reference))
47
-
48
- def test_lifecycle_state_does_not_invalidate_its_own_checkpoint(self) -> None:
49
- with tempfile.TemporaryDirectory() as directory:
50
- root = Path(directory)
51
- self.make_repository(root)
52
- initial = repository_snapshot(root)
53
- state = root / ".process" / "runs" / "change" / "run.json"
54
- state.parent.mkdir(parents=True)
55
- state.write_text("{}\n", encoding="utf-8")
56
- receipt = root / ".process" / "receipts" / "change.json"
57
- receipt.parent.mkdir(parents=True)
58
- receipt.write_text("{}\n", encoding="utf-8")
59
- self.assertTrue(same_checkpoint(initial, repository_snapshot(root)))
60
-
61
-
62
- if __name__ == "__main__":
63
- unittest.main()