@lsctech/polaris 0.5.11 → 0.5.13
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/dist/autoresearch/proposal.js +61 -0
- package/dist/autoresearch/score.js +124 -0
- package/dist/cli/adopt-canon.js +22 -8
- package/dist/cli/index.js +4 -0
- package/dist/cli/qc.js +97 -0
- package/dist/config/validator.js +16 -6
- package/dist/finalize/artifact-policy.js +12 -3
- package/dist/finalize/index.js +36 -21
- package/dist/finalize/run-report.js +189 -19
- package/dist/finalize/steps/05-generate-report.js +5 -1
- package/dist/finalize/steps/12-archive.js +6 -0
- package/dist/loop/adapters/terminal-cli.js +159 -25
- package/dist/loop/body-parser.js +73 -6
- package/dist/loop/continue.js +22 -4
- package/dist/loop/dispatch.js +103 -68
- package/dist/loop/orphan-recovery.js +2 -1
- package/dist/loop/parent.js +50 -15
- package/dist/loop/router/engine.js +1 -0
- package/dist/loop/run-bootstrap.js +3 -1
- package/dist/loop/run-preflight.js +4 -1
- package/dist/loop/worker-packet.js +81 -2
- package/dist/map/inference.js +4 -1
- package/dist/medic/routing-signals.js +60 -0
- package/dist/medic/run-health-consult.js +5 -4
- package/dist/medic/treatment-packets.js +5 -1
- package/dist/qc/policy.js +2 -0
- package/dist/qc/providers/coderabbit.js +140 -10
- package/dist/qc/repair-loop.js +147 -1
- package/dist/qc/repair-packets.js +16 -3
- package/dist/qc/routing.js +6 -0
- package/dist/qc/types.js +3 -0
- package/dist/run-health/index.js +12 -0
- package/dist/skill-packet/generator.js +234 -3
- package/dist/smartdocs-engine/canon-check.js +5 -5
- package/dist/smartdocs-engine/index.js +54 -0
- package/dist/smartdocs-engine/seed-instructions.js +159 -4
- package/dist/smartdocs-engine/validate-instructions.js +46 -4
- package/dist/tracker/local-graph.js +106 -3
- package/dist/workspace/POLARIS.md +4 -0
- package/dist/workspace/SUMMARY.md +56 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DRAFT_MARKER = void 0;
|
|
3
|
+
exports.TEMPLATE_VERSION_STAMP = exports.TEMPLATE_VERSION = exports.GENERATED_END_MARKER = exports.GENERATED_START_MARKER = exports.DRAFT_MARKER = void 0;
|
|
4
4
|
exports.hasDraftMarker = hasDraftMarker;
|
|
5
5
|
exports.generateDraft = generateDraft;
|
|
6
6
|
exports.generateSummaryDraft = generateSummaryDraft;
|
|
@@ -12,6 +12,7 @@ exports.seedInstructionsAll = seedInstructionsAll;
|
|
|
12
12
|
exports.seedSummaryAll = seedSummaryAll;
|
|
13
13
|
exports.seedIndex = seedIndex;
|
|
14
14
|
exports.seedIndexAll = seedIndexAll;
|
|
15
|
+
exports.reconcileAll = reconcileAll;
|
|
15
16
|
const node_fs_1 = require("node:fs");
|
|
16
17
|
const node_path_1 = require("node:path");
|
|
17
18
|
const loader_js_1 = require("../config/loader.js");
|
|
@@ -19,6 +20,10 @@ const atlas_js_1 = require("../map/atlas.js");
|
|
|
19
20
|
const smartdoc_ignore_js_1 = require("./smartdoc-ignore.js");
|
|
20
21
|
const doctrine_js_1 = require("./doctrine.js");
|
|
21
22
|
exports.DRAFT_MARKER = "<!-- polaris:draft -->";
|
|
23
|
+
exports.GENERATED_START_MARKER = "<!-- BEGIN POLARIS GENERATED -->";
|
|
24
|
+
exports.GENERATED_END_MARKER = "<!-- END POLARIS GENERATED -->";
|
|
25
|
+
exports.TEMPLATE_VERSION = 1;
|
|
26
|
+
exports.TEMPLATE_VERSION_STAMP = `<!-- polaris:template-version: ${exports.TEMPLATE_VERSION} -->`;
|
|
22
27
|
function hasDraftMarker(filePath) {
|
|
23
28
|
try {
|
|
24
29
|
const content = (0, node_fs_1.readFileSync)(filePath, "utf-8");
|
|
@@ -121,7 +126,7 @@ function generateDraft(targetDir, repoRoot, allRoutes) {
|
|
|
121
126
|
lines.push("## Related routes", "");
|
|
122
127
|
lines.push("<!-- Atlas route pointer to sibling or parent folders. -->");
|
|
123
128
|
lines.push("");
|
|
124
|
-
return lines.join("\n");
|
|
129
|
+
return [exports.DRAFT_MARKER, exports.GENERATED_START_MARKER, exports.TEMPLATE_VERSION_STAMP, ...lines.slice(1), exports.GENERATED_END_MARKER].join("\n");
|
|
125
130
|
}
|
|
126
131
|
function generateSummaryDraft(targetDir, repoRoot, _allRoutes) {
|
|
127
132
|
const dirLabel = (0, node_path_1.basename)(targetDir) || (0, node_path_1.basename)(repoRoot);
|
|
@@ -172,14 +177,51 @@ function generateSummaryDraft(targetDir, repoRoot, _allRoutes) {
|
|
|
172
177
|
"```yaml",
|
|
173
178
|
"canonical_docs:",
|
|
174
179
|
" - POLARIS.md",
|
|
175
|
-
"<!-- Add navigation paths to canonical docs, specs, or doctrine. These are retrieval paths, not reading assignments. -->",
|
|
176
180
|
"```",
|
|
177
181
|
"",
|
|
182
|
+
"<!-- Add navigation paths to canonical docs, specs, or doctrine. These are retrieval paths, not reading assignments. -->",
|
|
183
|
+
"",
|
|
178
184
|
"## Known Drift",
|
|
179
185
|
"<!-- Places where the summary may be stale (honesty field). -->",
|
|
180
186
|
"",
|
|
181
187
|
];
|
|
182
|
-
return lines.join("\n");
|
|
188
|
+
return [exports.DRAFT_MARKER, exports.GENERATED_START_MARKER, exports.TEMPLATE_VERSION_STAMP, ...lines.slice(1), exports.GENERATED_END_MARKER].join("\n");
|
|
189
|
+
}
|
|
190
|
+
function generateDraftRegion(targetDir, repoRoot, allRoutes) {
|
|
191
|
+
const full = generateDraft(targetDir, repoRoot, allRoutes);
|
|
192
|
+
const lines = full.split("\n");
|
|
193
|
+
// Remove the DRAFT marker, GENERATED_START_MARKER, and GENERATED_END_MARKER;
|
|
194
|
+
// the remaining lines are the region body, including the template-version stamp.
|
|
195
|
+
return lines.slice(2, -1).join("\n");
|
|
196
|
+
}
|
|
197
|
+
function generateSummaryDraftRegion(targetDir, repoRoot, allRoutes) {
|
|
198
|
+
const full = generateSummaryDraft(targetDir, repoRoot, allRoutes);
|
|
199
|
+
const lines = full.split("\n");
|
|
200
|
+
return lines.slice(2, -1).join("\n");
|
|
201
|
+
}
|
|
202
|
+
const TEMPLATE_VERSION_RE = /^<!-- polaris:template-version:\s*(\d+)\s*-->/;
|
|
203
|
+
function getGeneratedRegionVersion(content) {
|
|
204
|
+
const startIdx = content.indexOf(exports.GENERATED_START_MARKER);
|
|
205
|
+
if (startIdx === -1)
|
|
206
|
+
return null;
|
|
207
|
+
const startLineEnd = content.indexOf("\n", startIdx + exports.GENERATED_START_MARKER.length);
|
|
208
|
+
if (startLineEnd === -1)
|
|
209
|
+
return null;
|
|
210
|
+
const endIdx = content.indexOf(exports.GENERATED_END_MARKER, startLineEnd + 1);
|
|
211
|
+
if (endIdx === -1)
|
|
212
|
+
return null;
|
|
213
|
+
const region = content.slice(startLineEnd + 1, endIdx);
|
|
214
|
+
const firstLine = region.split("\n")[0] ?? "";
|
|
215
|
+
const match = TEMPLATE_VERSION_RE.exec(firstLine);
|
|
216
|
+
return match ? parseInt(match[1], 10) : undefined;
|
|
217
|
+
}
|
|
218
|
+
function replaceGeneratedRegion(content, newRegionBody) {
|
|
219
|
+
const startIdx = content.indexOf(exports.GENERATED_START_MARKER);
|
|
220
|
+
const startLineEnd = content.indexOf("\n", startIdx + exports.GENERATED_START_MARKER.length);
|
|
221
|
+
const endIdx = content.indexOf(exports.GENERATED_END_MARKER, startLineEnd + 1);
|
|
222
|
+
const before = content.slice(0, startLineEnd + 1);
|
|
223
|
+
const after = content.slice(endIdx);
|
|
224
|
+
return before + newRegionBody + "\n" + after;
|
|
183
225
|
}
|
|
184
226
|
const RESERVED_INDEX_NAMES = new Set(["index.md", "POLARIS.md", "SUMMARY.md", "log.md"]);
|
|
185
227
|
function isReservedIndexName(name) {
|
|
@@ -569,3 +611,116 @@ function seedIndexAll(repoRoot, opts = {}) {
|
|
|
569
611
|
}
|
|
570
612
|
return { written, skippedExists, skippedDraft, skippedIneligible: [] };
|
|
571
613
|
}
|
|
614
|
+
function reconcileFile(relDir, fileName, repoRoot, allRoutes, dryRun, generateFull, generateRegion) {
|
|
615
|
+
const absDir = (0, node_path_1.resolve)(repoRoot, relDir);
|
|
616
|
+
const relCheck = (0, node_path_1.relative)(repoRoot, absDir).replace(/\\/g, "/");
|
|
617
|
+
if (relCheck.startsWith("..") || relCheck.startsWith("/")) {
|
|
618
|
+
throw new Error(`Path traversal detected: target path is outside repo root`);
|
|
619
|
+
}
|
|
620
|
+
const outFile = (0, node_path_1.join)(absDir, fileName);
|
|
621
|
+
const relPath = (0, node_path_1.relative)(repoRoot, outFile).replace(/\\/g, "/");
|
|
622
|
+
if (!(0, node_fs_1.existsSync)(outFile)) {
|
|
623
|
+
const content = generateFull(relDir, repoRoot, allRoutes);
|
|
624
|
+
if (!dryRun) {
|
|
625
|
+
(0, node_fs_1.writeFileSync)(outFile, content, "utf-8");
|
|
626
|
+
}
|
|
627
|
+
return "created";
|
|
628
|
+
}
|
|
629
|
+
const content = (0, node_fs_1.readFileSync)(outFile, "utf-8");
|
|
630
|
+
if (content.includes(exports.DRAFT_MARKER)) {
|
|
631
|
+
const newContent = generateFull(relDir, repoRoot, allRoutes);
|
|
632
|
+
if (!dryRun) {
|
|
633
|
+
(0, node_fs_1.writeFileSync)(outFile, newContent, "utf-8");
|
|
634
|
+
}
|
|
635
|
+
return "regenerated";
|
|
636
|
+
}
|
|
637
|
+
const regionVersion = getGeneratedRegionVersion(content);
|
|
638
|
+
if (regionVersion !== null) {
|
|
639
|
+
if (regionVersion === exports.TEMPLATE_VERSION) {
|
|
640
|
+
return "skipped";
|
|
641
|
+
}
|
|
642
|
+
const newRegionBody = generateRegion(relDir, repoRoot, allRoutes);
|
|
643
|
+
if (!dryRun) {
|
|
644
|
+
const newContent = replaceGeneratedRegion(content, newRegionBody);
|
|
645
|
+
(0, node_fs_1.writeFileSync)(outFile, newContent, "utf-8");
|
|
646
|
+
}
|
|
647
|
+
return "regenerated-region";
|
|
648
|
+
}
|
|
649
|
+
return "blocked";
|
|
650
|
+
}
|
|
651
|
+
function reconcileAll(repoRoot, opts = {}) {
|
|
652
|
+
const config = (0, loader_js_1.loadConfig)(repoRoot);
|
|
653
|
+
const atlasPath = (0, node_path_1.resolve)(repoRoot, config.repo.sidecarOutputPath ?? ".polaris/map");
|
|
654
|
+
const allRoutes = {
|
|
655
|
+
...(0, atlas_js_1.readFileRoutes)(atlasPath),
|
|
656
|
+
...(0, atlas_js_1.readNeedsReview)(atlasPath),
|
|
657
|
+
};
|
|
658
|
+
// Root handling: skipped by default (root uses AGENTS.md/CLAUDE.md)
|
|
659
|
+
const rootEligibility = (0, smartdoc_ignore_js_1.isDirectoryEligible)(repoRoot, repoRoot, {
|
|
660
|
+
isRoot: true,
|
|
661
|
+
skipRoot: opts.includeRoot ? false : true,
|
|
662
|
+
});
|
|
663
|
+
let skippedRoot;
|
|
664
|
+
if (!rootEligibility.eligible) {
|
|
665
|
+
skippedRoot = { path: ".", reason: rootEligibility.reason || "root skipped" };
|
|
666
|
+
}
|
|
667
|
+
const eligibilityOpts = {
|
|
668
|
+
includeAgentFolders: opts.includeAgentFolders,
|
|
669
|
+
includeHidden: opts.includeHidden,
|
|
670
|
+
skipRoot: true,
|
|
671
|
+
};
|
|
672
|
+
const { eligible: dirs, ineligible: skippedIneligible } = collectDirs(repoRoot, repoRoot, eligibilityOpts);
|
|
673
|
+
const dirsToProcess = [];
|
|
674
|
+
if (rootEligibility.eligible) {
|
|
675
|
+
dirsToProcess.push(".");
|
|
676
|
+
}
|
|
677
|
+
dirsToProcess.push(...dirs);
|
|
678
|
+
const report = {
|
|
679
|
+
created: [],
|
|
680
|
+
regenerated: [],
|
|
681
|
+
regeneratedRegion: [],
|
|
682
|
+
skipped: [],
|
|
683
|
+
blocked: [],
|
|
684
|
+
skippedIneligible,
|
|
685
|
+
skippedRoot,
|
|
686
|
+
};
|
|
687
|
+
for (const relDir of dirsToProcess) {
|
|
688
|
+
const polarisStatus = reconcileFile(relDir, "POLARIS.md", repoRoot, allRoutes, opts.dryRun ?? false, generateDraft, generateDraftRegion);
|
|
689
|
+
switch (polarisStatus) {
|
|
690
|
+
case "created":
|
|
691
|
+
report.created.push(relDir === "." ? "POLARIS.md" : `${relDir}/POLARIS.md`);
|
|
692
|
+
break;
|
|
693
|
+
case "regenerated":
|
|
694
|
+
report.regenerated.push(relDir === "." ? "POLARIS.md" : `${relDir}/POLARIS.md`);
|
|
695
|
+
break;
|
|
696
|
+
case "regenerated-region":
|
|
697
|
+
report.regeneratedRegion.push(relDir === "." ? "POLARIS.md" : `${relDir}/POLARIS.md`);
|
|
698
|
+
break;
|
|
699
|
+
case "skipped":
|
|
700
|
+
report.skipped.push(relDir === "." ? "POLARIS.md" : `${relDir}/POLARIS.md`);
|
|
701
|
+
break;
|
|
702
|
+
case "blocked":
|
|
703
|
+
report.blocked.push(relDir === "." ? "POLARIS.md" : `${relDir}/POLARIS.md`);
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
const summaryStatus = reconcileFile(relDir, "SUMMARY.md", repoRoot, allRoutes, opts.dryRun ?? false, generateSummaryDraft, generateSummaryDraftRegion);
|
|
707
|
+
switch (summaryStatus) {
|
|
708
|
+
case "created":
|
|
709
|
+
report.created.push(relDir === "." ? "SUMMARY.md" : `${relDir}/SUMMARY.md`);
|
|
710
|
+
break;
|
|
711
|
+
case "regenerated":
|
|
712
|
+
report.regenerated.push(relDir === "." ? "SUMMARY.md" : `${relDir}/SUMMARY.md`);
|
|
713
|
+
break;
|
|
714
|
+
case "regenerated-region":
|
|
715
|
+
report.regeneratedRegion.push(relDir === "." ? "SUMMARY.md" : `${relDir}/SUMMARY.md`);
|
|
716
|
+
break;
|
|
717
|
+
case "skipped":
|
|
718
|
+
report.skipped.push(relDir === "." ? "SUMMARY.md" : `${relDir}/SUMMARY.md`);
|
|
719
|
+
break;
|
|
720
|
+
case "blocked":
|
|
721
|
+
report.blocked.push(relDir === "." ? "SUMMARY.md" : `${relDir}/SUMMARY.md`);
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return report;
|
|
726
|
+
}
|
|
@@ -129,6 +129,32 @@ function parseReadBeforeEditingLinks(content) {
|
|
|
129
129
|
}
|
|
130
130
|
return links;
|
|
131
131
|
}
|
|
132
|
+
function extractGeneratedRegion(content) {
|
|
133
|
+
const start = content.indexOf(seed_instructions_js_1.GENERATED_START_MARKER);
|
|
134
|
+
const end = content.indexOf(seed_instructions_js_1.GENERATED_END_MARKER);
|
|
135
|
+
if (start === -1 || end === -1 || start >= end)
|
|
136
|
+
return null;
|
|
137
|
+
return content.slice(start + seed_instructions_js_1.GENERATED_START_MARKER.length, end).trim();
|
|
138
|
+
}
|
|
139
|
+
function parseTemplateVersionStamp(content) {
|
|
140
|
+
const match = content.match(/<!--\s*polaris:template-version:\s*(\d+)\s*-->/);
|
|
141
|
+
if (!match)
|
|
142
|
+
return undefined;
|
|
143
|
+
return parseInt(match[1], 10);
|
|
144
|
+
}
|
|
145
|
+
function checkTemplateVersionStamp(content, fileLabel) {
|
|
146
|
+
const version = parseTemplateVersionStamp(content);
|
|
147
|
+
if (version === undefined) {
|
|
148
|
+
return { severity: "WARN", message: `${fileLabel} is unstamped (no template-version stamp)` };
|
|
149
|
+
}
|
|
150
|
+
if (version !== seed_instructions_js_1.TEMPLATE_VERSION) {
|
|
151
|
+
return {
|
|
152
|
+
severity: "WARN",
|
|
153
|
+
message: `${fileLabel} template version drift: found ${version}, expected ${seed_instructions_js_1.TEMPLATE_VERSION}`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
132
158
|
function getRequiredDirs(repoRoot) {
|
|
133
159
|
const config = (0, loader_js_1.loadConfig)(repoRoot);
|
|
134
160
|
// The config type may not yet have a docs key; access dynamically
|
|
@@ -178,6 +204,14 @@ function validateDir(relDir, repoRoot, allRoutes, similarityThreshold = DEFAULT_
|
|
|
178
204
|
}
|
|
179
205
|
const content = (0, node_fs_1.readFileSync)(polarisFile, "utf-8");
|
|
180
206
|
const findings = [];
|
|
207
|
+
const polarisGeneratedRaw = extractGeneratedRegion(content);
|
|
208
|
+
const polarisGenerated = polarisGeneratedRaw ?? content;
|
|
209
|
+
// Signal 0: template-version stamp inside generated region (only when present)
|
|
210
|
+
if (polarisGeneratedRaw !== null) {
|
|
211
|
+
const polarisStampFinding = checkTemplateVersionStamp(polarisGeneratedRaw, "POLARIS.md");
|
|
212
|
+
if (polarisStampFinding)
|
|
213
|
+
findings.push(polarisStampFinding);
|
|
214
|
+
}
|
|
181
215
|
// Signal 1: ≥3 files in directory changed since last POLARIS.md git modification
|
|
182
216
|
const lastMod = getLastGitModDate(polarisFile, repoRoot);
|
|
183
217
|
if (lastMod !== null) {
|
|
@@ -191,7 +225,7 @@ function validateDir(relDir, repoRoot, allRoutes, similarityThreshold = DEFAULT_
|
|
|
191
225
|
}
|
|
192
226
|
}
|
|
193
227
|
// Signal 2: broken links in "Read before editing"
|
|
194
|
-
const links = parseReadBeforeEditingLinks(
|
|
228
|
+
const links = parseReadBeforeEditingLinks(polarisGenerated);
|
|
195
229
|
for (const link of links) {
|
|
196
230
|
const absLink = (0, node_path_1.resolve)(absDir, link);
|
|
197
231
|
if (!(0, node_fs_1.existsSync)(absLink)) {
|
|
@@ -221,7 +255,7 @@ function validateDir(relDir, repoRoot, allRoutes, similarityThreshold = DEFAULT_
|
|
|
221
255
|
const headingRe = /^##\s+(.+)$/gm;
|
|
222
256
|
const parsedHeadings = [];
|
|
223
257
|
let headingMatch;
|
|
224
|
-
while ((headingMatch = headingRe.exec(
|
|
258
|
+
while ((headingMatch = headingRe.exec(polarisGenerated)) !== null) {
|
|
225
259
|
const original = headingMatch[1].trim();
|
|
226
260
|
parsedHeadings.push({
|
|
227
261
|
normalized: original.toLowerCase(),
|
|
@@ -289,9 +323,17 @@ function validateDir(relDir, repoRoot, allRoutes, similarityThreshold = DEFAULT_
|
|
|
289
323
|
else {
|
|
290
324
|
// Signal 6: SUMMARY.md doctrine-bleed scan
|
|
291
325
|
const summaryContent = (0, node_fs_1.readFileSync)(summaryFile, "utf-8");
|
|
326
|
+
const summaryGeneratedRaw = extractGeneratedRegion(summaryContent);
|
|
327
|
+
const summaryGenerated = summaryGeneratedRaw ?? summaryContent;
|
|
292
328
|
const summaryRel = (0, node_path_1.relative)(repoRoot, summaryFile).replace(/\\/g, "/");
|
|
329
|
+
// Signal 5.5: SUMMARY.md template-version stamp (only inside generated region)
|
|
330
|
+
if (summaryGeneratedRaw !== null) {
|
|
331
|
+
const summaryStampFinding = checkTemplateVersionStamp(summaryGeneratedRaw, "SUMMARY.md");
|
|
332
|
+
if (summaryStampFinding)
|
|
333
|
+
findings.push(summaryStampFinding);
|
|
334
|
+
}
|
|
293
335
|
const modalVerbs = ["must", "never", "always"];
|
|
294
|
-
const lines =
|
|
336
|
+
const lines = summaryGenerated.split("\n");
|
|
295
337
|
for (let i = 0; i < lines.length; i++) {
|
|
296
338
|
const line = lines[i].toLowerCase();
|
|
297
339
|
for (const verb of modalVerbs) {
|
|
@@ -305,7 +347,7 @@ function validateDir(relDir, repoRoot, allRoutes, similarityThreshold = DEFAULT_
|
|
|
305
347
|
}
|
|
306
348
|
// Signal 7: pairwise POLARIS.md / SUMMARY.md drift
|
|
307
349
|
const routeName = relDir === "." ? undefined : (0, node_path_1.basename)(relDir);
|
|
308
|
-
const similarity = computeNormalizedSimilarity(
|
|
350
|
+
const similarity = computeNormalizedSimilarity(polarisGenerated, summaryGenerated, routeName);
|
|
309
351
|
if (content === summaryContent) {
|
|
310
352
|
findings.push({
|
|
311
353
|
severity: "ERROR",
|
|
@@ -15,6 +15,7 @@ const migration_js_1 = require("./migration.js");
|
|
|
15
15
|
*/
|
|
16
16
|
class LocalGraph {
|
|
17
17
|
graph;
|
|
18
|
+
orderingDependenciesMerged = false;
|
|
18
19
|
constructor(graph) {
|
|
19
20
|
this.graph = graph;
|
|
20
21
|
}
|
|
@@ -54,19 +55,76 @@ class LocalGraph {
|
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
57
|
* Persists the graph to `.polaris/clusters/{clusterId}/clusters.json`.
|
|
57
|
-
*
|
|
58
|
+
* Children are sorted topologically before writing so the saved cluster
|
|
59
|
+
* definition reflects dependency order.
|
|
58
60
|
*
|
|
59
61
|
* @param clusterId The cluster ID to use as the directory name (e.g., "POL-198").
|
|
60
62
|
* @param repoRoot The root directory of the repository.
|
|
61
63
|
* @returns The absolute path of the written file.
|
|
62
64
|
*/
|
|
63
65
|
async save(clusterId, repoRoot = process.cwd()) {
|
|
66
|
+
this.mergeOrderingDependencies();
|
|
67
|
+
const persistedGraph = {
|
|
68
|
+
...this.graph,
|
|
69
|
+
clusters: Object.fromEntries(Object.entries(this.graph.clusters).map(([id, cluster]) => [
|
|
70
|
+
id,
|
|
71
|
+
Array.isArray(cluster.children) && cluster.children.length > 1
|
|
72
|
+
? { ...cluster, children: this.topoSortChildren(cluster.children) }
|
|
73
|
+
: cluster,
|
|
74
|
+
])),
|
|
75
|
+
};
|
|
64
76
|
const dir = node_path_1.default.join(repoRoot, ".polaris", "clusters", clusterId);
|
|
65
77
|
await (0, promises_1.mkdir)(dir, { recursive: true });
|
|
66
78
|
const filePath = node_path_1.default.join(dir, "clusters.json");
|
|
67
|
-
await (0, promises_1.writeFile)(filePath, JSON.stringify(
|
|
79
|
+
await (0, promises_1.writeFile)(filePath, JSON.stringify(persistedGraph, null, 2), "utf-8");
|
|
68
80
|
return filePath;
|
|
69
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Sort a list of children topologically using the graph dependencies.
|
|
84
|
+
* Children with no in-cluster dependencies come first; if a cycle exists,
|
|
85
|
+
* the remaining nodes are appended in their original order.
|
|
86
|
+
*/
|
|
87
|
+
topoSortChildren(children) {
|
|
88
|
+
const childSet = new Set(children);
|
|
89
|
+
const inDegree = new Map();
|
|
90
|
+
const dependents = new Map();
|
|
91
|
+
for (const child of children) {
|
|
92
|
+
inDegree.set(child, 0);
|
|
93
|
+
dependents.set(child, []);
|
|
94
|
+
}
|
|
95
|
+
for (const child of children) {
|
|
96
|
+
for (const dep of this.getDependencies(child)) {
|
|
97
|
+
if (!childSet.has(dep))
|
|
98
|
+
continue;
|
|
99
|
+
inDegree.set(child, (inDegree.get(child) ?? 0) + 1);
|
|
100
|
+
dependents.get(dep).push(child);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const queue = [];
|
|
104
|
+
for (const [child, deg] of inDegree) {
|
|
105
|
+
if (deg === 0)
|
|
106
|
+
queue.push(child);
|
|
107
|
+
}
|
|
108
|
+
const sorted = [];
|
|
109
|
+
while (queue.length > 0) {
|
|
110
|
+
const node = queue.shift();
|
|
111
|
+
sorted.push(node);
|
|
112
|
+
for (const dependent of dependents.get(node) ?? []) {
|
|
113
|
+
const newDeg = (inDegree.get(dependent) ?? 1) - 1;
|
|
114
|
+
inDegree.set(dependent, newDeg);
|
|
115
|
+
if (newDeg === 0)
|
|
116
|
+
queue.push(dependent);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (sorted.length < children.length) {
|
|
120
|
+
const sortedSet = new Set(sorted);
|
|
121
|
+
for (const child of children) {
|
|
122
|
+
if (!sortedSet.has(child))
|
|
123
|
+
sorted.push(child);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return sorted;
|
|
127
|
+
}
|
|
70
128
|
/**
|
|
71
129
|
* Returns the full v2 execution graph.
|
|
72
130
|
*/
|
|
@@ -87,12 +145,57 @@ class LocalGraph {
|
|
|
87
145
|
return this.graph.nodes[id];
|
|
88
146
|
}
|
|
89
147
|
/**
|
|
90
|
-
* Returns the dependencies for a given node
|
|
148
|
+
* Returns the dependencies for a given node, merging any dependencies declared
|
|
149
|
+
* in the node's `## Ordering` body section.
|
|
91
150
|
* @param id The ID of the node to get dependencies for.
|
|
92
151
|
* @returns An array of node IDs that the given node is blocked by.
|
|
93
152
|
*/
|
|
94
153
|
getDependencies(id) {
|
|
154
|
+
this.mergeOrderingDependencies();
|
|
95
155
|
return this.graph.dependencies[id] ?? [];
|
|
96
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Extracts issue IDs from the `## Ordering` section of a node body.
|
|
159
|
+
*
|
|
160
|
+
* Only lines that explicitly declare ordering ("depends on" or "after" / "sequence after")
|
|
161
|
+
* contribute, and "before or after" clauses are ignored because they are not strict.
|
|
162
|
+
*/
|
|
163
|
+
extractOrderingIds(body) {
|
|
164
|
+
const sectionMatch = body.match(/##\s*Ordering\b([\s\S]*?)(?:\n##\s|\n\n(?=\n##\s)|$)/i);
|
|
165
|
+
if (!sectionMatch)
|
|
166
|
+
return [];
|
|
167
|
+
const section = sectionMatch[1];
|
|
168
|
+
const cleaned = section.replace(/before\s+or\s+after\s*\[[^\]]*\]/gi, "");
|
|
169
|
+
const ids = new Set();
|
|
170
|
+
const re = /(?:depends\s+on|after)\s*\[?\s*(\w+-\d+)\b/gi;
|
|
171
|
+
for (const line of cleaned.split("\n")) {
|
|
172
|
+
if (!/^\s*[-*]\s/.test(line))
|
|
173
|
+
continue;
|
|
174
|
+
for (const match of line.matchAll(re)) {
|
|
175
|
+
ids.add(match[1]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return [...ids];
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Merges dependencies declared in node body `## Ordering` sections into the
|
|
182
|
+
* graph's explicit dependency map. This is idempotent for the loaded graph.
|
|
183
|
+
*/
|
|
184
|
+
mergeOrderingDependencies() {
|
|
185
|
+
if (this.orderingDependenciesMerged)
|
|
186
|
+
return;
|
|
187
|
+
for (const node of Object.values(this.graph.nodes)) {
|
|
188
|
+
const orderingIds = this.extractOrderingIds(node.body ?? "");
|
|
189
|
+
if (orderingIds.length === 0)
|
|
190
|
+
continue;
|
|
191
|
+
const existing = new Set(this.graph.dependencies[node.id] ?? []);
|
|
192
|
+
for (const dep of orderingIds) {
|
|
193
|
+
if (dep !== node.id)
|
|
194
|
+
existing.add(dep);
|
|
195
|
+
}
|
|
196
|
+
this.graph.dependencies[node.id] = [...existing].sort();
|
|
197
|
+
}
|
|
198
|
+
this.orderingDependenciesMerged = true;
|
|
199
|
+
}
|
|
97
200
|
}
|
|
98
201
|
exports.LocalGraph = LocalGraph;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<!-- polaris:draft -->
|
|
2
|
+
<!-- BEGIN POLARIS GENERATED -->
|
|
3
|
+
<!-- polaris:template-version: 1 -->
|
|
2
4
|
# workspace
|
|
3
5
|
|
|
4
6
|
> Polaris draft — review and remove the `<!-- polaris:draft -->` marker to promote.
|
|
@@ -38,3 +40,5 @@
|
|
|
38
40
|
## Related routes
|
|
39
41
|
|
|
40
42
|
<!-- Atlas route pointer to sibling or parent folders. -->
|
|
43
|
+
|
|
44
|
+
<!-- END POLARIS GENERATED -->
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!-- polaris:draft -->
|
|
2
|
+
<!-- BEGIN POLARIS GENERATED -->
|
|
3
|
+
<!-- polaris:template-version: 1 -->
|
|
4
|
+
# Summary: workspace
|
|
5
|
+
|
|
6
|
+
> Polaris draft — review and remove the `<!-- polaris:draft -->` marker to promote.
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
<!-- One-line statement of what this folder does. -->
|
|
10
|
+
|
|
11
|
+
## Core Concepts
|
|
12
|
+
<!-- 3–7 key concepts a reader needs before diving into source. -->
|
|
13
|
+
|
|
14
|
+
## Architectural Role
|
|
15
|
+
<!-- How this folder fits into the larger system. -->
|
|
16
|
+
|
|
17
|
+
## Key Constraints
|
|
18
|
+
<!-- The most important non-obvious behavioral limits. -->
|
|
19
|
+
|
|
20
|
+
## Important Relationships
|
|
21
|
+
<!-- Upstream/downstream dependencies on other folders. -->
|
|
22
|
+
|
|
23
|
+
## Current State
|
|
24
|
+
<!-- What is implemented, what is not yet, known gaps. -->
|
|
25
|
+
|
|
26
|
+
## Route Health
|
|
27
|
+
<!-- Current operational condition. Workers should understand route health in under 10 seconds. -->
|
|
28
|
+
|
|
29
|
+
### Healthy
|
|
30
|
+
<!-- If the route is healthy, state why. Otherwise omit this subsection. -->
|
|
31
|
+
|
|
32
|
+
### Monitoring
|
|
33
|
+
<!-- Any ongoing monitoring or observations. Omit if none. -->
|
|
34
|
+
|
|
35
|
+
### Known Issues
|
|
36
|
+
<!-- Any known problems or risks. Omit if none. -->
|
|
37
|
+
|
|
38
|
+
### Recent Treatments
|
|
39
|
+
<!-- Recent fixes or improvements, with chart references if applicable. Omit if none. -->
|
|
40
|
+
|
|
41
|
+
### Improvement Opportunities
|
|
42
|
+
<!-- Potential future improvements. Omit if none. -->
|
|
43
|
+
|
|
44
|
+
## Canonical References
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
canonical_docs:
|
|
48
|
+
- POLARIS.md
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
<!-- Add navigation paths to canonical docs, specs, or doctrine. These are retrieval paths, not reading assignments. -->
|
|
52
|
+
|
|
53
|
+
## Known Drift
|
|
54
|
+
<!-- Places where the summary may be stale (honesty field). -->
|
|
55
|
+
|
|
56
|
+
<!-- END POLARIS GENERATED -->
|