engineering-process 1.2.1__tar.gz → 1.2.3__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 (76) hide show
  1. {engineering_process-1.2.1/engineering_process.egg-info → engineering_process-1.2.3}/PKG-INFO +5 -1
  2. {engineering_process-1.2.1 → engineering_process-1.2.3}/README.md +4 -0
  3. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/__init__.py +1 -1
  4. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/publication_compat.py +6 -1
  5. {engineering_process-1.2.1 → engineering_process-1.2.3/engineering_process.egg-info}/PKG-INFO +5 -1
  6. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process.egg-info/SOURCES.txt +1 -0
  7. {engineering_process-1.2.1 → engineering_process-1.2.3}/process_assets/skills/finish-change/SKILL.md +6 -0
  8. {engineering_process-1.2.1 → engineering_process-1.2.3}/pyproject.toml +1 -1
  9. {engineering_process-1.2.1 → engineering_process-1.2.3}/templates/PULL_REQUEST_TEMPLATE.md +2 -0
  10. {engineering_process-1.2.1 → engineering_process-1.2.3}/tests/test_automation.py +29 -0
  11. {engineering_process-1.2.1 → engineering_process-1.2.3}/tests/test_publication_compat.py +36 -0
  12. engineering_process-1.2.3/tests/test_pypi_cache_horizon.py +81 -0
  13. {engineering_process-1.2.1 → engineering_process-1.2.3}/tests/test_skills.py +2 -0
  14. {engineering_process-1.2.1 → engineering_process-1.2.3}/LICENSE +0 -0
  15. {engineering_process-1.2.1 → engineering_process-1.2.3}/MANIFEST.in +0 -0
  16. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/__main__.py +0 -0
  17. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/_supervisor_contract.py +0 -0
  18. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/_supervisor_posix.py +0 -0
  19. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/_supervisor_windows.py +0 -0
  20. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/_windows_job.py +0 -0
  21. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/adoption.py +0 -0
  22. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/cli.py +0 -0
  23. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/commands.py +0 -0
  24. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/contracts.py +0 -0
  25. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/distribution.py +0 -0
  26. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/helper_launch.py +0 -0
  27. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/lifecycle.py +0 -0
  28. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/production_engineering.py +0 -0
  29. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/project.py +0 -0
  30. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/release.py +0 -0
  31. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/repository.py +0 -0
  32. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/requirements-dev.txt +0 -0
  33. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/requirements-runtime.txt +0 -0
  34. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/skills.py +0 -0
  35. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process/supervision.py +0 -0
  36. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process.egg-info/dependency_links.txt +0 -0
  37. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process.egg-info/entry_points.txt +0 -0
  38. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process.egg-info/requires.txt +0 -0
  39. {engineering_process-1.2.1 → engineering_process-1.2.3}/engineering_process.egg-info/top_level.txt +0 -0
  40. {engineering_process-1.2.1 → engineering_process-1.2.3}/process-graph.json +0 -0
  41. {engineering_process-1.2.1 → engineering_process-1.2.3}/process_assets/skills/implement-change/SKILL.md +0 -0
  42. {engineering_process-1.2.1 → engineering_process-1.2.3}/process_assets/skills/improve-process/SKILL.md +0 -0
  43. {engineering_process-1.2.1 → engineering_process-1.2.3}/process_assets/skills/plan-change/SKILL.md +0 -0
  44. {engineering_process-1.2.1 → engineering_process-1.2.3}/process_assets/skills/production-engineering/SKILL.md +0 -0
  45. {engineering_process-1.2.1 → engineering_process-1.2.3}/process_assets/skills/production-engineering/invariants.json +0 -0
  46. {engineering_process-1.2.1 → engineering_process-1.2.3}/process_assets/skills/review-change/SKILL.md +0 -0
  47. {engineering_process-1.2.1 → engineering_process-1.2.3}/process_assets/skills/run-change/SKILL.md +0 -0
  48. {engineering_process-1.2.1 → engineering_process-1.2.3}/process_assets/skills/start-change/SKILL.md +0 -0
  49. {engineering_process-1.2.1 → engineering_process-1.2.3}/process_assets/skills/verify-change/SKILL.md +0 -0
  50. {engineering_process-1.2.1 → engineering_process-1.2.3}/schemas/change.schema.json +0 -0
  51. {engineering_process-1.2.1 → engineering_process-1.2.3}/schemas/plan.schema.json +0 -0
  52. {engineering_process-1.2.1 → engineering_process-1.2.3}/schemas/process-graph.schema.json +0 -0
  53. {engineering_process-1.2.1 → engineering_process-1.2.3}/schemas/process-lock.schema.json +0 -0
  54. {engineering_process-1.2.1 → engineering_process-1.2.3}/schemas/production-engineering.schema.json +0 -0
  55. {engineering_process-1.2.1 → engineering_process-1.2.3}/schemas/project-legacy.schema.json +0 -0
  56. {engineering_process-1.2.1 → engineering_process-1.2.3}/schemas/project.schema.json +0 -0
  57. {engineering_process-1.2.1 → engineering_process-1.2.3}/schemas/receipt.schema.json +0 -0
  58. {engineering_process-1.2.1 → engineering_process-1.2.3}/schemas/release-change.schema.json +0 -0
  59. {engineering_process-1.2.1 → engineering_process-1.2.3}/schemas/release.schema.json +0 -0
  60. {engineering_process-1.2.1 → engineering_process-1.2.3}/schemas/review.schema.json +0 -0
  61. {engineering_process-1.2.1 → engineering_process-1.2.3}/schemas/run.schema.json +0 -0
  62. {engineering_process-1.2.1 → engineering_process-1.2.3}/setup.cfg +0 -0
  63. {engineering_process-1.2.1 → engineering_process-1.2.3}/templates/AGENTS.process.md +0 -0
  64. {engineering_process-1.2.1 → engineering_process-1.2.3}/templates/adopt-process-windows-job.py +0 -0
  65. {engineering_process-1.2.1 → engineering_process-1.2.3}/templates/adopt-process.py +0 -0
  66. {engineering_process-1.2.1 → engineering_process-1.2.3}/tests/test_adoption.py +0 -0
  67. {engineering_process-1.2.1 → engineering_process-1.2.3}/tests/test_architecture.py +0 -0
  68. {engineering_process-1.2.1 → engineering_process-1.2.3}/tests/test_cli.py +0 -0
  69. {engineering_process-1.2.1 → engineering_process-1.2.3}/tests/test_commands.py +0 -0
  70. {engineering_process-1.2.1 → engineering_process-1.2.3}/tests/test_contracts.py +0 -0
  71. {engineering_process-1.2.1 → engineering_process-1.2.3}/tests/test_distribution.py +0 -0
  72. {engineering_process-1.2.1 → engineering_process-1.2.3}/tests/test_lifecycle.py +0 -0
  73. {engineering_process-1.2.1 → engineering_process-1.2.3}/tests/test_production_engineering.py +0 -0
  74. {engineering_process-1.2.1 → engineering_process-1.2.3}/tests/test_release.py +0 -0
  75. {engineering_process-1.2.1 → engineering_process-1.2.3}/tests/test_repository.py +0 -0
  76. {engineering_process-1.2.1 → engineering_process-1.2.3}/tests/test_supervisor_posix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-process
