@hypequery/protocol-conformance 0.10.1 → 0.10.3

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/README.md CHANGED
@@ -35,6 +35,21 @@ hypequery-protocol-conformance run \
35
35
  hypequery-protocol-conformance list
36
36
  ```
37
37
 
38
+ `--families` selects cases but deliberately permits partial adapters. Release
39
+ gates should also pass `--expect-families` with the complete expected adapter
40
+ family set. The run then fails during the handshake if a family was added or
41
+ dropped without updating the gate. When both options are present, their family
42
+ sets must match so the gate cannot assert a family while filtering out its
43
+ cases. Every expected family must also retain at least one case after
44
+ `--skip-fuzz` or `--only-fuzz` filtering:
45
+
46
+ ```bash
47
+ hypequery-protocol-conformance run \
48
+ --families tagged-values-v1,identifiers-v1 \
49
+ --expect-families tagged-values-v1,identifiers-v1 \
50
+ -- python -m hypequery.protocol.adapter
51
+ ```
52
+
38
53
  Exit code `0` means every case passed, `1` means conformance failures, and `2` means the runner or adapter protocol could not be set up correctly.
39
54
 
40
55
  ## Adapter shape
@@ -51,6 +66,22 @@ The exported `createStdioAdapter` helper handles the loop. Your handler maps `(f
51
66
 
52
67
  The package includes a pinned fixture snapshot, so a pinned package version is also a pinned conformance target. Pass `--fixtures` to test local or newer specifications.
53
68
 
69
+ ## Repository gates
70
+
71
+ With Node.js, pnpm, and `uv` installed, build the workspaces and run the
72
+ TypeScript reference, SQL-portability, and Python adapters against the same
73
+ local fixtures:
74
+
75
+ ```bash
76
+ pnpm build
77
+ pnpm conformance
78
+ ```
79
+
80
+ The Python leg asserts its exact announced family set, so removing a supported
81
+ family cannot turn cases into a green “not run” result. See the [fixture update
82
+ runbook](../../specs/security-protocol/fixtures/README.md) when changing a
83
+ fixture family or adding an implementation.
84
+
54
85
  ## License
55
86
 
56
87
  Apache-2.0.
@@ -57,12 +57,21 @@ async function main() {
57
57
  const families = typeof options.families === 'string'
58
58
  ? options.families.split(',').map((f) => f.trim()).filter(Boolean)
59
59
  : undefined;
60
+ const expectedFamiliesOption = options['expect-families'];
61
+ if (expectedFamiliesOption === true) {
62
+ process.stderr.write('error: --expect-families requires a comma-separated value\n');
63
+ return 2;
64
+ }
65
+ const expectedFamilies = typeof expectedFamiliesOption === 'string'
66
+ ? expectedFamiliesOption.split(',').map((f) => f.trim()).filter(Boolean)
67
+ : undefined;
60
68
  let summary;
61
69
  try {
62
70
  summary = await runConformance({
63
71
  adapterCommand,
64
72
  fixturesDir,
65
73
  families,
74
+ expectedFamilies,
66
75
  timeoutMs: typeof options['timeout-ms'] === 'string' ? Number(options['timeout-ms']) : undefined,
67
76
  skipFuzz: options['skip-fuzz'] === true,
68
77
  onlyFuzz: options['only-fuzz'] === true,
@@ -0,0 +1,9 @@
1
+ /** Validates a release gate's independently maintained fixture-family list. */
2
+ export declare function validateExpectedFamilies(expectedFamilies: readonly string[], manifestFamilies: ReadonlySet<string>): void;
3
+ /** Fails unless an adapter announces exactly the release gate's family set. */
4
+ export declare function assertExpectedFamilies(announcedFamilies: readonly string[], expectedFamilies: readonly string[] | undefined): void;
5
+ /** Prevents a release gate from asserting families while filtering some out. */
6
+ export declare function assertSelectedFamilies(selectedFamilies: readonly string[] | undefined, expectedFamilies: readonly string[]): void;
7
+ /** Ensures filtering cannot leave an asserted family without executable cases. */
8
+ export declare function assertExpectedFamiliesHaveCases(expectedFamilies: readonly string[], effectiveCaseFamilies: ReadonlySet<string>): void;
9
+ //# sourceMappingURL=family-expectations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"family-expectations.d.ts","sourceRoot":"","sources":["../src/family-expectations.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,wBAAgB,wBAAwB,CACtC,gBAAgB,EAAE,SAAS,MAAM,EAAE,EACnC,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAAC,GACpC,IAAI,CAWN;AAED,+EAA+E;AAC/E,wBAAgB,sBAAsB,CACpC,iBAAiB,EAAE,SAAS,MAAM,EAAE,EACpC,gBAAgB,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAC9C,IAAI,CAaN;AAED,gFAAgF;AAChF,wBAAgB,sBAAsB,CACpC,gBAAgB,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,EAC/C,gBAAgB,EAAE,SAAS,MAAM,EAAE,GAClC,IAAI,CAaN;AAED,kFAAkF;AAClF,wBAAgB,+BAA+B,CAC7C,gBAAgB,EAAE,SAAS,MAAM,EAAE,EACnC,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAAC,GACzC,IAAI,CAON"}
@@ -0,0 +1,44 @@
1
+ /** Validates a release gate's independently maintained fixture-family list. */
2
+ export function validateExpectedFamilies(expectedFamilies, manifestFamilies) {
3
+ if (expectedFamilies.some((family) => typeof family !== 'string' || family.length === 0)
4
+ || new Set(expectedFamilies).size !== expectedFamilies.length) {
5
+ throw new Error('expected families must be unique non-empty strings');
6
+ }
7
+ const absent = expectedFamilies.filter((family) => !manifestFamilies.has(family)).sort();
8
+ if (absent.length > 0) {
9
+ throw new Error(`expected families are absent from the manifest: ${absent.join(', ')}`);
10
+ }
11
+ }
12
+ /** Fails unless an adapter announces exactly the release gate's family set. */
13
+ export function assertExpectedFamilies(announcedFamilies, expectedFamilies) {
14
+ if (!expectedFamilies)
15
+ return;
16
+ const announced = [...announcedFamilies].sort();
17
+ const expected = [...expectedFamilies].sort();
18
+ if (announced.length !== expected.length
19
+ || announced.some((family, index) => family !== expected[index])) {
20
+ throw new Error(`adapter families did not match expectation: expected ${expected.join(', ') || '(none)'}; `
21
+ + `announced ${announced.join(', ') || '(none)'}`);
22
+ }
23
+ }
24
+ /** Prevents a release gate from asserting families while filtering some out. */
25
+ export function assertSelectedFamilies(selectedFamilies, expectedFamilies) {
26
+ if (!selectedFamilies)
27
+ return;
28
+ const selected = [...selectedFamilies].sort();
29
+ const expected = [...expectedFamilies].sort();
30
+ if (selected.length !== expected.length
31
+ || selected.some((family, index) => family !== expected[index])) {
32
+ throw new Error(`selected families did not match expected families: expected `
33
+ + `${expected.join(', ') || '(none)'}; selected ${selected.join(', ') || '(none)'}`);
34
+ }
35
+ }
36
+ /** Ensures filtering cannot leave an asserted family without executable cases. */
37
+ export function assertExpectedFamiliesHaveCases(expectedFamilies, effectiveCaseFamilies) {
38
+ const untested = expectedFamilies
39
+ .filter((family) => !effectiveCaseFamilies.has(family))
40
+ .sort();
41
+ if (untested.length > 0) {
42
+ throw new Error(`expected families have no selected cases: ${untested.join(', ')}`);
43
+ }
44
+ }
package/dist/runner.d.ts CHANGED
@@ -4,6 +4,8 @@ export interface RunConformanceOptions {
4
4
  readonly fixturesDir?: string;
5
5
  /** Restrict to these families (intersected with what the adapter announces). */
6
6
  readonly families?: readonly string[];
7
+ /** Fail setup unless the adapter announces exactly these fixture families. */
8
+ readonly expectedFamilies?: readonly string[];
7
9
  readonly timeoutMs?: number;
8
10
  readonly skipFuzz?: boolean;
9
11
  readonly onlyFuzz?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAQA,OAAO,EAOL,KAAK,UAAU,EAChB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAyJD,wBAAsB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,UAAU,CAAC,CA6GxF"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAcA,OAAO,EAOL,KAAK,UAAU,EAChB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,8EAA8E;IAC9E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAyJD,wBAAsB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,UAAU,CAAC,CA6HxF"}
package/dist/runner.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { spawn } from 'node:child_process';
5
5
  import { createInterface } from 'node:readline';
6
6
  import { compareCase } from './compare.js';
7
+ import { assertExpectedFamilies, assertExpectedFamiliesHaveCases, assertSelectedFamilies, validateExpectedFamilies, } from './family-expectations.js';
7
8
  import { createJsonLoader, loadManifest, resolveFixturesDir } from './fs.js';
8
9
  import { enumerateAllCases } from './manifest.js';
9
10
  import { CONFORMANCE_MANIFEST_VERSION, CONFORMANCE_PROTOCOL_VERSION, } from './types.js';
@@ -145,6 +146,10 @@ export async function runConformance(options) {
145
146
  const fixturesDir = resolveFixturesDir(options.fixturesDir);
146
147
  const manifest = loadManifest(fixturesDir);
147
148
  const loadJson = createJsonLoader(fixturesDir);
149
+ if (options.expectedFamilies) {
150
+ validateExpectedFamilies(options.expectedFamilies, new Set(manifest.families.map((family) => family.name)));
151
+ assertSelectedFamilies(options.families, options.expectedFamilies);
152
+ }
148
153
  const allCases = enumerateAllCases(manifest, loadJson);
149
154
  const hostModelFamilies = new Set(allCases
150
155
  .filter((c) => {
@@ -163,6 +168,9 @@ export async function runConformance(options) {
163
168
  const requested = new Set(options.families);
164
169
  cases = cases.filter((c) => requested.has(c.family));
165
170
  }
171
+ if (options.expectedFamilies) {
172
+ assertExpectedFamiliesHaveCases(options.expectedFamilies, new Set(cases.map((conformanceCase) => conformanceCase.family)));
173
+ }
166
174
  // The handshake covers process spawn plus the adapter's first write, which
167
175
  // can be slow under load; it gets a generous timeout independent of the
168
176
  // per-case timeout so a busy machine never mistakes startup for a hang.
@@ -171,6 +179,7 @@ export async function runConformance(options) {
171
179
  let hello;
172
180
  try {
173
181
  hello = await connection.handshake(handshakeTimeoutMs, hostModelFamilies);
182
+ assertExpectedFamilies(hello.families, options.expectedFamilies);
174
183
  }
175
184
  catch (error) {
176
185
  connection.kill();
@@ -228,6 +237,7 @@ export async function runConformance(options) {
228
237
  connection = new AdapterConnection(options.adapterCommand);
229
238
  try {
230
239
  hello = await connection.handshake(handshakeTimeoutMs, hostModelFamilies);
240
+ assertExpectedFamilies(hello.families, options.expectedFamilies);
231
241
  }
232
242
  catch {
233
243
  // A replacement that cannot even complete a handshake ends the run
@@ -0,0 +1,48 @@
1
+ # Conformance fixture update runbook
2
+
3
+ This directory is the language-neutral conformance source of truth. A fixture
4
+ change is a protocol change, not an implementation-specific test edit.
5
+
6
+ ## Before editing
7
+
8
+ 1. Identify the RFC and extension version that owns the behavior. Accepted
9
+ grammar, limits, comparison rules, or canonical bytes cannot be changed in
10
+ place; introduce a new extension version when the RFC requires one.
11
+ 2. Decide whether the case is a success, rejection, identity, portability, or
12
+ deterministic fuzz case. Rejection cases must use the stable code owned by
13
+ their manifest family.
14
+ 3. Keep fixture IDs unique and inputs deterministic. Never put credentials,
15
+ customer data, nondeterministic timestamps, or host-specific paths in the
16
+ corpus.
17
+
18
+ ## Update workflow
19
+
20
+ 1. Edit the owning family files and its README. Add new files or fuzz targets
21
+ to `manifest.json`; do not bypass the manifest.
22
+ 2. Update the TypeScript reference implementation and every implementation
23
+ that already announces the family. A family may be implemented in a later
24
+ stacked PR, but no release gate may silently drop an announced family.
25
+ 3. When an adapter gains a family, update its exact `--expect-families` list in
26
+ the same PR. For Python, that list is owned by `conformance:python` in the
27
+ root `package.json`.
28
+ 4. Add a Changeset whenever the bundled `@hypequery/protocol-conformance`
29
+ fixture snapshot or runner behavior changes.
30
+ 5. Rebuild the conformance package after fixture edits; its build copies the
31
+ current corpus into the published package.
32
+
33
+ ## Verification
34
+
35
+ From the repository root:
36
+
37
+ ```console
38
+ pnpm --filter @hypequery/protocol build
39
+ pnpm --filter @hypequery/datasets build
40
+ pnpm --filter @hypequery/protocol-conformance build
41
+ pnpm --filter @hypequery/protocol-conformance test
42
+ pnpm conformance
43
+ uv run --project python/hypequery --frozen pytest
44
+ ```
45
+
46
+ Review the report's implementation, announced families, hostile-object suite,
47
+ and not-run count—not only its exit status. CI release gates must use an exact
48
+ family assertion for partial adapters.
@@ -3,6 +3,8 @@
3
3
  These adversarial inputs replay on every conformance run under RFC 0012. An implementation may accept a seed within documented limits or reject it with a stable `HQ_*` code; it must never crash, hang, partially execute input, or allocate without bounds.
4
4
 
5
5
  - `value-sources.json` targets duplicate-aware JSON decoding and tagged values.
6
+ - `identifiers.json` targets Unicode ambiguity, reserved names, separators,
7
+ validation limits, and non-string identifier inputs.
6
8
  - `structured-values.json` targets selected structural validator families.
7
9
  - `sql-expressions.json` targets the SQL portability compiler.
8
10
 
@@ -0,0 +1,17 @@
1
+ [
2
+ { "id": "identifier-null", "mode": "simple", "value": null },
3
+ { "id": "identifier-control-character", "mode": "simple", "value": "order\u0000id" },
4
+ { "id": "identifier-sql-shaped", "mode": "simple", "value": "orders;DROP_TABLE_users" },
5
+ { "id": "identifier-cyrillic-confusable", "mode": "simple", "value": "аdmin" },
6
+ { "id": "identifier-combining-mark", "mode": "simple", "value": "café" },
7
+ { "id": "identifier-fullwidth-dot", "mode": "qualified", "value": "orders.customer" },
8
+ { "id": "identifier-lone-high-surrogate", "mode": "simple", "value": "\ud800" },
9
+ { "id": "identifier-lone-low-surrogate", "mode": "simple", "value": "\udc00" },
10
+ { "id": "identifier-surrogate-pair", "mode": "simple", "value": "\ud83d\ude00" },
11
+ { "id": "identifier-reserved-mixed-case", "mode": "simple", "value": "__HyPeQuErY_admin" },
12
+ { "id": "identifier-qualified-leading-dot", "mode": "qualified", "value": ".orders" },
13
+ { "id": "identifier-qualified-trailing-dot", "mode": "qualified", "value": "orders." },
14
+ { "id": "identifier-qualified-repeated-dot", "mode": "qualified", "value": "orders..customer" },
15
+ { "id": "identifier-long-segment", "mode": "simple", "generator": { "type": "repeat-string", "value": "a", "count": 10000 } },
16
+ { "id": "identifier-many-segments", "mode": "qualified", "generator": { "type": "qualified-segments", "segment": "a", "count": 1000 } }
17
+ ]
@@ -228,6 +228,10 @@
228
228
  "path": "fuzz-seeds-v1/value-sources.json",
229
229
  "family": "tagged-values-v1"
230
230
  },
231
+ {
232
+ "path": "fuzz-seeds-v1/identifiers.json",
233
+ "family": "identifiers-v1"
234
+ },
231
235
  {
232
236
  "path": "fuzz-seeds-v1/structured-values.json"
233
237
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypequery/protocol-conformance",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "description": "Cross-language conformance runner for the Hypequery analytics security protocol",
5
5
  "keywords": [
6
6
  "hypequery",