@volter/twin-standard 1.0.30 → 1.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/dist/src/archive-source.d.ts +13 -0
  2. package/dist/src/archive-source.js +176 -0
  3. package/dist/src/check-sources.d.ts +19 -2
  4. package/dist/src/check-sources.js +95 -37
  5. package/dist/src/cli.js +52 -9
  6. package/dist/src/clients/ai-gateway.d.ts +1 -0
  7. package/dist/src/clients/ai-gateway.js +3 -0
  8. package/dist/src/clients/anthropic-messages.d.ts +10 -0
  9. package/dist/src/clients/anthropic-messages.js +52 -0
  10. package/dist/src/clients/anthropic.d.ts +1 -0
  11. package/dist/src/clients/anthropic.js +3 -0
  12. package/dist/src/clients/aws.d.ts +2 -0
  13. package/dist/src/clients/aws.js +137 -0
  14. package/dist/src/clients/cerebras.d.ts +1 -0
  15. package/dist/src/clients/cerebras.js +3 -0
  16. package/dist/src/clients/clerk.d.ts +2 -0
  17. package/dist/src/clients/clerk.js +99 -0
  18. package/dist/src/clients/cloudflare.d.ts +2 -0
  19. package/dist/src/clients/cloudflare.js +86 -0
  20. package/dist/src/clients/elevenlabs.d.ts +2 -0
  21. package/dist/src/clients/elevenlabs.js +39 -0
  22. package/dist/src/clients/gemini.d.ts +2 -0
  23. package/dist/src/clients/gemini.js +92 -0
  24. package/dist/src/clients/github.d.ts +2 -0
  25. package/dist/src/clients/github.js +71 -0
  26. package/dist/src/clients/groq.d.ts +1 -0
  27. package/dist/src/clients/groq.js +3 -0
  28. package/dist/src/clients/harness.d.ts +36 -0
  29. package/dist/src/clients/harness.js +74 -0
  30. package/dist/src/clients/merge.d.ts +1 -0
  31. package/dist/src/clients/merge.js +8 -0
  32. package/dist/src/clients/openai-compatible.d.ts +9 -0
  33. package/dist/src/clients/openai-compatible.js +51 -0
  34. package/dist/src/clients/openai.d.ts +2 -0
  35. package/dist/src/clients/openai.js +65 -0
  36. package/dist/src/clients/openrouter.d.ts +1 -0
  37. package/dist/src/clients/openrouter.js +3 -0
  38. package/dist/src/clients/perplexity.d.ts +1 -0
  39. package/dist/src/clients/perplexity.js +3 -0
  40. package/dist/src/clients/planetscale.d.ts +2 -0
  41. package/dist/src/clients/planetscale.js +109 -0
  42. package/dist/src/clients/posthog.d.ts +2 -0
  43. package/dist/src/clients/posthog.js +145 -0
  44. package/dist/src/clients/run.d.ts +1 -0
  45. package/dist/src/clients/run.js +25 -0
  46. package/dist/src/clients/sentry.d.ts +2 -0
  47. package/dist/src/clients/sentry.js +167 -0
  48. package/dist/src/clients/slack.d.ts +2 -0
  49. package/dist/src/clients/slack.js +107 -0
  50. package/dist/src/clients/stripe.d.ts +2 -0
  51. package/dist/src/clients/stripe.js +161 -0
  52. package/dist/src/clients/tinybird.d.ts +2 -0
  53. package/dist/src/clients/tinybird.js +71 -0
  54. package/dist/src/clients/tunnel.d.ts +2 -0
  55. package/dist/src/clients/tunnel.js +99 -0
  56. package/dist/src/clients/upstash.d.ts +2 -0
  57. package/dist/src/clients/upstash.js +143 -0
  58. package/dist/src/clients/volteridentity.d.ts +2 -0
  59. package/dist/src/clients/volteridentity.js +128 -0
  60. package/dist/src/clients/xai.d.ts +2 -0
  61. package/dist/src/clients/xai.js +23 -0
  62. package/dist/src/conformance.d.ts +33 -0
  63. package/dist/src/conformance.js +520 -0
  64. package/dist/src/derive.js +113 -22
  65. package/dist/src/gate.js +22 -16
  66. package/dist/src/index.d.ts +5 -3
  67. package/dist/src/index.js +5 -3
  68. package/dist/src/lanes.d.ts +2 -0
  69. package/dist/src/lanes.js +7 -0
  70. package/dist/src/p3-rules.js +0 -2
  71. package/dist/src/protocol-3.d.ts +1 -0
  72. package/dist/src/protocol-3.js +76 -30
  73. package/dist/src/published.d.ts +6 -0
  74. package/dist/src/published.js +65 -11
  75. package/dist/src/spec-documents.js +2 -1
  76. package/dist/src/spec-ir-client.d.ts +5 -1
  77. package/dist/src/spec-ir-client.js +12 -2
  78. package/dist/src/spec-ir.d.ts +7 -2
  79. package/dist/src/spec-ir.js +42 -10
  80. package/dist/src/verdict.d.ts +14 -0
  81. package/dist/src/verdict.js +30 -0
  82. package/dist/src/walk/behavior-journey.d.ts +47 -0
  83. package/dist/src/walk/behavior-journey.js +1138 -0
  84. package/dist/src/walk/git-client.d.ts +26 -0
  85. package/dist/src/walk/git-client.js +139 -0
  86. package/dist/src/walk/journey-kit.d.ts +445 -0
  87. package/dist/src/walk/journey-kit.js +743 -0
  88. package/dist/src/walk/shape.d.ts +54 -0
  89. package/dist/src/walk/shape.js +210 -0
  90. package/dist/src/walk/sse-example.d.ts +2 -0
  91. package/dist/src/walk/sse-example.js +32 -0
  92. package/dist/src/walk/vendor-fetch.d.ts +20 -0
  93. package/dist/src/walk/vendor-fetch.js +92 -0
  94. package/dist/src/walk/walk-database.d.ts +12 -0
  95. package/dist/src/walk/walk-database.js +96 -0
  96. package/dist/src/walk/xml-view.d.ts +2 -0
  97. package/dist/src/walk/xml-view.js +55 -0
  98. package/package.json +39 -3
  99. package/src/archive-source.ts +141 -0
  100. package/src/check-sources.ts +83 -33
  101. package/src/cli.ts +40 -8
  102. package/src/clients/ai-gateway.ts +4 -0
  103. package/src/clients/anthropic-messages.ts +60 -0
  104. package/src/clients/anthropic.ts +4 -0
  105. package/src/clients/aws.ts +119 -0
  106. package/src/clients/cerebras.ts +4 -0
  107. package/src/clients/clerk.ts +97 -0
  108. package/src/clients/cloudflare.ts +80 -0
  109. package/src/clients/elevenlabs.ts +38 -0
  110. package/src/clients/gemini.ts +89 -0
  111. package/src/clients/github.ts +67 -0
  112. package/src/clients/groq.ts +4 -0
  113. package/src/clients/harness.ts +101 -0
  114. package/src/clients/merge.ts +9 -0
  115. package/src/clients/openai-compatible.ts +59 -0
  116. package/src/clients/openai.ts +66 -0
  117. package/src/clients/openrouter.ts +4 -0
  118. package/src/clients/perplexity.ts +4 -0
  119. package/src/clients/planetscale.ts +98 -0
  120. package/src/clients/posthog.ts +132 -0
  121. package/src/clients/run.ts +18 -0
  122. package/src/clients/sentry.ts +157 -0
  123. package/src/clients/slack.ts +98 -0
  124. package/src/clients/stripe.ts +129 -0
  125. package/src/clients/tinybird.ts +69 -0
  126. package/src/clients/tunnel.ts +79 -0
  127. package/src/clients/upstash.ts +137 -0
  128. package/src/clients/volteridentity.ts +108 -0
  129. package/src/clients/xai.ts +25 -0
  130. package/src/conformance.ts +446 -0
  131. package/src/derive.ts +87 -16
  132. package/src/gate.ts +20 -14
  133. package/src/index.ts +5 -3
  134. package/src/lanes.ts +7 -0
  135. package/src/p3-rules.ts +0 -2
  136. package/src/protocol-3.ts +71 -27
  137. package/src/published.ts +59 -11
  138. package/src/spec-documents.ts +2 -1
  139. package/src/spec-ir-client.ts +15 -4
  140. package/src/spec-ir.ts +43 -5
  141. package/src/verdict.ts +28 -0
  142. package/src/walk/behavior-journey.ts +806 -0
  143. package/src/walk/git-client.ts +138 -0
  144. package/src/walk/journey-kit.ts +858 -0
  145. package/src/walk/shape.ts +208 -0
  146. package/src/walk/sse-example.ts +25 -0
  147. package/src/walk/vendor-fetch.ts +80 -0
  148. package/src/walk/walk-database.ts +97 -0
  149. package/src/walk/xml-view.ts +44 -0
