@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.
Files changed (103) 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 +6 -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 -3
  25. package/dist/src/conformance.js +293 -8
  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 +38 -4
  29. package/dist/src/derive.js +21 -13
  30. package/dist/src/gate.d.ts +1 -0
  31. package/dist/src/gate.js +47 -4
  32. package/dist/src/grade.d.ts +1 -0
  33. package/dist/src/grade.js +1 -1
  34. package/dist/src/pack-facts.d.ts +9 -1
  35. package/dist/src/pack-facts.js +11 -2
  36. package/dist/src/pack-files.js +2 -1
  37. package/dist/src/protocol-3.js +40 -6
  38. package/dist/src/spec-ir-client.js +3 -2
  39. package/dist/src/spec-ir-commands.d.ts +73 -0
  40. package/dist/src/spec-ir-commands.js +63 -0
  41. package/dist/src/spec-ir.d.ts +2 -0
  42. package/dist/src/spec-ir.js +43 -0
  43. package/dist/src/tools/judge-brief.d.ts +1 -0
  44. package/dist/src/tools/judge-brief.js +67 -0
  45. package/dist/src/tools/life-coverage-walk.d.ts +2 -0
  46. package/dist/src/tools/life-coverage-walk.js +220 -0
  47. package/dist/src/tools/life-coverage.d.ts +2 -0
  48. package/dist/src/tools/life-coverage.js +271 -0
  49. package/dist/src/tools/packs-root.d.ts +9 -0
  50. package/dist/src/tools/packs-root.js +17 -0
  51. package/dist/src/tools/run-tool.d.ts +1 -0
  52. package/dist/src/tools/run-tool.js +18 -0
  53. package/dist/src/tools/score-pack.d.ts +2 -0
  54. package/dist/src/tools/score-pack.js +264 -0
  55. package/dist/src/tools/vendor-examples.d.ts +2 -0
  56. package/dist/src/tools/vendor-examples.js +1493 -0
  57. package/dist/src/types.d.ts +2 -0
  58. package/dist/src/verdict.js +1 -1
  59. package/dist/src/walk/behavior-journey.d.ts +6 -0
  60. package/dist/src/walk/behavior-journey.js +108 -8
  61. package/dist/src/walk/journey-kit.d.ts +23 -1
  62. package/dist/src/walk/journey-kit.js +7 -2
  63. package/dist/src/walk/rendered-scalar.d.ts +3 -0
  64. package/dist/src/walk/rendered-scalar.js +23 -0
  65. package/dist/src/walk/sse-example.js +45 -0
  66. package/package.json +2 -2
  67. package/src/assessment.ts +2 -1
  68. package/src/cli.ts +15 -2
  69. package/src/clients/harness.ts +4 -2
  70. package/src/clients/jira.ts +73 -0
  71. package/src/clients/polar.ts +73 -0
  72. package/src/clients/resend.ts +32 -0
  73. package/src/clients/run.ts +2 -2
  74. package/src/clients/sentry.ts +1 -25
  75. package/src/clients/smtp.ts +49 -0
  76. package/src/clients/stripe.ts +1 -1
  77. package/src/clients/volteridentity.ts +4 -3
  78. package/src/clients/x.ts +66 -0
  79. package/src/conformance.ts +226 -10
  80. package/src/consumer.ts +5 -1
  81. package/src/create-pack.ts +35 -5
  82. package/src/derive.ts +21 -14
  83. package/src/gate.ts +40 -6
  84. package/src/grade.ts +2 -2
  85. package/src/pack-facts.ts +10 -3
  86. package/src/pack-files.ts +2 -1
  87. package/src/protocol-3.ts +32 -6
  88. package/src/spec-ir-client.ts +3 -2
  89. package/src/spec-ir-commands.ts +81 -0
  90. package/src/spec-ir.ts +31 -0
  91. package/src/tools/judge-brief.ts +63 -0
  92. package/src/tools/life-coverage-walk.ts +173 -0
  93. package/src/tools/life-coverage.ts +237 -0
  94. package/src/tools/packs-root.ts +21 -0
  95. package/src/tools/run-tool.ts +10 -0
  96. package/src/tools/score-pack.ts +244 -0
  97. package/src/tools/vendor-examples.ts +1250 -0
  98. package/src/types.ts +2 -0
  99. package/src/verdict.ts +1 -1
  100. package/src/walk/behavior-journey.ts +65 -4
  101. package/src/walk/journey-kit.ts +21 -3
  102. package/src/walk/rendered-scalar.ts +17 -0
  103. package/src/walk/sse-example.ts +37 -0
@@ -56,4 +56,36 @@ export const cases: ClientCase[] = [
56
56
  return answer.data === null && answer.error?.statusCode === 422 && answer.error?.name === 'missing_required_field' ? [] : [`missing sender returned ${JSON.stringify(answer)}`];
57
57
  },
58
58
  },