3
- Version: 1.2.1
3
+ Version: 1.2.3
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
@@ -422,6 +422,10 @@ review identities.
422
422
  It rejects missing, repeated, misplaced, hidden, unordered, or unsupported visible
423
423
  structure. Completion checkboxes belong only to the Completion gate section. Ready
424
424
  pull requests must have every checkbox checked; drafts may retain unchecked work.
425
+ One trailing `Refs ISSUE.` line remains optional. A ready, contract-identified final
426
+ consumer adoption may instead use `Closes ISSUE, closes OWNER/REPOSITORY#NUMBER.` with
427
+ the complete keyword/reference syntax repeated for every issue; drafts cannot close
428
+ issues. Producer and intermediate pull requests do not close release-source issues.
425
429
  The managed template never solicits execution identity,
426
430
  and authors plus independent review keep it out of free-form values. The validator is
427
431
  a positive grammar for public fields; it deliberately does not guess identities from
@@ -398,6 +398,10 @@ review identities.
398
398
  It rejects missing, repeated, misplaced, hidden, unordered, or unsupported visible
399
399
  structure. Completion checkboxes belong only to the Completion gate section. Ready
400
400
  pull requests must have every checkbox checked; drafts may retain unchecked work.
401
+ One trailing `Refs ISSUE.` line remains optional. A ready, contract-identified final
402
+ consumer adoption may instead use `Closes ISSUE, closes OWNER/REPOSITORY#NUMBER.` with
403
+ the complete keyword/reference syntax repeated for every issue; drafts cannot close
404
+ issues. Producer and intermediate pull requests do not close release-source issues.
401
405
  The managed template never solicits execution identity,
