@volter/twin-standard 1.0.149 → 1.0.151

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.
Files changed (83) hide show
  1. package/README.md +12 -3
  2. package/client-sdks/bun.lock +0 -32
  3. package/client-sdks/package.json +0 -2
  4. package/dist/client-sdks/bun.lock +0 -32
  5. package/dist/client-sdks/package.json +0 -2
  6. package/dist/src/assessment.d.ts +5 -4
  7. package/dist/src/assessment.js +2 -1
  8. package/dist/src/cli.js +21 -6
  9. package/dist/src/clients/harness.d.ts +3 -1
  10. package/dist/src/clients/harness.js +4 -2
  11. package/dist/src/clients/jira.d.ts +2 -0
  12. package/dist/src/clients/jira.js +71 -0
  13. package/dist/src/clients/polar.d.ts +2 -0
  14. package/dist/src/clients/polar.js +73 -0
  15. package/dist/src/clients/resend.js +38 -0
  16. package/dist/src/clients/run.js +1 -1
  17. package/dist/src/clients/sentry.js +1 -26
  18. package/dist/src/clients/smtp.d.ts +2 -0
  19. package/dist/src/clients/smtp.js +54 -0
  20. package/dist/src/clients/stripe.js +1 -1
  21. package/dist/src/clients/volteridentity.js +4 -3
  22. package/dist/src/clients/x.d.ts +2 -0
  23. package/dist/src/clients/x.js +64 -0
  24. package/dist/src/conformance.d.ts +4 -1
  25. package/dist/src/conformance.js +256 -6
  26. package/dist/src/consumer.js +5 -1
  27. package/dist/src/create-pack.d.ts +5 -0
  28. package/dist/src/create-pack.js +31 -3
  29. package/dist/src/protocol-3.js +30 -2
  30. package/dist/src/spec-ir.d.ts +2 -0
  31. package/dist/src/spec-ir.js +43 -0
  32. package/dist/src/tools/judge-brief.d.ts +1 -0
  33. package/dist/src/tools/judge-brief.js +67 -0
  34. package/dist/src/tools/life-coverage-walk.d.ts +2 -0
  35. package/dist/src/tools/life-coverage-walk.js +220 -0
  36. package/dist/src/tools/life-coverage.d.ts +2 -0
  37. package/dist/src/tools/life-coverage.js +271 -0
  38. package/dist/src/tools/packs-root.d.ts +9 -0
  39. package/dist/src/tools/packs-root.js +17 -0
  40. package/dist/src/tools/run-tool.d.ts +1 -0
  41. package/dist/src/tools/run-tool.js +18 -0
  42. package/dist/src/tools/score-pack.d.ts +2 -0
  43. package/dist/src/tools/score-pack.js +264 -0
  44. package/dist/src/tools/vendor-examples.d.ts +2 -0
  45. package/dist/src/tools/vendor-examples.js +1493 -0
  46. package/dist/src/verdict.js +1 -1
  47. package/dist/src/walk/behavior-journey.d.ts +6 -0
  48. package/dist/src/walk/behavior-journey.js +8 -6
  49. package/dist/src/walk/journey-kit.d.ts +2 -0
  50. package/dist/src/walk/journey-kit.js +6 -2
  51. package/dist/src/walk/rendered-scalar.d.ts +3 -0
  52. package/dist/src/walk/rendered-scalar.js +23 -0
  53. package/dist/src/walk/sse-example.js +45 -0
  54. package/package.json +2 -2
  55. package/src/assessment.ts +2 -1
  56. package/src/cli.ts +15 -2
  57. package/src/clients/harness.ts +4 -2
  58. package/src/clients/jira.ts +73 -0
  59. package/src/clients/polar.ts +73 -0
  60. package/src/clients/resend.ts +32 -0
  61. package/src/clients/run.ts +2 -2
  62. package/src/clients/sentry.ts +1 -25
  63. package/src/clients/smtp.ts +49 -0
  64. package/src/clients/stripe.ts +1 -1
  65. package/src/clients/volteridentity.ts +4 -3
  66. package/src/clients/x.ts +66 -0
  67. package/src/conformance.ts +192 -8
  68. package/src/consumer.ts +5 -1
  69. package/src/create-pack.ts +27 -3
  70. package/src/protocol-3.ts +24 -2
  71. package/src/spec-ir.ts +31 -0
  72. package/src/tools/judge-brief.ts +63 -0
  73. package/src/tools/life-coverage-walk.ts +173 -0
  74. package/src/tools/life-coverage.ts +237 -0
  75. package/src/tools/packs-root.ts +21 -0
  76. package/src/tools/run-tool.ts +10 -0
  77. package/src/tools/score-pack.ts +244 -0
  78. package/src/tools/vendor-examples.ts +1250 -0
  79. package/src/verdict.ts +1 -1
  80. package/src/walk/behavior-journey.ts +6 -2
  81. package/src/walk/journey-kit.ts +7 -2
  82. package/src/walk/rendered-scalar.ts +17 -0
  83. package/src/walk/sse-example.ts +37 -0
@@ -58,4 +58,42 @@ export const cases = [
58
58
  return answer.data === null && answer.error?.statusCode === 422 && answer.error?.name === 'missing_required_field' ? [] : [`missing sender returned ${JSON.stringify(answer)}`];
59
59
  },
60
60
  },