59
+ {
60
+ id: 'unverified-domain-validation-error',
61
+ asks: 'a send from a domain the team never verified comes back as the SDK error validation_error (403), with no data',
62
+ 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."`,
63
+ async run(c) {
64
+ const resend = client(c);
65
+ const sent = await resend.emails.send({ from: 'Someone <someone@never-verified.example>', to: ['client@example.org'], subject: 'Not from a verified domain', text: 'Refused.' });
66
+ const failures: string[] = [];
67
+ if (sent.data !== null) failures.push(`the send answered data ${JSON.stringify(sent.data)}`);
68
+ const error = sent.error as { name?: string; statusCode?: number | null } | null;
69
+ if (error?.name !== 'validation_error') failures.push(`the error is ${JSON.stringify(error)}, not validation_error`);
70
+ else if (error.statusCode !== 403) failures.push(`the validation_error's status is ${String(error.statusCode)}, not 403`);
71
+ return failures;
72
+ },
73
+ },
74
+ {
75
+ id: 'scheduled-send-waits',
76
+ asks: "a send scheduled for later (the SDK's scheduledAt, sent as scheduled_at) waits: it reads back scheduled, at its time",
77
+ source: `${SEND} "Schedule email to be sent later."`,
78
+ async run(c) {
79
+ const resend = client(c);
80
+ const at = '2030-01-01T08:00:00.000Z';
81
+ const sent = await resend.emails.send({ from: 'Acme <onboarding@resend.dev>', to: ['owner@world.test'], subject: 'Later', text: 'In good time.', scheduledAt: at });
82
+ if (sent.error || !sent.data?.id) return [`the send answered ${JSON.stringify(sent).slice(0, 200)}`];
83
+ const read = await resend.emails.get(sent.data.id);
84
+ const email = read.data as { last_event?: string; scheduled_at?: string | null } | null;
85
+ const failures: string[] = [];
86
+ if (email?.last_event !== 'scheduled') failures.push(`the scheduled email's last_event is ${String(email?.last_event)}`);
87
+ if (email?.scheduled_at === undefined || email.scheduled_at === null || Date.parse(email.scheduled_at) !== Date.parse(at)) failures.push(`it reads back scheduled_at ${String(email?.scheduled_at)}`);
88
+ return failures;
89
+ },
90
+ },
59
91
  ];
@@ -7,7 +7,7 @@ import { extname } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { clientContext, worldProxy, type ClientCase } from './harness.ts';
9
9
 
10
- const job = JSON.parse(readFileSync(process.argv[2]!, 'utf8')) as { vendor: string; port: number; host: string; env: Record<string, string>; scenarioPath: string; packFacts: string };
10
+ const job = JSON.parse(readFileSync(process.argv[2]!, 'utf8')) as { vendor: string; port: number; host: string; env: Record<string, string>; scenarioPath: string; packFacts: string; raw?: boolean };
11
11
  const { cases } = (await import(`./${job.vendor}${extname(fileURLToPath(import.meta.url))}`)) as { cases: ClientCase[] };
12
12
  if (!Array.isArray(cases) || cases.length === 0) throw new Error(`${job.vendor} client module declares no cases; there is no SDK execution evidence`);
13
13
  // every connection a case makes goes through the World's proxy, as an application's does in a World
@@ -19,7 +19,7 @@ require('@volter/world-core/pack-facts').extendPackFacts([job.packFacts]);
19
19
  const facts = JSON.parse(readFileSync(job.packFacts, 'utf8')).packs[job.vendor];
20
20
  const keys: string[] = [...new Set<string>((facts.hosts ?? []).map((rule: { key?: string }) => rule.key ?? job.vendor))];
21
21
  require('@volter/world-core/inject').install(Object.fromEntries(keys.map((key) => [key, `http://127.0.0.1:${proxy}`])));
