chore: enforce type evidence lint rules

This commit is contained in:
Amp
2026-08-12 21:49:50 +00:00
parent f9316c4697
commit 4d55cff030
55 changed files with 696 additions and 99 deletions
+10
View File
@@ -1,6 +1,7 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["unicorn", "typescript", "oxc"],
"jsPlugins": ["./scripts/oxlint-type-evidence.mjs"],
"categories": {
"correctness": "error",
"perf": "error",
@@ -66,6 +67,8 @@
"oxc/no-accumulating-spread": "error",
"oxc/no-async-endpoint-handlers": "error",
"oxc/no-map-spread": "error",
"openclaw-type-evidence/no-unknown-type-aliases": "error",
"openclaw-type-evidence/no-widen-then-assert": "error",
"promise/no-callback-in-promise": "error",
"promise/no-multiple-resolved": "error",
"promise/no-promise-in-callback": "error",
@@ -213,6 +216,7 @@
// Intentional negative-test corpora contain parser and lint violations by contract.
".agents/skills/autoreview/tests/fixtures/**",
"test/fixtures/oxlint-boundary-guards/**",
"test/fixtures/oxlint-type-evidence/**",
"**/a2ui.bundle.js",
"extensions/diffs/assets/viewer-runtime.js",
"extensions/diffs-language-pack/assets/viewer-runtime.js",
@@ -274,6 +278,12 @@
"typescript/no-explicit-any": "off"
}
},
{
"files": ["**/*.d.ts"],
"rules": {
"openclaw-type-evidence/no-unknown-type-aliases": "off"
}
},
{
"files": [
"src/**/*.{js,ts,mts,cts}",
+31
View File
@@ -35,3 +35,34 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## anti-slop
OpenClaw's type-evidence Oxlint rules were adapted from anti-slop.
- Upstream: https://github.com/dmmulroy/anti-slop
- Source revision: abaeb63b29e63062f778771d5447bd2e9c3c680f
- License: MIT
- Copyright: Copyright (c) 2026 Dillon Mulroy
MIT License
Copyright (c) 2026 Dillon Mulroy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1
View File
@@ -69,6 +69,7 @@ const ROOT_TEST_ENTRY_GLOBS = [
"test/fixtures/ts-topology/basic/**/*.{js,mjs,cjs,ts,mts,cts}!",
// The focused Oxlint test invokes these deliberate violations by path.
"test/fixtures/oxlint-boundary-guards/*.ts!",
"test/fixtures/oxlint-type-evidence/*.ts!",
] as const;
const workspaces = Object.fromEntries(
+2
View File
@@ -81,6 +81,8 @@ const repositoryScriptEntries = [
"scripts/openclaw-release-clawhub-runtime-state.ts!",
// Oxlint loads this JS plugin by path from config/oxlint/boundary-guards.json.
"scripts/oxlint-boundary-guards.mjs!",
// Oxlint loads this JS plugin by path from the root and focused lint configs.
"scripts/oxlint-type-evidence.mjs!",
"scripts/plugin-prerelease-liveish-matrix.mts!",
// Generates the checked-in native protocol models from core descriptor metadata.
"scripts/protocol-gen.ts!",
+1
View File
@@ -58,6 +58,7 @@ const config = {
],
// Oxlint consumes this required default export through a JSON config path.
"scripts/oxlint-boundary-guards.mjs": ["exports"],
"scripts/oxlint-type-evidence.mjs": ["exports"],
// Wrangler consumes the Worker default export and instantiates the Durable
// Object class by name from wrangler.jsonc; Knip cannot resolve either.
"scripts/cloudflare/src/index.ts": ["exports"],
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "../../node_modules/oxlint/configuration_schema.json",
"plugins": [],
"jsPlugins": ["../../scripts/oxlint-type-evidence.mjs"],
"categories": { "correctness": "off" },
"rules": {
"openclaw-type-evidence/no-unknown-type-aliases": "error",
"openclaw-type-evidence/no-widen-then-assert": "error"
}
}
@@ -1 +1 @@
{"contentHash":"e2a5ce75757e7a275312c91ce95bdb4f13091a3c613dd4c66fdda0b2e4d3bf83","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
{"contentHash":"f95093ed8782337b995c648a13f460ecddba40b11431d1eba6a9781804a0e99d","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"0e4494129d83b508c82788383f8dfead578976ed474be2cfbec557aa6caf88e7","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
{"contentHash":"afcb925bae1788d479e61faf6da9b1f4255d133cd42f387fc31555890fc3cfc1","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"8643a3553553855a519e825fcaa483024ecd3538675c5a2d474af841fb5070d7","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
{"contentHash":"9cac147d4e5170745d298b867ca8c6ffb0513b940cf1d26a62b46a6cceb10852","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
@@ -1 +1 @@
{"contentHash":"13ec7276e7c37ae4e7589e67302c2bf531674f2c439b70d4a8a44a4ee136f685","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
{"contentHash":"4cff7cdf2e9a2818661157b37c6d14dd020f527e00b90fe4452c3aa8281ef16e","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
@@ -1 +1 @@
{"contentHash":"ed0a5bf19bbe6e9641c8de12ea94105865850343c319a58953a2dce21e6d3f49","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"}
{"contentHash":"b4d9b4733b97ce9fa39e03b83b5c4c7b169e2992a2f1f75dcf052cc4c3c294c5","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"}
@@ -1 +1 @@
{"contentHash":"feb6187469b8f0e88c0445ba025cc557c94871db55f39075eb63044d8ba48cd7","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
{"contentHash":"d832c4bc18c7995c6971bbabf9cd742a01656278e8b51ab4d63cd7322f545a88","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
@@ -1 +1 @@
{"contentHash":"97d39817d24eaf5b2808960770447188daa0b2959514efbd44db007797e40e3b","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
{"contentHash":"8211226bd9a2b20b486cf7581e65f116c30063b3971a359cade2921a6a54d6f8","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
@@ -1 +1 @@
{"contentHash":"0d9fc3f9ef06aaa9995c5a670d62b99cfce25f0b2c42152aa33214f9dec78b19","entrypoint":"channel-pairing","importSpecifier":"openclaw/plugin-sdk/channel-pairing"}
{"contentHash":"0186f09bddb33bf9dbe6c2874d3b0e4704d8ce8d055abc694a1fe93f3e5cb980","entrypoint":"channel-pairing","importSpecifier":"openclaw/plugin-sdk/channel-pairing"}
@@ -1 +1 @@
{"contentHash":"e824840c22ccade58891ad2952a6aea4aafa8c163adb4383e9a5aa8b47cce44b","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
{"contentHash":"c89bfe63fcec8e912673caa0995ed2b0de7a624c385f22c24c061239cbbe12bb","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"08bba8059fb44659737936c0ef4c163cfff29872db3088b72d2fff82f4c4ac85","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
{"contentHash":"ece426ee0e06d143d79b9a761743e26564cff5562d6bff95c3467da7387f0518","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"db6ac3119377920f375bdb1cd01082815022f2ad49de3dc5d7ac258a6a864ca6","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
{"contentHash":"75e88b7733d802d1119709f0a88ab2e2a28d9ec95c42e3872bd3a065b8f9a663","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
@@ -1 +1 @@
{"contentHash":"7bb8bb13f101a7018aea253debd9c3611635541b8ad2d847e827300b9f94991f","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
{"contentHash":"d75dd7f78180cebeebd788eec6d738178e0e9a89fef7904ff3807ec35d690cc5","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
@@ -1 +1 @@
{"contentHash":"e5c950c85caf1fe7edf4d672f95afc074e926e300981d96707ed904304e001b4","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
{"contentHash":"d12d25f6cc1177632afe16e110dab1a12f65db41382d3507b99c5eba88fa55b4","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
@@ -1 +1 @@
{"contentHash":"5bb2b4560b715a99bb635510b031b4350b9af16ca4c9247c6d28b54753ef1b7a","entrypoint":"plugin-command-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-command-runtime"}
{"contentHash":"d25a630bd5eec03aadda39b73e4b6b8ef1ba636b2c98afb5e3a52d07f82448bb","entrypoint":"plugin-command-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-command-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"f6867c932f2632409f933d8e158d01f303679950b4e6e3a01ba28b0ea8b4d7da","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
{"contentHash":"ba3e6b379ababd8130f7e600b6860ce07f7121dbc104712a97805c2f3da3e510","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"53a44cf7b1b8866054e905dd7a34623e75dbf4be6da5b58e7d47fd638e2a92f5","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
{"contentHash":"e72b069106dbe98a76e548f1af7e3af244f381e2d6618dd069320a44e966608d","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
@@ -1 +1 @@
{"contentHash":"03aeade34faf6a1c6bfee3e88f684ccb4abe657ea8f3475c96162698baeeb314","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
{"contentHash":"aa2a15aa436ec9cb463a2521ded8cfd92225b441649445a0b1f2deb5f9f7a034","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"b965caa74f02ea0fa0b4b5a57bfacd0699df991452b5aa95f21237858930508f","entrypoint":"runtime-store","importSpecifier":"openclaw/plugin-sdk/runtime-store"}
{"contentHash":"10169d7b8325ccc0de4d24d3d17aeefc689b9508347b2a8698e74d90e54857ae","entrypoint":"runtime-store","importSpecifier":"openclaw/plugin-sdk/runtime-store"}
@@ -1 +1 @@
{"contentHash":"1c76ac2e6a40a56ae0dbf5f6e2b5d911883f8e11cc7c0f435f6cce0c185a60c2","entrypoint":"session-catalog","importSpecifier":"openclaw/plugin-sdk/session-catalog"}
{"contentHash":"016903fae0d5b0858eca5310dd81a38005d2de05343f9e058971a8ca4eaf25e4","entrypoint":"session-catalog","importSpecifier":"openclaw/plugin-sdk/session-catalog"}
@@ -1 +1 @@
{"contentHash":"96150841a6e550228fcf0310e0aaf56ebd75b53ad9c5eb2d0557bbb8a5b67290","entrypoint":"session-store-runtime","importSpecifier":"openclaw/plugin-sdk/session-store-runtime"}
{"contentHash":"42b029dd356f0bb03525f66bb7eb62dd3a2d02aaf72abafd47967d431c625c9b","entrypoint":"session-store-runtime","importSpecifier":"openclaw/plugin-sdk/session-store-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"83795fac5e17a90840255de8446aaf23b988a14d19bc76a327b0b74ef5f9ee8a","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
{"contentHash":"be52565eb108d7328f563882b5bcc26e03496bb3b5ef88879976f03f41b1a3bf","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
@@ -1 +1 @@
{"contentHash":"9511576a6c8d6631cbe81dc7aca200f670feaf7fc03d6b3308066b5caa3ab50c","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
{"contentHash":"4a7e8f3791e029e4ecb8aaa9750190dde04678c5164e151ec2643d677f2627c9","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
@@ -8,14 +8,12 @@ import {
installPwToolsCoreTestHooks();
const mod = await import("./pw-tools-core.interactions.js");
type EvaluateArg = unknown;
function evaluateMockReturning(view: { x: number; y: number; width?: number; height?: number }) {
// Caller reads { x, y, width, height } in one evaluate; default to a normal
// desktop viewport so refs near the top stay in-viewport unless a test puts
// them out of range explicitly.
const result = { width: 1280, height: 720, ...view };
return vi.fn(async (arg: EvaluateArg) => {
return vi.fn(async (arg: unknown) => {
if (typeof arg === "function") {
return result;
}
+13 -17
View File
@@ -12,7 +12,7 @@ import {
resolveMemoryLightDreamingConfig,
resolveMemoryRemDreamingConfig,
} from "openclaw/plugin-sdk/memory-core-host-status";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
import { normalizeStringEntries, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { appendFailedDreamingEvent } from "./dreaming-events.js";
import {
@@ -67,7 +67,6 @@ import {
} from "./short-term-promotion.js";
type Logger = Pick<OpenClawPluginApi["logger"], "info" | "warn" | "error">;
type DreamingHostConfig = unknown;
type DreamingPhaseStorageConfig = {
timezone?: string;
storage: { mode: "inline" | "separate" | "both"; separateReports: boolean };
@@ -557,7 +556,7 @@ function isCheckpointSessionTranscriptPath(absolutePath: string): boolean {
}
function resolveSessionAgentsForWorkspace(params: {
cfg: DreamingHostConfig;
cfg: OpenClawConfig;
workspaceDir: string;
primaryWorkspaceDir?: string;
}): string[] {
@@ -566,13 +565,10 @@ function resolveSessionAgentsForWorkspace(params: {
return [];
}
const target = normalizeMemoryCoreWorkspaceKey(workspaceDir);
const workspaces = resolveMemoryDreamingWorkspaces(
cfg as Parameters<typeof resolveMemoryDreamingWorkspaces>[0],
{
primaryWorkspaceDir,
primaryAgentId: "main",
},
);
const workspaces = resolveMemoryDreamingWorkspaces(cfg, {
primaryWorkspaceDir,
primaryAgentId: "main",
});
const match = workspaces.find(
(entry) => normalizeMemoryCoreWorkspaceKey(entry.workspaceDir) === target,
);
@@ -584,7 +580,7 @@ function resolveSessionAgentsForWorkspace(params: {
async function collectSessionIngestionBatches(params: {
workspaceDir: string;
cfg?: DreamingHostConfig;
cfg?: OpenClawConfig;
primaryWorkspaceDir?: string;
lookbackDays: number;
nowMs: number;
@@ -719,7 +715,7 @@ async function collectSessionIngestionBatches(params: {
async function ingestSessionTranscriptSignals(params: {
workspaceDir: string;
cfg?: DreamingHostConfig;
cfg?: OpenClawConfig;
primaryWorkspaceDir?: string;
lookbackDays: number;
nowMs: number;
@@ -1286,7 +1282,7 @@ export function previewRemDreaming(params: {
async function runLightDreaming(params: {
agentId?: string;
workspaceDir: string;
cfg?: DreamingHostConfig;
cfg?: OpenClawConfig;
primaryWorkspaceDir?: string;
config: LightDreamingConfig;
logger: Logger;
@@ -1386,7 +1382,7 @@ async function runLightDreaming(params: {
async function runRemDreaming(params: {
agentId?: string;
workspaceDir: string;
cfg?: DreamingHostConfig;
cfg?: OpenClawConfig;
primaryWorkspaceDir?: string;
config: RemDreamingConfig;
logger: Logger;
@@ -1504,7 +1500,7 @@ export async function runDreamingSweepPhases(params: {
agentId?: string;
workspaceDir: string;
pluginConfig?: Record<string, unknown>;
cfg?: DreamingHostConfig;
cfg?: OpenClawConfig;
logger: Logger;
subagent?: DreamNarrativeRequest["subagent"];
detachNarratives?: boolean;
@@ -1524,7 +1520,7 @@ export async function runDreamingSweepPhases(params: {
const light = resolveMemoryLightDreamingConfig({
pluginConfig: params.pluginConfig,
cfg: params.cfg as Parameters<typeof resolveMemoryLightDreamingConfig>[0]["cfg"],
cfg: params.cfg,
});
if (light.enabled && light.limit > 0) {
try {
@@ -1555,7 +1551,7 @@ export async function runDreamingSweepPhases(params: {
const rem = resolveMemoryRemDreamingConfig({
pluginConfig: params.pluginConfig,
cfg: params.cfg as Parameters<typeof resolveMemoryRemDreamingConfig>[0]["cfg"],
cfg: params.cfg,
});
if (rem.enabled && rem.limit > 0) {
try {
@@ -195,11 +195,11 @@ describe("searxng web search provider", () => {
it("persists base URL to plugin config via setConfiguredCredentialValue", () => {
const provider = createSearxngWebSearchProvider();
const config = {} as Record<string, unknown>;
const setConfiguredCredentialValue = provider.setConfiguredCredentialValue;
if (!setConfiguredCredentialValue) {
throw new Error("Expected SearXNG provider setConfiguredCredentialValue");
}
const config: Parameters<typeof setConfiguredCredentialValue>[0] = {};
setConfiguredCredentialValue(config, "http://search.local:9000");
+441
View File
@@ -0,0 +1,441 @@
// Adapted from dmmulroy/anti-slop at abaeb63b29e63062f778771d5447bd2e9c3c680f.
// See THIRD_PARTY_NOTICES.md for the upstream MIT license.
const functionBoundaryTypes = new Set([
"ArrowFunctionExpression",
"FunctionDeclaration",
"FunctionExpression",
"TSDeclareFunction",
"TSEmptyBodyFunctionExpression",
]);
function unwrapExpressionParentheses(expression) {
let current = expression;
while (current.type === "ParenthesizedExpression") {
current = current.expression;
}
return current;
}
function unwrapTypeParentheses(type) {
let current = type;
while (current.type === "TSParenthesizedType") {
current = current.typeAnnotation;
}
return current;
}
function typeReferenceName(type) {
return type.typeName.type === "Identifier" ? type.typeName.name : null;
}
function isUnknownOrAnyType(type) {
const unwrapped = unwrapTypeParentheses(type);
return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword";
}
function isBroadRecordKeyType(type) {
const unwrapped = unwrapTypeParentheses(type);
if (
unwrapped.type === "TSStringKeyword" ||
unwrapped.type === "TSNumberKeyword" ||
unwrapped.type === "TSSymbolKeyword"
) {
return true;
}
if (unwrapped.type === "TSUnionType") {
return unwrapped.types.every(isBroadRecordKeyType);
}
return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
}
function isBroadRecordType(type) {
const unwrapped = unwrapTypeParentheses(type);
if (unwrapped.type === "TSTypeReference") {
if (typeReferenceName(unwrapped) === "Readonly") {
const [inner] = unwrapped.typeArguments?.params ?? [];
return inner !== undefined && isBroadRecordType(inner);
}
if (typeReferenceName(unwrapped) !== "Record") {
return false;
}
const parameters = unwrapped.typeArguments?.params ?? [];
return (
parameters.length === 2 &&
parameters[0] !== undefined &&
parameters[1] !== undefined &&
isBroadRecordKeyType(parameters[0]) &&
isUnknownOrAnyType(parameters[1])
);
}
if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) {
return false;
}
const [member] = unwrapped.members;
const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
return (
member?.type === "TSIndexSignature" &&
member.parameters.length === 1 &&
parameter !== undefined &&
isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) &&
isUnknownOrAnyType(member.typeAnnotation.typeAnnotation)
);
}
function broadTypeKind(type) {
const unwrapped = unwrapTypeParentheses(type);
if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") {
return "top";
}
if (unwrapped.type === "TSObjectKeyword") {
return "object";
}
return isBroadRecordType(unwrapped) ? "record" : null;
}
function assertedExpression(node) {
return unwrapExpressionParentheses(node.expression);
}
function assertionFromExpression(expression) {
const unwrapped = unwrapExpressionParentheses(expression);
return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion"
? unwrapped
: null;
}
function normalizedTypeText(sourceText, type) {
return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, "");
}
function typesHaveSameSyntax(sourceText, left, right) {
return (
left !== null &&
normalizedTypeText(sourceText, unwrapTypeParentheses(left)) ===
normalizedTypeText(sourceText, unwrapTypeParentheses(right))
);
}
function isDefinitelyObjectType(type) {
const unwrapped = unwrapTypeParentheses(type);
switch (unwrapped.type) {
case "TSArrayType":
case "TSConstructorType":
case "TSFunctionType":
case "TSMappedType":
case "TSObjectKeyword":
case "TSTupleType":
return true;
case "TSTypeLiteral":
return unwrapped.members.length > 0;
case "TSIntersectionType":
return unwrapped.types.every(isDefinitelyObjectType);
case "TSTypeOperator":
return unwrapped.operator === "readonly" && isDefinitelyObjectType(unwrapped.typeAnnotation);
default:
return false;
}
}
function isDefinitelyNarrowerRecordType(type) {
const unwrapped = unwrapTypeParentheses(type);
if (unwrapped.type === "TSTypeLiteral") {
return unwrapped.members.some((member) => member.type !== "TSIndexSignature");
}
if (unwrapped.type !== "TSTypeReference") {
return false;
}
if (typeReferenceName(unwrapped) === "Readonly") {
const [inner] = unwrapped.typeArguments?.params ?? [];
return inner !== undefined && isDefinitelyNarrowerRecordType(inner);
}
if (typeReferenceName(unwrapped) !== "Record") {
return false;
}
const parameters = unwrapped.typeArguments?.params ?? [];
return (
parameters.length === 2 && parameters[1] !== undefined && !isUnknownOrAnyType(parameters[1])
);
}
function functionBoundary(node) {
let current = node.parent;
while (current !== null && current.type !== "Program") {
if (functionBoundaryTypes.has(current.type)) {
return current;
}
current = current.parent;
}
return null;
}
function resolvedVariableForIdentifier(scopes, identifier) {
for (const scope of scopes) {
const reference = scope.references.find(
(candidate) =>
candidate.identifier.start === identifier.start &&
candidate.identifier.end === identifier.end,
);
if (reference !== undefined) {
return reference.resolved;
}
}
return null;
}
function variableDeclarator(variable) {
for (const definition of variable.defs) {
if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") {
return definition.node;
}
}
return null;
}
function knownValueEvidence(expression, scopes, boundary, visitedVariables) {
const unwrapped = unwrapExpressionParentheses(expression);
if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
if (broadTypeKind(unwrapped.typeAnnotation) !== null) {
return null;
}
return { type: unwrapped.typeAnnotation };
}
if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") {
return { type: null };
}
if (
unwrapped.type === "ArrayExpression" ||
unwrapped.type === "ArrowFunctionExpression" ||
unwrapped.type === "ClassExpression" ||
unwrapped.type === "FunctionExpression" ||
unwrapped.type === "NewExpression" ||
unwrapped.type === "ObjectExpression"
) {
return { type: null };
}
if (unwrapped.type !== "Identifier") {
return null;
}
const variable = resolvedVariableForIdentifier(scopes, unwrapped);
if (variable === null || visitedVariables.has(variable)) {
return null;
}
const annotatedIdentifier = variable.identifiers.find(
(identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== undefined,
);
const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation;
if (annotation !== undefined && annotatedIdentifier !== undefined) {
if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) {
return null;
}
return { type: annotation };
}
const declarator = variableDeclarator(variable);
if (
declarator === null ||
declarator.parent.type !== "VariableDeclaration" ||
declarator.parent.kind !== "const" ||
declarator.init === null ||
variable.references.some((reference) => reference.isWrite() && !reference.init) ||
functionBoundary(declarator) !== boundary
) {
return null;
}
return knownValueEvidence(
declarator.init,
scopes,
boundary,
new Set([...visitedVariables, variable]),
);
}
function widenedBinding(variable, scopes) {
const declarator = variableDeclarator(variable);
if (
declarator === null ||
declarator.parent.type !== "VariableDeclaration" ||
declarator.parent.kind !== "const" ||
declarator.id.type !== "Identifier" ||
declarator.init === null ||
variable.references.some((reference) => reference.isWrite() && !reference.init)
) {
return null;
}
const boundary = functionBoundary(declarator);
const declaredType = declarator.id.typeAnnotation?.typeAnnotation;
const initializerAssertion = assertionFromExpression(declarator.init);
const initializerBroadKind =
initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation);
const declaredBroadKind = declaredType === undefined ? null : broadTypeKind(declaredType);
const broadKind = declaredBroadKind ?? initializerBroadKind;
if (broadKind === null) {
return null;
}
const originalExpression =
initializerAssertion !== null && initializerBroadKind !== null
? assertedExpression(initializerAssertion)
: declarator.init;
const evidence = knownValueEvidence(originalExpression, scopes, boundary, new Set([variable]));
return evidence === null ? null : { broadKind, evidence, declaredAt: declarator.end, boundary };
}
function assertionIsNarrower(sourceText, broadKind, evidence, assertedType) {
if (broadTypeKind(assertedType) !== null) {
return false;
}
if (broadKind === "top") {
return true;
}
if (typesHaveSameSyntax(sourceText, evidence.type, assertedType)) {
return true;
}
if (broadKind === "object") {
return isDefinitelyObjectType(assertedType);
}
return isDefinitelyNarrowerRecordType(assertedType);
}
const noUnknownTypeAliasesRule = {
meta: {
type: "problem",
docs: {
description:
"Disallow type aliases whose resolved type is unknown; unknown must remain visible at an allowed boundary.",
},
messages: {
unknownAlias:
"Type alias `{{alias}}` only renames `unknown`. Keep `unknown` explicit at the boundary or replace it with the parsed owner type.",
},
},
create(context) {
const aliases = new Map();
const resolvesToUnknown = (type, visited = new Set()) => {
if (type.type === "TSUnknownKeyword") {
return true;
}
if (type.type === "TSParenthesizedType") {
return resolvesToUnknown(type.typeAnnotation, visited);
}
const name =
type.type === "TSTypeReference" &&
type.typeName.type === "Identifier" &&
(type.typeArguments === null ||
type.typeArguments === undefined ||
type.typeArguments.params.length === 0)
? type.typeName.name
: null;
if (name === null || visited.has(name)) {
return false;
}
const alias = aliases.get(name);
if (
alias === undefined ||
(alias.typeParameters !== null && alias.typeParameters !== undefined)
) {
return false;
}
const nextVisited = new Set(visited);
nextVisited.add(name);
return resolvesToUnknown(alias.typeAnnotation, nextVisited);
};
return {
Program(node) {
for (const statement of node.body) {
const declaration =
statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
if (declaration?.type === "TSTypeAliasDeclaration") {
aliases.set(declaration.id.name, declaration);
}
}
for (const alias of aliases.values()) {
if (!resolvesToUnknown(alias.typeAnnotation, new Set([alias.id.name]))) {
continue;
}
context.report({
node: alias.id,
messageId: "unknownAlias",
data: { alias: alias.id.name },
});
}
},
};
},
};
const noWidenThenAssertRule = {
meta: {
type: "problem",
docs: {
description:
"Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type.",
},
messages: {
widenThenAssert:
'Binding "{{name}}" erases established type evidence by widening the value, then reconstructs that evidence with a type assertion. Preserve the precise type end-to-end; if the input is genuinely unknown, parse it once at the boundary instead.',
},
},
create(context) {
const scopes = context.sourceCode.scopeManager.scopes;
const checkAssertion = (node) => {
const expression = assertedExpression(node);
if (expression.type !== "Identifier") {
return;
}
const variable = resolvedVariableForIdentifier(scopes, expression);
if (variable === null) {
return;
}
const widened = widenedBinding(variable, scopes);
if (
widened === null ||
node.start <= widened.declaredAt ||
functionBoundary(node) !== widened.boundary ||
!assertionIsNarrower(
context.sourceCode.text,
widened.broadKind,
widened.evidence,
node.typeAnnotation,
)
) {
return;
}
context.report({
node,
messageId: "widenThenAssert",
data: { name: expression.name },
});
};
return {
TSAsExpression: checkAssertion,
TSTypeAssertion: checkAssertion,
};
},
};
export default {
meta: { name: "openclaw-type-evidence" },
rules: {
"no-unknown-type-aliases": noUnknownTypeAliasesRule,
"no-widen-then-assert": noWidenThenAssertRule,
},
};
@@ -512,9 +512,9 @@ export function wrapStreamFnSanitizeMalformedToolCalls(
}
}
const nextContext = {
...(context as unknown as Record<string, unknown>),
messages: nextMessages,
} as unknown;
return baseFn(model, nextContext as typeof context, options);
...context,
messages: nextMessages as typeof context.messages,
};
return baseFn(model, nextContext, options);
};
}
+6 -11
View File
@@ -1,4 +1,3 @@
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
/**
* Builds prepared runtime plans consumed by embedded agent runs. A plan
* centralizes provider hooks, auth, tool schema policy, transcript policy,
@@ -46,10 +45,6 @@ function formatResolvedRef(params: { provider: string; modelId: string }): strin
return `${params.provider}/${params.modelId}`;
}
function asOpenClawConfig(value: unknown): OpenClawConfig | undefined {
return asOptionalRecord(value) as OpenClawConfig | undefined;
}
function asProviderRuntimeModel(
value: BuildAgentRuntimePlanParams["model"],
): ProviderRuntimeModel | undefined {
@@ -62,7 +57,7 @@ type RuntimePlanMetadataParams = BuildAgentRuntimeDeliveryPlanParams & {
function resolveCompatibleMetadataSnapshot(
params: RuntimePlanMetadataParams,
config: OpenClawConfig | undefined = asOpenClawConfig(params.config),
config: OpenClawConfig | undefined = params.config,
): PluginMetadataSnapshot | undefined {
const metadataSnapshot = params.metadataSnapshot as PluginMetadataSnapshot | undefined;
return metadataSnapshot &&
@@ -96,7 +91,7 @@ function resolvePreparedProviderRuntimeHandle(
...resolveProviderRuntimePluginHandle({
provider: params.provider,
modelId: params.modelId,
config: asOpenClawConfig(params.config),
config: params.config,
workspaceDir: params.workspaceDir,
env: process.env,
...(compatibleMetadataSnapshot ? { pluginMetadataSnapshot: compatibleMetadataSnapshot } : {}),
@@ -110,7 +105,7 @@ function resolvePreparedProviderRuntimeHandle(
export function buildAgentRuntimeDeliveryPlan(
params: BuildAgentRuntimeDeliveryPlanParams,
): AgentRuntimeDeliveryPlan {
const config = asOpenClawConfig(params.config);
const config = params.config;
const providerRuntimeHandle = resolvePreparedProviderRuntimeHandle(params);
return {
isSilentPayload(payload): boolean {
@@ -151,7 +146,7 @@ function buildAgentRuntimeOutcomePlan(): AgentRuntimeOutcomePlan {
/** Build the complete runtime plan for an embedded agent attempt. */
export function buildAgentRuntimePlan(params: BuildAgentRuntimePlanParams): AgentRuntimePlan {
const config = asOpenClawConfig(params.config);
const config = params.config;
const model = asProviderRuntimeModel(params.model);
const modelApi = params.modelApi ?? params.model?.api ?? undefined;
const transport = params.resolvedTransport;
@@ -282,7 +277,7 @@ export function buildAgentRuntimePlan(params: BuildAgentRuntimePlanParams): Agen
runtimeHandle: providerRuntimeHandleForPlugins,
context: {
...context,
config: asOpenClawConfig(context.config),
config: context.config,
},
});
},
@@ -294,7 +289,7 @@ export function buildAgentRuntimePlan(params: BuildAgentRuntimePlanParams): Agen
runtimeHandle: providerRuntimeHandleForPlugins,
context: {
...context,
config: asOpenClawConfig(context.config),
config: context.config,
},
});
},
+5 -7
View File
@@ -4,6 +4,7 @@
* observability decisions shared across embedded-agent hot paths.
*/
import type { TSchema } from "typebox";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { ModelPickerAction } from "../../interactive/payload.js";
import type {
ModelApi,
@@ -51,9 +52,6 @@ type AgentRuntimeFailoverReason =
| "unclassified"
| "unknown";
/** Provider/runtime config object passed through plugin boundaries. */
type AgentRuntimeConfig = unknown;
/** Provider model descriptor consumed by runtime-plan hooks. */
type AgentRuntimeModel = {
id?: string;
@@ -91,7 +89,7 @@ type AgentRuntimeTextTransforms = {
type AgentRuntimeProviderHandle = {
provider: string;
modelId?: string | null;
config?: AgentRuntimeConfig;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
applyAutoEnable?: boolean;
@@ -341,7 +339,7 @@ type AgentRuntimeSystemPromptContribution = {
/** Context passed when resolving provider system prompt contributions. */
type AgentRuntimeSystemPromptContributionContext = {
config?: AgentRuntimeConfig;
config?: OpenClawConfig;
agentDir?: string;
workspaceDir?: string;
provider: string;
@@ -566,7 +564,7 @@ export type AgentRuntimePlan = {
/** Inputs needed to build delivery-only runtime decisions. */
export type BuildAgentRuntimeDeliveryPlanParams = {
config?: AgentRuntimeConfig;
config?: OpenClawConfig;
workspaceDir?: string;
agentDir?: string;
provider: string;
@@ -576,7 +574,7 @@ export type BuildAgentRuntimeDeliveryPlanParams = {
/** Inputs needed to build the full prepared runtime plan. */
export type BuildAgentRuntimePlanParams = {
config?: AgentRuntimeConfig;
config?: OpenClawConfig;
workspaceDir?: string;
agentDir?: string;
provider: string;
+1 -3
View File
@@ -1086,8 +1086,6 @@ export interface ContextEventResult {
messages?: AgentMessage[];
}
type BeforeProviderRequestEventResult = unknown;
export interface ToolCallEventResult {
/** Block tool execution. To modify arguments, mutate `event.input` in place instead. */
block?: boolean;
@@ -1225,7 +1223,7 @@ export interface ExtensionAPI {
on(event: "context", handler: ExtensionHandler<ContextEvent, ContextEventResult>): void;
on(
event: "before_provider_request",
handler: ExtensionHandler<BeforeProviderRequestEvent, BeforeProviderRequestEventResult>,
handler: ExtensionHandler<BeforeProviderRequestEvent, unknown>,
): void;
on(event: "after_provider_response", handler: ExtensionHandler<AfterProviderResponseEvent>): void;
on(
+4 -3
View File
@@ -1,4 +1,5 @@
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import type { TranscriptEvent } from "../../config/sessions/session-accessor.js";
import { selectSessionTranscriptLeafControlledPath } from "../../config/sessions/transcript-tree.js";
import { CURRENT_SESSION_VERSION } from "../../config/sessions/version.js";
import { logWarn } from "../../logger.js";
@@ -388,13 +389,13 @@ export function parseOpaqueLeafEntry(record: unknown):
};
}
export function partitionSessionFileEntries(entries: readonly FileEntry[]): {
export function partitionSessionFileEntries(entries: readonly TranscriptEvent[]): {
fileEntries: FileEntry[];
opaqueEntries: Array<{ index: number; record: unknown }>;
opaqueEntries: Array<{ index: number; record: TranscriptEvent }>;
fileEntriesByOriginalIndex: Array<FileEntry | undefined>;
} {
const fileEntries: FileEntry[] = [];
const opaqueEntries: Array<{ index: number; record: unknown }> = [];
const opaqueEntries: Array<{ index: number; record: TranscriptEvent }> = [];
const fileEntriesByOriginalIndex: Array<FileEntry | undefined> = [];
const header = entries.find(
(entry) => isRecord(entry) && entry.type === "session" && typeof entry.id === "string",
+19 -9
View File
@@ -1,7 +1,9 @@
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import {
loadTranscriptEventsSync,
replaceTranscriptEventsSync,
type SessionTranscriptRuntimeTarget,
type TranscriptEvent,
} from "../../config/sessions/session-accessor.js";
import { isSessionTranscriptSideAppendEntry } from "../../config/sessions/transcript-tree.js";
import { CURRENT_SESSION_VERSION } from "../../config/sessions/version.js";
@@ -58,19 +60,19 @@ export class SessionManagerCore {
}
setSessionTarget(target: SessionManagerPersistenceTarget): void {
const entries = loadTranscriptEventsSync(target) as FileEntry[];
const entries = loadTranscriptEventsSync(target);
const header = entries.find(
(entry) => typeof entry === "object" && entry !== null && entry.type === "session",
(entry): entry is Record<string, unknown> => isRecord(entry) && entry.type === "session",
);
this.setLoadedSessionTarget(target, entries);
if (header?.cwd) {
if (typeof header?.cwd === "string") {
this.cwd = header.cwd;
}
}
protected setLoadedSessionTarget(
target: SessionManagerPersistenceTarget | undefined,
entries: FileEntry[],
entries: TranscriptEvent[],
): void {
const partitioned = partitionSessionFileEntries(entries);
// Only a physically empty transcript may initialize lazily. Opaque persisted rows still need
@@ -441,13 +443,17 @@ export class SessionManagerCore {
protected getPersistedFileEntries(
leafAppendParentId: string | null = this.appendParentId,
leafAppendMode?: "side",
): unknown[] {
): TranscriptEvent[] {
this.clampOpaqueFileEntryIndexes();
const entries: unknown[] = [];
const entries: TranscriptEvent[] = [];
let opaqueIndex = 0;
for (let index = 0; index <= this.fileEntries.length; index += 1) {
while (this.opaqueFileEntries[opaqueIndex]?.index === index) {
entries.push(this.opaqueFileEntries[opaqueIndex]?.record);
const opaqueEntry = this.opaqueFileEntries[opaqueIndex];
if (!opaqueEntry) {
break;
}
entries.push(opaqueEntry.record);
opaqueIndex += 1;
}
const entry = this.fileEntries[index];
@@ -456,7 +462,11 @@ export class SessionManagerCore {
}
}
while (opaqueIndex < this.opaqueFileEntries.length) {
entries.push(this.opaqueFileEntries[opaqueIndex]?.record);
const opaqueEntry = this.opaqueFileEntries[opaqueIndex];
if (!opaqueEntry) {
break;
}
entries.push(opaqueEntry.record);
opaqueIndex += 1;
}
@@ -498,7 +508,7 @@ export class SessionManagerCore {
return entries;
}
getPersistedEntries(): unknown[] {
getPersistedEntries(): TranscriptEvent[] {
return this.getPersistedFileEntries();
}
@@ -180,7 +180,7 @@ export class SessionManagerPersistence extends SessionManagerCore {
this.persistenceHeaderPending = false;
}
const leafEntry = parseOpaqueLeafEntry(entry);
if (leafEntry) {
if (leafEntry && isRecord(entry)) {
requireTranscriptEventAppend(
appendTranscriptEventSync(scope, entry),
`Session transcript leaf control was not persisted: ${leafEntry.id}`,
+2 -1
View File
@@ -1,3 +1,4 @@
import type { TranscriptEvent } from "../../config/sessions/session-accessor.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { ImageContent, TextContent } from "../../llm/types.js";
import type { AgentMessage } from "../runtime/index.js";
@@ -133,7 +134,7 @@ export interface SessionContext {
export type PreservedOpaqueFileEntry = {
index: number;
record: unknown;
record: TranscriptEvent;
};
export type SessionLeafControl = {
@@ -4,6 +4,7 @@ import { SessionManager } from "../agents/sessions/session-manager.js";
import {
loadTranscriptEventsSync,
replaceTranscriptEventsSync,
type TranscriptEvent,
upsertSessionEntryCore,
} from "../config/sessions/session-accessor.js";
import { readTranscriptStorageRows } from "../config/sessions/session-accessor.sqlite-read.js";
@@ -67,7 +68,7 @@ describe("doctor SQLite session transcript header repair", () => {
});
async function seedHeaderlessTranscript(
events: readonly unknown[],
events: readonly TranscriptEvent[],
options: { spawnedCwd?: string } = { spawnedCwd: SPAWNED_CWD },
): Promise<void> {
await upsertSessionEntryCore(scope, {
@@ -14,6 +14,7 @@ import type {
SessionLifecycleArtifactCleanupResult,
SessionLifecycleStoreTarget,
} from "./session-accessor.lifecycle-types.js";
import type { TranscriptEvent } from "./session-accessor.types.js";
import type { ResolvedSessionMaintenanceConfig } from "./store-maintenance.js";
import type { TranscriptEntryAnchor } from "./transcript-entry-anchor.js";
import type { SessionEntry } from "./types.js";
@@ -77,8 +78,6 @@ export type SessionTranscriptInstance = SessionEntrySummary & {
updatedAtMs: number;
};
export type TranscriptEvent = unknown;
export type TranscriptEventAppendOptions = {
appendIntent?: "active-branch";
};
@@ -119,6 +118,7 @@ export type {
SessionTranscriptRawDeltaResult,
SessionTranscriptVisibleMessageDeltaLimits,
SessionTranscriptVisibleMessageDeltaResult,
TranscriptEvent,
} from "./session-accessor.types.js";
export type TranscriptMessageAppendOptions<TMessage> = {
@@ -193,8 +193,8 @@ export type ExactSessionEntry = {
entry: SessionEntry;
};
/** Raw transcript record for non-message events; message records use appendTranscriptMessage. */
export type TranscriptEvent = unknown;
/** JSON event persisted in a transcript; message records use appendTranscriptMessage. */
export type TranscriptEvent = object | string | number | boolean | null;
export type SessionTranscriptStats = {
eventCount: number;
@@ -1,5 +1,6 @@
import { randomUUID } from "node:crypto";
import path from "node:path";
import type { TranscriptEvent } from "./session-accessor.types.js";
import {
DEFAULT_REPLAY_MAX_MESSAGES,
replayableTranscriptRole,
@@ -21,7 +22,7 @@ type SessionResetBoundaryEvent = {
export type SessionResetBoundaryPlan = {
event: SessionResetBoundaryEvent;
seedEvents: unknown[];
seedEvents: object[];
};
function recordId(record: unknown): string | undefined {
@@ -154,7 +155,7 @@ async function readLegacyTranscriptEvents(sessionFile: string | undefined): Prom
}
export async function buildSessionResetBoundaryPlan(params: {
events: readonly unknown[];
events: readonly TranscriptEvent[];
legacySessionFile?: string;
reason: SessionResetBoundaryReason;
}): Promise<SessionResetBoundaryPlan> {
@@ -169,7 +170,7 @@ export async function buildSessionResetBoundaryPlan(params: {
? []
: await readLegacyTranscriptEvents(params.legacySessionFile);
const seedEvents = legacyEvents.filter(
(event) =>
(event): event is object =>
event !== null &&
typeof event === "object" &&
!Array.isArray(event) &&
@@ -1,7 +1,10 @@
import path from "node:path";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
import { replaceTranscriptEvents } from "../config/sessions/session-accessor.js";
import {
replaceTranscriptEvents,
type TranscriptEvent,
} from "../config/sessions/session-accessor.js";
import { closeOpenClawAgentDatabasesForTest } from "../state/openclaw-agent-db.js";
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
import { captureEnv, setTestEnvValue } from "../test-utils/env.js";
@@ -37,7 +40,7 @@ describe("session transcript reader marker projection", () => {
async function writeTranscript(
sessionId: string,
events: unknown[],
events: TranscriptEvent[],
): Promise<SessionTranscriptReadScope> {
const scope = {
agentId: "main",
@@ -7,6 +7,7 @@ import * as sessionAccessor from "../config/sessions/session-accessor.js";
import {
persistSessionTranscriptTurn,
replaceTranscriptEvents,
type TranscriptEvent,
upsertSessionEntryCore,
} from "../config/sessions/session-accessor.js";
import { waitForSessionTranscriptIndexReconcile } from "../config/sessions/session-transcript-reconcile.js";
@@ -65,7 +66,7 @@ describe("session transcript reader facade", () => {
async function writeTranscript(
sessionId: string,
events: unknown[],
events: TranscriptEvent[],
): Promise<SessionTranscriptReadScope> {
const scope = {
agentId: "main",
@@ -127,8 +127,9 @@ function assertEventIdentitiesUnchanged(
if (before.length !== after.length) {
throw new Error(`${owner} event count changed during media migration`);
}
for (let index = 0; index < before.length; index += 1) {
if (eventIdentity(before[index]) !== eventIdentity(after[index])) {
for (const [index, beforeEvent] of before.entries()) {
const afterEvent = after[index];
if (afterEvent === undefined || eventIdentity(beforeEvent) !== eventIdentity(afterEvent)) {
throw new Error(`${owner} event identity changed at index ${index}`);
}
}
+1 -1
View File
@@ -38,7 +38,7 @@ export type {
BundledEntryModuleLoadOptions,
} from "./channel-entry-contract.types.js";
type BundledChannelRuntime = unknown;
type BundledChannelRuntime = OpenClawPluginApi["runtime"];
type ChannelEntryConfigSchema<TPlugin> =
TPlugin extends ChannelPlugin<unknown>
@@ -3,6 +3,7 @@ import {
resolveSessionTranscriptRuntimeTarget,
withTranscriptWriteLock,
type SessionTranscriptWriteLockAccessorContext,
type TranscriptEvent,
type TranscriptMessageAppendOptions,
type TranscriptMessageAppendResult,
type TranscriptUpdatePayload,
@@ -31,7 +32,7 @@ export type InternalSessionTranscriptWriteLockContext = {
options: Omit<TranscriptMessageAppendOptions<TMessage>, "config">,
) => Promise<TranscriptMessageAppendResult<TMessage> | undefined>;
publishUpdate: (update?: TranscriptUpdatePayload) => Promise<void>;
readEvents: () => Promise<unknown[]>;
readEvents: () => Promise<TranscriptEvent[]>;
target: InternalSessionTranscriptTarget;
};
+2 -2
View File
@@ -20,6 +20,7 @@ import {
type SessionTranscriptRawDeltaLimits,
type SessionTranscriptRawDeltaResult,
type SessionTranscriptVisibleMessageDeltaLimits,
type TranscriptEvent as SessionTranscriptEvent,
} from "../config/sessions/session-accessor.js";
import { resolveMirroredTranscriptText } from "../config/sessions/transcript-mirror.js";
import {
@@ -67,10 +68,9 @@ export type {
SessionTranscriptMemoryHitKey,
SessionTranscriptMemoryHitKeyParams,
SessionTranscriptReadParams,
SessionTranscriptEvent,
};
export type SessionTranscriptEvent = unknown;
export type SessionTranscriptTargetParams = SessionTranscriptReadParams;
/** Scoped target and bounds for one raw generation-aware transcript page. */
@@ -0,0 +1,7 @@
type DirectUnknown = unknown;
type ParenthesizedUnknown = unknown;
type IndirectUnknown = DirectUnknown;
type GenericAlias<T> = T;
type ConcreteAlias = GenericAlias<unknown>;
type UsefulBoundary = { value: unknown };
@@ -0,0 +1,13 @@
type User = { id: string };
const user: unknown = { id: "user-1" };
const assertedUser = user as User;
const original = { id: "user-2" };
const erased = original as unknown;
const restoredUser = erased as User;
const preserved = { id: "user-3" } satisfies User;
const preservedUser: User = preserved;
export { assertedUser, preservedUser, restoredUser };
+14
View File
@@ -0,0 +1,14 @@
type User = { id: string };
type UsefulBoundary = { value: unknown };
const preserved = { id: "user-1" } satisfies User;
const preservedUser: User = preserved;
function parseBoundary(value: unknown): User | undefined {
if (typeof value !== "object" || value === null || !("id" in value)) {
return undefined;
}
return typeof value.id === "string" ? { id: value.id } : undefined;
}
export { parseBoundary, preservedUser, type UsefulBoundary };
+14
View File
@@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest";
type OxlintConfig = {
ignorePatterns?: string[];
jsPlugins?: string[];
overrides?: Array<{
excludeFiles?: string[];
files?: string[];
@@ -147,6 +148,7 @@ describe("oxlint config", () => {
"docs/_layouts/",
".agents/skills/autoreview/tests/fixtures/**",
"test/fixtures/oxlint-boundary-guards/**",
"test/fixtures/oxlint-type-evidence/**",
"**/a2ui.bundle.js",
"extensions/diffs/assets/viewer-runtime.js",
"extensions/diffs-language-pack/assets/viewer-runtime.js",
@@ -291,6 +293,18 @@ describe("oxlint config", () => {
]);
});
it("enables the repository type-evidence plugin", () => {
const config = readJson(".oxlintrc.json") as OxlintConfig;
expect(config.jsPlugins).toContain("./scripts/oxlint-type-evidence.mjs");
expect(config.rules?.["openclaw-type-evidence/no-unknown-type-aliases"]).toBe("error");
expect(config.rules?.["openclaw-type-evidence/no-widen-then-assert"]).toBe("error");
expect(config.overrides).toContainEqual({
files: ["**/*.d.ts"],
rules: { "openclaw-type-evidence/no-unknown-type-aliases": "off" },
});
});
it("enables clean zero-baseline lint rules", () => {
const config = readJson(".oxlintrc.json") as OxlintConfig;
+48
View File
@@ -0,0 +1,48 @@
import { spawnSync } from "node:child_process";
import { describe, expect, it } from "vitest";
const FIXTURES = "test/fixtures/oxlint-type-evidence";
const cases = [
{
rule: "openclaw-type-evidence/no-unknown-type-aliases",
violation: `${FIXTURES}/no-unknown-type-aliases-violation.ts`,
violations: 3,
},
{
rule: "openclaw-type-evidence/no-widen-then-assert",
violation: `${FIXTURES}/no-widen-then-assert-violation.ts`,
violations: 2,
},
];
function runRule(target: string) {
return spawnSync(
process.execPath,
[
"scripts/run-oxlint.mjs",
"--openclaw-focused-config",
"--config",
"config/oxlint/type-evidence.json",
target,
],
{ encoding: "utf8" },
);
}
describe("oxlint type-evidence rules", () => {
it.each(cases)("reports the intended $rule flows", (testCase) => {
const violation = runRule(testCase.violation);
const output = `${violation.stdout}${violation.stderr}`;
expect(violation.status).toBe(1);
expect(output.split(`${testCase.rule.replace("/", "(")})`)).toHaveLength(
testCase.violations + 1,
);
});
it("allows explicit unknown boundaries and preserved type evidence", () => {
const result = runRule(`${FIXTURES}/valid.ts`);
expect(result.status).toBe(0);
expect(`${result.stdout}${result.stderr}`).not.toContain("openclaw-type-evidence");
});
});