402
406
  and authors plus independent review keep it out of free-form values. The validator is
403
407
  a positive grammar for public fields; it deliberately does not guess identities from
@@ -1,3 +1,3 @@
1
1
  """Agent-neutral engineering process."""
2
2
 
3
- VERSION = "1.2.1"
3
+ VERSION = "1.2.3"
@@ -53,8 +53,9 @@ PR_CHECKS = (
53
53
  )
54
54
  FENCE = re.compile(r"^ {0,3}(`{3,}|~{3,})(.*)$")
55
55
  NON_MARKDOWN_LINE_SEPARATOR = re.compile("[\v\f\x85\u2028\u2029]")
56
+ ISSUE_TARGET = r"(?:[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)?#[1-9][0-9]*"
56
57
  ISSUE_REFERENCE = re.compile(
57
- r"^Refs (?:[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)?#[1-9][0-9]*\.$"
58
+ rf"^(?:Refs {ISSUE_TARGET}|Closes {ISSUE_TARGET}(?:, closes {ISSUE_TARGET})*)\.$"
58
59
  )
59
60
 
60
61
 
@@ -232,6 +233,10 @@ def _pull_request_body_issues(body: str, state: str) -> list[str]:
232
233
  and reference_positions[0] <= checklist_positions[-1]
233
234
  ):
234
235
  issues.append("pull request body issue reference must follow the checklist")
236
+ if state == "draft" and any(
237
+ lines[position].startswith("Closes ") for position in reference_positions
238
+ ):
239
+ issues.append("draft pull request cannot close issues")
235
240
  field_patterns = tuple(
236
241
  re.compile(rf"^- {re.escape(field)}:\s*.*$")
237
242
  for fields in PR_FIELDS.values()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-process
3
- Version: 1.2.1
3
+ Version: 1.2.3
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
@@ -422,6 +422,10 @@ review identities.
422
422
  It rejects missing, repeated, misplaced, hidden, unordered, or unsupported visible
423
423
  structure. Completion checkboxes belong only to the Completion gate section. Ready
424
424
  pull requests must have every checkbox checked; drafts may retain unchecked work.
425
+ One trailing `Refs ISSUE.` line remains optional. A ready, contract-identified final
426
+ consumer adoption may instead use `Closes ISSUE, closes OWNER/REPOSITORY#NUMBER.` with
427
+ the complete keyword/reference syntax repeated for every issue; drafts cannot close
428
+ issues. Producer and intermediate pull requests do not close release-source issues.
425
429
  The managed template never solicits execution identity,
426
430
  and authors plus independent review keep it out of free-form values. The validator is
427
431
  a positive grammar for public fields; it deliberately does not guess identities from
@@ -67,6 +67,7 @@ tests/test_distribution.py
67
67
  tests/test_lifecycle.py
68
68
  tests/test_production_engineering.py
69
69
  tests/test_publication_compat.py
70
+ tests/test_pypi_cache_horizon.py
70
71
  tests/test_release.py
71
72
  tests/test_repository.py
72
73
  tests/test_skills.py
@@ -15,6 +15,12 @@ The command writes one bounded completion receipt and marks the run completed.
15
15
  Completion does not itself grant merge, deployment, or release authority; those
16
16
  remain project-owned operations. Never report completion from prose alone.
17
17
 
18
+ Use a normal trailing `Refs ISSUE.` line for producer and intermediate pull requests.
19
+ Only a contract-identified final consumer adoption that has verified the released
20
+ behavior may use `Closes ISSUE, closes ISSUE.` after the completed public checklist.
21
+ Repeat the full keyword and local or `OWNER/REPOSITORY#NUMBER` reference for every
22
+ issue; never close source issues merely because the producer release merged.
23
+
18
24
  Report the readiness capabilities protected or advanced and the remaining planned
19
25
  gaps after completion. Carry the owner and stable record URL for every accepted-risk
20
26
  or tracked-follow-up disposition into the durable handoff. Finish never edits
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "engineering-process"
7
- version = "1.2.1"
7
+ version = "1.2.3"
8
8
  description = "A small agent-neutral engineering lifecycle with managed adoption"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -34,4 +34,6 @@ tracked-follow-up entries need an owner and durable HTTPS record. -->
34
34
  - [ ] Required profiles pass on the reviewed snapshot.
35
35
  - [ ] Independent review approved with no blocking finding.
36
36
  - [ ] Every non-blocking finding has a recorded disposition.
37
+ <!-- Producer/intermediate PR: Refs ISSUE. Final verified consumer adoption only:
38
+ Closes ISSUE, closes OWNER/REPOSITORY#NUMBER. Repeat the keyword for every issue. -->
37
39
  <!-- engineering-process:pr-description:end -->
@@ -55,6 +55,8 @@ class AutomationTests(unittest.TestCase):
55
55
  template.index("## Completion gate"),
56
56
  )
