@clue-ai/cli 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -2
- package/bin/clue-cli.mjs +836 -17
- package/commands/claude-code/clue-init.md +7 -1
- package/commands/codex/clue-init.md +7 -1
- package/package.json +1 -1
- package/src/ai-provider.mjs +146 -0
- package/src/command-spec.mjs +7 -7
- package/src/contracts.mjs +53 -14
- package/src/init-tool.mjs +153 -20
- package/src/lifecycle-guard.mjs +141 -0
- package/src/lifecycle-init.mjs +91 -73
- package/src/path-policy.mjs +2 -0
- package/src/public-schema.cjs +27 -1
- package/src/semantic-ci.mjs +771 -122
- package/src/setup-check.mjs +436 -0
- package/src/setup-detect.mjs +198 -0
- package/src/setup-prepare.mjs +289 -0
- package/src/setup-tool.mjs +94 -27
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const LIFECYCLE_CALL_PATTERN =
|
|
2
|
+
/\b(ClueInit|ClueIdentify|ClueSetAccount|ClueLogout)\s*\(/g;
|
|
3
|
+
const SAFE_HELPER_PATTERN =
|
|
4
|
+
/\b(?:safeClue|safe_clue|safe_clue_call|safeClueCall|withClueGuard|with_clue_guard)\s*\(/g;
|
|
5
|
+
|
|
6
|
+
const findMatchingDelimiter = (text, openIndex, open, close) => {
|
|
7
|
+
let depth = 0;
|
|
8
|
+
for (let index = openIndex; index < text.length; index += 1) {
|
|
9
|
+
const character = text[index];
|
|
10
|
+
if (character === open) depth += 1;
|
|
11
|
+
if (character === close) {
|
|
12
|
+
depth -= 1;
|
|
13
|
+
if (depth === 0) return index;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return -1;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const lineNumberForIndex = (text, index) =>
|
|
20
|
+
text.slice(0, index).split("\n").length;
|
|
21
|
+
|
|
22
|
+
const isAwaitedOnCallLine = (text, callIndex) => {
|
|
23
|
+
const lineStart = text.lastIndexOf("\n", callIndex - 1) + 1;
|
|
24
|
+
return /\bawait\b/.test(text.slice(lineStart, callIndex));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const isInsideSafeHelperCall = (text, callIndex) => {
|
|
28
|
+
for (const match of text.matchAll(SAFE_HELPER_PATTERN)) {
|
|
29
|
+
const helperIndex = match.index ?? 0;
|
|
30
|
+
if (helperIndex > callIndex) return false;
|
|
31
|
+
const openParenIndex = text.indexOf("(", helperIndex);
|
|
32
|
+
const closeParenIndex = findMatchingDelimiter(
|
|
33
|
+
text,
|
|
34
|
+
openParenIndex,
|
|
35
|
+
"(",
|
|
36
|
+
")",
|
|
37
|
+
);
|
|
38
|
+
if (openParenIndex < callIndex && callIndex < closeParenIndex) return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const isInsideJsTryBlock = (text, callIndex) => {
|
|
44
|
+
for (const match of text.matchAll(/\btry\s*{/g)) {
|
|
45
|
+
const tryIndex = match.index ?? 0;
|
|
46
|
+
if (tryIndex > callIndex) return false;
|
|
47
|
+
const openBraceIndex = text.indexOf("{", tryIndex);
|
|
48
|
+
const closeBraceIndex = findMatchingDelimiter(
|
|
49
|
+
text,
|
|
50
|
+
openBraceIndex,
|
|
51
|
+
"{",
|
|
52
|
+
"}",
|
|
53
|
+
);
|
|
54
|
+
if (openBraceIndex < callIndex && callIndex < closeBraceIndex) return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const buildLines = (text) => {
|
|
60
|
+
const lines = [];
|
|
61
|
+
let start = 0;
|
|
62
|
+
for (const line of text.split("\n")) {
|
|
63
|
+
const indent = line.match(/^\s*/)?.[0].length ?? 0;
|
|
64
|
+
lines.push({
|
|
65
|
+
start,
|
|
66
|
+
end: start + line.length,
|
|
67
|
+
indent,
|
|
68
|
+
text: line,
|
|
69
|
+
});
|
|
70
|
+
start += line.length + 1;
|
|
71
|
+
}
|
|
72
|
+
return lines;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const isInsidePythonTryBlock = (text, callIndex) => {
|
|
76
|
+
const lines = buildLines(text);
|
|
77
|
+
const callLineIndex = lines.findIndex(
|
|
78
|
+
(line) => line.start <= callIndex && callIndex <= line.end,
|
|
79
|
+
);
|
|
80
|
+
if (callLineIndex < 0) return false;
|
|
81
|
+
const callLine = lines[callLineIndex];
|
|
82
|
+
for (let index = callLineIndex - 1; index >= 0; index -= 1) {
|
|
83
|
+
const candidate = lines[index];
|
|
84
|
+
if (!candidate.text.trim()) continue;
|
|
85
|
+
if (!/^\s*try\s*:\s*(?:#.*)?$/.test(candidate.text)) continue;
|
|
86
|
+
if (candidate.indent >= callLine.indent) continue;
|
|
87
|
+
const escaped = lines
|
|
88
|
+
.slice(index + 1, callLineIndex)
|
|
89
|
+
.some(
|
|
90
|
+
(line) =>
|
|
91
|
+
line.text.trim() &&
|
|
92
|
+
line.indent <= candidate.indent &&
|
|
93
|
+
!/^\s*(?:except|finally|else)\b/.test(line.text),
|
|
94
|
+
);
|
|
95
|
+
if (!escaped) return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const hasCatchHandlerOnCall = (text, callIndex) => {
|
|
101
|
+
const openParenIndex = text.indexOf("(", callIndex);
|
|
102
|
+
const closeParenIndex = findMatchingDelimiter(text, openParenIndex, "(", ")");
|
|
103
|
+
if (closeParenIndex < 0) return false;
|
|
104
|
+
return /^\s*\.catch\s*\(/.test(text.slice(closeParenIndex + 1));
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const isGuardedLifecycleCall = (text, callIndex) =>
|
|
108
|
+
hasCatchHandlerOnCall(text, callIndex) ||
|
|
109
|
+
isInsideSafeHelperCall(text, callIndex) ||
|
|
110
|
+
isInsideJsTryBlock(text, callIndex) ||
|
|
111
|
+
isInsidePythonTryBlock(text, callIndex);
|
|
112
|
+
|
|
113
|
+
export const findLifecycleGuardViolations = (text) => {
|
|
114
|
+
const violations = [];
|
|
115
|
+
for (const match of text.matchAll(LIFECYCLE_CALL_PATTERN)) {
|
|
116
|
+
const callIndex = match.index ?? 0;
|
|
117
|
+
const apiName = match[1];
|
|
118
|
+
if (isAwaitedOnCallLine(text, callIndex)) {
|
|
119
|
+
violations.push({
|
|
120
|
+
api_name: apiName,
|
|
121
|
+
line: lineNumberForIndex(text, callIndex),
|
|
122
|
+
reason: "awaited_lifecycle_call",
|
|
123
|
+
});
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (!isGuardedLifecycleCall(text, callIndex)) {
|
|
127
|
+
violations.push({
|
|
128
|
+
api_name: apiName,
|
|
129
|
+
line: lineNumberForIndex(text, callIndex),
|
|
130
|
+
reason: "unguarded_lifecycle_call",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return violations;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const findLifecycleCallApiNames = (text) => [
|
|
138
|
+
...new Set(
|
|
139
|
+
[...text.matchAll(LIFECYCLE_CALL_PATTERN)].map((match) => match[1]),
|
|
140
|
+
),
|
|
141
|
+
];
|
package/src/lifecycle-init.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import { callJsonAiProvider, resolveAiProviderConfig } from "./ai-provider.mjs";
|
|
4
|
+
import { findLifecycleGuardViolations } from "./lifecycle-guard.mjs";
|
|
3
5
|
import { listAllowedSourceFiles } from "./path-policy.mjs";
|
|
4
6
|
|
|
5
7
|
const API_NAMES = new Set([
|
|
@@ -25,10 +27,7 @@ const safeRelativePath = (repoRoot, filePath) => {
|
|
|
25
27
|
const root = resolve(repoRoot);
|
|
26
28
|
const absolutePath = resolve(root, filePath);
|
|
27
29
|
const relativePath = relative(root, absolutePath);
|
|
28
|
-
if (
|
|
29
|
-
relativePath.startsWith("..") ||
|
|
30
|
-
isAbsolute(relativePath)
|
|
31
|
-
) {
|
|
30
|
+
if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
32
31
|
throw new Error(`edit path escapes repo root: ${filePath}`);
|
|
33
32
|
}
|
|
34
33
|
return { absolutePath, relativePath };
|
|
@@ -50,6 +49,15 @@ const assertNoForbiddenInstrumentation = (replacement) => {
|
|
|
50
49
|
}
|
|
51
50
|
};
|
|
52
51
|
|
|
52
|
+
const assertLifecycleCallsAreGuarded = (replacement) => {
|
|
53
|
+
const violations = findLifecycleGuardViolations(replacement);
|
|
54
|
+
if (violations.length > 0) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Clue lifecycle calls must be failure-isolated with try/catch, try/except, or an explicit safe Clue helper: ${JSON.stringify(violations)}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
53
61
|
const normalizeLifecycleInsertion = (input) => ({
|
|
54
62
|
api_name: assertApiName(nonEmpty(input.api_name, "api_name")),
|
|
55
63
|
file_path: nonEmpty(input.file_path, "file_path"),
|
|
@@ -95,7 +103,12 @@ export const applyLifecyclePlan = async ({ repoRoot, plan: rawPlan }) => {
|
|
|
95
103
|
`edit.find must match exactly once in ${edit.file_path}; matched ${occurrences}`,
|
|
96
104
|
);
|
|
97
105
|
}
|
|
98
|
-
|
|
106
|
+
assertLifecycleCallsAreGuarded(edit.replace);
|
|
107
|
+
await writeFile(
|
|
108
|
+
absolutePath,
|
|
109
|
+
current.replace(edit.find, edit.replace),
|
|
110
|
+
"utf8",
|
|
111
|
+
);
|
|
99
112
|
}
|
|
100
113
|
return {
|
|
101
114
|
lifecycleInsertions: plan.lifecycleInsertions,
|
|
@@ -128,79 +141,84 @@ const readContextFiles = async ({ repoRoot, request }) => {
|
|
|
128
141
|
return context;
|
|
129
142
|
};
|
|
130
143
|
|
|
131
|
-
const buildLifecyclePrompt = ({ request, files }) =>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
144
|
+
const buildLifecyclePrompt = ({ request, files }) =>
|
|
145
|
+
JSON.stringify({
|
|
146
|
+
task: "Add Clue SDK lifecycle API calls to this repository using exact text replacements.",
|
|
147
|
+
rules: [
|
|
148
|
+
"Return JSON only.",
|
|
149
|
+
"Use only exact replacements. Each find string must be copied exactly from source.",
|
|
150
|
+
"Add ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout where repository code has clear lifecycle points.",
|
|
151
|
+
"Every Clue lifecycle call must be failure-isolated. If Clue fails, the host service must continue without throwing, rejecting, or blocking login/API behavior.",
|
|
152
|
+
"Use a small safe Clue helper, local try/catch/try/except wrapper, or direct .catch handler around Clue lifecycle calls.",
|
|
153
|
+
"Never await a Clue lifecycle call in a way that can block login, logout, account selection, request handling, page rendering, or API responses.",
|
|
154
|
+
"Find all clear login success paths and add ClueIdentify to every one of them. Do not stop after the first login flow.",
|
|
155
|
+
"Find all clear account, workspace, organization, or tenant resolution paths and add ClueSetAccount to every one of them.",
|
|
156
|
+
"Find all clear logout or session reset paths and add ClueLogout to every one of them.",
|
|
157
|
+
"Inspect backend lifecycle points as carefully as frontend lifecycle points. Backend login/session/account code is especially important.",
|
|
158
|
+
"For FastAPI backends, add the clue-fastapi-sdk dependency when missing, import clue_init_fastapi plus ClueIdentify/ClueSetAccount/ClueLogout where needed, and initialize the SDK at FastAPI app creation.",
|
|
159
|
+
"For Django backends, add the clue-django-sdk dependency when missing, import the Django SDK lifecycle helpers where needed, and initialize the SDK in the Django integration point.",
|
|
160
|
+
"For other backend frameworks, use the matching Clue backend SDK if one exists; if no backend SDK exists, report a blocker instead of silently frontend-only setup.",
|
|
161
|
+
"Do not add broad ClueTrack instrumentation.",
|
|
162
|
+
"Do not add data-clue-id, data-clue-key, or similar DOM tags.",
|
|
163
|
+
"Do not create route semantics files or layer files.",
|
|
164
|
+
"Do not copy project keys, API keys, service secrets, or environment-specific values into code.",
|
|
165
|
+
"Do not send raw email, raw person names, tokens, workspace names, organization names, or tenant names as lifecycle traits unless the repository already has an explicit Clue privacy policy allowing them.",
|
|
166
|
+
"Prefer stable ids and non-PII booleans/counts for ClueIdentify and ClueSetAccount traits.",
|
|
167
|
+
"Use environment variable names for Clue configuration values.",
|
|
168
|
+
"For Python/FastAPI code, read CLUE_PROJECT_KEY, CLUE_ENVIRONMENT, CLUE_API_KEY, and CLUE_INGEST_ENDPOINT from environment variables.",
|
|
169
|
+
"For browser code, read CLUE_PROJECT_KEY, CLUE_ENVIRONMENT, and CLUE_INGEST_ENDPOINT from environment variables through the target framework's safe client config mechanism. Do not hard-code a Next.js-only prefix.",
|
|
170
|
+
"Prefer minimal edits that engineers can review in one PR.",
|
|
171
|
+
"If a lifecycle point is unclear, skip that edit and include a warning.",
|
|
157
172
|
],
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
export const planLifecycleInsertions = async ({ repoRoot, request, env }) => {
|
|
172
|
-
const apiKey = env.AI_PROVIDER_API_KEY;
|
|
173
|
-
if (!apiKey) {
|
|
174
|
-
throw new Error("AI_PROVIDER_API_KEY is required for lifecycle API insertion");
|
|
175
|
-
}
|
|
176
|
-
const files = await readContextFiles({ repoRoot, request });
|
|
177
|
-
const baseUrl = String(env.AI_PROVIDER_BASE_URL || "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
178
|
-
const model = String(env.CLUE_INIT_AI_MODEL || env.CLUE_AI_MODEL || "gpt-5.4-mini");
|
|
179
|
-
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
180
|
-
method: "POST",
|
|
181
|
-
headers: {
|
|
182
|
-
"content-type": "application/json",
|
|
183
|
-
authorization: `Bearer ${apiKey}`,
|
|
173
|
+
repository_context: {
|
|
174
|
+
target_tool: request.target_tool,
|
|
175
|
+
framework: request.framework,
|
|
176
|
+
project_key_env: "CLUE_PROJECT_KEY",
|
|
177
|
+
browser_project_key_env: "CLUE_PROJECT_KEY",
|
|
178
|
+
environment_env: "CLUE_ENVIRONMENT",
|
|
179
|
+
browser_environment_env: "CLUE_ENVIRONMENT",
|
|
180
|
+
clue_api_base_url_env: "CLUE_API_BASE_URL",
|
|
181
|
+
clue_ingest_endpoint_env: "CLUE_INGEST_ENDPOINT",
|
|
182
|
+
browser_ingest_endpoint_env: "CLUE_INGEST_ENDPOINT",
|
|
183
|
+
service_key: request.service_key,
|
|
184
184
|
},
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
messages: [
|
|
185
|
+
output_shape: {
|
|
186
|
+
edits: [
|
|
188
187
|
{
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
file_path: "app/main.py",
|
|
189
|
+
find: "exact original text",
|
|
190
|
+
replace: "exact replacement text",
|
|
191
191
|
},
|
|
192
|
-
{ role: "user", content: buildLifecyclePrompt({ request, files }) },
|
|
193
192
|
],
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
lifecycle_insertions: [
|
|
194
|
+
{
|
|
195
|
+
api_name: "ClueInit",
|
|
196
|
+
file_path: "app/main.py",
|
|
197
|
+
confidence: 0.8,
|
|
198
|
+
reason: "SDK initialized where FastAPI app is created.",
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
warnings: ["short engineer review note"],
|
|
202
|
+
},
|
|
203
|
+
files,
|
|
196
204
|
});
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
205
|
+
|
|
206
|
+
export const planLifecycleInsertions = async ({ repoRoot, request, env }) => {
|
|
207
|
+
const apiKey = env.CLUE_AI_PROVIDER_API_KEY;
|
|
208
|
+
if (!apiKey) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
"CLUE_AI_PROVIDER_API_KEY is required for lifecycle API insertion",
|
|
211
|
+
);
|
|
204
212
|
}
|
|
205
|
-
|
|
213
|
+
const files = await readContextFiles({ repoRoot, request });
|
|
214
|
+
return callJsonAiProvider({
|
|
215
|
+
config: resolveAiProviderConfig({ env, apiKey }),
|
|
216
|
+
system:
|
|
217
|
+
"You are a safe code-edit planner for Clue SDK initialization. Return schema-valid JSON only.",
|
|
218
|
+
user: buildLifecyclePrompt({ request, files }),
|
|
219
|
+
toolName: "return_lifecycle_plan",
|
|
220
|
+
toolDescription: "Return the Clue SDK lifecycle insertion plan.",
|
|
221
|
+
failureMessage: "AI provider failed during lifecycle planning",
|
|
222
|
+
emptyMessage: "AI provider returned empty lifecycle plan",
|
|
223
|
+
});
|
|
206
224
|
};
|
package/src/path-policy.mjs
CHANGED
package/src/public-schema.cjs
CHANGED
|
@@ -7,7 +7,7 @@ const clueInitToolTargetSchema = zod_1.z.enum(["codex", "claude_code"]);
|
|
|
7
7
|
const clueInitToolFrameworkSchema = nonEmptyStringSchema;
|
|
8
8
|
const clueInitToolRequiredSecretSchema = zod_1.z.enum([
|
|
9
9
|
"CLUE_API_KEY",
|
|
10
|
-
"
|
|
10
|
+
"CLUE_AI_PROVIDER_API_KEY",
|
|
11
11
|
]);
|
|
12
12
|
const clueInitToolRequiredSecretsSchema = zod_1.z
|
|
13
13
|
.array(clueInitToolRequiredSecretSchema)
|
|
@@ -137,6 +137,15 @@ const semanticSnapshotSelectedSourceValues = [
|
|
|
137
137
|
"deterministic",
|
|
138
138
|
"ai",
|
|
139
139
|
];
|
|
140
|
+
const semanticSnapshotRouteOriginValues = [
|
|
141
|
+
"unchanged_route_reused",
|
|
142
|
+
"changed_route_semantic_reused",
|
|
143
|
+
"changed_route_semantic_regenerated",
|
|
144
|
+
"new_route_ai_generated",
|
|
145
|
+
"deleted_route_removed",
|
|
146
|
+
"changed_route_needs_review",
|
|
147
|
+
"fallback",
|
|
148
|
+
];
|
|
140
149
|
const targetObjectKeySchema = nonEmptyStringSchema.regex(snakeSegmentPattern, {
|
|
141
150
|
message: "must be a lowercase snake_case key",
|
|
142
151
|
});
|
|
@@ -190,6 +199,16 @@ const semanticSnapshotAnalysisSummarySchema = zod_1.z
|
|
|
190
199
|
routes_generated: zod_1.z.number().int().nonnegative(),
|
|
191
200
|
uncertain_routes: zod_1.z.number().int().nonnegative(),
|
|
192
201
|
failed_routes: zod_1.z.number().int().nonnegative(),
|
|
202
|
+
routes_reused: zod_1.z.number().int().nonnegative().default(0),
|
|
203
|
+
routes_ai_generated: zod_1.z.number().int().nonnegative().default(0),
|
|
204
|
+
routes_deleted: zod_1.z.number().int().nonnegative().default(0),
|
|
205
|
+
routes_needs_review: zod_1.z.number().int().nonnegative().default(0),
|
|
206
|
+
changed_routes_semantic_reused: zod_1.z.number().int().nonnegative().default(0),
|
|
207
|
+
changed_routes_semantic_regenerated: zod_1.z
|
|
208
|
+
.number()
|
|
209
|
+
.int()
|
|
210
|
+
.nonnegative()
|
|
211
|
+
.default(0),
|
|
193
212
|
})
|
|
194
213
|
.strict();
|
|
195
214
|
const semanticCandidateSchema = zod_1.z
|
|
@@ -375,8 +394,15 @@ const routeSemanticSnapshotEntrySchema = zod_1.z
|
|
|
375
394
|
.object({
|
|
376
395
|
operation_source_key: nonEmptyStringSchema,
|
|
377
396
|
semantic_snapshot_version: nonEmptyStringSchema.optional(),
|
|
397
|
+
previous_semantic_snapshot_version: nonEmptyStringSchema.optional(),
|
|
378
398
|
method: nonEmptyStringSchema.optional(),
|
|
379
399
|
path_template: nonEmptyStringSchema.optional(),
|
|
400
|
+
route_input_hash: semanticSnapshotHashSchema.optional(),
|
|
401
|
+
previous_route_input_hash: semanticSnapshotHashSchema.optional(),
|
|
402
|
+
route_semantic_hash: semanticSnapshotHashSchema.optional(),
|
|
403
|
+
previous_route_semantic_hash: semanticSnapshotHashSchema.optional(),
|
|
404
|
+
semantic_origin: zod_1.z.enum(semanticSnapshotRouteOriginValues).optional(),
|
|
405
|
+
semantic_change_reason: nonEmptyStringSchema.optional(),
|
|
380
406
|
semantics: zod_1.z
|
|
381
407
|
.object({
|
|
382
408
|
route_summary: nonEmptyStringSchema,
|