package/src/derive.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bun
2
+ import { parseExactJson, exactInteger } from '@volter/world-core/exact-json';
2
3
  // derive — the derived-pack generator's build step (docs/contributing/architecture.md, "The derived pack"), the
3
4
  // standard's own: the grade's `derived-reproducible` runs it, so every grader derives the same way. Reads a pack's
4
5
  // vendored spec (`<pack>/spec/openapi.json[.gz]`), builds the spec IR (./spec-ir.ts) and writes the pack's
@@ -16,7 +17,7 @@ import { fromProto, protoSchema, readProtoFiles } from './spec-ir-proto.ts';
16
17
  import { fromRedisCommands, type CommandTableSpec } from './spec-ir-commands.ts';
17
18
  import { fromClientCalls, type ClientOpsSpec } from './spec-ir-client.ts';
18
19
  import { discoveryMethods, fromDiscovery, isDiscovery } from './spec-ir-discovery.ts';
19
- import { handlerCitations, quotedPage, type Patch, type SourceRecord, urlsIn } from './check-sources.ts';
20
+ import { handlerCitations, quotedPage, sourceHeld, type Patch, type SourceRecord, urlsIn } from './check-sources.ts';
20
21
  import { applySpecPatch } from './spec-patches.ts';
21
22
  import { unitAt } from './unit.ts';
22
23
  import { mergeDocuments, readSpecDocuments } from './spec-documents.ts';
