engineering-process 1.2.3__tar.gz → 1.2.5__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.3/engineering_process.egg-info → engineering_process-1.2.5}/PKG-INFO +13 -1
  2. {engineering_process-1.2.3 → engineering_process-1.2.5}/README.md +12 -0
  3. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/__init__.py +1 -1
  4. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/production_engineering.py +6 -1
  5. {engineering_process-1.2.3 → engineering_process-1.2.5/engineering_process.egg-info}/PKG-INFO +13 -1
  6. {engineering_process-1.2.3 → engineering_process-1.2.5}/process_assets/skills/production-engineering/SKILL.md +5 -0
  7. {engineering_process-1.2.3 → engineering_process-1.2.5}/process_assets/skills/review-change/SKILL.md +25 -0
  8. {engineering_process-1.2.3 → engineering_process-1.2.5}/pyproject.toml +1 -1
  9. {engineering_process-1.2.3 → engineering_process-1.2.5}/tests/test_lifecycle.py +65 -0
  10. {engineering_process-1.2.3 → engineering_process-1.2.5}/tests/test_production_engineering.py +25 -0
  11. {engineering_process-1.2.3 → engineering_process-1.2.5}/tests/test_release.py +43 -0
  12. {engineering_process-1.2.3 → engineering_process-1.2.5}/LICENSE +0 -0
  13. {engineering_process-1.2.3 → engineering_process-1.2.5}/MANIFEST.in +0 -0
  14. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/__main__.py +0 -0
  15. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/_supervisor_contract.py +0 -0
  16. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/_supervisor_posix.py +0 -0
  17. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/_supervisor_windows.py +0 -0
  18. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/_windows_job.py +0 -0
  19. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/adoption.py +0 -0
  20. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/cli.py +0 -0
  21. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/commands.py +0 -0
  22. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/contracts.py +0 -0
  23. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/distribution.py +0 -0
  24. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/helper_launch.py +0 -0
  25. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/lifecycle.py +0 -0
  26. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/project.py +0 -0
  27. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/publication_compat.py +0 -0
  28. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/release.py +0 -0
  29. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/repository.py +0 -0
  30. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/requirements-dev.txt +0 -0
  31. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/requirements-runtime.txt +0 -0
  32. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/skills.py +0 -0
  33. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process/supervision.py +0 -0
  34. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process.egg-info/SOURCES.txt +0 -0
  35. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process.egg-info/dependency_links.txt +0 -0
  36. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process.egg-info/entry_points.txt +0 -0
  37. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process.egg-info/requires.txt +0 -0
  38. {engineering_process-1.2.3 → engineering_process-1.2.5}/engineering_process.egg-info/top_level.txt +0 -0
  39. {engineering_process-1.2.3 → engineering_process-1.2.5}/process-graph.json +0 -0
  40. {engineering_process-1.2.3 → engineering_process-1.2.5}/process_assets/skills/finish-change/SKILL.md +0 -0
  41. {engineering_process-1.2.3 → engineering_process-1.2.5}/process_assets/skills/implement-change/SKILL.md +0 -0
  42. {engineering_process-1.2.3 → engineering_process-1.2.5}/process_assets/skills/improve-process/SKILL.md +0 -0
  43. {engineering_process-1.2.3 → engineering_process-1.2.5}/process_assets/skills/plan-change/SKILL.md +0 -0
  44. {engineering_process-1.2.3 → engineering_process-1.2.5}/process_assets/skills/production-engineering/invariants.json +0 -0
  45. {engineering_process-1.2.3 → engineering_process-1.2.5}/process_assets/skills/run-change/SKILL.md +0 -0
  46. {engineering_process-1.2.3 → engineering_process-1.2.5}/process_assets/skills/start-change/SKILL.md +0 -0
  47. {engineering_process-1.2.3 → engineering_process-1.2.5}/process_assets/skills/verify-change/SKILL.md +0 -0
  48. {engineering_process-1.2.3 → engineering_process-1.2.5}/schemas/change.schema.json +0 -0
  49. {engineering_process-1.2.3 → engineering_process-1.2.5}/schemas/plan.schema.json +0 -0
  50. {engineering_process-1.2.3 → engineering_process-1.2.5}/schemas/process-graph.schema.json +0 -0
  51. {engineering_process-1.2.3 → engineering_process-1.2.5}/schemas/process-lock.schema.json +0 -0
  52. {engineering_process-1.2.3 → engineering_process-1.2.5}/schemas/production-engineering.schema.json +0 -0
  53. {engineering_process-1.2.3 → engineering_process-1.2.5}/schemas/project-legacy.schema.json +0 -0
  54. {engineering_process-1.2.3 → engineering_process-1.2.5}/schemas/project.schema.json +0 -0
  55. {engineering_process-1.2.3 → engineering_process-1.2.5}/schemas/receipt.schema.json +0 -0
  56. {engineering_process-1.2.3 → engineering_process-1.2.5}/schemas/release-change.schema.json +0 -0
  57. {engineering_process-1.2.3 → engineering_process-1.2.5}/schemas/release.schema.json +0 -0
  58. {engineering_process-1.2.3 → engineering_process-1.2.5}/schemas/review.schema.json +0 -0
  59. {engineering_process-1.2.3 → engineering_process-1.2.5}/schemas/run.schema.json +0 -0
  60. {engineering_process-1.2.3 → engineering_process-1.2.5}/setup.cfg +0 -0
  61. {engineering_process-1.2.3 → engineering_process-1.2.5}/templates/AGENTS.process.md +0 -0
  62. {engineering_process-1.2.3 → engineering_process-1.2.5}/templates/PULL_REQUEST_TEMPLATE.md +0 -0
  63. {engineering_process-1.2.3 → engineering_process-1.2.5}/templates/adopt-process-windows-job.py +0 -0
  64. {engineering_process-1.2.3 → engineering_process-1.2.5}/templates/adopt-process.py +0 -0
  65. {engineering_process-1.2.3 → engineering_process-1.2.5}/tests/test_adoption.py +0 -0
  66. {engineering_process-1.2.3 → engineering_process-1.2.5}/tests/test_architecture.py +0 -0
  67. {engineering_process-1.2.3 → engineering_process-1.2.5}/tests/test_automation.py +0 -0
  68. {engineering_process-1.2.3 → engineering_process-1.2.5}/tests/test_cli.py +0 -0
  69. {engineering_process-1.2.3 → engineering_process-1.2.5}/tests/test_commands.py +0 -0
  70. {engineering_process-1.2.3 → engineering_process-1.2.5}/tests/test_contracts.py +0 -0
  71. {engineering_process-1.2.3 → engineering_process-1.2.5}/tests/test_distribution.py +0 -0
  72. {engineering_process-1.2.3 → engineering_process-1.2.5}/tests/test_publication_compat.py +0 -0
  73. {engineering_process-1.2.3 → engineering_process-1.2.5}/tests/test_pypi_cache_horizon.py +0 -0
  74. {engineering_process-1.2.3 → engineering_process-1.2.5}/tests/test_repository.py +0 -0
  75. {engineering_process-1.2.3 → engineering_process-1.2.5}/tests/test_skills.py +0 -0
  76. {engineering_process-1.2.3 → engineering_process-1.2.5}/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.3
