engineering-process 2.1.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. {engineering_process-2.1.0 → engineering_process-2.2.0}/MANIFEST.in +1 -0
  2. {engineering_process-2.1.0/engineering_process.egg-info → engineering_process-2.2.0}/PKG-INFO +19 -7
  3. {engineering_process-2.1.0 → engineering_process-2.2.0}/README.md +18 -6
  4. engineering_process-2.2.0/RELEASE_NOTES.md +18 -0
  5. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/__init__.py +1 -1
  6. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/adoption.py +10 -3
  7. engineering_process-2.2.0/engineering_process/artifact_standards.py +147 -0
  8. engineering_process-2.2.0/engineering_process/automation_name.py +31 -0
  9. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/cli.py +64 -1
  10. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/contracts.py +6 -0
  11. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/distribution.py +5 -2
  12. engineering_process-2.1.0/engineering_process/publication_compat.py → engineering_process-2.2.0/engineering_process/pr_description.py +99 -138
  13. engineering_process-2.2.0/engineering_process/publication_compat.py +119 -0
  14. engineering_process-2.2.0/engineering_process/release_notes.py +76 -0
  15. {engineering_process-2.1.0 → engineering_process-2.2.0/engineering_process.egg-info}/PKG-INFO +19 -7
  16. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process.egg-info/SOURCES.txt +14 -0
  17. {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/change-complete/SKILL.md +8 -0
  18. {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/change-start/SKILL.md +4 -0
  19. {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/change-verify/SKILL.md +7 -0
  20. {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/production-engineering/SKILL.md +7 -0
  21. engineering_process-2.2.0/process_assets/standards/automation-name.v1.json +16 -0
  22. engineering_process-2.2.0/process_assets/standards/pull-request.v1.json +129 -0
  23. engineering_process-2.2.0/process_assets/standards/release-notes.v1.json +34 -0
  24. {engineering_process-2.1.0 → engineering_process-2.2.0}/pyproject.toml +12 -1
  25. engineering_process-2.2.0/schemas/artifact-selection.schema.json +67 -0
  26. engineering_process-2.2.0/schemas/artifact-standard.schema.json +322 -0
  27. engineering_process-2.2.0/schemas/automation-name-data.schema.json +35 -0
  28. engineering_process-2.2.0/schemas/pr-description-data.schema.json +63 -0
  29. engineering_process-2.2.0/schemas/release-notes-data.schema.json +103 -0
  30. engineering_process-2.2.0/schemas/renovate-preset.schema.json +30 -0
  31. engineering_process-2.2.0/templates/PULL_REQUEST_TEMPLATE.md +36 -0
  32. {engineering_process-2.1.0 → engineering_process-2.2.0}/templates/renovate.json +1 -1
  33. {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_adoption.py +60 -0
  34. {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_architecture.py +5 -1
  35. engineering_process-2.2.0/tests/test_artifact_standards.py +259 -0
  36. {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_cli.py +1 -0
  37. {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_contracts.py +11 -0
  38. {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_distribution.py +13 -1
  39. {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_lifecycle.py +18 -0
  40. engineering_process-2.1.0/RELEASE_NOTES.md +0 -23
  41. engineering_process-2.1.0/templates/PULL_REQUEST_TEMPLATE.md +0 -39
  42. {engineering_process-2.1.0 → engineering_process-2.2.0}/LICENSE +0 -0
  43. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/__main__.py +0 -0
  44. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/_supervisor_contract.py +0 -0
  45. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/_supervisor_posix.py +0 -0
  46. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/_supervisor_windows.py +0 -0
  47. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/_windows_job.py +0 -0
  48. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/commands.py +0 -0
  49. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/helper_launch.py +0 -0
  50. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/lifecycle.py +0 -0
  51. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/production_engineering.py +0 -0
  52. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/project.py +0 -0
  53. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/release.py +0 -0
  54. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/repository.py +0 -0
  55. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/requirements-dev.txt +0 -0
  56. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/requirements-runtime.txt +0 -0
  57. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/skills.py +0 -0
  58. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/supervision.py +0 -0
  59. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process.egg-info/dependency_links.txt +0 -0
  60. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process.egg-info/entry_points.txt +0 -0
  61. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process.egg-info/requires.txt +0 -0
  62. {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process.egg-info/top_level.txt +0 -0
  63. {engineering_process-2.1.0 → engineering_process-2.2.0}/process-graph.json +0 -0
  64. {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/change-implement/SKILL.md +0 -0
  65. {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/change-plan/SKILL.md +0 -0
  66. {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/change-review/SKILL.md +0 -0
  67. {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/deliver-change/SKILL.md +0 -0
  68. {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/process-improve/SKILL.md +0 -0
  69. {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/production-engineering/invariants.json +0 -0
  70. {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/change.schema.json +0 -0
  71. {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/plan.schema.json +0 -0
  72. {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/process-graph.schema.json +0 -0
  73. {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/process-lock.schema.json +0 -0
  74. {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/production-engineering.schema.json +0 -0
  75. {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/project-legacy.schema.json +0 -0
  76. {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/project.schema.json +0 -0
  77. {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/receipt.schema.json +0 -0
  78. {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/release-change.schema.json +0 -0
  79. {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/release.schema.json +0 -0
  80. {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/review.schema.json +0 -0
  81. {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/run.schema.json +0 -0
  82. {engineering_process-2.1.0 → engineering_process-2.2.0}/setup.cfg +0 -0
  83. {engineering_process-2.1.0 → engineering_process-2.2.0}/templates/AGENTS.process.md +0 -0
  84. {engineering_process-2.1.0 → engineering_process-2.2.0}/templates/adopt-process-windows-job.py +0 -0
  85. {engineering_process-2.1.0 → engineering_process-2.2.0}/templates/adopt-process.py +0 -0
  86. {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_automation.py +0 -0
  87. {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_commands.py +0 -0
  88. {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_production_engineering.py +0 -0
  89. {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_publication_compat.py +0 -0
  90. {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_publication_workflow.py +0 -0
  91. {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_pypi_cache_horizon.py +0 -0
  92. {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_release.py +0 -0
  93. {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_repository.py +0 -0
  94. {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_skills.py +0 -0
  95. {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_supervisor_posix.py +0 -0
@@ -1,5 +1,6 @@
1
1
  include process-graph.json
2
2
  include RELEASE_NOTES.md
3
3
  recursive-include process_assets/skills SKILL.md *.json
4
+ recursive-include process_assets/standards *.json
4
5
  recursive-include schemas *.json
5
6
  recursive-include templates *.md *.py *.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-process
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: A small agent-neutral engineering lifecycle with managed adoption
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/phuongnse/engineering-process
@@ -270,6 +270,11 @@ The transaction writes only managed surfaces:
270
270
 
271
271
  It removes obsolete skills named by the previous process lock and preserves
272
272
  consumer-owned skills and instructions. Applying the same version twice is a no-op.
273
+ Consumer-owned `.process/standards.json` selections and override definitions are also
274
+ preserved; the managed PR template follows the effective supported standard. See
275
+ [consumer document standards](ARTIFACT_STANDARDS.md).
276
+ [Automation naming](ARTIFACT_STANDARDS.md#automation-names) uses the same versioned
277
+ selection and override mechanism; consumers apply it in their bootstrap and provider checks.
273
278
  The legacy managed runner can enter 1.0 directly, so consumers do not need a chain of
274
279
  per-version migration documents. The same transaction deletes the retired migration
275
280
  directory and standing automation policy; the Windows Job Object helper remains a
@@ -482,7 +487,7 @@ supported review schemas; ordinary legacy non-blocking observations remain reada
482
487
 
483
488
  ### Public pull-request evidence
484
489
 
485
- The managed pull-request template keeps public assurance separate from local
490
+ The default pull-request standard keeps public assurance separate from local
486
491
  lifecycle identity. Its five sections and labeled fields are ordered and stable:
487
492
  outcome and scope; source, risk, compatibility, and stack; profiles, snapshot, and
488
493
  completion receipt; verdict, cycles, blocking status, and non-blocking dispositions;
@@ -492,10 +497,15 @@ handle, or local `.process/runs` path. Those values remain in lifecycle state, w
492
497
  they enforce self-review rejection but do not pretend to be provider-authenticated
493
498
  review identities.
494
499
 
495
- `processctl publication validate-pr` checks that public contract deterministically.
500
+ The template and validator derive this contract from the same versioned definition.
501
+ Consumers can select a supported override through `.process/standards.json`; see
502
+ [generation, verification and custom-format boundaries](ARTIFACT_STANDARDS.md).
503
+ `processctl publication validate-pr` checks that selected contract deterministically.
496
504
  It rejects missing, repeated, misplaced, hidden, unordered, or unsupported visible
497
505
  structure. Completion checkboxes belong only to the Completion gate section. Ready
498
- pull requests must have every checkbox checked; drafts may retain unchecked work.
506
+ pull requests must have every checkbox checked and no unresolved default placeholder
507
+ values; drafts may retain pending fields and unchecked work. The author/coordinator
508
+ replaces them with actual evidence before ready/merge; the reviewer supplies the verdict.
499
509
  One trailing `Refs ISSUE.` line remains optional. A ready, contract-identified final
500
510
  consumer adoption may instead use `Closes ISSUE, closes OWNER/REPOSITORY#NUMBER.` with
501
511
  the complete keyword/reference syntax repeated for every issue; drafts cannot close
@@ -555,17 +565,19 @@ authorizes that merge.
555
565
  This repository opts in through .github/renovate.json, so it receives the same
556
566
  adoption PR as every other consumer. See SELF_HOSTING.md and RELEASING.md.
557
567
 
558
- The optional [Renovate preset](templates/renovate.json) is generated from the
559
- canonical public template. It supplies only `prHeader` and `prBodyTemplate`;
568
+ The optional [Renovate preset](templates/renovate.json) and public template are generated
569
+ from the packaged PR standard. It supplies only `prHeader` and `prBodyTemplate`;
560
570
  dependency selection, supported platforms, schedules, major-update approval,
561
571
  commands, draft policy, and merge authority remain consumer-owned. Regenerate it
562
572
  with `python verification/generate_renovate_preset.py`; `--check` rejects drift.
573
+ Consumers overriding that standard can generate a matching preset with
574
+ `processctl artifact renovate-preset`; wire it through their own Renovate configuration.
563
575
 
564
576
  Consumers add `github>phuongnse/engineering-process//templates/renovate#COMMIT_SHA`
565
577
  to their existing `extends` array, replacing `COMMIT_SHA` with the full source
566
578
  commit of a verified release that contains the preset. Remove obsolete inline
567
579
  `prHeader` and `prBodyTemplate` overrides, including matching package-rule overrides.
568
- The preset targets the canonical draft grammar used by 1.2.4 and this distribution;
580
+ The default preset targets the draft grammar used by 1.2.4 and this distribution;
569
581
  it does not claim compatibility with earlier publication contracts. A future
570
582
  grammar change must preserve this adapter or ship an explicit consumer migration.
571
583
 
@@ -246,6 +246,11 @@ The transaction writes only managed surfaces:
246
246
 
247
247
  It removes obsolete skills named by the previous process lock and preserves
248
248
  consumer-owned skills and instructions. Applying the same version twice is a no-op.
249
+ Consumer-owned `.process/standards.json` selections and override definitions are also
250
+ preserved; the managed PR template follows the effective supported standard. See
251
+ [consumer document standards](ARTIFACT_STANDARDS.md).
252
+ [Automation naming](ARTIFACT_STANDARDS.md#automation-names) uses the same versioned
253
+ selection and override mechanism; consumers apply it in their bootstrap and provider checks.
249
254
  The legacy managed runner can enter 1.0 directly, so consumers do not need a chain of
250
255
  per-version migration documents. The same transaction deletes the retired migration
251
256
  directory and standing automation policy; the Windows Job Object helper remains a
@@ -458,7 +463,7 @@ supported review schemas; ordinary legacy non-blocking observations remain reada
458
463
 
459
464
  ### Public pull-request evidence
460
465
 
461
- The managed pull-request template keeps public assurance separate from local
466
+ The default pull-request standard keeps public assurance separate from local
462
467
  lifecycle identity. Its five sections and labeled fields are ordered and stable:
463
468
  outcome and scope; source, risk, compatibility, and stack; profiles, snapshot, and
464
469
  completion receipt; verdict, cycles, blocking status, and non-blocking dispositions;
@@ -468,10 +473,15 @@ handle, or local `.process/runs` path. Those values remain in lifecycle state, w
468
473
  they enforce self-review rejection but do not pretend to be provider-authenticated
469
474
  review identities.
470
475
 
471
- `processctl publication validate-pr` checks that public contract deterministically.
476
+ The template and validator derive this contract from the same versioned definition.
477
+ Consumers can select a supported override through `.process/standards.json`; see
478
+ [generation, verification and custom-format boundaries](ARTIFACT_STANDARDS.md).
479
+ `processctl publication validate-pr` checks that selected contract deterministically.
472
480
  It rejects missing, repeated, misplaced, hidden, unordered, or unsupported visible
473
481
  structure. Completion checkboxes belong only to the Completion gate section. Ready
474
- pull requests must have every checkbox checked; drafts may retain unchecked work.
482
+ pull requests must have every checkbox checked and no unresolved default placeholder
483
+ values; drafts may retain pending fields and unchecked work. The author/coordinator
484
+ replaces them with actual evidence before ready/merge; the reviewer supplies the verdict.
475
485
  One trailing `Refs ISSUE.` line remains optional. A ready, contract-identified final
476
486
  consumer adoption may instead use `Closes ISSUE, closes OWNER/REPOSITORY#NUMBER.` with
477
487
  the complete keyword/reference syntax repeated for every issue; drafts cannot close
@@ -531,17 +541,19 @@ authorizes that merge.
531
541
  This repository opts in through .github/renovate.json, so it receives the same
532
542
  adoption PR as every other consumer. See SELF_HOSTING.md and RELEASING.md.
533
543
 
534
- The optional [Renovate preset](templates/renovate.json) is generated from the
535
- canonical public template. It supplies only `prHeader` and `prBodyTemplate`;
544
+ The optional [Renovate preset](templates/renovate.json) and public template are generated
545
+ from the packaged PR standard. It supplies only `prHeader` and `prBodyTemplate`;
536
546
  dependency selection, supported platforms, schedules, major-update approval,
537
547
  commands, draft policy, and merge authority remain consumer-owned. Regenerate it
538
548
  with `python verification/generate_renovate_preset.py`; `--check` rejects drift.
549
+ Consumers overriding that standard can generate a matching preset with
550
+ `processctl artifact renovate-preset`; wire it through their own Renovate configuration.
539
551
 
540
552
  Consumers add `github>phuongnse/engineering-process//templates/renovate#COMMIT_SHA`
541
553
  to their existing `extends` array, replacing `COMMIT_SHA` with the full source
542
554
  commit of a verified release that contains the preset. Remove obsolete inline
543
555
  `prHeader` and `prBodyTemplate` overrides, including matching package-rule overrides.
544
- The preset targets the canonical draft grammar used by 1.2.4 and this distribution;
556
+ The default preset targets the draft grammar used by 1.2.4 and this distribution;
545
557
  it does not claim compatibility with earlier publication contracts. A future
546
558
  grammar change must preserve this adapter or ship an explicit consumer migration.
547
559
 
@@ -0,0 +1,18 @@
1
+ # Engineering Process v2.2.0
2
+
3
+ Changes since v2.1.0.
4
+
5
+ ## Features
6
+
7
+ - Generate and verify PR descriptions and release notes from versioned standards with consumer\-owned overrides preserved by adoption\. Matching template and validator definitions reject unresolved default PR placeholders in ready state\; custom formats retain consumer\-owned validation profiles\. ([#174](https://github.com/phuongnse/engineering-process/issues/174))
8
+ - Generate and verify automation names from a default owner\-role convention or a consumer\-owned override\. Bootstrap integrations can consume structured CLI output and check actual provider names before side effects\; provider limits and authenticated identity remain consumer\-owned\. ([#176](https://github.com/phuongnse/engineering-process/issues/176))
9
+
10
+ ## Upgrade and compatibility
11
+
12
+ Merge the complete hash-locked package/adoption PR, update the local and CI environments to the selected version, and start a fresh agent session.
13
+
14
+ Consumer CI, naming conventions and branch-protection settings remain consumer-owned; adoption does not configure them automatically.
15
+
16
+ See [versioning and compatibility](https://github.com/phuongnse/engineering-process/blob/v2.2.0/VERSIONING.md) and [adoption guidance](https://github.com/phuongnse/engineering-process/blob/v2.2.0/SELF_HOSTING.md).
17
+
18
+ [Full change comparison](https://github.com/phuongnse/engineering-process/compare/v2.1.0...v2.2.0)
@@ -1,3 +1,3 @@
1
1
  """Agent-neutral engineering process."""
2
2
 
3
- VERSION = "2.1.0"
3
+ VERSION = "2.2.0"
@@ -12,6 +12,7 @@ import tempfile
12
12
  from typing import Any, Callable
13
13
 
14
14
  from . import VERSION
15
+ from .artifact_standards import load_standard_catalog
15
16
  from .contracts import ProcessError, read_json, validate_document
16
17
  from .distribution import (
17
18
  distribution_digest,
@@ -21,6 +22,7 @@ from .distribution import (
21
22
  skills_root,
22
23
  )
23
24
  from .project import normalize_project, project_path
25
+ from .pr_description import render_template
24
26
 
25
27
 
26
28
  MAX_REQUIREMENTS_BYTES = 2_000_000
@@ -253,9 +255,8 @@ def _expected_files(
253
255
  "utf-8"
254
256
  )
255
257
 
256
- pull_request_template = (
257
- process_root / "templates" / "PULL_REQUEST_TEMPLATE.md"
258
- ).read_text(encoding="utf-8")
258
+ standards = load_standard_catalog(project_root, process_root)
259
+ pull_request_template = render_template(standards.resolve("pull-request"), process_root=process_root)
259
260
  pull_request_path = project_root / ".github" / "PULL_REQUEST_TEMPLATE.md"
260
261
  existing_pull_request = (
261
262
  pull_request_path.read_text(encoding="utf-8")
@@ -301,6 +302,12 @@ def _expected_files(
301
302
  writes[Path(".process/process.lock")] = (
302
303
  json.dumps(lock, indent=2, sort_keys=True) + "\n"
303
304
  ).encode("utf-8")
305
+ collisions = standards.consumer_files.intersection(writes.keys() | deletions)
306
+ if collisions:
307
+ raise ProcessError(
308
+ "consumer standard files conflict with managed adoption paths; move the definitions and update the selection: "
309
+ + ", ".join(sorted(path.as_posix() for path in collisions))
310
+ )
304
311
  total = sum(len(value) for value in writes.values())
305
312
  if total > MAX_MANAGED_BYTES:
306
313
  raise ProcessError("managed adoption output exceeds its aggregate limit")
@@ -0,0 +1,147 @@
1
+ """Resolve one versioned artifact contract for both generation and verification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import os
7
+ from pathlib import Path
8
+ import re
9
+ from typing import Any
10
+
11
+ from .contracts import ProcessError, digest_json, load_and_validate
12
+ from .distribution import schemas_root
13
+ from .repository import STATE_PREFIXES, _git
14
+
15
+
16
+ IDENTIFIER = re.compile(r"[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?")
17
+ MAX_DOCUMENT_BYTES = 1_000_000
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class ArtifactStandard:
22
+ document: dict[str, Any]
23
+ source: str
24
+
25
+ @property
26
+ def rules(self) -> dict[str, Any]:
27
+ return self.document["rules"]
28
+
29
+ @property
30
+ def metadata(self) -> dict[str, Any]:
31
+ return {
32
+ **{key: self.document[key] for key in ("id", "version", "artifact", "adapter")},
33
+ "digest": digest_json(self.document),
34
+ "source": self.source,
35
+ }
36
+
37
+ def unresolved(self, value: str) -> bool:
38
+ # These are reserved values in an owned protocol, not prose classification.
39
+ return value.strip().casefold() in {
40
+ item.strip().casefold() for item in self.document.get("pendingValues", [])
41
+ }
42
+
43
+
44
+ def _consumer_file(root: Path, relative: str) -> Path:
45
+ path = root / relative
46
+ if not path.resolve().is_relative_to(root):
47
+ raise ProcessError(f"standard path escapes the consumer repository: {relative}")
48
+ if path.resolve() != path or Path(relative).as_posix() != relative:
49
+ raise ProcessError(f"standard path must be canonical without links: {relative}")
50
+ if any(part in {"..", ".git"} for part in Path(relative).parts):
51
+ raise ProcessError(f"standard path is not an owned repository file: {relative}")
52
+ current = root
53
+ for part in Path(relative).parts:
54
+ current /= part
55
+ if current.is_symlink():
56
+ raise ProcessError(f"standard paths cannot traverse symlinks: {relative}")
57
+ if not path.is_file():
58
+ raise ProcessError(f"consumer standard file is missing: {relative}")
59
+ encoded = os.fsencode(Path(relative).as_posix())
60
+ if any(encoded.startswith(prefix) for prefix in STATE_PREFIXES):
61
+ raise ProcessError(f"standard file is excluded from lifecycle snapshots: {relative}")
62
+ inventory = _git(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", relative])
63
+ if encoded not in inventory.split(b"\0"):
64
+ raise ProcessError(f"standard file must be included in the consumer Git snapshot: {relative}")
65
+ return path
66
+
67
+
68
+ def _unique(values: list[str], description: str) -> None:
69
+ if len(values) != len(set(values)):
70
+ raise ProcessError(f"artifact standard requires unique {description}")
71
+
72
+
73
+ def _validate_relations(document: dict[str, Any]) -> None:
74
+ _unique([value.strip().casefold() for value in document.get("pendingValues", [])], "pending values")
75
+ rules = document["rules"]
76
+ if document["adapter"] == "pr-description":
77
+ sections = rules["sections"]
78
+ _unique([section["heading"] for section in sections], "section headings")
79
+ for kind in ("fields", "checks"):
80
+ entries = [entry for section in sections for entry in section[kind]]
81
+ _unique([entry["id"] for entry in entries], f"{kind} ids")
82
+ _unique([entry["label"] for entry in entries], f"{kind} labels")
83
+ elif document["adapter"] == "release-notes":
84
+ _unique([group["type"] for group in rules["groups"]], "change types")
85
+ _unique([section["id"] for section in rules["sections"]], "release section ids")
86
+ _unique([item["heading"] for item in rules["groups"] + rules["sections"]], "release headings")
87
+
88
+
89
+ def read_document(path: Path) -> bytes:
90
+ with path.open("rb") as stream:
91
+ data = stream.read(MAX_DOCUMENT_BYTES + 1)
92
+ if len(data) > MAX_DOCUMENT_BYTES:
93
+ raise ProcessError("artifact body exceeds its size limit")
94
+ data.decode("utf-8")
95
+ return data
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class StandardCatalog:
100
+ """One selection snapshot for adapter resolution and adoption ownership checks."""
101
+
102
+ project_root: Path | None
103
+ process_root: Path
104
+ selections: dict[str, Any]
105
+ consumer_files: frozenset[Path]
106
+
107
+ def resolve(self, artifact: str) -> ArtifactStandard:
108
+ if len(artifact) > 128 or not IDENTIFIER.fullmatch(artifact):
109
+ raise ProcessError("artifact must be a canonical identifier")
110
+ selection = self.selections.get(artifact, {"builtin": f"{artifact}@1"})
111
+ if "path" in selection:
112
+ path = _consumer_file(self.project_root, selection["path"])
113
+ source = selection["path"]
114
+ else:
115
+ name, version = selection["builtin"].rsplit("@", 1)
116
+ path = self.process_root / "process_assets" / "standards" / f"{name}.v{version}.json"
117
+ source = selection["builtin"]
118
+ if path.is_symlink() or not path.is_file():
119
+ raise ProcessError(f"unsupported packaged artifact standard: {source}")
120
+ document = load_and_validate(path, "artifact-standard", schema_root=schemas_root(self.process_root))
121
+ if document["artifact"] != artifact:
122
+ raise ProcessError(f"selected standard is for {document['artifact']}, not {artifact}")
123
+ if "builtin" in selection and (name != artifact or document["version"] != int(version)):
124
+ raise ProcessError("packaged standard identity does not match its selection")
125
+ _validate_relations(document)
126
+ return ArtifactStandard(document, source)
127
+
128
+
129
+ def load_standard_catalog(project_root: Path | None, process_root: Path) -> StandardCatalog:
130
+ selections: dict[str, Any] = {}
131
+ consumer_files: set[Path] = set()
132
+ if project_root is not None:
133
+ project_root = project_root.resolve()
134
+ selector = project_root / ".process" / "standards.json"
135
+ if selector.exists() or selector.is_symlink():
136
+ document = load_and_validate(
137
+ _consumer_file(project_root, ".process/standards.json"),
138
+ "artifact-selection", schema_root=schemas_root(process_root),
139
+ )
140
+ selections = document["artifacts"]
141
+ consumer_files = {Path(".process/standards.json")}
142
+ consumer_files.update(Path(value["path"]) for value in selections.values() if "path" in value)
143
+ return StandardCatalog(project_root, process_root, selections, frozenset(consumer_files))
144
+
145
+
146
+ def resolve_standard(project_root: Path | None, process_root: Path, artifact: str) -> ArtifactStandard:
147
+ return load_standard_catalog(project_root, process_root).resolve(artifact)
@@ -0,0 +1,31 @@
1
+ """Compose automation names from a selected convention and consumer-owned components."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from .artifact_standards import ArtifactStandard
7
+ from .contracts import ProcessError, validate_document
8
+ from .distribution import distribution_root, schemas_root
9
+
10
+
11
+ def render_name(
12
+ standard: ArtifactStandard,
13
+ data: dict[str, Any],
14
+ *,
15
+ state: str = "ready",
16
+ process_root: Path | None = None,
17
+ ) -> str:
18
+ if standard.document["adapter"] != "automation-name":
19
+ raise ProcessError("selected standard requires a different name adapter")
20
+ validate_document(data, "automation-name-data", schema_root=schemas_root(distribution_root(process_root)))
21
+ if state not in {"draft", "ready"}:
22
+ raise ProcessError("artifact state must be draft or ready")
23
+ rules = standard.rules
24
+ if set(data["components"]) != set(rules["components"]):
25
+ raise ProcessError("name components must exactly match the selected standard")
26
+ name = rules["separator"].join(data["components"][key] for key in rules["components"])
27
+ if rules["case"] == "lower":
28
+ name = name.lower()
29
+ if len(name) > rules["maxLength"]:
30
+ raise ProcessError("automation name exceeds the selected length limit")
31
+ return name + "\n"
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
+ import hashlib
6
7
  import json
7
8
  from pathlib import Path
8
9
  import sys
@@ -10,10 +11,14 @@ from typing import Any, Callable
10
11
 
11
12
  from . import VERSION
12
13
  from .adoption import apply_adoption, check_adoption
14
+ from .automation_name import render_name
15
+ from .artifact_standards import MAX_DOCUMENT_BYTES, read_document, resolve_standard
13
16
  from .commands import run_check, run_profile
14
17
  from .contracts import (
15
18
  CONTRACT_KINDS,
16
19
  ProcessError,
20
+ digest_json,
21
+ formatted_json_bytes,
17
22
  load_and_validate,
18
23
  read_json,
19
24
  validate_document,
@@ -47,6 +52,8 @@ from .publication_compat import (
47
52
  validate_range,
48
53
  )
49
54
  from .release import validate_release
55
+ from .pr_description import body_issues, render_description, render_renovate_preset, render_template
56
+ from .release_notes import render_notes
50
57
  from .repository import repository_snapshot, same_checkpoint
51
58
  from .skills import validate_skills
52
59
 
@@ -391,11 +398,54 @@ def command_publication(args: argparse.Namespace) -> Result:
391
398
  branch=args.branch,
392
399
  state=args.state,
393
400
  body_path=args.body_file,
401
+ project_root=args.project_root,
402
+ process_root=_process_root(args),
394
403
  )
395
404
  status = "passed" if not details["issues"] else "failed"
396
405
  return _result(f"publication {name}", status=status, **details), (0 if status == "passed" else 1)
397
406
 
398
407
 
408
+ def command_artifact(args: argparse.Namespace) -> Result:
409
+ process_root = _process_root(args)
410
+ standard = resolve_standard(args.project_root, process_root, args.artifact)
411
+ details: dict[str, Any] = {"standard": standard.metadata}
412
+ operation = args.artifact_command
413
+ issues: list[str] = []
414
+ if operation == "show":
415
+ payload = formatted_json_bytes(standard.document)
416
+ if args.output is None:
417
+ details["definition"] = standard.document
418
+ elif operation == "template":
419
+ payload = render_template(standard, process_root=process_root).encode("utf-8")
420
+ elif operation == "renovate-preset":
421
+ payload = formatted_json_bytes(render_renovate_preset(standard, process_root=process_root))
422
+ else:
423
+ data = load_and_validate(args.data_file, standard.document["adapter"] + "-data", schema_root=schemas_root(process_root)) if args.data_file is not None else None
424
+ if data is not None:
425
+ renderer = {"pr-description": render_description, "release-notes": render_notes, "automation-name": render_name}[standard.document["adapter"]]
426
+ payload = renderer(standard, data, state=args.state, process_root=process_root).encode("utf-8")
427
+ details["dataDigest"] = digest_json(data)
428
+ elif operation == "render" or standard.document["adapter"] != "pr-description":
429
+ raise ProcessError("this artifact operation requires --data-file")
430
+ if operation == "validate":
431
+ actual = read_document(args.body_file)
432
+ if data is None:
433
+ issues = body_issues(actual.decode("utf-8"), args.state, standard)
434
+ elif actual != payload:
435
+ issues.append("artifact bytes differ from the selected standard and input data")
436
+ payload = actual
437
+ if len(payload) > MAX_DOCUMENT_BYTES:
438
+ raise ProcessError("artifact output exceeds its size limit")
439
+ details["artifactDigest"] = "sha256:" + hashlib.sha256(payload).hexdigest()
440
+ if operation != "validate" and args.output is not None:
441
+ args.output.write_bytes(payload)
442
+ details["output"] = str(args.output)
443
+ elif operation == "render":
444
+ details["content"] = payload.decode("utf-8")
445
+ status = "failed" if issues else "passed"
446
+ return _result(f"artifact {operation}", status, **details, issues=issues), (1 if issues else 0)
447
+
448
+
399
449
  def _add_common(parser: argparse.ArgumentParser, *, project: bool = True) -> None:
400
450
  if project:
401
451
  parser.add_argument("--project-root", type=_root, default=Path.cwd())
@@ -495,6 +545,19 @@ def build_parser() -> argparse.ArgumentParser:
495
545
  release_validate = _leaf(release_commands, "validate", command_release_validate)
496
546
  release_validate.add_argument("--tag")
497
547
 
548
+ artifact = commands.add_parser("artifact", help="Generate and verify consumer-selected artifact standards")
549
+ artifact_commands = artifact.add_subparsers(dest="artifact_command", required=True)
550
+ for name in ("show", "template", "renovate-preset", "render", "validate"):
551
+ leaf = _leaf(artifact_commands, name, command_artifact)
552
+ leaf.add_argument("--artifact", required=True)
553
+ if name in {"render", "validate"}:
554
+ leaf.add_argument("--state", choices=("draft", "ready"), default="ready")
555
+ leaf.add_argument("--data-file", type=Path, required=name == "render")
556
+ if name == "validate":
557
+ leaf.add_argument("--body-file", type=Path, required=True)
558
+ else:
559
+ leaf.add_argument("--output", type=Path, required=name not in {"show", "render"})
560
+
498
561
  publication = commands.add_parser("publication", help=argparse.SUPPRESS)
499
562
  publication_commands = publication.add_subparsers(
500
563
  dest="publication_command", required=True
@@ -511,7 +574,7 @@ def build_parser() -> argparse.ArgumentParser:
511
574
  publication_range.add_argument("--branch", required=True)
512
575
  publication_range.add_argument("--range", dest="range_spec", required=True)
513
576
  publication_pr = _leaf(
514
- publication_commands, "validate-pr", command_publication, project=False
577
+ publication_commands, "validate-pr", command_publication
515
578
  )
516
579
  publication_pr.add_argument("--title", required=True)
517
580
  publication_pr.add_argument("--branch", required=True)
@@ -17,16 +17,22 @@ from jsonschema import Draft202012Validator
17
17
 
18
18
  MAX_JSON_BYTES = 2_000_000
19
19
  CONTRACT_KINDS = (
20
+ "artifact-selection",
21
+ "artifact-standard",
22
+ "automation-name-data",
20
23
  "change",
21
24
  "plan",
22
25
  "process-graph",
23
26
  "process-lock",
24
27
  "production-engineering",
28
+ "pr-description-data",
25
29
  "project",
26
30
  "project-legacy",
27
31
  "receipt",
28
32
  "release-change",
29
33
  "release",
34
+ "release-notes-data",
35
+ "renovate-preset",
30
36
  "review",
31
37
  "run",
32
38
  )
@@ -18,8 +18,8 @@ MAX_DISTRIBUTION_BYTES = 10_000_000
18
18
  def distribution_root(explicit: Path | None = None) -> Path:
19
19
  if explicit is not None:
20
20
  root = explicit.resolve()
21
- if not (root / "process_assets" / "skills").is_dir():
22
- raise ProcessError(f"{root}: process_assets/skills is missing")
21
+ if not skills_root(root).is_dir():
22
+ raise ProcessError(f"{root}: process_assets/skills or skills is missing")
23
23
  return root
24
24
 
25
25
  source = Path(__file__).resolve().parent.parent
@@ -77,6 +77,9 @@ def _asset_paths(root: Path) -> Iterable[tuple[str, Path]]:
77
77
  schema_directory = schemas_root(root)
78
78
  for path in _ordered_paths(schema_directory, schema_directory.glob("*.json")):
79
79
  yield f"schemas/{path.name}", path
80
+ standard_directory = root / "process_assets" / "standards"
81
+ for path in _ordered_paths(standard_directory, standard_directory.glob("*.json")):
82
+ yield f"standards/{path.name}", path
80
83
  for name in (
81
84
  "AGENTS.process.md",
82
85
  "PULL_REQUEST_TEMPLATE.md",