57
57
  self.assertIn("Every non-blocking finding has a recorded disposition.", template)
58
+ self.assertIn("Final verified consumer adoption only", template)
59
+ self.assertIn("Closes ISSUE, closes OWNER/REPOSITORY#NUMBER", template)
58
60
 
59
61
  def test_renovate_process_rule_materializes_the_complete_adoption(self) -> None:
60
62
  config = json.loads((ROOT / ".github" / "renovate.json").read_text(encoding="utf-8"))
@@ -103,6 +105,31 @@ class AutomationTests(unittest.TestCase):
103
105
  " dispatch-adoption:\n name: Dispatch consumer adoption\n", workflow
104
106
  )
105
107
  self.assertIn("needs: [metadata, publish]", workflow)
108
+ dispatch_job = workflow.split("\n dispatch-adoption:\n", maxsplit=1)[1]
109
+ self.assertIn(" timeout-minutes: 20\n", dispatch_job)
110
+ cache_wait = dispatch_job.split(
111
+ " - name: Wait for consumer Simple caches to expire\n", maxsplit=1
112
+ )[1].split(
113
+ " - name: Create short-lived Renovate event token\n", maxsplit=1
114
+ )[0]
115
+ self.assertIn(
116
+ "python verification/wait_for_pypi_cache_horizon.py",
117
+ cache_wait,
118
+ )
119
+ self.assertIn("${{ steps.release.outputs.published_at }}", cache_wait)
120
+ self.assertIn(
121
+ "ref: ${{ github.sha }}",
122
+ dispatch_job,
123
+ )
124
+ self.assertNotIn("ref: ${{ needs.metadata.outputs.source_sha }}", dispatch_job)
125
+ self.assertIn(
126
+ 'published_at=$(gh release view "$RELEASE_TAG"',
127
+ dispatch_job,
128
+ )
129
+ self.assertLess(
130
+ dispatch_job.index(" - name: Bind the published distribution\n"),
131
+ dispatch_job.index(" - name: Wait for consumer Simple caches to expire\n"),
132
+ )
106
133
  self.assertIn("Verify PyPI exposes the exact built hashes", workflow)
107
134
  visibility = workflow.split(
108
135
  " - name: Verify PyPI exposes the exact built hashes\n", maxsplit=1
@@ -140,6 +167,8 @@ class AutomationTests(unittest.TestCase):
140
167
  publish_job = workflow.split(" publish:\n", maxsplit=1)[1].split(
141
168
  "\n dispatch-adoption:\n", maxsplit=1
142
169
  )[0]
170
+ self.assertIn("ref: ${{ needs.metadata.outputs.source_sha }}", publish_job)
171
+ self.assertNotIn("ref: ${{ github.sha }}", publish_job)
143
172
  self.assertIn("id: release-token", publish_job)
144
173
  self.assertIn("repositories: engineering-process", publish_job)
145
174
  self.assertIn("permission-contents: write", publish_job)
@@ -290,6 +290,42 @@ class PublicationCompatibilityTests(unittest.TestCase):
290
290
  self.validate_body(repeated),
291
291
  )
292
292
 
293
+ def test_final_adoption_can_close_multiple_issues_only_when_ready(self) -> None:
294
+ closing = (
295
+ f"{CANONICAL_BODY}\n"
296
+ "Closes example/process#123, closes #456, closes other/repository#789.\n"
297
+ )
298
+ self.assertEqual([], self.validate_body(closing))
299
+ self.assertIn(
300
+ "draft pull request cannot close issues",
301
+ self.validate_body(closing, state="draft"),
302
+ )
303
+
304
+ unchecked = closing.replace(
305
+ "- [x] Required profiles", "- [ ] Required profiles", 1
306
+ )
307
+ self.assertIn(
308
+ "ready pull request has unchecked item: Required profiles pass on the reviewed snapshot.",
309
+ self.validate_body(unchecked),
310
+ )
311
+
312
+ for malformed in (
313
+ "Closes example/process#123, #456.",
314
+ "Closes example/process#123, Closes #456.",
315
+ "Fixes example/process#123.",
316
+ "Closes example/process#0.",
317
+ ):
318
+ with self.subTest(malformed=malformed):
319
+ issues = self.validate_body(f"{CANONICAL_BODY}\n{malformed}\n")
320
+ self.assertTrue(
321
+ any(
322
+ issue.startswith(
323
+ "pull request body has unsupported visible content at line "
324
+ )
325
+ for issue in issues
326
+ )
327
+ )
328
+
293
329
  def test_pull_request_ignores_template_comments(self) -> None:
