@lsctech/polaris 0.4.7 → 0.4.8
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.
|
@@ -4,6 +4,7 @@ exports.CANDIDATE_MARKER = void 0;
|
|
|
4
4
|
exports.parseFrontMatter = parseFrontMatter;
|
|
5
5
|
exports.addCandidateGovernanceMetadata = addCandidateGovernanceMetadata;
|
|
6
6
|
exports.stampIngestFrontMatter = stampIngestFrontMatter;
|
|
7
|
+
exports.backfillOkfType = backfillOkfType;
|
|
7
8
|
exports.doctrineDraft = doctrineDraft;
|
|
8
9
|
exports.doctrinePromote = doctrinePromote;
|
|
9
10
|
exports.doctrineDeprecate = doctrineDeprecate;
|
|
@@ -90,6 +91,10 @@ function parseFrontMatter(content) {
|
|
|
90
91
|
*/
|
|
91
92
|
function addCandidateGovernanceMetadata(content, docType) {
|
|
92
93
|
const govDefaults = {
|
|
94
|
+
// OKF-conformant type field. Mirrors doc-type's value; added alongside
|
|
95
|
+
// (not instead of) doc-type since ingest.ts and requiredFields still
|
|
96
|
+
// read doc-type for internal classification/validation.
|
|
97
|
+
"type": docType,
|
|
93
98
|
"doc-type": docType,
|
|
94
99
|
"confidence": "0.0",
|
|
95
100
|
"recommended-action": "hold",
|
|
@@ -184,6 +189,90 @@ function stampIngestFrontMatter(content, stamp) {
|
|
|
184
189
|
const block = Object.entries(fields).map(([k, v]) => `${k}: ${v}`).join("\n");
|
|
185
190
|
return `---\n${block}\n---\n\n${normalized}`;
|
|
186
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Insert a single missing frontmatter field into markdown content, preserving
|
|
194
|
+
* an existing frontmatter block if present or creating a new one if not.
|
|
195
|
+
* No-op (returns content unchanged) if the key is already present.
|
|
196
|
+
*/
|
|
197
|
+
function addMissingFrontMatterField(content, key, value) {
|
|
198
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
199
|
+
if (normalized.startsWith("---\n")) {
|
|
200
|
+
const end = normalized.indexOf("\n---", 4);
|
|
201
|
+
if (end !== -1) {
|
|
202
|
+
const frontMatter = normalized.slice(4, end);
|
|
203
|
+
const afterFrontMatter = normalized.slice(end + 4);
|
|
204
|
+
const hasKey = frontMatter
|
|
205
|
+
.split("\n")
|
|
206
|
+
.filter((l) => l.includes(":"))
|
|
207
|
+
.some((l) => l.slice(0, l.indexOf(":")).trim().toLowerCase() === key.toLowerCase());
|
|
208
|
+
if (hasKey)
|
|
209
|
+
return normalized;
|
|
210
|
+
return `---\n${frontMatter}\n${key}: ${value}\n---${afterFrontMatter}`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return `---\n${key}: ${value}\n---\n\n${normalized}`;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Retrofit existing SmartDocs with an OKF-conformant `type` frontmatter field.
|
|
217
|
+
*
|
|
218
|
+
* Walks every `.md` file under `smartdocs/`. For each file already missing a
|
|
219
|
+
* `type` key: derives the value from `kind` if present, else `doc-type` if
|
|
220
|
+
* present, else defaults to `"raw"`. Files that already declare `type` are
|
|
221
|
+
* left untouched and reported in `skipped`.
|
|
222
|
+
*
|
|
223
|
+
* This is the retroactive counterpart to `addCandidateGovernanceMetadata`,
|
|
224
|
+
* which stamps `type` on newly-governed candidate docs going forward — this
|
|
225
|
+
* function backfills docs that predate that change (or were authored by
|
|
226
|
+
* hand without a `type` field at all).
|
|
227
|
+
*
|
|
228
|
+
* @param repoRoot - Repository root; files are read from and written to
|
|
229
|
+
* `<repoRoot>/smartdocs/**\/*.md`.
|
|
230
|
+
* @param options.dryRun - When true, computes the result without writing
|
|
231
|
+
* any files.
|
|
232
|
+
*/
|
|
233
|
+
function backfillOkfType(repoRoot, options = {}) {
|
|
234
|
+
const smartdocsDir = (0, node_path_1.join)(repoRoot, "smartdocs");
|
|
235
|
+
const result = { updated: [], skipped: [] };
|
|
236
|
+
function walk(dir) {
|
|
237
|
+
let entries;
|
|
238
|
+
try {
|
|
239
|
+
entries = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
for (const entry of entries) {
|
|
245
|
+
const full = (0, node_path_1.join)(dir, entry.name);
|
|
246
|
+
if (entry.isDirectory()) {
|
|
247
|
+
walk(full);
|
|
248
|
+
}
|
|
249
|
+
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
250
|
+
processFile(full);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function processFile(filePath) {
|
|
255
|
+
let content;
|
|
256
|
+
try {
|
|
257
|
+
content = (0, node_fs_1.readFileSync)(filePath, "utf-8");
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const fm = parseFrontMatter(content);
|
|
263
|
+
if (fm["type"]) {
|
|
264
|
+
result.skipped.push(filePath);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const derivedType = fm.kind ?? fm["doc-type"] ?? "raw";
|
|
268
|
+
if (!options.dryRun) {
|
|
269
|
+
(0, node_fs_1.writeFileSync)(filePath, addMissingFrontMatterField(content, "type", derivedType), "utf-8");
|
|
270
|
+
}
|
|
271
|
+
result.updated.push({ path: filePath, type: derivedType });
|
|
272
|
+
}
|
|
273
|
+
walk(smartdocsDir);
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
187
276
|
function appendLifecycle(lifecyclePath, event) {
|
|
188
277
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(lifecyclePath), { recursive: true });
|
|
189
278
|
(0, node_fs_1.appendFileSync)(lifecyclePath, JSON.stringify(event) + "\n", "utf-8");
|
|
@@ -434,9 +434,29 @@ function createDocsCommand(options = {}) {
|
|
|
434
434
|
console.log(`skipped (draft exists): ${targetPath}/index.md`);
|
|
435
435
|
}
|
|
436
436
|
});
|
|
437
|
+
docs
|
|
438
|
+
.command("backfill-type")
|
|
439
|
+
.description("Add OKF-conformant `type` frontmatter to existing smartdocs/ files that are missing it, " +
|
|
440
|
+
"deriving the value from `kind` or `doc-type` when present, else defaulting to \"raw\".")
|
|
441
|
+
.option("-r, --repo-root <path>", "Repository root", defaultRepoRoot)
|
|
442
|
+
.option("--dry-run", "Print what would be written without writing files")
|
|
443
|
+
.action((options) => {
|
|
444
|
+
try {
|
|
445
|
+
const { updated, skipped } = (0, doctrine_js_1.backfillOkfType)(options.repoRoot, { dryRun: options.dryRun });
|
|
446
|
+
for (const { path, type } of updated) {
|
|
447
|
+
console.log(`${options.dryRun ? "[dry-run] would add" : "added"} type: ${type} — ${path}`);
|
|
448
|
+
}
|
|
449
|
+
console.log(`Done. ${updated.length} updated, ${skipped.length} already had type.`);
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
console.error(`polaris docs backfill-type: ${err instanceof Error ? err.message : String(err)}`);
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
437
456
|
docs
|
|
438
457
|
.command("reformat-okf")
|
|
439
|
-
.description("Migrate existing smartdocs to OKF structure in one step: runs migrate → seed-index --all →
|
|
458
|
+
.description("Migrate existing smartdocs to OKF structure in one step: runs migrate → seed-index --all → " +
|
|
459
|
+
"seed-instructions --all → backfill-type. " +
|
|
440
460
|
"Agent instruction files (CLAUDE.md, AGENTS.md, etc.) are never touched. " +
|
|
441
461
|
"Use --dry-run to preview changes before writing.")
|
|
442
462
|
.option("--dry-run", "Preview what would change without writing any files")
|
|
@@ -446,7 +466,7 @@ function createDocsCommand(options = {}) {
|
|
|
446
466
|
const repoRoot = options.repoRoot;
|
|
447
467
|
const label = dryRun ? "[dry-run]" : "";
|
|
448
468
|
// Step 1: migrate (moves scattered markdown to smartdocs/raw/)
|
|
449
|
-
console.log(`${label ? label + " " : ""}Step 1/
|
|
469
|
+
console.log(`${label ? label + " " : ""}Step 1/4: migrate`);
|
|
450
470
|
try {
|
|
451
471
|
const migrateResult = (0, migrate_js_1.migrateDocs)({ repoRoot, dryRun });
|
|
452
472
|
(0, migrate_js_1.printMigrateResults)(migrateResult);
|
|
@@ -455,8 +475,8 @@ function createDocsCommand(options = {}) {
|
|
|
455
475
|
console.error(`reformat-okf: migrate failed — ${err instanceof Error ? err.message : String(err)}`);
|
|
456
476
|
process.exit(1);
|
|
457
477
|
}
|
|
458
|
-
// Step 2: seed-index --all (ensures index.md with okf_version frontmatter in every smartdocs dir)
|
|
459
|
-
console.log(`\n${label ? label + " " : ""}Step 2/
|
|
478
|
+
// Step 2: seed-index --all (ensures index.md with okf_version + type frontmatter in every smartdocs dir)
|
|
479
|
+
console.log(`\n${label ? label + " " : ""}Step 2/4: seed-index --all`);
|
|
460
480
|
try {
|
|
461
481
|
const { written, skippedExists, skippedDraft } = (0, seed_instructions_js_1.seedIndexAll)(repoRoot, { dryRun });
|
|
462
482
|
printSeedAllResult("index.md", dryRun, { written, skippedExists, skippedDraft }, { leadingBlankLine: false });
|
|
@@ -466,7 +486,7 @@ function createDocsCommand(options = {}) {
|
|
|
466
486
|
process.exit(1);
|
|
467
487
|
}
|
|
468
488
|
// Step 3: seed-instructions --all (ensures POLARIS.md drafts; never touches CLAUDE.md/AGENTS.md)
|
|
469
|
-
console.log(`\n${label ? label + " " : ""}Step 3/
|
|
489
|
+
console.log(`\n${label ? label + " " : ""}Step 3/4: seed-instructions --all`);
|
|
470
490
|
try {
|
|
471
491
|
const { written, skippedExists, skippedDraft, skippedIneligible, skippedRoot } = (0, seed_instructions_js_1.seedInstructionsAll)(repoRoot, { dryRun });
|
|
472
492
|
const rootCount = skippedRoot ? 1 : 0;
|
|
@@ -476,6 +496,19 @@ function createDocsCommand(options = {}) {
|
|
|
476
496
|
console.error(`reformat-okf: seed-instructions failed — ${err instanceof Error ? err.message : String(err)}`);
|
|
477
497
|
process.exit(1);
|
|
478
498
|
}
|
|
499
|
+
// Step 4: backfill-type (existing docs missing `type` get it derived from kind/doc-type, or "raw")
|
|
500
|
+
console.log(`\n${label ? label + " " : ""}Step 4/4: backfill-type`);
|
|
501
|
+
try {
|
|
502
|
+
const { updated, skipped } = (0, doctrine_js_1.backfillOkfType)(repoRoot, { dryRun });
|
|
503
|
+
for (const { path, type } of updated) {
|
|
504
|
+
console.log(`${dryRun ? "[dry-run] would add" : "added"} type: ${type} — ${path}`);
|
|
505
|
+
}
|
|
506
|
+
console.log(`Done. ${updated.length} updated, ${skipped.length} already had type.`);
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
console.error(`reformat-okf: backfill-type failed — ${err instanceof Error ? err.message : String(err)}`);
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
479
512
|
console.log(`\nreformat-okf complete.${dryRun ? " (dry-run — no files written)" : ""}`);
|
|
480
513
|
});
|
|
481
514
|
docs
|
|
@@ -196,6 +196,18 @@ function listConceptFiles(dir) {
|
|
|
196
196
|
return [];
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
|
+
/** Immediate child subdirectories eligible for their own index.md (excludes hidden dirs and raw/). */
|
|
200
|
+
function listChildDirs(dir) {
|
|
201
|
+
try {
|
|
202
|
+
return (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })
|
|
203
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "raw")
|
|
204
|
+
.map((e) => e.name)
|
|
205
|
+
.sort();
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
199
211
|
function conceptLabel(filePath) {
|
|
200
212
|
try {
|
|
201
213
|
const content = (0, node_fs_1.readFileSync)(filePath, "utf-8");
|
|
@@ -232,6 +244,7 @@ function generateBundleRootIndex(repoRoot, allRoutes) {
|
|
|
232
244
|
const lines = [
|
|
233
245
|
"---",
|
|
234
246
|
"okf_version: \"0.1\"",
|
|
247
|
+
"type: index",
|
|
235
248
|
"---",
|
|
236
249
|
"",
|
|
237
250
|
exports.DRAFT_MARKER,
|
|
@@ -289,6 +302,7 @@ function generateDirectoryIndex(targetDir, repoRoot) {
|
|
|
289
302
|
const lines = [
|
|
290
303
|
"---",
|
|
291
304
|
"okf_version: \"0.1\"",
|
|
305
|
+
"type: index",
|
|
292
306
|
"---",
|
|
293
307
|
"",
|
|
294
308
|
exports.DRAFT_MARKER,
|
|
@@ -309,6 +323,18 @@ function generateDirectoryIndex(targetDir, repoRoot) {
|
|
|
309
323
|
lines.push("<!-- No concept files in this directory yet. -->");
|
|
310
324
|
}
|
|
311
325
|
lines.push("");
|
|
326
|
+
const childDirs = listChildDirs(absDir);
|
|
327
|
+
if (childDirs.length > 0) {
|
|
328
|
+
lines.push("## Subdirectories", "");
|
|
329
|
+
for (const child of childDirs) {
|
|
330
|
+
// Always link to the child's index.md rather than checking existsSync: generateDirectoryIndex
|
|
331
|
+
// only ever runs under smartdocs/ (enforced by seedIndex's own path guard), where every
|
|
332
|
+
// eligible subdirectory gets its own index.md — and seedIndexAll writes parents before
|
|
333
|
+
// children in the same pass, so existsSync would be write-order-dependent here.
|
|
334
|
+
lines.push(`- [${child}/](${child}/index.md)`);
|
|
335
|
+
}
|
|
336
|
+
lines.push("");
|
|
337
|
+
}
|
|
312
338
|
return lines.join("\n");
|
|
313
339
|
}
|
|
314
340
|
function seedInstructions(targetPath, repoRoot, opts = {}) {
|
|
@@ -10,10 +10,13 @@ exports.deleteTriageCheckpoint = deleteTriageCheckpoint;
|
|
|
10
10
|
exports.writeTriageQueue = writeTriageQueue;
|
|
11
11
|
exports.resolveTriageModel = resolveTriageModel;
|
|
12
12
|
exports.runBatchComparison = runBatchComparison;
|
|
13
|
+
exports.resolveLibrarianProviderConfig = resolveLibrarianProviderConfig;
|
|
13
14
|
exports.runGraphCheck = runGraphCheck;
|
|
14
15
|
exports.runTriage = runTriage;
|
|
15
16
|
const node_fs_1 = require("node:fs");
|
|
17
|
+
const node_child_process_1 = require("node:child_process");
|
|
16
18
|
const node_path_1 = require("node:path");
|
|
19
|
+
const index_js_1 = require("../config/index.js");
|
|
17
20
|
// ---------------------------------------------------------------------------
|
|
18
21
|
// clusterCandidates
|
|
19
22
|
// ---------------------------------------------------------------------------
|
|
@@ -307,7 +310,7 @@ function resolveTriageModel(configured) {
|
|
|
307
310
|
"claude-haiku-4-5-20251001");
|
|
308
311
|
}
|
|
309
312
|
async function runBatchComparison(candidates, canonicals, options) {
|
|
310
|
-
const client = options.llmClient ?? (await buildDefaultLlmClient());
|
|
313
|
+
const client = options.llmClient ?? (await buildDefaultLlmClient(options.repoRoot ?? process.cwd()));
|
|
311
314
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
312
315
|
try {
|
|
313
316
|
return await client.compare(candidates, canonicals, options.model);
|
|
@@ -318,8 +321,103 @@ async function runBatchComparison(candidates, canonicals, options) {
|
|
|
318
321
|
}
|
|
319
322
|
return [];
|
|
320
323
|
}
|
|
321
|
-
|
|
324
|
+
function buildComparisonPrompt(candidates, canonicals) {
|
|
325
|
+
const candidateSummaries = candidates
|
|
326
|
+
.map((c, i) => `[${i}] ${(0, node_path_1.basename)(c.path)} (${c.type || "unknown type"}) tags: ${c.tags.join(", ") || "none"}`)
|
|
327
|
+
.join("\n");
|
|
328
|
+
const canonicalSummaries = canonicals
|
|
329
|
+
.map((c) => `- ${(0, node_path_1.basename)(c.path)} (${c.type || "unknown type"}) tags: ${c.tags.join(", ") || "none"}`)
|
|
330
|
+
.join("\n");
|
|
331
|
+
return [
|
|
332
|
+
"You are comparing candidate documentation files against canonical documentation.",
|
|
333
|
+
"Identify any candidates that CONTRADICT or DUPLICATE a canonical.",
|
|
334
|
+
'Return ONLY a JSON array. If no issues found, return [].',
|
|
335
|
+
'Each item must be: { "candidatePath": string, "flagType": "contradiction" | "duplicate", "canonicalPath"?: string, "reason": string }',
|
|
336
|
+
"",
|
|
337
|
+
"CANDIDATES:",
|
|
338
|
+
candidateSummaries,
|
|
339
|
+
"",
|
|
340
|
+
"CANONICALS:",
|
|
341
|
+
canonicalSummaries,
|
|
342
|
+
"",
|
|
343
|
+
"Respond with JSON only. No prose.",
|
|
344
|
+
].join("\n");
|
|
345
|
+
}
|
|
346
|
+
function parseFlagsFromText(text) {
|
|
347
|
+
const cleaned = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
348
|
+
return JSON.parse(cleaned);
|
|
349
|
+
}
|
|
350
|
+
/** Expand $VAR and ${VAR} references from process.env. */
|
|
351
|
+
function expandEnvVars(str) {
|
|
352
|
+
return str
|
|
353
|
+
.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/g, (_, name) => process.env[name] ?? "")
|
|
354
|
+
.replace(/\$([A-Z_][A-Z0-9_]*)/g, (_, name) => process.env[name] ?? "");
|
|
355
|
+
}
|
|
356
|
+
/** Substitute {{key}} template variables. Unknown keys are left as-is. */
|
|
357
|
+
function substituteTemplates(str, vars) {
|
|
358
|
+
return str.replace(/\{\{([^}]+)\}\}/g, (original, key) => vars[key.trim()] ?? original);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Resolve the configured CLI provider for triage's librarian-role LLM calls, per
|
|
362
|
+
* polaris.config.json's execution.providerPolicy.librarian / execution.providers —
|
|
363
|
+
* the same config the terminal-cli dispatch adapter reads for Foreman/Worker
|
|
364
|
+
* sessions. Returns undefined if no usable provider is configured, in which case
|
|
365
|
+
* callers fall back to a raw API-key-based client.
|
|
366
|
+
*/
|
|
367
|
+
function resolveLibrarianProviderConfig(repoRoot) {
|
|
368
|
+
try {
|
|
369
|
+
const config = (0, index_js_1.loadConfig)(repoRoot);
|
|
370
|
+
const providers = config.execution?.providers ?? {};
|
|
371
|
+
const policyNames = config.execution?.providerPolicy?.librarian?.providers ?? [];
|
|
372
|
+
for (const name of policyNames) {
|
|
373
|
+
const cfg = providers[name];
|
|
374
|
+
if (cfg)
|
|
375
|
+
return cfg;
|
|
376
|
+
}
|
|
377
|
+
// Fall back to a directly-named "claude" provider if no policy entry resolved.
|
|
378
|
+
return providers["claude"];
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Build an LlmClient that shells out to the configured CLI provider (e.g. `claude
|
|
386
|
+
* --print`), the same mechanism the terminal-cli execution adapter uses for
|
|
387
|
+
* Foreman/Worker dispatch. This authenticates via the CLI's own session (OAuth),
|
|
388
|
+
* not a raw ANTHROPIC_API_KEY.
|
|
389
|
+
*/
|
|
390
|
+
function buildCliLlmClient(providerCfg) {
|
|
391
|
+
return {
|
|
392
|
+
async compare(candidates, canonicals) {
|
|
393
|
+
const prompt = buildComparisonPrompt(candidates, canonicals);
|
|
394
|
+
const templateVars = { worker_prompt: prompt };
|
|
395
|
+
const command = substituteTemplates(expandEnvVars(providerCfg.command), templateVars);
|
|
396
|
+
if (!command.trim()) {
|
|
397
|
+
throw new Error(`Provider command "${providerCfg.command}" expanded to an empty string — likely an unset environment variable.`);
|
|
398
|
+
}
|
|
399
|
+
const args = (providerCfg.args ?? []).map((arg) => substituteTemplates(expandEnvVars(arg), templateVars));
|
|
400
|
+
const stdout = (0, node_child_process_1.execFileSync)(command, args, {
|
|
401
|
+
encoding: "utf-8",
|
|
402
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
403
|
+
timeout: 300000,
|
|
404
|
+
});
|
|
405
|
+
return parseFlagsFromText(stdout);
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
async function buildDefaultLlmClient(repoRoot) {
|
|
410
|
+
const cliProviderCfg = resolveLibrarianProviderConfig(repoRoot);
|
|
411
|
+
if (cliProviderCfg) {
|
|
412
|
+
return buildCliLlmClient(cliProviderCfg);
|
|
413
|
+
}
|
|
322
414
|
const apiKey = process.env["ANTHROPIC_API_KEY"] ?? "";
|
|
415
|
+
if (!apiKey) {
|
|
416
|
+
throw new Error("No CLI provider configured for the librarian role (execution.providerPolicy.librarian / " +
|
|
417
|
+
"execution.providers in polaris.config.json) and ANTHROPIC_API_KEY is not set. " +
|
|
418
|
+
"Configure a librarian provider (preferred — uses the CLI's own session auth) or set " +
|
|
419
|
+
"ANTHROPIC_API_KEY as a fallback.");
|
|
420
|
+
}
|
|
323
421
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
324
422
|
// @ts-ignore: @anthropic-ai/sdk may not be installed as a dependency
|
|
325
423
|
const mod = await import("@anthropic-ai/sdk");
|
|
@@ -328,26 +426,7 @@ async function buildDefaultLlmClient() {
|
|
|
328
426
|
const sdkClient = new Anthropic({ apiKey });
|
|
329
427
|
return {
|
|
330
428
|
async compare(candidates, canonicals, model) {
|
|
331
|
-
const
|
|
332
|
-
.map((c, i) => `[${i}] ${(0, node_path_1.basename)(c.path)} (${c.type || "unknown type"}) tags: ${c.tags.join(", ") || "none"}`)
|
|
333
|
-
.join("\n");
|
|
334
|
-
const canonicalSummaries = canonicals
|
|
335
|
-
.map((c) => `- ${(0, node_path_1.basename)(c.path)} (${c.type || "unknown type"}) tags: ${c.tags.join(", ") || "none"}`)
|
|
336
|
-
.join("\n");
|
|
337
|
-
const prompt = [
|
|
338
|
-
"You are comparing candidate documentation files against canonical documentation.",
|
|
339
|
-
"Identify any candidates that CONTRADICT or DUPLICATE a canonical.",
|
|
340
|
-
'Return ONLY a JSON array. If no issues found, return [].',
|
|
341
|
-
'Each item must be: { "candidatePath": string, "flagType": "contradiction" | "duplicate", "canonicalPath"?: string, "reason": string }',
|
|
342
|
-
"",
|
|
343
|
-
"CANDIDATES:",
|
|
344
|
-
candidateSummaries,
|
|
345
|
-
"",
|
|
346
|
-
"CANONICALS:",
|
|
347
|
-
canonicalSummaries,
|
|
348
|
-
"",
|
|
349
|
-
"Respond with JSON only. No prose.",
|
|
350
|
-
].join("\n");
|
|
429
|
+
const prompt = buildComparisonPrompt(candidates, canonicals);
|
|
351
430
|
const response = await sdkClient.messages.create({
|
|
352
431
|
model,
|
|
353
432
|
max_tokens: 1024,
|
|
@@ -357,8 +436,7 @@ async function buildDefaultLlmClient() {
|
|
|
357
436
|
.filter((b) => b.type === "text")
|
|
358
437
|
.map((b) => b.text)
|
|
359
438
|
.join("");
|
|
360
|
-
|
|
361
|
-
return JSON.parse(cleaned);
|
|
439
|
+
return parseFlagsFromText(text);
|
|
362
440
|
},
|
|
363
441
|
};
|
|
364
442
|
}
|
|
@@ -436,7 +514,7 @@ async function runTriage(options) {
|
|
|
436
514
|
output(` Comparing cluster "${clusterName}" (${cluster.candidates.length} candidates, ${cluster.canonicals.length} canonicals)...`);
|
|
437
515
|
for (let i = 0; i < cluster.candidates.length; i += batchSize) {
|
|
438
516
|
const batch = cluster.candidates.slice(i, i + batchSize);
|
|
439
|
-
const flags = await runBatchComparison(batch, cluster.canonicals, { model, llmClient });
|
|
517
|
+
const flags = await runBatchComparison(batch, cluster.canonicals, { model, llmClient, repoRoot });
|
|
440
518
|
accumulatedFlags.push(...flags);
|
|
441
519
|
}
|
|
442
520
|
completedClusters.add(clusterName);
|