@lsctech/polaris 0.5.12 → 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.
@@ -1,9 +1,15 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.SUPPORTED_SKILLS = exports.SKILL_ROLE_MAP = void 0;
4
7
  exports.generateSetupBootstrapPacket = generateSetupBootstrapPacket;
5
8
  exports.generateSkillPacket = generateSkillPacket;
6
9
  const node_crypto_1 = require("node:crypto");
10
+ const node_child_process_1 = require("node:child_process");
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_path_1 = __importDefault(require("node:path"));
7
13
  exports.SKILL_ROLE_MAP = {
8
14
  analyze: "Analyst",
9
15
  run: "Foreman",
@@ -362,7 +368,37 @@ function buildCatalogPacket() {
362
368
  ],
363
369
  };
364
370
  }
365
- function buildReconcilePacket() {
371
+ function buildReconcilePacket(_config, options) {
372
+ const repoRoot = node_path_1.default.resolve(options?.repoRoot ?? process.cwd());
373
+ const issueId = resolveIssueId(repoRoot, options?.issueId);
374
+ const runId = buildReconcileRunId(issueId);
375
+ const changedFiles = getReconcileChangedFiles(repoRoot);
376
+ const affectedFolders = resolveAffectedFolders(repoRoot, changedFiles);
377
+ if (affectedFolders.length === 0) {
378
+ return buildBlockedReconcilePacket(runId, issueId, repoRoot);
379
+ }
380
+ const allowedWritePaths = affectedFolders.flatMap((folder) => {
381
+ const paths = [node_path_1.default.join(repoRoot, folder, "POLARIS.md")];
382
+ const summaryPath = node_path_1.default.join(repoRoot, folder, "SUMMARY.md");
383
+ if (node_fs_1.default.existsSync(summaryPath)) {
384
+ paths.push(summaryPath);
385
+ }
386
+ return paths;
387
+ });
388
+ const workInventory = {
389
+ affected_folders: affectedFolders,
390
+ all_changed_files: changedFiles,
391
+ child_summaries: [],
392
+ pending_cognition_notes: [],
393
+ polaris_md_files: Object.fromEntries(affectedFolders.map((folder) => [
394
+ folder,
395
+ readFileOrNull(node_path_1.default.join(repoRoot, folder, "POLARIS.md")),
396
+ ])),
397
+ summary_md_files: Object.fromEntries(affectedFolders.map((folder) => [
398
+ folder,
399
+ readFileOrNull(node_path_1.default.join(repoRoot, folder, "SUMMARY.md")),
400
+ ])),
401
+ };
366
402
  return {
367
403
  authority_boundaries: [
368
404
  "Read packet-scoped folders and their POLARIS.md and SUMMARY.md files",
@@ -389,9 +425,204 @@ function buildReconcilePacket() {
389
425
  "A requested write falls outside packet-allowed paths",
390
426
  "Work evidence is missing or contradictory",
391
427
  ],
428
+ packet_kind: "reconcile",
429
+ run_id: runId,
430
+ issue_id: issueId,
431
+ affected_folders: affectedFolders,
432
+ work_inventory: workInventory,
433
+ allowed_write_paths: allowedWritePaths,
434
+ prohibited_write_paths: [repoRoot],
435
+ constraints: {
436
+ max_summary_addition_lines: 50,
437
+ },
392
438
  };
393
439
  }
394
- function generateSkillPacket(skillName, config) {
440
+ function buildBlockedReconcilePacket(runId, issueId, repoRoot) {
441
+ return {
442
+ authority_boundaries: [
443
+ "Read packet-scoped folders and their POLARIS.md and SUMMARY.md files",
444
+ "Update cognition files only within packet-allowed write paths",
445
+ "Create one sealed local cognition commit",
446
+ ],
447
+ prohibited_actions: [
448
+ "Modify implementation source code, tests, or configuration",
449
+ "Move, ingest, classify, or promote documents",
450
+ "Write outside packet-allowed paths",
451
+ "Call polaris loop continue or polaris finalize",
452
+ "Git push or create a pull request",
453
+ "Fabricate affected folders or work inventory when no git diff is available",
454
+ ],
455
+ allowed_outputs: [
456
+ "Updated POLARIS.md and SUMMARY.md files in packet-allowed paths",
457
+ "A sealed local cognition commit",
458
+ ],
459
+ deliverables: [
460
+ "Packet-scoped cognition reconciled with completed work",
461
+ "All cognition changes recorded in one sealed local commit",
462
+ ],
463
+ stop_conditions: [
464
+ "All packet-scoped cognition reconciled",
465
+ "A requested write falls outside packet-allowed paths",
466
+ "Work evidence is missing or contradictory",
467
+ "No git diff is available to scope reconciliation",
468
+ ],
469
+ packet_kind: "reconcile",
470
+ run_id: runId,
471
+ issue_id: issueId,
472
+ affected_folders: [],
473
+ work_inventory: {
474
+ affected_folders: [],
475
+ all_changed_files: [],
476
+ child_summaries: [],
477
+ pending_cognition_notes: [],
478
+ polaris_md_files: {},
479
+ summary_md_files: {},
480
+ },
481
+ allowed_write_paths: [],
482
+ prohibited_write_paths: [repoRoot],
483
+ constraints: {
484
+ max_summary_addition_lines: 50,
485
+ },
486
+ };
487
+ }
488
+ // ── Reconcile helpers ─────────────────────────────────────────────────────────
489
+ function resolveIssueId(repoRoot, explicitIssueId) {
490
+ if (explicitIssueId)
491
+ return explicitIssueId;
492
+ const stateCandidates = [
493
+ node_path_1.default.join(repoRoot, ".taskchain_artifacts", "polaris-run", "current-state.json"),
494
+ node_path_1.default.join(repoRoot, ".polaris", "runs", "current-state.json"),
495
+ ];
496
+ for (const candidate of stateCandidates) {
497
+ try {
498
+ const raw = JSON.parse(node_fs_1.default.readFileSync(candidate, "utf-8"));
499
+ const activeChild = raw["active_child"];
500
+ if (typeof activeChild === "string" && activeChild)
501
+ return activeChild;
502
+ const clusterId = raw["cluster_id"];
503
+ if (typeof clusterId === "string" && clusterId)
504
+ return clusterId;
505
+ }
506
+ catch {
507
+ // ignore missing or malformed state
508
+ }
509
+ }
510
+ try {
511
+ const branch = execGit(repoRoot, ["branch", "--show-current"]).join("").trim();
512
+ const match = /^pol-?(\d+)(?:-.*)?$/i.exec(branch);
513
+ if (match && match[1]) {
514
+ return `POL-${match[1]}`;
515
+ }
516
+ }
517
+ catch {
518
+ // ignore
519
+ }
520
+ return "unknown";
521
+ }
522
+ function buildReconcileRunId(issueId) {
523
+ const slug = issueId.replace(/\s+/g, "-");
524
+ const date = new Date().toISOString().slice(0, 10);
525
+ // Minimal sequence: 001 for now; each call is a distinct run by packet_id.
526
+ const seq = "001";
527
+ return `polaris-reconcile-${slug}-${date}-${seq}`;
528
+ }
529
+ function getReconcileChangedFiles(repoRoot) {
530
+ const files = new Set();
531
+ try {
532
+ for (const file of execGit(repoRoot, ["diff", "--name-only", "HEAD"])) {
533
+ if (file)
534
+ files.add(file);
535
+ }
536
+ }
537
+ catch {
538
+ // ignore
539
+ }
540
+ try {
541
+ const base = resolveReconcileBase(repoRoot);
542
+ if (base) {
543
+ for (const file of execGit(repoRoot, ["diff", "--name-only", `${base}..HEAD`])) {
544
+ if (file)
545
+ files.add(file);
546
+ }
547
+ }
548
+ }
549
+ catch {
550
+ // ignore
551
+ }
552
+ return [...files].sort();
553
+ }
554
+ function resolveReconcileBase(repoRoot) {
555
+ const candidates = [
556
+ resolveRemoteDefaultBranch(repoRoot),
557
+ "origin/main",
558
+ "origin/master",
559
+ "main",
560
+ "master",
561
+ ];
562
+ for (const candidate of candidates) {
563
+ if (!candidate)
564
+ continue;
565
+ try {
566
+ const base = execGit(repoRoot, ["merge-base", "HEAD", candidate]).join("").trim();
567
+ if (base)
568
+ return base;
569
+ }
570
+ catch {
571
+ // try next candidate
572
+ }
573
+ }
574
+ return null;
575
+ }
576
+ function resolveRemoteDefaultBranch(repoRoot) {
577
+ try {
578
+ const ref = execGit(repoRoot, ["rev-parse", "--abbrev-ref", "refs/remotes/origin/HEAD"]).join("").trim();
579
+ return ref || null;
580
+ }
581
+ catch {
582
+ return null;
583
+ }
584
+ }
585
+ function resolveAffectedFolders(repoRoot, changedFiles) {
586
+ const routeMap = loadFileRouteMap(repoRoot);
587
+ const folders = new Set();
588
+ for (const file of changedFiles) {
589
+ const entry = routeMap[file];
590
+ if (entry && typeof entry.route === "string") {
591
+ folders.add(entry.route.replace(/\/+$/, "") + "/");
592
+ }
593
+ }
594
+ return [...folders].sort();
595
+ }
596
+ function loadFileRouteMap(repoRoot) {
597
+ const mapPath = node_path_1.default.join(repoRoot, ".polaris", "map", "file-routes.json");
598
+ try {
599
+ const raw = JSON.parse(node_fs_1.default.readFileSync(mapPath, "utf-8"));
600
+ const normalized = {};
601
+ for (const [key, value] of Object.entries(raw)) {
602
+ if (value && typeof value === "object") {
603
+ const entry = value;
604
+ normalized[key] = { route: typeof entry.route === "string" ? entry.route : undefined };
605
+ }
606
+ }
607
+ return normalized;
608
+ }
609
+ catch {
610
+ return {};
611
+ }
612
+ }
613
+ function readFileOrNull(filePath) {
614
+ try {
615
+ return node_fs_1.default.readFileSync(filePath, "utf-8");
616
+ }
617
+ catch {
618
+ return null;
619
+ }
620
+ }
621
+ function execGit(repoRoot, args) {
622
+ const output = (0, node_child_process_1.execFileSync)("git", args, { cwd: repoRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
623
+ return output.split("\n").filter(Boolean);
624
+ }
625
+ function generateSkillPacket(skillName, config, options) {
395
626
  const active_role = exports.SKILL_ROLE_MAP[skillName];
396
627
  const role_summary = ROLE_SUMMARIES[active_role];
397
628
  const generated_at = new Date().toISOString();
@@ -425,7 +656,7 @@ function generateSkillPacket(skillName, config) {
425
656
  body = buildCatalogPacket();
426
657
  break;
427
658
  case "reconcile":
428
- body = buildReconcilePacket();
659
+ body = buildReconcilePacket(config, options);
429
660
  break;
430
661
  }
431
662
  return {
@@ -43,7 +43,7 @@ function locateCanonFiles(changedFiles, repoRoot) {
43
43
  canon.add(polarisMd);
44
44
  }
45
45
  // Active doctrine
46
- const doctrineDir = (0, node_path_1.join)(repoRoot, "docs", "doctrine", "active");
46
+ const doctrineDir = (0, node_path_1.join)(repoRoot, "smartdocs", "doctrine", "active");
47
47
  if ((0, node_fs_1.existsSync)(doctrineDir)) {
48
48
  for (const f of (0, node_fs_1.readdirSync)(doctrineDir)) {
49
49
  if (f.endsWith(".md") && (0, node_path_1.basename)(f).toLowerCase() !== "summary.md")
@@ -61,7 +61,7 @@ function locateCanonFiles(changedFiles, repoRoot) {
61
61
  }
62
62
  }
63
63
  for (const specSubdir of ["active", "implemented"]) {
64
- const specDir = (0, node_path_1.join)(repoRoot, "docs", "specs", specSubdir);
64
+ const specDir = (0, node_path_1.join)(repoRoot, "smartdocs", "specs", specSubdir);
65
65
  if (!(0, node_fs_1.existsSync)(specDir))
66
66
  continue;
67
67
  for (const f of (0, node_fs_1.readdirSync)(specDir)) {
@@ -124,7 +124,7 @@ function checkPolarisMd(canonFile, changedFiles, repoRoot) {
124
124
  // Determine conflict type based on rule text and changed file
125
125
  let conflictType = "stale-implementation";
126
126
  const ext = changedFile.split(".").pop()?.toLowerCase() ?? "";
127
- const isDocFile = ext === "md" || changedFile.startsWith("docs/");
127
+ const isDocFile = ext === "md" || changedFile.startsWith("smartdocs/");
128
128
  const hasDocKeywords = /\b(doc|documentation|readme|guide|spec)\b/i.test(lower);
129
129
  const hasCandidateKeywords = /\b(candidate|divergence|proposal)\b/i.test(lower);
130
130
  if (isDocFile || hasDocKeywords) {
@@ -282,7 +282,7 @@ function runCanonCheck(options) {
282
282
  return result;
283
283
  }
284
284
  function writeStaleDraftDocs(conflicts, repoRoot, childId) {
285
- const rawDir = (0, node_path_1.join)(repoRoot, "docs", "raw");
285
+ const rawDir = (0, node_path_1.join)(repoRoot, "smartdocs", "raw");
286
286
  try {
287
287
  (0, node_fs_1.mkdirSync)(rawDir, { recursive: true });
288
288
  for (const conflict of conflicts) {
@@ -309,7 +309,7 @@ function writeStaleDraftDocs(conflicts, repoRoot, childId) {
309
309
  }
310
310
  }
311
311
  function writeCandidateDraftDocs(conflicts, repoRoot, childId) {
312
- const candidateDir = (0, node_path_1.join)(repoRoot, "docs", "doctrine", "candidate");
312
+ const candidateDir = (0, node_path_1.join)(repoRoot, "smartdocs", "doctrine", "candidate");
313
313
  try {
314
314
  (0, node_fs_1.mkdirSync)(candidateDir, { recursive: true });
315
315
  for (const conflict of conflicts) {
@@ -38,6 +38,32 @@ function printSeedAllResult(filename, dryRun, { written, skippedExists, skippedD
38
38
  }
39
39
  console.log(`${leadingBlankLine ? "\n" : ""}Done. ${written.length} written, ${skippedExists.length} skipped (exists), ${skippedDraft.length} skipped (draft)${extraSummary}.`);
40
40
  }
41
+ function printReconcileReport(report, dryRun) {
42
+ for (const path of report.created) {
43
+ console.log(`${dryRun ? "[dry-run] would create" : "created"}: ${path}`);
44
+ }
45
+ for (const path of report.regenerated) {
46
+ console.log(`${dryRun ? "[dry-run] would regenerate" : "regenerated"}: ${path}`);
47
+ }
48
+ for (const path of report.regeneratedRegion) {
49
+ console.log(`${dryRun ? "[dry-run] would regenerate (region)" : "regenerated (region)"}: ${path}`);
50
+ }
51
+ for (const path of report.skipped) {
52
+ console.log(`skipped (up-to-date): ${path}`);
53
+ }
54
+ for (const path of report.blocked) {
55
+ console.log(`blocked (manual migration): ${path}`);
56
+ }
57
+ if (report.skippedRoot) {
58
+ console.log(`skipped (root): ${report.skippedRoot.path}/ (${report.skippedRoot.reason})`);
59
+ }
60
+ if (report.skippedIneligible.length > 0) {
61
+ for (const entry of report.skippedIneligible) {
62
+ console.log(`skipped (ineligible): ${entry.path}/ (${entry.reason})`);
63
+ }
64
+ }
65
+ console.log(`\nDone. ${report.created.length} created, ${report.regenerated.length} regenerated, ${report.regeneratedRegion.length} regenerated (region), ${report.skipped.length} skipped, ${report.blocked.length} blocked.`);
66
+ }
41
67
  /**
42
68
  * Build and return the top-level "docs" Commander command group for Polaris docs lifecycle workflows.
43
69
  *
@@ -434,6 +460,34 @@ function createDocsCommand(options = {}) {
434
460
  console.log(`skipped (draft exists): ${targetPath}/index.md`);
435
461
  }
436
462
  });
463
+ docs
464
+ .command("reconcile")
465
+ .description("Reconcile POLARIS.md and SUMMARY.md files across all eligible route folders")
466
+ .option("--all", "Reconcile all eligible directories")
467
+ .option("--dry-run", "Print what would be changed without writing files")
468
+ .option("--include-agent-folders", "Include .codex, .claude, .agents folders (default: skip)")
469
+ .option("--include-hidden", "Include all hidden directories starting with . (default: skip)")
470
+ .option("--include-root", "Include root directory (default: skip)")
471
+ .option("-r, --repo-root <path>", "Repository root", defaultRepoRoot)
472
+ .action((options) => {
473
+ if (!options.all) {
474
+ console.error("Error: provide --all to reconcile all eligible directories");
475
+ process.exit(1);
476
+ }
477
+ try {
478
+ const report = (0, seed_instructions_js_1.reconcileAll)(options.repoRoot, {
479
+ dryRun: options.dryRun,
480
+ includeAgentFolders: options.includeAgentFolders,
481
+ includeHidden: options.includeHidden,
482
+ includeRoot: options.includeRoot,
483
+ });
484
+ printReconcileReport(report, options.dryRun);
485
+ }
486
+ catch (err) {
487
+ console.error(`polaris docs reconcile: ${err instanceof Error ? err.message : String(err)}`);
488
+ process.exit(1);
489
+ }
490
+ });
437
491
  docs
438
492
  .command("backfill-type")
439
493
  .description("Add OKF-conformant `type` frontmatter to existing smartdocs/ files that are missing it, " +
@@ -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(content);
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(content)) !== null) {
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 = summaryContent.split("\n");
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(content, summaryContent, routeName);
350
+ const similarity = computeNormalizedSimilarity(polarisGenerated, summaryGenerated, routeName);
309
351
  if (content === summaryContent) {
310
352
  findings.push({
311
353
  severity: "ERROR",