@volter/twin-standard 1.0.29 → 1.0.31
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/dist/src/archive-source.d.ts +13 -0
- package/dist/src/archive-source.js +176 -0
- package/dist/src/check-sources.d.ts +19 -2
- package/dist/src/check-sources.js +95 -37
- package/dist/src/cli.js +52 -9
- package/dist/src/clients/ai-gateway.d.ts +1 -0
- package/dist/src/clients/ai-gateway.js +3 -0
- package/dist/src/clients/anthropic-messages.d.ts +10 -0
- package/dist/src/clients/anthropic-messages.js +52 -0
- package/dist/src/clients/anthropic.d.ts +1 -0
- package/dist/src/clients/anthropic.js +3 -0
- package/dist/src/clients/aws.d.ts +2 -0
- package/dist/src/clients/aws.js +137 -0
- package/dist/src/clients/cerebras.d.ts +1 -0
- package/dist/src/clients/cerebras.js +3 -0
- package/dist/src/clients/clerk.d.ts +2 -0
- package/dist/src/clients/clerk.js +99 -0
- package/dist/src/clients/cloudflare.d.ts +2 -0
- package/dist/src/clients/cloudflare.js +86 -0
- package/dist/src/clients/elevenlabs.d.ts +2 -0
- package/dist/src/clients/elevenlabs.js +39 -0
- package/dist/src/clients/gemini.d.ts +2 -0
- package/dist/src/clients/gemini.js +92 -0
- package/dist/src/clients/github.d.ts +2 -0
- package/dist/src/clients/github.js +71 -0
- package/dist/src/clients/groq.d.ts +1 -0
- package/dist/src/clients/groq.js +3 -0
- package/dist/src/clients/harness.d.ts +36 -0
- package/dist/src/clients/harness.js +74 -0
- package/dist/src/clients/merge.d.ts +1 -0
- package/dist/src/clients/merge.js +8 -0
- package/dist/src/clients/openai-compatible.d.ts +9 -0
- package/dist/src/clients/openai-compatible.js +51 -0
- package/dist/src/clients/openai.d.ts +2 -0
- package/dist/src/clients/openai.js +65 -0
- package/dist/src/clients/openrouter.d.ts +1 -0
- package/dist/src/clients/openrouter.js +3 -0
- package/dist/src/clients/perplexity.d.ts +1 -0
- package/dist/src/clients/perplexity.js +3 -0
- package/dist/src/clients/planetscale.d.ts +2 -0
- package/dist/src/clients/planetscale.js +109 -0
- package/dist/src/clients/posthog.d.ts +2 -0
- package/dist/src/clients/posthog.js +145 -0
- package/dist/src/clients/run.d.ts +1 -0
- package/dist/src/clients/run.js +25 -0
- package/dist/src/clients/sentry.d.ts +2 -0
- package/dist/src/clients/sentry.js +167 -0
- package/dist/src/clients/slack.d.ts +2 -0
- package/dist/src/clients/slack.js +107 -0
- package/dist/src/clients/stripe.d.ts +2 -0
- package/dist/src/clients/stripe.js +161 -0
- package/dist/src/clients/tinybird.d.ts +2 -0
- package/dist/src/clients/tinybird.js +71 -0
- package/dist/src/clients/tunnel.d.ts +2 -0
- package/dist/src/clients/tunnel.js +99 -0
- package/dist/src/clients/upstash.d.ts +2 -0
- package/dist/src/clients/upstash.js +143 -0
- package/dist/src/clients/volteridentity.d.ts +2 -0
- package/dist/src/clients/volteridentity.js +128 -0
- package/dist/src/clients/xai.d.ts +2 -0
- package/dist/src/clients/xai.js +23 -0
- package/dist/src/conformance.d.ts +33 -0
- package/dist/src/conformance.js +520 -0
- package/dist/src/derive.js +113 -22
- package/dist/src/gate.js +24 -17
- package/dist/src/index.d.ts +5 -3
- package/dist/src/index.js +5 -3
- package/dist/src/lanes.d.ts +2 -0
- package/dist/src/lanes.js +7 -0
- package/dist/src/p3-rules.js +0 -2
- package/dist/src/pack-files.d.ts +2 -0
- package/dist/src/pack-files.js +4 -0
- package/dist/src/protocol-3.d.ts +1 -0
- package/dist/src/protocol-3.js +83 -24
- package/dist/src/published.d.ts +6 -0
- package/dist/src/published.js +65 -11
- package/dist/src/spec-documents.js +2 -1
- package/dist/src/spec-ir-client.d.ts +5 -1
- package/dist/src/spec-ir-client.js +12 -2
- package/dist/src/spec-ir.d.ts +7 -2
- package/dist/src/spec-ir.js +42 -10
- package/dist/src/verdict.d.ts +14 -0
- package/dist/src/verdict.js +30 -0
- package/dist/src/walk/behavior-journey.d.ts +47 -0
- package/dist/src/walk/behavior-journey.js +1138 -0
- package/dist/src/walk/git-client.d.ts +26 -0
- package/dist/src/walk/git-client.js +139 -0
- package/dist/src/walk/journey-kit.d.ts +445 -0
- package/dist/src/walk/journey-kit.js +743 -0
- package/dist/src/walk/shape.d.ts +54 -0
- package/dist/src/walk/shape.js +210 -0
- package/dist/src/walk/sse-example.d.ts +2 -0
- package/dist/src/walk/sse-example.js +32 -0
- package/dist/src/walk/vendor-fetch.d.ts +20 -0
- package/dist/src/walk/vendor-fetch.js +92 -0
- package/dist/src/walk/walk-database.d.ts +12 -0
- package/dist/src/walk/walk-database.js +96 -0
- package/dist/src/walk/xml-view.d.ts +2 -0
- package/dist/src/walk/xml-view.js +55 -0
- package/package.json +39 -3
- package/src/archive-source.ts +141 -0
- package/src/check-sources.ts +83 -33
- package/src/cli.ts +40 -8
- package/src/clients/ai-gateway.ts +4 -0
- package/src/clients/anthropic-messages.ts +60 -0
- package/src/clients/anthropic.ts +4 -0
- package/src/clients/aws.ts +119 -0
- package/src/clients/cerebras.ts +4 -0
- package/src/clients/clerk.ts +97 -0
- package/src/clients/cloudflare.ts +80 -0
- package/src/clients/elevenlabs.ts +38 -0
- package/src/clients/gemini.ts +89 -0
- package/src/clients/github.ts +67 -0
- package/src/clients/groq.ts +4 -0
- package/src/clients/harness.ts +101 -0
- package/src/clients/merge.ts +9 -0
- package/src/clients/openai-compatible.ts +59 -0
- package/src/clients/openai.ts +66 -0
- package/src/clients/openrouter.ts +4 -0
- package/src/clients/perplexity.ts +4 -0
- package/src/clients/planetscale.ts +98 -0
- package/src/clients/posthog.ts +132 -0
- package/src/clients/run.ts +18 -0
- package/src/clients/sentry.ts +157 -0
- package/src/clients/slack.ts +98 -0
- package/src/clients/stripe.ts +129 -0
- package/src/clients/tinybird.ts +69 -0
- package/src/clients/tunnel.ts +79 -0
- package/src/clients/upstash.ts +137 -0
- package/src/clients/volteridentity.ts +108 -0
- package/src/clients/xai.ts +25 -0
- package/src/conformance.ts +446 -0
- package/src/derive.ts +87 -16
- package/src/gate.ts +22 -15
- package/src/index.ts +5 -3
- package/src/lanes.ts +7 -0
- package/src/p3-rules.ts +0 -2
- package/src/pack-files.ts +4 -0
- package/src/protocol-3.ts +77 -22
- package/src/published.ts +59 -11
- package/src/spec-documents.ts +2 -1
- package/src/spec-ir-client.ts +15 -4
- package/src/spec-ir.ts +43 -5
- package/src/verdict.ts +28 -0
- package/src/walk/behavior-journey.ts +806 -0
- package/src/walk/git-client.ts +138 -0
- package/src/walk/journey-kit.ts +858 -0
- package/src/walk/shape.ts +208 -0
- package/src/walk/sse-example.ts +25 -0
- package/src/walk/vendor-fetch.ts +80 -0
- package/src/walk/walk-database.ts +97 -0
- package/src/walk/xml-view.ts +44 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type ArchiveSource = {
|
|
2
|
+
url: string;
|
|
3
|
+
sha256: string;
|
|
4
|
+
member: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function archiveSource(locator: string): ArchiveSource | undefined;
|
|
7
|
+
/** One cache per source-check run; multiple pinned members share the archive's one download. */
|
|
8
|
+
export declare function archiveReader(): (source: ArchiveSource) => Promise<{
|
|
9
|
+
status: number;
|
|
10
|
+
raw: string;
|
|
11
|
+
sha256?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
}>;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Pinned vendor archive evidence. Members are read in memory, never extracted to the filesystem.
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { gunzipSync, inflateRawSync } from 'node:zlib';
|
|
4
|
+
export function archiveSource(locator) {
|
|
5
|
+
const m = /^archive:(https?:\/\/\S+)#sha256=([a-fA-F0-9]{64})!\/(\S+)$/.exec(locator);
|
|
6
|
+
if (!m || m[3].startsWith('/') || m[3].includes('\\') || m[3].split('/').some(p => p === '..' || p === '.' || !p))
|
|
7
|
+
return undefined;
|
|
8
|
+
return { url: m[1], sha256: m[2].toLowerCase(), member: m[3] };
|
|
9
|
+
}
|
|
10
|
+
const hashOf = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
11
|
+
const stringAt = (bytes, start, size) => bytes.subarray(start, start + size).toString('utf8').replace(/\0.*$/s, '');
|
|
12
|
+
function zipMember(bytes, member) {
|
|
13
|
+
// The central directory supplies sizes even when local headers use data descriptors.
|
|
14
|
+
let end = bytes.length - 22;
|
|
15
|
+
while (end >= Math.max(0, bytes.length - 65557) && bytes.readUInt32LE(end) !== 0x06054b50)
|
|
16
|
+
end--;
|
|
17
|
+
if (end < Math.max(0, bytes.length - 65557))
|
|
18
|
+
throw new Error('ZIP has no end directory');
|
|
19
|
+
if (bytes.readUInt16LE(end + 4) || bytes.readUInt16LE(end + 6))
|
|
20
|
+
throw new Error('multi-disk ZIP is not supported');
|
|
21
|
+
let count = bytes.readUInt16LE(end + 10), offset = bytes.readUInt32LE(end + 16);
|
|
22
|
+
if (count === 0xffff || offset === 0xffffffff) {
|
|
23
|
+
const loc = end - 20;
|
|
24
|
+
if (loc < 0 || bytes.readUInt32LE(loc) !== 0x07064b50)
|
|
25
|
+
throw new Error('ZIP64 has no directory locator');
|
|
26
|
+
const pos = Number(bytes.readBigUInt64LE(loc + 8));
|
|
27
|
+
if (!Number.isSafeInteger(pos) || bytes.readUInt32LE(pos) !== 0x06064b50)
|
|
28
|
+
throw new Error('invalid ZIP64 directory');
|
|
29
|
+
count = Number(bytes.readBigUInt64LE(pos + 32));
|
|
30
|
+
offset = Number(bytes.readBigUInt64LE(pos + 48));
|
|
31
|
+
}
|
|
32
|
+
for (let i = 0; i < count; i++) {
|
|
33
|
+
if (bytes.readUInt32LE(offset) !== 0x02014b50)
|
|
34
|
+
throw new Error('invalid ZIP directory member');
|
|
35
|
+
const flags = bytes.readUInt16LE(offset + 8), method = bytes.readUInt16LE(offset + 10);
|
|
36
|
+
let size = bytes.readUInt32LE(offset + 20), plain = bytes.readUInt32LE(offset + 24), local = bytes.readUInt32LE(offset + 42);
|
|
37
|
+
const names = bytes.readUInt16LE(offset + 28), extras = bytes.readUInt16LE(offset + 30), comments = bytes.readUInt16LE(offset + 32);
|
|
38
|
+
const name = stringAt(bytes, offset + 46, names);
|
|
39
|
+
if (name === member) {
|
|
40
|
+
if (flags & 1)
|
|
41
|
+
throw new Error('encrypted ZIP member');
|
|
42
|
+
// ZIP64 values appear only for fields whose ordinary value is the sentinel.
|
|
43
|
+
if (size === 0xffffffff || plain === 0xffffffff || local === 0xffffffff) {
|
|
44
|
+
let at = offset + 46 + names, stop = at + extras;
|
|
45
|
+
while (at < stop) {
|
|
46
|
+
const id = bytes.readUInt16LE(at), len = bytes.readUInt16LE(at + 2);
|
|
47
|
+
if (id === 1) {
|
|
48
|
+
let v = at + 4;
|
|
49
|
+
if (plain === 0xffffffff) {
|
|
50
|
+
plain = Number(bytes.readBigUInt64LE(v));
|
|
51
|
+
v += 8;
|
|
52
|
+
}
|
|
53
|
+
if (size === 0xffffffff) {
|
|
54
|
+
size = Number(bytes.readBigUInt64LE(v));
|
|
55
|
+
v += 8;
|
|
56
|
+
}
|
|
57
|
+
if (local === 0xffffffff)
|
|
58
|
+
local = Number(bytes.readBigUInt64LE(v));
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
at += 4 + len;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (![size, plain, local].every(Number.isSafeInteger) || bytes.readUInt32LE(local) !== 0x04034b50)
|
|
65
|
+
throw new Error('invalid ZIP local member');
|
|
66
|
+
const mode = bytes.readUInt32LE(offset + 38) >>> 16;
|
|
67
|
+
if ((mode & 0xf000) === 0xa000 || name.endsWith('/'))
|
|
68
|
+
throw new Error('ZIP member is not a regular file');
|
|
69
|
+
const start = local + 30 + bytes.readUInt16LE(local + 26) + bytes.readUInt16LE(local + 28);
|
|
70
|
+
if (start + size > bytes.length)
|
|
71
|
+
throw new Error('truncated ZIP member');
|
|
72
|
+
const data = bytes.subarray(start, start + size);
|
|
73
|
+
const result = method === 0 ? data : method === 8 ? inflateRawSync(data) : undefined;
|
|
74
|
+
if (!result)
|
|
75
|
+
throw new Error(`unsupported ZIP compression ${method}`);
|
|
76
|
+
if (result.length !== plain)
|
|
77
|
+
throw new Error('ZIP member size differs from its directory');
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
offset += 46 + names + extras + comments;
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`archive has no member ${member}`);
|
|
83
|
+
}
|
|
84
|
+
function tarNumber(bytes) {
|
|
85
|
+
if (bytes[0] & 0x80) {
|
|
86
|
+
let n = BigInt(bytes[0] & 0x7f);
|
|
87
|
+
for (const b of bytes.subarray(1))
|
|
88
|
+
n = (n << 8n) | BigInt(b);
|
|
89
|
+
const value = Number(n);
|
|
90
|
+
if (!Number.isSafeInteger(value))
|
|
91
|
+
throw new Error('tar size is too large');
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
const s = bytes.toString('ascii').replace(/\0.*$/s, '').trim();
|
|
95
|
+
if (s && !/^[0-7]+$/.test(s))
|
|
96
|
+
throw new Error('invalid tar number');
|
|
97
|
+
return s ? parseInt(s, 8) : 0;
|
|
98
|
+
}
|
|
99
|
+
function paxFields(bytes) {
|
|
100
|
+
const out = {};
|
|
101
|
+
let at = 0;
|
|
102
|
+
while (at < bytes.length) {
|
|
103
|
+
const space = bytes.indexOf(32, at), n = Number(bytes.subarray(at, space).toString());
|
|
104
|
+
if (space < at || !Number.isInteger(n) || n <= space - at + 1 || at + n > bytes.length)
|
|
105
|
+
throw new Error('invalid tar PAX record');
|
|
106
|
+
const entry = bytes.subarray(space + 1, at + n - 1).toString('utf8'), eq = entry.indexOf('=');
|
|
107
|
+
if (eq >= 0)
|
|
108
|
+
out[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
109
|
+
at += n;
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
function tarMember(bytes, member) {
|
|
114
|
+
let at = 0, longName, local = {}, global = {};
|
|
115
|
+
while (at + 512 <= bytes.length) {
|
|
116
|
+
const header = bytes.subarray(at, at + 512);
|
|
117
|
+
if (header.every(b => b === 0))
|
|
118
|
+
break;
|
|
119
|
+
const sum = header.reduce((n, b, i) => n + (i >= 148 && i < 156 ? 32 : b), 0);
|
|
120
|
+
if (sum !== tarNumber(header.subarray(148, 156)))
|
|
121
|
+
throw new Error('invalid tar header checksum');
|
|
122
|
+
const kind = stringAt(header, 156, 1), baseSize = tarNumber(header.subarray(124, 136));
|
|
123
|
+
const meta = kind === 'x' || kind === 'g' || kind === 'L' || kind === 'K';
|
|
124
|
+
const attributes = { ...global, ...local };
|
|
125
|
+
const size = !meta && attributes.size !== undefined ? Number(attributes.size) : baseSize;
|
|
126
|
+
if (!Number.isSafeInteger(size) || size < 0 || at + 512 + size > bytes.length)
|
|
127
|
+
throw new Error('invalid tar member size');
|
|
128
|
+
const data = bytes.subarray(at + 512, at + 512 + size);
|
|
129
|
+
if (kind === 'x')
|
|
130
|
+
local = paxFields(data);
|
|
131
|
+
else if (kind === 'g')
|
|
132
|
+
global = { ...global, ...paxFields(data) };
|
|
133
|
+
else if (kind === 'L')
|
|
134
|
+
longName = data.toString('utf8').replace(/\0.*$/s, '');
|
|
135
|
+
else if (kind !== 'K') {
|
|
136
|
+
const prefix = stringAt(header, 345, 155);
|
|
137
|
+
const name = attributes.path ?? longName ?? `${prefix ? `${prefix}/` : ''}${stringAt(header, 0, 100)}`;
|
|
138
|
+
if (name.replace(/^\.\//, '') === member) {
|
|
139
|
+
if (kind !== '' && kind !== '0' && kind !== '7')
|
|
140
|
+
throw new Error('tar member is not a regular file');
|
|
141
|
+
return data;
|
|
142
|
+
}
|
|
143
|
+
local = {};
|
|
144
|
+
longName = undefined;
|
|
145
|
+
}
|
|
146
|
+
at += 512 + Math.ceil(size / 512) * 512;
|
|
147
|
+
}
|
|
148
|
+
throw new Error(`archive has no member ${member}`);
|
|
149
|
+
}
|
|
150
|
+
/** One cache per source-check run; multiple pinned members share the archive's one download. */
|
|
151
|
+
export function archiveReader() {
|
|
152
|
+
const downloads = new Map();
|
|
153
|
+
return async (source) => {
|
|
154
|
+
try {
|
|
155
|
+
let pending = downloads.get(source.url);
|
|
156
|
+
if (!pending) {
|
|
157
|
+
pending = (async () => {
|
|
158
|
+
const res = await fetch(source.url, { redirect: 'follow', signal: AbortSignal.timeout(30000), headers: { 'user-agent': 'volter-source-check/1.0 (+https://volter.ai)' } });
|
|
159
|
+
return { status: res.status, bytes: Buffer.from(await res.arrayBuffer()) };
|
|
160
|
+
})();
|
|
161
|
+
downloads.set(source.url, pending);
|
|
162
|
+
}
|
|
163
|
+
const { status, bytes } = await pending;
|
|
164
|
+
if (status !== 200)
|
|
165
|
+
return { status, raw: '' };
|
|
166
|
+
const sha256 = hashOf(bytes);
|
|
167
|
+
if (sha256 !== source.sha256)
|
|
168
|
+
return { status: 0, raw: '', sha256, error: `SHA256 differs: expected ${source.sha256}, received ${sha256}` };
|
|
169
|
+
const member = bytes.readUInt32LE(0) === 0x04034b50 ? zipMember(bytes, source.member) : tarMember(bytes[0] === 0x1f && bytes[1] === 0x8b ? gunzipSync(bytes) : bytes, source.member);
|
|
170
|
+
return { status, raw: member.toString('utf8'), sha256 };
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
return { status: 0, raw: '', error: err instanceof Error ? err.message : String(err) };
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -3,6 +3,8 @@ export type SourceRecord = {
|
|
|
3
3
|
status: number;
|
|
4
4
|
checked: string;
|
|
5
5
|
quotes?: Record<string, boolean>;
|
|
6
|
+
sha256?: string;
|
|
7
|
+
error?: string;
|
|
6
8
|
};
|
|
7
9
|
export type Patch = {
|
|
8
10
|
op: string;
|
|
@@ -11,13 +13,14 @@ export type Patch = {
|
|
|
11
13
|
source?: {
|
|
12
14
|
url?: string;
|
|
13
15
|
spec?: string;
|
|
16
|
+
recording?: string;
|
|
14
17
|
quote: string;
|
|
15
18
|
};
|
|
16
19
|
};
|
|
17
20
|
/** Every URL in a text, without trailing punctuation. */
|
|
18
21
|
export declare const urlsIn: (text: string) => string[];
|
|
19
|
-
/** The citations a pack's
|
|
20
|
-
*
|
|
22
|
+
/** The citations beside vendor rules in a pack's handwritten source, including hosted screens and engines.
|
|
23
|
+
* Generated source and test files do not supply author evidence. */
|
|
21
24
|
export declare function handlerCitations(pkg: string): Array<{
|
|
22
25
|
where: string;
|
|
23
26
|
source: string;
|
|
@@ -27,7 +30,21 @@ export declare const quotedPage: (source: string) => {
|
|
|
27
30
|
url: string;
|
|
28
31
|
quote: string;
|
|
29
32
|
} | undefined;
|
|
33
|
+
/** Evidence is usable offline only when the page answered or the archive digest matched its pin. */
|
|
34
|
+
export declare function sourceHeld(url: string, record: SourceRecord | undefined): boolean;
|
|
30
35
|
/** What a pack cites: each URL, and the quotes a patch says the URL's page holds. */
|
|
31
36
|
export declare function citations(pkg: string): Promise<Map<string, Set<string>>>;
|
|
32
37
|
/** Fetch and record the sources the unit at `args[0]` cites (`--recheck`: every one, not only those due). */
|
|
38
|
+
/** A documentation page as the citation check reads it: fetched as a browser-like agent (asked again as this tool where
|
|
39
|
+
* that is refused), an HTML page's text without its markup, a plain page as it is. `twin-standard read-page <url>` prints
|
|
40
|
+
* it, so an author reads a page through the same path its citations are checked on; `--raw` prints the page's own bytes
|
|
41
|
+
* (a spec writer that reads the page's tables). */
|
|
42
|
+
export declare function readPage(url: string): Promise<{
|
|
43
|
+
status: number;
|
|
44
|
+
text: string;
|
|
45
|
+
readable: string;
|
|
46
|
+
raw: string;
|
|
47
|
+
sha256?: string;
|
|
48
|
+
error?: string;
|
|
49
|
+
}>;
|
|
33
50
|
export declare function checkSources(args: string[]): Promise<void>;
|
|
@@ -18,27 +18,51 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
18
18
|
//
|
|
19
19
|
// Usage: twin-standard check-sources <pack dir> [--recheck] (twin-world: bun scripts/check-sources.ts <vendor>)
|
|
20
20
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
21
|
-
import { join } from 'node:path';
|
|
21
|
+
import { dirname, join } from 'node:path';
|
|
22
22
|
import { unitAt } from "./unit.js";
|
|
23
|
+
import { archiveReader, archiveSource } from "./archive-source.js";
|
|
23
24
|
/** Every URL in a text, without trailing punctuation. */
|
|
24
|
-
export const urlsIn = (text) => (text.match(/https?:\/\/[^\s"'<>]+/g) ?? []).map((u) => u.replace(/[),.;:\]]+$/, ''));
|
|
25
|
-
/** The citations a pack's
|
|
26
|
-
*
|
|
25
|
+
export const urlsIn = (text) => (text.match(/(?:archive:)?https?:\/\/[^\s"'<>]+/g) ?? []).map((u) => u.replace(/[),.;:\]]+$/, ''));
|
|
26
|
+
/** The citations beside vendor rules in a pack's handwritten source, including hosted screens and engines.
|
|
27
|
+
* Generated source and test files do not supply author evidence. */
|
|
27
28
|
export function handlerCitations(pkg) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const files = [];
|
|
30
|
+
const walk = (dir, prefix) => {
|
|
31
|
+
if (!existsSync(dir))
|
|
32
|
+
return;
|
|
33
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
34
|
+
const name = `${prefix}${entry.name}`, at = join(dir, entry.name);
|
|
35
|
+
if (entry.isDirectory()) {
|
|
36
|
+
if (entry.name !== 'generated')
|
|
37
|
+
walk(at, `${name}/`);
|
|
38
|
+
}
|
|
39
|
+
else if (/\.tsx?$/.test(entry.name) && !/\.(?:test|uitest|spec)\.tsx?$/.test(entry.name))
|
|
40
|
+
files.push({ name, at });
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
walk(join(pkg, 'src'), '');
|
|
44
|
+
const comments = files.flatMap(({ name: f, at }) => readFileSync(at, 'utf8').split('\n').flatMap((line, i) => {
|
|
33
45
|
const m = /(?:^\s*(?:\/\/|\/?\*+)|[\s;,)}\]]\/\/)\s*source:\s*(.+?)\s*(?:\*\/)?\s*$/i.exec(line);
|
|
34
46
|
return m ? [{ where: `${f}:${i + 1}`, source: m[1] }] : [];
|
|
35
47
|
}));
|
|
48
|
+
const examplesPath = join(pkg, 'spec', 'doc-examples.json');
|
|
49
|
+
const examples = existsSync(examplesPath) ? JSON.parse(readFileSync(examplesPath, 'utf8')) : [];
|
|
50
|
+
return [...comments, ...examples.flatMap((e, i) => e.quote !== undefined && (e.source?.startsWith('archive:') || /raw\.githubusercontent\.com\/.*\/tests?\//.test(e.source ?? '')) ? [{ where: `spec/doc-examples.json[${i}]`, source: `${e.source} "${e.quote.replace(/"/g, '\\"')}"` }] : [])];
|
|
36
51
|
}
|
|
37
52
|
/** A handler citation's page and the words it says the page holds (`https://… "quote"`), when it quotes. */
|
|
38
53
|
export const quotedPage = (source) => {
|
|
39
|
-
const m = /^(https?:\/\/\S+)\s+"([\s\S]+)"$/.exec(source.trim());
|
|
40
|
-
return m && m[2].trim() ? { url: m[1].replace(/[),.;:\]]+$/, ''), quote: m[2].replace(/\\"/g, '"') } : undefined;
|
|
54
|
+
const m = /^((?:archive:)?https?:\/\/\S+)\s+"([\s\S]+)"$/.exec(source.trim());
|
|
55
|
+
return m && m[2].trim() && (!m[1].startsWith('archive:') || archiveSource(m[1])) ? { url: m[1].replace(/[),.;:\]]+$/, ''), quote: m[2].replace(/\\"/g, '"') } : undefined;
|
|
41
56
|
};
|
|
57
|
+
/** Evidence is usable offline only when the page answered or the archive digest matched its pin. */
|
|
58
|
+
export function sourceHeld(url, record) {
|
|
59
|
+
if (record?.status !== 200)
|
|
60
|
+
return false;
|
|
61
|
+
if (!url.startsWith('archive:'))
|
|
62
|
+
return true;
|
|
63
|
+
const archive = archiveSource(url);
|
|
64
|
+
return !!archive && record.sha256 === archive.sha256;
|
|
65
|
+
}
|
|
42
66
|
/** What a pack cites: each URL, and the quotes a patch says the URL's page holds. */
|
|
43
67
|
export async function citations(pkg) {
|
|
44
68
|
const cited = new Map();
|
|
@@ -46,17 +70,24 @@ export async function citations(pkg) {
|
|
|
46
70
|
cited.set(url, new Set()); if (quote)
|
|
47
71
|
cited.get(url).add(quote); };
|
|
48
72
|
const { manifest } = (await import(__rewriteRelativeImportExtension(join(pkg, 'src', 'manifest.ts'))));
|
|
49
|
-
|
|
73
|
+
// a vendor of lanes' root declares no resources of its own (its lanes do): only its handlers' and manifest's citations
|
|
74
|
+
for (const r of Object.values(manifest.resources ?? {}))
|
|
50
75
|
for (const f of Object.values(r.state ?? {}))
|
|
51
|
-
for (const t of f.transitions)
|
|
52
|
-
|
|
53
|
-
|
|
76
|
+
for (const t of f.transitions) {
|
|
77
|
+
const source = String(t.source ?? ''), q = quotedPage(source);
|
|
78
|
+
if (q?.url.startsWith('archive:'))
|
|
79
|
+
cite(q.url, q.quote);
|
|
80
|
+
else
|
|
81
|
+
for (const u of urlsIn(source))
|
|
82
|
+
cite(u);
|
|
83
|
+
}
|
|
54
84
|
for (const c of handlerCitations(pkg)) {
|
|
55
85
|
const q = quotedPage(c.source);
|
|
56
86
|
if (q)
|
|
57
87
|
cite(q.url, q.quote);
|
|
88
|
+
// a URL inside the quoted words (`spec:<ref> "… https://…"`) is what the spec says, not a page cited
|
|
58
89
|
else
|
|
59
|
-
for (const u of urlsIn(c.source))
|
|
90
|
+
for (const u of urlsIn(c.source.replace(/"[^"]*"/g, '')))
|
|
60
91
|
cite(u);
|
|
61
92
|
}
|
|
62
93
|
const patchPath = join(pkg, 'spec', 'patches.json');
|
|
@@ -76,7 +107,40 @@ export async function citations(pkg) {
|
|
|
76
107
|
const textOf = (html) => bare(html.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<[^>]+>/g, '')
|
|
77
108
|
.replace(/"/g, '"').replace(/'|'/g, "'").replace(/</g, '<').replace(/>/g, '>').replace(/ /g, ' ').replace(/&/g, '&'));
|
|
78
109
|
const bare = (s) => s.replace(/[\s\p{Cf}]+/gu, '');
|
|
110
|
+
/** A page's markup taken out and its words kept apart, as a person reads it: block ends as line breaks, runs of space as one. */
|
|
111
|
+
const readable = (html) => html.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
112
|
+
.replace(/<\/(p|div|li|h[1-6]|pre|tr|section|article|br)\s*>|<br\s*\/?>/gi, '\n').replace(/<[^>]+>/g, ' ')
|
|
113
|
+
.replace(/"/g, '"').replace(/'|'/g, "'").replace(/</g, '<').replace(/>/g, '>').replace(/ /g, ' ').replace(/&/g, '&')
|
|
114
|
+
.replace(/[ \t]+/g, ' ').replace(/\n\s*\n\s*/g, '\n\n').trim();
|
|
79
115
|
/** Fetch and record the sources the unit at `args[0]` cites (`--recheck`: every one, not only those due). */
|
|
116
|
+
/** A documentation page as the citation check reads it: fetched as a browser-like agent (asked again as this tool where
|
|
117
|
+
* that is refused), an HTML page's text without its markup, a plain page as it is. `twin-standard read-page <url>` prints
|
|
118
|
+
* it, so an author reads a page through the same path its citations are checked on; `--raw` prints the page's own bytes
|
|
119
|
+
* (a spec writer that reads the page's tables). */
|
|
120
|
+
export async function readPage(url) {
|
|
121
|
+
if (url.startsWith('archive:')) {
|
|
122
|
+
const archive = archiveSource(url);
|
|
123
|
+
if (!archive)
|
|
124
|
+
return { status: 0, text: '', readable: '', raw: '', error: 'malformed archive locator' };
|
|
125
|
+
const result = await archiveReader()(archive);
|
|
126
|
+
return { ...result, text: bare(result.raw), readable: result.raw };
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const get = (agent) => fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(8000), headers: { 'user-agent': agent } });
|
|
130
|
+
let res = await get('Mozilla/5.0 (volter twin source check)');
|
|
131
|
+
// a site that refuses a browser-like agent it cannot verify (dev.mysql.com) is asked again as the tool this is
|
|
132
|
+
if (res.status === 403)
|
|
133
|
+
res = await get('volter-source-check/1.0 (+https://volter.ai)');
|
|
134
|
+
// an HTML page's text without its markup; a plain page (a raw source file, a markdown document) as it is: its
|
|
135
|
+
// `<name>` placeholders and `=>` arrows are not tags
|
|
136
|
+
const body = await res.text();
|
|
137
|
+
const html = (res.headers.get('content-type') ?? '').includes('html');
|
|
138
|
+
return { status: res.status, text: html ? textOf(body) : bare(body), readable: html ? readable(body) : body, raw: body };
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return { status: 0, text: '', readable: '', raw: '' };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
80
144
|
export async function checkSources(args) {
|
|
81
145
|
const vendor = args[0];
|
|
82
146
|
if (!vendor) {
|
|
@@ -88,41 +152,35 @@ export async function checkSources(args) {
|
|
|
88
152
|
const record = existsSync(recordPath) ? JSON.parse(readFileSync(recordPath, 'utf8')) : {};
|
|
89
153
|
const cited = await citations(pkg);
|
|
90
154
|
const recheck = args.includes('--recheck');
|
|
91
|
-
const due = [...cited].filter(([url, quotes]) => recheck || record[url]
|
|
155
|
+
const due = [...cited].filter(([url, quotes]) => recheck || !sourceHeld(url, record[url]) || [...quotes].some((q) => record[url]?.quotes?.[q] !== true));
|
|
92
156
|
const today = new Date().toISOString().slice(0, 10);
|
|
157
|
+
const readArchive = archiveReader();
|
|
93
158
|
let next = 0;
|
|
94
159
|
await Promise.all(Array.from({ length: 16 }, async () => {
|
|
95
160
|
while (next < due.length) {
|
|
96
161
|
const [url, quotes] = due[next++];
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
let res = await get('Mozilla/5.0 (volter twin source check)');
|
|
102
|
-
// a site that refuses a browser-like agent it cannot verify (dev.mysql.com) is asked again as the tool this is
|
|
103
|
-
if (res.status === 403)
|
|
104
|
-
res = await get('volter-source-check/1.0 (+https://volter.ai)');
|
|
105
|
-
status = res.status;
|
|
106
|
-
// an HTML page's text without its markup; a plain page (a raw source file, a markdown document) as it is: its
|
|
107
|
-
// `<name>` placeholders and `=>` arrows are not tags
|
|
108
|
-
text = (res.headers.get('content-type') ?? '').includes('html') ? textOf(await res.text()) : bare(await res.text());
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
status = 0;
|
|
112
|
-
}
|
|
162
|
+
const archive = archiveSource(url);
|
|
163
|
+
const result = archive ? await readArchive(archive) : await readPage(url);
|
|
164
|
+
const { status, sha256, error } = result;
|
|
165
|
+
const text = 'text' in result ? String(result.text) : bare(result.raw);
|
|
113
166
|
const found = Object.fromEntries([...quotes].map((q) => [q, status === 200 && text.includes(bare(q))]));
|
|
114
|
-
record[url] = { status, checked: today, ...(quotes.size ? { quotes: found } : {}) };
|
|
167
|
+
record[url] = { status, checked: today, ...(sha256 ? { sha256 } : {}), ...(error ? { error } : {}), ...(quotes.size ? { quotes: found } : {}) };
|
|
115
168
|
}
|
|
116
169
|
}));
|
|
117
170
|
const sorted = Object.fromEntries(Object.entries(record).filter(([url]) => cited.has(url)).sort(([a], [b]) => a.localeCompare(b)));
|
|
118
|
-
|
|
119
|
-
|
|
171
|
+
// a vendor of lanes' root holds no spec/ (its lanes do) and no derive reads it: its citations are checked, not recorded
|
|
172
|
+
if (existsSync(dirname(recordPath)))
|
|
173
|
+
writeFileSync(recordPath, `${JSON.stringify(sorted, null, 2)}\n`);
|
|
174
|
+
const bad = Object.entries(sorted).filter(([url, r]) => !sourceHeld(url, r));
|
|
120
175
|
const unquoted = Object.entries(sorted).flatMap(([url, r]) => Object.entries(r.quotes ?? {}).filter(([, ok]) => !ok).map(([q]) => `${url}: "${q}"`));
|
|
121
176
|
console.log(`${vendor}: ${cited.size} cited pages, ${due.length} fetched; ${bad.length} not found or not fetchable, ${unquoted.length} quotes not on their page`);
|
|
122
177
|
for (const [url, r] of bad)
|
|
123
|
-
console.log(` ${r.status || 'no answer'} ${url}`);
|
|
178
|
+
console.log(` ${r.status || 'no answer'} ${url}${r.error ? `: ${r.error}` : ''}`);
|
|
124
179
|
for (const u of unquoted)
|
|
125
180
|
console.log(` quote missing: ${u}`);
|
|
181
|
+
// a citation that does not hold fails the command, so a caller reads the verdict from the exit code, not the output
|
|
182
|
+
if (bad.length || unquoted.length)
|
|
183
|
+
process.exitCode = 1;
|
|
126
184
|
}
|
|
127
185
|
if (import.meta.main)
|
|
128
186
|
await checkSources(process.argv.slice(2));
|
package/dist/src/cli.js
CHANGED
|
@@ -9,15 +9,20 @@
|
|
|
9
9
|
// twin-standard grade <pack or lane dir>… [--static] [--form] each criterion's result
|
|
10
10
|
// twin-standard derive <pack or lane dir> [--out <dir>] write the unit's src/generated/ from its spec
|
|
11
11
|
// twin-standard check-sources <pack or lane dir> [--recheck] fetch and record the sources the unit cites
|
|
12
|
+
// twin-standard conform <pack dir> | <name>@<version> [--registry <url>]
|
|
13
|
+
// the pack served and read as an application and the
|
|
14
|
+
// catalog meet it (conformance.ts): a checkout, or a
|
|
15
|
+
// published version as the catalog admits it; or exit 1
|
|
12
16
|
// twin-standard --version the package's version and each standard's
|
|
13
|
-
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
18
|
import { relative, resolve } from 'node:path';
|
|
15
19
|
import { unitsOf, p3Gate } from "./gate.js";
|
|
16
20
|
import { grade, STANDARDS } from "./grade.js";
|
|
21
|
+
import { withCheckoutFiles } from "./protocol-3.js";
|
|
17
22
|
import { gatePublished } from "./published.js";
|
|
18
23
|
const [command, ...args] = process.argv.slice(2);
|
|
19
24
|
const usage = () => {
|
|
20
|
-
console.error('usage: twin-standard gate <repo> [units…] | gate-published <name>@<version>… [--registry <url>] | grade <unit>… [--static] [--form] | derive <unit> [--out <dir>] | check-sources <unit> [--recheck] | --version');
|
|
25
|
+
console.error('usage: twin-standard gate <repo> [units…] | gate-published <name>@<version>… [--registry <url>] | grade <unit>… [--static] [--form] | derive <unit> [--out <dir>] | read-page <url> [--raw] | check-sources <unit> [--recheck] | conform <pack dir> | <name>@<version> [--registry <url>] | --version');
|
|
21
26
|
process.exit(2);
|
|
22
27
|
};
|
|
23
28
|
if (command === '--version') {
|
|
@@ -79,14 +84,16 @@ else if (command === 'grade') {
|
|
|
79
84
|
const units = args.filter((a) => !a.startsWith('--'));
|
|
80
85
|
if (!units.length)
|
|
81
86
|
usage();
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
await withCheckoutFiles(async () => {
|
|
88
|
+
for (const unit of units) {
|
|
89
|
+
const grades = await grade(unit, { static: args.includes('--static'), ...(args.includes('--form') ? { sections: ['form'] } : {}) });
|
|
90
|
+
for (const [key, s] of Object.entries(grades)) {
|
|
91
|
+
console.log(`${unit} ${key} ${s.met}/${s.total}`);
|
|
92
|
+
for (const c of s.criteria)
|
|
93
|
+
console.log(` ${c.pass ? 'ok ' : 'NO '} ${c.section.padEnd(9)} ${c.id.padEnd(22)} ${c.reason}`);
|
|
94
|
+
}
|
|
88
95
|
}
|
|
89
|
-
}
|
|
96
|
+
});
|
|
90
97
|
}
|
|
91
98
|
else if (command === 'derive') {
|
|
92
99
|
if (!args[0])
|
|
@@ -100,6 +107,42 @@ else if (command === 'check-sources') {
|
|
|
100
107
|
usage();
|
|
101
108
|
await (await import("./check-sources.js")).checkSources(args);
|
|
102
109
|
}
|
|
110
|
+
else if (command === 'read-page') {
|
|
111
|
+
// a documentation page's text, read as the citation check reads it: where an author finds the vendor's examples and words
|
|
112
|
+
if (!args[0])
|
|
113
|
+
usage();
|
|
114
|
+
const { status, readable, raw, error } = await (await import("./check-sources.js")).readPage(args[0]);
|
|
115
|
+
if (status !== 200) {
|
|
116
|
+
console.error(`read-page: ${args[0]} answered ${status || 'nothing'}${error ? `: ${error}` : ''}`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
// --raw: the page's own bytes, as fetched (a spec writer reads its tables, which the readable text drops)
|
|
120
|
+
if (args.includes('--raw'))
|
|
121
|
+
process.stdout.write(raw);
|
|
122
|
+
else
|
|
123
|
+
console.log(readable);
|
|
124
|
+
}
|
|
125
|
+
else if (command === 'conform') {
|
|
126
|
+
if (!args[0])
|
|
127
|
+
usage();
|
|
128
|
+
const at = args.indexOf('--registry');
|
|
129
|
+
const target = args.find((a, i) => !a.startsWith('--') && (at < 0 || i !== at + 1));
|
|
130
|
+
// a directory is a checkout; `<name>@<version>` (a scope's leading @ aside) is a version on the registry, and a
|
|
131
|
+
// `.tgz` the tarball `npm pack` makes of one, the bytes a publish would send
|
|
132
|
+
const published = target.endsWith('.tgz') || (!existsSync(target) && /^@?[^@]+@[^@]+$/.test(target));
|
|
133
|
+
const results = published
|
|
134
|
+
? await (await import("./published.js")).conformPublished(target, at >= 0 ? { registry: args[at + 1] } : {})
|
|
135
|
+
: await (await import("./conformance.js")).conformance(target);
|
|
136
|
+
for (const r of results) {
|
|
137
|
+
console.log(`${r.failures.length ? '✗' : '✓'} ${r.id.padEnd(15)} ${r.asks}${r.note ? ` (${r.note})` : ''}`);
|
|
138
|
+
for (const f of r.failures)
|
|
139
|
+
console.log(` ${f}`);
|
|
140
|
+
}
|
|
141
|
+
const failing = results.filter((r) => r.failures.length).length;
|
|
142
|
+
console.log(failing ? `conform: ${failing} of ${results.length} checks failing` : `conform: all ${results.length} checks hold`);
|
|
143
|
+
// a served pack may leave handles open (a socket's timers): the verdict is the exit code
|
|
144
|
+
process.exit(failing ? 1 : 0);
|
|
145
|
+
}
|
|
103
146
|
else {
|
|
104
147
|
usage();
|
|
105
148
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const cases: import("./harness.js").ClientCase[];
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Vercel AI Gateway's chat through openai-node (its OpenAI-compatible API), the shared chat cases.
|
|
2
|
+
import { chatCases } from "./openai-compatible.js";
|
|
3
|
+
export const cases = chatCases({ key: 'AI_GATEWAY_API_KEY', base: (host) => `https://${host}/v1`, model: 'anthropic/claude-sonnet-5' });
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ClientCase } from './harness.js';
|
|
2
|
+
/** A vendor's Messages as the SDK reaches it: the variable its key is issued in, the base URL on its host, a model it
|
|
3
|
+
* serves, and a turn that calls the tool `lookup` in the World's scenario grammar for the vendor (the pack's GET /twin). */
|
|
4
|
+
export type MessagesVendor = {
|
|
5
|
+
key: string;
|
|
6
|
+
base: (host: string) => string;
|
|
7
|
+
model: string;
|
|
8
|
+
toolTurn: Record<string, unknown>;
|
|
9
|
+
};
|
|
10
|
+
export declare function messagesCases(v: MessagesVendor): ClientCase[];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const MESSAGES = 'https://platform.claude.com/docs/en/api/messages';
|
|
2
|
+
const TOOLS = [{ name: 'lookup', input_schema: { type: 'object', properties: {} } }];
|
|
3
|
+
export function messagesCases(v) {
|
|
4
|
+
const SCRIPT = { handlers: [
|
|
5
|
+
{ id: 'calls', on: { hasTool: 'lookup' }, respond: v.toolTurn },
|
|
6
|
+
{ id: 'words', on: { userTextIncludes: 'hello' }, respond: { text: 'scripted words' } },
|
|
7
|
+
] };
|
|
8
|
+
const ask = async (c, extra = {}) => {
|
|
9
|
+
const { default: Anthropic } = await import('@anthropic-ai/sdk');
|
|
10
|
+
const client = new Anthropic({ apiKey: c.env[v.key] ?? '', baseURL: v.base(c.host), fetch: c.fetch, maxRetries: 0, timeout: 10_000 });
|
|
11
|
+
return client.messages.create({ model: v.model, max_tokens: 256, messages: [{ role: 'user', content: 'hello' }], ...extra });
|
|
12
|
+
};
|
|
13
|
+
const text = (m) => m.content?.find((b) => b.type === 'text')?.text;
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
id: 'messages-answers',
|
|
17
|
+
asks: "a message is answered, the World's scenario loaded",
|
|
18
|
+
source: `${MESSAGES} "Send a structured list of input messages with text and/or image content, and the model will generate the next message in the conversation."`,
|
|
19
|
+
async run(c) {
|
|
20
|
+
c.scenario(SCRIPT);
|
|
21
|
+
const answer = await ask(c).catch((e) => e);
|
|
22
|
+
if (answer instanceof Error)
|
|
23
|
+
return [`the client's messages.create failed: ${answer.message}`];
|
|
24
|
+
return text(answer) === 'scripted words' ? [] : [`the scripted turn was not answered: ${JSON.stringify(answer.content).slice(0, 120)}`];
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'messages-tool-choice-none',
|
|
29
|
+
asks: "tool_choice { type: 'none' } is answered without a tool_use block",
|
|
30
|
+
source: `${MESSAGES} "The model will not be allowed to use tools."`,
|
|
31
|
+
async run(c) {
|
|
32
|
+
c.scenario(SCRIPT);
|
|
33
|
+
const none = await ask(c, { tools: TOOLS, tool_choice: { type: 'none' } }).catch((e) => e);
|
|
34
|
+
if (none instanceof Error)
|
|
35
|
+
return [`the client's messages.create failed: ${none.message}`];
|
|
36
|
+
return none.content?.some((b) => b.type === 'tool_use') ? ["tool_choice none was answered with a tool_use block"] : [];
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'messages-scenario-each-turn',
|
|
41
|
+
asks: "the World's scenario rewritten between two turns decides the second (no reboot)",
|
|
42
|
+
source: 'architecture "The scenario is read from the World\'s store at every turn"',
|
|
43
|
+
async run(c) {
|
|
44
|
+
c.scenario({ handlers: [{ id: 'one', on: { userTextIncludes: 'hello' }, respond: { text: 'first script' } }] });
|
|
45
|
+
const first = await ask(c).then(text, (e) => `error ${e.message}`);
|
|
46
|
+
c.scenario({ handlers: [{ id: 'two', on: { userTextIncludes: 'hello' }, respond: { text: 'second script' } }] });
|
|
47
|
+
const second = await ask(c).then(text, (e) => `error ${e.message}`);
|
|
48
|
+
return first === 'first script' && second === 'second script' ? [] : [`the turns answered "${first}" then "${second}"`];
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const cases: import("./harness.js").ClientCase[];
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Anthropic's Messages through its own SDK (@anthropic-ai/sdk), the shared Messages cases.
|
|
2
|
+
import { messagesCases } from "./anthropic-messages.js";
|
|
3
|
+
export const cases = messagesCases({ key: 'ANTHROPIC_API_KEY', base: (host) => `https://${host}`, model: 'claude-sonnet-4-5', toolTurn: { toolUses: [{ name: 'lookup', input: {} }] } });
|