engineering-process 2.2.0__tar.gz → 2.3.1__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.2.0/engineering_process.egg-info → engineering_process-2.3.1}/PKG-INFO +70 -7
- {engineering_process-2.2.0 → engineering_process-2.3.1}/README.md +69 -6
- engineering_process-2.3.1/RELEASE_NOTES.md +18 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/__init__.py +1 -1
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/_supervisor_posix.py +10 -4
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/_supervisor_windows.py +2 -1
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/artifact_standards.py +8 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/cli.py +22 -5
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/commands.py +6 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/contracts.py +15 -3
- engineering_process-2.3.1/engineering_process/issue.py +112 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/lifecycle.py +109 -3
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/project.py +5 -0
- engineering_process-2.3.1/engineering_process/publication_compat.py +58 -0
- engineering_process-2.3.1/engineering_process/review_contexts.py +291 -0
- engineering_process-2.2.0/engineering_process/publication_compat.py → engineering_process-2.3.1/engineering_process/source_publication.py +55 -41
- {engineering_process-2.2.0 → engineering_process-2.3.1/engineering_process.egg-info}/PKG-INFO +70 -7
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process.egg-info/SOURCES.txt +7 -1
- {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/change-complete/SKILL.md +32 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/change-review/SKILL.md +40 -4
- {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/change-start/SKILL.md +7 -1
- {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/change-verify/SKILL.md +10 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/deliver-change/SKILL.md +29 -0
- engineering_process-2.3.1/process_assets/standards/issue.v1.json +78 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/pyproject.toml +3 -1
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/artifact-standard.schema.json +86 -1
- engineering_process-2.3.1/schemas/issue-data.schema.json +44 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/project.schema.json +8 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/receipt.schema.json +20 -2
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/run.schema.json +28 -1
- {engineering_process-2.2.0 → engineering_process-2.3.1}/templates/adopt-process.py +10 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_adoption.py +11 -2
- {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_architecture.py +3 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_artifact_standards.py +155 -1
- {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_automation.py +5 -15
- {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_cli.py +1 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_commands.py +100 -2
- {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_contracts.py +70 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_lifecycle.py +153 -1
- {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_publication_compat.py +101 -0
- engineering_process-2.3.1/tests/test_pypi_cache.py +184 -0
- engineering_process-2.3.1/tests/test_review_contexts.py +507 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_supervisor_posix.py +35 -0
- engineering_process-2.2.0/RELEASE_NOTES.md +0 -18
- engineering_process-2.2.0/tests/test_pypi_cache_horizon.py +0 -81
- {engineering_process-2.2.0 → engineering_process-2.3.1}/LICENSE +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/MANIFEST.in +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/__main__.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/_supervisor_contract.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/_windows_job.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/adoption.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/automation_name.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/distribution.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/helper_launch.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/pr_description.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/production_engineering.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/release.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/release_notes.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/repository.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/requirements-dev.txt +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/requirements-runtime.txt +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/skills.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/supervision.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process.egg-info/dependency_links.txt +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process.egg-info/entry_points.txt +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process.egg-info/requires.txt +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process.egg-info/top_level.txt +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/process-graph.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/change-implement/SKILL.md +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/change-plan/SKILL.md +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/process-improve/SKILL.md +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/production-engineering/SKILL.md +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/skills/production-engineering/invariants.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/standards/automation-name.v1.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/standards/pull-request.v1.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/process_assets/standards/release-notes.v1.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/artifact-selection.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/automation-name-data.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/change.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/plan.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/pr-description-data.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/process-graph.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/process-lock.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/production-engineering.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/project-legacy.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/release-change.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/release-notes-data.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/release.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/renovate-preset.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/schemas/review.schema.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/setup.cfg +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/templates/AGENTS.process.md +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/templates/PULL_REQUEST_TEMPLATE.md +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/templates/adopt-process-windows-job.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/templates/renovate.json +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_distribution.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_production_engineering.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_publication_workflow.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_release.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_repository.py +0 -0
- {engineering_process-2.2.0 → engineering_process-2.3.1}/tests/test_skills.py +0 -0
{engineering_process-2.2.0/engineering_process.egg-info → engineering_process-2.3.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: engineering-process
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.1
|
|
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
|
|
@@ -64,19 +64,56 @@ under .process/receipts. Both paths are ignored by Git. Verification evidence is
|
|
|
64
64
|
to HEAD plus a fingerprint of tracked and non-ignored untracked files. Any relevant
|
|
65
65
|
mutation invalidates it.
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
Neither the reviewer actor nor reviewer context may have implemented the current
|
|
68
|
+
cycle. An agent reviewer context also cannot be recorded in another accepted change
|
|
69
|
+
in this repository or its registered worktrees. There is no attestation hierarchy,
|
|
69
70
|
recommendation chain, authority-transition protocol, remote-evidence federation, or
|
|
70
71
|
second handwritten validator.
|
|
71
72
|
|
|
72
|
-
The coordinator hands
|
|
73
|
-
|
|
74
|
-
reviewer continues corrections. The reviewer inspects the change and authors its
|
|
73
|
+
The coordinator hands each new change to a newly spawned reviewer, with the accepted
|
|
74
|
+
source artifacts and no inherited implementation or other-change review conversation.
|
|
75
|
+
The same reviewer continues only that change's corrections. The reviewer inspects the change and authors its
|
|
75
76
|
own report. The runner's existing task/session interaction and returned result make
|
|
76
|
-
that work inspectable. Sharing a
|
|
77
|
+
that work inspectable. Sharing a provider or account is allowed; this is a
|
|
77
78
|
workflow for independent judgment, not authenticated identity or merge enforcement.
|
|
78
79
|
See [change-review](process_assets/skills/change-review/SKILL.md) for the handoff.
|
|
79
80
|
|
|
81
|
+
Start, submission and finish check schema-valid canonical run history with bounded
|
|
82
|
+
worktree/file reads. A short OS lock in common Git metadata serializes canonical
|
|
83
|
+
history writes and review checks; profiles run outside it and reload current state
|
|
84
|
+
before recording results. The lock stores no identity registry. The check rejects known context reuse,
|
|
85
|
+
but cannot establish native conversation freshness or inspect removed history,
|
|
86
|
+
other clones or other repositories. Keep native creation, non-inherited dispatch
|
|
87
|
+
and effective settings evidence in the existing handoff.
|
|
88
|
+
|
|
89
|
+
`processctl change review replace-reused` repairs only an initial pending assignment
|
|
90
|
+
proven to reuse another change's agent context, before a submitted review or normal
|
|
91
|
+
report file exists. It validates a fresh replacement and current verification,
|
|
92
|
+
preserves the prior assignment and conflict in canonical history, and leaves cycle
|
|
93
|
+
and evidence unchanged. Valid assignments and reviewed correction cycles cannot use
|
|
94
|
+
this operation. It does not provide an unrestricted reviewer reset.
|
|
95
|
+
|
|
96
|
+
Every delegated agent and reviewer follows the active user-selected model and
|
|
97
|
+
reasoning effort under [deliver-change's settings rule](process_assets/skills/deliver-change/SKILL.md#preserve-agent-execution-settings).
|
|
98
|
+
It covers fresh agents and resumed sessions, requires native runtime confirmation,
|
|
99
|
+
and permits no autonomous upgrade or downgrade. The portable guidance does not make
|
|
100
|
+
processctl a provider runtime or a model-quality evaluator.
|
|
101
|
+
|
|
102
|
+
Consumers using the packaged publication compatibility policy can enable
|
|
103
|
+
`lifecycle.publication.required: true` in `.process/project.json` after adopting a
|
|
104
|
+
version that supports it. Start then rejects an invalid branch before creating a
|
|
105
|
+
run. Finish checks the current branch, head subject, and pinned comparison-base-to-head
|
|
106
|
+
range, and writes publication metadata in a version 2 receipt. Version 1 receipts and
|
|
107
|
+
consumers without the opt-in remain supported. Missing pinned bases and mutations
|
|
108
|
+
during the check fail completion.
|
|
109
|
+
|
|
110
|
+
PR readiness additionally requires the selected ready-state body/title and exact
|
|
111
|
+
head/range checks. Follow the [completion preflight](process_assets/skills/change-complete/SKILL.md)
|
|
112
|
+
and repeat those checks in required CI. This repository's existing
|
|
113
|
+
`verification/verify_publication.py --pull-request` uses the actual PR metadata and
|
|
114
|
+
selected consumer standard; local lifecycle completion alone does not establish
|
|
115
|
+
external PR readiness. Integration-branch pushes remain consumer-owned operations.
|
|
116
|
+
|
|
80
117
|
Runtime architecture is enforced by semantic fitness functions, not module or source-
|
|
81
118
|
line quotas. Every module has an explicit dependency layer, imports point toward lower
|
|
82
119
|
layers, the internal graph remains acyclic, and lifecycle.py alone owns state
|
|
@@ -122,6 +159,20 @@ Commands are argument arrays, never shell strings. Each command has a finite tim
|
|
|
122
159
|
Output has a hard aggregate budget; evidence stores byte counts and hashes, never raw
|
|
123
160
|
stdout or stderr that could contain secrets.
|
|
124
161
|
|
|
162
|
+
For a fresh checkout of this repository, create a virtual environment with a supported
|
|
163
|
+
Python and install `engineering_process/requirements-runtime.txt`,
|
|
164
|
+
`engineering_process/requirements-dev.txt`, and
|
|
165
|
+
`engineering_process/requirements-build.txt` through that interpreter. Use
|
|
166
|
+
`.venv/Scripts/python.exe` on Windows or `.venv/bin/python` on POSIX to invoke the
|
|
167
|
+
source CLI and verification scripts. Other consumers own their runtime paths and
|
|
168
|
+
setup commands; installed consumers can use their environment's `processctl` entry
|
|
169
|
+
point without a source checkout.
|
|
170
|
+
|
|
171
|
+
The bounded runner prepends the invoking Python executable's directory to child
|
|
172
|
+
`PATH`. Bare commands therefore search that environment before the inherited path,
|
|
173
|
+
while declared argument arrays, explicit executable paths, secret filtering and
|
|
174
|
+
remaining path entries are preserved. This alignment does not install dependencies.
|
|
175
|
+
|
|
125
176
|
A failed profile also reports a safe `diagnostic` descriptor containing only its
|
|
126
177
|
validated profile identifier, check identifier, canonical one-based profile position,
|
|
127
178
|
and a fixed `processctl` argument array. The position remains unambiguous even when a
|
|
@@ -273,6 +324,8 @@ consumer-owned skills and instructions. Applying the same version twice is a no-
|
|
|
273
324
|
Consumer-owned `.process/standards.json` selections and override definitions are also
|
|
274
325
|
preserved; the managed PR template follows the effective supported standard. See
|
|
275
326
|
[consumer document standards](ARTIFACT_STANDARDS.md).
|
|
327
|
+
[Issue records](ARTIFACT_STANDARDS.md#issues) use the same default-and-override
|
|
328
|
+
mechanism for open requests and closed evidence while tracker actions remain consumer-owned.
|
|
276
329
|
[Automation naming](ARTIFACT_STANDARDS.md#automation-names) uses the same versioned
|
|
277
330
|
selection and override mechanism; consumers apply it in their bootstrap and provider checks.
|
|
278
331
|
The legacy managed runner can enter 1.0 directly, so consumers do not need a chain of
|
|
@@ -548,6 +601,16 @@ package rule keeps the adoption pull request in draft and runs exactly:
|
|
|
548
601
|
|
|
549
602
|
python .process/adopt-process.py --project-root . --requirements-lock requirements/process.txt
|
|
550
603
|
|
|
604
|
+
The bootstrap refreshes `engineering-process` index metadata using pip's
|
|
605
|
+
`--refresh-package` option, with `--no-cache-dir` for older pip. Exact pins, hashes,
|
|
606
|
+
isolated installation and bounded failures still apply. Consumer CI that installs
|
|
607
|
+
the lock directly must also request fresh metadata: use
|
|
608
|
+
`--refresh-package engineering-process` with pip 26.2 or newer, or the portable
|
|
609
|
+
`--no-cache-dir` option. Keep this setting on the process installation command;
|
|
610
|
+
other dependency caches do not need to be disabled. For non-isolated pip-compile,
|
|
611
|
+
the equivalent `PIP_REFRESH_PACKAGE=engineering-process` environment setting keeps
|
|
612
|
+
fresh release lookup compatible with older pip, which revalidated by default.
|
|
613
|
+
|
|
551
614
|
postUpgradeTasks.fileFilters includes the managed paths, so Renovate commits the new
|
|
552
615
|
hash lock and the fully materialized process in the same pull request. A self-hosted
|
|
553
616
|
Renovate administrator must allow only this anchored command and must keep shell
|
|
@@ -40,19 +40,56 @@ under .process/receipts. Both paths are ignored by Git. Verification evidence is
|
|
|
40
40
|
to HEAD plus a fingerprint of tracked and non-ignored untracked files. Any relevant
|
|
41
41
|
mutation invalidates it.
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
Neither the reviewer actor nor reviewer context may have implemented the current
|
|
44
|
+
cycle. An agent reviewer context also cannot be recorded in another accepted change
|
|
45
|
+
in this repository or its registered worktrees. There is no attestation hierarchy,
|
|
45
46
|
recommendation chain, authority-transition protocol, remote-evidence federation, or
|
|
46
47
|
second handwritten validator.
|
|
47
48
|
|
|
48
|
-
The coordinator hands
|
|
49
|
-
|
|
50
|
-
reviewer continues corrections. The reviewer inspects the change and authors its
|
|
49
|
+
The coordinator hands each new change to a newly spawned reviewer, with the accepted
|
|
50
|
+
source artifacts and no inherited implementation or other-change review conversation.
|
|
51
|
+
The same reviewer continues only that change's corrections. The reviewer inspects the change and authors its
|
|
51
52
|
own report. The runner's existing task/session interaction and returned result make
|
|
52
|
-
that work inspectable. Sharing a
|
|
53
|
+
that work inspectable. Sharing a provider or account is allowed; this is a
|
|
53
54
|
workflow for independent judgment, not authenticated identity or merge enforcement.
|
|
54
55
|
See [change-review](process_assets/skills/change-review/SKILL.md) for the handoff.
|
|
55
56
|
|
|
57
|
+
Start, submission and finish check schema-valid canonical run history with bounded
|
|
58
|
+
worktree/file reads. A short OS lock in common Git metadata serializes canonical
|
|
59
|
+
history writes and review checks; profiles run outside it and reload current state
|
|
60
|
+
before recording results. The lock stores no identity registry. The check rejects known context reuse,
|
|
61
|
+
but cannot establish native conversation freshness or inspect removed history,
|
|
62
|
+
other clones or other repositories. Keep native creation, non-inherited dispatch
|
|
63
|
+
and effective settings evidence in the existing handoff.
|
|
64
|
+
|
|
65
|
+
`processctl change review replace-reused` repairs only an initial pending assignment
|
|
66
|
+
proven to reuse another change's agent context, before a submitted review or normal
|
|
67
|
+
report file exists. It validates a fresh replacement and current verification,
|
|
68
|
+
preserves the prior assignment and conflict in canonical history, and leaves cycle
|
|
69
|
+
and evidence unchanged. Valid assignments and reviewed correction cycles cannot use
|
|
70
|
+
this operation. It does not provide an unrestricted reviewer reset.
|
|
71
|
+
|
|
72
|
+
Every delegated agent and reviewer follows the active user-selected model and
|
|
73
|
+
reasoning effort under [deliver-change's settings rule](process_assets/skills/deliver-change/SKILL.md#preserve-agent-execution-settings).
|
|
74
|
+
It covers fresh agents and resumed sessions, requires native runtime confirmation,
|
|
75
|
+
and permits no autonomous upgrade or downgrade. The portable guidance does not make
|
|
76
|
+
processctl a provider runtime or a model-quality evaluator.
|
|
77
|
+
|
|
78
|
+
Consumers using the packaged publication compatibility policy can enable
|
|
79
|
+
`lifecycle.publication.required: true` in `.process/project.json` after adopting a
|
|
80
|
+
version that supports it. Start then rejects an invalid branch before creating a
|
|
81
|
+
run. Finish checks the current branch, head subject, and pinned comparison-base-to-head
|
|
82
|
+
range, and writes publication metadata in a version 2 receipt. Version 1 receipts and
|
|
83
|
+
consumers without the opt-in remain supported. Missing pinned bases and mutations
|
|
84
|
+
during the check fail completion.
|
|
85
|
+
|
|
86
|
+
PR readiness additionally requires the selected ready-state body/title and exact
|
|
87
|
+
head/range checks. Follow the [completion preflight](process_assets/skills/change-complete/SKILL.md)
|
|
88
|
+
and repeat those checks in required CI. This repository's existing
|
|
89
|
+
`verification/verify_publication.py --pull-request` uses the actual PR metadata and
|
|
90
|
+
selected consumer standard; local lifecycle completion alone does not establish
|
|
91
|
+
external PR readiness. Integration-branch pushes remain consumer-owned operations.
|
|
92
|
+
|
|
56
93
|
Runtime architecture is enforced by semantic fitness functions, not module or source-
|
|
57
94
|
line quotas. Every module has an explicit dependency layer, imports point toward lower
|
|
58
95
|
layers, the internal graph remains acyclic, and lifecycle.py alone owns state
|
|
@@ -98,6 +135,20 @@ Commands are argument arrays, never shell strings. Each command has a finite tim
|
|
|
98
135
|
Output has a hard aggregate budget; evidence stores byte counts and hashes, never raw
|
|
99
136
|
stdout or stderr that could contain secrets.
|
|
100
137
|
|
|
138
|
+
For a fresh checkout of this repository, create a virtual environment with a supported
|
|
139
|
+
Python and install `engineering_process/requirements-runtime.txt`,
|
|
140
|
+
`engineering_process/requirements-dev.txt`, and
|
|
141
|
+
`engineering_process/requirements-build.txt` through that interpreter. Use
|
|
142
|
+
`.venv/Scripts/python.exe` on Windows or `.venv/bin/python` on POSIX to invoke the
|
|
143
|
+
source CLI and verification scripts. Other consumers own their runtime paths and
|
|
144
|
+
setup commands; installed consumers can use their environment's `processctl` entry
|
|
145
|
+
point without a source checkout.
|
|
146
|
+
|
|
147
|
+
The bounded runner prepends the invoking Python executable's directory to child
|
|
148
|
+
`PATH`. Bare commands therefore search that environment before the inherited path,
|
|
149
|
+
while declared argument arrays, explicit executable paths, secret filtering and
|
|
150
|
+
remaining path entries are preserved. This alignment does not install dependencies.
|
|
151
|
+
|
|
101
152
|
A failed profile also reports a safe `diagnostic` descriptor containing only its
|
|
102
153
|
validated profile identifier, check identifier, canonical one-based profile position,
|
|
103
154
|
and a fixed `processctl` argument array. The position remains unambiguous even when a
|
|
@@ -249,6 +300,8 @@ consumer-owned skills and instructions. Applying the same version twice is a no-
|
|
|
249
300
|
Consumer-owned `.process/standards.json` selections and override definitions are also
|
|
250
301
|
preserved; the managed PR template follows the effective supported standard. See
|
|
251
302
|
[consumer document standards](ARTIFACT_STANDARDS.md).
|
|
303
|
+
[Issue records](ARTIFACT_STANDARDS.md#issues) use the same default-and-override
|
|
304
|
+
mechanism for open requests and closed evidence while tracker actions remain consumer-owned.
|
|
252
305
|
[Automation naming](ARTIFACT_STANDARDS.md#automation-names) uses the same versioned
|
|
253
306
|
selection and override mechanism; consumers apply it in their bootstrap and provider checks.
|
|
254
307
|
The legacy managed runner can enter 1.0 directly, so consumers do not need a chain of
|
|
@@ -524,6 +577,16 @@ package rule keeps the adoption pull request in draft and runs exactly:
|
|
|
524
577
|
|
|
525
578
|
python .process/adopt-process.py --project-root . --requirements-lock requirements/process.txt
|
|
526
579
|
|
|
580
|
+
The bootstrap refreshes `engineering-process` index metadata using pip's
|
|
581
|
+
`--refresh-package` option, with `--no-cache-dir` for older pip. Exact pins, hashes,
|
|
582
|
+
isolated installation and bounded failures still apply. Consumer CI that installs
|
|
583
|
+
the lock directly must also request fresh metadata: use
|
|
584
|
+
`--refresh-package engineering-process` with pip 26.2 or newer, or the portable
|
|
585
|
+
`--no-cache-dir` option. Keep this setting on the process installation command;
|
|
586
|
+
other dependency caches do not need to be disabled. For non-isolated pip-compile,
|
|
587
|
+
the equivalent `PIP_REFRESH_PACKAGE=engineering-process` environment setting keeps
|
|
588
|
+
fresh release lookup compatible with older pip, which revalidated by default.
|
|
589
|
+
|
|
527
590
|
postUpgradeTasks.fileFilters includes the managed paths, so Renovate commits the new
|
|
528
591
|
hash lock and the fully materialized process in the same pull request. A self-hosted
|
|
529
592
|
Renovate administrator must allow only this anchored command and must keep shell
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Engineering Process v2.3.1
|
|
2
|
+
|
|
3
|
+
Changes since v2.3.0.
|
|
4
|
+
|
|
5
|
+
## Fixes
|
|
6
|
+
|
|
7
|
+
- Refresh package metadata when installing the hash\-locked process so newly published releases remain visible through warm pip caches\; remove the publication\-time cache expiry wait\. Consumer CI using pip 26\.2 or newer should pass \-\-refresh\-package engineering\-process\, or use \-\-no\-cache\-dir with older pip\. ([#134](https://github.com/phuongnse/engineering-process/issues/134))
|
|
8
|
+
- Reject recorded agent reviewer\-context reuse across changes in the same repository and its worktrees\. Preserve same\-change correction reviews\, and provide an audited replacement only for a reused initial assignment with no submitted review\. Native fresh\-session and unchanged model\/effort evidence remain required from the host\. ([#182](https://github.com/phuongnse/engineering-process/issues/182))
|
|
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.3.1/VERSIONING.md) and [adoption guidance](https://github.com/phuongnse/engineering-process/blob/v2.3.1/SELF_HOSTING.md).
|
|
17
|
+
|
|
18
|
+
[Full change comparison](https://github.com/phuongnse/engineering-process/compare/v2.3.0...v2.3.1)
|
{engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/_supervisor_posix.py
RENAMED
|
@@ -7,6 +7,7 @@ import ctypes
|
|
|
7
7
|
import os
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
import secrets
|
|
10
|
+
import shutil
|
|
10
11
|
import signal
|
|
11
12
|
import subprocess
|
|
12
13
|
import sys
|
|
@@ -35,9 +36,13 @@ def _enable_subreaper() -> None:
|
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
def _ps_process_table() -> tuple[dict[int, int], str | None]:
|
|
39
|
+
# Supervision must not depend on the consumer's PATH.
|
|
40
|
+
application = shutil.which("ps", path=os.defpath)
|
|
41
|
+
if application is None:
|
|
42
|
+
return {}, "process table snapshot could not start"
|
|
38
43
|
try:
|
|
39
44
|
result = subprocess.run(
|
|
40
|
-
[
|
|
45
|
+
[application, "-axo", "pid=,ppid="],
|
|
41
46
|
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
|
42
47
|
timeout=PROCESS_TABLE_TIMEOUT_SECONDS, check=False, text=True,
|
|
43
48
|
)
|
|
@@ -190,7 +195,7 @@ class PosixProcessSupervisor:
|
|
|
190
195
|
if not command or "\x00" in command:
|
|
191
196
|
raise OSError("command executable is invalid")
|
|
192
197
|
supplied = Path(command)
|
|
193
|
-
if supplied.is_absolute() or
|
|
198
|
+
if supplied.is_absolute() or os.path.dirname(command):
|
|
194
199
|
candidate = supplied if supplied.is_absolute() else working_directory / supplied
|
|
195
200
|
try:
|
|
196
201
|
resolved = candidate.resolve(strict=True)
|
|
@@ -198,7 +203,8 @@ class PosixProcessSupervisor:
|
|
|
198
203
|
raise OSError(f"executable is unavailable: {command}") from error
|
|
199
204
|
if not resolved.is_file() or not os.access(resolved, os.X_OK):
|
|
200
205
|
raise OSError(f"executable is unavailable: {command}")
|
|
201
|
-
|
|
206
|
+
# The symlink path may select a virtual environment.
|
|
207
|
+
return candidate.absolute()
|
|
202
208
|
for raw_directory in environment.get("PATH", "").split(os.pathsep):
|
|
203
209
|
directory = Path(raw_directory or ".")
|
|
204
210
|
if not directory.is_absolute():
|
|
@@ -209,7 +215,7 @@ class PosixProcessSupervisor:
|
|
|
209
215
|
except OSError:
|
|
210
216
|
continue
|
|
211
217
|
if resolved.is_file() and os.access(resolved, os.X_OK):
|
|
212
|
-
return
|
|
218
|
+
return candidate.absolute()
|
|
213
219
|
raise OSError(f"executable is unavailable: {command}")
|
|
214
220
|
|
|
215
221
|
def spawn(
|
{engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/_supervisor_windows.py
RENAMED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import ntpath
|
|
6
7
|
import os
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
import subprocess
|
|
@@ -99,7 +100,7 @@ def resolve_windows_application(
|
|
|
99
100
|
raise OSError("command executable is invalid")
|
|
100
101
|
separator = os.pathsep if path_separator is None else path_separator
|
|
101
102
|
supplied = Path(command)
|
|
102
|
-
explicit_path = supplied.is_absolute() or
|
|
103
|
+
explicit_path = supplied.is_absolute() or bool(ntpath.dirname(command))
|
|
103
104
|
suffix = supplied.suffix.casefold()
|
|
104
105
|
if suffix in _UNSUPPORTED_SHELL_SUFFIXES:
|
|
105
106
|
raise OSError(
|
{engineering_process-2.2.0 → engineering_process-2.3.1}/engineering_process/artifact_standards.py
RENAMED
|
@@ -84,6 +84,14 @@ def _validate_relations(document: dict[str, Any]) -> None:
|
|
|
84
84
|
_unique([group["type"] for group in rules["groups"]], "change types")
|
|
85
85
|
_unique([section["id"] for section in rules["sections"]], "release section ids")
|
|
86
86
|
_unique([item["heading"] for item in rules["groups"] + rules["sections"]], "release headings")
|
|
87
|
+
elif document["adapter"] == "issue":
|
|
88
|
+
for state, definition in rules["states"].items():
|
|
89
|
+
sections = definition["sections"]
|
|
90
|
+
_unique([section["heading"] for section in sections], f"{state} issue section headings")
|
|
91
|
+
for kind in ("fields", "checks"):
|
|
92
|
+
entries = [entry for section in sections for entry in section[kind]]
|
|
93
|
+
_unique([entry["id"] for entry in entries], f"{state} issue {kind} ids")
|
|
94
|
+
_unique([entry["label"] for entry in entries], f"{state} issue {kind} labels")
|
|
87
95
|
|
|
88
96
|
|
|
89
97
|
def read_document(path: Path) -> bytes:
|
|
@@ -40,6 +40,7 @@ from .lifecycle import (
|
|
|
40
40
|
submit_review,
|
|
41
41
|
verify_change,
|
|
42
42
|
)
|
|
43
|
+
from .issue import render_issue
|
|
43
44
|
from .project import load_project, readiness_summary
|
|
44
45
|
from .production_engineering import (
|
|
45
46
|
validate_plan_assessments,
|
|
@@ -329,9 +330,10 @@ def command_change_review_start(args: argparse.Namespace) -> Result:
|
|
|
329
330
|
actor_id=args.actor,
|
|
330
331
|
context_id=args.context,
|
|
331
332
|
kind=args.actor_kind,
|
|
333
|
+
replace_reused=args.review_command == "replace-reused",
|
|
332
334
|
)
|
|
333
335
|
return _state_result(
|
|
334
|
-
"change review
|
|
336
|
+
f"change review {args.review_command}",
|
|
335
337
|
state,
|
|
336
338
|
assignment=state["reviewAssignment"],
|
|
337
339
|
processSignals=process_improvement_signals(state),
|
|
@@ -420,17 +422,29 @@ def command_artifact(args: argparse.Namespace) -> Result:
|
|
|
420
422
|
elif operation == "renovate-preset":
|
|
421
423
|
payload = formatted_json_bytes(render_renovate_preset(standard, process_root=process_root))
|
|
422
424
|
else:
|
|
425
|
+
state = args.state or ("open" if standard.document["adapter"] == "issue" else "ready")
|
|
426
|
+
allowed_states = {
|
|
427
|
+
"pr-description": {"draft", "ready"},
|
|
428
|
+
"release-notes": {"draft", "ready"},
|
|
429
|
+
"automation-name": {"draft", "ready"},
|
|
430
|
+
"issue": {"open", "closed"},
|
|
431
|
+
}[standard.document["adapter"]]
|
|
432
|
+
if state not in allowed_states:
|
|
433
|
+
raise ProcessError(
|
|
434
|
+
f"{standard.document['adapter']} state must be "
|
|
435
|
+
+ " or ".join(sorted(allowed_states))
|
|
436
|
+
)
|
|
423
437
|
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
438
|
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=
|
|
439
|
+
renderer = {"pr-description": render_description, "release-notes": render_notes, "automation-name": render_name, "issue": render_issue}[standard.document["adapter"]]
|
|
440
|
+
payload = renderer(standard, data, state=state, process_root=process_root).encode("utf-8")
|
|
427
441
|
details["dataDigest"] = digest_json(data)
|
|
428
442
|
elif operation == "render" or standard.document["adapter"] != "pr-description":
|
|
429
443
|
raise ProcessError("this artifact operation requires --data-file")
|
|
430
444
|
if operation == "validate":
|
|
431
445
|
actual = read_document(args.body_file)
|
|
432
446
|
if data is None:
|
|
433
|
-
issues = body_issues(actual.decode("utf-8"),
|
|
447
|
+
issues = body_issues(actual.decode("utf-8"), state, standard)
|
|
434
448
|
elif actual != payload:
|
|
435
449
|
issues.append("artifact bytes differ from the selected standard and input data")
|
|
436
450
|
payload = actual
|
|
@@ -529,6 +543,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
529
543
|
review_start = _leaf(review_commands, "start", command_change_review_start)
|
|
530
544
|
_add_actor(review_start)
|
|
531
545
|
review_start.add_argument("--change-id", required=True)
|
|
546
|
+
review_replace = _leaf(review_commands, "replace-reused", command_change_review_start)
|
|
547
|
+
_add_actor(review_replace)
|
|
548
|
+
review_replace.add_argument("--change-id", required=True)
|
|
532
549
|
review_submit = _leaf(review_commands, "submit", command_change_review_submit)
|
|
533
550
|
review_submit.add_argument("--change-id", required=True)
|
|
534
551
|
review_submit.add_argument("--review", type=Path, required=True)
|
|
@@ -551,7 +568,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
551
568
|
leaf = _leaf(artifact_commands, name, command_artifact)
|
|
552
569
|
leaf.add_argument("--artifact", required=True)
|
|
553
570
|
if name in {"render", "validate"}:
|
|
554
|
-
leaf.add_argument("--state", choices=("draft", "ready"
|
|
571
|
+
leaf.add_argument("--state", choices=("draft", "ready", "open", "closed"))
|
|
555
572
|
leaf.add_argument("--data-file", type=Path, required=name == "render")
|
|
556
573
|
if name == "validate":
|
|
557
574
|
leaf.add_argument("--body-file", type=Path, required=True)
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import os
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
import sys
|
|
8
9
|
import threading
|
|
9
10
|
import time
|
|
10
11
|
from typing import Any, BinaryIO
|
|
@@ -28,6 +29,11 @@ def _child_environment() -> dict[str, str]:
|
|
|
28
29
|
if any(marker in upper for marker in SECRET_MARKERS):
|
|
29
30
|
continue
|
|
30
31
|
environment[name] = value
|
|
32
|
+
runtime_directory = str(Path(sys.executable).absolute().parent)
|
|
33
|
+
inherited_path = environment.get("PATH", "")
|
|
34
|
+
environment["PATH"] = os.pathsep.join(
|
|
35
|
+
entry for entry in (runtime_directory, inherited_path) if entry
|
|
36
|
+
)
|
|
31
37
|
environment["PYTHONUNBUFFERED"] = "1"
|
|
32
38
|
return environment
|
|
33
39
|
|
|
@@ -20,6 +20,7 @@ CONTRACT_KINDS = (
|
|
|
20
20
|
"artifact-selection",
|
|
21
21
|
"artifact-standard",
|
|
22
22
|
"automation-name-data",
|
|
23
|
+
"issue-data",
|
|
23
24
|
"change",
|
|
24
25
|
"plan",
|
|
25
26
|
"process-graph",
|
|
@@ -50,9 +51,20 @@ def read_json(path: Path, *, maximum_bytes: int = MAX_JSON_BYTES) -> Any:
|
|
|
50
51
|
if size > maximum_bytes:
|
|
51
52
|
raise ProcessError(f"{path} exceeds {maximum_bytes} bytes")
|
|
52
53
|
try:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
with path.open("rb") as stream:
|
|
55
|
+
content = stream.read(maximum_bytes + 1)
|
|
56
|
+
except OSError as error:
|
|
57
|
+
raise ProcessError(f"cannot read {path}: {error}") from error
|
|
58
|
+
if len(content) > maximum_bytes:
|
|
59
|
+
raise ProcessError(f"{path} exceeds {maximum_bytes} bytes")
|
|
60
|
+
return parse_json_bytes(content, source=str(path))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_json_bytes(content: bytes, *, source: str) -> Any:
|
|
64
|
+
try:
|
|
65
|
+
return json.loads(content.decode("utf-8"))
|
|
66
|
+
except (UnicodeError, json.JSONDecodeError) as error:
|
|
67
|
+
raise ProcessError(f"{source} is not valid UTF-8 JSON: {error}") from error
|
|
56
68
|
|
|
57
69
|
|
|
58
70
|
def canonical_bytes(value: Any) -> bytes:
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Issue record generation from one consumer-selected standard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import urlsplit
|
|
9
|
+
|
|
10
|
+
from .artifact_standards import ArtifactStandard, MAX_DOCUMENT_BYTES
|
|
11
|
+
from .contracts import ProcessError, validate_document
|
|
12
|
+
from .distribution import distribution_root, schemas_root
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_HTTPS_AUTHORITY = re.compile(
|
|
16
|
+
r"(?:\[[^\[\]]+\]|(?:[A-Za-z0-9._~!$&'()*+,;=-]|%[0-9A-Fa-f]{2})+)"
|
|
17
|
+
r"(?::[0-9]*)?"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _record_url(value: str) -> bool:
|
|
22
|
+
if any(character.isspace() or ord(character) < 32 or 127 <= ord(character) <= 159
|
|
23
|
+
for character in value):
|
|
24
|
+
return False
|
|
25
|
+
try:
|
|
26
|
+
parsed = urlsplit(value)
|
|
27
|
+
# urlsplit defers port validation until this attribute is read.
|
|
28
|
+
parsed.port
|
|
29
|
+
return (
|
|
30
|
+
parsed.scheme == "https"
|
|
31
|
+
and parsed.hostname is not None
|
|
32
|
+
and parsed.username is None
|
|
33
|
+
and parsed.password is None
|
|
34
|
+
and _HTTPS_AUTHORITY.fullmatch(parsed.netloc) is not None
|
|
35
|
+
)
|
|
36
|
+
except ValueError:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _validate_field(field: dict[str, Any], value: str) -> None:
|
|
41
|
+
format_name = field.get("format", "text")
|
|
42
|
+
if format_name == "record-url" and not _record_url(value):
|
|
43
|
+
raise ProcessError(f"issue field {field['label']} requires one durable HTTPS URL")
|
|
44
|
+
if format_name == "record-references":
|
|
45
|
+
references = value.split(", ")
|
|
46
|
+
if value.casefold() != "none" and (
|
|
47
|
+
not references or any(not _record_url(reference) for reference in references)
|
|
48
|
+
):
|
|
49
|
+
raise ProcessError(
|
|
50
|
+
f"issue field {field['label']} requires none or comma-separated durable HTTPS URLs"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def render_issue(
|
|
55
|
+
standard: ArtifactStandard,
|
|
56
|
+
data: dict[str, Any],
|
|
57
|
+
*,
|
|
58
|
+
state: str = "open",
|
|
59
|
+
process_root: Path | None = None,
|
|
60
|
+
) -> str:
|
|
61
|
+
if standard.document["adapter"] != "issue":
|
|
62
|
+
raise ProcessError("selected standard requires a different issue adapter")
|
|
63
|
+
if state not in {"open", "closed"}:
|
|
64
|
+
raise ProcessError("issue state must be open or closed")
|
|
65
|
+
validate_document(
|
|
66
|
+
data,
|
|
67
|
+
"issue-data",
|
|
68
|
+
schema_root=schemas_root(distribution_root(process_root)),
|
|
69
|
+
)
|
|
70
|
+
title_rules = standard.rules["title"]
|
|
71
|
+
title = data["title"]
|
|
72
|
+
if len(title) > title_rules["maxLength"]:
|
|
73
|
+
raise ProcessError("issue title exceeds the selected length limit")
|
|
74
|
+
if prefix := title_rules.get("prefix"):
|
|
75
|
+
if not title.startswith(prefix):
|
|
76
|
+
raise ProcessError(f"issue title must start with {prefix}")
|
|
77
|
+
title_value = title[len(prefix):] if prefix else title
|
|
78
|
+
if not title_value.strip() or standard.unresolved(title_value):
|
|
79
|
+
raise ProcessError(f"{state} issue has unresolved title")
|
|
80
|
+
|
|
81
|
+
sections = standard.rules["states"][state]["sections"]
|
|
82
|
+
fields = [field for section in sections for field in section["fields"]]
|
|
83
|
+
checks = [check for section in sections for check in section["checks"]]
|
|
84
|
+
expected_fields = {field["id"] for field in fields}
|
|
85
|
+
expected_checks = {check["id"] for check in checks}
|
|
86
|
+
unknown_fields = sorted(data["fields"].keys() - expected_fields)
|
|
87
|
+
unknown_checks = sorted(data["checks"].keys() - expected_checks)
|
|
88
|
+
if unknown_fields:
|
|
89
|
+
raise ProcessError("issue data contains unknown fields: " + ", ".join(unknown_fields))
|
|
90
|
+
if unknown_checks:
|
|
91
|
+
raise ProcessError("issue data contains unknown checks: " + ", ".join(unknown_checks))
|
|
92
|
+
|
|
93
|
+
lines = [f"# {title}", ""]
|
|
94
|
+
for section in sections:
|
|
95
|
+
lines.extend([section["heading"], ""])
|
|
96
|
+
for field in section["fields"]:
|
|
97
|
+
value = data["fields"].get(field["id"], standard.document["pendingValues"][0])
|
|
98
|
+
if standard.unresolved(value):
|
|
99
|
+
raise ProcessError(f"{state} issue has unresolved value for {field['label']}")
|
|
100
|
+
_validate_field(field, value)
|
|
101
|
+
lines.extend([f"### {field['label']}", "", value, ""])
|
|
102
|
+
for check in section["checks"]:
|
|
103
|
+
completed = data["checks"].get(check["id"], False)
|
|
104
|
+
if state == "closed" and not completed:
|
|
105
|
+
raise ProcessError(f"closed issue has unchecked item: {check['label']}")
|
|
106
|
+
lines.append(f"- [{'x' if completed else ' '}] {check['label']}")
|
|
107
|
+
if section["checks"]:
|
|
108
|
+
lines.append("")
|
|
109
|
+
body = "\n".join(lines)
|
|
110
|
+
if len(body.encode("utf-8")) > MAX_DOCUMENT_BYTES:
|
|
111
|
+
raise ProcessError("issue record exceeds its size limit")
|
|
112
|
+
return body
|