@@ -50,13 +51,14 @@ const text = (specPath.endsWith('.gz') ? gunzipSync(bytes) : bytes).toString('ut
50
51
  // corrects a JSON spec
51
52
  const lineProtocol = specPath.endsWith('.txt');
52
53
  const commandVersion = commandTable ? (/\*\*Version:\*\* `([^`]+)`/.exec(existsSync(join(PKG, 'spec', 'SOURCE.md')) ? readFileSync(join(PKG, 'spec', 'SOURCE.md'), 'utf8') : '')?.[1] ?? '') : '';
53
- let doc: any = severalDocuments ? readSpecDocuments(join(PKG, 'spec')) : commandTable ? fromRedisCommands(Object.fromEntries(readdirSync(specPath).filter((f) => f.endsWith('.json')).sort().map((f) => [f, JSON.parse(readFileSync(join(specPath, f), 'utf8'))])), commandVersion) : rpcService ? readProtoFiles(protoTree(specPath)) : lineProtocol ? fromCommandReplies(text) : /\.ya?ml(\.gz)?$/.test(specPath) ? parseYaml(text) : JSON.parse(text);
54
+ let doc: any = severalDocuments ? readSpecDocuments(join(PKG, 'spec')) : commandTable ? fromRedisCommands(Object.fromEntries(readdirSync(specPath).filter((f) => f.endsWith('.json')).sort().map((f) => [f, parseExactJson(readFileSync(join(specPath, f), 'utf8'))])), commandVersion) : rpcService ? readProtoFiles(protoTree(specPath)) : lineProtocol ? fromCommandReplies(text) : /\.ya?ml(\.gz)?$/.test(specPath) ? parseExactJson(JSON.stringify(parseYaml(text, { intAsBigInt: true }), (_, v) => typeof v === 'bigint' ? exactInteger(String(v)) : v)) : parseExactJson(text);
54
55
 
55
56
  // ── the evidence the pack cites ──────────────────────────────────────────────────────────────
56
57
  // A source is a page check-sources (./check-sources.ts) fetched and recorded found (spec/sources.json), a quote
57
- // that page holds, or `spec:<operationId or /json/pointer> "quote"`: words of the vendored spec itself
58
- // (read before any patch, so a patch never vouches for itself).
59
- const vendored = lineProtocol || commandTable ? structuredClone(doc) : JSON.parse(text.startsWith('{') ? text : JSON.stringify(doc));
58
+ // that page holds, `spec:<operationId or /json/pointer> "quote"`: words of the vendored spec itself (read before any
59
+ // patch, so a patch never vouches for itself), or `recording:<file> "quote"`: words of an answer the vendor gave,
60
+ // recorded in spec/recordings/.
61
+ const vendored = lineProtocol || commandTable ? structuredClone(doc) : parseExactJson(text.startsWith('{') ? text : JSON.stringify(doc));
60
62
  /** An RFC's section by its number (`4.1.4`): its heading's line to the next heading's. */
61
63
  const sectionOf = (n: string): string | undefined => {
62
64
  const at = text.search(new RegExp(`^${n.replace(/\./g, '\\.')}\\. `, 'm'));
@@ -65,7 +67,7 @@ const sectionOf = (n: string): string | undefined => {
65
67
  return next < 0 ? text.slice(at) : text.slice(at, at + 1 + next);
66
68
  };
67
69
  const sourcesPath = join(PKG, 'spec', 'sources.json');
68
- const recorded: Record<string, SourceRecord> = existsSync(sourcesPath) ? JSON.parse(readFileSync(sourcesPath, 'utf8')) : {};
70
+ const recorded: Record<string, SourceRecord> = existsSync(sourcesPath) ? parseExactJson(readFileSync(sourcesPath, 'utf8')) : {};
69
71
  const HTTP_METHODS = new Set(['get', 'put', 'post', 'delete', 'patch', 'head', 'options', 'trace']);
70
72
  const specOps = new Map<string, unknown>();
71
73
  // an operation by its id: the spec's operationId, or the `method_path` slug the IR mints where the spec names none (QStash)
@@ -91,8 +93,21 @@ const wordsIn = (node: unknown): string => (typeof node === 'string' ? node : no
91
93
  const bare = (s: string): string => s.replace(/\s+/g, '');
92
94
  // a JSON pointer into the spec, an operation by id, or (an RFC's) a section by number
93
95
  const specNode = (ref: string): unknown => (ref.startsWith('/') ? ref.split('/').slice(1).map((k) => k.replace(/~1/g, '/').replace(/~0/g, '~')).reduce((n: any, k) => n?.[k], severalDocuments && !ref.startsWith('/documents/') ? pointed : vendored) : lineProtocol && /^\d+(\.\d+)*$/.test(ref) ? sectionOf(ref) : specOps.get(ref));
96
+ /** Validate structured recording evidence before any string interpolation or page fallback. */
97
+ function unheldRecording(name: unknown, quote: unknown): string | undefined {
98
+ if (typeof name !== 'string' || !/^[\w.-]+$/.test(name)) return 'recording filename is no recording (use a safe basename)';
99
+ if (typeof quote !== 'string' || !quote.trim()) return 'recording evidence quotes nothing (use a nonempty string)';
100
+ const file = join(PKG, 'spec', 'recordings', name);
101
+ if (!existsSync(file)) return `spec/recordings/${name} is no recording`;
102
+ return bare(readFileSync(file, 'utf8')).includes(bare(quote.replace(/\\"/g, '"'))) ? undefined : `spec/recordings/${name} does not hold "${quote}"`;
103
+ }
104
+
94
105
  /** Why a citation does not hold, or undefined when it does. */
95
106
  function unheld(source: string): string | undefined {
107
+ // Malformed recording citations never fall back to a checked page.
108
+ const recording = /^recording:(\S+)\s+"([\s\S]*)"$/.exec(source.trim());
109
+ if (recording) return unheldRecording(recording[1], recording[2]);
110
+ if (source.trim().startsWith('recording:')) return 'malformed recording citation';
96
111
  const spec = /^spec:(\S+)\s+"([\s\S]*)"$/.exec(source.trim());
97
112
  if (spec && !spec[2]!.trim()) return `spec:${spec[1]} quotes nothing`;
98
113
  if (spec) {
@@ -100,9 +115,12 @@ function unheld(source: string): string | undefined {
100
115
  if (node === undefined) return `the spec has nothing at ${spec[1]}`;
101
116
  return bare(wordsIn(node)).includes(bare(spec[2]!.replace(/\\"/g, '"'))) ? undefined : `the spec at ${spec[1]} does not say "${spec[2]}"`;
102
117
  }
118
+ if (source.trim().startsWith('archive:') && !quotedPage(source)) return 'archive evidence needs a valid pinned member and quote';
119
+ const archiveQuote = quotedPage(source);
120
+ if (archiveQuote?.url.startsWith('archive:') && recorded[archiveQuote.url]?.quotes?.[archiveQuote.quote] !== true) return 'archive member does not hold its quote as recorded';
103
121
  const urls = urlsIn(source);
104
122
  if (!urls.length) return `"${source.slice(0, 60)}" names no page and no spec words`;
105
- const dead = urls.filter((u) => recorded[u]?.status !== 200);
123
+ const dead = urls.filter((u) => !sourceHeld(u, recorded[u]));
106
124
  return dead.length ? dead.map((u) => `${u} is ${recorded[u] ? `answering ${recorded[u]!.status || 'nothing'}` : 'not checked'} (twin-standard check-sources ${vendor})`).join('; ') : undefined;
107
125
  }
108
126
  const evidence: string[] = [];
@@ -111,21 +129,22 @@ for (const c of handlerCitations(PKG)) {
111
129
  const q = quotedPage(c.source);
112
130
  // a quote is checked only in the two forms that say where it is; any other quote is refused, never passed unread
113
131
  const trimmed = c.source.trim();
114
- const loose = !q && (/^spec:/.test(trimmed) ? !/^spec:\S+\s+"[\s\S]*"$/.test(trimmed) : trimmed.includes('"'));
115
- const held = loose ? `"${c.source.slice(0, 60)}" quotes words in no form a check reads (\`<url> "<quote>"\` or \`spec:<ref> "<quote>"\`)` : q ? (recorded[q.url]?.quotes?.[q.quote] === true ? undefined : `${q.url} does not hold "${q.quote}" as recorded (twin-standard check-sources ${vendor})`) : unheld(c.source);
116
- if (held) evidence.push(`semantics/${c.where}: ${held}`);
132
+ const loose = !q && (/^(spec|recording):/.test(trimmed) ? !/^(spec|recording):\S+\s+"[\s\S]*"$/.test(trimmed) : trimmed.includes('"'));
133
+ const held = loose ? `"${c.source.slice(0, 60)}" quotes words in no form a check reads (\`<url> "<quote>"\`, \`spec:<ref> "<quote>"\` or \`recording:<file> "<quote>"\`)` : q ? ((!q.url.startsWith('archive:') || sourceHeld(q.url, recorded[q.url])) && recorded[q.url]?.quotes?.[q.quote] === true ? undefined : `${q.url} does not hold "${q.quote}" as recorded (twin-standard check-sources ${vendor})`) : unheld(c.source);
134
+ if (held) evidence.push(`${c.where}: ${held}`);
117
135
  }
118
136
  // Corrections to the vendor's spec, as data: RFC 6902 operations (add, replace, remove, move, copy)
119
137
  // in spec/patches.json, applied before the IR so the vendored file stays the vendor's own bytes. Each says
120
138
  // `why`, citing what shows the vendor differs from its spec (its docs, the spec's own example, a recording).
121
139
  const patchPath = join(PKG, 'spec', 'patches.json');
