engineering-process 1.2.4__tar.gz → 1.2.6__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 (77) hide show
  1. {engineering_process-1.2.4 → engineering_process-1.2.6}/MANIFEST.in +1 -1
  2. {engineering_process-1.2.4/engineering_process.egg-info → engineering_process-1.2.6}/PKG-INFO +42 -1
  3. {engineering_process-1.2.4 → engineering_process-1.2.6}/README.md +41 -0
  4. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/__init__.py +1 -1
  5. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/contracts.py +8 -3
  6. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/production_engineering.py +6 -1
  7. {engineering_process-1.2.4 → engineering_process-1.2.6/engineering_process.egg-info}/PKG-INFO +42 -1
  8. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process.egg-info/SOURCES.txt +1 -0
  9. {engineering_process-1.2.4 → engineering_process-1.2.6}/process_assets/skills/production-engineering/SKILL.md +5 -0
  10. {engineering_process-1.2.4 → engineering_process-1.2.6}/pyproject.toml +2 -1
  11. engineering_process-1.2.6/templates/renovate.json +6 -0
  12. {engineering_process-1.2.4 → engineering_process-1.2.6}/tests/test_automation.py +45 -1
  13. {engineering_process-1.2.4 → engineering_process-1.2.6}/tests/test_contracts.py +18 -1
  14. {engineering_process-1.2.4 → engineering_process-1.2.6}/tests/test_lifecycle.py +37 -0
  15. {engineering_process-1.2.4 → engineering_process-1.2.6}/tests/test_production_engineering.py +25 -0
  16. {engineering_process-1.2.4 → engineering_process-1.2.6}/tests/test_release.py +43 -0
  17. {engineering_process-1.2.4 → engineering_process-1.2.6}/LICENSE +0 -0
  18. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/__main__.py +0 -0
  19. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/_supervisor_contract.py +0 -0
  20. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/_supervisor_posix.py +0 -0
  21. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/_supervisor_windows.py +0 -0
  22. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/_windows_job.py +0 -0
  23. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/adoption.py +0 -0
  24. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/cli.py +0 -0
  25. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/commands.py +0 -0
  26. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/distribution.py +0 -0
  27. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/helper_launch.py +0 -0
  28. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/lifecycle.py +0 -0
  29. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/project.py +0 -0
  30. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/publication_compat.py +0 -0
  31. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/release.py +0 -0
  32. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/repository.py +0 -0
  33. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/requirements-dev.txt +0 -0
  34. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/requirements-runtime.txt +0 -0
  35. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/skills.py +0 -0
  36. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process/supervision.py +0 -0
  37. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process.egg-info/dependency_links.txt +0 -0
  38. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process.egg-info/entry_points.txt +0 -0
  39. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process.egg-info/requires.txt +0 -0
  40. {engineering_process-1.2.4 → engineering_process-1.2.6}/engineering_process.egg-info/top_level.txt +0 -0
  41. {engineering_process-1.2.4 → engineering_process-1.2.6}/process-graph.json +0 -0
  42. {engineering_process-1.2.4 → engineering_process-1.2.6}/process_assets/skills/finish-change/SKILL.md +0 -0
  43. {engineering_process-1.2.4 → engineering_process-1.2.6}/process_assets/skills/implement-change/SKILL.md +0 -0
  44. {engineering_process-1.2.4 → engineering_process-1.2.6}/process_assets/skills/improve-process/SKILL.md +0 -0
  45. {engineering_process-1.2.4 → engineering_process-1.2.6}/process_assets/skills/plan-change/SKILL.md +0 -0
  46. {engineering_process-1.2.4 → engineering_process-1.2.6}/process_assets/skills/production-engineering/invariants.json +0 -0
  47. {engineering_process-1.2.4 → engineering_process-1.2.6}/process_assets/skills/review-change/SKILL.md +0 -0
  48. {engineering_process-1.2.4 → engineering_process-1.2.6}/process_assets/skills/run-change/SKILL.md +0 -0
  49. {engineering_process-1.2.4 → engineering_process-1.2.6}/process_assets/skills/start-change/SKILL.md +0 -0
  50. {engineering_process-1.2.4 → engineering_process-1.2.6}/process_assets/skills/verify-change/SKILL.md +0 -0
  51. {engineering_process-1.2.4 → engineering_process-1.2.6}/schemas/change.schema.json +0 -0
  52. {engineering_process-1.2.4 → engineering_process-1.2.6}/schemas/plan.schema.json +0 -0
  53. {engineering_process-1.2.4 → engineering_process-1.2.6}/schemas/process-graph.schema.json +0 -0
  54. {engineering_process-1.2.4 → engineering_process-1.2.6}/schemas/process-lock.schema.json +0 -0
  55. {engineering_process-1.2.4 → engineering_process-1.2.6}/schemas/production-engineering.schema.json +0 -0
  56. {engineering_process-1.2.4 → engineering_process-1.2.6}/schemas/project-legacy.schema.json +0 -0
  57. {engineering_process-1.2.4 → engineering_process-1.2.6}/schemas/project.schema.json +0 -0
  58. {engineering_process-1.2.4 → engineering_process-1.2.6}/schemas/receipt.schema.json +0 -0
  59. {engineering_process-1.2.4 → engineering_process-1.2.6}/schemas/release-change.schema.json +0 -0
  60. {engineering_process-1.2.4 → engineering_process-1.2.6}/schemas/release.schema.json +0 -0
  61. {engineering_process-1.2.4 → engineering_process-1.2.6}/schemas/review.schema.json +0 -0
  62. {engineering_process-1.2.4 → engineering_process-1.2.6}/schemas/run.schema.json +0 -0
  63. {engineering_process-1.2.4 → engineering_process-1.2.6}/setup.cfg +0 -0
  64. {engineering_process-1.2.4 → engineering_process-1.2.6}/templates/AGENTS.process.md +0 -0
  65. {engineering_process-1.2.4 → engineering_process-1.2.6}/templates/PULL_REQUEST_TEMPLATE.md +0 -0
  66. {engineering_process-1.2.4 → engineering_process-1.2.6}/templates/adopt-process-windows-job.py +0 -0
  67. {engineering_process-1.2.4 → engineering_process-1.2.6}/templates/adopt-process.py +0 -0
  68. {engineering_process-1.2.4 → engineering_process-1.2.6}/tests/test_adoption.py +0 -0
  69. {engineering_process-1.2.4 → engineering_process-1.2.6}/tests/test_architecture.py +0 -0
  70. {engineering_process-1.2.4 → engineering_process-1.2.6}/tests/test_cli.py +0 -0
  71. {engineering_process-1.2.4 → engineering_process-1.2.6}/tests/test_commands.py +0 -0
  72. {engineering_process-1.2.4 → engineering_process-1.2.6}/tests/test_distribution.py +0 -0
  73. {engineering_process-1.2.4 → engineering_process-1.2.6}/tests/test_publication_compat.py +0 -0
  74. {engineering_process-1.2.4 → engineering_process-1.2.6}/tests/test_pypi_cache_horizon.py +0 -0
  75. {engineering_process-1.2.4 → engineering_process-1.2.6}/tests/test_repository.py +0 -0
  76. {engineering_process-1.2.4 → engineering_process-1.2.6}/tests/test_skills.py +0 -0
  77. {engineering_process-1.2.4 → engineering_process-1.2.6}/tests/test_supervisor_posix.py +0 -0
