@fenglimg/fabric-server 2.0.0-rc.10 → 2.0.0-rc.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.
|
@@ -88,7 +88,7 @@ import { agentsMetaNodeSchema, agentsMetaSchema as agentsMetaSchema2 } from "@fe
|
|
|
88
88
|
var AgentsMetaFileMissingError = class extends IOFabricError {
|
|
89
89
|
constructor(metaPath, opts) {
|
|
90
90
|
super(`Fabric agents metadata file is missing: ${metaPath}`, {
|
|
91
|
-
actionHint: opts?.actionHint ?? "Run `fab
|
|
91
|
+
actionHint: opts?.actionHint ?? "Run `fab install` to scaffold the .fabric/agents.meta.json file"
|
|
92
92
|
});
|
|
93
93
|
this.metaPath = metaPath;
|
|
94
94
|
}
|
|
@@ -1381,12 +1381,14 @@ async function runDoctorReport(target) {
|
|
|
1381
1381
|
const relevancePathsDrift = inspectRelevancePathsDrift(projectRoot);
|
|
1382
1382
|
const narrowTooFew = inspectNarrowTooFew(projectRoot, lintNow);
|
|
1383
1383
|
const sessionHintsStale = inspectSessionHintsStale(projectRoot, lintNow);
|
|
1384
|
+
const relevanceFieldsMissing = inspectRelevanceFieldsMissing(projectRoot);
|
|
1385
|
+
const skillMdYamlInvalid = inspectSkillMdYamlInvalid(projectRoot);
|
|
1384
1386
|
const checks = [
|
|
1385
1387
|
createBootstrapAnchorCheck(bootstrapAnchor),
|
|
1386
1388
|
createKnowledgeDirMissingCheck(knowledgeDirMissing),
|
|
1387
1389
|
createForensicCheck(forensic, framework.kind, entryPoints.length),
|
|
1388
1390
|
// v2.0: removed `createInitContextCheck` — `.fabric/init-context.json`
|
|
1389
|
-
// is owned by the AI-side client init skill, not by `fabric
|
|
1391
|
+
// is owned by the AI-side client init skill, not by `fabric install` CLI.
|
|
1390
1392
|
// The file's absence is a legitimate post-init state when the skill has
|
|
1391
1393
|
// not yet run, so flagging it as a doctor manual_error misrepresents
|
|
1392
1394
|
// ownership.
|
|
@@ -1436,6 +1438,13 @@ async function runDoctorReport(target) {
|
|
|
1436
1438
|
createNarrowTooFewCheck(narrowTooFew),
|
|
1437
1439
|
// rc.6 TASK-021 (E3): session-hints cache hygiene (lint #27). Info kind.
|
|
1438
1440
|
createSessionHintsStaleCheck(sessionHintsStale),
|
|
1441
|
+
// v2.0.0-rc.9 TASK-003 (A3): relevance fields back-fill (lint #28).
|
|
1442
|
+
// Info kind — applies to pending entries only; canonical entries get
|
|
1443
|
+
// the fields written verbatim by fab_review.approve/modify.
|
|
1444
|
+
createRelevanceFieldsMissingCheck(relevanceFieldsMissing),
|
|
1445
|
+
// rc.12 lint #29: skill_md_yaml_invalid. Warning kind — surfaces
|
|
1446
|
+
// SKILL.md frontmatter that Codex CLI silently drops at load.
|
|
1447
|
+
createSkillMdYamlInvalidCheck(skillMdYamlInvalid),
|
|
1439
1448
|
createPreexistingRootFilesCheck(preexistingRootFiles)
|
|
1440
1449
|
// v2.0 / rc.2: `createLegacyClientPathCheck` removed. The schema now
|
|
1441
1450
|
// rejects retired clientPaths keys (windsurf/rooCode/geminiCLI) at Zod
|
|
@@ -1592,6 +1601,24 @@ async function runDoctorApplyLint(target) {
|
|
|
1592
1601
|
for (const candidate of sessionHintsStale.candidates) {
|
|
1593
1602
|
mutations.push(await applySessionHintsStaleCleanup(projectRoot, candidate));
|
|
1594
1603
|
}
|
|
1604
|
+
const relevanceFieldsMissing = inspectRelevanceFieldsMissing(projectRoot);
|
|
1605
|
+
let relevanceTouchedCount = 0;
|
|
1606
|
+
for (const candidate of relevanceFieldsMissing.candidates) {
|
|
1607
|
+
const mutation = await applyRelevanceFieldsMissing(candidate);
|
|
1608
|
+
mutations.push(mutation);
|
|
1609
|
+
if (mutation.applied) {
|
|
1610
|
+
relevanceTouchedCount += 1;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
try {
|
|
1614
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1615
|
+
event_type: "relevance_migration_run",
|
|
1616
|
+
timestamp: new Date(now).toISOString(),
|
|
1617
|
+
scanned_count: relevanceFieldsMissing.scanned_count,
|
|
1618
|
+
touched_count: relevanceTouchedCount
|
|
1619
|
+
});
|
|
1620
|
+
} catch {
|
|
1621
|
+
}
|
|
1595
1622
|
const meta = await inspectMeta(projectRoot);
|
|
1596
1623
|
const indexDrift = inspectIndexDrift(projectRoot, meta);
|
|
1597
1624
|
if (indexDrift.drifts.length > 0) {
|
|
@@ -2105,7 +2132,7 @@ function createBootstrapAnchorCheck(inspection) {
|
|
|
2105
2132
|
"fixable_error",
|
|
2106
2133
|
"bootstrap_anchor_missing",
|
|
2107
2134
|
"Neither AGENTS.md nor CLAUDE.md exists at the repo root. Fabric requires a bootstrap anchor file at the project root.",
|
|
2108
|
-
"Run `fabric
|
|
2135
|
+
"Run `fabric install` to generate the AGENTS.md / CLAUDE.md bootstrap anchor at the repo root."
|
|
2109
2136
|
);
|
|
2110
2137
|
}
|
|
2111
2138
|
const present = [
|
|
@@ -2150,11 +2177,11 @@ function createForensicCheck(forensic, frameworkKind, entryPointCount) {
|
|
|
2150
2177
|
"manual_error",
|
|
2151
2178
|
"forensic_missing",
|
|
2152
2179
|
`${forensic.error ?? ".fabric/forensic.json is missing."} Live scan detects ${frameworkKind} with ${entryPointCount} entry point${entryPointCount === 1 ? "" : "s"}.`,
|
|
2153
|
-
"Run `fab
|
|
2180
|
+
"Run `fab install` to regenerate .fabric/forensic.json."
|
|
2154
2181
|
);
|
|
2155
2182
|
}
|
|
2156
2183
|
if (!forensic.valid) {
|
|
2157
|
-
return issueCheck("Scan evidence", "error", "manual_error", "forensic_invalid", forensic.error ?? ".fabric/forensic.json is invalid.", "Run `fab
|
|
2184
|
+
return issueCheck("Scan evidence", "error", "manual_error", "forensic_invalid", forensic.error ?? ".fabric/forensic.json is invalid.", "Run `fab install` to regenerate .fabric/forensic.json.");
|
|
2158
2185
|
}
|
|
2159
2186
|
return okCheck("Scan evidence", `.fabric/forensic.json is valid for ${forensic.report?.framework.kind ?? "unknown"}.`);
|
|
2160
2187
|
}
|
|
@@ -2234,8 +2261,8 @@ function createMcpConfigInWrongFileCheck(inspection) {
|
|
|
2234
2261
|
"error",
|
|
2235
2262
|
"fixable_error",
|
|
2236
2263
|
"mcp_config_in_wrong_file",
|
|
2237
|
-
`.claude/settings.json contains mcpServers.fabric \u2014 this file is for hooks/permissions only. Run --fix to remove it, then re-run fab
|
|
2238
|
-
"Run `fab doctor --fix` to remove mcpServers.fabric from .claude/settings.json, then run `fab
|
|
2264
|
+
`.claude/settings.json contains mcpServers.fabric \u2014 this file is for hooks/permissions only. Run --fix to remove it, then re-run fab install to write .mcp.json.`,
|
|
2265
|
+
"Run `fab doctor --fix` to remove mcpServers.fabric from .claude/settings.json, then run `fab install` to write .mcp.json."
|
|
2239
2266
|
);
|
|
2240
2267
|
}
|
|
2241
2268
|
return okCheck("Claude MCP config location", "mcpServers.fabric is not in .claude/settings.json.");
|
|
@@ -3458,6 +3485,254 @@ function createRelevancePathsDriftCheck(inspection) {
|
|
|
3458
3485
|
"Review whether the entry is still relevant \u2014 use `fab_review.modify` to refresh the anchors or `fab_review.reject` to archive."
|
|
3459
3486
|
);
|
|
3460
3487
|
}
|
|
3488
|
+
function inspectRelevanceFieldsMissing(projectRoot) {
|
|
3489
|
+
const candidates = [];
|
|
3490
|
+
let scannedCount = 0;
|
|
3491
|
+
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
3492
|
+
const teamRoot = join5(projectRoot, ".fabric", "knowledge", "pending");
|
|
3493
|
+
const personalRoot = join5(
|
|
3494
|
+
resolvePersonalRootForPending(),
|
|
3495
|
+
".fabric",
|
|
3496
|
+
"knowledge",
|
|
3497
|
+
"pending"
|
|
3498
|
+
);
|
|
3499
|
+
for (const [root, displayPrefix] of [
|
|
3500
|
+
[teamRoot, ".fabric/knowledge/pending"],
|
|
3501
|
+
[personalRoot, "~/.fabric/knowledge/pending"]
|
|
3502
|
+
]) {
|
|
3503
|
+
if (!existsSync4(root)) {
|
|
3504
|
+
continue;
|
|
3505
|
+
}
|
|
3506
|
+
let typeDirs = [];
|
|
3507
|
+
try {
|
|
3508
|
+
typeDirs = readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
3509
|
+
} catch {
|
|
3510
|
+
continue;
|
|
3511
|
+
}
|
|
3512
|
+
for (const typeDir of typeDirs) {
|
|
3513
|
+
const dir = join5(root, typeDir);
|
|
3514
|
+
let entries;
|
|
3515
|
+
try {
|
|
3516
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
3517
|
+
} catch {
|
|
3518
|
+
continue;
|
|
3519
|
+
}
|
|
3520
|
+
for (const entry of entries) {
|
|
3521
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
3522
|
+
continue;
|
|
3523
|
+
}
|
|
3524
|
+
const absPath = join5(dir, entry.name);
|
|
3525
|
+
let source;
|
|
3526
|
+
try {
|
|
3527
|
+
source = readFileSync(absPath, "utf8");
|
|
3528
|
+
} catch {
|
|
3529
|
+
continue;
|
|
3530
|
+
}
|
|
3531
|
+
const fm = FM_PATTERN.exec(source);
|
|
3532
|
+
if (fm === null) {
|
|
3533
|
+
continue;
|
|
3534
|
+
}
|
|
3535
|
+
scannedCount += 1;
|
|
3536
|
+
const block = fm[1];
|
|
3537
|
+
const missingScope = !RELEVANCE_SCOPE_LINE_PATTERN.test(block);
|
|
3538
|
+
const missingPaths = !RELEVANCE_PATHS_LINE_PATTERN.test(block);
|
|
3539
|
+
if (!missingScope && !missingPaths) {
|
|
3540
|
+
continue;
|
|
3541
|
+
}
|
|
3542
|
+
candidates.push({
|
|
3543
|
+
pending_path: posix.join(displayPrefix, typeDir, entry.name),
|
|
3544
|
+
pending_path_abs: absPath,
|
|
3545
|
+
missing_scope: missingScope,
|
|
3546
|
+
missing_paths: missingPaths
|
|
3547
|
+
});
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
candidates.sort((a, b) => a.pending_path.localeCompare(b.pending_path));
|
|
3552
|
+
return { candidates, scanned_count: scannedCount };
|
|
3553
|
+
}
|
|
3554
|
+
function appendRelevanceFieldsToFrontmatter(source, needsScope, needsPaths) {
|
|
3555
|
+
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
3556
|
+
const fm = FM_PATTERN.exec(source);
|
|
3557
|
+
if (fm === null) {
|
|
3558
|
+
return null;
|
|
3559
|
+
}
|
|
3560
|
+
const block = fm[1];
|
|
3561
|
+
const actuallyNeedsScope = needsScope && !RELEVANCE_SCOPE_LINE_PATTERN.test(block);
|
|
3562
|
+
const actuallyNeedsPaths = needsPaths && !RELEVANCE_PATHS_LINE_PATTERN.test(block);
|
|
3563
|
+
if (!actuallyNeedsScope && !actuallyNeedsPaths) {
|
|
3564
|
+
return source;
|
|
3565
|
+
}
|
|
3566
|
+
const additions = [];
|
|
3567
|
+
if (actuallyNeedsScope) {
|
|
3568
|
+
additions.push("relevance_scope: broad");
|
|
3569
|
+
}
|
|
3570
|
+
if (actuallyNeedsPaths) {
|
|
3571
|
+
additions.push("relevance_paths: []");
|
|
3572
|
+
}
|
|
3573
|
+
const trailing = block.endsWith("\n") ? "" : "\n";
|
|
3574
|
+
const replacedBlock = `${block}${trailing}${additions.join("\n")}`;
|
|
3575
|
+
const blockStart = source.indexOf(block);
|
|
3576
|
+
if (blockStart < 0) {
|
|
3577
|
+
return null;
|
|
3578
|
+
}
|
|
3579
|
+
return source.slice(0, blockStart) + replacedBlock + source.slice(blockStart + block.length);
|
|
3580
|
+
}
|
|
3581
|
+
async function applyRelevanceFieldsMissing(candidate) {
|
|
3582
|
+
const parts = [];
|
|
3583
|
+
if (candidate.missing_scope) parts.push("relevance_scope: broad");
|
|
3584
|
+
if (candidate.missing_paths) parts.push("relevance_paths: []");
|
|
3585
|
+
const detail = `back-filled: ${parts.join(", ")}`;
|
|
3586
|
+
try {
|
|
3587
|
+
const source = await readFile5(candidate.pending_path_abs, "utf8");
|
|
3588
|
+
const rewritten = appendRelevanceFieldsToFrontmatter(
|
|
3589
|
+
source,
|
|
3590
|
+
candidate.missing_scope,
|
|
3591
|
+
candidate.missing_paths
|
|
3592
|
+
);
|
|
3593
|
+
if (rewritten === null) {
|
|
3594
|
+
return {
|
|
3595
|
+
kind: "knowledge_relevance_fields_missing",
|
|
3596
|
+
path: candidate.pending_path,
|
|
3597
|
+
detail,
|
|
3598
|
+
applied: false,
|
|
3599
|
+
error: "frontmatter not parseable; cannot back-fill"
|
|
3600
|
+
};
|
|
3601
|
+
}
|
|
3602
|
+
if (rewritten === source) {
|
|
3603
|
+
return {
|
|
3604
|
+
kind: "knowledge_relevance_fields_missing",
|
|
3605
|
+
path: candidate.pending_path,
|
|
3606
|
+
detail,
|
|
3607
|
+
applied: false,
|
|
3608
|
+
error: "fields already present at write time (no diff)"
|
|
3609
|
+
};
|
|
3610
|
+
}
|
|
3611
|
+
await atomicWriteText3(candidate.pending_path_abs, rewritten);
|
|
3612
|
+
return {
|
|
3613
|
+
kind: "knowledge_relevance_fields_missing",
|
|
3614
|
+
path: candidate.pending_path,
|
|
3615
|
+
detail,
|
|
3616
|
+
applied: true
|
|
3617
|
+
};
|
|
3618
|
+
} catch (error) {
|
|
3619
|
+
return {
|
|
3620
|
+
kind: "knowledge_relevance_fields_missing",
|
|
3621
|
+
path: candidate.pending_path,
|
|
3622
|
+
detail,
|
|
3623
|
+
applied: false,
|
|
3624
|
+
error: truncateErrorMessage(error)
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
function createRelevanceFieldsMissingCheck(inspection) {
|
|
3629
|
+
if (inspection.candidates.length === 0) {
|
|
3630
|
+
return okCheck(
|
|
3631
|
+
"Knowledge relevance fields missing",
|
|
3632
|
+
"All pending entries declare both relevance_scope and relevance_paths."
|
|
3633
|
+
);
|
|
3634
|
+
}
|
|
3635
|
+
const first = inspection.candidates[0];
|
|
3636
|
+
const missingParts = [];
|
|
3637
|
+
if (first.missing_scope) missingParts.push("relevance_scope");
|
|
3638
|
+
if (first.missing_paths) missingParts.push("relevance_paths");
|
|
3639
|
+
const detail = `${first.pending_path} (missing: ${missingParts.join(", ")})`;
|
|
3640
|
+
return issueCheck(
|
|
3641
|
+
"Knowledge relevance fields missing",
|
|
3642
|
+
"ok",
|
|
3643
|
+
"info",
|
|
3644
|
+
"knowledge_relevance_fields_missing",
|
|
3645
|
+
`${inspection.candidates.length} pending entr${inspection.candidates.length === 1 ? "y is" : "ies are"} missing relevance_scope and/or relevance_paths in frontmatter. First: ${detail}.`,
|
|
3646
|
+
"Run `fab doctor --apply-lint` to back-fill the schema defaults (relevance_scope: broad, relevance_paths: [])."
|
|
3647
|
+
);
|
|
3648
|
+
}
|
|
3649
|
+
var SKILL_MD_FRONTMATTER_ROOTS = [".claude/skills", ".codex/skills"];
|
|
3650
|
+
var SKILL_FRONTMATTER_KEY_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]+(.+?)[ \t]*$/u;
|
|
3651
|
+
var SKILL_QUOTED_VALUE_LEADS = /* @__PURE__ */ new Set(['"', "'", "[", "{", ">", "|"]);
|
|
3652
|
+
function inspectSkillMdYamlInvalid(projectRoot) {
|
|
3653
|
+
const candidates = [];
|
|
3654
|
+
for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
|
|
3655
|
+
const rootAbs = join5(projectRoot, rootRel);
|
|
3656
|
+
if (!existsSync4(rootAbs)) continue;
|
|
3657
|
+
let dirEntries;
|
|
3658
|
+
try {
|
|
3659
|
+
dirEntries = readdirSync(rootAbs, { withFileTypes: true });
|
|
3660
|
+
} catch {
|
|
3661
|
+
continue;
|
|
3662
|
+
}
|
|
3663
|
+
for (const dirEntry of dirEntries) {
|
|
3664
|
+
if (!dirEntry.isDirectory()) continue;
|
|
3665
|
+
const skillFile = join5(rootAbs, dirEntry.name, "SKILL.md");
|
|
3666
|
+
if (!existsSync4(skillFile)) continue;
|
|
3667
|
+
let raw;
|
|
3668
|
+
try {
|
|
3669
|
+
raw = readFileSync(skillFile, "utf8");
|
|
3670
|
+
} catch {
|
|
3671
|
+
continue;
|
|
3672
|
+
}
|
|
3673
|
+
const frontmatter = extractSkillFrontmatterLines(raw);
|
|
3674
|
+
if (frontmatter === null) continue;
|
|
3675
|
+
for (const { line, lineNumber } of frontmatter) {
|
|
3676
|
+
const match = SKILL_FRONTMATTER_KEY_PATTERN.exec(line);
|
|
3677
|
+
if (!match) continue;
|
|
3678
|
+
const [, key, value] = match;
|
|
3679
|
+
if (value.length === 0) continue;
|
|
3680
|
+
if (SKILL_QUOTED_VALUE_LEADS.has(value[0])) continue;
|
|
3681
|
+
const colonSpaceIdx = value.indexOf(": ");
|
|
3682
|
+
const trailingColon = value.endsWith(":");
|
|
3683
|
+
if (colonSpaceIdx < 0 && !trailingColon) continue;
|
|
3684
|
+
const anchor = colonSpaceIdx >= 0 ? colonSpaceIdx : value.length - 1;
|
|
3685
|
+
const previewStart = Math.max(0, anchor - 25);
|
|
3686
|
+
const previewEnd = Math.min(value.length, anchor + 30);
|
|
3687
|
+
const preview = `${previewStart > 0 ? "\u2026" : ""}${value.slice(previewStart, previewEnd)}${previewEnd < value.length ? "\u2026" : ""}`;
|
|
3688
|
+
candidates.push({
|
|
3689
|
+
path: posix.join(rootRel, dirEntry.name, "SKILL.md"),
|
|
3690
|
+
line: lineNumber,
|
|
3691
|
+
key,
|
|
3692
|
+
preview
|
|
3693
|
+
});
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
candidates.sort((a, b) => {
|
|
3698
|
+
const byPath = a.path.localeCompare(b.path);
|
|
3699
|
+
return byPath !== 0 ? byPath : a.line - b.line;
|
|
3700
|
+
});
|
|
3701
|
+
return { candidates };
|
|
3702
|
+
}
|
|
3703
|
+
function extractSkillFrontmatterLines(raw) {
|
|
3704
|
+
const rawLines = raw.split(/\r?\n/u);
|
|
3705
|
+
if (rawLines.length < 2) return null;
|
|
3706
|
+
if (rawLines[0]?.trim() !== "---") return null;
|
|
3707
|
+
const out = [];
|
|
3708
|
+
for (let i = 1; i < rawLines.length; i++) {
|
|
3709
|
+
const line = rawLines[i];
|
|
3710
|
+
if (line.trim() === "---") {
|
|
3711
|
+
return out;
|
|
3712
|
+
}
|
|
3713
|
+
out.push({ line, lineNumber: i + 1 });
|
|
3714
|
+
}
|
|
3715
|
+
return null;
|
|
3716
|
+
}
|
|
3717
|
+
function createSkillMdYamlInvalidCheck(inspection) {
|
|
3718
|
+
if (inspection.candidates.length === 0) {
|
|
3719
|
+
return okCheck(
|
|
3720
|
+
"Skill markdown YAML",
|
|
3721
|
+
"All .claude/.codex SKILL.md frontmatter values parse as strict YAML."
|
|
3722
|
+
);
|
|
3723
|
+
}
|
|
3724
|
+
const first = inspection.candidates[0];
|
|
3725
|
+
const detail = `${first.path}:${first.line} (key \`${first.key}\` value contains an unquoted ': ' \u2014 preview: \`${first.preview}\`)`;
|
|
3726
|
+
const plural = inspection.candidates.length === 1;
|
|
3727
|
+
return issueCheck(
|
|
3728
|
+
"Skill markdown YAML",
|
|
3729
|
+
"warn",
|
|
3730
|
+
"warning",
|
|
3731
|
+
"skill_md_yaml_invalid",
|
|
3732
|
+
`${inspection.candidates.length} SKILL.md frontmatter ${plural ? "value contains" : "values contain"} an unquoted ': ' that strict YAML parsers reject (Claude Code tolerates it; Codex CLI drops the skill at load). First: ${detail}.`,
|
|
3733
|
+
'Quote the value with double quotes (`description: "\u2026"`) or rewrite the inner `key: value` token to `key=value`.'
|
|
3734
|
+
);
|
|
3735
|
+
}
|
|
3461
3736
|
function createNarrowTooFewCheck(inspection) {
|
|
3462
3737
|
const { structural_flagged, telemetry_flagged } = inspection;
|
|
3463
3738
|
if (!structural_flagged && !telemetry_flagged) {
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
readEventLedger,
|
|
15
15
|
runDoctorReport,
|
|
16
16
|
sha256
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-3ZBYYPXQ.js";
|
|
18
18
|
|
|
19
19
|
// src/http.ts
|
|
20
20
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -942,7 +942,7 @@ function toPosixPath(path) {
|
|
|
942
942
|
function buildRecommendations(input) {
|
|
943
943
|
const recommendations = [];
|
|
944
944
|
if (!input.hasExistingFabric) {
|
|
945
|
-
recommendations.push("L0: Run `fab
|
|
945
|
+
recommendations.push("L0: Run `fab install` to scaffold the .fabric/ knowledge layout (decisions, pitfalls, guidelines, models, processes).");
|
|
946
946
|
}
|
|
947
947
|
if (input.readmeQuality === "stub") {
|
|
948
948
|
recommendations.push("L0: Expand README.md before promoting project facts into Fabric knowledge entries.");
|
package/dist/index.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ type DoctorFixReport = {
|
|
|
77
77
|
message: string;
|
|
78
78
|
report: DoctorReport;
|
|
79
79
|
};
|
|
80
|
-
type DoctorApplyLintMutationKind = "knowledge_orphan_demote_required" | "knowledge_stale_archive_required" | "knowledge_index_drift" | "knowledge_pending_auto_archive" | "knowledge_session_hints_stale_cleanup";
|
|
80
|
+
type DoctorApplyLintMutationKind = "knowledge_orphan_demote_required" | "knowledge_stale_archive_required" | "knowledge_index_drift" | "knowledge_pending_auto_archive" | "knowledge_session_hints_stale_cleanup" | "knowledge_relevance_fields_missing";
|
|
81
81
|
type DoctorApplyLintMutation = {
|
|
82
82
|
kind: DoctorApplyLintMutationKind;
|
|
83
83
|
path: string;
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
sha256,
|
|
27
27
|
stableStringify,
|
|
28
28
|
writeKnowledgeMeta
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-3ZBYYPXQ.js";
|
|
30
30
|
|
|
31
31
|
// src/index.ts
|
|
32
32
|
import { existsSync as existsSync4 } from "fs";
|
|
@@ -149,6 +149,21 @@ async function extractKnowledge(projectRoot, input) {
|
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
151
|
const layer = input.layer ?? "team";
|
|
152
|
+
let relevanceScope = input.relevance_scope;
|
|
153
|
+
let relevancePaths = input.relevance_paths;
|
|
154
|
+
const shouldAutoDegrade = layer === "personal" && relevanceScope === "narrow";
|
|
155
|
+
if (shouldAutoDegrade) {
|
|
156
|
+
relevanceScope = "broad";
|
|
157
|
+
relevancePaths = [];
|
|
158
|
+
await emitEventBestEffort(projectRoot, {
|
|
159
|
+
event_type: "knowledge_scope_degraded",
|
|
160
|
+
stable_id: `pending:${idempotencyKey}`,
|
|
161
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
162
|
+
from_scope: "narrow",
|
|
163
|
+
to_scope: "broad",
|
|
164
|
+
reason: "personal-implies-broad"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
152
167
|
const baseDir = pendingBase(layer, projectRoot);
|
|
153
168
|
const absolutePath = join2(baseDir, input.type, `${sanitizedSlug}.md`);
|
|
154
169
|
const reportedPath = layer === "personal" ? `~/${relative(resolvePersonalRoot(), absolutePath)}` : relative(projectRoot, absolutePath);
|
|
@@ -190,7 +205,9 @@ async function extractKnowledge(projectRoot, input) {
|
|
|
190
205
|
recentPaths: input.recent_paths,
|
|
191
206
|
layer,
|
|
192
207
|
proposedReason: input.proposed_reason,
|
|
193
|
-
sessionContext: input.session_context
|
|
208
|
+
sessionContext: input.session_context,
|
|
209
|
+
relevanceScope,
|
|
210
|
+
relevancePaths
|
|
194
211
|
});
|
|
195
212
|
await atomicWriteText(absolutePath, fresh);
|
|
196
213
|
await emitEventBestEffort(projectRoot, {
|
|
@@ -216,7 +233,7 @@ function sanitizeSlug(raw) {
|
|
|
216
233
|
}
|
|
217
234
|
function renderFreshEntry(args) {
|
|
218
235
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
219
|
-
const
|
|
236
|
+
const frontmatterLines = [
|
|
220
237
|
"---",
|
|
221
238
|
`type: ${args.type}`,
|
|
222
239
|
"maturity: draft",
|
|
@@ -224,10 +241,20 @@ function renderFreshEntry(args) {
|
|
|
224
241
|
`created_at: ${createdAt}`,
|
|
225
242
|
`source_sessions: [${args.sourceSessions.map((s) => JSON.stringify(s)).join(", ")}]`,
|
|
226
243
|
`proposed_reason: ${args.proposedReason}`,
|
|
227
|
-
"tags: []"
|
|
244
|
+
"tags: []"
|
|
245
|
+
];
|
|
246
|
+
if (args.relevanceScope !== void 0) {
|
|
247
|
+
frontmatterLines.push(`relevance_scope: ${args.relevanceScope}`);
|
|
248
|
+
}
|
|
249
|
+
if (args.relevancePaths !== void 0) {
|
|
250
|
+
const pathsBody = args.relevancePaths.map((p) => quoteRelevancePath(p)).join(", ");
|
|
251
|
+
frontmatterLines.push(`relevance_paths: [${pathsBody}]`);
|
|
252
|
+
}
|
|
253
|
+
frontmatterLines.push(
|
|
228
254
|
`x-fabric-idempotency-key: ${args.idempotencyKey}`,
|
|
229
255
|
"---"
|
|
230
|
-
|
|
256
|
+
);
|
|
257
|
+
const frontmatter = frontmatterLines.join("\n");
|
|
231
258
|
const reasonExplanation = PROPOSED_REASON_DESCRIPTIONS[args.proposedReason];
|
|
232
259
|
const body = [
|
|
233
260
|
"",
|
|
@@ -251,6 +278,9 @@ function renderFreshEntry(args) {
|
|
|
251
278
|
return `${frontmatter}
|
|
252
279
|
${body}`;
|
|
253
280
|
}
|
|
281
|
+
function quoteRelevancePath(value) {
|
|
282
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
283
|
+
}
|
|
254
284
|
function renderEvidenceBlock(summary, recentPaths) {
|
|
255
285
|
const pathLines = recentPaths.length === 0 ? "_(no recent paths reported)_" : recentPaths.map((p) => `- ${p}`).join("\n");
|
|
256
286
|
return [
|
|
@@ -1860,7 +1890,7 @@ function formatPreexistingRootMessage(projectRoot) {
|
|
|
1860
1890
|
function createFabricServer(tracker) {
|
|
1861
1891
|
const server = new McpServer({
|
|
1862
1892
|
name: "fabric-knowledge-server",
|
|
1863
|
-
version: "2.0.0-rc.
|
|
1893
|
+
version: "2.0.0-rc.13"
|
|
1864
1894
|
});
|
|
1865
1895
|
registerPlanContext(server, tracker);
|
|
1866
1896
|
registerKnowledgeSections(server, tracker);
|
|
@@ -1960,7 +1990,7 @@ function createShutdownHandler(deps) {
|
|
|
1960
1990
|
};
|
|
1961
1991
|
}
|
|
1962
1992
|
async function startHttpServer(options) {
|
|
1963
|
-
const { createFabricHttpApp } = await import("./http-
|
|
1993
|
+
const { createFabricHttpApp } = await import("./http-B3UKRP5Y.js");
|
|
1964
1994
|
const { port, projectRoot, host = "127.0.0.1", authToken } = options;
|
|
1965
1995
|
const app = createFabricHttpApp({ projectRoot, host, authToken });
|
|
1966
1996
|
return await new Promise((resolveServer, rejectServer) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fenglimg/fabric-server",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"express": "^5.2.1",
|
|
14
14
|
"minimatch": "^10.0.1",
|
|
15
15
|
"zod": "^3.25.0",
|
|
16
|
-
"@fenglimg/fabric-shared": "2.0.0-rc.
|
|
16
|
+
"@fenglimg/fabric-shared": "2.0.0-rc.13"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/express": "^5.0.6",
|