saturndocs 0.1.0
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/LICENSE +21 -0
- package/dist/auth-FRCFGNYT.js +14 -0
- package/dist/brokenLinks-TTOBP4P7.js +48 -0
- package/dist/build-F5PWGP57.js +39 -0
- package/dist/chunk-3P2OFTXR.js +71 -0
- package/dist/chunk-3V753MRI.js +100 -0
- package/dist/chunk-5KX4ZK5E.js +104 -0
- package/dist/chunk-CMNX5DLQ.js +119 -0
- package/dist/chunk-GYRVGWDM.js +3499 -0
- package/dist/chunk-I6U4V7B4.js +1552 -0
- package/dist/chunk-J6P3VGGE.js +21 -0
- package/dist/chunk-KGJESHJC.js +37 -0
- package/dist/chunk-M2BZEDAS.js +57 -0
- package/dist/chunk-N7QPZMLP.js +82 -0
- package/dist/chunk-OEAEX5YW.js +51 -0
- package/dist/chunk-OZWTXMO3.js +584 -0
- package/dist/chunk-QLFODLVN.js +46 -0
- package/dist/chunk-R3X2DPFF.js +568 -0
- package/dist/chunk-S45UWCML.js +588 -0
- package/dist/chunk-SABK3R2P.js +2452 -0
- package/dist/chunk-STCCGFKC.js +246 -0
- package/dist/chunk-TKISMN3P.js +39 -0
- package/dist/chunk-U7PKMSB3.js +465 -0
- package/dist/chunk-VNYDYHXM.js +22 -0
- package/dist/chunk-VXEUNRVA.js +26 -0
- package/dist/chunk-XO4CUC7V.js +99 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +95 -0
- package/dist/deploy-QU7DCKWU.js +11 -0
- package/dist/dev-VDSDCBO2.js +43 -0
- package/dist/index.d.ts +382 -0
- package/dist/index.js +191 -0
- package/dist/init-7T5TEPBH.js +14 -0
- package/dist/manage-MKH27U3B.js +12 -0
- package/dist/openapiCheck-KGBQJTIQ.js +66 -0
- package/dist/pages-HIPXRLSY.js +15 -0
- package/dist/read-DMSJUEDA.js +160 -0
- package/dist/read-ZEVUZNNB.js +146 -0
- package/dist/requests-H5MIGBIS.js +13 -0
- package/dist/schema-XJUEZSIW.js +13 -0
- package/dist/sites-I4WI2MPG.js +14 -0
- package/dist/status-7R3NOI5H.js +14 -0
- package/dist/suggestions-4WFQ3APX.js +739 -0
- package/dist/validate-VXL4PL42.js +30 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1552 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseOpenApi
|
|
3
|
+
} from "./chunk-S45UWCML.js";
|
|
4
|
+
import {
|
|
5
|
+
navigationGroups,
|
|
6
|
+
parseDocsConfig,
|
|
7
|
+
parseModelsConfig
|
|
8
|
+
} from "./chunk-R3X2DPFF.js";
|
|
9
|
+
import {
|
|
10
|
+
messageOf
|
|
11
|
+
} from "./chunk-VNYDYHXM.js";
|
|
12
|
+
|
|
13
|
+
// src/commands/deploy.ts
|
|
14
|
+
import { randomUUID } from "crypto";
|
|
15
|
+
import { createReadStream as createReadStream2 } from "fs";
|
|
16
|
+
import pc from "picocolors";
|
|
17
|
+
|
|
18
|
+
// src/lib/bundle.ts
|
|
19
|
+
import { createHash } from "crypto";
|
|
20
|
+
import { createReadStream, createWriteStream } from "fs";
|
|
21
|
+
import { lstat, mkdir, mkdtemp, readFile, readdir, rm, stat } from "fs/promises";
|
|
22
|
+
import { tmpdir } from "os";
|
|
23
|
+
import { join, posix, resolve } from "path";
|
|
24
|
+
import { Readable } from "stream";
|
|
25
|
+
import { pipeline } from "stream/promises";
|
|
26
|
+
import { createGzip } from "zlib";
|
|
27
|
+
|
|
28
|
+
// ../source-abi/dist/index.js
|
|
29
|
+
import { parse, parseDocument } from "yaml";
|
|
30
|
+
import { unified } from "unified";
|
|
31
|
+
import remarkGfm from "remark-gfm";
|
|
32
|
+
import remarkMdx from "remark-mdx";
|
|
33
|
+
import remarkParse from "remark-parse";
|
|
34
|
+
var SOURCE_ABI_VERSION = 3;
|
|
35
|
+
var SOURCE_LIMITS = Object.freeze({
|
|
36
|
+
maxFileBytes: 26214400,
|
|
37
|
+
maxTotalBytes: 209715200,
|
|
38
|
+
maxFileCount: 5e3,
|
|
39
|
+
maxPageCount: 2e3,
|
|
40
|
+
maxPathDepth: 12,
|
|
41
|
+
maxPathBytes: 512,
|
|
42
|
+
maxSegmentBytes: 255,
|
|
43
|
+
maxDocsJsonBytes: 1048576,
|
|
44
|
+
maxModelsTomlBytes: 65536,
|
|
45
|
+
maxOpenApiBytes: 4194304,
|
|
46
|
+
maxFrontmatterBytes: 65536,
|
|
47
|
+
maxThemeCssBytes: 262144
|
|
48
|
+
});
|
|
49
|
+
var SOURCE_ALLOWED_ROOTS = Object.freeze([
|
|
50
|
+
"docs.json",
|
|
51
|
+
"models.toml",
|
|
52
|
+
"pages/**",
|
|
53
|
+
"public/**",
|
|
54
|
+
"openapi/**"
|
|
55
|
+
]);
|
|
56
|
+
var SOURCE_ALLOWED_EXTENSIONS = Object.freeze({
|
|
57
|
+
root: Object.freeze([".json", ".toml"]),
|
|
58
|
+
pages: Object.freeze([".md", ".mdx"]),
|
|
59
|
+
openapi: Object.freeze([".json", ".yaml", ".yml"]),
|
|
60
|
+
public: Object.freeze([
|
|
61
|
+
".css",
|
|
62
|
+
".png",
|
|
63
|
+
".jpg",
|
|
64
|
+
".jpeg",
|
|
65
|
+
".gif",
|
|
66
|
+
".webp",
|
|
67
|
+
".avif",
|
|
68
|
+
".svg",
|
|
69
|
+
".ico",
|
|
70
|
+
".woff",
|
|
71
|
+
".woff2",
|
|
72
|
+
".ttf",
|
|
73
|
+
".otf",
|
|
74
|
+
".eot",
|
|
75
|
+
".mp4",
|
|
76
|
+
".webm",
|
|
77
|
+
".mp3",
|
|
78
|
+
".wav",
|
|
79
|
+
".ogg",
|
|
80
|
+
".oga",
|
|
81
|
+
".ogv",
|
|
82
|
+
".pdf",
|
|
83
|
+
".txt",
|
|
84
|
+
".csv"
|
|
85
|
+
])
|
|
86
|
+
});
|
|
87
|
+
var RESERVED_URL_PREFIXES = Object.freeze([
|
|
88
|
+
"/api",
|
|
89
|
+
"/api/*",
|
|
90
|
+
"/auth",
|
|
91
|
+
"/auth/*",
|
|
92
|
+
"/mcp",
|
|
93
|
+
"/ask",
|
|
94
|
+
"/runtime",
|
|
95
|
+
"/runtime/*"
|
|
96
|
+
]);
|
|
97
|
+
var REJECTED_GENERATED_ROOTS = Object.freeze([
|
|
98
|
+
"dist",
|
|
99
|
+
"pagefind",
|
|
100
|
+
"_astro",
|
|
101
|
+
"worker/generated",
|
|
102
|
+
"node_modules",
|
|
103
|
+
".git"
|
|
104
|
+
]);
|
|
105
|
+
var REJECTED_GENERATED_FILENAMES = Object.freeze([
|
|
106
|
+
"llms.txt",
|
|
107
|
+
"llms-full.txt",
|
|
108
|
+
"robots.txt",
|
|
109
|
+
"sitemap*",
|
|
110
|
+
"*auth-manifest*.json"
|
|
111
|
+
]);
|
|
112
|
+
var SOURCE_MEDIA_TYPES = Object.freeze({
|
|
113
|
+
".avif": "image/avif",
|
|
114
|
+
".csv": "text/csv",
|
|
115
|
+
".css": "text/css",
|
|
116
|
+
".eot": "application/vnd.ms-fontobject",
|
|
117
|
+
".gif": "image/gif",
|
|
118
|
+
".ico": "image/x-icon",
|
|
119
|
+
".jpeg": "image/jpeg",
|
|
120
|
+
".jpg": "image/jpeg",
|
|
121
|
+
".json": "application/json",
|
|
122
|
+
".md": "text/markdown",
|
|
123
|
+
".mdx": "text/mdx",
|
|
124
|
+
".mp3": "audio/mpeg",
|
|
125
|
+
".mp4": "video/mp4",
|
|
126
|
+
".oga": "audio/ogg",
|
|
127
|
+
".ogg": "audio/ogg",
|
|
128
|
+
".ogv": "video/ogg",
|
|
129
|
+
".otf": "font/otf",
|
|
130
|
+
".pdf": "application/pdf",
|
|
131
|
+
".png": "image/png",
|
|
132
|
+
".svg": "image/svg+xml",
|
|
133
|
+
".toml": "application/toml",
|
|
134
|
+
".ttf": "font/ttf",
|
|
135
|
+
".txt": "text/plain",
|
|
136
|
+
".wav": "audio/wav",
|
|
137
|
+
".webm": "video/webm",
|
|
138
|
+
".webp": "image/webp",
|
|
139
|
+
".woff": "font/woff",
|
|
140
|
+
".woff2": "font/woff2",
|
|
141
|
+
".yaml": "application/yaml",
|
|
142
|
+
".yml": "application/yaml"
|
|
143
|
+
});
|
|
144
|
+
var textEncoder = new TextEncoder();
|
|
145
|
+
var WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
|
|
146
|
+
var ASCII_SOURCE_PATH = /^[A-Za-z0-9._/-]+$/;
|
|
147
|
+
var ASCII_SOURCE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
148
|
+
var PAGE_EXTENSIONS = new Set(SOURCE_ALLOWED_EXTENSIONS.pages);
|
|
149
|
+
var PUBLIC_EXTENSIONS = new Set(SOURCE_ALLOWED_EXTENSIONS.public);
|
|
150
|
+
var OPENAPI_EXTENSIONS = new Set(SOURCE_ALLOWED_EXTENSIONS.openapi);
|
|
151
|
+
function sourceExtension(path) {
|
|
152
|
+
const filename = path.slice(path.lastIndexOf("/") + 1);
|
|
153
|
+
const dot = filename.lastIndexOf(".");
|
|
154
|
+
return dot <= 0 ? null : filename.slice(dot).toLowerCase();
|
|
155
|
+
}
|
|
156
|
+
function inspectSourcePath(raw) {
|
|
157
|
+
const normalized = raw;
|
|
158
|
+
const segments = normalized.split("/");
|
|
159
|
+
const tooDeep = segments.length > SOURCE_LIMITS.maxPathDepth;
|
|
160
|
+
const tooLong = textEncoder.encode(normalized).byteLength > SOURCE_LIMITS.maxPathBytes;
|
|
161
|
+
const segmentTooLong = segments.some(
|
|
162
|
+
(segment) => textEncoder.encode(segment).byteLength > SOURCE_LIMITS.maxSegmentBytes
|
|
163
|
+
);
|
|
164
|
+
let syntaxValid = normalized.length > 0;
|
|
165
|
+
if (normalized.startsWith("/") || normalized.endsWith("/") || !ASCII_SOURCE_PATH.test(normalized)) {
|
|
166
|
+
syntaxValid = false;
|
|
167
|
+
}
|
|
168
|
+
for (const segment of segments) {
|
|
169
|
+
if (segment.length === 0 || !ASCII_SOURCE_SEGMENT.test(segment) || segment === "." || segment === ".." || WINDOWS_DEVICE_NAME.test(segment) || segment.endsWith(".")) {
|
|
170
|
+
syntaxValid = false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { normalized, segments, syntaxValid, tooDeep, tooLong, segmentTooLong };
|
|
174
|
+
}
|
|
175
|
+
function isKnownSourceRoot(path) {
|
|
176
|
+
if (path === "docs.json" || path === "models.toml") return true;
|
|
177
|
+
return path.startsWith("pages/") || path.startsWith("public/") || path.startsWith("openapi/");
|
|
178
|
+
}
|
|
179
|
+
function isAllowedSourceLocation(path) {
|
|
180
|
+
const extension = sourceExtension(path);
|
|
181
|
+
if (path === "docs.json") return extension === ".json";
|
|
182
|
+
if (path === "models.toml") return extension === ".toml";
|
|
183
|
+
if (path.startsWith("pages/")) {
|
|
184
|
+
return extension !== null && PAGE_EXTENSIONS.has(extension);
|
|
185
|
+
}
|
|
186
|
+
if (path.startsWith("public/")) {
|
|
187
|
+
if (extension === ".css") return path === "public/theme.css";
|
|
188
|
+
return extension !== null && PUBLIC_EXTENSIONS.has(extension);
|
|
189
|
+
}
|
|
190
|
+
if (path.startsWith("openapi/")) {
|
|
191
|
+
return extension !== null && OPENAPI_EXTENSIONS.has(extension);
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
function isStaticallyGeneratedArtifact(path) {
|
|
196
|
+
const lower = path.toLowerCase();
|
|
197
|
+
const filename = lower.slice(lower.lastIndexOf("/") + 1);
|
|
198
|
+
if (lower === "dist" || lower.startsWith("dist/") || lower === "pagefind" || lower.startsWith("pagefind/") || lower === "_astro" || lower.startsWith("_astro/") || lower === "node_modules" || lower.startsWith("node_modules/") || lower === ".git" || lower.startsWith(".git/") || lower === "worker/generated" || lower.startsWith("worker/generated/")) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
return filename === "llms.txt" || filename === "llms-full.txt" || filename === "robots.txt" || filename.startsWith("sitemap") || filename.endsWith(".json") && filename.includes("auth-manifest");
|
|
202
|
+
}
|
|
203
|
+
function canonicalizeSourcePath(raw) {
|
|
204
|
+
const inspected = inspectSourcePath(raw);
|
|
205
|
+
if (!inspected.syntaxValid || inspected.tooDeep || inspected.tooLong || inspected.segmentTooLong || !isAllowedSourceLocation(inspected.normalized) || isStaticallyGeneratedArtifact(inspected.normalized)) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
return inspected.normalized;
|
|
209
|
+
}
|
|
210
|
+
function detectSourceMediaType(path) {
|
|
211
|
+
const extension = sourceExtension(path);
|
|
212
|
+
if (extension === null) return null;
|
|
213
|
+
return SOURCE_MEDIA_TYPES[extension] ?? null;
|
|
214
|
+
}
|
|
215
|
+
function unicodeCaseFold(value) {
|
|
216
|
+
return value.toLowerCase();
|
|
217
|
+
}
|
|
218
|
+
var textEncoder2 = new TextEncoder();
|
|
219
|
+
function compareCodePoints2(left, right) {
|
|
220
|
+
const leftPoints = Array.from(left, (point) => point.codePointAt(0));
|
|
221
|
+
const rightPoints = Array.from(right, (point) => point.codePointAt(0));
|
|
222
|
+
const count = Math.min(leftPoints.length, rightPoints.length);
|
|
223
|
+
for (let index = 0; index < count; index += 1) {
|
|
224
|
+
const difference = leftPoints[index] - rightPoints[index];
|
|
225
|
+
if (difference !== 0) return difference;
|
|
226
|
+
}
|
|
227
|
+
return leftPoints.length - rightPoints.length;
|
|
228
|
+
}
|
|
229
|
+
function sortViolations(violations) {
|
|
230
|
+
return violations.sort((left, right) => {
|
|
231
|
+
if (left.path === null && right.path !== null) return -1;
|
|
232
|
+
if (left.path !== null && right.path === null) return 1;
|
|
233
|
+
const pathOrder = compareCodePoints2(left.path ?? "", right.path ?? "");
|
|
234
|
+
if (pathOrder !== 0) return pathOrder;
|
|
235
|
+
const codeOrder = compareCodePoints2(left.code, right.code);
|
|
236
|
+
if (codeOrder !== 0) return codeOrder;
|
|
237
|
+
return compareCodePoints2(left.message, right.message);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function isRootSpecificTypeInvalid(path) {
|
|
241
|
+
return (path.startsWith("pages/") || path.startsWith("public/") || path.startsWith("openapi/") || path === "docs.json" || path === "models.toml") && !isAllowedSourceLocation(path);
|
|
242
|
+
}
|
|
243
|
+
function isReservedRoute(path) {
|
|
244
|
+
return RESERVED_URL_PREFIXES.some((pattern) => {
|
|
245
|
+
if (pattern.endsWith("/*")) {
|
|
246
|
+
return path.startsWith(pattern.slice(0, -1));
|
|
247
|
+
}
|
|
248
|
+
return path === pattern;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function servedRoute(path) {
|
|
252
|
+
if (path.startsWith("pages/")) {
|
|
253
|
+
const extension = sourceExtension(path);
|
|
254
|
+
if (extension !== ".md" && extension !== ".mdx") return null;
|
|
255
|
+
const logical = path.slice("pages/".length, -extension.length);
|
|
256
|
+
return logical === "index" ? "/" : `/${logical}`;
|
|
257
|
+
}
|
|
258
|
+
if (path.startsWith("public/")) {
|
|
259
|
+
return `/${path.slice("public/".length)}`;
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
function pageTwinPaths(paths) {
|
|
264
|
+
const pages = /* @__PURE__ */ new Set();
|
|
265
|
+
for (const path of paths) {
|
|
266
|
+
if (!path.startsWith("pages/")) continue;
|
|
267
|
+
const extension = sourceExtension(path);
|
|
268
|
+
if (extension === ".md" || extension === ".mdx") {
|
|
269
|
+
pages.add(path.slice("pages/".length, -extension.length));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const twins = /* @__PURE__ */ new Set();
|
|
273
|
+
for (const path of paths) {
|
|
274
|
+
if (path.startsWith("pages/") || sourceExtension(path) !== ".md") continue;
|
|
275
|
+
if (pages.has(path.slice(0, -3))) twins.add(path);
|
|
276
|
+
}
|
|
277
|
+
return twins;
|
|
278
|
+
}
|
|
279
|
+
function validateBundleStructure(files) {
|
|
280
|
+
const violations = [];
|
|
281
|
+
const inspected = files.map((file) => ({ file, path: inspectSourcePath(file.path) }));
|
|
282
|
+
const normalizedPaths = inspected.filter((item) => item.path.syntaxValid).map((item) => item.path.normalized);
|
|
283
|
+
const generatedTwins = pageTwinPaths(normalizedPaths);
|
|
284
|
+
if (files.length > SOURCE_LIMITS.maxFileCount) {
|
|
285
|
+
violations.push({
|
|
286
|
+
path: null,
|
|
287
|
+
code: "too_many_files",
|
|
288
|
+
severity: "error",
|
|
289
|
+
message: `bundle has ${files.length} files; maximum is ${SOURCE_LIMITS.maxFileCount}`
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
let totalSize = 0;
|
|
293
|
+
let pageCount = 0;
|
|
294
|
+
let hasDocsJson = false;
|
|
295
|
+
for (const { file, path } of inspected) {
|
|
296
|
+
const sizeValid = Number.isSafeInteger(file.size) && file.size >= 0;
|
|
297
|
+
totalSize += sizeValid ? file.size : 0;
|
|
298
|
+
const displayPath = path.syntaxValid ? path.normalized : file.path;
|
|
299
|
+
if (!sizeValid) {
|
|
300
|
+
violations.push({
|
|
301
|
+
path: displayPath,
|
|
302
|
+
code: "invalid_size",
|
|
303
|
+
severity: "error",
|
|
304
|
+
message: "source file size must be a non-negative safe integer"
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
if (!path.syntaxValid || path.tooLong || path.segmentTooLong) {
|
|
308
|
+
violations.push({
|
|
309
|
+
path: displayPath,
|
|
310
|
+
code: "invalid_path",
|
|
311
|
+
severity: "error",
|
|
312
|
+
message: "source path is not a valid canonical logical path"
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
if (path.tooDeep) {
|
|
316
|
+
violations.push({
|
|
317
|
+
path: displayPath,
|
|
318
|
+
code: "path_too_deep",
|
|
319
|
+
severity: "error",
|
|
320
|
+
message: `source path has ${path.segments.length} segments; maximum is ${SOURCE_LIMITS.maxPathDepth}`
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (sizeValid && file.size > SOURCE_LIMITS.maxFileBytes) {
|
|
324
|
+
violations.push({
|
|
325
|
+
path: displayPath,
|
|
326
|
+
code: "file_too_large",
|
|
327
|
+
severity: "error",
|
|
328
|
+
message: `source file is ${file.size} bytes; maximum is ${SOURCE_LIMITS.maxFileBytes}`
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
if (sizeValid && path.syntaxValid && path.normalized === "public/theme.css" && file.size > SOURCE_LIMITS.maxThemeCssBytes) {
|
|
332
|
+
violations.push({
|
|
333
|
+
path: path.normalized,
|
|
334
|
+
code: "theme_css_too_large",
|
|
335
|
+
severity: "error",
|
|
336
|
+
message: `public/theme.css is ${file.size} bytes; maximum is ${SOURCE_LIMITS.maxThemeCssBytes}`
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (!path.syntaxValid) continue;
|
|
340
|
+
const normalized = path.normalized;
|
|
341
|
+
if (normalized === "docs.json" && !path.tooLong && !path.segmentTooLong && isAllowedSourceLocation(normalized) && !isStaticallyGeneratedArtifact(normalized)) {
|
|
342
|
+
hasDocsJson = true;
|
|
343
|
+
}
|
|
344
|
+
const mediaType = detectSourceMediaType(normalized);
|
|
345
|
+
if (mediaType === null || isRootSpecificTypeInvalid(normalized)) {
|
|
346
|
+
violations.push({
|
|
347
|
+
path: normalized,
|
|
348
|
+
code: "inadmissible_type",
|
|
349
|
+
severity: "error",
|
|
350
|
+
message: "file extension is not admissible at this source location"
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if (!isKnownSourceRoot(normalized)) {
|
|
354
|
+
violations.push({
|
|
355
|
+
path: normalized,
|
|
356
|
+
code: "unknown_root",
|
|
357
|
+
severity: "error",
|
|
358
|
+
message: "source path is not under an allowed source root"
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
if (isStaticallyGeneratedArtifact(normalized) || generatedTwins.has(normalized)) {
|
|
362
|
+
violations.push({
|
|
363
|
+
path: normalized,
|
|
364
|
+
code: "generated_artifact",
|
|
365
|
+
severity: "error",
|
|
366
|
+
message: "build-generated output is not admissible as source"
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (sourceExtension(normalized) === ".svg") {
|
|
370
|
+
violations.push({
|
|
371
|
+
path: normalized,
|
|
372
|
+
code: "active_content",
|
|
373
|
+
severity: "warning",
|
|
374
|
+
message: "SVG is active content and requires release-layer policy"
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
const route = servedRoute(normalized);
|
|
378
|
+
if (route !== null && isReservedRoute(route)) {
|
|
379
|
+
violations.push({
|
|
380
|
+
path: normalized,
|
|
381
|
+
code: "reserved_route",
|
|
382
|
+
severity: "error",
|
|
383
|
+
message: `source path would claim reserved route ${route}`
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
const extension = sourceExtension(normalized);
|
|
387
|
+
if (normalized.startsWith("pages/") && (extension === ".md" || extension === ".mdx")) {
|
|
388
|
+
pageCount += 1;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (!hasDocsJson) {
|
|
392
|
+
violations.push({
|
|
393
|
+
path: null,
|
|
394
|
+
code: "missing_docs_json",
|
|
395
|
+
severity: "error",
|
|
396
|
+
message: "bundle must contain an admissible docs.json entry"
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
if (totalSize > SOURCE_LIMITS.maxTotalBytes) {
|
|
400
|
+
violations.push({
|
|
401
|
+
path: null,
|
|
402
|
+
code: "bundle_too_large",
|
|
403
|
+
severity: "error",
|
|
404
|
+
message: `bundle is ${totalSize} bytes; maximum is ${SOURCE_LIMITS.maxTotalBytes}`
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
if (pageCount > SOURCE_LIMITS.maxPageCount) {
|
|
408
|
+
violations.push({
|
|
409
|
+
path: null,
|
|
410
|
+
code: "too_many_pages",
|
|
411
|
+
severity: "error",
|
|
412
|
+
message: `bundle has ${pageCount} page files; maximum is ${SOURCE_LIMITS.maxPageCount}`
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
const caseGroups = /* @__PURE__ */ new Map();
|
|
416
|
+
for (const path of normalizedPaths) {
|
|
417
|
+
const folded = unicodeCaseFold(path);
|
|
418
|
+
const group = caseGroups.get(folded) ?? [];
|
|
419
|
+
group.push(path);
|
|
420
|
+
caseGroups.set(folded, group);
|
|
421
|
+
}
|
|
422
|
+
for (const group of caseGroups.values()) {
|
|
423
|
+
const paths = [...group].sort(compareCodePoints2);
|
|
424
|
+
for (let index = 1; index < paths.length; index += 1) {
|
|
425
|
+
violations.push({
|
|
426
|
+
path: paths[index],
|
|
427
|
+
code: "case_collision",
|
|
428
|
+
severity: "error",
|
|
429
|
+
message: `source path collides case-insensitively with ${paths[0]}`
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const pageKinds = /* @__PURE__ */ new Map();
|
|
434
|
+
for (const path of normalizedPaths) {
|
|
435
|
+
if (!path.startsWith("pages/")) continue;
|
|
436
|
+
const extension = sourceExtension(path);
|
|
437
|
+
if (extension !== ".md" && extension !== ".mdx") continue;
|
|
438
|
+
const logical = path.slice(0, -extension.length);
|
|
439
|
+
const kind = pageKinds.get(logical) ?? {};
|
|
440
|
+
if (extension === ".md") kind.md = path;
|
|
441
|
+
else kind.mdx = path;
|
|
442
|
+
pageKinds.set(logical, kind);
|
|
443
|
+
}
|
|
444
|
+
for (const kind of pageKinds.values()) {
|
|
445
|
+
if (kind.md && kind.mdx) {
|
|
446
|
+
violations.push({
|
|
447
|
+
path: kind.mdx,
|
|
448
|
+
code: "md_mdx_collision",
|
|
449
|
+
severity: "error",
|
|
450
|
+
message: `logical page resolves to both ${kind.md} and ${kind.mdx}`
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return sortViolations(violations);
|
|
455
|
+
}
|
|
456
|
+
var STRING = "string";
|
|
457
|
+
var BOOLEAN = "boolean";
|
|
458
|
+
var HREF = "href";
|
|
459
|
+
var SRC = "src";
|
|
460
|
+
var CALLOUT_ATTRIBUTES = { title: STRING, icon: STRING };
|
|
461
|
+
var COMPONENTS = {
|
|
462
|
+
Accordion: {
|
|
463
|
+
attributes: { title: STRING, icon: STRING, defaultOpen: BOOLEAN },
|
|
464
|
+
required: ["title"]
|
|
465
|
+
},
|
|
466
|
+
AccordionGroup: { attributes: {} },
|
|
467
|
+
AppPreview: {
|
|
468
|
+
attributes: {
|
|
469
|
+
url: STRING,
|
|
470
|
+
query: STRING,
|
|
471
|
+
tool: STRING,
|
|
472
|
+
response: STRING,
|
|
473
|
+
entities: STRING,
|
|
474
|
+
relationships: STRING
|
|
475
|
+
},
|
|
476
|
+
required: ["query", "tool", "response", "entities", "relationships"]
|
|
477
|
+
},
|
|
478
|
+
BrandAsset: {
|
|
479
|
+
attributes: {
|
|
480
|
+
title: STRING,
|
|
481
|
+
src: SRC,
|
|
482
|
+
alt: STRING,
|
|
483
|
+
surface: ["light", "dark", "neutral"],
|
|
484
|
+
background: "color",
|
|
485
|
+
description: STRING,
|
|
486
|
+
downloadLabel: STRING
|
|
487
|
+
},
|
|
488
|
+
required: ["title", "src", "alt"],
|
|
489
|
+
empty: true
|
|
490
|
+
},
|
|
491
|
+
Callout: {
|
|
492
|
+
attributes: {
|
|
493
|
+
type: ["note", "info", "tip", "warning", "check", "danger"],
|
|
494
|
+
title: STRING,
|
|
495
|
+
icon: STRING
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
Card: { attributes: { title: STRING, icon: STRING, href: HREF } },
|
|
499
|
+
CardGroup: { attributes: { cols: "cols" } },
|
|
500
|
+
Check: { attributes: CALLOUT_ATTRIBUTES },
|
|
501
|
+
CodeGroup: { attributes: { label: STRING } },
|
|
502
|
+
Columns: { attributes: { cols: "cols" } },
|
|
503
|
+
ColorSwatch: {
|
|
504
|
+
attributes: { name: STRING, value: "color", usage: STRING },
|
|
505
|
+
required: ["name", "value"],
|
|
506
|
+
empty: true
|
|
507
|
+
},
|
|
508
|
+
CommandStep: {
|
|
509
|
+
attributes: { number: STRING, title: STRING, command: STRING },
|
|
510
|
+
required: ["number", "title", "command"],
|
|
511
|
+
empty: true
|
|
512
|
+
},
|
|
513
|
+
CommandTimeline: { attributes: {} },
|
|
514
|
+
Danger: { attributes: CALLOUT_ATTRIBUTES },
|
|
515
|
+
DomainCard: {
|
|
516
|
+
attributes: { name: STRING, emoji: STRING, domain: STRING, entities: STRING },
|
|
517
|
+
required: ["name", "emoji", "domain", "entities"],
|
|
518
|
+
empty: true
|
|
519
|
+
},
|
|
520
|
+
DomainCarousel: { attributes: {} },
|
|
521
|
+
Expandable: {
|
|
522
|
+
attributes: { title: STRING, defaultOpen: BOOLEAN },
|
|
523
|
+
required: ["title"]
|
|
524
|
+
},
|
|
525
|
+
FooterColumn: { attributes: { title: STRING }, required: ["title"] },
|
|
526
|
+
Frame: { attributes: { caption: STRING, surface: ["default", "light", "dark"] } },
|
|
527
|
+
FrameworkCard: {
|
|
528
|
+
attributes: { title: STRING, support: ["Full Streaming", "Tool Events"] },
|
|
529
|
+
required: ["title", "support"]
|
|
530
|
+
},
|
|
531
|
+
FrameworkGrid: { attributes: {} },
|
|
532
|
+
Icon: {
|
|
533
|
+
attributes: { icon: STRING, size: "iconSize" },
|
|
534
|
+
required: ["icon"],
|
|
535
|
+
empty: true
|
|
536
|
+
},
|
|
537
|
+
Info: { attributes: CALLOUT_ATTRIBUTES },
|
|
538
|
+
LandingCta: {
|
|
539
|
+
attributes: {
|
|
540
|
+
title: STRING,
|
|
541
|
+
description: STRING,
|
|
542
|
+
primaryLabel: STRING,
|
|
543
|
+
primaryHref: HREF,
|
|
544
|
+
secondaryLabel: STRING,
|
|
545
|
+
secondaryHref: HREF
|
|
546
|
+
},
|
|
547
|
+
required: ["title"]
|
|
548
|
+
},
|
|
549
|
+
LandingFooter: {
|
|
550
|
+
attributes: { name: STRING, logo: SRC, description: STRING },
|
|
551
|
+
required: ["name"]
|
|
552
|
+
},
|
|
553
|
+
LandingHero: {
|
|
554
|
+
attributes: {
|
|
555
|
+
title: STRING,
|
|
556
|
+
description: STRING,
|
|
557
|
+
eyebrow: STRING,
|
|
558
|
+
primaryLabel: STRING,
|
|
559
|
+
primaryHref: HREF,
|
|
560
|
+
secondaryLabel: STRING,
|
|
561
|
+
secondaryHref: HREF,
|
|
562
|
+
align: ["left", "center"],
|
|
563
|
+
visualMode: ["static", "sequence"],
|
|
564
|
+
terminalLabel: STRING,
|
|
565
|
+
previewLabel: STRING
|
|
566
|
+
},
|
|
567
|
+
required: ["title"]
|
|
568
|
+
},
|
|
569
|
+
LandingSection: {
|
|
570
|
+
attributes: {
|
|
571
|
+
title: STRING,
|
|
572
|
+
description: STRING,
|
|
573
|
+
eyebrow: STRING,
|
|
574
|
+
align: ["left", "center"],
|
|
575
|
+
tone: ["default", "muted", "accent"]
|
|
576
|
+
},
|
|
577
|
+
required: ["title"]
|
|
578
|
+
},
|
|
579
|
+
MemorySequence: { attributes: {} },
|
|
580
|
+
MemoryStage: {
|
|
581
|
+
attributes: {
|
|
582
|
+
stage: ["short", "long", "reasoning", "connected"],
|
|
583
|
+
badge: STRING,
|
|
584
|
+
title: STRING,
|
|
585
|
+
description: STRING
|
|
586
|
+
},
|
|
587
|
+
required: ["stage", "badge", "title", "description"],
|
|
588
|
+
empty: true
|
|
589
|
+
},
|
|
590
|
+
Note: { attributes: CALLOUT_ATTRIBUTES },
|
|
591
|
+
ParamField: {
|
|
592
|
+
attributes: {
|
|
593
|
+
path: STRING,
|
|
594
|
+
name: STRING,
|
|
595
|
+
query: STRING,
|
|
596
|
+
body: STRING,
|
|
597
|
+
type: STRING,
|
|
598
|
+
required: BOOLEAN,
|
|
599
|
+
default: "default"
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
PortalCard: {
|
|
603
|
+
attributes: { title: STRING, icon: STRING, href: HREF, description: STRING },
|
|
604
|
+
required: ["title", "href"]
|
|
605
|
+
},
|
|
606
|
+
ResponseField: {
|
|
607
|
+
attributes: { name: STRING, type: STRING, required: BOOLEAN },
|
|
608
|
+
required: ["name"]
|
|
609
|
+
},
|
|
610
|
+
Stat: {
|
|
611
|
+
attributes: {
|
|
612
|
+
label: STRING,
|
|
613
|
+
value: "numberOrString",
|
|
614
|
+
caption: STRING,
|
|
615
|
+
title: STRING,
|
|
616
|
+
accent: BOOLEAN,
|
|
617
|
+
bare: BOOLEAN
|
|
618
|
+
},
|
|
619
|
+
required: ["label", "value"]
|
|
620
|
+
},
|
|
621
|
+
Step: { attributes: { title: STRING } },
|
|
622
|
+
Steps: { attributes: {} },
|
|
623
|
+
Tab: { attributes: { title: STRING }, required: ["title"] },
|
|
624
|
+
Tabs: { attributes: { label: STRING } },
|
|
625
|
+
TerminalWindow: { attributes: { title: STRING, command: STRING, variant: ["default", "showcase", "compact"] } },
|
|
626
|
+
Tip: { attributes: CALLOUT_ATTRIBUTES },
|
|
627
|
+
TypeSpecimen: {
|
|
628
|
+
attributes: {
|
|
629
|
+
family: STRING,
|
|
630
|
+
role: STRING,
|
|
631
|
+
weights: STRING,
|
|
632
|
+
sample: STRING,
|
|
633
|
+
variant: ["body", "heading", "mono"]
|
|
634
|
+
},
|
|
635
|
+
required: ["family", "role", "weights", "sample", "variant"],
|
|
636
|
+
empty: true
|
|
637
|
+
},
|
|
638
|
+
UsageExample: {
|
|
639
|
+
attributes: { verdict: ["do", "dont"], title: STRING },
|
|
640
|
+
required: ["verdict", "title"]
|
|
641
|
+
},
|
|
642
|
+
Video: {
|
|
643
|
+
attributes: {
|
|
644
|
+
src: SRC,
|
|
645
|
+
title: STRING,
|
|
646
|
+
summary: STRING,
|
|
647
|
+
caption: STRING,
|
|
648
|
+
poster: SRC,
|
|
649
|
+
kind: ["auto", "video", "iframe"]
|
|
650
|
+
},
|
|
651
|
+
required: ["src", "title"]
|
|
652
|
+
},
|
|
653
|
+
Warning: { attributes: CALLOUT_ATTRIBUTES }
|
|
654
|
+
};
|
|
655
|
+
var NATIVE_ELEMENTS = {
|
|
656
|
+
img: {
|
|
657
|
+
attributes: { src: SRC, alt: STRING, width: "dimension", height: "dimension" },
|
|
658
|
+
required: ["src"],
|
|
659
|
+
empty: true
|
|
660
|
+
},
|
|
661
|
+
kbd: { attributes: {} }
|
|
662
|
+
};
|
|
663
|
+
var STANDARD_NODES = /* @__PURE__ */ new Set([
|
|
664
|
+
"blockquote",
|
|
665
|
+
"break",
|
|
666
|
+
"code",
|
|
667
|
+
"definition",
|
|
668
|
+
"delete",
|
|
669
|
+
"emphasis",
|
|
670
|
+
"heading",
|
|
671
|
+
"image",
|
|
672
|
+
"imageReference",
|
|
673
|
+
"inlineCode",
|
|
674
|
+
"link",
|
|
675
|
+
"linkReference",
|
|
676
|
+
"list",
|
|
677
|
+
"listItem",
|
|
678
|
+
"paragraph",
|
|
679
|
+
"root",
|
|
680
|
+
"strong",
|
|
681
|
+
"table",
|
|
682
|
+
"tableCell",
|
|
683
|
+
"tableRow",
|
|
684
|
+
"text",
|
|
685
|
+
"thematicBreak"
|
|
686
|
+
]);
|
|
687
|
+
var markdownParser = unified().use(remarkParse).use(remarkGfm);
|
|
688
|
+
var mdxParser = unified().use(remarkParse).use(remarkMdx).use(remarkGfm);
|
|
689
|
+
function issue(message) {
|
|
690
|
+
return { code: "inadmissible_page_content", message };
|
|
691
|
+
}
|
|
692
|
+
function expressionValue(attribute) {
|
|
693
|
+
return attribute.value && typeof attribute.value === "object" ? attribute.value.value : null;
|
|
694
|
+
}
|
|
695
|
+
function directBoolean(attribute) {
|
|
696
|
+
if (attribute.value === null) return true;
|
|
697
|
+
const value = expressionValue(attribute);
|
|
698
|
+
return value === "true" || value === "false";
|
|
699
|
+
}
|
|
700
|
+
function directNumber(attribute) {
|
|
701
|
+
const value = expressionValue(attribute);
|
|
702
|
+
if (value === null || !/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) return null;
|
|
703
|
+
const number = Number(value);
|
|
704
|
+
return Number.isFinite(number) ? number : null;
|
|
705
|
+
}
|
|
706
|
+
function safeUrl(raw, context) {
|
|
707
|
+
if (raw.length === 0 || raw.trim() !== raw || /[\u0000-\u001f\u007f]/.test(raw) || /\s/u.test(raw) || raw.includes("\\") || raw.startsWith("//")) {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
if (raw.startsWith("/") || raw.startsWith("./") || raw.startsWith("../") || raw.startsWith("#") || raw.startsWith("?")) {
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):/.exec(raw)?.[1]?.toLowerCase();
|
|
714
|
+
if (scheme !== void 0) {
|
|
715
|
+
return scheme === "http" || scheme === "https" || context === "href" && scheme === "mailto";
|
|
716
|
+
}
|
|
717
|
+
return !raw.includes(":");
|
|
718
|
+
}
|
|
719
|
+
function validAttribute(attribute, kind) {
|
|
720
|
+
if (attribute.type !== "mdxJsxAttribute") return false;
|
|
721
|
+
if (kind === "boolean") return directBoolean(attribute);
|
|
722
|
+
if (kind === "color") {
|
|
723
|
+
return typeof attribute.value === "string" && /^#[0-9A-Fa-f]{6}$/.test(attribute.value);
|
|
724
|
+
}
|
|
725
|
+
if (kind === "cols") {
|
|
726
|
+
const value = directNumber(attribute);
|
|
727
|
+
return value !== null && Number.isInteger(value) && value >= 1 && value <= 6;
|
|
728
|
+
}
|
|
729
|
+
if (kind === "default") {
|
|
730
|
+
return typeof attribute.value === "string" || directBoolean(attribute) || directNumber(attribute) !== null;
|
|
731
|
+
}
|
|
732
|
+
if (kind === "numberOrString") {
|
|
733
|
+
return typeof attribute.value === "string" || directNumber(attribute) !== null;
|
|
734
|
+
}
|
|
735
|
+
if (kind === "dimension") {
|
|
736
|
+
if (typeof attribute.value === "string") return /^(?:0|[1-9]\d*)$/.test(attribute.value);
|
|
737
|
+
const value = directNumber(attribute);
|
|
738
|
+
return value !== null && Number.isInteger(value) && value >= 0;
|
|
739
|
+
}
|
|
740
|
+
if (kind === "iconSize") {
|
|
741
|
+
if (typeof attribute.value === "string") {
|
|
742
|
+
const match = /^(0|(?:0|[1-9]\d*)(?:\.\d+)?)(px|rem|em)?$/.exec(attribute.value);
|
|
743
|
+
return match !== null && Number(match[1]) <= 512;
|
|
744
|
+
}
|
|
745
|
+
const value = directNumber(attribute);
|
|
746
|
+
return value !== null && value >= 0 && value <= 512;
|
|
747
|
+
}
|
|
748
|
+
if (kind === "href" || kind === "src") {
|
|
749
|
+
return typeof attribute.value === "string" && safeUrl(attribute.value, kind);
|
|
750
|
+
}
|
|
751
|
+
if (kind === "string") return typeof attribute.value === "string";
|
|
752
|
+
return typeof attribute.value === "string" && kind.includes(attribute.value);
|
|
753
|
+
}
|
|
754
|
+
function validateElement(node) {
|
|
755
|
+
if (node.name === null || node.name === void 0) return issue("MDX fragments are not admissible");
|
|
756
|
+
const policy = COMPONENTS[node.name] ?? NATIVE_ELEMENTS[node.name];
|
|
757
|
+
if (policy === void 0) return issue(`element ${node.name} is not admissible`);
|
|
758
|
+
const seen = /* @__PURE__ */ new Set();
|
|
759
|
+
for (const attribute of node.attributes ?? []) {
|
|
760
|
+
if (attribute.type !== "mdxJsxAttribute" || attribute.name === void 0) {
|
|
761
|
+
return issue(`spread attributes are not admissible on ${node.name}`);
|
|
762
|
+
}
|
|
763
|
+
if (seen.has(attribute.name)) return issue(`duplicate attribute ${attribute.name} on ${node.name}`);
|
|
764
|
+
seen.add(attribute.name);
|
|
765
|
+
const kind = policy.attributes[attribute.name];
|
|
766
|
+
if (kind === void 0) return issue(`attribute ${attribute.name} is not admissible on ${node.name}`);
|
|
767
|
+
if (!validAttribute(attribute, kind)) {
|
|
768
|
+
return issue(`attribute ${attribute.name} has an inadmissible value on ${node.name}`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
for (const required of policy.required ?? []) {
|
|
772
|
+
if (!seen.has(required)) return issue(`attribute ${required} is required on ${node.name}`);
|
|
773
|
+
}
|
|
774
|
+
if (policy.empty && (node.children?.length ?? 0) > 0) return issue(`${node.name} must not have children`);
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
function validatePageSource(path, source, policy = {}) {
|
|
778
|
+
let tree;
|
|
779
|
+
try {
|
|
780
|
+
tree = (path.toLowerCase().endsWith(".mdx") ? mdxParser : markdownParser).parse(source);
|
|
781
|
+
} catch (error) {
|
|
782
|
+
return {
|
|
783
|
+
code: "invalid_page_syntax",
|
|
784
|
+
message: error instanceof Error ? error.message : String(error)
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
788
|
+
const collectDefinitions = (node) => {
|
|
789
|
+
if (node.type === "definition" && node.identifier !== void 0 && node.url !== void 0) {
|
|
790
|
+
definitions.set(node.identifier, node.url);
|
|
791
|
+
}
|
|
792
|
+
for (const child of node.children ?? []) collectDefinitions(child);
|
|
793
|
+
};
|
|
794
|
+
collectDefinitions(tree);
|
|
795
|
+
const visit = (node) => {
|
|
796
|
+
if (node.type === "heading" && node.depth === 1 && policy.allowLevelOneHeading === false) {
|
|
797
|
+
return {
|
|
798
|
+
code: "page_heading_conflict",
|
|
799
|
+
message: "standard documentation pages receive their level-one heading from frontmatter title; remove the authored level-one heading or set pageHeader: false when the page owns its title"
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
if (node.type === "mdxjsEsm") return issue("ESM is not admissible in page source");
|
|
803
|
+
if (node.type === "mdxFlowExpression" || node.type === "mdxTextExpression") {
|
|
804
|
+
return issue("MDX expressions are not admissible in page content");
|
|
805
|
+
}
|
|
806
|
+
if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") {
|
|
807
|
+
const elementIssue = validateElement(node);
|
|
808
|
+
if (elementIssue !== null) return elementIssue;
|
|
809
|
+
} else if (node.type === "html") {
|
|
810
|
+
return issue("raw HTML is not admissible in page source");
|
|
811
|
+
} else if (!STANDARD_NODES.has(node.type)) {
|
|
812
|
+
return issue(`syntax node ${node.type} is not admissible`);
|
|
813
|
+
}
|
|
814
|
+
if ((node.type === "link" || node.type === "definition") && node.url !== void 0) {
|
|
815
|
+
if (!safeUrl(node.url, "href")) return issue(`URL scheme is not admissible: ${node.url}`);
|
|
816
|
+
}
|
|
817
|
+
if (node.type === "image" && node.url !== void 0 && !safeUrl(node.url, "src")) {
|
|
818
|
+
return issue(`image URL scheme is not admissible: ${node.url}`);
|
|
819
|
+
}
|
|
820
|
+
if (node.type === "linkReference" && node.identifier !== void 0) {
|
|
821
|
+
const url = definitions.get(node.identifier);
|
|
822
|
+
if (url !== void 0 && !safeUrl(url, "href")) return issue(`URL scheme is not admissible: ${url}`);
|
|
823
|
+
}
|
|
824
|
+
if (node.type === "imageReference" && node.identifier !== void 0) {
|
|
825
|
+
const url = definitions.get(node.identifier);
|
|
826
|
+
if (url !== void 0 && !safeUrl(url, "src")) return issue(`image URL scheme is not admissible: ${url}`);
|
|
827
|
+
}
|
|
828
|
+
for (const child of node.children ?? []) {
|
|
829
|
+
const childIssue = visit(child);
|
|
830
|
+
if (childIssue !== null) return childIssue;
|
|
831
|
+
}
|
|
832
|
+
return null;
|
|
833
|
+
};
|
|
834
|
+
return visit(tree);
|
|
835
|
+
}
|
|
836
|
+
var RUNBOOK_MIN_STEPS = 3;
|
|
837
|
+
var RUNBOOK_MAX_STEPS = 8;
|
|
838
|
+
var RUNBOOK_MAX_STEP_WORDS = 40;
|
|
839
|
+
var RUNBOOK_TARGETS = ["code", "workspace"];
|
|
840
|
+
var RUNBOOK_KEYS = ["goal", "prerequisites", "steps", "expected", "verify", "target"];
|
|
841
|
+
var RUNBOOK_KEY_SET = new Set(RUNBOOK_KEYS);
|
|
842
|
+
var LINE_BREAK = /[\r\n]/;
|
|
843
|
+
var CONTROL_CHARACTER = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
|
|
844
|
+
function isRecord(value) {
|
|
845
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
846
|
+
}
|
|
847
|
+
function filledString(value) {
|
|
848
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
849
|
+
}
|
|
850
|
+
function wordCount(value) {
|
|
851
|
+
return value.trim().split(/\s+/).length;
|
|
852
|
+
}
|
|
853
|
+
function proseProblems(value, field) {
|
|
854
|
+
const problems = [];
|
|
855
|
+
if (LINE_BREAK.test(value)) {
|
|
856
|
+
problems.push(
|
|
857
|
+
`runbook.${field} must not contain a line break. Move long context into the page body.`
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
if (CONTROL_CHARACTER.test(value)) {
|
|
861
|
+
problems.push(`runbook.${field} must not contain a control character.`);
|
|
862
|
+
}
|
|
863
|
+
return problems;
|
|
864
|
+
}
|
|
865
|
+
function stringListProblems(value, field) {
|
|
866
|
+
if (!Array.isArray(value)) return [`runbook.${field} must be a list of strings.`];
|
|
867
|
+
return value.flatMap(
|
|
868
|
+
(entry, index) => filledString(entry) ? [] : [`runbook.${field}[${index}] must be a non-empty string.`]
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
function runbookProblems(value) {
|
|
872
|
+
if (!isRecord(value)) return ["runbook must be an object with goal, steps, expected, and verify."];
|
|
873
|
+
const problems = [];
|
|
874
|
+
for (const key of Object.keys(value)) {
|
|
875
|
+
if (!RUNBOOK_KEY_SET.has(key)) {
|
|
876
|
+
problems.push(
|
|
877
|
+
`runbook.${key} is not a runbook field. Allowed fields: ${RUNBOOK_KEYS.join(", ")}.`
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
for (const field of ["goal", "expected", "verify"]) {
|
|
882
|
+
const entry = value[field];
|
|
883
|
+
if (!filledString(entry)) {
|
|
884
|
+
problems.push(`runbook.${field} is required and must be a non-empty string.`);
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
problems.push(...proseProblems(entry, field));
|
|
888
|
+
}
|
|
889
|
+
if (value["prerequisites"] !== void 0) {
|
|
890
|
+
const listProblems = stringListProblems(value["prerequisites"], "prerequisites");
|
|
891
|
+
problems.push(...listProblems);
|
|
892
|
+
if (listProblems.length === 0) {
|
|
893
|
+
for (const [index, entry] of value["prerequisites"].entries()) {
|
|
894
|
+
problems.push(...proseProblems(entry, `prerequisites[${index}]`));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (value["steps"] === void 0) {
|
|
899
|
+
problems.push("runbook.steps is required and must be a list of strings.");
|
|
900
|
+
} else {
|
|
901
|
+
const listProblems = stringListProblems(value["steps"], "steps");
|
|
902
|
+
problems.push(...listProblems);
|
|
903
|
+
if (listProblems.length === 0) {
|
|
904
|
+
const steps = value["steps"];
|
|
905
|
+
if (steps.length < RUNBOOK_MIN_STEPS || steps.length > RUNBOOK_MAX_STEPS) {
|
|
906
|
+
problems.push(
|
|
907
|
+
`runbook.steps must hold ${RUNBOOK_MIN_STEPS} to ${RUNBOOK_MAX_STEPS} steps, not ${steps.length}. Move long context into the page body.`
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
for (const [index, step] of steps.entries()) {
|
|
911
|
+
problems.push(...proseProblems(step, `steps[${index}]`));
|
|
912
|
+
if (wordCount(step) > RUNBOOK_MAX_STEP_WORDS) {
|
|
913
|
+
problems.push(
|
|
914
|
+
`runbook.steps[${index}] is ${wordCount(step)} words. A step may hold at most ${RUNBOOK_MAX_STEP_WORDS} words.`
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
const target = value["target"];
|
|
921
|
+
if (target !== void 0 && !RUNBOOK_TARGETS.includes(target)) {
|
|
922
|
+
problems.push(`runbook.target must be ${RUNBOOK_TARGETS.join(" or ")}.`);
|
|
923
|
+
}
|
|
924
|
+
return problems;
|
|
925
|
+
}
|
|
926
|
+
var fatalUtf8Decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
927
|
+
var boundedUtf8Decoder = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
928
|
+
var textEncoder3 = new TextEncoder();
|
|
929
|
+
function errorMessage(error) {
|
|
930
|
+
return error instanceof Error ? error.message : String(error);
|
|
931
|
+
}
|
|
932
|
+
function addViolation(violations, path, code, message) {
|
|
933
|
+
violations.push({ path, code, severity: "error", message });
|
|
934
|
+
}
|
|
935
|
+
function decodeUtf8(bytes, path, violations, maxBytes) {
|
|
936
|
+
if (maxBytes !== void 0 && bytes.byteLength > maxBytes) {
|
|
937
|
+
addViolation(
|
|
938
|
+
violations,
|
|
939
|
+
path,
|
|
940
|
+
"semantic_input_too_large",
|
|
941
|
+
`${path} exceeds ${maxBytes} bytes`
|
|
942
|
+
);
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
try {
|
|
946
|
+
const source = fatalUtf8Decoder.decode(bytes);
|
|
947
|
+
if (source.startsWith("\uFEFF")) {
|
|
948
|
+
addViolation(violations, path, "invalid_utf8", `${path} must not begin with a UTF-8 BOM`);
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
return source;
|
|
952
|
+
} catch (error) {
|
|
953
|
+
addViolation(
|
|
954
|
+
violations,
|
|
955
|
+
path,
|
|
956
|
+
"invalid_utf8",
|
|
957
|
+
`${path} is not valid UTF-8: ${errorMessage(error)}`
|
|
958
|
+
);
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
function readSemanticFile(input, path, violations) {
|
|
963
|
+
try {
|
|
964
|
+
const bytes = input.readFile(path);
|
|
965
|
+
return bytes === null ? { present: false, bytes: null } : { present: true, bytes };
|
|
966
|
+
} catch (error) {
|
|
967
|
+
addViolation(violations, path, "read_error", errorMessage(error));
|
|
968
|
+
return { present: true, bytes: null };
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
function parseDocsJson(bytes, violations) {
|
|
972
|
+
const source = decodeUtf8(bytes, "docs.json", violations, SOURCE_LIMITS.maxDocsJsonBytes);
|
|
973
|
+
if (source === null) return null;
|
|
974
|
+
try {
|
|
975
|
+
const document = parseDocument(source, { schema: "json", uniqueKeys: true });
|
|
976
|
+
if (document.errors.length > 0) throw document.errors[0];
|
|
977
|
+
} catch (error) {
|
|
978
|
+
addViolation(
|
|
979
|
+
violations,
|
|
980
|
+
"docs.json",
|
|
981
|
+
"invalid_docs_json",
|
|
982
|
+
`docs.json has duplicate keys or invalid JSON structure: ${errorMessage(error)}`
|
|
983
|
+
);
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
let parsed;
|
|
987
|
+
try {
|
|
988
|
+
parsed = JSON.parse(source);
|
|
989
|
+
} catch (error) {
|
|
990
|
+
addViolation(
|
|
991
|
+
violations,
|
|
992
|
+
"docs.json",
|
|
993
|
+
"invalid_docs_json",
|
|
994
|
+
`docs.json is not valid JSON: ${errorMessage(error)}`
|
|
995
|
+
);
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
try {
|
|
999
|
+
return parseDocsConfig(parsed);
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
addViolation(violations, "docs.json", "invalid_docs_json", errorMessage(error));
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function frontmatterPolicy(bytes) {
|
|
1006
|
+
let openingLength = 0;
|
|
1007
|
+
if (bytes[0] === 45 && bytes[1] === 45 && bytes[2] === 45) {
|
|
1008
|
+
let index = 3;
|
|
1009
|
+
while (bytes[index] === 32 || bytes[index] === 9) index += 1;
|
|
1010
|
+
if (bytes[index] === 10) openingLength = index + 1;
|
|
1011
|
+
else if (bytes[index] === 13 && bytes[index + 1] === 10) openingLength = index + 2;
|
|
1012
|
+
else if (index < bytes.byteLength) throw new Error("frontmatter opening marker is malformed");
|
|
1013
|
+
}
|
|
1014
|
+
if (openingLength === 0) {
|
|
1015
|
+
return {
|
|
1016
|
+
grouped: false,
|
|
1017
|
+
pagefindFalse: false,
|
|
1018
|
+
pageLayout: "docs",
|
|
1019
|
+
pageHeader: true,
|
|
1020
|
+
bodyOffset: 0
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
const searchEnd = Math.min(
|
|
1024
|
+
bytes.byteLength,
|
|
1025
|
+
openingLength + SOURCE_LIMITS.maxFrontmatterBytes
|
|
1026
|
+
);
|
|
1027
|
+
const bounded = boundedUtf8Decoder.decode(bytes.subarray(openingLength, searchEnd));
|
|
1028
|
+
const closing = /(?:^|\r?\n)---[\t ]*(?:\r?\n|$)/.exec(bounded);
|
|
1029
|
+
if (!closing) {
|
|
1030
|
+
throw new Error(
|
|
1031
|
+
`frontmatter closing marker was not found within ${SOURCE_LIMITS.maxFrontmatterBytes} bytes`
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
const block = bounded.slice(0, closing.index);
|
|
1035
|
+
if (textEncoder3.encode(block).byteLength > SOURCE_LIMITS.maxFrontmatterBytes) {
|
|
1036
|
+
throw new Error(`frontmatter exceeds ${SOURCE_LIMITS.maxFrontmatterBytes} bytes`);
|
|
1037
|
+
}
|
|
1038
|
+
const bodyOffset = openingLength + textEncoder3.encode(bounded.slice(0, closing.index + closing[0].length)).byteLength;
|
|
1039
|
+
const parsed = parse(block, {
|
|
1040
|
+
maxAliasCount: 0,
|
|
1041
|
+
schema: "core",
|
|
1042
|
+
uniqueKeys: true
|
|
1043
|
+
});
|
|
1044
|
+
if (parsed === null || parsed === void 0) {
|
|
1045
|
+
return {
|
|
1046
|
+
grouped: false,
|
|
1047
|
+
pagefindFalse: false,
|
|
1048
|
+
pageLayout: "docs",
|
|
1049
|
+
pageHeader: true,
|
|
1050
|
+
bodyOffset
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1054
|
+
throw new Error("frontmatter must be a top-level YAML mapping");
|
|
1055
|
+
}
|
|
1056
|
+
const record = parsed;
|
|
1057
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
1058
|
+
"title",
|
|
1059
|
+
"description",
|
|
1060
|
+
"pageLayout",
|
|
1061
|
+
"groups",
|
|
1062
|
+
"pagefind",
|
|
1063
|
+
"noindex",
|
|
1064
|
+
"public",
|
|
1065
|
+
"pageHeader",
|
|
1066
|
+
"runbook"
|
|
1067
|
+
]);
|
|
1068
|
+
for (const key of Object.keys(record)) {
|
|
1069
|
+
if (!allowed.has(key)) throw new Error(`frontmatter key is not admissible: ${key}`);
|
|
1070
|
+
}
|
|
1071
|
+
for (const key of ["title", "description"]) {
|
|
1072
|
+
if (record[key] !== void 0 && typeof record[key] !== "string") {
|
|
1073
|
+
throw new Error(`frontmatter ${key} must be a string`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
if (record["pageLayout"] !== void 0 && !["docs", "landing"].includes(String(record["pageLayout"]))) {
|
|
1077
|
+
throw new Error("frontmatter pageLayout must be docs or landing");
|
|
1078
|
+
}
|
|
1079
|
+
if (record["groups"] !== void 0 && (!Array.isArray(record["groups"]) || record["groups"].some((group) => typeof group !== "string"))) {
|
|
1080
|
+
throw new Error("frontmatter groups must be an array of strings");
|
|
1081
|
+
}
|
|
1082
|
+
for (const key of ["pagefind", "noindex", "public", "pageHeader"]) {
|
|
1083
|
+
if (record[key] !== void 0 && typeof record[key] !== "boolean") {
|
|
1084
|
+
throw new Error(`frontmatter ${key} must be a boolean`);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
if (record["runbook"] !== void 0) {
|
|
1088
|
+
const problems = runbookProblems(record["runbook"]);
|
|
1089
|
+
if (problems.length > 0) {
|
|
1090
|
+
throw new Error(`frontmatter runbook is invalid: ${problems.join(" ")}`);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
return {
|
|
1094
|
+
grouped: Array.isArray(record["groups"]) && record["groups"].length > 0,
|
|
1095
|
+
pagefindFalse: record["pagefind"] === false,
|
|
1096
|
+
pageLayout: record["pageLayout"] === "landing" ? "landing" : "docs",
|
|
1097
|
+
pageHeader: record["pageHeader"] !== false,
|
|
1098
|
+
bodyOffset
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
function pageCandidates(entry) {
|
|
1102
|
+
const mdx = `pages/${entry}.mdx`;
|
|
1103
|
+
const md = `pages/${entry}.md`;
|
|
1104
|
+
if (canonicalizeSourcePath(mdx) !== mdx || canonicalizeSourcePath(md) !== md) return null;
|
|
1105
|
+
return { mdx, md };
|
|
1106
|
+
}
|
|
1107
|
+
function isRecord2(value) {
|
|
1108
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1109
|
+
}
|
|
1110
|
+
function hasExternalRef(value, seen = /* @__PURE__ */ new Set()) {
|
|
1111
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1112
|
+
if (seen.has(value)) return false;
|
|
1113
|
+
seen.add(value);
|
|
1114
|
+
if (Array.isArray(value)) return value.some((item) => hasExternalRef(item, seen));
|
|
1115
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1116
|
+
if (key === "$ref" && typeof child === "string") {
|
|
1117
|
+
const hash = child.indexOf("#");
|
|
1118
|
+
const filePart = hash === -1 ? child : child.slice(0, hash);
|
|
1119
|
+
if (filePart.length > 0) return true;
|
|
1120
|
+
}
|
|
1121
|
+
if (hasExternalRef(child, seen)) return true;
|
|
1122
|
+
}
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
function validateOpenApiDocument(source) {
|
|
1126
|
+
const document = parse(source, {
|
|
1127
|
+
maxAliasCount: 0,
|
|
1128
|
+
schema: "core",
|
|
1129
|
+
uniqueKeys: true
|
|
1130
|
+
});
|
|
1131
|
+
if (!isRecord2(document)) throw new Error("OpenAPI document must be an object");
|
|
1132
|
+
const isOpenApi3 = typeof document["openapi"] === "string" && /^3\./.test(document["openapi"]);
|
|
1133
|
+
const isSwagger2 = document["swagger"] === "2.0";
|
|
1134
|
+
if (!isOpenApi3 && !isSwagger2) {
|
|
1135
|
+
throw new Error("document must declare top-level openapi 3.x or swagger 2.0");
|
|
1136
|
+
}
|
|
1137
|
+
if (hasExternalRef(document)) {
|
|
1138
|
+
throw new Error(`external OpenAPI $ref values are not admissible in source ABI v${SOURCE_ABI_VERSION}`);
|
|
1139
|
+
}
|
|
1140
|
+
parseOpenApi(source);
|
|
1141
|
+
}
|
|
1142
|
+
function prepareSemanticValidation(input) {
|
|
1143
|
+
const violations = [];
|
|
1144
|
+
const config = parseDocsJson(input.docsJson, violations);
|
|
1145
|
+
if (config === null) return { config, pagePaths: [], pagePathSet: /* @__PURE__ */ new Set(), violations };
|
|
1146
|
+
if (input.modelsToml !== void 0) {
|
|
1147
|
+
const source = decodeUtf8(
|
|
1148
|
+
input.modelsToml,
|
|
1149
|
+
"models.toml",
|
|
1150
|
+
violations,
|
|
1151
|
+
SOURCE_LIMITS.maxModelsTomlBytes
|
|
1152
|
+
);
|
|
1153
|
+
if (source !== null) {
|
|
1154
|
+
try {
|
|
1155
|
+
parseModelsConfig(source);
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
addViolation(violations, "models.toml", "invalid_models_toml", errorMessage(error));
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
const pagePaths = [...input.pagePaths].sort();
|
|
1162
|
+
const pagePathSet = /* @__PURE__ */ new Set();
|
|
1163
|
+
for (const path of pagePaths) {
|
|
1164
|
+
const extension = sourceExtension(path);
|
|
1165
|
+
if (pagePathSet.has(path) || !path.startsWith("pages/") || canonicalizeSourcePath(path) !== path || extension !== ".md" && extension !== ".mdx") {
|
|
1166
|
+
addViolation(
|
|
1167
|
+
violations,
|
|
1168
|
+
path,
|
|
1169
|
+
"inadmissible_page_content",
|
|
1170
|
+
`page path is not a unique canonical page source path: ${path}`
|
|
1171
|
+
);
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
pagePathSet.add(path);
|
|
1175
|
+
}
|
|
1176
|
+
return { config, pagePaths, pagePathSet, violations };
|
|
1177
|
+
}
|
|
1178
|
+
function validatePageBytes(path, bytes, violations) {
|
|
1179
|
+
const source = decodeUtf8(bytes, path, violations);
|
|
1180
|
+
if (source === null) return;
|
|
1181
|
+
try {
|
|
1182
|
+
const policy = frontmatterPolicy(bytes);
|
|
1183
|
+
if (policy.grouped && !policy.pagefindFalse) {
|
|
1184
|
+
addViolation(
|
|
1185
|
+
violations,
|
|
1186
|
+
path,
|
|
1187
|
+
"pagefind_required",
|
|
1188
|
+
"grouped pages must set pagefind: false"
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
const body = fatalUtf8Decoder.decode(bytes.subarray(policy.bodyOffset));
|
|
1192
|
+
const pageIssue = validatePageSource(path, body, {
|
|
1193
|
+
allowLevelOneHeading: policy.pageLayout === "landing" || !policy.pageHeader
|
|
1194
|
+
});
|
|
1195
|
+
if (pageIssue !== null) addViolation(violations, path, pageIssue.code, pageIssue.message);
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
addViolation(violations, path, "invalid_frontmatter", errorMessage(error));
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
function validateNavigation(config, prepared) {
|
|
1201
|
+
const { pagePaths, pagePathSet, violations } = prepared;
|
|
1202
|
+
const pages = new Set(navigationGroups(config).flatMap((group) => group.pages));
|
|
1203
|
+
const referencedPagePaths = /* @__PURE__ */ new Set();
|
|
1204
|
+
for (const entry of pages) {
|
|
1205
|
+
const candidates = pageCandidates(entry);
|
|
1206
|
+
if (!candidates) {
|
|
1207
|
+
addViolation(
|
|
1208
|
+
violations,
|
|
1209
|
+
"docs.json",
|
|
1210
|
+
"invalid_navigation_path",
|
|
1211
|
+
`navigation page is not a canonical source path: ${entry}`
|
|
1212
|
+
);
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
const hasMdx = pagePathSet.has(candidates.mdx);
|
|
1216
|
+
const hasMd = pagePathSet.has(candidates.md);
|
|
1217
|
+
if (hasMdx) referencedPagePaths.add(candidates.mdx);
|
|
1218
|
+
if (hasMd) referencedPagePaths.add(candidates.md);
|
|
1219
|
+
if (hasMdx && hasMd) {
|
|
1220
|
+
addViolation(
|
|
1221
|
+
violations,
|
|
1222
|
+
candidates.mdx,
|
|
1223
|
+
"md_mdx_collision",
|
|
1224
|
+
`navigation page resolves to both ${candidates.mdx} and ${candidates.md}`
|
|
1225
|
+
);
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
if (!hasMdx && !hasMd) {
|
|
1229
|
+
addViolation(
|
|
1230
|
+
violations,
|
|
1231
|
+
candidates.mdx,
|
|
1232
|
+
"missing_page",
|
|
1233
|
+
`navigation page ${entry} has no .mdx or .md source file`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
for (const path of pagePaths) {
|
|
1238
|
+
if (!referencedPagePaths.has(path)) {
|
|
1239
|
+
addViolation(
|
|
1240
|
+
violations,
|
|
1241
|
+
path,
|
|
1242
|
+
"unreferenced_page",
|
|
1243
|
+
`page source is not referenced by docs.json navigation: ${path}`
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
function openApiPaths(config, violations) {
|
|
1249
|
+
const valid = [];
|
|
1250
|
+
const paths = new Set(
|
|
1251
|
+
navigationGroups(config).map((group) => group.openapi).filter((path) => path !== void 0)
|
|
1252
|
+
);
|
|
1253
|
+
for (const path of paths) {
|
|
1254
|
+
const extension = sourceExtension(path);
|
|
1255
|
+
if (!path.startsWith("openapi/") || canonicalizeSourcePath(path) !== path || extension !== ".json" && extension !== ".yaml" && extension !== ".yml") {
|
|
1256
|
+
addViolation(
|
|
1257
|
+
violations,
|
|
1258
|
+
"docs.json",
|
|
1259
|
+
"invalid_openapi_path",
|
|
1260
|
+
`OpenAPI path must be a canonical openapi/** path: ${path}`
|
|
1261
|
+
);
|
|
1262
|
+
} else {
|
|
1263
|
+
valid.push(path);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return valid;
|
|
1267
|
+
}
|
|
1268
|
+
function validateOpenApiBytes(path, bytes, violations) {
|
|
1269
|
+
const source = decodeUtf8(bytes, path, violations, SOURCE_LIMITS.maxOpenApiBytes);
|
|
1270
|
+
if (source === null) return;
|
|
1271
|
+
try {
|
|
1272
|
+
validateOpenApiDocument(source);
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
addViolation(violations, path, "invalid_openapi", errorMessage(error));
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
function validateBundleSemantics(input) {
|
|
1278
|
+
const prepared = prepareSemanticValidation(input);
|
|
1279
|
+
if (!prepared.config) return sortViolations(prepared.violations);
|
|
1280
|
+
for (const path of prepared.pagePaths) {
|
|
1281
|
+
if (!prepared.pagePathSet.has(path)) continue;
|
|
1282
|
+
const read = readSemanticFile(input, path, prepared.violations);
|
|
1283
|
+
if (!read.present) {
|
|
1284
|
+
addViolation(prepared.violations, path, "read_error", "listed page source is missing");
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
if (read.bytes === null) continue;
|
|
1288
|
+
validatePageBytes(path, read.bytes, prepared.violations);
|
|
1289
|
+
}
|
|
1290
|
+
validateNavigation(prepared.config, prepared);
|
|
1291
|
+
for (const path of openApiPaths(prepared.config, prepared.violations)) {
|
|
1292
|
+
const read = readSemanticFile(input, path, prepared.violations);
|
|
1293
|
+
if (!read.present) {
|
|
1294
|
+
addViolation(prepared.violations, path, "missing_openapi", "referenced OpenAPI file is missing");
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
if (read.bytes === null) continue;
|
|
1298
|
+
validateOpenApiBytes(path, read.bytes, prepared.violations);
|
|
1299
|
+
}
|
|
1300
|
+
return sortViolations(prepared.violations);
|
|
1301
|
+
}
|
|
1302
|
+
var encoder = new TextEncoder();
|
|
1303
|
+
var TAR_BLOCK_SIZE = 512;
|
|
1304
|
+
var TAR_TRAILER_SIZE = 1024;
|
|
1305
|
+
function tarPadding(size) {
|
|
1306
|
+
return (TAR_BLOCK_SIZE - size % TAR_BLOCK_SIZE) % TAR_BLOCK_SIZE;
|
|
1307
|
+
}
|
|
1308
|
+
function writeText(target, offset, length, value) {
|
|
1309
|
+
target.set(encoder.encode(value).subarray(0, length), offset);
|
|
1310
|
+
}
|
|
1311
|
+
function octal(value, length) {
|
|
1312
|
+
return `${value.toString(8).padStart(length - 1, "0")}\0`;
|
|
1313
|
+
}
|
|
1314
|
+
function tarHeader(name, size, options = {}) {
|
|
1315
|
+
const header = new Uint8Array(TAR_BLOCK_SIZE);
|
|
1316
|
+
writeText(header, 0, 100, name);
|
|
1317
|
+
writeText(header, 100, 8, octal(options.mode ?? 420, 8));
|
|
1318
|
+
writeText(header, 108, 8, octal(0, 8));
|
|
1319
|
+
writeText(header, 116, 8, octal(0, 8));
|
|
1320
|
+
writeText(header, 124, 12, octal(size, 12));
|
|
1321
|
+
writeText(header, 136, 12, octal(0, 12));
|
|
1322
|
+
header.fill(32, 148, 156);
|
|
1323
|
+
header[156] = (options.type ?? "0").charCodeAt(0);
|
|
1324
|
+
writeText(header, 257, 6, "ustar\0");
|
|
1325
|
+
writeText(header, 263, 2, "00");
|
|
1326
|
+
const checksum = header.reduce((sum, byte) => sum + byte, 0);
|
|
1327
|
+
writeText(header, 148, 8, `${checksum.toString(8).padStart(6, "0")}\0 `);
|
|
1328
|
+
return header;
|
|
1329
|
+
}
|
|
1330
|
+
function paxRecord(key, value) {
|
|
1331
|
+
let length = encoder.encode(`${key}=${value}
|
|
1332
|
+
`).byteLength + 3;
|
|
1333
|
+
for (; ; ) {
|
|
1334
|
+
const record = encoder.encode(`${length} ${key}=${value}
|
|
1335
|
+
`);
|
|
1336
|
+
if (record.byteLength === length) return record;
|
|
1337
|
+
length = record.byteLength;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// src/lib/bundle.ts
|
|
1342
|
+
var encoder2 = new TextEncoder();
|
|
1343
|
+
async function collectDirectory(root, relative, files) {
|
|
1344
|
+
const absolute = join(root, ...relative.split("/"));
|
|
1345
|
+
const entries = await readdir(absolute, { withFileTypes: true });
|
|
1346
|
+
for (const entry of entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) {
|
|
1347
|
+
const path = posix.join(relative, entry.name);
|
|
1348
|
+
const absolutePath = join(root, ...path.split("/"));
|
|
1349
|
+
if (entry.isSymbolicLink()) throw new Error(`source bundle cannot contain a symbolic link: ${path}`);
|
|
1350
|
+
if (entry.isDirectory()) await collectDirectory(root, path, files);
|
|
1351
|
+
else if (entry.isFile()) files.push({ path, absolutePath, bytes: new Uint8Array(await readFile(absolutePath)) });
|
|
1352
|
+
else throw new Error(`source bundle contains an unsupported filesystem entry: ${path}`);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
async function collectSourceFiles(docsDir) {
|
|
1356
|
+
const files = [];
|
|
1357
|
+
for (const [legacy, replacement] of [["assets", "public"], ["api-reference", "openapi"]]) {
|
|
1358
|
+
if (await lstat(join(docsDir, legacy)).catch(() => null) !== null) {
|
|
1359
|
+
throw new Error(`legacy ${legacy}/ source is not publishable; move it to ${replacement}/`);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
for (const name of ["docs.json", "models.toml"]) {
|
|
1363
|
+
const absolutePath = join(docsDir, name);
|
|
1364
|
+
const info = await lstat(absolutePath).catch(() => null);
|
|
1365
|
+
if (info === null) continue;
|
|
1366
|
+
if (info.isSymbolicLink() || !info.isFile()) throw new Error(`${name} must be a regular file`);
|
|
1367
|
+
files.push({ path: name, absolutePath, bytes: new Uint8Array(await readFile(absolutePath)) });
|
|
1368
|
+
}
|
|
1369
|
+
for (const name of ["pages", "public", "openapi"]) {
|
|
1370
|
+
const absolutePath = join(docsDir, name);
|
|
1371
|
+
const info = await lstat(absolutePath).catch(() => null);
|
|
1372
|
+
if (info === null) continue;
|
|
1373
|
+
if (info.isSymbolicLink() || !info.isDirectory()) throw new Error(`${name} must be a regular directory`);
|
|
1374
|
+
await collectDirectory(docsDir, name, files);
|
|
1375
|
+
}
|
|
1376
|
+
return files.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
1377
|
+
}
|
|
1378
|
+
function validateSourceFiles(files) {
|
|
1379
|
+
const structural = validateBundleStructure(files.map((file) => ({ path: file.path, size: file.bytes.byteLength })));
|
|
1380
|
+
const errors = structural.filter((violation) => violation.severity === "error");
|
|
1381
|
+
if (errors.length > 0) throw new Error(errors.map((violation) => `${violation.path ?? "bundle"}: ${violation.message}`).join("\n"));
|
|
1382
|
+
const byPath = new Map(files.map((file) => [file.path, file.bytes]));
|
|
1383
|
+
const semantic = validateBundleSemantics({
|
|
1384
|
+
docsJson: byPath.get("docs.json") ?? new Uint8Array(),
|
|
1385
|
+
modelsToml: byPath.get("models.toml"),
|
|
1386
|
+
pagePaths: files.map((file) => file.path).filter((path) => path.startsWith("pages/") && /\.(?:md|mdx)$/i.test(path)),
|
|
1387
|
+
readFile: (path) => byPath.get(path) ?? null
|
|
1388
|
+
});
|
|
1389
|
+
if (semantic.length > 0) throw new Error(semantic.map((violation) => `${violation.path ?? "bundle"}: ${violation.message}`).join("\n"));
|
|
1390
|
+
return structural.filter((violation) => violation.severity === "warning");
|
|
1391
|
+
}
|
|
1392
|
+
async function* tarEntries(files) {
|
|
1393
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
1394
|
+
const file = files[index];
|
|
1395
|
+
let headerName = file.path;
|
|
1396
|
+
if (encoder2.encode(file.path).byteLength > 100) {
|
|
1397
|
+
const pax = paxRecord("path", file.path);
|
|
1398
|
+
yield tarHeader(`PaxHeaders/${index}`, pax.byteLength, { type: "x", mode: 384 });
|
|
1399
|
+
yield pax;
|
|
1400
|
+
const paxPadding = tarPadding(pax.byteLength);
|
|
1401
|
+
if (paxPadding > 0) yield new Uint8Array(paxPadding);
|
|
1402
|
+
headerName = `file-${index}`;
|
|
1403
|
+
}
|
|
1404
|
+
yield tarHeader(headerName, file.bytes.byteLength);
|
|
1405
|
+
yield file.bytes;
|
|
1406
|
+
const filePadding = tarPadding(file.bytes.byteLength);
|
|
1407
|
+
if (filePadding > 0) yield new Uint8Array(filePadding);
|
|
1408
|
+
}
|
|
1409
|
+
yield new Uint8Array(TAR_TRAILER_SIZE);
|
|
1410
|
+
}
|
|
1411
|
+
async function fileSha256(path) {
|
|
1412
|
+
const hash = createHash("sha256");
|
|
1413
|
+
for await (const chunk of createReadStream(path)) hash.update(chunk);
|
|
1414
|
+
return hash.digest("hex");
|
|
1415
|
+
}
|
|
1416
|
+
async function createBundleArchive(inputDir) {
|
|
1417
|
+
const docsDir = resolve(inputDir);
|
|
1418
|
+
const info = await lstat(docsDir).catch(() => null);
|
|
1419
|
+
if (info === null || !info.isDirectory() || info.isSymbolicLink()) throw new Error(`docs directory not found: ${docsDir}`);
|
|
1420
|
+
const files = await collectSourceFiles(docsDir);
|
|
1421
|
+
const warnings = validateSourceFiles(files);
|
|
1422
|
+
const temporaryRoot = await mkdtemp(join(tmpdir(), "saturndocs-deploy-"));
|
|
1423
|
+
await mkdir(temporaryRoot, { recursive: true });
|
|
1424
|
+
const archivePath = join(temporaryRoot, "source.tar.gz");
|
|
1425
|
+
try {
|
|
1426
|
+
await pipeline(Readable.from(tarEntries(files)), createGzip({ level: 9 }), createWriteStream(archivePath, { mode: 384 }));
|
|
1427
|
+
const archiveInfo = await stat(archivePath);
|
|
1428
|
+
return {
|
|
1429
|
+
path: archivePath,
|
|
1430
|
+
byteLength: archiveInfo.size,
|
|
1431
|
+
sha256: await fileSha256(archivePath),
|
|
1432
|
+
warnings,
|
|
1433
|
+
cleanup: () => rm(temporaryRoot, { recursive: true, force: true })
|
|
1434
|
+
};
|
|
1435
|
+
} catch (error) {
|
|
1436
|
+
await rm(temporaryRoot, { recursive: true, force: true });
|
|
1437
|
+
throw error;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// src/commands/deploy.ts
|
|
1442
|
+
var DEFAULT_API_URL = "https://saturndocs-ops.saturndocs.workers.dev";
|
|
1443
|
+
var TERMINAL_STATES = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled", "superseded", "timed_out"]);
|
|
1444
|
+
function trimSlash(value) {
|
|
1445
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
1446
|
+
}
|
|
1447
|
+
async function responseValue(response) {
|
|
1448
|
+
const value = await response.json().catch(() => null);
|
|
1449
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1450
|
+
}
|
|
1451
|
+
function apiError(response, value) {
|
|
1452
|
+
const code = typeof value.code === "string" ? value.code : `http_${response.status}`;
|
|
1453
|
+
const message = typeof value.error === "string" ? value.error : "request failed";
|
|
1454
|
+
return new Error(`${code}: ${message}`);
|
|
1455
|
+
}
|
|
1456
|
+
async function deployBundle(input) {
|
|
1457
|
+
const archive = await createBundleArchive(input.docsDir);
|
|
1458
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
1459
|
+
const apiUrl = trimSlash(input.apiUrl);
|
|
1460
|
+
const idempotencyKey = input.idempotencyKey ?? randomUUID();
|
|
1461
|
+
try {
|
|
1462
|
+
let accepted = {};
|
|
1463
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1464
|
+
let response;
|
|
1465
|
+
try {
|
|
1466
|
+
response = await fetchImpl(`${apiUrl}/v1/deployments`, {
|
|
1467
|
+
method: "POST",
|
|
1468
|
+
headers: {
|
|
1469
|
+
authorization: `Bearer ${input.token}`,
|
|
1470
|
+
"content-type": "application/gzip",
|
|
1471
|
+
"content-length": String(archive.byteLength),
|
|
1472
|
+
"x-saturndocs-content-sha256": archive.sha256,
|
|
1473
|
+
"x-saturndocs-idempotency-key": idempotencyKey,
|
|
1474
|
+
"x-saturndocs-promote": String(input.promote)
|
|
1475
|
+
},
|
|
1476
|
+
body: createReadStream2(archive.path),
|
|
1477
|
+
duplex: "half",
|
|
1478
|
+
redirect: "error"
|
|
1479
|
+
});
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
if (attempt === 2) throw error;
|
|
1482
|
+
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
accepted = await responseValue(response);
|
|
1486
|
+
if (response.ok) break;
|
|
1487
|
+
if (response.status === 503 && accepted.code === "queue_unavailable" && attempt < 2) {
|
|
1488
|
+
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
throw apiError(response, accepted);
|
|
1492
|
+
}
|
|
1493
|
+
if (typeof accepted.job_id !== "string" || typeof accepted.status_url !== "string") throw new Error("deployment response is invalid");
|
|
1494
|
+
const statusUrl = new URL(accepted.status_url, `${apiUrl}/`);
|
|
1495
|
+
if (statusUrl.origin !== new URL(apiUrl).origin) throw new Error("deployment status URL has an unexpected origin");
|
|
1496
|
+
const startedAt = Date.now();
|
|
1497
|
+
const timeoutMs = input.timeoutMs ?? 9e5;
|
|
1498
|
+
let previousState = "";
|
|
1499
|
+
for (; ; ) {
|
|
1500
|
+
if (Date.now() - startedAt > timeoutMs) throw new Error(`deployment timed out after ${Math.ceil(timeoutMs / 1e3)} seconds`);
|
|
1501
|
+
const statusResponse = await fetchImpl(statusUrl, { headers: { authorization: `Bearer ${input.token}` }, redirect: "error" });
|
|
1502
|
+
const value = await responseValue(statusResponse);
|
|
1503
|
+
if (!statusResponse.ok) throw apiError(statusResponse, value);
|
|
1504
|
+
if (typeof value.job_id !== "string" || typeof value.state !== "string") throw new Error("job status response is invalid");
|
|
1505
|
+
const status = value;
|
|
1506
|
+
if (status.state !== previousState) {
|
|
1507
|
+
previousState = status.state;
|
|
1508
|
+
input.onState?.(status);
|
|
1509
|
+
}
|
|
1510
|
+
if (TERMINAL_STATES.has(status.state)) {
|
|
1511
|
+
if (status.state !== "succeeded") throw new Error(`${status.error_code ?? status.state}: deployment ${status.state}`);
|
|
1512
|
+
if (input.promote && (!Number.isSafeInteger(status.generation) || status.generation < 1)) {
|
|
1513
|
+
throw new Error("promotion_not_applied: deployment succeeded without activating the release");
|
|
1514
|
+
}
|
|
1515
|
+
return status;
|
|
1516
|
+
}
|
|
1517
|
+
await new Promise((resolve2) => setTimeout(resolve2, input.pollIntervalMs ?? 2e3));
|
|
1518
|
+
}
|
|
1519
|
+
} finally {
|
|
1520
|
+
await archive.cleanup();
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
function registerDeploy(program) {
|
|
1524
|
+
program.command("deploy").description("Publish a docs directory through the SaturnDocs operations pipeline").argument("[dir]", "docs directory (contains docs.json)", ".").option("--api-url <url>", "operations API URL", process.env.SATURNDOCS_API_URL ?? DEFAULT_API_URL).option("--token <token>", "site-bound API token", process.env.SATURNDOCS_TOKEN).option("--candidate", "build a verified candidate without activating it").option("--timeout <seconds>", "maximum wait in seconds", "900").action(async (dir, options) => {
|
|
1525
|
+
if (!options.token) throw new Error("SATURNDOCS_TOKEN or --token is required");
|
|
1526
|
+
const timeoutSeconds = Number(options.timeout);
|
|
1527
|
+
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) throw new Error("--timeout must be a positive number of seconds");
|
|
1528
|
+
try {
|
|
1529
|
+
const result = await deployBundle({
|
|
1530
|
+
docsDir: dir,
|
|
1531
|
+
apiUrl: options.apiUrl,
|
|
1532
|
+
token: options.token,
|
|
1533
|
+
promote: options.candidate !== true,
|
|
1534
|
+
timeoutMs: timeoutSeconds * 1e3,
|
|
1535
|
+
onState(status) {
|
|
1536
|
+
console.log(pc.dim(`deployment ${status.state}`));
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
const outcome = result.generation === void 0 ? "verified candidate" : `served generation ${result.generation}`;
|
|
1540
|
+
console.log(pc.green(`\u2713 ${outcome}: ${result.release_id ?? result.job_id}`));
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
console.error(pc.red(`error: ${messageOf(error)}`));
|
|
1543
|
+
process.exitCode = 1;
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
export {
|
|
1549
|
+
createBundleArchive,
|
|
1550
|
+
deployBundle,
|
|
1551
|
+
registerDeploy
|
|
1552
|
+
};
|