fix: repair task 6 composite expression projection

This commit is contained in:
lda
2026-08-14 00:16:32 +07:00 Verified
parent b8a16a0fe7
commit 29d7a11da9
10 changed files with 278 additions and 54 deletions
+4
View File
@@ -6,6 +6,10 @@
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./input-expression-limits": {
"types": "./dist/json-schema/input-expression-limits.d.ts",
"default": "./dist/json-schema/input-expression-limits.js"
}
},
"scripts": {
+6
View File
@@ -11,6 +11,12 @@ export {
export { normalizeLoopbackTarget } from "./target-policy.js";
export {
hasBoundedInputExpressionNodeBudget,
MAX_INPUT_EXPRESSION_DEPTH,
MAX_INPUT_EXPRESSION_NODES,
} from "./json-schema/input-expression-limits.js";
// Compile-time wire inventory only; supported operations and browser policy stay authored.
export * from "./generated/workflow-contract.js";
@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import {
hasBoundedInputExpressionNodeBudget,
MAX_INPUT_EXPRESSION_DEPTH,
MAX_INPUT_EXPRESSION_NODES,
} from "./input-expression-limits.js";
const nestedExpression = (arrayDepth: number): unknown => {
let expression: unknown = { kind: "literal", value: "leaf" };
for (let depth = 0; depth < arrayDepth; depth += 1) {
expression = { kind: "array", items: [expression] };
}
return expression;
};
describe("input expression limits", () => {
it("uses the Python root-at-depth-one boundary", () => {
expect(hasBoundedInputExpressionNodeBudget(nestedExpression(MAX_INPUT_EXPRESSION_DEPTH - 1))).toBe(true);
expect(hasBoundedInputExpressionNodeBudget(nestedExpression(MAX_INPUT_EXPRESSION_DEPTH))).toBe(false);
});
it("counts literal array and object containers", () => {
const expression = (objectCount: number) => ({
kind: "literal",
value: { items: Array.from({ length: objectCount }, () => ({})) },
});
expect(hasBoundedInputExpressionNodeBudget(expression(MAX_INPUT_EXPRESSION_NODES - 3))).toBe(true);
expect(hasBoundedInputExpressionNodeBudget(expression(MAX_INPUT_EXPRESSION_NODES - 2))).toBe(false);
});
it("counts literal container depth from the expression root", () => {
const nestedLiteral = (containerDepth: number): unknown => {
let value: unknown = "leaf";
for (let depth = 0; depth < containerDepth; depth += 1) value = { nested: value };
return { kind: "literal", value };
};
expect(hasBoundedInputExpressionNodeBudget(nestedLiteral(MAX_INPUT_EXPRESSION_DEPTH - 1))).toBe(true);
expect(hasBoundedInputExpressionNodeBudget(nestedLiteral(MAX_INPUT_EXPRESSION_DEPTH))).toBe(false);
});
it("rejects sparse arrays instead of skipping holes", () => {
const sparse = [] as unknown[];
sparse.length = 1;
expect(hasBoundedInputExpressionNodeBudget({ kind: "literal", value: sparse })).toBe(false);
});
});
@@ -2,6 +2,7 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
export const MAX_INPUT_EXPRESSION_NODES = 1024;
export const MAX_INPUT_EXPRESSION_DEPTH = 64;
const expressionKinds = new Set(["literal", "path", "array", "object"]);
@@ -24,39 +25,70 @@ export const hasBoundedInputExpressionNodeBudget = (
return true;
};
const visitJson = (value: unknown): boolean => {
const visitJson = (value: unknown, depth: number): boolean => {
if (typeof value !== "object" || value === null) return true;
if (depth > MAX_INPUT_EXPRESSION_DEPTH) return false;
if (!visitNode(value)) return false;
const valid = Array.isArray(value)
? value.every(visitJson)
: Object.values(value).every(visitJson);
if (Array.isArray(value)) {
for (let index = 0; index < value.length; index += 1) {
if (!Object.prototype.hasOwnProperty.call(value, index) || !visitJson(value[index], depth + 1)) return false;
}
active.delete(value);
return true;
}
const valid = Object.values(value).every((item) => visitJson(item, depth + 1));
active.delete(value);
return valid;
};
const visitExpression = (value: unknown): boolean => {
const visitExpression = (value: unknown, depth: number): boolean => {
if (depth > MAX_INPUT_EXPRESSION_DEPTH) return false;
if (!isRecord(value) || typeof value.kind !== "string") return false;
if (!expressionKinds.has(value.kind) || !visitNode(value)) return false;
let valid = true;
switch (value.kind) {
case "literal":
valid = visitJson(value.value);
valid = visitJson(value.value, depth + 1);
break;
case "path":
break;
case "array":
valid = Array.isArray(value.items) && value.items.every(visitExpression);
{
const items = value.items;
if (!Array.isArray(items)) {
valid = false;
break;
}
for (let index = 0; index < items.length; index += 1) {
if (!Object.prototype.hasOwnProperty.call(items, index) || !visitExpression(items[index], depth + 1)) {
valid = false;
break;
}
}
}
break;
case "object":
valid = isRecord(value.fields) && Object.values(value.fields).every(visitExpression);
{
const fields = value.fields;
if (!isRecord(fields)) {
valid = false;
break;
}
for (const item of Object.values(fields)) {
if (!visitExpression(item, depth + 1)) {
valid = false;
break;
}
}
}
break;
}
active.delete(value);
return valid;
};
return visitExpression(input);
return visitExpression(input, 1);
};
type JsonSchemaRecord = Readonly<Record<string, unknown>>;