@openwop/openwop-conformance 1.136.5 → 1.136.7
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/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Provenance of this vendored schemas/ copy. See conformance/README.md \u00a7\"Resolving the contract\". Compare against the stamp in your installed @openwop/openwop-conformance to detect a stale hand-copied contract.",
|
|
3
|
-
"suiteVersion": "1.136.
|
|
4
|
-
"corpusCommit": "
|
|
3
|
+
"suiteVersion": "1.136.7",
|
|
4
|
+
"corpusCommit": "b71c4c7c3a3d368c98ef97fd4bed097d16a3fbdd"
|
|
5
5
|
}
|
|
@@ -306,6 +306,26 @@ export function expandChain(chain: WorkflowChain, ctx: ExpansionContext): Expand
|
|
|
306
306
|
// If it is gated on an advertised capability, it goes below.
|
|
307
307
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
308
308
|
|
|
309
|
+
// ─── Begin the MIRRORED COMPENSATION pass ───────────────────────────────────
|
|
310
|
+
//
|
|
311
|
+
// SP-01 (2026-08-18): this pass is a SECOND byte-mirrored region, gated by
|
|
312
|
+
// `scripts/check-workflow-chain-expansion-sync.mjs` alongside the core above.
|
|
313
|
+
//
|
|
314
|
+
// It sits below "End of the MIRRORED CORE" because it composes on the core's
|
|
315
|
+
// OUTPUT rather than editing it — but unlike the capability-gated surfaces that
|
|
316
|
+
// follow (RFC 0124 deferred parameters, RFC 0133 sub-chains, which a host that
|
|
317
|
+
// does not advertise them MUST REFUSE rather than implement), carrying a
|
|
318
|
+
// chain's compensation declaration is UNCONDITIONAL: `compensation.md`
|
|
319
|
+
// §"Workflow policy" makes the declaration descriptive, so a host that does not
|
|
320
|
+
// advertise `capabilities.compensation` still MUST carry it verbatim and refuse
|
|
321
|
+
// only a chain-level POLICY (`capability_required`). Unconditional ⇒ genuinely
|
|
322
|
+
// mirrorable ⇒ gated. RFC 0157's own commit claimed this pass was already
|
|
323
|
+
// "CI-gated against the reference host"; it was not — the gate stopped at the
|
|
324
|
+
// core sentinel — so the mirror never received it. Anything added between the
|
|
325
|
+
// two sentinels below MUST be mirrored in the reference host — that is the
|
|
326
|
+
// whole RFC 0157 unit: the types, the two error classes, the helpers, and
|
|
327
|
+
// `carryCompensation` / `expandChainWithCompensation`.
|
|
328
|
+
|
|
309
329
|
// ---------------------------------------------------------------------------
|
|
310
330
|
// RFC 0157 (RFC 0013 revision × RFC 0151 §B) — chain fragments carry
|
|
311
331
|
// compensation.
|
|
@@ -525,6 +545,8 @@ export function expandChainWithCompensation(
|
|
|
525
545
|
return { ...expanded, nodes: carried.nodes, settingsCompensation: carried.settingsCompensation };
|
|
526
546
|
}
|
|
527
547
|
|
|
548
|
+
// ─── End of the MIRRORED COMPENSATION pass ──────────────────────────────────
|
|
549
|
+
|
|
528
550
|
// ---------------------------------------------------------------------------
|
|
529
551
|
// RFC 0124 (WCP4) — Portable per-run parameter deferral.
|
|
530
552
|
//
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { describe, it, expect } from 'vitest';
|
|
18
18
|
import { driver } from '../lib/driver.js';
|
|
19
19
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
20
|
+
import { readErrorCode } from '../lib/error-envelope.js';
|
|
20
21
|
|
|
21
22
|
const WORKFLOW_ID = 'conformance-idempotent';
|
|
22
23
|
const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
|
|
@@ -85,5 +86,44 @@ describe.skipIf(SKIP_NO_FIXTURE)('idempotency: same key + different body conflic
|
|
|
85
86
|
'idempotency.md §Layer 1',
|
|
86
87
|
'same Idempotency-Key with a different body MUST return 409',
|
|
87
88
|
)).toBe(409);
|
|
89
|
+
|
|
90
|
+
// SP-03 (2026-08-18): the code, not just the status. `idempotency.md`
|
|
91
|
+
// named NO mismatch error until v1.5, and implementations diverged exactly
|
|
92
|
+
// as an unnamed error invites: `grpc-transport.md` mapped BOTH
|
|
93
|
+
// `idempotency_key_conflict` and `idempotency_key_mismatch` in one row, the
|
|
94
|
+
// published TypeScript SDK's `HTTP_ERROR_CODES` carried
|
|
95
|
+
// `idempotency_key_mismatch`, the SQLite reference host emitted
|
|
96
|
+
// `idempotency_key_conflict`, and a tier-1 host emitted
|
|
97
|
+
// `idempotency_key_replay_mismatch` — a spelling in no corpus artifact at
|
|
98
|
+
// all. This leg asserted the status alone, so every one of them passed.
|
|
99
|
+
//
|
|
100
|
+
// `idempotency_key_mismatch` is canonical (idempotency.md §"Record shape,
|
|
101
|
+
// digest, and lease"): the only spelling already in more than one shipped
|
|
102
|
+
// artifact. The two legacy spellings are TOLERATED here through the first
|
|
103
|
+
// minor after 2026-11-10 so a converging host is not red on a rename it is
|
|
104
|
+
// mid-flight on — the same deprecation shape S22 used for the error
|
|
105
|
+
// envelope. Remove the tolerance then; the canonical assertion stays.
|
|
106
|
+
const code = readErrorCode(conflict.json);
|
|
107
|
+
const LEGACY = ['idempotency_key_conflict', 'idempotency_key_replay_mismatch'];
|
|
108
|
+
expect(
|
|
109
|
+
code === 'idempotency_key_mismatch' || LEGACY.includes(code ?? ''),
|
|
110
|
+
driver.describe(
|
|
111
|
+
'idempotency.md §"Record shape, digest, and lease"',
|
|
112
|
+
`a different request digest under the same scoped key MUST fail with the canonical ` +
|
|
113
|
+
`\`idempotency_key_mismatch\` (legacy \`${LEGACY.join('` / `')}\` tolerated through the ` +
|
|
114
|
+
`first minor after 2026-11-10); got \`${code ?? '<none>'}\``,
|
|
115
|
+
),
|
|
116
|
+
).toBe(true);
|
|
117
|
+
|
|
118
|
+
// …and MUST NOT hand back the first run's body. Answering a question the
|
|
119
|
+
// caller did not ask is the read half of the same confusion.
|
|
120
|
+
const conflictRunId = (conflict.json as { runId?: unknown } | undefined)?.runId;
|
|
121
|
+
expect(
|
|
122
|
+
conflictRunId,
|
|
123
|
+
driver.describe(
|
|
124
|
+
'idempotency.md §"Record shape, digest, and lease"',
|
|
125
|
+
'a digest mismatch MUST NOT return the cached body (no runId on the 409)',
|
|
126
|
+
),
|
|
127
|
+
).toBeUndefined();
|
|
88
128
|
});
|
|
89
129
|
});
|
|
@@ -3,8 +3,26 @@
|
|
|
3
3
|
* immutable recorded fact: a `replay`-mode fork MUST NOT mint a new
|
|
4
4
|
* `memoryId` for a write the source run already recorded. This asserts the
|
|
5
5
|
* "MUST NOT regenerate" half — every `memory.written` on a replayed run
|
|
6
|
-
* reuses a `memoryId` the source run recorded
|
|
7
|
-
*
|
|
6
|
+
* reuses a `memoryId` the source run recorded.
|
|
7
|
+
*
|
|
8
|
+
* H2 (2026-08-18): this file used to say, in this docstring, that "a
|
|
9
|
+
* compliant host that suppresses re-mint on replay satisfies this vacuously
|
|
10
|
+
* with zero events" — and it did: the assertion is a `for` loop over the
|
|
11
|
+
* replayed events, so a host that emits NONE passed trivially. Two hosts
|
|
12
|
+
* diverged under it and both stayed green (openwop-app re-emits the source's
|
|
13
|
+
* events; a tier-2 host suppressed them, which leaves the replayed log SHORT
|
|
14
|
+
* of the source's and breaks RFC 0041 §C byte-equivalence).
|
|
15
|
+
*
|
|
16
|
+
* The divergence is the spec's, not the hosts': RFC 0057 §D says the host
|
|
17
|
+
* "MUST re-emit the recorded events from the log" and then, in a
|
|
18
|
+
* non-normative implementation note in the same section, blesses the
|
|
19
|
+
* reference host for "suppress[ing] rather than re-emit[ting]". Resolving
|
|
20
|
+
* that contradiction is a normative decision (it plausibly makes one shipped
|
|
21
|
+
* host non-conformant), so this leg does NOT pick a side. What it stops
|
|
22
|
+
* doing is passing silently: an empty replay now records `blocked` naming
|
|
23
|
+
* the contradiction, per RFC 0148 §A — a leg that cannot observe MUST NOT
|
|
24
|
+
* read as a pass. Once §D is resolved, the winning side becomes an assertion
|
|
25
|
+
* here.
|
|
8
26
|
*
|
|
9
27
|
* Gated on `capabilities.memory.attribution.emitsWriteEvents`; soft-skips
|
|
10
28
|
* when unadvertised, when the seeded run wrote no memory, or when the host
|
|
@@ -50,6 +68,19 @@ describe('memory-attribution-replay-stable (RFC 0057 §D)', () => {
|
|
|
50
68
|
}
|
|
51
69
|
|
|
52
70
|
const replayed = await memoryWrittenEvents(forkId);
|
|
71
|
+
if (replayed.length === 0) {
|
|
72
|
+
// NOT a pass. The source run recorded `memory.written` events and the
|
|
73
|
+
// replay carries none, so this host is on the "suppress" side of the
|
|
74
|
+
// RFC 0057 §D contradiction. Whether that is conformant is undecided;
|
|
75
|
+
// that it is unobserved here is not (RFC 0148 §A).
|
|
76
|
+
return softSkip(
|
|
77
|
+
'blocked',
|
|
78
|
+
`replay emitted no memory.written while the source recorded ${recordedIds.size} — ` +
|
|
79
|
+
'the host suppresses rather than re-emits. RFC 0057 §D requires re-emission in its ' +
|
|
80
|
+
'normative half and blesses suppression in its implementation note; until that ' +
|
|
81
|
+
'contradiction is resolved this leg records the divergence instead of passing on it.',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
53
84
|
for (const e of replayed) {
|
|
54
85
|
const id = memoryIdOf(e.payload);
|
|
55
86
|
expect(
|