1import * as Effect from "effect/Effect";
2import { createPhysicalName } from "../../PhysicalName.ts";
3import { tagRecord } from "../../Tags.ts";
4import {
5 alchemyLabelKeys,
6 createInternalLabels as createInternalLabelsImpl,
7 hasAlchemyLabels,
8 stripInternalLabels,
9 toLabels as toLabelsImpl,
10} from "../Labels.ts";
11import { resourceNameFromOperation as resourceNameFromOperationImpl } from "./operations.ts";
12
13export const MAX_ID_LENGTH = 63;
14export const MAX_DISPLAY_NAME_LENGTH = 128;
15/** Locations `list` scans: the pre-`GCP.Region` default plus the stack region. */
16export const listLocations = (region: string): string[] => [
17 ...new Set(["us-central1", region]),
18];
19
20export const createInternalLabels = createInternalLabelsImpl;
21export const toLabels = toLabelsImpl;
22export const resourceNameFromOperation = resourceNameFromOperationImpl;
23
24export const lastSegment = (value: string) => {
25 const trimmed = value.replace(/\/+$/, "");
26 const parts = trimmed.split("/");
27 return parts[parts.length - 1] || trimmed;
28};
29
30export const normalizeLocation = (
31 location: string | undefined,
32 fallback: string,
33) => lastSegment(location ?? fallback).toLowerCase();
34
35export const parentOf = (project: string, location: string) =>
36 `projects/${project}/locations/${location}`;
37
38export const locationOf = (name: string, fallback = "") => {
39 const parts = name.split("/");
40 const index = parts.indexOf("locations");
41 return index >= 0 ? (parts[index + 1] ?? fallback) : fallback;
42};
43
44export const projectOf = (name: string, fallback = "") => {
45 const parts = name.split("/");
46 const index = parts.indexOf("projects");
47 return index >= 0 ? (parts[index + 1] ?? fallback) : fallback;
48};
49
50export const parseName = (name: string, collection: string) => {
51 const parts = name.split("/").filter((part) => part.length > 0);
52 const collectionAt = parts.lastIndexOf(collection);
53 const locationsAt = parts.lastIndexOf("locations");
54 const projectsAt = parts.lastIndexOf("projects");
55 const enginesAt = parts.lastIndexOf("reasoningEngines");
56 return {
57 project:
58 projectsAt >= 0 && parts[projectsAt + 1] ? parts[projectsAt + 1]! : "",
59 location:
60 locationsAt >= 0 && parts[locationsAt + 1] ? parts[locationsAt + 1]! : "",
61 reasoningEngineId:
62 enginesAt >= 0 && parts[enginesAt + 1] ? parts[enginesAt + 1]! : "",
63 resourceId:
64 collectionAt >= 0 && parts[collectionAt + 1]
65 ? parts[collectionAt + 1]!
66 : lastSegment(name),
67 };
68};
69
70export const toPhysicalId = (
71 id: string,
72 explicit: string | undefined,
73 existing?: string,
74 maxLength = MAX_ID_LENGTH,
75) =>
76 Effect.gen(function* () {
77 if (explicit !== undefined) return explicit;
78 if (existing !== undefined) return existing;
79 const generated = yield* createPhysicalName({
80 id,
81 maxLength,
82 lowercase: true,
83 });
84 const starts = /^[a-z]/.test(generated) ? generated : `a${generated}`;
85 return starts.replace(/-+$/g, "").slice(0, maxLength);
86 });
87
88export const toResourceId = (
89 id: string,
90 requested: string | undefined,
91 existing: string | undefined,
92 maxLength = MAX_ID_LENGTH,
93) => toPhysicalId(id, requested, existing, maxLength);
94
95export const toDisplayName = (
96 id: string,
97 explicit: string | undefined,
98 existing: string | undefined,
99) =>
100 Effect.gen(function* () {
101 if (explicit !== undefined) return explicit;
102 if (existing !== undefined) return existing;
103 return yield* toPhysicalId(id, undefined, undefined);
104 });
105
106export const compact = <T extends Record<string, unknown>>(value: T): T =>
107 Object.fromEntries(
108 Object.entries(value).filter(([, item]) => item !== undefined),
109 ) as T;
110
111export const stableJson = (value: unknown): string =>
112 JSON.stringify(value ?? null, (_key, item) => {
113 if (item && typeof item === "object" && !Array.isArray(item)) {
114 return Object.fromEntries(
115 Object.entries(item as Record<string, unknown>).sort(
116 ([left], [right]) => left.localeCompare(right),
117 ),
118 );
119 }
120 return item;
121 });
122
123export const userLabels = (
124 labels: Record<string, string | undefined> | null | undefined,
125): Record<string, string> => stripInternalLabels(tagRecord(labels));
126
127export const hasAlchemyPrefix = (
128 labels: Record<string, string | undefined> | null | undefined,
129) => Object.keys(labels ?? {}).some((key) => key.startsWith("alchemy-"));
130
131export const alchemyIdFilter = (labels: Record<string, string>) => {
132 const id = labels[alchemyLabelKeys.id];
133 return id !== undefined && id.length > 0
134 ? `labels.${alchemyLabelKeys.id}="${id}"`
135 : undefined;
136};
137
138export const labelsDiffer = (
139 observed: Record<string, string | undefined> | null | undefined,
140 desired: Record<string, string>,
141) => {
142 const left = tagRecord(observed);
143 const keys = new Set([...Object.keys(left), ...Object.keys(desired)]);
144 for (const key of keys) {
145 if ((left[key] ?? "") !== (desired[key] ?? "")) return true;
146 }
147 return false;
148};
149
150const markerOf = (labels: Record<string, string>) =>
151 `[alchemy ${alchemyLabelKeys.stack}=${labels[alchemyLabelKeys.stack]} ${alchemyLabelKeys.stage}=${labels[alchemyLabelKeys.stage]} ${alchemyLabelKeys.id}=${labels[alchemyLabelKeys.id]}]`;
152
153/** 64-bit FNV-1a, hex. Deterministic and dependency-free. */
154const fnv1a64 = (input: string): string => {
155 let hash = 0xcbf29ce484222325n;
156 for (let index = 0; index < input.length; index++) {
157 hash ^= BigInt(input.charCodeAt(index));
158 hash = (hash * 0x100000001b3n) & 0xffffffffffffffffn;
159 }
160 return hash.toString(16).padStart(16, "0");
161};
162
163const HASH_KEY = "alchemy-hash";
164
165const ownershipHash = (labels: Record<string, string>) =>
166 fnv1a64(
167 JSON.stringify([
168 labels[alchemyLabelKeys.stack],
169 labels[alchemyLabelKeys.stage],
170 labels[alchemyLabelKeys.id],
171 ]),
172 );
173
174/**
175 * Prefix `displayName` with the ownership marker. When the full
176 * `[alchemy stack=… stage=… id=…]` marker would push the name past
177 * Vertex AI's 128-character limit (long stack names), a fixed-width
178 * `[alchemy alchemy-hash=…]` marker is used instead so it is never cut.
179 */
180export const encodeDisplayName = (
181 labels: Record<string, string>,
182 displayName: string | undefined,
183): string => {
184 const trimmed = displayName?.replace(/[\r\n]+/g, " ").trim();
185 const withMarker = (marker: string) =>
186 trimmed && trimmed.length > 0 ? `${marker} ${trimmed}` : marker;
187 const full = withMarker(markerOf(labels));
188 if (full.length <= MAX_DISPLAY_NAME_LENGTH) return full;
189 return withMarker(`[alchemy ${HASH_KEY}=${ownershipHash(labels)}]`).slice(
190 0,
191 MAX_DISPLAY_NAME_LENGTH,
192 );
193};
194
195/** Whether a marked display name belongs to logical id `id` of this stack. */
196export const ownsDisplayName = (id: string, displayName: string | undefined) =>
197 Effect.gen(function* () {
198 const { labels } = parseDisplayName(displayName);
199 const expected = yield* createInternalLabelsImpl(id);
200 if (labels[HASH_KEY] !== undefined) {
201 return labels[HASH_KEY] === ownershipHash(expected);
202 }
203 return yield* hasAlchemyLabels(id, labels);
204 });
205
206export const parseDisplayName = (
207 displayName: string | undefined,
208): {
209 labels: Record<string, string>;
210 displayName: string | undefined;
211} => {
212 if (!displayName?.startsWith("[alchemy ")) {
213 return { labels: {}, displayName };
214 }
215 const end = displayName.indexOf("]");
216 if (end < 0) return { labels: {}, displayName };
217 const labels: Record<string, string> = {};
218 for (const part of displayName.slice("[alchemy ".length, end).split(/\s+/)) {
219 const eq = part.indexOf("=");
220 if (eq > 0) {
221 labels[part.slice(0, eq)] = part.slice(eq + 1);
222 }
223 }
224 const rest = displayName.slice(end + 1).replace(/^[\s\n]+/, "");
225 return { labels, displayName: rest.length > 0 ? rest : undefined };
226};
227
228export const encodeDescription = (
229 labels: Record<string, string>,
230 description: string | undefined,
231): string => {
232 const marker = markerOf(labels);
233 return description ? `${marker}\n${description}` : marker;
234};
235
236export const parseDescription = (
237 description: string | undefined,
238): {
239 labels: Record<string, string>;
240 description: string | undefined;
241} => {
242 const parsed = parseDisplayName(description);
243 return { labels: parsed.labels, description: parsed.displayName };
244};
245
246export const hasOwnershipMarker = (text: string | undefined) =>
247 Object.keys(parseDisplayName(text).labels).some((key) =>
248 key.startsWith("alchemy-"),
249 );