294
330
  body = CANONICAL_BODY.replace(
295
331
  "- Outcome: Preserve the requested behavior.",
@@ -0,0 +1,81 @@
1
+ from datetime import datetime, timezone
2
+ from email.message import Message
3
+ import unittest
4
+ from unittest.mock import patch
5
+
6
+ from verification.wait_for_pypi_cache_horizon import (
7
+ MAX_SIMPLE_CACHE_SECONDS,
8
+ read_simple_max_age,
9
+ parse_max_age,
10
+ remaining_seconds,
11
+ )
12
+
13
+
14
+ class _Response:
15
+ def __init__(self, headers: Message) -> None:
16
+ self.headers = headers
17
+
18
+ def __enter__(self) -> "_Response":
19
+ return self
20
+
21
+ def __exit__(self, *args: object) -> None:
22
+ return None
23
+
24
+
25
+ class PyPICacheHorizonTests(unittest.TestCase):
26
+ NOW = datetime(2026, 9, 4, 13, 30, tzinfo=timezone.utc)
27
+
28
+ def test_parses_current_bounded_header(self) -> None:
29
+ self.assertEqual(600, parse_max_age(["max-age=600, public"]))
30
+ self.assertEqual(
31
+ MAX_SIMPLE_CACHE_SECONDS,
32
+ parse_max_age([f"public, max-age={MAX_SIMPLE_CACHE_SECONDS}"]),
33
+ )
34
+
35
+ def test_rejects_missing_duplicate_malformed_and_excessive_max_age(self) -> None:
36
+ invalid = [
37
+ None,
38
+ ["public"],
39
+ ["max-age=600", "max-age=300"],
40
+ ["max-age=-1"],
41
+ ["max-age=invalid"],
42
+ [f"max-age={MAX_SIMPLE_CACHE_SECONDS + 1}"],
43
+ ]
44
+ for values in invalid:
45
+ with self.subTest(values=values), self.assertRaises(RuntimeError):
46
+ parse_max_age(values)
47
+
48
+ def test_reads_every_cache_control_field(self) -> None:
49
+ headers = Message()
50
+ headers["Content-Type"] = "application/vnd.pypi.simple.v1+json"
51
+ headers["Cache-Control"] = "public, max-age=600"
52
+ headers["Cache-Control"] = "max-age=300"
53
+ with patch(
54
+ "verification.wait_for_pypi_cache_horizon.urlopen",
55
+ return_value=_Response(headers),
56
+ ), self.assertRaises(RuntimeError):
57
+ read_simple_max_age()
58
+
59
+ def test_current_horizon_and_elapsed_resume(self) -> None:
60
+ self.assertEqual(
61
+ 305,
62
+ remaining_seconds("2026-09-04T13:20:00Z", 900, now=self.NOW),
63
+ )
64
+ self.assertEqual(
65
+ 0,
66
+ remaining_seconds("2026-09-04T13:00:00Z", 600, now=self.NOW),
67
+ )
68
+
69
+ def test_rejects_malformed_naive_and_future_timestamps(self) -> None:
70
+ invalid = [
71
+ "invalid",
72
+ "2026-09-04T13:20:00",
73
+ "2026-09-04T13:31:01Z",
74
+ ]
75
+ for published_at in invalid:
76
+ with self.subTest(published_at=published_at), self.assertRaises(RuntimeError):
77
+ remaining_seconds(published_at, 600, now=self.NOW)
78
+
79
+
80
+ if __name__ == "__main__":
81
+ unittest.main()
@@ -78,6 +78,8 @@ class SkillTests(unittest.TestCase):
78
78
  "Finish never edits\nreadiness",
79
79
  "remaining planned\ngaps",
80
80
  "owner and stable record URL",
81
+ "contract-identified final consumer adoption",
82
+ "Closes ISSUE, closes ISSUE",
81
83
  ),
82
84
  "improve-process": (
83
85
  "new immutable pack version",