@@ -1,4 +1,4 @@
1
1
  include process-graph.json
2
2
  recursive-include process_assets/skills SKILL.md *.json
3
3
  recursive-include schemas *.json
4
- recursive-include templates *.md *.py
4
+ recursive-include templates *.md *.py *.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-process
3
- Version: 1.2.4
3
+ Version: 1.2.6
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
@@ -441,6 +441,14 @@ At any point:
441
441
 
442
442
  ## Release to consumer PR
443
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
+
444
452
  Every opted-in consumer uses Renovate's pip-compile manager. Its engineering-process
445
453
  package rule keeps the adoption pull request in draft and runs exactly:
446
454
 
@@ -463,6 +471,39 @@ authorizes that merge.
463
471
  This repository opts in through .github/renovate.json, so it receives the same
464
472
  adoption PR as every other consumer. See SELF_HOSTING.md and RELEASING.md.
465
473
 
474
+ The optional [Renovate preset](templates/renovate.json) is generated from the
475
+ canonical public template. It supplies only `prHeader` and `prBodyTemplate`;
476
+ dependency selection, supported platforms, schedules, major-update approval,
477
+ commands, draft policy, and merge authority remain consumer-owned. Regenerate it
478
+ with `python verification/generate_renovate_preset.py`; `--check` rejects drift.
479
+
480
+ Consumers add `github>phuongnse/engineering-process//templates/renovate#COMMIT_SHA`
481
+ to their existing `extends` array, replacing `COMMIT_SHA` with the full source
482
+ commit of a verified release that contains the preset. Remove obsolete inline
483
+ `prHeader` and `prBodyTemplate` overrides, including matching package-rule overrides.
484
+ The preset targets the canonical draft grammar used by 1.2.4 and this distribution;
485
+ it does not claim compatibility with earlier publication contracts. A future
486
+ grammar change must preserve this adapter or ship an explicit consumer migration.
487
+
488
+ Renovate resolves presets from the protected base configuration before dependency
489
+ updates and post-upgrade tasks. Bootstrap the compatible preset in that base before
490
+ relying on its generated drafts; changing the candidate configuration alone cannot
491
+ repair the same run's body. Verify the native rendered body against both the base
492
+ and candidate process authority during adoption. Pending fields and unchecked
493
+ Completion gate items are proposals, never evidence or approval.
494
+
495
+ Before collecting lifecycle evidence for a bot PR, apply its configured
496
+ `stopUpdatingLabel` (Renovate's default is `stop-updating`) and confirm the head is
497
+ unchanged after any in-flight bot run finishes. Keep the label through verification,
498
+ independent review, receipt and the consumer-owned merge. Do not request a native
499
+ rebase or dashboard retry while reviewing: an explicit retry can resume updates.
500
+ If the candidate changes, open a new implementation cycle and collect fresh
501
+ evidence. Remove the pause label only when returning the PR to automation.
502
+
503
+ These are native [shared preset](https://docs.renovatebot.com/config-presets/) and
504
+ [review pause](https://docs.renovatebot.com/configuration-options/#stopupdatinglabel)
505
+ boundaries; the control plane does not become a second PR publisher.
506
+
466
507
  ## Compatibility
467
508
 
468
509
  Version 1.x retains a few small pre-1.0 command shapes so existing consumers can
@@ -417,6 +417,14 @@ At any point:
417
417
 
418
418
  ## Release to consumer PR
419
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
+
420
428
  Every opted-in consumer uses Renovate's pip-compile manager. Its engineering-process
421
429
  package rule keeps the adoption pull request in draft and runs exactly:
422
430
 
@@ -439,6 +447,39 @@ authorizes that merge.
439
447
  This repository opts in through .github/renovate.json, so it receives the same
440
448
  adoption PR as every other consumer. See SELF_HOSTING.md and RELEASING.md.
441
449
 
450
+ The optional [Renovate preset](templates/renovate.json) is generated from the
451
+ canonical public template. It supplies only `prHeader` and `prBodyTemplate`;
452
+ dependency selection, supported platforms, schedules, major-update approval,
453
+ commands, draft policy, and merge authority remain consumer-owned. Regenerate it
454
+ with `python verification/generate_renovate_preset.py`; `--check` rejects drift.
455
+
456
+ Consumers add `github>phuongnse/engineering-process//templates/renovate#COMMIT_SHA`
457
+ to their existing `extends` array, replacing `COMMIT_SHA` with the full source
458
+ commit of a verified release that contains the preset. Remove obsolete inline
459
+ `prHeader` and `prBodyTemplate` overrides, including matching package-rule overrides.
460
+ The preset targets the canonical draft grammar used by 1.2.4 and this distribution;
461
+ it does not claim compatibility with earlier publication contracts. A future
462
+ grammar change must preserve this adapter or ship an explicit consumer migration.
463
+
464
+ Renovate resolves presets from the protected base configuration before dependency
465
+ updates and post-upgrade tasks. Bootstrap the compatible preset in that base before
466
+ relying on its generated drafts; changing the candidate configuration alone cannot
467
+ repair the same run's body. Verify the native rendered body against both the base
468
+ and candidate process authority during adoption. Pending fields and unchecked
469
+ Completion gate items are proposals, never evidence or approval.
470
+
471
+ Before collecting lifecycle evidence for a bot PR, apply its configured
472
+ `stopUpdatingLabel` (Renovate's default is `stop-updating`) and confirm the head is
473
+ unchanged after any in-flight bot run finishes. Keep the label through verification,
474
+ independent review, receipt and the consumer-owned merge. Do not request a native
475
+ rebase or dashboard retry while reviewing: an explicit retry can resume updates.
476
+ If the candidate changes, open a new implementation cycle and collect fresh
477
+ evidence. Remove the pause label only when returning the PR to automation.
478
+
479
+ These are native [shared preset](https://docs.renovatebot.com/config-presets/) and
480
+ [review pause](https://docs.renovatebot.com/configuration-options/#stopupdatinglabel)
481
+ boundaries; the control plane does not become a second PR publisher.
482
+
442
483
  ## Compatibility
443
484
 
444
485
  Version 1.x retains a few small pre-1.0 command shapes so existing consumers can
@@ -1,3 +1,3 @@
1
1
  """Agent-neutral engineering process."""
2
2
 
3
- VERSION = "1.2.4"
3
+ VERSION = "1.2.6"
@@ -67,9 +67,9 @@ def digest_json(value: Any) -> str:
67
67
  return "sha256:" + hashlib.sha256(canonical_bytes(value)).hexdigest()
68
68
 
69
69
 
70
- def write_json_atomic(path: Path, value: Any) -> None:
71
- """Write canonical human-readable JSON without exposing a partial file."""
72
- data = (
70
+ def formatted_json_bytes(value: Any) -> bytes:
71
+ """Serialize human-readable JSON as UTF-8 without BOM, using LF."""
72
+ return (
73
73
  json.dumps(
74
74
  value,
75
75
  ensure_ascii=False,
@@ -79,6 +79,11 @@ def write_json_atomic(path: Path, value: Any) -> None:
79
79
  )
80
80
  + "\n"
81
81
  ).encode("utf-8")
82
+
83
+
84
+ def write_json_atomic(path: Path, value: Any) -> None:
85
+ """Write canonical human-readable JSON without exposing a partial file."""
86
+ data = formatted_json_bytes(value)
82
87
  path.parent.mkdir(parents=True, exist_ok=True)
83
88
  temporary = path.with_name(f".{path.name}.tmp")
84
89
  try:
@@ -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.4
3
+ Version: 1.2.6
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
@@ -441,6 +441,14 @@ At any point:
441
441
 
442
442
  ## Release to consumer PR
443
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
+
444
452
  Every opted-in consumer uses Renovate's pip-compile manager. Its engineering-process
445
453
  package rule keeps the adoption pull request in draft and runs exactly:
446
454
 
@@ -463,6 +471,39 @@ authorizes that merge.
463
471
  This repository opts in through .github/renovate.json, so it receives the same
464
472
  adoption PR as every other consumer. See SELF_HOSTING.md and RELEASING.md.
465
473
 
474
+ The optional [Renovate preset](templates/renovate.json) is generated from the
475
+ canonical public template. It supplies only `prHeader` and `prBodyTemplate`;
476
+ dependency selection, supported platforms, schedules, major-update approval,
477
+ commands, draft policy, and merge authority remain consumer-owned. Regenerate it
478
+ with `python verification/generate_renovate_preset.py`; `--check` rejects drift.
479
+
480
+ Consumers add `github>phuongnse/engineering-process//templates/renovate#COMMIT_SHA`
481
+ to their existing `extends` array, replacing `COMMIT_SHA` with the full source
482
+ commit of a verified release that contains the preset. Remove obsolete inline
483
+ `prHeader` and `prBodyTemplate` overrides, including matching package-rule overrides.
484
+ The preset targets the canonical draft grammar used by 1.2.4 and this distribution;
485
+ it does not claim compatibility with earlier publication contracts. A future
486
+ grammar change must preserve this adapter or ship an explicit consumer migration.
487
+
488
+ Renovate resolves presets from the protected base configuration before dependency
489
+ updates and post-upgrade tasks. Bootstrap the compatible preset in that base before
490
+ relying on its generated drafts; changing the candidate configuration alone cannot
491
+ repair the same run's body. Verify the native rendered body against both the base
492
+ and candidate process authority during adoption. Pending fields and unchecked
493
+ Completion gate items are proposals, never evidence or approval.
494
+
495
+ Before collecting lifecycle evidence for a bot PR, apply its configured
496
+ `stopUpdatingLabel` (Renovate's default is `stop-updating`) and confirm the head is
497
+ unchanged after any in-flight bot run finishes. Keep the label through verification,
498
+ independent review, receipt and the consumer-owned merge. Do not request a native
499
+ rebase or dashboard retry while reviewing: an explicit retry can resume updates.
500
+ If the candidate changes, open a new implementation cycle and collect fresh
501
+ evidence. Remove the pause label only when returning the PR to automation.
502
+
503
+ These are native [shared preset](https://docs.renovatebot.com/config-presets/) and
504
+ [review pause](https://docs.renovatebot.com/configuration-options/#stopupdatinglabel)
505
+ boundaries; the control plane does not become a second PR publisher.
506
+
466
507
  ## Compatibility
467
508
 
468
509
  Version 1.x retains a few small pre-1.0 command shapes so existing consumers can
@@ -57,6 +57,7 @@ templates/AGENTS.process.md
57
57
  templates/PULL_REQUEST_TEMPLATE.md
58
58
  templates/adopt-process-windows-job.py
59
59
  templates/adopt-process.py
60
+ templates/renovate.json
60
61
  tests/test_adoption.py
61
62
  tests/test_architecture.py
62
63
  tests/test_automation.py
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "engineering-process"
7
- version = "1.2.4"
7
+ version = "1.2.6"
8
8
  description = "A small agent-neutral engineering lifecycle with managed adoption"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -42,6 +42,7 @@ dev = {file = ["engineering_process/requirements-dev.txt"]}
42
42
  "share/engineering-process/templates" = [
43
43
  "templates/AGENTS.process.md",
44
44
  "templates/PULL_REQUEST_TEMPLATE.md",
45
+ "templates/renovate.json",
45
46
  "templates/adopt-process.py",
46
47
  "templates/adopt-process-windows-job.py",
47
48
  ]
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "description": "Pending process draft; generated from templates/PULL_REQUEST_TEMPLATE.md.",
4
+ "prBodyTemplate": "{{{header}}}",
5
+ "prHeader": "## Summary\n\n- Outcome: Update {{#each upgrades}}{{depName}} from {{currentValue}}{{#if currentDigest}} ({{currentDigest}}){{/if}} to {{newValue}}{{#if newDigest}} ({{newDigest}}){{/if}}{{#unless @last}}; {{/unless}}{{/each}}.\n- Scope: {{#each upgrades}}{{packageFile}}{{#unless @last}}; {{/unless}}{{/each}}.\n\n## Contract and risk\n\n- Source: pending\n- Risk: pending\n- Compatibility: pending\n- Stack: pending\n\n## Verification\n\n- Profiles: pending\n- Snapshot: pending\n- Completion receipt: pending\n\n## Independent review\n\n- Verdict: pending\n- Cycles: pending\n- Blocking findings: pending\n- Non-blocking dispositions: pending\n\n## Completion gate\n\n- [ ] Accepted scope is implemented without silent expansion.\n- [ ] Required profiles pass on the reviewed snapshot.\n- [ ] Independent review approved with no blocking finding.\n- [ ] Every non-blocking finding has a recorded disposition.\n"
6
+ }
@@ -3,9 +3,14 @@ from __future__ import annotations
3
3
  import json
4
4
  from pathlib import Path
5
5
  import re
6
+ import tempfile
6
7
  import unittest
8
+ from unittest.mock import patch
7
9
 
8
- from engineering_process.publication_compat import validate_pull_request
10
+ from engineering_process.contracts import ProcessError
11
+ from engineering_process.publication_compat import _pull_request_body_issues, validate_pull_request
12
+ from verification.generate_renovate_preset import generate_preset
13
+ from verification import generate_renovate_preset
9
14
 
10
15
 
11
16
  ROOT = Path(__file__).resolve().parent.parent
@@ -15,6 +20,45 @@ ACTIVE_PROCESS_PIN = re.compile(
15
20
 
16
21
 
17
22
  class AutomationTests(unittest.TestCase):
23
+ def test_renovate_preset_matches_the_canonical_pending_template(self) -> None:
24
+ template = (ROOT / "templates" / "PULL_REQUEST_TEMPLATE.md").read_text(encoding="utf-8")
25
+ preset = json.loads((ROOT / "templates" / "renovate.json").read_text(encoding="utf-8"))
26
+ self.assertEqual(generate_preset(template), preset)
27
+ self.assertEqual([], _pull_request_body_issues(preset["prHeader"], "draft"))
28
+ self.assertTrue(_pull_request_body_issues(preset["prHeader"], "ready"))
29
+
30
+ def test_renovate_preset_rejects_an_incomplete_public_contract(self) -> None:
31
+ template = (ROOT / "templates" / "PULL_REQUEST_TEMPLATE.md").read_text(encoding="utf-8")
32
+ with self.assertRaises(ProcessError):
33
+ generate_preset(template.replace("## Completion gate", "## Other"))
34
+
35
+ def test_renovate_generator_rejects_changed_bytes_and_recovers(self) -> None:
36
+ template = (ROOT / "templates" / "PULL_REQUEST_TEMPLATE.md").read_bytes()
37
+ with tempfile.TemporaryDirectory() as directory:
38
+ root = Path(directory)
39
+ (root / "templates").mkdir()
40
+ (root / "templates" / "PULL_REQUEST_TEMPLATE.md").write_bytes(template)
41
+ target = root / "templates" / "renovate.json"
42
+ with patch.object(generate_renovate_preset, "PROJECT_ROOT", root), patch("sys.argv", ["generate"]):
43
+ self.assertEqual(0, generate_renovate_preset.main())
44
+ expected = target.read_bytes()
45
+ self.assertNotIn(b"\r", expected)
46
+ self.assertFalse(expected.startswith(b"\xef\xbb\xbf"))
47
+ for changed in (
48
+ expected.replace(b"\n", b"\r\n"),
49
+ expected.replace(b"\n", b"\r\n", 1),
50
+ b"\xef\xbb\xbf" + expected,
51
+ ):
52
+ with self.subTest(changed=changed[:20]):
53
+ target.write_bytes(changed)
54
+ with patch("sys.argv", ["generate", "--check"]):
55
+ with self.assertRaisesRegex(ProcessError, "is stale"):
56
+ generate_renovate_preset.main()
57
+ self.assertEqual(0, generate_renovate_preset.main())
58
+ self.assertEqual(expected, target.read_bytes())
59
+ with patch("sys.argv", ["generate", "--check"]):
60
+ self.assertEqual(0, generate_renovate_preset.main())
61
+
18
62
  def test_pull_request_template_defines_public_evidence_hierarchy(self) -> None:
19
63
  template = (ROOT / "templates" / "PULL_REQUEST_TEMPLATE.md").read_text(
20
64
  encoding="utf-8"
@@ -2,11 +2,18 @@ from __future__ import annotations
2
2
 
3
3
  from copy import deepcopy
4
4
  from pathlib import Path
5
+ import tempfile
5
6
  import unittest
6
7
 
7
8
  from jsonschema import Draft202012Validator
8
9
 
9
- from engineering_process.contracts import ProcessError, read_json, validate_document
10
+ from engineering_process.contracts import (
11
+ ProcessError,
12
+ formatted_json_bytes,
13
+ read_json,
14
+ validate_document,
15
+ write_json_atomic,
16
+ )
10
17
  from engineering_process.distribution import schemas_root
11
18
  from engineering_process.project import (
12
19
  load_project,
@@ -21,6 +28,16 @@ SCHEMAS = schemas_root(ROOT)
21
28
 
22
29
 
23
30
  class ContractTests(unittest.TestCase):
31
+ def test_json_documents_have_one_utf8_lf_representation(self) -> None:
32
+ document = {"z": "\u0111", "a": 1}
33
+ expected = b'{\n "a": 1,\n "z": "\xc4\x91"\n}\n'
34
+ self.assertEqual(expected, formatted_json_bytes(document))
35
+ with tempfile.TemporaryDirectory() as directory:
36
+ target = Path(directory) / "document.json"
37
+ write_json_atomic(target, document)
38
+ self.assertEqual(expected, target.read_bytes())
39
+ self.assertEqual(document, read_json(target))
40
+
24
41
  def test_every_schema_is_valid_and_is_used(self) -> None:
25
42
  expected = {
26
43
  "change",
@@ -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()
@@ -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):