engineering-process 1.2.5__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.5 → engineering_process-1.2.6}/MANIFEST.in +1 -1
  2. {engineering_process-1.2.5/engineering_process.egg-info → engineering_process-1.2.6}/PKG-INFO +34 -1
  3. {engineering_process-1.2.5 → engineering_process-1.2.6}/README.md +33 -0
  4. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/__init__.py +1 -1
  5. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/contracts.py +8 -3
  6. {engineering_process-1.2.5 → engineering_process-1.2.6/engineering_process.egg-info}/PKG-INFO +34 -1
  7. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process.egg-info/SOURCES.txt +1 -0
  8. {engineering_process-1.2.5 → engineering_process-1.2.6}/pyproject.toml +2 -1
  9. engineering_process-1.2.6/templates/renovate.json +6 -0
  10. {engineering_process-1.2.5 → engineering_process-1.2.6}/tests/test_automation.py +45 -1
  11. {engineering_process-1.2.5 → engineering_process-1.2.6}/tests/test_contracts.py +18 -1
  12. {engineering_process-1.2.5 → engineering_process-1.2.6}/LICENSE +0 -0
  13. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/__main__.py +0 -0
  14. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/_supervisor_contract.py +0 -0
  15. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/_supervisor_posix.py +0 -0
  16. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/_supervisor_windows.py +0 -0
  17. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/_windows_job.py +0 -0
  18. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/adoption.py +0 -0
  19. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/cli.py +0 -0
  20. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/commands.py +0 -0
  21. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/distribution.py +0 -0
  22. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/helper_launch.py +0 -0
  23. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/lifecycle.py +0 -0
  24. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/production_engineering.py +0 -0
  25. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/project.py +0 -0
  26. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/publication_compat.py +0 -0
  27. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/release.py +0 -0
  28. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/repository.py +0 -0
  29. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/requirements-dev.txt +0 -0
  30. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/requirements-runtime.txt +0 -0
  31. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/skills.py +0 -0
  32. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process/supervision.py +0 -0
  33. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process.egg-info/dependency_links.txt +0 -0
  34. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process.egg-info/entry_points.txt +0 -0
  35. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process.egg-info/requires.txt +0 -0
  36. {engineering_process-1.2.5 → engineering_process-1.2.6}/engineering_process.egg-info/top_level.txt +0 -0
  37. {engineering_process-1.2.5 → engineering_process-1.2.6}/process-graph.json +0 -0
  38. {engineering_process-1.2.5 → engineering_process-1.2.6}/process_assets/skills/finish-change/SKILL.md +0 -0
  39. {engineering_process-1.2.5 → engineering_process-1.2.6}/process_assets/skills/implement-change/SKILL.md +0 -0
  40. {engineering_process-1.2.5 → engineering_process-1.2.6}/process_assets/skills/improve-process/SKILL.md +0 -0
  41. {engineering_process-1.2.5 → engineering_process-1.2.6}/process_assets/skills/plan-change/SKILL.md +0 -0
  42. {engineering_process-1.2.5 → engineering_process-1.2.6}/process_assets/skills/production-engineering/SKILL.md +0 -0
  43. {engineering_process-1.2.5 → engineering_process-1.2.6}/process_assets/skills/production-engineering/invariants.json +0 -0
  44. {engineering_process-1.2.5 → engineering_process-1.2.6}/process_assets/skills/review-change/SKILL.md +0 -0
  45. {engineering_process-1.2.5 → engineering_process-1.2.6}/process_assets/skills/run-change/SKILL.md +0 -0
  46. {engineering_process-1.2.5 → engineering_process-1.2.6}/process_assets/skills/start-change/SKILL.md +0 -0
  47. {engineering_process-1.2.5 → engineering_process-1.2.6}/process_assets/skills/verify-change/SKILL.md +0 -0
  48. {engineering_process-1.2.5 → engineering_process-1.2.6}/schemas/change.schema.json +0 -0
  49. {engineering_process-1.2.5 → engineering_process-1.2.6}/schemas/plan.schema.json +0 -0
  50. {engineering_process-1.2.5 → engineering_process-1.2.6}/schemas/process-graph.schema.json +0 -0
  51. {engineering_process-1.2.5 → engineering_process-1.2.6}/schemas/process-lock.schema.json +0 -0
  52. {engineering_process-1.2.5 → engineering_process-1.2.6}/schemas/production-engineering.schema.json +0 -0
  53. {engineering_process-1.2.5 → engineering_process-1.2.6}/schemas/project-legacy.schema.json +0 -0
  54. {engineering_process-1.2.5 → engineering_process-1.2.6}/schemas/project.schema.json +0 -0
  55. {engineering_process-1.2.5 → engineering_process-1.2.6}/schemas/receipt.schema.json +0 -0
  56. {engineering_process-1.2.5 → engineering_process-1.2.6}/schemas/release-change.schema.json +0 -0
  57. {engineering_process-1.2.5 → engineering_process-1.2.6}/schemas/release.schema.json +0 -0
  58. {engineering_process-1.2.5 → engineering_process-1.2.6}/schemas/review.schema.json +0 -0
  59. {engineering_process-1.2.5 → engineering_process-1.2.6}/schemas/run.schema.json +0 -0
  60. {engineering_process-1.2.5 → engineering_process-1.2.6}/setup.cfg +0 -0
  61. {engineering_process-1.2.5 → engineering_process-1.2.6}/templates/AGENTS.process.md +0 -0
  62. {engineering_process-1.2.5 → engineering_process-1.2.6}/templates/PULL_REQUEST_TEMPLATE.md +0 -0
  63. {engineering_process-1.2.5 → engineering_process-1.2.6}/templates/adopt-process-windows-job.py +0 -0
  64. {engineering_process-1.2.5 → engineering_process-1.2.6}/templates/adopt-process.py +0 -0
  65. {engineering_process-1.2.5 → engineering_process-1.2.6}/tests/test_adoption.py +0 -0
  66. {engineering_process-1.2.5 → engineering_process-1.2.6}/tests/test_architecture.py +0 -0
  67. {engineering_process-1.2.5 → engineering_process-1.2.6}/tests/test_cli.py +0 -0
  68. {engineering_process-1.2.5 → engineering_process-1.2.6}/tests/test_commands.py +0 -0
  69. {engineering_process-1.2.5 → engineering_process-1.2.6}/tests/test_distribution.py +0 -0
  70. {engineering_process-1.2.5 → engineering_process-1.2.6}/tests/test_lifecycle.py +0 -0
  71. {engineering_process-1.2.5 → engineering_process-1.2.6}/tests/test_production_engineering.py +0 -0
  72. {engineering_process-1.2.5 → engineering_process-1.2.6}/tests/test_publication_compat.py +0 -0
  73. {engineering_process-1.2.5 → engineering_process-1.2.6}/tests/test_pypi_cache_horizon.py +0 -0
  74. {engineering_process-1.2.5 → engineering_process-1.2.6}/tests/test_release.py +0 -0
  75. {engineering_process-1.2.5 → engineering_process-1.2.6}/tests/test_repository.py +0 -0
  76. {engineering_process-1.2.5 → engineering_process-1.2.6}/tests/test_skills.py +0 -0
  77. {engineering_process-1.2.5 → 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.5
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
@@ -471,6 +471,39 @@ authorizes that merge.
471
471
  This repository opts in through .github/renovate.json, so it receives the same
472
472
  adoption PR as every other consumer. See SELF_HOSTING.md and RELEASING.md.
473
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
+
474
507
  ## Compatibility
475
508
 
476
509
  Version 1.x retains a few small pre-1.0 command shapes so existing consumers can
@@ -447,6 +447,39 @@ authorizes that merge.
447
447
  This repository opts in through .github/renovate.json, so it receives the same
448
448
  adoption PR as every other consumer. See SELF_HOSTING.md and RELEASING.md.
449
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
+
450
483
  ## Compatibility
451
484
 
452
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.5"
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-process
3
- Version: 1.2.5
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
@@ -471,6 +471,39 @@ authorizes that merge.
471
471
  This repository opts in through .github/renovate.json, so it receives the same
472
472
  adoption PR as every other consumer. See SELF_HOSTING.md and RELEASING.md.
473
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
+
474
507
  ## Compatibility
475
508
 
476
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "engineering-process"
7
- version = "1.2.5"
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",