feat(eve): enforce extension capability compatibility (#931)

Signed-off-by: Casey Gowrie <ctgowrie@gmail.com>
This commit is contained in:
Casey Gowrie
2026-07-20 10:42:13 -04:00
committed by GitHub
parent c61f8cae0c
commit 8e9990b6f2
40 changed files with 1716 additions and 96 deletions
+1
View File
@@ -14,6 +14,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
persist-credentials: false
- name: Setup pnpm
+1
View File
@@ -16,6 +16,7 @@ coverage/
# Test artifacts
.vitest/
.extension-contracts-cache/
packages/eve/.workflow-vitest/
packages/eve/.generated/
playwright-report/
+30
View File
@@ -92,6 +92,36 @@ pnpm docs:check # docs frontmatter and nav validation
All of these run in CI, so running them locally before pushing saves a round trip.
### Extension capability contracts
The extension capabilities in
[`extension-compatibility.ts`](./packages/eve/src/compiler/extension-compatibility.ts)
have immutable API reports keyed by epoch. If an extension-facing type or
signature changes, CI fails with the affected capability. Classify whether the
new consumer retains the previous epoch while bumping it automatically:
```bash
pnpm update:extension-contracts --update hook
```
The command bumps changes it can prove structurally backward compatible,
retains the previous epoch, and scaffolds the required fixture under
`packages/eve/extension-contracts/compatibility/`. Replace the scaffold with a
representative example of the retained authoring contract, then rerun
`pnpm update:extension-contracts` to generate the new epoch report. If the
change cannot be classified automatically, pass `--retain` after verifying
runtime compatibility. To stop accepting the previous epoch, pass
`--drop "why the old contract cannot run"`; this bumps the capability and
records the reason.
Every historical epoch must be classified exactly once as supported or dropped.
Supported historical epochs require compiling fixtures. Each epoch also retains
a readable `vN.api.md` declaration report and compact `vN.json` metadata; do not
edit or delete either file after merge. The invariant guard verifies the support
history, fixtures, report integrity, and assignment of every public authoring
export to a capability. Reports and fixtures cover structural compatibility;
behavior changes still need focused compatibility tests.
## Documentation
User-facing docs live in [`docs/`](./docs) and are published with the `eve` npm package and rendered by the docs site in [`apps/docs`](./apps/docs). If your change alters public behavior, update the relevant doc in the same PR and run `pnpm docs:check`.
+14 -1
View File
@@ -85,7 +85,7 @@ Declare separate authoring and distribution roots and run `eve extension build`
},
"files": ["dist"],
"peerDependencies": { "eve": "*" },
"devDependencies": { "eve": "^x", "typescript": "^x" },
"devDependencies": { "eve": "x.y.z", "typescript": "^x" },
"dependencies": { "zod": "^3" },
"scripts": { "build": "eve extension build", "prepare": "eve extension build" },
}
@@ -120,6 +120,19 @@ During local development, `eve dev` automatically rebuilds mounted workspace ext
`eve` is a required wildcard **peer** dependency: one eve lives in the consuming app and the extension's `eve/*` imports resolve to it. The extension's concrete eve version belongs in `devDependencies` for authoring types and build tooling. npm peer semver does not decide extension compatibility; eve validates the generated per-capability requirements. Do not mark the eve peer optional and do not add eve to regular `dependencies`.
`eve extension init` pins `devDependencies.eve` to the exact eve release that created the scaffold:
```jsonc title="package.json"
{
"peerDependencies": { "eve": "*" },
"devDependencies": { "eve": "0.25.0" },
}
```
The exact pin makes builds reproducible. Keep it until the extension intentionally upgrades its eve authoring API. `eve extension build` records the capability contracts required by the build, and each consuming eve validates those requirements before it runs the extension.
If the extension must support an older eve release, replace the development pin with that exact version, then typecheck and build with it. Test the same dist against both the oldest and latest supported consumers; rebuilding with each consumer would produce and test different artifacts.
Everything else the extension imports at execution time (SDKs, `zod`, …) goes in `dependencies`; each extension resolves its own versions. Build-only and test-only packages go in `devDependencies`.
Those deps resolve from `node_modules` under `eve dev`/`eve eval` and are bundled into the deployable by the consuming agent's `eve build`. A dependency that can't be bundled (a native addon) must be listed in the **consuming agent's** `build.externalDependencies` — an extension can't declare build config, so note it in your README.
+3 -1
View File
@@ -31,10 +31,12 @@
"typecheck": "turbo run typecheck",
"version-packages": "changeset version",
"release": "pnpm build && node ./scripts/assert-changeset-publish-packages.mjs && changeset publish",
"test:tui": "pnpm --filter eve run test:tui"
"test:tui": "pnpm --filter eve run test:tui",
"update:extension-contracts": "node ./scripts/extension-capability-contracts.mjs"
},
"devDependencies": {
"@changesets/cli": "2.31.0",
"@microsoft/api-extractor": "7.58.10",
"@types/node": "catalog:",
"ai": "catalog:",
"gray-matter": "4.0.3",
@@ -0,0 +1,29 @@
# Retained capability compatibility
When eve continues to support an older extension capability epoch, add an
authored TypeScript fixture at `<capability>/v<epoch>.ts`. The fixture must use
the old contract in a representative way and continues to compile against the
current eve API whenever capability contracts are checked.
Only compact API hashes and authoring roots are committed for each epoch. When
the current hash changes, the updater regenerates the previous API from the Git
commit that recorded that epoch and compares it with the working tree. Full API
Extractor reports remain temporary build artifacts.
Capability entrypoints root extraction at public authoring values such as
`defineTool` and `defineHook`; API Extractor follows every type reachable from
their signatures. Export a type from an entrypoint only when it is a standalone
extension API that no authoring value reaches. The invariant checks both value
ownership and type reachability.
`pnpm update:extension-contracts --update <capability>` retains the previous
epoch automatically when its declaration change is structurally backward
compatible, updates the support table, and creates a marked scaffold at the
required path. Replace that scaffold with the representative example before
rerunning `pnpm update:extension-contracts` to create the new epoch metadata. Use
`--retain` to confirm a change the classifier cannot prove, or `--drop "reason"`
when the current consumer cannot run the previous epoch.
Keep the fixture immutable once merged. Structural compatibility is only one
part of consumer support, so retain focused runtime coverage for any behavior
that changed across the epoch boundary.
@@ -0,0 +1 @@
export { defineExtension } from "../../src/public/extension/index.ts";
@@ -0,0 +1,9 @@
export {
ConnectionAuthorizationFailedError,
ConnectionAuthorizationRequiredError,
defineInteractiveAuthorization,
defineMcpClientConnection,
defineOpenAPIConnection,
isConnectionAuthorizationFailedError,
isConnectionAuthorizationRequiredError,
} from "../../src/public/connections/index.ts";
@@ -0,0 +1 @@
export { defineDynamic } from "../../src/public/instructions/index.ts";
@@ -0,0 +1 @@
export { defineDynamic } from "../../src/public/skills/index.ts";
@@ -0,0 +1,7 @@
export {
type DynamicToolEntry,
type DynamicToolEvents,
type DynamicToolResult,
type DynamicToolSet,
defineDynamic,
} from "../../src/public/tools/index.ts";
@@ -0,0 +1 @@
export { defineExtension } from "../../src/public/extension/index.ts";
@@ -0,0 +1 @@
export { defineHook } from "../../src/public/hooks/index.ts";
@@ -0,0 +1 @@
export { defineInstructions } from "../../src/public/instructions/index.ts";
@@ -0,0 +1,6 @@
export {
type NamedSkillDefinition,
type SkillFile,
type SkillHandle,
defineSkill,
} from "../../src/public/skills/index.ts";
@@ -0,0 +1,9 @@
export {
type Session,
type SessionAuth,
type SessionAuthContext,
type SessionContext,
type SessionParent,
type SessionTurn,
defineState,
} from "../../src/public/context/index.ts";
@@ -0,0 +1,13 @@
export {
defineBashTool,
defineGlobTool,
defineGrepTool,
defineReadFileTool,
defineTool,
defineWriteFileTool,
disableTool,
experimental_workflow,
isDisabledToolSentinel,
isExperimentalWorkflowToolDefinition,
toolResultFrom,
} from "../../src/public/tools/index.ts";
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "config",
"epoch": 1,
"sha256": "c6caa66908198514041a0e9f0860de217576e4951c5909d341056814f4287d8f",
"exports": ["defineExtension"]
}
@@ -0,0 +1,15 @@
{
"kind": "eve-extension-capability-contract",
"capability": "connection",
"epoch": 1,
"sha256": "9ecdadda8da5ef4574cd5889644c85f3b907ba7c6cf724d39867c7b65894b8a7",
"exports": [
"ConnectionAuthorizationFailedError",
"ConnectionAuthorizationRequiredError",
"defineInteractiveAuthorization",
"defineMcpClientConnection",
"defineOpenAPIConnection",
"isConnectionAuthorizationFailedError",
"isConnectionAuthorizationRequiredError"
]
}
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicInstructions",
"epoch": 1,
"sha256": "f3828b64739c7505e4bcdea8bebce7a9fcb310cc04eb2e58de85205d4e81ae39",
"exports": ["defineDynamic"]
}
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicSkill",
"epoch": 1,
"sha256": "f3828b64739c7505e4bcdea8bebce7a9fcb310cc04eb2e58de85205d4e81ae39",
"exports": ["defineDynamic"]
}
@@ -0,0 +1,13 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicTool",
"epoch": 1,
"sha256": "87bb11461baf2b14e80f4df50fd22b5a7d8e6b5b898c708878379bace429773f",
"exports": [
"DynamicToolEntry",
"DynamicToolEvents",
"DynamicToolResult",
"DynamicToolSet",
"defineDynamic"
]
}
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "extension",
"epoch": 1,
"sha256": "c6caa66908198514041a0e9f0860de217576e4951c5909d341056814f4287d8f",
"exports": ["defineExtension"]
}
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "hook",
"epoch": 1,
"sha256": "eac172cde4b34723a99ba388dcee42a93936133ffd15090211ec6eb07a4df582",
"exports": ["defineHook"]
}
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "instructions",
"epoch": 1,
"sha256": "af9586d38fd6b183dbf374bae736b57bc0f81ae6ee9105dcfad7345bab26d4b9",
"exports": ["defineInstructions"]
}
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "skill",
"epoch": 1,
"sha256": "10faaba6e2a29a612c3ede712f2f62066cc534ec5cd94047f699e4a7e8bd6331",
"exports": ["NamedSkillDefinition", "SkillFile", "SkillHandle", "defineSkill"]
}
@@ -0,0 +1,15 @@
{
"kind": "eve-extension-capability-contract",
"capability": "state",
"epoch": 1,
"sha256": "2a8d0743c25dd626179e4410b5ecb46e40a2a46fab4ff2364d30e0fa304bc1e7",
"exports": [
"Session",
"SessionAuth",
"SessionAuthContext",
"SessionContext",
"SessionParent",
"SessionTurn",
"defineState"
]
}
@@ -0,0 +1,19 @@
{
"kind": "eve-extension-capability-contract",
"capability": "tool",
"epoch": 1,
"sha256": "805a48f0dadf19d43436c7d7525d54b08af0e21e320d8507d4a911f6a464a274",
"exports": [
"defineBashTool",
"defineGlobTool",
"defineGrepTool",
"defineReadFileTool",
"defineTool",
"defineWriteFileTool",
"disableTool",
"experimental_workflow",
"isDisabledToolSentinel",
"isExperimentalWorkflowToolDefinition",
"toolResultFrom"
]
}
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../tsconfig.build.json",
"compilerOptions": {
"rootDir": ".."
},
"include": ["entrypoints/**/*.ts", "compatibility/**/*.ts"],
"exclude": []
}
@@ -116,7 +116,7 @@ describe("runExtensionInitCommand", () => {
expect(packageJson.files).toEqual(["dist"]);
expect(packageJson.peerDependencies?.eve).toBe("*");
expect(packageJson.peerDependenciesMeta).toBeUndefined();
expect(packageJson.devDependencies?.eve).toBe("^0.6.0");
expect(packageJson.devDependencies?.eve).toBe("0.6.0");
expect(packageJson.dependencies?.zod).toBe("4.0.0");
expect(packageJson.dependencies?.ai).toBeUndefined();
expect(packageJson.scripts?.build).toBe("eve extension build");
@@ -84,9 +84,41 @@ describe("extension compatibility manifest", () => {
]);
});
it("supports every capability version it stamps", () => {
it("publishes valid support history for every capability version it stamps", () => {
for (const [capability, version] of Object.entries(EXTENSION_CAPABILITY_VERSIONS)) {
expect(EXTENSION_CAPABILITY_SUPPORT[capability as ExtensionCapability]).toContain(version);
const supportedVersions = EXTENSION_CAPABILITY_SUPPORT[capability as ExtensionCapability];
expect(supportedVersions).toContain(version);
expect(supportedVersions).toEqual(
[...new Set(supportedVersions)].sort((left, right) => left - right),
);
expect(supportedVersions.every((supported) => supported > 0 && supported <= version)).toBe(
true,
);
}
});
it("accepts every advertised capability epoch and rejects the next epoch", () => {
for (const [capability, supportedVersions] of Object.entries(EXTENSION_CAPABILITY_SUPPORT)) {
for (const supportedVersion of supportedVersions) {
expect(
findUnsupportedExtensionCapabilities({
kind: EXTENSION_COMPATIBILITY_MANIFEST_KIND,
formatVersion: EXTENSION_COMPATIBILITY_MANIFEST_FORMAT_VERSION,
builtWithEve: "0.25.1",
requires: { [capability]: supportedVersion },
}),
).toEqual([]);
}
const unsupportedVersion = Math.max(...supportedVersions) + 1;
expect(
findUnsupportedExtensionCapabilities({
kind: EXTENSION_COMPATIBILITY_MANIFEST_KIND,
formatVersion: EXTENSION_COMPATIBILITY_MANIFEST_FORMAT_VERSION,
builtWithEve: "0.25.1",
requires: { [capability]: unsupportedVersion },
}),
).toEqual([{ capability, requiredVersion: unsupportedVersion, supportedVersions }]);
}
});
});
@@ -13,54 +13,54 @@ export const EXTENSION_COMPATIBILITY_MANIFEST_FORMAT_VERSION = 1;
/** Filename emitted at the root of an extension's agent-shaped dist tree. */
export const EXTENSION_COMPATIBILITY_MANIFEST_FILENAME = "_manifest.json";
/** Current producer contract version for each extension-facing capability. */
export const EXTENSION_CAPABILITY_VERSIONS = {
extension: 1,
tool: 1,
dynamicTool: 1,
connection: 1,
hook: 1,
skill: 1,
dynamicSkill: 1,
instructions: 1,
dynamicInstructions: 1,
config: 1,
state: 1,
} as const;
interface ExtensionCapabilityContract {
readonly current: number;
readonly supported: readonly number[];
readonly dropped: Readonly<Record<number, string>>;
}
const EXTENSION_CAPABILITY_CONTRACTS = {
extension: { current: 1, supported: [1], dropped: {} },
tool: { current: 1, supported: [1], dropped: {} },
dynamicTool: { current: 1, supported: [1], dropped: {} },
connection: { current: 1, supported: [1], dropped: {} },
hook: { current: 1, supported: [1], dropped: {} },
skill: { current: 1, supported: [1], dropped: {} },
dynamicSkill: { current: 1, supported: [1], dropped: {} },
instructions: { current: 1, supported: [1], dropped: {} },
dynamicInstructions: { current: 1, supported: [1], dropped: {} },
config: { current: 1, supported: [1], dropped: {} },
state: { current: 1, supported: [1], dropped: {} },
} as const satisfies Record<string, ExtensionCapabilityContract>;
/** One independently versioned extension-facing contract. */
export type ExtensionCapability = keyof typeof EXTENSION_CAPABILITY_VERSIONS;
export type ExtensionCapability = keyof typeof EXTENSION_CAPABILITY_CONTRACTS;
/** Current producer contract version for each extension-facing capability. */
export const EXTENSION_CAPABILITY_VERSIONS = Object.fromEntries(
Object.entries(EXTENSION_CAPABILITY_CONTRACTS).map(([capability, contract]) => [
capability,
contract.current,
]),
) as {
readonly [TCapability in ExtensionCapability]: (typeof EXTENSION_CAPABILITY_CONTRACTS)[TCapability]["current"];
};
/** Capability requirements stamped by one extension build. */
export type ExtensionCapabilityRequirements = Partial<Record<ExtensionCapability, number>>;
/**
* Older contract versions this eve release still consumes. List a version here
* only when a capability bump keeps the previous format readable.
*/
const ADDITIONAL_SUPPORTED_CAPABILITY_VERSIONS: Partial<
Record<ExtensionCapability, readonly number[]>
> = {};
function deriveCapabilitySupport(): Readonly<Record<ExtensionCapability, readonly number[]>> {
const support = {} as Record<ExtensionCapability, readonly number[]>;
for (const capability of Object.keys(EXTENSION_CAPABILITY_VERSIONS) as ExtensionCapability[]) {
support[capability] = [
...(ADDITIONAL_SUPPORTED_CAPABILITY_VERSIONS[capability] ?? []),
EXTENSION_CAPABILITY_VERSIONS[capability],
];
}
return support;
}
/**
* Capability contract versions this eve release can consume. Derived from
* {@link EXTENSION_CAPABILITY_VERSIONS} so the version this release stamps is
* always one it accepts.
* Capability contract versions this eve release can consume.
*/
export const EXTENSION_CAPABILITY_SUPPORT: Readonly<
Record<ExtensionCapability, readonly number[]>
> = deriveCapabilitySupport();
> = (Object.keys(EXTENSION_CAPABILITY_CONTRACTS) as ExtensionCapability[]).reduce(
(support, capability) => {
support[capability] = EXTENSION_CAPABILITY_CONTRACTS[capability].supported;
return support;
},
{} as Record<ExtensionCapability, readonly number[]>,
);
/** Consumer support table used to validate one extension distribution. */
export type ExtensionCapabilitySupport = Readonly<Record<string, readonly number[]>>;
@@ -15,7 +15,6 @@ import {
CURRENT_DIRECTORY_PROJECT_NAME,
DEFAULT_EVE_PACKAGE_CONTRACT,
DEFAULT_ZOD_PACKAGE_VERSION,
formatEveDependencySpecifier,
resolveEvePackageContract,
ROOT_ONLY_PACKAGE_JSON_TEMPLATE_SUFFIX,
type EvePackageContract,
@@ -36,7 +35,7 @@ interface ExtensionTemplateContext {
function renderTemplate(content: string, ctx: ExtensionTemplateContext): string {
return content
.replaceAll("__EVE_INIT_APP_NAME__", ctx.appName)
.replaceAll("__EVE_INIT_PACKAGE_VERSION__", formatEveDependencySpecifier(ctx.eveVersion))
.replaceAll("__EVE_INIT_PACKAGE_VERSION__", ctx.eveVersion)
.replaceAll("__EVE_INIT_ZOD_VERSION__", ctx.zodPackageVersion)
.replaceAll("__EVE_INIT_TYPESCRIPT_VERSION__", ctx.typescriptPackageVersion)
.replaceAll("__EVE_INIT_TYPES_NODE_VERSION__", ctx.nodeTypesVersion)
@@ -160,7 +159,9 @@ unavailable, use https://eve.dev/docs/extensions as a fallback.
agent-shaped source tree into \`dist/extension/\`, emits type declarations and a
compatibility manifest, and fills the package \`exports\` map. Ship \`dist/\` only.
Keep \`eve\` as a required wildcard peer so the consumer's eve is the one that runs;
eve validates extension compatibility from the generated manifest.
eve validates extension compatibility from the generated manifest. Keep the eve
development dependency pinned exactly so builds remain reproducible. Upgrade it
when the extension intentionally adopts a newer eve authoring API.
`;
const CLAUDE_MD_TEMPLATE = `@AGENTS.md
@@ -764,7 +764,7 @@ describe("scaffoldExtensionProject", () => {
},
engines: { node: "24.x" },
});
expect(packageJson.devDependencies?.eve).toBe("^0.25.0");
expect(packageJson.devDependencies?.eve).toBe("0.25.0");
expect(packageJson.peerDependenciesMeta).toBeUndefined();
expect(packageJson.devDependencies?.typescript).toBe("7.0.2");
expect(packageJson.dependencies?.ai).toBeUndefined();
@@ -788,6 +788,7 @@ describe("scaffoldExtensionProject", () => {
const agentsMd = await readFile(join(projectRoot, "AGENTS.md"), "utf8");
expect(agentsMd).toContain("eve extension");
expect(agentsMd).toContain("extensions.md");
expect(agentsMd).toContain("development dependency pinned exactly");
expect(agentsMd).toContain("cannot declare");
});
});
+225 -49
View File
@@ -80,6 +80,9 @@ importers:
'@changesets/cli':
specifier: 2.31.0
version: 2.31.0(@types/node@25.9.1)
'@microsoft/api-extractor':
specifier: 7.58.10
version: 7.58.10(@types/node@25.9.1)
'@types/node':
specifier: 'catalog:'
version: 25.9.1
@@ -1228,7 +1231,7 @@ importers:
version: 1.28.1
'@workflow/core':
specifier: 5.0.0-beta.35
version: 5.0.0-beta.35(@opentelemetry/api@1.9.1)(ws@8.21.0)
version: 5.0.0-beta.35(@opentelemetry/api@1.9.1)(ws@8.21.1)
'@workflow/errors':
specifier: 5.0.0-beta.11
version: 5.0.0-beta.11
@@ -1252,7 +1255,7 @@ importers:
version: 7.0.26(zod@4.4.3)
autoevals:
specifier: 0.0.132
version: 0.0.132(ws@8.21.0)
version: 0.0.132(ws@8.21.1)
chat:
specifier: 4.31.0
version: 4.31.0(ai@7.0.26(zod@4.4.3))(zod@4.4.3)
@@ -2933,6 +2936,19 @@ packages:
'@mermaid-js/parser@1.1.1':
resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==}
'@microsoft/api-extractor-model@7.33.9':
resolution: {integrity: sha512-ddxNWRNxqJX+/fXnwRmPWWY0UnTB+f8hPTeAygoZ+AxosCYYu5rvDuNrUrq0BTlLeDV60RpkbygDjFepWNczTg==}
'@microsoft/api-extractor@7.58.10':
resolution: {integrity: sha512-p8uSra1V3k0Pt/+017ZfHphZmkjXi+ZVYLen3msIURYwz3jrBrP/u7WV0ELUkOW88bx1EMIVvI0m80QK7Ij1Cw==}
hasBin: true
'@microsoft/tsdoc-config@0.18.1':
resolution: {integrity: sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==}
'@microsoft/tsdoc@0.16.0':
resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==}
'@mixmark-io/domino@2.2.0':
resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
@@ -6156,6 +6172,36 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
'@rushstack/node-core-library@5.23.2':
resolution: {integrity: sha512-dCdCdy+5/L+wss7bymRPWuAhhUMT3kEzN7g9xE0AgzrPFtJ8JS+gMsfgm9spvTSl1YkhSgM3TK4u8BruUQj4Kg==}
peerDependencies:
'@types/node': '*'
peerDependenciesMeta:
'@types/node':
optional: true
'@rushstack/problem-matcher@0.2.1':
resolution: {integrity: sha512-gulfhBs6n+I5b7DvjKRfhMGyUejtSgOHTclF/eONr8hcgF1APEDjhxIsfdUYYMzC3rvLwGluqLjbwCFZ8nxrog==}
peerDependencies:
'@types/node': '*'
peerDependenciesMeta:
'@types/node':
optional: true
'@rushstack/rig-package@0.7.3':
resolution: {integrity: sha512-aAA518n6wxxjCfnTAOjQnm7ngNE0FVHxHAw2pxKlIhxrMn0XQjGcXKF0oKWpjBgJOmsaJpVob/v+zr3zxgPWuA==}
'@rushstack/terminal@0.24.1':
resolution: {integrity: sha512-1NPimt0UnLP0mOxpQpcROPpkArwjr/rEThalQN49bh2rJLNL+PiryGp3iAoW/fLbXxuuazN6SvHqsQl0qnxy+Q==}
peerDependencies:
'@types/node': '*'
peerDependenciesMeta:
'@types/node':
optional: true
'@rushstack/ts-command-line@5.3.11':
resolution: {integrity: sha512-b/eO2GGyiNqGZLHR3ayTrgrPtfBBEvPs4o+HjUmF7Ca6EAZEoA4yrN2RJMOZfrJhYaWBdLk5R7jwQ4w3xf90Aw==}
'@shikijs/core@3.23.0':
resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==}
@@ -6565,6 +6611,9 @@ packages:
'@tybys/wasm-util@0.10.3':
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
'@types/argparse@1.0.38':
resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==}
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
@@ -7601,9 +7650,28 @@ packages:
peerDependencies:
zod: ^3.25.76 || ^4.1.8
ajv-draft-04@1.0.0:
resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==}
peerDependencies:
ajv: ^8.5.0
peerDependenciesMeta:
ajv:
optional: true
ajv-formats@3.0.1:
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
peerDependencies:
ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
ajv@6.15.0:
resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
ajv@8.18.0:
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
ajv@8.20.0:
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
@@ -9276,6 +9344,10 @@ packages:
resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==}
engines: {node: '>=14.14'}
fs-extra@11.3.6:
resolution: {integrity: sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==}
engines: {node: '>=14.14'}
fs-extra@7.0.1:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
engines: {node: '>=6 <7 || >=8'}
@@ -9622,6 +9694,10 @@ packages:
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
engines: {node: '>= 0.4'}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
has-property-descriptors@1.0.2:
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
@@ -9801,6 +9877,10 @@ packages:
resolution: {integrity: sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==}
engines: {node: '>=18'}
import-lazy@4.0.0:
resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==}
engines: {node: '>=8'}
import-meta-resolve@4.2.0:
resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==}
@@ -10088,6 +10168,9 @@ packages:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
jju@1.4.0:
resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==}
jose@5.10.0:
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
@@ -10713,6 +10796,10 @@ packages:
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
engines: {node: 20 || >=22}
minimatch@10.2.3:
resolution: {integrity: sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==}
engines: {node: 18 || 20 || >=22}
minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
engines: {node: 18 || 20 || >=22}
@@ -11619,10 +11706,6 @@ packages:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.19:
resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==}
engines: {node: ^10 || ^12 || >=14}
powershell-utils@0.1.0:
resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
engines: {node: '>=20'}
@@ -12398,6 +12481,10 @@ packages:
streamx@2.28.0:
resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==}
string-argv@0.3.2:
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
engines: {node: '>=0.6.19'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@@ -12518,6 +12605,10 @@ packages:
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
engines: {node: '>=18'}
supports-color@8.1.1:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'}
supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
@@ -13534,18 +13625,6 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
ws@8.21.0:
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
ws@8.21.1:
resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==}
engines: {node: '>=10.0.0'}
@@ -15053,6 +15132,41 @@ snapshots:
dependencies:
'@chevrotain/types': 11.1.2
'@microsoft/api-extractor-model@7.33.9(@types/node@25.9.1)':
dependencies:
'@microsoft/tsdoc': 0.16.0
'@microsoft/tsdoc-config': 0.18.1
'@rushstack/node-core-library': 5.23.2(@types/node@25.9.1)
transitivePeerDependencies:
- '@types/node'
'@microsoft/api-extractor@7.58.10(@types/node@25.9.1)':
dependencies:
'@microsoft/api-extractor-model': 7.33.9(@types/node@25.9.1)
'@microsoft/tsdoc': 0.16.0
'@microsoft/tsdoc-config': 0.18.1
'@rushstack/node-core-library': 5.23.2(@types/node@25.9.1)
'@rushstack/rig-package': 0.7.3
'@rushstack/terminal': 0.24.1(@types/node@25.9.1)
'@rushstack/ts-command-line': 5.3.11(@types/node@25.9.1)
diff: 8.0.4
minimatch: 10.2.3
resolve: 1.22.12
semver: 7.7.4
source-map: 0.6.1
typescript: 5.9.3
transitivePeerDependencies:
- '@types/node'
'@microsoft/tsdoc-config@0.18.1':
dependencies:
'@microsoft/tsdoc': 0.16.0
ajv: 8.18.0
jju: 1.4.0
resolve: 1.22.12
'@microsoft/tsdoc@0.16.0': {}
'@mixmark-io/domino@2.2.0': {}
'@mongodb-js/zstd@7.0.0':
@@ -15280,7 +15394,7 @@ snapshots:
magicast: 0.5.3
pathe: 2.0.3
pkg-types: 2.3.1
semver: 7.8.5
semver: 7.8.4
'@nuxt/devtools@3.2.4(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.3))':
dependencies:
@@ -15307,7 +15421,7 @@ snapshots:
pathe: 2.0.3
perfect-debounce: 2.1.0
pkg-types: 2.3.1
semver: 7.8.5
semver: 7.8.4
simple-git: 3.36.0
sirv: 3.0.2
structured-clone-es: 2.0.0
@@ -15349,7 +15463,7 @@ snapshots:
pathe: 2.0.3
perfect-debounce: 2.1.0
pkg-types: 2.3.1
semver: 7.8.5
semver: 7.8.4
simple-git: 3.36.0
sirv: 3.0.2
structured-clone-es: 2.0.0
@@ -18096,6 +18210,45 @@ snapshots:
'@rtsao/scc@1.1.0': {}
'@rushstack/node-core-library@5.23.2(@types/node@25.9.1)':
dependencies:
ajv: 8.20.0
ajv-draft-04: 1.0.0(ajv@8.20.0)
ajv-formats: 3.0.1(ajv@8.20.0)
fs-extra: 11.3.6
import-lazy: 4.0.0
jju: 1.4.0
resolve: 1.22.12
semver: 7.7.4
optionalDependencies:
'@types/node': 25.9.1
'@rushstack/problem-matcher@0.2.1(@types/node@25.9.1)':
optionalDependencies:
'@types/node': 25.9.1
'@rushstack/rig-package@0.7.3':
dependencies:
jju: 1.4.0
resolve: 1.22.12
'@rushstack/terminal@0.24.1(@types/node@25.9.1)':
dependencies:
'@rushstack/node-core-library': 5.23.2(@types/node@25.9.1)
'@rushstack/problem-matcher': 0.2.1(@types/node@25.9.1)
supports-color: 8.1.1
optionalDependencies:
'@types/node': 25.9.1
'@rushstack/ts-command-line@5.3.11(@types/node@25.9.1)':
dependencies:
'@rushstack/terminal': 0.24.1(@types/node@25.9.1)
'@types/argparse': 1.0.38
argparse: 1.0.10
string-argv: 0.3.2
transitivePeerDependencies:
- '@types/node'
'@shikijs/core@3.23.0':
dependencies:
'@shikijs/types': 3.23.0
@@ -18204,7 +18357,7 @@ snapshots:
'@types/node': 25.9.1
'@types/ws': 8.18.1
eventemitter3: 5.0.4
ws: 8.21.0
ws: 8.21.1
transitivePeerDependencies:
- bufferutil
- debug
@@ -18556,6 +18709,8 @@ snapshots:
tslib: 2.8.1
optional: true
'@types/argparse@1.0.38': {}
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
@@ -19150,20 +19305,12 @@ snapshots:
optionalDependencies:
'@aws-sdk/credential-provider-web-identity': 3.972.49
'@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.49)(ws@8.21.0)':
dependencies:
'@vercel/oidc': 3.8.0
optionalDependencies:
'@aws-sdk/credential-provider-web-identity': 3.972.49
ws: 8.21.0
'@vercel/functions@3.7.5(@aws-sdk/credential-provider-web-identity@3.972.49)(ws@8.21.1)':
dependencies:
'@vercel/oidc': 3.8.0
optionalDependencies:
'@aws-sdk/credential-provider-web-identity': 3.972.49
ws: 8.21.1
optional: true
'@vercel/gatsby-plugin-vercel-analytics@1.0.11':
dependencies:
@@ -19746,13 +19893,13 @@ snapshots:
'@webgpu/types@0.1.71': {}
'@workflow/core@5.0.0-beta.35(@opentelemetry/api@1.9.1)(ws@8.21.0)':
'@workflow/core@5.0.0-beta.35(@opentelemetry/api@1.9.1)(ws@8.21.1)':
dependencies:
'@aws-sdk/credential-provider-web-identity': 3.972.49
'@jridgewell/trace-mapping': 0.3.31
'@standard-schema/spec': 1.0.0
'@types/ms': 2.1.0
'@vercel/functions': 3.7.5(@aws-sdk/credential-provider-web-identity@3.972.49)(ws@8.21.0)
'@vercel/functions': 3.7.5(@aws-sdk/credential-provider-web-identity@3.972.49)(ws@8.21.1)
'@workflow/errors': 5.0.0-beta.11
'@workflow/serde': 5.0.0-beta.2
'@workflow/utils': 5.0.0-beta.6
@@ -19869,6 +20016,14 @@ snapshots:
'@ai-sdk/provider-utils': 5.0.9(zod@4.4.3)
zod: 4.4.3
ajv-draft-04@1.0.0(ajv@8.20.0):
optionalDependencies:
ajv: 8.20.0
ajv-formats@3.0.1(ajv@8.20.0):
optionalDependencies:
ajv: 8.20.0
ajv@6.15.0:
dependencies:
fast-deep-equal: 3.1.3
@@ -19876,6 +20031,13 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
ajv@8.18.0:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.1.2
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
ajv@8.20.0:
dependencies:
fast-deep-equal: 3.1.3
@@ -20061,7 +20223,7 @@ snapshots:
asynckit@0.4.0: {}
autoevals@0.0.132(ws@8.21.0):
autoevals@0.0.132(ws@8.21.1):
dependencies:
ajv: 8.20.0
compute-cosine-similarity: 1.1.0
@@ -20069,7 +20231,7 @@ snapshots:
js-yaml: 4.1.1
linear-sum-assignment: 1.0.9
mustache: 4.2.0
openai: 6.39.1(ws@8.21.0)(zod@3.25.76)
openai: 6.39.1(ws@8.21.1)(zod@3.25.76)
zod: 3.25.76
zod-to-json-schema: 3.25.2(zod@3.25.76)
transitivePeerDependencies:
@@ -21927,6 +22089,12 @@ snapshots:
jsonfile: 6.2.1
universalify: 2.0.1
fs-extra@11.3.6:
dependencies:
graceful-fs: 4.2.11
jsonfile: 6.2.1
universalify: 2.0.1
fs-extra@7.0.1:
dependencies:
graceful-fs: 4.2.11
@@ -22329,6 +22497,8 @@ snapshots:
has-bigints@1.1.0: {}
has-flag@4.0.0: {}
has-property-descriptors@1.0.2:
dependencies:
es-define-property: 1.0.1
@@ -22603,6 +22773,8 @@ snapshots:
es-module-lexer: 2.3.1
module-details-from-path: 1.0.4
import-lazy@4.0.0: {}
import-meta-resolve@4.2.0: {}
impound@1.1.5(esbuild@0.27.7)(rolldown@1.1.0)(rollup@4.62.2)(vite@8.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.9.1)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)):
@@ -22900,6 +23072,8 @@ snapshots:
jiti@2.7.0: {}
jju@1.4.0: {}
jose@5.10.0: {}
jose@5.9.6: {}
@@ -23823,6 +23997,10 @@ snapshots:
dependencies:
'@isaacs/brace-expansion': 5.0.1
minimatch@10.2.3:
dependencies:
brace-expansion: 5.0.7
minimatch@10.2.5:
dependencies:
brace-expansion: 5.0.7
@@ -24090,7 +24268,7 @@ snapshots:
rollup: 4.62.2
rollup-plugin-visualizer: 7.0.1(rolldown@1.1.0)(rollup@4.62.2)
scule: 1.3.0
semver: 7.8.5
semver: 7.8.4
serve-placeholder: 2.0.2
serve-static: 2.2.1
source-map: 0.7.6
@@ -24202,7 +24380,7 @@ snapshots:
rollup: 4.62.2
rollup-plugin-visualizer: 7.0.1(rolldown@1.1.0)(rollup@4.62.2)
scule: 1.3.0
semver: 7.8.5
semver: 7.8.4
serve-placeholder: 2.0.2
serve-static: 2.2.1
source-map: 0.7.6
@@ -24379,7 +24557,7 @@ snapshots:
pkg-types: 2.3.1
rou3: 0.8.1
scule: 1.3.0
semver: 7.8.5
semver: 7.8.4
std-env: 4.2.0
tinyglobby: 0.2.17
ufo: 1.6.4
@@ -24515,7 +24693,7 @@ snapshots:
pkg-types: 2.3.1
rou3: 0.8.1
scule: 1.3.0
semver: 7.8.5
semver: 7.8.4
std-env: 4.2.0
tinyglobby: 0.2.17
ufo: 1.6.4
@@ -24721,9 +24899,9 @@ snapshots:
is-docker: 2.2.1
is-wsl: 2.2.0
openai@6.39.1(ws@8.21.0)(zod@3.25.76):
openai@6.39.1(ws@8.21.1)(zod@3.25.76):
optionalDependencies:
ws: 8.21.0
ws: 8.21.1
zod: 3.25.76
optionator@0.9.4:
@@ -25273,12 +25451,6 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.19:
dependencies:
nanoid: 3.3.16
picocolors: 1.1.1
source-map-js: 1.2.1
powershell-utils@0.1.0: {}
preact-render-to-string@6.5.11(preact@10.24.3):
@@ -26412,6 +26584,8 @@ snapshots:
- bare-abort-controller
- react-native-b4a
string-argv@0.3.2: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
@@ -26551,6 +26725,10 @@ snapshots:
supports-color@10.2.2: {}
supports-color@8.1.1:
dependencies:
has-flag: 4.0.0
supports-preserve-symlinks-flag@1.0.0: {}
svelte@5.56.1(@typescript-eslint/types@8.59.4):
@@ -27489,7 +27667,7 @@ snapshots:
'@oxc-project/runtime': 0.115.0
lightningcss: 1.32.0
picomatch: 4.0.5
postcss: 8.5.19
postcss: 8.5.15
rolldown: 1.0.0-rc.9(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
tinyglobby: 0.2.17
optionalDependencies:
@@ -27739,8 +27917,6 @@ snapshots:
wrappy@1.0.2: {}
ws@8.21.0: {}
ws@8.21.1: {}
wsl-utils@0.3.1:
+237
View File
@@ -0,0 +1,237 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { createRequire } from "node:module";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
COMPATIBILITY_FIXTURE_ROOT,
COMPATIBILITY_SOURCE,
REPORT_ROOT,
REPO_ROOT,
bumpCapabilityConfiguration,
parseCapabilityConfiguration,
retainedCompatibilityFixture,
toPosix,
validateCapabilityConfiguration,
} from "./extension-contracts/configuration.mjs";
import { classifyStructuralBackwardCompatibility } from "./extension-contracts/compatibility.mjs";
import {
checkCapabilityReports,
generateHistoricalCapabilityReport,
reportInventoryIssues,
} from "./extension-contracts/reports.mjs";
function gitOutput(args) {
try {
return execFileSync("git", args, {
cwd: REPO_ROOT,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
});
} catch {
return undefined;
}
}
function immutableContractHistoryIssues() {
const protectedPaths = [
toPosix(relative(REPO_ROOT, REPORT_ROOT)),
toPosix(relative(REPO_ROOT, COMPATIBILITY_FIXTURE_ROOT)),
];
const comparisons = [
["diff", "--name-status", "--", ...protectedPaths],
["diff", "--cached", "--name-status", "--", ...protectedPaths],
];
const hasBase = gitOutput(["rev-parse", "--verify", "origin/main"]) !== undefined;
if (hasBase) {
comparisons.push(["diff", "--name-status", "origin/main...HEAD", "--", ...protectedPaths]);
}
const changes = new Set();
for (const args of comparisons) {
for (const line of (gitOutput(args) ?? "").trim().split("\n")) {
if (line !== "") changes.add(line);
}
}
const issues = [];
for (const change of changes) {
const [status, ...paths] = change.split("\t");
if (status === "A") continue;
if (
hasBase &&
paths.every((path) => gitOutput(["cat-file", "-e", `origin/main:${path}`]) === undefined)
) {
continue;
}
if (paths.every((path) => path.endsWith("README.md"))) continue;
issues.push({
file: paths.at(-1) ?? protectedPaths[0],
message: `Published capability metadata and retained compatibility fixtures are immutable (git status ${status}). Bump the capability epoch and add new files instead of changing or deleting existing contract history.`,
});
}
return issues;
}
function formatted(source, path) {
const require = createRequire(import.meta.url);
const formatterPackage = require.resolve("oxfmt/package.json");
const formatter = join(dirname(formatterPackage), "bin/oxfmt");
return execFileSync(process.execPath, [formatter, "--stdin-filepath", path], {
cwd: REPO_ROOT,
encoding: "utf8",
input: source,
stdio: ["pipe", "pipe", "pipe"],
});
}
function updateRequest(args) {
const updateIndex = args.indexOf("--update");
const capability = updateIndex === -1 ? undefined : args[updateIndex + 1];
const retain = args.includes("--retain");
const dropIndex = args.indexOf("--drop");
const reason = dropIndex === -1 ? undefined : args[dropIndex + 1];
if (updateIndex !== -1 && (!capability || capability.startsWith("--"))) {
throw new Error("--update requires a capability name.");
}
if (capability === undefined && (retain || dropIndex !== -1)) {
throw new Error("--retain and --drop require --update <capability>.");
}
if (retain && dropIndex !== -1) {
throw new Error("--retain and --drop cannot be used together.");
}
if (dropIndex !== -1 && (!reason || reason.startsWith("--"))) {
throw new Error("--drop requires a non-empty reason.");
}
return capability === undefined
? undefined
: {
capability,
decision:
dropIndex !== -1 ? { retain: false, reason } : retain ? { retain: true } : undefined,
};
}
async function scaffoldRetainedFixture(capability, version) {
const path = join(COMPATIBILITY_FIXTURE_ROOT, capability, `v${version}.ts`);
try {
await readFile(path);
return;
} catch (error) {
if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) {
throw error;
}
}
await mkdir(dirname(path), { recursive: true });
await writeFile(path, formatted(retainedCompatibilityFixture(capability, version), path), "utf8");
}
export async function checkExtensionCapabilityContracts({ update = false } = {}) {
const source = await readFile(COMPATIBILITY_SOURCE, "utf8");
const configuration = parseCapabilityConfiguration(source);
const issues = await validateCapabilityConfiguration(configuration);
if (!update) issues.push(...immutableContractHistoryIssues());
if (issues.length === 0) {
issues.push(...(await checkCapabilityReports(configuration, update)));
issues.push(...(await reportInventoryIssues(configuration)));
}
return issues;
}
async function main() {
const args = process.argv.slice(2);
const request = updateRequest(args);
if (request !== undefined) {
const source = await readFile(COMPATIBILITY_SOURCE, "utf8");
const configuration = parseCapabilityConfiguration(source);
const initialIssues = [
...(await validateCapabilityConfiguration(configuration)),
...immutableContractHistoryIssues(),
...(await reportInventoryIssues(configuration)),
];
const reportIssues = await checkCapabilityReports(configuration, false);
const selectedMismatch = reportIssues.find(
(issue) => issue.kind === "contract-mismatch" && issue.capability === request.capability,
);
initialIssues.push(
...reportIssues.filter(
(issue) => issue.kind !== "contract-mismatch" || issue.capability !== request.capability,
),
);
if (selectedMismatch === undefined) {
initialIssues.push({
file: toPosix(relative(REPO_ROOT, COMPATIBILITY_SOURCE)),
message: `Capability "${request.capability}" has no detected API change to bump.`,
});
}
if (initialIssues.length > 0) return initialIssues;
let decision = request.decision;
if (decision === undefined) {
const previousReport = await generateHistoricalCapabilityReport(
request.capability,
configuration.current[request.capability],
);
const classification = classifyStructuralBackwardCompatibility(
previousReport,
selectedMismatch.currentReport,
);
if (!classification.compatible) {
return [
{
file: selectedMismatch.file,
message: `Could not prove the ${request.capability} change is backward compatible. ${classification.reasons.slice(0, 3).join(" ")} Rerun with \`--retain\` after verifying runtime compatibility, or \`--drop "reason"\` to stop accepting the previous epoch.`,
},
];
}
decision = { retain: true };
}
const bumped = bumpCapabilityConfiguration(source, request.capability, decision);
await writeFile(COMPATIBILITY_SOURCE, formatted(bumped.source, COMPATIBILITY_SOURCE), "utf8");
if (decision.retain) {
await scaffoldRetainedFixture(request.capability, bumped.previousVersion);
}
process.stdout.write(
decision.retain
? `[eve:extension-contracts] ${request.capability} is structurally backward compatible; retaining epoch ${bumped.previousVersion} and bumping to ${bumped.version}.\n`
: `[eve:extension-contracts] dropping ${request.capability} epoch ${bumped.previousVersion} and bumping to ${bumped.version}.\n`,
);
}
const issues = await checkExtensionCapabilityContracts({ update: true });
return issues;
}
async function run() {
let issues;
try {
issues = await main();
} catch (error) {
issues = [
{
file: toPosix(relative(REPO_ROOT, COMPATIBILITY_SOURCE)),
message: error instanceof Error ? error.message : String(error),
},
];
}
if (issues.length > 0) {
process.stderr.write(
`[eve:extension-contracts] FAIL: ${issues.length} capability contract issue${issues.length === 1 ? "" : "s"}.\n\n`,
);
for (const issue of issues) {
process.stderr.write(` ${issue.file}\n ${issue.message}\n`);
}
process.exitCode = 1;
return;
}
process.stdout.write("[eve:extension-contracts] updated current capability metadata.\n");
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
await run();
}
@@ -0,0 +1,135 @@
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const extractorRequire = createRequire(require.resolve("@microsoft/api-extractor/package.json"));
const ts = extractorRequire("typescript");
function reportSource(report) {
const match = report.match(/```ts\n([\s\S]*?)\n```/);
if (!match)
throw new Error("Could not read the TypeScript declaration block from an API report.");
return match[1];
}
function parsedReport(report, name) {
const source = reportSource(report);
return ts.createSourceFile(name, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
}
/** Names every declaration API Extractor traced from one capability root. */
export function collectReportDeclarationNames(report) {
const sourceFile = parsedReport(report, "capability-report.d.ts");
const names = new Set();
for (const statement of sourceFile.statements) {
if (
(ts.isClassDeclaration(statement) ||
ts.isEnumDeclaration(statement) ||
ts.isFunctionDeclaration(statement) ||
ts.isInterfaceDeclaration(statement) ||
ts.isTypeAliasDeclaration(statement)) &&
statement.name
) {
names.add(statement.name.text);
continue;
}
if (ts.isVariableStatement(statement)) {
for (const declaration of statement.declarationList.declarations) {
if (ts.isIdentifier(declaration.name)) names.add(declaration.name.text);
}
}
}
return names;
}
function interfaceName(statement) {
return statement.name.text;
}
function textMultiset(nodes, sourceFile) {
const counts = new Map();
for (const node of nodes) {
const text = node.getText(sourceFile);
counts.set(text, (counts.get(text) ?? 0) + 1);
}
return counts;
}
function consumeText(counts, text) {
const count = counts.get(text) ?? 0;
if (count === 0) return false;
if (count === 1) counts.delete(text);
else counts.set(text, count - 1);
return true;
}
function interfaceCompatibility(previous, current, previousSource, currentSource) {
const reasons = [];
const currentMembers = textMultiset(current.members, currentSource);
for (const member of previous.members) {
const text = member.getText(previousSource);
if (!consumeText(currentMembers, text)) {
reasons.push(`${interfaceName(previous)} changed or removed member: ${text}`);
}
}
const previousShape = [
...(previous.typeParameters ?? []).map((node) => node.getText(previousSource)),
...(previous.heritageClauses ?? []).map((node) => node.getText(previousSource)),
];
const currentShape = [
...(current.typeParameters ?? []).map((node) => node.getText(currentSource)),
...(current.heritageClauses ?? []).map((node) => node.getText(currentSource)),
];
if (JSON.stringify(previousShape) !== JSON.stringify(currentShape)) {
reasons.push(`${interfaceName(previous)} changed its type parameters or heritage clauses.`);
}
for (const [text, count] of currentMembers) {
const members = current.members.filter((member) => member.getText(currentSource) === text);
if (members.slice(0, count).some((member) => member.questionToken === undefined)) {
reasons.push(`${interfaceName(previous)} added a required member: ${text}`);
}
}
return reasons;
}
/**
* Conservatively recognizes declaration changes that preserve old authored
* source: new top-level declarations and optional interface members. Anything
* else requires an explicit compatibility decision.
*/
export function classifyStructuralBackwardCompatibility(previousReport, currentReport) {
const previous = parsedReport(previousReport, "previous.d.ts");
const current = parsedReport(currentReport, "current.d.ts");
const reasons = [];
const currentInterfaces = new Map(
current.statements
.filter(ts.isInterfaceDeclaration)
.map((statement) => [interfaceName(statement), statement]),
);
for (const previousInterface of previous.statements.filter(ts.isInterfaceDeclaration)) {
const name = interfaceName(previousInterface);
const currentInterface = currentInterfaces.get(name);
if (!currentInterface) {
reasons.push(`Interface ${name} was removed.`);
continue;
}
reasons.push(...interfaceCompatibility(previousInterface, currentInterface, previous, current));
}
const currentStatements = textMultiset(
current.statements.filter((statement) => !ts.isInterfaceDeclaration(statement)),
current,
);
for (const statement of previous.statements.filter(
(candidate) => !ts.isInterfaceDeclaration(candidate),
)) {
const text = statement.getText(previous);
if (!consumeText(currentStatements, text)) {
reasons.push(`Declaration changed or was removed: ${text.split("\n", 1)[0]}`);
}
}
return { compatible: reasons.length === 0, reasons };
}
@@ -0,0 +1,377 @@
import { readFile, readdir } from "node:fs/promises";
import { createRequire } from "node:module";
import { join, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
const require = createRequire(import.meta.url);
const extractorRequire = createRequire(require.resolve("@microsoft/api-extractor/package.json"));
const ts = extractorRequire("typescript");
export const REPO_ROOT = resolve(fileURLToPath(import.meta.url), "../../..");
export const EVE_ROOT = join(REPO_ROOT, "packages/eve");
export const COMPATIBILITY_SOURCE = join(EVE_ROOT, "src/compiler/extension-compatibility.ts");
export const CONTRACT_ROOT = join(EVE_ROOT, "extension-contracts");
export const ENTRYPOINT_ROOT = join(CONTRACT_ROOT, "entrypoints");
export const COMPATIBILITY_FIXTURE_ROOT = join(CONTRACT_ROOT, "compatibility");
export const REPORT_ROOT = join(CONTRACT_ROOT, "reports");
export const COMPATIBILITY_FIXTURE_PLACEHOLDER = "REPLACE_WITH_RETAINED_AUTHORING_EXAMPLE";
export const PUBLIC_SURFACES = [
{ path: "src/public/extension/index.ts", capabilities: ["extension", "config"] },
{ path: "src/public/tools/index.ts", capabilities: ["tool", "dynamicTool"] },
{ path: "src/public/connections/index.ts", capabilities: ["connection"] },
{ path: "src/public/hooks/index.ts", capabilities: ["hook"] },
{ path: "src/public/skills/index.ts", capabilities: ["skill", "dynamicSkill"] },
{
path: "src/public/instructions/index.ts",
capabilities: ["instructions", "dynamicInstructions"],
},
{ path: "src/public/context/index.ts", capabilities: ["state"] },
];
export function toPosix(path) {
return sep === "/" ? path : path.split(sep).join("/");
}
function unwrapExpression(expression) {
let current = expression;
while (
ts.isAsExpression(current) ||
ts.isSatisfiesExpression(current) ||
ts.isParenthesizedExpression(current)
) {
current = current.expression;
}
return current;
}
function propertyName(property) {
if (
ts.isIdentifier(property.name) ||
ts.isStringLiteral(property.name) ||
ts.isNumericLiteral(property.name)
) {
return property.name.text;
}
throw new Error("Extension capability contracts must use static property names.");
}
function objectLiteral(expression, description) {
const unwrapped = unwrapExpression(expression);
if (!ts.isObjectLiteralExpression(unwrapped)) {
throw new Error(`${description} must be an object literal.`);
}
return unwrapped;
}
function propertyAssignment(object, name, description) {
const property = object.properties.find(
(candidate) => ts.isPropertyAssignment(candidate) && propertyName(candidate) === name,
);
if (!property || !ts.isPropertyAssignment(property)) {
throw new Error(`${description} must define ${name}.`);
}
return property;
}
function numericValue(expression, description) {
const unwrapped = unwrapExpression(expression);
if (!ts.isNumericLiteral(unwrapped)) throw new Error(`${description} must be a number.`);
return Number(unwrapped.text);
}
function numericArray(expression, description) {
const unwrapped = unwrapExpression(expression);
if (!ts.isArrayLiteralExpression(unwrapped)) {
throw new Error(`${description} must be an array literal.`);
}
return unwrapped.elements.map((element) => numericValue(element, `${description} entry`));
}
function droppedEpochs(expression, description) {
const object = objectLiteral(expression, description);
return Object.fromEntries(
object.properties.map((property) => {
if (!ts.isPropertyAssignment(property)) {
throw new Error(`${description} must contain property assignments.`);
}
const reason = unwrapExpression(property.initializer);
if (!ts.isStringLiteral(reason)) {
throw new Error(`${description}.${propertyName(property)} must be a string literal.`);
}
return [propertyName(property), reason.text];
}),
);
}
function contractTable(source) {
const sourceFile = ts.createSourceFile(
COMPATIBILITY_SOURCE,
source,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
);
for (const statement of sourceFile.statements) {
if (!ts.isVariableStatement(statement)) continue;
for (const declaration of statement.declarationList.declarations) {
if (
ts.isIdentifier(declaration.name) &&
declaration.name.text === "EXTENSION_CAPABILITY_CONTRACTS" &&
declaration.initializer
) {
return objectLiteral(declaration.initializer, "EXTENSION_CAPABILITY_CONTRACTS");
}
}
}
throw new Error("Could not find EXTENSION_CAPABILITY_CONTRACTS.");
}
export function parseCapabilityConfiguration(source) {
const contracts = Object.fromEntries(
contractTable(source).properties.map((property) => {
if (!ts.isPropertyAssignment(property)) {
throw new Error("EXTENSION_CAPABILITY_CONTRACTS must contain property assignments.");
}
const capability = propertyName(property);
const contract = objectLiteral(property.initializer, `Capability ${capability}`);
return [
capability,
{
current: numericValue(
propertyAssignment(contract, "current", `Capability ${capability}`).initializer,
`Capability ${capability}.current`,
),
supported: numericArray(
propertyAssignment(contract, "supported", `Capability ${capability}`).initializer,
`Capability ${capability}.supported`,
),
dropped: droppedEpochs(
propertyAssignment(contract, "dropped", `Capability ${capability}`).initializer,
`Capability ${capability}.dropped`,
),
},
];
}),
);
return {
contracts,
current: Object.fromEntries(
Object.entries(contracts).map(([capability, contract]) => [capability, contract.current]),
),
support: Object.fromEntries(
Object.entries(contracts).map(([capability, contract]) => [capability, contract.supported]),
),
};
}
export function bumpCapabilityConfiguration(source, capability, decision) {
const table = contractTable(source);
const property = table.properties.find(
(candidate) => ts.isPropertyAssignment(candidate) && propertyName(candidate) === capability,
);
if (!property || !ts.isPropertyAssignment(property)) {
throw new Error(`Unknown extension capability "${capability}".`);
}
const configuration = parseCapabilityConfiguration(source);
const contract = configuration.contracts[capability];
const nextVersion = contract.current + 1;
const supported = decision.retain
? [...contract.supported, nextVersion]
: [...contract.supported.filter((version) => version !== contract.current), nextVersion];
const dropped = decision.retain
? contract.dropped
: { ...contract.dropped, [contract.current]: decision.reason };
const droppedSource = Object.entries(dropped)
.map(([version, reason]) => `${version}: ${JSON.stringify(reason)}`)
.join(", ");
const replacement = `${capability}: { current: ${nextVersion}, supported: [${supported.join(", ")}], dropped: {${droppedSource === "" ? "" : ` ${droppedSource} `}} }`;
return {
source: `${source.slice(0, property.getStart())}${replacement}${source.slice(property.end)}`,
previousVersion: contract.current,
version: nextVersion,
};
}
export function retainedCompatibilityFixture(capability, version) {
return `/**
* Replace this scaffold with a representative ${capability} epoch ${version}
* authoring example that must continue to compile against the current eve API.
* ${COMPATIBILITY_FIXTURE_PLACEHOLDER}
*/
export {};
`;
}
export function collectExportNames(source, { valuesOnly = false } = {}) {
const names = new Set();
const sourceFile = ts.createSourceFile(
"extension-capability-entrypoint.ts",
source,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
);
for (const statement of sourceFile.statements) {
if (ts.isExportDeclaration(statement) && statement.exportClause) {
if (!ts.isNamedExports(statement.exportClause)) continue;
for (const specifier of statement.exportClause.elements) {
if (valuesOnly && (statement.isTypeOnly || specifier.isTypeOnly)) continue;
names.add(specifier.name.text);
}
continue;
}
const isExported = statement.modifiers?.some(
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
);
if (!isExported) continue;
if (ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) {
if (statement.name) names.add(statement.name.text);
continue;
}
if (ts.isVariableStatement(statement)) {
for (const declaration of statement.declarationList.declarations) {
if (ts.isIdentifier(declaration.name)) names.add(declaration.name.text);
}
continue;
}
if (ts.isEnumDeclaration(statement)) {
names.add(statement.name.text);
continue;
}
if (
!valuesOnly &&
(ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement))
) {
names.add(statement.name.text);
}
}
return names;
}
export async function validateCapabilityConfiguration(configuration) {
const issues = [];
const capabilities = Object.keys(configuration.current);
const entrypointEntries = await readdir(ENTRYPOINT_ROOT, { withFileTypes: true });
const entrypointCapabilities = entrypointEntries
.filter((entry) => entry.isFile() && entry.name.endsWith(".ts"))
.map((entry) => entry.name.slice(0, -3))
.sort();
for (const capability of capabilities) {
const { current: version, dropped, supported } = configuration.contracts[capability];
if (!Number.isInteger(version) || version < 1) {
issues.push({
file: toPosix(relative(REPO_ROOT, COMPATIBILITY_SOURCE)),
message: `Capability "${capability}" must have a positive integer epoch.`,
});
}
if (!supported.includes(version)) {
issues.push({
file: toPosix(relative(REPO_ROOT, COMPATIBILITY_SOURCE)),
message: `Capability "${capability}" does not list its current epoch ${version} as supported.`,
});
}
if (new Set(supported).size !== supported.length) {
issues.push({
file: toPosix(relative(REPO_ROOT, COMPATIBILITY_SOURCE)),
message: `Capability "${capability}" lists a supported epoch more than once.`,
});
}
const droppedVersions = Object.keys(dropped).map(Number);
for (const supportedVersion of supported) {
if (
!Number.isInteger(supportedVersion) ||
supportedVersion < 1 ||
supportedVersion > version
) {
issues.push({
file: toPosix(relative(REPO_ROOT, COMPATIBILITY_SOURCE)),
message: `Capability "${capability}" has invalid supported epoch ${supportedVersion}; supported epochs must be positive and no newer than current epoch ${version}.`,
});
}
}
for (const droppedVersion of droppedVersions) {
if (!Number.isInteger(droppedVersion) || droppedVersion < 1 || droppedVersion >= version) {
issues.push({
file: toPosix(relative(REPO_ROOT, COMPATIBILITY_SOURCE)),
message: `Capability "${capability}" has invalid dropped epoch ${droppedVersion}; only historical epochs before current epoch ${version} can be dropped.`,
});
}
if (dropped[String(droppedVersion)].trim() === "") {
issues.push({
file: toPosix(relative(REPO_ROOT, COMPATIBILITY_SOURCE)),
message: `Capability "${capability}" must record why epoch ${droppedVersion} was dropped.`,
});
}
}
for (let historicalVersion = 1; historicalVersion < version; historicalVersion++) {
const isSupported = supported.includes(historicalVersion);
const isDropped = Object.hasOwn(dropped, historicalVersion);
if (isSupported === isDropped) {
issues.push({
file: toPosix(relative(REPO_ROOT, COMPATIBILITY_SOURCE)),
message: `Capability "${capability}" epoch ${historicalVersion} must be classified exactly once as supported or dropped.`,
});
}
if (!isSupported) continue;
const fixturePath = join(COMPATIBILITY_FIXTURE_ROOT, capability, `v${historicalVersion}.ts`);
try {
const fixture = await readFile(fixturePath, "utf8");
if (fixture.includes(COMPATIBILITY_FIXTURE_PLACEHOLDER)) {
issues.push({
file: toPosix(relative(REPO_ROOT, fixturePath)),
message: `Replace the scaffold with a representative ${capability} epoch ${historicalVersion} authoring example before advertising retained support.`,
});
}
} catch (error) {
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
issues.push({
file: toPosix(relative(REPO_ROOT, fixturePath)),
message: `Advertising ${capability} epoch ${historicalVersion} requires an immutable compatibility fixture that exercises the retained authoring contract.`,
});
continue;
}
throw error;
}
}
}
const configured = [...capabilities].sort();
if (JSON.stringify(configured) !== JSON.stringify(entrypointCapabilities)) {
issues.push({
file: toPosix(relative(REPO_ROOT, ENTRYPOINT_ROOT)),
message: `Contract entrypoints must exactly match configured capabilities. Expected ${configured.join(", ")}; found ${entrypointCapabilities.join(", ")}.`,
});
}
for (const surface of PUBLIC_SURFACES) {
const publicSource = await readFile(join(EVE_ROOT, surface.path), "utf8");
const publicNames = collectExportNames(publicSource, { valuesOnly: true });
const contractNames = new Set();
for (const capability of surface.capabilities) {
const contractSource = await readFile(join(ENTRYPOINT_ROOT, `${capability}.ts`), "utf8");
for (const name of collectExportNames(contractSource, { valuesOnly: true })) {
contractNames.add(name);
}
}
const missing = [...publicNames].filter((name) => !contractNames.has(name)).sort();
const extra = [...contractNames].filter((name) => !publicNames.has(name)).sort();
if (missing.length > 0 || extra.length > 0) {
const details = [
missing.length > 0 ? `unassigned exports: ${missing.join(", ")}` : "",
extra.length > 0 ? `unknown exports: ${extra.join(", ")}` : "",
]
.filter(Boolean)
.join("; ");
issues.push({
file: toPosix(relative(REPO_ROOT, join(EVE_ROOT, surface.path))),
message: `Capability contract roots are incomplete (${details}). Assign every public authoring value to one of: ${surface.capabilities.join(", ")}.`,
});
}
}
return issues;
}
+404
View File
@@ -0,0 +1,404 @@
import { CompilerState, Extractor, ExtractorConfig } from "@microsoft/api-extractor";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { createRequire } from "node:module";
import { cp, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises";
import { dirname, join, relative } from "node:path";
import {
CONTRACT_ROOT,
ENTRYPOINT_ROOT,
EVE_ROOT,
PUBLIC_SURFACES,
REPORT_ROOT,
REPO_ROOT,
collectExportNames,
toPosix,
} from "./configuration.mjs";
import { collectReportDeclarationNames } from "./compatibility.mjs";
async function* walkFiles(root) {
for (const entry of await readdir(root, { withFileTypes: true })) {
const path = join(root, entry.name);
if (entry.isDirectory()) yield* walkFiles(path);
if (entry.isFile()) yield path;
}
}
function relativeModuleSpecifier(fromFile, targetFile) {
const path = toPosix(relative(dirname(fromFile), targetFile));
return path.startsWith(".") ? path : `./${path}`;
}
function formatSnapshot(snapshot, snapshotPath) {
const require = createRequire(import.meta.url);
const formatterPackage = require.resolve("oxfmt/package.json");
const formatter = join(dirname(formatterPackage), "bin/oxfmt");
return execFileSync(process.execPath, [formatter, "--stdin-filepath", snapshotPath], {
cwd: REPO_ROOT,
encoding: "utf8",
input: snapshot,
stdio: ["pipe", "pipe", "pipe"],
});
}
async function rewriteDeclarationSpecifiers(declarationRoot) {
for await (const path of walkFiles(declarationRoot)) {
if (!path.endsWith(".d.ts")) continue;
const original = await readFile(path, "utf8");
const rewritten = original
.replace(/(["'])(#[^"']+)\1/g, (_match, quote, specifier) => {
const target = specifier.startsWith("#compiled/")
? join(declarationRoot, "compiled", specifier.slice("#compiled/".length))
: join(declarationRoot, "src", specifier.slice(1));
return `${quote}${relativeModuleSpecifier(path, target)}${quote}`;
})
.replace(/(["'])(\.{1,2}\/[^"']+)\.ts\1/g, (_match, quote, specifier) => {
return `${quote}${specifier}.js${quote}`;
});
if (rewritten !== original) await writeFile(path, rewritten, "utf8");
}
}
async function emitDeclarations(tempRoot, { contractRoot, eveRoot }) {
const declarationRoot = join(tempRoot, "declarations");
await mkdir(declarationRoot, { recursive: true });
execFileSync(process.execPath, [join(eveRoot, "scripts/vendor-compiled.mjs")], {
cwd: eveRoot,
stdio: ["ignore", "pipe", "pipe"],
});
const require = createRequire(import.meta.url);
const typescriptPackage = require.resolve("typescript/package.json");
const tsc = join(dirname(typescriptPackage), "bin/tsc");
execFileSync(
process.execPath,
[
tsc,
"-p",
join(contractRoot, "tsconfig.json"),
"--outDir",
declarationRoot,
"--declarationMap",
"false",
"--sourceMap",
"false",
"--removeComments",
"true",
"--pretty",
"false",
],
{ cwd: eveRoot, stdio: ["ignore", "pipe", "pipe"] },
);
await cp(join(eveRoot, ".generated/compiled"), join(declarationRoot, "compiled"), {
recursive: true,
});
await rewriteDeclarationSpecifiers(declarationRoot);
const packageJson = JSON.parse(await readFile(join(eveRoot, "package.json"), "utf8"));
packageJson.name = "eve-extension-contracts";
packageJson.version = "0.0.0";
packageJson.private = true;
packageJson.types = "./extension-contracts/entrypoints/extension.d.ts";
delete packageJson.exports;
delete packageJson.imports;
await writeFile(
join(declarationRoot, "package.json"),
`${JSON.stringify(packageJson, null, 2)}\n`,
);
return declarationRoot;
}
function extractorConfig({ capabilities, capability, declarationRoot, tempRoot }) {
const reportFolder = join(tempRoot, "generated-reports", capability);
const reportTempFolder = join(tempRoot, "temporary-reports", capability);
return {
reportFolder,
reportTempFolder,
config: ExtractorConfig.prepare({
configObject: {
projectFolder: declarationRoot,
mainEntryPointFilePath: join(
declarationRoot,
"extension-contracts/entrypoints",
`${capability}.d.ts`,
),
newlineKind: "lf",
testMode: true,
compiler: {
overrideTsconfig: {
compilerOptions: {
lib: ["ES2024", "DOM", "DOM.Iterable"],
module: "NodeNext",
moduleResolution: "NodeNext",
skipLibCheck: true,
strict: true,
target: "ES2024",
types: ["node"],
},
files: capabilities.map((name) =>
join(declarationRoot, "extension-contracts/entrypoints", `${name}.d.ts`),
),
},
skipLibCheck: true,
},
apiReport: {
enabled: true,
includeForgottenExports: true,
reportFileName: "current",
reportFolder,
reportTempFolder,
},
docModel: { enabled: false },
dtsRollup: { enabled: false },
tsdocMetadata: { enabled: false },
messages: {
compilerMessageReporting: { default: { logLevel: "error" } },
extractorMessageReporting: { default: { logLevel: "none" } },
tsdocMessageReporting: { default: { logLevel: "none" } },
},
},
configObjectFullPath: undefined,
packageJsonFullPath: join(declarationRoot, "package.json"),
}),
};
}
export async function generateCapabilityReports(
configuration,
{ contractRoot = CONTRACT_ROOT, eveRoot = EVE_ROOT } = {},
) {
const cacheRoot = join(EVE_ROOT, ".extension-contracts-cache");
await mkdir(cacheRoot, { recursive: true });
const tempRoot = await mkdtemp(join(cacheRoot, "extension-contracts-"));
try {
const declarationRoot = await emitDeclarations(tempRoot, { contractRoot, eveRoot });
const capabilities = Object.keys(configuration.current);
const configs = [];
for (const [capability, version] of Object.entries(configuration.current)) {
const item = extractorConfig({ capabilities, capability, declarationRoot, tempRoot });
await mkdir(item.reportFolder, { recursive: true });
await mkdir(item.reportTempFolder, { recursive: true });
configs.push({ capability, version, ...item });
}
const entrypoints = configs.map((item) => item.config.mainEntryPointFilePath);
const compilerState = CompilerState.create(configs[0].config, {
additionalEntryPoints: entrypoints.slice(1),
});
const reports = new Map();
for (const item of configs) {
const messages = [];
const result = Extractor.invoke(item.config, {
compilerState,
localBuild: true,
printApiReportDiff: false,
messageCallback(message) {
if (message.logLevel === "error") messages.push(message.formatMessageWithoutLocation());
message.handled = true;
},
});
if (!result.succeeded) {
throw new Error(
messages[0] ?? `Could not extract the ${item.capability} API for epoch ${item.version}.`,
);
}
reports.set(
item.capability,
await readFile(join(item.reportFolder, "current.api.md"), "utf8"),
);
}
return reports;
} finally {
await rm(tempRoot, { recursive: true, force: true });
}
}
function gitOutput(args) {
return execFileSync("git", args, {
cwd: REPO_ROOT,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
}).trim();
}
/** Regenerates an epoch's API report from the Git commit that last recorded its metadata. */
export async function generateHistoricalCapabilityReport(capability, version) {
const metadataPath = join(REPORT_ROOT, capability, `v${version}.json`);
const metadataRelativePath = toPosix(relative(REPO_ROOT, metadataPath));
const baselineCommit = gitOutput(["log", "-1", "--format=%H", "--", metadataRelativePath])
.split("\n")
.find(Boolean);
if (baselineCommit === undefined) {
throw new Error(
`Could not find the Git commit that recorded ${metadataRelativePath}. Commit the current epoch metadata before classifying another capability change.`,
);
}
const cacheRoot = join(EVE_ROOT, ".extension-contracts-cache");
await mkdir(cacheRoot, { recursive: true });
const historyRoot = await mkdtemp(join(cacheRoot, "history-"));
const worktreeRoot = join(historyRoot, "worktree");
let addedWorktree = false;
try {
execFileSync("git", ["worktree", "add", "--detach", worktreeRoot, baselineCommit], {
cwd: REPO_ROOT,
stdio: ["ignore", "pipe", "pipe"],
});
addedWorktree = true;
const historicalEveRoot = join(worktreeRoot, "packages/eve");
const historicalContractRoot = join(historicalEveRoot, "extension-contracts");
await symlink(join(EVE_ROOT, "node_modules"), join(historicalEveRoot, "node_modules"), "dir");
const historicalCapabilities = (await readdir(join(historicalContractRoot, "entrypoints")))
.filter((name) => name.endsWith(".ts"))
.map((name) => name.slice(0, -3));
const historicalConfiguration = {
current: Object.fromEntries(historicalCapabilities.map((name) => [name, 1])),
};
const reports = await generateCapabilityReports(historicalConfiguration, {
contractRoot: historicalContractRoot,
eveRoot: historicalEveRoot,
});
const report = reports.get(capability);
if (report === undefined) {
throw new Error(
`The Git baseline ${baselineCommit} does not contain capability "${capability}".`,
);
}
return report;
} finally {
if (addedWorktree) {
execFileSync("git", ["worktree", "remove", "--force", worktreeRoot], {
cwd: REPO_ROOT,
stdio: ["ignore", "pipe", "pipe"],
});
}
await rm(historyRoot, { recursive: true, force: true });
}
}
export async function checkCapabilityReports(configuration, update) {
const issues = [];
try {
const reports = await generateCapabilityReports(configuration);
for (const surface of PUBLIC_SURFACES) {
const publicSource = await readFile(join(EVE_ROOT, surface.path), "utf8");
const publicNames = collectExportNames(publicSource);
const publicValues = collectExportNames(publicSource, { valuesOnly: true });
const tracedNames = new Set();
for (const capability of surface.capabilities) {
const report = reports.get(capability);
if (report === undefined) continue;
for (const name of collectReportDeclarationNames(report)) tracedNames.add(name);
}
const missingTypes = [...publicNames]
.filter((name) => !publicValues.has(name) && !tracedNames.has(name))
.sort();
if (missingTypes.length > 0) {
issues.push({
file: toPosix(relative(REPO_ROOT, join(EVE_ROOT, surface.path))),
message: `Public extension types are not reachable from the ${surface.capabilities.join("/")} authoring roots: ${missingTypes.join(", ")}. Add only these standalone types to the appropriate capability entrypoint.`,
});
}
}
for (const [capability, version] of Object.entries(configuration.current)) {
const generatedReport = reports.get(capability);
if (generatedReport === undefined) {
throw new Error(`Could not generate the ${capability} API for epoch ${version}.`);
}
const contractSource = await readFile(join(ENTRYPOINT_ROOT, `${capability}.ts`), "utf8");
const metadataPath = join(REPORT_ROOT, capability, `v${version}.json`);
const snapshot = formatSnapshot(
JSON.stringify({
kind: "eve-extension-capability-contract",
capability,
epoch: version,
sha256: createHash("sha256").update(generatedReport).digest("hex"),
exports: [...collectExportNames(contractSource)].sort(),
}),
metadataPath,
);
let existingMetadata;
try {
existingMetadata = await readFile(metadataPath, "utf8");
} catch (error) {
if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) {
throw error;
}
}
if (update && existingMetadata === undefined) {
await mkdir(dirname(metadataPath), { recursive: true });
await writeFile(metadataPath, snapshot, "utf8");
} else if (existingMetadata !== snapshot) {
issues.push({
capability,
currentReport: generatedReport,
kind: "contract-mismatch",
file: toPosix(relative(REPO_ROOT, metadataPath)),
message: `The ${capability} API no longer matches epoch ${version}. Run \`pnpm update:extension-contracts --update ${capability}\` to classify the change and add the new epoch metadata.`,
});
}
}
} catch (error) {
const stderr =
error && typeof error === "object" && "stderr" in error
? String(error.stderr).trim()
: undefined;
issues.push({
file: toPosix(relative(REPO_ROOT, CONTRACT_ROOT)),
message: `Could not generate extension capability reports: ${stderr || (error instanceof Error ? error.message : String(error))}`,
});
}
return issues;
}
export async function reportInventoryIssues(configuration) {
const issues = [];
const entries = await readdir(REPORT_ROOT, { withFileTypes: true });
const reportCapabilities = entries
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();
const configuredCapabilities = Object.keys(configuration.current).sort();
if (JSON.stringify(reportCapabilities) !== JSON.stringify(configuredCapabilities)) {
issues.push({
file: toPosix(relative(REPO_ROOT, REPORT_ROOT)),
message: `Report directories must exactly match configured capabilities. Expected ${configuredCapabilities.join(", ")}; found ${reportCapabilities.join(", ")}.`,
});
}
for (const [capability, currentVersion] of Object.entries(configuration.current)) {
const expectedReportNames = Array.from(
{ length: currentVersion },
(_, index) => `v${index + 1}.json`,
).sort();
const actualReportNames = (
await readdir(join(REPORT_ROOT, capability), { withFileTypes: true })
)
.filter((entry) => entry.isFile())
.map((entry) => entry.name)
.sort();
if (JSON.stringify(actualReportNames) !== JSON.stringify(expectedReportNames)) {
issues.push({
file: toPosix(relative(REPO_ROOT, join(REPORT_ROOT, capability))),
message: `Capability ${capability} metadata must cover every epoch from 1 through ${currentVersion}. Expected ${expectedReportNames.join(", ")}; found ${actualReportNames.join(", ")}.`,
});
}
for (let version = 1; version <= currentVersion; version++) {
const reportPath = join(REPORT_ROOT, capability, `v${version}.json`);
try {
await readFile(reportPath);
} catch (error) {
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
issues.push({
file: toPosix(relative(REPO_ROOT, reportPath)),
message: `Capability ${capability} is at epoch ${currentVersion}, so immutable metadata v${version}.json must be retained. Restore it or bump epochs sequentially and generate the missing metadata.`,
});
continue;
}
throw error;
}
}
}
return issues;
}
+11
View File
@@ -87,6 +87,11 @@
* engines `eval()` a `---js` frontmatter fence, so every call must
* route through `parseFrontmatter`, which is safe by default. A
* direct import lets untrusted input reach an evaluating engine.
* rule 36 Extension capability epochs have immutable hashed API metadata
* and explicit support history. The current hash must match the
* authoring roots, every historical epoch must be supported or
* dropped, every retained epoch needs a compiling fixture, and
* every public authoring value must belong to a capability.
*
* Baselines for rules with pre-existing violations live in
* `guard-invariants-baseline.json`. Counts and allowlists in that file
@@ -96,6 +101,7 @@ import { readFile, readdir, lstat } from "node:fs/promises";
import { join, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
import matter from "gray-matter";
import { checkExtensionCapabilityContracts } from "./extension-capability-contracts.mjs";
const REPO_ROOT = resolve(fileURLToPath(import.meta.url), "../..");
const BASELINE_PATH = join(REPO_ROOT, "scripts/guard-invariants-baseline.json");
@@ -1149,6 +1155,11 @@ async function main() {
// Rule 35
violations.push(...state.rule35);
// Rule 36
for (const issue of await checkExtensionCapabilityContracts()) {
violations.push({ rule: 36, ...issue });
}
if (violations.length === 0) {
process.stdout.write("[eve:guard:invariants] ok — all mechanical lints passed.\n");
return;