@starci/skills 1.1.0 → 1.2.0

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 (75) hide show
  1. package/INDEX.md +3 -1
  2. package/INDEX.vi.md +3 -1
  3. package/README.md +6 -1
  4. package/README.vi.md +6 -1
  5. package/knowledge/ui/presentation/INDEX.md +5 -0
  6. package/knowledge/ui/presentation/INDEX.vi.md +1 -0
  7. package/knowledge/ui/presentation/radius.md +183 -0
  8. package/knowledge/ui/presentation/radius.vi.md +182 -0
  9. package/operators/INDEX.md +5 -5
  10. package/operators/INDEX.vi.md +5 -5
  11. package/operators/architecture-decide/operator.md +27 -4
  12. package/operators/architecture-decide/operator.vi.md +23 -4
  13. package/operators/architecture-decide/self-test.mjs +18 -1
  14. package/operators/architecture-decide/validate.mjs +29 -1
  15. package/operators/backend-source-apply/operator.md +197 -182
  16. package/operators/backend-source-apply/operator.vi.md +190 -178
  17. package/operators/backend-source-apply/self-test.mjs +257 -257
  18. package/operators/backend-source-apply/validate.mjs +240 -240
  19. package/operators/business-decide/self-test.mjs +2 -1
  20. package/operators/business-decide/validate.mjs +3 -0
  21. package/operators/content-generate/self-test.mjs +1 -1
  22. package/operators/frontend-direction-decide/self-test.mjs +1 -1
  23. package/operators/frontend-presentation-resolve/operator.md +6 -0
  24. package/operators/frontend-presentation-resolve/operator.vi.md +6 -0
  25. package/operators/frontend-presentation-resolve/self-test.mjs +6 -5
  26. package/operators/frontend-presentation-resolve/validate.mjs +30 -6
  27. package/operators/frontend-source-apply/operator.md +3 -1
  28. package/operators/frontend-source-apply/operator.vi.md +3 -1
  29. package/operators/frontend-source-apply/self-test.mjs +1 -1
  30. package/operators/frontend-surface-audit/self-test.mjs +1 -1
  31. package/operators/git-publish/self-test.mjs +1 -1
  32. package/operators/platform-operate/self-test.mjs +1 -1
  33. package/operators/quality-verify/operator.md +5 -1
  34. package/operators/quality-verify/operator.vi.md +5 -1
  35. package/operators/quality-verify/self-test.mjs +15 -2
  36. package/operators/quality-verify/validate.mjs +21 -1
  37. package/operators/release-deploy/self-test.mjs +235 -235
  38. package/operators/uat-verify/operator.md +20 -6
  39. package/operators/uat-verify/operator.vi.md +21 -7
  40. package/operators/uat-verify/self-test.mjs +6 -5
  41. package/operators/uat-verify/validate.mjs +10 -4
  42. package/operators/workspace-bind/operator.md +18 -4
  43. package/operators/workspace-bind/operator.vi.md +16 -4
  44. package/operators/workspace-bind/self-test.mjs +3 -1
  45. package/operators/workspace-bind/validate.mjs +1 -0
  46. package/package.json +1 -1
  47. package/readiness/initialization/workspaces/local-route.schema.json +1 -1
  48. package/resources/orchestrator.json +14 -8
  49. package/routing.json +2 -1
  50. package/scripts/validate-request.mjs +12 -1
  51. package/scripts/validate-response.mjs +14 -3
  52. package/scripts/validate-step.mjs +2 -1
  53. package/scripts/validate-workflows.mjs +8 -1
  54. package/templates/kinds/architecture-decision.contract.json +1 -0
  55. package/templates/kinds/architecture-decision.skeleton.md +6 -0
  56. package/templates/kinds/backend-source-application.contract.json +1 -1
  57. package/templates/kinds/backend-source-application.skeleton.md +1 -1
  58. package/templates/kinds/frontend-presentation-resolution.contract.json +1 -1
  59. package/templates/kinds/frontend-presentation-resolution.skeleton.md +1 -0
  60. package/templates/kinds/mutations.schema.json +65 -65
  61. package/templates/kinds/route.schema.json +4 -2
  62. package/templates/kinds/stack-model.schema.json +23 -1
  63. package/templates/kinds/workspace-route-binding.contract.json +2 -2
  64. package/templates/step/response.schema.json +10 -0
  65. package/templates/step/state.schema.json +205 -0
  66. package/workflows/README.md +3 -0
  67. package/workflows/README.vi.md +3 -0
  68. package/workflows/backend-feature.json +26 -5
  69. package/workflows/content-unit.json +4 -1
  70. package/workflows/frontend-new-surface.json +15 -5
  71. package/workflows/frontend-reconstruct.json +10 -4
  72. package/workflows/frontend-refine.json +10 -4
  73. package/workflows/frontend-with-uat.json +15 -3
  74. package/workflows/full-feature.json +31 -8
  75. package/workflows/release.json +7 -1
