project-librarian 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +245 -93
- package/README.md +174 -76
- package/SKILL.md +15 -10
- package/dist/args.js +11 -2
- package/dist/code-index-file-policy.js +105 -2
- package/dist/code-index.js +77 -59
- package/dist/hooks.js +78 -0
- package/dist/init-project-wiki.js +234 -166
- package/dist/install-skill.js +2 -9
- package/dist/mcp-server.js +449 -0
- package/dist/migration.js +875 -60
- package/dist/modes.js +205 -59
- package/dist/taxonomy.js +193 -0
- package/dist/templates.js +214 -36
- package/dist/wiki-files.js +15 -27
- package/dist/wiki-graph.js +141 -0
- package/package.json +3 -3
- package/README.ja.md +0 -215
- package/README.zh.md +0 -215
package/dist/modes.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.buildRefreshIndexBlock = buildRefreshIndexBlock;
|
|
37
37
|
exports.runQueryMode = runQueryMode;
|
|
38
|
+
exports.runWikiImpactMode = runWikiImpactMode;
|
|
38
39
|
exports.projectCandidatesContent = projectCandidatesContent;
|
|
39
40
|
exports.appendCaptureInbox = appendCaptureInbox;
|
|
40
41
|
exports.runIssueDraftMode = runIssueDraftMode;
|
|
@@ -49,6 +50,7 @@ exports.runQualityCheckMode = runQualityCheckMode;
|
|
|
49
50
|
exports.runMigrationQualityCheckMode = runMigrationQualityCheckMode;
|
|
50
51
|
exports.runMigrationLintMode = runMigrationLintMode;
|
|
51
52
|
exports.runMigrationDoctorMode = runMigrationDoctorMode;
|
|
53
|
+
exports.collectRouterTruthDiagnostics = collectRouterTruthDiagnostics;
|
|
52
54
|
exports.runDoctorMode = runDoctorMode;
|
|
53
55
|
exports.runLintMode = runLintMode;
|
|
54
56
|
const fs = __importStar(require("node:fs"));
|
|
@@ -60,6 +62,7 @@ const workspace_1 = require("./workspace");
|
|
|
60
62
|
const templates_1 = require("./templates");
|
|
61
63
|
const migration_1 = require("./migration");
|
|
62
64
|
const wiki_files_1 = require("./wiki-files");
|
|
65
|
+
const wiki_graph_1 = require("./wiki-graph");
|
|
63
66
|
const scopedAutoIndexThreshold = 40;
|
|
64
67
|
const scopedAutoIndexMarker = "<!-- PROJECT-WIKI-SCOPED-AUTO-INDEX -->";
|
|
65
68
|
function isScopedAutoIndex(file) {
|
|
@@ -167,26 +170,46 @@ This block is managed by \`--refresh-index\`. Move useful rows into a hand-writt
|
|
|
167
170
|
| --- | --- | --- | --- |
|
|
168
171
|
${rows}<!-- PROJECT-WIKI-AUTO-INDEX:END -->`;
|
|
169
172
|
}
|
|
173
|
+
// Answer-shaped query output (2026-06-12 method-transfer decision): first line is
|
|
174
|
+
// the answer, each result carries the page's TL;DR first bullet so the agent can
|
|
175
|
+
// pick a page without opening it, and the whole body sits under the shared hard
|
|
176
|
+
// cap with an explicit truncation notice.
|
|
170
177
|
function runQueryMode() {
|
|
171
178
|
if (!args_1.queryTerm.trim()) {
|
|
172
179
|
console.error("missing query: use --query \"search terms\"");
|
|
173
180
|
process.exit(1);
|
|
174
181
|
}
|
|
175
182
|
const terms = args_1.queryTerm.toLowerCase().split(/\s+/).filter(Boolean);
|
|
176
|
-
const
|
|
183
|
+
const matches = (0, wiki_files_1.wikiMarkdownFiles)().map((file) => {
|
|
177
184
|
const text = (0, workspace_1.read)(file);
|
|
178
185
|
const body = (0, workspace_1.stripMetadataHeader)(text);
|
|
179
186
|
const title = (0, wiki_files_1.wikiTitleForFile)(file, text);
|
|
180
187
|
const meta = (0, wiki_files_1.metadataSummary)(file, text);
|
|
181
188
|
const weighted = `${file}\n${title}\n${meta.scope}\n${(0, workspace_1.metadataValue)(text, "tags")}\n${body}`.toLowerCase();
|
|
182
189
|
const score = terms.reduce((sum, term) => sum + (weighted.split(term).length - 1) + (file.toLowerCase().includes(term) ? 3 : 0) + (title.toLowerCase().includes(term) ? 5 : 0), 0);
|
|
183
|
-
return { file, title, score, ...meta };
|
|
184
|
-
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score || a.file.localeCompare(b.file))
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
+
return { file, title, score, tldr: (0, wiki_files_1.firstTldrBullet)(text), ...meta };
|
|
191
|
+
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
192
|
+
const results = matches.slice(0, 10);
|
|
193
|
+
const best = results[0];
|
|
194
|
+
const lines = [best
|
|
195
|
+
? `Project wiki query "${args_1.queryTerm}": best match ${best.file} — ${best.title} (${matches.length} matching page${matches.length === 1 ? "" : "s"}, top ${results.length} shown).`
|
|
196
|
+
: `Project wiki query "${args_1.queryTerm}": no matches.`];
|
|
197
|
+
for (const item of results) {
|
|
198
|
+
lines.push(`${item.score.toString().padStart(3)} ${item.file} [${item.scope}|${item.status}|${item.budget}] ${item.title}`);
|
|
199
|
+
if (item.tldr)
|
|
200
|
+
lines.push(` tldr: ${item.tldr}`);
|
|
201
|
+
}
|
|
202
|
+
console.log((0, wiki_graph_1.finalizeWikiAnswer)(lines.join("\n")));
|
|
203
|
+
}
|
|
204
|
+
// Wiki impact mode: backlink/decision_ref/routing evidence for a page so wiki
|
|
205
|
+
// maintenance can find review candidates when project truth changes.
|
|
206
|
+
function runWikiImpactMode() {
|
|
207
|
+
if (!args_1.wikiImpactTarget.trim()) {
|
|
208
|
+
console.error("missing wiki impact target: use --wiki-impact \"page-or-term\"");
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
const pages = (0, wiki_files_1.wikiMarkdownFiles)().map((file) => ({ file, text: (0, workspace_1.read)(file) }));
|
|
212
|
+
console.log((0, wiki_graph_1.wikiImpactAnswer)(pages, args_1.wikiImpactTarget.trim()));
|
|
190
213
|
}
|
|
191
214
|
function projectCandidatesContent() {
|
|
192
215
|
return `${(0, templates_1.metadata)("inbox", "on-demand", "wiki/meta/wiki-ops-v1-decisions.md", "candidates are adopted, rejected, or stale")}
|
|
@@ -456,15 +479,12 @@ function printDiagnostics(title, diagnostics, checked) {
|
|
|
456
479
|
console.log(`passed: ${checked} wiki markdown files checked, ${warnings} warnings`);
|
|
457
480
|
return true;
|
|
458
481
|
}
|
|
459
|
-
function collectWikiLinkReferences(files) {
|
|
460
|
-
return files.flatMap((file) => (0, wiki_files_1.extractWikiLinks)(file, (0, workspace_1.read)(file)));
|
|
461
|
-
}
|
|
462
482
|
function collectLinkDiagnostics() {
|
|
463
483
|
const diagnostics = [];
|
|
464
484
|
const files = (0, wiki_files_1.wikiMarkdownFiles)();
|
|
465
485
|
const fileSet = new Set(files);
|
|
466
|
-
const
|
|
467
|
-
for (const link of links) {
|
|
486
|
+
const graph = (0, wiki_graph_1.buildWikiGraph)(files.map((file) => ({ file, text: (0, workspace_1.read)(file) })));
|
|
487
|
+
for (const link of graph.links) {
|
|
468
488
|
if (!fileSet.has(link.normalizedTarget)) {
|
|
469
489
|
diagnostics.push({
|
|
470
490
|
code: "broken-link",
|
|
@@ -474,25 +494,29 @@ function collectLinkDiagnostics() {
|
|
|
474
494
|
});
|
|
475
495
|
}
|
|
476
496
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
});
|
|
490
|
-
}
|
|
497
|
+
const indexTargets = new Map();
|
|
498
|
+
for (const link of graph.outgoingLinks.get("wiki/index.md") ?? []) {
|
|
499
|
+
indexTargets.set(link.normalizedTarget, (indexTargets.get(link.normalizedTarget) ?? 0) + 1);
|
|
500
|
+
}
|
|
501
|
+
for (const [target, count] of indexTargets) {
|
|
502
|
+
if (count > 1) {
|
|
503
|
+
diagnostics.push({
|
|
504
|
+
code: "duplicate-route",
|
|
505
|
+
severity: "warn",
|
|
506
|
+
file: "wiki/index.md",
|
|
507
|
+
message: `${count} index routes resolve to ${target}`,
|
|
508
|
+
});
|
|
491
509
|
}
|
|
492
510
|
}
|
|
511
|
+
// Self-links are not connectivity: a page whose only incoming link is its own
|
|
512
|
+
// self-loop has no route into it and must stay an orphan-page finding, keeping
|
|
513
|
+
// the orphan and router-unreachable rules disjoint.
|
|
493
514
|
const incoming = new Map();
|
|
494
|
-
for (const link of links)
|
|
515
|
+
for (const link of graph.links) {
|
|
516
|
+
if (link.file === link.normalizedTarget)
|
|
517
|
+
continue;
|
|
495
518
|
incoming.set(link.normalizedTarget, (incoming.get(link.normalizedTarget) ?? 0) + 1);
|
|
519
|
+
}
|
|
496
520
|
const orphanExemptions = new Set(["wiki/index.md", "wiki/startup.md", "wiki/README.md"]);
|
|
497
521
|
for (const file of files) {
|
|
498
522
|
if (orphanExemptions.has(file))
|
|
@@ -506,6 +530,50 @@ function collectLinkDiagnostics() {
|
|
|
506
530
|
});
|
|
507
531
|
}
|
|
508
532
|
}
|
|
533
|
+
// Bounded router reachability, promoted from the benchmark fixture A1 hard
|
|
534
|
+
// assert (benchmarks/lib/llm-fixtures.js assertBoundedAnswerReachability) to the
|
|
535
|
+
// real wiki: fixture wikis were guaranteed navigable from startup while real
|
|
536
|
+
// wikis were never checked for the same property. Pages with zero incoming
|
|
537
|
+
// links are already the orphan-page rule's finding, so reachability reports
|
|
538
|
+
// only the cases orphan cannot see: linked-but-disconnected islands, an index
|
|
539
|
+
// the startup router never links (hop 1), and routes deeper than the budget.
|
|
540
|
+
// When wiki/startup.md itself is missing, lint owns that as a required-file
|
|
541
|
+
// error and reachability has no root to check from.
|
|
542
|
+
if (fileSet.has(wiki_graph_1.wikiRouterRoot)) {
|
|
543
|
+
const depths = (0, wiki_graph_1.wikiRouterDepths)(graph);
|
|
544
|
+
for (const file of files) {
|
|
545
|
+
if (wiki_graph_1.wikiRouterExemptPages.has(file))
|
|
546
|
+
continue;
|
|
547
|
+
const depth = depths.get(file);
|
|
548
|
+
const isIndex = file === "wiki/index.md";
|
|
549
|
+
if (depth === undefined) {
|
|
550
|
+
if (isIndex) {
|
|
551
|
+
diagnostics.push({
|
|
552
|
+
code: "router-unreachable",
|
|
553
|
+
severity: "warn",
|
|
554
|
+
file,
|
|
555
|
+
message: `${wiki_graph_1.wikiRouterRoot} does not link [[index]], so the router chain never starts (hop 1 broken)`,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
else if ((incoming.get(file) ?? 0) > 0) {
|
|
559
|
+
diagnostics.push({
|
|
560
|
+
code: "router-unreachable",
|
|
561
|
+
severity: "warn",
|
|
562
|
+
file,
|
|
563
|
+
message: `linked only from pages that never connect to ${wiki_graph_1.wikiRouterRoot}; route it from wiki/index.md or a scoped router`,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
else if (depth > wiki_graph_1.wikiRouterDepthBudget) {
|
|
568
|
+
diagnostics.push({
|
|
569
|
+
code: "router-depth-exceeded",
|
|
570
|
+
severity: "warn",
|
|
571
|
+
file,
|
|
572
|
+
message: `reachable from ${wiki_graph_1.wikiRouterRoot} only at depth ${depth} (budget ${wiki_graph_1.wikiRouterDepthBudget}); add a shorter route`,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
509
577
|
return diagnostics.sort((a, b) => a.severity.localeCompare(b.severity) || a.file.localeCompare(b.file) || a.code.localeCompare(b.code));
|
|
510
578
|
}
|
|
511
579
|
function legacyWikiRoots() {
|
|
@@ -585,15 +653,19 @@ function collectMigrationQualityDiagnostics() {
|
|
|
585
653
|
function collectMigrationLintDiagnostics() {
|
|
586
654
|
if (legacyWikiRoots().length === 0)
|
|
587
655
|
return [];
|
|
588
|
-
const
|
|
656
|
+
const requiredCoreFiles = [
|
|
657
|
+
"wiki/meta/document-taxonomy.md",
|
|
589
658
|
"wiki/migration/inventory.md",
|
|
659
|
+
"wiki/migration/unit-map.md",
|
|
660
|
+
"wiki/migration/split-plan.md",
|
|
590
661
|
"wiki/migration/coverage.md",
|
|
591
662
|
"wiki/migration/plan.md",
|
|
663
|
+
"wiki/migration/review.md",
|
|
592
664
|
"wiki/migration/verification.md",
|
|
593
|
-
"wiki/
|
|
594
|
-
"wiki/decisions/migration-inbox.md",
|
|
595
|
-
"wiki/sources/migration-inbox.md",
|
|
665
|
+
"wiki/migration/bulk-review.md",
|
|
596
666
|
];
|
|
667
|
+
const requiredInboxFiles = (0, migration_1.migrationSemanticReviewComplete)() ? [] : [...migration_1.generatedMigrationInboxFiles];
|
|
668
|
+
const requiredFiles = [...requiredCoreFiles, ...requiredInboxFiles];
|
|
597
669
|
const diagnostics = requiredFiles
|
|
598
670
|
.filter((file) => !(0, workspace_1.exists)(file))
|
|
599
671
|
.map((file) => ({
|
|
@@ -603,6 +675,8 @@ function collectMigrationLintDiagnostics() {
|
|
|
603
675
|
message: "migration review files are missing; run --migrate or keep migration diagnostics out of normal doctor",
|
|
604
676
|
}));
|
|
605
677
|
diagnostics.push(...(0, migration_1.collectMigrationCoverageDiagnostics)());
|
|
678
|
+
diagnostics.push(...(0, migration_1.collectMigrationUnitMapDiagnostics)());
|
|
679
|
+
diagnostics.push(...(0, migration_1.collectMigrationSplitPlanDiagnostics)());
|
|
606
680
|
return diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
|
|
607
681
|
}
|
|
608
682
|
function runLinkCheckMode() {
|
|
@@ -631,6 +705,73 @@ function runMigrationDoctorMode() {
|
|
|
631
705
|
if (!lintOk || !qualityOk)
|
|
632
706
|
process.exit(1);
|
|
633
707
|
}
|
|
708
|
+
// B2 router-truth contradiction rule. A compact router that contradicts the
|
|
709
|
+
// decision log is worse than none: the measured 2026-06-10 run spiraled into
|
|
710
|
+
// post-answer verification because wiki/startup.md Recent Decisions and
|
|
711
|
+
// wiki/decisions/recent.md said "None yet." while wiki/decisions/log.md held the
|
|
712
|
+
// dated answer. This flags that exact contradiction as an error-level diagnostic.
|
|
713
|
+
// "None yet." is the bootstrap template marker for an empty decision surface
|
|
714
|
+
// (startup template "## Recent Project Decisions" and recent.md template
|
|
715
|
+
// "## Decisions" both seed "- None yet."), so its presence while the log carries a
|
|
716
|
+
// dated entry is the template-equivalent of an unmaintained router.
|
|
717
|
+
//
|
|
718
|
+
// SECTION-ANCHORED SCAN: the rule checks the relevant section body only, not the
|
|
719
|
+
// whole file, to avoid false-positives on other sections (e.g. an open-questions
|
|
720
|
+
// list that legitimately says "None yet." while Recent Decisions is maintained).
|
|
721
|
+
// wiki/startup.md → "## Recent Project Decisions" section body
|
|
722
|
+
// decisions/recent.md → "## Decisions" section body
|
|
723
|
+
//
|
|
724
|
+
// MINOR 2: the marker regex is tolerant of trailing whitespace / omitted terminal
|
|
725
|
+
// period ("None yet", "None yet. ") but stays anchored to the section scope above.
|
|
726
|
+
// Coupling: this English-only marker matches the bootstrap template text only;
|
|
727
|
+
// a project using a different language for these sections will not be checked.
|
|
728
|
+
const ROUTER_TRUTH_NONE_YET_REGEX = /\bNone yet\.?\s*$/m;
|
|
729
|
+
// Extract the body of a named heading section (from the heading line to the next
|
|
730
|
+
// same-or-higher-level heading, or end of string). Returns empty string when the
|
|
731
|
+
// heading is absent so the caller can decide whether to flag or skip.
|
|
732
|
+
function extractSectionBody(markdown, headingText) {
|
|
733
|
+
// Match `## <headingText>` (level-2 only, matching the template structure).
|
|
734
|
+
const headingRe = new RegExp(`^##\\s+${headingText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
|
|
735
|
+
const match = headingRe.exec(markdown);
|
|
736
|
+
if (!match)
|
|
737
|
+
return "";
|
|
738
|
+
const afterHeading = markdown.slice(match.index + match[0].length);
|
|
739
|
+
// Stop at the next ## heading (same or higher level), or end of string.
|
|
740
|
+
const nextHeading = /^##\s/m.exec(afterHeading);
|
|
741
|
+
return nextHeading ? afterHeading.slice(0, nextHeading.index) : afterHeading;
|
|
742
|
+
}
|
|
743
|
+
function collectRouterTruthDiagnostics() {
|
|
744
|
+
const logPath = "wiki/decisions/log.md";
|
|
745
|
+
if (!(0, workspace_1.exists)(logPath))
|
|
746
|
+
return [];
|
|
747
|
+
const logHasDatedEntry = /\b\d{4}-\d{2}-\d{2}\b/.test((0, workspace_1.stripMetadataHeader)((0, workspace_1.read)(logPath)));
|
|
748
|
+
if (!logHasDatedEntry)
|
|
749
|
+
return [];
|
|
750
|
+
const diagnostics = [];
|
|
751
|
+
// Each tuple: [file, headingText, surfaceLabel]
|
|
752
|
+
// headingText must match the bootstrap template section heading exactly so the
|
|
753
|
+
// section-anchored scan never accidentally reads unrelated sections.
|
|
754
|
+
const routers = [
|
|
755
|
+
["wiki/startup.md", "Recent Project Decisions", "Recent Decisions"],
|
|
756
|
+
["wiki/decisions/recent.md", "Decisions", "Decisions"],
|
|
757
|
+
];
|
|
758
|
+
for (const [file, heading, surface] of routers) {
|
|
759
|
+
if (!(0, workspace_1.exists)(file))
|
|
760
|
+
continue;
|
|
761
|
+
const section = extractSectionBody((0, workspace_1.stripMetadataHeader)((0, workspace_1.read)(file)), heading);
|
|
762
|
+
if (section === "")
|
|
763
|
+
continue; // section absent — skip rather than false-positive
|
|
764
|
+
if (ROUTER_TRUTH_NONE_YET_REGEX.test(section)) {
|
|
765
|
+
diagnostics.push({
|
|
766
|
+
code: "router-truth-contradiction",
|
|
767
|
+
severity: "error",
|
|
768
|
+
file,
|
|
769
|
+
message: `${file} ${surface} still says "None yet." while ${logPath} holds a dated decision entry; update ${file} to reflect the recorded decision`,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code));
|
|
774
|
+
}
|
|
634
775
|
function runDoctorMode(fix) {
|
|
635
776
|
if (fix) {
|
|
636
777
|
console.log("Project wiki doctor --fix");
|
|
@@ -645,40 +786,45 @@ function runDoctorMode(fix) {
|
|
|
645
786
|
const files = (0, wiki_files_1.wikiMarkdownFiles)();
|
|
646
787
|
const linkOk = printDiagnostics("Project wiki link-check", collectLinkDiagnostics(), files.length);
|
|
647
788
|
const qualityOk = printDiagnostics("Project wiki quality-check", collectQualityDiagnostics(), files.length);
|
|
789
|
+
const routerTruthOk = printDiagnostics("Project wiki router-truth check", collectRouterTruthDiagnostics(), files.length);
|
|
648
790
|
runLintMode();
|
|
649
|
-
if (!linkOk || !qualityOk)
|
|
791
|
+
if (!linkOk || !qualityOk || !routerTruthOk)
|
|
650
792
|
process.exit(1);
|
|
651
793
|
}
|
|
794
|
+
const commonLintRequiredFiles = [
|
|
795
|
+
"AGENTS.md",
|
|
796
|
+
"wiki/AGENTS.md",
|
|
797
|
+
"wiki/startup.md",
|
|
798
|
+
"wiki/index.md",
|
|
799
|
+
"wiki/decisions/log.md",
|
|
800
|
+
"wiki/decisions/recent.md",
|
|
801
|
+
"wiki/meta/operating-model.md",
|
|
802
|
+
"wiki/meta/decision-policy.md",
|
|
803
|
+
"wiki/meta/wiki-ops-v1-decisions.md",
|
|
804
|
+
".githooks/prepare-commit-msg",
|
|
805
|
+
".githooks/wiki-commit-trailers.js",
|
|
806
|
+
];
|
|
807
|
+
const agentLintRequiredFiles = {
|
|
808
|
+
codex: [".codex/hooks/wiki-session-start.js", ".codex/hooks.json"],
|
|
809
|
+
claude: ["CLAUDE.md", ".claude/hooks/wiki-session-start.js", ".claude/settings.json"],
|
|
810
|
+
cursor: [".cursor/rules/project-librarian.mdc", ".cursor/hooks/wiki-session-start.js", ".cursor/hooks.json"],
|
|
811
|
+
gemini: ["GEMINI.md", ".gemini/hooks/wiki-session-start.js", ".gemini/settings.json"],
|
|
812
|
+
};
|
|
813
|
+
function activeLintAgentSurfaces() {
|
|
814
|
+
const active = new Set();
|
|
815
|
+
for (const [agent, files] of Object.entries(agentLintRequiredFiles)) {
|
|
816
|
+
if (files.some((file) => (0, workspace_1.exists)(file)))
|
|
817
|
+
active.add(agent);
|
|
818
|
+
}
|
|
819
|
+
return active;
|
|
820
|
+
}
|
|
652
821
|
function runLintMode() {
|
|
653
822
|
const errors = [];
|
|
654
823
|
const warnings = [];
|
|
824
|
+
const activeAgents = activeLintAgentSurfaces();
|
|
655
825
|
const requiredFiles = [
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
"GEMINI.md",
|
|
659
|
-
"wiki/AGENTS.md",
|
|
660
|
-
"wiki/startup.md",
|
|
661
|
-
"wiki/index.md",
|
|
662
|
-
"wiki/canonical/project-brief.md",
|
|
663
|
-
"wiki/canonical/open-questions.md",
|
|
664
|
-
"wiki/canonical/assumptions.md",
|
|
665
|
-
"wiki/canonical/risks.md",
|
|
666
|
-
"wiki/decisions/log.md",
|
|
667
|
-
"wiki/decisions/recent.md",
|
|
668
|
-
"wiki/meta/operating-model.md",
|
|
669
|
-
"wiki/meta/decision-policy.md",
|
|
670
|
-
"wiki/meta/wiki-ops-v1-decisions.md",
|
|
671
|
-
".githooks/prepare-commit-msg",
|
|
672
|
-
".githooks/wiki-commit-trailers.js",
|
|
673
|
-
".codex/hooks/wiki-session-start.js",
|
|
674
|
-
".codex/hooks.json",
|
|
675
|
-
".claude/hooks/wiki-session-start.js",
|
|
676
|
-
".claude/settings.json",
|
|
677
|
-
".cursor/rules/project-librarian.mdc",
|
|
678
|
-
".cursor/hooks/wiki-session-start.js",
|
|
679
|
-
".cursor/hooks.json",
|
|
680
|
-
".gemini/hooks/wiki-session-start.js",
|
|
681
|
-
".gemini/settings.json",
|
|
826
|
+
...commonLintRequiredFiles,
|
|
827
|
+
...Array.from(activeAgents).flatMap((agent) => agentLintRequiredFiles[agent]),
|
|
682
828
|
];
|
|
683
829
|
for (const file of requiredFiles) {
|
|
684
830
|
if (!(0, workspace_1.exists)(file))
|
package/dist/taxonomy.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classifyMigrationUnit = classifyMigrationUnit;
|
|
4
|
+
exports.storageToMigrationKind = storageToMigrationKind;
|
|
5
|
+
const taxonomyAreas = [
|
|
6
|
+
{
|
|
7
|
+
id: "research-sources",
|
|
8
|
+
label: "Research / Source Evidence",
|
|
9
|
+
storage: "sources",
|
|
10
|
+
targetSlug: "research-sources",
|
|
11
|
+
keywords: ["source", "sources", "reference", "references", "citation", "bibliography", "research", "paper", "article", "link", "출처", "참고", "자료", "근거", "링크", "리서치", "논문"],
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "decision-record",
|
|
15
|
+
label: "Decision Record",
|
|
16
|
+
storage: "decisions",
|
|
17
|
+
targetSlug: "decision-records",
|
|
18
|
+
keywords: ["adr", "decision", "decisions", "rationale", "tradeoff", "trade-off", "alternative", "rejected", "accepted", "decided", "결정", "의사결정", "기각", "대안", "트레이드오프", "선택", "채택"],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "strategy",
|
|
22
|
+
label: "Strategy / Business Context",
|
|
23
|
+
storage: "canonical",
|
|
24
|
+
targetSlug: "strategy-context",
|
|
25
|
+
keywords: ["vision", "mission", "strategy", "business model", "market", "positioning", "competitive", "gtm", "pricing", "roadmap", "비전", "미션", "전략", "시장", "포지셔닝", "경쟁", "사업", "가격", "로드맵"],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "product-requirements",
|
|
29
|
+
label: "Product Requirements",
|
|
30
|
+
storage: "canonical",
|
|
31
|
+
targetSlug: "product-requirements",
|
|
32
|
+
keywords: ["prd", "requirement", "requirements", "feature", "features", "function", "functional", "scope", "goal", "goals", "acceptance", "use case", "기능", "기능명세", "요구사항", "요건", "범위", "목표", "수용기준", "유스케이스", "서비스기획", "기획"],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "user-flow",
|
|
36
|
+
label: "User Flow / Journey",
|
|
37
|
+
storage: "canonical",
|
|
38
|
+
targetSlug: "user-flows",
|
|
39
|
+
keywords: ["user flow", "flow", "journey", "scenario", "persona", "story", "navigation", "screen flow", "유저플로우", "사용자 흐름", "사용자 여정", "시나리오", "페르소나", "스토리", "화면흐름", "동선"],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "ux-content",
|
|
43
|
+
label: "UX / Content",
|
|
44
|
+
storage: "canonical",
|
|
45
|
+
targetSlug: "ux-content",
|
|
46
|
+
keywords: ["ux", "interaction", "wireframe", "copy", "content", "microcopy", "empty state", "error state", "accessibility", "i18n", "사용성", "인터랙션", "와이어프레임", "카피", "콘텐츠", "빈상태", "오류상태", "접근성", "문구"],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "design-system",
|
|
50
|
+
label: "Design System / UI",
|
|
51
|
+
storage: "canonical",
|
|
52
|
+
targetSlug: "design-system",
|
|
53
|
+
keywords: ["design", "ui", "component", "prototype", "figma", "style guide", "brand", "visual", "layout", "디자인", "컴포넌트", "프로토타입", "스타일가이드", "브랜드", "시각", "레이아웃"],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "data-analytics",
|
|
57
|
+
label: "Data / Analytics",
|
|
58
|
+
storage: "canonical",
|
|
59
|
+
targetSlug: "data-analytics",
|
|
60
|
+
keywords: ["data", "schema", "database", "db", "erd", "entity", "event", "metric", "analytics", "kpi", "tracking", "데이터", "스키마", "디비", "엔티티", "이벤트", "지표", "분석", "트래킹"],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "api-contract",
|
|
64
|
+
label: "API Contract",
|
|
65
|
+
storage: "canonical",
|
|
66
|
+
targetSlug: "api-contracts",
|
|
67
|
+
keywords: ["api", "endpoint", "request", "response", "rest", "graphql", "webhook", "sdk", "openapi", "swagger", "인증", "토큰", "요청", "응답", "엔드포인트", "웹훅", "api명세", "api 명세"],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "engineering-architecture",
|
|
71
|
+
label: "Engineering / Architecture",
|
|
72
|
+
storage: "canonical",
|
|
73
|
+
targetSlug: "engineering-architecture",
|
|
74
|
+
keywords: ["architecture", "system", "service", "module", "implementation", "tech stack", "dependency", "infra", "deployment", "queue", "cache", "아키텍처", "시스템", "서비스", "모듈", "구현", "기술스택", "인프라", "배포", "큐", "캐시"],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "security-legal",
|
|
78
|
+
label: "Security / Legal",
|
|
79
|
+
storage: "canonical",
|
|
80
|
+
targetSlug: "security-legal",
|
|
81
|
+
keywords: ["security", "privacy", "permission", "role", "auth", "authorization", "compliance", "legal", "terms", "consent", "보안", "개인정보", "권한", "역할", "인가", "인증", "준수", "법무", "약관", "동의"],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "policy",
|
|
85
|
+
label: "Policy / Governance",
|
|
86
|
+
storage: "canonical",
|
|
87
|
+
targetSlug: "policy-governance",
|
|
88
|
+
keywords: ["policy", "rule", "moderation", "governance", "sla", "retention", "eligibility", "refund", "정책", "규칙", "운영정책", "거버넌스", "보관", "자격", "환불"],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "qa",
|
|
92
|
+
label: "QA / Test",
|
|
93
|
+
storage: "canonical",
|
|
94
|
+
targetSlug: "qa-test-plan",
|
|
95
|
+
keywords: ["qa", "test", "testing", "validation", "verification", "bug", "edge case", "regression", "acceptance test", "테스트", "검증", "품질", "버그", "엣지케이스", "회귀", "테스트케이스"],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "release-ops",
|
|
99
|
+
label: "Release / Operations",
|
|
100
|
+
storage: "canonical",
|
|
101
|
+
targetSlug: "release-operations",
|
|
102
|
+
keywords: ["release", "launch", "operation", "runbook", "monitoring", "alert", "incident", "support", "cs", "migration", "rollout", "릴리즈", "출시", "운영", "런북", "모니터링", "알림", "장애", "고객지원", "마이그레이션"],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "business-ops",
|
|
106
|
+
label: "Business Operations",
|
|
107
|
+
storage: "canonical",
|
|
108
|
+
targetSlug: "business-operations",
|
|
109
|
+
keywords: ["sales", "marketing", "crm", "customer success", "finance", "billing", "invoice", "contract", "영업", "마케팅", "고객성공", "재무", "청구", "계약", "정산"],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "wiki-meta",
|
|
113
|
+
label: "Wiki Operations / Meta",
|
|
114
|
+
storage: "meta",
|
|
115
|
+
targetSlug: "wiki-operations",
|
|
116
|
+
keywords: ["wiki", "documentation", "document taxonomy", "canonical", "source of truth", "lint", "doctor", "위키", "문서화", "문서체계", "정본", "정보구조", "린트", "닥터"],
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
function slugPart(value, maxLength = 36) {
|
|
120
|
+
return value.toLowerCase().replace(/[^a-z0-9가-힣ぁ-んァ-ン一-龥]+/g, "-").replace(/^-|-$/g, "").slice(0, maxLength) || "migration";
|
|
121
|
+
}
|
|
122
|
+
function legacyStem(legacyPath) {
|
|
123
|
+
return slugPart(legacyPath.replace(/\.(md|mdx)$/i, "").split(/[\\/]+/).pop() ?? legacyPath);
|
|
124
|
+
}
|
|
125
|
+
function fallbackStorageFromLegacyPath(legacyPath) {
|
|
126
|
+
if (/^meta\//.test(legacyPath))
|
|
127
|
+
return "meta";
|
|
128
|
+
if (/^decisions\//.test(legacyPath))
|
|
129
|
+
return "decisions";
|
|
130
|
+
if (/^sources\//.test(legacyPath))
|
|
131
|
+
return "sources";
|
|
132
|
+
return "canonical";
|
|
133
|
+
}
|
|
134
|
+
function keywordScore(haystack, keyword) {
|
|
135
|
+
const normalized = keyword.toLowerCase();
|
|
136
|
+
const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
137
|
+
if (/^[a-z0-9 -]+$/.test(normalized)) {
|
|
138
|
+
const matches = haystack.match(new RegExp(`\\b${escaped}\\b`, "g"));
|
|
139
|
+
return matches ? matches.length : 0;
|
|
140
|
+
}
|
|
141
|
+
return haystack.includes(normalized) ? 1 : 0;
|
|
142
|
+
}
|
|
143
|
+
function areaScore(area, haystack) {
|
|
144
|
+
return area.keywords.reduce((sum, keyword) => sum + keywordScore(haystack, keyword), 0);
|
|
145
|
+
}
|
|
146
|
+
function targetPath(storage, base, targetSlug) {
|
|
147
|
+
return `wiki/${storage}/${slugPart(base, 32)}-${targetSlug}.md`;
|
|
148
|
+
}
|
|
149
|
+
function classifyMigrationUnit(input) {
|
|
150
|
+
const haystack = [
|
|
151
|
+
legacyStem(input.legacyPath),
|
|
152
|
+
input.heading,
|
|
153
|
+
input.headingPath.join(" "),
|
|
154
|
+
input.summary,
|
|
155
|
+
input.content,
|
|
156
|
+
].join("\n").toLowerCase();
|
|
157
|
+
const scored = taxonomyAreas
|
|
158
|
+
.map((area) => ({ area, score: areaScore(area, haystack) }))
|
|
159
|
+
.filter((item) => item.score > 0)
|
|
160
|
+
.sort((left, right) => right.score - left.score || left.area.id.localeCompare(right.area.id));
|
|
161
|
+
const fallbackStorage = fallbackStorageFromLegacyPath(input.legacyPath);
|
|
162
|
+
const best = scored[0]?.area ?? {
|
|
163
|
+
id: "unclassified",
|
|
164
|
+
label: "Needs Human Review",
|
|
165
|
+
storage: fallbackStorage,
|
|
166
|
+
targetSlug: "migration-review",
|
|
167
|
+
keywords: [],
|
|
168
|
+
};
|
|
169
|
+
const bestScore = scored[0]?.score ?? 0;
|
|
170
|
+
const secondScore = scored[1]?.score ?? 0;
|
|
171
|
+
const confidence = bestScore >= 3 && bestScore > secondScore ? "high" : bestScore >= 1 && bestScore >= secondScore ? "medium" : "low";
|
|
172
|
+
const base = legacyStem(input.legacyPath);
|
|
173
|
+
const reason = confidence === "low"
|
|
174
|
+
? "no strong taxonomy signal; human review required"
|
|
175
|
+
: `matched ${best.label}${secondScore > 0 && bestScore === secondScore ? " with overlapping signals" : ""}`;
|
|
176
|
+
return {
|
|
177
|
+
area: best.id,
|
|
178
|
+
label: best.label,
|
|
179
|
+
storage: best.storage,
|
|
180
|
+
target: targetPath(best.storage, base, best.targetSlug),
|
|
181
|
+
confidence,
|
|
182
|
+
reason,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function storageToMigrationKind(storage) {
|
|
186
|
+
if (storage === "decisions")
|
|
187
|
+
return "decision";
|
|
188
|
+
if (storage === "sources")
|
|
189
|
+
return "source";
|
|
190
|
+
if (storage === "meta")
|
|
191
|
+
return "meta";
|
|
192
|
+
return "canonical";
|
|
193
|
+
}
|