@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.
- package/dist/cli/adopt-canon.js +22 -8
- package/dist/finalize/artifact-policy.js +1 -24
- package/dist/finalize/index.js +13 -17
- package/dist/finalize/run-report.js +3 -3
- package/dist/finalize/steps/06-commit.js +1 -2
- package/dist/finalize/steps/12-archive.js +1 -1
- package/dist/loop/adapters/terminal-cli.js +31 -3
- package/dist/loop/body-parser.js +4 -0
- package/dist/loop/continue.js +3 -3
- package/dist/loop/dispatch.js +1 -1
- package/dist/loop/orphan-recovery.js +2 -1
- package/dist/loop/parent.js +28 -12
- package/dist/loop/run-bootstrap.js +3 -1
- package/dist/map/inference.js +4 -1
- package/dist/medic/run-health-consult.js +5 -4
- package/dist/medic/treatment-packets.js +5 -1
- package/dist/qc/providers/coderabbit.js +2 -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/workspace/POLARIS.md +4 -0
- package/dist/workspace/SUMMARY.md +56 -0
- package/package.json +1 -1
|
@@ -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
|
|
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, "
|
|
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, "
|
|
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("
|
|
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, "
|
|
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, "
|
|
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(
|
|
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",
|