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.
- {engineering_process-2.1.0 → engineering_process-2.2.0}/MANIFEST.in +1 -0
- {engineering_process-2.1.0/engineering_process.egg-info → engineering_process-2.2.0}/PKG-INFO +19 -7
- {engineering_process-2.1.0 → engineering_process-2.2.0}/README.md +18 -6
- engineering_process-2.2.0/RELEASE_NOTES.md +18 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/__init__.py +1 -1
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/adoption.py +10 -3
- engineering_process-2.2.0/engineering_process/artifact_standards.py +147 -0
- engineering_process-2.2.0/engineering_process/automation_name.py +31 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/cli.py +64 -1
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/contracts.py +6 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/distribution.py +5 -2
- engineering_process-2.1.0/engineering_process/publication_compat.py → engineering_process-2.2.0/engineering_process/pr_description.py +99 -138
- engineering_process-2.2.0/engineering_process/publication_compat.py +119 -0
- engineering_process-2.2.0/engineering_process/release_notes.py +76 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0/engineering_process.egg-info}/PKG-INFO +19 -7
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process.egg-info/SOURCES.txt +14 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/change-complete/SKILL.md +8 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/change-start/SKILL.md +4 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/change-verify/SKILL.md +7 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/production-engineering/SKILL.md +7 -0
- engineering_process-2.2.0/process_assets/standards/automation-name.v1.json +16 -0
- engineering_process-2.2.0/process_assets/standards/pull-request.v1.json +129 -0
- engineering_process-2.2.0/process_assets/standards/release-notes.v1.json +34 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/pyproject.toml +12 -1
- engineering_process-2.2.0/schemas/artifact-selection.schema.json +67 -0
- engineering_process-2.2.0/schemas/artifact-standard.schema.json +322 -0
- engineering_process-2.2.0/schemas/automation-name-data.schema.json +35 -0
- engineering_process-2.2.0/schemas/pr-description-data.schema.json +63 -0
- engineering_process-2.2.0/schemas/release-notes-data.schema.json +103 -0
- engineering_process-2.2.0/schemas/renovate-preset.schema.json +30 -0
- engineering_process-2.2.0/templates/PULL_REQUEST_TEMPLATE.md +36 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/templates/renovate.json +1 -1
- {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_adoption.py +60 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_architecture.py +5 -1
- engineering_process-2.2.0/tests/test_artifact_standards.py +259 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_cli.py +1 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_contracts.py +11 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_distribution.py +13 -1
- {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_lifecycle.py +18 -0
- engineering_process-2.1.0/RELEASE_NOTES.md +0 -23
- engineering_process-2.1.0/templates/PULL_REQUEST_TEMPLATE.md +0 -39
- {engineering_process-2.1.0 → engineering_process-2.2.0}/LICENSE +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/__main__.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/_supervisor_contract.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/_supervisor_posix.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/_supervisor_windows.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/_windows_job.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/commands.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/helper_launch.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/lifecycle.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/production_engineering.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/project.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/release.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/repository.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/requirements-dev.txt +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/requirements-runtime.txt +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/skills.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process/supervision.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process.egg-info/dependency_links.txt +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process.egg-info/entry_points.txt +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process.egg-info/requires.txt +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/engineering_process.egg-info/top_level.txt +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/process-graph.json +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/change-implement/SKILL.md +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/change-plan/SKILL.md +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/change-review/SKILL.md +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/deliver-change/SKILL.md +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/process-improve/SKILL.md +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/process_assets/skills/production-engineering/invariants.json +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/change.schema.json +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/plan.schema.json +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/process-graph.schema.json +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/process-lock.schema.json +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/production-engineering.schema.json +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/project-legacy.schema.json +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/project.schema.json +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/receipt.schema.json +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/release-change.schema.json +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/release.schema.json +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/review.schema.json +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/schemas/run.schema.json +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/setup.cfg +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/templates/AGENTS.process.md +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/templates/adopt-process-windows-job.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/templates/adopt-process.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_automation.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_commands.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_production_engineering.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_publication_compat.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_publication_workflow.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_pypi_cache_horizon.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_release.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_repository.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_skills.py +0 -0
- {engineering_process-2.1.0 → engineering_process-2.2.0}/tests/test_supervisor_posix.py +0 -0
{engineering_process-2.1.0/engineering_process.egg-info → engineering_process-2.2.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: engineering-process
|
|
3
|
-
Version: 2.
|
|
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
|
|
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
|
-
|
|
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
|
|
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)
|
|
559
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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)
|
|
535
|
-
|
|
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
|
|
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)
|
|
@@ -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
|
-
|
|
257
|
-
|
|
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
|
|
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
|
|
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",
|