@opum-ai/lore 0.1.0
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/LICENSE +21 -0
- package/README.md +306 -0
- package/bin/lore.cjs +109 -0
- package/package.json +67 -0
- package/src/adapters/backlog.ts +1084 -0
- package/src/adapters/git.ts +221 -0
- package/src/cli.ts +667 -0
- package/src/commands/agent.ts +301 -0
- package/src/commands/agents.ts +302 -0
- package/src/commands/args.ts +209 -0
- package/src/commands/changed.ts +70 -0
- package/src/commands/check.ts +1031 -0
- package/src/commands/codex-bridge.ts +49 -0
- package/src/commands/concurrency.ts +48 -0
- package/src/commands/context.ts +292 -0
- package/src/commands/discover.ts +89 -0
- package/src/commands/explorer.ts +253 -0
- package/src/commands/export.ts +93 -0
- package/src/commands/fswrite.ts +928 -0
- package/src/commands/graph.ts +291 -0
- package/src/commands/help.ts +151 -0
- package/src/commands/impact.ts +59 -0
- package/src/commands/init.ts +583 -0
- package/src/commands/instructions.ts +91 -0
- package/src/commands/link.ts +929 -0
- package/src/commands/new.ts +476 -0
- package/src/commands/orphans.ts +457 -0
- package/src/commands/path.ts +67 -0
- package/src/commands/provenance.ts +68 -0
- package/src/commands/query.ts +312 -0
- package/src/commands/reconcile-shared.ts +280 -0
- package/src/commands/rename.ts +585 -0
- package/src/commands/replace.ts +320 -0
- package/src/commands/scaffold.ts +346 -0
- package/src/commands/schema.ts +293 -0
- package/src/commands/snapshot.ts +130 -0
- package/src/commands/supersede.ts +400 -0
- package/src/commands/sync.ts +371 -0
- package/src/commands/tasks.ts +271 -0
- package/src/commands/traversal.ts +151 -0
- package/src/commands/validate.ts +226 -0
- package/src/config.ts +598 -0
- package/src/core/agent-bridge.ts +287 -0
- package/src/core/agent-context.ts +498 -0
- package/src/core/agent-profile.ts +447 -0
- package/src/core/bundle.ts +893 -0
- package/src/core/check.ts +853 -0
- package/src/core/codex-bridge.ts +100 -0
- package/src/core/concept.ts +597 -0
- package/src/core/consumer-scaffold.ts +433 -0
- package/src/core/context.ts +271 -0
- package/src/core/explorer-contract.ts +441 -0
- package/src/core/explorer-qualification.ts +58 -0
- package/src/core/explorer.ts +518 -0
- package/src/core/finding.ts +31 -0
- package/src/core/graph.ts +201 -0
- package/src/core/indexes.ts +436 -0
- package/src/core/instructions.ts +209 -0
- package/src/core/ladybug-driver.ts +1795 -0
- package/src/core/ladybug-lifecycle.ts +1178 -0
- package/src/core/ladybug-native.ts +95 -0
- package/src/core/ladybug-source.ts +667 -0
- package/src/core/links.ts +681 -0
- package/src/core/log.ts +253 -0
- package/src/core/managed-block.ts +540 -0
- package/src/core/manifest.ts +718 -0
- package/src/core/order.ts +13 -0
- package/src/core/profile.ts +1007 -0
- package/src/core/projection.ts +195 -0
- package/src/core/query.ts +542 -0
- package/src/core/reconcile.ts +236 -0
- package/src/core/replace.ts +419 -0
- package/src/core/retrieval.ts +213 -0
- package/src/core/rewrite.ts +940 -0
- package/src/core/scaffold.ts +255 -0
- package/src/core/schema.ts +366 -0
- package/src/core/snapshot-runtime.ts +52 -0
- package/src/core/snapshot-store.ts +287 -0
- package/src/core/snapshot.ts +711 -0
- package/src/core/template.ts +429 -0
- package/src/core/traversal.ts +487 -0
- package/src/core/validate.ts +517 -0
- package/src/core/workspace-contract.ts +473 -0
- package/src/core/workspace-projection.ts +365 -0
- package/src/core/workspace-retrieval.ts +196 -0
- package/src/core/workspace-source.ts +174 -0
- package/src/errors.ts +697 -0
- package/src/meta.ts +7 -0
- package/src/output.ts +589 -0
- package/src/scripts/upstream-backlog-watch.ts +288 -0
- package/src/state.ts +390 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict, repository-resident context profiles for the singular `lore agent`
|
|
3
|
+
* command family. Profiles are portable retrieval policy only: they name an
|
|
4
|
+
* explicit set of Lore concepts/sections and an optional direct-delegate roster.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type Dirent, lstatSync, readdirSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join, posix } from "node:path";
|
|
9
|
+
import GithubSlugger from "github-slugger";
|
|
10
|
+
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { errnoCode, LoreError } from "../errors";
|
|
13
|
+
import { type BundleGraph, nodeText, walkMdast } from "./bundle";
|
|
14
|
+
import { idFromPath } from "./concept";
|
|
15
|
+
import { compareCodeUnits } from "./order";
|
|
16
|
+
|
|
17
|
+
export const AGENT_PROFILES_DIR = ".lore/agents";
|
|
18
|
+
export const DEFAULT_AGENT_MAX_TOKENS = 8_000;
|
|
19
|
+
|
|
20
|
+
export type AgentProfileKind = "specialist" | "orchestrator";
|
|
21
|
+
|
|
22
|
+
export interface AgentProfileReference {
|
|
23
|
+
readonly raw: string;
|
|
24
|
+
readonly conceptId: string;
|
|
25
|
+
readonly anchor?: string;
|
|
26
|
+
readonly normalized: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AgentProfile {
|
|
30
|
+
readonly schemaVersion: 1;
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly description: string;
|
|
33
|
+
readonly kind: AgentProfileKind;
|
|
34
|
+
readonly maxTokens: number;
|
|
35
|
+
readonly pinned: readonly AgentProfileReference[];
|
|
36
|
+
readonly sources: readonly AgentProfileReference[];
|
|
37
|
+
readonly delegates: readonly string[];
|
|
38
|
+
readonly path: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AgentProfileSnapshot {
|
|
42
|
+
readonly profiles: ReadonlyMap<string, AgentProfile>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const PROFILE_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
46
|
+
|
|
47
|
+
const ProfileFileSchema = z.strictObject({
|
|
48
|
+
schema_version: z.literal(1),
|
|
49
|
+
name: z.string(),
|
|
50
|
+
description: z.string(),
|
|
51
|
+
kind: z.enum(["specialist", "orchestrator"]),
|
|
52
|
+
max_tokens: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional(),
|
|
53
|
+
pinned: z.array(z.string()).optional(),
|
|
54
|
+
sources: z.array(z.string()).optional(),
|
|
55
|
+
delegates: z.array(z.string()).optional(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
type ProfileFile = z.infer<typeof ProfileFileSchema>;
|
|
59
|
+
|
|
60
|
+
/** Load every direct `.toml` profile as one validated deterministic snapshot. */
|
|
61
|
+
export function loadAgentProfiles(root: string): AgentProfileSnapshot {
|
|
62
|
+
const dir = join(root, AGENT_PROFILES_DIR);
|
|
63
|
+
const entries = readProfileDirectory(dir);
|
|
64
|
+
if (entries === undefined) {
|
|
65
|
+
return { profiles: new Map() };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const profiles = new Map<string, AgentProfile>();
|
|
69
|
+
const foldedNames = new Map<string, string>();
|
|
70
|
+
for (const entry of entries.sort((a, b) => compareCodeUnits(a.name, b.name))) {
|
|
71
|
+
if (!entry.name.endsWith(".toml")) continue;
|
|
72
|
+
const relPath = `${AGENT_PROFILES_DIR}/${entry.name}`;
|
|
73
|
+
if (entry.isSymbolicLink() || !entry.isFile()) {
|
|
74
|
+
throw validation(
|
|
75
|
+
`${relPath} must be a regular file, not a symlink or directory`,
|
|
76
|
+
`replace ${relPath} with a regular TOML file`,
|
|
77
|
+
{ path: relPath },
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const stem = entry.name.slice(0, -".toml".length);
|
|
81
|
+
const folded = stem.toLowerCase();
|
|
82
|
+
const previous = foldedNames.get(folded);
|
|
83
|
+
if (previous !== undefined) {
|
|
84
|
+
throw validation(
|
|
85
|
+
`agent profile filenames "${previous}" and "${entry.name}" collide by case`,
|
|
86
|
+
"rename one profile so every filename is unique on case-insensitive filesystems",
|
|
87
|
+
{ path: relPath },
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
foldedNames.set(folded, entry.name);
|
|
91
|
+
const parsed = parseProfileFile(readProfileFile(join(dir, entry.name), relPath), relPath);
|
|
92
|
+
const profile = compileProfile(parsed, stem, relPath);
|
|
93
|
+
profiles.set(profile.name, profile);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
validateDelegateGraph(profiles);
|
|
97
|
+
return { profiles };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Resolve every declared concept/anchor against one verified retrieval snapshot. */
|
|
101
|
+
export function validateAgentProfileReferences(snapshot: AgentProfileSnapshot, graph: BundleGraph): void {
|
|
102
|
+
const slugs = new Map<string, ReadonlySet<string>>();
|
|
103
|
+
for (const profile of snapshot.profiles.values()) {
|
|
104
|
+
for (const reference of [...profile.pinned, ...profile.sources]) {
|
|
105
|
+
const concept = graph.concepts.get(reference.conceptId);
|
|
106
|
+
if (concept === undefined) {
|
|
107
|
+
throw validation(
|
|
108
|
+
`${profile.path} references missing concept "${reference.conceptId}"`,
|
|
109
|
+
"fix the profile reference or add the concept to the active bundle",
|
|
110
|
+
{ path: profile.path, reference: reference.normalized },
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (reference.anchor === undefined) continue;
|
|
114
|
+
let headings = slugs.get(concept.id);
|
|
115
|
+
if (headings === undefined) {
|
|
116
|
+
headings = headingSlugs(concept.body);
|
|
117
|
+
slugs.set(concept.id, headings);
|
|
118
|
+
}
|
|
119
|
+
if (!headings.has(reference.anchor)) {
|
|
120
|
+
throw validation(
|
|
121
|
+
`${profile.path} references missing heading "${reference.normalized}"`,
|
|
122
|
+
"use a GitHub-compatible heading anchor that exists in the referenced concept",
|
|
123
|
+
{ path: profile.path, reference: reference.normalized },
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function findAgentProfile(snapshot: AgentProfileSnapshot, name: string): AgentProfile {
|
|
131
|
+
const profile = snapshot.profiles.get(name);
|
|
132
|
+
if (profile !== undefined) return profile;
|
|
133
|
+
throw new LoreError(
|
|
134
|
+
"not_found",
|
|
135
|
+
`agent profile "${name}" was not found`,
|
|
136
|
+
`add ${AGENT_PROFILES_DIR}/${name}.toml or run \`lore agent list\``,
|
|
137
|
+
{ profile: name },
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readProfileDirectory(dir: string): Dirent<string>[] | undefined {
|
|
142
|
+
try {
|
|
143
|
+
const stat = lstatSync(dir);
|
|
144
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
145
|
+
throw validation(
|
|
146
|
+
`${AGENT_PROFILES_DIR} must be a real directory, not a symlink or file`,
|
|
147
|
+
`replace ${AGENT_PROFILES_DIR} with a real directory`,
|
|
148
|
+
{ path: AGENT_PROFILES_DIR },
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
152
|
+
} catch (cause) {
|
|
153
|
+
const code = errnoCode(cause);
|
|
154
|
+
if (code === "ENOENT") return undefined;
|
|
155
|
+
if (cause instanceof LoreError) throw cause;
|
|
156
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
157
|
+
throw new LoreError(
|
|
158
|
+
"denied",
|
|
159
|
+
`cannot read ${AGENT_PROFILES_DIR}`,
|
|
160
|
+
`check filesystem permissions on ${AGENT_PROFILES_DIR}`,
|
|
161
|
+
{ path: AGENT_PROFILES_DIR, code },
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
throw validation(
|
|
165
|
+
`${AGENT_PROFILES_DIR} could not be inspected`,
|
|
166
|
+
`ensure ${AGENT_PROFILES_DIR} is a readable directory`,
|
|
167
|
+
{ path: AGENT_PROFILES_DIR, ...(code === undefined ? {} : { code }) },
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function readProfileFile(path: string, relPath: string): string {
|
|
173
|
+
try {
|
|
174
|
+
return readFileSync(path, "utf8");
|
|
175
|
+
} catch (cause) {
|
|
176
|
+
const code = errnoCode(cause);
|
|
177
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
178
|
+
throw new LoreError("denied", `cannot read ${relPath}`, `check filesystem permissions on ${relPath}`, {
|
|
179
|
+
path: relPath,
|
|
180
|
+
code,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
throw validation(`${relPath} could not be read`, `ensure ${relPath} is a readable regular file`, {
|
|
184
|
+
path: relPath,
|
|
185
|
+
...(code === undefined ? {} : { code }),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function parseProfileFile(raw: string, path: string): ProfileFile {
|
|
191
|
+
let text = raw;
|
|
192
|
+
while (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
|
|
193
|
+
let parsed: unknown;
|
|
194
|
+
try {
|
|
195
|
+
parsed = Bun.TOML.parse(text);
|
|
196
|
+
} catch (cause) {
|
|
197
|
+
throw validation(`${path} is not valid TOML${reason(cause)}`, `fix the TOML syntax in ${path}`, { path });
|
|
198
|
+
}
|
|
199
|
+
const result = ProfileFileSchema.safeParse(parsed);
|
|
200
|
+
if (!result.success) {
|
|
201
|
+
const issue = result.error.issues[0];
|
|
202
|
+
const at = issue?.path.length ? ` at ${issue.path.join(".")}` : "";
|
|
203
|
+
throw validation(
|
|
204
|
+
`${path} does not match agent profile schema 1${at}: ${issue?.message ?? "invalid profile"}`,
|
|
205
|
+
"remove unknown keys and supply the documented schema_version, name, description, kind, and profile fields",
|
|
206
|
+
{ path },
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return result.data;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function compileProfile(file: ProfileFile, stem: string, path: string): AgentProfile {
|
|
213
|
+
if (!PROFILE_NAME.test(file.name)) {
|
|
214
|
+
throw validation(`${path} has invalid profile name "${file.name}"`, "use lower-kebab names such as frontend-dev", {
|
|
215
|
+
path,
|
|
216
|
+
profile: file.name,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (file.name !== stem) {
|
|
220
|
+
throw validation(
|
|
221
|
+
`${path} declares name "${file.name}" but its filename stem is "${stem}"`,
|
|
222
|
+
"make the profile name and filename stem identical",
|
|
223
|
+
{ path, profile: file.name },
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (
|
|
227
|
+
file.description.trim() === "" ||
|
|
228
|
+
file.description !== file.description.trim() ||
|
|
229
|
+
/[\r\n]/.test(file.description)
|
|
230
|
+
) {
|
|
231
|
+
throw validation(
|
|
232
|
+
`${path} description must be one non-empty trimmed line`,
|
|
233
|
+
"use a concise single-line routing description",
|
|
234
|
+
{ path, profile: file.name },
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
if (file.description.length > 300) {
|
|
238
|
+
throw validation(`${path} description exceeds 300 characters`, "shorten the routing description", {
|
|
239
|
+
path,
|
|
240
|
+
profile: file.name,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const pinned = parseReferences(file.pinned ?? [], path, "pinned");
|
|
245
|
+
const sources = parseReferences(file.sources ?? [], path, "sources");
|
|
246
|
+
assertNoReferenceOverlap(pinned, sources, path);
|
|
247
|
+
const delegates = file.delegates ?? [];
|
|
248
|
+
assertUniqueStrings(delegates, path, "delegates");
|
|
249
|
+
for (const delegate of delegates) {
|
|
250
|
+
if (!PROFILE_NAME.test(delegate)) {
|
|
251
|
+
throw validation(`${path} has invalid delegate name "${delegate}"`, "use lower-kebab profile names", {
|
|
252
|
+
path,
|
|
253
|
+
profile: file.name,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (file.kind === "specialist") {
|
|
259
|
+
if (pinned.length + sources.length === 0) {
|
|
260
|
+
throw validation(
|
|
261
|
+
`${path} specialist profile has no pinned or ranked source`,
|
|
262
|
+
"declare at least one pinned or sources reference",
|
|
263
|
+
{ path, profile: file.name },
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (delegates.length > 0) {
|
|
267
|
+
throw validation(
|
|
268
|
+
`${path} specialist profile cannot declare delegates`,
|
|
269
|
+
'remove delegates or use kind = "orchestrator"',
|
|
270
|
+
{
|
|
271
|
+
path,
|
|
272
|
+
profile: file.name,
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
} else if (delegates.length === 0) {
|
|
277
|
+
throw validation(`${path} orchestrator profile has no delegates`, "declare at least one direct delegate", {
|
|
278
|
+
path,
|
|
279
|
+
profile: file.name,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
schemaVersion: 1,
|
|
285
|
+
name: file.name,
|
|
286
|
+
description: file.description,
|
|
287
|
+
kind: file.kind,
|
|
288
|
+
maxTokens: file.max_tokens ?? DEFAULT_AGENT_MAX_TOKENS,
|
|
289
|
+
pinned,
|
|
290
|
+
sources,
|
|
291
|
+
delegates,
|
|
292
|
+
path,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function parseReferences(values: readonly string[], path: string, field: string): AgentProfileReference[] {
|
|
297
|
+
const refs = values.map((value) => parseAgentReference(value, path, field));
|
|
298
|
+
const seen = new Set<string>();
|
|
299
|
+
for (const reference of refs) {
|
|
300
|
+
if (seen.has(reference.normalized)) {
|
|
301
|
+
throw validation(
|
|
302
|
+
`${path} repeats ${field} reference "${reference.normalized}"`,
|
|
303
|
+
"remove the duplicate reference",
|
|
304
|
+
{ path, reference: reference.normalized },
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
seen.add(reference.normalized);
|
|
308
|
+
}
|
|
309
|
+
return refs;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function parseAgentReference(raw: string, path: string, field: string): AgentProfileReference {
|
|
313
|
+
const value = raw.trim();
|
|
314
|
+
if (value === "" || value !== raw || value.includes("\\")) {
|
|
315
|
+
throw validation(
|
|
316
|
+
`${path} has invalid ${field} reference "${raw}"`,
|
|
317
|
+
"use a trimmed POSIX bundle concept id with an optional #heading-anchor",
|
|
318
|
+
{ path, reference: raw },
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
const hash = value.indexOf("#");
|
|
322
|
+
const conceptPart = hash === -1 ? value : value.slice(0, hash);
|
|
323
|
+
const anchor = hash === -1 ? undefined : value.slice(hash + 1);
|
|
324
|
+
if (
|
|
325
|
+
conceptPart === "" ||
|
|
326
|
+
conceptPart.startsWith("/") ||
|
|
327
|
+
posix.isAbsolute(conceptPart) ||
|
|
328
|
+
conceptPart.split("/").some((segment) => segment === "..") ||
|
|
329
|
+
(hash !== -1 && (anchor === undefined || anchor === "" || anchor.includes("#")))
|
|
330
|
+
) {
|
|
331
|
+
throw validation(
|
|
332
|
+
`${path} has invalid ${field} reference "${raw}"`,
|
|
333
|
+
"use a bundle-relative concept id with one optional non-empty #heading-anchor",
|
|
334
|
+
{ path, reference: raw },
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const conceptId = idFromPath(conceptPart);
|
|
338
|
+
if (conceptId === "" || conceptId === ".") {
|
|
339
|
+
throw validation(`${path} has invalid ${field} reference "${raw}"`, "name a concrete bundle concept", {
|
|
340
|
+
path,
|
|
341
|
+
reference: raw,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
const normalized = anchor === undefined ? conceptId : `${conceptId}#${anchor}`;
|
|
345
|
+
return { raw, conceptId, ...(anchor === undefined ? {} : { anchor }), normalized };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function assertNoReferenceOverlap(
|
|
349
|
+
pinned: readonly AgentProfileReference[],
|
|
350
|
+
sources: readonly AgentProfileReference[],
|
|
351
|
+
path: string,
|
|
352
|
+
): void {
|
|
353
|
+
const all = [...pinned.map((ref) => ({ ref, tier: "pinned" })), ...sources.map((ref) => ({ ref, tier: "sources" }))];
|
|
354
|
+
for (let i = 0; i < all.length; i++) {
|
|
355
|
+
const left = all[i] as { ref: AgentProfileReference; tier: string };
|
|
356
|
+
for (let j = i + 1; j < all.length; j++) {
|
|
357
|
+
const right = all[j] as { ref: AgentProfileReference; tier: string };
|
|
358
|
+
if (left.ref.conceptId !== right.ref.conceptId) continue;
|
|
359
|
+
if (
|
|
360
|
+
left.ref.normalized === right.ref.normalized ||
|
|
361
|
+
left.ref.anchor === undefined ||
|
|
362
|
+
right.ref.anchor === undefined
|
|
363
|
+
) {
|
|
364
|
+
throw validation(
|
|
365
|
+
`${path} has overlapping ${left.tier}/${right.tier} references "${left.ref.normalized}" and "${right.ref.normalized}"`,
|
|
366
|
+
"keep a whole document or its sections in only one tier",
|
|
367
|
+
{ path, reference: right.ref.normalized },
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function assertUniqueStrings(values: readonly string[], path: string, field: string): void {
|
|
375
|
+
const seen = new Set<string>();
|
|
376
|
+
for (const value of values) {
|
|
377
|
+
if (seen.has(value)) {
|
|
378
|
+
throw validation(`${path} repeats ${field} value "${value}"`, `remove the duplicate ${field} value`, {
|
|
379
|
+
path,
|
|
380
|
+
value,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
seen.add(value);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function validateDelegateGraph(profiles: ReadonlyMap<string, AgentProfile>): void {
|
|
388
|
+
for (const profile of profiles.values()) {
|
|
389
|
+
for (const delegate of profile.delegates) {
|
|
390
|
+
if (delegate === profile.name) {
|
|
391
|
+
throw validation(`${profile.path} delegates to itself`, "remove the self-delegate edge", {
|
|
392
|
+
path: profile.path,
|
|
393
|
+
profile: profile.name,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
if (!profiles.has(delegate)) {
|
|
397
|
+
throw validation(
|
|
398
|
+
`${profile.path} delegates to missing profile "${delegate}"`,
|
|
399
|
+
`add ${AGENT_PROFILES_DIR}/${delegate}.toml or remove the delegate`,
|
|
400
|
+
{ path: profile.path, profile: profile.name, delegate },
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const visiting = new Set<string>();
|
|
407
|
+
const visited = new Set<string>();
|
|
408
|
+
const path: string[] = [];
|
|
409
|
+
const visit = (name: string): void => {
|
|
410
|
+
if (visited.has(name)) return;
|
|
411
|
+
if (visiting.has(name)) {
|
|
412
|
+
const start = path.indexOf(name);
|
|
413
|
+
const cycle = [...path.slice(start), name];
|
|
414
|
+
const profile = profiles.get(name) as AgentProfile;
|
|
415
|
+
throw validation(
|
|
416
|
+
`agent delegate cycle: ${cycle.join(" -> ")}`,
|
|
417
|
+
"remove at least one delegate edge so the profile graph is acyclic",
|
|
418
|
+
{ path: profile.path, cycle },
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
visiting.add(name);
|
|
422
|
+
path.push(name);
|
|
423
|
+
const profile = profiles.get(name) as AgentProfile;
|
|
424
|
+
for (const delegate of profile.delegates) visit(delegate);
|
|
425
|
+
path.pop();
|
|
426
|
+
visiting.delete(name);
|
|
427
|
+
visited.add(name);
|
|
428
|
+
};
|
|
429
|
+
for (const name of profiles.keys()) visit(name);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function headingSlugs(body: string): ReadonlySet<string> {
|
|
433
|
+
const slugger = new GithubSlugger();
|
|
434
|
+
const slugs = new Set<string>();
|
|
435
|
+
walkMdast(fromMarkdown(body), (node) => {
|
|
436
|
+
if (node.type === "heading") slugs.add(slugger.slug(nodeText(node)));
|
|
437
|
+
});
|
|
438
|
+
return slugs;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function reason(cause: unknown): string {
|
|
442
|
+
return cause instanceof Error && cause.message.trim() !== "" ? `: ${cause.message.trim()}` : "";
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function validation(message: string, hint: string, input: Record<string, unknown>): LoreError {
|
|
446
|
+
return new LoreError("validation", message, hint, input);
|
|
447
|
+
}
|