fix: repair task 6 composite expression projection
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
Reference in New Issue
Block a user