-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathdep-dedup.ts
More file actions
140 lines (127 loc) · 5.54 KB
/
Copy pathdep-dedup.ts
File metadata and controls
140 lines (127 loc) · 5.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// Dependency deduplication helpers for the push pipeline.
//
// On a targeted assistant push, `ensureToolExists` / `ensureStructuredOutputExists`
// previously skipped auto-create only when (a) the dep id was UUID-shaped,
// (b) `state.tools[depId]` was an exact key match, or (c) we'd already
// auto-applied this id in the current run. Bootstrap pull stores resources
// under `<slug>-<uuid8>` keys (e.g. `end-call-67aea057`), so a local file
// referencing `my-end-call` would miss the exact-key check and POST a
// duplicate dashboard tool. Repeated targeted pushes accumulated orphans
// on the dashboard.
//
// This module's helpers detect those collisions BEFORE create:
// - `findExistingResourceByName` — match local payload's canonical name
// against existing state entries (renamed/state-only keys) and the live
// dashboard list. Returns the UUID to adopt, plus an `ambiguous` flag
// when multiple distinct UUIDs share the same name (real on-dashboard
// duplicates from prior bug runs — the caller should warn and surface
// the loser UUIDs so a follow-up `npm run cleanup` can prune them).
//
// `slugify` and `extractBaseSlug` previously lived inline here as a
// deliberate copy of pull.ts's definitions — pull.ts imports config.ts
// which `process.exit(1)`s under unit tests, blocking direct reuse. They
// now live in `./slug-utils.ts` (config-free, side-effect-free) and are
// re-exported below so any existing test importing them from this module
// keeps working unchanged.
import { extractBaseSlug, slugify } from "./slug-utils.ts";
import type { ResourceState } from "./types.ts";
export { extractBaseSlug, slugify } from "./slug-utils.ts";
export interface RemoteResource {
id: string;
name?: string;
function?: { name?: string };
}
export interface DedupMatch {
uuid: string;
source: "state" | "dashboard" | "both";
ambiguous: boolean;
// Other distinct UUIDs we saw under the same canonical name. Empty when
// `ambiguous` is false. Caller should surface these in a warning so the
// user can run `npm run cleanup` to prune the duplicates.
duplicateUuids: string[];
}
// Minimal payload shape this module needs. Local resource files are loaded
// as `Record<string, unknown>`, so the only fields we know exist are `name`
// (top-level, used by SOs / assistants / squads) and a nested `function.name`
// (used by tools). Everything else stays opaque — we narrow at use.
export type NameablePayload = { name?: unknown; function?: unknown };
// Pulls the canonical name from a tool / SO / assistant payload.
// For tools: `function.name` is the canonical name.
// For SOs / assistants / etc.: top-level `name`. Top-level wins when both
// are present.
export function extractResourceName(
payload: NameablePayload,
): string | undefined {
if (typeof payload.name === "string" && payload.name) return payload.name;
const fn = payload.function;
if (
fn !== null &&
typeof fn === "object" &&
"name" in fn &&
typeof fn.name === "string" &&
fn.name
) {
return fn.name;
}
return undefined;
}
// Find an existing resource (in state or dashboard) whose canonical name
// matches the local payload's canonical name. Used by ensureToolExists /
// ensureStructuredOutputExists to avoid creating a duplicate when bootstrap
// pull has already stored the same dashboard resource under a different
// state key.
//
// `localResourceId` is the key the engine WANTS to use in state. If a state
// entry already exists under that exact key, the caller short-circuits BEFORE
// calling this — so we exclude it here as a safety belt.
//
// Tiebreaker for >1 distinct UUID matching the same slugified name (i.e.
// real on-dashboard duplicates from prior bug runs): pick lexically smallest
// UUID for stable, deterministic adoption. Set `ambiguous=true` and surface
// the loser UUIDs in `duplicateUuids` so the caller can warn.
export function findExistingResourceByName(args: {
localResourceId: string;
localPayload: NameablePayload;
stateSection: Record<string, ResourceState>;
remoteList?: RemoteResource[];
}): DedupMatch | undefined {
const localName = extractResourceName(args.localPayload);
if (!localName) return undefined;
const localSlug = slugify(localName);
// uuid -> set of source labels (state:<key>, dashboard:<id>)
const matches = new Map<string, Set<string>>();
for (const [stateKey, entry] of Object.entries(args.stateSection)) {
if (stateKey === args.localResourceId) continue;
if (extractBaseSlug(stateKey) === localSlug) {
const set = matches.get(entry.uuid) ?? new Set<string>();
set.add(`state:${stateKey}`);
matches.set(entry.uuid, set);
}
}
for (const remote of args.remoteList ?? []) {
const remoteName =
(typeof remote.name === "string" && remote.name) || remote.function?.name;
if (!remoteName) continue;
if (slugify(remoteName) === localSlug) {
const set = matches.get(remote.id) ?? new Set<string>();
set.add(`dashboard:${remote.id}`);
matches.set(remote.id, set);
}
}
if (matches.size === 0) return undefined;
const sorted = [...matches.keys()].sort();
const winner = sorted[0]!;
const winnerSources = matches.get(winner)!;
const hasState = [...winnerSources].some((s) => s.startsWith("state:"));
const hasDashboard = [...winnerSources].some((s) =>
s.startsWith("dashboard:"),
);
const source: DedupMatch["source"] =
hasState && hasDashboard ? "both" : hasState ? "state" : "dashboard";
return {
uuid: winner,
source,
ambiguous: matches.size > 1,
duplicateUuids: sorted.slice(1),
};
}