@@ -1,235 +1,235 @@
1
- // Proves validate.mjs on a synthetic session branch: one steady deployment, one that took the
2
- // recovery fallback, one that took both fallbacks and ended rolled back, one blocked on an unproven
3
- // steady state, and one mutation per law, each of which must fail with a line that names the defect.
4
- import assert from 'node:assert/strict';
5
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
6
- import { tmpdir } from 'node:os';
7
- import path from 'node:path';
8
- import { validateReleaseStep } from './validate.mjs';
9
-
10
- const RELEASE = 'release:2026.01.10-1';
11
- const PREVIOUS = 'release:2026.01.03-2';
12
- const DIGEST = `sha256:${'1'.repeat(64)}`;
13
- const SAFE_DIGEST = `sha256:${'2'.repeat(64)}`;
14
- const TARGET = 'production/api';
15
- const APPROVAL = '@worktrees/businesses/features/release/model.json#deploy-grant';
16
- const MANIFEST = '.stacks/production/api.manifest.json';
17
- const ARTIFACT = '@remote/ghcr/starci/academy-api';
18
- const DEADLINE = 600;
19
- const PROBES = [
20
- { probeId: 'public-graphql-typename', kind: 'public', endpointRef: 'https://api.example/graphql', expectStatus: 200 },
21
- ];
22
- const ROLLBACK = { releaseId: PREVIOUS, artifactRef: ARTIFACT, digest: SAFE_DIGEST, dataCompatible: true };
23
-
24
- const observation = (at, condition, status = 'pass') => ({
25
- observedAt: at, condition, activeReleaseIds: [RELEASE], activeDigest: DIGEST, availableTargets: 1,
26
- probeResults: [{ probeId: 'public-graphql-typename', status, observedStatus: status === 'pass' ? 200 : 502, observedAt: at }],
27
- });
28
-
29
- const probesJson = ({ observations = [observation('2026-01-10T00:01:00.000Z', 'progressing'), observation('2026-01-10T00:07:00.000Z', 'steady')], finalCondition = 'steady', deadlineSeconds = DEADLINE, elapsedSeconds = 420, backoffSeconds = 30 } = {}) => ({
30
- deadlineSeconds, elapsedSeconds, backoffSeconds, observations, finalCondition,
31
- });
32
-
33
- const NL = String.fromCharCode(10);
34
- const STEPS = [
35
- ['`authorize`', 'applied', '—', '—', 'the declared grant covers this project, environment and target'],
36
- ['`rollout`', 'applied', '4', '5', 'the target moved to the immutable digest'],
37
- ['`monitor`', 'applied', '—', '—', 'the window was observed to its end'],
38
- ];
39
-
40
- function responseMd({
41
- release = RELEASE, digest = DIGEST, target = TARGET, approval = APPROVAL, deadline = DEADLINE,
42
- rollbackId = PREVIOUS, outcome = 'deployed', branch = 'none', steps = STEPS,
43
- monitoring = { Deadline: DEADLINE, Elapsed: 420, Backoff: 30, 'Final condition': 'steady' },
44
- steady = { 'Active digest': DIGEST, 'Available targets': '1 of 1', 'Superseded active': '0', 'Window elapsed': '300' },
45
- fallbacksTaken = [],
46
- } = {}) {
47
- return `# release-deployment — ${release}
48
-
49
- The immutable release reached its declared target under the declared grant, and the steady state
50
- rests on the observed digest, the available targets and every declared probe across the window.
51
-
52
- ## Binding
53
-
54
- | Field | Value |
55
- | --- | --- |
56
- | Operator | \`release.deploy\` |
57
- | Step | \`step-1/parallel-1\` |
58
- | Project | \`starci-academy\` |
59
- | Release | ${release} |
60
- | Artifact | \`${ARTIFACT}\` |
61
- | Digest | \`${digest}\` |
62
- | Target | ${target} |
63
- | Environment | production |
64
- | Replaced release | \`${PREVIOUS}\` |
65
- | Approval | ${approval} |
66
- | Manifest | \`${MANIFEST}\` |
67
- | Steady deadline | ${deadline} |
68
- | Rollback identity | ${rollbackId} |
69
-
70
- ## Outcome
71
-
72
- | Field | Value |
73
- | --- | --- |
74
- | Outcome | ${outcome} |
75
- | Branch | ${branch} |
76
-
77
- ## Steps
78
-
79
- | Step | State | Revision before | Revision after | Statement |
80
- | --- | --- | --- | --- | --- |
81
- ${steps.map((s) => `| ${s[0]} | ${s[1]} | ${s[2]} | ${s[3]} | ${s[4]} |`).join('\n')}
82
-
83
- ## Monitoring
84
-
85
- | Field | Value |
86
- | --- | --- |
87
- | Deadline | ${monitoring.Deadline} |
88
- | Elapsed | ${monitoring.Elapsed} |
89
- | Backoff | ${monitoring.Backoff} |
90
- | Final condition | ${monitoring['Final condition']} |
91
-
92
- ## Steady state
93
-
94
- | Metric | Value |
95
- | --- | --- |
96
- ${Object.entries(steady).map(([k, v]) => `| ${k} | ${/^sha256:/.test(String(v)) ? `\`${v}\`` : v} |`).join('\n')}
97
-
98
- ## Findings
99
-
100
- | Code | Step | Statement |
101
- | --- | --- | --- |
102
- | \`IDEMPOTENT_NO_OP\` | \`host-prepare\` | the host already matched the declaration |
103
-
104
- ## Fallbacks taken
105
-
106
- | Code | Action |
107
- | --- | --- |
108
- ${fallbacksTaken.map((c) => `| \`${c}\` | the branch was taken and recorded |`).join(NL)}
109
- `;
110
- }
111
-
112
- const requestJson = ({ release = RELEASE, target = TARGET, approval = APPROVAL, probes = PROBES, deadline = DEADLINE, rollbackIdentity = ROLLBACK, extra = {} } = {}) => ({
113
- schemaVersion: 9, operatorId: 'release.deploy', step: 1, parallel: 1, sessionId: 's-test',
114
- contexts: [{ alias: '@remote/ghcr/starci/academy-api', head: null }],
115
- requirements: { release, target, approval, probes, steadyDeadline: deadline, rollbackIdentity, resume: null, ...extra },
116
- inputs: { 'quality-verification': 'step-1/parallel-1/response/quality.md' }, resume: null,
117
- });
118
-
119
- const responseJson = ({ status = 'done', stop, fallbacks = [], next = ['platform.operate'] } = {}) => ({
120
- schemaVersion: 9, operatorId: 'release.deploy', step: 1, parallel: 1, status, ...(stop ? { stop } : {}),
121
- fallbacks,
122
- fields: status === 'blocked' ? {} : { 'release-deployment': 'response/response.md', probes: 'response/data/probes.json' },
123
- commits: [], next,
124
- });
125
-
126
- function writeBranch(files) {
127
- const session = mkdtempSync(path.join(tmpdir(), 'release-session-'));
128
- const branch = path.join(session, 'step-1', 'parallel-1');
129
- for (const d of ['request', 'response/data', 'response/artifacts']) mkdirSync(path.join(branch, d), { recursive: true });
130
- writeFileSync(path.join(session, 'state.json'), JSON.stringify({ id: 's-test', chain: [['1/1']], steps: { '1/1': 'release.deploy' }, current: '1/1', status: 'running' }));
131
- writeFileSync(path.join(branch, 'response', 'quality.md'), '# quality\n');
132
- for (const [name, content] of Object.entries(files)) {
133
- if (content === null) continue;
134
- writeFileSync(path.join(branch, name), typeof content === 'string' ? content : JSON.stringify(content, null, 2));
135
- }
136
- return { branch, session };
137
- }
138
-
139
- const baseline = (over = {}) => ({
140
- 'request/request.json': requestJson(),
141
- 'response/response.json': responseJson(),
142
- 'response/response.md': responseMd(),
143
- 'response/data/probes.json': probesJson(),
144
- ...over,
145
- });
146
-
147
-
148
- // The recovery fallback: two failing observations, then steady again on the same release.
149
- const recovered = () => baseline({
150
- 'response/response.json': responseJson({ fallbacks: ['ROLLOUT_FAILED'] }),
151
- 'response/data/probes.json': probesJson({
152
- observations: [observation('2026-01-10T00:01:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:03:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:07:00.000Z', 'steady')],
153
- }),
154
- 'response/response.md': responseMd({
155
- branch: 'recover',
156
- steps: [...STEPS, ['`recover`', 'applied', '5', '6', 'the approved reversible action restored the target']],
157
- fallbacksTaken: ['ROLLOUT_FAILED'],
158
- }),
159
- });
160
-
161
- // Both fallbacks in order: the recovery ran out and the safe release was restored by digest.
162
- const rolledBack = () => baseline({
163
- 'response/response.json': responseJson({ fallbacks: ['ROLLOUT_FAILED', 'RECOVERY_EXHAUSTED'] }),
164
- 'response/data/probes.json': probesJson({
165
- observations: [observation('2026-01-10T00:01:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:03:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:09:00.000Z', 'steady')],
166
- }),
167
- 'response/response.md': responseMd({
168
- outcome: 'rolled-back', branch: 'rollback',
169
- steps: [...STEPS, ['`recover`', 'applied', '5', '6', 'the approved actions ran out'], ['`rollback`', 'applied', '6', '7', 'the safe release was restored by digest']],
170
- steady: { 'Active digest': SAFE_DIGEST, 'Available targets': '1 of 1', 'Superseded active': '0', 'Window elapsed': '300' },
171
- fallbacksTaken: ['ROLLOUT_FAILED', 'RECOVERY_EXHAUSTED'],
172
- }),
173
- });
174
-
175
- async function expectValid(files, label) {
176
- const { branch, session } = writeBranch(files);
177
- const { errors } = await validateReleaseStep(branch);
178
- rmSync(session, { recursive: true, force: true });
179
- assert.deepEqual(errors, [], `${label} should be valid`);
180
- }
181
- async function expectError(files, needle, label) {
182
- const { branch, session } = writeBranch(files);
183
- const { errors } = await validateReleaseStep(branch);
184
- rmSync(session, { recursive: true, force: true });
185
- assert.ok(errors.some((e) => e.includes(needle)), `${label}: expected an error containing "${needle}", got:\n${errors.join('\n') || '(none)'}`);
186
- }
187
-
188
- await expectValid(baseline(), 'a steady deployment of the immutable release');
189
- await expectValid(recovered(), 'the recovery fallback taken after a persistent failure');
190
- await expectValid(rolledBack(), 'both fallbacks in order, ending in a restored release');
191
- await expectValid({
192
- 'request/request.json': requestJson(),
193
- 'response/response.json': responseJson({ status: 'blocked', stop: 'STEADY_STATE_UNPROVEN', next: [] }),
194
- 'response/response.md': null, 'response/data/probes.json': null,
195
- }, 'blocked because the steady window never closed');
196
-
197
- await expectError(baseline({ 'response/response.json': { ...responseJson(), stop: 'ROLLOUT_FAILED' } }), 'only a blocked response carries a stop', 'done with a stop');
198
- await expectError(baseline({ 'response/response.json': responseJson({ status: 'blocked', stop: 'ROLLOUT_FAILED', next: [] }) }), 'has disposition fallback under these requirements', 'a fallback code used to block the branch');
199
- await expectError(baseline({ 'response/response.json': responseJson({ fallbacks: ['STEADY_STATE_UNPROVEN'] }) }), 'cannot be taken as a fallback', 'a terminate code taken as a fallback');
200
- await expectError(baseline({
201
- 'response/response.json': responseJson({ fallbacks: ['RECOVERY_EXHAUSTED'] }),
202
- 'response/response.md': responseMd({ outcome: 'rolled-back', branch: 'rollback', steps: [...STEPS, ['`rollback`', 'applied', '6', '7', 'restored']], steady: { 'Active digest': SAFE_DIGEST, 'Available targets': '1 of 1', 'Superseded active': '0', 'Window elapsed': '300' }, fallbacksTaken: ['RECOVERY_EXHAUSTED'] }),
203
- }), 'reached only through an exhausted recovery', 'a rollback that skipped the recovery branch');
204
- await expectError(baseline({ 'request/request.json': requestJson({ extra: { strategy: 'rolling' } }) }), 'requirements.strategy is not a field', 'a field the operator no longer declares');
205
- await expectError(baseline({ 'request/request.json': requestJson({ approval: null }) }), 'required field approval has no value', 'a production deploy nobody approved');
206
- await expectError(baseline({ 'request/request.json': requestJson({ probes: [{ probeId: 'container-health', kind: 'internal', endpointRef: 'unix:///health', expectStatus: 200 }] }), 'response/data/probes.json': probesJson({ observations: [{ ...observation('2026-01-10T00:01:00.000Z', 'progressing'), probeResults: [{ probeId: 'container-health', status: 'pass', observedStatus: 200, observedAt: '2026-01-10T00:01:00.000Z' }] }, { ...observation('2026-01-10T00:07:00.000Z', 'steady'), probeResults: [{ probeId: 'container-health', status: 'pass', observedStatus: 200, observedAt: '2026-01-10T00:07:00.000Z' }] }] }) }), 'at least one declared probe is public', 'a run proving only container health');
207
- await expectError(baseline({ 'request/request.json': requestJson({ rollbackIdentity: { ...ROLLBACK, digest: 'v1.4.0' } }) }), 'a rollback by tag restores whatever the tag now points at', 'a rollback identity named by tag');
208
- await expectError(baseline({ 'request/request.json': requestJson({ extra: { release: RELEASE }, target: TARGET, approval: 'ghp_abcdefghijklmnopqrstuvwxyz' }) }), 'looks like a resolved credential value', 'a token where a handle belongs');
209
- await expectError(baseline({ 'request/request.json': requestJson({ deadline: 120 }) }), 'but the request pinned 120s', 'monitoring measured against another deadline');
210
- await expectError(baseline({ 'response/data/probes.json': probesJson({ elapsedSeconds: 900 }) }), 'ran past its own bounded deadline', 'monitoring that outran its deadline');
211
- await expectError(baseline({ 'response/data/probes.json': probesJson({ backoffSeconds: 900 }) }), 'backoff cannot exceed the deadline', 'a backoff wider than the window');
212
- await expectError({
213
- ...recovered(),
214
- 'response/data/probes.json': probesJson({ observations: [observation('2026-01-10T00:01:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:07:00.000Z', 'steady')] }),
215
- }, 'one transient probe is not a persistent failure', 'recovery entered on a single failing probe');
216
- await expectError(baseline({ 'response/data/probes.json': probesJson({ finalCondition: 'deadline-exceeded' }) }), 'which is STEADY_STATE_UNPROVEN, not a deployment', 'a deployment over an exceeded deadline');
217
- await expectError(baseline({ 'response/response.md': responseMd({ release: 'release:someone-elses' }) }), 'but the request bound', 'a receipt for another release');
218
- await expectError(baseline({ 'response/response.md': responseMd({ digest: 'latest' }) }), 'identified by its digest, never by a tag', 'a release named by tag');
219
- await expectError(baseline({ 'response/response.md': responseMd({ approval: '`someone-elses-grant`' }) }), 'names an approval the request did not bind', 'a grant borrowed from another deployment');
220
- await expectError(baseline({ 'response/response.md': responseMd({ branch: 'recover' }) }), 'but the fallbacks taken say none', 'a branch the fallbacks never recorded');
221
- await expectError({
222
- ...rolledBack(),
223
- 'response/response.md': responseMd({ outcome: 'deployed', branch: 'rollback', steps: [...STEPS, ['`recover`', 'applied', '5', '6', 'x'], ['`rollback`', 'applied', '6', '7', 'y']], fallbacksTaken: ['ROLLOUT_FAILED', 'RECOVERY_EXHAUSTED'] }),
224
- }, 'a restored release is its own terminal', 'a rollback reported as delivery');
225
- await expectError(baseline({ 'response/response.md': responseMd({ steps: [['`authorize`', 'applied', '—', '—', 'x'], ['`rollout`', 'applied', '4', '4', 'y'], ['`monitor`', 'applied', '—', '—', 'z']] }) }), 'claims it applied without moving a revision', 'an application that moved nothing');
226
- await expectError(baseline({ 'response/response.md': responseMd({ steps: [['`authorize`', 'applied', '2', '3', 'x'], ['`rollout`', 'applied', '4', '5', 'y'], ['`monitor`', 'applied', '—', '—', 'z']] }) }), 'reports a revision for a boundary it never touched', 'a reading step inventing a revision');
227
- await expectError(baseline({ 'response/response.md': responseMd({ steady: { 'Active digest': SAFE_DIGEST, 'Available targets': '1 of 1', 'Superseded active': '0', 'Window elapsed': '300' } }) }), 'while another digest is active', 'a deployment over the old digest');
228
- await expectError(baseline({ 'response/response.md': responseMd({ steady: { 'Active digest': DIGEST, 'Available targets': '1 of 2', 'Superseded active': '0', 'Window elapsed': '300' } }) }), 'a partly available target set is not steady state', 'one target absorbing the load for two');
229
- await expectError(baseline({ 'response/response.md': responseMd({ steady: { 'Active digest': DIGEST, 'Available targets': '1 of 1', 'Superseded active': '1', 'Window elapsed': '300' } }) }), 'still serving traffic', 'a superseded target left active');
230
- await expectError(baseline({ 'response/data/probes.json': probesJson({ observations: [observation('2026-01-10T00:07:00.000Z', 'steady')] }) }), 'claimed from a single observation', 'steady state read off one probe');
231
- await expectError(baseline({ 'response/response.md': responseMd({ monitoring: { Deadline: DEADLINE, Elapsed: 420, Backoff: 30, 'Final condition': 'progressing' } }) }), 'but the series ended steady', 'a receipt disagreeing with its own series');
232
- await expectError(baseline({ 'response/data/probes.json': { ...probesJson(), finalCondition: 'nope' } }), 'finalCondition', 'probes schema');
233
- await expectError(baseline({ 'response/response.md': responseMd().replace('## Steady state', '## Stable state') }), 'missing section ^## Steady state$', 'receipt section renamed');
234
-
235
- process.stdout.write('release.deploy self-test: 4 valid branches, 26 rejected mutations\n');
1
+ // Proves validate.mjs on a synthetic session branch: one steady deployment, one that took the
2
+ // recovery fallback, one that took both fallbacks and ended rolled back, one blocked on an unproven
3
+ // steady state, and one mutation per law, each of which must fail with a line that names the defect.
4
+ import assert from 'node:assert/strict';
5
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
6
+ import { tmpdir } from 'node:os';
7
+ import path from 'node:path';
8
+ import { validateReleaseStep } from './validate.mjs';
9
+
10
+ const RELEASE = 'release:2026.01.10-1';
11
+ const PREVIOUS = 'release:2026.01.03-2';
12
+ const DIGEST = `sha256:${'1'.repeat(64)}`;
13
+ const SAFE_DIGEST = `sha256:${'2'.repeat(64)}`;
14
+ const TARGET = 'production/api';
15
+ const APPROVAL = '@worktrees/businesses/features/release/model.json#deploy-grant';
16
+ const MANIFEST = '.stacks/production/api.manifest.json';
17
+ const ARTIFACT = '@remote/ghcr/starci/academy-api';
18
+ const DEADLINE = 600;
19
+ const PROBES = [
20
+ { probeId: 'public-graphql-typename', kind: 'public', endpointRef: 'https://api.example/graphql', expectStatus: 200 },
21
+ ];
22
+ const ROLLBACK = { releaseId: PREVIOUS, artifactRef: ARTIFACT, digest: SAFE_DIGEST, dataCompatible: true };
23
+
24
+ const observation = (at, condition, status = 'pass') => ({
25
+ observedAt: at, condition, activeReleaseIds: [RELEASE], activeDigest: DIGEST, availableTargets: 1,
26
+ probeResults: [{ probeId: 'public-graphql-typename', status, observedStatus: status === 'pass' ? 200 : 502, observedAt: at }],
27
+ });
28
+
29
+ const probesJson = ({ observations = [observation('2026-01-10T00:01:00.000Z', 'progressing'), observation('2026-01-10T00:07:00.000Z', 'steady')], finalCondition = 'steady', deadlineSeconds = DEADLINE, elapsedSeconds = 420, backoffSeconds = 30 } = {}) => ({
30
+ deadlineSeconds, elapsedSeconds, backoffSeconds, observations, finalCondition,
31
+ });
32
+
33
+ const NL = String.fromCharCode(10);
34
+ const STEPS = [
35
+ ['`authorize`', 'applied', '—', '—', 'the declared grant covers this project, environment and target'],
36
+ ['`rollout`', 'applied', '4', '5', 'the target moved to the immutable digest'],
37
+ ['`monitor`', 'applied', '—', '—', 'the window was observed to its end'],
38
+ ];
39
+
40
+ function responseMd({
41
+ release = RELEASE, digest = DIGEST, target = TARGET, approval = APPROVAL, deadline = DEADLINE,
42
+ rollbackId = PREVIOUS, outcome = 'deployed', branch = 'none', steps = STEPS,
43
+ monitoring = { Deadline: DEADLINE, Elapsed: 420, Backoff: 30, 'Final condition': 'steady' },
44
+ steady = { 'Active digest': DIGEST, 'Available targets': '1 of 1', 'Superseded active': '0', 'Window elapsed': '300' },
45
+ fallbacksTaken = [],
46
+ } = {}) {
47
+ return `# release-deployment — ${release}
48
+
49
+ The immutable release reached its declared target under the declared grant, and the steady state
50
+ rests on the observed digest, the available targets and every declared probe across the window.
51
+
52
+ ## Binding
53
+
54
+ | Field | Value |
55
+ | --- | --- |
56
+ | Operator | \`release.deploy\` |
57
+ | Step | \`step-1/parallel-1\` |
58
+ | Project | \`starci-academy\` |
59
+ | Release | ${release} |
60
+ | Artifact | \`${ARTIFACT}\` |
61
+ | Digest | \`${digest}\` |
62
+ | Target | ${target} |
63
+ | Environment | production |
64
+ | Replaced release | \`${PREVIOUS}\` |
65
+ | Approval | ${approval} |
66
+ | Manifest | \`${MANIFEST}\` |
67
+ | Steady deadline | ${deadline} |
68
+ | Rollback identity | ${rollbackId} |
69
+
70
+ ## Outcome
71
+
72
+ | Field | Value |
73
+ | --- | --- |
74
+ | Outcome | ${outcome} |
75
+ | Branch | ${branch} |
76
+
77
+ ## Steps
78
+
79
+ | Step | State | Revision before | Revision after | Statement |
80
+ | --- | --- | --- | --- | --- |
81
+ ${steps.map((s) => `| ${s[0]} | ${s[1]} | ${s[2]} | ${s[3]} | ${s[4]} |`).join('\n')}
82
+
83
+ ## Monitoring
84
+
85
+ | Field | Value |
86
+ | --- | --- |
87
+ | Deadline | ${monitoring.Deadline} |
88
+ | Elapsed | ${monitoring.Elapsed} |
89
+ | Backoff | ${monitoring.Backoff} |
90
+ | Final condition | ${monitoring['Final condition']} |
91
+
92
+ ## Steady state
93
+
94
+ | Metric | Value |
95
+ | --- | --- |
96
+ ${Object.entries(steady).map(([k, v]) => `| ${k} | ${/^sha256:/.test(String(v)) ? `\`${v}\`` : v} |`).join('\n')}
97
+
98
+ ## Findings
99
+
100
+ | Code | Step | Statement |
101
+ | --- | --- | --- |
102
+ | \`IDEMPOTENT_NO_OP\` | \`host-prepare\` | the host already matched the declaration |
103
+
104
+ ## Fallbacks taken
105
+
106
+ | Code | Action |
107
+ | --- | --- |
108
+ ${fallbacksTaken.map((c) => `| \`${c}\` | the branch was taken and recorded |`).join(NL)}
109
+ `;
110
+ }
111
+
112
+ const requestJson = ({ release = RELEASE, target = TARGET, approval = APPROVAL, probes = PROBES, deadline = DEADLINE, rollbackIdentity = ROLLBACK, extra = {} } = {}) => ({
113
+ schemaVersion: 9, operatorId: 'release.deploy', step: 1, parallel: 1, sessionId: 's-test',
114
+ contexts: [{ alias: '@remote/ghcr/starci/academy-api', head: null }],
115
+ requirements: { release, target, approval, probes, steadyDeadline: deadline, rollbackIdentity, resume: null, ...extra },
116
+ inputs: { 'quality-verification': 'step-1/parallel-1/response/quality.md' }, resume: null,
117
+ });
118
+
119
+ const responseJson = ({ status = 'done', stop, fallbacks = [], next = ['platform.operate'] } = {}) => ({
120
+ schemaVersion: 9, operatorId: 'release.deploy', step: 1, parallel: 1, status, ...(stop ? { stop } : {}),
121
+ fallbacks,
122
+ fields: status === 'blocked' ? {} : { 'release-deployment': 'response/response.md', probes: 'response/data/probes.json' },
123
+ commits: [], next,
124
+ });
125
+
126
+ function writeBranch(files) {
127
+ const session = mkdtempSync(path.join(tmpdir(), 'release-session-'));
128
+ const branch = path.join(session, 'step-1', 'parallel-1');
129
+ for (const d of ['request', 'response/data', 'response/artifacts']) mkdirSync(path.join(branch, d), { recursive: true });
130
+ writeFileSync(path.join(session, 'state.json'), JSON.stringify({ id: 's-test', project: 'starci-academy', startedAt: '2026-09-03T00:00:00Z', requestHashes: {}, chain: [['1/1']], steps: { '1/1': 'release.deploy' }, current: '1/1', status: 'running' }));
131
+ writeFileSync(path.join(branch, 'response', 'quality.md'), '# quality\n');
132
+ for (const [name, content] of Object.entries(files)) {
133
+ if (content === null) continue;
134
+ writeFileSync(path.join(branch, name), typeof content === 'string' ? content : JSON.stringify(content, null, 2));
135
+ }
136
+ return { branch, session };
137
+ }
138
+
139
+ const baseline = (over = {}) => ({
140
+ 'request/request.json': requestJson(),
141
+ 'response/response.json': responseJson(),
142
+ 'response/response.md': responseMd(),
143
+ 'response/data/probes.json': probesJson(),
144
+ ...over,
145
+ });
146
+
147
+
148
+ // The recovery fallback: two failing observations, then steady again on the same release.
149
+ const recovered = () => baseline({
150
+ 'response/response.json': responseJson({ fallbacks: ['ROLLOUT_FAILED'] }),
151
+ 'response/data/probes.json': probesJson({
152
+ observations: [observation('2026-01-10T00:01:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:03:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:07:00.000Z', 'steady')],
153
+ }),
154
+ 'response/response.md': responseMd({
155
+ branch: 'recover',
156
+ steps: [...STEPS, ['`recover`', 'applied', '5', '6', 'the approved reversible action restored the target']],
157
+ fallbacksTaken: ['ROLLOUT_FAILED'],
158
+ }),
159
+ });
160
+
161
+ // Both fallbacks in order: the recovery ran out and the safe release was restored by digest.
162
+ const rolledBack = () => baseline({
163
+ 'response/response.json': responseJson({ fallbacks: ['ROLLOUT_FAILED', 'RECOVERY_EXHAUSTED'] }),
164
+ 'response/data/probes.json': probesJson({
165
+ observations: [observation('2026-01-10T00:01:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:03:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:09:00.000Z', 'steady')],
166
+ }),
167
+ 'response/response.md': responseMd({
168
+ outcome: 'rolled-back', branch: 'rollback',
169
+ steps: [...STEPS, ['`recover`', 'applied', '5', '6', 'the approved actions ran out'], ['`rollback`', 'applied', '6', '7', 'the safe release was restored by digest']],
170
+ steady: { 'Active digest': SAFE_DIGEST, 'Available targets': '1 of 1', 'Superseded active': '0', 'Window elapsed': '300' },
171
+ fallbacksTaken: ['ROLLOUT_FAILED', 'RECOVERY_EXHAUSTED'],
172
+ }),
173
+ });
174
+
175
+ async function expectValid(files, label) {
176
+ const { branch, session } = writeBranch(files);
177
+ const { errors } = await validateReleaseStep(branch);
178
+ rmSync(session, { recursive: true, force: true });
179
+ assert.deepEqual(errors, [], `${label} should be valid`);
180
+ }
181
+ async function expectError(files, needle, label) {
182
+ const { branch, session } = writeBranch(files);
183
+ const { errors } = await validateReleaseStep(branch);
184
+ rmSync(session, { recursive: true, force: true });
185
+ assert.ok(errors.some((e) => e.includes(needle)), `${label}: expected an error containing "${needle}", got:\n${errors.join('\n') || '(none)'}`);
186
+ }
187
+
188
+ await expectValid(baseline(), 'a steady deployment of the immutable release');
189
+ await expectValid(recovered(), 'the recovery fallback taken after a persistent failure');
190
+ await expectValid(rolledBack(), 'both fallbacks in order, ending in a restored release');
191
+ await expectValid({
192
+ 'request/request.json': requestJson(),
193
+ 'response/response.json': responseJson({ status: 'blocked', stop: 'STEADY_STATE_UNPROVEN', next: [] }),
194
+ 'response/response.md': null, 'response/data/probes.json': null,
195
+ }, 'blocked because the steady window never closed');
196
+
197
+ await expectError(baseline({ 'response/response.json': { ...responseJson(), stop: 'ROLLOUT_FAILED' } }), 'only a blocked response carries a stop', 'done with a stop');
198
+ await expectError(baseline({ 'response/response.json': responseJson({ status: 'blocked', stop: 'ROLLOUT_FAILED', next: [] }) }), 'has disposition fallback under these requirements', 'a fallback code used to block the branch');
199
+ await expectError(baseline({ 'response/response.json': responseJson({ fallbacks: ['STEADY_STATE_UNPROVEN'] }) }), 'cannot be taken as a fallback', 'a terminate code taken as a fallback');
200
+ await expectError(baseline({
201
+ 'response/response.json': responseJson({ fallbacks: ['RECOVERY_EXHAUSTED'] }),
202
+ 'response/response.md': responseMd({ outcome: 'rolled-back', branch: 'rollback', steps: [...STEPS, ['`rollback`', 'applied', '6', '7', 'restored']], steady: { 'Active digest': SAFE_DIGEST, 'Available targets': '1 of 1', 'Superseded active': '0', 'Window elapsed': '300' }, fallbacksTaken: ['RECOVERY_EXHAUSTED'] }),
203
+ }), 'reached only through an exhausted recovery', 'a rollback that skipped the recovery branch');
204
+ await expectError(baseline({ 'request/request.json': requestJson({ extra: { strategy: 'rolling' } }) }), 'requirements.strategy is not a field', 'a field the operator no longer declares');
205
+ await expectError(baseline({ 'request/request.json': requestJson({ approval: null }) }), 'required field approval has no value', 'a production deploy nobody approved');
206
+ await expectError(baseline({ 'request/request.json': requestJson({ probes: [{ probeId: 'container-health', kind: 'internal', endpointRef: 'unix:///health', expectStatus: 200 }] }), 'response/data/probes.json': probesJson({ observations: [{ ...observation('2026-01-10T00:01:00.000Z', 'progressing'), probeResults: [{ probeId: 'container-health', status: 'pass', observedStatus: 200, observedAt: '2026-01-10T00:01:00.000Z' }] }, { ...observation('2026-01-10T00:07:00.000Z', 'steady'), probeResults: [{ probeId: 'container-health', status: 'pass', observedStatus: 200, observedAt: '2026-01-10T00:07:00.000Z' }] }] }) }), 'at least one declared probe is public', 'a run proving only container health');
207
+ await expectError(baseline({ 'request/request.json': requestJson({ rollbackIdentity: { ...ROLLBACK, digest: 'v1.4.0' } }) }), 'a rollback by tag restores whatever the tag now points at', 'a rollback identity named by tag');
208
+ await expectError(baseline({ 'request/request.json': requestJson({ extra: { release: RELEASE }, target: TARGET, approval: 'ghp_abcdefghijklmnopqrstuvwxyz' }) }), 'looks like a resolved credential value', 'a token where a handle belongs');
209
+ await expectError(baseline({ 'request/request.json': requestJson({ deadline: 120 }) }), 'but the request pinned 120s', 'monitoring measured against another deadline');
210
+ await expectError(baseline({ 'response/data/probes.json': probesJson({ elapsedSeconds: 900 }) }), 'ran past its own bounded deadline', 'monitoring that outran its deadline');
211
+ await expectError(baseline({ 'response/data/probes.json': probesJson({ backoffSeconds: 900 }) }), 'backoff cannot exceed the deadline', 'a backoff wider than the window');
212
+ await expectError({
213
+ ...recovered(),
214
+ 'response/data/probes.json': probesJson({ observations: [observation('2026-01-10T00:01:00.000Z', 'failing', 'fail'), observation('2026-01-10T00:07:00.000Z', 'steady')] }),
215
+ }, 'one transient probe is not a persistent failure', 'recovery entered on a single failing probe');
216
+ await expectError(baseline({ 'response/data/probes.json': probesJson({ finalCondition: 'deadline-exceeded' }) }), 'which is STEADY_STATE_UNPROVEN, not a deployment', 'a deployment over an exceeded deadline');
217
+ await expectError(baseline({ 'response/response.md': responseMd({ release: 'release:someone-elses' }) }), 'but the request bound', 'a receipt for another release');
218
+ await expectError(baseline({ 'response/response.md': responseMd({ digest: 'latest' }) }), 'identified by its digest, never by a tag', 'a release named by tag');
219
+ await expectError(baseline({ 'response/response.md': responseMd({ approval: '`someone-elses-grant`' }) }), 'names an approval the request did not bind', 'a grant borrowed from another deployment');
220
+ await expectError(baseline({ 'response/response.md': responseMd({ branch: 'recover' }) }), 'but the fallbacks taken say none', 'a branch the fallbacks never recorded');
221
+ await expectError({
222
+ ...rolledBack(),
223
+ 'response/response.md': responseMd({ outcome: 'deployed', branch: 'rollback', steps: [...STEPS, ['`recover`', 'applied', '5', '6', 'x'], ['`rollback`', 'applied', '6', '7', 'y']], fallbacksTaken: ['ROLLOUT_FAILED', 'RECOVERY_EXHAUSTED'] }),
224
+ }, 'a restored release is its own terminal', 'a rollback reported as delivery');
225
+ await expectError(baseline({ 'response/response.md': responseMd({ steps: [['`authorize`', 'applied', '—', '—', 'x'], ['`rollout`', 'applied', '4', '4', 'y'], ['`monitor`', 'applied', '—', '—', 'z']] }) }), 'claims it applied without moving a revision', 'an application that moved nothing');
226
+ await expectError(baseline({ 'response/response.md': responseMd({ steps: [['`authorize`', 'applied', '2', '3', 'x'], ['`rollout`', 'applied', '4', '5', 'y'], ['`monitor`', 'applied', '—', '—', 'z']] }) }), 'reports a revision for a boundary it never touched', 'a reading step inventing a revision');
227
+ await expectError(baseline({ 'response/response.md': responseMd({ steady: { 'Active digest': SAFE_DIGEST, 'Available targets': '1 of 1', 'Superseded active': '0', 'Window elapsed': '300' } }) }), 'while another digest is active', 'a deployment over the old digest');
228
+ await expectError(baseline({ 'response/response.md': responseMd({ steady: { 'Active digest': DIGEST, 'Available targets': '1 of 2', 'Superseded active': '0', 'Window elapsed': '300' } }) }), 'a partly available target set is not steady state', 'one target absorbing the load for two');
229
+ await expectError(baseline({ 'response/response.md': responseMd({ steady: { 'Active digest': DIGEST, 'Available targets': '1 of 1', 'Superseded active': '1', 'Window elapsed': '300' } }) }), 'still serving traffic', 'a superseded target left active');
230
+ await expectError(baseline({ 'response/data/probes.json': probesJson({ observations: [observation('2026-01-10T00:07:00.000Z', 'steady')] }) }), 'claimed from a single observation', 'steady state read off one probe');
231
+ await expectError(baseline({ 'response/response.md': responseMd({ monitoring: { Deadline: DEADLINE, Elapsed: 420, Backoff: 30, 'Final condition': 'progressing' } }) }), 'but the series ended steady', 'a receipt disagreeing with its own series');
232
+ await expectError(baseline({ 'response/data/probes.json': { ...probesJson(), finalCondition: 'nope' } }), 'finalCondition', 'probes schema');
233
+ await expectError(baseline({ 'response/response.md': responseMd().replace('## Steady state', '## Stable state') }), 'missing section ^## Steady state$', 'receipt section renamed');
234
+
235
+ process.stdout.write('release.deploy self-test: 4 valid branches, 26 rejected mutations\n');
@@ -14,7 +14,20 @@ a shared runtime and leaves a permanent run record behind, so the trigger is a p
14
14
  schedule, a chain default or another agent's convenience. `runId` and `lease` are not questions for a
15
15
  person either: the orchestrator generates the run identifier and grants the exclusive lease on the
16
16
  flow directory before the branch starts, and an invocation that arrives without them is
17
- `INVALID_INPUT` rather than a prompt.
17
+ `INVALID_INPUT` at step 1 rather than a prompt. Their Default is therefore `—`: a Default that reads
18
+ "the orchestrator's run id" is prose, not a value the gate can use, and a gate that accepts prose
19
+ accepts an empty field. `LEASE_INVALID` is a different failure and keeps its own place: it is the
20
+ lease that exists and is expired, foreign, or bound to another run, noticed at step 6 against the
21
+ flow directory this run holds.
22
+
23
+ ## The endpoint is the bound one, never a re-derived one
24
+
25
+ The flow is driven against the endpoint the `route` Input carries, the one the `workspace.bind` branch
26
+ of this chain observed and closed. This operator does not re-derive readiness from the runtime
27
+ registry: a registry that advertises `ready` while nothing listens is exactly the source that sends a
28
+ browser at a dead port, and the bind step already refused a merely listening port on this chain's
29
+ behalf. When the bound endpoint does not answer, the stop is `RUNTIME_UNAVAILABLE` against a named
30
+ endpoint rather than a guess about which origin was meant.
18
31
 
19
32
  ## The password is a name, never a value
20
33
 
@@ -81,8 +94,8 @@ anything outside its own fixture namespace.
81
94
  | Alias | Bind | Required |
82
95
  | --- | --- | --- |
83
96
  | `@worktrees/uat/<flow>/<case>` | the flow directory: `flow.md`, `account.json`, `seed/`, the append-only `runs/<runId>/` history and the `latest` pointer, bound by fingerprint per file and written only under the exclusive lease | yes |
84
- | `@worktrees/_templates` | the UAT flow template a new flow directory is created from; consumed, never modified | yes |
85
- | `@worktrees/sessions/central-runtime` | the ready runtime owner, its generation and its exact origins; readiness is proved, not assumed | yes |
97
+ | `@worktrees/_templates` | the UAT flow template a new flow directory is created from, which is exactly the three things step 4 reads: `uat/flow.md` with the cases and their named assertions, `uat/account.json` with a username, a role, a credential name and the sealed file's path and no field that could hold a secret, and `uat/seed/` with the records a run namespaces; consumed, never modified | yes |
98
+ | `@worktrees/sessions/central-runtime` | the runtime owner's generation behind the bound endpoint; readiness is proved by the `route` Input, never re-derived from this registry | yes |
86
99
  | `@workspaces/device-state` | the sealed credential roster; the shared UAT password is resolved by name here at login and read nowhere else | yes |
87
100
  | `@workspaces/be` | the routed backend checkout at the pinned commit, whose behaviour the flow verifies and whose store holds the namespaced records | yes |
88
101
 
@@ -92,6 +105,7 @@ anything outside its own fixture namespace.
92
105
  | --- | --- | --- |
93
106
  | `frontend-surface-audit` | the surface audit that found the frontend clean, taken at the pinned commit | yes |
94
107
  | `quality-verification` | the quality gate that passed, taken at the same pinned commit | yes |
108
+ | `route` | `workspace.bind` on the fe role; the bound route whose endpoint this run drives | yes |
95
109
 
96
110
  ## Requirements
97
111
 
@@ -101,8 +115,8 @@ anything outside its own fixture namespace.
101
115
  | `feature` | id | — | The feature key that addresses the flow directory |
102
116
  | `flow` | id | — | The one product flow this invocation verifies |
103
117
  | `cases` | list of `caseId` | every case of the flow | Which frozen cases to run; the default is every case `flow.md` declares, in its order |
104
- | `runId` | id | the orchestrator's run id | Not asked of a person: the orchestrator generates it and it namespaces every record this run writes |
105
- | `lease` | token | the orchestrator's lease | Not asked of a person: the orchestrator grants the exclusive lease on the flow directory before the branch starts |
118
+ | `runId` | id | | Not asked of a person: the orchestrator fills it, and it namespaces every record this run writes |
119
+ | `lease` | token | | Not asked of a person: the orchestrator fills it, granting the exclusive lease on the flow directory before the branch starts |
106
120
  | `resume` | token | null | The blocked branch's token when re-entering after a stop |
107
121
 
108
122
  ## Steps
@@ -114,7 +128,7 @@ anything outside its own fixture namespace.
114
128
  | 3 | Preflight the runtime: the sealed credential resolves by name, the account exists, the store answers | — | @workspaces/device-state for the credential named by `account.json`, @worktrees/sessions/central-runtime for the generation and origins, @tools/secrets, @tools/http | — | `PROVISIONING_UNAVAILABLE` |
115
129
  | 4 | Freeze the snapshot from `flow.md`, `account.json` and `seed/` | `feature`, `flow`, `cases` | @worktrees/uat/<flow>/<case>, @worktrees/_templates for the flow template | @worktrees/uat/<flow>/<case> (snapshot), `response/data/snapshot.json`, @tools/sourcewrite | `CANONICAL_WRITE_DENIED` |
116
130
  | 5 | Seed the frozen records into the run namespace | `runId` | `response/data/snapshot.json`, @workspaces/be | @tools/database | `FIXTURE_VIOLATION` |
117
- | 6 | Execute the frozen cases in order on the session worktree at the pinned commit | — | `response/data/snapshot.json`, @worktrees/sessions/central-runtime for the origin and generation, @workspaces/device-state for the credential at login only, @tools/browsercontrol, @tools/websearch | — | `LEASE_INVALID`, `RUNTIME_UNAVAILABLE` |
131
+ | 6 | Execute the frozen cases in order against the endpoint the bound route carries, at the pinned commit | — | `response/data/snapshot.json`, input `route` for the endpoint this run drives, @worktrees/sessions/central-runtime for the generation behind that endpoint, @workspaces/device-state for the credential at login only, @tools/browsercontrol, @tools/websearch | — | `LEASE_INVALID`, `RUNTIME_UNAVAILABLE` |
118
132
  | 7 | Capture at each named assertion with the login field masked, and stitch the sheet | — | `response/data/snapshot.json`, @worktrees/sessions/central-runtime for the most direct runtime evidence | `response/data/captures/<case>.json`, `response/artifacts/<case>.png`, `response/artifacts/sheet.png`, @tools/visualize | `EVIDENCE_UNAVAILABLE` |
119
133
  | 8 | Judge the three lanes apart | — | `response/data/captures/<case>.json` | `response/data/verdicts.json` | — |
120
134
  | 9 | Verify read-only, then delete the run namespace and nothing else | `runId` | @workspaces/be for the records carrying `is_uat=true` and this namespace, `response/data/verdicts.json` | @tools/database | — |