@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.
- package/INDEX.md +3 -1
- package/INDEX.vi.md +3 -1
- package/README.md +6 -1
- package/README.vi.md +6 -1
- package/knowledge/ui/presentation/INDEX.md +5 -0
- package/knowledge/ui/presentation/INDEX.vi.md +1 -0
- package/knowledge/ui/presentation/radius.md +183 -0
- package/knowledge/ui/presentation/radius.vi.md +182 -0
- package/operators/INDEX.md +5 -5
- package/operators/INDEX.vi.md +5 -5
- package/operators/architecture-decide/operator.md +27 -4
- package/operators/architecture-decide/operator.vi.md +23 -4
- package/operators/architecture-decide/self-test.mjs +18 -1
- package/operators/architecture-decide/validate.mjs +29 -1
- package/operators/backend-source-apply/operator.md +197 -182
- package/operators/backend-source-apply/operator.vi.md +190 -178
- package/operators/backend-source-apply/self-test.mjs +257 -257
- package/operators/backend-source-apply/validate.mjs +240 -240
- package/operators/business-decide/self-test.mjs +2 -1
- package/operators/business-decide/validate.mjs +3 -0
- package/operators/content-generate/self-test.mjs +1 -1
- package/operators/frontend-direction-decide/self-test.mjs +1 -1
- package/operators/frontend-presentation-resolve/operator.md +6 -0
- package/operators/frontend-presentation-resolve/operator.vi.md +6 -0
- package/operators/frontend-presentation-resolve/self-test.mjs +6 -5
- package/operators/frontend-presentation-resolve/validate.mjs +30 -6
- package/operators/frontend-source-apply/operator.md +3 -1
- package/operators/frontend-source-apply/operator.vi.md +3 -1
- package/operators/frontend-source-apply/self-test.mjs +1 -1
- package/operators/frontend-surface-audit/self-test.mjs +1 -1
- package/operators/git-publish/self-test.mjs +1 -1
- package/operators/platform-operate/self-test.mjs +1 -1
- package/operators/quality-verify/operator.md +5 -1
- package/operators/quality-verify/operator.vi.md +5 -1
- package/operators/quality-verify/self-test.mjs +15 -2
- package/operators/quality-verify/validate.mjs +21 -1
- package/operators/release-deploy/self-test.mjs +235 -235
- package/operators/uat-verify/operator.md +20 -6
- package/operators/uat-verify/operator.vi.md +21 -7
- package/operators/uat-verify/self-test.mjs +6 -5
- package/operators/uat-verify/validate.mjs +10 -4
- package/operators/workspace-bind/operator.md +18 -4
- package/operators/workspace-bind/operator.vi.md +16 -4
- package/operators/workspace-bind/self-test.mjs +3 -1
- package/operators/workspace-bind/validate.mjs +1 -0
- package/package.json +1 -1
- package/readiness/initialization/workspaces/local-route.schema.json +1 -1
- package/resources/orchestrator.json +14 -8
- package/routing.json +2 -1
- package/scripts/validate-request.mjs +12 -1
- package/scripts/validate-response.mjs +14 -3
- package/scripts/validate-step.mjs +2 -1
- package/scripts/validate-workflows.mjs +8 -1
- package/templates/kinds/architecture-decision.contract.json +1 -0
- package/templates/kinds/architecture-decision.skeleton.md +6 -0
- package/templates/kinds/backend-source-application.contract.json +1 -1
- package/templates/kinds/backend-source-application.skeleton.md +1 -1
- package/templates/kinds/frontend-presentation-resolution.contract.json +1 -1
- package/templates/kinds/frontend-presentation-resolution.skeleton.md +1 -0
- package/templates/kinds/mutations.schema.json +65 -65
- package/templates/kinds/route.schema.json +4 -2
- package/templates/kinds/stack-model.schema.json +23 -1
- package/templates/kinds/workspace-route-binding.contract.json +2 -2
- package/templates/step/response.schema.json +10 -0
- package/templates/step/state.schema.json +205 -0
- package/workflows/README.md +3 -0
- package/workflows/README.vi.md +3 -0
- package/workflows/backend-feature.json +26 -5
- package/workflows/content-unit.json +4 -1
- package/workflows/frontend-new-surface.json +15 -5
- package/workflows/frontend-reconstruct.json +10 -4
- package/workflows/frontend-refine.json +10 -4
- package/workflows/frontend-with-uat.json +15 -3
- package/workflows/full-feature.json +31 -8
- package/workflows/release.json +7 -1
|
@@ -1,257 +1,257 @@
|
|
|
1
|
-
// Proves validate.mjs on a synthetic session branch: one conforming implementation of a frozen
|
|
2
|
-
// contract committed once on the session branch, with its change record, one conformance file per
|
|
3
|
-
// facet and one proof file per proof kind, one dry run that plans the same write set and commits,
|
|
4
|
-
// measures and writes nothing, one branch blocked on a terminate code, and one mutation per law,
|
|
5
|
-
// each of which must fail with a line that names the defect.
|
|
6
|
-
import assert from 'node:assert/strict';
|
|
7
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
8
|
-
import { tmpdir } from 'node:os';
|
|
9
|
-
import path from 'node:path';
|
|
10
|
-
import { validateBackendStep } from './validate.mjs';
|
|
11
|
-
|
|
12
|
-
const BASE = 'f'.repeat(40);
|
|
13
|
-
const COMMIT = 'a1b2'.repeat(10);
|
|
14
|
-
const BRANCH = 'session/s-test';
|
|
15
|
-
const hash = (c) => `sha256:${c.repeat(64)}`;
|
|
16
|
-
const CONTRACT_FP = hash('a');
|
|
17
|
-
const OP = 'enrol-course';
|
|
18
|
-
const WRITER = 'src/features/api/core/graphql/mutations/enrol-course.handler.ts';
|
|
19
|
-
const SPEC = 'src/features/api/core/graphql/mutations/enrol-course.handler.spec.ts';
|
|
20
|
-
const FACETS = ['transport', 'writer', 'transaction', 'idempotency', 'exception-identity', 'authorization'];
|
|
21
|
-
const PROOF_KINDS = ['unit', 'integration'];
|
|
22
|
-
|
|
23
|
-
const operation = (overrides = {}) => ({
|
|
24
|
-
operationId: OP, name: 'enrolCourse', transport: 'graphql-mutation', writerRef: WRITER,
|
|
25
|
-
storeRefs: ['enrollments'], transactionBoundary: 'single-transaction', idempotencyKind: 'request-token',
|
|
26
|
-
migrationRefs: [],
|
|
27
|
-
...overrides,
|
|
28
|
-
});
|
|
29
|
-
const CHANGES = [
|
|
30
|
-
{ path: WRITER, change: 'added', operationId: OP, beforeHash: null, afterHash: hash('1') },
|
|
31
|
-
{ path: SPEC, change: 'added', operationId: OP, beforeHash: null, afterHash: hash('2') },
|
|
32
|
-
];
|
|
33
|
-
const DRY_CHANGES = CHANGES.map((c) => ({ ...c, afterHash: null }));
|
|
34
|
-
const mutationsJson = ({ operations = [operation()], changes = CHANGES, commit = COMMIT, base = BASE, branch = BRANCH, mode = 'apply' } = {}) => ({
|
|
35
|
-
mode, contractFingerprint: CONTRACT_FP, base, branch, commit, operations, changes,
|
|
36
|
-
});
|
|
37
|
-
const conformanceRecord = (facet, overrides = {}) => ({ operationId: OP, facet, verdict: 'conforms', evidenceRef: `${SPEC}:40`, statement: `the ${facet} of the operation matches the contract`, contractFingerprint: CONTRACT_FP, ...overrides });
|
|
38
|
-
const proofRecord = (proofKind, overrides = {}) => ({ operationId: OP, proofKind, commandRef: `npm run test:${proofKind}`, exitCode: 0, result: 'passed', output: `${proofKind} suite: 12 passed`, statement: `the ${proofKind} proof ran and passed`, contractFingerprint: CONTRACT_FP, ...overrides });
|
|
39
|
-
const conformancePath = (operationId, facet) => `response/data/conformance/${operationId}.${facet}.json`;
|
|
40
|
-
const proofPath = (operationId, proofKind) => `response/data/proofs/${operationId}.${proofKind}.json`;
|
|
41
|
-
|
|
42
|
-
function responseMd({ operations = null, changes = null, findings = null, contractFingerprint = CONTRACT_FP, commit = COMMIT, base = BASE, branch = BRANCH, mode = 'apply' } = {}) {
|
|
43
|
-
const operationRows = (operations ?? [[OP, 'graphql-mutation', WRITER, 'single-transaction', 'request-token', '
|
|
44
|
-
const changeRows = (changes ?? [[WRITER, 'added', OP, '—', hash('1')], [SPEC, 'added', OP, '—', hash('2')]]).map((r) => `| \`${r[0]}\` | ${r[1]} | \`${r[2]}\` | ${r[3]} | ${r[4]} |`).join('\n');
|
|
45
|
-
const findingRows = (findings ?? [['PATTERN_BOUND', OP, WRITER, 'the mutation handler mirrors the published command family']]).map(([code, op, file, statement]) => `| \`${code}\` | ${op === null ? '—' : `\`${op}\``} | ${file === null ? '—' : `\`${file}\``} | ${statement} |`).join('\n');
|
|
46
|
-
return `# backend-source-application — enrol-course
|
|
47
|
-
|
|
48
|
-
The enrolment mutation, filled inside the frozen contract and measured on every declared facet.
|
|
49
|
-
|
|
50
|
-
## Binding
|
|
51
|
-
|
|
52
|
-
| Field | Value |
|
|
53
|
-
| --- | --- |
|
|
54
|
-
| Outcome | one enrolment mutation behind the paid access promise |
|
|
55
|
-
| Feature | paid-access |
|
|
56
|
-
| Mode | ${mode} |
|
|
57
|
-
| Contract fingerprint | ${contractFingerprint} |
|
|
58
|
-
| Base | ${base} |
|
|
59
|
-
| Branch | ${branch} |
|
|
60
|
-
| Commit | ${commit} |
|
|
61
|
-
|
|
62
|
-
## Operations
|
|
63
|
-
|
|
64
|
-
| Operation | Transport | Writer | Transaction | Idempotency | Decisions |
|
|
65
|
-
| --- | --- | --- | --- | --- | --- |
|
|
66
|
-
${operationRows}
|
|
67
|
-
|
|
68
|
-
## Changes
|
|
69
|
-
|
|
70
|
-
| Path | Change | Operation | Before | After |
|
|
71
|
-
| --- | --- | --- | --- | --- |
|
|
72
|
-
${changeRows}
|
|
73
|
-
|
|
74
|
-
## Findings
|
|
75
|
-
|
|
76
|
-
| Code | Operation | File | Statement |
|
|
77
|
-
| --- | --- | --- | --- |
|
|
78
|
-
${findingRows}
|
|
79
|
-
`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const changesMd = ({ files = null, checkout = `\`@workspaces/be\` at \`${BASE}\` → \`${COMMIT}\` on \`${BRANCH}\`` } = {}) => {
|
|
83
|
-
const rows = (files ?? [[WRITER, 'created'], [SPEC, 'created']]).map(([p, kind, why]) => `| \`${p}\` | ${kind} | ${why ?? 'the decision this file carries'} | BE-1 |`).join('\n');
|
|
84
|
-
return `# changes — backend.source.apply step-1/parallel-1
|
|
85
|
-
|
|
86
|
-
The enrolment mutation and its unit spec were written into the session branch of the routed backend
|
|
87
|
-
checkout and committed once.
|
|
88
|
-
|
|
89
|
-
## Binding
|
|
90
|
-
|
|
91
|
-
| Field | Value |
|
|
92
|
-
| --- | --- |
|
|
93
|
-
| Operator | \`backend.source.apply\` |
|
|
94
|
-
| Step | \`step-1/parallel-1\` |
|
|
95
|
-
| Checkout | ${checkout} |
|
|
96
|
-
| Predecessor | \`step-1/parallel-2/response/response.md\` |
|
|
97
|
-
|
|
98
|
-
## Files
|
|
99
|
-
|
|
100
|
-
| Path | Change | Why | Claims |
|
|
101
|
-
| --- | --- | --- | --- |
|
|
102
|
-
${rows}
|
|
103
|
-
|
|
104
|
-
## What the next step must know
|
|
105
|
-
|
|
106
|
-
- Gates to run: the backend lint, type and test gates the checkout pins for these paths.
|
|
107
|
-
- Surfaces to observe: the enrolment mutation of the api.
|
|
108
|
-
- Not changed on purpose: the legacy checkout path, which the contract does not carry.
|
|
109
|
-
`;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const requestJson = ({ extra = {} } = {}) => ({
|
|
113
|
-
schemaVersion: 9, operatorId: 'backend.source.apply', step: 1, parallel: 1, sessionId: 's-test',
|
|
114
|
-
contexts: [{ alias: '@workspaces/be', head: BASE }, { alias: '@worktrees/businesses/paid-access', head: null }, { alias: '@knowledge/patterns/be', head: null }],
|
|
115
|
-
requirements: {
|
|
116
|
-
featureId: 'paid-access', outcome: 'one enrolment mutation behind the paid access promise',
|
|
117
|
-
mutableFileRefs: [WRITER, SPEC], resume: null, ...extra,
|
|
118
|
-
},
|
|
119
|
-
inputs: { 'architecture-decision': 'step-1/parallel-2/response/response.md' }, resume: null,
|
|
120
|
-
});
|
|
121
|
-
function responseJson({ status = 'done', stop, fallbacks = [], fields = null, commits = [COMMIT], next = ['quality.verify'] } = {}) {
|
|
122
|
-
return {
|
|
123
|
-
schemaVersion: 9, operatorId: 'backend.source.apply', step: 1, parallel: 1, status, ...(stop ? { stop } : {}), fallbacks,
|
|
124
|
-
fields: fields ?? {
|
|
125
|
-
'backend-source-application': 'response/response.md',
|
|
126
|
-
changes: 'response/changes.md',
|
|
127
|
-
mutations: 'response/data/mutations.json',
|
|
128
|
-
conformance: FACETS.map((facet) => conformancePath(OP, facet)),
|
|
129
|
-
proof: PROOF_KINDS.map((kind) => proofPath(OP, kind)),
|
|
130
|
-
},
|
|
131
|
-
commits, next,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function writeBranch(files) {
|
|
136
|
-
const session = mkdtempSync(path.join(tmpdir(), 'backend-session-'));
|
|
137
|
-
const branch = path.join(session, 'step-1', 'parallel-1');
|
|
138
|
-
for (const d of ['request', 'response/data/conformance', 'response/data/proofs', 'response/artifacts']) mkdirSync(path.join(branch, d), { recursive: true });
|
|
139
|
-
mkdirSync(path.join(session, 'step-1', 'parallel-2', 'response'), { recursive: true });
|
|
140
|
-
writeFileSync(path.join(session, 'step-1', 'parallel-2', 'response', 'response.md'), '# architecture-decision — enrol-course\n');
|
|
141
|
-
writeFileSync(path.join(session, 'state.json'), JSON.stringify({ id: 's-test', chain: [['1/1']], steps: { '1/1': 'backend.source.apply' }, current: '1/1', status: 'running' }));
|
|
142
|
-
for (const [name, content] of Object.entries(files)) {
|
|
143
|
-
if (content === null) continue;
|
|
144
|
-
writeFileSync(path.join(branch, name), typeof content === 'string' ? content : JSON.stringify(content, null, 2));
|
|
145
|
-
}
|
|
146
|
-
return { branch, session };
|
|
147
|
-
}
|
|
148
|
-
const records = (conformance = null, proofs = null) => ({
|
|
149
|
-
...Object.fromEntries((conformance ?? FACETS.map((f) => conformanceRecord(f))).map((r) => [conformancePath(r.operationId, r.facet), r])),
|
|
150
|
-
...Object.fromEntries((proofs ?? PROOF_KINDS.map((k) => proofRecord(k))).map((r) => [proofPath(r.operationId, r.proofKind), r])),
|
|
151
|
-
});
|
|
152
|
-
const baseline = () => ({
|
|
153
|
-
'request/request.json': requestJson(),
|
|
154
|
-
'response/response.json': responseJson(),
|
|
155
|
-
'response/response.md': responseMd(),
|
|
156
|
-
'response/changes.md': changesMd(),
|
|
157
|
-
'response/data/mutations.json': mutationsJson(),
|
|
158
|
-
...records(),
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
async function expectValid(files, label) {
|
|
162
|
-
const { branch, session } = writeBranch(files);
|
|
163
|
-
const { errors } = await validateBackendStep(branch);
|
|
164
|
-
rmSync(session, { recursive: true, force: true });
|
|
165
|
-
assert.deepEqual(errors, [], `${label} should be valid`);
|
|
166
|
-
}
|
|
167
|
-
async function expectError(files, needle, label) {
|
|
168
|
-
const { branch, session } = writeBranch(files);
|
|
169
|
-
const { errors } = await validateBackendStep(branch);
|
|
170
|
-
rmSync(session, { recursive: true, force: true });
|
|
171
|
-
assert.ok(errors.some((e) => e.includes(needle)), `${label}: expected an error containing "${needle}", got:\n${errors.join('\n') || '(none)'}`);
|
|
172
|
-
}
|
|
173
|
-
// A dry run: the same plan, with nothing committed, nothing measured and nothing moved in the tree.
|
|
174
|
-
const dryFiles = () => ({
|
|
175
|
-
'request/request.json': requestJson({ extra: { mode: 'dry' } }),
|
|
176
|
-
'response/response.json': responseJson({
|
|
177
|
-
commits: [],
|
|
178
|
-
fields: { 'backend-source-application': 'response/response.md', changes: 'response/changes.md', mutations: 'response/data/mutations.json' },
|
|
179
|
-
}),
|
|
180
|
-
'response/response.md': responseMd({ mode: 'dry', commit: '—', changes: [[WRITER, 'added', OP, '—', '—'], [SPEC, 'added', OP, '—', '—']] }),
|
|
181
|
-
'response/changes.md': changesMd({
|
|
182
|
-
files: [[WRITER, 'unchanged', 'the mutation handler this run would add'], [SPEC, 'unchanged', 'the unit spec this run would add']],
|
|
183
|
-
checkout: `\`@workspaces/be\` at \`${BASE}\` on \`${BRANCH}\`, nothing written`,
|
|
184
|
-
}),
|
|
185
|
-
'response/data/mutations.json': mutationsJson({ mode: 'dry', commit: null, changes: DRY_CHANGES }),
|
|
186
|
-
});
|
|
187
|
-
const blockedFiles = () => ({
|
|
188
|
-
'request/request.json': requestJson(),
|
|
189
|
-
'response/response.json': responseJson({ status: 'blocked', stop: 'CONTRACT_WIDENED', fields: {}, commits: [], next: [] }),
|
|
190
|
-
});
|
|
191
|
-
// One mutation of the frozen contract, with the receipt and the records that must follow it.
|
|
192
|
-
const withOperation = (patch) => {
|
|
193
|
-
const op = operation(patch);
|
|
194
|
-
return {
|
|
195
|
-
...baseline(),
|
|
196
|
-
'response/data/mutations.json': mutationsJson({ operations: [op] }),
|
|
197
|
-
...records(op.facets.map((f) => conformanceRecord(f)), op.proofKinds.map((k) => proofRecord(k))),
|
|
198
|
-
'response/response.json': responseJson({
|
|
199
|
-
fields: {
|
|
200
|
-
'backend-source-application': 'response/response.md', changes: 'response/changes.md', mutations: 'response/data/mutations.json',
|
|
201
|
-
conformance: op.facets.map((f) => conformancePath(OP, f)), proof: op.proofKinds.map((k) => proofPath(OP, k)),
|
|
202
|
-
},
|
|
203
|
-
}),
|
|
204
|
-
'response/response.md': responseMd({ operations: [[OP, op.transport, op.writerRef, op.transactionBoundary, op.idempotencyKind, op.
|
|
205
|
-
};
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
await expectValid(baseline(), 'one operation filled inside the frozen contract and committed once');
|
|
209
|
-
await expectValid(dryFiles(), 'a dry run that plans the write set and commits, measures and writes nothing');
|
|
210
|
-
await expectValid(blockedFiles(), 'blocked on a contract the outcome would widen');
|
|
211
|
-
|
|
212
|
-
await expectError({ ...baseline(), 'response/response.json': { ...responseJson(), stop: 'CONTRACT_WIDENED' } }, 'only a blocked response carries a stop', 'done with a stop');
|
|
213
|
-
await expectError({ ...blockedFiles(), 'response/response.json': responseJson({ status: 'blocked', stop: 'MADE_UP_CODE', fields: {}, commits: [], next: [] }) }, 'not a registered code', 'unknown stop code');
|
|
214
|
-
await expectError({ ...baseline(), 'response/response.json': responseJson({ fallbacks: ['PROOF_UNAVAILABLE'] }) }, 'has disposition terminate under these requirements; it cannot be taken as a fallback', 'fallback on a terminate code');
|
|
215
|
-
await expectError({ ...baseline(), 'response/response.json': responseJson({ status: 'blocked', stop: 'PROOF_UNAVAILABLE', commits: [], next: [] }) }, 'a blocked branch cannot carry an implementation', 'blocked while reporting an implementation');
|
|
216
|
-
await expectError({ ...blockedFiles(), 'response/response.json': responseJson({ status: 'blocked', stop: 'CONTRACT_WIDENED', fields: {}, next: [] }) }, 'a blocked branch commits nothing', 'blocked with a commit');
|
|
217
|
-
await expectError({ ...baseline(), 'response/response.json': responseJson({ commits: [] }) }, 'commits its whole write set once, found 0 commits', 'a done branch that never committed');
|
|
218
|
-
await expectError({ ...baseline(), 'response/response.json': responseJson({ commits: [COMMIT, hash('9').slice(7, 47)] }) }, 'commits its whole write set once, found 2 commits', 'a done branch with two commits');
|
|
219
|
-
await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ commit: 'b'.repeat(40) }) }, 'differs from response.json commits[0]', 'the mutation record names another commit');
|
|
220
|
-
await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ commit: BASE }), 'response/response.json': responseJson({ commits: [BASE] }), 'response/response.md': responseMd({ commit: BASE }), 'response/changes.md': changesMd({ checkout: `\`@workspaces/be\` at \`${BASE}\` → \`${BASE}\` on \`${BRANCH}\`` }) }, 'nothing was written on the session branch', 'a commit equal to the base');
|
|
221
|
-
await expectError({ ...baseline(), 'response/changes.md': changesMd({ checkout: `\`@workspaces/be\` at \`${BASE}\` → uncommitted` }) }, 'so the next request can pin exactly what was written', 'a change record that pins no commit');
|
|
222
|
-
await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { mystery: 1 } }) }, 'requirements.mystery is not a field', 'undeclared requirement');
|
|
223
|
-
await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { outcome: '' } }) }, 'required field outcome has no value', 'missing required outcome');
|
|
224
|
-
await expectError(withOperation({ writerRef: 'src/other/handler.ts' }), 'outside the mutable ceiling', 'a writer outside the mutable ceiling');
|
|
225
|
-
await expectError(withOperation({ migrationRefs: ['1700000000000-AddEnrolment.ts'], facets: [...FACETS, 'migration'] }), 'ships a migration without declaring the migration-replay proof', 'a migration with no replay proof');
|
|
226
|
-
await expectError(withOperation({ migrationRefs: ['1700000000000-AddEnrolment.ts'], proofKinds: [...PROOF_KINDS, 'migration-replay'] }), 'ships a migration without declaring the migration facet', 'a migration with no migration facet');
|
|
227
|
-
await expectError(withOperation({ transactionBoundary: 'read-only', migrationRefs: ['m.ts'], facets: [...FACETS, 'migration'], proofKinds: [...PROOF_KINDS, 'migration-replay'] }), 'is read-only but ships a migration', 'a read-only operation shipping a migration');
|
|
228
|
-
await expectError(withOperation({ transport: 'event-consumer', idempotencyKind: 'none' }), 'will apply twice on redelivery', 'an event consumer with no idempotency');
|
|
229
|
-
await expectError({ ...baseline(), 'response/response.json': responseJson({ fields: { ...responseJson().fields, conformance: FACETS.slice(0, 5).map((f) => conformancePath(OP, f)) } }) }, 'the operation declares the authorization facet and no record proves it', 'a declared facet with no record');
|
|
230
|
-
await expectError({ ...baseline(), [conformancePath(OP, 'writer')]: conformanceRecord('writer', { verdict: 'widened' }) }, 'reports widened writer conformance in a done branch', 'a widened facet in a done branch');
|
|
231
|
-
await expectError({ ...baseline(), 'response/response.json': responseJson({ fields: { ...responseJson().fields, conformance: [...FACETS.map((f) => conformancePath(OP, f)), conformancePath(OP, 'store')] } }), [conformancePath(OP, 'store')]: conformanceRecord('store') }, 'proves undeclared facet store', 'a facet the contract never declared');
|
|
232
|
-
await expectError({ ...baseline(), [conformancePath(OP, 'writer')]: conformanceRecord('transport') }, 'must be filed as response/data/conformance/enrol-course.transport.json', 'a conformance record filed under another name');
|
|
233
|
-
await expectError({ ...baseline(), [conformancePath(OP, 'writer')]: conformanceRecord('writer', { contractFingerprint: hash('9') }) }, 'measured against another contract fingerprint', 'a facet measured against another contract');
|
|
234
|
-
await expectError({ ...baseline(), 'response/response.json': responseJson({ fields: { ...responseJson().fields, proof: [proofPath(OP, 'unit')] } }) }, 'declares the integration proof and never ran it', 'a declared proof that never ran');
|
|
235
|
-
await expectError({ ...baseline(), [proofPath(OP, 'unit')]: proofRecord('unit', { result: 'failed', exitCode: 1 }) }, 'reports a failed unit proof in a done branch', 'a failed proof in a done branch');
|
|
236
|
-
await expectError({ ...baseline(), [proofPath(OP, 'unit')]: proofRecord('unit', { exitCode: 1 }) }, 'contradicts the passed verdict', 'a passing verdict over a non-zero exit code');
|
|
237
|
-
await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ changes: [{ ...CHANGES[0], change: 'modified', beforeHash: hash('1'), afterHash: hash('1') }, CHANGES[1]] }), 'response/response.md': responseMd({ changes: [[WRITER, 'modified', OP, hash('1'), hash('1')], [SPEC, 'added', OP, '—', hash('2')]] }) }, 'records a modification whose hashes are identical', 'a modification that never happened');
|
|
238
|
-
await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ changes: [{ ...CHANGES[0], beforeHash: hash('1') }, CHANGES[1]] }) }, 'is added with the wrong before hash', 'an added file carrying a before hash');
|
|
239
|
-
await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ changes: [CHANGES[0], { ...CHANGES[1], path: WRITER, afterHash: hash('2') }] }) }, 'carries more than one change record', 'one file changed twice');
|
|
240
|
-
await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ changes: [CHANGES[0], { ...CHANGES[1], operationId: 'ghost-op' }] }) }, 'names undeclared operation ghost-op', 'a change owned by no operation');
|
|
241
|
-
await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ changes: [CHANGES[0], { ...CHANGES[1], path: 'src/other/thing.ts' }] }) }, 'lies outside the mutable ceiling', 'a change outside the ceiling');
|
|
242
|
-
await expectError({ ...baseline(), 'response/response.md': responseMd({ operations: [[OP, 'rest', WRITER, 'single-transaction', 'request-token', '
|
|
243
|
-
await expectError({ ...baseline(), 'response/response.md': responseMd({ operations: [[OP, 'graphql-mutation', WRITER, 'single-transaction', 'request-token', '
|
|
244
|
-
await expectError({ ...baseline(), 'response/response.md': responseMd({ findings: [['BUSINESS_QUESTION_RAISED', OP, null, 'the voucher rule on a foreign gateway was never decided']] }) }, 'cannot raise an unresolved business question', 'shipping with an open business question');
|
|
245
|
-
await expectError({ ...baseline(), 'response/response.md': responseMd({ contractFingerprint: hash('9') }) }, 'differs from the fingerprint the mutations were measured against', 'a receipt measured against another contract');
|
|
246
|
-
await expectError({ ...baseline(), 'response/changes.md': changesMd({ files: [[WRITER, 'created']] }) }, 'which the change record omits', 'a changed file missing from the change record');
|
|
247
|
-
await expectError({ ...baseline(), 'response/response.md': responseMd().replace('## Changes', '## Change set') }, 'missing section ^## Changes$', 'response section renamed');
|
|
248
|
-
await expectError({ ...baseline(), 'response/response.json': (() => { const o = responseJson(); delete o.fields.mutations; return o; })() }, 'required output mutations is not in fields', 'missing required output');
|
|
249
|
-
await expectError({ ...baseline(), 'response/response.json': (() => { const o = responseJson(); delete o.fields.conformance; return o; })() }, 'required output conformance is not in fields', 'an applied branch that measured no facet');
|
|
250
|
-
await expectError({ ...dryFiles(), 'response/data/mutations.json': mutationsJson({ mode: 'dry', commit: COMMIT, changes: DRY_CHANGES }) }, 'a dry run commits nothing, so commit must be null', 'a dry plan carrying a commit');
|
|
251
|
-
await expectError({ ...dryFiles(), 'response/response.json': responseJson({ commits: [COMMIT], fields: { 'backend-source-application': 'response/response.md', changes: 'response/changes.md', mutations: 'response/data/mutations.json' } }) }, 'a dry run records no commit', 'a dry run that committed');
|
|
252
|
-
await expectError({ ...dryFiles(), 'response/data/mutations.json': mutationsJson({ mode: 'dry', commit: null }) }, 'reports an after hash under a dry run', 'a dry plan carrying an after hash');
|
|
253
|
-
await expectError({ ...dryFiles(), ...records(), 'response/response.json': responseJson({ commits: [], fields: { ...responseJson().fields } }) }, 'a dry run measures nothing', 'a dry run carrying conformance and proof records');
|
|
254
|
-
await expectError({ ...dryFiles(), 'response/changes.md': changesMd({ files: [[WRITER, 'created'], [SPEC, 'created']], checkout: `\`@workspaces/be\` at \`${BASE}\` on \`${BRANCH}\`, nothing written` }) }, 'under a dry run, which leaves every path unchanged', 'a dry change record reporting a move');
|
|
255
|
-
await expectError({ ...dryFiles(), 'response/data/mutations.json': mutationsJson({ commit: null, changes: DRY_CHANGES }) }, "mode apply differs from the request's dry", 'a plan that re-decides the mode');
|
|
256
|
-
|
|
257
|
-
process.stdout.write('backend.source.apply self-test: 3 valid branches, 43 rejected mutations\n');
|
|
1
|
+
// Proves validate.mjs on a synthetic session branch: one conforming implementation of a frozen
|
|
2
|
+
// contract committed once on the session branch, with its change record, one conformance file per
|
|
3
|
+
// facet and one proof file per proof kind, one dry run that plans the same write set and commits,
|
|
4
|
+
// measures and writes nothing, one branch blocked on a terminate code, and one mutation per law,
|
|
5
|
+
// each of which must fail with a line that names the defect.
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { validateBackendStep } from './validate.mjs';
|
|
11
|
+
|
|
12
|
+
const BASE = 'f'.repeat(40);
|
|
13
|
+
const COMMIT = 'a1b2'.repeat(10);
|
|
14
|
+
const BRANCH = 'session/s-test';
|
|
15
|
+
const hash = (c) => `sha256:${c.repeat(64)}`;
|
|
16
|
+
const CONTRACT_FP = hash('a');
|
|
17
|
+
const OP = 'enrol-course';
|
|
18
|
+
const WRITER = 'src/features/api/core/graphql/mutations/enrol-course.handler.ts';
|
|
19
|
+
const SPEC = 'src/features/api/core/graphql/mutations/enrol-course.handler.spec.ts';
|
|
20
|
+
const FACETS = ['transport', 'writer', 'transaction', 'idempotency', 'exception-identity', 'authorization'];
|
|
21
|
+
const PROOF_KINDS = ['unit', 'integration'];
|
|
22
|
+
|
|
23
|
+
const operation = (overrides = {}) => ({
|
|
24
|
+
operationId: OP, name: 'enrolCourse', transport: 'graphql-mutation', writerRef: WRITER,
|
|
25
|
+
storeRefs: ['enrollments'], transactionBoundary: 'single-transaction', idempotencyKind: 'request-token',
|
|
26
|
+
migrationRefs: [], authorityDimensionIds: ['effective-access'], facets: FACETS, proofKinds: PROOF_KINDS,
|
|
27
|
+
...overrides,
|
|
28
|
+
});
|
|
29
|
+
const CHANGES = [
|
|
30
|
+
{ path: WRITER, change: 'added', operationId: OP, beforeHash: null, afterHash: hash('1') },
|
|
31
|
+
{ path: SPEC, change: 'added', operationId: OP, beforeHash: null, afterHash: hash('2') },
|
|
32
|
+
];
|
|
33
|
+
const DRY_CHANGES = CHANGES.map((c) => ({ ...c, afterHash: null }));
|
|
34
|
+
const mutationsJson = ({ operations = [operation()], changes = CHANGES, commit = COMMIT, base = BASE, branch = BRANCH, mode = 'apply' } = {}) => ({
|
|
35
|
+
mode, contractFingerprint: CONTRACT_FP, base, branch, commit, operations, changes,
|
|
36
|
+
});
|
|
37
|
+
const conformanceRecord = (facet, overrides = {}) => ({ operationId: OP, facet, verdict: 'conforms', evidenceRef: `${SPEC}:40`, statement: `the ${facet} of the operation matches the contract`, contractFingerprint: CONTRACT_FP, ...overrides });
|
|
38
|
+
const proofRecord = (proofKind, overrides = {}) => ({ operationId: OP, proofKind, commandRef: `npm run test:${proofKind}`, exitCode: 0, result: 'passed', output: `${proofKind} suite: 12 passed`, statement: `the ${proofKind} proof ran and passed`, contractFingerprint: CONTRACT_FP, ...overrides });
|
|
39
|
+
const conformancePath = (operationId, facet) => `response/data/conformance/${operationId}.${facet}.json`;
|
|
40
|
+
const proofPath = (operationId, proofKind) => `response/data/proofs/${operationId}.${proofKind}.json`;
|
|
41
|
+
|
|
42
|
+
function responseMd({ operations = null, changes = null, findings = null, contractFingerprint = CONTRACT_FP, commit = COMMIT, base = BASE, branch = BRANCH, mode = 'apply' } = {}) {
|
|
43
|
+
const operationRows = (operations ?? [[OP, 'graphql-mutation', WRITER, 'single-transaction', 'request-token', 'effective-access']]).map((r) => `| \`${r[0]}\` | ${r[1]} | \`${r[2]}\` | ${r[3]} | ${r[4]} | ${r[5]} |`).join('\n');
|
|
44
|
+
const changeRows = (changes ?? [[WRITER, 'added', OP, '—', hash('1')], [SPEC, 'added', OP, '—', hash('2')]]).map((r) => `| \`${r[0]}\` | ${r[1]} | \`${r[2]}\` | ${r[3]} | ${r[4]} |`).join('\n');
|
|
45
|
+
const findingRows = (findings ?? [['PATTERN_BOUND', OP, WRITER, 'the mutation handler mirrors the published command family']]).map(([code, op, file, statement]) => `| \`${code}\` | ${op === null ? '—' : `\`${op}\``} | ${file === null ? '—' : `\`${file}\``} | ${statement} |`).join('\n');
|
|
46
|
+
return `# backend-source-application — enrol-course
|
|
47
|
+
|
|
48
|
+
The enrolment mutation, filled inside the frozen contract and measured on every declared facet.
|
|
49
|
+
|
|
50
|
+
## Binding
|
|
51
|
+
|
|
52
|
+
| Field | Value |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| Outcome | one enrolment mutation behind the paid access promise |
|
|
55
|
+
| Feature | paid-access |
|
|
56
|
+
| Mode | ${mode} |
|
|
57
|
+
| Contract fingerprint | ${contractFingerprint} |
|
|
58
|
+
| Base | ${base} |
|
|
59
|
+
| Branch | ${branch} |
|
|
60
|
+
| Commit | ${commit} |
|
|
61
|
+
|
|
62
|
+
## Operations
|
|
63
|
+
|
|
64
|
+
| Operation | Transport | Writer | Transaction | Idempotency | Decisions |
|
|
65
|
+
| --- | --- | --- | --- | --- | --- |
|
|
66
|
+
${operationRows}
|
|
67
|
+
|
|
68
|
+
## Changes
|
|
69
|
+
|
|
70
|
+
| Path | Change | Operation | Before | After |
|
|
71
|
+
| --- | --- | --- | --- | --- |
|
|
72
|
+
${changeRows}
|
|
73
|
+
|
|
74
|
+
## Findings
|
|
75
|
+
|
|
76
|
+
| Code | Operation | File | Statement |
|
|
77
|
+
| --- | --- | --- | --- |
|
|
78
|
+
${findingRows}
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const changesMd = ({ files = null, checkout = `\`@workspaces/be\` at \`${BASE}\` → \`${COMMIT}\` on \`${BRANCH}\`` } = {}) => {
|
|
83
|
+
const rows = (files ?? [[WRITER, 'created'], [SPEC, 'created']]).map(([p, kind, why]) => `| \`${p}\` | ${kind} | ${why ?? 'the decision this file carries'} | BE-1 |`).join('\n');
|
|
84
|
+
return `# changes — backend.source.apply step-1/parallel-1
|
|
85
|
+
|
|
86
|
+
The enrolment mutation and its unit spec were written into the session branch of the routed backend
|
|
87
|
+
checkout and committed once.
|
|
88
|
+
|
|
89
|
+
## Binding
|
|
90
|
+
|
|
91
|
+
| Field | Value |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| Operator | \`backend.source.apply\` |
|
|
94
|
+
| Step | \`step-1/parallel-1\` |
|
|
95
|
+
| Checkout | ${checkout} |
|
|
96
|
+
| Predecessor | \`step-1/parallel-2/response/response.md\` |
|
|
97
|
+
|
|
98
|
+
## Files
|
|
99
|
+
|
|
100
|
+
| Path | Change | Why | Claims |
|
|
101
|
+
| --- | --- | --- | --- |
|
|
102
|
+
${rows}
|
|
103
|
+
|
|
104
|
+
## What the next step must know
|
|
105
|
+
|
|
106
|
+
- Gates to run: the backend lint, type and test gates the checkout pins for these paths.
|
|
107
|
+
- Surfaces to observe: the enrolment mutation of the api.
|
|
108
|
+
- Not changed on purpose: the legacy checkout path, which the contract does not carry.
|
|
109
|
+
`;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const requestJson = ({ extra = {} } = {}) => ({
|
|
113
|
+
schemaVersion: 9, operatorId: 'backend.source.apply', step: 1, parallel: 1, sessionId: 's-test',
|
|
114
|
+
contexts: [{ alias: '@workspaces/be', head: BASE }, { alias: '@worktrees/businesses/paid-access', head: null }, { alias: '@knowledge/patterns/be', head: null }],
|
|
115
|
+
requirements: {
|
|
116
|
+
featureId: 'paid-access', outcome: 'one enrolment mutation behind the paid access promise',
|
|
117
|
+
mutableFileRefs: [WRITER, SPEC], resume: null, ...extra,
|
|
118
|
+
},
|
|
119
|
+
inputs: { 'architecture-decision': 'step-1/parallel-2/response/response.md' }, resume: null,
|
|
120
|
+
});
|
|
121
|
+
function responseJson({ status = 'done', stop, fallbacks = [], fields = null, commits = [COMMIT], next = ['quality.verify'] } = {}) {
|
|
122
|
+
return {
|
|
123
|
+
schemaVersion: 9, operatorId: 'backend.source.apply', step: 1, parallel: 1, status, ...(stop ? { stop } : {}), fallbacks,
|
|
124
|
+
fields: fields ?? {
|
|
125
|
+
'backend-source-application': 'response/response.md',
|
|
126
|
+
changes: 'response/changes.md',
|
|
127
|
+
mutations: 'response/data/mutations.json',
|
|
128
|
+
conformance: FACETS.map((facet) => conformancePath(OP, facet)),
|
|
129
|
+
proof: PROOF_KINDS.map((kind) => proofPath(OP, kind)),
|
|
130
|
+
},
|
|
131
|
+
commits, next,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function writeBranch(files) {
|
|
136
|
+
const session = mkdtempSync(path.join(tmpdir(), 'backend-session-'));
|
|
137
|
+
const branch = path.join(session, 'step-1', 'parallel-1');
|
|
138
|
+
for (const d of ['request', 'response/data/conformance', 'response/data/proofs', 'response/artifacts']) mkdirSync(path.join(branch, d), { recursive: true });
|
|
139
|
+
mkdirSync(path.join(session, 'step-1', 'parallel-2', 'response'), { recursive: true });
|
|
140
|
+
writeFileSync(path.join(session, 'step-1', 'parallel-2', 'response', 'response.md'), '# architecture-decision — enrol-course\n');
|
|
141
|
+
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': 'backend.source.apply' }, current: '1/1', status: 'running' }));
|
|
142
|
+
for (const [name, content] of Object.entries(files)) {
|
|
143
|
+
if (content === null) continue;
|
|
144
|
+
writeFileSync(path.join(branch, name), typeof content === 'string' ? content : JSON.stringify(content, null, 2));
|
|
145
|
+
}
|
|
146
|
+
return { branch, session };
|
|
147
|
+
}
|
|
148
|
+
const records = (conformance = null, proofs = null) => ({
|
|
149
|
+
...Object.fromEntries((conformance ?? FACETS.map((f) => conformanceRecord(f))).map((r) => [conformancePath(r.operationId, r.facet), r])),
|
|
150
|
+
...Object.fromEntries((proofs ?? PROOF_KINDS.map((k) => proofRecord(k))).map((r) => [proofPath(r.operationId, r.proofKind), r])),
|
|
151
|
+
});
|
|
152
|
+
const baseline = () => ({
|
|
153
|
+
'request/request.json': requestJson(),
|
|
154
|
+
'response/response.json': responseJson(),
|
|
155
|
+
'response/response.md': responseMd(),
|
|
156
|
+
'response/changes.md': changesMd(),
|
|
157
|
+
'response/data/mutations.json': mutationsJson(),
|
|
158
|
+
...records(),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
async function expectValid(files, label) {
|
|
162
|
+
const { branch, session } = writeBranch(files);
|
|
163
|
+
const { errors } = await validateBackendStep(branch);
|
|
164
|
+
rmSync(session, { recursive: true, force: true });
|
|
165
|
+
assert.deepEqual(errors, [], `${label} should be valid`);
|
|
166
|
+
}
|
|
167
|
+
async function expectError(files, needle, label) {
|
|
168
|
+
const { branch, session } = writeBranch(files);
|
|
169
|
+
const { errors } = await validateBackendStep(branch);
|
|
170
|
+
rmSync(session, { recursive: true, force: true });
|
|
171
|
+
assert.ok(errors.some((e) => e.includes(needle)), `${label}: expected an error containing "${needle}", got:\n${errors.join('\n') || '(none)'}`);
|
|
172
|
+
}
|
|
173
|
+
// A dry run: the same plan, with nothing committed, nothing measured and nothing moved in the tree.
|
|
174
|
+
const dryFiles = () => ({
|
|
175
|
+
'request/request.json': requestJson({ extra: { mode: 'dry' } }),
|
|
176
|
+
'response/response.json': responseJson({
|
|
177
|
+
commits: [],
|
|
178
|
+
fields: { 'backend-source-application': 'response/response.md', changes: 'response/changes.md', mutations: 'response/data/mutations.json' },
|
|
179
|
+
}),
|
|
180
|
+
'response/response.md': responseMd({ mode: 'dry', commit: '—', changes: [[WRITER, 'added', OP, '—', '—'], [SPEC, 'added', OP, '—', '—']] }),
|
|
181
|
+
'response/changes.md': changesMd({
|
|
182
|
+
files: [[WRITER, 'unchanged', 'the mutation handler this run would add'], [SPEC, 'unchanged', 'the unit spec this run would add']],
|
|
183
|
+
checkout: `\`@workspaces/be\` at \`${BASE}\` on \`${BRANCH}\`, nothing written`,
|
|
184
|
+
}),
|
|
185
|
+
'response/data/mutations.json': mutationsJson({ mode: 'dry', commit: null, changes: DRY_CHANGES }),
|
|
186
|
+
});
|
|
187
|
+
const blockedFiles = () => ({
|
|
188
|
+
'request/request.json': requestJson(),
|
|
189
|
+
'response/response.json': responseJson({ status: 'blocked', stop: 'CONTRACT_WIDENED', fields: {}, commits: [], next: [] }),
|
|
190
|
+
});
|
|
191
|
+
// One mutation of the frozen contract, with the receipt and the records that must follow it.
|
|
192
|
+
const withOperation = (patch) => {
|
|
193
|
+
const op = operation(patch);
|
|
194
|
+
return {
|
|
195
|
+
...baseline(),
|
|
196
|
+
'response/data/mutations.json': mutationsJson({ operations: [op] }),
|
|
197
|
+
...records(op.facets.map((f) => conformanceRecord(f)), op.proofKinds.map((k) => proofRecord(k))),
|
|
198
|
+
'response/response.json': responseJson({
|
|
199
|
+
fields: {
|
|
200
|
+
'backend-source-application': 'response/response.md', changes: 'response/changes.md', mutations: 'response/data/mutations.json',
|
|
201
|
+
conformance: op.facets.map((f) => conformancePath(OP, f)), proof: op.proofKinds.map((k) => proofPath(OP, k)),
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
204
|
+
'response/response.md': responseMd({ operations: [[OP, op.transport, op.writerRef, op.transactionBoundary, op.idempotencyKind, op.authorityDimensionIds.join(', ')]] }),
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
await expectValid(baseline(), 'one operation filled inside the frozen contract and committed once');
|
|
209
|
+
await expectValid(dryFiles(), 'a dry run that plans the write set and commits, measures and writes nothing');
|
|
210
|
+
await expectValid(blockedFiles(), 'blocked on a contract the outcome would widen');
|
|
211
|
+
|
|
212
|
+
await expectError({ ...baseline(), 'response/response.json': { ...responseJson(), stop: 'CONTRACT_WIDENED' } }, 'only a blocked response carries a stop', 'done with a stop');
|
|
213
|
+
await expectError({ ...blockedFiles(), 'response/response.json': responseJson({ status: 'blocked', stop: 'MADE_UP_CODE', fields: {}, commits: [], next: [] }) }, 'not a registered code', 'unknown stop code');
|
|
214
|
+
await expectError({ ...baseline(), 'response/response.json': responseJson({ fallbacks: ['PROOF_UNAVAILABLE'] }) }, 'has disposition terminate under these requirements; it cannot be taken as a fallback', 'fallback on a terminate code');
|
|
215
|
+
await expectError({ ...baseline(), 'response/response.json': responseJson({ status: 'blocked', stop: 'PROOF_UNAVAILABLE', commits: [], next: [] }) }, 'a blocked branch cannot carry an implementation', 'blocked while reporting an implementation');
|
|
216
|
+
await expectError({ ...blockedFiles(), 'response/response.json': responseJson({ status: 'blocked', stop: 'CONTRACT_WIDENED', fields: {}, next: [] }) }, 'a blocked branch commits nothing', 'blocked with a commit');
|
|
217
|
+
await expectError({ ...baseline(), 'response/response.json': responseJson({ commits: [] }) }, 'commits its whole write set once, found 0 commits', 'a done branch that never committed');
|
|
218
|
+
await expectError({ ...baseline(), 'response/response.json': responseJson({ commits: [COMMIT, hash('9').slice(7, 47)] }) }, 'commits its whole write set once, found 2 commits', 'a done branch with two commits');
|
|
219
|
+
await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ commit: 'b'.repeat(40) }) }, 'differs from response.json commits[0]', 'the mutation record names another commit');
|
|
220
|
+
await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ commit: BASE }), 'response/response.json': responseJson({ commits: [BASE] }), 'response/response.md': responseMd({ commit: BASE }), 'response/changes.md': changesMd({ checkout: `\`@workspaces/be\` at \`${BASE}\` → \`${BASE}\` on \`${BRANCH}\`` }) }, 'nothing was written on the session branch', 'a commit equal to the base');
|
|
221
|
+
await expectError({ ...baseline(), 'response/changes.md': changesMd({ checkout: `\`@workspaces/be\` at \`${BASE}\` → uncommitted` }) }, 'so the next request can pin exactly what was written', 'a change record that pins no commit');
|
|
222
|
+
await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { mystery: 1 } }) }, 'requirements.mystery is not a field', 'undeclared requirement');
|
|
223
|
+
await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { outcome: '' } }) }, 'required field outcome has no value', 'missing required outcome');
|
|
224
|
+
await expectError(withOperation({ writerRef: 'src/other/handler.ts' }), 'outside the mutable ceiling', 'a writer outside the mutable ceiling');
|
|
225
|
+
await expectError(withOperation({ migrationRefs: ['1700000000000-AddEnrolment.ts'], facets: [...FACETS, 'migration'] }), 'ships a migration without declaring the migration-replay proof', 'a migration with no replay proof');
|
|
226
|
+
await expectError(withOperation({ migrationRefs: ['1700000000000-AddEnrolment.ts'], proofKinds: [...PROOF_KINDS, 'migration-replay'] }), 'ships a migration without declaring the migration facet', 'a migration with no migration facet');
|
|
227
|
+
await expectError(withOperation({ transactionBoundary: 'read-only', migrationRefs: ['m.ts'], facets: [...FACETS, 'migration'], proofKinds: [...PROOF_KINDS, 'migration-replay'] }), 'is read-only but ships a migration', 'a read-only operation shipping a migration');
|
|
228
|
+
await expectError(withOperation({ transport: 'event-consumer', idempotencyKind: 'none' }), 'will apply twice on redelivery', 'an event consumer with no idempotency');
|
|
229
|
+
await expectError({ ...baseline(), 'response/response.json': responseJson({ fields: { ...responseJson().fields, conformance: FACETS.slice(0, 5).map((f) => conformancePath(OP, f)) } }) }, 'the operation declares the authorization facet and no record proves it', 'a declared facet with no record');
|
|
230
|
+
await expectError({ ...baseline(), [conformancePath(OP, 'writer')]: conformanceRecord('writer', { verdict: 'widened' }) }, 'reports widened writer conformance in a done branch', 'a widened facet in a done branch');
|
|
231
|
+
await expectError({ ...baseline(), 'response/response.json': responseJson({ fields: { ...responseJson().fields, conformance: [...FACETS.map((f) => conformancePath(OP, f)), conformancePath(OP, 'store')] } }), [conformancePath(OP, 'store')]: conformanceRecord('store') }, 'proves undeclared facet store', 'a facet the contract never declared');
|
|
232
|
+
await expectError({ ...baseline(), [conformancePath(OP, 'writer')]: conformanceRecord('transport') }, 'must be filed as response/data/conformance/enrol-course.transport.json', 'a conformance record filed under another name');
|
|
233
|
+
await expectError({ ...baseline(), [conformancePath(OP, 'writer')]: conformanceRecord('writer', { contractFingerprint: hash('9') }) }, 'measured against another contract fingerprint', 'a facet measured against another contract');
|
|
234
|
+
await expectError({ ...baseline(), 'response/response.json': responseJson({ fields: { ...responseJson().fields, proof: [proofPath(OP, 'unit')] } }) }, 'declares the integration proof and never ran it', 'a declared proof that never ran');
|
|
235
|
+
await expectError({ ...baseline(), [proofPath(OP, 'unit')]: proofRecord('unit', { result: 'failed', exitCode: 1 }) }, 'reports a failed unit proof in a done branch', 'a failed proof in a done branch');
|
|
236
|
+
await expectError({ ...baseline(), [proofPath(OP, 'unit')]: proofRecord('unit', { exitCode: 1 }) }, 'contradicts the passed verdict', 'a passing verdict over a non-zero exit code');
|
|
237
|
+
await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ changes: [{ ...CHANGES[0], change: 'modified', beforeHash: hash('1'), afterHash: hash('1') }, CHANGES[1]] }), 'response/response.md': responseMd({ changes: [[WRITER, 'modified', OP, hash('1'), hash('1')], [SPEC, 'added', OP, '—', hash('2')]] }) }, 'records a modification whose hashes are identical', 'a modification that never happened');
|
|
238
|
+
await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ changes: [{ ...CHANGES[0], beforeHash: hash('1') }, CHANGES[1]] }) }, 'is added with the wrong before hash', 'an added file carrying a before hash');
|
|
239
|
+
await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ changes: [CHANGES[0], { ...CHANGES[1], path: WRITER, afterHash: hash('2') }] }) }, 'carries more than one change record', 'one file changed twice');
|
|
240
|
+
await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ changes: [CHANGES[0], { ...CHANGES[1], operationId: 'ghost-op' }] }) }, 'names undeclared operation ghost-op', 'a change owned by no operation');
|
|
241
|
+
await expectError({ ...baseline(), 'response/data/mutations.json': mutationsJson({ changes: [CHANGES[0], { ...CHANGES[1], path: 'src/other/thing.ts' }] }) }, 'lies outside the mutable ceiling', 'a change outside the ceiling');
|
|
242
|
+
await expectError({ ...baseline(), 'response/response.md': responseMd({ operations: [[OP, 'rest', WRITER, 'single-transaction', 'request-token', 'effective-access']] }) }, 'reports transport rest, the contract froze graphql-mutation', 'a transport the contract did not freeze');
|
|
243
|
+
await expectError({ ...baseline(), 'response/response.md': responseMd({ operations: [[OP, 'graphql-mutation', WRITER, 'single-transaction', 'request-token', 'unapproved-dimension']] }) }, 'cites dimension unapproved-dimension, which the contract does not bind', 'an unapproved business dimension');
|
|
244
|
+
await expectError({ ...baseline(), 'response/response.md': responseMd({ findings: [['BUSINESS_QUESTION_RAISED', OP, null, 'the voucher rule on a foreign gateway was never decided']] }) }, 'cannot raise an unresolved business question', 'shipping with an open business question');
|
|
245
|
+
await expectError({ ...baseline(), 'response/response.md': responseMd({ contractFingerprint: hash('9') }) }, 'differs from the fingerprint the mutations were measured against', 'a receipt measured against another contract');
|
|
246
|
+
await expectError({ ...baseline(), 'response/changes.md': changesMd({ files: [[WRITER, 'created']] }) }, 'which the change record omits', 'a changed file missing from the change record');
|
|
247
|
+
await expectError({ ...baseline(), 'response/response.md': responseMd().replace('## Changes', '## Change set') }, 'missing section ^## Changes$', 'response section renamed');
|
|
248
|
+
await expectError({ ...baseline(), 'response/response.json': (() => { const o = responseJson(); delete o.fields.mutations; return o; })() }, 'required output mutations is not in fields', 'missing required output');
|
|
249
|
+
await expectError({ ...baseline(), 'response/response.json': (() => { const o = responseJson(); delete o.fields.conformance; return o; })() }, 'required output conformance is not in fields', 'an applied branch that measured no facet');
|
|
250
|
+
await expectError({ ...dryFiles(), 'response/data/mutations.json': mutationsJson({ mode: 'dry', commit: COMMIT, changes: DRY_CHANGES }) }, 'a dry run commits nothing, so commit must be null', 'a dry plan carrying a commit');
|
|
251
|
+
await expectError({ ...dryFiles(), 'response/response.json': responseJson({ commits: [COMMIT], fields: { 'backend-source-application': 'response/response.md', changes: 'response/changes.md', mutations: 'response/data/mutations.json' } }) }, 'a dry run records no commit', 'a dry run that committed');
|
|
252
|
+
await expectError({ ...dryFiles(), 'response/data/mutations.json': mutationsJson({ mode: 'dry', commit: null }) }, 'reports an after hash under a dry run', 'a dry plan carrying an after hash');
|
|
253
|
+
await expectError({ ...dryFiles(), ...records(), 'response/response.json': responseJson({ commits: [], fields: { ...responseJson().fields } }) }, 'a dry run measures nothing', 'a dry run carrying conformance and proof records');
|
|
254
|
+
await expectError({ ...dryFiles(), 'response/changes.md': changesMd({ files: [[WRITER, 'created'], [SPEC, 'created']], checkout: `\`@workspaces/be\` at \`${BASE}\` on \`${BRANCH}\`, nothing written` }) }, 'under a dry run, which leaves every path unchanged', 'a dry change record reporting a move');
|
|
255
|
+
await expectError({ ...dryFiles(), 'response/data/mutations.json': mutationsJson({ commit: null, changes: DRY_CHANGES }) }, "mode apply differs from the request's dry", 'a plan that re-decides the mode');
|
|
256
|
+
|
|
257
|
+
process.stdout.write('backend.source.apply self-test: 3 valid branches, 43 rejected mutations\n');
|