@danypops/vehicle-conformance 0.3.0 → 0.4.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/README.md +45 -0
- package/package.json +4 -4
- package/src/vehicle-conformance.ts +94 -0
package/README.md
CHANGED
|
@@ -26,5 +26,50 @@ expanded immutability, schema-sensitive call rendering, 40/80/120-column
|
|
|
26
26
|
physical-line safety, partial output, and projector exception policy. Pi-specific
|
|
27
27
|
component construction stays in the adapter fixture rather than this package.
|
|
28
28
|
|
|
29
|
+
## The five boundaries
|
|
30
|
+
|
|
31
|
+
Every conformant Tool Shell provider keeps five things independent:
|
|
32
|
+
|
|
33
|
+
1. **Application DTO** -- the domain's own real output shape, transport-neutral,
|
|
34
|
+
untouched by any presentation concern.
|
|
35
|
+
2. **Model content** -- what the LLM reads: independently bounded, ANSI-free,
|
|
36
|
+
semantic. Never derived from or coupled to what a human sees.
|
|
37
|
+
3. **Persisted presentation details** -- a *projected*, versioned, discriminated-union
|
|
38
|
+
DTO, independently bounded, with explicit `{total, returned, omitted}`
|
|
39
|
+
completeness metadata. Projected once, before persistence -- never inferred
|
|
40
|
+
from raw output at render time.
|
|
41
|
+
4. **Interactive component** -- the rendered view of #3. Expanded mode may only
|
|
42
|
+
reveal rows already inside the bounded DTO, never bypass the bound by
|
|
43
|
+
reaching back into raw application output.
|
|
44
|
+
5. **CLI presenters** -- a separate, JSON/human-text presentation path outside
|
|
45
|
+
the interactive TUI entirely; out of scope for this suite.
|
|
46
|
+
|
|
47
|
+
The fail-closed rule that matters most: a parser for #3 must reject a
|
|
48
|
+
malformed/unknown-version/oversized/cyclic details object and fall back to
|
|
49
|
+
**content** (#2) -- never render raw, unbounded application output as a human
|
|
50
|
+
view.
|
|
51
|
+
|
|
52
|
+
## Declared-value coverage
|
|
53
|
+
|
|
54
|
+
A fixture's `ToolShellDualChannelSubject` can optionally supply
|
|
55
|
+
`declaredValueCases` (one `{ value, rawPayload }` per value of a discriminator
|
|
56
|
+
field the provider's own schema declares -- a `format`/`kind`/`action`/...) plus
|
|
57
|
+
a matching `renderDeclaredValue(value, rawPayload, options)`. When present, the
|
|
58
|
+
suite renders one result per declared value and fails if fewer than
|
|
59
|
+
`min(2, cases.length)` of them escape being textually indistinguishable from a
|
|
60
|
+
raw `JSON.stringify(rawPayload, null, 2)` dump.
|
|
61
|
+
|
|
62
|
+
This is the generic version of a `never`-typed exhaustiveness guard on a
|
|
63
|
+
discriminated switch -- it catches the same bug class (most declared values
|
|
64
|
+
silently falling through to an undifferentiated raw-JSON view) in bespoke
|
|
65
|
+
non-switch code too (an `if`-chain, a plain-string `action` switch with no
|
|
66
|
+
compile-time exhaustiveness), which a TypeScript-only lint rule would miss
|
|
67
|
+
entirely. `evaluateDeclaredValueCoverage(cases, renderDeclaredValue, options)`
|
|
68
|
+
is exported separately for direct unit testing of the classifier against a
|
|
69
|
+
known-bad fixture shape, independent of the wrapping `bun:test` assertion.
|
|
70
|
+
|
|
71
|
+
Omit `declaredValueCases` entirely for a subject with no such discriminator --
|
|
72
|
+
the check then no-ops.
|
|
73
|
+
|
|
29
74
|
See the [workspace README](https://github.com/DanyPops/vehicle#readme) for
|
|
30
75
|
the full Vehicle package layout.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/vehicle-conformance",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Host-neutral conformance suite for any VehicleClient implementation: one shared bun:test assertion set that LocalVehicleClient, RemoteVehicleClient, and any future transport must satisfy identically. A Bun-only devDependency for testing, not a runtime library -- ships raw TypeScript, never precompiled.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
"typecheck": "tsc --noEmit"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@danypops/vehicle-core": "^0.
|
|
18
|
-
"@danypops/vehicle-server": "^0.
|
|
17
|
+
"@danypops/vehicle-core": "^0.17.1",
|
|
18
|
+
"@danypops/vehicle-server": "^0.25.2"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@danypops/vehicle-client": "^0.
|
|
21
|
+
"@danypops/vehicle-client": "^0.10.3",
|
|
22
22
|
"@types/node": "^22.0.0",
|
|
23
23
|
"typescript": "^5.9.3"
|
|
24
24
|
},
|
|
@@ -442,6 +442,76 @@ export interface ToolShellDualChannelSubject {
|
|
|
442
442
|
replay(details: unknown, fallbackContent: string, options: ToolShellRenderOptions): readonly string[];
|
|
443
443
|
renderCall(args: unknown, width: 40 | 80 | 120): readonly string[];
|
|
444
444
|
invalidProjection(): Promise<unknown>;
|
|
445
|
+
/**
|
|
446
|
+
* Optional -- the discriminator values (a `format`/`kind`/`action`/... field) this provider's
|
|
447
|
+
* own presentation-details schema declares, each paired with a representative raw application
|
|
448
|
+
* payload for that value. Supplying this (together with renderDeclaredValue) enables the
|
|
449
|
+
* declared-value coverage check below, which generically catches the pi-web-spider bug class
|
|
450
|
+
* (see doc 4e9e08c1, Finding 1/4): most declared values falling through to an undifferentiated
|
|
451
|
+
* JSON.stringify dump of their own payload instead of a real projected view. Omit entirely for
|
|
452
|
+
* a subject with no such discriminator -- the check then no-ops.
|
|
453
|
+
*/
|
|
454
|
+
readonly declaredValueCases?: readonly ToolShellDeclaredValueCase[];
|
|
455
|
+
/** Required alongside declaredValueCases: renders the expanded view for one declared value's
|
|
456
|
+
* own raw payload, through exactly the same projection+render pipeline the real handler uses. */
|
|
457
|
+
renderDeclaredValue?(value: string, rawPayload: unknown, options: ToolShellRenderOptions): readonly string[];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export interface ToolShellDeclaredValueCase {
|
|
461
|
+
/** e.g. a WebFormat value ('search'/'lean'/...), a PackageToolDetails['kind'], a tickets action name. */
|
|
462
|
+
readonly value: string;
|
|
463
|
+
/** The real, untransformed application output this declared value would carry. */
|
|
464
|
+
readonly rawPayload: unknown;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export interface DeclaredValueCoverageResult {
|
|
468
|
+
/** Declared values whose rendered output is NOT indistinguishable from a raw JSON.stringify dump of their own payload. */
|
|
469
|
+
readonly nonRawValues: readonly string[];
|
|
470
|
+
/** Declared values whose rendered output IS indistinguishable from a raw JSON.stringify dump of their own payload. */
|
|
471
|
+
readonly rawValues: readonly string[];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function normalizeForComparison(text: string): string {
|
|
475
|
+
return text.replace(/\s+/g, "");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* True when `renderedLines` is textually indistinguishable (ignoring ANSI styling and whitespace)
|
|
480
|
+
* from `JSON.stringify(rawPayload, null, 2)` -- the exact shape pi-web-spider's `primaryLines()`
|
|
481
|
+
* fell back to for every non-"markdown" format. Whitespace-insensitive so a renderer that reflows
|
|
482
|
+
* the same JSON text to a narrower width still counts as "raw", matching the real bug (a Text
|
|
483
|
+
* component wrapping the identical JSON.stringify output).
|
|
484
|
+
*/
|
|
485
|
+
function looksLikeRawJsonDump(renderedLines: readonly string[], rawPayload: unknown): boolean {
|
|
486
|
+
let rawJson: string;
|
|
487
|
+
try {
|
|
488
|
+
rawJson = JSON.stringify(rawPayload, null, 2) ?? "";
|
|
489
|
+
} catch {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
if (rawJson.length === 0) return false;
|
|
493
|
+
const renderedText = renderedLines.join("\n").replace(ANSI_CSI_PATTERN, "");
|
|
494
|
+
return normalizeForComparison(renderedText) === normalizeForComparison(rawJson);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Pure, independently unit-testable core of the declared-value coverage check -- separated from
|
|
499
|
+
* the bun:test `it()` wiring below so a fixture reproducing a known-bad shape (e.g.
|
|
500
|
+
* pi-web-spider's own pre-fix behavior) can be asserted against directly, proving the classifier
|
|
501
|
+
* itself actually detects that bug class rather than trusting the wrapping `it()` alone.
|
|
502
|
+
*/
|
|
503
|
+
export function evaluateDeclaredValueCoverage(
|
|
504
|
+
cases: readonly ToolShellDeclaredValueCase[],
|
|
505
|
+
renderDeclaredValue: (value: string, rawPayload: unknown, options: ToolShellRenderOptions) => readonly string[],
|
|
506
|
+
options: ToolShellRenderOptions = { width: 80, expanded: true },
|
|
507
|
+
): DeclaredValueCoverageResult {
|
|
508
|
+
const nonRawValues: string[] = [];
|
|
509
|
+
const rawValues: string[] = [];
|
|
510
|
+
for (const { value, rawPayload } of cases) {
|
|
511
|
+
const lines = renderDeclaredValue(value, rawPayload, options);
|
|
512
|
+
(looksLikeRawJsonDump(lines, rawPayload) ? rawValues : nonRawValues).push(value);
|
|
513
|
+
}
|
|
514
|
+
return { nonRawValues, rawValues };
|
|
445
515
|
}
|
|
446
516
|
|
|
447
517
|
export interface ToolShellDualChannelFixture {
|
|
@@ -529,5 +599,29 @@ export function runToolShellDualChannelConformance(fixture: ToolShellDualChannel
|
|
|
529
599
|
await cleanup();
|
|
530
600
|
}
|
|
531
601
|
});
|
|
602
|
+
|
|
603
|
+
it("renders most of its own declared discriminator values as more than a raw JSON dump of their own payload", async () => {
|
|
604
|
+
const { subject, cleanup } = await fixture.create();
|
|
605
|
+
try {
|
|
606
|
+
const cases = subject.declaredValueCases;
|
|
607
|
+
if (!cases || cases.length === 0) return; // opt-in: no discriminator declared, nothing to check
|
|
608
|
+
if (!subject.renderDeclaredValue) {
|
|
609
|
+
throw new Error("declaredValueCases supplied without a matching renderDeclaredValue implementation");
|
|
610
|
+
}
|
|
611
|
+
const renderDeclaredValue = subject.renderDeclaredValue.bind(subject);
|
|
612
|
+
const options: ToolShellRenderOptions = { width: 80, expanded: true };
|
|
613
|
+
for (const { value, rawPayload } of cases) {
|
|
614
|
+
assertPhysicalLines(renderDeclaredValue(value, rawPayload, options), options.width);
|
|
615
|
+
}
|
|
616
|
+
const { nonRawValues, rawValues } = evaluateDeclaredValueCoverage(cases, renderDeclaredValue, options);
|
|
617
|
+
expect(
|
|
618
|
+
nonRawValues.length,
|
|
619
|
+
`declared values [${cases.map((c) => c.value).join(", ")}] mostly render as an undifferentiated JSON.stringify dump of ` +
|
|
620
|
+
`their own payload -- only [${nonRawValues.join(", ") || "none"}] escape it, [${rawValues.join(", ")}] don't`,
|
|
621
|
+
).toBeGreaterThanOrEqual(Math.min(2, cases.length));
|
|
622
|
+
} finally {
|
|
623
|
+
await cleanup();
|
|
624
|
+
}
|
|
625
|
+
});
|
|
532
626
|
});
|
|
533
627
|
}
|