3
+ Version: 1.2.5
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
@@ -352,6 +352,10 @@ URL; without it, the review remains pending. Earlier plan and review documents r
352
352
  readable, and their runs remain registrable or finishable with the version selected by
353
353
  the authority that started the relevant phase.
354
354
 
355
+ The [finding priority definitions](process_assets/skills/review-change/SKILL.md#finding-priority)
356
+ are the canonical P0-P3 impact convention for this process, including examples and
357
+ their relationship to blocking decisions.
358
+
355
359
  ## Running a change
356
360
 
357
361
  Create and validate a change contract, then register it:
@@ -437,6 +441,14 @@ At any point:
437
441
 
438
442
  ## Release to consumer PR
439
443
 
444
+ This producer's release identity inputs and text assets declared by
445
+ `tool.setuptools.data-files` use UTF-8 without BOM and LF, matching `.gitattributes`.
446
+ Writers select that representation explicitly; JSON writers use
447
+ `write_json_atomic`. Existing distribution verification checks the declared text
448
+ bytes before building. It rejects CRLF, mixed endings, BOM and invalid UTF-8 even
449
+ when text-mode reads or Git conversion would hide the difference. Binary data and
450
+ fixtures outside that text inventory retain their own format contracts.
451
+
440
452
  Every opted-in consumer uses Renovate's pip-compile manager. Its engineering-process
441
453
  package rule keeps the adoption pull request in draft and runs exactly:
442
454
 
@@ -328,6 +328,10 @@ URL; without it, the review remains pending. Earlier plan and review documents r
328
328
  readable, and their runs remain registrable or finishable with the version selected by
329
329
  the authority that started the relevant phase.
330
330
 
331
+ The [finding priority definitions](process_assets/skills/review-change/SKILL.md#finding-priority)
332
+ are the canonical P0-P3 impact convention for this process, including examples and
333
+ their relationship to blocking decisions.
334
+
331
335
  ## Running a change
332
336
 
333
337
  Create and validate a change contract, then register it:
@@ -413,6 +417,14 @@ At any point:
413
417
 
414
418
  ## Release to consumer PR
415
419
 
420
+ This producer's release identity inputs and text assets declared by
421
+ `tool.setuptools.data-files` use UTF-8 without BOM and LF, matching `.gitattributes`.
422
+ Writers select that representation explicitly; JSON writers use
423
+ `write_json_atomic`. Existing distribution verification checks the declared text
424
+ bytes before building. It rejects CRLF, mixed endings, BOM and invalid UTF-8 even
425
+ when text-mode reads or Git conversion would hide the difference. Binary data and
426
+ fixtures outside that text inventory retain their own format contracts.
427
+
416
428
  Every opted-in consumer uses Renovate's pip-compile manager. Its engineering-process
417
429
  package rule keeps the adoption pull request in draft and runs exactly:
418
430
 
@@ -1,3 +1,3 @@
1
1
  """Agent-neutral engineering process."""
2
2
 
3
- VERSION = "1.2.3"
3
+ VERSION = "1.2.5"
@@ -98,7 +98,12 @@ def validate_review_assessments(review: dict[str, Any], process_root: Path) -> N
98
98
  unlinked = sorted(
99
99
  finding_id
100
100
  for finding_id, finding in findings.items()
101
- if finding["origin"] == "production-invariant" and finding_id not in linked
101
+ if finding["origin"] == "production-invariant"
102
+ and finding_id not in linked
103
+ and not (
104
+ finding["severity"] == "non-blocking"
105
+ and finding.get("disposition", {}).get("status") == "resolved"
106
+ )
102
107
  )
103
108
  if unlinked:
104
109
  raise ProcessError(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-process
3
- Version: 1.2.3
3
+ Version: 1.2.5
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
@@ -352,6 +352,10 @@ URL; without it, the review remains pending. Earlier plan and review documents r
352
352
  readable, and their runs remain registrable or finishable with the version selected by
353
353
  the authority that started the relevant phase.
354
354
 
355
+ The [finding priority definitions](process_assets/skills/review-change/SKILL.md#finding-priority)
356
+ are the canonical P0-P3 impact convention for this process, including examples and
357
+ their relationship to blocking decisions.
358
+
355
359
  ## Running a change
356
360
 
357
361
  Create and validate a change contract, then register it:
@@ -437,6 +441,14 @@ At any point:
437
441
 
438
442
  ## Release to consumer PR
439
443
 
444
+ This producer's release identity inputs and text assets declared by
445
+ `tool.setuptools.data-files` use UTF-8 without BOM and LF, matching `.gitattributes`.
446
+ Writers select that representation explicitly; JSON writers use
447
+ `write_json_atomic`. Existing distribution verification checks the declared text
448
+ bytes before building. It rejects CRLF, mixed endings, BOM and invalid UTF-8 even
449
+ when text-mode reads or Git conversion would hide the difference. Binary data and
450
+ fixtures outside that text inventory retain their own format contracts.
451
+
440
452
  Every opted-in consumer uses Renovate's pip-compile manager. Its engineering-process
441
453
  package rule keeps the adoption pull request in draft and runs exactly:
442
454
 
@@ -29,6 +29,11 @@ Verification proves only the deterministic properties exercised by its exact
29
29
  commands on the unchanged candidate. It must not claim to infer architecture,
30
30
  security, intent, or arbitrary prose semantics that those commands do not observe.
31
31
 
32
+ When an artifact claim depends on byte identity, make encoding and newline rules
33
+ explicit at its writer and compare bytes at the exported or installed boundary.
34
+ Text-mode newline normalization and a clean Git diff cannot supply that proof;
35
+ follow the artifact owner's format rather than imposing one text policy on consumers.
36
+
32
37
  Independent review reassesses every invariant against the contract, plan, complete
33
38
  diff, profile evidence, and readiness result. Mark it `satisfied`, `not-applicable`,
34
39
  or `violated`, give a concrete rationale, and cite evidence for `satisfied`. A
@@ -62,3 +62,28 @@ Follow-up scope is only carried findings, remediation diffs, and regressions aga
62
62
  the frozen contract. A new blocker must be either remediation-caused or a P0/P1 late
63
63
  violation with a rationale. After two correction cycles, another changes-requested
64
64
  verdict blocks the change; it never turns into approval or permission to skip review.
65
+
66
+ ## Finding priority
67
+
68
+ P0-P3 are this process's review-impact convention. Assess the consequences if the
69
+ finding remains unresolved in a supported scenario. Consumer scheduling and incident
70
+ response deadlines remain consumer-owned.
71
+
72
+ | Priority | Impact | Illustrative example |
73
+ | --- | --- | --- |
74
+ | P0 - critical | Catastrophic loss of essential operation, authoritative data, or a critical security guarantee. | Irrecoverable loss of the only authoritative dataset. |
75
+ | P1 - high | Severe failure of a core workflow or guarantee, below P0's catastrophic consequences. | A primary workflow produces wrong results that can be regenerated from intact inputs. |
76
+ | P2 - medium | Material but contained incorrect behavior or operational or maintenance cost; core workflows remain usable. | A recoverable failure in a secondary workflow with a safe alternative. |
77
+ | P3 - low | Minor inconvenience or clarity or consistency defect with little effect on behavior or reliability. | An unclear optional explanation when all required steps remain unambiguous. |
78
+
79
+ Choose the highest level whose consequences the evidence supports. Explain affected
80
+ guarantees, reach, preconditions, recoverability, and safe alternatives. Examples are
81
+ illustrative, not an exhaustive classifier; file type, finding category, keywords,
82
+ and ease of repair do not determine priority.
83
+
84
+ Severity follows whether the reviewed snapshot violates the accepted contract or an
85
+ applicable production invariant; priority is not a waiver. A P2 or P3 finding can
86
+ therefore be blocking. The existing P0/P1 restriction on a new `critical-late` blocker
87
+ is an eligibility condition, not permission to block without the required bounded
88
+ late rationale. Preserve carried finding priorities and historical reports under the
89
+ existing correction-cycle rules.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "engineering-process"
7
- version = "1.2.3"
7
+ version = "1.2.5"
8
8
  description = "A small agent-neutral engineering lifecycle with managed adoption"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -599,6 +599,43 @@ class LifecycleTests(unittest.TestCase):
599
599
  self.assertEqual("implementing", state["phase"])
600
600
  self.assertEqual({}, state["verification"])
601
601
 
602
+ def test_resolved_invariant_finishes_with_its_identity_and_history_preserved(self) -> None:
603
+ self.begin()
604
+ self.verify_all()
605
+ start_review(self.root, PROCESS_ROOT, "sample-change", actor_id="reviewer", context_id="review-context", kind="agent")
606
+ first = self.review_document("changes-requested")
607
+ first["findings"][0]["priority"] = "P2"
608
+ first_path = self.root / ".process" / "runs" / "first-review.json"
609
+ write_json(first_path, first)
610
+ state = submit_review(self.root, PROCESS_ROOT, "sample-change", first_path)
611
+ self.assertEqual("changes-requested", state["phase"])
612
+
613
+ begin_implementation(self.root, PROCESS_ROOT, "sample-change", actor_id="implementer", context_id="implementation-context-2", kind="agent")
614
+ (self.root / "product.txt").write_text("corrected\n", encoding="utf-8")
615
+ self.verify_all()
616
+ start_review(self.root, PROCESS_ROOT, "sample-change", actor_id="reviewer", context_id="review-context", kind="agent")
617
+ approved = self.review_document("approved")
618
+ carried = deepcopy(first["findings"][0])
619
+ carried["severity"] = "non-blocking"
620
+ carried["disposition"] = {"status": "resolved", "rationale": "The corrected candidate satisfies the invariant with fresh verification."}
621
+ approved["findings"] = [carried]
622
+ second_path = self.root / ".process" / "runs" / "second-review.json"
623
+ for field, changed in (("priority", "P3"), ("origin", "contract")):
624
+ invalid = deepcopy(approved)
625
+ invalid["findings"][0][field] = changed
626
+ write_json(second_path, invalid)
627
+ with self.subTest(field=field), self.assertRaisesRegex(ProcessError, "identity fields are immutable"):
628
+ submit_review(self.root, PROCESS_ROOT, "sample-change", second_path)
629
+ write_json(second_path, approved)
630
+ state = submit_review(self.root, PROCESS_ROOT, "sample-change", second_path)
631
+ self.assertEqual("approved", state["phase"])
632
+ self.assertEqual(first, state["reviewHistory"][0]["document"])
633
+ self.assertEqual(approved, state["reviewHistory"][1]["document"])
634
+ state, receipt = finish_change(self.root, PROCESS_ROOT, "sample-change", actor_id="coordinator", context_id="finish-context", kind="agent")
635
+ self.assertEqual("completed", state["phase"])
636
+ self.assertEqual(2, state["cycle"])
637
+ self.assertEqual("approved", receipt["review"]["verdict"])
638
+
602
639
  def test_correction_review_requires_the_original_reviewer_identity(self) -> None:
603
640
  self.begin()
604
641
  self.verify_all()
@@ -793,6 +830,34 @@ class LifecycleTests(unittest.TestCase):
793
830
  (self.root / ".process" / "runs" / "sample-change").exists()
794
831
  )
795
832
 
833
+ def test_live_producer_policy_requires_its_accepted_issue(self) -> None:
834
+ project = json.loads((PROCESS_ROOT / ".process" / "project.json").read_text())
835
+ self.project["lifecycle"]["processChanges"] = project["lifecycle"]["processChanges"]
836
+ self.contract["consumerEvidence"] = [
837
+ {"repository": "engineering-process", "incident": "Deferred issue handoff enablement."}
838
+ ]
839
+ for source in (
840
+ "issue-110",
841
+ "https://github.com/other/process/issues/110",
842
+ "https://github.com/phuongnse/engineering-process/pull/118",
843
+ ):
844
+ with self.subTest(source=source):
845
+ self.contract["source"] = source
846
+ write_json(self.contract_path, self.contract)
847
+ with self.assertRaisesRegex(ProcessError, "numbered issue"):
848
+ start_change(
849
+ self.root, PROCESS_ROOT, self.project, self.contract_path,
850
+ actor_id="author", context_id="author-context", kind="agent",
851
+ )
852
+ self.assertFalse((self.root / ".process" / "runs").exists())
853
+ self.contract["source"] = "https://github.com/phuongnse/engineering-process/issues/110"
854
+ write_json(self.contract_path, self.contract)
855
+ state = start_change(
856
+ self.root, PROCESS_ROOT, self.project, self.contract_path,
857
+ actor_id="author", context_id="author-context", kind="agent",
858
+ )
859
+ self.assertEqual("specified", state["phase"])
860
+
796
861
  def test_evidence_only_process_change_policy_remains_compatible(self) -> None:
797
862
  self.project["lifecycle"]["processChanges"] = {
798
863
  "requireConsumerEvidence": True
@@ -190,6 +190,31 @@ class ProductionEngineeringTests(unittest.TestCase):
190
190
  with self.assertRaisesRegex(ProcessError, "blocking finding"):
191
191
  validate_review_assessments(violation, ROOT)
192
192
 
193
+ def test_resolved_invariant_findings_do_not_require_a_current_violation(self) -> None:
194
+ review = deepcopy(self.review)
195
+ review["findings"] = [{
196
+ "id": "resolved-invariant",
197
+ "severity": "non-blocking",
198
+ "priority": "P2",
199
+ "criterionId": "works",
200
+ "origin": "production-invariant",
201
+ "summary": "The previous invariant violation is resolved on this snapshot.",
202
+ "disposition": {"status": "resolved", "rationale": "The corrected structure and current evidence satisfy the invariant."},
203
+ }]
204
+ validate_document(review, "review", schema_root=SCHEMAS)
205
+ validate_review_assessments(review, ROOT)
206
+ for status in ("accepted-risk", "tracked-follow-up"):
207
+ with self.subTest(status=status):
208
+ review["findings"][0]["disposition"] = {
209
+ "status": status,
210
+ "rationale": "The invariant violation remains open.",
211
+ "owner": "consumer-owner",
212
+ "recordUrl": "https://example.invalid/issues/1",
213
+ }
214
+ validate_document(review, "review", schema_root=SCHEMAS)
215
+ with self.assertRaisesRegex(ProcessError, "must be linked"):
216
+ validate_review_assessments(review, ROOT)
217
+
193
218
  def test_review_cannot_approve_a_violation_or_leave_invariant_finding_unlinked(self) -> None:
194
219
  violation = deepcopy(self.review)
195
220
  assessment = violation["productionEngineering"][0]
@@ -12,12 +12,54 @@ import unittest
12
12
  from engineering_process.contracts import ProcessError, read_json, write_json_atomic
13
13
  from engineering_process.release import derive_next_version, validate_release
14
14
  from verification.normalize_sdist import normalize
15
+ from verification.prepare_release import _replace_once
16
+ from verification.verify_distribution import validate_distribution_text
15
17
 
16
18
 
17
19
  ROOT = Path(__file__).resolve().parent.parent
18
20
 
19
21
 
20
22
  class ReleaseTests(unittest.TestCase):
23
+ def test_release_replacement_writes_explicit_lf_on_every_host(self) -> None:
24
+ with tempfile.TemporaryDirectory() as directory:
25
+ target = Path(directory) / "version.txt"
26
+ for source in (b"version=old\nnext\n", b"version=old\r\nnext\r\n", b"version=old\r\nnext\n"):
27
+ with self.subTest(source=source):
28
+ target.write_bytes(source)
29
+ _replace_once(target, "old", "new")
30
+ self.assertEqual(b"version=new\nnext\n", target.read_bytes())
31
+
32
+ def test_release_replacement_rejects_bom_before_writing(self) -> None:
33
+ with tempfile.TemporaryDirectory() as directory:
34
+ target = Path(directory) / "version.txt"
35
+ original = b'\xef\xbb\xbfVERSION = "old"\r\n'
36
+ target.write_bytes(original)
37
+ with self.assertRaisesRegex(ProcessError, "UTF-8 without BOM"):
38
+ _replace_once(target, "old", "new")
39
+ self.assertEqual(original, target.read_bytes())
40
+ self.assertFalse(target.with_name(f".{target.name}.release.tmp").exists())
41
+
42
+ def test_distribution_text_uses_the_declared_inventory_and_rejects_byte_drift(self) -> None:
43
+ with tempfile.TemporaryDirectory() as directory:
44
+ root = Path(directory)
45
+ (root / "engineering_process").mkdir()
46
+ (root / "assets").mkdir()
47
+ (root / "pyproject.toml").write_bytes(b'[tool.setuptools.data-files]\n"share/process" = ["assets/newly-declared.fragment"]\n')
48
+ (root / "release.json").write_bytes(b"{}\n")
49
+ (root / "engineering_process" / "__init__.py").write_bytes(b'VERSION = "1.0.0"\n')
50
+ (root / "opaque.bin").write_bytes(b"\x00\xff\r\n")
51
+ target = root / "assets" / "newly-declared.fragment"
52
+ valid = b"first\nsecond\n"
53
+ target.write_bytes(valid)
54
+ validate_distribution_text(root)
55
+ for invalid in (b"first\r\nsecond\r\n", b"first\r\nsecond\n", b"\xef\xbb\xbf" + valid, b"\xff\n"):
56
+ with self.subTest(invalid=invalid):
57
+ target.write_bytes(invalid)
58
+ with self.assertRaisesRegex(RuntimeError, "UTF-8 without BOM and use LF"):
59
+ validate_distribution_text(root)
60
+ target.write_bytes(valid)
61
+ validate_distribution_text(root)
62
+
21
63
  def test_current_release_identity_is_consistent(self) -> None:
22
64
  release = read_json(ROOT / "release.json")
23
65
  result = validate_release(ROOT, ROOT, tag=f"v{release['version']}")
@@ -90,6 +132,7 @@ class ReleaseTests(unittest.TestCase):
90
132
  self.assertIn(
91
133
  f'version = "{expected}"', (target / "pyproject.toml").read_text()
92
134
  )
135
+ validate_distribution_text(target)
93
136
 
94
137
  def test_invalid_change_type_fails(self) -> None:
95
138
  with self.assertRaises(ProcessError):