@openwop/openwop-conformance 1.18.1 → 1.20.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 +2 -2
- package/api/.redocly.lint-ignore.yaml +22 -0
- package/api/openapi.yaml +13 -4
- package/coverage.md +2 -1
- package/dist/cli.js +235 -4
- package/dist/lib/paths.js +160 -0
- package/dist/lib/profiles.js +461 -0
- package/fixtures/conformance-agent-channel-dispatch.json +27 -0
- package/fixtures.md +15 -0
- package/package.json +1 -1
- package/schemas/README.md +1 -0
- package/schemas/capabilities.schema.json +5 -0
- package/schemas/conformance-certification-bundle.schema.json +86 -0
- package/src/cli.ts +268 -4
- package/src/lib/profiles.ts +85 -0
- package/src/scenarios/agent-channel-dispatch.test.ts +234 -0
- package/src/scenarios/spec-corpus-validity.test.ts +183 -0
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* openwop-conformance --offline # server-free subset only
|
|
12
12
|
* openwop-conformance --filter discovery # category filter
|
|
13
13
|
* openwop-conformance --base-url ... --api-key ... --filter "interrupt|cancellation"
|
|
14
|
+
* openwop-conformance --base-url ... --api-key ... --certify out.json # RFC 0089 bundle
|
|
14
15
|
*
|
|
15
16
|
* Environment variables override flags (per the conformance harness's
|
|
16
17
|
* existing convention):
|
|
@@ -24,7 +25,14 @@
|
|
|
24
25
|
*/
|
|
25
26
|
import { spawnSync } from 'node:child_process';
|
|
26
27
|
import { fileURLToPath } from 'node:url';
|
|
27
|
-
import { dirname, resolve as resolvePath } from 'node:path';
|
|
28
|
+
import { dirname, resolve as resolvePath, join } from 'node:path';
|
|
29
|
+
import { createHash } from 'node:crypto';
|
|
30
|
+
import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs';
|
|
31
|
+
import { tmpdir } from 'node:os';
|
|
32
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
33
|
+
import addFormats from 'ajv-formats';
|
|
34
|
+
import { SCHEMAS_DIR } from './lib/paths.js';
|
|
35
|
+
import { deriveProfiles, isCoreStandard, agentPlatformStatus, } from './lib/profiles.js';
|
|
28
36
|
function parseArgs(argv) {
|
|
29
37
|
let baseUrl;
|
|
30
38
|
let apiKey;
|
|
@@ -33,6 +41,7 @@ function parseArgs(argv) {
|
|
|
33
41
|
let help = false;
|
|
34
42
|
let impl;
|
|
35
43
|
let implVersion;
|
|
44
|
+
let certify;
|
|
36
45
|
for (let i = 0; i < argv.length; i++) {
|
|
37
46
|
const arg = argv[i] ?? '';
|
|
38
47
|
if (arg === '-h' || arg === '--help') {
|
|
@@ -74,13 +83,16 @@ function parseArgs(argv) {
|
|
|
74
83
|
case '--implementation-version':
|
|
75
84
|
implVersion = nextValue();
|
|
76
85
|
break;
|
|
86
|
+
case '--certify':
|
|
87
|
+
certify = nextValue();
|
|
88
|
+
break;
|
|
77
89
|
default:
|
|
78
90
|
if (arg.startsWith('-')) {
|
|
79
91
|
// Unknown flag — pass through to vitest by ignoring here.
|
|
80
92
|
}
|
|
81
93
|
}
|
|
82
94
|
}
|
|
83
|
-
return { baseUrl, apiKey, offline, filter, help, impl, implVersion };
|
|
95
|
+
return { baseUrl, apiKey, offline, filter, help, impl, implVersion, certify };
|
|
84
96
|
}
|
|
85
97
|
const HELP_TEXT = `openwop-conformance — run the openwop conformance suite against a server
|
|
86
98
|
|
|
@@ -99,6 +111,14 @@ Implementation labels (cosmetic — surface in failure messages):
|
|
|
99
111
|
--impl <name> Implementation name (env: OPENWOP_IMPLEMENTATION_NAME)
|
|
100
112
|
--impl-version <version> Implementation version (env: OPENWOP_IMPLEMENTATION_VERSION)
|
|
101
113
|
|
|
114
|
+
Certification (RFC 0089):
|
|
115
|
+
--certify <out.json> Generate a machine-readable conformance certification
|
|
116
|
+
bundle: fetch /.well-known/openwop (captured verbatim +
|
|
117
|
+
SHA-256), derive claimedProfiles from it, run the suite
|
|
118
|
+
recording each scenario's terminal state, validate the
|
|
119
|
+
assembled bundle against the bundle schema, and write it
|
|
120
|
+
to <out.json>. Requires --base-url (and --api-key as usual).
|
|
121
|
+
|
|
102
122
|
Other:
|
|
103
123
|
--help, -h Show this message
|
|
104
124
|
|
|
@@ -106,8 +126,210 @@ Examples:
|
|
|
106
126
|
openwop-conformance --offline
|
|
107
127
|
openwop-conformance --base-url https://api.example.com --api-key hk_test_abc
|
|
108
128
|
openwop-conformance --filter "discovery|errors"
|
|
129
|
+
openwop-conformance --base-url https://api.example.com --api-key hk_test_abc \\
|
|
130
|
+
--certify certification-bundle.json
|
|
109
131
|
`;
|
|
110
|
-
|
|
132
|
+
/** This CLI package's own version — surfaced as `generator.version` + `suite.version`. */
|
|
133
|
+
function suiteVersion() {
|
|
134
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
135
|
+
const pkgPath = resolvePath(here, '..', 'package.json');
|
|
136
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
137
|
+
return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Deterministic canonical-JSON serialization (RFC 8785 spirit): object keys
|
|
141
|
+
* sorted lexicographically at every level, arrays preserved in order. Used to
|
|
142
|
+
* compute `discovery.sha256` so a verifier can re-derive the same digest from
|
|
143
|
+
* a live `/.well-known/openwop` fetch regardless of incidental key order.
|
|
144
|
+
*/
|
|
145
|
+
function canonicalJSON(value) {
|
|
146
|
+
if (value === null || typeof value !== 'object')
|
|
147
|
+
return JSON.stringify(value);
|
|
148
|
+
if (Array.isArray(value))
|
|
149
|
+
return `[${value.map(canonicalJSON).join(',')}]`;
|
|
150
|
+
const obj = value;
|
|
151
|
+
const keys = Object.keys(obj).sort();
|
|
152
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJSON(obj[k])}`).join(',')}}`;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The full set of profiles a discovery document derives — the closed
|
|
156
|
+
* `deriveProfiles` catalog plus the two operational annexes
|
|
157
|
+
* (`openwop-core-standard`, `openwop-agent-platform`) when their discovery
|
|
158
|
+
* predicate holds. This is `claimedProfiles`: a generated bundle MUST NOT
|
|
159
|
+
* claim a profile its own discovery document does not derive (RFC 0089 §B(1)).
|
|
160
|
+
*/
|
|
161
|
+
function claimedProfilesFor(doc) {
|
|
162
|
+
const profiles = [...deriveProfiles(doc)];
|
|
163
|
+
if (isCoreStandard(doc))
|
|
164
|
+
profiles.push('openwop-core-standard');
|
|
165
|
+
if (agentPlatformStatus(doc) !== 'none')
|
|
166
|
+
profiles.push('openwop-agent-platform');
|
|
167
|
+
return profiles;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Reduce a vitest JSON report into a per-scenario-file terminal state, keyed by
|
|
171
|
+
* the test-file basename (e.g. `discovery.test.ts`) to align with the basenames
|
|
172
|
+
* in `PROFILE_FLOOR_SCENARIOS`. A file is `passed` only if it ran AND had ≥1
|
|
173
|
+
* passing assertion AND zero failures (non-vacuous, per §C); a fully-skipped
|
|
174
|
+
* file is `skipped`; any failed assertion makes the file `failed`.
|
|
175
|
+
*/
|
|
176
|
+
function scenarioStatesFromReport(report) {
|
|
177
|
+
const states = new Map();
|
|
178
|
+
for (const file of report.testResults ?? []) {
|
|
179
|
+
const name = file.name;
|
|
180
|
+
if (typeof name !== 'string')
|
|
181
|
+
continue;
|
|
182
|
+
const basename = name.split('/').pop() ?? name;
|
|
183
|
+
const assertions = file.assertionResults ?? [];
|
|
184
|
+
let passes = 0;
|
|
185
|
+
let failures = 0;
|
|
186
|
+
let nonSkipped = 0;
|
|
187
|
+
for (const a of assertions) {
|
|
188
|
+
if (a.status === 'passed') {
|
|
189
|
+
passes++;
|
|
190
|
+
nonSkipped++;
|
|
191
|
+
}
|
|
192
|
+
else if (a.status === 'failed') {
|
|
193
|
+
failures++;
|
|
194
|
+
nonSkipped++;
|
|
195
|
+
}
|
|
196
|
+
// `skipped` / `todo` / `pending` count toward neither pass nor fail.
|
|
197
|
+
}
|
|
198
|
+
let state;
|
|
199
|
+
if (failures > 0)
|
|
200
|
+
state = 'failed';
|
|
201
|
+
else if (passes > 0 && nonSkipped > 0)
|
|
202
|
+
state = 'passed';
|
|
203
|
+
else
|
|
204
|
+
state = 'skipped';
|
|
205
|
+
states.set(basename, state);
|
|
206
|
+
}
|
|
207
|
+
return states;
|
|
208
|
+
}
|
|
209
|
+
/** Generate + validate + write an RFC 0089 conformance certification bundle. */
|
|
210
|
+
async function runCertify(args, baseUrl, apiKey) {
|
|
211
|
+
const outPath = args.certify;
|
|
212
|
+
if (outPath === undefined)
|
|
213
|
+
process.exit(2);
|
|
214
|
+
// (a) Fetch /.well-known/openwop verbatim + its canonical-JSON SHA-256.
|
|
215
|
+
const discoveryUrl = `${baseUrl.replace(/\/$/, '')}/.well-known/openwop`;
|
|
216
|
+
let document;
|
|
217
|
+
try {
|
|
218
|
+
const resp = await fetch(discoveryUrl, { headers: { Accept: 'application/json' } });
|
|
219
|
+
if (!resp.ok) {
|
|
220
|
+
process.stderr.write(`openwop-conformance --certify: GET ${discoveryUrl} returned HTTP ${resp.status}.\n`);
|
|
221
|
+
process.exit(2);
|
|
222
|
+
}
|
|
223
|
+
document = (await resp.json());
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
process.stderr.write(`openwop-conformance --certify: failed to fetch ${discoveryUrl}: ${String(err)}\n`);
|
|
227
|
+
process.exit(2);
|
|
228
|
+
}
|
|
229
|
+
const sha256 = createHash('sha256').update(canonicalJSON(document)).digest('hex');
|
|
230
|
+
// (b) Derive claimedProfiles from the captured document.
|
|
231
|
+
const claimedProfiles = claimedProfilesFor(document);
|
|
232
|
+
// (c) Run the suite, capturing per-scenario terminal state via the vitest
|
|
233
|
+
// JSON reporter. server-targeted scenarios live under src/scenarios/.
|
|
234
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
235
|
+
const conformanceRoot = resolvePath(here, '..');
|
|
236
|
+
const reportDir = mkdtempSync(join(tmpdir(), 'owp-certify-'));
|
|
237
|
+
const reportFile = join(reportDir, 'vitest-report.json');
|
|
238
|
+
const env = { ...process.env };
|
|
239
|
+
env.OPENWOP_BASE_URL = baseUrl;
|
|
240
|
+
env.OPENWOP_API_KEY = apiKey;
|
|
241
|
+
if (args.impl)
|
|
242
|
+
env.OPENWOP_IMPLEMENTATION_NAME = args.impl;
|
|
243
|
+
if (args.implVersion)
|
|
244
|
+
env.OPENWOP_IMPLEMENTATION_VERSION = args.implVersion;
|
|
245
|
+
const vitestArgs = [
|
|
246
|
+
'vitest',
|
|
247
|
+
'run',
|
|
248
|
+
'--config',
|
|
249
|
+
resolvePath(conformanceRoot, 'vitest.config.ts'),
|
|
250
|
+
'--reporter=json',
|
|
251
|
+
`--outputFile=${reportFile}`,
|
|
252
|
+
];
|
|
253
|
+
const runResult = spawnSync('npx', vitestArgs, { cwd: conformanceRoot, env, stdio: 'inherit' });
|
|
254
|
+
if (runResult.error) {
|
|
255
|
+
process.stderr.write(`openwop-conformance --certify: failed to spawn vitest: ${String(runResult.error)}\n`);
|
|
256
|
+
process.exit(2);
|
|
257
|
+
}
|
|
258
|
+
let report;
|
|
259
|
+
try {
|
|
260
|
+
report = JSON.parse(readFileSync(reportFile, 'utf8'));
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
process.stderr.write(`openwop-conformance --certify: could not read vitest JSON report at ${reportFile}: ${String(err)}\n`);
|
|
264
|
+
process.exit(2);
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
rmSync(reportDir, { recursive: true, force: true });
|
|
268
|
+
}
|
|
269
|
+
const states = scenarioStatesFromReport(report);
|
|
270
|
+
const passed = [];
|
|
271
|
+
const failed = [];
|
|
272
|
+
const skipped = [];
|
|
273
|
+
for (const [basename, state] of [...states.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
274
|
+
if (state === 'passed')
|
|
275
|
+
passed.push(basename);
|
|
276
|
+
else if (state === 'failed')
|
|
277
|
+
failed.push(basename);
|
|
278
|
+
else
|
|
279
|
+
skipped.push(basename);
|
|
280
|
+
}
|
|
281
|
+
// (d) Assemble the bundle.
|
|
282
|
+
const version = suiteVersion();
|
|
283
|
+
const impl = document
|
|
284
|
+
.implementation;
|
|
285
|
+
const hostName = args.impl ?? (typeof impl?.name === 'string' ? impl.name : 'unknown-host');
|
|
286
|
+
const hostVersion = args.implVersion ?? (typeof impl?.version === 'string' ? impl.version : '0.0.0');
|
|
287
|
+
const host = {
|
|
288
|
+
name: hostName,
|
|
289
|
+
version: hostVersion,
|
|
290
|
+
};
|
|
291
|
+
if (typeof impl?.vendor === 'string')
|
|
292
|
+
host.vendor = impl.vendor;
|
|
293
|
+
const bundle = {
|
|
294
|
+
bundleVersion: '1',
|
|
295
|
+
generatedAt: new Date().toISOString(),
|
|
296
|
+
generator: { name: '@openwop/openwop-conformance --certify', version },
|
|
297
|
+
suite: { package: '@openwop/openwop-conformance', version },
|
|
298
|
+
host,
|
|
299
|
+
discovery: { url: discoveryUrl, sha256, document },
|
|
300
|
+
claimedProfiles,
|
|
301
|
+
results: {
|
|
302
|
+
totals: {
|
|
303
|
+
passed: passed.length,
|
|
304
|
+
failed: failed.length,
|
|
305
|
+
skipped: skipped.length,
|
|
306
|
+
total: passed.length + failed.length + skipped.length,
|
|
307
|
+
},
|
|
308
|
+
passed,
|
|
309
|
+
failed,
|
|
310
|
+
skipped,
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
// (e) Validate against the bundle schema BEFORE writing.
|
|
314
|
+
const schema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'conformance-certification-bundle.schema.json'), 'utf8'));
|
|
315
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
316
|
+
addFormats(ajv);
|
|
317
|
+
const validate = ajv.compile(schema);
|
|
318
|
+
if (!validate(bundle)) {
|
|
319
|
+
process.stderr.write('openwop-conformance --certify: assembled bundle FAILED schema validation:\n' +
|
|
320
|
+
`${JSON.stringify(validate.errors, null, 2)}\n`);
|
|
321
|
+
process.exit(2);
|
|
322
|
+
}
|
|
323
|
+
writeFileSync(outPath, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
324
|
+
process.stdout.write(`openwop-conformance --certify: wrote certification bundle to ${outPath}\n` +
|
|
325
|
+
` host: ${host.name}@${host.version}\n` +
|
|
326
|
+
` claimedProfiles: ${claimedProfiles.length > 0 ? claimedProfiles.join(', ') : '(none)'}\n` +
|
|
327
|
+
` results: ${passed.length} passed / ${failed.length} failed / ${skipped.length} skipped\n`);
|
|
328
|
+
// Exit code mirrors the suite outcome: a failing run still produces a bundle
|
|
329
|
+
// (the failures are recorded), but the process exit reflects pass/fail.
|
|
330
|
+
process.exit(failed.length > 0 ? 1 : 0);
|
|
331
|
+
}
|
|
332
|
+
async function main() {
|
|
111
333
|
const args = parseArgs(process.argv.slice(2));
|
|
112
334
|
if (args.help) {
|
|
113
335
|
process.stdout.write(HELP_TEXT);
|
|
@@ -124,6 +346,15 @@ function main() {
|
|
|
124
346
|
env.OPENWOP_IMPLEMENTATION_NAME = args.impl;
|
|
125
347
|
if (args.implVersion)
|
|
126
348
|
env.OPENWOP_IMPLEMENTATION_VERSION = args.implVersion;
|
|
349
|
+
// RFC 0089 — certification-bundle generation requires a live host.
|
|
350
|
+
if (args.certify !== undefined) {
|
|
351
|
+
if (!env.OPENWOP_BASE_URL || !env.OPENWOP_API_KEY) {
|
|
352
|
+
process.stderr.write('openwop-conformance --certify: --base-url and --api-key are required.\n' +
|
|
353
|
+
'Run `openwop-conformance --help` for usage.\n');
|
|
354
|
+
process.exit(2);
|
|
355
|
+
}
|
|
356
|
+
return runCertify(args, env.OPENWOP_BASE_URL, env.OPENWOP_API_KEY);
|
|
357
|
+
}
|
|
127
358
|
if (!args.offline && (!env.OPENWOP_BASE_URL || !env.OPENWOP_API_KEY)) {
|
|
128
359
|
process.stderr.write('openwop-conformance: --base-url and --api-key are required (or use --offline).\n' +
|
|
129
360
|
'Run `openwop-conformance --help` for usage.\n');
|
|
@@ -158,4 +389,4 @@ function main() {
|
|
|
158
389
|
}
|
|
159
390
|
process.exit(result.status ?? 1);
|
|
160
391
|
}
|
|
161
|
-
main();
|
|
392
|
+
void main();
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout-aware path resolver for the offline subset.
|
|
3
|
+
*
|
|
4
|
+
* The same suite source runs in two layouts:
|
|
5
|
+
*
|
|
6
|
+
* 1. Repo checkout — `openwop/conformance/src/scenarios/X.test.ts`. Schemas,
|
|
7
|
+
* api/, and prose docs live one level above the conformance package
|
|
8
|
+
* at the repo root.
|
|
9
|
+
*
|
|
10
|
+
* 2. Published tarball — `node_modules/@openwop/openwop-conformance/src/...`.
|
|
11
|
+
* The `prepack` script vendors `api/` and `schemas/` INTO the package,
|
|
12
|
+
* so they resolve relative to the package root instead of a parent.
|
|
13
|
+
* Spec prose (`spec/v1/*.md`) is NOT bundled — those tests skip.
|
|
14
|
+
*
|
|
15
|
+
* Earlier offline scenarios computed `__dirname/../../..` to find
|
|
16
|
+
* the repo root. That works in a checkout but lands in `node_modules/@openwop/`
|
|
17
|
+
* after npx-style install, breaking `npx -y @openwop/openwop-conformance --offline`
|
|
18
|
+
* with `ENOENT: ... node_modules/@openwop/schemas/workflow-definition.schema.json`.
|
|
19
|
+
*
|
|
20
|
+
* This module centralises the resolution. Strategy:
|
|
21
|
+
*
|
|
22
|
+
* - If `OPENWOP_CONFORMANCE_ROOT` is set, treat its value as the layout root
|
|
23
|
+
* (the directory that contains `schemas/`, `api/`, and either
|
|
24
|
+
* `conformance/fixtures/` (repo) or `fixtures/` directly (vendored)).
|
|
25
|
+
* Used by integrators who put the suite in an unusual location.
|
|
26
|
+
*
|
|
27
|
+
* - Otherwise compute the package root from `import.meta.url` (= the
|
|
28
|
+
* directory containing `package.json`) and probe whether the schemas
|
|
29
|
+
* are vendored at the package root (published) or at the parent (repo).
|
|
30
|
+
*
|
|
31
|
+
* Exported paths are non-null for the materials always present in both
|
|
32
|
+
* layouts; the prose-doc and fixtures.md catalog dirs may resolve to
|
|
33
|
+
* `null` under the published layout, in which case the corresponding
|
|
34
|
+
* scenarios skip cleanly (see `spec-corpus-validity.test.ts`).
|
|
35
|
+
*/
|
|
36
|
+
import { existsSync } from 'node:fs';
|
|
37
|
+
import { fileURLToPath } from 'node:url';
|
|
38
|
+
import { dirname, join, resolve as pathResolve } from 'node:path';
|
|
39
|
+
// `dirname(fileURLToPath(import.meta.url))` for an ESM module compiled or
|
|
40
|
+
// run from `src/lib/paths.ts` returns `<pkg>/src/lib/`. The conformance
|
|
41
|
+
// package root is therefore two directories above this file in BOTH the
|
|
42
|
+
// repo checkout and the published tarball — the source layout is
|
|
43
|
+
// identical between the two; only the parent of `<pkg>` differs.
|
|
44
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
45
|
+
const PKG_ROOT = pathResolve(HERE, '..', '..');
|
|
46
|
+
function resolveFromRoot(root, layout) {
|
|
47
|
+
// Two on-disk shapes for the layout root:
|
|
48
|
+
// - Repo: <root>/schemas, <root>/api, <root>/conformance/fixtures,
|
|
49
|
+
// <root>/conformance/{fixtures.md,coverage.md}, <root>/spec/v1/*.md
|
|
50
|
+
// (Where `<root>` = the repo root, e.g. `openwop/`.)
|
|
51
|
+
// - Vendored / published: <root>/schemas, <root>/api, <root>/fixtures,
|
|
52
|
+
// <root>/fixtures.md (when bundled), no spec/v1.
|
|
53
|
+
// Probe by checking whether `schemas/` lives at the conformance pkg root
|
|
54
|
+
// (vendored) vs one level up (repo).
|
|
55
|
+
const schemasDir = join(root, 'schemas');
|
|
56
|
+
const apiDir = join(root, 'api');
|
|
57
|
+
const repoFixturesDir = join(root, 'conformance', 'fixtures');
|
|
58
|
+
const vendoredFixturesDir = join(root, 'fixtures');
|
|
59
|
+
const fixturesDir = existsSync(repoFixturesDir) ? repoFixturesDir : vendoredFixturesDir;
|
|
60
|
+
const repoScenariosDir = join(root, 'conformance', 'src', 'scenarios');
|
|
61
|
+
const vendoredScenariosDir = join(PKG_ROOT, 'src', 'scenarios');
|
|
62
|
+
const scenariosDir = existsSync(repoScenariosDir)
|
|
63
|
+
? repoScenariosDir
|
|
64
|
+
: existsSync(vendoredScenariosDir)
|
|
65
|
+
? vendoredScenariosDir
|
|
66
|
+
: null;
|
|
67
|
+
const repoConformanceReadme = join(root, 'conformance', 'README.md');
|
|
68
|
+
const vendoredConformanceReadme = join(PKG_ROOT, 'README.md');
|
|
69
|
+
const conformanceReadmePath = existsSync(repoConformanceReadme)
|
|
70
|
+
? repoConformanceReadme
|
|
71
|
+
: existsSync(vendoredConformanceReadme)
|
|
72
|
+
? vendoredConformanceReadme
|
|
73
|
+
: null;
|
|
74
|
+
const repoFixturesDoc = join(root, 'conformance', 'fixtures.md');
|
|
75
|
+
const vendoredFixturesDoc = join(root, 'fixtures.md');
|
|
76
|
+
const fixturesDocPath = existsSync(repoFixturesDoc)
|
|
77
|
+
? repoFixturesDoc
|
|
78
|
+
: existsSync(vendoredFixturesDoc)
|
|
79
|
+
? vendoredFixturesDoc
|
|
80
|
+
: null;
|
|
81
|
+
const repoCoverageDoc = join(root, 'conformance', 'coverage.md');
|
|
82
|
+
const vendoredCoverageDoc = join(root, 'coverage.md');
|
|
83
|
+
const coverageDocPath = existsSync(repoCoverageDoc)
|
|
84
|
+
? repoCoverageDoc
|
|
85
|
+
: existsSync(vendoredCoverageDoc)
|
|
86
|
+
? vendoredCoverageDoc
|
|
87
|
+
: null;
|
|
88
|
+
const v1Probe = join(root, 'spec', 'v1');
|
|
89
|
+
const v1Dir = existsSync(v1Probe) ? v1Probe : null;
|
|
90
|
+
const readmeProbe = join(root, 'README.md');
|
|
91
|
+
const readmePath = existsSync(readmeProbe) ? readmeProbe : null;
|
|
92
|
+
const typescriptRunHelpersProbe = join(root, 'sdk', 'typescript', 'src', 'run-helpers.ts');
|
|
93
|
+
const typescriptRunHelpersPath = existsSync(typescriptRunHelpersProbe) ? typescriptRunHelpersProbe : null;
|
|
94
|
+
const pythonTypesProbe = join(root, 'sdk', 'python', 'src', 'openwop_client', 'types.py');
|
|
95
|
+
const pythonTypesPath = existsSync(pythonTypesProbe) ? pythonTypesProbe : null;
|
|
96
|
+
const goTypesProbe = join(root, 'sdk', 'go', 'types.go');
|
|
97
|
+
const goTypesPath = existsSync(goTypesProbe) ? goTypesProbe : null;
|
|
98
|
+
return {
|
|
99
|
+
pkgRoot: PKG_ROOT,
|
|
100
|
+
schemasDir,
|
|
101
|
+
apiDir,
|
|
102
|
+
fixturesDir,
|
|
103
|
+
scenariosDir,
|
|
104
|
+
conformanceReadmePath,
|
|
105
|
+
fixturesDocPath,
|
|
106
|
+
coverageDocPath,
|
|
107
|
+
v1Dir,
|
|
108
|
+
readmePath,
|
|
109
|
+
typescriptRunHelpersPath,
|
|
110
|
+
pythonTypesPath,
|
|
111
|
+
goTypesPath,
|
|
112
|
+
layout,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function resolveLayout() {
|
|
116
|
+
const override = process.env.OPENWOP_CONFORMANCE_ROOT?.trim();
|
|
117
|
+
if (override && override.length > 0) {
|
|
118
|
+
return resolveFromRoot(pathResolve(override), 'env-override');
|
|
119
|
+
}
|
|
120
|
+
// Vendored / published-tarball layout: `prepack` copies `schemas/` +
|
|
121
|
+
// `api/` to the package root. Repo layout: schemas live one level
|
|
122
|
+
// above the conformance package.
|
|
123
|
+
//
|
|
124
|
+
// Edge case: a developer running `npm pack` locally without a
|
|
125
|
+
// postpack cleanup leaves schemas/ in BOTH places transiently. When
|
|
126
|
+
// both exist, prefer the parent (repo layout) so prose-doc tests
|
|
127
|
+
// continue to run — the parent is the canonical source.
|
|
128
|
+
const parent = pathResolve(PKG_ROOT, '..');
|
|
129
|
+
const parentHasSchemas = existsSync(join(parent, 'schemas'));
|
|
130
|
+
const pkgHasSchemas = existsSync(join(PKG_ROOT, 'schemas'));
|
|
131
|
+
if (parentHasSchemas) {
|
|
132
|
+
return resolveFromRoot(parent, 'repo');
|
|
133
|
+
}
|
|
134
|
+
if (pkgHasSchemas) {
|
|
135
|
+
return resolveFromRoot(PKG_ROOT, 'published');
|
|
136
|
+
}
|
|
137
|
+
// Neither — return the published-style resolution rooted at PKG_ROOT
|
|
138
|
+
// so error messages name a concrete directory rather than a
|
|
139
|
+
// computed-from-undefined path.
|
|
140
|
+
return resolveFromRoot(PKG_ROOT, 'published');
|
|
141
|
+
}
|
|
142
|
+
const _layout = resolveLayout();
|
|
143
|
+
export const PKG_ROOT_PATH = _layout.pkgRoot;
|
|
144
|
+
export const SCHEMAS_DIR = _layout.schemasDir;
|
|
145
|
+
export const API_DIR = _layout.apiDir;
|
|
146
|
+
export const FIXTURES_DIR = _layout.fixturesDir;
|
|
147
|
+
export const SCENARIOS_DIR = _layout.scenariosDir;
|
|
148
|
+
export const CONFORMANCE_README_PATH = _layout.conformanceReadmePath;
|
|
149
|
+
export const FIXTURES_DOC_PATH = _layout.fixturesDocPath;
|
|
150
|
+
export const COVERAGE_DOC_PATH = _layout.coverageDocPath;
|
|
151
|
+
export const V1_DIR = _layout.v1Dir;
|
|
152
|
+
export const README_PATH = _layout.readmePath;
|
|
153
|
+
export const TYPESCRIPT_RUN_HELPERS_PATH = _layout.typescriptRunHelpersPath;
|
|
154
|
+
export const PYTHON_TYPES_PATH = _layout.pythonTypesPath;
|
|
155
|
+
export const GO_TYPES_PATH = _layout.goTypesPath;
|
|
156
|
+
export const LAYOUT = _layout.layout;
|
|
157
|
+
/** Test-only — re-resolve in case env var or filesystem changed. */
|
|
158
|
+
export function __resolveLayoutForTests() {
|
|
159
|
+
return resolveLayout();
|
|
160
|
+
}
|