61
+ {
62
+ id: 'unverified-domain-validation-error',
63
+ asks: 'a send from a domain the team never verified comes back as the SDK error validation_error (403), with no data',
64
+ source: `${ERRORS} "To send emails to other recipients, please verify a domain at resend.com/domains, and change the \`from\` address to an email using this domain."`,
65
+ async run(c) {
66
+ const resend = client(c);
67
+ const sent = await resend.emails.send({ from: 'Someone <someone@never-verified.example>', to: ['client@example.org'], subject: 'Not from a verified domain', text: 'Refused.' });
68
+ const failures = [];
69
+ if (sent.data !== null)
70
+ failures.push(`the send answered data ${JSON.stringify(sent.data)}`);
71
+ const error = sent.error;
72
+ if (error?.name !== 'validation_error')
73
+ failures.push(`the error is ${JSON.stringify(error)}, not validation_error`);
74
+ else if (error.statusCode !== 403)
75
+ failures.push(`the validation_error's status is ${String(error.statusCode)}, not 403`);
76
+ return failures;
77
+ },
78
+ },
79
+ {
80
+ id: 'scheduled-send-waits',
81
+ asks: "a send scheduled for later (the SDK's scheduledAt, sent as scheduled_at) waits: it reads back scheduled, at its time",
82
+ source: `${SEND} "Schedule email to be sent later."`,
83
+ async run(c) {
84
+ const resend = client(c);
85
+ const at = '2030-01-01T08:00:00.000Z';
86
+ const sent = await resend.emails.send({ from: 'Acme <onboarding@resend.dev>', to: ['owner@world.test'], subject: 'Later', text: 'In good time.', scheduledAt: at });
87
+ if (sent.error || !sent.data?.id)
88
+ return [`the send answered ${JSON.stringify(sent).slice(0, 200)}`];
89
+ const read = await resend.emails.get(sent.data.id);
90
+ const email = read.data;
91
+ const failures = [];
92
+ if (email?.last_event !== 'scheduled')
93
+ failures.push(`the scheduled email's last_event is ${String(email?.last_event)}`);
94
+ if (email?.scheduled_at === undefined || email.scheduled_at === null || Date.parse(email.scheduled_at) !== Date.parse(at))
95
+ failures.push(`it reads back scheduled_at ${String(email?.scheduled_at)}`);
96
+ return failures;
97
+ },
98
+ },
61
99
  ];
@@ -27,7 +27,7 @@ require('@volter/world-core/pack-facts').extendPackFacts([job.packFacts]);
27
27
  const facts = JSON.parse(readFileSync(job.packFacts, 'utf8')).packs[job.vendor];
28
28
  const keys = [...new Set((facts.hosts ?? []).map((rule) => rule.key ?? job.vendor))];
29
29
  require('@volter/world-core/inject').install(Object.fromEntries(keys.map((key) => [key, `http://127.0.0.1:${proxy}`])));