22
- const context = clientContext(proxy, job.host, job.env, job.scenarioPath);
22
+ const context = clientContext(proxy, job.host, job.env, job.scenarioPath, job.raw ? job.port : undefined);
23
23
  for (const c of cases) {
24
24
  const failures = await Promise.race([
25
25
  c.run(context).catch((e: Error) => [`the case threw: ${e.message}`]),
@@ -22,7 +22,6 @@ async function sdkOf(name: string): Promise<any> {
22
22
  const pins = [
23
23
  ['rallly', '@sentry/node', '10.38.0'],
24
24
  ['twenty', 'sentry-node-twenty', '10.73.0'],
25
- ['postiz', 'sentry-node-postiz', '10.56.0'],
26
25
  ['cal-api', 'sentry-node-cal', '9.46.0'],
27
26
  ['cal-web', 'sentry-node-cal-web', '10.33.0'],
28
27
  ] as const;
@@ -67,7 +66,7 @@ export const cases: ClientCase[] = pins.map(([application, name, version]) => ({
67
66
  },
68
67
  }));
69
68
 
70
- for (const [name, version] of [['@sentry/browser', '10.38.0'], ['sentry-browser-twenty', '10.59.0'], ['sentry-browser-postiz', '10.56.0']] as const) cases.push({
69
+ for (const [name, version] of [['@sentry/browser', '10.38.0'], ['sentry-browser-twenty', '10.59.0']] as const) cases.push({
71
70
  id: 'browser-' + version + '-dsn-query-auth-and-session-envelope',
72
71
  asks: 'the browser fetch transport uses DSN query authentication and flushes event and session envelopes',
73
72
  source: `https://unpkg.com/@sentry/core@${version}/build/esm/api.js "sentry_key: publicKey"`,
@@ -133,26 +132,3 @@ cases.push({
133
132
  });
134
133
 
135
134
 
136
- cases.push({
137
- id: 'postiz-feedback-attribution',
138
- asks: 'captureFeedback preserves the widget message, explicit name/email, associated event and scoped user',
139
- source: 'https://unpkg.com/@sentry/core@10.56.0/build/esm/feedback.js "associated_event_id: associatedEventId"',
140
- async run(c) {
141
- const sdk = await sdkOf('sentry-browser-postiz');
142
- const client = new sdk.BrowserClient({ dsn: c.env.SENTRY_DSN, integrations: [], stackParser: sdk.defaultStackParser,
143
- transport: (options: any) => sdk.createTransport(options, async (request: { body: string | Uint8Array }) => {
144
- const answer = await c.fetch(options.url, { method: 'POST', body: request.body as BodyInit });
145
- return { statusCode: answer.status, headers: {} };
146
- }) });
147
- let id = '';
148
- sdk.withScope((scope: any) => {
149
- scope.setClient(client); scope.setUser({ id: 'feedback-author' });
150
- id = sdk.captureFeedback({ message: 'The scheduling button is hard to find', name: 'Priya', email: 'priya@harbourline.test', associatedEventId: '00000000000000000000000000000002' }, {}, scope);
151
- });
152
- const drained = await client.flush(10_000);
153
- const event = await read(c, id, await token(c));
154
- await client.close(10_000);
155
- const feedback = event.contexts?.feedback;
156
- 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'];
157
- },
158
- });
@@ -0,0 +1,49 @@
1
+ // Nodemailer 10, unmodified, as Dub, Rallly, Cal.com, Twenty and LibreChat send their mail: its SMTP transport, plain
2
+ // on the relay's port, given the World's TCP connection through its getSocket hook (SMTP is a raw protocol: the case
3
+ // names a stand-in relay host the connection never resolves). The relay's policy at boot asks no authentication.
4
+ import type { ClientCase, ClientContext } from './harness.ts';
5
+
6
+ const RFC5321 = 'https://www.rfc-editor.org/rfc/rfc5321';
7
+
8
+ async function transport(c: ClientContext) {
9
+ const nodemailer = (await import('nodemailer')).default;
10
+ return nodemailer.createTransport({
11
+ host: c.host, port: 25, secure: false, ignoreTLS: true,
12
+ getSocket: (_options: unknown, callback: (error: Error | null, socket?: { connection: unknown }) => void) => callback(null, { connection: c.connect() }),
13
+ } as never);
14
+ }
15
+
16
+ export const cases: ClientCase[] = [
17
+ {
18
+ id: 'verify-greets',
19
+ asks: "nodemailer's verify opens the session, greets with EHLO and quits: the relay answers it as an SMTP server",
20
+ source: `${RFC5321} "These commands are used to identify the SMTP client to the SMTP server."`,
21
+ async run(c) {
22
+ const t = await transport(c);
23
+ try {
24
+ return (await t.verify()) === true ? [] : ['verify resolved without true'];
25
+ } catch (error) {
26
+ return [`verify failed: ${String(error).slice(0, 200)}`];
27
+ } finally { t.close(); }
28
+ },
29
+ },
30
+ {
31
+ id: 'transaction-to-two-recipients',
32
+ 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',
33
+ 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."`,
34
+ async run(c) {
35
+ const t = await transport(c);
36
+ try {
37
+ 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>' }) as { accepted: string[]; rejected: string[]; response: string; messageId: string };
38
+ const failures: string[] = [];
39
+ if (JSON.stringify([...info.accepted].sort()) !== JSON.stringify(['an@case.example', 'bo@case.example'])) failures.push(`accepted ${JSON.stringify(info.accepted)}`);
40
+ if (info.rejected.length) failures.push(`rejected ${JSON.stringify(info.rejected)}`);
41
+ if (!info.response.startsWith('250')) failures.push(`the data was answered ${info.response}`);
42
+ if (!info.messageId) failures.push('no message id');
43
+ return failures;
44
+ } catch (error) {
45
+ return [`the send failed: ${String(error).slice(0, 200)}`];
46
+ } finally { t.close(); }
47
+ },
48
+ },
49
+ ];
@@ -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
  import type { ClientCase, ClientContext } from './harness.ts';
9
10
 
@@ -36,7 +37,7 @@ async function setup(c: ClientContext, name: string, shared = false): Promise<{
36
37
  await send(c, `/admin/clients/${registered.client_id}/resources`, { resource: 'https://teams.sdk.example' }, operator);
37
38
  }
38
39
  const verifier = 'official-sdk-case-verifier-with-enough-entropy-for-s256';
39
- 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' });
40
+ 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' });
40
41
  const authorized = await send(c, `/api/auth/oauth2/authorize?${query}`, undefined, { cookie });
41
42
  const code = new URL(authorized.headers.get('location')!).searchParams.get('code')!;
42
43
  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,66 @@
1
+ // twitter-api-v2 1.29, unmodified (one of the SDKs the x descriptor's adoption names),
2
+ // with an OAuth 2.0 user access token made at the pack's doors for an account made there, as a person's sign-in at
3
+ // x.com and an App's authorize leg make them. The client sends through node's https with the agent it is given, so the
4
+ // World's routed agent is its httpAgent.
5
+ import type { ClientCase, ClientContext } from './harness.ts';
6
+
7
+ const CREATE = 'https://docs.x.com/x-api/posts/create-post';
8
+ const ME = 'https://docs.x.com/x-api/users/get-my-user';
9
+ const README = 'https://www.npmjs.com/package/twitter-api-v2/v/1.29.1';
10
+
11
+ /** An account named `username` and an access token for it with `scopes`; the client on that token. */
12
+ async function signedIn(c: ClientContext, username: string, scopes: string[], token = `case-${username}-token`) {
13
+ const door = async (path: string, body: unknown) => {
14
+ const answer = await c.fetch(`https://${c.host}${path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
15
+ if (answer.status >= 300) throw new Error(`${path} answered ${answer.status}`);
16
+ return ((await answer.json()) as { data: Record<string, string> }).data;
17
+ };
18
+ const account = await door('/_twin/accounts', { username, name: 'Case Account' });
19
+ await door('/_twin/tokens', { token, account_id: account.id, scopes });
20
+ const { TwitterApi } = await import('twitter-api-v2');
21
+ return { client: new TwitterApi(token, { httpAgent: c.agent }), account };
22
+ }
23
+
24
+ export const cases: ClientCase[] = [
25
+ {
26
+ id: 'post-and-read-back',
27
+ 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",
28
+ source: `${CREATE} "Create Posts"`,
29
+ async run(c) {
30
+ const { client, account } = await signedIn(c, 'case_poster', ['tweet.read', 'tweet.write', 'users.read']);
31
+ const made = await client.v2.tweet('Shipping the case today');
32
+ const failures: string[] = [];
33
+ if (!made.data?.id || made.data.text !== 'Shipping the case today') failures.push(`tweet answered ${JSON.stringify(made)}`);
34
+ const read = await client.v2.singleTweet(made.data.id, { 'tweet.fields': ['author_id'] });
35
+ if (read.data?.text !== 'Shipping the case today') failures.push(`the post reads back text ${JSON.stringify(read.data?.text)}`);
36
+ if (read.data?.author_id !== account.id) failures.push(`the post reads back author ${String(read.data?.author_id)}, not ${account.id}`);
37
+ return failures;
38
+ },
39
+ },
40
+ {
41
+ id: 'me-is-the-signed-in-account',
42
+ asks: "v2.me answers the account the token was issued to, by its id and username (an app's account check after sign-in)",
43
+ source: `${ME} "Get Users Me"`,
44
+ async run(c) {
45
+ const { client, account } = await signedIn(c, 'case_me', ['tweet.read', 'users.read']);
46
+ const me = await client.v2.me();
47
+ return me.data?.id === account.id && me.data.username === 'case_me' ? [] : [`me answered ${JSON.stringify(me)}`];
48
+ },
49
+ },
50
+ {
51
+ id: 'unknown-token-api-response-error',
52
+ asks: 'a token X never issued is refused, and the client throws its ApiResponseError carrying the 401',
53
+ source: `${README} "Typed errors, meaningful error messages, error enumerations for both v1.1 and v2"`,
54
+ async run(c) {
55
+ const { TwitterApi, ApiResponseError } = await import('twitter-api-v2');
56
+ const client = new TwitterApi('case-never-issued-token', { httpAgent: c.agent });
57
+ try {
58
+ await client.v2.me();
59
+ return ['the never-issued token was accepted'];
60
+ } catch (error) {
61
+ if (!(error instanceof ApiResponseError)) return [`the refusal is not an ApiResponseError: ${String(error).slice(0, 160)}`];
62
+ return error.code === 401 ? [] : [`the refusal's code is ${error.code}, not 401`];
63
+ }
64
+ },
65
+ },
66
+ ];
@@ -65,6 +65,38 @@ export function unpublishedUnitFiles(unit: string): string[] {
65
65
 
66
66
  /** Each of UNIT_FILES a unit lacks where conformance reads it: the manifest of every unit, the life at its vendor's root
67
67
  * (lifeServed), the spec's record and the decision table of a unit with a spec (decided()). */
68
+ const trackedJourneySources = new Map<string, string[]>();
69
+ /** Table ownership follows one index snapshot per checkout/run. */
70
+ function hasJourneySource(unit: string): boolean {
71
+ const dir = join(unit, 'journeys');
72
+ let checkout = resolve(unit);
73
+ while (!existsSync(join(checkout, '.git'))) {
74
+ const parent = dirname(checkout);
75
+ if (parent === checkout) {
76
+ // An unpacked published package has no Git index; its delivered files
77
+ // are its source. Markers and empty directories are not table inputs.
78
+ if (!existsSync(dir)) return false;
79
+ const holdsFile = (at: string): boolean => readdirSync(at, { withFileTypes: true }).some(entry => entry.name !== '.metadata_never_index'
80
+ && (entry.isFile() || (entry.isDirectory() && holdsFile(join(at, entry.name)))));
81
+ return holdsFile(dir);
82
+ }
83
+ checkout = parent;
84
+ }
85
+ const env = { ...process.env };
86
+ for (const key of ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_WORK_TREE', 'GIT_PREFIX', 'GIT_INDEX_FILE']) delete env[key];
87
+ if (process.env.VOLTER_PACK_HOOK_INDEX) env.GIT_INDEX_FILE = process.env.VOLTER_PACK_HOOK_INDEX;
88
+ const key = `${checkout}\0${env.GIT_INDEX_FILE ?? ''}`;
89
+ let source = trackedJourneySources.get(key);
90
+ if (!source) {
91
+ const files = spawnSync('git', ['-C', checkout, 'ls-files', '-z', '--cached', '--', ':(glob)**/journeys/**'], { env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 16 * 1024 * 1024 });
92
+ if (files.status !== 0 || files.error) throw new Error(`cannot read tracked journey source: ${String(files.stderr || files.error?.message)}`);
93
+ source = files.stdout.split('\0').filter(path => path && !path.split('/').includes('.metadata_never_index'));
94
+ trackedJourneySources.set(key, source);
95
+ }
96
+ const prefix = relative(checkout, dir).replaceAll('\\', '/') + '/';
97
+ return source.some(path => path.startsWith(prefix));
98
+ }
99
+
68
100
  export function unitFilesMissing(unit: string): string[] {
69
101
  const vendor = existsSync(join(unit, 'package.json')) ? unit : dirname(unit);
70
102
  const spec = hasOwnSurface(unit);
@@ -72,7 +104,7 @@ export function unitFilesMissing(unit: string): string[] {
72
104
  'src/manifest.ts': unit,
73
105
  'spec/SOURCE.md': spec ? unit : undefined,
74
106
  'journeys/customer-life.json': vendor,
75
- 'journeys/decisions.json': spec ? (existsSync(join(unit, 'journeys')) ? unit : vendor) : undefined,
107
+ 'journeys/decisions.json': spec ? (hasJourneySource(unit) ? unit : vendor) : undefined,
76
108
  };
77
109
  return UNIT_FILES.filter((f) => at[f] !== undefined && !existsSync(join(at[f]!, f))).map((f) => `${relative(unit, join(at[f]!, f))} is missing (conformance reads it)`);
78
110
  }
@@ -125,7 +157,7 @@ async function decided(pack: string): Promise<string[]> {
125
157
  for (const unit of unitsIn(pack).filter(hasOwnSurface)) {
126
158
  const name = nameOf(pack, unit);
127
159
  const key = unit === pack ? '' : basename(unit);
128
- const journeys = existsSync(join(unit, 'journeys')) ? join(unit, 'journeys') : join(unit, '..', 'journeys');
160
+ const journeys = hasJourneySource(unit) ? join(unit, 'journeys') : join(unit, '..', 'journeys');
129
161
  const owners = byUnit ? byUnit[key] ?? {} : (await makeOf(unit))?.().owners?.();
130
162
  if (!owners) { out.push(`${name}: its fetch (${fetchName(unit)}) names no owners() to check the decision table against`); continue; }
131
163
  const table = readJson(join(journeys, 'decisions.json')) as { decisions?: Decision[] } | undefined;
@@ -285,6 +317,131 @@ async function standalone(pack: string): Promise<string[]> {
285
317
  return out;
286
318
  }
287
319
 
320
+ type SweptOperation = {
321
+ id: string; method: string; path: string; basePath?: string; pathParams?: string[];
322
+ query?: Array<{ name: string; type?: string; required?: boolean }>; body?: Array<{ name: string; required?: boolean }>;
323
+ bodyEncoding?: string; headers?: Record<string, string | null>; discriminator?: Record<string, string | null>;
324
+ responseMedia?: Record<string, string[]>; answers?: unknown;
325
+ };
326
+
327
+ /** The request an application's first call of an operation sends when it knows nothing yet: every path parameter, every
328
+ * required query parameter and every header the operation is routed by holding a placeholder, its discriminator's
329
+ * values, and an empty body in the operation's encoding. What it measures is the twin's answer to a call it did not
330
+ * 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. */
331
+ function sweepRequest(op: SweptOperation, basePath: string, host: string, credential: Record<string, string>): Request {
332
+ const fill = (name: string, type?: string): string => (type === 'integer' || type === 'number' ? '1' : type === 'boolean' ? 'true' : `conformance-${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`);
333
+ const path = `${op.basePath ?? basePath}${op.path}`.replace(/\{([^}+]+)\+?\}/g, (_m, name: string) => fill(name));
334
+ const url = new URL(`https://${host}${path}`);
335
+ for (const q of op.query ?? []) if (q.required) url.searchParams.set(q.name, fill(q.name, q.type));
336
+ for (const [name, value] of Object.entries(op.discriminator ?? {})) url.searchParams.set(name, value ?? fill(name));
337
+ const headers = new Headers(credential);
338
+ for (const [name, value] of Object.entries(op.headers ?? {})) headers.set(name, value ?? fill(name));
339
+ const method = op.method.toUpperCase();
340
+ let body: string | undefined;
341
+ if (op.bodyEncoding === 'form') { headers.set('content-type', 'application/x-www-form-urlencoded'); body = ''; }
342
+ else if (op.bodyEncoding === 'xml') { headers.set('content-type', 'application/xml'); body = ''; }
343
+ else if (op.bodyEncoding === 'json' || op.bodyEncoding === 'multipart') { headers.set('content-type', 'application/json'); body = '{}'; }
344
+ return new Request(url, { method, headers, ...(body !== undefined && !['GET', 'HEAD'].includes(method) ? { body } : {}) });
345
+ }
346
+
347
+ /** Every operation the pack serves, called over HTTP as an application calls it, against its server.ts served as a World
348
+ * runs it: once plainly and once with a scenario loaded (an empty one, as a World that scripts nothing loads it). Each
349
+ * answers within 10 s, never with a 500 the spec does not declare, never with a field of the twin's own (`_`
350
+ * bookkeeping, the kernel's `updatedAt`) and, answered successfully, in the shape the spec gives the operation; and every
351
+ * refresh list a fresh account can read (one with no parameter in its path) answers it successfully, so a World rooted
352
+ * at this twin reads its resources back. Escaped before: the scenario hang (a served pack given a scenario answered
353
+ * nothing), leaked kernel fields in answers no life step read, and refresh scopes on operations the pack answered with
354
+ * the gap. The life walks what a customer does; this calls what the pack declares it serves. */
355
+ async function surfaceSwept(pack: string, scratch: string): Promise<{ failures: string[]; note: string }> {
356
+ const out: string[] = [];
357
+ const descriptor = ((await manifestOf(pack)) as { descriptor?: { hosts?: Array<{ host?: string }>; credentialDoor?: { path: string; body: Record<string, unknown>; fill: Record<string, string> } } }).descriptor;
358
+ const fetchOf = ((await import(join(pack, 'src', 'fetch.ts'))) as Record<string, unknown>)[fetchName(pack)] as (() => { owners?: () => Owners; ownersByUnit?: () => Record<string, Owners> }) | undefined;
359
+ const root = fetchOf?.();
360
+ const byUnit = root?.ownersByUnit?.() ?? { '': root?.owners?.() ?? {} };
361
+ const { shapeChecker } = await import('./walk/shape.ts');
362
+ const { leaked } = await import('./walk/behavior-journey.ts');
363
+ const shape = await shapeChecker(pack);
364
+ type Unit = { name: string; host: string; basePath: string; ops: SweptOperation[]; refreshLists: Array<{ resource: string; id: string }>; auth?: { header: string; scheme: string } };
365
+ const units: Unit[] = [];
366
+ const vendorFields = new Set<string>();
367
+ for (const unit of unitsIn(pack).filter(hasOwnSurface)) {
368
+ const surface = readJson(join(unit, 'src', 'generated', 'surface.gen.json')) as { format?: string; basePath?: string; operations?: SweptOperation[]; resources?: Array<{ fields: Array<{ name: string }> }> } | undefined;
369
+ for (const r of surface?.resources ?? []) for (const f of r.fields) vendorFields.add(f.name);
370
+ // a line protocol and a command table ride in no path of their own: the life's line steps and the examples read them
371
+ if (!surface?.operations || ['command-reply', 'command-table'].includes(String(surface.format))) continue;
372
+ const m = (await manifestOf(unit)) as Manifest & { hosts?: Array<{ host?: string }>; auth?: { header: string; scheme: string } };
373
+ const host = m.hosts?.find((h) => h.host)?.host ?? descriptor?.hosts?.find((h) => h.host)?.host;
374
+ if (!host) { out.push(`${nameOf(pack, unit)}: no host is named (its manifest's hosts or the descriptor's) for an application to call it at`); continue; }
375
+ const owners = byUnit[unit === pack ? '' : basename(unit)] ?? {};
376
+ const refreshLists = Object.entries(m.resources ?? {}).flatMap(([resource, decl]) => (decl.refresh?.list ? [{ resource, id: decl.refresh.list }] : []));
377
+ 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 } : {}) });
378
+ }
379
+ if (!units.length) return { failures: out, note: 'no unit serves an operation on a path of its own' };
380
+ const tally = { calls: 0, ok: 0, refused: 0 };
381
+ const env = (globalThis as { process: { env: Record<string, string | undefined> } }).process.env;
382
+ const scenarioPath = join(scratch, 'surface-scenario.json');
383
+ writeFileSync(scenarioPath, '{"handlers":[]}');
384
+ for (const mode of ['plain', 'scenario'] as const) {
385
+ const before = env.VOLTER_WORLD_BOOT_ID;
386
+ env.VOLTER_WORLD_BOOT_ID = 'conformance';
387
+ let server: Served;
388
+ try { server = await serve(pack, { root: join(scratch, `surface-${mode}`), ...(mode === 'scenario' ? { scenarioPath } : {}) }); } finally { if (before === undefined) delete env.VOLTER_WORLD_BOOT_ID; else env.VOLTER_WORLD_BOOT_ID = before; }
389
+ const http = servedHttpFetch(server.inspectPort ?? server.port);
390
+ const send = async (request: Request): Promise<Response | 'timeout' | Error> => {
391
+ await server.managedDatabaseUrl?.();
392
+ const abort = new AbortController();
393
+ try { return await within(10_000, http(request, { signal: abort.signal })); } catch (error) { return error as Error; } finally { abort.abort(); }
394
+ };
395
+ try {
396
+ // the account an application holds: the key the credential door issues, sent as the vendor reads its credential
397
+ let issued: string | undefined;
398
+ if (descriptor?.credentialDoor) {
399
+ const door = descriptor.credentialDoor;
400
+ 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) }));
401
+ const fields = answer instanceof Response ? ((await answer.json().catch(() => ({}))) as Record<string, unknown>) : {};
402
+ issued = Object.values(door.fill).map((f) => fields[f]).find((v): v is string => typeof v === 'string');
403
+ }
404
+ const said = mode === 'scenario' ? ' with a scenario loaded' : '';
405
+ for (const u of units) {
406
+ // a unit whose manifest declares no `auth` checks its key in its own front: the key goes as most vendors read
407
+ // one, a bearer token, and a refusal of it is no finding of the refresh below
408
+ const auth = u.auth ?? { header: 'authorization', scheme: 'Bearer' };
409
+ const credential: Record<string, string> = issued ? { [auth.header]: auth.scheme ? `${auth.scheme} ${issued}` : issued } : {};
410
+ const answered = new Map<string, number>();
411
+ for (const op of u.ops) {
412
+ const request = sweepRequest(op, u.basePath, u.host, credential);
413
+ const where = `${u.name}: ${op.id} (${request.method} ${new URL(request.url).pathname})`;
414
+ const res = await send(request);
415
+ if (res === 'timeout') { out.push(`${where} answered nothing in 10 s${said}`); continue; }
416
+ if (res instanceof Error) { out.push(`${where}: the connection failed${said} (${res.message})`); continue; }
417
+ answered.set(op.id, res.status);
418
+ if (mode === 'plain') { tally.calls++; if (res.status < 300) tally.ok++; else tally.refused++; }
419
+ const text = await res.text();
420
+ if (res.status === 500 && !op.responseMedia?.['500'] && !op.responseMedia?.['5XX']) { out.push(`${where} answered 500${said}, which the spec does not declare: ${text.slice(0, 160)}`); continue; }
421
+ if (mode === 'scenario') continue;
422
+ const type = res.headers.get('content-type') ?? '';
423
+ let body: unknown;
424
+ if (/json/.test(type)) { try { body = JSON.parse(text); } catch { body = undefined; } }
425
+ for (const key of leaked(body, vendorFields, '', !op.answers || res.status >= 300)) out.push(`${where} answers the twin's own field ${key}`);
426
+ if (res.status >= 200 && res.status < 300 && shape) {
427
+ const hit = shape.operationOf(request.method, request.url, Object.fromEntries(request.headers));
428
+ if (hit) for (const detail of shape.failures(hit.op, body, type, request.headers.get('accept') ?? undefined, res.status, { method: request.method, responseText: text })) out.push(`${where}: ${detail}`);
429
+ }
430
+ }
431
+ if (mode === 'plain') for (const r of u.refreshLists) {
432
+ const op = u.ops.find((o) => o.id === r.id);
433
+ if (!op || op.path.includes('{') || (op.basePath ?? '').includes('{')) continue;
434
+ const status = answered.get(r.id);
435
+ if (status !== undefined && (status === 401 || status === 403) && (!u.auth || !issued)) continue;
436
+ if (status !== undefined && (status < 200 || status >= 300)) 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`);
437
+ }
438
+ }
439
+ } finally { await server.stop(); }
440
+ }
441
+ // one defect repeated over a surface reads as one: the first 40 findings, and how many more
442
+ 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` };
443
+ }
444
+
288
445
  /** A line protocol's connection over TCP, in the walk's stream shape. A line drew its replies when bytes arrived and then
289
446
  * 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
290
447
  * reply: a message's body before its end); 10 s bounds a server that keeps writing. The windows are timing, not protocol:
@@ -344,7 +501,9 @@ async function lifeServed(pack: string): Promise<string[]> {
344
501
  socket: (url: string, headers: Record<string, string>) => new WebSocket(`ws://127.0.0.1:${port}${url}`, { headers } as unknown as string[]),
345
502
  // a line connection opened as an application's client opens it: TCP to the protocol listener (the primary port)
346
503
  stream: lineConnection(server.port),
347
- stop: () => server.stop(),
504
+ // bounded: Bun's server.stop(true) never settles once a WebSocket peer has closed under it (discord's life, Bun
505
+ // 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
506
+ stop: async () => { await within(5000, Promise.resolve(server.stop())); },
348
507
  };
349
508
  };
350
509
  const world = inMemoryWorld(`conform-${basename(pack)}`);
@@ -374,10 +533,12 @@ async function clientCases(pack: string, scratch: string): Promise<{ failures: s
374
533
  ? { failures: [`${vendor} names npm clients (${sdks.join(', ')}) and has no client cases: add packages/twin-standard/src/clients/${vendor}.ts, the SDK driving the pack as an application does, or the descriptor's clientCasesNone saying why none is owed`] }
375
534
  : { failures: [], note: `${vendor} declares no SDK of its own` };
376
535
  }
377
- const descriptor = ((await manifestOf(pack)) as { descriptor?: { hosts?: Array<{ host?: string; suffix?: string }>; credentialDoor?: { path: string; body: Record<string, unknown>; fill: Record<string, string> } } }).descriptor;
536
+ const descriptor = ((await manifestOf(pack)) as { descriptor?: { hosts?: Array<{ host?: string; suffix?: string }>; hostsNone?: string; credentialDoor?: { path: string; body: Record<string, unknown>; fill: Record<string, string> } } }).descriptor;
378
537
  // the first host that names one: a descriptor may lead with a suffix (a region's hosts under one domain), which no
379
538
  // request addresses; the case's client reaches whatever host its issued credentials name
380
- const host = descriptor?.hosts?.find((h) => h.host)?.host;
539
+ // a raw protocol's pack (hostsNone: SMTP, whose relay is whatever host the operator names) is reached by its TCP
540
+ // connection alone; its client names a stand-in host the connection never resolves
541
+ const host = descriptor?.hosts?.find((h) => h.host)?.host ?? (descriptor?.hostsNone ? 'relay.world.test' : undefined);
381
542
  if (!host) return { failures: ['the descriptor names no host for its client to address'] };
382
543
  const scenarioPath = join(scratch, 'client-scenario.json');
383
544
  writeFileSync(scenarioPath, '{"handlers":[]}');
@@ -391,11 +552,27 @@ async function clientCases(pack: string, scratch: string): Promise<{ failures: s
391
552
  const issued: Record<string, string> = {};
392
553
  const door = descriptor?.credentialDoor;
393
554
  if (door) {
394
- 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(() => ({}))) as Record<string, unknown>;
555
+ // a door that answers nothing is the pack's failure, never conform's hang (a pack served with a scenario that answered
556
+ // nothing once held this request open for good)
557
+ const abort = new AbortController();
558
+ 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(() => ({}))));
559
+ abort.abort();
560
+ if (asked === 'timeout') return { failures: [`the credential door (POST ${door.path}) answered nothing in 10 s, served with a scenario loaded`] };
561
+ const answer = asked as Record<string, unknown>;
395
562
  for (const [name, field] of Object.entries(door.fill)) if (answer[field] !== undefined) issued[name] = String(answer[field]);
396
563
  }
397
564
  const job = join(scratch, 'client-job.json');
398
- writeFileSync(job, JSON.stringify({ vendor, port: server.port, host, env: issued, scenarioPath, packFacts: join(pack, 'generated', 'pack-facts.json') }));
565
+ // a release carries its facts (generated/pack-facts.json, `twin-standard facts`); a source checkout does not, so its
566
+ // facts are compiled here, as that command compiles them, for the client the case runs
567
+ let packFacts = join(pack, 'generated', 'pack-facts.json');
568
+ if (!existsSync(packFacts)) {
569
+ const { compilePackFacts, renderPackFacts } = await import('./pack-facts.ts');
570
+ const compiled = await compilePackFacts([{ vendor, dir: pack }]);
571
+ if (compiled.undeclared.length) throw new Error('Pack descriptor is missing');
572
+ packFacts = join(scratch, 'pack-facts.json');
573
+ writeFileSync(packFacts, renderPackFacts(compiled));
574
+ }
575
+ writeFileSync(job, JSON.stringify({ vendor, port: server.port, host, env: issued, scenarioPath, packFacts, ...(descriptor?.hostsNone ? { raw: true } : {}) }));
399
576
  // Node, as an application runs its SDK (asynchronous: this process serves the pack meanwhile)
400
577
  const out = await new Promise<string>((resolve) => {
401
578
  const child = spawn('node', ['--experimental-strip-types', '--no-warnings', join(clients, `run${extension}`), job], { stdio: ['ignore', 'pipe', 'pipe'] });
@@ -412,8 +589,42 @@ async function clientCases(pack: string, scratch: string): Promise<{ failures: s
412
589
  } finally { await server.stop(); }
413
590
  }
414
591
 
592
+ /** The gate over every unit of the pack: the form section of the grade, which holds the rules an author's code keeps
593
+ * (p3-rules.ts: ids from ctx.mint, the twin's URL from ctx.publicBase, the kernel's crypto, bytes in ctx.blobs, secrets
594
+ * drawn from ctx.secret) and the pack layout, handler, kernel and manifest contracts. A contributor's conform is the
595
+ * whole of what a machine decides of the pack, as the catalog's admission reads it. */
596
+ async function formOf(pack: string): Promise<string[]> {
597
+ const { p3Gate } = await import('./gate.ts');
598
+ return (await p3Gate(unitsIn(pack))).flatMap((r) => r.failures.map((f) => `${nameOf(pack, r.unit)}: ${f}`));
599
+ }
600
+
601
+ /** The pack's records hold what a reviewer reads them for: each demand row cites the application's line it was read at
602
+ * (`<path>:<line>`, or `<package>@<version>:<path>:<line>` for a call a dependency makes), the spec's record names a
603
+ * vendored file that is there, the life names the session that wrote it (what makes a judge's verdict independent),
604
+ * and the package says what it serves in a README. Rules every builder brief carried by hand, decided here. */
605
+ function records(pack: string): string[] {
606
+ const out: string[] = [];
607
+ if (!existsSync(join(pack, 'README.md'))) out.push('README.md is missing: the package says what it serves, for whom, its doors and what it leaves out');
608
+ const life = readJson(join(pack, 'journeys', 'customer-life.json')) as { author?: unknown } | undefined;
609
+ if (life && (typeof life.author !== 'string' || !life.author.trim())) out.push('journeys/customer-life.json names no author (the session that wrote it), so no verdict on it can be shown independent');
610
+ for (const unit of unitsIn(pack)) {
611
+ const name = nameOf(pack, unit);
612
+ const journeys = hasJourneySource(unit) ? join(unit, 'journeys') : join(unit, '..', 'journeys');
613
+ const demand = unit === pack || hasJourneySource(unit) ? readJson(join(journeys, 'demand.json')) as { applications?: Array<{ name?: string; backend?: Array<{ operation?: string; evidence?: unknown }> }> } | undefined : undefined;
614
+ for (const app of demand?.applications ?? []) for (const row of app.backend ?? []) {
615
+ if (typeof row.evidence !== 'string' || !/:\d+/.test(row.evidence)) out.push(`${name}: ${app.name ?? 'an application'}'s call of ${row.operation ?? '(no operation)'} cites no line it was read at (evidence "<path>:<line>")`);
616
+ }
617
+ if (!hasOwnSurface(unit)) continue;
618
+ const source = existsSync(join(unit, 'spec', 'SOURCE.md')) ? readFileSync(join(unit, 'spec', 'SOURCE.md'), 'utf8') : '';
619
+ const file = /\*\*File:\*\*[^`\n]*`([^`]+)`/.exec(source)?.[1];
620
+ if (file && !existsSync(join(unit, 'spec', file))) out.push(`${name}: spec/SOURCE.md names ${file} as the vendored spec, and spec/ holds no such file`);
621
+ }
622
+ return out;
623
+ }
624
+
415
625
  /** Every check over the pack at `dir`, in order. */
416
- export async function conformance(dir: string): Promise<ConformanceResult[]> {
626
+ /** `onCheck` hears each check's id as it starts, so a caller can show where a long run stands. */
627
+ export async function conformance(dir: string, options: { onCheck?: (id: string) => void } = {}): Promise<ConformanceResult[]> {
417
628
  const pack = resolve(dir);
418
629
  if (!existsSync(join(pack, 'package.json'))) throw new Error(`${dir} is no pack (no package.json): conformance takes a pack's directory`);
419
630
  // the kernel the pack itself resolves: one registry, one serve seam, the pack's own
@@ -421,8 +632,9 @@ export async function conformance(dir: string): Promise<ConformanceResult[]> {
421
632
  const scratch = mkdtempSync(join(tmpdir(), 'conformance-'));
422
633
  const here = process.cwd();
423
634
  process.chdir(scratch);
424
- const run = async (id: string, asks: string, check: () => Promise<string[]> | string[]): Promise<ConformanceResult> => {
425
- try { return { id, asks, failures: await check() }; } catch (error) { return { id, asks, failures: [`the check could not run: ${(error as Error).message.split('\n')[0]}`] }; }
635
+ const run = async (id: string, asks: string, check: () => Promise<string[] | { failures: string[]; note: string }> | string[]): Promise<ConformanceResult> => {
636
+ options.onCheck?.(id);
637
+ try { const r = await check(); return Array.isArray(r) ? { id, asks, failures: r } : { id, asks, failures: r.failures, note: r.note }; } catch (error) { return { id, asks, failures: [`the check could not run: ${(error as Error).message.split('\n')[0]}`] }; }
426
638
  };
427
639
  try {
428
640
  return [
@@ -433,11 +645,15 @@ export async function conformance(dir: string): Promise<ConformanceResult[]> {
433
645
  await run('registered', 'a vendor-backed unit, lane or not, reaches the registered pack\'s state system', () => registered(pack, runtime)),
434
646
  await run('allowance', 'a rate budget above the fallback rests on the vendor\'s documented allowance, cited in its manifest', () => allowance(pack, runtime)),
435
647
  await (async (): Promise<ConformanceResult> => {
648
+ options.onCheck?.('client');
436
649
  const asks = "driven by the vendor's own client (its official SDK), served as a World runs it, the vendor's documented behaviour holds";
437
650
  try { const r = await clientCases(pack, scratch); return { id: 'client', asks, failures: r.failures, ...(r.note ? { note: r.note } : {}) }; } catch (error) { return { id: 'client', asks, failures: [`the check could not run: ${(error as Error).message.split('\n')[0]}`] }; }
438
651
  })(),
439
652
  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)),
440
653
  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)),
654
+ 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)),
655
+ 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)),
656
+ 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)),
441
657
  ];
442
658
  } finally {
443
659
  process.chdir(here);
package/src/consumer.ts CHANGED
@@ -181,7 +181,11 @@ export async function runConsumerWorkflow(packDir: string, options: ConsumerOpti
181
181
  if (JSON.stringify(before) !== JSON.stringify(resumed)) throw new Error('Resume changed recorded application state before readback');
182
182
  await run('retained-readback', ['run', '--', ...recipe.retention.readback]);
183
183
  const after = JSON.parse(await run('log-after-readback', ['log', '--json']));
184
- if (JSON.stringify(before) !== JSON.stringify(after)) throw new Error('Retained readback added mutations; it must only inspect existing state');
184
+ // the readback adds no write of the application's: what it adds may only be the vendor's own moves (a key's
185
+ // last-used time, recorded by the vendor as a request uses it: actor `system`, ctx.asVendor), never an API call's
186
+ const added = (after as Array<{ id?: string; actor?: { kind?: string } }>).filter((entry) => !(before as Array<{ id?: string }>).some((prior) => prior.id === entry.id));
187
+ const kept = (before as Array<{ id?: string }>).every((prior) => (after as Array<{ id?: string }>).some((entry) => JSON.stringify(entry) === JSON.stringify(prior)));
188
+ if (!kept || added.some((entry) => entry.actor?.kind !== 'system')) throw new Error('Retained readback added mutations; it must only inspect existing state');
185
189
  retainedReadback = true;
186
190
  }
187
191
  } catch (error) { failures.push((error as Error).message); }