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.
- {engineering_process-2.3.1/engineering_process.egg-info → engineering_process-2.4.0}/PKG-INFO +9 -5
- {engineering_process-2.3.1 → engineering_process-2.4.0}/README.md +8 -4
- engineering_process-2.4.0/RELEASE_NOTES.md +22 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/__init__.py +1 -1
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/lifecycle.py +35 -20
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/repository.py +49 -1
- {engineering_process-2.3.1 → engineering_process-2.4.0/engineering_process.egg-info}/PKG-INFO +9 -5
- {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/change-implement/SKILL.md +18 -2
- {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/change-plan/SKILL.md +15 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/change-review/SKILL.md +18 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/change-start/SKILL.md +7 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/change-verify/SKILL.md +8 -2
- {engineering_process-2.3.1 → engineering_process-2.4.0}/pyproject.toml +1 -1
- {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_lifecycle.py +118 -1
- engineering_process-2.4.0/tests/test_repository.py +203 -0
- engineering_process-2.3.1/RELEASE_NOTES.md +0 -18
- engineering_process-2.3.1/tests/test_repository.py +0 -63
- {engineering_process-2.3.1 → engineering_process-2.4.0}/LICENSE +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/MANIFEST.in +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/__main__.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/_supervisor_contract.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/_supervisor_posix.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/_supervisor_windows.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/_windows_job.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/adoption.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/artifact_standards.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/automation_name.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/cli.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/commands.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/contracts.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/distribution.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/helper_launch.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/issue.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/pr_description.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/production_engineering.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/project.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/publication_compat.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/release.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/release_notes.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/requirements-dev.txt +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/requirements-runtime.txt +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/review_contexts.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/skills.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/source_publication.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/supervision.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process.egg-info/SOURCES.txt +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process.egg-info/dependency_links.txt +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process.egg-info/entry_points.txt +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process.egg-info/requires.txt +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process.egg-info/top_level.txt +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/process-graph.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/change-complete/SKILL.md +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/deliver-change/SKILL.md +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/process-improve/SKILL.md +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/production-engineering/SKILL.md +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/production-engineering/invariants.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/standards/automation-name.v1.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/standards/issue.v1.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/standards/pull-request.v1.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/standards/release-notes.v1.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/artifact-selection.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/artifact-standard.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/automation-name-data.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/change.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/issue-data.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/plan.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/pr-description-data.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/process-graph.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/process-lock.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/production-engineering.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/project-legacy.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/project.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/receipt.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/release-change.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/release-notes-data.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/release.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/renovate-preset.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/review.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/run.schema.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/setup.cfg +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/templates/AGENTS.process.md +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/templates/PULL_REQUEST_TEMPLATE.md +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/templates/adopt-process-windows-job.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/templates/adopt-process.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/templates/renovate.json +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_adoption.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_architecture.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_artifact_standards.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_automation.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_cli.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_commands.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_contracts.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_distribution.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_production_engineering.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_publication_compat.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_publication_workflow.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_pypi_cache.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_release.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_review_contexts.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_skills.py +0 -0
- {engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_supervisor_posix.py +0 -0
{engineering_process-2.3.1/engineering_process.egg-info → engineering_process-2.4.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: engineering-process
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: A small agent-neutral engineering lifecycle with managed adoption
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/phuongnse/engineering-process
|
|
@@ -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.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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)
|
|
@@ -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
|
|
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 =
|
|
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(
|
|
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():
|
{engineering_process-2.3.1 → engineering_process-2.4.0/engineering_process.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: engineering-process
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: A small agent-neutral engineering lifecycle with managed adoption
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/phuongnse/engineering-process
|
|
@@ -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.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
17
|
-
|
|
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
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/change-plan/SKILL.md
RENAMED
|
@@ -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
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/change-review/SKILL.md
RENAMED
|
@@ -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
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/change-start/SKILL.md
RENAMED
|
@@ -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.
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/skills/change-verify/SKILL.md
RENAMED
|
@@ -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.
|
|
9
|
-
|
|
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
|
|
|
@@ -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
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/_supervisor_contract.py
RENAMED
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/_supervisor_posix.py
RENAMED
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/_supervisor_windows.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/artifact_standards.py
RENAMED
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/automation_name.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/helper_launch.py
RENAMED
|
File without changes
|
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/pr_description.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/publication_compat.py
RENAMED
|
File without changes
|
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/release_notes.py
RENAMED
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/requirements-dev.txt
RENAMED
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/requirements-runtime.txt
RENAMED
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/review_contexts.py
RENAMED
|
File without changes
|
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process/source_publication.py
RENAMED
|
File without changes
|
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process.egg-info/requires.txt
RENAMED
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/engineering_process.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/process_assets/standards/issue.v1.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/artifact-selection.schema.json
RENAMED
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/artifact-standard.schema.json
RENAMED
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/automation-name-data.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/pr-description-data.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/production-engineering.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/schemas/release-notes-data.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/templates/adopt-process-windows-job.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{engineering_process-2.3.1 → engineering_process-2.4.0}/tests/test_production_engineering.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|