@lsctech/polaris 0.4.4 → 0.4.6
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-command.js +2 -1
- package/dist/cli/adopt-genesis.js +17 -7
- package/dist/cli/adopt-smartdocs.js +70 -30
- package/dist/cognition/librarian-packet.js +16 -7
- package/dist/loop/adapters/cli-subtask-bridge.js +1 -0
- package/dist/loop/adapters/terminal-cli.js +1 -0
- package/dist/loop/body-parser.js +40 -1
- package/dist/loop/continue.js +71 -0
- package/dist/smartdocs-engine/canon-check.js +80 -0
- package/dist/smartdocs-engine/doctrine.js +231 -0
- package/dist/smartdocs-engine/index.js +105 -22
- package/dist/smartdocs-engine/seed-instructions.js +210 -0
- package/dist/tracker/adapters/linear/index.js +106 -11
- package/package.json +1 -1
|
@@ -7,6 +7,8 @@ exports.stampIngestFrontMatter = stampIngestFrontMatter;
|
|
|
7
7
|
exports.doctrineDraft = doctrineDraft;
|
|
8
8
|
exports.doctrinePromote = doctrinePromote;
|
|
9
9
|
exports.doctrineDeprecate = doctrineDeprecate;
|
|
10
|
+
exports.detectDoctrineSupersession = detectDoctrineSupersession;
|
|
11
|
+
exports.checkSmartDocsLinks = checkSmartDocsLinks;
|
|
10
12
|
exports.specPromote = specPromote;
|
|
11
13
|
exports.migrateProvenance = migrateProvenance;
|
|
12
14
|
const node_fs_1 = require("node:fs");
|
|
@@ -186,6 +188,59 @@ function appendLifecycle(lifecyclePath, event) {
|
|
|
186
188
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(lifecyclePath), { recursive: true });
|
|
187
189
|
(0, node_fs_1.appendFileSync)(lifecyclePath, JSON.stringify(event) + "\n", "utf-8");
|
|
188
190
|
}
|
|
191
|
+
const DIRECTORY_LOG_HEADER = "# Directory Update Log";
|
|
192
|
+
/**
|
|
193
|
+
* Append a dated prose entry to a per-directory log.md file, following the OKF convention:
|
|
194
|
+
* date-grouped YYYY-MM-DD headings, newest-first, with a bold verb prefix.
|
|
195
|
+
*
|
|
196
|
+
* Creates the file with the canonical header if it does not yet exist. If the
|
|
197
|
+
* newest heading already matches today's date, the entry is inserted under that
|
|
198
|
+
* heading; otherwise a new today's heading is prepended before the existing entries.
|
|
199
|
+
*
|
|
200
|
+
* This function is best-effort: failures during read/write are caught and logged
|
|
201
|
+
* as warnings but do not propagate to the caller, ensuring log.md update failures
|
|
202
|
+
* do not block directory transitions.
|
|
203
|
+
*
|
|
204
|
+
* @param directory - Directory that contains (or will contain) log.md
|
|
205
|
+
* @param verb - Lifecycle verb rendered as the bold prefix (Draft, Promote, Deprecate)
|
|
206
|
+
* @param reason - Human-readable prose entry for this change
|
|
207
|
+
*/
|
|
208
|
+
function logDirectoryChange(directory, verb, reason) {
|
|
209
|
+
try {
|
|
210
|
+
const logPath = (0, node_path_1.join)(directory, "log.md");
|
|
211
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
212
|
+
const entry = `**${verb}**: ${reason}`;
|
|
213
|
+
let content = (0, node_fs_1.existsSync)(logPath) ? (0, node_fs_1.readFileSync)(logPath, "utf-8") : "";
|
|
214
|
+
content = content.replace(/\r\n/g, "\n");
|
|
215
|
+
const todayHeading = `## ${today}`;
|
|
216
|
+
// Use anchored regex to match heading at start of line, not substring in prose.
|
|
217
|
+
const firstHeadingMatch = content.match(/^## (\d{4}-\d{2}-\d{2})/m);
|
|
218
|
+
if (firstHeadingMatch && firstHeadingMatch[1] === today) {
|
|
219
|
+
// Find the actual heading line position using anchored search.
|
|
220
|
+
const headingMatch = content.match(new RegExp(`^${todayHeading}`, 'm'));
|
|
221
|
+
if (headingMatch && headingMatch.index !== undefined) {
|
|
222
|
+
const headingIndex = headingMatch.index;
|
|
223
|
+
const afterHeading = headingIndex + todayHeading.length;
|
|
224
|
+
const newContent = `${content.slice(0, afterHeading)}\n${entry}${content.slice(afterHeading)}`;
|
|
225
|
+
(0, node_fs_1.writeFileSync)(logPath, newContent, "utf-8");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if (content.trim().startsWith(DIRECTORY_LOG_HEADER)) {
|
|
229
|
+
const afterHeader = content.indexOf(DIRECTORY_LOG_HEADER) + DIRECTORY_LOG_HEADER.length;
|
|
230
|
+
const tail = content.slice(afterHeader).replace(/^\n*/, "\n\n");
|
|
231
|
+
const newContent = `${DIRECTORY_LOG_HEADER}\n\n${todayHeading}\n${entry}${tail}`;
|
|
232
|
+
(0, node_fs_1.writeFileSync)(logPath, newContent, "utf-8");
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
const existing = content.trim() ? `${content}\n\n` : "";
|
|
236
|
+
(0, node_fs_1.writeFileSync)(logPath, `${DIRECTORY_LOG_HEADER}\n\n${todayHeading}\n${entry}\n${existing}`, "utf-8");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
// Best-effort: log warning but do not propagate failure.
|
|
241
|
+
console.warn(`[warn] Failed to update log.md in ${directory}: ${err instanceof Error ? err.message : String(err)}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
189
244
|
/**
|
|
190
245
|
* Resolve a filesystem path against a repository root and return an absolute path.
|
|
191
246
|
*
|
|
@@ -236,6 +291,8 @@ function doctrineDraft(path, options) {
|
|
|
236
291
|
destination,
|
|
237
292
|
timestamp: new Date().toISOString(),
|
|
238
293
|
});
|
|
294
|
+
const reason = options.reason ?? `${(0, node_path_1.basename)(destination)} drafted to doctrine/candidate/`;
|
|
295
|
+
logDirectoryChange((0, node_path_1.dirname)(destination), "Draft", reason);
|
|
239
296
|
return { source, destination, runId, lifecyclePath };
|
|
240
297
|
}
|
|
241
298
|
/**
|
|
@@ -322,6 +379,8 @@ function doctrinePromote(path, options) {
|
|
|
322
379
|
destination,
|
|
323
380
|
timestamp: new Date().toISOString(),
|
|
324
381
|
});
|
|
382
|
+
const reason = options.reason ?? `${(0, node_path_1.basename)(destination)} promoted to doctrine/active/`;
|
|
383
|
+
logDirectoryChange((0, node_path_1.dirname)(destination), "Promote", reason);
|
|
325
384
|
return { source, destination, runId, lifecyclePath };
|
|
326
385
|
}
|
|
327
386
|
/**
|
|
@@ -371,6 +430,8 @@ function doctrineDeprecate(path, options) {
|
|
|
371
430
|
deprecated_at: deprecatedAt,
|
|
372
431
|
timestamp: deprecatedAt,
|
|
373
432
|
});
|
|
433
|
+
const reason = options.reason ?? `${(0, node_path_1.basename)(destination)} deprecated to doctrine/deprecated/`;
|
|
434
|
+
logDirectoryChange((0, node_path_1.dirname)(destination), "Deprecate", reason);
|
|
374
435
|
return { source, destination, runId, lifecyclePath };
|
|
375
436
|
}
|
|
376
437
|
// ── Spec verb-keyword conflict detection ──────────────────────────────────────
|
|
@@ -390,6 +451,174 @@ function extractSpecKeywords(content, pattern) {
|
|
|
390
451
|
}
|
|
391
452
|
return result;
|
|
392
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* Compute the Jaccard similarity between two keyword sets.
|
|
456
|
+
* Returns a value in [0, 1]; 0 means no overlap, 1 means identical.
|
|
457
|
+
*/
|
|
458
|
+
function jaccardSimilarity(a, b) {
|
|
459
|
+
if (a.size === 0 && b.size === 0)
|
|
460
|
+
return 0;
|
|
461
|
+
let intersection = 0;
|
|
462
|
+
for (const kw of a) {
|
|
463
|
+
if (b.has(kw))
|
|
464
|
+
intersection++;
|
|
465
|
+
}
|
|
466
|
+
return intersection / (a.size + b.size - intersection);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Extract a combined keyword set from a document using both REQUIRES and PROHIBITS patterns.
|
|
470
|
+
* Reuses the existing extractSpecKeywords function — no second algorithm.
|
|
471
|
+
*/
|
|
472
|
+
function extractAllKeywords(content) {
|
|
473
|
+
const requires = extractSpecKeywords(content, MODAL_REQUIRES);
|
|
474
|
+
const prohibits = extractSpecKeywords(content, MODAL_PROHIBITS);
|
|
475
|
+
return new Set([...requires, ...prohibits]);
|
|
476
|
+
}
|
|
477
|
+
// Jaccard threshold above which an active doc is considered a supersession candidate.
|
|
478
|
+
// 0.3 is deliberately permissive: better to surface an advisory than to miss it.
|
|
479
|
+
const SUPERSESSION_THRESHOLD = 0.3;
|
|
480
|
+
/**
|
|
481
|
+
* Detect whether an incoming doctrine-candidate document has high content overlap with
|
|
482
|
+
* any existing active doctrine document. When overlap exceeds the threshold, the active
|
|
483
|
+
* doc is returned as a suggested-supersession advisory conflict.
|
|
484
|
+
*
|
|
485
|
+
* This is report-only: it never writes to `supersedes`/`superseded_by` frontmatter.
|
|
486
|
+
* The caller (or the user via CLI) decides whether to act on the suggestion.
|
|
487
|
+
*
|
|
488
|
+
* @param candidatePath - Absolute or repo-relative path to the candidate document
|
|
489
|
+
* @param options - Doctrine options (must include `repoRoot`)
|
|
490
|
+
* @returns Array of SpecConflict entries with type "suggested-supersession"; empty when
|
|
491
|
+
* no active docs exist or no overlap exceeds the threshold
|
|
492
|
+
*/
|
|
493
|
+
function detectDoctrineSupersession(candidatePath, options) {
|
|
494
|
+
const repoRoot = (0, node_path_1.resolve)(options.repoRoot);
|
|
495
|
+
const source = resolvePath(candidatePath, repoRoot);
|
|
496
|
+
if (!(0, node_fs_1.existsSync)(source))
|
|
497
|
+
return [];
|
|
498
|
+
let candidateContent;
|
|
499
|
+
try {
|
|
500
|
+
candidateContent = (0, node_fs_1.readFileSync)(source, "utf-8");
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
const candidateKeywords = extractAllKeywords(candidateContent);
|
|
506
|
+
// No keywords in candidate → no meaningful overlap can be computed
|
|
507
|
+
if (candidateKeywords.size === 0)
|
|
508
|
+
return [];
|
|
509
|
+
const activeDir = (0, node_path_1.resolve)(repoRoot, "smartdocs", "doctrine", "active");
|
|
510
|
+
if (!(0, node_fs_1.existsSync)(activeDir))
|
|
511
|
+
return [];
|
|
512
|
+
const conflicts = [];
|
|
513
|
+
let activeFiles;
|
|
514
|
+
try {
|
|
515
|
+
activeFiles = (0, node_fs_1.readdirSync)(activeDir).filter((f) => f.endsWith(".md"));
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
for (const file of activeFiles) {
|
|
521
|
+
let activeContent;
|
|
522
|
+
try {
|
|
523
|
+
activeContent = (0, node_fs_1.readFileSync)((0, node_path_1.join)(activeDir, file), "utf-8");
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const activeKeywords = extractAllKeywords(activeContent);
|
|
529
|
+
if (activeKeywords.size === 0)
|
|
530
|
+
continue;
|
|
531
|
+
const score = jaccardSimilarity(candidateKeywords, activeKeywords);
|
|
532
|
+
if (score >= SUPERSESSION_THRESHOLD) {
|
|
533
|
+
conflicts.push({
|
|
534
|
+
type: "suggested-supersession",
|
|
535
|
+
conflictingFile: file,
|
|
536
|
+
detail: `candidate has ${Math.round(score * 100)}% keyword overlap with active doc "${file}" — consider adding supersedes: ${file.replace(/\.md$/, "")} to frontmatter`,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return conflicts;
|
|
541
|
+
}
|
|
542
|
+
// Regex to extract markdown links: [text](href)
|
|
543
|
+
const MD_LINK_RE = /\[([^\]]*)\]\(([^)]+)\)/g;
|
|
544
|
+
// Tiers that are subject to strict link checking (raw/ is permissive per OKF §5.3)
|
|
545
|
+
const STRICT_LINK_TIERS = ["candidate", "active"];
|
|
546
|
+
/**
|
|
547
|
+
* Determine whether a file path is in a strict-check tier (candidate/ or active/)
|
|
548
|
+
* under smartdocs/. Returns false for raw/ or anything outside smartdocs/.
|
|
549
|
+
*/
|
|
550
|
+
function isStrictTier(filePath, repoRoot) {
|
|
551
|
+
const rel = (0, node_path_1.relative)((0, node_path_1.resolve)(repoRoot), (0, node_path_1.resolve)(filePath)).replace(/\\/g, "/");
|
|
552
|
+
if (!rel.startsWith("smartdocs/"))
|
|
553
|
+
return false;
|
|
554
|
+
return STRICT_LINK_TIERS.some((tier) => rel.includes(`/${tier}/`) || rel.includes(`/${tier}\\`));
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Resolve a markdown link href found in `sourceFile` to an absolute filesystem path.
|
|
558
|
+
*
|
|
559
|
+
* Supports two forms:
|
|
560
|
+
* - Bundle-relative: `/smartdocs/...` — resolved from repoRoot
|
|
561
|
+
* - Relative: `./foo.md`, `../foo.md`, `foo.md` — resolved from the source file's directory
|
|
562
|
+
*
|
|
563
|
+
* Returns null if the href does not point into smartdocs/ (e.g. external URLs, src/ links).
|
|
564
|
+
*/
|
|
565
|
+
function resolveSmartDocsLink(href, sourceFile, repoRoot) {
|
|
566
|
+
// Strip anchors
|
|
567
|
+
const bare = href.split("#")[0];
|
|
568
|
+
if (!bare.endsWith(".md"))
|
|
569
|
+
return null;
|
|
570
|
+
let abs;
|
|
571
|
+
if (bare.startsWith("/")) {
|
|
572
|
+
// Bundle-relative: treat / as repoRoot
|
|
573
|
+
abs = (0, node_path_1.join)((0, node_path_1.resolve)(repoRoot), bare.slice(1));
|
|
574
|
+
}
|
|
575
|
+
else if (bare.startsWith("http://") || bare.startsWith("https://")) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
// Relative to source file's directory
|
|
580
|
+
abs = (0, node_path_1.join)((0, node_path_1.dirname)((0, node_path_1.resolve)(sourceFile)), bare);
|
|
581
|
+
}
|
|
582
|
+
// Only check links that land inside smartdocs/
|
|
583
|
+
const rel = (0, node_path_1.relative)((0, node_path_1.resolve)(repoRoot), abs).replace(/\\/g, "/");
|
|
584
|
+
if (!rel.startsWith("smartdocs/"))
|
|
585
|
+
return null;
|
|
586
|
+
return abs;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Check all markdown cross-links in a strict-tier (candidate/ or active/) SmartDoc.
|
|
590
|
+
*
|
|
591
|
+
* For each link whose href resolves into smartdocs/**, verifies the target exists.
|
|
592
|
+
* Missing targets are returned as SpecConflict entries with type "stale-assumption".
|
|
593
|
+
* Files from raw/ are never checked — pass isRaw=true or detect via filePath.
|
|
594
|
+
*
|
|
595
|
+
* @param filePath - Absolute path to the source document
|
|
596
|
+
* @param content - File content (already read by caller)
|
|
597
|
+
* @param repoRoot - Repository root directory
|
|
598
|
+
* @returns Array of stale-assumption conflicts for every broken smartdocs/ link
|
|
599
|
+
*/
|
|
600
|
+
function checkSmartDocsLinks(filePath, content, repoRoot) {
|
|
601
|
+
if (!isStrictTier(filePath, repoRoot))
|
|
602
|
+
return [];
|
|
603
|
+
const conflicts = [];
|
|
604
|
+
const re = new RegExp(MD_LINK_RE.source, MD_LINK_RE.flags);
|
|
605
|
+
for (const match of content.matchAll(re)) {
|
|
606
|
+
const href = match[2];
|
|
607
|
+
if (!href)
|
|
608
|
+
continue;
|
|
609
|
+
const target = resolveSmartDocsLink(href, filePath, repoRoot);
|
|
610
|
+
if (target === null)
|
|
611
|
+
continue; // not a smartdocs/ link — skip
|
|
612
|
+
if (!(0, node_fs_1.existsSync)(target)) {
|
|
613
|
+
conflicts.push({
|
|
614
|
+
type: "stale-assumption",
|
|
615
|
+
conflictingFile: (0, node_path_1.relative)((0, node_path_1.resolve)(repoRoot), (0, node_path_1.resolve)(filePath)).replace(/\\/g, "/"),
|
|
616
|
+
detail: `broken link: "${href}" → target not found: ${(0, node_path_1.relative)((0, node_path_1.resolve)(repoRoot), target).replace(/\\/g, "/")}`,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return conflicts;
|
|
621
|
+
}
|
|
393
622
|
/**
|
|
394
623
|
* Promotes a spec file from smartdocs/raw/ into smartdocs/specs/active/.
|
|
395
624
|
*
|
|
@@ -529,6 +758,8 @@ function specPromote(path, options) {
|
|
|
529
758
|
approved: options.approve ?? false,
|
|
530
759
|
timestamp: new Date().toISOString(),
|
|
531
760
|
});
|
|
761
|
+
const reason = options.reason ?? `${(0, node_path_1.basename)(destination)} promoted to specs/active/`;
|
|
762
|
+
logDirectoryChange((0, node_path_1.dirname)(destination), "Promote", reason);
|
|
532
763
|
return { source, destination, runId, lifecyclePath, conflicts, halted: false, report };
|
|
533
764
|
}
|
|
534
765
|
/**
|
|
@@ -15,6 +15,29 @@ const audit_js_1 = require("./audit.js");
|
|
|
15
15
|
const triage_js_1 = require("./triage.js");
|
|
16
16
|
const adapter_js_1 = require("../graph/store/adapter.js");
|
|
17
17
|
const index_js_1 = require("../graph/query/index.js");
|
|
18
|
+
/**
|
|
19
|
+
* Prints the shared written/skipped-exists/skipped-draft report and summary line used by
|
|
20
|
+
* every `seed-*-all` command (seed-index, seed-instructions, seed-summary, reformat-okf).
|
|
21
|
+
*
|
|
22
|
+
* @param filename - The generated filename (e.g. "index.md") to report against each directory.
|
|
23
|
+
* @param dryRun - Whether this was a dry-run pass (controls the "would write" vs "written" label).
|
|
24
|
+
* @param result - The written/skippedExists/skippedDraft arrays from a seed-*-all call.
|
|
25
|
+
* @param options.leadingBlankLine - Whether to prefix the summary line with a blank line (default true).
|
|
26
|
+
* @param options.extraSummary - Extra summary text appended after "skipped (draft)" (e.g. root/ineligible counts).
|
|
27
|
+
*/
|
|
28
|
+
function printSeedAllResult(filename, dryRun, { written, skippedExists, skippedDraft }, options = {}) {
|
|
29
|
+
const { leadingBlankLine = true, extraSummary = "" } = options;
|
|
30
|
+
for (const dir of written) {
|
|
31
|
+
console.log(`${dryRun ? "[dry-run] would write" : "written"}: ${dir}/${filename}`);
|
|
32
|
+
}
|
|
33
|
+
for (const dir of skippedExists) {
|
|
34
|
+
console.log(`skipped (human-edited): ${dir}/${filename}`);
|
|
35
|
+
}
|
|
36
|
+
for (const dir of skippedDraft) {
|
|
37
|
+
console.log(`skipped (draft exists): ${dir}/${filename}`);
|
|
38
|
+
}
|
|
39
|
+
console.log(`${leadingBlankLine ? "\n" : ""}Done. ${written.length} written, ${skippedExists.length} skipped (exists), ${skippedDraft.length} skipped (draft)${extraSummary}.`);
|
|
40
|
+
}
|
|
18
41
|
/**
|
|
19
42
|
* Build and return the top-level "docs" Commander command group for Polaris docs lifecycle workflows.
|
|
20
43
|
*
|
|
@@ -283,15 +306,6 @@ function createDocsCommand(options = {}) {
|
|
|
283
306
|
includeHidden: options.includeHidden,
|
|
284
307
|
includeRoot: options.includeRoot,
|
|
285
308
|
});
|
|
286
|
-
for (const dir of written) {
|
|
287
|
-
console.log(`${options.dryRun ? "[dry-run] would write" : "written"}: ${dir}/POLARIS.md`);
|
|
288
|
-
}
|
|
289
|
-
for (const dir of skippedExists) {
|
|
290
|
-
console.log(`skipped (human-edited): ${dir}/POLARIS.md`);
|
|
291
|
-
}
|
|
292
|
-
for (const dir of skippedDraft) {
|
|
293
|
-
console.log(`skipped (draft exists): ${dir}/POLARIS.md`);
|
|
294
|
-
}
|
|
295
309
|
if (options.dryRun) {
|
|
296
310
|
if (skippedRoot) {
|
|
297
311
|
console.log(`\nSkipped (root):`);
|
|
@@ -315,7 +329,7 @@ function createDocsCommand(options = {}) {
|
|
|
315
329
|
}
|
|
316
330
|
}
|
|
317
331
|
const rootCount = skippedRoot ? 1 : 0;
|
|
318
|
-
|
|
332
|
+
printSeedAllResult("POLARIS.md", options.dryRun, { written, skippedExists, skippedDraft }, { extraSummary: `, ${rootCount} skipped (root), ${skippedIneligible.length} skipped (ineligible)` });
|
|
319
333
|
return;
|
|
320
334
|
}
|
|
321
335
|
if (!pathArg) {
|
|
@@ -351,15 +365,6 @@ function createDocsCommand(options = {}) {
|
|
|
351
365
|
includeHidden: options.includeHidden,
|
|
352
366
|
includeRoot: options.includeRoot,
|
|
353
367
|
});
|
|
354
|
-
for (const dir of written) {
|
|
355
|
-
console.log(`${options.dryRun ? "[dry-run] would write" : "written"}: ${dir}/SUMMARY.md`);
|
|
356
|
-
}
|
|
357
|
-
for (const dir of skippedExists) {
|
|
358
|
-
console.log(`skipped (human-edited): ${dir}/SUMMARY.md`);
|
|
359
|
-
}
|
|
360
|
-
for (const dir of skippedDraft) {
|
|
361
|
-
console.log(`skipped (draft exists): ${dir}/SUMMARY.md`);
|
|
362
|
-
}
|
|
363
368
|
if (options.dryRun) {
|
|
364
369
|
if (skippedRoot) {
|
|
365
370
|
console.log(`\nSkipped (root):`);
|
|
@@ -383,7 +388,7 @@ function createDocsCommand(options = {}) {
|
|
|
383
388
|
}
|
|
384
389
|
}
|
|
385
390
|
const rootCount = skippedRoot ? 1 : 0;
|
|
386
|
-
|
|
391
|
+
printSeedAllResult("SUMMARY.md", options.dryRun, { written, skippedExists, skippedDraft }, { extraSummary: `, ${rootCount} skipped (root), ${skippedIneligible.length} skipped (ineligible)` });
|
|
387
392
|
return;
|
|
388
393
|
}
|
|
389
394
|
if (!pathArg) {
|
|
@@ -402,6 +407,77 @@ function createDocsCommand(options = {}) {
|
|
|
402
407
|
console.log(`skipped (draft exists): ${pathArg}/SUMMARY.md`);
|
|
403
408
|
}
|
|
404
409
|
});
|
|
410
|
+
docs
|
|
411
|
+
.command("seed-index [path]")
|
|
412
|
+
.description("Generate OKF-conformant index.md files for smartdocs/")
|
|
413
|
+
.option("-r, --repo-root <path>", "Repository root", defaultRepoRoot)
|
|
414
|
+
.option("--all", "Generate index.md for all smartdocs directories lacking one")
|
|
415
|
+
.option("--dry-run", "Print what would be written without writing files")
|
|
416
|
+
.action((pathArg, options) => {
|
|
417
|
+
if (options.all) {
|
|
418
|
+
const { written, skippedExists, skippedDraft, } = (0, seed_instructions_js_1.seedIndexAll)(options.repoRoot, {
|
|
419
|
+
dryRun: options.dryRun,
|
|
420
|
+
});
|
|
421
|
+
printSeedAllResult("index.md", options.dryRun, { written, skippedExists, skippedDraft });
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const targetPath = pathArg || "smartdocs";
|
|
425
|
+
const result = (0, seed_instructions_js_1.seedIndex)(targetPath, options.repoRoot, { dryRun: options.dryRun });
|
|
426
|
+
if (result === "written") {
|
|
427
|
+
const label = options.dryRun ? "[dry-run] would write" : "written";
|
|
428
|
+
console.log(`${label}: ${targetPath}/index.md`);
|
|
429
|
+
}
|
|
430
|
+
else if (result === "skipped-exists") {
|
|
431
|
+
console.warn(`warning: ${targetPath}/index.md already exists (no draft marker) — skipped`);
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
console.log(`skipped (draft exists): ${targetPath}/index.md`);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
docs
|
|
438
|
+
.command("reformat-okf")
|
|
439
|
+
.description("Migrate existing smartdocs to OKF structure in one step: runs migrate → seed-index --all → seed-instructions --all. " +
|
|
440
|
+
"Agent instruction files (CLAUDE.md, AGENTS.md, etc.) are never touched. " +
|
|
441
|
+
"Use --dry-run to preview changes before writing.")
|
|
442
|
+
.option("--dry-run", "Preview what would change without writing any files")
|
|
443
|
+
.option("-r, --repo-root <path>", "Repository root", defaultRepoRoot)
|
|
444
|
+
.action((options) => {
|
|
445
|
+
const dryRun = options.dryRun;
|
|
446
|
+
const repoRoot = options.repoRoot;
|
|
447
|
+
const label = dryRun ? "[dry-run]" : "";
|
|
448
|
+
// Step 1: migrate (moves scattered markdown to smartdocs/raw/)
|
|
449
|
+
console.log(`${label ? label + " " : ""}Step 1/3: migrate`);
|
|
450
|
+
try {
|
|
451
|
+
const migrateResult = (0, migrate_js_1.migrateDocs)({ repoRoot, dryRun });
|
|
452
|
+
(0, migrate_js_1.printMigrateResults)(migrateResult);
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
console.error(`reformat-okf: migrate failed — ${err instanceof Error ? err.message : String(err)}`);
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
// Step 2: seed-index --all (ensures index.md with okf_version frontmatter in every smartdocs dir)
|
|
459
|
+
console.log(`\n${label ? label + " " : ""}Step 2/3: seed-index --all`);
|
|
460
|
+
try {
|
|
461
|
+
const { written, skippedExists, skippedDraft } = (0, seed_instructions_js_1.seedIndexAll)(repoRoot, { dryRun });
|
|
462
|
+
printSeedAllResult("index.md", dryRun, { written, skippedExists, skippedDraft }, { leadingBlankLine: false });
|
|
463
|
+
}
|
|
464
|
+
catch (err) {
|
|
465
|
+
console.error(`reformat-okf: seed-index failed — ${err instanceof Error ? err.message : String(err)}`);
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
// Step 3: seed-instructions --all (ensures POLARIS.md drafts; never touches CLAUDE.md/AGENTS.md)
|
|
469
|
+
console.log(`\n${label ? label + " " : ""}Step 3/3: seed-instructions --all`);
|
|
470
|
+
try {
|
|
471
|
+
const { written, skippedExists, skippedDraft, skippedIneligible, skippedRoot } = (0, seed_instructions_js_1.seedInstructionsAll)(repoRoot, { dryRun });
|
|
472
|
+
const rootCount = skippedRoot ? 1 : 0;
|
|
473
|
+
printSeedAllResult("POLARIS.md", dryRun, { written, skippedExists, skippedDraft }, { leadingBlankLine: false, extraSummary: `, ${rootCount} skipped (root), ${skippedIneligible.length} skipped (ineligible)` });
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
console.error(`reformat-okf: seed-instructions failed — ${err instanceof Error ? err.message : String(err)}`);
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
console.log(`\nreformat-okf complete.${dryRun ? " (dry-run — no files written)" : ""}`);
|
|
480
|
+
});
|
|
405
481
|
docs
|
|
406
482
|
.command("audit")
|
|
407
483
|
.description("Scan repo for files at risk of recursive ingestion")
|
|
@@ -464,9 +540,10 @@ function createDoctrineCommand() {
|
|
|
464
540
|
.description("Move a doc from smartdocs/raw/ or smartdocs/doctrine/raw/ to docs/doctrine/candidate/")
|
|
465
541
|
.option("-r, --repo-root <path>", "Repository root", process.cwd())
|
|
466
542
|
.option("--run-id <id>", "Override the generated doctrine run ID")
|
|
543
|
+
.option("--reason <text>", "Reason for this draft")
|
|
467
544
|
.action((path, options) => {
|
|
468
545
|
try {
|
|
469
|
-
const result = (0, doctrine_js_1.doctrineDraft)(path, { repoRoot: options.repoRoot, runId: options.runId });
|
|
546
|
+
const result = (0, doctrine_js_1.doctrineDraft)(path, { repoRoot: options.repoRoot, runId: options.runId, reason: options.reason });
|
|
470
547
|
console.log(`drafted: ${result.destination}`);
|
|
471
548
|
console.log(`provenance: ${result.lifecyclePath}`);
|
|
472
549
|
}
|
|
@@ -480,11 +557,13 @@ function createDoctrineCommand() {
|
|
|
480
557
|
.description("Move a doc from smartdocs/doctrine/candidate/ to smartdocs/doctrine/active/")
|
|
481
558
|
.option("-r, --repo-root <path>", "Repository root", process.cwd())
|
|
482
559
|
.option("--run-id <id>", "Override the generated doctrine run ID")
|
|
560
|
+
.option("--reason <text>", "Reason for this promotion")
|
|
483
561
|
.action((path, options) => {
|
|
484
562
|
try {
|
|
485
563
|
const result = (0, doctrine_js_1.doctrinePromote)(path, {
|
|
486
564
|
repoRoot: options.repoRoot,
|
|
487
|
-
runId: options.runId
|
|
565
|
+
runId: options.runId,
|
|
566
|
+
reason: options.reason,
|
|
488
567
|
});
|
|
489
568
|
console.log(`promoted: ${result.destination}`);
|
|
490
569
|
console.log(`provenance: ${result.lifecyclePath}`);
|
|
@@ -499,11 +578,13 @@ function createDoctrineCommand() {
|
|
|
499
578
|
.description("Move a doc from smartdocs/doctrine/active/ to smartdocs/doctrine/deprecated/")
|
|
500
579
|
.option("-r, --repo-root <path>", "Repository root", process.cwd())
|
|
501
580
|
.option("--run-id <id>", "Override the generated doctrine run ID")
|
|
581
|
+
.option("--reason <text>", "Reason for this deprecation")
|
|
502
582
|
.action((path, options) => {
|
|
503
583
|
try {
|
|
504
584
|
const result = (0, doctrine_js_1.doctrineDeprecate)(path, {
|
|
505
585
|
repoRoot: options.repoRoot,
|
|
506
586
|
runId: options.runId,
|
|
587
|
+
reason: options.reason,
|
|
507
588
|
});
|
|
508
589
|
console.log(`deprecated: ${result.destination}`);
|
|
509
590
|
console.log(`provenance: ${result.lifecyclePath}`);
|
|
@@ -519,12 +600,14 @@ function createDoctrineCommand() {
|
|
|
519
600
|
.option("-r, --repo-root <path>", "Repository root", process.cwd())
|
|
520
601
|
.option("--run-id <id>", "Override the generated run ID")
|
|
521
602
|
.option("--approve", "Proceed despite detected conflicts")
|
|
603
|
+
.option("--reason <text>", "Reason for this promotion")
|
|
522
604
|
.action((path, options) => {
|
|
523
605
|
try {
|
|
524
606
|
const result = (0, doctrine_js_1.specPromote)(path, {
|
|
525
607
|
repoRoot: options.repoRoot,
|
|
526
608
|
runId: options.runId,
|
|
527
609
|
approve: options.approve,
|
|
610
|
+
reason: options.reason,
|
|
528
611
|
});
|
|
529
612
|
console.log(result.report);
|
|
530
613
|
if (result.halted) {
|