@volter/twin-standard 1.0.30 → 1.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/archive-source.d.ts +13 -0
- package/dist/src/archive-source.js +176 -0
- package/dist/src/check-sources.d.ts +19 -2
- package/dist/src/check-sources.js +95 -37
- package/dist/src/cli.js +52 -9
- package/dist/src/clients/ai-gateway.d.ts +1 -0
- package/dist/src/clients/ai-gateway.js +3 -0
- package/dist/src/clients/anthropic-messages.d.ts +10 -0
- package/dist/src/clients/anthropic-messages.js +52 -0
- package/dist/src/clients/anthropic.d.ts +1 -0
- package/dist/src/clients/anthropic.js +3 -0
- package/dist/src/clients/aws.d.ts +2 -0
- package/dist/src/clients/aws.js +137 -0
- package/dist/src/clients/cerebras.d.ts +1 -0
- package/dist/src/clients/cerebras.js +3 -0
- package/dist/src/clients/clerk.d.ts +2 -0
- package/dist/src/clients/clerk.js +99 -0
- package/dist/src/clients/cloudflare.d.ts +2 -0
- package/dist/src/clients/cloudflare.js +86 -0
- package/dist/src/clients/elevenlabs.d.ts +2 -0
- package/dist/src/clients/elevenlabs.js +39 -0
- package/dist/src/clients/gemini.d.ts +2 -0
- package/dist/src/clients/gemini.js +92 -0
- package/dist/src/clients/github.d.ts +2 -0
- package/dist/src/clients/github.js +71 -0
- package/dist/src/clients/groq.d.ts +1 -0
- package/dist/src/clients/groq.js +3 -0
- package/dist/src/clients/harness.d.ts +36 -0
- package/dist/src/clients/harness.js +74 -0
- package/dist/src/clients/merge.d.ts +1 -0
- package/dist/src/clients/merge.js +8 -0
- package/dist/src/clients/openai-compatible.d.ts +9 -0
- package/dist/src/clients/openai-compatible.js +51 -0
- package/dist/src/clients/openai.d.ts +2 -0
- package/dist/src/clients/openai.js +65 -0
- package/dist/src/clients/openrouter.d.ts +1 -0
- package/dist/src/clients/openrouter.js +3 -0
- package/dist/src/clients/perplexity.d.ts +1 -0
- package/dist/src/clients/perplexity.js +3 -0
- package/dist/src/clients/planetscale.d.ts +2 -0
- package/dist/src/clients/planetscale.js +109 -0
- package/dist/src/clients/posthog.d.ts +2 -0
- package/dist/src/clients/posthog.js +145 -0
- package/dist/src/clients/run.d.ts +1 -0
- package/dist/src/clients/run.js +25 -0
- package/dist/src/clients/sentry.d.ts +2 -0
- package/dist/src/clients/sentry.js +167 -0
- package/dist/src/clients/slack.d.ts +2 -0
- package/dist/src/clients/slack.js +107 -0
- package/dist/src/clients/stripe.d.ts +2 -0
- package/dist/src/clients/stripe.js +161 -0
- package/dist/src/clients/tinybird.d.ts +2 -0
- package/dist/src/clients/tinybird.js +71 -0
- package/dist/src/clients/tunnel.d.ts +2 -0
- package/dist/src/clients/tunnel.js +99 -0
- package/dist/src/clients/upstash.d.ts +2 -0
- package/dist/src/clients/upstash.js +143 -0
- package/dist/src/clients/volteridentity.d.ts +2 -0
- package/dist/src/clients/volteridentity.js +128 -0
- package/dist/src/clients/xai.d.ts +2 -0
- package/dist/src/clients/xai.js +23 -0
- package/dist/src/conformance.d.ts +33 -0
- package/dist/src/conformance.js +520 -0
- package/dist/src/derive.js +113 -22
- package/dist/src/gate.js +22 -16
- package/dist/src/index.d.ts +5 -3
- package/dist/src/index.js +5 -3
- package/dist/src/lanes.d.ts +2 -0
- package/dist/src/lanes.js +7 -0
- package/dist/src/p3-rules.js +0 -2
- package/dist/src/protocol-3.d.ts +1 -0
- package/dist/src/protocol-3.js +76 -30
- package/dist/src/published.d.ts +6 -0
- package/dist/src/published.js +65 -11
- package/dist/src/spec-documents.js +2 -1
- package/dist/src/spec-ir-client.d.ts +5 -1
- package/dist/src/spec-ir-client.js +12 -2
- package/dist/src/spec-ir.d.ts +7 -2
- package/dist/src/spec-ir.js +42 -10
- package/dist/src/verdict.d.ts +14 -0
- package/dist/src/verdict.js +30 -0
- package/dist/src/walk/behavior-journey.d.ts +47 -0
- package/dist/src/walk/behavior-journey.js +1138 -0
- package/dist/src/walk/git-client.d.ts +26 -0
- package/dist/src/walk/git-client.js +139 -0
- package/dist/src/walk/journey-kit.d.ts +445 -0
- package/dist/src/walk/journey-kit.js +743 -0
- package/dist/src/walk/shape.d.ts +54 -0
- package/dist/src/walk/shape.js +210 -0
- package/dist/src/walk/sse-example.d.ts +2 -0
- package/dist/src/walk/sse-example.js +32 -0
- package/dist/src/walk/vendor-fetch.d.ts +20 -0
- package/dist/src/walk/vendor-fetch.js +92 -0
- package/dist/src/walk/walk-database.d.ts +12 -0
- package/dist/src/walk/walk-database.js +96 -0
- package/dist/src/walk/xml-view.d.ts +2 -0
- package/dist/src/walk/xml-view.js +55 -0
- package/package.json +39 -3
- package/src/archive-source.ts +141 -0
- package/src/check-sources.ts +83 -33
- package/src/cli.ts +40 -8
- package/src/clients/ai-gateway.ts +4 -0
- package/src/clients/anthropic-messages.ts +60 -0
- package/src/clients/anthropic.ts +4 -0
- package/src/clients/aws.ts +119 -0
- package/src/clients/cerebras.ts +4 -0
- package/src/clients/clerk.ts +97 -0
- package/src/clients/cloudflare.ts +80 -0
- package/src/clients/elevenlabs.ts +38 -0
- package/src/clients/gemini.ts +89 -0
- package/src/clients/github.ts +67 -0
- package/src/clients/groq.ts +4 -0
- package/src/clients/harness.ts +101 -0
- package/src/clients/merge.ts +9 -0
- package/src/clients/openai-compatible.ts +59 -0
- package/src/clients/openai.ts +66 -0
- package/src/clients/openrouter.ts +4 -0
- package/src/clients/perplexity.ts +4 -0
- package/src/clients/planetscale.ts +98 -0
- package/src/clients/posthog.ts +132 -0
- package/src/clients/run.ts +18 -0
- package/src/clients/sentry.ts +157 -0
- package/src/clients/slack.ts +98 -0
- package/src/clients/stripe.ts +129 -0
- package/src/clients/tinybird.ts +69 -0
- package/src/clients/tunnel.ts +79 -0
- package/src/clients/upstash.ts +137 -0
- package/src/clients/volteridentity.ts +108 -0
- package/src/clients/xai.ts +25 -0
- package/src/conformance.ts +446 -0
- package/src/derive.ts +87 -16
- package/src/gate.ts +20 -14
- package/src/index.ts +5 -3
- package/src/lanes.ts +7 -0
- package/src/p3-rules.ts +0 -2
- package/src/protocol-3.ts +71 -27
- package/src/published.ts +59 -11
- package/src/spec-documents.ts +2 -1
- package/src/spec-ir-client.ts +15 -4
- package/src/spec-ir.ts +43 -5
- package/src/verdict.ts +28 -0
- package/src/walk/behavior-journey.ts +806 -0
- package/src/walk/git-client.ts +138 -0
- package/src/walk/journey-kit.ts +858 -0
- package/src/walk/shape.ts +208 -0
- package/src/walk/sse-example.ts +25 -0
- package/src/walk/vendor-fetch.ts +80 -0
- package/src/walk/walk-database.ts +97 -0
- package/src/walk/xml-view.ts +44 -0
package/dist/src/derive.js
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
// derive — the derived-pack generator's build step (docs/contributing/architecture.md, "The derived pack"), the
|
|
3
|
-
// standard's own: the grade's `derived-reproducible` runs it, so every grader derives the same way. Reads a pack's
|
|
4
|
-
// vendored spec (`<pack>/spec/openapi.json[.gz]`), builds the spec IR (./spec-ir.ts) and writes the pack's
|
|
5
|
-
// `src/generated/`. Deterministic: a second run over the same spec writes the same bytes. Never hand-edit what it writes.
|
|
6
|
-
//
|
|
7
|
-
// Usage: twin-standard derive <pack dir> [--out <dir>] (twin-world: bun scripts/derive-pack.ts <vendor>)
|
|
8
2
|
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
9
3
|
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
10
4
|
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
@@ -13,6 +7,13 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
13
7
|
}
|
|
14
8
|
return path;
|
|
15
9
|
};
|
|
10
|
+
import { parseExactJson, exactInteger } from '@volter/world-core/exact-json';
|
|
11
|
+
// derive — the derived-pack generator's build step (docs/contributing/architecture.md, "The derived pack"), the
|
|
12
|
+
// standard's own: the grade's `derived-reproducible` runs it, so every grader derives the same way. Reads a pack's
|
|
13
|
+
// vendored spec (`<pack>/spec/openapi.json[.gz]`), builds the spec IR (./spec-ir.ts) and writes the pack's
|
|
14
|
+
// `src/generated/`. Deterministic: a second run over the same spec writes the same bytes. Never hand-edit what it writes.
|
|
15
|
+
//
|
|
16
|
+
// Usage: twin-standard derive <pack dir> [--out <dir>] (twin-world: bun scripts/derive-pack.ts <vendor>)
|
|
16
17
|
import { parse as parseYaml } from 'yaml';
|
|
17
18
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
18
19
|
import { join } from 'node:path';
|
|
@@ -23,7 +24,7 @@ import { fromProto, protoSchema, readProtoFiles } from "./spec-ir-proto.js";
|
|
|
23
24
|
import { fromRedisCommands } from "./spec-ir-commands.js";
|
|
24
25
|
import { fromClientCalls } from "./spec-ir-client.js";
|
|
25
26
|
import { discoveryMethods, fromDiscovery, isDiscovery } from "./spec-ir-discovery.js";
|
|
26
|
-
import { handlerCitations, quotedPage, urlsIn } from "./check-sources.js";
|
|
27
|
+
import { handlerCitations, quotedPage, sourceHeld, urlsIn } from "./check-sources.js";
|
|
27
28
|
import { applySpecPatch } from "./spec-patches.js";
|
|
28
29
|
import { unitAt } from "./unit.js";
|
|
29
30
|
import { mergeDocuments, readSpecDocuments } from "./spec-documents.js";
|
|
@@ -56,12 +57,13 @@ const text = (specPath.endsWith('.gz') ? gunzipSync(bytes) : bytes).toString('ut
|
|
|
56
57
|
// corrects a JSON spec
|
|
57
58
|
const lineProtocol = specPath.endsWith('.txt');
|
|
58
59
|
const commandVersion = commandTable ? (/\*\*Version:\*\* `([^`]+)`/.exec(existsSync(join(PKG, 'spec', 'SOURCE.md')) ? readFileSync(join(PKG, 'spec', 'SOURCE.md'), 'utf8') : '')?.[1] ?? '') : '';
|
|
59
|
-
let doc = severalDocuments ? readSpecDocuments(join(PKG, 'spec')) : commandTable ? fromRedisCommands(Object.fromEntries(readdirSync(specPath).filter((f) => f.endsWith('.json')).sort().map((f) => [f,
|
|
60
|
+
let doc = 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);
|
|
60
61
|
// ── the evidence the pack cites ──────────────────────────────────────────────────────────────
|
|
61
62
|
// A source is a page check-sources (./check-sources.ts) fetched and recorded found (spec/sources.json), a quote
|
|
62
|
-
// that page holds,
|
|
63
|
-
//
|
|
64
|
-
|
|
63
|
+
// that page holds, `spec:<operationId or /json/pointer> "quote"`: words of the vendored spec itself (read before any
|
|
64
|
+
// patch, so a patch never vouches for itself), or `recording:<file> "quote"`: words of an answer the vendor gave,
|
|
65
|
+
// recorded in spec/recordings/.
|
|
66
|
+
const vendored = lineProtocol || commandTable ? structuredClone(doc) : parseExactJson(text.startsWith('{') ? text : JSON.stringify(doc));
|
|
65
67
|
/** An RFC's section by its number (`4.1.4`): its heading's line to the next heading's. */
|
|
66
68
|
const sectionOf = (n) => {
|
|
67
69
|
const at = text.search(new RegExp(`^${n.replace(/\./g, '\\.')}\\. `, 'm'));
|
|
@@ -71,7 +73,7 @@ const sectionOf = (n) => {
|
|
|
71
73
|
return next < 0 ? text.slice(at) : text.slice(at, at + 1 + next);
|
|
72
74
|
};
|
|
73
75
|
const sourcesPath = join(PKG, 'spec', 'sources.json');
|
|
74
|
-
const recorded = existsSync(sourcesPath) ?
|
|
76
|
+
const recorded = existsSync(sourcesPath) ? parseExactJson(readFileSync(sourcesPath, 'utf8')) : {};
|
|
75
77
|
const HTTP_METHODS = new Set(['get', 'put', 'post', 'delete', 'patch', 'head', 'options', 'trace']);
|
|
76
78
|
const specOps = new Map();
|
|
77
79
|
// an operation by its id: the spec's operationId, or the `method_path` slug the IR mints where the spec names none (QStash)
|
|
@@ -104,8 +106,25 @@ const wordsIn = (node) => (typeof node === 'string' ? node : node && typeof node
|
|
|
104
106
|
const bare = (s) => s.replace(/\s+/g, '');
|
|
105
107
|
// a JSON pointer into the spec, an operation by id, or (an RFC's) a section by number
|
|
106
108
|
const specNode = (ref) => (ref.startsWith('/') ? ref.split('/').slice(1).map((k) => k.replace(/~1/g, '/').replace(/~0/g, '~')).reduce((n, k) => n?.[k], severalDocuments && !ref.startsWith('/documents/') ? pointed : vendored) : lineProtocol && /^\d+(\.\d+)*$/.test(ref) ? sectionOf(ref) : specOps.get(ref));
|
|
109
|
+
/** Validate structured recording evidence before any string interpolation or page fallback. */
|
|
110
|
+
function unheldRecording(name, quote) {
|
|
111
|
+
if (typeof name !== 'string' || !/^[\w.-]+$/.test(name))
|
|
112
|
+
return 'recording filename is no recording (use a safe basename)';
|
|
113
|
+
if (typeof quote !== 'string' || !quote.trim())
|
|
114
|
+
return 'recording evidence quotes nothing (use a nonempty string)';
|
|
115
|
+
const file = join(PKG, 'spec', 'recordings', name);
|
|
116
|
+
if (!existsSync(file))
|
|
117
|
+
return `spec/recordings/${name} is no recording`;
|
|
118
|
+
return bare(readFileSync(file, 'utf8')).includes(bare(quote.replace(/\\"/g, '"'))) ? undefined : `spec/recordings/${name} does not hold "${quote}"`;
|
|
119
|
+
}
|
|
107
120
|
/** Why a citation does not hold, or undefined when it does. */
|
|
108
121
|
function unheld(source) {
|
|
122
|
+
// Malformed recording citations never fall back to a checked page.
|
|
123
|
+
const recording = /^recording:(\S+)\s+"([\s\S]*)"$/.exec(source.trim());
|
|
124
|
+
if (recording)
|
|
125
|
+
return unheldRecording(recording[1], recording[2]);
|
|
126
|
+
if (source.trim().startsWith('recording:'))
|
|
127
|
+
return 'malformed recording citation';
|
|
109
128
|
const spec = /^spec:(\S+)\s+"([\s\S]*)"$/.exec(source.trim());
|
|
110
129
|
if (spec && !spec[2].trim())
|
|
111
130
|
return `spec:${spec[1]} quotes nothing`;
|
|
@@ -115,10 +134,15 @@ function unheld(source) {
|
|
|
115
134
|
return `the spec has nothing at ${spec[1]}`;
|
|
116
135
|
return bare(wordsIn(node)).includes(bare(spec[2].replace(/\\"/g, '"'))) ? undefined : `the spec at ${spec[1]} does not say "${spec[2]}"`;
|
|
117
136
|
}
|
|
137
|
+
if (source.trim().startsWith('archive:') && !quotedPage(source))
|
|
138
|
+
return 'archive evidence needs a valid pinned member and quote';
|
|
139
|
+
const archiveQuote = quotedPage(source);
|
|
140
|
+
if (archiveQuote?.url.startsWith('archive:') && recorded[archiveQuote.url]?.quotes?.[archiveQuote.quote] !== true)
|
|
141
|
+
return 'archive member does not hold its quote as recorded';
|
|
118
142
|
const urls = urlsIn(source);
|
|
119
143
|
if (!urls.length)
|
|
120
144
|
return `"${source.slice(0, 60)}" names no page and no spec words`;
|
|
121
|
-
const dead = urls.filter((u) => recorded[u]
|
|
145
|
+
const dead = urls.filter((u) => !sourceHeld(u, recorded[u]));
|
|
122
146
|
return dead.length ? dead.map((u) => `${u} is ${recorded[u] ? `answering ${recorded[u].status || 'nothing'}` : 'not checked'} (twin-standard check-sources ${vendor})`).join('; ') : undefined;
|
|
123
147
|
}
|
|
124
148
|
const evidence = [];
|
|
@@ -127,26 +151,27 @@ for (const c of handlerCitations(PKG)) {
|
|
|
127
151
|
const q = quotedPage(c.source);
|
|
128
152
|
// a quote is checked only in the two forms that say where it is; any other quote is refused, never passed unread
|
|
129
153
|
const trimmed = c.source.trim();
|
|
130
|
-
const loose = !q && (/^spec:/.test(trimmed) ? !/^spec:\S+\s+"[\s\S]*"$/.test(trimmed) : trimmed.includes('"'));
|
|
131
|
-
const held = loose ? `"${c.source.slice(0, 60)}" quotes words in no form a check reads (\`<url> "<quote>"\` or \`
|
|
154
|
+
const loose = !q && (/^(spec|recording):/.test(trimmed) ? !/^(spec|recording):\S+\s+"[\s\S]*"$/.test(trimmed) : trimmed.includes('"'));
|
|
155
|
+
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);
|
|
132
156
|
if (held)
|
|
133
|
-
evidence.push(
|
|
157
|
+
evidence.push(`${c.where}: ${held}`);
|
|
134
158
|
}
|
|
135
159
|
// Corrections to the vendor's spec, as data: RFC 6902 operations (add, replace, remove, move, copy)
|
|
136
160
|
// in spec/patches.json, applied before the IR so the vendored file stays the vendor's own bytes. Each says
|
|
137
161
|
// `why`, citing what shows the vendor differs from its spec (its docs, the spec's own example, a recording).
|
|
138
162
|
const patchPath = join(PKG, 'spec', 'patches.json');
|
|
139
163
|
if (existsSync(patchPath)) {
|
|
140
|
-
for (const p of
|
|
164
|
+
for (const p of parseExactJson(readFileSync(patchPath, 'utf8')).patches) {
|
|
141
165
|
if (!p.why?.trim()) {
|
|
142
166
|
console.error(`${vendor}: spec patch ${p.op} ${p.path} gives no why`);
|
|
143
167
|
process.exit(1);
|
|
144
168
|
}
|
|
145
169
|
{
|
|
146
170
|
const s = p.source;
|
|
147
|
-
const held = s?.
|
|
148
|
-
: s?.
|
|
149
|
-
:
|
|
171
|
+
const held = s?.recording !== undefined ? unheldRecording(s.recording, s.quote)
|
|
172
|
+
: s?.spec ? unheld(`spec:${s.spec} "${s.quote}"`)
|
|
173
|
+
: 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`)
|
|
174
|
+
: urlsIn(p.why).length ? unheld(p.why) : 'it cites no page and quotes nothing';
|
|
150
175
|
if (held)
|
|
151
176
|
evidence.push(`spec patch ${p.op} ${p.path}: ${held}`);
|
|
152
177
|
}
|
|
@@ -197,7 +222,7 @@ const ir = isDiscovery(doc) ? fromDiscovery(doc) : doc?.format === 'client' ? fr
|
|
|
197
222
|
// A spec that groups its operations by tag (Cloudflare's: nearly every path begins /accounts/{account_id}/, so the
|
|
198
223
|
// path's first segment names one family for thousands of operations) says so in spec/grouping.json, `{ "by": "tag" }`:
|
|
199
224
|
// each operation's family is then its first tag, as a file name (`Worker Script` is `worker-script`).
|
|
200
|
-
const grouping = existsSync(join(PKG, 'spec', 'grouping.json')) ?
|
|
225
|
+
const grouping = existsSync(join(PKG, 'spec', 'grouping.json')) ? parseExactJson(readFileSync(join(PKG, 'spec', 'grouping.json'), 'utf8')) : {};
|
|
201
226
|
if (grouping.by === 'tag') {
|
|
202
227
|
// an operation the spec names no operationId for is found by its method and path, in the spec as corrected (an
|
|
203
228
|
// operation a patch adds carries its own tags: PostHog's person delete)
|
|
@@ -218,10 +243,14 @@ if (grouping.by === 'tag') {
|
|
|
218
243
|
const sdlPath = ['schema.graphql.gz', 'schema.graphql'].map((f) => join(PKG, 'spec', f)).find((p) => existsSync(p));
|
|
219
244
|
// the GraphQL wire's root fields (`mutation.issueArchive`), which a transition may name as a REST operation is named
|
|
220
245
|
const graphqlOperations = new Set();
|
|
246
|
+
let graphqlSchema;
|
|
247
|
+
let graphqlLib;
|
|
221
248
|
if (sdlPath) {
|
|
222
249
|
const { fromGraphQL } = await import("./spec-ir-graphql.js");
|
|
223
250
|
const sdl = (sdlPath.endsWith('.gz') ? gunzipSync(readFileSync(sdlPath)) : readFileSync(sdlPath)).toString('utf8');
|
|
224
251
|
const gql = fromGraphQL(sdl);
|
|
252
|
+
graphqlLib = await import('graphql');
|
|
253
|
+
graphqlSchema = graphqlLib.buildSchema(sdl, { assumeValidSDL: true });
|
|
225
254
|
for (const o of gql.operations)
|
|
226
255
|
graphqlOperations.add(o.id);
|
|
227
256
|
mkdirSync(out, { recursive: true });
|
|
@@ -314,17 +343,79 @@ if (existsSync(manifestPath)) {
|
|
|
314
343
|
const r = decl.refresh;
|
|
315
344
|
if (!r)
|
|
316
345
|
return [];
|
|
346
|
+
if ('graphql' in r && r.graphql) {
|
|
347
|
+
const g = graphqlLib;
|
|
348
|
+
if (!g || !graphqlSchema)
|
|
349
|
+
return [`${name}: a GraphQL refresh needs a vendored schema`];
|
|
350
|
+
const refresh = r.graphql;
|
|
351
|
+
const id = r.list ?? r.get;
|
|
352
|
+
if (!id || !graphqlOperations.has(id) || !id.startsWith('query.'))
|
|
353
|
+
return [`${name}: refresh names no GraphQL read operation (${id})`];
|
|
354
|
+
let document;
|
|
355
|
+
try {
|
|
356
|
+
document = g.parse(refresh.query);
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
return [`${name}: invalid GraphQL refresh: ${String(error)}`];
|
|
360
|
+
}
|
|
361
|
+
const invalid = g.validate(graphqlSchema, document);
|
|
362
|
+
if (invalid.length)
|
|
363
|
+
return invalid.map((e) => `${name}: GraphQL refresh: ${e.message}`);
|
|
364
|
+
const operation = g.getOperationAST(document);
|
|
365
|
+
if (!operation || operation.operation !== 'query')
|
|
366
|
+
return [`${name}: a refresh document must select one query`];
|
|
367
|
+
const parts = refresh.items.replace(/^\$body\.?/, '').split('.');
|
|
368
|
+
if (parts.shift() !== 'data')
|
|
369
|
+
return [`${name}: GraphQL items must name a path under data`];
|
|
370
|
+
let selections = operation.selectionSet;
|
|
371
|
+
let type = graphqlSchema.getQueryType();
|
|
372
|
+
for (const part of parts) {
|
|
373
|
+
const field = selections.selections.find((n) => n.kind === 'Field' && (n.alias?.value ?? n.name.value) === part);
|
|
374
|
+
if (!field || !g.isObjectType(type))
|
|
375
|
+
return [`${name}: GraphQL items path ${refresh.items} is not selected by the document`];
|
|
376
|
+
if (type === graphqlSchema.getQueryType() && `query.${field.name.value}` !== id)
|
|
377
|
+
return [`${name}: GraphQL refresh declares ${id} but selects query.${field.name.value}`];
|
|
378
|
+
const definition = type.getFields()[field.name.value];
|
|
379
|
+
type = g.getNamedType(definition.type);
|
|
380
|
+
selections = field.selectionSet;
|
|
381
|
+
}
|
|
382
|
+
const normalize = (value) => value.replace(/_/g, '').toLowerCase();
|
|
383
|
+
if (normalize(type.name) !== normalize(name) && normalize(type.name) !== normalize(decl.storedAs ?? name))
|
|
384
|
+
return [`${name}: GraphQL items return ${type.name}, not ${name}`];
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
317
387
|
const op = (id) => ir.operations.find((o) => o.id === id);
|
|
318
388
|
const ours = (o) => !specNamed.has(name) || o.resource === name;
|
|
319
389
|
if (r.list !== undefined) {
|
|
320
390
|
const o = op(r.list);
|
|
391
|
+
// a list the spec's reading does not class as one (a nullable array, a union of list envelopes, a list nested in its
|
|
392
|
+
// envelope: Discord's, Polar's, Cloudflare's) is read where the scope says its items are: any read the spec names
|
|
393
|
+
// (still this resource's read: one the spec's reading gives another resource is refused)
|
|
394
|
+
if (o && r.items) {
|
|
395
|
+
// a read asked by POST (an RPC list: AWS JSON's ListSecrets) is a read too; one the spec classes a write is not
|
|
396
|
+
if (String(o.method).toUpperCase() !== 'GET' && ['create', 'update', 'delete', 'action'].includes(String(o.class)))
|
|
397
|
+
return [`${name}: refresh.list ${r.list} is a ${o.class}, not a read`];
|
|
398
|
+
const detail = r.detail ? op(r.detail) : undefined;
|
|
399
|
+
const detailed = detail?.method.toUpperCase() === 'GET' && ours(detail);
|
|
400
|
+
return !o.resource || ours(o) || detailed ? [] : [`${name}: refresh.list ${r.list} reads ${o.resource}, not ${name}`];
|
|
401
|
+
}
|
|
321
402
|
if (!(o?.class === 'list' && ours(o)))
|
|
322
403
|
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}`];
|
|
323
404
|
return o.answers?.list || r.items ? [] : [`${name}: refresh.list ${r.list} answers no list the spec names; say where its items are (items)`];
|
|
324
405
|
}
|
|
325
406
|
if (r.get !== undefined) {
|
|
326
407
|
const o = op(r.get);
|
|
327
|
-
|
|
408
|
+
if (o?.class === 'retrieve' && ours(o))
|
|
409
|
+
return [];
|
|
410
|
+
// a resource the spec does not name (a client-derived spec classes every request an action) is read by the GET
|
|
411
|
+
// the scope names, the answer itself the subject
|
|
412
|
+
if (o && !specNamed.has(name) && !o.resource && String(o.method).toUpperCase() === 'GET')
|
|
413
|
+
return [];
|
|
414
|
+
// ...or by the POST the scope names that the manifest's `reads` rules a read (an RPC method: Telegram's getChat),
|
|
415
|
+
// whatever schema it answers (getChat's ChatFullInfo, a chat's fuller form, under `result`)
|
|
416
|
+
if (o && !specNamed.has(name) && (manifest.reads ?? []).includes(o.id))
|
|
417
|
+
return [];
|
|
418
|
+
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}`];
|
|
328
419
|
}
|
|
329
420
|
return typeof r.none === 'string' && r.none.trim() ? [] : [`${name}: refresh names no list, no get and no reason`];
|
|
330
421
|
});
|
package/dist/src/gate.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { unpublishedUnitFiles } from "./conformance.js";
|
|
2
|
+
import { hasOwnSurface } from "./lanes.js";
|
|
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
|
-
//
|
|
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.js";
|
|
20
|
+
import { withCheckoutFiles } from "./protocol-3.js";
|
|
19
21
|
import { isPackArtifact } from "./pack-files.js";
|
|
20
22
|
/** Every unit of a pack repository: each pack (a directory with a package.json) and each lane in it (a directory with
|
|
21
23
|
* its own src/manifest.ts and no package.json). */
|
|
@@ -38,7 +40,7 @@ export function unitsOf(repo) {
|
|
|
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, names) {
|
|
@@ -55,6 +57,7 @@ export function laneRootFailures(dir) {
|
|
|
55
57
|
const skip = ignored(dir, stray);
|
|
56
58
|
const outside = [
|
|
57
59
|
...stray.filter((n) => !skip.has(n)),
|
|
60
|
+
...(existsSync(join(dir, 'spec')) ? readdirSync(join(dir, 'spec')).filter((n) => !['sources.json', 'SOURCE.md', 'README.md'].includes(n)).map((n) => `spec/${n}`) : []),
|
|
58
61
|
...(existsSync(join(dir, 'src')) ? readdirSync(join(dir, 'src')).filter((n) => !ROOT_SRC.has(n) && !n.endsWith('.d.ts') && n !== 'semantics').map((n) => `src/${n}`) : []),
|
|
59
62
|
// what the lanes share, and nothing else (architecture, "Pack layout": a vendor whose every API is a lane)
|
|
60
63
|
...(existsSync(join(dir, 'src', 'semantics')) ? readdirSync(join(dir, 'src', 'semantics')).filter((n) => n !== 'shared.ts').map((n) => `src/semantics/${n}`) : []),
|
|
@@ -62,6 +65,7 @@ export function laneRootFailures(dir) {
|
|
|
62
65
|
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` : ''}`] : [];
|
|
63
66
|
// the vendor's front is declared and the kernel serves it: a manifest, and a fetch that is createVendorFetch over it
|
|
64
67
|
const src = (n) => (existsSync(join(dir, 'src', n)) ? readFileSync(join(dir, 'src', n), 'utf8') : '');
|
|
68
|
+
failures.push(...unpublishedUnitFiles(dir).map((f) => `layout: ${f}`));
|
|
65
69
|
if (!src('manifest.ts'))
|
|
66
70
|
failures.push("layout: no src/manifest.ts: a vendor of lanes declares its lanes' routes, discovery and descriptor");
|
|
67
71
|
if (!/\bcreateVendorFetch\(/.test(src('fetch.ts')))
|
|
@@ -93,18 +97,20 @@ export function laneRootFailures(dir) {
|
|
|
93
97
|
}
|
|
94
98
|
/** Each unit's blocking form failures (empty when it may be in the repository). */
|
|
95
99
|
export async function p3Gate(units) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
return withCheckoutFiles(async () => {
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const unit of units) {
|
|
103
|
+
// 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
|
|
104
|
+
// front (architecture, "Pack layout", "Other wires: lanes")
|
|
105
|
+
const lanesOnly = !hasOwnSurface(unit) && readdirSync(unit).some((n) => existsSync(join(unit, n, 'src', 'manifest.ts')));
|
|
106
|
+
if (lanesOnly || !existsSync(join(unit, 'src', 'manifest.ts'))) {
|
|
107
|
+
out.push({ unit, failures: laneRootFailures(unit) });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const grades = await grade(unit, { static: true, sections: ['form'] });
|
|
111
|
+
const form = Object.values(grades).flatMap((g) => g.criteria).filter((c) => !c.pass);
|
|
112
|
+
out.push({ unit, failures: form.map((c) => `${c.id}: ${c.reason}`) });
|
|
104
113
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
out.push({ unit, failures: form.map((c) => `${c.id}: ${c.reason}`) });
|
|
108
|
-
}
|
|
109
|
-
return out;
|
|
114
|
+
return out;
|
|
115
|
+
});
|
|
110
116
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { grade, STANDARDS, type CriterionGrade, type GradeOptions, type StandardGrade } from './grade.js';
|
|
2
|
+
export { conformance, decisionTableProblems, type ConformanceResult, UNIT_FILES, unitFilesMissing } from './conformance.js';
|
|
2
3
|
export { laneRootFailures, p3Gate, unitsOf } from './gate.js';
|
|
3
|
-
export { gatePublished, vendorOfPackage, type PublishedGate } from './published.js';
|
|
4
|
-
export { handlerName, protocol3, PLANNED } from './protocol-3.js';
|
|
5
|
-
export {
|
|
4
|
+
export { conformPublished, gatePublished, vendorOfPackage, type PublishedGate } from './published.js';
|
|
5
|
+
export { handlerName, protocol3, PLANNED, withCheckoutFiles } from './protocol-3.js';
|
|
6
|
+
export { notIndependent, type Verdict } from './verdict.js';
|
|
7
|
+
export { sharingLanesIn, hasOwnSurface } from './lanes.js';
|
|
6
8
|
export { unitAt } from './unit.js';
|
|
7
9
|
export type { Criterion, CriterionResult, GradeUnit, ScoreReport, Standard } from './types.js';
|
package/dist/src/index.js
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
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 } from "./grade.js";
|
|
7
|
+
export { conformance, decisionTableProblems, UNIT_FILES, unitFilesMissing } from "./conformance.js";
|
|
7
8
|
export { laneRootFailures, p3Gate, unitsOf } from "./gate.js";
|
|
8
|
-
export { gatePublished, vendorOfPackage } from "./published.js";
|
|
9
|
-
export { handlerName, protocol3, PLANNED } from "./protocol-3.js";
|
|
10
|
-
export {
|
|
9
|
+
export { conformPublished, gatePublished, vendorOfPackage } from "./published.js";
|
|
10
|
+
export { handlerName, protocol3, PLANNED, withCheckoutFiles } from "./protocol-3.js";
|
|
11
|
+
export { notIndependent } from "./verdict.js";
|
|
12
|
+
export { sharingLanesIn, hasOwnSurface } from "./lanes.js";
|
|
11
13
|
export { unitAt } from "./unit.js";
|
package/dist/src/lanes.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
/** Metadata such as sources.json is not a derivable API of a vendor whose APIs all belong to its lanes. */
|
|
2
|
+
export declare function hasOwnSurface(pkg: string): boolean;
|
|
1
3
|
/** The lanes over a vendor's state in the vendor's directory `pkg`, wherever its repository is. */
|
|
2
4
|
export declare function sharingLanesIn(pkg: string, vendor: string): string[];
|
package/dist/src/lanes.js
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
// walked, measured and graded through the vendor's life; every other lane keeps its own.
|
|
4
4
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
/** Metadata such as sources.json is not a derivable API of a vendor whose APIs all belong to its lanes. */
|
|
7
|
+
export function hasOwnSurface(pkg) {
|
|
8
|
+
if (existsSync(join(pkg, 'src', 'generated', 'surface.gen.json')))
|
|
9
|
+
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
|
+
}
|
|
6
13
|
/** The service a manifest names: the top-level `service:` of its exported manifest object. */
|
|
7
14
|
function serviceOf(manifestPath) {
|
|
8
15
|
const text = readFileSync(manifestPath, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
|
package/dist/src/p3-rules.js
CHANGED
|
@@ -11,8 +11,6 @@ const RULES = [
|
|
|
11
11
|
{ rule: "the twin's URL from the request (use ctx.publicBase)", test: (l) => /request\.url\)\.origin/.test(l) },
|
|
12
12
|
// signing, digests and randomness are the kernel's (ctx.crypto, world-core's exports)
|
|
13
13
|
{ rule: 'node:crypto in a pack (use ctx.crypto or the kernel\'s digest)', test: (l) => /from ['"](node:)?crypto['"]|require\(['"](node:)?crypto['"]\)/.test(l) },
|
|
14
|
-
// a webhook is the manifest's events; ctx.deliver is for a message that is no declared event (a push notification)
|
|
15
|
-
{ rule: 'ctx.deliver where the manifest\'s events belong (or record why)', test: (l) => /ctx\.deliver\(/.test(l) },
|
|
16
14
|
// an object's bytes are ctx.blobs, never a row's field
|
|
17
15
|
{ rule: "bytes kept in a row (use ctx.blobs)", test: (l) => /ctx\.(write|record)\([^;]*toString\(['"]base64['"]\)/.test(l) || /\b_bytes\s*:/.test(l) },
|
|
18
16
|
// a secret the vendor issues is drawn from ctx.secret, never computed from ids
|
package/dist/src/protocol-3.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Standard } from './types.js';
|
|
2
|
+
export declare function withCheckoutFiles<T>(fn: () => T): T;
|
|
2
3
|
export declare const handlerName: (operationId: string) => string;
|
|
3
4
|
/** Named by "The grade" and decided by no tool yet: counted in no grade until a version of this standard checks them. */
|
|
4
5
|
export declare const PLANNED: string[];
|
package/dist/src/protocol-3.js
CHANGED
|
@@ -12,7 +12,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
12
12
|
// Version 1 checks what the repository's tools can decide today. Criteria the grade section names that no tool
|
|
13
13
|
// decides yet (a credential stored as issued; hollow capabilities; the invariants row) are listed in PLANNED and
|
|
14
14
|
// counted in no grade until a version of this standard checks them.
|
|
15
|
+
import { unpublishedUnitFiles } from "./conformance.js";
|
|
16
|
+
import { attributionName, notIndependent } from "./verdict.js";
|
|
15
17
|
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
16
19
|
import { createHash } from 'node:crypto';
|
|
17
20
|
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
18
21
|
import { tmpdir } from 'node:os';
|
|
@@ -50,31 +53,57 @@ catch {
|
|
|
50
53
|
} };
|
|
51
54
|
/** What spec/ holds beside the vendored documents: provenance, corrections, the grouping, the examples index, recordings. */
|
|
52
55
|
const SPEC_METADATA = /^(SOURCE\.md|sources\.json|patches\.json|grouping\.json|doc-examples\.json|README\.md|[^/]*\.sha256|recordings\/.*)$/;
|
|
53
|
-
/** A tool in spec/ is not a vendored document
|
|
56
|
+
/** A tool in spec/ is not a vendored document: never hashed, and outside the layout unless it is the script that wrote
|
|
57
|
+
* the document from the vendor's reference page, which SOURCE.md names (architecture, "Creating a pack", step 2). */
|
|
54
58
|
const SPEC_TOOL = /\.(py|sh|ts|js|mjs|cjs|rb)$/;
|
|
59
|
+
/** spec/'s tools SOURCE.md names: the writers of a document a vendor publishes only as a page. */
|
|
60
|
+
function namedSpecTools(unit) {
|
|
61
|
+
const path = join(unit.dir, 'spec', 'SOURCE.md');
|
|
62
|
+
const source = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
63
|
+
return new Set(filesUnder(join(unit.dir, 'spec')).filter((f) => SPEC_TOOL.test(f) && !f.includes('/') && source.includes(`\`${f}\``)));
|
|
64
|
+
}
|
|
55
65
|
/** The vendored documents: every file under spec/ that is not its metadata. */
|
|
56
66
|
function specFiles(unit) {
|
|
57
67
|
const dir = join(unit.dir, 'spec');
|
|
58
68
|
return filesUnder(dir).filter((f) => !SPEC_METADATA.test(f) && !SPEC_TOOL.test(f)).map((f) => join(dir, f));
|
|
59
69
|
}
|
|
60
|
-
/**
|
|
70
|
+
/** The checkout a directory is in: the nearest ancestor holding `.git` (a directory, or a worktree's file). */
|
|
71
|
+
function checkoutOf(dir) {
|
|
72
|
+
for (let d = dir;; d = dirname(d)) {
|
|
73
|
+
if (existsSync(join(d, '.git')))
|
|
74
|
+
return d;
|
|
75
|
+
if (dirname(d) === d)
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Grading fans out over a checkout's directories. A caller explicitly owns the immutable listing for one pass;
|
|
80
|
+
// nested gates share it, while concurrent passes have separate snapshots and direct checks always read git afresh.
|
|
81
|
+
const checkoutFiles = new AsyncLocalStorage();
|
|
82
|
+
export function withCheckoutFiles(fn) {
|
|
83
|
+
return checkoutFiles.getStore() ? fn() : checkoutFiles.run(new Map(), fn);
|
|
84
|
+
}
|
|
85
|
+
/** Files relative to a directory: Git's tree in a checkout (one listing per checkout per pass), the disk tree of an
|
|
86
|
+
* unpacked package outside one. A failed Git listing refuses; it never reads as an empty tree. */
|
|
61
87
|
function filesUnder(dir, base = dir) {
|
|
62
88
|
if (!existsSync(dir))
|
|
63
89
|
return [];
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
const top = checkoutOf(dir);
|
|
91
|
+
if (!top)
|
|
92
|
+
return allFilesUnder(dir, base, true);
|
|
93
|
+
const listing = checkoutFiles.getStore();
|
|
94
|
+
let files = listing?.get(top);
|
|
95
|
+
if (!files) {
|
|
96
|
+
// tracked and not-yet-added files alike (T0 runs before the commit); ignored runtime state (.volter) never
|
|
97
|
+
const run = spawnSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '.'], { cwd: top, stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 256 * 1024 * 1024 });
|
|
98
|
+
if (run.status !== 0 || run.error) {
|
|
99
|
+
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 ?? ''))}`);
|
|
100
|
+
}
|
|
101
|
+
files = [...new Set(String(run.stdout).split('\0').filter(Boolean))];
|
|
102
|
+
listing?.set(top, files);
|
|
75
103
|
}
|
|
76
|
-
const
|
|
77
|
-
|
|
104
|
+
const under = relative(top, dir);
|
|
105
|
+
const mine = under === '' ? files : files.filter((f) => f.startsWith(`${under}/`)).map((f) => f.slice(under.length + 1));
|
|
106
|
+
return mine.map((f) => relative(base, join(dir, f))).filter((f) => existsSync(join(base, f)));
|
|
78
107
|
}
|
|
79
108
|
/** Disk files outside a checkout, or the complete output of the grader's own derivation. */
|
|
80
109
|
function allFilesUnder(dir, base = dir, excludeArtifacts = false) {
|
|
@@ -110,15 +139,28 @@ const isLane = (unit) => unit.name.includes('/');
|
|
|
110
139
|
function srcFileInForm(unit, name) {
|
|
111
140
|
if (name.endsWith('.d.ts'))
|
|
112
141
|
return true; // the types of a vendor SDK the pack's tests drive
|
|
113
|
-
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);
|
|
142
|
+
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);
|
|
114
143
|
}
|
|
115
144
|
/** The fixed files of semantics/ besides the families' handler files and their tests. */
|
|
116
|
-
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']);
|
|
145
|
+
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']);
|
|
117
146
|
/** journeys/'s closed set. */
|
|
118
147
|
const JOURNEY_FILES = new Set(['customer-life.outline.md', 'customer-life.outline.verdict.json', 'customer-life.json', 'customer-life.verdict.json', 'decisions.json',
|
|
119
148
|
'vendor-examples.json', 'vendor-examples.verdict.json', 'demand.json', 'unreachable.json', 'score.json', 'coverage.json',
|
|
120
149
|
// a generative pack's life names the World's scenario it walks under (architecture, "Writing a life")
|
|
121
150
|
'scenario.json']);
|
|
151
|
+
/** Only scenario basenames the two walks explicitly declare extend the closed layout. */
|
|
152
|
+
function journeyFiles(unit) {
|
|
153
|
+
const files = new Set(JOURNEY_FILES);
|
|
154
|
+
for (const name of ['customer-life.json', 'vendor-examples.json']) {
|
|
155
|
+
try {
|
|
156
|
+
const journey = JSON.parse(readFileSync(join(unit.dir, 'journeys', name), 'utf8'));
|
|
157
|
+
if (typeof journey.scenario === 'string' && /^[a-z0-9][a-z0-9.-]*\.json$/i.test(journey.scenario))
|
|
158
|
+
files.add(journey.scenario);
|
|
159
|
+
}
|
|
160
|
+
catch { /* malformed or absent journeys get no additional files */ }
|
|
161
|
+
}
|
|
162
|
+
return files;
|
|
163
|
+
}
|
|
122
164
|
/** Entries at the unit's root the form does not name (a vendor's lanes are units of their own). */
|
|
123
165
|
function rootOutsideForm(unit) {
|
|
124
166
|
return entriesOf(unit.dir).filter((name) => {
|
|
@@ -157,7 +199,7 @@ const FORBIDDEN_REACH = [
|
|
|
157
199
|
[/\bprocess\.env\b|\bBun\.env\b|\bDeno\.env\b/, 'the environment'],
|
|
158
200
|
[/(^|[^\w])fetch\s*\(|\bXMLHttpRequest\b|\bnode:(net|http|https|tls|dgram)\b/m, 'the network'],
|
|
159
201
|
[/\bimport\s*\(|\brequire\s*\(/, 'a module loaded at run time'],
|
|
160
|
-
[/\bctx\.(now|core|atomically|tree|writeDetailed)\b|\}\s*=\s*ctx\b|\bctx\s*\[/, 'a context member outside the contract, or the context taken apart'],
|
|
202
|
+
[/\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'],
|
|
161
203
|
[/\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)"],
|
|
162
204
|
];
|
|
163
205
|
/** A handler's name for an operationId: the id with every character a JavaScript name cannot hold made `_`, and a
|
|
@@ -228,7 +270,7 @@ const criteria = [
|
|
|
228
270
|
const patches = doc?.patches;
|
|
229
271
|
if (!Array.isArray(patches))
|
|
230
272
|
return fail('spec/patches.json is not { patches: [...] }');
|
|
231
|
-
const bare = patches.filter((p) => !p.why || !(p.source?.url || p.source?.spec || /https?:\/\//.test(p.why))).length;
|
|
273
|
+
const bare = patches.filter((p) => !p.why || !(p.source?.url || p.source?.spec || p.source?.recording || /https?:\/\//.test(p.why))).length;
|
|
232
274
|
return bare ? fail(`${bare} of ${patches.length} corrections lack a reason or a source`) : pass(`${patches.length} corrections, each sourced`);
|
|
233
275
|
},
|
|
234
276
|
},
|
|
@@ -274,7 +316,8 @@ const criteria = [
|
|
|
274
316
|
const families = new Set(operationsOf(unit).values());
|
|
275
317
|
const outside = [
|
|
276
318
|
...rootOutsideForm(unit),
|
|
277
|
-
...
|
|
319
|
+
...unpublishedUnitFiles(unit.dir),
|
|
320
|
+
...filesUnder(join(unit.dir, 'spec')).filter((f) => SPEC_TOOL.test(f) && !namedSpecTools(unit).has(f)).map((f) => `spec/${f}`),
|
|
278
321
|
...entriesOf(src).filter((name) => statSync(join(src, name)).isDirectory() ? !LAYOUT_DIRS.has(name) : !srcFileInForm(unit, name)).map((n) => `src/${n}`),
|
|
279
322
|
...filesUnder(join(src, 'semantics')).filter((f) => {
|
|
280
323
|
if (f.includes('/'))
|
|
@@ -283,7 +326,7 @@ const criteria = [
|
|
|
283
326
|
return !(SEMANTICS_FIXED.has(f) || SEMANTICS_FIXED.has(`${stem}.ts`) || (families.has(stem.replace(/-ops$/, '')) && /\.(test\.)?ts$/.test(f)));
|
|
284
327
|
}).map((f) => `src/semantics/${f}`),
|
|
285
328
|
// a lane over its vendor's state has no journeys of its own: the vendor's life walks it ("Pack layout")
|
|
286
|
-
...filesUnder(join(unit.dir, 'journeys')).filter((f) => vendorLanes(unit).length > 0 || !
|
|
329
|
+
...filesUnder(join(unit.dir, 'journeys')).filter((f) => vendorLanes(unit).length > 0 || !journeyFiles(unit).has(f)).map((f) => `journeys/${f}`),
|
|
287
330
|
];
|
|
288
331
|
// screens/: one file per screen the manifest declares, named by its id
|
|
289
332
|
if (existsSync(join(src, 'screens'))) {
|
|
@@ -366,6 +409,12 @@ const criteria = [
|
|
|
366
409
|
found.push(`clock.ts exports ${names.join(', ') || 'nothing'}, not only clock`);
|
|
367
410
|
continue;
|
|
368
411
|
}
|
|
412
|
+
if (f === 'answer-views.ts') {
|
|
413
|
+
const names = [...text.matchAll(/^export\s+const\s+([A-Za-z0-9_$]+)/gm)].map((m) => m[1]);
|
|
414
|
+
if (names.join() !== 'answerViews' || /^export\s+(?:async\s+)?function\s|^export\s+(let|var|class)\s|^export\s*\{/m.test(text))
|
|
415
|
+
found.push(`answer-views.ts exports ${names.join(', ') || 'nothing'}, not only answerViews`);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
369
418
|
if (f === 'board.ts') {
|
|
370
419
|
const names = [...text.matchAll(/^export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)/gm)].map((m) => m[1]);
|
|
371
420
|
if (names.join() !== 'board' || /^export\s+(const|let|var|class)\s|^export\s*\{/m.test(text))
|
|
@@ -618,18 +667,15 @@ const criteria = [
|
|
|
618
667
|
const verdict = readJson(join(journeys, 'customer-life.outline.verdict.json'));
|
|
619
668
|
if (!existsSync(outline))
|
|
620
669
|
return fail('no journeys/customer-life.outline.md');
|
|
670
|
+
const author = /^Author:\s*(.+?)\.?\s*$/m.exec(readFileSync(outline, 'utf8'))?.[1];
|
|
671
|
+
attributionName(author);
|
|
621
672
|
if (!verdict)
|
|
622
673
|
return fail('no outline verdict');
|
|
623
674
|
if (!verdict.plausible)
|
|
624
675
|
return fail('judged not plausible');
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
return fail('the verdict does not name its judge (judge.agent)');
|
|
629
|
-
if (!verdict.authors?.length)
|
|
630
|
-
return fail('the verdict does not name the authors, so the judge is not shown to be another session');
|
|
631
|
-
if (verdict.authors.includes(verdict.judge.agent))
|
|
632
|
-
return fail('the judge is among the authors');
|
|
676
|
+
const dependent = notIndependent(verdict);
|
|
677
|
+
if (dependent)
|
|
678
|
+
return fail(dependent);
|
|
633
679
|
return pass(`plausible, judge ${verdict.judge.agent}`);
|
|
634
680
|
},
|
|
635
681
|
},
|
|
@@ -640,7 +686,7 @@ const criteria = [
|
|
|
640
686
|
const r = report(unit);
|
|
641
687
|
if (!r)
|
|
642
688
|
return fail(noReport(unit));
|
|
643
|
-
return r.life.verdict === 'plausible' ? pass(`${r.life.steps} steps, judge ${r.life.judge}`) : fail(`verdict: ${r.life.verdict}`);
|
|
689
|
+
return r.life.steps > 0 && r.life.verdict === 'plausible' ? pass(`${r.life.steps} steps, judge ${r.life.judge}`) : fail(`verdict: ${r.life.verdict}`);
|
|
644
690
|
},
|
|
645
691
|
},
|
|
646
692
|
{
|
package/dist/src/published.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ConformanceResult } from './conformance.js';
|
|
1
2
|
export type PublishedGate = {
|
|
2
3
|
spec: string;
|
|
3
4
|
failures: Array<{
|
|
@@ -7,6 +8,11 @@ export type PublishedGate = {
|
|
|
7
8
|
};
|
|
8
9
|
/** The vendor a package twins: its name without its scope and the `twin-` prefix (`@volter/twin-supabase` is supabase). */
|
|
9
10
|
export declare const vendorOfPackage: (name: string) => string;
|
|
11
|
+
/** The conformance of `spec` as the registry serves it: the published tarball with its own dependencies installed,
|
|
12
|
+
* served and read against the kernel (kernelDir). */
|
|
13
|
+
export declare function conformPublished(spec: string, opts?: {
|
|
14
|
+
registry?: string;
|
|
15
|
+
}): Promise<ConformanceResult[]>;
|
|
10
16
|
/** The gate over `spec` (`<name>@<version>`) as the registry serves it; throws when it cannot be fetched or unpacked. */
|
|
11
17
|
export declare function gatePublished(spec: string, opts?: {
|
|
12
18
|
registry?: string;
|