Merge pull request #843 from morluto/fix/exact-empty-graph-identities
fix(javascript): keep exact empty values in graph identities
This commit is contained in:
commit
f608127635
|
|
@ -645,9 +645,9 @@
|
|||
"mcp_prompts": 6
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "1f1acf182aa87964404e65f3acb00c44f1c392373c5a8a6f40c250da8b94c274",
|
||||
"tools_sha256": "5d4fa325e1649ae3f75aa6f9cbba7fac1175440a0e2792e6e1a469849e2c2dc3",
|
||||
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
|
||||
"combined_sha256": "0efbf17dddbdf52f84d4ca1f9a3e007a0d77c2623be62ce0a69844d617bf16da",
|
||||
"combined_sha256": "d8e9c865c40f7d8f96e21f575d7157cbd000e86991a1944ba2aa07c2f1d70bc7",
|
||||
"providers_sha256": "0bccfa9588376a90350d18abded999db7e1d0e4424a61a21d03d392a1c5418fe"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"manifest_id": "ecm_d24d65186c309bbfa713e50851ddae373e1dd874ff444f717e8e379946ed2407",
|
||||
"manifest_id": "ecm_6376482bb14d464efebd1947cd6820fac3edfdd17592cedb0ddfe5ad58ef2493",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "6dfe4f9c24f9438982910f3170a7b0b1798761da6ad9a487d73e662f4c23e9ba"
|
||||
"sha256": "f765c513b854f5ea2db449801cc39a27e34dc05c378760eda5a049b529e80392"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi
|
|||
metadata:
|
||||
version: "26"
|
||||
tool_count: 127
|
||||
catalog_digest: "0efbf17dddbdf52f84d4ca1f9a3e007a0d77c2623be62ce0a69844d617bf16da"
|
||||
catalog_digest: "d8e9c865c40f7d8f96e21f575d7157cbd000e86991a1944ba2aa07c2f1d70bc7"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../domain/javascriptApplicationAnalysis.js";
|
||||
import { matchJavaScriptApplicationVersions } from "../domain/javascriptApplicationVersionKeys.js";
|
||||
import { findApplicationFeatureSeeds } from "../domain/javascriptFeatureSeed.js";
|
||||
import { traceApplicationFeatureInputSchema } from "../domain/javascriptFeatureTraceSchemas.js";
|
||||
import { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js";
|
||||
|
||||
const analyzeSource = async (
|
||||
|
|
@ -50,9 +55,9 @@ it.each([
|
|||
await expect(analyzeSource(source)).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("displays empty literals as nonempty text while retaining exact values", async () => {
|
||||
it("keeps exact values for empty literals without synthetic application labels", async () => {
|
||||
const output = await analyzeSource(
|
||||
"fetch(''); fetch('/api'); process.on('', () => 1);",
|
||||
"fetch(''); fetch('\"\"'); fetch('/api'); process.on('', () => 1);",
|
||||
);
|
||||
const endpoints = output.graph.nodes
|
||||
.filter(({ kind }) => kind === "endpoint")
|
||||
|
|
@ -61,12 +66,16 @@ it("displays empty literals as nonempty text while retaining exact values", asyn
|
|||
labels: observations.map(({ label }) => label),
|
||||
values: observations.map(({ properties }) => properties.value),
|
||||
}));
|
||||
// The empty endpoint has no display label, so it cannot be confused with
|
||||
// a literal two-quote endpoint; both keep exact identities.
|
||||
expect(endpoints).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ key: '""', labels: ['""'], values: [""] },
|
||||
{ key: "", labels: [null], values: [""] },
|
||||
{ key: '""', labels: ['""'], values: ['""'] },
|
||||
{ key: "/api", labels: ["/api"], values: ["/api"] },
|
||||
]),
|
||||
);
|
||||
expect(endpoints).toHaveLength(3);
|
||||
expect(
|
||||
output.semantic_graph?.nodes
|
||||
.filter(({ kind }) => kind === "event")
|
||||
|
|
@ -101,5 +110,142 @@ it.each([
|
|||
: null,
|
||||
sources: observations.map(({ properties }) => properties.source),
|
||||
}));
|
||||
expect(originals).toEqual([{ originalSource: '""', sources: [""] }]);
|
||||
expect(originals).toEqual([{ originalSource: "", sources: [""] }]);
|
||||
});
|
||||
|
||||
it("keeps empty and two-quote source-map names distinct", async () => {
|
||||
const output = await analyzeSource("//# sourceMappingURL=app.js.map", {
|
||||
"app.js.map": JSON.stringify({
|
||||
version: 3,
|
||||
sources: ["", '""'],
|
||||
names: [],
|
||||
mappings: "AAAA,CCAA",
|
||||
}),
|
||||
});
|
||||
expect(
|
||||
output.graph.nodes
|
||||
.filter(({ kind }) => kind === "source-module")
|
||||
.map(({ identity }) =>
|
||||
identity.strategy === "source-map-original"
|
||||
? identity.original_source
|
||||
: null,
|
||||
)
|
||||
.sort(),
|
||||
).toEqual(["", '""']);
|
||||
});
|
||||
|
||||
it("does not match an empty endpoint with a literal two-quote seed", async () => {
|
||||
const output = await analyzeSource("fetch(''); fetch('\"\"');");
|
||||
const endpoints = new Map(
|
||||
output.graph.nodes
|
||||
.filter(({ kind }) => kind === "endpoint")
|
||||
.map((node) => [
|
||||
node.node_id,
|
||||
node.identity.strategy === "artifact-local-key"
|
||||
? node.identity.key
|
||||
: null,
|
||||
]),
|
||||
);
|
||||
const matches = findApplicationFeatureSeeds(output.graph.nodes, {
|
||||
kind: "string",
|
||||
value: '""',
|
||||
match: "exact",
|
||||
case_sensitive: true,
|
||||
}).filter(({ node_id: nodeId }) => endpoints.has(nodeId));
|
||||
expect(matches.map(({ node_id: nodeId }) => endpoints.get(nodeId))).toEqual([
|
||||
'""',
|
||||
]);
|
||||
});
|
||||
|
||||
const IPC_SOURCE =
|
||||
"const { ipcMain } = require('electron'); ipcMain.handle('', () => 1); ipcMain.handle('\"\"', () => 2);";
|
||||
|
||||
const ipcChannels = (
|
||||
nodes: Awaited<ReturnType<typeof analyzeSource>>["graph"]["nodes"],
|
||||
) =>
|
||||
new Map(
|
||||
nodes
|
||||
.filter(({ kind }) => kind === "ipc-channel")
|
||||
.map((node) => [node.node_id, node.observations[0]?.properties.channel]),
|
||||
);
|
||||
|
||||
it("selects an empty IPC channel with an exact empty seed", async () => {
|
||||
const output = await analyzeSource(IPC_SOURCE);
|
||||
const channels = ipcChannels(output.graph.nodes);
|
||||
const matches = findApplicationFeatureSeeds(output.graph.nodes, {
|
||||
kind: "channel",
|
||||
value: "",
|
||||
match: "exact",
|
||||
case_sensitive: true,
|
||||
});
|
||||
expect(matches.map(({ node_id: nodeId }) => channels.get(nodeId))).toEqual([
|
||||
"",
|
||||
]);
|
||||
});
|
||||
|
||||
const seedSchema = traceApplicationFeatureInputSchema.shape.seed;
|
||||
const validateAdvertisedSeed = new Ajv2020({
|
||||
strict: false,
|
||||
validateFormats: false,
|
||||
}).compile(z.toJSONSchema(seedSchema, { io: "input" }));
|
||||
|
||||
it.each([
|
||||
[{ kind: "channel", value: "", match: "exact" }, true],
|
||||
[{ kind: "string", value: "", match: "exact" }, true],
|
||||
[{ kind: "channel", value: "/x" }, true],
|
||||
[{ kind: "string", value: "" }, false],
|
||||
[{ kind: "channel", value: "" }, false],
|
||||
[{ kind: "channel", value: "", match: "contains" }, false],
|
||||
[{ kind: "node-id", value: "", match: "exact" }, false],
|
||||
] as const)(
|
||||
"validates empty seed %j at runtime and in the advertised schema",
|
||||
(seed, accepted) => {
|
||||
expect(seedSchema.safeParse(seed).success).toBe(accepted);
|
||||
expect(validateAdvertisedSeed(seed)).toBe(accepted);
|
||||
},
|
||||
);
|
||||
|
||||
it("pairs the same empty IPC channel across application versions", async () => {
|
||||
const left = await analyzeSource(IPC_SOURCE);
|
||||
const right = await analyzeSource(`${IPC_SOURCE} // v2`);
|
||||
const leftChannels = ipcChannels(left.graph.nodes);
|
||||
const rightChannels = ipcChannels(right.graph.nodes);
|
||||
const matching = matchJavaScriptApplicationVersions(
|
||||
left.graph.nodes,
|
||||
right.graph.nodes,
|
||||
);
|
||||
expect(
|
||||
matching.pairs
|
||||
.filter(({ left: node }) => node.kind === "ipc-channel")
|
||||
.map(({ left: leftNode, right: rightNode }) => [
|
||||
leftChannels.get(leftNode.node_id),
|
||||
rightChannels.get(rightNode.node_id),
|
||||
]),
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
["", ""],
|
||||
['""', '""'],
|
||||
]),
|
||||
);
|
||||
expect(matching.unmatchedLeft.map(({ kind }) => kind)).not.toContain(
|
||||
"ipc-channel",
|
||||
);
|
||||
});
|
||||
|
||||
it("selects an empty context-bridge API with an exact empty seed", async () => {
|
||||
const output = await analyzeSource(
|
||||
"const { contextBridge } = require('electron'); contextBridge.exposeInMainWorld('', {}); contextBridge.exposeInMainWorld('\"\"', {});",
|
||||
);
|
||||
const apis = new Map(
|
||||
output.graph.nodes
|
||||
.filter(({ kind }) => kind === "context-bridge-api")
|
||||
.map((node) => [node.node_id, node.observations[0]?.properties.api_key]),
|
||||
);
|
||||
const matches = findApplicationFeatureSeeds(output.graph.nodes, {
|
||||
kind: "api",
|
||||
value: "",
|
||||
match: "exact",
|
||||
case_sensitive: true,
|
||||
});
|
||||
expect(matches.map(({ node_id: nodeId }) => apis.get(nodeId))).toEqual([""]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import {
|
|||
type ApplicationEdge,
|
||||
type ApplicationNode,
|
||||
} from "../domain/javascriptApplicationGraph.js";
|
||||
import { javascriptDisplayText } from "../domain/javascriptAstValues.js";
|
||||
|
||||
/** Deduplicate graph entities while retaining distinct evidence observations. */
|
||||
export class JavaScriptArtifactGraphAccumulator {
|
||||
|
|
@ -68,46 +67,22 @@ const observationInput = (
|
|||
|
||||
/** Node fields whose display form is normalized before validation. */
|
||||
interface DisplayableNodeInput {
|
||||
readonly identity: {
|
||||
readonly strategy: string;
|
||||
readonly key?: string;
|
||||
readonly original_source?: string;
|
||||
};
|
||||
readonly observations: readonly { readonly label: string | null }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Static findings can carry legal empty strings, such as `fetch("")`,
|
||||
* `require("")`, or a source map's `sources: [""]`. Labels, artifact-local
|
||||
* keys, and original source names must be nonempty, so display the empty value
|
||||
* as `""`; each observation keeps the exact value in its properties.
|
||||
* `require("")`, or a source map's `sources: [""]`. Labels must be nonempty,
|
||||
* so an empty value has no label rather than synthetic text that a real value
|
||||
* could share; literal feature seeds match labels. Identities and observation
|
||||
* properties keep the exact value.
|
||||
*/
|
||||
const displayableNode = (
|
||||
input: DisplayableNodeInput,
|
||||
): DisplayableNodeInput => ({
|
||||
...input,
|
||||
identity: displayableIdentity(input.identity),
|
||||
observations: input.observations.map((observation) => ({
|
||||
...observation,
|
||||
label:
|
||||
observation.label === null
|
||||
? null
|
||||
: javascriptDisplayText(observation.label),
|
||||
label: observation.label === "" ? null : observation.label,
|
||||
})),
|
||||
});
|
||||
|
||||
const displayableIdentity = (
|
||||
identity: DisplayableNodeInput["identity"],
|
||||
): DisplayableNodeInput["identity"] => {
|
||||
if (identity.strategy === "artifact-local-key" && identity.key !== undefined)
|
||||
return { ...identity, key: javascriptDisplayText(identity.key) };
|
||||
if (
|
||||
identity.strategy === "source-map-original" &&
|
||||
identity.original_source !== undefined
|
||||
)
|
||||
return {
|
||||
...identity,
|
||||
original_source: javascriptDisplayText(identity.original_source),
|
||||
};
|
||||
return identity;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -370,14 +370,24 @@ it("keeps exact empty environment keys distinct from dynamic keys", () => {
|
|||
|
||||
it("labels exact empty method names without inventing computed names", () => {
|
||||
const graph = graphFor(`
|
||||
const target = { ['']() { return 42; }, [key]() { return 1; } };
|
||||
const target = {
|
||||
['']() { return 42; },
|
||||
['""']() { return 2; },
|
||||
[key]() { return 1; },
|
||||
};
|
||||
`);
|
||||
const labels = graph.nodes
|
||||
const functions = graph.nodes
|
||||
.filter(({ kind }) => kind === "function")
|
||||
.map(({ label }) => label);
|
||||
expect(labels).toContain('""');
|
||||
.map(({ label, properties }) => ({ label, name: properties.name }));
|
||||
// Both exact names display as `""`; only `properties.name` tells them apart.
|
||||
expect(functions).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ label: '""', name: "" },
|
||||
{ label: '""', name: '""' },
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
labels.filter((label) => label?.startsWith("[computed@")),
|
||||
functions.filter(({ label }) => label?.startsWith("[computed@")),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -167,6 +167,8 @@ const projectFile = (
|
|||
location: callable.location,
|
||||
label: callable.name,
|
||||
functionNodeId: null,
|
||||
// The label is display text; keep the exact name, which may be "".
|
||||
properties: { name: callable.name },
|
||||
},
|
||||
state,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -26,4 +26,27 @@ describe("input issue projection", () => {
|
|||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports union branch issues at the union's path once", () => {
|
||||
const schema = z.object({
|
||||
seed: z.union([
|
||||
z.object({ value: z.string().min(1) }),
|
||||
z.object({ value: z.literal(""), match: z.literal("exact") }),
|
||||
]),
|
||||
});
|
||||
const missing = schema.safeParse({});
|
||||
if (missing.success) throw new Error("expected invalid input");
|
||||
expect(projectInputIssues(missing.error.issues, {})).toEqual([
|
||||
{ path: ["seed"], reason: "missing_argument", expected: "object" },
|
||||
]);
|
||||
|
||||
const input = { seed: { value: 1 } };
|
||||
const nested = schema.safeParse(input);
|
||||
if (nested.success) throw new Error("expected invalid input");
|
||||
expect(projectInputIssues(nested.error.issues, input)).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ path: ["seed", "value"], reason: "invalid_type", expected: "string" },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,9 +15,17 @@ const projectIssue = (
|
|||
const path = issue.path.flatMap((part) =>
|
||||
typeof part === "string" || typeof part === "number" ? [part] : [],
|
||||
);
|
||||
// Branch issue paths are relative to the union; branches often agree.
|
||||
if (issue.code === "invalid_union")
|
||||
return issue.errors.flatMap((branch) =>
|
||||
branch.flatMap((branchIssue) => projectIssue(branchIssue, input)),
|
||||
return uniqueIssues(
|
||||
issue.errors.flatMap((branch) =>
|
||||
branch.flatMap((branchIssue) =>
|
||||
projectIssue(
|
||||
{ ...branchIssue, path: [...issue.path, ...branchIssue.path] },
|
||||
input,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (issue.code === "unrecognized_keys")
|
||||
return issue.keys.map((key) => ({
|
||||
|
|
@ -58,6 +66,12 @@ const projectIssue = (
|
|||
];
|
||||
};
|
||||
|
||||
const uniqueIssues = (
|
||||
issues: readonly AnalysisInputIssue[],
|
||||
): AnalysisInputIssue[] => [
|
||||
...new Map(issues.map((issue) => [JSON.stringify(issue), issue])).values(),
|
||||
];
|
||||
|
||||
const valueAtPath = (
|
||||
input: unknown,
|
||||
path: readonly (string | number)[],
|
||||
|
|
|
|||
|
|
@ -419,7 +419,8 @@ export const applicationNodeIdentitySchema = z.discriminatedUnion("strategy", [
|
|||
strategy: z.literal("source-map-original"),
|
||||
stability: z.literal("source-map-exact"),
|
||||
source_map_sha256: digestSchema,
|
||||
original_source: boundedTextSchema,
|
||||
// The exact `sources` entry; `""` is a legal relative reference.
|
||||
original_source: z.string(),
|
||||
source_sha256: digestSchema.nullable(),
|
||||
}),
|
||||
z.strictObject({
|
||||
|
|
@ -451,7 +452,8 @@ export const applicationNodeIdentitySchema = z.discriminatedUnion("strategy", [
|
|||
stability: z.literal("artifact-version"),
|
||||
artifact_sha256: digestSchema,
|
||||
namespace: boundedKeySchema,
|
||||
key: boundedTextSchema,
|
||||
// The exact artifact-local value, such as `""` from `fetch("")`.
|
||||
key: z.string(),
|
||||
}),
|
||||
z.strictObject({
|
||||
strategy: z.literal("runtime-instance"),
|
||||
|
|
|
|||
|
|
@ -229,8 +229,11 @@ const semanticValue = (
|
|||
node: ApplicationNode,
|
||||
properties: Readonly<Record<string, JsonValue>>,
|
||||
): string | null => {
|
||||
if (node.kind === "endpoint")
|
||||
return joined(properties.endpoint_kind, properties.value);
|
||||
if (node.kind === "endpoint") {
|
||||
const kind = nonEmptyString(properties.endpoint_kind);
|
||||
const value = exactString(properties.value);
|
||||
return kind === null || value === null ? null : `${kind}\0${value}`;
|
||||
}
|
||||
if (node.kind === "storage")
|
||||
return joined(
|
||||
properties.storage_kind,
|
||||
|
|
@ -239,9 +242,13 @@ const semanticValue = (
|
|||
if (node.kind === "ipc-channel")
|
||||
return properties.resolution === "dynamic"
|
||||
? null
|
||||
: (nonEmptyString(properties.channel) ?? firstLabel(node));
|
||||
: (exactString(properties.channel) ?? firstLabel(node));
|
||||
if (node.kind === "context-bridge-api")
|
||||
return firstLabel(node) ?? nonEmptyString(properties.api_name);
|
||||
return (
|
||||
firstLabel(node) ??
|
||||
exactString(properties.api_key) ??
|
||||
nonEmptyString(properties.api_name)
|
||||
);
|
||||
if (node.kind === "native-export")
|
||||
return (
|
||||
joined(
|
||||
|
|
@ -293,6 +300,10 @@ const joined = (...values: readonly unknown[]): string | null => {
|
|||
const nonEmptyString = (value: unknown): string | null =>
|
||||
typeof value === "string" && value !== "" ? value : null;
|
||||
|
||||
/** A literal value such as `ipcMain.handle("")` keeps its exact empty text. */
|
||||
const exactString = (value: unknown): string | null =>
|
||||
typeof value === "string" ? value : null;
|
||||
|
||||
const stringList = (value: unknown): string[] =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { compareCodePoints } from "./canonicalOrdering.js";
|
||||
import type { ApplicationNode } from "./javascriptApplicationGraph.js";
|
||||
import type { ApplicationFeatureSeed } from "./javascriptFeatureTraceSchemas.js";
|
||||
import {
|
||||
featureSeedMatchMode,
|
||||
type ApplicationFeatureSeed,
|
||||
} from "./javascriptFeatureTraceSchemas.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
|
||||
/** One explicit field that matched a feature seed. */
|
||||
|
|
@ -49,7 +52,7 @@ const seedMatch = (
|
|||
]
|
||||
: [];
|
||||
if (!kindMatches(node, seed.kind)) return [];
|
||||
const mode = seed.match ?? (seed.kind === "string" ? "contains" : "exact");
|
||||
const mode = featureSeedMatchMode(seed);
|
||||
const query = normalized(seed.value, seed.case_sensitive);
|
||||
const match = candidateFields(node, seed.kind).find((candidate) => {
|
||||
const value = normalized(candidate.value, seed.case_sensitive);
|
||||
|
|
@ -129,7 +132,7 @@ const memberFieldSuffixes = new Set([
|
|||
|
||||
const relevantFields = {
|
||||
route: [".label", ".value", ".path"],
|
||||
api: [".label", ".api_name", ".key", ".methods", ".members"],
|
||||
api: [".label", ".api_name", ".api_key", ".key", ".methods", ".members"],
|
||||
channel: [".label", ".channel", ".key"],
|
||||
module: [
|
||||
".label",
|
||||
|
|
|
|||
|
|
@ -10,21 +10,41 @@ const nodeIdSchema = prefixedDigestSchema("jag_node");
|
|||
const edgeIdSchema = prefixedDigestSchema("jag_edge");
|
||||
const boundedTextSchema = z.string().min(1);
|
||||
|
||||
/** Literal starting point for one application feature trace. */
|
||||
const applicationFeatureSeedSchema = z.strictObject({
|
||||
kind: z.enum([
|
||||
"node-id",
|
||||
"route",
|
||||
"string",
|
||||
"api",
|
||||
"channel",
|
||||
"module",
|
||||
"native-export",
|
||||
]),
|
||||
value: boundedTextSchema,
|
||||
match: z.enum(["exact", "contains"]).nullable().default(null),
|
||||
case_sensitive: z.boolean().default(false),
|
||||
});
|
||||
/** Matching mode for a literal seed: string seeds default to containment. */
|
||||
export const featureSeedMatchMode = (seed: {
|
||||
readonly kind: string;
|
||||
readonly match: "exact" | "contains" | null;
|
||||
}): "exact" | "contains" =>
|
||||
seed.match ?? (seed.kind === "string" ? "contains" : "exact");
|
||||
|
||||
const literalSeedKinds = [
|
||||
"route",
|
||||
"string",
|
||||
"api",
|
||||
"channel",
|
||||
"module",
|
||||
"native-export",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Literal starting point for one application feature trace. An empty value,
|
||||
* such as the channel in `ipcMain.handle("")`, must be an explicit exact
|
||||
* literal seed: empty containment would select every node.
|
||||
*/
|
||||
const applicationFeatureSeedSchema = z.union([
|
||||
z.strictObject({
|
||||
kind: z.enum(["node-id", ...literalSeedKinds]),
|
||||
value: boundedTextSchema,
|
||||
match: z.enum(["exact", "contains"]).nullable().default(null),
|
||||
case_sensitive: z.boolean().default(false),
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.enum(literalSeedKinds),
|
||||
value: z.literal(""),
|
||||
match: z.literal("exact"),
|
||||
case_sensitive: z.boolean().default(false),
|
||||
}),
|
||||
]);
|
||||
|
||||
/** Evidence-backed application graph and feature seed. */
|
||||
export const traceApplicationFeatureInputSchema = z
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue