@volter/twin-standard 1.0.148 → 1.0.150
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 +12 -3
- package/client-sdks/bun.lock +0 -32
- package/client-sdks/package.json +0 -2
- package/dist/client-sdks/bun.lock +0 -32
- package/dist/client-sdks/package.json +0 -2
- package/dist/src/assessment.d.ts +6 -4
- package/dist/src/assessment.js +2 -1
- package/dist/src/cli.js +21 -6
- package/dist/src/clients/harness.d.ts +3 -1
- package/dist/src/clients/harness.js +4 -2
- package/dist/src/clients/jira.d.ts +2 -0
- package/dist/src/clients/jira.js +71 -0
- package/dist/src/clients/polar.d.ts +2 -0
- package/dist/src/clients/polar.js +73 -0
- package/dist/src/clients/resend.js +38 -0
- package/dist/src/clients/run.js +1 -1
- package/dist/src/clients/sentry.js +1 -26
- package/dist/src/clients/smtp.d.ts +2 -0
- package/dist/src/clients/smtp.js +54 -0
- package/dist/src/clients/stripe.js +1 -1
- package/dist/src/clients/volteridentity.js +4 -3
- package/dist/src/clients/x.d.ts +2 -0
- package/dist/src/clients/x.js +64 -0
- package/dist/src/conformance.d.ts +4 -3
- package/dist/src/conformance.js +293 -8
- package/dist/src/consumer.js +5 -1
- package/dist/src/create-pack.d.ts +5 -0
- package/dist/src/create-pack.js +38 -4
- package/dist/src/derive.js +21 -13
- package/dist/src/gate.d.ts +1 -0
- package/dist/src/gate.js +47 -4
- package/dist/src/grade.d.ts +1 -0
- package/dist/src/grade.js +1 -1
- package/dist/src/pack-facts.d.ts +9 -1
- package/dist/src/pack-facts.js +11 -2
- package/dist/src/pack-files.js +2 -1
- package/dist/src/protocol-3.js +40 -6
- package/dist/src/spec-ir-client.js +3 -2
- package/dist/src/spec-ir-commands.d.ts +73 -0
- package/dist/src/spec-ir-commands.js +63 -0
- package/dist/src/spec-ir.d.ts +2 -0
- package/dist/src/spec-ir.js +43 -0
- package/dist/src/tools/judge-brief.d.ts +1 -0
- package/dist/src/tools/judge-brief.js +67 -0
- package/dist/src/tools/life-coverage-walk.d.ts +2 -0
- package/dist/src/tools/life-coverage-walk.js +220 -0
- package/dist/src/tools/life-coverage.d.ts +2 -0
- package/dist/src/tools/life-coverage.js +271 -0
- package/dist/src/tools/packs-root.d.ts +9 -0
- package/dist/src/tools/packs-root.js +17 -0
- package/dist/src/tools/run-tool.d.ts +1 -0
- package/dist/src/tools/run-tool.js +18 -0
- package/dist/src/tools/score-pack.d.ts +2 -0
- package/dist/src/tools/score-pack.js +264 -0
- package/dist/src/tools/vendor-examples.d.ts +2 -0
- package/dist/src/tools/vendor-examples.js +1493 -0
- package/dist/src/types.d.ts +2 -0
- package/dist/src/verdict.js +1 -1
- package/dist/src/walk/behavior-journey.d.ts +6 -0
- package/dist/src/walk/behavior-journey.js +108 -8
- package/dist/src/walk/journey-kit.d.ts +23 -1
- package/dist/src/walk/journey-kit.js +7 -2
- package/dist/src/walk/rendered-scalar.d.ts +3 -0
- package/dist/src/walk/rendered-scalar.js +23 -0
- package/dist/src/walk/sse-example.js +45 -0
- package/package.json +2 -2
- package/src/assessment.ts +2 -1
- package/src/cli.ts +15 -2
- package/src/clients/harness.ts +4 -2
- package/src/clients/jira.ts +73 -0
- package/src/clients/polar.ts +73 -0
- package/src/clients/resend.ts +32 -0
- package/src/clients/run.ts +2 -2
- package/src/clients/sentry.ts +1 -25
- package/src/clients/smtp.ts +49 -0
- package/src/clients/stripe.ts +1 -1
- package/src/clients/volteridentity.ts +4 -3
- package/src/clients/x.ts +66 -0
- package/src/conformance.ts +226 -10
- package/src/consumer.ts +5 -1
- package/src/create-pack.ts +35 -5
- package/src/derive.ts +21 -14
- package/src/gate.ts +40 -6
- package/src/grade.ts +2 -2
- package/src/pack-facts.ts +10 -3
- package/src/pack-files.ts +2 -1
- package/src/protocol-3.ts +32 -6
- package/src/spec-ir-client.ts +3 -2
- package/src/spec-ir-commands.ts +81 -0
- package/src/spec-ir.ts +31 -0
- package/src/tools/judge-brief.ts +63 -0
- package/src/tools/life-coverage-walk.ts +173 -0
- package/src/tools/life-coverage.ts +237 -0
- package/src/tools/packs-root.ts +21 -0
- package/src/tools/run-tool.ts +10 -0
- package/src/tools/score-pack.ts +244 -0
- package/src/tools/vendor-examples.ts +1250 -0
- package/src/types.ts +2 -0
- package/src/verdict.ts +1 -1
- package/src/walk/behavior-journey.ts +65 -4
- package/src/walk/journey-kit.ts +21 -3
- package/src/walk/rendered-scalar.ts +17 -0
- package/src/walk/sse-example.ts +37 -0
package/src/types.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface ScoreReport {
|
|
|
29
29
|
|
|
30
30
|
export interface CriterionResult {
|
|
31
31
|
pass: boolean;
|
|
32
|
+
/** An interrupted check is not a verdict about unchanged source. */
|
|
33
|
+
cacheable?: boolean;
|
|
32
34
|
/** why it failed, or what it measured when it passed */
|
|
33
35
|
reason: string;
|
|
34
36
|
}
|
package/src/verdict.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
/** Committed attribution names people or author roles, never execution identities. */
|
|
8
8
|
export function attributionName(name: string | undefined): string | undefined {
|
|
9
9
|
if (name === undefined) return undefined;
|
|
10
|
-
if (/\bcx-|\bt_[0-9a-f]{8}|wt\/|\/root(?:\/|$|[: )])|\b(?:pane|session)[
|
|
10
|
+
if (/\bcx-|\bt_[0-9a-f]{8}|wt\/|\/root(?:\/|$|[: )])|\b(?:pane|session)[ :-]|[0-9a-f]{8}-[0-9a-f-]{27}|\b[0-9a-f]{16,64}\b/i.test(name)) throw new Error('attribution must use an author name, not an execution identifier');
|
|
11
11
|
return name;
|
|
12
12
|
}
|
|
13
13
|
export type Verdict = { judge?: { agent?: string; name?: string; session?: string; review?: string }; authors?: string[] };
|
|
@@ -9,7 +9,8 @@ import { attributionName } from '../verdict.ts';
|
|
|
9
9
|
//
|
|
10
10
|
// The caller names the pack repository first (usePacks).
|
|
11
11
|
|
|
12
|
-
import { mysql, redis } from '@volter/world-core';
|
|
12
|
+
import { mongodb, mysql, redis } from '@volter/world-core';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
13
14
|
import { gzipSync } from 'node:zlib';
|
|
14
15
|
import { parse, visit } from 'graphql';
|
|
15
16
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
@@ -17,7 +18,7 @@ import { join } from 'node:path';
|
|
|
17
18
|
import { databaseSession, type DatabaseSession, getActivePackAssets, getActiveWorldStore, LocalPackAssets, MemoryBlobStore, MemoryWorldStore, setActiveBlobStore, setActivePackAssets, setActiveWorldStore, setDeliveryObserver, setInProcessTwins, type InProcessTwin, type TwinStream, type TwinStreamConnection } from '@volter/world-core/runtime';
|
|
18
19
|
import { createHostTable, type HostFacts } from '@volter/world-core/host-routing';
|
|
19
20
|
import packFactsDocument from '@volter/world-core/generated/pack-facts.json' with { type: 'json' };
|
|
20
|
-
import { type Captured, type GitStep, type Journey, type JourneyStep, type LineStep, type SocketStep, type SqlStep, type RespStep, type MysqlStep, isMysql, isResp, at, captureAt, echoFailures, expandFills, expectFailures, fill, fillJson, formBody, isGit, isLine, isSql, isSocket, isWait, jwtAuthorization, oauth1Authorization, presignedPath, sigv4Headers, waitMs } from './journey-kit.ts';
|
|
21
|
+
import { type Captured, type GitStep, type Journey, type JourneyStep, type LineStep, type SocketStep, type SqlStep, type RespStep, type MysqlStep, type MongoStep, isMongo, isMysql, isResp, at, captureAt, echoFailures, expandFills, expectFailures, fill, fillJson, formBody, isGit, isLine, isSql, isSocket, isWait, jwtAuthorization, oauth1Authorization, presignedPath, sigv4Headers, waitMs } from './journey-kit.ts';
|
|
21
22
|
import { lanesOf, lineShape, shapeChecker, type LineConnection, type ShapeChecker } from './shape.ts';
|
|
22
23
|
import { walkPackDirectory } from './pack-directory.ts';
|
|
23
24
|
import { xmlMembers } from './xml-view.ts';
|
|
@@ -141,7 +142,7 @@ export type Failure = { step: string; beat: string; detail: string };
|
|
|
141
142
|
* reads (an event's delivery, a socket frame), the kernel's own `updatedAt`, which a raw row filtered on `_` alone
|
|
142
143
|
* carries (Slack's message events once did). The kernel's `deleted` is not read there: vendors send their own
|
|
143
144
|
* (Stripe's deleted customer's `deleted: true`). */
|
|
144
|
-
function leaked(v: unknown, vendorFields: Set<string>, at = '', unshaped = false): string[] {
|
|
145
|
+
export function leaked(v: unknown, vendorFields: Set<string>, at = '', unshaped = false): string[] {
|
|
145
146
|
if (Array.isArray(v)) return v.flatMap((x, i) => leaked(x, vendorFields, `${at}[${i}]`, unshaped)).slice(0, 5);
|
|
146
147
|
if (!v || typeof v !== 'object') return [];
|
|
147
148
|
return Object.entries(v).flatMap(([k, x]) => ((k.startsWith('_') || (unshaped && k === 'updatedAt')) && !vendorFields.has(k) ? [`${at}${at ? '.' : ''}${k}`] : leaked(x, vendorFields, `${at}${at ? '.' : ''}${k}`, unshaped))).slice(0, 5);
|
|
@@ -446,6 +447,61 @@ export async function walk(journey: Journey, root: string, collect: Failure[], o
|
|
|
446
447
|
finally { opts.hooks?.after?.(step as never, link, answered, collect.slice(before)); }
|
|
447
448
|
};
|
|
448
449
|
|
|
450
|
+
// A MongoDB step speaks the pack's MongoDB wire as an application's driver does (@volter/world-core/mongodb's client), on
|
|
451
|
+
// the same byte-stream adapter the owned TCP listener serves. The document codec is BSON, the library the pack
|
|
452
|
+
// installs: the walk loads it from the pack's own dependencies, so a step is encoded by the library the pack decodes with.
|
|
453
|
+
const mongoLinks = new Map<string, { client: ReturnType<typeof mongodb.mongoClient>; database: string; close: () => void }>();
|
|
454
|
+
type Bson = { serialize(document: Record<string, unknown>): Uint8Array; deserialize(bytes: Uint8Array, options: Record<string, unknown>): Record<string, unknown>; EJSON: { deserialize(value: unknown, options: { relaxed: boolean }): Record<string, unknown>; serialize(value: unknown, options: { relaxed: boolean }): Record<string, unknown> } };
|
|
455
|
+
let bson: Bson | undefined;
|
|
456
|
+
const bareNumber = (value: unknown, at = 'mongo'): string | undefined => {
|
|
457
|
+
if (typeof value === 'number') return at;
|
|
458
|
+
if (Array.isArray(value)) { for (const [i, item] of value.entries()) { const found = bareNumber(item, `${at}[${i}]`); if (found) return found; } return undefined; }
|
|
459
|
+
if (value && typeof value === 'object') for (const [key, item] of Object.entries(value)) { const found = bareNumber(item, `${at}.${key}`); if (found) return found; }
|
|
460
|
+
return undefined;
|
|
461
|
+
};
|
|
462
|
+
const mongoStep = async (step: MongoStep): Promise<void> => {
|
|
463
|
+
const before = collect.length; let answered: { status: number; body: unknown } = { status: 0, body: undefined };
|
|
464
|
+
const link = step.link ?? 'mongo';
|
|
465
|
+
opts.hooks?.before?.(step as never, link, step.mongo);
|
|
466
|
+
try {
|
|
467
|
+
if (!bson) {
|
|
468
|
+
const pack = walkPackDirectory(packsDir(), journey.vendor, dependenciesDir());
|
|
469
|
+
if (!pack) throw new Error(`no pack directory for ${journey.vendor} to load its BSON library from`);
|
|
470
|
+
bson = createRequire(join(pack, 'package.json'))('bson') as Bson;
|
|
471
|
+
}
|
|
472
|
+
const codec: mongodb.MongoCodec = { serialize: (document) => bson!.serialize(document), deserialize: (bytes) => bson!.deserialize(bytes, { promoteValues: false, promoteLongs: false, bsonRegExp: true }) };
|
|
473
|
+
let held = mongoLinks.get(link);
|
|
474
|
+
if (!held) {
|
|
475
|
+
if (!served?.stream) streams ??= await vendorStream(packsDir(), journey.vendor, dependenciesDir());
|
|
476
|
+
const factory = served?.stream ?? streams?.(root, {}); if (!factory) throw new Error(`no create<Name>TwinStream adapter enrolled for ${journey.vendor}`);
|
|
477
|
+
let out: number[] = [];
|
|
478
|
+
const conn = factory({ write: (b) => { out.push(...b); }, end: () => {} }, '127.0.0.1');
|
|
479
|
+
const exchange: mongodb.MongoExchange = async (bytes) => { conn.data(bytes); await conn.settled(); const got = Uint8Array.from(out); out = []; return got; };
|
|
480
|
+
held = { client: mongodb.mongoClient(exchange, codec), database: 'test', close: () => conn.close() };
|
|
481
|
+
mongoLinks.set(link, held);
|
|
482
|
+
}
|
|
483
|
+
if (step.database) held.database = fill(step.database, captured);
|
|
484
|
+
const written = fillJson(step.mongo, captured);
|
|
485
|
+
const bare = bareNumber(written);
|
|
486
|
+
if (bare) throw new Error(`${bare} is a bare JSON number: write its kind ({ "$numberInt": "…" }, $numberLong or $numberDouble)`);
|
|
487
|
+
const reply = await held.client.command(held.database, bson.EJSON.deserialize(written, { relaxed: false }));
|
|
488
|
+
const body = bson.EJSON.serialize(reply, { relaxed: false });
|
|
489
|
+
const ok = (body.ok as Record<string, unknown> | undefined), failed = !ok || Number(Object.values(ok)[0]) !== 1;
|
|
490
|
+
const code = failed ? Number(Object.values((body.code as Record<string, unknown> | undefined) ?? { none: Number.NaN })[0]) : undefined;
|
|
491
|
+
answered = { status: code ?? 0, body };
|
|
492
|
+
if (opts.verbose) console.log(` ${String(code ?? 'OK').padEnd(5)} ${link.padEnd(6)} ${String(Object.keys(step.mongo)[0] ?? '').padEnd(20)} ${step.name}`);
|
|
493
|
+
if (step.refused !== undefined ? code !== step.refused : failed) {
|
|
494
|
+
collect.push({ step: step.name, beat: 'STATUS', detail: failed ? `MongoDB refused it: ${code} ${String(body.codeName ?? '')} ${String(body.errmsg ?? '')}${step.refused ? ` (wanted ${step.refused})` : ''}` : `MongoDB completed it, wanted ${step.refused}` });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
for (const [name, dot] of Object.entries(step.capture ?? {})) { const value = captureAt(body, dot); if (value === undefined || value === null) collect.push({ step: step.name, beat: 'CAPTURE', detail: `nothing at "${dot}" in the answer to capture as {${name}}` }); else remember(name, value); }
|
|
498
|
+
try { for (const detail of expectFailures(step.expect ?? [], body, captured)) collect.push({ step: step.name, beat: 'EXPECT', detail }); }
|
|
499
|
+
catch (e) { collect.push({ step: step.name, beat: 'CAPTURE', detail: (e as Error).message }); }
|
|
500
|
+
if (step.close) { mongoLinks.get(link)?.close(); mongoLinks.delete(link); }
|
|
501
|
+
} catch (e) { collect.push({ step: step.name, beat: 'MONGO', detail: (e as Error).message }); }
|
|
502
|
+
finally { opts.hooks?.after?.(step as never, link, answered, collect.slice(before)); }
|
|
503
|
+
};
|
|
504
|
+
|
|
449
505
|
// A git step is a git client's clone or push on the pack's smart-HTTP plane (packages/twin-standard/src/walk/git-client.ts); its answer is
|
|
450
506
|
// checked as a request's is (status, captures, expectations).
|
|
451
507
|
const gitStep = async (step: GitStep): Promise<void> => {
|
|
@@ -679,6 +735,7 @@ export async function walk(journey: Journey, root: string, collect: Failure[], o
|
|
|
679
735
|
try {
|
|
680
736
|
for (const step of journey.steps) {
|
|
681
737
|
stepNow = step.name;
|
|
738
|
+
if (isMongo(step)) { await mongoStep(step); continue; }
|
|
682
739
|
if (isMysql(step)) { await mysqlStep(step); continue; }
|
|
683
740
|
if (isResp(step)) { await respStep(step); continue; }
|
|
684
741
|
if (isLine(step)) { await speak(step); continue; }
|
|
@@ -766,7 +823,11 @@ export async function walk(journey: Journey, root: string, collect: Failure[], o
|
|
|
766
823
|
let res: Answer;
|
|
767
824
|
if (deferred) {
|
|
768
825
|
pending.delete(step.await!);
|
|
769
|
-
|
|
826
|
+
const got = await deferred.answer.then((a) => ({ a }), (error: unknown) => ({ error }));
|
|
827
|
+
// a body the vendor broke off, as the collect expects (`breaks`): its words are the answer
|
|
828
|
+
if ('error' in got) { if (step.breaks === undefined || !String(got.error).includes(step.breaks)) collect.push({ step: step.name, beat: 'PENDING', detail: String(got.error) }); continue; }
|
|
829
|
+
// over a listener, which ends a broken stream as a short body, the ordinary checks hold the part that arrived
|
|
830
|
+
res = got.a;
|
|
770
831
|
} else res = await send({ ...step, body, headers, ...(formed ? { formed } : {}) }, path);
|
|
771
832
|
answered = { status: res.status, body: res.body, headers: res.headers };
|
|
772
833
|
if (opts.verbose) console.log(` ${String(res.status).padEnd(4)} ${step.method.padEnd(6)} ${path.slice(0, 70).padEnd(70)} ${step.name}`);
|
package/src/walk/journey-kit.ts
CHANGED
|
@@ -52,6 +52,8 @@ export type JourneyStep = {
|
|
|
52
52
|
defer?: string;
|
|
53
53
|
/** Collect a previously deferred request once, with all ordinary response checks. */
|
|
54
54
|
await?: string;
|
|
55
|
+
/** A collect whose answer's body breaks off with these words (a stream the vendor ends in an error), not arriving whole. */
|
|
56
|
+
breaks?: string;
|
|
55
57
|
body?: unknown;
|
|
56
58
|
/** A body of raw bytes (a gzip'd package, an image, an audio file), base64 in the journey and sent decoded: a
|
|
57
59
|
* string body is sent as UTF-8, which no binary format survives. A step carries `body` or `bodyBase64`, never both. */
|
|
@@ -215,7 +217,8 @@ export function expectFailures(expect: NonNullable<JourneyStep['expect']>, body:
|
|
|
215
217
|
if (e.contains !== undefined) {
|
|
216
218
|
const want = fill(e.contains, captured);
|
|
217
219
|
const text = typeof v === 'string' ? v : JSON.stringify(v ?? '');
|
|
218
|
-
|
|
220
|
+
// what it holds instead, its first characters, so a failure says what the twin answered
|
|
221
|
+
if (!text.includes(want)) out.push(`${e.path || 'the answer'} does not contain ${JSON.stringify(want)} (it begins ${JSON.stringify(text.replace(/\s+/g, ' ').slice(0, 160))})`);
|
|
219
222
|
}
|
|
220
223
|
// `notContains`: the value does not hold the text (a list that omits what the caller may not see)
|
|
221
224
|
if (e.notContains !== undefined) {
|
|
@@ -327,7 +330,7 @@ export type Journey = {
|
|
|
327
330
|
steps: Step[];
|
|
328
331
|
};
|
|
329
332
|
|
|
330
|
-
export type Step = JourneyStep | WaitStep | LineStep | GitStep | SocketStep | SqlStep | RespStep | MysqlStep;
|
|
333
|
+
export type Step = JourneyStep | WaitStep | LineStep | GitStep | SocketStep | SqlStep | RespStep | MysqlStep | MongoStep;
|
|
331
334
|
|
|
332
335
|
/** A step on a MySQL wire (the kernel's @volter/world-core/mysql, a pack's `create<Name>TwinStream`), spoken as an
|
|
333
336
|
* application's driver speaks it. `signIn` opens a named connection (`link`, `mysql` by default) with a user and
|
|
@@ -342,6 +345,19 @@ export type MysqlStep = {
|
|
|
342
345
|
};
|
|
343
346
|
export const isMysql = (step: Step): step is MysqlStep => 'mysql' in step || 'signIn' in step;
|
|
344
347
|
|
|
348
|
+
/** A step on a pack's MongoDB wire, sent as an application's driver sends it (`@volter/world-core/mongodb`'s client)
|
|
349
|
+
* on a named `link`, opened by the first step that names it. `mongo` is ONE command document, written in
|
|
350
|
+
* canonical extended JSON so that what a document holds is written as it is: an ObjectId as `{ "$oid": … }`, a date
|
|
351
|
+
* as `{ "$date": … }`, and a number with its kind (`{ "$numberInt": "1" }`, `$numberLong`, `$numberDouble`); a bare
|
|
352
|
+
* JSON number is refused, because JSON does not say which of three kinds it is. `database` is the database it is
|
|
353
|
+
* sent to (the link's last one, else `test`). Its answer is the reply document in the same form. A reply whose
|
|
354
|
+
* `ok` is 0 fails the step unless `refused` names its `code`. */
|
|
355
|
+
export type MongoStep = {
|
|
356
|
+
name: string; mongo: Record<string, unknown>; database?: string; link?: string; close?: boolean;
|
|
357
|
+
refused?: number; capture?: JourneyStep['capture']; expect?: JourneyStep['expect']; example?: string; exampleDiffers?: Record<string, string>;
|
|
358
|
+
};
|
|
359
|
+
export const isMongo = (step: Step): step is MongoStep => 'mongo' in step;
|
|
360
|
+
|
|
345
361
|
/** A typed request/reply stream step. `resp` is one command or a pipeline of commands, on a named connection.
|
|
346
362
|
* Captures and expectations see `{ replies, last, error }`; binary arguments use `{ base64 }`.
|
|
347
363
|
* `read` drains pushes or a blocking command's answer without sending bytes. */
|
|
@@ -530,7 +546,9 @@ export function fill(template: string, captured: Captured, graphqlDocument = fal
|
|
|
530
546
|
}
|
|
531
547
|
// a doubled brace is the vendor's own template syntax ({{sample.output_text}} in an OpenAI grader), not a capture; a
|
|
532
548
|
// capture's name starts with a letter, so a pattern's count (`[A-Za-z0-9]{40}` in an expectation) is no capture
|
|
533
|
-
|
|
549
|
+
// `{literal:word}` is `{word}` as the vendor's page prints it (a Redis hash tag, `{queue}`), never a capture
|
|
550
|
+
return encoded.replace(/\{literal:([^{}]*)\}|\{\{[^{}]*\}\}|\{([A-Za-z_][A-Za-z0-9_.]*)\}/g, (m, literal: string | undefined, key: string | undefined, offset: number) => {
|
|
551
|
+
if (literal !== undefined) return `{${literal}}`;
|
|
534
552
|
if (selections.has(offset)) return m;
|
|
535
553
|
if (key === undefined) return m;
|
|
536
554
|
const v = captured[key];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Unfilled scalar type words in schema-rendered examples. Real values and enum words remain literal. */
|
|
2
|
+
const BOOLEAN = Symbol('rendered boolean');
|
|
3
|
+
const INTEGER = Symbol('rendered integer');
|
|
4
|
+
const NUMBER = Symbol('rendered number');
|
|
5
|
+
export function renderedScalar(value: unknown, types: unknown[], enumerated: boolean): symbol | undefined {
|
|
6
|
+
if (enumerated || typeof value !== 'string') return undefined;
|
|
7
|
+
if (value === 'boolean' && types.includes('boolean')) return BOOLEAN;
|
|
8
|
+
if (value === 'integer' && types.includes('integer')) return INTEGER;
|
|
9
|
+
if (value === 'number' && types.includes('number')) return NUMBER;
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
export function isRenderedScalar(value: unknown): boolean { return value === BOOLEAN || value === INTEGER || value === NUMBER; }
|
|
13
|
+
export function matchesRenderedScalar(marker: unknown, value: unknown): boolean {
|
|
14
|
+
if (marker === BOOLEAN) return typeof value === 'boolean';
|
|
15
|
+
if (marker === INTEGER) return typeof value === 'bigint' || (typeof value === 'number' && Number.isInteger(value));
|
|
16
|
+
return marker === NUMBER && typeof value === 'number' && Number.isFinite(value);
|
|
17
|
+
}
|
package/src/walk/sse-example.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseExactJson } from '@volter/world-core/exact-json';
|
|
2
2
|
/** Match a published SDK-decoded SSE sample to a real HTTP frame; preserve non-SSE answers and mismatches. */
|
|
3
3
|
export function sseExampleAnswer(example: unknown, answer: unknown, matches: (frame: unknown) => boolean): unknown {
|
|
4
|
+
if (typeof example === 'string' && typeof answer === 'string') return streamSample(example, answer);
|
|
4
5
|
if (typeof answer !== 'string' || !example || typeof example !== 'object') return answer;
|
|
5
6
|
const wrapped = 'data' in example;
|
|
6
7
|
if (!wrapped && !('type' in example) && !('object' in example)) return answer;
|
|
@@ -23,3 +24,39 @@ export function sseExampleAnswer(example: unknown, answer: unknown, matches: (fr
|
|
|
23
24
|
const candidates = relevant.map((frame) => wrapped ? frame : frame.data);
|
|
24
25
|
return candidates.find(matches) ?? candidates[0] ?? answer;
|
|
25
26
|
}
|
|
27
|
+
|
|
28
|
+
type Frame = { data: unknown; event?: string };
|
|
29
|
+
/** A stream's frames: each block's `event:` and joined `data:` lines, or a block that is JSON alone (a page printing the
|
|
30
|
+
* chunks without their framing); `[DONE]`, an elision (`...`, `....`) and anything else not JSON are no frame. */
|
|
31
|
+
function framesOf(text: string): Frame[] {
|
|
32
|
+
const frames: Frame[] = [];
|
|
33
|
+
for (const block of text.replace(/\r\n/g, '\n').split(/\n\s*\n/)) {
|
|
34
|
+
const lines = block.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
35
|
+
const framed = lines.some((l) => l.startsWith('data:'));
|
|
36
|
+
const data = framed ? lines.filter((l) => l.startsWith('data:')).map((l) => l.slice(5).trim()).join('\n') : lines.join('\n');
|
|
37
|
+
if (!data || data === '[DONE]' || /^\.{3,}$/.test(data)) continue;
|
|
38
|
+
const event = lines.find((l) => l.startsWith('event:'))?.slice(6).trim();
|
|
39
|
+
try { frames.push({ data: parseExactJson(data), ...(event ? { event } : {}) }); } catch { /* not a JSON frame */ }
|
|
40
|
+
}
|
|
41
|
+
return frames;
|
|
42
|
+
}
|
|
43
|
+
const kindOf = (v: unknown): string => (v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v);
|
|
44
|
+
/** Whether an answer's frame holds a published frame's keys with the same kinds of value (a null published value admits
|
|
45
|
+
* any), its type or object the same. */
|
|
46
|
+
function holds(published: unknown, got: unknown): boolean {
|
|
47
|
+
if (!published || typeof published !== 'object' || Array.isArray(published)) return kindOf(published) === kindOf(got) || published === null;
|
|
48
|
+
if (!got || typeof got !== 'object' || Array.isArray(got)) return false;
|
|
49
|
+
const p = published as Record<string, unknown>, g = got as Record<string, unknown>;
|
|
50
|
+
for (const key of ['type', 'object']) if (typeof p[key] === 'string' && p[key] !== g[key]) return false;
|
|
51
|
+
return Object.entries(p).every(([k, v]) => k in g && (v === null || kindOf(v) === kindOf(g[k]) || (kindOf(v) === 'object' && holds(v, g[k]))));
|
|
52
|
+
}
|
|
53
|
+
/** A published stream printed as text (a page's sample of a stream, its middle elided): each frame it shows has a frame
|
|
54
|
+
* of the answer holding it, the answer then standing for the sample; else the answer, unchanged, which the literal
|
|
55
|
+
* comparison reports. */
|
|
56
|
+
function streamSample(example: string, answer: string): unknown {
|
|
57
|
+
const published = framesOf(example);
|
|
58
|
+
if (published.length < 2) return answer;
|
|
59
|
+
const frames = framesOf(answer);
|
|
60
|
+
const held = published.every((p) => frames.some((f) => (!p.event || p.event === f.event) && holds(p.data, f.data)));
|
|
61
|
+
return held ? example : answer;
|
|
62
|
+
}
|