30
- const context = clientContext(proxy, job.host, job.env, job.scenarioPath);
30
+ const context = clientContext(proxy, job.host, job.env, job.scenarioPath, job.raw ? job.port : undefined);
31
31
  for (const c of cases) {
32
32
  const failures = await Promise.race([
33
33
  c.run(context).catch((e) => [`the case threw: ${e.message}`]),
@@ -28,7 +28,6 @@ async function sdkOf(name) {
28
28
  const pins = [
29
29
  ['rallly', '@sentry/node', '10.38.0'],
30
30
  ['twenty', 'sentry-node-twenty', '10.73.0'],
31
- ['postiz', 'sentry-node-postiz', '10.56.0'],
32
31
  ['cal-api', 'sentry-node-cal', '9.46.0'],
33
32
  ['cal-web', 'sentry-node-cal-web', '10.33.0'],
34
33
  ];
@@ -76,7 +75,7 @@ export const cases = pins.map(([application, name, version]) => ({
76
75
  return failures;
77
76
  },
78
77
  }));
79
- for (const [name, version] of [['@sentry/browser', '10.38.0'], ['sentry-browser-twenty', '10.59.0'], ['sentry-browser-postiz', '10.56.0']])
78
+ for (const [name, version] of [['@sentry/browser', '10.38.0'], ['sentry-browser-twenty', '10.59.0']])
80
79
  cases.push({
81
80
  id: 'browser-' + version + '-dsn-query-auth-and-session-envelope',
82
81
  asks: 'the browser fetch transport uses DSN query authentication and flushes event and session envelopes',
@@ -142,27 +141,3 @@ cases.push({
142
141
  return drained && attempts === 2 && dropped.includes('ratelimit_backoff') ? [] : ['transport retried 503, failed to drain, or sent a rate-limited event'];
143
142
  },
144
143
  });
145
- cases.push({
146
- id: 'postiz-feedback-attribution',
147
- asks: 'captureFeedback preserves the widget message, explicit name/email, associated event and scoped user',
148
- source: 'https://unpkg.com/@sentry/core@10.56.0/build/esm/feedback.js "associated_event_id: associatedEventId"',
149
- async run(c) {
150
- const sdk = await sdkOf('sentry-browser-postiz');
151
- const client = new sdk.BrowserClient({ dsn: c.env.SENTRY_DSN, integrations: [], stackParser: sdk.defaultStackParser,
152
- transport: (options) => sdk.createTransport(options, async (request) => {
153
- const answer = await c.fetch(options.url, { method: 'POST', body: request.body });
154
- return { statusCode: answer.status, headers: {} };
155
- }) });
156
- let id = '';
157
- sdk.withScope((scope) => {
158
- scope.setClient(client);
159
- scope.setUser({ id: 'feedback-author' });
160
- id = sdk.captureFeedback({ message: 'The scheduling button is hard to find', name: 'Priya', email: 'priya@harbourline.test', associatedEventId: '00000000000000000000000000000002' }, {}, scope);
161
- });
162
- const drained = await client.flush(10_000);
163
- const event = await read(c, id, await token(c));
164
- await client.close(10_000);
165
- const feedback = event.contexts?.feedback;
166
- return drained && event.type === 'feedback' && event.user?.id === 'feedback-author' && feedback?.name === 'Priya' && feedback?.contact_email === 'priya@harbourline.test' && feedback?.associated_event_id === '00000000000000000000000000000002' ? [] : ['feedback attribution or associated event was lost'];
167
- },
168
- });
@@ -0,0 +1,2 @@
1
+ import type { ClientCase } from './harness.js';
2
+ export declare const cases: ClientCase[];
@@ -0,0 +1,54 @@
1
+ const RFC5321 = 'https://www.rfc-editor.org/rfc/rfc5321';
2
+ async function transport(c) {
3
+ const nodemailer = (await import('nodemailer')).default;
4
+ return nodemailer.createTransport({
5
+ host: c.host, port: 25, secure: false, ignoreTLS: true,
6
+ getSocket: (_options, callback) => callback(null, { connection: c.connect() }),
7
+ });
8
+ }
9
+ export const cases = [
10
+ {
11
+ id: 'verify-greets',
12
+ asks: "nodemailer's verify opens the session, greets with EHLO and quits: the relay answers it as an SMTP server",
13
+ source: `${RFC5321} "These commands are used to identify the SMTP client to the SMTP server."`,
14
+ async run(c) {
15
+ const t = await transport(c);
16
+ try {
17
+ return (await t.verify()) === true ? [] : ['verify resolved without true'];
18
+ }
19
+ catch (error) {
20
+ return [`verify failed: ${String(error).slice(0, 200)}`];
21
+ }
22
+ finally {
23
+ t.close();
24
+ }
25
+ },
26
+ },
27
+ {
28
+ id: 'transaction-to-two-recipients',
29
+ asks: 'a message with text and HTML to two recipients runs MAIL, a RCPT for each, then DATA: both accepted, none rejected, the data answered 250',
30
+ source: `${RFC5321} "Once started, a mail transaction consists of a transaction beginning command, one or more RCPT commands, and a DATA command, in that order."`,
31
+ async run(c) {
32
+ const t = await transport(c);
33
+ try {
34
+ const info = await t.sendMail({ from: 'Polls <polls@case.example>', to: ['an@case.example', 'bo@case.example'], subject: 'Pick a date', text: 'Please pick a date.', html: '<p>Please pick a date.</p>' });
35
+ const failures = [];
36
+ if (JSON.stringify([...info.accepted].sort()) !== JSON.stringify(['an@case.example', 'bo@case.example']))
37
+ failures.push(`accepted ${JSON.stringify(info.accepted)}`);
38
+ if (info.rejected.length)
39
+ failures.push(`rejected ${JSON.stringify(info.rejected)}`);
40
+ if (!info.response.startsWith('250'))
41
+ failures.push(`the data was answered ${info.response}`);
42
+ if (!info.messageId)
43
+ failures.push('no message id');
44
+ return failures;
45
+ }
46
+ catch (error) {
47
+ return [`the send failed: ${String(error).slice(0, 200)}`];
48
+ }
49
+ finally {
50
+ t.close();
51
+ }
52
+ },
53
+ },
54
+ ];
@@ -1,4 +1,4 @@
1
- // Stripe's own client (stripe-node), driving the stripe pack as its applications do (Cal.com, Dub, Postiz, Rallly and
1
+ // Stripe's own client (stripe-node), driving the stripe pack as its applications do (Cal.com, Dub, Rallly and
2
2
  // Twenty call it): an endpoint registered through the API and the event Stripe sends it verified by constructEvent, as an
3
3
  // application's webhook handler verifies it; a customer list read page by page by the client's auto-pagination; a retry
4
4
  // sent with its idempotency key; the error a missing object raises; and the customer portal a billing page opens.
@@ -1,9 +1,10 @@
1
1
  // @volter/identity 0.4.1, as Volter Harness uses it; discovery and token verification are also the APIs Open Autonomy
2
- // uses in 0.1.0. These cases are owned by conformance and are not run during pack authoring.
2
+ // uses in 0.1.0. These cases are owned by conformance and are not run during pack authoring. They import nothing of the
3
+ // kernel: the runner is Node's own type stripping, which the kernel's parameter properties do not pass.
3
4
  import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
5
  import { tmpdir } from 'node:os';
5
6
  import { join } from 'node:path';
6
- import { digest } from '@volter/world-core';
7
+ import { createHash } from 'node:crypto';
7
8
  import { resolveIdentity, verifyIdentityToken, tokenFor, readSharedSignIn, SHARED_CLIENTS } from '@volter/identity';
8
9
  const INDEX = 'https://unpkg.com/@volter/identity@0.4.1/dist/index.js';
9
10
  const SHARED = 'https://unpkg.com/@volter/identity@0.4.1/dist/shared.js';
@@ -34,7 +35,7 @@ async function setup(c, name, shared = false) {
34
35
  await send(c, `/admin/clients/${registered.client_id}/resources`, { resource: 'https://teams.sdk.example' }, operator);
35
36
  }
36
37
  const verifier = 'official-sdk-case-verifier-with-enough-entropy-for-s256';
37
- const query = new URLSearchParams({ client_id: registered.client_id, redirect_uri: callback, response_type: 'code', scope: 'openid profile email offline_access', code_challenge: digest('sha256', verifier, 'base64url'), code_challenge_method: 'S256' });
38
+ const query = new URLSearchParams({ client_id: registered.client_id, redirect_uri: callback, response_type: 'code', scope: 'openid profile email offline_access', code_challenge: createHash('sha256').update(verifier).digest('base64url'), code_challenge_method: 'S256' });
38
39
  const authorized = await send(c, `/api/auth/oauth2/authorize?${query}`, undefined, { cookie });
39
40
  const code = new URL(authorized.headers.get('location')).searchParams.get('code');
40
41
  const form = new URLSearchParams({ client_id: registered.client_id, redirect_uri: callback, grant_type: 'authorization_code', code, code_verifier: verifier, ...(registered.client_secret ? { client_secret: registered.client_secret } : {}) });
@@ -0,0 +1,2 @@
1
+ import type { ClientCase } from './harness.js';
2
+ export declare const cases: ClientCase[];
@@ -0,0 +1,64 @@
1
+ const CREATE = 'https://docs.x.com/x-api/posts/create-post';
2
+ const ME = 'https://docs.x.com/x-api/users/get-my-user';
3
+ const README = 'https://www.npmjs.com/package/twitter-api-v2/v/1.29.1';
4
+ /** An account named `username` and an access token for it with `scopes`; the client on that token. */
5
+ async function signedIn(c, username, scopes, token = `case-${username}-token`) {
6
+ const door = async (path, body) => {
7
+ const answer = await c.fetch(`https://${c.host}${path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
8
+ if (answer.status >= 300)
9
+ throw new Error(`${path} answered ${answer.status}`);
10
+ return (await answer.json()).data;
11
+ };
12
+ const account = await door('/_twin/accounts', { username, name: 'Case Account' });
13
+ await door('/_twin/tokens', { token, account_id: account.id, scopes });
14
+ const { TwitterApi } = await import('twitter-api-v2');
15
+ return { client: new TwitterApi(token, { httpAgent: c.agent }), account };
16
+ }
17
+ export const cases = [
18
+ {
19
+ id: 'post-and-read-back',
20
+ asks: "a post made through the SDK answers its id and text, and reads back by id with that text, its author the signed-in account",
21
+ source: `${CREATE} "Create Posts"`,
22
+ async run(c) {
23
+ const { client, account } = await signedIn(c, 'case_poster', ['tweet.read', 'tweet.write', 'users.read']);
24
+ const made = await client.v2.tweet('Shipping the case today');
25
+ const failures = [];
26
+ if (!made.data?.id || made.data.text !== 'Shipping the case today')
27
+ failures.push(`tweet answered ${JSON.stringify(made)}`);
28
+ const read = await client.v2.singleTweet(made.data.id, { 'tweet.fields': ['author_id'] });
29
+ if (read.data?.text !== 'Shipping the case today')
30
+ failures.push(`the post reads back text ${JSON.stringify(read.data?.text)}`);
31
+ if (read.data?.author_id !== account.id)
32
+ failures.push(`the post reads back author ${String(read.data?.author_id)}, not ${account.id}`);
33
+ return failures;
34
+ },
35
+ },
36
+ {
37
+ id: 'me-is-the-signed-in-account',
38
+ asks: "v2.me answers the account the token was issued to, by its id and username (an app's account check after sign-in)",
39
+ source: `${ME} "Get Users Me"`,
40
+ async run(c) {
41
+ const { client, account } = await signedIn(c, 'case_me', ['tweet.read', 'users.read']);
42
+ const me = await client.v2.me();
43
+ return me.data?.id === account.id && me.data.username === 'case_me' ? [] : [`me answered ${JSON.stringify(me)}`];
44
+ },
45
+ },
46
+ {
47
+ id: 'unknown-token-api-response-error',
48
+ asks: 'a token X never issued is refused, and the client throws its ApiResponseError carrying the 401',
49
+ source: `${README} "Typed errors, meaningful error messages, error enumerations for both v1.1 and v2"`,
50
+ async run(c) {
51
+ const { TwitterApi, ApiResponseError } = await import('twitter-api-v2');
52
+ const client = new TwitterApi('case-never-issued-token', { httpAgent: c.agent });
53
+ try {
54
+ await client.v2.me();
55
+ return ['the never-issued token was accepted'];
56
+ }
57
+ catch (error) {
58
+ if (!(error instanceof ApiResponseError))
59
+ return [`the refusal is not an ApiResponseError: ${String(error).slice(0, 160)}`];
60
+ return error.code === 401 ? [] : [`the refusal's code is ${error.code}, not 401`];
61
+ }
62
+ },
63
+ },
64
+ ];
@@ -30,5 +30,8 @@ export declare function decisionTableProblems(unit: string, byUnit: Record<strin
30
30
  building?: boolean;
31
31
  }): string[];
32
32
  /** Every check over the pack at `dir`, in order. */
33
- export declare function conformance(dir: string): Promise<ConformanceResult[]>;
33
+ /** `onCheck` hears each check's id as it starts, so a caller can show where a long run stands. */
34
+ export declare function conformance(dir: string, options?: {
35
+ onCheck?: (id: string) => void;
36
+ }): Promise<ConformanceResult[]>;
34
37
  export {};
@@ -356,6 +356,190 @@ async function standalone(pack) {
356
356
  }
357
357
  return out;
358
358
  }
359
+ /** The request an application's first call of an operation sends when it knows nothing yet: every path parameter, every
360
+ * required query parameter and every header the operation is routed by holding a placeholder, its discriminator's
361
+ * values, and an empty body in the operation's encoding. What it measures is the twin's answer to a call it did not
362
+ * script: a refusal in the vendor's form, or a result in the vendor's shape, and never a crash, a hang or a row. */
363
+ function sweepRequest(op, basePath, host, credential) {
364
+ const fill = (name, type) => (type === 'integer' || type === 'number' ? '1' : type === 'boolean' ? 'true' : `conformance-${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`);
365
+ const path = `${op.basePath ?? basePath}${op.path}`.replace(/\{([^}+]+)\+?\}/g, (_m, name) => fill(name));
366
+ const url = new URL(`https://${host}${path}`);
367
+ for (const q of op.query ?? [])
368
+ if (q.required)
369
+ url.searchParams.set(q.name, fill(q.name, q.type));
370
+ for (const [name, value] of Object.entries(op.discriminator ?? {}))
371
+ url.searchParams.set(name, value ?? fill(name));
372
+ const headers = new Headers(credential);
373
+ for (const [name, value] of Object.entries(op.headers ?? {}))
374
+ headers.set(name, value ?? fill(name));
375
+ const method = op.method.toUpperCase();
376
+ let body;
377
+ if (op.bodyEncoding === 'form') {
378
+ headers.set('content-type', 'application/x-www-form-urlencoded');
379
+ body = '';
380
+ }
381
+ else if (op.bodyEncoding === 'xml') {
382
+ headers.set('content-type', 'application/xml');
383
+ body = '';
384
+ }
385
+ else if (op.bodyEncoding === 'json' || op.bodyEncoding === 'multipart') {
386
+ headers.set('content-type', 'application/json');
387
+ body = '{}';
388
+ }
389
+ return new Request(url, { method, headers, ...(body !== undefined && !['GET', 'HEAD'].includes(method) ? { body } : {}) });
390
+ }
391
+ /** Every operation the pack serves, called over HTTP as an application calls it, against its server.ts served as a World
392
+ * runs it: once plainly and once with a scenario loaded (an empty one, as a World that scripts nothing loads it). Each
393
+ * answers within 10 s, never with a 500 the spec does not declare, never with a field of the twin's own (`_`
394
+ * bookkeeping, the kernel's `updatedAt`) and, answered successfully, in the shape the spec gives the operation; and every
395
+ * refresh list a fresh account can read (one with no parameter in its path) answers it successfully, so a World rooted
396
+ * at this twin reads its resources back. Escaped before: the scenario hang (a served pack given a scenario answered
397
+ * nothing), leaked kernel fields in answers no life step read, and refresh scopes on operations the pack answered with
398
+ * the gap. The life walks what a customer does; this calls what the pack declares it serves. */
399
+ async function surfaceSwept(pack, scratch) {
400
+ const out = [];
401
+ const descriptor = (await manifestOf(pack)).descriptor;
402
+ const fetchOf = (await import(__rewriteRelativeImportExtension(join(pack, 'src', 'fetch.ts'))))[fetchName(pack)];
403
+ const root = fetchOf?.();
404
+ const byUnit = root?.ownersByUnit?.() ?? { '': root?.owners?.() ?? {} };
405
+ const { shapeChecker } = await import("./walk/shape.js");
406
+ const { leaked } = await import("./walk/behavior-journey.js");
407
+ const shape = await shapeChecker(pack);
408
+ const units = [];
409
+ const vendorFields = new Set();
410
+ for (const unit of unitsIn(pack).filter(hasOwnSurface)) {
411
+ const surface = readJson(join(unit, 'src', 'generated', 'surface.gen.json'));
412
+ for (const r of surface?.resources ?? [])
413
+ for (const f of r.fields)
414
+ vendorFields.add(f.name);
415
+ // a line protocol and a command table ride in no path of their own: the life's line steps and the examples read them
416
+ if (!surface?.operations || ['command-reply', 'command-table'].includes(String(surface.format)))
417
+ continue;
418
+ const m = (await manifestOf(unit));
419
+ const host = m.hosts?.find((h) => h.host)?.host ?? descriptor?.hosts?.find((h) => h.host)?.host;
420
+ if (!host) {
421
+ out.push(`${nameOf(pack, unit)}: no host is named (its manifest's hosts or the descriptor's) for an application to call it at`);
422
+ continue;
423
+ }
424
+ const owners = byUnit[unit === pack ? '' : basename(unit)] ?? {};
425
+ const refreshLists = Object.entries(m.resources ?? {}).flatMap(([resource, decl]) => (decl.refresh?.list ? [{ resource, id: decl.refresh.list }] : []));
426
+ units.push({ name: nameOf(pack, unit), host, basePath: surface.basePath ?? '', ops: surface.operations.filter((o) => owners[o.id] !== undefined && owners[o.id] !== 'gap'), refreshLists, ...(m.auth ? { auth: m.auth } : {}) });
427
+ }
428
+ if (!units.length)
429
+ return { failures: out, note: 'no unit serves an operation on a path of its own' };
430
+ const tally = { calls: 0, ok: 0, refused: 0 };
431
+ const env = globalThis.process.env;
432
+ const scenarioPath = join(scratch, 'surface-scenario.json');
433
+ writeFileSync(scenarioPath, '{"handlers":[]}');
434
+ for (const mode of ['plain', 'scenario']) {
435
+ const before = env.VOLTER_WORLD_BOOT_ID;
436
+ env.VOLTER_WORLD_BOOT_ID = 'conformance';
437
+ let server;
438
+ try {
439
+ server = await serve(pack, { root: join(scratch, `surface-${mode}`), ...(mode === 'scenario' ? { scenarioPath } : {}) });
440
+ }
441
+ finally {
442
+ if (before === undefined)
443
+ delete env.VOLTER_WORLD_BOOT_ID;
444
+ else
445
+ env.VOLTER_WORLD_BOOT_ID = before;
446
+ }
447
+ const http = servedHttpFetch(server.inspectPort ?? server.port);
448
+ const send = async (request) => {
449
+ await server.managedDatabaseUrl?.();
450
+ const abort = new AbortController();
451
+ try {
452
+ return await within(10_000, http(request, { signal: abort.signal }));
453
+ }
454
+ catch (error) {
455
+ return error;
456
+ }
457
+ finally {
458
+ abort.abort();
459
+ }
460
+ };
461
+ try {
462
+ // the account an application holds: the key the credential door issues, sent as the vendor reads its credential
463
+ let issued;
464
+ if (descriptor?.credentialDoor) {
465
+ const door = descriptor.credentialDoor;
466
+ const answer = await send(new Request(`https://${descriptor.hosts?.find((h) => h.host)?.host ?? units[0].host}${door.path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(door.body) }));
467
+ const fields = answer instanceof Response ? (await answer.json().catch(() => ({}))) : {};
468
+ issued = Object.values(door.fill).map((f) => fields[f]).find((v) => typeof v === 'string');
469
+ }
470
+ const said = mode === 'scenario' ? ' with a scenario loaded' : '';
471
+ for (const u of units) {
472
+ // a unit whose manifest declares no `auth` checks its key in its own front: the key goes as most vendors read
473
+ // one, a bearer token, and a refusal of it is no finding of the refresh below
474
+ const auth = u.auth ?? { header: 'authorization', scheme: 'Bearer' };
475
+ const credential = issued ? { [auth.header]: auth.scheme ? `${auth.scheme} ${issued}` : issued } : {};
476
+ const answered = new Map();
477
+ for (const op of u.ops) {
478
+ const request = sweepRequest(op, u.basePath, u.host, credential);
479
+ const where = `${u.name}: ${op.id} (${request.method} ${new URL(request.url).pathname})`;
480
+ const res = await send(request);
481
+ if (res === 'timeout') {
482
+ out.push(`${where} answered nothing in 10 s${said}`);
483
+ continue;
484
+ }
485
+ if (res instanceof Error) {
486
+ out.push(`${where}: the connection failed${said} (${res.message})`);
487
+ continue;
488
+ }
489
+ answered.set(op.id, res.status);
490
+ if (mode === 'plain') {
491
+ tally.calls++;
492
+ if (res.status < 300)
493
+ tally.ok++;
494
+ else
495
+ tally.refused++;
496
+ }
497
+ const text = await res.text();
498
+ if (res.status === 500 && !op.responseMedia?.['500'] && !op.responseMedia?.['5XX']) {
499
+ out.push(`${where} answered 500${said}, which the spec does not declare: ${text.slice(0, 160)}`);
500
+ continue;
501
+ }
502
+ if (mode === 'scenario')
503
+ continue;
504
+ const type = res.headers.get('content-type') ?? '';
505
+ let body;
506
+ if (/json/.test(type)) {
507
+ try {
508
+ body = JSON.parse(text);
509
+ }
510
+ catch {
511
+ body = undefined;
512
+ }
513
+ }
514
+ for (const key of leaked(body, vendorFields, '', !op.answers || res.status >= 300))
515
+ out.push(`${where} answers the twin's own field ${key}`);
516
+ if (res.status >= 200 && res.status < 300 && shape) {
517
+ const hit = shape.operationOf(request.method, request.url, Object.fromEntries(request.headers));
518
+ if (hit)
519
+ for (const detail of shape.failures(hit.op, body, type, request.headers.get('accept') ?? undefined, res.status, { method: request.method, responseText: text }))
520
+ out.push(`${where}: ${detail}`);
521
+ }
522
+ }
523
+ if (mode === 'plain')
524
+ for (const r of u.refreshLists) {
525
+ const op = u.ops.find((o) => o.id === r.id);
526
+ if (!op || op.path.includes('{') || (op.basePath ?? '').includes('{'))
527
+ continue;
528
+ const status = answered.get(r.id);
529
+ if (status !== undefined && (status === 401 || status === 403) && (!u.auth || !issued))
530
+ continue;
531
+ if (status !== undefined && (status < 200 || status >= 300))
532
+ out.push(`${u.name}: ${r.resource}'s refresh lists by ${r.id}, which answered a fresh account ${status}: a World rooted at this twin reads nothing back`);
533
+ }
534
+ }
535
+ }
536
+ finally {
537
+ await server.stop();
538
+ }
539
+ }
540
+ // one defect repeated over a surface reads as one: the first 40 findings, and how many more
541
+ return { failures: out.length > 40 ? [...out.slice(0, 40), `… ${out.length - 40} more`] : out, note: `${tally.calls} served operations called, twice each: ${tally.ok} answered successfully, ${tally.refused} refused, unscripted` };
542
+ }
359
543
  /** A line protocol's connection over TCP, in the walk's stream shape. A line drew its replies when bytes arrived and then
360
544
  * 50 ms passed with no more (a multi-line reply arrives in pieces), or when 500 ms passed with none (a line that draws no
361
545
  * reply: a message's body before its end); 10 s bounds a server that keeps writing. The windows are timing, not protocol:
@@ -429,7 +613,9 @@ async function lifeServed(pack) {
429
613
  socket: (url, headers) => new WebSocket(`ws://127.0.0.1:${port}${url}`, { headers }),
430
614
  // a line connection opened as an application's client opens it: TCP to the protocol listener (the primary port)
431
615
  stream: lineConnection(server.port),
432
- stop: () => server.stop(),
616
+ // bounded: Bun's server.stop(true) never settles once a WebSocket peer has closed under it (discord's life, Bun
617
+ // 1.3.11: the walk ended and the stop spun at full CPU for 45 minutes), so the check waits 5 s and goes on
618
+ stop: async () => { await within(5000, Promise.resolve(server.stop())); },
433
619
  };
434
620
  };
435
621
  const world = inMemoryWorld(`conform-${basename(pack)}`);
@@ -465,7 +651,9 @@ async function clientCases(pack, scratch) {
465
651
  const descriptor = (await manifestOf(pack)).descriptor;
466
652
  // the first host that names one: a descriptor may lead with a suffix (a region's hosts under one domain), which no
467
653
  // request addresses; the case's client reaches whatever host its issued credentials name
468
- const host = descriptor?.hosts?.find((h) => h.host)?.host;
654
+ // a raw protocol's pack (hostsNone: SMTP, whose relay is whatever host the operator names) is reached by its TCP
655
+ // connection alone; its client names a stand-in host the connection never resolves
656
+ const host = descriptor?.hosts?.find((h) => h.host)?.host ?? (descriptor?.hostsNone ? 'relay.world.test' : undefined);
469
657
  if (!host)
470
658
  return { failures: ['the descriptor names no host for its client to address'] };
471
659
  const scenarioPath = join(scratch, 'client-scenario.json');
@@ -488,13 +676,31 @@ async function clientCases(pack, scratch) {
488
676
  const issued = {};
489
677
  const door = descriptor?.credentialDoor;
490
678
  if (door) {
491
- const answer = (await (await fetch(`http://127.0.0.1:${server.port}${door.path}`, { method: 'POST', headers: { host, 'content-type': 'application/json' }, body: JSON.stringify(door.body) })).json().catch(() => ({})));
679
+ // a door that answers nothing is the pack's failure, never conform's hang (a pack served with a scenario that answered
680
+ // nothing once held this request open for good)
681
+ const abort = new AbortController();
682
+ const asked = await within(10_000, fetch(`http://127.0.0.1:${server.port}${door.path}`, { method: 'POST', headers: { host, 'content-type': 'application/json' }, body: JSON.stringify(door.body), signal: abort.signal }).then((r) => r.json().catch(() => ({}))));
683
+ abort.abort();
684
+ if (asked === 'timeout')
685
+ return { failures: [`the credential door (POST ${door.path}) answered nothing in 10 s, served with a scenario loaded`] };
686
+ const answer = asked;
492
687
  for (const [name, field] of Object.entries(door.fill))
493
688
  if (answer[field] !== undefined)
494
689
  issued[name] = String(answer[field]);
495
690
  }
496
691
  const job = join(scratch, 'client-job.json');
497
- writeFileSync(job, JSON.stringify({ vendor, port: server.port, host, env: issued, scenarioPath, packFacts: join(pack, 'generated', 'pack-facts.json') }));
692
+ // a release carries its facts (generated/pack-facts.json, `twin-standard facts`); a source checkout does not, so its
693
+ // facts are compiled here, as that command compiles them, for the client the case runs
694
+ let packFacts = join(pack, 'generated', 'pack-facts.json');
695
+ if (!existsSync(packFacts)) {
696
+ const { compilePackFacts, renderPackFacts } = await import("./pack-facts.js");
697
+ const compiled = await compilePackFacts([{ vendor, dir: pack }]);
698
+ if (compiled.undeclared.length)
699
+ throw new Error('Pack descriptor is missing');
700
+ packFacts = join(scratch, 'pack-facts.json');
701
+ writeFileSync(packFacts, renderPackFacts(compiled));
702
+ }
703
+ writeFileSync(job, JSON.stringify({ vendor, port: server.port, host, env: issued, scenarioPath, packFacts, ...(descriptor?.hostsNone ? { raw: true } : {}) }));
498
704
  // Node, as an application runs its SDK (asynchronous: this process serves the pack meanwhile)
499
705
  const out = await new Promise((resolve) => {
500
706
  const child = spawn('node', ['--experimental-strip-types', '--no-warnings', join(clients, `run${extension}`), job], { stdio: ['ignore', 'pipe', 'pipe'] });
@@ -514,8 +720,46 @@ async function clientCases(pack, scratch) {
514
720
  await server.stop();
515
721
  }
516
722
  }
723
+ /** The gate over every unit of the pack: the form section of the grade, which holds the rules an author's code keeps
724
+ * (p3-rules.ts: ids from ctx.mint, the twin's URL from ctx.publicBase, the kernel's crypto, bytes in ctx.blobs, secrets
725
+ * drawn from ctx.secret) and the pack layout, handler, kernel and manifest contracts. A contributor's conform is the
726
+ * whole of what a machine decides of the pack, as the catalog's admission reads it. */
727
+ async function formOf(pack) {
728
+ const { p3Gate } = await import("./gate.js");
729
+ return (await p3Gate(unitsIn(pack))).flatMap((r) => r.failures.map((f) => `${nameOf(pack, r.unit)}: ${f}`));
730
+ }
731
+ /** The pack's records hold what a reviewer reads them for: each demand row cites the application's line it was read at
732
+ * (`<path>:<line>`, or `<package>@<version>:<path>:<line>` for a call a dependency makes), the spec's record names a
733
+ * vendored file that is there, the life names the session that wrote it (what makes a judge's verdict independent),
734
+ * and the package says what it serves in a README. Rules every builder brief carried by hand, decided here. */
735
+ function records(pack) {
736
+ const out = [];
737
+ if (!existsSync(join(pack, 'README.md')))
738
+ out.push('README.md is missing: the package says what it serves, for whom, its doors and what it leaves out');
739
+ const life = readJson(join(pack, 'journeys', 'customer-life.json'));
740
+ if (life && (typeof life.author !== 'string' || !life.author.trim()))
741
+ out.push('journeys/customer-life.json names no author (the session that wrote it), so no verdict on it can be shown independent');
742
+ for (const unit of unitsIn(pack)) {
743
+ const name = nameOf(pack, unit);
744
+ const journeys = hasJourneySource(unit) ? join(unit, 'journeys') : join(unit, '..', 'journeys');
745
+ const demand = unit === pack || hasJourneySource(unit) ? readJson(join(journeys, 'demand.json')) : undefined;
746
+ for (const app of demand?.applications ?? [])
747
+ for (const row of app.backend ?? []) {
748
+ if (typeof row.evidence !== 'string' || !/:\d+/.test(row.evidence))
749
+ out.push(`${name}: ${app.name ?? 'an application'}'s call of ${row.operation ?? '(no operation)'} cites no line it was read at (evidence "<path>:<line>")`);
750
+ }
751
+ if (!hasOwnSurface(unit))
752
+ continue;
753
+ const source = existsSync(join(unit, 'spec', 'SOURCE.md')) ? readFileSync(join(unit, 'spec', 'SOURCE.md'), 'utf8') : '';
754
+ const file = /\*\*File:\*\*[^`\n]*`([^`]+)`/.exec(source)?.[1];
755
+ if (file && !existsSync(join(unit, 'spec', file)))
756
+ out.push(`${name}: spec/SOURCE.md names ${file} as the vendored spec, and spec/ holds no such file`);
757
+ }
758
+ return out;
759
+ }
517
760
  /** Every check over the pack at `dir`, in order. */
518
- export async function conformance(dir) {
761
+ /** `onCheck` hears each check's id as it starts, so a caller can show where a long run stands. */
762
+ export async function conformance(dir, options = {}) {
519
763
  const pack = resolve(dir);
520
764
  if (!existsSync(join(pack, 'package.json')))
521
765
  throw new Error(`${dir} is no pack (no package.json): conformance takes a pack's directory`);
@@ -525,8 +769,10 @@ export async function conformance(dir) {
525
769
  const here = process.cwd();
526
770
  process.chdir(scratch);
527
771
  const run = async (id, asks, check) => {
772
+ options.onCheck?.(id);
528
773
  try {
529
- return { id, asks, failures: await check() };
774
+ const r = await check();
775
+ return Array.isArray(r) ? { id, asks, failures: r } : { id, asks, failures: r.failures, note: r.note };
530
776
  }
531
777
  catch (error) {
532
778
  return { id, asks, failures: [`the check could not run: ${error.message.split('\n')[0]}`] };
@@ -541,6 +787,7 @@ export async function conformance(dir) {
541
787
  await run('registered', 'a vendor-backed unit, lane or not, reaches the registered pack\'s state system', () => registered(pack, runtime)),
542
788
  await run('allowance', 'a rate budget above the fallback rests on the vendor\'s documented allowance, cited in its manifest', () => allowance(pack, runtime)),
543
789
  await (async () => {
790
+ options.onCheck?.('client');
544
791
  const asks = "driven by the vendor's own client (its official SDK), served as a World runs it, the vendor's documented behaviour holds";
545
792
  try {
546
793
  const r = await clientCases(pack, scratch);
@@ -552,6 +799,9 @@ export async function conformance(dir) {
552
799
  })(),
553
800
  await run('life', 'the life walked over HTTP against the pack served as a World runs it, with its scenario: every check held', () => lifeServed(pack)),
554
801
  await run('standalone', 'served by its server.ts as a World runs it, the pack answers HTTP and the boot probe, and each declared socket upgrades', () => standalone(pack)),
802
+ await run('surface', 'every served operation called over HTTP as an application calls it, plainly and with a scenario loaded: answered within 10 s, no undeclared 500, no field of the twin\'s own, a success in the spec\'s shape, and every refresh list a fresh account reads answering', () => surfaceSwept(pack, scratch)),
803
+ await run('records', 'each demand row cites the line it was read at, the spec\'s record names a vendored file that is there, the life names its author, the package has its README', () => records(pack)),
804
+ await run('form', 'every unit passes the form section of the grade (the gate): the layout, the handler and kernel contracts, the manifest as data, the rules an author\'s code holds', () => formOf(pack)),
555
805
  ];
556
806
  }
557
807
  finally {
@@ -201,7 +201,11 @@ export async function runConsumerWorkflow(packDir, options) {
201
201
  throw new Error('Resume changed recorded application state before readback');
202
202
  await run('retained-readback', ['run', '--', ...recipe.retention.readback]);
203
203
  const after = JSON.parse(await run('log-after-readback', ['log', '--json']));
204
- if (JSON.stringify(before) !== JSON.stringify(after))
204
+ // the readback adds no write of the application's: what it adds may only be the vendor's own moves (a key's
205
+ // last-used time, recorded by the vendor as a request uses it: actor `system`, ctx.asVendor), never an API call's
206
+ const added = after.filter((entry) => !before.some((prior) => prior.id === entry.id));
207
+ const kept = before.every((prior) => after.some((entry) => JSON.stringify(entry) === JSON.stringify(prior)));
208
+ if (!kept || added.some((entry) => entry.actor?.kind !== 'system'))
205
209
  throw new Error('Retained readback added mutations; it must only inspect existing state');
206
210
  retainedReadback = true;
207
211
  }
@@ -39,4 +39,9 @@ export declare function initFiles(vendor: string): Record<string, string>;
39
39
  * surface of its own under `<lane>/src`, keeping the vendor's one store and sharing its `src/semantics/shared.ts`, and the
40
40
  * pack's server routing each lane's host to it. */
41
41
  export declare function laneInitFiles(vendor: string, lanes: string[]): Record<string, string>;
42
+ /** A unit's fixed files as the manifest and the spec make them now (architecture, "Creating a pack"): a lane's fetch, and
43
+ * a pack's fetch, index, server and cli. A pack with a wire of its own (`src/stream.ts`, SMTP's and Redis's byte-stream
44
+ * door) writes its server and cli itself: they carry its listener beside the fetch, so they are not fixed. `--index`
45
+ * writes these and the grade's `fixed-reproducible` compares them byte for byte. */
46
+ export declare function fixedFiles(dir: string): Promise<Record<string, string>>;
42
47
  export declare function createPack(args: string[]): Promise<void>;