@openwop/openwop-conformance 2.0.5 → 2.0.6
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 +39 -0
- package/dist/lib/paths.js +4 -0
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +3 -3
- package/schemas/CORPUS-STAMP.json +8 -8
- package/src/lib/era2-seed.ts +57 -3
- package/src/lib/paths.ts +19 -0
- package/src/scenarios/v2-unmapped-type-refused.test.ts +19 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [2.0.6] — 2026-09-06 — the release that made a rule uncheckable
|
|
4
|
+
|
|
5
|
+
**If you pinned 2.0.5, `v2-unmapped-type-refused` did not run against your
|
|
6
|
+
host.** It soft-skipped `inapplicable` and the lane went green. Pin 2.0.6 and
|
|
7
|
+
re-measure before trusting any result that scenario gave you.
|
|
8
|
+
|
|
9
|
+
2.0.5 shipped four fixes for rules whose instruments could not answer, and one
|
|
10
|
+
of those fixes did the same thing to a fifth. Reported by a host operator who
|
|
11
|
+
kept a local witness for a defect they knew was unfixed and re-measured before
|
|
12
|
+
accepting a green they wanted. Two defects, one symptom:
|
|
13
|
+
|
|
14
|
+
- **The corpus resolver was anchored on a repo-only directory.**
|
|
15
|
+
`registeredOrgs()` and `codemapV1toV2()` located `spec/v2/` via
|
|
16
|
+
`V1_DIR/../v2` — a **v1**-anchored path to a **v2** file. No published package
|
|
17
|
+
ships `spec/v1/`, so `V1_DIR` is `null` in every install and both lookups
|
|
18
|
+
returned nothing. The data was never missing; it is in the exact-pinned
|
|
19
|
+
`@openwop/spec-artifacts` peer. Both now resolve through `SPEC_V2_DIR`
|
|
20
|
+
(`lib/paths`), anchored on the contract root, which holds in a repo checkout
|
|
21
|
+
and an install alike. The codemap failed more quietly — 7 fallback rows
|
|
22
|
+
instead of 118, so era-2 readers asserted against names it never had.
|
|
23
|
+
|
|
24
|
+
- **A precondition written for one leg gated the other.**
|
|
25
|
+
`v2-unmapped-type-refused` drives two opposite halves of the reader rule.
|
|
26
|
+
Both were gated on a single check that demanded a resolvable registry, which
|
|
27
|
+
only the *control* leg needs. With the registry unreachable, the *refusal*
|
|
28
|
+
leg skipped on exactly the hosts it exists to catch: one answering `200` and
|
|
29
|
+
one answering `500` were both green. Gates are now separate and pure
|
|
30
|
+
(`unmappedRefusalGate` / `vendorControlGate`), with the fail-closed reading
|
|
31
|
+
explicit — an unreadable registry registers nothing, so the refusal is still
|
|
32
|
+
required and the leg stays drivable.
|
|
33
|
+
|
|
34
|
+
Added `src/lib/era2-unmapped-gates.test.ts` (the gates' truth table, including
|
|
35
|
+
the rows a live-host scenario cannot check about itself; sabotage-verified) and
|
|
36
|
+
a post-install corpus-resolution assertion in `verify-installable` — a clean
|
|
37
|
+
`npm install` was never evidence the suite works.
|
|
38
|
+
|
|
39
|
+
**A ratchet's STALE signal means the scenario stopped failing. It is not
|
|
40
|
+
evidence the defect is fixed.**
|
|
41
|
+
|
|
3
42
|
## [2.0.5] — 2026-09-06 — four rules with no way to be checked
|
|
4
43
|
|
|
5
44
|
Four instruments that could not, even in principle, return the answer they
|
package/dist/lib/paths.js
CHANGED
|
@@ -103,6 +103,8 @@ function resolveFromRoot(root, layout, contractRoot = root) {
|
|
|
103
103
|
: null;
|
|
104
104
|
const v1Probe = join(root, 'spec', 'v1');
|
|
105
105
|
const v1Dir = existsSync(v1Probe) ? v1Probe : null;
|
|
106
|
+
const specV2Probe = join(contractRoot, 'spec', 'v2');
|
|
107
|
+
const specV2Dir = existsSync(specV2Probe) ? specV2Probe : null;
|
|
106
108
|
const readmeProbe = join(root, 'README.md');
|
|
107
109
|
const readmePath = existsSync(readmeProbe) ? readmeProbe : null;
|
|
108
110
|
const typescriptRunHelpersProbe = join(root, 'sdk', 'typescript', 'src', 'run-helpers.ts');
|
|
@@ -121,6 +123,7 @@ function resolveFromRoot(root, layout, contractRoot = root) {
|
|
|
121
123
|
fixturesDocPath,
|
|
122
124
|
coverageDocPath,
|
|
123
125
|
v1Dir,
|
|
126
|
+
specV2Dir,
|
|
124
127
|
readmePath,
|
|
125
128
|
typescriptRunHelpersPath,
|
|
126
129
|
pythonTypesPath,
|
|
@@ -171,6 +174,7 @@ export const CONFORMANCE_README_PATH = _layout.conformanceReadmePath;
|
|
|
171
174
|
export const FIXTURES_DOC_PATH = _layout.fixturesDocPath;
|
|
172
175
|
export const COVERAGE_DOC_PATH = _layout.coverageDocPath;
|
|
173
176
|
export const V1_DIR = _layout.v1Dir;
|
|
177
|
+
export const SPEC_V2_DIR = _layout.specV2Dir;
|
|
174
178
|
export const README_PATH = _layout.readmePath;
|
|
175
179
|
export const TYPESCRIPT_RUN_HELPERS_PATH = _layout.typescriptRunHelpersPath;
|
|
176
180
|
export const PYTHON_TYPES_PATH = _layout.pythonTypesPath;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
4
4
|
"description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,6 +56,6 @@
|
|
|
56
56
|
"@openwop/spec-artifacts": "file:../spec-artifacts"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@openwop/spec-artifacts": "2.0.
|
|
59
|
+
"@openwop/spec-artifacts": "2.0.6"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/requirements.json
CHANGED
|
@@ -28687,7 +28687,7 @@
|
|
|
28687
28687
|
{
|
|
28688
28688
|
"id": "openwop.it.v2-unmapped-type-refused.poll-over-a-log-with-an-unmapped-unprefixed-type-fails-with-500-event-type-unmap",
|
|
28689
28689
|
"file": "v2-unmapped-type-refused.test.ts",
|
|
28690
|
-
"line":
|
|
28690
|
+
"line": 92,
|
|
28691
28691
|
"title": "poll over a log with an unmapped, unprefixed type fails with 500 event_type_unmapped",
|
|
28692
28692
|
"explicitId": "openwop.requirement.0176.unmapped-type-refused",
|
|
28693
28693
|
"citations": [
|
|
@@ -28711,7 +28711,7 @@
|
|
|
28711
28711
|
{
|
|
28712
28712
|
"id": "openwop.it.v2-unmapped-type-refused.a-registered-vendor-type-the-codemap-does-not-name-is-read-under-its-own-name-un",
|
|
28713
28713
|
"file": "v2-unmapped-type-refused.test.ts",
|
|
28714
|
-
"line":
|
|
28714
|
+
"line": 117,
|
|
28715
28715
|
"title": "a REGISTERED vendor type the codemap does not name is read under its own name, unchanged",
|
|
28716
28716
|
"explicitId": "openwop.requirement.0176.vendor-type-passthrough",
|
|
28717
28717
|
"citations": [
|
|
@@ -28730,7 +28730,7 @@
|
|
|
28730
28730
|
{
|
|
28731
28731
|
"id": "openwop.it.v2-unmapped-type-refused.a-fork-of-the-same-log-is-refused-for-the-same-reason-the-rule-binds-every-reade",
|
|
28732
28732
|
"file": "v2-unmapped-type-refused.test.ts",
|
|
28733
|
-
"line":
|
|
28733
|
+
"line": 143,
|
|
28734
28734
|
"title": "a fork of the same log is refused for the same reason — the rule binds every reader",
|
|
28735
28735
|
"explicitId": "openwop.requirement.0176.unmapped-type-refused.fork",
|
|
28736
28736
|
"citations": [
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
|
|
3
3
|
"package": "@openwop/spec-artifacts",
|
|
4
|
-
"version": "2.0.
|
|
5
|
-
"corpusTag": "v2.0.
|
|
4
|
+
"version": "2.0.6",
|
|
5
|
+
"corpusTag": "v2.0.6",
|
|
6
6
|
"files": {
|
|
7
7
|
"api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
|
|
8
8
|
"api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
|
|
9
9
|
"api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
|
|
10
10
|
"api/openapi.yaml": "39081c59fb696159806b0f2f9a42e7e9ff830d622fcf2ad4159b21357580a955",
|
|
11
11
|
"api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
|
|
12
|
-
"api/seams-v2.yaml": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
12
|
+
"api/seams-v2.yaml": "594aad07baceb9c8e00dc9fe91dd8f6f99bc6624c64eaf5be91dbb5f22a35c48",
|
|
13
|
+
"api/v2/asyncapi.yaml": "5839f6cb16ed8997a335491a6700ddc2f4166c994ca2838863d4029c1ef6d58e",
|
|
14
|
+
"api/v2/openapi.yaml": "f1288076f774dbb46b885519b94719df84b31b4535fe2a5bccb9881d2e09b0ea",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
|
|
@@ -199,7 +199,7 @@
|
|
|
199
199
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
200
200
|
"spec/v1/alias-detectors.json": "fee4594ef49953953ffcd0b3813300067d16b3e65ebff2aac722034ac9b3f545",
|
|
201
201
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
202
|
-
"spec/v1/core-standard-manifest.json": "
|
|
202
|
+
"spec/v1/core-standard-manifest.json": "0018230eb426fb0af82545b9f63920cc9504a9eed2a03c44ef61cf73f429e82b",
|
|
203
203
|
"spec/v1/deprecations.json": "1d5acb69a9b8ccb57275a95605f74aef1d920685f8407c9d382a46b59dc803bb",
|
|
204
204
|
"spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
|
|
205
205
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
@@ -268,8 +268,8 @@
|
|
|
268
268
|
"spec/v2/path-manifest.json": "034152e09b1458c66810d4050e20a273b2b9b8fe2d92b66e8b819477de58a1be",
|
|
269
269
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
270
270
|
"spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
|
|
271
|
-
"spec/v2/release.json": "
|
|
271
|
+
"spec/v2/release.json": "7735a176082e42a30d7884654d79ac7affd7fc50f4b7346085f7d841769c6819",
|
|
272
272
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
|
|
273
273
|
},
|
|
274
|
-
"corpusCommit": "
|
|
274
|
+
"corpusCommit": "0ebdbc9ad741d49c44731e2bd255499a7da6c4a7"
|
|
275
275
|
}
|
package/src/lib/era2-seed.ts
CHANGED
|
@@ -35,7 +35,7 @@ import { driver, type OpenWOPResponse } from './driver.js';
|
|
|
35
35
|
import { loadEnv } from './env.js';
|
|
36
36
|
import { seamPath, seamsProfileAdvertised } from './seams.js';
|
|
37
37
|
import { readErrorCode } from './error-envelope.js';
|
|
38
|
-
import {
|
|
38
|
+
import { SPEC_V2_DIR } from './paths.js';
|
|
39
39
|
|
|
40
40
|
export const SEED_PATH_V1 = '/v1/host/sample/event-log/seed';
|
|
41
41
|
export const SEED_PATH = seamPath(SEED_PATH_V1);
|
|
@@ -171,7 +171,12 @@ let codemap: Map<string, string> | undefined;
|
|
|
171
171
|
export function codemapV1toV2(): Map<string, string> {
|
|
172
172
|
if (codemap) return codemap;
|
|
173
173
|
const m = new Map<string, string>(FALLBACK);
|
|
174
|
-
|
|
174
|
+
// Same wrong anchor as `registeredOrgs` carried, found while fixing that one:
|
|
175
|
+
// v1-anchored, so null on every published layout. It fails less loudly here
|
|
176
|
+
// because FALLBACK stands — but 7 rows are not 117, and a scenario asserting
|
|
177
|
+
// `got.type === map.get(s.type)` for a type outside the fallback was checking
|
|
178
|
+
// against a name the truncated map never had.
|
|
179
|
+
const candidate = SPEC_V2_DIR ? join(SPEC_V2_DIR, 'event-codemap.json') : null;
|
|
175
180
|
if (candidate && existsSync(candidate)) {
|
|
176
181
|
try {
|
|
177
182
|
const doc = JSON.parse(readFileSync(candidate, 'utf8')) as { rows?: Array<{ v1?: unknown; v2?: unknown }> };
|
|
@@ -198,7 +203,12 @@ export function codemapV1toV2(): Map<string, string> {
|
|
|
198
203
|
let orgs: ReadonlySet<string> | null | undefined;
|
|
199
204
|
export function registeredOrgs(): ReadonlySet<string> | undefined {
|
|
200
205
|
if (orgs !== undefined) return orgs ?? undefined;
|
|
201
|
-
|
|
206
|
+
// SPEC_V2_DIR, not `V1_DIR/../v2`. The v1-anchored form shipped in 2.0.5 and
|
|
207
|
+
// resolved to null for every consumer of the published package — `spec/v1/`
|
|
208
|
+
// exists only in a repo checkout, so a v2 lookup routed through a v1 probe
|
|
209
|
+
// lost the registry the moment anyone installed the suite. The declaration is
|
|
210
|
+
// reachable in both layouts; only the anchor was wrong.
|
|
211
|
+
const candidate = SPEC_V2_DIR ? join(SPEC_V2_DIR, 'declaration.json') : null;
|
|
202
212
|
orgs = null;
|
|
203
213
|
if (candidate && existsSync(candidate)) {
|
|
204
214
|
try {
|
|
@@ -209,6 +219,50 @@ export function registeredOrgs(): ReadonlySet<string> | undefined {
|
|
|
209
219
|
return orgs ?? undefined;
|
|
210
220
|
}
|
|
211
221
|
|
|
222
|
+
export type Gate = { ok: true } | { ok: false; kind: 'blocked' | 'inapplicable'; reason: string };
|
|
223
|
+
|
|
224
|
+
const orgOf = (type: string): string => type.split('.')[0] ?? '';
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Preconditions for the REFUSAL half of the reader rule (`v2-unmapped-type-
|
|
228
|
+
* refused`): a type the codemap does not name, whose org is not registered,
|
|
229
|
+
* MUST fail the read.
|
|
230
|
+
*
|
|
231
|
+
* `registered === undefined` (no resolvable registry) is NOT a blocker here.
|
|
232
|
+
* An unreadable registry registers nothing, so the driven org is unregistered
|
|
233
|
+
* either way and the refusal is still the required outcome — the fail-CLOSED
|
|
234
|
+
* reading. Only a registry that resolves AND names the org invalidates the leg.
|
|
235
|
+
*
|
|
236
|
+
* Suite 2.0.5 got this wrong by sharing one precondition with the control leg,
|
|
237
|
+
* which does need the registry. On published layouts the registry did not
|
|
238
|
+
* resolve, so the refusal leg soft-skipped `inapplicable` on exactly the hosts
|
|
239
|
+
* it exists to catch: one still answering `200` to an unmapped type and one
|
|
240
|
+
* correctly answering `500` were both green. A precondition belongs to the leg
|
|
241
|
+
* that needs it, never to the file.
|
|
242
|
+
*/
|
|
243
|
+
export function unmappedRefusalGate(registered: ReadonlySet<string> | undefined, map: ReadonlyMap<string, string>, type: string): Gate {
|
|
244
|
+
if (registered?.has(orgOf(type)) === true) {
|
|
245
|
+
return { ok: false, kind: 'blocked', reason: `the refusal leg drives ${type}, whose org '${orgOf(type)}' is NOW REGISTERED in spec/v2/declaration.json extensions — it is a vendor type that must pass through, not an unmapped one that must be refused; pick an unregistered org for this leg` };
|
|
246
|
+
}
|
|
247
|
+
if (map.has(type)) return { ok: false, kind: 'blocked', reason: `${type} now has a codemap row (→ ${String(map.get(type))}) — the refusal leg requires a type the codemap does not name` };
|
|
248
|
+
return { ok: true };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Preconditions for the CONTROL half: a registered vendor org the codemap does
|
|
253
|
+
* not name MUST be read under its own name unchanged.
|
|
254
|
+
*
|
|
255
|
+
* This leg genuinely needs the registry to resolve — it asserts that an org IS
|
|
256
|
+
* registered, and an absent registry cannot establish that. `inapplicable` here
|
|
257
|
+
* is honest rather than over-gating.
|
|
258
|
+
*/
|
|
259
|
+
export function vendorControlGate(registered: ReadonlySet<string> | undefined, map: ReadonlyMap<string, string>, type: string): Gate {
|
|
260
|
+
if (registered === undefined) return { ok: false, kind: 'inapplicable', reason: 'spec/v2/declaration.json is not resolvable in this layout — the control leg asserts that a REGISTERED org passes through, and guessing which orgs are registered would make the suite the registry' };
|
|
261
|
+
if (!registered.has(orgOf(type))) return { ok: false, kind: 'blocked', reason: `the control leg needs org '${orgOf(type)}' registered in spec/v2/declaration.json extensions (registered: ${[...registered].join(', ') || 'none'}) — without a registered org the positive half of the vendor rule cannot be driven at all` };
|
|
262
|
+
if (map.has(type)) return { ok: false, kind: 'blocked', reason: `${type} now has a codemap row (→ ${String(map.get(type))}) — the control leg requires a type the codemap does not name` };
|
|
263
|
+
return { ok: true };
|
|
264
|
+
}
|
|
265
|
+
|
|
212
266
|
/** A minimal era-2 log in v1 vocabulary: two renamed rows between run.started and run.completed. */
|
|
213
267
|
export function v1FixtureLog(workflowId = 'conformance-noop'): SeedEvent[] {
|
|
214
268
|
const t0 = Date.parse('2026-01-15T10:00:00.000Z');
|
package/src/lib/paths.ts
CHANGED
|
@@ -66,6 +66,21 @@ interface ResolvedLayout {
|
|
|
66
66
|
readonly coverageDocPath: string | null;
|
|
67
67
|
/** Directory containing v1 prose docs (`*.md`), if present in this layout. */
|
|
68
68
|
readonly v1Dir: string | null;
|
|
69
|
+
/**
|
|
70
|
+
* Directory containing the v2 corpus data (`declaration.json`, the codemap,
|
|
71
|
+
* `spec/v2/core/*.md`), if present in this layout.
|
|
72
|
+
*
|
|
73
|
+
* Anchored on the CONTRACT root, not the layout root, because that is the
|
|
74
|
+
* only anchor that holds in both shapes: in a repo checkout the contract
|
|
75
|
+
* root is the repo and `spec/v2/` sits inside it; in a published install it
|
|
76
|
+
* is the `@openwop/spec-artifacts` peer, which ships `spec/` while the
|
|
77
|
+
* conformance package ships none. A resolver anchored on `v1Dir` instead
|
|
78
|
+
* (the shape before 2.0.6) returned null for every consumer of the published
|
|
79
|
+
* package, because `spec/v1/` is a repo-only directory — so a v2 lookup was
|
|
80
|
+
* routed through a v1 probe and every published layout silently lost the
|
|
81
|
+
* data. See `era2-seed.registeredOrgs`.
|
|
82
|
+
*/
|
|
83
|
+
readonly specV2Dir: string | null;
|
|
69
84
|
/** Path to repository README.md, if present in this layout. */
|
|
70
85
|
readonly readmePath: string | null;
|
|
71
86
|
/** Path to the TypeScript SDK run-helper source, if present in this layout. */
|
|
@@ -137,6 +152,8 @@ function resolveFromRoot(root: string, layout: ResolvedLayout['layout'], contrac
|
|
|
137
152
|
: null;
|
|
138
153
|
const v1Probe = join(root, 'spec', 'v1');
|
|
139
154
|
const v1Dir = existsSync(v1Probe) ? v1Probe : null;
|
|
155
|
+
const specV2Probe = join(contractRoot, 'spec', 'v2');
|
|
156
|
+
const specV2Dir = existsSync(specV2Probe) ? specV2Probe : null;
|
|
140
157
|
const readmeProbe = join(root, 'README.md');
|
|
141
158
|
const readmePath = existsSync(readmeProbe) ? readmeProbe : null;
|
|
142
159
|
const typescriptRunHelpersProbe = join(root, 'sdk', 'typescript', 'src', 'run-helpers.ts');
|
|
@@ -155,6 +172,7 @@ function resolveFromRoot(root: string, layout: ResolvedLayout['layout'], contrac
|
|
|
155
172
|
fixturesDocPath,
|
|
156
173
|
coverageDocPath,
|
|
157
174
|
v1Dir,
|
|
175
|
+
specV2Dir,
|
|
158
176
|
readmePath,
|
|
159
177
|
typescriptRunHelpersPath,
|
|
160
178
|
pythonTypesPath,
|
|
@@ -208,6 +226,7 @@ export const CONFORMANCE_README_PATH: string | null = _layout.conformanceReadmeP
|
|
|
208
226
|
export const FIXTURES_DOC_PATH: string | null = _layout.fixturesDocPath;
|
|
209
227
|
export const COVERAGE_DOC_PATH: string | null = _layout.coverageDocPath;
|
|
210
228
|
export const V1_DIR: string | null = _layout.v1Dir;
|
|
229
|
+
export const SPEC_V2_DIR: string | null = _layout.specV2Dir;
|
|
211
230
|
export const README_PATH: string | null = _layout.readmePath;
|
|
212
231
|
export const TYPESCRIPT_RUN_HELPERS_PATH: string | null = _layout.typescriptRunHelpersPath;
|
|
213
232
|
export const PYTHON_TYPES_PATH: string | null = _layout.pythonTypesPath;
|
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
* lacked. The two legs are each other's non-vacuity check: refusing both is a
|
|
28
28
|
* defect, accepting both is a defect, and only the pair can tell them apart.
|
|
29
29
|
*
|
|
30
|
+
* BECAUSE THEY CHECK OPPOSITE HALVES, THEY GATE SEPARATELY (`refusalGate` /
|
|
31
|
+
* `controlGate`). Suite 2.0.5 gated both on the control leg's precondition and
|
|
32
|
+
* so disabled the refusal witness wherever the registry did not resolve — on
|
|
33
|
+
* exactly the hosts it exists to catch. A precondition belongs to the leg that
|
|
34
|
+
* needs it, never to the file.
|
|
35
|
+
*
|
|
30
36
|
* @see spec/v2/core/persistence.md §The reader rule
|
|
31
37
|
* @see spec/v2/core/events.md §Reading an era-2 log
|
|
32
38
|
*/
|
|
@@ -36,7 +42,7 @@ import { v2Discovery, gateFamily } from '../lib/v2.js';
|
|
|
36
42
|
import { readErrorCode, readRetriable } from '../lib/error-envelope.js';
|
|
37
43
|
import { softSkip } from '../lib/soft-skip.js';
|
|
38
44
|
import { req } from '../lib/requirement-ids.js';
|
|
39
|
-
import { codemapV1toV2, era2Gate, eventsOf, forkRun, pollEvents, registeredOrgs, seedEra2Log, type SeedEvent } from '../lib/era2-seed.js';
|
|
45
|
+
import { codemapV1toV2, era2Gate, eventsOf, forkRun, pollEvents, registeredOrgs, seedEra2Log, unmappedRefusalGate, vendorControlGate, type Gate, type SeedEvent } from '../lib/era2-seed.js';
|
|
40
46
|
|
|
41
47
|
const DOC = 'spec/v2/core/persistence.md §The reader rule';
|
|
42
48
|
const MAP = 'spec/v2/core/persistence.md §The codemap is data';
|
|
@@ -45,26 +51,17 @@ const ID_VENDOR = 'openwop.requirement.0176.vendor-type-passthrough';
|
|
|
45
51
|
const UNMAPPED = 'foo.bar';
|
|
46
52
|
/** Registered (`extensions` in the declaration), unmapped, owned by no host. */
|
|
47
53
|
const VENDOR = 'example.thing-happened';
|
|
48
|
-
const orgOf = (type: string): string => type.split('.')[0] ?? '';
|
|
49
54
|
|
|
50
55
|
/**
|
|
51
|
-
* The
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* here so the file fails loudly instead.
|
|
56
|
+
* The gate logic is PURE and lives in lib/era2-seed (`unmappedRefusalGate` /
|
|
57
|
+
* `vendorControlGate`) so its truth table can be measured directly — see
|
|
58
|
+
* `src/lib/era2-unmapped-gates.test.ts`. The rows that matter cannot be
|
|
59
|
+
* exercised from here: they are the layouts where the registry does NOT
|
|
60
|
+
* resolve, which is where the 2.0.5 defect lived and where a scenario running
|
|
61
|
+
* against a live host can never put itself.
|
|
58
62
|
*/
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (registered === undefined) return { ok: false, kind: 'inapplicable', reason: 'spec/v2/declaration.json is not resolvable in this layout — the vendor-org registry decides which half of the reader rule each type exercises, and guessing it would make the suite the registry' };
|
|
62
|
-
if (registered.has(orgOf(UNMAPPED))) return { ok: false, kind: 'blocked', reason: `the refusal leg drives ${UNMAPPED}, whose org '${orgOf(UNMAPPED)}' is NOW REGISTERED in spec/v2/declaration.json extensions — it is a vendor type that must pass through, not an unmapped one that must be refused; pick an unregistered org for this leg` };
|
|
63
|
-
if (!registered.has(orgOf(VENDOR))) return { ok: false, kind: 'blocked', reason: `the control leg needs org '${orgOf(VENDOR)}' registered in spec/v2/declaration.json extensions (registered: ${[...registered].join(', ') || 'none'}) — without a registered org the positive half of the vendor rule cannot be driven at all` };
|
|
64
|
-
const map = codemapV1toV2();
|
|
65
|
-
for (const t of [UNMAPPED, VENDOR]) if (map.has(t)) return { ok: false, kind: 'blocked', reason: `${t} now has a codemap row (→ ${String(map.get(t))}) — both legs require a type the codemap does not name` };
|
|
66
|
-
return { ok: true };
|
|
67
|
-
}
|
|
63
|
+
const refusalGate = (): Gate => unmappedRefusalGate(registeredOrgs(), codemapV1toV2(), UNMAPPED);
|
|
64
|
+
const controlGate = (): Gate => vendorControlGate(registeredOrgs(), codemapV1toV2(), VENDOR);
|
|
68
65
|
|
|
69
66
|
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
70
67
|
try { return await v2Discovery(); } catch { return null; }
|
|
@@ -97,7 +94,7 @@ describe('RFC 0176 §A.3 — unmapped-type-refused (seam-gated)', () => {
|
|
|
97
94
|
if (!doc) return softSkip('blocked', 'discovery unreachable');
|
|
98
95
|
const gate = era2Gate(doc);
|
|
99
96
|
if (gate !== null && !gate.ok) return softSkip(gate.kind, gate.reason);
|
|
100
|
-
const pre =
|
|
97
|
+
const pre = refusalGate();
|
|
101
98
|
if (!pre.ok) return softSkip(pre.kind, pre.reason);
|
|
102
99
|
const seeded = await seedEra2Log(unmappedLog(), 'completed');
|
|
103
100
|
if (!seeded.ok) return softSkip(seeded.kind, seeded.reason);
|
|
@@ -122,7 +119,7 @@ describe('RFC 0176 §A.3 — unmapped-type-refused (seam-gated)', () => {
|
|
|
122
119
|
if (!doc) return softSkip('blocked', 'discovery unreachable');
|
|
123
120
|
const gate = era2Gate(doc);
|
|
124
121
|
if (gate !== null && !gate.ok) return softSkip(gate.kind, gate.reason);
|
|
125
|
-
const pre =
|
|
122
|
+
const pre = controlGate();
|
|
126
123
|
if (!pre.ok) return softSkip(pre.kind, pre.reason);
|
|
127
124
|
const seeded = await seedEra2Log(vendorLog(), 'completed');
|
|
128
125
|
if (!seeded.ok) return softSkip(seeded.kind, seeded.reason);
|
|
@@ -149,7 +146,8 @@ describe('RFC 0176 §A.3 — unmapped-type-refused (seam-gated)', () => {
|
|
|
149
146
|
const gate = era2Gate(doc);
|
|
150
147
|
if (gate !== null && !gate.ok) return softSkip(gate.kind, gate.reason);
|
|
151
148
|
if (!(await gateFamily('replay'))) return softSkip('inapplicable', 'replay family not advertised (gate recorded under openwop.family.replay) — the fork reader has no surface');
|
|
152
|
-
|
|
149
|
+
// The fork leg drives the UNMAPPED type, so it takes the refusal gate.
|
|
150
|
+
const pre = refusalGate();
|
|
153
151
|
if (!pre.ok) return softSkip(pre.kind, pre.reason);
|
|
154
152
|
const log = unmappedLog();
|
|
155
153
|
const seeded = await seedEra2Log(log, 'completed');
|