122
140
  if (existsSync(patchPath)) {
123
- for (const p of JSON.parse(readFileSync(patchPath, 'utf8')).patches as Array<{ op: string; path: string; from?: string; value?: unknown; why: string }>) {
141
+ for (const p of parseExactJson(readFileSync(patchPath, 'utf8')).patches as Array<{ op: string; path: string; from?: string; value?: unknown; why: string }>) {
124
142
  if (!p.why?.trim()) { console.error(`${vendor}: spec patch ${p.op} ${p.path} gives no why`); process.exit(1); }
125
143
  {
126
144
  const s = (p as Patch).source;
127
- const held = s?.spec ? unheld(`spec:${s.spec} "${s.quote}"`)
128
- : s?.url ? (recorded[s.url]?.quotes?.[s.quote] === true ? undefined : `${s.url} does not hold "${s.quote}" as recorded`)
145
+ const held = s?.recording !== undefined ? unheldRecording(s.recording, s.quote)
146
+ : s?.spec ? unheld(`spec:${s.spec} "${s.quote}"`)
147
+ : s?.url ? ((!s.url.startsWith('archive:') || sourceHeld(s.url, recorded[s.url])) && recorded[s.url]?.quotes?.[s.quote] === true ? undefined : `${s.url} does not hold "${s.quote}" as recorded`)
129
148
  : urlsIn(p.why).length ? unheld(p.why) : 'it cites no page and quotes nothing';
130
149
  if (held) evidence.push(`spec patch ${p.op} ${p.path}: ${held}`);
131
150
  }
@@ -174,7 +193,7 @@ const ir: SpecIR = isDiscovery(doc) ? fromDiscovery(doc) : doc?.format === 'clie
174
193
  // A spec that groups its operations by tag (Cloudflare's: nearly every path begins /accounts/{account_id}/, so the
175
194
  // path's first segment names one family for thousands of operations) says so in spec/grouping.json, `{ "by": "tag" }`:
176
195
  // each operation's family is then its first tag, as a file name (`Worker Script` is `worker-script`).
177
- const grouping = existsSync(join(PKG, 'spec', 'grouping.json')) ? (JSON.parse(readFileSync(join(PKG, 'spec', 'grouping.json'), 'utf8')) as { by?: string }) : {};
196
+ const grouping = existsSync(join(PKG, 'spec', 'grouping.json')) ? (parseExactJson(readFileSync(join(PKG, 'spec', 'grouping.json'), 'utf8')) as { by?: string }) : {};
178
197
  if (grouping.by === 'tag') {
179
198
  // an operation the spec names no operationId for is found by its method and path, in the spec as corrected (an
180
199
  // operation a patch adds carries its own tags: PostHog's person delete)
@@ -193,10 +212,14 @@ if (grouping.by === 'tag') {
193
212
  const sdlPath = ['schema.graphql.gz', 'schema.graphql'].map((f) => join(PKG, 'spec', f)).find((p) => existsSync(p));
194
213
  // the GraphQL wire's root fields (`mutation.issueArchive`), which a transition may name as a REST operation is named
195
214
  const graphqlOperations = new Set<string>();
215
+ let graphqlSchema: import('graphql').GraphQLSchema | undefined;
216
+ let graphqlLib: typeof import('graphql') | undefined;
196
217
  if (sdlPath) {
197
218
  const { fromGraphQL } = await import('./spec-ir-graphql.ts');
198
219
  const sdl = (sdlPath.endsWith('.gz') ? gunzipSync(readFileSync(sdlPath)) : readFileSync(sdlPath)).toString('utf8');
199
220
  const gql = fromGraphQL(sdl);
221
+ graphqlLib = await import('graphql');
222
+ graphqlSchema = graphqlLib.buildSchema(sdl, { assumeValidSDL: true });
200
223
  for (const o of gql.operations) graphqlOperations.add(o.id);
201
224
  mkdirSync(out, { recursive: true });
202
225
  writeFileSync(join(out, 'graphql.gen.json'), `${JSON.stringify({ generatedBy: 'scripts/derive-pack.ts', spec: sdlPath.split('/').pop(), ...gql })}\n`);
@@ -282,17 +305,65 @@ if (existsSync(manifestPath)) {
282
305
  // (a resource the spec names is read by its own list or retrieve; one the manifest names apart from the spec's, an RPC
283
306
  // vendor's, by the list or read the scope names, with `items` where the answer holds them when the spec does not say)
284
307
  const specNamed = new Set(ir.resources.map((r) => r.name));
285
- const scopes = Object.entries(manifest.resources as Record<string, { refresh?: { list?: string; get?: string; none?: string; items?: string } }>).flatMap(([name, decl]) => {
308
+ const scopes = Object.entries(manifest.resources as Record<string, { storedAs?: string; refresh?: { list?: string; get?: string; none?: string; items?: string; detail?: string } }>).flatMap(([name, decl]) => {
286
309
  const r = decl.refresh;
287
310
  if (!r) return [];
311
+ if ('graphql' in r && r.graphql) {
312
+ const g = graphqlLib;
313
+ if (!g || !graphqlSchema) return [`${name}: a GraphQL refresh needs a vendored schema`];
314
+ const refresh = r.graphql as { query: string; items: string; pageInfo?: string };
315
+ const id = r.list ?? r.get;
316
+ if (!id || !graphqlOperations.has(id) || !id.startsWith('query.')) return [`${name}: refresh names no GraphQL read operation (${id})`];
317
+ let document: import('graphql').DocumentNode;
318
+ try { document = g.parse(refresh.query); } catch (error) { return [`${name}: invalid GraphQL refresh: ${String(error)}`]; }
319
+ const invalid = g.validate(graphqlSchema, document);
320
+ if (invalid.length) return invalid.map((e) => `${name}: GraphQL refresh: ${e.message}`);
321
+ const operation = g.getOperationAST(document);
322
+ if (!operation || operation.operation !== 'query') return [`${name}: a refresh document must select one query`];
323
+ const parts = refresh.items.replace(/^\$body\.?/, '').split('.');
324
+ if (parts.shift() !== 'data') return [`${name}: GraphQL items must name a path under data`];
325
+ let selections = operation.selectionSet;
326
+ let type: import('graphql').GraphQLNamedType = graphqlSchema.getQueryType()!;
327
+ for (const part of parts) {
328
+ const field = selections.selections.find((n): n is import('graphql').FieldNode => n.kind === 'Field' && (n.alias?.value ?? n.name.value) === part);
329
+ if (!field || !g.isObjectType(type)) return [`${name}: GraphQL items path ${refresh.items} is not selected by the document`];
330
+ if (type === graphqlSchema.getQueryType() && `query.${field.name.value}` !== id) return [`${name}: GraphQL refresh declares ${id} but selects query.${field.name.value}`];
331
+ const definition: import('graphql').GraphQLField<unknown, unknown> = type.getFields()[field.name.value]!;
332
+ type = g.getNamedType(definition.type);
333
+ selections = field.selectionSet!;
334
+ }
335
+ const normalize = (value: string) => value.replace(/_/g, '').toLowerCase();
336
+ if (normalize(type.name) !== normalize(name) && normalize(type.name) !== normalize(decl.storedAs ?? name)) return [`${name}: GraphQL items return ${type.name}, not ${name}`];
337
+ return [];
338
+ }
288
339
  const op = (id: string | undefined) => ir.operations.find((o) => o.id === id);
289
340
  const ours = (o: { resource?: string }): boolean => !specNamed.has(name) || o.resource === name;
290
341
  if (r.list !== undefined) {
291
342
  const o = op(r.list);
343
+ // a list the spec's reading does not class as one (a nullable array, a union of list envelopes, a list nested in its
344
+ // envelope: Discord's, Polar's, Cloudflare's) is read where the scope says its items are: any read the spec names
345
+ // (still this resource's read: one the spec's reading gives another resource is refused)
346
+ if (o && r.items) {
347
+ // a read asked by POST (an RPC list: AWS JSON's ListSecrets) is a read too; one the spec classes a write is not
348
+ if (String(o.method).toUpperCase() !== 'GET' && ['create', 'update', 'delete', 'action'].includes(String(o.class))) return [`${name}: refresh.list ${r.list} is a ${o.class}, not a read`];
349
+ const detail = r.detail ? op(r.detail) : undefined;
350
+ const detailed = detail?.method.toUpperCase() === 'GET' && ours(detail);
351
+ return !o.resource || ours(o) || detailed ? [] : [`${name}: refresh.list ${r.list} reads ${o.resource}, not ${name}`];
352
+ }
292
353
  if (!(o?.class === 'list' && ours(o))) return [`${name}: refresh.list ${r.list} is ${o ? `a ${o.class} of ${o.resource ?? 'no resource'}` : 'no operation of the spec'}, not a list of ${name}`];
293
354
  return o.answers?.list || r.items ? [] : [`${name}: refresh.list ${r.list} answers no list the spec names; say where its items are (items)`];
294
355
  }
295
- if (r.get !== undefined) { const o = op(r.get); return o?.class === 'retrieve' && ours(o) ? [] : [`${name}: refresh.get ${r.get} is ${o ? `a ${o.class} of ${o.resource ?? 'no resource'}` : 'no operation of the spec'}, not the retrieve of ${name}`]; }
356
+ if (r.get !== undefined) {
357
+ const o = op(r.get);
358
+ if (o?.class === 'retrieve' && ours(o)) return [];
359
+ // a resource the spec does not name (a client-derived spec classes every request an action) is read by the GET
360
+ // the scope names, the answer itself the subject
361
+ if (o && !specNamed.has(name) && !o.resource && String(o.method).toUpperCase() === 'GET') return [];
362
+ // ...or by the POST the scope names that the manifest's `reads` rules a read (an RPC method: Telegram's getChat),
363
+ // whatever schema it answers (getChat's ChatFullInfo, a chat's fuller form, under `result`)
364
+ if (o && !specNamed.has(name) && ((manifest as { reads?: string[] }).reads ?? []).includes(o.id)) return [];
365
+ return [`${name}: refresh.get ${r.get} is ${o ? `a ${o.class} of ${o.resource ?? 'no resource'}` : 'no operation of the spec'}, not the retrieve of ${name}`];
366
+ }
296
367
  return typeof r.none === 'string' && r.none.trim() ? [] : [`${name}: refresh names no list, no get and no reason`];
297
368
  });
298
369
  if (scopes.length) {
package/src/gate.ts CHANGED
@@ -1,8 +1,9 @@
1
+ import { unpublishedUnitFiles } from './conformance.ts';
2
+ import { hasOwnSurface } from './lanes.ts';
1
3
  // The gate — what may be in a Protocol 3 pack repository or catalog: every pack and lane passes the form
2
4
  // section of the grade (docs/contributing/architecture.md, "The grade"), so nothing that is not Protocol 3 enters it:
3
5
  // nothing outside the pack layout, no handler past its contract.
4
- // The repository was made so that this holds (the owner, 2026-09-29: "this was WHY we created p3 so this can't
5
- // happen"); this is what makes it hold. It runs before a commit (the pack repository's hook, on the units it touches),
6
+ // Protocol 3's repository was made so that this holds; this is what makes it hold. It runs before a commit (the pack repository's hook, on the units it touches),
6
7
  // before a release (pack-release, on each pack it releases), and on every version submitted to the catalog
7
8
  // (twin-catalog's check, on the published tarball) — each at the version of this package it pins, so all of them judge by
8
9
  // one standard.
@@ -16,6 +17,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
16
17
  import { join } from 'node:path';
17
18
  import ts from 'typescript';
18
19
  import { grade } from './grade.ts';
20
+ import { withCheckoutFiles } from './protocol-3.ts';
19
21
  import { isPackArtifact } from './pack-files.ts';
20
22
 
21
23
  /** Every unit of a pack repository: each pack (a directory with a package.json) and each lane in it (a directory with
@@ -38,7 +40,7 @@ export function unitsOf(repo: string): string[] {
38
40
  * generated facts, its lanes, and in src/ only its manifest (its lanes' routes, discovery and descriptor, as data), the
39
41
  * fetch the kernel serves them by (createVendorFetch, generated) and the fixed serve factory, index and bin. Nothing
40
42
  * routes by hand: only the fetch reaches a lane. A directory with no manifest and no lane is no Protocol 3 pack. */
41
- const ROOT_ENTRIES = new Set(['README.md', 'package.json', 'LICENSE', 'CHANGELOG.md', 'tsconfig.json', 'tsconfig.build.json', 'gate.ts', 'census.json', 'vendor', 'journeys', 'generated', 'src', 'node_modules']);
43
+ const ROOT_ENTRIES = new Set(['README.md', 'package.json', 'LICENSE', 'CHANGELOG.md', 'tsconfig.json', 'tsconfig.build.json', 'gate.ts', 'census.json', 'vendor', 'journeys', 'spec', 'generated', 'src', 'node_modules']);
42
44
  const ROOT_SRC = new Set(['manifest.ts', 'fetch.ts', 'index.ts', 'cli.ts', 'server.ts']);
43
45
  /** What the repository ignores (a build's dist/, a World's state) is no file of the pack, as protocol-3's layout reads it. */
44
46
  function ignored(dir: string, names: string[]): Set<string> {
@@ -53,6 +55,7 @@ export function laneRootFailures(dir: string): string[] {
53
55
  const skip = ignored(dir, stray);
54
56
  const outside = [
55
57
  ...stray.filter((n) => !skip.has(n)),
58
+ ...(existsSync(join(dir, 'spec')) ? readdirSync(join(dir, 'spec')).filter((n) => !['sources.json', 'SOURCE.md', 'README.md'].includes(n)).map((n) => `spec/${n}`) : []),
56
59
  ...(existsSync(join(dir, 'src')) ? readdirSync(join(dir, 'src')).filter((n) => !ROOT_SRC.has(n) && !n.endsWith('.d.ts') && n !== 'semantics').map((n) => `src/${n}`) : []),
57
60
  // what the lanes share, and nothing else (architecture, "Pack layout": a vendor whose every API is a lane)
58
61
  ...(existsSync(join(dir, 'src', 'semantics')) ? readdirSync(join(dir, 'src', 'semantics')).filter((n) => n !== 'shared.ts').map((n) => `src/semantics/${n}`) : []),
@@ -60,6 +63,7 @@ export function laneRootFailures(dir: string): string[] {
60
63
  const failures = outside.length ? [`layout: ${outside.length} outside the vendor's root (its lanes hold the rest): ${outside.slice(0, 8).join(', ')}${outside.length > 8 ? ` and ${outside.length - 8} more` : ''}`] : [];
61
64
  // the vendor's front is declared and the kernel serves it: a manifest, and a fetch that is createVendorFetch over it
62
65
  const src = (n: string): string => (existsSync(join(dir, 'src', n)) ? readFileSync(join(dir, 'src', n), 'utf8') : '');
66
+ failures.push(...unpublishedUnitFiles(dir).map((f) => `layout: ${f}`));
63
67
  if (!src('manifest.ts')) failures.push("layout: no src/manifest.ts: a vendor of lanes declares its lanes' routes, discovery and descriptor");
64
68
  if (!/\bcreateVendorFetch\(/.test(src('fetch.ts'))) failures.push('layout: src/fetch.ts builds no createVendorFetch: the kernel routes a vendor\'s lanes from its manifest, nothing by hand');
65
69
  // the root's own files hold to the pack contract as a unit's do (the grade's kernel-contract)
@@ -82,15 +86,17 @@ export function laneRootFailures(dir: string): string[] {
82
86
 
83
87
  /** Each unit's blocking form failures (empty when it may be in the repository). */
84
88
  export async function p3Gate(units: string[]): Promise<Array<{ unit: string; failures: string[] }>> {
85
- const out: Array<{ unit: string; failures: string[] }> = [];
86
- for (const unit of units) {
87
- // a vendor whose every API is a lane has no spec of its own: its lanes are graded, and its root is only the vendor's
88
- // front (architecture, "Pack layout", "Other wires: lanes")
89
- const lanesOnly = !existsSync(join(unit, 'spec')) && readdirSync(unit).some((n) => existsSync(join(unit, n, 'src', 'manifest.ts')));
90
- if (lanesOnly || !existsSync(join(unit, 'src', 'manifest.ts'))) { out.push({ unit, failures: laneRootFailures(unit) }); continue; }
91
- const grades = await grade(unit, { static: true, sections: ['form'] });
92
- const form = Object.values(grades).flatMap((g) => g.criteria).filter((c) => !c.pass);
93
- out.push({ unit, failures: form.map((c) => `${c.id}: ${c.reason}`) });
94
- }
95
- return out;
89
+ return withCheckoutFiles(async () => {
90
+ const out: Array<{ unit: string; failures: string[] }> = [];
91
+ for (const unit of units) {
92
+ // a vendor whose every API is a lane has no spec of its own: its lanes are graded, and its root is only the vendor's
93
+ // front (architecture, "Pack layout", "Other wires: lanes")
94
+ const lanesOnly = !hasOwnSurface(unit) && readdirSync(unit).some((n) => existsSync(join(unit, n, 'src', 'manifest.ts')));
95
+ if (lanesOnly || !existsSync(join(unit, 'src', 'manifest.ts'))) { out.push({ unit, failures: laneRootFailures(unit) }); continue; }
96
+ const grades = await grade(unit, { static: true, sections: ['form'] });
97
+ const form = Object.values(grades).flatMap((g) => g.criteria).filter((c) => !c.pass);
98
+ out.push({ unit, failures: form.map((c) => `${c.id}: ${c.reason}`) });
99
+ }
100
+ return out;
101
+ });
96
102
  }
package/src/index.ts CHANGED
@@ -4,9 +4,11 @@
4
4
  // of a submission — judges by the same standard at the version it pins. Changing a criterion is a release of this
5
5
  // package: a criterion's id never changes meaning, a changed criterion takes a new id and a new standard version.
6
6
  export { grade, STANDARDS, type CriterionGrade, type GradeOptions, type StandardGrade } from './grade.ts';
7
+ export { conformance, decisionTableProblems, type ConformanceResult, UNIT_FILES, unitFilesMissing } from './conformance.ts';
7
8
  export { laneRootFailures, p3Gate, unitsOf } from './gate.ts';
8
- export { gatePublished, vendorOfPackage, type PublishedGate } from './published.ts';
9
- export { handlerName, protocol3, PLANNED } from './protocol-3.ts';
10
- export { sharingLanesIn } from './lanes.ts';
9
+ export { conformPublished, gatePublished, vendorOfPackage, type PublishedGate } from './published.ts';
10
+ export { handlerName, protocol3, PLANNED, withCheckoutFiles } from './protocol-3.ts';
11
+ export { notIndependent, type Verdict } from './verdict.ts';
12
+ export { sharingLanesIn, hasOwnSurface } from './lanes.ts';
11
13
  export { unitAt } from './unit.ts';
12
14
  export type { Criterion, CriterionResult, GradeUnit, ScoreReport, Standard } from './types.ts';
package/src/lanes.ts CHANGED
@@ -4,6 +4,13 @@
4
4
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
 
7
+ /** Metadata such as sources.json is not a derivable API of a vendor whose APIs all belong to its lanes. */
8
+ export function hasOwnSurface(pkg: string): boolean {
9
+ if (existsSync(join(pkg, 'src', 'generated', 'surface.gen.json'))) return true;
10
+ const spec = join(pkg, 'spec');
11
+ return existsSync(spec) && readdirSync(spec).some((name) => /^(openapi\.(json|yaml)(\.gz)?|smithy\.json(\.gz)?|discovery\.json(\.gz)?|rfc\d+\.txt|openapi|proto|commands|client-ops\.json)$/.test(name));
12
+ }
13
+
7
14
  /** The service a manifest names: the top-level `service:` of its exported manifest object. */
8
15
  function serviceOf(manifestPath: string): string | undefined {
9
16
  const text = readFileSync(manifestPath, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
package/src/p3-rules.ts CHANGED
@@ -14,8 +14,6 @@ const RULES: Array<{ rule: string; test: (line: string) => boolean }> = [
14
14
  { rule: "the twin's URL from the request (use ctx.publicBase)", test: (l) => /request\.url\)\.origin/.test(l) },
15
15
  // signing, digests and randomness are the kernel's (ctx.crypto, world-core's exports)
16
16
  { rule: 'node:crypto in a pack (use ctx.crypto or the kernel\'s digest)', test: (l) => /from ['"](node:)?crypto['"]|require\(['"](node:)?crypto['"]\)/.test(l) },
17
- // a webhook is the manifest's events; ctx.deliver is for a message that is no declared event (a push notification)
18
- { rule: 'ctx.deliver where the manifest\'s events belong (or record why)', test: (l) => /ctx\.deliver\(/.test(l) },
19
17
  // an object's bytes are ctx.blobs, never a row's field
20
18
  { rule: "bytes kept in a row (use ctx.blobs)", test: (l) => /ctx\.(write|record)\([^;]*toString\(['"]base64['"]\)/.test(l) || /\b_bytes\s*:/.test(l) },
21
19
  // a secret the vendor issues is drawn from ctx.secret, never computed from ids
package/src/protocol-3.ts CHANGED
@@ -4,7 +4,10 @@
4
4
  // Version 1 checks what the repository's tools can decide today. Criteria the grade section names that no tool
5
5
  // decides yet (a credential stored as issued; hollow capabilities; the invariants row) are listed in PLANNED and
6
6
  // counted in no grade until a version of this standard checks them.
7
+ import { unpublishedUnitFiles } from './conformance.ts';
8
+ import { attributionName, notIndependent } from './verdict.ts';
7
9
  import { spawnSync } from 'node:child_process';
10
+ import { AsyncLocalStorage } from 'node:async_hooks';
8
11
  import { createHash } from 'node:crypto';
9
12
  import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
10
13
  import { tmpdir } from 'node:os';
@@ -39,30 +42,54 @@ const readJson = (path: string): unknown => { try { return JSON.parse(readFileSy
39
42
 
40
43
  /** What spec/ holds beside the vendored documents: provenance, corrections, the grouping, the examples index, recordings. */
41
44
  const SPEC_METADATA = /^(SOURCE\.md|sources\.json|patches\.json|grouping\.json|doc-examples\.json|README\.md|[^/]*\.sha256|recordings\/.*)$/;
42
- /** A tool in spec/ is not a vendored document (it belongs in scripts/): named by the layout criterion, never hashed. */
45
+ /** A tool in spec/ is not a vendored document: never hashed, and outside the layout unless it is the script that wrote
46
+ * the document from the vendor's reference page, which SOURCE.md names (architecture, "Creating a pack", step 2). */
43
47
  const SPEC_TOOL = /\.(py|sh|ts|js|mjs|cjs|rb)$/;
48
+ /** spec/'s tools SOURCE.md names: the writers of a document a vendor publishes only as a page. */
49
+ function namedSpecTools(unit: GradeUnit): Set<string> {
50
+ const path = join(unit.dir, 'spec', 'SOURCE.md');
51
+ const source = existsSync(path) ? readFileSync(path, 'utf8') : '';
52
+ return new Set(filesUnder(join(unit.dir, 'spec')).filter((f) => SPEC_TOOL.test(f) && !f.includes('/') && source.includes(`\`${f}\``)));
53
+ }
44
54
  /** The vendored documents: every file under spec/ that is not its metadata. */
45
55
  function specFiles(unit: GradeUnit): string[] {
46
56
  const dir = join(unit.dir, 'spec');
47
57
  return filesUnder(dir).filter((f) => !SPEC_METADATA.test(f) && !SPEC_TOOL.test(f)).map((f) => join(dir, f));
48
58
  }
49
59
 
50
- /** Files relative to a directory: Git's tree in a checkout, the disk tree of an unpacked package outside one. */
60
+ /** The checkout a directory is in: the nearest ancestor holding `.git` (a directory, or a worktree's file). */
61
+ function checkoutOf(dir: string): string | undefined {
62
+ for (let d = dir; ; d = dirname(d)) {
63
+ if (existsSync(join(d, '.git'))) return d;
64
+ if (dirname(d) === d) return undefined;
65
+ }
66
+ }
67
+ // Grading fans out over a checkout's directories. A caller explicitly owns the immutable listing for one pass;
68
+ // nested gates share it, while concurrent passes have separate snapshots and direct checks always read git afresh.
69
+ const checkoutFiles = new AsyncLocalStorage<Map<string, string[]>>();
70
+ export function withCheckoutFiles<T>(fn: () => T): T {
71
+ return checkoutFiles.getStore() ? fn() : checkoutFiles.run(new Map(), fn);
72
+ }
73
+ /** Files relative to a directory: Git's tree in a checkout (one listing per checkout per pass), the disk tree of an
74
+ * unpacked package outside one. A failed Git listing refuses; it never reads as an empty tree. */
51
75
  function filesUnder(dir: string, base = dir): string[] {
52
76
  if (!existsSync(dir)) return [];
53
- const checkout = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
54
- if (checkout.status !== 0 || checkout.error || checkout.stdout.trim() !== 'true') {
55
- if (checkout.status !== null && checkout.status !== 0 && !checkout.error && !checkout.signal
56
- && /^fatal: not a git repository(?:[ (:]|$)/m.test(String(checkout.stderr ?? ''))) return allFilesUnder(dir, base, true);
57
- throw new Error(`git rev-parse failed: directory=${JSON.stringify(dir)} status=${checkout.status} signal=${JSON.stringify(checkout.signal)} error=${JSON.stringify(checkout.error?.message ?? null)} stdout=${JSON.stringify(String(checkout.stdout ?? '').trim())} stderr=${JSON.stringify(String(checkout.stderr ?? '').trim())}`);
58
- }
59
- // tracked and not-yet-added files alike (T0 runs before the commit); ignored runtime state (.volter) never
60
- const run = spawnSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '.'], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'] });
61
- if (run.status !== 0 || run.error) {
62
- throw new Error(`git ls-files failed: directory=${JSON.stringify(dir)} status=${run.status} signal=${JSON.stringify(run.signal)} error=${JSON.stringify(run.error?.message ?? null)} stderr=${JSON.stringify(String(run.stderr ?? '').trim())}`);
77
+ const top = checkoutOf(dir);
78
+ if (!top) return allFilesUnder(dir, base, true);
79
+ const listing = checkoutFiles.getStore();
80
+ let files = listing?.get(top);
81
+ if (!files) {
82
+ // tracked and not-yet-added files alike (T0 runs before the commit); ignored runtime state (.volter) never
83
+ const run = spawnSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '.'], { cwd: top, stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 256 * 1024 * 1024 });
84
+ if (run.status !== 0 || run.error) {
85
+ throw new Error(`git ls-files failed: directory=${JSON.stringify(top)} status=${run.status} signal=${JSON.stringify(run.signal)} error=${JSON.stringify(run.error?.message ?? null)} stderr=${JSON.stringify(String(run.stderr ?? ''))}`);
86
+ }
87
+ files = [...new Set(String(run.stdout).split('\0').filter(Boolean))];
88
+ listing?.set(top, files);
63
89
  }
64
- const tracked = [...new Set(String(run.stdout).split('\0').filter(Boolean))];
65
- return tracked.map((f) => relative(base, join(dir, f))).filter((f) => existsSync(join(base, f)));
90
+ const under = relative(top, dir);
91
+ const mine = under === '' ? files : files.filter((f) => f.startsWith(`${under}/`)).map((f) => f.slice(under.length + 1));
92
+ return mine.map((f) => relative(base, join(dir, f))).filter((f) => existsSync(join(base, f)));
66
93
  }
67
94
  /** Disk files outside a checkout, or the complete output of the grader's own derivation. */
68
95
  function allFilesUnder(dir: string, base = dir, excludeArtifacts = false): string[] {
@@ -94,15 +121,26 @@ const isLane = (unit: GradeUnit) => unit.name.includes('/');
94
121
  /** The fixed names at src/'s root ("Pack layout"); a lane's serve factory and bin are its vendor's. */
95
122
  function srcFileInForm(unit: GradeUnit, name: string): boolean {
96
123
  if (name.endsWith('.d.ts')) return true; // the types of a vendor SDK the pack's tests drive
97
- return (isLane(unit) ? ['manifest.ts', 'index.ts', 'fetch.ts', 'budget.ts'] : ['manifest.ts', 'index.ts', 'cli.ts', 'fetch.ts', 'server.ts', 'budget.ts']).includes(name);
124
+ return (isLane(unit) ? ['manifest.ts', 'index.ts', 'fetch.ts', 'budget.ts'] : ['manifest.ts', 'index.ts', 'cli.ts', 'fetch.ts', 'server.ts', 'stream.ts', 'budget.ts']).includes(name);
98
125
  }
99
126
  /** The fixed files of semantics/ besides the families' handler files and their tests. */
100
- const SEMANTICS_FIXED = new Set(['states.ts', 'index.ts', 'shared.ts', 'doors.ts', 'seed.ts', 'around.ts', 'events.ts', 'clock.ts', 'tenant.ts', 'scenario.ts', 'gap.ts', 'graphql.ts', 'sockets.ts', 'board.ts']);
127
+ const SEMANTICS_FIXED = new Set(['answer-views.ts', 'states.ts', 'index.ts', 'shared.ts', 'doors.ts', 'seed.ts', 'around.ts', 'events.ts', 'clock.ts', 'tenant.ts', 'scenario.ts', 'gap.ts', 'graphql.ts', 'sockets.ts', 'board.ts']);
101
128
  /** journeys/'s closed set. */
102
129
  const JOURNEY_FILES = new Set(['customer-life.outline.md', 'customer-life.outline.verdict.json', 'customer-life.json', 'customer-life.verdict.json', 'decisions.json',
103
130
  'vendor-examples.json', 'vendor-examples.verdict.json', 'demand.json', 'unreachable.json', 'score.json', 'coverage.json',
104
131
  // a generative pack's life names the World's scenario it walks under (architecture, "Writing a life")
105
132
  'scenario.json']);
133
+ /** Only scenario basenames the two walks explicitly declare extend the closed layout. */
134
+ function journeyFiles(unit: GradeUnit): Set<string> {
135
+ const files = new Set(JOURNEY_FILES);
136
+ for (const name of ['customer-life.json', 'vendor-examples.json']) {
137
+ try {
138
+ const journey = JSON.parse(readFileSync(join(unit.dir, 'journeys', name), 'utf8')) as { scenario?: unknown };
139
+ if (typeof journey.scenario === 'string' && /^[a-z0-9][a-z0-9.-]*\.json$/i.test(journey.scenario)) files.add(journey.scenario);
140
+ } catch { /* malformed or absent journeys get no additional files */ }
141
+ }
142
+ return files;
143
+ }
106
144
  /** Entries at the unit's root the form does not name (a vendor's lanes are units of their own). */
107
145
  function rootOutsideForm(unit: GradeUnit): string[] {
108
146
  return entriesOf(unit.dir).filter((name) => {
@@ -141,7 +179,7 @@ const FORBIDDEN_REACH: ReadonlyArray<readonly [RegExp, string]> = [
141
179
  [/\bprocess\.env\b|\bBun\.env\b|\bDeno\.env\b/, 'the environment'],
142
180
  [/(^|[^\w])fetch\s*\(|\bXMLHttpRequest\b|\bnode:(net|http|https|tls|dgram)\b/m, 'the network'],
143
181
  [/\bimport\s*\(|\brequire\s*\(/, 'a module loaded at run time'],
144
- [/\bctx\.(now|core|atomically|tree|writeDetailed)\b|\}\s*=\s*ctx\b|\bctx\s*\[/, 'a context member outside the contract, or the context taken apart'],
182
+ [/\bctx\.(now|core|atomically|tree|writeDetailed)\b|\}\s*=\s*ctx\b(?!\s*[.?])|\bctx\s*\[/, 'a context member outside the contract, or the context taken apart'],
145
183
  [/\bSemanticsContext\b|\bSemantics\b|\bsemanticsContext\b/, "the kernel's wide context (a handler is typed Handler, over HandlerContext; a lane reaches its vendor's through ctx.over)"],
146
184
  ];
147
185
  /** A handler's name for an operationId: the id with every character a JavaScript name cannot hold made `_`, and a
@@ -201,10 +239,10 @@ const criteria: Criterion[] = [
201
239
  // a criterion whose premise is absent is not met: no spec, no corrections to judge
202
240
  if (!specFiles(unit).length) return fail('no vendored spec');
203
241
  if (!existsSync(path)) return pass('no corrections');
204
- const doc = readJson(path) as { patches?: Array<{ why?: string; source?: { url?: string; spec?: string } }> } | undefined;
242
+ const doc = readJson(path) as { patches?: Array<{ why?: string; source?: { url?: string; spec?: string; recording?: string } }> } | undefined;
205
243
  const patches = doc?.patches;
206
244
  if (!Array.isArray(patches)) return fail('spec/patches.json is not { patches: [...] }');
207
- const bare = patches.filter((p) => !p.why || !(p.source?.url || p.source?.spec || /https?:\/\//.test(p.why))).length;
245
+ const bare = patches.filter((p) => !p.why || !(p.source?.url || p.source?.spec || p.source?.recording || /https?:\/\//.test(p.why))).length;
208
246
  return bare ? fail(`${bare} of ${patches.length} corrections lack a reason or a source`) : pass(`${patches.length} corrections, each sourced`);
209
247
  },
210
248
  },
@@ -246,7 +284,8 @@ const criteria: Criterion[] = [
246
284
  const families = new Set(operationsOf(unit).values());
247
285
  const outside = [
248
286
  ...rootOutsideForm(unit),
249
- ...filesUnder(join(unit.dir, 'spec')).filter((f) => SPEC_TOOL.test(f)).map((f) => `spec/${f}`),
287
+ ...unpublishedUnitFiles(unit.dir),
288
+ ...filesUnder(join(unit.dir, 'spec')).filter((f) => SPEC_TOOL.test(f) && !namedSpecTools(unit).has(f)).map((f) => `spec/${f}`),
250
289
  ...entriesOf(src).filter((name) => statSync(join(src, name)).isDirectory() ? !LAYOUT_DIRS.has(name) : !srcFileInForm(unit, name)).map((n) => `src/${n}`),
251
290
  ...filesUnder(join(src, 'semantics')).filter((f) => {
252
291
  if (f.includes('/')) return true;
@@ -254,7 +293,7 @@ const criteria: Criterion[] = [
254
293
  return !(SEMANTICS_FIXED.has(f) || SEMANTICS_FIXED.has(`${stem}.ts`) || (families.has(stem.replace(/-ops$/, '')) && /\.(test\.)?ts$/.test(f)));
255
294
  }).map((f) => `src/semantics/${f}`),
256
295
  // a lane over its vendor's state has no journeys of its own: the vendor's life walks it ("Pack layout")
257
- ...filesUnder(join(unit.dir, 'journeys')).filter((f) => vendorLanes(unit).length > 0 || !JOURNEY_FILES.has(f)).map((f) => `journeys/${f}`),
296
+ ...filesUnder(join(unit.dir, 'journeys')).filter((f) => vendorLanes(unit).length > 0 || !journeyFiles(unit).has(f)).map((f) => `journeys/${f}`),
258
297
  ];
259
298
  // screens/: one file per screen the manifest declares, named by its id
260
299
  if (existsSync(join(src, 'screens'))) {
@@ -324,6 +363,11 @@ const criteria: Criterion[] = [
324
363
  if (names.join() !== 'clock' || /^export\s+(const|let|var|class)\s|^export\s*\{/m.test(text)) found.push(`clock.ts exports ${names.join(', ') || 'nothing'}, not only clock`);
325
364
  continue;
326
365
  }
366
+ if (f === 'answer-views.ts') {
367
+ const names = [...text.matchAll(/^export\s+const\s+([A-Za-z0-9_$]+)/gm)].map((m) => m[1]);
368
+ if (names.join() !== 'answerViews' || /^export\s+(?:async\s+)?function\s|^export\s+(let|var|class)\s|^export\s*\{/m.test(text)) found.push(`answer-views.ts exports ${names.join(', ') || 'nothing'}, not only answerViews`);
369
+ continue;
370
+ }
327
371
  if (f === 'board.ts') {
328
372
  const names = [...text.matchAll(/^export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)/gm)].map((m) => m[1]);
329
373
  if (names.join() !== 'board' || /^export\s+(const|let|var|class)\s|^export\s*\{/m.test(text)) found.push(`board.ts exports ${names.join(', ') || 'nothing'}, not only board`);
@@ -527,13 +571,13 @@ const criteria: Criterion[] = [
527
571
  const outline = join(journeys, 'customer-life.outline.md');
528
572
  const verdict = readJson(join(journeys, 'customer-life.outline.verdict.json')) as { sha256?: string; plausible?: boolean; judge?: { agent?: string; session?: string }; authors?: string[] } | undefined;
529
573
  if (!existsSync(outline)) return fail('no journeys/customer-life.outline.md');
574
+ const author = /^Author:\s*(.+?)\.?\s*$/m.exec(readFileSync(outline, 'utf8'))?.[1];
575
+ attributionName(author);
530
576
  if (!verdict) return fail('no outline verdict');
531
577
  if (!verdict.plausible) return fail('judged not plausible');
532
- if (/self-judged/i.test(verdict.judge?.session ?? '')) return fail('self-judged (not a verdict)');
533
- if (!verdict.judge?.agent) return fail('the verdict does not name its judge (judge.agent)');
534
- if (!verdict.authors?.length) return fail('the verdict does not name the authors, so the judge is not shown to be another session');
535
- if (verdict.authors.includes(verdict.judge.agent)) return fail('the judge is among the authors');
536
- return pass(`plausible, judge ${verdict.judge.agent}`);
578
+ const dependent = notIndependent(verdict);
579
+ if (dependent) return fail(dependent);
580
+ return pass(`plausible, judge ${verdict.judge!.agent}`);
537
581
  },
538
582
  },
539
583
  {
@@ -542,7 +586,7 @@ const criteria: Criterion[] = [
542
586
  check(unit) {
543
587
  const r = report(unit);
544
588
  if (!r) return fail(noReport(unit));
545
- return r.life.verdict === 'plausible' ? pass(`${r.life.steps} steps, judge ${r.life.judge}`) : fail(`verdict: ${r.life.verdict}`);
589
+ return r.life.steps > 0 && r.life.verdict === 'plausible' ? pass(`${r.life.steps} steps, judge ${r.life.judge}`) : fail(`verdict: ${r.life.verdict}`);
546
590
  },
547
591
  },
548
592
  {