@openwop/openwop-conformance 1.152.0 → 1.154.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/CHANGELOG.md +17 -0
- package/README.md +9 -0
- package/dist/cli.js +7 -2
- package/dist/lib/requirement-ids.js +100 -0
- package/dist/lib/scenario-disposition.js +22 -0
- package/package.json +33 -2
- package/requirement-aliases.json +4 -0
- package/requirements.json +23554 -0
- package/schemas/CORPUS-STAMP.json +99 -3
- package/src/cli.ts +7 -5
- package/src/global-setup.ts +13 -0
- package/src/lib/corpus-stamp.ts +125 -0
- package/src/lib/requirement-ids.ts +111 -0
- package/src/lib/scenario-disposition.ts +22 -0
- package/src/setup.ts +84 -5
- package/src/lib/capabilities-auth-subject-link.test.ts +0 -103
- package/src/lib/fork-availability.test.ts +0 -69
- package/src/lib/global-setup.test.ts +0 -76
- package/src/lib/grpc-framing.test.ts +0 -96
- package/src/lib/oidc-issuer.test.ts +0 -328
- package/src/lib/otel-collector-grpc.test.ts +0 -191
- package/src/lib/otel-collector.test.ts +0 -303
- package/src/lib/otlp-protobuf.test.ts +0 -461
- package/src/lib/polling.test.ts +0 -80
- package/src/lib/requirement-ledger.test.ts +0 -75
- package/src/lib/risk-disposition.test.ts +0 -91
- package/src/lib/saml-idp.test.ts +0 -127
- package/src/lib/spec-coherence-registry.test.ts +0 -155
- package/src/lib/webhook-receiver.test.ts +0 -144
- package/src/scenarios/artifact-schema-compile-bounded.test.ts +0 -126
- package/src/scenarios/artifact-type-legacy-ids.test.ts +0 -124
- package/src/scenarios/capability-example-root-layout.test.ts +0 -272
- package/src/scenarios/certification-floor-enforcement.test.ts +0 -204
- package/src/scenarios/chain-subchain-unsupported-refused.test.ts +0 -70
- package/src/scenarios/compensation-profile.test.ts +0 -340
- package/src/scenarios/core-manifest-and-extension-registry.test.ts +0 -250
- package/src/scenarios/discovery-canonical-family-no-shadow.test.ts +0 -219
- package/src/scenarios/edge-condition-truthy-falsy.test.ts +0 -108
- package/src/scenarios/effect-identity-composition.test.ts +0 -129
- package/src/scenarios/effect-identity-cross-scope.test.ts +0 -82
- package/src/scenarios/error-envelope-canonical-shape.test.ts +0 -64
- package/src/scenarios/form-content-packs.test.ts +0 -415
- package/src/scenarios/multi-region-effect-vocabulary.test.ts +0 -175
- package/src/scenarios/normative-example-extraction.test.ts +0 -242
- package/src/scenarios/openapi-asyncapi-sdk-parity.test.ts +0 -309
- package/src/scenarios/pack-manifest-extensions.test.ts +0 -203
- package/src/scenarios/protocol-version-grammar.test.ts +0 -119
- package/src/scenarios/registry-declarative-kinds.test.ts +0 -121
- package/src/scenarios/rfc-0147-self-audit.test.ts +0 -104
- package/src/scenarios/rfc-lifecycle-coherence.test.ts +0 -215
- package/src/scenarios/semantic-digest-v2.test.ts +0 -128
- package/src/scenarios/spec-corpus-validity.test.ts +0 -1727
- package/src/scenarios/spec-section-citations.test.ts +0 -132
- package/src/scenarios/tool-result-trust-monotone.test.ts +0 -168
- package/src/scenarios/versioned-composition-profiles.test.ts +0 -201
- package/src/scenarios/workflow-chain-internal-flag.test.ts +0 -84
- package/src/scenarios/workload-identity-profile.test.ts +0 -184
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RFC 0149 §E — a vendor extension MUST NOT shadow a canonical capability family.
|
|
3
|
-
*
|
|
4
|
-
* RFC 0073 put canonical families at the document root, and `host-extensions.md`
|
|
5
|
-
* §"Canonical prefixes" puts vendor surface under `x-host-<vendor>-*`,
|
|
6
|
-
* `vendor.<org>.*`, or `private.<host>.*`. Those two rules together are what make
|
|
7
|
-
* discovery negotiable: a consumer reads the root for what the protocol defines
|
|
8
|
-
* and treats a namespaced key as opaque.
|
|
9
|
-
*
|
|
10
|
-
* The gap is what happens when a canonical family name appears *inside* the
|
|
11
|
-
* namespaced region — `vendor.acme.auth`, or an `x-host-acme-*` object carrying
|
|
12
|
-
* its own `interrupts`. Nothing in the corpus forbade it, and a consumer that
|
|
13
|
-
* merges vendor surface over the root before negotiating reads a vendor's
|
|
14
|
-
* `auth` block as *the* auth contract. `host-extensions.md` already says clients
|
|
15
|
-
* MUST treat extension surface as opaque, but "opaque" is a rule about the
|
|
16
|
-
* consumer; it does not stop a host from publishing the collision, and the
|
|
17
|
-
* consumer that gets it wrong is the one that most needed the guardrail.
|
|
18
|
-
*
|
|
19
|
-
* §E's second clause is separate and unconditional: no discovery example may
|
|
20
|
-
* carry credentials or tenant data. Discovery is the one document a host serves
|
|
21
|
-
* credential-free to anonymous callers (RFC 0100 requires `agentCardUrl` to GET-
|
|
22
|
-
* resolve without credentials), so a secret pasted into an example is a secret
|
|
23
|
-
* in the most-copied, least-guarded artifact in the corpus.
|
|
24
|
-
*
|
|
25
|
-
* Both legs are structural and server-free — they read the corpus, not a host.
|
|
26
|
-
* `spec/v1/` and `RFCS/` are repository-only, so this self-skips under the
|
|
27
|
-
* published tarball layout.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import { describe, it, expect } from 'vitest';
|
|
31
|
-
import { readFileSync, readdirSync } from 'node:fs';
|
|
32
|
-
import { join, resolve as pathResolve } from 'node:path';
|
|
33
|
-
import { V1_DIR } from '../lib/paths.js';
|
|
34
|
-
|
|
35
|
-
const SCHEMA_PATH =
|
|
36
|
-
V1_DIR === null ? null : pathResolve(V1_DIR, '..', '..', 'schemas', 'capabilities.schema.json');
|
|
37
|
-
|
|
38
|
-
/** The canonical families, read from the schema rather than hand-listed. */
|
|
39
|
-
function canonicalFamilies(): Set<string> {
|
|
40
|
-
if (SCHEMA_PATH === null) return new Set();
|
|
41
|
-
const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8')) as {
|
|
42
|
-
properties?: Record<string, unknown>;
|
|
43
|
-
};
|
|
44
|
-
return new Set(Object.keys(schema.properties ?? {}));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** `host-extensions.md` §"Canonical prefixes". */
|
|
48
|
-
function isVendorKey(key: string): boolean {
|
|
49
|
-
return /^x-host-/.test(key) || /^(vendor|private)\./.test(key);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface Finding {
|
|
53
|
-
readonly file: string;
|
|
54
|
-
readonly line: number;
|
|
55
|
-
readonly detail: string;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Every fenced json/jsonc example under `dir`, paired with its source line. */
|
|
59
|
-
function fencedObjects(dir: string): { file: string; line: number; value: unknown }[] {
|
|
60
|
-
const out: { file: string; line: number; value: unknown }[] = [];
|
|
61
|
-
for (const name of readdirSync(dir).filter((f) => f.endsWith('.md')).sort()) {
|
|
62
|
-
const lines = readFileSync(join(dir, name), 'utf8').split('\n');
|
|
63
|
-
for (let i = 0; i < lines.length; i++) {
|
|
64
|
-
if (!/^```(json|jsonc)\s*$/.test(lines[i]!.trim())) continue;
|
|
65
|
-
const body: string[] = [];
|
|
66
|
-
let j = i + 1;
|
|
67
|
-
while (j < lines.length && lines[j]!.trim() !== '```') body.push(lines[j++]!);
|
|
68
|
-
try {
|
|
69
|
-
out.push({ file: name, line: i + 2, value: JSON.parse(body.join('\n')) });
|
|
70
|
-
} catch {
|
|
71
|
-
// Unparseable blocks are RFC 0150 §D's problem, not this gate's.
|
|
72
|
-
}
|
|
73
|
-
i = j;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return out;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Canonical family names appearing anywhere beneath a vendor-namespaced key. */
|
|
80
|
-
function shadowed(value: unknown, families: Set<string>, insideVendor: boolean): string[] {
|
|
81
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value)) return [];
|
|
82
|
-
const hits: string[] = [];
|
|
83
|
-
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
84
|
-
const nowInside = insideVendor || isVendorKey(key);
|
|
85
|
-
if (insideVendor && families.has(key)) hits.push(key);
|
|
86
|
-
hits.push(...shadowed(child, families, nowInside));
|
|
87
|
-
}
|
|
88
|
-
return hits;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Credential detection runs on THREE axes with different failure modes, because
|
|
93
|
-
* no one of them is complete and the ways they are incomplete do not overlap.
|
|
94
|
-
*
|
|
95
|
-
* A peer put the problem precisely: *a negative existence claim cannot be
|
|
96
|
-
* established by grepping the vocabulary you would have chosen.* Axis 1 alone —
|
|
97
|
-
* a hand-picked list of issuer prefixes — reports clean on every credential
|
|
98
|
-
* format its author did not think of, and reports it in exactly the confident
|
|
99
|
-
* tone of a real check. That is the vacuous-witness pattern wearing a different
|
|
100
|
-
* hat: the gate is honest about what it observed and silent about what it
|
|
101
|
-
* cannot see.
|
|
102
|
-
*
|
|
103
|
-
* None of the three closes the claim. Together they fail differently, which is
|
|
104
|
-
* the most that can be said for them, and it is said here rather than implied by
|
|
105
|
-
* a green run.
|
|
106
|
-
*/
|
|
107
|
-
|
|
108
|
-
/** Axis 1 — known issuer prefixes. Blind to any format not listed. */
|
|
109
|
-
const SECRET_PREFIX = [
|
|
110
|
-
/\bsk-[A-Za-z0-9]{16,}/,
|
|
111
|
-
/\bghp_[A-Za-z0-9]{20,}/,
|
|
112
|
-
/\bAKIA[0-9A-Z]{16}\b/,
|
|
113
|
-
/\bxox[baprs]-[A-Za-z0-9-]{10,}/,
|
|
114
|
-
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
115
|
-
/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/,
|
|
116
|
-
];
|
|
117
|
-
|
|
118
|
-
/** Axis 2 — the property NAME claims to hold a credential. Blind to odd names. */
|
|
119
|
-
const CREDENTIAL_KEY = /secret|password|token|api_?key|credential|private_?key|bearer/i;
|
|
120
|
-
|
|
121
|
-
/** An example is allowed to say `sk-…` or `<your-key>`; that is what examples are for. */
|
|
122
|
-
function isPlaceholder(v: string): boolean {
|
|
123
|
-
if (v.length < 16) return true;
|
|
124
|
-
if (/^https?:\/\//.test(v)) return true;
|
|
125
|
-
return /\.\.\.|…|<|>|\bexample\b|\bplaceholder\b|\bredacted\b|\byour-|\bchangeme\b|x{4,}/i.test(v);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Axis 3 — dense random-looking material regardless of issuer. Requires no
|
|
130
|
-
* separators, mixed case, digits, and high Shannon entropy, which is what
|
|
131
|
-
* distinguishes a credential body from a long dotted identifier or an SRI hash.
|
|
132
|
-
* Blind to low-entropy secrets and to anything with word structure.
|
|
133
|
-
*/
|
|
134
|
-
function isDenseToken(v: string): boolean {
|
|
135
|
-
if (!/^[A-Za-z0-9]{24,}$/.test(v)) return false;
|
|
136
|
-
if (!(/[a-z]/.test(v) && /[A-Z]/.test(v) && /[0-9]/.test(v))) return false;
|
|
137
|
-
const counts = new Map<string, number>();
|
|
138
|
-
for (const ch of v) counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
139
|
-
let entropy = 0;
|
|
140
|
-
for (const c of counts.values()) {
|
|
141
|
-
const p = c / v.length;
|
|
142
|
-
entropy -= p * Math.log2(p);
|
|
143
|
-
}
|
|
144
|
-
return entropy >= 4.2;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function secrets(value: unknown): string[] {
|
|
148
|
-
const hits: string[] = [];
|
|
149
|
-
const walk = (v: unknown, key: string | null): void => {
|
|
150
|
-
if (typeof v === 'string') {
|
|
151
|
-
for (const re of SECRET_PREFIX) if (re.test(v)) hits.push(`${v.slice(0, 24)} [issuer-prefix]`);
|
|
152
|
-
if (key !== null && CREDENTIAL_KEY.test(key) && !isPlaceholder(v)) {
|
|
153
|
-
hits.push(`${key}=${v.slice(0, 24)} [credential-named]`);
|
|
154
|
-
}
|
|
155
|
-
if (isDenseToken(v)) hits.push(`${v.slice(0, 24)} [dense-token]`);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
if (Array.isArray(v)) return v.forEach((c) => walk(c, key));
|
|
159
|
-
if (v !== null && typeof v === 'object') {
|
|
160
|
-
for (const [k, c] of Object.entries(v)) walk(c, k);
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
walk(value, null);
|
|
164
|
-
return [...new Set(hits)];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
describe.skipIf(V1_DIR === null)('RFC 0149 §E — canonical families are not shadowed', () => {
|
|
168
|
-
const v1Dir = V1_DIR as string;
|
|
169
|
-
const rfcsDir = V1_DIR === null ? '' : pathResolve(v1Dir, '..', '..', 'RFCS');
|
|
170
|
-
|
|
171
|
-
it('the canonical family list and the example corpus are both non-empty', () => {
|
|
172
|
-
// Guard: an empty family set or an empty scan makes both legs vacuous —
|
|
173
|
-
// the failure RFC 0148 exists to close.
|
|
174
|
-
expect(canonicalFamilies().size, 'capabilities.schema.json MUST declare families').toBeGreaterThan(50);
|
|
175
|
-
expect(fencedObjects(v1Dir).length, 'spec/v1 MUST contain parseable json examples').toBeGreaterThan(20);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('no vendor-namespaced object re-declares a canonical family', () => {
|
|
179
|
-
const families = canonicalFamilies();
|
|
180
|
-
const findings: Finding[] = [];
|
|
181
|
-
for (const dir of [v1Dir, rfcsDir]) {
|
|
182
|
-
for (const { file, line, value } of fencedObjects(dir)) {
|
|
183
|
-
const hits = shadowed(value, families, false);
|
|
184
|
-
if (hits.length > 0) {
|
|
185
|
-
const rel = dir === v1Dir ? 'spec/v1' : 'RFCS';
|
|
186
|
-
findings.push({ file, line, detail: `${rel}/${file}:${line} → ${[...new Set(hits)].join(', ')}` });
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
expect(
|
|
191
|
-
findings.map((f) => f.detail),
|
|
192
|
-
'RFC 0149 §E: a canonical family name inside `x-host-*` / `vendor.*` / `private.*` shadows ' +
|
|
193
|
-
'the family a consumer negotiates on. `host-extensions.md` tells clients to treat ' +
|
|
194
|
-
'extension surface as opaque, but that binds the consumer — it does not stop a host from ' +
|
|
195
|
-
'publishing the collision, and the consumer that merges vendor over root before ' +
|
|
196
|
-
'negotiating is exactly the one the rule was meant to protect.\n ' +
|
|
197
|
-
findings.map((f) => f.detail).join('\n '),
|
|
198
|
-
).toEqual([]);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('no discovery example carries credential material', () => {
|
|
202
|
-
const findings: string[] = [];
|
|
203
|
-
for (const dir of [v1Dir, rfcsDir]) {
|
|
204
|
-
for (const { file, line, value } of fencedObjects(dir)) {
|
|
205
|
-
const hits = secrets(value);
|
|
206
|
-
if (hits.length > 0) {
|
|
207
|
-
const rel = dir === v1Dir ? 'spec/v1' : 'RFCS';
|
|
208
|
-
findings.push(`${rel}/${file}:${line} → ${hits.join(', ')}…`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
expect(
|
|
213
|
-
findings,
|
|
214
|
-
'RFC 0149 §E: discovery is served credential-free to anonymous callers, so a real-shaped ' +
|
|
215
|
-
'secret in an example sits in the most-copied, least-guarded artifact in the corpus. ' +
|
|
216
|
-
'Use an obvious placeholder.\n ' + findings.join('\n '),
|
|
217
|
-
).toEqual([]);
|
|
218
|
-
});
|
|
219
|
-
});
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Edge conditions — `truthy` / `falsy` operators (RFC 0134).
|
|
3
|
-
*
|
|
4
|
-
* TWO parts:
|
|
5
|
-
* A. Always-on corpus legs — `workflow-definition.schema.json` §EdgeCondition and
|
|
6
|
-
* the inlined `workflow-chain-pack-manifest.schema.json` §EdgeCondition both carry
|
|
7
|
-
* `truthy`/`falsy` in the `type` enum; a `truthy`/`falsy` edge (no `right`)
|
|
8
|
-
* validates; the spec documents the no-`right` + required-`left` semantics.
|
|
9
|
-
* B. Capability-gated host leg — a chain whose fragment carries a `truthy` + a `falsy`
|
|
10
|
-
* edge off one approval-gate node instantiates through `from-chain` and the expanded
|
|
11
|
-
* edges carry the mapped host-native truthy/falsy conditions; a `truthy` edge with no
|
|
12
|
-
* `left` is refused. Gated on `workflowChainPacks.supported`; soft-skips until a
|
|
13
|
-
* reference host maps the operators (landed at RFC 0134 `Active`, per §Conformance).
|
|
14
|
-
*
|
|
15
|
-
* @see spec/v1/workflow-chain-packs.md §"Edge-condition operators (RFC 0134)"
|
|
16
|
-
* @see schemas/workflow-definition.schema.json §EdgeCondition
|
|
17
|
-
* @see RFCS/0134-edge-condition-truthy-falsy.md
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { describe, it, expect } from 'vitest';
|
|
21
|
-
import { readFileSync } from 'node:fs';
|
|
22
|
-
import { join } from 'node:path';
|
|
23
|
-
import Ajv2020 from 'ajv/dist/2020.js';
|
|
24
|
-
import addFormats from 'ajv-formats';
|
|
25
|
-
import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
|
|
26
|
-
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
27
|
-
import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
|
|
28
|
-
|
|
29
|
-
const cite = (section: string, requirement: string): string => `${section} — ${requirement}`;
|
|
30
|
-
const WORKFLOW_DEF = join(SCHEMAS_DIR, 'workflow-definition.schema.json');
|
|
31
|
-
const MANIFEST = join(SCHEMAS_DIR, 'workflow-chain-pack-manifest.schema.json');
|
|
32
|
-
// S38 (2026-08-17): `spec/` is NOT in the published package (`files`), so a path built
|
|
33
|
-
// from SCHEMAS_DIR/../spec ENOENTs for every npm consumer — five always-on legs reddened
|
|
34
|
-
// MyndHyve's bundle for a reason that had nothing to do with the host. Prose legs are
|
|
35
|
-
// repo-layout only: `null` in the published layout and skipped, never thrown.
|
|
36
|
-
const CHAIN_DOC: string | null = V1_DIR === null ? null : join(V1_DIR, 'workflow-chain-packs.md');
|
|
37
|
-
|
|
38
|
-
function loadSchema(path: string): Record<string, unknown> {
|
|
39
|
-
return JSON.parse(readFileSync(path, 'utf8')) as Record<string, unknown>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
describe('edge-condition-truthy-falsy §A: corpus (RFC 0134, always-on)', () => {
|
|
43
|
-
it('workflow-definition + manifest §EdgeCondition `type` enums both include truthy + falsy', () => {
|
|
44
|
-
for (const path of [WORKFLOW_DEF, MANIFEST]) {
|
|
45
|
-
const raw = readFileSync(path, 'utf8');
|
|
46
|
-
expect(raw.includes('"truthy"'), cite('§EdgeCondition', `truthy in ${path}`)).toBe(true);
|
|
47
|
-
expect(raw.includes('"falsy"'), cite('§EdgeCondition', `falsy in ${path}`)).toBe(true);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('a truthy/falsy edge condition (no `right`) validates against the manifest schema', () => {
|
|
52
|
-
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
53
|
-
addFormats(ajv);
|
|
54
|
-
const validate = ajv.compile(loadSchema(MANIFEST));
|
|
55
|
-
const pack = {
|
|
56
|
-
name: 'vendor.acme.branch',
|
|
57
|
-
version: '1.0.0',
|
|
58
|
-
kind: 'workflow-chain',
|
|
59
|
-
engines: { openwop: '^1' },
|
|
60
|
-
chains: [
|
|
61
|
-
{
|
|
62
|
-
chainId: 'acme.branch',
|
|
63
|
-
version: '1.0.0',
|
|
64
|
-
label: 'Branch',
|
|
65
|
-
description: 'Approval branch.',
|
|
66
|
-
parameters: {},
|
|
67
|
-
dag: {
|
|
68
|
-
nodes: [
|
|
69
|
-
{ id: 'approve', typeId: 'core.chat.approvalGate', config: {} },
|
|
70
|
-
{ id: 'apply', typeId: 'core.ai.callPrompt', config: {} },
|
|
71
|
-
{ id: 'reject', typeId: 'core.fail', config: {} },
|
|
72
|
-
],
|
|
73
|
-
edges: [
|
|
74
|
-
{ from: 'approve', to: 'apply', condition: { type: 'truthy', left: 'approved' } },
|
|
75
|
-
{ from: 'approve', to: 'reject', condition: { type: 'falsy', left: 'approved' } },
|
|
76
|
-
],
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
],
|
|
80
|
-
};
|
|
81
|
-
expect(validate(pack), cite('§EdgeCondition', `truthy/falsy edges validate: ${ajv.errorsText(validate.errors)}`)).toBe(true);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it.skipIf(CHAIN_DOC === null)('the spec documents the no-`right` + required-`left` truthy/falsy semantics', () => {
|
|
85
|
-
const doc = readFileSync(CHAIN_DOC as string, 'utf8');
|
|
86
|
-
expect(doc.includes('truthy'), cite('§Edge-condition operators', 'documents truthy')).toBe(true);
|
|
87
|
-
expect(
|
|
88
|
-
/truthy[\s\S]{0,400}(no|without).{0,20}`?right`?/i.test(doc) || /(no|without).{0,20}`?right`?[\s\S]{0,400}truthy/i.test(doc),
|
|
89
|
-
cite('§Edge-condition operators', 'documents that truthy/falsy take no right operand'),
|
|
90
|
-
).toBe(true);
|
|
91
|
-
expect(
|
|
92
|
-
/`?left`?[\s\S]{0,120}(required|MUST)/i.test(doc),
|
|
93
|
-
cite('§Edge-condition operators', 'documents left is required'),
|
|
94
|
-
).toBe(true);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe('edge-condition-truthy-falsy §B: host mapping (RFC 0134, capability-gated)', () => {
|
|
99
|
-
it('a host expanding chains maps truthy/falsy edge conditions onto the expanded workflow', async () => {
|
|
100
|
-
const wcp = await readCapabilityFamily<{ supported?: boolean }>('workflowChainPacks');
|
|
101
|
-
if (!behaviorGate('workflowChainPacks.supported', wcp?.supported === true)) return;
|
|
102
|
-
// Behavioral leg — exercised once a reference host maps the operators (RFC 0134
|
|
103
|
-
// Active): a chain carrying truthy/falsy edges instantiates via from-chain and the
|
|
104
|
-
// expanded edges carry the host-native truthy/falsy conditions; a truthy edge with
|
|
105
|
-
// no `left` is refused `chain_edge_condition_invalid`. Gate on base chain expansion.
|
|
106
|
-
expect(wcp?.supported, 'host advertising chain expansion honors the 0134 operators').toBe(true);
|
|
107
|
-
});
|
|
108
|
-
});
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RFC 0150 §B — the Layer-2 logical effect identity is stable across retries.
|
|
3
|
-
*
|
|
4
|
-
* `spec/v1/idempotency.md` contradicted itself. §"Idempotency key composition"
|
|
5
|
-
* put `attempt` — documented one line later as the "zero-based retry attempt
|
|
6
|
-
* counter" — inside the hash, while §"Composition: how the layers compose"
|
|
7
|
-
* promised that when "the engine retries the OpenAI call internally (transient
|
|
8
|
-
* 503), Layer 2's `invocationId` is identical, so the second call either
|
|
9
|
-
* short-circuits (cache hit) or hits OpenAI's own idempotency cache".
|
|
10
|
-
*
|
|
11
|
-
* Both cannot hold. A retry counter in the key means every retry hashes to a
|
|
12
|
-
* NEW key, so the invocation log never hits, the injected `Idempotency-Key`
|
|
13
|
-
* differs, and the provider's own dedup is defeated too. The composition
|
|
14
|
-
* guaranteed a duplicate side effect on precisely the path Layer 2 exists to
|
|
15
|
-
* protect — a duplicate charge, a duplicate send, a duplicate completion.
|
|
16
|
-
*
|
|
17
|
-
* The defect was invisible to every gate in the corpus because both halves are
|
|
18
|
-
* prose. Nothing parsed the formula, and nothing cross-read it against the
|
|
19
|
-
* paragraph asserting the opposite.
|
|
20
|
-
*
|
|
21
|
-
* This gate reads the normative composition block and holds it to §B: domain
|
|
22
|
-
* separation, tenant binding, a per-logical-invocation ordinal that is stable
|
|
23
|
-
* across retries, and NO retry counter. `attempt` remains legitimate telemetry;
|
|
24
|
-
* §B's requirement is that it MUST NOT participate in the identity.
|
|
25
|
-
*
|
|
26
|
-
* Server-free and always-on: it reads the corpus, never a host. `spec/v1/`
|
|
27
|
-
* ships in the repository and NOT in the published tarball, so it self-skips
|
|
28
|
-
* under the published layout — the asymmetry that has already produced three
|
|
29
|
-
* defects here (the `CORPUS-STAMP` gate, the link-checker's filesystem walk,
|
|
30
|
-
* and RFC 0146 leg A4).
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
import { describe, it, expect } from 'vitest';
|
|
34
|
-
import { readFileSync } from 'node:fs';
|
|
35
|
-
import { join } from 'node:path';
|
|
36
|
-
import { V1_DIR } from '../lib/paths.js';
|
|
37
|
-
|
|
38
|
-
const HEADING = '### Idempotency key composition';
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* The first fenced block under the composition heading. Returns null when the
|
|
42
|
-
* heading or its fence is absent, so the guard leg can fail loudly rather than
|
|
43
|
-
* letting every assertion below pass over an empty string.
|
|
44
|
-
*/
|
|
45
|
-
function compositionBlock(doc: string): string | null {
|
|
46
|
-
const lines = doc.split('\n');
|
|
47
|
-
const start = lines.findIndex((l) => l.trim() === HEADING);
|
|
48
|
-
if (start === -1) return null;
|
|
49
|
-
const open = lines.findIndex((l, i) => i > start && /^```/.test(l.trim()));
|
|
50
|
-
if (open === -1) return null;
|
|
51
|
-
const body: string[] = [];
|
|
52
|
-
for (let i = open + 1; i < lines.length && lines[i]!.trim() !== '```'; i++) body.push(lines[i]!);
|
|
53
|
-
return body.length === 0 ? null : body.join('\n');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
describe.skipIf(V1_DIR === null)('RFC 0150 §B — Layer-2 effect identity is retry-stable', () => {
|
|
57
|
-
const doc = V1_DIR === null ? '' : readFileSync(join(V1_DIR as string, 'idempotency.md'), 'utf8');
|
|
58
|
-
|
|
59
|
-
it('the normative composition block is found at all', () => {
|
|
60
|
-
// Guard: an extractor that matched nothing would make every leg below
|
|
61
|
-
// vacuously true. That is the exact failure RFC 0148 exists to close, and
|
|
62
|
-
// this gate must not become an instance of it.
|
|
63
|
-
expect(
|
|
64
|
-
compositionBlock(doc),
|
|
65
|
-
`spec/v1/idempotency.md MUST carry a fenced key composition under "${HEADING}"`,
|
|
66
|
-
).not.toBeNull();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('the retry counter does not participate in the identity', () => {
|
|
70
|
-
const block = compositionBlock(doc) ?? '';
|
|
71
|
-
expect(
|
|
72
|
-
/\battempt\b/.test(block),
|
|
73
|
-
'RFC 0150 §B: `attempt` is separate telemetry and MUST NOT participate in the logical ID. ' +
|
|
74
|
-
'A retry counter inside the hash gives every retry a different key, so the invocation ' +
|
|
75
|
-
'log never hits and the injected Idempotency-Key differs — the duplicate side effect ' +
|
|
76
|
-
'Layer 2 exists to prevent.\n' +
|
|
77
|
-
block,
|
|
78
|
-
).toBe(false);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('the identity is domain-separated and version-tagged', () => {
|
|
82
|
-
const block = compositionBlock(doc) ?? '';
|
|
83
|
-
expect(
|
|
84
|
-
block,
|
|
85
|
-
'RFC 0150 §B: the preimage MUST open with the `openwop:activity:v2` domain tag so a v1 ' +
|
|
86
|
-
'and a v2 identity for the same effect cannot collide.',
|
|
87
|
-
).toContain('openwop:activity:v2');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('the identity binds the tenant', () => {
|
|
91
|
-
const block = compositionBlock(doc) ?? '';
|
|
92
|
-
expect(
|
|
93
|
-
block,
|
|
94
|
-
'RFC 0150 §B: `tenantId` is part of the preimage. Without it two tenants that collide on ' +
|
|
95
|
-
'(runId, nodeId, providerKey) share an invocation-log entry, and one tenant reads the ' +
|
|
96
|
-
"other's cached provider response.",
|
|
97
|
-
).toContain('tenantId');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('the ordinal is documented as stable across retries', () => {
|
|
101
|
-
const block = compositionBlock(doc) ?? '';
|
|
102
|
-
expect(block, 'RFC 0150 §B: the preimage carries `logicalInvocationOrdinal`').toContain(
|
|
103
|
-
'logicalInvocationOrdinal',
|
|
104
|
-
);
|
|
105
|
-
// The ordinal only does its job if the prose pins it. An ordinal that a host
|
|
106
|
-
// is free to re-derive per attempt reintroduces the defect under a new name.
|
|
107
|
-
// Emphasis and code spans are stripped first so the assertion reads the
|
|
108
|
-
// requirement, not the markdown that happens to decorate it.
|
|
109
|
-
const plain = doc.replace(/[`*_]/g, '').replace(/\s+/g, ' ');
|
|
110
|
-
expect(
|
|
111
|
-
/logicalInvocationOrdinal MUST NOT change/.test(plain),
|
|
112
|
-
'RFC 0150 §B: `logicalInvocationOrdinal` MUST NOT change across transport/provider retries, ' +
|
|
113
|
-
'and the spec MUST say so — otherwise a host may re-derive it per attempt and the ' +
|
|
114
|
-
'retry-instability returns under a different field name.',
|
|
115
|
-
).toBe(true);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('the composition agrees with the claim that a retried call reuses the identity', () => {
|
|
119
|
-
// The two halves of the contradiction. This leg anchors the ones above to a
|
|
120
|
-
// real promise in the document rather than to the RFC alone: if the claim
|
|
121
|
-
// is ever deleted instead of the formula being fixed, this fails and says so.
|
|
122
|
-
expect(
|
|
123
|
-
/is identical/.test(doc),
|
|
124
|
-
'spec/v1/idempotency.md §"Composition: how the layers compose" MUST keep the guarantee ' +
|
|
125
|
-
'that an internally retried call reuses the same Layer-2 identity. It is the promise the ' +
|
|
126
|
-
'composition above has to honor.',
|
|
127
|
-
).toBe(true);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RFC 0150 §B — Layer-2 identity is run-scoped, and the spec has to say what
|
|
3
|
-
* that costs.
|
|
4
|
-
*
|
|
5
|
-
* `runId` is in the §B preimage. An effect issued outside any run has no
|
|
6
|
-
* `runId`, so the two identities can never collide, and Layer 2 cannot
|
|
7
|
-
* deduplicate an in-run effect against the same logical effect issued through
|
|
8
|
-
* an operator route, an admin action, or a scheduled job.
|
|
9
|
-
*
|
|
10
|
-
* §"Why this exists" already says implementations "MUST support layer 2 for any
|
|
11
|
-
* node executor that performs an external side effect", and §"Layer 2" opens
|
|
12
|
-
* "Inside a workflow run…". A host reading those literally uses the §B form for
|
|
13
|
-
* the node path — correctly. If that same effect is *also* reachable outside a
|
|
14
|
-
* run, the two paths issue two effects for one logical operation, which is
|
|
15
|
-
* precisely the duplicate-effect class §B exists to kill, on the highest-stakes
|
|
16
|
-
* path it touches.
|
|
17
|
-
*
|
|
18
|
-
* Reported by a tier-1 host from a shipped node pack, not a thought experiment:
|
|
19
|
-
* `feature.commerce.nodes.refund-order` calls the same `refundOrder` that an
|
|
20
|
-
* HTTP route, a connect-admin route, and a seeder call. They key it on business
|
|
21
|
-
* identity rather than the §B form *deliberately*, because run-scoped identity
|
|
22
|
-
* is the wrong scope for that effect — and §B says as much about itself in the
|
|
23
|
-
* fork note, one face of the same limitation.
|
|
24
|
-
*
|
|
25
|
-
* The corpus was silent on this. Silence here reads as "the ordinal form is
|
|
26
|
-
* sufficient", which for a cross-entry-point effect is false.
|
|
27
|
-
*
|
|
28
|
-
* Server-free; reads the corpus, never a host.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import { describe, it, expect } from 'vitest';
|
|
32
|
-
import { readFileSync } from 'node:fs';
|
|
33
|
-
import { join } from 'node:path';
|
|
34
|
-
import { V1_DIR } from '../lib/paths.js';
|
|
35
|
-
|
|
36
|
-
describe.skipIf(V1_DIR === null)('RFC 0150 §B — cross-scope effect identity', () => {
|
|
37
|
-
const doc = V1_DIR === null ? '' : readFileSync(join(V1_DIR as string, 'idempotency.md'), 'utf8');
|
|
38
|
-
const plain = doc.replace(/[`*_]/g, '').replace(/\s+/g, ' ');
|
|
39
|
-
|
|
40
|
-
it('the Layer-2 section is found at all', () => {
|
|
41
|
-
// Guard: an empty read makes every leg below vacuously true.
|
|
42
|
-
expect(doc.length, 'idempotency.md MUST be readable').toBeGreaterThan(1000);
|
|
43
|
-
expect(plain, 'the Layer-2 section MUST exist').toContain('Layer 2: Activity-level idempotency');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('the spec states that Layer-2 identity is run-scoped', () => {
|
|
47
|
-
expect(
|
|
48
|
-
/run-scoped/.test(plain),
|
|
49
|
-
'RFC 0150 §B: `runId` is in the preimage, so the identity is scoped to a run. Saying so ' +
|
|
50
|
-
'explicitly is what makes the next requirement follow rather than look arbitrary.',
|
|
51
|
-
).toBe(true);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('the spec requires a business identity when the effect escapes the run', () => {
|
|
55
|
-
expect(
|
|
56
|
-
/reachable outside any run/.test(plain),
|
|
57
|
-
'RFC 0150 §B: the spec MUST name the case — a node side effect that is ALSO reachable ' +
|
|
58
|
-
'outside any run (operator route, admin action, scheduled job).',
|
|
59
|
-
).toBe(true);
|
|
60
|
-
expect(
|
|
61
|
-
/MUST additionally key/.test(plain),
|
|
62
|
-
'RFC 0150 §B: for such an effect the host MUST additionally key on an identity derived from ' +
|
|
63
|
-
'the business operation. Layer-2 identity alone cannot dedupe across the boundary, because ' +
|
|
64
|
-
'the out-of-run path has no `runId` to put in the preimage — so a host following the ' +
|
|
65
|
-
'ordinal form literally reintroduces the duplicate effect §B exists to prevent.',
|
|
66
|
-
).toBe(true);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('the run-scope cost is tied to the fork limitation it shares a cause with', () => {
|
|
70
|
-
// Both are the same fact seen from two sides: `runId` in the preimage. The
|
|
71
|
-
// spec already documented the fork face; documenting only that one taught
|
|
72
|
-
// half a limitation.
|
|
73
|
-
// Asserts the linkage, not a magic phrase: the cross-scope section must
|
|
74
|
-
// name the fork limitation as the same fact seen from another side.
|
|
75
|
-
expect(
|
|
76
|
-
/fork note[\s\S]{0,120}?(one face|other face|same)/.test(plain),
|
|
77
|
-
'RFC 0150 §B: the fork note and the cross-scope note are one limitation seen twice — ' +
|
|
78
|
-
'`runId` in the preimage. The cross-scope section MUST reference the fork note, or a ' +
|
|
79
|
-
'reader concludes the fork case is a special exception rather than an instance.',
|
|
80
|
-
).toBe(true);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S22 (2026-08-16) — the HTTP error envelope is FLAT, and the suite reads it
|
|
3
|
-
* through one helper.
|
|
4
|
-
*
|
|
5
|
-
* `rest-endpoints.md` §"Error response shape" and `schemas/error-envelope.schema.json`
|
|
6
|
-
* lock `{ error: <string code>, message, details? }` with `additionalProperties:
|
|
7
|
-
* false`. A nested `{ error: { code, retriable } }` shape had crept into three
|
|
8
|
-
* code-list entries, four seam contracts and ~15 scenarios; this scenario pins
|
|
9
|
-
* the decision at the three places it lives — the schema, the helper that every
|
|
10
|
-
* HTTP-envelope leg now reads through, and the prose — so it cannot re-open
|
|
11
|
-
* quietly. Server-free, always-on.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect } from 'vitest';
|
|
15
|
-
import { readFileSync } from 'node:fs';
|
|
16
|
-
import { join } from 'node:path';
|
|
17
|
-
import Ajv2020 from 'ajv/dist/2020.js';
|
|
18
|
-
import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
|
|
19
|
-
import { readErrorCode, readRetriable, isCanonicalErrorEnvelope, isLegacyNestedEnvelope } from '../lib/error-envelope.js';
|
|
20
|
-
|
|
21
|
-
export const HOST_CALLBACK_NOT_REQUIRED = 'server-free: pins the flat error-envelope decision against the schema, the helper and the prose';
|
|
22
|
-
|
|
23
|
-
describe('S22 — the canonical HTTP error envelope is flat', () => {
|
|
24
|
-
const schema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'error-envelope.schema.json'), 'utf8')) as Record<string, unknown>;
|
|
25
|
-
const validate = new Ajv2020({ allErrors: true, strict: false }).compile(schema);
|
|
26
|
-
|
|
27
|
-
it('the schema says `error` is a string, requires `message`, and forbids other top-level keys', () => {
|
|
28
|
-
expect(validate({ error: 'runner_unavailable', message: 'no runner', details: { retriable: true } })).toBe(true);
|
|
29
|
-
expect(validate({ error: { code: 'runner_unavailable', retriable: true } })).toBe(false);
|
|
30
|
-
expect(validate({ error: 'x', message: 'y', retriable: true })).toBe(false); // top-level retriable is illegal
|
|
31
|
-
expect(validate({ error: 'x' })).toBe(false); // message required
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('readErrorCode / readRetriable read the canonical shape, and tolerate the legacy nested shape only as legacy', () => {
|
|
35
|
-
const flat = { error: 'interop_version_unsupported', message: 'peer offers 0.3', details: { retriable: false, protocol: 'a2a' } };
|
|
36
|
-
expect(readErrorCode(flat)).toBe('interop_version_unsupported');
|
|
37
|
-
expect(readRetriable(flat)).toBe(false);
|
|
38
|
-
expect(isCanonicalErrorEnvelope(flat)).toBe(true);
|
|
39
|
-
expect(isLegacyNestedEnvelope(flat)).toBe(false);
|
|
40
|
-
const nested = { error: { code: 'runner_unavailable', retriable: true } };
|
|
41
|
-
expect(readErrorCode(nested)).toBe('runner_unavailable');
|
|
42
|
-
expect(readRetriable(nested)).toBe(true);
|
|
43
|
-
expect(isCanonicalErrorEnvelope(nested)).toBe(false);
|
|
44
|
-
expect(isLegacyNestedEnvelope(nested)).toBe(true);
|
|
45
|
-
expect(readErrorCode({ message: 'no code' })).toBeUndefined();
|
|
46
|
-
expect(readErrorCode(null)).toBeUndefined();
|
|
47
|
-
expect(readRetriable({ error: 'x', message: 'y' })).toBeUndefined();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it.skipIf(V1_DIR === null)('rest-endpoints.md states the precedence, the details.retriable convention, and carries no nested code-list entry', () => {
|
|
51
|
-
const prose = readFileSync(join(V1_DIR as string, 'rest-endpoints.md'), 'utf8');
|
|
52
|
-
expect(prose).toContain('flat** shape above');
|
|
53
|
-
expect(prose).toContain('### `details.retriable` convention');
|
|
54
|
-
// the three formerly-nested code-list entries are flat now
|
|
55
|
-
for (const code of ['runner_unavailable', 'residency_unavailable', 'interop_version_unsupported']) {
|
|
56
|
-
const line = prose.split('\n').find((l) => l.startsWith('- `' + code + '`')) ?? '';
|
|
57
|
-
expect(line, code + ' code-list entry MUST exist').not.toBe('');
|
|
58
|
-
expect(line, code + ' MUST NOT be described with the nested envelope').not.toMatch(/\{\s*"error"\s*:\s*\{/);
|
|
59
|
-
}
|
|
60
|
-
// the seam catalog too
|
|
61
|
-
const seams = readFileSync(join(V1_DIR as string, 'host-sample-test-seams.md'), 'utf8');
|
|
62
|
-
expect(seams).not.toMatch(/"error":\s*\{\s*"code"/);
|
|
63
|
-
});
|
|
64
|
-
});
|