@fenglimg/fabric-server 2.0.0-rc.27 → 2.0.0-rc.29

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.
@@ -14,7 +14,7 @@ import {
14
14
  readEventLedger,
15
15
  runDoctorReport,
16
16
  sha256
17
- } from "./chunk-NZSGNQKE.js";
17
+ } from "./chunk-CTQ4UMO4.js";
18
18
 
19
19
  // src/http.ts
20
20
  import { randomUUID as randomUUID2 } from "crypto";
@@ -972,6 +972,16 @@ function createBearerAuthMiddleware(token) {
972
972
  next();
973
973
  };
974
974
  }
975
+ function createLoopbackDenyMiddleware() {
976
+ return function loopbackDenyMiddleware(_req, res, _next) {
977
+ sendError(
978
+ res,
979
+ 401,
980
+ "UNAUTHORIZED",
981
+ "FABRIC_AUTH_TOKEN is not set. Either export FABRIC_AUTH_TOKEN=<secret> before running `fab serve`, or pass `--allow-loopback-no-auth` to explicitly opt in to unauthenticated loopback access (security risk)."
982
+ );
983
+ };
984
+ }
975
985
  function readAuthorizationHeader(value) {
976
986
  if (typeof value === "string" && value.length > 0) {
977
987
  return value;
@@ -998,6 +1008,10 @@ function hashToken(token) {
998
1008
  // src/http.ts
999
1009
  var DEFAULT_HOST = "127.0.0.1";
1000
1010
  var NOTIFY_DEBOUNCE_MS = 200;
1011
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
1012
+ function isLoopbackHost(host) {
1013
+ return LOOPBACK_HOSTS.has(host);
1014
+ }
1001
1015
  var JsonlEventStore = class {
1002
1016
  constructor(projectRoot) {
1003
1017
  this.projectRoot = projectRoot;
@@ -1089,7 +1103,12 @@ function handleCacheWatcherEvent(relativePath, projectRoot, sessions, timers) {
1089
1103
  }
1090
1104
  }
1091
1105
  function createFabricHttpApp(options) {
1092
- const { projectRoot, host = DEFAULT_HOST, authToken } = options;
1106
+ const { projectRoot, host = DEFAULT_HOST, authToken, allowLoopbackNoAuth = false } = options;
1107
+ if (allowLoopbackNoAuth && authToken === void 0 && !isLoopbackHost(host)) {
1108
+ throw new Error(
1109
+ `createFabricHttpApp: allowLoopbackNoAuth=true requires a loopback host (127.0.0.1 / localhost / ::1); got ${JSON.stringify(host)}. Either bind to loopback or set FABRIC_AUTH_TOKEN.`
1110
+ );
1111
+ }
1093
1112
  const app = createMcpExpressApp({ host });
1094
1113
  const eventStore = new JsonlEventStore(projectRoot);
1095
1114
  const sessions = /* @__PURE__ */ new Map();
@@ -1142,6 +1161,11 @@ function createFabricHttpApp(options) {
1142
1161
  app.use("/api", bearerAuth);
1143
1162
  app.use("/events", bearerAuth);
1144
1163
  app.use("/mcp", bearerAuth);
1164
+ } else if (!allowLoopbackNoAuth) {
1165
+ const denyAll = createLoopbackDenyMiddleware();
1166
+ app.use("/api", denyAll);
1167
+ app.use("/events", denyAll);
1168
+ app.use("/mcp", denyAll);
1145
1169
  }
1146
1170
  registerKnowledgeApi(app, projectRoot);
1147
1171
  registerKnowledgeContextApi(app, projectRoot);
package/dist/index.d.ts CHANGED
@@ -40,6 +40,11 @@ type DoctorIssue = {
40
40
  path?: string;
41
41
  actionHint?: string;
42
42
  };
43
+ type DoctorPayloadLimits = {
44
+ warn_bytes: number;
45
+ hard_bytes: number;
46
+ source: "default" | "config";
47
+ };
43
48
  type DoctorSummary = {
44
49
  target: string;
45
50
  framework: {
@@ -60,6 +65,7 @@ type DoctorSummary = {
60
65
  warningCount: number;
61
66
  infoCount: number;
62
67
  targetFiles: Record<string, boolean>;
68
+ payload_limits: DoctorPayloadLimits;
63
69
  };
64
70
  type DoctorReport = {
65
71
  status: DoctorStatus;
@@ -156,7 +162,7 @@ type ArchiveHistoryReport = {
156
162
  declare function runDoctorArchiveHistory(projectRoot: string, options: {
157
163
  since: number;
158
164
  }): Promise<ArchiveHistoryReport>;
159
- type EnrichDescriptionsMode = "auto" | "interactive";
165
+ type EnrichDescriptionsMode = "auto" | "preview" | "readonly" | "interactive";
160
166
  type EnrichDescriptionsCandidate = {
161
167
  path: string;
162
168
  missing: Array<"intent_clues" | "tech_stack" | "impact" | "must_read_if">;
@@ -194,13 +200,13 @@ type WriteKnowledgeMetaOptions = {
194
200
  * guideline+process = deferred to rc.25 LLM-judge). Cited ids absent from
195
201
  * this map fall into the `cite_id_unresolved` bucket.
196
202
  *
197
- * **Singular knowledge_type contract (rc.24 lock):** the returned map values
198
- * are the SINGULAR `KnowledgeType` enum (`"model" | "decision" | "guideline"
199
- * | "pitfall" | "process"`) — matching both the on-disk `agents.meta.json`
200
- * storage AND the canonical `KnowledgeTypeSchema` exported from
201
- * `@fenglimg/fabric-shared`. No normalization happens at this boundary; the
202
- * loader is a thin extract over engine-maintained meta. Downstream callers
203
- * (TASK-08 doctor) must match against the singular enum.
203
+ * **Plural knowledge_type contract (rc.29 BUG-C1 unification):** the returned
204
+ * map values are the PLURAL `KnowledgeType` enum (`"models" | "decisions" |
205
+ * "guidelines" | "pitfalls" | "processes"`) — matching disk frontmatter,
206
+ * filesystem layout, MCP I/O surface, and the canonical `KnowledgeTypeSchema`
207
+ * exported from `@fenglimg/fabric-shared`. Legacy singular frontmatter is
208
+ * normalized at parse time (see `SINGULAR_TO_PLURAL` in `parseFrontmatter`);
209
+ * downstream callers (TASK-08 doctor) match against the plural enum.
204
210
  *
205
211
  * Both team (KT-*) and personal (KP-*) entries are included — they live in
206
212
  * the same `meta.nodes` map.
@@ -382,6 +388,19 @@ interface KnowledgeSyncOptions {
382
388
  mode?: "incremental" | "full";
383
389
  /** When true, invalid frontmatter throws RuleValidationError (default: false — collect as warning). */
384
390
  throwOnInvalidFrontmatter?: boolean;
391
+ /**
392
+ * v2.0.0-rc.29 TASK-005 (BUG-G1): when true, `ensureKnowledgeFresh`
393
+ * synchronously follows a drift detection with a `reconcileKnowledge`
394
+ * call to materialize the auto-heal (rewrite agents.meta.json + emit a
395
+ * paired `knowledge_meta_auto_healed` event). Default false preserves
396
+ * the rc.28 hot-path semantics where drift detection never blocks the
397
+ * MCP read on a meta rebuild. Opt-in is intended for callers that can
398
+ * tolerate ~tens-of-ms extra latency in exchange for the invariant
399
+ * "every knowledge_drift_detected has a paired heal event in the same
400
+ * tail window." Audit (BUG-G1) found 5/72 drifts healed on this repo
401
+ * (~7%) because the hot path emitted detect-only events.
402
+ */
403
+ autoHealOnDrift?: boolean;
385
404
  }
386
405
  interface StructuredWarning {
387
406
  code: string;
@@ -432,7 +451,7 @@ interface ReconcileKnowledgeOptions {
432
451
  * plan_context call's auto-heal, which leaves the entry undiscoverable in
433
452
  * the description_index window between approve and the next hint call.
434
453
  */
435
- trigger?: "startup" | "doctor" | "manual" | "auto-heal-description" | "post-approve" | "post-modify";
454
+ trigger?: "startup" | "doctor" | "manual" | "auto-heal-description" | "auto-heal-after-drift" | "post-approve" | "post-modify";
436
455
  }
437
456
  /**
438
457
  * Full scan + rewrites agents.meta.json with ground-truth disk state + emits
@@ -501,6 +520,7 @@ declare function startHttpServer(options: {
501
520
  projectRoot: string;
502
521
  host?: string;
503
522
  authToken?: string;
523
+ allowLoopbackNoAuth?: boolean;
504
524
  }): Promise<Server>;
505
525
 
506
526
  export { AGENTS_MD_RESOURCE_URI, type AcquireOptions, type ArchiveHistoryEntry, type ArchiveHistoryReport, type CiteCoverageReport, type DoctorApplyLintMutation, type DoctorApplyLintMutationKind, type DoctorApplyLintReport, type DoctorFixReport, type DoctorIssue, type DoctorReport, EVENT_LEDGER_PATH, type EnrichDescriptionsCandidate, type EnrichDescriptionsMode, type EnrichDescriptionsReport, type InFlightTracker, KnowledgeIdAllocator, type KnowledgeMetaBuildResult, type KnowledgeMetaBuildSource, type KnowledgeSyncLedgerEvent, type KnowledgeSyncOptions, type KnowledgeSyncReport, LEDGER_PATH, LEGACY_LEDGER_PATH, type LedgerEvent, type LockState, type PlanContextInput, type PlanContextResult, type ReconcileKnowledgeOptions, type RequirementProfile, type SelectionTokenState, ServeLockHeldError, type ShutdownHandlerDeps, type StructuredWarning, type WriteKnowledgeMetaOptions, acquireLock, appendEventLedgerEvent, buildKnowledgeMeta, checkLockOrThrow, computeKnowledgeBasedAgentsMeta, computeKnowledgeTestIndex, createFabricServer, createInFlightTracker, createShutdownHandler, deriveKnowledgeMetaLayer, deriveKnowledgeMetaTopologyType, enrichDescriptions, ensureKnowledgeFresh, extractKnowledge, flushAndSyncEventLedger, formatPreexistingRootMessage, getEventLedgerPath, getLedgerPath, getLegacyLedgerPath, isSameKnowledgeTestIndex, loadKbIdTypeMap, planContext, readLockState, readSelectionToken, reconcileKnowledge, releaseLock, reviewKnowledge, runDoctorApplyLint, runDoctorArchiveHistory, runDoctorCiteCoverage, runDoctorFix, runDoctorReport, stableStringify, startHttpServer, startStdioServer, writeKnowledgeMeta };
package/dist/index.js CHANGED
@@ -26,6 +26,8 @@ import {
26
26
  loadKbIdTypeMap,
27
27
  normalizeKnowledgePath,
28
28
  readLockState,
29
+ readPayloadLimits,
30
+ readSelectionTokenTtlMs,
29
31
  reconcileKnowledge,
30
32
  releaseLock,
31
33
  resolveProjectRoot,
@@ -37,12 +39,12 @@ import {
37
39
  sha256,
38
40
  stableStringify,
39
41
  writeKnowledgeMeta
40
- } from "./chunk-NZSGNQKE.js";
42
+ } from "./chunk-CTQ4UMO4.js";
41
43
 
42
44
  // src/index.ts
43
- import { existsSync as existsSync4 } from "fs";
45
+ import { existsSync as existsSync3 } from "fs";
44
46
  import { readFile as readFile5 } from "fs/promises";
45
- import { join as join5, resolve as resolve2 } from "path";
47
+ import { join as join4, resolve as resolve2 } from "path";
46
48
  import { fileURLToPath } from "url";
47
49
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
48
50
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -155,29 +157,11 @@ import {
155
157
  } from "@fenglimg/fabric-shared/schemas/api-contracts";
156
158
  import { enforcePayloadLimit } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
157
159
 
158
- // src/config-loader.ts
159
- import { existsSync, readFileSync } from "fs";
160
- import { join } from "path";
161
- function readFabricConfig(projectRoot) {
162
- const configPath = join(projectRoot, "fabric.config.json");
163
- if (!existsSync(configPath)) {
164
- return {};
165
- }
166
- const parsed = JSON.parse(readFileSync(configPath, "utf8"));
167
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
168
- throw new Error(`Expected object in ${configPath}`);
169
- }
170
- return parsed;
171
- }
172
- function readPayloadLimits(projectRoot) {
173
- return readFabricConfig(projectRoot).mcpPayloadLimits;
174
- }
175
-
176
160
  // src/services/extract-knowledge.ts
177
- import { existsSync as existsSync2 } from "fs";
161
+ import { existsSync } from "fs";
178
162
  import { readFile } from "fs/promises";
179
163
  import { homedir } from "os";
180
- import { join as join2, relative } from "path";
164
+ import { join, relative } from "path";
181
165
  import {
182
166
  PROPOSED_REASON_DESCRIPTIONS
183
167
  } from "@fenglimg/fabric-shared/schemas/api-contracts";
@@ -185,9 +169,9 @@ var TEAM_PENDING_REL = ".fabric/knowledge/pending";
185
169
  var SLUG_MAX_LENGTH = 40;
186
170
  function pendingBase(layer, projectRoot) {
187
171
  if (layer === "personal") {
188
- return join2(resolvePersonalRoot(), ".fabric", "knowledge", "pending");
172
+ return join(resolvePersonalRoot(), ".fabric", "knowledge", "pending");
189
173
  }
190
- return join2(projectRoot, TEAM_PENDING_REL);
174
+ return join(projectRoot, TEAM_PENDING_REL);
191
175
  }
192
176
  function resolvePersonalRoot() {
193
177
  return process.env.FABRIC_HOME ?? homedir();
@@ -243,10 +227,10 @@ async function extractKnowledge(projectRoot, input) {
243
227
  });
244
228
  }
245
229
  const baseDir = pendingBase(layer, projectRoot);
246
- const absolutePath = join2(baseDir, input.type, `${sanitizedSlug}.md`);
230
+ const absolutePath = join(baseDir, input.type, `${sanitizedSlug}.md`);
247
231
  const reportedPath = layer === "personal" ? `~/${relative(resolvePersonalRoot(), absolutePath)}` : relative(projectRoot, absolutePath);
248
232
  await ensureParentDirectory(absolutePath);
249
- if (existsSync2(absolutePath)) {
233
+ if (existsSync(absolutePath)) {
250
234
  const existing = await readFile(absolutePath, "utf8");
251
235
  const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
252
236
  if (existingKey === idempotencyKey) {
@@ -584,7 +568,7 @@ import { enforcePayloadLimit as enforcePayloadLimit2 } from "@fenglimg/fabric-sh
584
568
  // src/services/plan-context.ts
585
569
  import { minimatch } from "minimatch";
586
570
  import { deriveAgentsMetaLayer } from "@fenglimg/fabric-shared";
587
- var SELECTION_TOKEN_TTL_MS = 5 * 60 * 1e3;
571
+ var SELECTION_TOKEN_TTL_DEFAULT_MS = 5 * 60 * 1e3;
588
572
  var selectionTokenCache = /* @__PURE__ */ new Map();
589
573
  function assertPathInSandbox(rawPath) {
590
574
  if (rawPath === "**" || rawPath === "*") return;
@@ -644,7 +628,8 @@ async function planContext(projectRoot, input) {
644
628
  });
645
629
  const sharedDescriptionIndex = dedupeDescriptionIndex(entries.flatMap((entry) => entry.description_index));
646
630
  const sharedStableIds = sharedDescriptionIndex.map((item) => item.stable_id);
647
- const selectionToken = createSelectionToken(meta.revision, uniquePaths, [], sharedStableIds);
631
+ const ttlMs = readSelectionTokenTtlMs(projectRoot) ?? SELECTION_TOKEN_TTL_DEFAULT_MS;
632
+ const selectionToken = createSelectionToken(meta.revision, uniquePaths, [], sharedStableIds, Date.now(), ttlMs);
648
633
  const result = {
649
634
  revision_hash: meta.revision,
650
635
  stale,
@@ -694,7 +679,7 @@ function readSelectionToken(token, now = Date.now()) {
694
679
  }
695
680
  return state2;
696
681
  }
697
- function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSelectableStableIds, now = Date.now()) {
682
+ function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSelectableStableIds, now = Date.now(), ttlMs = SELECTION_TOKEN_TTL_DEFAULT_MS) {
698
683
  const token = `selection:${revisionHash}:${now.toString(36)}:${Math.random().toString(36).slice(2)}`;
699
684
  selectionTokenCache.set(token, {
700
685
  token,
@@ -703,7 +688,7 @@ function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSe
703
688
  required_stable_ids: requiredStableIds,
704
689
  ai_selectable_stable_ids: aiSelectableStableIds,
705
690
  created_at: now,
706
- expires_at: now + SELECTION_TOKEN_TTL_MS
691
+ expires_at: now + ttlMs
707
692
  });
708
693
  return token;
709
694
  }
@@ -916,10 +901,10 @@ import { enforcePayloadLimit as enforcePayloadLimit3 } from "@fenglimg/fabric-sh
916
901
 
917
902
  // src/services/review.ts
918
903
  import { execFileSync } from "child_process";
919
- import { existsSync as existsSync3 } from "fs";
904
+ import { existsSync as existsSync2 } from "fs";
920
905
  import { readFile as readFile3, readdir, unlink } from "fs/promises";
921
906
  import { homedir as homedir2 } from "os";
922
- import { basename, join as join3, relative as relative2, resolve } from "path";
907
+ import { basename, join as join2, relative as relative2, resolve } from "path";
923
908
 
924
909
  // src/services/knowledge-id-allocator.ts
925
910
  import { readFile as readFile2 } from "fs/promises";
@@ -1002,9 +987,9 @@ function isNodeError(err) {
1002
987
  var PENDING_BASE_TEAM_REL = ".fabric/knowledge/pending";
1003
988
  function pendingBaseAbs(layer, projectRoot) {
1004
989
  if (layer === "personal") {
1005
- return join3(resolvePersonalRoot2(), ".fabric", "knowledge", "pending");
990
+ return join2(resolvePersonalRoot2(), ".fabric", "knowledge", "pending");
1006
991
  }
1007
- return join3(projectRoot, PENDING_BASE_TEAM_REL);
992
+ return join2(projectRoot, PENDING_BASE_TEAM_REL);
1008
993
  }
1009
994
  var PLURAL_TYPES = [
1010
995
  "decisions",
@@ -1013,13 +998,6 @@ var PLURAL_TYPES = [
1013
998
  "models",
1014
999
  "processes"
1015
1000
  ];
1016
- var PLURAL_TO_SINGULAR = {
1017
- decisions: "decision",
1018
- pitfalls: "pitfall",
1019
- guidelines: "guideline",
1020
- models: "model",
1021
- processes: "process"
1022
- };
1023
1001
  async function reviewKnowledge(projectRoot, input) {
1024
1002
  switch (input.action) {
1025
1003
  case "list":
@@ -1114,8 +1092,8 @@ async function listPending(projectRoot, filters) {
1114
1092
  ];
1115
1093
  for (const source of sources) {
1116
1094
  for (const type of typesToScan) {
1117
- const dir = join3(source.root, type);
1118
- if (!existsSync3(dir)) {
1095
+ const dir = join2(source.root, type);
1096
+ if (!existsSync2(dir)) {
1119
1097
  continue;
1120
1098
  }
1121
1099
  let entries;
@@ -1126,7 +1104,7 @@ async function listPending(projectRoot, filters) {
1126
1104
  }
1127
1105
  for (const name of entries) {
1128
1106
  if (!name.endsWith(".md")) continue;
1129
- const absolutePath = join3(dir, name);
1107
+ const absolutePath = join2(dir, name);
1130
1108
  let content;
1131
1109
  try {
1132
1110
  content = await readFile3(absolutePath, "utf8");
@@ -1182,7 +1160,7 @@ async function listPending(projectRoot, filters) {
1182
1160
  }
1183
1161
  async function approveAll(projectRoot, pendingPaths) {
1184
1162
  const allocator = new KnowledgeIdAllocator(
1185
- join3(projectRoot, ".fabric", "agents.meta.json")
1163
+ join2(projectRoot, ".fabric", "agents.meta.json")
1186
1164
  );
1187
1165
  const approved = [];
1188
1166
  for (const pendingPath of pendingPaths) {
@@ -1233,12 +1211,11 @@ async function approveOne(projectRoot, pendingPath, allocator) {
1233
1211
  throw new Error(`pending file missing or invalid 'type' frontmatter: ${pendingPath}`);
1234
1212
  }
1235
1213
  const layer = fm.layer ?? "team";
1236
- const singularType = PLURAL_TO_SINGULAR[pluralType];
1237
- const stableId = await allocator.allocate(layer, singularType);
1214
+ const stableId = await allocator.allocate(layer, pluralType);
1238
1215
  allocatedId = stableId;
1239
1216
  const newFilename = `${stableId}--${slug}.md`;
1240
- const layerRoot = layer === "personal" ? join3(resolvePersonalRoot2(), ".fabric") : join3(projectRoot, ".fabric");
1241
- targetAbs = join3(layerRoot, "knowledge", pluralType, newFilename);
1217
+ const layerRoot = layer === "personal" ? join2(resolvePersonalRoot2(), ".fabric") : join2(projectRoot, ".fabric");
1218
+ targetAbs = join2(layerRoot, "knowledge", pluralType, newFilename);
1242
1219
  await ensureParentDirectory(targetAbs);
1243
1220
  const rewritten = rewriteFrontmatterForPromote(content, stableId);
1244
1221
  await atomicWriteText(targetAbs, rewritten);
@@ -1250,12 +1227,12 @@ async function approveOne(projectRoot, pendingPath, allocator) {
1250
1227
  stdio: ["ignore", "pipe", "pipe"]
1251
1228
  });
1252
1229
  } catch {
1253
- if (existsSync3(sourceAbs)) {
1230
+ if (existsSync2(sourceAbs)) {
1254
1231
  await unlink(sourceAbs);
1255
1232
  }
1256
1233
  }
1257
1234
  } else {
1258
- if (existsSync3(sourceAbs)) {
1235
+ if (existsSync2(sourceAbs)) {
1259
1236
  await unlink(sourceAbs);
1260
1237
  }
1261
1238
  }
@@ -1271,7 +1248,7 @@ async function approveOne(projectRoot, pendingPath, allocator) {
1271
1248
  }
1272
1249
  return { pending_path: pendingPath, stable_id: stableId };
1273
1250
  } catch (err) {
1274
- if (writtenTarget && targetAbs !== void 0 && existsSync3(targetAbs)) {
1251
+ if (writtenTarget && targetAbs !== void 0 && existsSync2(targetAbs)) {
1275
1252
  try {
1276
1253
  await unlink(targetAbs);
1277
1254
  } catch {
@@ -1292,7 +1269,7 @@ async function rejectAll(projectRoot, pendingPaths, reason) {
1292
1269
  for (const pendingPath of pendingPaths) {
1293
1270
  try {
1294
1271
  const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
1295
- if (existsSync3(sandboxed.abs)) {
1272
+ if (existsSync2(sandboxed.abs)) {
1296
1273
  const content = await readFile3(sandboxed.abs, "utf8");
1297
1274
  const merged = rewriteFrontmatterMerge(content, { status: "rejected" });
1298
1275
  if (merged !== content) {
@@ -1335,7 +1312,7 @@ function resolveModifyTarget(projectRoot, pendingPath) {
1335
1312
  } catch {
1336
1313
  return null;
1337
1314
  }
1338
- if (existsSync3(sandboxed.abs)) {
1315
+ if (existsSync2(sandboxed.abs)) {
1339
1316
  return {
1340
1317
  absPath: sandboxed.abs,
1341
1318
  isInProjectTree: sandboxed.isInProjectTree,
@@ -1375,12 +1352,11 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
1375
1352
  const fromScope = fm.relevance_scope ?? "broad";
1376
1353
  const shouldAutoDegrade = fromScope === "narrow" && fromLayer === "team" && toLayer === "personal";
1377
1354
  const allocator = new KnowledgeIdAllocator(
1378
- join3(projectRoot, ".fabric", "agents.meta.json")
1355
+ join2(projectRoot, ".fabric", "agents.meta.json")
1379
1356
  );
1380
- const singularType = PLURAL_TO_SINGULAR[pluralType];
1381
- const newStableId = await allocator.allocate(toLayer, singularType);
1382
- const toRoot = toLayer === "personal" ? join3(resolvePersonalRoot2(), ".fabric") : join3(projectRoot, ".fabric");
1383
- const toAbs = join3(toRoot, "knowledge", pluralType, `${newStableId}--${slug}.md`);
1357
+ const newStableId = await allocator.allocate(toLayer, pluralType);
1358
+ const toRoot = toLayer === "personal" ? join2(resolvePersonalRoot2(), ".fabric") : join2(projectRoot, ".fabric");
1359
+ const toAbs = join2(toRoot, "knowledge", pluralType, `${newStableId}--${slug}.md`);
1384
1360
  await ensureParentDirectory(toAbs);
1385
1361
  await emitEventBestEffort2(projectRoot, {
1386
1362
  event_type: "knowledge_promote_started",
@@ -1404,11 +1380,11 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
1404
1380
  stdio: ["ignore", "pipe", "pipe"]
1405
1381
  });
1406
1382
  } catch {
1407
- if (existsSync3(target.absPath)) {
1383
+ if (existsSync2(target.absPath)) {
1408
1384
  await unlink(target.absPath);
1409
1385
  }
1410
1386
  }
1411
- } else if (existsSync3(target.absPath)) {
1387
+ } else if (existsSync2(target.absPath)) {
1412
1388
  await unlink(target.absPath);
1413
1389
  }
1414
1390
  await emitEventBestEffort2(projectRoot, {
@@ -1447,14 +1423,14 @@ async function searchEntries(projectRoot, query, filters) {
1447
1423
  const sources = [
1448
1424
  { root: pendingBaseAbs("team", projectRoot), isPending: true, isPersonal: false },
1449
1425
  { root: pendingBaseAbs("personal", projectRoot), isPending: true, isPersonal: true },
1450
- { root: join3(projectRoot, ".fabric", "knowledge"), isPending: false, isPersonal: false },
1451
- { root: join3(resolvePersonalRoot2(), ".fabric", "knowledge"), isPending: false, isPersonal: true }
1426
+ { root: join2(projectRoot, ".fabric", "knowledge"), isPending: false, isPersonal: false },
1427
+ { root: join2(resolvePersonalRoot2(), ".fabric", "knowledge"), isPending: false, isPersonal: true }
1452
1428
  ];
1453
1429
  const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
1454
1430
  for (const source of sources) {
1455
1431
  for (const type of typesToScan) {
1456
- const dir = join3(source.root, type);
1457
- if (!existsSync3(dir)) continue;
1432
+ const dir = join2(source.root, type);
1433
+ if (!existsSync2(dir)) continue;
1458
1434
  let entries;
1459
1435
  try {
1460
1436
  entries = await readdir(dir);
@@ -1463,7 +1439,7 @@ async function searchEntries(projectRoot, query, filters) {
1463
1439
  }
1464
1440
  for (const name of entries) {
1465
1441
  if (!name.endsWith(".md")) continue;
1466
- const absolutePath = join3(dir, name);
1442
+ const absolutePath = join2(dir, name);
1467
1443
  let content;
1468
1444
  try {
1469
1445
  content = await readFile3(absolutePath, "utf8");
@@ -1505,15 +1481,14 @@ async function searchEntries(projectRoot, query, filters) {
1505
1481
  if (!matches) continue;
1506
1482
  const reportedPath = source.isPersonal ? `~/${relative2(resolvePersonalRoot2(), absolutePath)}` : relative2(projectRoot, absolutePath);
1507
1483
  items.push({
1508
- pending_path: reportedPath,
1509
- // v2.0.0-rc.27 TASK-001 (§2.12): absolute companion for personal
1510
- // entries (mirrors listPending).
1511
- ...source.isPersonal ? { pending_path_absolute: absolutePath } : {},
1484
+ area: source.isPending ? "pending" : "canonical",
1485
+ path: reportedPath,
1486
+ ...source.isPersonal ? { path_absolute: absolutePath } : {},
1512
1487
  type,
1513
1488
  layer,
1514
1489
  maturity,
1515
- // Only pending entries carry an origin tag (search results that are
1516
- // canonical entries don't have a pending root to point back to).
1490
+ // Only pending entries carry an origin tag (canonical hits live
1491
+ // outside the dual-pending-root convention).
1517
1492
  ...source.isPending ? { origin: source.isPersonal ? "personal" : "team" } : {},
1518
1493
  ...fm.tags !== void 0 && fm.tags.length > 0 ? { tags: fm.tags } : {},
1519
1494
  ...fm.title !== void 0 ? { title: fm.title } : {},
@@ -1521,10 +1496,11 @@ async function searchEntries(projectRoot, query, filters) {
1521
1496
  ...fm.status !== void 0 ? { status: fm.status } : {},
1522
1497
  ...fm.deferred_until !== void 0 ? { deferred_until: fm.deferred_until } : {},
1523
1498
  // v2.0.0-rc.27 TASK-006 (audit §2.23): body emission when opted in.
1524
- // Reuse the already-computed bodyForSearch to avoid a second pass
1525
- // over the content (the search loop above extracted it iff
1526
- // include_body=true).
1527
- ...filters?.include_body === true ? { body: bodyForSearch } : {}
1499
+ ...filters?.include_body === true ? { body: bodyForSearch } : {},
1500
+ // Canonical hits always have an id; pending hits typically don't
1501
+ // yet — surface the frontmatter id when present so consumers can
1502
+ // dedupe across runs.
1503
+ ...fm.id !== void 0 ? { stable_id: fm.id } : {}
1528
1504
  });
1529
1505
  }
1530
1506
  }
@@ -1536,7 +1512,7 @@ async function deferAll(projectRoot, pendingPaths, until, reason) {
1536
1512
  for (const pendingPath of pendingPaths) {
1537
1513
  try {
1538
1514
  const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
1539
- if (existsSync3(sandboxed.abs)) {
1515
+ if (existsSync2(sandboxed.abs)) {
1540
1516
  const content = await readFile3(sandboxed.abs, "utf8");
1541
1517
  const patch = {
1542
1518
  status: "deferred",
@@ -1797,7 +1773,7 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
1797
1773
  // src/services/knowledge-sections.ts
1798
1774
  import { readFile as readFile4 } from "fs/promises";
1799
1775
  import { homedir as homedir3 } from "os";
1800
- import { join as join4 } from "path";
1776
+ import { join as join3 } from "path";
1801
1777
  var PRIORITY_ORDER = {
1802
1778
  high: 0,
1803
1779
  medium: 1,
@@ -1957,9 +1933,9 @@ function outputLevelOrder(level) {
1957
1933
  function resolveRuleSourcePath(projectRoot, contentRef) {
1958
1934
  if (contentRef.startsWith("~/.fabric/knowledge/")) {
1959
1935
  const home = process.env.FABRIC_HOME ?? homedir3();
1960
- return join4(home, ".fabric", "knowledge", contentRef.slice("~/.fabric/knowledge/".length));
1936
+ return join3(home, ".fabric", "knowledge", contentRef.slice("~/.fabric/knowledge/".length));
1961
1937
  }
1962
- return join4(projectRoot, contentRef);
1938
+ return join3(projectRoot, contentRef);
1963
1939
  }
1964
1940
  function pickSelectionReasons(selectedStableIds, reasons) {
1965
1941
  return Object.fromEntries(selectedStableIds.map((stableId) => [stableId, reasons[stableId] ?? ""]));
@@ -2028,15 +2004,15 @@ function formatError(error) {
2028
2004
  }
2029
2005
  function formatPreexistingRootMessage(projectRoot) {
2030
2006
  const preexisting = [];
2031
- if (existsSync4(join5(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
2032
- if (existsSync4(join5(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
2007
+ if (existsSync3(join4(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
2008
+ if (existsSync3(join4(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
2033
2009
  if (preexisting.length === 0) return null;
2034
2010
  return `[startup] info: detected ${preexisting.join(", ")} at project root. Note: Fabric serves knowledge from .fabric/knowledge/ via MCP \u2014 root markdown files are not auto-loaded into the AI context.`;
2035
2011
  }
2036
2012
  function createFabricServer(tracker) {
2037
2013
  const server = new McpServer({
2038
2014
  name: "fabric-knowledge-server",
2039
- version: "2.0.0-rc.27"
2015
+ version: "2.0.0-rc.29"
2040
2016
  });
2041
2017
  registerPlanContext(server, tracker);
2042
2018
  registerKnowledgeSections(server, tracker);
@@ -2051,9 +2027,9 @@ function createFabricServer(tracker) {
2051
2027
  },
2052
2028
  async (_uri) => {
2053
2029
  const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
2054
- const path = join5(projectRoot, ".fabric", "bootstrap", "README.md");
2030
+ const path = join4(projectRoot, ".fabric", "bootstrap", "README.md");
2055
2031
  let text = "";
2056
- if (existsSync4(path)) {
2032
+ if (existsSync3(path)) {
2057
2033
  text = await readFile5(path, "utf8");
2058
2034
  }
2059
2035
  return {
@@ -2144,9 +2120,9 @@ function createShutdownHandler(deps) {
2144
2120
  };
2145
2121
  }
2146
2122
  async function startHttpServer(options) {
2147
- const { createFabricHttpApp } = await import("./http-3WADEK3O.js");
2148
- const { port, projectRoot, host = "127.0.0.1", authToken } = options;
2149
- const app = createFabricHttpApp({ projectRoot, host, authToken });
2123
+ const { createFabricHttpApp } = await import("./http-TAI5X7U5.js");
2124
+ const { port, projectRoot, host = "127.0.0.1", authToken, allowLoopbackNoAuth } = options;
2125
+ const app = createFabricHttpApp({ projectRoot, host, authToken, allowLoopbackNoAuth });
2150
2126
  return await new Promise((resolveServer, rejectServer) => {
2151
2127
  const server = app.listen(port, host);
2152
2128
  server.once("close", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fenglimg/fabric-server",
3
- "version": "2.0.0-rc.27",
3
+ "version": "2.0.0-rc.29",
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.27"
16
+ "@fenglimg/fabric-shared": "2.0.0-rc.29"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/express": "^5.0.6",