@lsctech/polaris 0.2.1 → 0.3.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.
@@ -11,7 +11,7 @@ async function loadTreeSitterRuntime() {
11
11
  return await cachedRuntimePromise;
12
12
  }
13
13
  cachedRuntimePromise = (async () => {
14
- const loaded = await loadTreeSitterModules();
14
+ const loaded = loadTreeSitterModules();
15
15
  const parser = new loaded.parserConstructor();
16
16
  parser.setLanguage(loaded.language);
17
17
  const runtime = {
@@ -25,18 +25,15 @@ async function loadTreeSitterRuntime() {
25
25
  })();
26
26
  return await cachedRuntimePromise;
27
27
  }
28
- async function loadTreeSitterModules() {
29
- const dynamicImport = createDynamicImporter();
30
- const [treeSitterModule, cModule] = await Promise.all([dynamicImport("tree-sitter"), dynamicImport("tree-sitter-c")]);
28
+ function loadTreeSitterModules() {
29
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
30
+ const nodeRequire = require;
31
+ const [treeSitterModule, cModule] = [nodeRequire("tree-sitter"), nodeRequire("tree-sitter-c")];
31
32
  return {
32
33
  parserConstructor: resolveParserConstructor(treeSitterModule),
33
34
  language: resolveGrammar(cModule, "tree-sitter-c"),
34
35
  };
35
36
  }
36
- function createDynamicImporter() {
37
- const importer = new Function("specifier", "return import(specifier);");
38
- return (specifier) => importer(specifier);
39
- }
40
37
  function resolveParserConstructor(moduleValue) {
41
38
  if (typeof moduleValue === "function") {
42
39
  return moduleValue;
@@ -6,7 +6,7 @@ async function loadTreeSitterRuntime() {
6
6
  if (cachedRuntime) {
7
7
  return cachedRuntime;
8
8
  }
9
- const loaded = await loadTreeSitterModules();
9
+ const loaded = loadTreeSitterModules();
10
10
  const parser = new loaded.parserConstructor();
11
11
  parser.setLanguage(loaded.language);
12
12
  cachedRuntime = {
@@ -16,21 +16,18 @@ async function loadTreeSitterRuntime() {
16
16
  };
17
17
  return cachedRuntime;
18
18
  }
19
- async function loadTreeSitterModules() {
20
- const dynamicImport = createDynamicImporter();
21
- const [treeSitterModule, cppModule] = await Promise.all([
22
- dynamicImport("tree-sitter"),
23
- dynamicImport("tree-sitter-cpp"),
24
- ]);
19
+ function loadTreeSitterModules() {
20
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
21
+ const nodeRequire = require;
22
+ const [treeSitterModule, cppModule] = [
23
+ nodeRequire("tree-sitter"),
24
+ nodeRequire("tree-sitter-cpp"),
25
+ ];
25
26
  return {
26
27
  parserConstructor: resolveParserConstructor(treeSitterModule),
27
28
  language: resolveGrammar(cppModule, "tree-sitter-cpp"),
28
29
  };
29
30
  }
30
- function createDynamicImporter() {
31
- const importer = new Function("specifier", "return import(specifier);");
32
- return (specifier) => importer(specifier);
33
- }
34
31
  function resolveParserConstructor(moduleValue) {
35
32
  if (typeof moduleValue === "function") {
36
33
  return moduleValue;
@@ -6,7 +6,7 @@ async function loadTreeSitterRuntime() {
6
6
  if (cachedRuntime) {
7
7
  return cachedRuntime;
8
8
  }
9
- const loaded = await loadTreeSitterModules();
9
+ const loaded = loadTreeSitterModules();
10
10
  const parser = new loaded.parserConstructor();
11
11
  parser.setLanguage(loaded.language);
12
12
  cachedRuntime = {
@@ -16,21 +16,18 @@ async function loadTreeSitterRuntime() {
16
16
  };
17
17
  return cachedRuntime;
18
18
  }
19
- async function loadTreeSitterModules() {
20
- const dynamicImport = createDynamicImporter();
21
- const [treeSitterModule, csharpModule] = await Promise.all([
22
- dynamicImport("tree-sitter"),
23
- dynamicImport("tree-sitter-c-sharp"),
24
- ]);
19
+ function loadTreeSitterModules() {
20
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
21
+ const nodeRequire = require;
22
+ const [treeSitterModule, csharpModule] = [
23
+ nodeRequire("tree-sitter"),
24
+ nodeRequire("tree-sitter-c-sharp"),
25
+ ];
25
26
  return {
26
27
  parserConstructor: resolveParserConstructor(treeSitterModule),
27
28
  language: resolveGrammar(csharpModule, "tree-sitter-c-sharp"),
28
29
  };
29
30
  }
30
- function createDynamicImporter() {
31
- const importer = new Function("specifier", "return import(specifier);");
32
- return (specifier) => importer(specifier);
33
- }
34
31
  function resolveParserConstructor(moduleValue) {
35
32
  if (typeof moduleValue === "function") {
36
33
  return moduleValue;
@@ -6,7 +6,7 @@ async function loadTreeSitterRuntime() {
6
6
  if (cachedRuntime) {
7
7
  return cachedRuntime;
8
8
  }
9
- const loaded = await loadTreeSitterModules();
9
+ const loaded = loadTreeSitterModules();
10
10
  const parser = new loaded.parserConstructor();
11
11
  parser.setLanguage(loaded.language);
12
12
  cachedRuntime = {
@@ -16,21 +16,18 @@ async function loadTreeSitterRuntime() {
16
16
  };
17
17
  return cachedRuntime;
18
18
  }
19
- async function loadTreeSitterModules() {
20
- const dynamicImport = createDynamicImporter();
21
- const [treeSitterModule, dartModule] = await Promise.all([
22
- dynamicImport("tree-sitter"),
23
- dynamicImport("tree-sitter-dart"),
24
- ]);
19
+ function loadTreeSitterModules() {
20
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
21
+ const nodeRequire = require;
22
+ const [treeSitterModule, dartModule] = [
23
+ nodeRequire("tree-sitter"),
24
+ nodeRequire("tree-sitter-dart"),
25
+ ];
25
26
  return {
26
27
  parserConstructor: resolveParserConstructor(treeSitterModule),
27
28
  language: resolveGrammar(dartModule, "tree-sitter-dart"),
28
29
  };
29
30
  }
30
- function createDynamicImporter() {
31
- const importer = new Function("specifier", "return import(specifier);");
32
- return (specifier) => importer(specifier);
33
- }
34
31
  function resolveParserConstructor(moduleValue) {
35
32
  if (typeof moduleValue === "function") {
36
33
  return moduleValue;
@@ -6,7 +6,7 @@ async function loadTreeSitterRuntime() {
6
6
  if (cachedRuntime) {
7
7
  return cachedRuntime;
8
8
  }
9
- const loaded = await loadTreeSitterModules();
9
+ const loaded = loadTreeSitterModules();
10
10
  const parser = new loaded.parserConstructor();
11
11
  parser.setLanguage(loaded.language);
12
12
  cachedRuntime = {
@@ -16,18 +16,15 @@ async function loadTreeSitterRuntime() {
16
16
  };
17
17
  return cachedRuntime;
18
18
  }
19
- async function loadTreeSitterModules() {
20
- const dynamicImport = createDynamicImporter();
21
- const [treeSitterModule, goModule] = await Promise.all([dynamicImport("tree-sitter"), dynamicImport("tree-sitter-go")]);
19
+ function loadTreeSitterModules() {
20
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
21
+ const nodeRequire = require;
22
+ const [treeSitterModule, goModule] = [nodeRequire("tree-sitter"), nodeRequire("tree-sitter-go")];
22
23
  return {
23
24
  parserConstructor: resolveParserConstructor(treeSitterModule),
24
25
  language: resolveGrammar(goModule, "tree-sitter-go"),
25
26
  };
26
27
  }
27
- function createDynamicImporter() {
28
- const importer = new Function("specifier", "return import(specifier);");
29
- return (specifier) => importer(specifier);
30
- }
31
28
  function resolveParserConstructor(moduleValue) {
32
29
  if (typeof moduleValue === "function") {
33
30
  return moduleValue;
@@ -6,7 +6,7 @@ async function loadTreeSitterRuntime() {
6
6
  if (cachedRuntime) {
7
7
  return cachedRuntime;
8
8
  }
9
- const loaded = await loadTreeSitterModules();
9
+ const loaded = loadTreeSitterModules();
10
10
  const parserByLanguage = new Map();
11
11
  cachedRuntime = {
12
12
  parse(source, language) {
@@ -26,13 +26,14 @@ function getOrCreateParser(language, loaded, parserByLanguage) {
26
26
  parserByLanguage.set(language, parser);
27
27
  return parser;
28
28
  }
29
- async function loadTreeSitterModules() {
30
- const dynamicImport = createDynamicImporter();
31
- const [treeSitterModule, javaModule, kotlinModule] = await Promise.all([
32
- dynamicImport("tree-sitter"),
33
- dynamicImport("tree-sitter-java"),
34
- dynamicImport("tree-sitter-kotlin"),
35
- ]);
29
+ function loadTreeSitterModules() {
30
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
31
+ const nodeRequire = require;
32
+ const [treeSitterModule, javaModule, kotlinModule] = [
33
+ nodeRequire("tree-sitter"),
34
+ nodeRequire("tree-sitter-java"),
35
+ nodeRequire("tree-sitter-kotlin"),
36
+ ];
36
37
  return {
37
38
  parserConstructor: resolveParserConstructor(treeSitterModule),
38
39
  languages: {
@@ -41,10 +42,6 @@ async function loadTreeSitterModules() {
41
42
  },
42
43
  };
43
44
  }
44
- function createDynamicImporter() {
45
- const importer = new Function("specifier", "return import(specifier);");
46
- return (specifier) => importer(specifier);
47
- }
48
45
  function resolveParserConstructor(moduleValue) {
49
46
  if (typeof moduleValue === "function") {
50
47
  return moduleValue;
@@ -11,7 +11,7 @@ async function loadTreeSitterRuntime() {
11
11
  return await runtimeInitPromise;
12
12
  }
13
13
  runtimeInitPromise = (async () => {
14
- const loaded = await loadTreeSitterModules();
14
+ const loaded = loadTreeSitterModules();
15
15
  const parser = new loaded.parserConstructor();
16
16
  parser.setLanguage(loaded.language);
17
17
  const runtime = {
@@ -25,21 +25,18 @@ async function loadTreeSitterRuntime() {
25
25
  })();
26
26
  return await runtimeInitPromise;
27
27
  }
28
- async function loadTreeSitterModules() {
29
- const dynamicImport = createDynamicImporter();
30
- const [treeSitterModule, pythonModule] = await Promise.all([
31
- dynamicImport("tree-sitter"),
32
- dynamicImport("tree-sitter-python"),
33
- ]);
28
+ function loadTreeSitterModules() {
29
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
30
+ const nodeRequire = require;
31
+ const [treeSitterModule, pythonModule] = [
32
+ nodeRequire("tree-sitter"),
33
+ nodeRequire("tree-sitter-python"),
34
+ ];
34
35
  return {
35
36
  parserConstructor: resolveParserConstructor(treeSitterModule),
36
37
  language: resolveGrammar(pythonModule, "tree-sitter-python"),
37
38
  };
38
39
  }
39
- function createDynamicImporter() {
40
- const importer = new Function("specifier", "return import(specifier);");
41
- return (specifier) => importer(specifier);
42
- }
43
40
  function resolveParserConstructor(moduleValue) {
44
41
  if (typeof moduleValue === "function") {
45
42
  return moduleValue;
@@ -12,7 +12,7 @@ async function loadTreeSitterRuntime() {
12
12
  }
13
13
  cachedRuntimePromise = (async () => {
14
14
  try {
15
- const loaded = await loadTreeSitterModules();
15
+ const loaded = loadTreeSitterModules();
16
16
  const parser = new loaded.parserConstructor();
17
17
  parser.setLanguage(loaded.language);
18
18
  const runtime = {
@@ -29,21 +29,18 @@ async function loadTreeSitterRuntime() {
29
29
  })();
30
30
  return await cachedRuntimePromise;
31
31
  }
32
- async function loadTreeSitterModules() {
33
- const dynamicImport = createDynamicImporter();
34
- const [treeSitterModule, rustModule] = await Promise.all([
35
- dynamicImport("tree-sitter"),
36
- dynamicImport("tree-sitter-rust"),
37
- ]);
32
+ function loadTreeSitterModules() {
33
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
34
+ const nodeRequire = require;
35
+ const [treeSitterModule, rustModule] = [
36
+ nodeRequire("tree-sitter"),
37
+ nodeRequire("tree-sitter-rust"),
38
+ ];
38
39
  return {
39
40
  parserConstructor: resolveParserConstructor(treeSitterModule),
40
41
  language: resolveGrammar(rustModule, "tree-sitter-rust"),
41
42
  };
42
43
  }
43
- function createDynamicImporter() {
44
- const importer = new Function("specifier", "return import(specifier);");
45
- return (specifier) => importer(specifier);
46
- }
47
44
  function resolveParserConstructor(moduleValue) {
48
45
  if (typeof moduleValue === "function") {
49
46
  return moduleValue;
@@ -11,7 +11,7 @@ async function loadTreeSitterRuntime() {
11
11
  return await cachedRuntimePromise;
12
12
  }
13
13
  cachedRuntimePromise = (async () => {
14
- const loaded = await loadTreeSitterModules();
14
+ const loaded = loadTreeSitterModules();
15
15
  const parser = new loaded.parserConstructor();
16
16
  parser.setLanguage(loaded.language);
17
17
  const runtime = {
@@ -25,21 +25,18 @@ async function loadTreeSitterRuntime() {
25
25
  })();
26
26
  return await cachedRuntimePromise;
27
27
  }
28
- async function loadTreeSitterModules() {
29
- const dynamicImport = createDynamicImporter();
30
- const [treeSitterModule, swiftModule] = await Promise.all([
31
- dynamicImport("tree-sitter"),
32
- dynamicImport("tree-sitter-swift"),
33
- ]);
28
+ function loadTreeSitterModules() {
29
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
30
+ const nodeRequire = require;
31
+ const [treeSitterModule, swiftModule] = [
32
+ nodeRequire("tree-sitter"),
33
+ nodeRequire("tree-sitter-swift"),
34
+ ];
34
35
  return {
35
36
  parserConstructor: resolveParserConstructor(treeSitterModule),
36
37
  language: resolveGrammar(swiftModule, "tree-sitter-swift"),
37
38
  };
38
39
  }
39
- function createDynamicImporter() {
40
- const importer = new Function("specifier", "return import(specifier);");
41
- return (specifier) => importer(specifier);
42
- }
43
40
  function resolveParserConstructor(moduleValue) {
44
41
  if (typeof moduleValue === "function") {
45
42
  return moduleValue;
@@ -6,7 +6,7 @@ async function loadTreeSitterRuntime() {
6
6
  if (cachedRuntime) {
7
7
  return cachedRuntime;
8
8
  }
9
- const loaded = await loadTreeSitterModules();
9
+ const loaded = loadTreeSitterModules();
10
10
  const parserByLanguage = new Map();
11
11
  cachedRuntime = {
12
12
  parse(source, language) {
@@ -27,13 +27,12 @@ function getOrCreateParser(language, loaded, parserByLanguage) {
27
27
  parserByLanguage.set(language, parser);
28
28
  return parser;
29
29
  }
30
- async function loadTreeSitterModules() {
31
- const dynamicImport = createDynamicImporter();
32
- const [treeSitterModule, typeScriptModule, javaScriptModule] = await Promise.all([
33
- dynamicImport("tree-sitter"),
34
- dynamicImport("@tree-sitter/typescript"),
35
- dynamicImport("@tree-sitter/javascript"),
36
- ]);
30
+ function loadTreeSitterModules() {
31
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
32
+ const nodeRequire = require;
33
+ const treeSitterModule = nodeRequire("tree-sitter");
34
+ const typeScriptModule = nodeRequire("tree-sitter-typescript");
35
+ const javaScriptModule = nodeRequire("tree-sitter-javascript");
37
36
  const parserConstructor = resolveParserConstructor(treeSitterModule);
38
37
  const typeScriptGrammar = resolveTypeScriptGrammar(typeScriptModule);
39
38
  const javaScriptGrammar = resolveJavaScriptGrammar(javaScriptModule);
@@ -45,10 +44,6 @@ async function loadTreeSitterModules() {
45
44
  },
46
45
  };
47
46
  }
48
- function createDynamicImporter() {
49
- const importer = new Function("specifier", "return import(specifier);");
50
- return (specifier) => importer(specifier);
51
- }
52
47
  function resolveParserConstructor(moduleValue) {
53
48
  if (typeof moduleValue === "function") {
54
49
  return moduleValue;
@@ -86,6 +81,10 @@ function resolveJavaScriptGrammar(moduleValue) {
86
81
  if (direct.javascript) {
87
82
  return direct.javascript;
88
83
  }
84
+ // tree-sitter-javascript exports { name, language, nodeTypeInfo } — pass the whole module
85
+ if (direct.name) {
86
+ return moduleValue;
87
+ }
89
88
  if (direct.default && typeof direct.default === "object" && "javascript" in direct.default) {
90
89
  return direct.default.javascript;
91
90
  }
@@ -93,5 +92,5 @@ function resolveJavaScriptGrammar(moduleValue) {
93
92
  return direct.default;
94
93
  }
95
94
  }
96
- throw new Error("Unable to resolve @tree-sitter/javascript grammar.");
95
+ throw new Error("Unable to resolve tree-sitter-javascript grammar.");
97
96
  }
@@ -12,6 +12,7 @@ const seed_instructions_js_1 = require("./seed-instructions.js");
12
12
  const validate_instructions_js_1 = require("./validate-instructions.js");
13
13
  const doctrine_js_1 = require("./doctrine.js");
14
14
  const audit_js_1 = require("./audit.js");
15
+ const triage_js_1 = require("./triage.js");
15
16
  /**
16
17
  * Build and return the top-level "docs" Commander command group for Polaris docs lifecycle workflows.
17
18
  *
@@ -151,6 +152,27 @@ function createDocsCommand(options = {}) {
151
152
  process.exit(1);
152
153
  }
153
154
  });
155
+ docs
156
+ .command("triage")
157
+ .description("Detect contradictions, duplicates, and stale code references among candidate docs")
158
+ .option("-r, --repo-root <path>", "Repository root", process.cwd())
159
+ .option("--batch-size <n>", "Docs per LLM call", "10")
160
+ .option("--resume", "Resume from last checkpoint (auto-detected by default)")
161
+ .option("--dry-run", "Plan batches and print cost estimate without calling the LLM")
162
+ .action(async (options) => {
163
+ try {
164
+ await (0, triage_js_1.runTriage)({
165
+ repoRoot: options.repoRoot,
166
+ batchSize: parseInt(options.batchSize, 10) || 10,
167
+ resume: options.resume,
168
+ dryRun: options.dryRun,
169
+ });
170
+ }
171
+ catch (err) {
172
+ console.error(`polaris docs triage: ${err instanceof Error ? err.message : String(err)}`);
173
+ process.exit(1);
174
+ }
175
+ });
154
176
  docs
155
177
  .command("migrate")
156
178
  .description("Find scattered markdown files, move them to smartdocs/raw/, and produce an ingest cluster list")
@@ -0,0 +1,494 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.clusterCandidates = clusterCandidates;
4
+ exports.extractSymbols = extractSymbols;
5
+ exports.loadDocMeta = loadDocMeta;
6
+ exports.loadAllDocMeta = loadAllDocMeta;
7
+ exports.readTriageCheckpoint = readTriageCheckpoint;
8
+ exports.writeTriageCheckpoint = writeTriageCheckpoint;
9
+ exports.deleteTriageCheckpoint = deleteTriageCheckpoint;
10
+ exports.writeTriageQueue = writeTriageQueue;
11
+ exports.resolveTriageModel = resolveTriageModel;
12
+ exports.runBatchComparison = runBatchComparison;
13
+ exports.runGraphCheck = runGraphCheck;
14
+ exports.runTriage = runTriage;
15
+ const node_fs_1 = require("node:fs");
16
+ const node_path_1 = require("node:path");
17
+ // ---------------------------------------------------------------------------
18
+ // clusterCandidates
19
+ // ---------------------------------------------------------------------------
20
+ /**
21
+ * Groups candidate docs into named clusters using metadata overlap with canonicals.
22
+ * Candidates with no signal match go into the "general" bucket.
23
+ */
24
+ function clusterCandidates(candidates, canonicals) {
25
+ const clusters = {};
26
+ // Build cluster names from canonicals
27
+ for (const canonical of canonicals) {
28
+ const names = clusterNamesFor(canonical);
29
+ for (const name of names) {
30
+ if (!clusters[name]) {
31
+ clusters[name] = { candidates: [], canonicals: [] };
32
+ }
33
+ if (!clusters[name].canonicals.includes(canonical)) {
34
+ clusters[name].canonicals.push(canonical);
35
+ }
36
+ }
37
+ }
38
+ // Assign each candidate to its best cluster
39
+ for (const candidate of candidates) {
40
+ const candidateNames = clusterNamesFor(candidate);
41
+ let assigned = false;
42
+ for (const name of candidateNames) {
43
+ if (clusters[name]) {
44
+ clusters[name].candidates.push(candidate);
45
+ assigned = true;
46
+ break; // assign to first matching cluster only — multi-cluster candidates would inflate batch sizes
47
+ }
48
+ }
49
+ if (!assigned) {
50
+ if (!clusters["general"]) {
51
+ clusters["general"] = { candidates: [], canonicals: [] };
52
+ }
53
+ clusters["general"].candidates.push(candidate);
54
+ }
55
+ }
56
+ // Ensure general bucket exists
57
+ if (!clusters["general"]) {
58
+ clusters["general"] = { candidates: [], canonicals: [] };
59
+ }
60
+ return clusters;
61
+ }
62
+ function clusterNamesFor(doc) {
63
+ const names = [];
64
+ // Tags take priority
65
+ for (const tag of doc.tags) {
66
+ if (tag.trim())
67
+ names.push(tag.toLowerCase().trim());
68
+ }
69
+ // Cluster membership
70
+ for (const c of doc.clusterMembership) {
71
+ if (c.trim())
72
+ names.push(c.toLowerCase().trim());
73
+ }
74
+ // Filename prefix (e.g. "ADR", "EVOlearn")
75
+ for (const p of doc.filenamePrefixes) {
76
+ if (p.trim())
77
+ names.push(p.toLowerCase().trim());
78
+ }
79
+ // Type as fallback
80
+ if (doc.type.trim())
81
+ names.push(doc.type.toLowerCase().trim());
82
+ return names;
83
+ }
84
+ // ---------------------------------------------------------------------------
85
+ // extractSymbols
86
+ // ---------------------------------------------------------------------------
87
+ const BACKTICK_RE = /`([A-Za-z_][A-Za-z0-9_]{3,})`/g;
88
+ const CAMEL_PASCAL_RE = /(?<![`\w])([A-Z][a-z]+(?:[A-Z][a-z]+)+|[a-z][a-z]+(?:[A-Z][a-z0-9]+)+)(?![`\w])/g;
89
+ /**
90
+ * Extracts likely code symbol names from markdown text.
91
+ * Returns deduplicated symbol names of 4+ characters.
92
+ */
93
+ function extractSymbols(text) {
94
+ const found = new Set();
95
+ let m;
96
+ BACKTICK_RE.lastIndex = 0;
97
+ while ((m = BACKTICK_RE.exec(text)) !== null) {
98
+ found.add(m[1]);
99
+ }
100
+ CAMEL_PASCAL_RE.lastIndex = 0;
101
+ while ((m = CAMEL_PASCAL_RE.exec(text)) !== null) {
102
+ found.add(m[1]);
103
+ }
104
+ return Array.from(found);
105
+ }
106
+ // ---------------------------------------------------------------------------
107
+ // DocMeta loader
108
+ // ---------------------------------------------------------------------------
109
+ /**
110
+ * Parses YAML frontmatter from a markdown file and returns DocMeta.
111
+ * Does not throw — returns empty arrays on any parse failure.
112
+ */
113
+ function loadDocMeta(filePath) {
114
+ let content = "";
115
+ try {
116
+ content = (0, node_fs_1.readFileSync)(filePath, "utf-8");
117
+ }
118
+ catch {
119
+ return emptyMeta(filePath);
120
+ }
121
+ const frontmatter = parseFrontmatter(content);
122
+ const filenamePrefixes = extractFilenamePrefixes((0, node_path_1.basename)(filePath, (0, node_path_1.extname)(filePath)));
123
+ return {
124
+ path: filePath,
125
+ tags: toStringArray(frontmatter["Tags"] ?? frontmatter["tags"]),
126
+ type: String(frontmatter["Type"] ?? frontmatter["type"] ?? "").trim(),
127
+ clusterMembership: toStringArray(frontmatter["Member Of Concept Cluster"]),
128
+ relatedNotes: toStringArray(frontmatter["Related Notes"]),
129
+ filenamePrefixes,
130
+ };
131
+ }
132
+ /**
133
+ * Reads all .md files from a directory and returns their DocMeta.
134
+ */
135
+ function loadAllDocMeta(dir) {
136
+ let entries = [];
137
+ try {
138
+ entries = (0, node_fs_1.readdirSync)(dir).filter((f) => f.endsWith(".md"));
139
+ }
140
+ catch {
141
+ return [];
142
+ }
143
+ return entries.map((f) => loadDocMeta((0, node_path_1.join)(dir, f)));
144
+ }
145
+ function emptyMeta(filePath) {
146
+ return {
147
+ path: filePath,
148
+ tags: [],
149
+ type: "",
150
+ clusterMembership: [],
151
+ relatedNotes: [],
152
+ filenamePrefixes: extractFilenamePrefixes((0, node_path_1.basename)(filePath, (0, node_path_1.extname)(filePath))),
153
+ };
154
+ }
155
+ function parseFrontmatter(content) {
156
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
157
+ if (!match)
158
+ return {};
159
+ const result = {};
160
+ const lines = match[1].split("\n");
161
+ let currentKey = null;
162
+ const listBuffer = [];
163
+ const flushList = () => {
164
+ if (currentKey && listBuffer.length > 0) {
165
+ result[currentKey] = [...listBuffer];
166
+ listBuffer.length = 0;
167
+ }
168
+ };
169
+ for (const line of lines) {
170
+ // Inline array: Tags: [a, b]
171
+ const inlineMatch = line.match(/^([^:]+):\s*\[(.*)\]$/);
172
+ if (inlineMatch) {
173
+ flushList();
174
+ currentKey = null;
175
+ const key = inlineMatch[1].trim();
176
+ result[key] = inlineMatch[2]
177
+ .split(",")
178
+ .map((s) => s.trim().replace(/^["']|["']$/g, ""))
179
+ .filter(Boolean);
180
+ continue;
181
+ }
182
+ // Key with value: Type: Decision
183
+ const kvMatch = line.match(/^([^:]+):\s*(.+)$/);
184
+ if (kvMatch && !line.startsWith(" ")) {
185
+ flushList();
186
+ currentKey = kvMatch[1].trim();
187
+ const val = kvMatch[2].trim();
188
+ if (val !== "") {
189
+ result[currentKey] = val.replace(/^["']|["']$/g, "");
190
+ currentKey = null;
191
+ }
192
+ continue;
193
+ }
194
+ // Key with no inline value (list follows)
195
+ const keyOnlyMatch = line.match(/^([^:]+):\s*$/);
196
+ if (keyOnlyMatch && !line.startsWith(" ")) {
197
+ flushList();
198
+ currentKey = keyOnlyMatch[1].trim();
199
+ continue;
200
+ }
201
+ // List item — wikilink style: - "[[ADR-002|ADR-002]]"
202
+ const listItemMatch = line.match(/^\s+-\s+"?\[\[([^\]|]+)(?:\|[^\]]+)?\]\]"?$/);
203
+ if (listItemMatch && currentKey) {
204
+ listBuffer.push(listItemMatch[1].trim());
205
+ continue;
206
+ }
207
+ // List item — plain
208
+ const simpleItemMatch = line.match(/^\s+-\s+(.+)$/);
209
+ if (simpleItemMatch && currentKey) {
210
+ listBuffer.push(simpleItemMatch[1].trim().replace(/^["']|["']$/g, ""));
211
+ }
212
+ }
213
+ flushList();
214
+ return result;
215
+ }
216
+ function toStringArray(value) {
217
+ if (!value)
218
+ return [];
219
+ if (Array.isArray(value))
220
+ return value.map(String).filter(Boolean);
221
+ if (typeof value === "string" && value.trim())
222
+ return [value.trim()];
223
+ return [];
224
+ }
225
+ function extractFilenamePrefixes(stem) {
226
+ // "ADR-001 - Some Title" → ["ADR"]
227
+ // "EVOlearn_Governance" → ["EVOlearn"]
228
+ const parts = stem.split(/[-_ ]/);
229
+ return parts.slice(0, 2).filter((p) => p.length >= 3 && /[A-Za-z]/.test(p));
230
+ }
231
+ const CHECKPOINT_FILENAME = "_triage-checkpoint.json";
232
+ function readTriageCheckpoint(outputDir) {
233
+ const p = (0, node_path_1.join)(outputDir, CHECKPOINT_FILENAME);
234
+ if (!(0, node_fs_1.existsSync)(p))
235
+ return null;
236
+ try {
237
+ return JSON.parse((0, node_fs_1.readFileSync)(p, "utf-8"));
238
+ }
239
+ catch {
240
+ return null;
241
+ }
242
+ }
243
+ function writeTriageCheckpoint(checkpoint, outputDir) {
244
+ (0, node_fs_1.mkdirSync)(outputDir, { recursive: true });
245
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(outputDir, CHECKPOINT_FILENAME), JSON.stringify(checkpoint, null, 2), "utf-8");
246
+ }
247
+ function deleteTriageCheckpoint(outputDir) {
248
+ const p = (0, node_path_1.join)(outputDir, CHECKPOINT_FILENAME);
249
+ if ((0, node_fs_1.existsSync)(p))
250
+ (0, node_fs_1.unlinkSync)(p);
251
+ }
252
+ // ---------------------------------------------------------------------------
253
+ // Triage queue writer
254
+ // ---------------------------------------------------------------------------
255
+ function writeTriageQueue(packets, outputDir) {
256
+ (0, node_fs_1.mkdirSync)(outputDir, { recursive: true });
257
+ const queueFile = {
258
+ generated_at: new Date().toISOString(),
259
+ run_id: `triage-${Date.now()}`,
260
+ packets,
261
+ };
262
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(outputDir, "_triage-queue.json"), JSON.stringify(queueFile, null, 2), "utf-8");
263
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(outputDir, "_triage-report.md"), renderTriageReport(packets), "utf-8");
264
+ }
265
+ function renderTriageReport(packets) {
266
+ const contradictions = packets.filter((p) => p.triageFlag === "contradiction");
267
+ const duplicates = packets.filter((p) => p.triageFlag === "duplicate");
268
+ const stale = packets.filter((p) => p.triageFlag === "stale-reference");
269
+ const lines = [
270
+ "# Polaris Docs Triage Report",
271
+ "",
272
+ `Generated: ${new Date().toISOString()}`,
273
+ "",
274
+ "## Summary",
275
+ "",
276
+ `| Flag | Count |`,
277
+ `|------|-------|`,
278
+ `| contradiction | ${contradictions.length} |`,
279
+ `| duplicate | ${duplicates.length} |`,
280
+ `| stale-reference | ${stale.length} |`,
281
+ `| **total** | **${packets.length}** |`,
282
+ "",
283
+ "## Flagged Documents",
284
+ "",
285
+ "This file is display-only. Edit `_triage-queue.json` to record decisions,",
286
+ "or run `polaris docs review` to walk through them interactively.",
287
+ "",
288
+ ];
289
+ for (const p of packets) {
290
+ lines.push(`### ${p.triageFlag.toUpperCase()} · ${p.sourcePath}`);
291
+ lines.push("");
292
+ lines.push(`**Reason:** ${p.outcomeReason}`);
293
+ if (p.relatedCanonical) {
294
+ lines.push(`**Conflicts with:** ${p.relatedCanonical}`);
295
+ }
296
+ if (p.staleSymbols && p.staleSymbols.length > 0) {
297
+ lines.push(`**Stale symbols:** ${p.staleSymbols.join(", ")}`);
298
+ }
299
+ lines.push(`**Review decision:** ${p.reviewDecision ?? "← pending"}`);
300
+ lines.push("");
301
+ }
302
+ return lines.join("\n");
303
+ }
304
+ function resolveTriageModel(configured) {
305
+ return (process.env["POLARIS_TRIAGE_MODEL"] ??
306
+ configured ??
307
+ "claude-haiku-4-5-20251001");
308
+ }
309
+ async function runBatchComparison(candidates, canonicals, options) {
310
+ const client = options.llmClient ?? (await buildDefaultLlmClient());
311
+ for (let attempt = 0; attempt < 2; attempt++) {
312
+ try {
313
+ return await client.compare(candidates, canonicals, options.model);
314
+ }
315
+ catch (err) {
316
+ process.stderr.write(`[triage] LLM batch failed (attempt ${attempt + 1})${attempt === 0 ? ", retrying" : ", skipping batch"}: ${err instanceof Error ? err.message : String(err)}\n`);
317
+ }
318
+ }
319
+ return [];
320
+ }
321
+ async function buildDefaultLlmClient() {
322
+ const apiKey = process.env["ANTHROPIC_API_KEY"] ?? "";
323
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
324
+ // @ts-ignore: @anthropic-ai/sdk may not be installed as a dependency
325
+ const mod = await import("@anthropic-ai/sdk");
326
+ const Anthropic = mod.default ?? mod.Anthropic;
327
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
328
+ const sdkClient = new Anthropic({ apiKey });
329
+ return {
330
+ async compare(candidates, canonicals, model) {
331
+ const candidateSummaries = candidates
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");
351
+ const response = await sdkClient.messages.create({
352
+ model,
353
+ max_tokens: 1024,
354
+ messages: [{ role: "user", content: prompt }],
355
+ });
356
+ const text = response.content
357
+ .filter((b) => b.type === "text")
358
+ .map((b) => b.text)
359
+ .join("");
360
+ const cleaned = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
361
+ return JSON.parse(cleaned);
362
+ },
363
+ };
364
+ }
365
+ const STALE_SYMBOL_THRESHOLD = 2;
366
+ const GRAPH_SYMBOL_MINIMUM = 1000;
367
+ function runGraphCheck(candidates, options) {
368
+ const stats = options.graphStats();
369
+ if (stats.symbolCount < GRAPH_SYMBOL_MINIMUM) {
370
+ process.stderr.write(`[triage] Graph coverage too low for doc-vs-code check (${stats.symbolCount} symbols indexed).\n` +
371
+ ` Run: polaris-cli graph build\n`);
372
+ return [];
373
+ }
374
+ const flags = [];
375
+ for (const candidate of candidates) {
376
+ let content = "";
377
+ try {
378
+ content = options.getContent(candidate.path);
379
+ }
380
+ catch {
381
+ continue;
382
+ }
383
+ const symbols = extractSymbols(content);
384
+ const missing = symbols.filter((s) => !options.symbolLookup(s));
385
+ if (missing.length >= STALE_SYMBOL_THRESHOLD) {
386
+ flags.push({
387
+ candidatePath: candidate.path,
388
+ flagType: "stale-reference",
389
+ staleSymbols: missing,
390
+ });
391
+ }
392
+ }
393
+ return flags;
394
+ }
395
+ async function runTriage(options) {
396
+ const { repoRoot, batchSize = 10, dryRun = false, output = (m) => process.stdout.write(m + "\n"), llmClient, symbolLookup, graphStats, } = options;
397
+ const activeDir = (0, node_path_1.join)(repoRoot, "smartdocs", "doctrine", "active");
398
+ const candidateDir = (0, node_path_1.join)(repoRoot, "smartdocs", "doctrine", "candidate");
399
+ const outputDir = (0, node_path_1.join)(repoRoot, "smartdocs", "raw");
400
+ output(`Loading canonical docs from ${activeDir}...`);
401
+ const canonicals = loadAllDocMeta(activeDir);
402
+ output(`Loading candidate docs from ${candidateDir}...`);
403
+ const candidates = loadAllDocMeta(candidateDir);
404
+ output(`Clustering ${candidates.length} candidates against ${canonicals.length} canonicals...`);
405
+ const clusterMap = clusterCandidates(candidates, canonicals);
406
+ const clusterNames = Object.keys(clusterMap).filter((k) => k !== "general");
407
+ if (clusterMap["general"])
408
+ clusterNames.push("general");
409
+ if (dryRun) {
410
+ const batchCount = clusterNames.reduce((sum, name) => {
411
+ return sum + Math.ceil((clusterMap[name].candidates.length || 0) / batchSize);
412
+ }, 0);
413
+ output(` ${clusterNames.length} clusters identified`);
414
+ output(` Estimated batches: ${batchCount}`);
415
+ output(` Estimated tokens: ~${batchCount * 3000} input / ~${batchCount * 200} output`);
416
+ output(` Model: ${resolveTriageModel(options.model)} (configured)`);
417
+ output(`\nRun without --dry-run to execute.`);
418
+ return { flagCount: 0, outputDir };
419
+ }
420
+ let checkpoint = readTriageCheckpoint(outputDir);
421
+ const completedClusters = new Set(checkpoint?.completedClusters ?? []);
422
+ const accumulatedFlags = [...(checkpoint?.flags ?? [])];
423
+ if (completedClusters.size > 0) {
424
+ output(`Resuming triage from checkpoint (${completedClusters.size}/${clusterNames.length} clusters complete)...`);
425
+ }
426
+ const model = resolveTriageModel(options.model);
427
+ // Phase 1: doc-vs-doc
428
+ for (const clusterName of clusterNames) {
429
+ if (completedClusters.has(clusterName))
430
+ continue;
431
+ const cluster = clusterMap[clusterName];
432
+ if (cluster.candidates.length === 0) {
433
+ completedClusters.add(clusterName);
434
+ continue;
435
+ }
436
+ output(` Comparing cluster "${clusterName}" (${cluster.candidates.length} candidates, ${cluster.canonicals.length} canonicals)...`);
437
+ for (let i = 0; i < cluster.candidates.length; i += batchSize) {
438
+ const batch = cluster.candidates.slice(i, i + batchSize);
439
+ const flags = await runBatchComparison(batch, cluster.canonicals, { model, llmClient });
440
+ accumulatedFlags.push(...flags);
441
+ }
442
+ completedClusters.add(clusterName);
443
+ writeTriageCheckpoint({ completedClusters: Array.from(completedClusters), flags: accumulatedFlags }, outputDir);
444
+ }
445
+ // Phase 2: doc-vs-code
446
+ output(`Running doc-vs-code graph check...`);
447
+ const graphCheckOptions = {
448
+ getContent: (p) => (0, node_fs_1.readFileSync)(p, "utf-8"),
449
+ symbolLookup: symbolLookup ?? ((name) => {
450
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
451
+ const { lookupSymbol } = require("../graph/query/index.js");
452
+ return lookupSymbol(name) !== null;
453
+ }),
454
+ graphStats: graphStats ?? (() => {
455
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
456
+ const { getGraphStats } = require("../graph/query/index.js");
457
+ return getGraphStats();
458
+ }),
459
+ };
460
+ const graphFlags = runGraphCheck(candidates, graphCheckOptions);
461
+ const allPackets = [
462
+ ...accumulatedFlags.map((f) => ({
463
+ sourcePath: f.candidatePath,
464
+ proposedDestination: f.candidatePath,
465
+ classificationConfidence: 0,
466
+ destinationCertainty: 0,
467
+ authorityRisk: "medium",
468
+ reasoning: [f.reason],
469
+ conflicts: f.canonicalPath ? [f.canonicalPath] : [],
470
+ recommendation: "defer",
471
+ outcomeReason: f.reason,
472
+ triageFlag: f.flagType,
473
+ relatedCanonical: f.canonicalPath,
474
+ })),
475
+ ...graphFlags.map((f) => ({
476
+ sourcePath: f.candidatePath,
477
+ proposedDestination: f.candidatePath,
478
+ classificationConfidence: 0,
479
+ destinationCertainty: 0,
480
+ authorityRisk: "low",
481
+ reasoning: [`Stale symbol references: ${f.staleSymbols.join(", ")}`],
482
+ conflicts: [],
483
+ recommendation: "defer",
484
+ outcomeReason: `${f.staleSymbols.length} code symbols not found in graph: ${f.staleSymbols.join(", ")}`,
485
+ triageFlag: "stale-reference",
486
+ staleSymbols: f.staleSymbols,
487
+ })),
488
+ ];
489
+ writeTriageQueue(allPackets, outputDir);
490
+ deleteTriageCheckpoint(outputDir);
491
+ output(`\nTriage complete: ${allPackets.length} flags written to ${outputDir}/_triage-queue.json`);
492
+ output(`Run \`polaris docs review\` to walk through decisions.`);
493
+ return { flagCount: allPackets.length, outputDir };
494
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lsctech/polaris",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Polaris — standalone taskchain orchestration framework for governed AI agent workflows",
5
5
  "bin": {
6
6
  "polaris-cli": "dist/cli/index.js"
@@ -54,6 +54,17 @@
54
54
  "@modelcontextprotocol/sdk": "^1.29.0",
55
55
  "commander": "^12.0.0",
56
56
  "ignore": "^6.0.2",
57
+ "tree-sitter": "^0.22.0",
58
+ "tree-sitter-c": "^0.24.1",
59
+ "tree-sitter-c-sharp": "^0.23.5",
60
+ "tree-sitter-cpp": "^0.23.4",
61
+ "tree-sitter-dart": "^1.0.0",
62
+ "tree-sitter-go": "^0.25.0",
63
+ "tree-sitter-javascript": "^0.23.1",
64
+ "tree-sitter-python": "^0.23.6",
65
+ "tree-sitter-rust": "^0.24.0",
66
+ "tree-sitter-swift": "^0.7.1",
67
+ "tree-sitter-typescript": "^0.23.2",
57
68
  "zod": "^3.23.8"
58
69
  },
59
70
  "devDependencies": {