@open-agent-toolkit/cli 0.2.25 → 0.2.27
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/assets/NOTICES.md +156 -0
- package/assets/docs/cli-utilities/configuration.md +42 -11
- package/assets/docs/contributing/explainer-kit-verification.md +125 -0
- package/assets/docs/contributing/index.md +1 -0
- package/assets/docs/reference/troubleshooting.md +47 -0
- package/assets/docs/workflows/projects/artifacts.md +24 -6
- package/assets/docs/workflows/projects/dispatch-ceiling.md +67 -16
- package/assets/docs/workflows/skills/explainer-kit-providers.md +144 -0
- package/assets/docs/workflows/skills/explainer-kit.md +121 -69
- package/assets/docs/workflows/skills/index.md +1 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +18 -3
- package/assets/skills/explainer-kit/recipes/project-recap.json +43 -16
- package/assets/skills/explainer-kit/references/contracts.md +167 -20
- package/assets/skills/explainer-kit/references/golden-conformance.md +80 -0
- package/assets/skills/explainer-kit/references/visual-authoring.md +92 -0
- package/assets/skills/explainer-kit/references/visual-review.md +57 -0
- package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +172 -1
- package/assets/skills/explainer-kit/schemas/build-record.schema.json +7 -1
- package/assets/skills/explainer-kit/schemas/fact-base.schema.json +38 -2
- package/assets/skills/explainer-kit/schemas/manifest.schema.json +25 -1
- package/assets/skills/explainer-kit/schemas/run-request.schema.json +4 -0
- package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +149 -0
- package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +117 -0
- package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +80 -0
- package/assets/skills/explainer-kit/scripts/lib/browser-runtime.mjs +148 -4
- package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +243 -0
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +586 -8
- package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +285 -8
- package/assets/skills/explainer-kit/scripts/lib/durability.mjs +35 -0
- package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +144 -8
- package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +379 -0
- package/assets/skills/explainer-kit/scripts/lib/png.mjs +287 -0
- package/assets/skills/explainer-kit/scripts/lib/qa.mjs +280 -8
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +132 -3
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +513 -21
- package/assets/skills/explainer-kit/scripts/lib/render.mjs +67 -3
- package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +43 -1
- package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +208 -0
- package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +218 -0
- package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +380 -0
- package/assets/skills/explainer-kit/scripts/render-qa.mjs +48 -10
- package/assets/skills/explainer-kit/scripts/run.mjs +859 -134
- package/assets/skills/oat-explainer-kit/SKILL.md +40 -12
- package/assets/skills/oat-explainer-kit/references/author-callback.md +12 -10
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +40 -2
- package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +72 -0
- package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +167 -5
- package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +92 -5
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +324 -2
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +1 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-implement/SKILL.md +9 -11
- package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +18 -9
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-implement/references/phase-execution.md +13 -4
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +1 -1
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +27 -3
- package/dist/commands/project/archive/archive-utils.d.ts +1 -0
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +109 -42
- package/dist/commands/project/archive/explainer-package-coverage.d.ts +14 -0
- package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -0
- package/dist/commands/project/archive/explainer-package-coverage.js +27 -0
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts +18 -0
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +1 -0
- package/dist/commands/project/archive/explainer-source-backlinks.js +27 -0
- package/dist/commands/project/archive/push-runner.d.ts +2 -1
- package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
- package/dist/commands/project/archive/push-runner.js +5 -1
- package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
- package/dist/commands/project/dispatch-ceiling/index.js +90 -0
- package/dist/config/dispatch-notices.d.ts +8 -0
- package/dist/config/dispatch-notices.d.ts.map +1 -0
- package/dist/config/dispatch-notices.js +79 -0
- package/dist/config/dispatch-policy-options.d.ts +2 -0
- package/dist/config/dispatch-policy-options.d.ts.map +1 -1
- package/dist/config/dispatch-policy-options.js +14 -2
- package/dist/providers/identity/dispatch-report.d.ts +17 -0
- package/dist/providers/identity/dispatch-report.d.ts.map +1 -1
- package/dist/providers/identity/dispatch-report.js +30 -0
- package/dist/release/public-package-contract.d.ts +6 -0
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +75 -0
- package/package.json +2 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
4
|
import { createServer } from 'node:http';
|
|
5
|
+
import { dirname } from 'node:path';
|
|
4
6
|
|
|
5
7
|
// Locations a headless Chromium may already exist at on a developer or CI
|
|
6
8
|
// machine. The core never installs a browser; it only uses one that is there.
|
|
@@ -23,6 +25,7 @@ const PROBE_REQUEST_FIELDS = new Set([
|
|
|
23
25
|
'media',
|
|
24
26
|
'reducedMotion',
|
|
25
27
|
'scenario',
|
|
28
|
+
'screenshotPath',
|
|
26
29
|
'themeToggle',
|
|
27
30
|
'viewport',
|
|
28
31
|
'wideContent',
|
|
@@ -34,6 +37,13 @@ const DISABLE_ANIMATIONS_CSS = `*, *::before, *::after {
|
|
|
34
37
|
transition-duration: 0s !important;
|
|
35
38
|
scroll-behavior: auto !important;
|
|
36
39
|
}`;
|
|
40
|
+
const BROWSER_SESSION_BRANDS = new WeakSet();
|
|
41
|
+
const CAPTURE_SETTINGS = deepFreeze({
|
|
42
|
+
format: 'png',
|
|
43
|
+
fullPage: false,
|
|
44
|
+
reducedMotion: 'reduce',
|
|
45
|
+
animationsDisabled: true,
|
|
46
|
+
});
|
|
37
47
|
|
|
38
48
|
export const RUNTIME_UNAVAILABLE_REASONS = Object.freeze({
|
|
39
49
|
driverMissing: 'browser-driver-not-installed',
|
|
@@ -134,6 +144,13 @@ export async function createBrowserProbeSession(options = {}) {
|
|
|
134
144
|
}
|
|
135
145
|
|
|
136
146
|
const browser = await runtime.launch();
|
|
147
|
+
let browserRuntime;
|
|
148
|
+
try {
|
|
149
|
+
browserRuntime = launchedBrowserRuntime(browser);
|
|
150
|
+
} catch (cause) {
|
|
151
|
+
await browser.close();
|
|
152
|
+
throw cause;
|
|
153
|
+
}
|
|
137
154
|
const pages = new Map();
|
|
138
155
|
const server = createServer((request, response) => {
|
|
139
156
|
const html = pages.get(request.url);
|
|
@@ -156,9 +173,10 @@ export async function createBrowserProbeSession(options = {}) {
|
|
|
156
173
|
throw cause;
|
|
157
174
|
}
|
|
158
175
|
|
|
159
|
-
return {
|
|
176
|
+
return brandBrowserProbeSession({
|
|
160
177
|
available: true,
|
|
161
|
-
runtime:
|
|
178
|
+
runtime: browserRuntime,
|
|
179
|
+
capture: CAPTURE_SETTINGS,
|
|
162
180
|
probe: async (probeRequest) => {
|
|
163
181
|
const route = `/${randomBytes(12).toString('hex')}.html`;
|
|
164
182
|
pages.set(route, probeRequest.artifact.html);
|
|
@@ -175,7 +193,49 @@ export async function createBrowserProbeSession(options = {}) {
|
|
|
175
193
|
close: async () => {
|
|
176
194
|
await Promise.allSettled([browser.close(), closeServer(server)]);
|
|
177
195
|
},
|
|
178
|
-
};
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Deterministic probes are useful for bounded unit and integration tests, but
|
|
201
|
+
* they are never trusted production browser evidence.
|
|
202
|
+
*/
|
|
203
|
+
export function createFixtureBrowserProbeSession({ probe } = {}) {
|
|
204
|
+
if (typeof probe !== 'function') {
|
|
205
|
+
throw new TypeError('Fixture browser sessions require a probe callback.');
|
|
206
|
+
}
|
|
207
|
+
return brandBrowserProbeSession({
|
|
208
|
+
available: true,
|
|
209
|
+
runtime: {
|
|
210
|
+
kind: 'fixture',
|
|
211
|
+
name: 'chromium',
|
|
212
|
+
version: 'deterministic-fixture',
|
|
213
|
+
},
|
|
214
|
+
capture: CAPTURE_SETTINGS,
|
|
215
|
+
probe,
|
|
216
|
+
close: async () => {},
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function assertBrowserProbeSession(
|
|
221
|
+
session,
|
|
222
|
+
{ allowFixture = false } = {},
|
|
223
|
+
) {
|
|
224
|
+
if (
|
|
225
|
+
!session ||
|
|
226
|
+
typeof session !== 'object' ||
|
|
227
|
+
!BROWSER_SESSION_BRANDS.has(session)
|
|
228
|
+
) {
|
|
229
|
+
throw new TypeError(
|
|
230
|
+
'Browser evidence requires a trusted browser session brand created by createBrowserProbeSession().',
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (session.runtime.kind === 'fixture' && !allowFixture) {
|
|
234
|
+
throw new TypeError(
|
|
235
|
+
'Deterministic fixture browser sessions are not allowed for production evidence.',
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return session;
|
|
179
239
|
}
|
|
180
240
|
|
|
181
241
|
export async function probeRenderedPage(browser, url, request) {
|
|
@@ -222,6 +282,13 @@ export async function probeRenderedPage(browser, url, request) {
|
|
|
222
282
|
const themeToggle = request.themeToggle
|
|
223
283
|
? await probeThemeToggle(page, request.themeToggle)
|
|
224
284
|
: layout.themeToggle;
|
|
285
|
+
if (request.screenshotPath) {
|
|
286
|
+
await mkdir(dirname(request.screenshotPath), { recursive: true });
|
|
287
|
+
await page.screenshot({
|
|
288
|
+
path: request.screenshotPath,
|
|
289
|
+
fullPage: false,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
225
292
|
return {
|
|
226
293
|
...layout,
|
|
227
294
|
keyboard,
|
|
@@ -256,6 +323,13 @@ function assertProbeRequestFields(request) {
|
|
|
256
323
|
) {
|
|
257
324
|
throw new TypeError('Browser probe injectedCss must be a string.');
|
|
258
325
|
}
|
|
326
|
+
if (
|
|
327
|
+
request.screenshotPath !== undefined &&
|
|
328
|
+
(typeof request.screenshotPath !== 'string' ||
|
|
329
|
+
request.screenshotPath.length === 0)
|
|
330
|
+
) {
|
|
331
|
+
throw new TypeError('Browser probe screenshotPath must be a path string.');
|
|
332
|
+
}
|
|
259
333
|
}
|
|
260
334
|
|
|
261
335
|
function probeStylesheet(request) {
|
|
@@ -440,3 +514,73 @@ function listen(server) {
|
|
|
440
514
|
function closeServer(server) {
|
|
441
515
|
return new Promise((resolveClose) => server.close(resolveClose));
|
|
442
516
|
}
|
|
517
|
+
|
|
518
|
+
function launchedBrowserRuntime(browser) {
|
|
519
|
+
const name = browser?.browserType?.().name?.();
|
|
520
|
+
const version = browser?.version?.();
|
|
521
|
+
if (
|
|
522
|
+
name !== 'chromium' ||
|
|
523
|
+
typeof version !== 'string' ||
|
|
524
|
+
version.trim().length === 0
|
|
525
|
+
) {
|
|
526
|
+
throw new TypeError(
|
|
527
|
+
'Launched Chromium did not expose a trusted browser type and version.',
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
return deepFreeze({
|
|
531
|
+
kind: 'launched',
|
|
532
|
+
name,
|
|
533
|
+
version: version.trim(),
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function brandBrowserProbeSession({
|
|
538
|
+
available,
|
|
539
|
+
runtime,
|
|
540
|
+
capture,
|
|
541
|
+
probe,
|
|
542
|
+
close,
|
|
543
|
+
}) {
|
|
544
|
+
const trustedRuntime = deepFreeze(structuredClone(runtime));
|
|
545
|
+
const trustedCapture = deepFreeze(structuredClone(capture));
|
|
546
|
+
const session = Object.freeze({
|
|
547
|
+
available,
|
|
548
|
+
runtime: trustedRuntime,
|
|
549
|
+
capture: trustedCapture,
|
|
550
|
+
captureIdentity: browserCaptureIdentity(trustedRuntime, trustedCapture),
|
|
551
|
+
probe,
|
|
552
|
+
close,
|
|
553
|
+
});
|
|
554
|
+
BROWSER_SESSION_BRANDS.add(session);
|
|
555
|
+
return session;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function browserCaptureIdentity(runtime, capture) {
|
|
559
|
+
return `sha256:${createHash('sha256')
|
|
560
|
+
.update(canonicalStringify({ runtime, capture }))
|
|
561
|
+
.digest('hex')}`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function canonicalStringify(value) {
|
|
565
|
+
return JSON.stringify(canonicalize(value));
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function canonicalize(value) {
|
|
569
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
570
|
+
if (value && typeof value === 'object') {
|
|
571
|
+
return Object.fromEntries(
|
|
572
|
+
Object.keys(value)
|
|
573
|
+
.sort()
|
|
574
|
+
.map((key) => [key, canonicalize(value[key])]),
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
return value;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function deepFreeze(value) {
|
|
581
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) {
|
|
582
|
+
return value;
|
|
583
|
+
}
|
|
584
|
+
for (const nested of Object.values(value)) deepFreeze(nested);
|
|
585
|
+
return Object.freeze(value);
|
|
586
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { canonicalStringify, validateContract } from './contracts.mjs';
|
|
2
|
+
|
|
3
|
+
const CATALOG_SCHEMA_VERSION = 'explainer-kit.initiative-catalog/v1';
|
|
4
|
+
|
|
5
|
+
export function initiativeCatalogPath(slug) {
|
|
6
|
+
if (typeof slug !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
|
|
7
|
+
throw new TypeError('Initiative catalog requires a safe initiative slug.');
|
|
8
|
+
}
|
|
9
|
+
return `site/initiatives/${slug}/catalog.json`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function catalogFromManifest(manifest, publicBaseUrl) {
|
|
13
|
+
const validation = validateContract('manifest', manifest);
|
|
14
|
+
if (!validation.valid) {
|
|
15
|
+
throw new TypeError(
|
|
16
|
+
`Initiative catalog requires a valid finalized manifest: ${validation.errors[0].message}`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
const baseUrl = normalizePublicBaseUrl(publicBaseUrl);
|
|
20
|
+
const artifacts = manifest.artifacts.map((artifact) => {
|
|
21
|
+
if (
|
|
22
|
+
artifact.status !== 'built' ||
|
|
23
|
+
typeof artifact.renderedPath !== 'string' ||
|
|
24
|
+
!artifact.renderedPath.startsWith('site/') ||
|
|
25
|
+
typeof artifact.hash !== 'string'
|
|
26
|
+
) {
|
|
27
|
+
throw new TypeError(
|
|
28
|
+
`Initiative catalog cannot publish non-built artifact ${artifact.id}.`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
id: artifact.id,
|
|
33
|
+
type: artifact.type,
|
|
34
|
+
status: artifact.status,
|
|
35
|
+
renderedPath: artifact.renderedPath,
|
|
36
|
+
...(artifact.mediaType && { mediaType: artifact.mediaType }),
|
|
37
|
+
hash: artifact.hash,
|
|
38
|
+
url: absoluteArtifactUrl(baseUrl, artifact.renderedPath),
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
schemaVersion: CATALOG_SCHEMA_VERSION,
|
|
44
|
+
runId: manifest.runId,
|
|
45
|
+
slug: manifest.slug,
|
|
46
|
+
recipe: structuredClone(manifest.recipe),
|
|
47
|
+
createdAt: manifest.createdAt,
|
|
48
|
+
artifacts,
|
|
49
|
+
sourceBacklinks: structuredClone(manifest.source.backlinks ?? []),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function validateInitiativeCatalog(catalog, manifest, publicBaseUrl) {
|
|
54
|
+
const errors = [];
|
|
55
|
+
let normalizedPublicBaseUrl;
|
|
56
|
+
try {
|
|
57
|
+
normalizedPublicBaseUrl = normalizePublicBaseUrl(publicBaseUrl);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
add(
|
|
60
|
+
errors,
|
|
61
|
+
'$.artifacts',
|
|
62
|
+
'catalog-public-root',
|
|
63
|
+
error instanceof Error
|
|
64
|
+
? error.message
|
|
65
|
+
: 'Initiative catalog requires an HTTPS public base URL.',
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (!catalog || typeof catalog !== 'object' || Array.isArray(catalog)) {
|
|
69
|
+
return {
|
|
70
|
+
valid: false,
|
|
71
|
+
errors: [
|
|
72
|
+
{
|
|
73
|
+
path: '$',
|
|
74
|
+
code: 'catalog-shape',
|
|
75
|
+
message: 'Initiative catalog must be an object.',
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const expectedRootKeys = new Set([
|
|
81
|
+
'schemaVersion',
|
|
82
|
+
'runId',
|
|
83
|
+
'slug',
|
|
84
|
+
'recipe',
|
|
85
|
+
'createdAt',
|
|
86
|
+
'artifacts',
|
|
87
|
+
'sourceBacklinks',
|
|
88
|
+
]);
|
|
89
|
+
if (Object.keys(catalog).some((key) => !expectedRootKeys.has(key))) {
|
|
90
|
+
add(
|
|
91
|
+
errors,
|
|
92
|
+
'$',
|
|
93
|
+
'catalog-shape',
|
|
94
|
+
'Initiative catalog contains an unknown property.',
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const [field, expected] of [
|
|
99
|
+
['schemaVersion', CATALOG_SCHEMA_VERSION],
|
|
100
|
+
['runId', manifest?.runId],
|
|
101
|
+
['slug', manifest?.slug],
|
|
102
|
+
['createdAt', manifest?.createdAt],
|
|
103
|
+
]) {
|
|
104
|
+
if (catalog[field] !== expected) {
|
|
105
|
+
add(
|
|
106
|
+
errors,
|
|
107
|
+
`$.${field}`,
|
|
108
|
+
'catalog-manifest-mismatch',
|
|
109
|
+
`Catalog ${field} does not match the finalized manifest.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
canonicalStringify(catalog.recipe) !== canonicalStringify(manifest?.recipe)
|
|
115
|
+
) {
|
|
116
|
+
add(
|
|
117
|
+
errors,
|
|
118
|
+
'$.recipe',
|
|
119
|
+
'catalog-manifest-mismatch',
|
|
120
|
+
'Catalog recipe does not match the finalized manifest.',
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const catalogArtifacts = Array.isArray(catalog.artifacts)
|
|
125
|
+
? catalog.artifacts
|
|
126
|
+
: [];
|
|
127
|
+
const manifestArtifacts = Array.isArray(manifest?.artifacts)
|
|
128
|
+
? manifest.artifacts
|
|
129
|
+
: [];
|
|
130
|
+
if (
|
|
131
|
+
!Array.isArray(catalog.artifacts) ||
|
|
132
|
+
catalogArtifacts.length !== manifestArtifacts.length
|
|
133
|
+
) {
|
|
134
|
+
add(
|
|
135
|
+
errors,
|
|
136
|
+
'$.artifacts',
|
|
137
|
+
'catalog-artifact-parity',
|
|
138
|
+
'Catalog artifacts must exactly cover the finalized manifest artifacts.',
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
for (const [index, artifact] of manifestArtifacts.entries()) {
|
|
142
|
+
const entry = catalogArtifacts[index];
|
|
143
|
+
const expected = {
|
|
144
|
+
id: artifact.id,
|
|
145
|
+
type: artifact.type,
|
|
146
|
+
status: artifact.status,
|
|
147
|
+
renderedPath: artifact.renderedPath,
|
|
148
|
+
...(artifact.mediaType && { mediaType: artifact.mediaType }),
|
|
149
|
+
hash: artifact.hash,
|
|
150
|
+
url:
|
|
151
|
+
normalizedPublicBaseUrl === undefined
|
|
152
|
+
? undefined
|
|
153
|
+
: absoluteArtifactUrl(normalizedPublicBaseUrl, artifact.renderedPath),
|
|
154
|
+
};
|
|
155
|
+
const actual = entry && typeof entry === 'object' ? entry : undefined;
|
|
156
|
+
if (canonicalStringify(actual) !== canonicalStringify(expected)) {
|
|
157
|
+
add(
|
|
158
|
+
errors,
|
|
159
|
+
`$.artifacts[${index}]`,
|
|
160
|
+
'catalog-artifact-mismatch',
|
|
161
|
+
`Catalog artifact ${artifact.id} is stale or does not match the finalized manifest.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (
|
|
167
|
+
canonicalStringify(catalog.sourceBacklinks) !==
|
|
168
|
+
canonicalStringify(manifest?.source?.backlinks ?? [])
|
|
169
|
+
) {
|
|
170
|
+
add(
|
|
171
|
+
errors,
|
|
172
|
+
'$.sourceBacklinks',
|
|
173
|
+
'catalog-source-mismatch',
|
|
174
|
+
'Catalog source backlinks do not match the finalized manifest.',
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
for (const [index, backlink] of (Array.isArray(catalog.sourceBacklinks)
|
|
178
|
+
? catalog.sourceBacklinks
|
|
179
|
+
: []
|
|
180
|
+
).entries()) {
|
|
181
|
+
if (
|
|
182
|
+
typeof backlink?.sourceId !== 'string' ||
|
|
183
|
+
!isImmutableGithubUrl(backlink?.url)
|
|
184
|
+
) {
|
|
185
|
+
add(
|
|
186
|
+
errors,
|
|
187
|
+
`$.sourceBacklinks[${index}]`,
|
|
188
|
+
'catalog-source-url',
|
|
189
|
+
'Catalog source backlinks must be immutable absolute GitHub URLs.',
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { valid: errors.length === 0, errors };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function serializeInitiativeCatalog(catalog) {
|
|
198
|
+
return `${JSON.stringify(catalog, null, 2)}\n`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizePublicBaseUrl(value) {
|
|
202
|
+
let url;
|
|
203
|
+
try {
|
|
204
|
+
url = new URL(value);
|
|
205
|
+
} catch {
|
|
206
|
+
throw new TypeError(
|
|
207
|
+
'Initiative catalog requires an HTTPS public base URL.',
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (
|
|
211
|
+
url.protocol !== 'https:' ||
|
|
212
|
+
url.username ||
|
|
213
|
+
url.password ||
|
|
214
|
+
url.search ||
|
|
215
|
+
url.hash
|
|
216
|
+
) {
|
|
217
|
+
throw new TypeError(
|
|
218
|
+
'Initiative catalog requires credential-free HTTPS without query or fragment.',
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return url.href.replace(/\/+$/, '');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function absoluteArtifactUrl(baseUrl, renderedPath) {
|
|
225
|
+
return `${baseUrl}/${renderedPath
|
|
226
|
+
.slice('site/'.length)
|
|
227
|
+
.split('/')
|
|
228
|
+
.map((part) => encodeURIComponent(part))
|
|
229
|
+
.join('/')}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isImmutableGithubUrl(value) {
|
|
233
|
+
return (
|
|
234
|
+
typeof value === 'string' &&
|
|
235
|
+
/^https:\/\/github\.com\/[^/]+\/[^/]+\/blob\/[a-f0-9]{40}\/.+#L[1-9][0-9]*(?:-L[1-9][0-9]*)?$/.test(
|
|
236
|
+
value,
|
|
237
|
+
)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function add(errors, path, code, message) {
|
|
242
|
+
errors.push({ path, code, message });
|
|
243
|
+
}
|