@hypequery/protocol-conformance 0.0.0-canary-20260722115402
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/LICENSE +201 -0
- package/README.md +40 -0
- package/dist/adapters/generators.d.ts +13 -0
- package/dist/adapters/generators.d.ts.map +1 -0
- package/dist/adapters/generators.js +366 -0
- package/dist/adapters/reference.d.ts +6 -0
- package/dist/adapters/reference.d.ts.map +1 -0
- package/dist/adapters/reference.js +172 -0
- package/dist/adapters/stdio.d.ts +17 -0
- package/dist/adapters/stdio.d.ts.map +1 -0
- package/dist/adapters/stdio.js +80 -0
- package/dist/bin/conformance.d.ts +3 -0
- package/dist/bin/conformance.d.ts.map +1 -0
- package/dist/bin/conformance.js +84 -0
- package/dist/bin/reference-adapter.d.ts +3 -0
- package/dist/bin/reference-adapter.d.ts.map +1 -0
- package/dist/bin/reference-adapter.js +16 -0
- package/dist/compare.d.ts +3 -0
- package/dist/compare.d.ts.map +1 -0
- package/dist/compare.js +125 -0
- package/dist/fs.d.ts +11 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +36 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/manifest.d.ts +13 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +118 -0
- package/dist/report.d.ts +4 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +23 -0
- package/dist/runner.d.ts +12 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +197 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/fixtures/deployment-bundles-v1/README.md +61 -0
- package/fixtures/deployment-bundles-v1/identity.json +7 -0
- package/fixtures/deployment-bundles-v1/rejections.json +11 -0
- package/fixtures/deployment-bundles-v1/success.json +23 -0
- package/fixtures/deployment-releases-v1/README.md +36 -0
- package/fixtures/deployment-releases-v1/identity.json +7 -0
- package/fixtures/deployment-releases-v1/rejections.json +8 -0
- package/fixtures/deployment-releases-v1/success.json +14 -0
- package/fixtures/deployments-v1/README.md +77 -0
- package/fixtures/deployments-v1/identity.json +7 -0
- package/fixtures/deployments-v1/rejections.json +12 -0
- package/fixtures/deployments-v1/success.json +81 -0
- package/fixtures/expressions-v1/README.md +25 -0
- package/fixtures/expressions-v1/rejections.json +17 -0
- package/fixtures/expressions-v1/success.json +47 -0
- package/fixtures/fuzz-seeds-v1/README.md +29 -0
- package/fixtures/fuzz-seeds-v1/sql-expressions.json +32 -0
- package/fixtures/fuzz-seeds-v1/structured-values.json +32 -0
- package/fixtures/fuzz-seeds-v1/value-sources.json +191 -0
- package/fixtures/identifiers-v1/README.md +15 -0
- package/fixtures/identifiers-v1/rejections.json +58 -0
- package/fixtures/identifiers-v1/success.json +26 -0
- package/fixtures/manifest.json +117 -0
- package/fixtures/query-diagnostics-v1/README.md +49 -0
- package/fixtures/query-diagnostics-v1/rejections.json +13 -0
- package/fixtures/query-diagnostics-v1/success.json +27 -0
- package/fixtures/query-events-v1/README.md +60 -0
- package/fixtures/query-events-v1/rejections.json +18 -0
- package/fixtures/query-events-v1/success.json +40 -0
- package/fixtures/query-implementations-v1/README.md +33 -0
- package/fixtures/query-implementations-v1/rejections.json +80 -0
- package/fixtures/query-implementations-v1/success.json +96 -0
- package/fixtures/query-schemas-v1/README.md +25 -0
- package/fixtures/query-schemas-v1/rejections.json +15 -0
- package/fixtures/query-schemas-v1/success.json +17 -0
- package/fixtures/sql-portability-v1/README.md +15 -0
- package/fixtures/sql-portability-v1/non-portable.json +146 -0
- package/fixtures/sql-portability-v1/portable.json +346 -0
- package/fixtures/tagged-values-v1/README.md +44 -0
- package/fixtures/tagged-values-v1/rejections.json +298 -0
- package/fixtures/tagged-values-v1/success.json +262 -0
- package/package.json +52 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// The TypeScript reference adapter. Maps each family/role/case to the public
|
|
2
|
+
// root export of @hypequery/protocol and returns a wire result. Every operation
|
|
3
|
+
// goes through the package's supported surface — this is exactly what a second
|
|
4
|
+
// implementation must reproduce.
|
|
5
|
+
import { decodeCanonicalValue, encodeCanonicalValue, hashCanonicalValue, parseProtocolIdentifier, parseProtocolQualifiedIdentifier, prepareProtocolDeploymentBundleManifest, prepareProtocolDeploymentContract, prepareProtocolDeploymentReleaseEnvelope, splitProtocolQualifiedIdentifier, validateCanonicalValue, validateProtocolDeploymentBundleManifest, validateProtocolDeploymentContract, validateProtocolDeploymentReleaseEnvelope, validateProtocolExpression, validateProtocolQueryDiagnostics, validateProtocolQueryEvent, validateProtocolQueryImplementation, validateProtocolSchema, validateProtocolSemanticQuery, validateProtocolSqlExpression, } from '@hypequery/protocol';
|
|
6
|
+
import { materializeBundle, materializeDeployment, materializeDiagnostics, materializeEvent, materializeExpression, materializeIdentifier, materializeImplementation, materializeRelease, materializeSchema, materializeTaggedValue, } from './generators.js';
|
|
7
|
+
const STABLE_CODE = /^HQ_[A-Z0-9_]+$/;
|
|
8
|
+
/** Turns a thrown protocol error into a wire rejection; rethrows anything else. */
|
|
9
|
+
function mapError(error) {
|
|
10
|
+
const code = error?.code;
|
|
11
|
+
if (typeof code === 'string' && STABLE_CODE.test(code))
|
|
12
|
+
return { ok: false, code };
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
function attempt(fn) {
|
|
16
|
+
try {
|
|
17
|
+
return fn();
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return mapError(error);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const ACCEPT = { ok: true };
|
|
24
|
+
/** The input a validation case feeds to its validator. */
|
|
25
|
+
function validationInput(c, materialize) {
|
|
26
|
+
return c.generator ? materialize(c.generator) : c.value;
|
|
27
|
+
}
|
|
28
|
+
export const REFERENCE_FAMILIES = [
|
|
29
|
+
'tagged-values-v1',
|
|
30
|
+
'identifiers-v1',
|
|
31
|
+
'expressions-v1',
|
|
32
|
+
'query-schemas-v1',
|
|
33
|
+
'query-implementations-v1',
|
|
34
|
+
'query-events-v1',
|
|
35
|
+
'query-diagnostics-v1',
|
|
36
|
+
'deployments-v1',
|
|
37
|
+
'deployment-bundles-v1',
|
|
38
|
+
'deployment-releases-v1',
|
|
39
|
+
];
|
|
40
|
+
export function referenceHandle(family, role, c, section) {
|
|
41
|
+
switch (family) {
|
|
42
|
+
case 'tagged-values-v1':
|
|
43
|
+
return handleTaggedValue(role, c);
|
|
44
|
+
case 'identifiers-v1':
|
|
45
|
+
return handleIdentifier(role, c);
|
|
46
|
+
case 'expressions-v1':
|
|
47
|
+
return handleExpression(role, c, section);
|
|
48
|
+
case 'query-schemas-v1':
|
|
49
|
+
return attempt(() => {
|
|
50
|
+
validateProtocolSchema(validationInput(c, materializeSchema));
|
|
51
|
+
return ACCEPT;
|
|
52
|
+
});
|
|
53
|
+
case 'query-implementations-v1':
|
|
54
|
+
return handleImplementation(c);
|
|
55
|
+
case 'query-events-v1':
|
|
56
|
+
return attempt(() => {
|
|
57
|
+
validateProtocolQueryEvent(validationInput(c, materializeEvent));
|
|
58
|
+
return ACCEPT;
|
|
59
|
+
});
|
|
60
|
+
case 'query-diagnostics-v1':
|
|
61
|
+
return attempt(() => {
|
|
62
|
+
validateProtocolQueryDiagnostics(validationInput(c, materializeDiagnostics));
|
|
63
|
+
return ACCEPT;
|
|
64
|
+
});
|
|
65
|
+
case 'deployments-v1':
|
|
66
|
+
return handleDeployment(role, c);
|
|
67
|
+
case 'deployment-bundles-v1':
|
|
68
|
+
return handleBundle(role, c);
|
|
69
|
+
case 'deployment-releases-v1':
|
|
70
|
+
return handleRelease(role, c);
|
|
71
|
+
default:
|
|
72
|
+
throw new Error(`reference adapter does not support family ${family}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function handleTaggedValue(role, c) {
|
|
76
|
+
if (role === 'success') {
|
|
77
|
+
return attempt(() => ({
|
|
78
|
+
ok: true,
|
|
79
|
+
output: {
|
|
80
|
+
canonicalHex: Buffer.from(encodeCanonicalValue(c.value)).toString('hex'),
|
|
81
|
+
sha256: hashCanonicalValue(c.value),
|
|
82
|
+
},
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
return attempt(() => {
|
|
86
|
+
if (c.sourceUtf8 !== undefined) {
|
|
87
|
+
decodeCanonicalValue(c.sourceUtf8);
|
|
88
|
+
return ACCEPT;
|
|
89
|
+
}
|
|
90
|
+
const input = c.generator ? materializeTaggedValue(c.generator) : c.value;
|
|
91
|
+
validateCanonicalValue(input, { declaredClickHouseType: c.declaredClickHouseType });
|
|
92
|
+
return ACCEPT;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function handleIdentifier(role, c) {
|
|
96
|
+
const simple = c.mode === 'simple';
|
|
97
|
+
if (role === 'success') {
|
|
98
|
+
return attempt(() => {
|
|
99
|
+
const segments = simple
|
|
100
|
+
? [parseProtocolIdentifier(c.value)]
|
|
101
|
+
: splitProtocolQualifiedIdentifier(parseProtocolQualifiedIdentifier(c.value));
|
|
102
|
+
return { ok: true, output: { segments } };
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return attempt(() => {
|
|
106
|
+
const input = c.generator ? materializeIdentifier(c.generator) : c.value;
|
|
107
|
+
if (simple)
|
|
108
|
+
parseProtocolIdentifier(input);
|
|
109
|
+
else
|
|
110
|
+
parseProtocolQualifiedIdentifier(input);
|
|
111
|
+
return ACCEPT;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function handleExpression(role, c, section) {
|
|
115
|
+
const isQuery = section === '/queries' || c.mode === 'query';
|
|
116
|
+
const validate = isQuery ? validateProtocolSemanticQuery : validateProtocolExpression;
|
|
117
|
+
if (role === 'success') {
|
|
118
|
+
return attempt(() => {
|
|
119
|
+
validate(c.value);
|
|
120
|
+
return ACCEPT;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return attempt(() => {
|
|
124
|
+
validate(validationInput(c, materializeExpression));
|
|
125
|
+
return ACCEPT;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function handleImplementation(c) {
|
|
129
|
+
const validate = c.surface === 'sql-expression'
|
|
130
|
+
? validateProtocolSqlExpression
|
|
131
|
+
: validateProtocolQueryImplementation;
|
|
132
|
+
return attempt(() => {
|
|
133
|
+
validate(validationInput(c, materializeImplementation));
|
|
134
|
+
return ACCEPT;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function handleDeployment(role, c) {
|
|
138
|
+
if (role === 'identity') {
|
|
139
|
+
return attempt(() => {
|
|
140
|
+
const prepared = prepareProtocolDeploymentContract(c.value);
|
|
141
|
+
return { ok: true, output: { canonical: prepared.canonical, sha256: prepared.identity } };
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return attempt(() => {
|
|
145
|
+
validateProtocolDeploymentContract(validationInput(c, materializeDeployment));
|
|
146
|
+
return ACCEPT;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function handleBundle(role, c) {
|
|
150
|
+
if (role === 'identity') {
|
|
151
|
+
return attempt(() => {
|
|
152
|
+
const prepared = prepareProtocolDeploymentBundleManifest(c.value);
|
|
153
|
+
return { ok: true, output: { canonical: prepared.canonical, sha256: prepared.identity } };
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return attempt(() => {
|
|
157
|
+
validateProtocolDeploymentBundleManifest(validationInput(c, materializeBundle));
|
|
158
|
+
return ACCEPT;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function handleRelease(role, c) {
|
|
162
|
+
if (role === 'identity') {
|
|
163
|
+
return attempt(() => {
|
|
164
|
+
const prepared = prepareProtocolDeploymentReleaseEnvelope(c.value);
|
|
165
|
+
return { ok: true, output: { canonical: prepared.canonical, sha256: prepared.identity } };
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return attempt(() => {
|
|
169
|
+
validateProtocolDeploymentReleaseEnvelope(validationInput(c, materializeRelease));
|
|
170
|
+
return ACCEPT;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FixtureRole, HandlerResult } from '../types.js';
|
|
2
|
+
export interface StdioAdapterOptions {
|
|
3
|
+
readonly implementation?: string;
|
|
4
|
+
readonly version?: string;
|
|
5
|
+
readonly language?: string;
|
|
6
|
+
readonly families: readonly string[];
|
|
7
|
+
readonly handle: (family: string, role: FixtureRole, fixtureCase: Record<string, unknown>, section: string | undefined) => HandlerResult | Promise<HandlerResult>;
|
|
8
|
+
/** Streams default to the process stdio; overridable for tests. */
|
|
9
|
+
readonly input?: NodeJS.ReadableStream;
|
|
10
|
+
readonly output?: NodeJS.WritableStream;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Runs the adapter loop until the runner sends `end` or the input closes.
|
|
14
|
+
* Resolves with the process exit code the caller should use.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createStdioAdapter(options: StdioAdapterOptions): Promise<number>;
|
|
17
|
+
//# sourceMappingURL=stdio.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../src/adapters/stdio.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE9D,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,CACf,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,WAAW,EACjB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,OAAO,EAAE,MAAM,GAAG,SAAS,KACxB,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5C,mEAAmE;IACnE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;CACzC;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CA6EhF"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Reusable NDJSON adapter loop. An adapter wraps an implementation under test:
|
|
2
|
+
// it announces the families it supports, then answers one result line per case
|
|
3
|
+
// the runner sends. Used by the reference adapter and the datasets adapter.
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
import { CONFORMANCE_PROTOCOL_VERSION } from '../types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Runs the adapter loop until the runner sends `end` or the input closes.
|
|
8
|
+
* Resolves with the process exit code the caller should use.
|
|
9
|
+
*/
|
|
10
|
+
export function createStdioAdapter(options) {
|
|
11
|
+
const input = options.input ?? process.stdin;
|
|
12
|
+
const output = options.output ?? process.stdout;
|
|
13
|
+
const families = [...options.families];
|
|
14
|
+
const write = (message) => {
|
|
15
|
+
output.write(`${JSON.stringify(message)}\n`);
|
|
16
|
+
};
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
19
|
+
// Serializes async handlers so results are emitted in the order received.
|
|
20
|
+
let chain = Promise.resolve();
|
|
21
|
+
let exitCode = 0;
|
|
22
|
+
const finish = () => {
|
|
23
|
+
rl.close();
|
|
24
|
+
resolve(exitCode);
|
|
25
|
+
};
|
|
26
|
+
rl.on('line', (line) => {
|
|
27
|
+
const trimmed = line.trim();
|
|
28
|
+
if (trimmed === '')
|
|
29
|
+
return;
|
|
30
|
+
let message;
|
|
31
|
+
try {
|
|
32
|
+
message = JSON.parse(trimmed);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
process.stderr.write(`adapter: unparseable line\n`);
|
|
36
|
+
exitCode = 2;
|
|
37
|
+
finish();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (message.type === 'hello') {
|
|
41
|
+
if (message.protocol !== CONFORMANCE_PROTOCOL_VERSION) {
|
|
42
|
+
process.stderr.write(`adapter: unsupported protocol ${String(message.protocol)}\n`);
|
|
43
|
+
exitCode = 2;
|
|
44
|
+
finish();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
write({
|
|
48
|
+
type: 'hello',
|
|
49
|
+
protocol: CONFORMANCE_PROTOCOL_VERSION,
|
|
50
|
+
implementation: options.implementation,
|
|
51
|
+
version: options.version,
|
|
52
|
+
language: options.language,
|
|
53
|
+
families,
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (message.type === 'end') {
|
|
58
|
+
chain = chain.then(finish);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (message.type === 'case') {
|
|
62
|
+
const seq = message.seq;
|
|
63
|
+
const family = message.family;
|
|
64
|
+
const role = message.role;
|
|
65
|
+
const fixtureCase = message.case;
|
|
66
|
+
const section = message.section;
|
|
67
|
+
chain = chain.then(async () => {
|
|
68
|
+
const result = await options.handle(family, role, fixtureCase, section);
|
|
69
|
+
write({ type: 'result', seq, ...result });
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
process.stderr.write(`adapter: unknown message type ${String(message.type)}\n`);
|
|
74
|
+
});
|
|
75
|
+
input.on('error', () => {
|
|
76
|
+
exitCode = 2;
|
|
77
|
+
finish();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../../src/bin/conformance.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Conformance runner CLI.
|
|
3
|
+
//
|
|
4
|
+
// hypequery-protocol-conformance run [options] -- <adapter command...>
|
|
5
|
+
// hypequery-protocol-conformance list [--fixtures <dir>]
|
|
6
|
+
//
|
|
7
|
+
// Everything after `--` is the adapter argv, spawned without a shell.
|
|
8
|
+
import { createJsonLoader, loadManifest, resolveFixturesDir } from '../fs.js';
|
|
9
|
+
import { enumerateAllCases } from '../manifest.js';
|
|
10
|
+
import { formatJsonReport, formatPrettyReport } from '../report.js';
|
|
11
|
+
import { runConformance } from '../runner.js';
|
|
12
|
+
function parseArgs(argv) {
|
|
13
|
+
const separator = argv.indexOf('--');
|
|
14
|
+
const head = separator === -1 ? argv : argv.slice(0, separator);
|
|
15
|
+
const adapterCommand = separator === -1 ? [] : argv.slice(separator + 1);
|
|
16
|
+
const command = head[0] && !head[0].startsWith('--') ? head[0] : undefined;
|
|
17
|
+
const options = {};
|
|
18
|
+
for (let i = command ? 1 : 0; i < head.length; i += 1) {
|
|
19
|
+
const token = head[i];
|
|
20
|
+
if (!token.startsWith('--'))
|
|
21
|
+
continue;
|
|
22
|
+
const key = token.slice(2);
|
|
23
|
+
const next = head[i + 1];
|
|
24
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
25
|
+
options[key] = next;
|
|
26
|
+
i += 1;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
options[key] = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { command, options, adapterCommand };
|
|
33
|
+
}
|
|
34
|
+
async function main() {
|
|
35
|
+
const { command, options, adapterCommand } = parseArgs(process.argv.slice(2));
|
|
36
|
+
const fixturesDir = typeof options.fixtures === 'string' ? options.fixtures : undefined;
|
|
37
|
+
if (command === 'list') {
|
|
38
|
+
const dir = resolveFixturesDir(fixturesDir);
|
|
39
|
+
const manifest = loadManifest(dir);
|
|
40
|
+
const cases = enumerateAllCases(manifest, createJsonLoader(dir));
|
|
41
|
+
const counts = new Map();
|
|
42
|
+
for (const c of cases)
|
|
43
|
+
counts.set(c.family, (counts.get(c.family) ?? 0) + 1);
|
|
44
|
+
for (const [family, count] of counts)
|
|
45
|
+
process.stdout.write(`${family}\t${count}\n`);
|
|
46
|
+
process.stdout.write(`total\t${cases.length}\n`);
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
if (command !== 'run') {
|
|
50
|
+
process.stderr.write('usage: hypequery-protocol-conformance run [options] -- <adapter command...>\n');
|
|
51
|
+
return 2;
|
|
52
|
+
}
|
|
53
|
+
if (adapterCommand.length === 0) {
|
|
54
|
+
process.stderr.write('error: no adapter command given after `--`\n');
|
|
55
|
+
return 2;
|
|
56
|
+
}
|
|
57
|
+
const families = typeof options.families === 'string'
|
|
58
|
+
? options.families.split(',').map((f) => f.trim()).filter(Boolean)
|
|
59
|
+
: undefined;
|
|
60
|
+
let summary;
|
|
61
|
+
try {
|
|
62
|
+
summary = await runConformance({
|
|
63
|
+
adapterCommand,
|
|
64
|
+
fixturesDir,
|
|
65
|
+
families,
|
|
66
|
+
timeoutMs: typeof options['timeout-ms'] === 'string' ? Number(options['timeout-ms']) : undefined,
|
|
67
|
+
skipFuzz: options['skip-fuzz'] === true,
|
|
68
|
+
onlyFuzz: options['only-fuzz'] === true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
process.stderr.write(`conformance run failed: ${String(error)}\n`);
|
|
73
|
+
return 2;
|
|
74
|
+
}
|
|
75
|
+
const report = options.report === 'json' ? formatJsonReport(summary) : formatPrettyReport(summary);
|
|
76
|
+
process.stdout.write(`${report}\n`);
|
|
77
|
+
return summary.failed > 0 ? 1 : 0;
|
|
78
|
+
}
|
|
79
|
+
main()
|
|
80
|
+
.then((code) => process.exit(code))
|
|
81
|
+
.catch((error) => {
|
|
82
|
+
process.stderr.write(`${String(error)}\n`);
|
|
83
|
+
process.exit(2);
|
|
84
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reference-adapter.d.ts","sourceRoot":"","sources":["../../src/bin/reference-adapter.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Reference adapter executable. Wraps @hypequery/protocol behind the RFC 0012
|
|
3
|
+
// stdio protocol so the runner can drive it like any other implementation.
|
|
4
|
+
import { createStdioAdapter } from '../adapters/stdio.js';
|
|
5
|
+
import { REFERENCE_FAMILIES, referenceHandle } from '../adapters/reference.js';
|
|
6
|
+
createStdioAdapter({
|
|
7
|
+
implementation: '@hypequery/protocol',
|
|
8
|
+
language: 'typescript',
|
|
9
|
+
families: [...REFERENCE_FAMILIES],
|
|
10
|
+
handle: referenceHandle,
|
|
11
|
+
})
|
|
12
|
+
.then((code) => process.exit(code))
|
|
13
|
+
.catch((error) => {
|
|
14
|
+
process.stderr.write(`${String(error)}\n`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare.d.ts","sourceRoot":"","sources":["../src/compare.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAwC7E,wBAAgB,WAAW,CAAC,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,aAAa,GAAG,WAAW,CA2ElF"}
|
package/dist/compare.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const STABLE_CODE = /^HQ_[A-Z0-9_]+$/;
|
|
2
|
+
/** Order-insensitive structural equality via canonical JSON. */
|
|
3
|
+
function stableStringify(value) {
|
|
4
|
+
if (value === null || typeof value !== 'object') {
|
|
5
|
+
return JSON.stringify(value) ?? 'null';
|
|
6
|
+
}
|
|
7
|
+
if (Array.isArray(value)) {
|
|
8
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
9
|
+
}
|
|
10
|
+
const object = value;
|
|
11
|
+
return `{${Object.keys(object)
|
|
12
|
+
.sort()
|
|
13
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`)
|
|
14
|
+
.join(',')}}`;
|
|
15
|
+
}
|
|
16
|
+
function deepEqual(a, b) {
|
|
17
|
+
return stableStringify(a) === stableStringify(b);
|
|
18
|
+
}
|
|
19
|
+
function outcome(ec, status, extra = {}) {
|
|
20
|
+
return { family: ec.family, role: ec.role, id: ec.id, status, ...extra };
|
|
21
|
+
}
|
|
22
|
+
function isUnsafeAccessorCase(ec) {
|
|
23
|
+
const generator = ec.case.generator;
|
|
24
|
+
return generator?.type === 'unsafe-accessor';
|
|
25
|
+
}
|
|
26
|
+
function output(result) {
|
|
27
|
+
return ('output' in result && result.output ? result.output : {});
|
|
28
|
+
}
|
|
29
|
+
export function compareCase(ec, result) {
|
|
30
|
+
if ('skipped' in result) {
|
|
31
|
+
// A skip is only legitimate for the host-model-conditional accessor case.
|
|
32
|
+
if (ec.role === 'rejection' && isUnsafeAccessorCase(ec)) {
|
|
33
|
+
return outcome(ec, 'skip', { message: result.reason });
|
|
34
|
+
}
|
|
35
|
+
return outcome(ec, 'fail', {
|
|
36
|
+
expected: 'a result',
|
|
37
|
+
actual: 'skipped',
|
|
38
|
+
message: result.reason ?? 'case may not be skipped',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const ok = result.ok === true;
|
|
42
|
+
const code = result.ok === false ? result.code : undefined;
|
|
43
|
+
switch (ec.role) {
|
|
44
|
+
case 'success': {
|
|
45
|
+
if (!ok)
|
|
46
|
+
return outcome(ec, 'fail', { expected: 'accepted', actual: code });
|
|
47
|
+
return compareSuccessOutput(ec, output(result));
|
|
48
|
+
}
|
|
49
|
+
case 'identity': {
|
|
50
|
+
if (!ok)
|
|
51
|
+
return outcome(ec, 'fail', { expected: 'accepted', actual: code });
|
|
52
|
+
const o = output(result);
|
|
53
|
+
if (o.canonical !== ec.case.canonical) {
|
|
54
|
+
return outcome(ec, 'fail', { expected: 'canonical bytes', actual: 'mismatch' });
|
|
55
|
+
}
|
|
56
|
+
if (o.sha256 !== ec.case.sha256) {
|
|
57
|
+
return outcome(ec, 'fail', { expected: String(ec.case.sha256), actual: String(o.sha256) });
|
|
58
|
+
}
|
|
59
|
+
return outcome(ec, 'pass');
|
|
60
|
+
}
|
|
61
|
+
case 'portable': {
|
|
62
|
+
if (!ok)
|
|
63
|
+
return outcome(ec, 'fail', { expected: 'portable', actual: code });
|
|
64
|
+
const o = output(result);
|
|
65
|
+
if (!deepEqual(o.expression, ec.case.expression)) {
|
|
66
|
+
return outcome(ec, 'fail', { expected: 'expression AST', actual: 'mismatch' });
|
|
67
|
+
}
|
|
68
|
+
if (!deepEqual(o.dependencies, ec.case.dependencies)) {
|
|
69
|
+
return outcome(ec, 'fail', { expected: 'dependencies', actual: 'mismatch' });
|
|
70
|
+
}
|
|
71
|
+
return outcome(ec, 'pass');
|
|
72
|
+
}
|
|
73
|
+
case 'rejection': {
|
|
74
|
+
if (ok)
|
|
75
|
+
return outcome(ec, 'fail', { expected: String(ec.case.error), actual: 'accepted' });
|
|
76
|
+
if (code !== ec.case.error) {
|
|
77
|
+
return outcome(ec, 'fail', { expected: String(ec.case.error), actual: String(code) });
|
|
78
|
+
}
|
|
79
|
+
return outcome(ec, 'pass');
|
|
80
|
+
}
|
|
81
|
+
case 'non-portable': {
|
|
82
|
+
if (ok)
|
|
83
|
+
return outcome(ec, 'fail', { expected: String(ec.case.code), actual: 'accepted' });
|
|
84
|
+
if (code !== ec.case.code) {
|
|
85
|
+
return outcome(ec, 'fail', { expected: String(ec.case.code), actual: String(code) });
|
|
86
|
+
}
|
|
87
|
+
const o = output(result);
|
|
88
|
+
if (o.start !== ec.case.start) {
|
|
89
|
+
return outcome(ec, 'fail', {
|
|
90
|
+
expected: `start ${String(ec.case.start)}`,
|
|
91
|
+
actual: `start ${String(o.start)}`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return outcome(ec, 'pass');
|
|
95
|
+
}
|
|
96
|
+
case 'fuzz': {
|
|
97
|
+
if (ok)
|
|
98
|
+
return outcome(ec, 'pass');
|
|
99
|
+
if (code !== undefined && STABLE_CODE.test(code))
|
|
100
|
+
return outcome(ec, 'pass');
|
|
101
|
+
return outcome(ec, 'fail', {
|
|
102
|
+
expected: 'accept or HQ_ code',
|
|
103
|
+
actual: code ?? 'malformed result',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
default:
|
|
107
|
+
return outcome(ec, 'fail', { message: `unknown role ${ec.role}` });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function compareSuccessOutput(ec, o) {
|
|
111
|
+
if (ec.family === 'tagged-values-v1') {
|
|
112
|
+
if (o.canonicalHex !== ec.case.canonicalHex) {
|
|
113
|
+
return outcome(ec, 'fail', { expected: 'canonicalHex', actual: 'mismatch' });
|
|
114
|
+
}
|
|
115
|
+
if (o.sha256 !== ec.case.sha256) {
|
|
116
|
+
return outcome(ec, 'fail', { expected: String(ec.case.sha256), actual: String(o.sha256) });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (ec.family === 'identifiers-v1') {
|
|
120
|
+
if (!deepEqual(o.segments, ec.case.segments)) {
|
|
121
|
+
return outcome(ec, 'fail', { expected: 'segments', actual: 'mismatch' });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return outcome(ec, 'pass');
|
|
125
|
+
}
|
package/dist/fs.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ConformanceManifest } from './types.js';
|
|
2
|
+
import type { JsonLoader } from './manifest.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the fixtures directory. When no explicit directory is given, prefer
|
|
5
|
+
* the snapshot bundled with a published build (`../fixtures`), then fall back
|
|
6
|
+
* to the in-repo source of truth so tests and local runs work without a build.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveFixturesDir(explicit?: string): string;
|
|
9
|
+
export declare function createJsonLoader(fixturesDir: string): JsonLoader;
|
|
10
|
+
export declare function loadManifest(fixturesDir: string): ConformanceManifest;
|
|
11
|
+
//# sourceMappingURL=fs.d.ts.map
|
package/dist/fs.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAU5D;AAOD,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,UAAU,CAKhE;AAED,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,mBAAmB,CAQrE"}
|
package/dist/fs.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The only module that touches the filesystem. Everything else operates on
|
|
2
|
+
// values injected from here, per Decision 0001's separation of pure contract
|
|
3
|
+
// logic from I/O.
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the fixtures directory. When no explicit directory is given, prefer
|
|
8
|
+
* the snapshot bundled with a published build (`../fixtures`), then fall back
|
|
9
|
+
* to the in-repo source of truth so tests and local runs work without a build.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveFixturesDir(explicit) {
|
|
12
|
+
if (explicit !== undefined)
|
|
13
|
+
return explicit;
|
|
14
|
+
const bundled = fileURLToPath(new URL('../fixtures/', import.meta.url));
|
|
15
|
+
if (existsSync(new URL('../fixtures/manifest.json', import.meta.url))) {
|
|
16
|
+
return bundled;
|
|
17
|
+
}
|
|
18
|
+
return fileURLToPath(new URL('../../../specs/security-protocol/fixtures/', import.meta.url));
|
|
19
|
+
}
|
|
20
|
+
function joinFixturePath(fixturesDir, relativePath) {
|
|
21
|
+
const base = fixturesDir.endsWith('/') ? fixturesDir : `${fixturesDir}/`;
|
|
22
|
+
return `${base}${relativePath}`;
|
|
23
|
+
}
|
|
24
|
+
export function createJsonLoader(fixturesDir) {
|
|
25
|
+
return (relativePath) => {
|
|
26
|
+
const path = joinFixturePath(fixturesDir, relativePath);
|
|
27
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function loadManifest(fixturesDir) {
|
|
31
|
+
const manifest = JSON.parse(readFileSync(joinFixturePath(fixturesDir, 'manifest.json'), 'utf8'));
|
|
32
|
+
if (manifest.kind !== 'hypequery-conformance-manifest') {
|
|
33
|
+
throw new Error('manifest.json is not a hypequery conformance manifest');
|
|
34
|
+
}
|
|
35
|
+
return manifest;
|
|
36
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { runConformance } from './runner.js';
|
|
2
|
+
export type { RunConformanceOptions } from './runner.js';
|
|
3
|
+
export { loadManifest, resolveFixturesDir, createJsonLoader } from './fs.js';
|
|
4
|
+
export { enumerateAllCases, enumerateFamilyCases, enumerateFuzzCases, resolveJsonPointer, } from './manifest.js';
|
|
5
|
+
export type { JsonLoader } from './manifest.js';
|
|
6
|
+
export { compareCase } from './compare.js';
|
|
7
|
+
export { formatPrettyReport, formatJsonReport } from './report.js';
|
|
8
|
+
export { createStdioAdapter } from './adapters/stdio.js';
|
|
9
|
+
export { referenceHandle, REFERENCE_FAMILIES } from './adapters/reference.js';
|
|
10
|
+
export * from './types.js';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,YAAY,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC9E,cAAc,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Public API for the conformance runner and TypeScript reference adapter.
|
|
2
|
+
export { runConformance } from './runner.js';
|
|
3
|
+
export { loadManifest, resolveFixturesDir, createJsonLoader } from './fs.js';
|
|
4
|
+
export { enumerateAllCases, enumerateFamilyCases, enumerateFuzzCases, resolveJsonPointer, } from './manifest.js';
|
|
5
|
+
export { compareCase } from './compare.js';
|
|
6
|
+
export { formatPrettyReport, formatJsonReport } from './report.js';
|
|
7
|
+
export { createStdioAdapter } from './adapters/stdio.js';
|
|
8
|
+
export { referenceHandle, REFERENCE_FAMILIES } from './adapters/reference.js';
|
|
9
|
+
export * from './types.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ConformanceManifest, EnumeratedCase } from './types.js';
|
|
2
|
+
export type JsonLoader = (relativePath: string) => unknown;
|
|
3
|
+
/** Minimal RFC 6901 JSON pointer resolution for the section selectors. */
|
|
4
|
+
export declare function resolveJsonPointer(root: unknown, pointer: string): unknown;
|
|
5
|
+
/** Cases derived from the family fixture files, in manifest order. */
|
|
6
|
+
export declare function enumerateFamilyCases(manifest: ConformanceManifest, loadJson: JsonLoader): EnumeratedCase[];
|
|
7
|
+
/**
|
|
8
|
+
* Cases derived from the fuzz corpus. A seed with its own `targets` fans out
|
|
9
|
+
* to one case per target family; otherwise it uses the manifest entry family.
|
|
10
|
+
*/
|
|
11
|
+
export declare function enumerateFuzzCases(manifest: ConformanceManifest, loadJson: JsonLoader): EnumeratedCase[];
|
|
12
|
+
export declare function enumerateAllCases(manifest: ConformanceManifest, loadJson: JsonLoader): EnumeratedCase[];
|
|
13
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,mBAAmB,EACnB,cAAc,EAGf,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,UAAU,GAAG,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC;AAM3D,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAiB1E;AAgDD,sEAAsE;AACtE,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,mBAAmB,EAC7B,QAAQ,EAAE,UAAU,GACnB,cAAc,EAAE,CAmBlB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,mBAAmB,EAC7B,QAAQ,EAAE,UAAU,GACnB,cAAc,EAAE,CA0BlB;AAED,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,mBAAmB,EAC7B,QAAQ,EAAE,UAAU,GACnB,cAAc,EAAE,CAElB"}
|