@fenglimg/fabric-server 2.0.0-rc.26 → 2.0.0-rc.27
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.
|
@@ -184,6 +184,46 @@ var EVENT_LEDGER_DEFAULT_RETENTION_DAYS = 30;
|
|
|
184
184
|
var EVENT_LEDGER_SIZE_WARN_BYTES = 50 * 1024 * 1024;
|
|
185
185
|
var EVENT_LEDGER_ARCHIVE_DIR = ".fabric/events.archive";
|
|
186
186
|
var warnedOversize = false;
|
|
187
|
+
var knownEventTypesCache = null;
|
|
188
|
+
function getKnownEventTypes() {
|
|
189
|
+
if (knownEventTypesCache !== null) return knownEventTypesCache;
|
|
190
|
+
const set = /* @__PURE__ */ new Set();
|
|
191
|
+
for (const opt of eventLedgerEventSchema.options) {
|
|
192
|
+
const shape = opt.shape;
|
|
193
|
+
if (shape && typeof shape.event_type?.value === "string") {
|
|
194
|
+
set.add(shape.event_type.value);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
knownEventTypesCache = set;
|
|
198
|
+
return set;
|
|
199
|
+
}
|
|
200
|
+
function classifyRejection(line, index) {
|
|
201
|
+
let parsed;
|
|
202
|
+
try {
|
|
203
|
+
parsed = JSON.parse(line);
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
if (parsed === null || typeof parsed !== "object") return null;
|
|
208
|
+
if ("schema_version" in parsed && parsed.schema_version !== 1 && (typeof parsed.schema_version === "number" || parsed.schema_version === null)) {
|
|
209
|
+
return {
|
|
210
|
+
kind: "schema_version_unsupported",
|
|
211
|
+
line_index: index,
|
|
212
|
+
schema_version: parsed.schema_version,
|
|
213
|
+
snippet_first_120: line.slice(0, 120)
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const known = getKnownEventTypes();
|
|
217
|
+
if (typeof parsed.event_type === "string" && !known.has(parsed.event_type)) {
|
|
218
|
+
return {
|
|
219
|
+
kind: "event_type_unknown",
|
|
220
|
+
line_index: index,
|
|
221
|
+
event_type: parsed.event_type,
|
|
222
|
+
snippet_first_120: line.slice(0, 120)
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
187
227
|
async function appendEventLedgerEvent(projectRoot, event) {
|
|
188
228
|
const eventPath = getEventLedgerPath(projectRoot);
|
|
189
229
|
const nextEvent = eventLedgerEventSchema.parse({
|
|
@@ -238,8 +278,20 @@ async function readEventLedger(projectRoot, options = {}) {
|
|
|
238
278
|
snippet_first_120: partialLine.slice(0, 120)
|
|
239
279
|
});
|
|
240
280
|
}
|
|
241
|
-
const
|
|
242
|
-
|
|
281
|
+
const trimmed = lines.map((line) => line.trim()).filter((line) => line.length > 0);
|
|
282
|
+
const events = [];
|
|
283
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
284
|
+
const line = trimmed[i];
|
|
285
|
+
const parsed = parseEventLedgerLine(line, i);
|
|
286
|
+
if (parsed !== null) {
|
|
287
|
+
events.push(parsed);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const rejection = classifyRejection(line, i);
|
|
291
|
+
if (rejection !== null) warnings.push(rejection);
|
|
292
|
+
}
|
|
293
|
+
const filtered = events.filter((entry) => options.event_type === void 0 || entry.event_type === options.event_type).filter((entry) => options.since === void 0 || entry.ts >= options.since).filter((entry) => options.correlation_id === void 0 || entry.correlation_id === options.correlation_id).filter((entry) => options.session_id === void 0 || entry.session_id === options.session_id);
|
|
294
|
+
return { events: filtered, warnings };
|
|
243
295
|
}
|
|
244
296
|
async function truncateLedgerToLastNewline(path2) {
|
|
245
297
|
const raw = await readFile2(path2);
|
|
@@ -951,16 +1003,18 @@ function extractRuleDescription(source) {
|
|
|
951
1003
|
}
|
|
952
1004
|
const heading = /^#\s+(.+?)\s*$/mu.exec(source);
|
|
953
1005
|
const summary = heading?.[1]?.trim();
|
|
954
|
-
|
|
1006
|
+
const knowledge = frontmatter !== null ? extractKnowledgeFieldsFromFrontmatter(frontmatter[1]) : void 0;
|
|
1007
|
+
const isStructurallyAKnowledgeEntry = summary !== void 0 && summary.length > 0 ? true : knowledge !== void 0 && (knowledge.id !== void 0 || knowledge.knowledge_type !== void 0 || knowledge.tags !== void 0 && knowledge.tags.length > 0);
|
|
1008
|
+
if (!isStructurallyAKnowledgeEntry) {
|
|
955
1009
|
return void 0;
|
|
956
1010
|
}
|
|
957
|
-
const
|
|
1011
|
+
const synthesizedSummary = summary !== void 0 && summary.length > 0 ? summary : knowledge?.id ?? (knowledge?.tags !== void 0 && knowledge.tags.length > 0 ? `(unnamed; tags: ${knowledge.tags.join(", ")})` : "(unnamed knowledge entry)");
|
|
958
1012
|
return {
|
|
959
|
-
summary,
|
|
1013
|
+
summary: synthesizedSummary,
|
|
960
1014
|
intent_clues: [],
|
|
961
1015
|
tech_stack: [],
|
|
962
1016
|
impact: [],
|
|
963
|
-
must_read_if:
|
|
1017
|
+
must_read_if: synthesizedSummary,
|
|
964
1018
|
// v2.0-rc.22: when frontmatter is present, merge its knowledge fields;
|
|
965
1019
|
// when fully absent (no `---` block), all knowledge fields stay
|
|
966
1020
|
// undefined, matching the original heading-only fallback contract.
|
|
@@ -1757,6 +1811,10 @@ async function runDoctorReport(target) {
|
|
|
1757
1811
|
createKnowledgeTestIndexCheck(t, knowledgeTestIndex),
|
|
1758
1812
|
createEventLedgerCheck(t, eventLedger),
|
|
1759
1813
|
createEventLedgerPartialWriteCheck(t, eventLedger),
|
|
1814
|
+
// v2.0.0-rc.27 TASK-010 (audit §2.24): forward-compat warning surface for
|
|
1815
|
+
// events.jsonl rows that fail Zod validation because of unknown
|
|
1816
|
+
// schema_version or event_type tokens. Previously silently dropped.
|
|
1817
|
+
createEventLedgerSchemaCompatCheck(t, eventLedger),
|
|
1760
1818
|
createMcpConfigInWrongFileCheck(t, mcpConfigInWrongFile),
|
|
1761
1819
|
createMetaManuallyDivergedCheck(t, metaManuallyDiverged),
|
|
1762
1820
|
createKnowledgeDirUnindexedCheck(t, knowledgeDirUnindexed),
|
|
@@ -1905,7 +1963,8 @@ async function runDoctorFix(target) {
|
|
|
1905
1963
|
"knowledge_test_index_missing",
|
|
1906
1964
|
"knowledge_test_index_stale",
|
|
1907
1965
|
"content_ref_missing",
|
|
1908
|
-
"knowledge_dir_unindexed"
|
|
1966
|
+
"knowledge_dir_unindexed",
|
|
1967
|
+
"meta_manually_diverged"
|
|
1909
1968
|
];
|
|
1910
1969
|
if (before.fixable_errors.some((issue) => reconcileCodes.includes(issue.code)) || before.warnings.some((issue) => reconcileCodes.includes(issue.code))) {
|
|
1911
1970
|
await reconcileKnowledge(projectRoot, { trigger: "doctor" });
|
|
@@ -2487,7 +2546,19 @@ async function inspectEventLedger(projectRoot) {
|
|
|
2487
2546
|
const path2 = getEventLedgerPath(projectRoot);
|
|
2488
2547
|
const exists = existsSync4(path2);
|
|
2489
2548
|
if (!exists) {
|
|
2490
|
-
return {
|
|
2549
|
+
return {
|
|
2550
|
+
exists: false,
|
|
2551
|
+
writable: false,
|
|
2552
|
+
parseable: false,
|
|
2553
|
+
hasPartialWrite: false,
|
|
2554
|
+
partialWriteByteOffset: 0,
|
|
2555
|
+
partialWriteByteLength: 0,
|
|
2556
|
+
schemaVersionUnsupportedCount: 0,
|
|
2557
|
+
eventTypeUnknownCount: 0,
|
|
2558
|
+
schemaVersionSamples: [],
|
|
2559
|
+
eventTypeSamples: [],
|
|
2560
|
+
path: path2
|
|
2561
|
+
};
|
|
2491
2562
|
}
|
|
2492
2563
|
try {
|
|
2493
2564
|
await access(path2, constants.W_OK);
|
|
@@ -2495,6 +2566,25 @@ async function inspectEventLedger(projectRoot) {
|
|
|
2495
2566
|
const raw = await readFile5(path2, "utf8");
|
|
2496
2567
|
const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
|
|
2497
2568
|
const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
|
|
2569
|
+
const schemaVersionSamples = [];
|
|
2570
|
+
const eventTypeSamples = [];
|
|
2571
|
+
let schemaVersionUnsupportedCount = 0;
|
|
2572
|
+
let eventTypeUnknownCount = 0;
|
|
2573
|
+
for (const w of warnings) {
|
|
2574
|
+
if (w.kind === "schema_version_unsupported") {
|
|
2575
|
+
schemaVersionUnsupportedCount += 1;
|
|
2576
|
+
const token = String(w.schema_version);
|
|
2577
|
+
if (!schemaVersionSamples.includes(token) && schemaVersionSamples.length < 3) {
|
|
2578
|
+
schemaVersionSamples.push(token);
|
|
2579
|
+
}
|
|
2580
|
+
} else if (w.kind === "event_type_unknown") {
|
|
2581
|
+
eventTypeUnknownCount += 1;
|
|
2582
|
+
const token = String(w.event_type);
|
|
2583
|
+
if (!eventTypeSamples.includes(token) && eventTypeSamples.length < 3) {
|
|
2584
|
+
eventTypeSamples.push(token);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2498
2588
|
return {
|
|
2499
2589
|
exists: true,
|
|
2500
2590
|
writable: true,
|
|
@@ -2502,6 +2592,10 @@ async function inspectEventLedger(projectRoot) {
|
|
|
2502
2592
|
hasPartialWrite: partialWarning !== void 0,
|
|
2503
2593
|
partialWriteByteOffset: partialWarning?.byte_offset ?? 0,
|
|
2504
2594
|
partialWriteByteLength: partialWarning?.byte_length ?? 0,
|
|
2595
|
+
schemaVersionUnsupportedCount,
|
|
2596
|
+
eventTypeUnknownCount,
|
|
2597
|
+
schemaVersionSamples,
|
|
2598
|
+
eventTypeSamples,
|
|
2505
2599
|
path: path2,
|
|
2506
2600
|
error: invalidLine === void 0 ? void 0 : "events.jsonl contains an invalid JSON line."
|
|
2507
2601
|
};
|
|
@@ -2513,6 +2607,10 @@ async function inspectEventLedger(projectRoot) {
|
|
|
2513
2607
|
hasPartialWrite: false,
|
|
2514
2608
|
partialWriteByteOffset: 0,
|
|
2515
2609
|
partialWriteByteLength: 0,
|
|
2610
|
+
schemaVersionUnsupportedCount: 0,
|
|
2611
|
+
eventTypeUnknownCount: 0,
|
|
2612
|
+
schemaVersionSamples: [],
|
|
2613
|
+
eventTypeSamples: [],
|
|
2516
2614
|
path: path2,
|
|
2517
2615
|
error: error instanceof Error ? error.message : String(error)
|
|
2518
2616
|
};
|
|
@@ -3053,6 +3151,47 @@ function createMcpConfigInWrongFileCheck(t, inspection) {
|
|
|
3053
3151
|
t("doctor.check.mcp_config_in_wrong_file.ok")
|
|
3054
3152
|
);
|
|
3055
3153
|
}
|
|
3154
|
+
function createEventLedgerSchemaCompatCheck(t, ledger) {
|
|
3155
|
+
if (!ledger.exists || !ledger.writable) {
|
|
3156
|
+
return okCheck(
|
|
3157
|
+
t("doctor.check.event_ledger_schema_compat.name"),
|
|
3158
|
+
t("doctor.check.event_ledger_schema_compat.ok.skipped")
|
|
3159
|
+
);
|
|
3160
|
+
}
|
|
3161
|
+
const hasUnsupportedVersion = ledger.schemaVersionUnsupportedCount > 0;
|
|
3162
|
+
const hasUnknownEventType = ledger.eventTypeUnknownCount > 0;
|
|
3163
|
+
if (!hasUnsupportedVersion && !hasUnknownEventType) {
|
|
3164
|
+
return okCheck(
|
|
3165
|
+
t("doctor.check.event_ledger_schema_compat.name"),
|
|
3166
|
+
t("doctor.check.event_ledger_schema_compat.ok.clean")
|
|
3167
|
+
);
|
|
3168
|
+
}
|
|
3169
|
+
const parts = [];
|
|
3170
|
+
if (hasUnsupportedVersion) {
|
|
3171
|
+
parts.push(
|
|
3172
|
+
t("doctor.check.event_ledger_schema_compat.message.schema_version", {
|
|
3173
|
+
count: String(ledger.schemaVersionUnsupportedCount),
|
|
3174
|
+
samples: ledger.schemaVersionSamples.join(", ")
|
|
3175
|
+
})
|
|
3176
|
+
);
|
|
3177
|
+
}
|
|
3178
|
+
if (hasUnknownEventType) {
|
|
3179
|
+
parts.push(
|
|
3180
|
+
t("doctor.check.event_ledger_schema_compat.message.event_type", {
|
|
3181
|
+
count: String(ledger.eventTypeUnknownCount),
|
|
3182
|
+
samples: ledger.eventTypeSamples.join(", ")
|
|
3183
|
+
})
|
|
3184
|
+
);
|
|
3185
|
+
}
|
|
3186
|
+
return issueCheck(
|
|
3187
|
+
t("doctor.check.event_ledger_schema_compat.name"),
|
|
3188
|
+
"warn",
|
|
3189
|
+
"warning",
|
|
3190
|
+
"event_ledger_schema_compat",
|
|
3191
|
+
parts.join(" "),
|
|
3192
|
+
t("doctor.check.event_ledger_schema_compat.remediation")
|
|
3193
|
+
);
|
|
3194
|
+
}
|
|
3056
3195
|
function createEventLedgerPartialWriteCheck(t, ledger) {
|
|
3057
3196
|
if (!ledger.exists || !ledger.writable) {
|
|
3058
3197
|
return okCheck(
|
package/dist/index.d.ts
CHANGED
|
@@ -425,8 +425,14 @@ interface ReconcileKnowledgeOptions {
|
|
|
425
425
|
* v2.0.0-rc.23 TASK-005 (a-B): `auto-heal-description` added so plan_context
|
|
426
426
|
* can drive a full reconcile when it detects nodes with `description === undefined`
|
|
427
427
|
* (legacy meta drift the revision-hash gate cannot detect).
|
|
428
|
+
*
|
|
429
|
+
* v2.0.0-rc.27 TASK-001 (§2.9 root): `post-approve` / `post-modify` added so
|
|
430
|
+
* `fab_review` approve/modify-layer-flip can drive an immediate meta rebuild
|
|
431
|
+
* — without this the new entry's `nodes[id]` stays empty until the next
|
|
432
|
+
* plan_context call's auto-heal, which leaves the entry undiscoverable in
|
|
433
|
+
* the description_index window between approve and the next hint call.
|
|
428
434
|
*/
|
|
429
|
-
trigger?: "startup" | "doctor" | "manual" | "auto-heal-description";
|
|
435
|
+
trigger?: "startup" | "doctor" | "manual" | "auto-heal-description" | "post-approve" | "post-modify";
|
|
430
436
|
}
|
|
431
437
|
/**
|
|
432
438
|
* Full scan + rewrites agents.meta.json with ground-truth disk state + emits
|
package/dist/index.js
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
sha256,
|
|
38
38
|
stableStringify,
|
|
39
39
|
writeKnowledgeMeta
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-NZSGNQKE.js";
|
|
41
41
|
|
|
42
42
|
// src/index.ts
|
|
43
43
|
import { existsSync as existsSync4 } from "fs";
|
|
@@ -250,7 +250,24 @@ async function extractKnowledge(projectRoot, input) {
|
|
|
250
250
|
const existing = await readFile(absolutePath, "utf8");
|
|
251
251
|
const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
|
|
252
252
|
if (existingKey === idempotencyKey) {
|
|
253
|
-
const
|
|
253
|
+
const fresh2 = renderFreshEntry({
|
|
254
|
+
type: input.type,
|
|
255
|
+
sourceSessions,
|
|
256
|
+
idempotencyKey,
|
|
257
|
+
summary,
|
|
258
|
+
recentPaths: input.recent_paths,
|
|
259
|
+
layer,
|
|
260
|
+
proposedReason: input.proposed_reason,
|
|
261
|
+
sessionContext: input.session_context,
|
|
262
|
+
relevanceScope,
|
|
263
|
+
relevancePaths,
|
|
264
|
+
intentClues: input.intent_clues,
|
|
265
|
+
techStack: input.tech_stack,
|
|
266
|
+
impact: input.impact,
|
|
267
|
+
mustReadIf: input.must_read_if,
|
|
268
|
+
onboardSlot: input.onboard_slot
|
|
269
|
+
});
|
|
270
|
+
const augmented = mergeEvidenceNotes(existing, fresh2);
|
|
254
271
|
await atomicWriteText(absolutePath, augmented);
|
|
255
272
|
await emitEventBestEffort(projectRoot, {
|
|
256
273
|
event_type: "knowledge_proposed",
|
|
@@ -402,62 +419,18 @@ function renderEvidenceBlock(summary, recentPaths) {
|
|
|
402
419
|
`- ${summary.trim()}`
|
|
403
420
|
].join("\n");
|
|
404
421
|
}
|
|
405
|
-
function mergeEvidenceNotes(existing,
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
);
|
|
410
|
-
if (beforeMatch === null) {
|
|
411
|
-
const trimmed = existing.endsWith("\n") ? existing : `${existing}
|
|
412
|
-
`;
|
|
413
|
-
return `${trimmed}
|
|
414
|
-
## Evidence
|
|
415
|
-
|
|
416
|
-
${renderEvidenceBlock(newSummary, newRecentPaths)}
|
|
422
|
+
function mergeEvidenceNotes(existing, fresh) {
|
|
423
|
+
const freshSplit = splitAtEvidence(fresh);
|
|
424
|
+
if (freshSplit === null) {
|
|
425
|
+
return fresh.endsWith("\n") ? fresh : `${fresh}
|
|
417
426
|
`;
|
|
418
427
|
}
|
|
419
|
-
const
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
const evidenceBlockRe = /\n## Evidence(?:\s*\(call \d+\))?\s*\n([\s\S]*?)(?=\n## |$)/gu;
|
|
423
|
-
let m;
|
|
424
|
-
while ((m = evidenceBlockRe.exec(`${existing}
|
|
425
|
-
`)) !== null) {
|
|
426
|
-
const block = m[1] ?? "";
|
|
427
|
-
const pathSection = /Recent paths:\s*\n([\s\S]*?)(?:\n\s*Notes:|$)/u.exec(block);
|
|
428
|
-
if (pathSection !== null) {
|
|
429
|
-
for (const rawLine of (pathSection[1] ?? "").split(/\r?\n/u)) {
|
|
430
|
-
const t = rawLine.trim();
|
|
431
|
-
if (t.startsWith("- ")) {
|
|
432
|
-
existingPaths.push(t.slice(2).trim());
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
const notesSection = /Notes:\s*\n([\s\S]*?)$/u.exec(block);
|
|
437
|
-
const noteBody = (notesSection !== null ? notesSection[1] : block) ?? "";
|
|
438
|
-
const bulletLines = [];
|
|
439
|
-
let prose = [];
|
|
440
|
-
for (const rawLine of noteBody.split(/\r?\n/u)) {
|
|
441
|
-
const t = rawLine.trim();
|
|
442
|
-
if (t.length === 0) continue;
|
|
443
|
-
if (t.startsWith("- ")) {
|
|
444
|
-
if (prose.length > 0) {
|
|
445
|
-
existingNotes.push(prose.join(" ").trim());
|
|
446
|
-
prose = [];
|
|
447
|
-
}
|
|
448
|
-
bulletLines.push(t.slice(2).trim());
|
|
449
|
-
} else {
|
|
450
|
-
prose.push(t);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
if (prose.length > 0) existingNotes.push(prose.join(" ").trim());
|
|
454
|
-
for (const n of bulletLines) existingNotes.push(n);
|
|
455
|
-
}
|
|
428
|
+
const freshHead = freshSplit.head;
|
|
429
|
+
const oldEvidence = collectEvidenceItems(existing);
|
|
430
|
+
const freshEvidence = collectEvidenceItems(fresh);
|
|
456
431
|
const mergedNotes = [];
|
|
457
432
|
const seenNotes = /* @__PURE__ */ new Set();
|
|
458
|
-
const
|
|
459
|
-
const candidates = [...existingNotes, incomingNote];
|
|
460
|
-
for (const note of candidates) {
|
|
433
|
+
for (const note of [...oldEvidence.notes, ...freshEvidence.notes]) {
|
|
461
434
|
const key = note.replace(/\s+/gu, " ").trim();
|
|
462
435
|
if (key.length === 0) continue;
|
|
463
436
|
if (seenNotes.has(key)) continue;
|
|
@@ -466,7 +439,7 @@ ${renderEvidenceBlock(newSummary, newRecentPaths)}
|
|
|
466
439
|
}
|
|
467
440
|
const mergedPaths = [];
|
|
468
441
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
469
|
-
for (const p of [...
|
|
442
|
+
for (const p of [...oldEvidence.paths, ...freshEvidence.paths]) {
|
|
470
443
|
const key = p.trim();
|
|
471
444
|
if (key.length === 0) continue;
|
|
472
445
|
if (seenPaths.has(key)) continue;
|
|
@@ -484,12 +457,58 @@ ${renderEvidenceBlock(newSummary, newRecentPaths)}
|
|
|
484
457
|
"",
|
|
485
458
|
noteLines
|
|
486
459
|
].join("\n");
|
|
487
|
-
return `${
|
|
460
|
+
return `${freshHead}
|
|
488
461
|
## Evidence
|
|
489
462
|
|
|
490
463
|
${evidenceBody}
|
|
491
464
|
`;
|
|
492
465
|
}
|
|
466
|
+
function splitAtEvidence(content) {
|
|
467
|
+
const tail = content.endsWith("\n") ? content : `${content}
|
|
468
|
+
`;
|
|
469
|
+
const match = /^([\s\S]*?)(\n## Evidence(?:\s*\(call \d+\))?\s*\n)/u.exec(tail);
|
|
470
|
+
if (match === null) return null;
|
|
471
|
+
return { head: match[1] ?? "" };
|
|
472
|
+
}
|
|
473
|
+
function collectEvidenceItems(content) {
|
|
474
|
+
const notes = [];
|
|
475
|
+
const paths = [];
|
|
476
|
+
const evidenceBlockRe = /\n## Evidence(?:\s*\(call \d+\))?\s*\n([\s\S]*?)(?=\n## |$)/gu;
|
|
477
|
+
let m;
|
|
478
|
+
while ((m = evidenceBlockRe.exec(`${content}
|
|
479
|
+
`)) !== null) {
|
|
480
|
+
const block = m[1] ?? "";
|
|
481
|
+
const pathSection = /Recent paths:\s*\n([\s\S]*?)(?:\n\s*Notes:|$)/u.exec(block);
|
|
482
|
+
if (pathSection !== null) {
|
|
483
|
+
for (const rawLine of (pathSection[1] ?? "").split(/\r?\n/u)) {
|
|
484
|
+
const t = rawLine.trim();
|
|
485
|
+
if (t.startsWith("- ")) {
|
|
486
|
+
paths.push(t.slice(2).trim());
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const notesSection = /Notes:\s*\n([\s\S]*?)$/u.exec(block);
|
|
491
|
+
const noteBody = (notesSection !== null ? notesSection[1] : block) ?? "";
|
|
492
|
+
const bulletLines = [];
|
|
493
|
+
let prose = [];
|
|
494
|
+
for (const rawLine of noteBody.split(/\r?\n/u)) {
|
|
495
|
+
const t = rawLine.trim();
|
|
496
|
+
if (t.length === 0) continue;
|
|
497
|
+
if (t.startsWith("- ")) {
|
|
498
|
+
if (prose.length > 0) {
|
|
499
|
+
notes.push(prose.join(" ").trim());
|
|
500
|
+
prose = [];
|
|
501
|
+
}
|
|
502
|
+
bulletLines.push(t.slice(2).trim());
|
|
503
|
+
} else {
|
|
504
|
+
prose.push(t);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (prose.length > 0) notes.push(prose.join(" ").trim());
|
|
508
|
+
for (const n of bulletLines) notes.push(n);
|
|
509
|
+
}
|
|
510
|
+
return { notes, paths };
|
|
511
|
+
}
|
|
493
512
|
function readFrontmatterKey(content, key) {
|
|
494
513
|
const match = /^---\n([\s\S]*?)\n---/u.exec(content);
|
|
495
514
|
if (match === null) {
|
|
@@ -567,7 +586,34 @@ import { minimatch } from "minimatch";
|
|
|
567
586
|
import { deriveAgentsMetaLayer } from "@fenglimg/fabric-shared";
|
|
568
587
|
var SELECTION_TOKEN_TTL_MS = 5 * 60 * 1e3;
|
|
569
588
|
var selectionTokenCache = /* @__PURE__ */ new Map();
|
|
589
|
+
function assertPathInSandbox(rawPath) {
|
|
590
|
+
if (rawPath === "**" || rawPath === "*") return;
|
|
591
|
+
const normalized = rawPath.replaceAll("\\", "/");
|
|
592
|
+
if (normalized.startsWith("/")) {
|
|
593
|
+
throw new Error(
|
|
594
|
+
`plan_context: absolute paths are not allowed (got "${rawPath}"); pass a path relative to the project root`
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
if (normalized.startsWith("~/") || normalized === "~") {
|
|
598
|
+
throw new Error(
|
|
599
|
+
`plan_context: shell sigil "~" is not allowed (got "${rawPath}"); expand to a project-relative path before calling`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if (normalized.split("/").some((seg) => seg === "..")) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`plan_context: ".." traversal is not allowed (got "${rawPath}"); pass a path that resolves under the project root`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
570
608
|
async function planContext(projectRoot, input) {
|
|
609
|
+
for (const p of input.paths) {
|
|
610
|
+
assertPathInSandbox(p);
|
|
611
|
+
}
|
|
612
|
+
if (input.target_paths !== void 0) {
|
|
613
|
+
for (const p of input.target_paths) {
|
|
614
|
+
assertPathInSandbox(p);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
571
617
|
let metaResult = await loadActiveMetaOrStale(projectRoot, { caller: "planContext" });
|
|
572
618
|
let meta = metaResult.meta;
|
|
573
619
|
let firstSeenPreviousRevision = metaResult.previous_revision_hash;
|
|
@@ -1042,6 +1088,23 @@ function resolveSandboxedPath(projectRoot, candidate, options = {}) {
|
|
|
1042
1088
|
}
|
|
1043
1089
|
throw new Error(`path escapes knowledge root: ${candidate}`);
|
|
1044
1090
|
}
|
|
1091
|
+
function extractBody(content) {
|
|
1092
|
+
const match = /^(?:\uFEFF)?---\r?\n[\s\S]*?\r?\n---\s*(?:\r?\n|$)/u.exec(content);
|
|
1093
|
+
if (match === null) {
|
|
1094
|
+
return content.trim();
|
|
1095
|
+
}
|
|
1096
|
+
return content.slice(match[0].length).trim();
|
|
1097
|
+
}
|
|
1098
|
+
function isVisibleByLifecycle(fm, filters) {
|
|
1099
|
+
if (fm.status === "rejected" && filters?.include_rejected !== true) {
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
if (fm.status === "deferred" && filters?.include_deferred !== true) {
|
|
1103
|
+
if (fm.deferred_until === void 0) return false;
|
|
1104
|
+
if (fm.deferred_until > (/* @__PURE__ */ new Date()).toISOString()) return false;
|
|
1105
|
+
}
|
|
1106
|
+
return true;
|
|
1107
|
+
}
|
|
1045
1108
|
async function listPending(projectRoot, filters) {
|
|
1046
1109
|
const items = [];
|
|
1047
1110
|
const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
|
|
@@ -1090,14 +1153,27 @@ async function listPending(projectRoot, filters) {
|
|
|
1090
1153
|
continue;
|
|
1091
1154
|
}
|
|
1092
1155
|
}
|
|
1156
|
+
if (!isVisibleByLifecycle(fm, filters)) {
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1093
1159
|
const reportedPath = source.origin === "personal" ? `~/${relative2(resolvePersonalRoot2(), absolutePath)}` : relative2(projectRoot, absolutePath);
|
|
1094
1160
|
items.push({
|
|
1095
1161
|
pending_path: reportedPath,
|
|
1162
|
+
// v2.0.0-rc.27 TASK-001 (§2.12): absolute path companion for
|
|
1163
|
+
// personal entries so programmatic consumers (Read, fs.readFile)
|
|
1164
|
+
// don't need to shell-expand the `~` themselves.
|
|
1165
|
+
...source.origin === "personal" ? { pending_path_absolute: absolutePath } : {},
|
|
1096
1166
|
type,
|
|
1097
1167
|
layer,
|
|
1098
1168
|
maturity,
|
|
1099
1169
|
origin: source.origin,
|
|
1100
|
-
...fm.tags !== void 0 && fm.tags.length > 0 ? { tags: fm.tags } : {}
|
|
1170
|
+
...fm.tags !== void 0 && fm.tags.length > 0 ? { tags: fm.tags } : {},
|
|
1171
|
+
...fm.status !== void 0 ? { status: fm.status } : {},
|
|
1172
|
+
...fm.deferred_until !== void 0 ? { deferred_until: fm.deferred_until } : {},
|
|
1173
|
+
// v2.0.0-rc.27 TASK-006 (audit §2.23): full body when caller
|
|
1174
|
+
// opted in. Reviewer UI consumes this to scan for prompt-injection
|
|
1175
|
+
// payloads hidden under `## Evidence` body.
|
|
1176
|
+
...filters?.include_body === true ? { body: extractBody(content) } : {}
|
|
1101
1177
|
});
|
|
1102
1178
|
}
|
|
1103
1179
|
}
|
|
@@ -1189,6 +1265,10 @@ async function approveOne(projectRoot, pendingPath, allocator) {
|
|
|
1189
1265
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1190
1266
|
reason: `approve:${slug}`
|
|
1191
1267
|
});
|
|
1268
|
+
try {
|
|
1269
|
+
await reconcileKnowledge(projectRoot, { trigger: "post-approve" });
|
|
1270
|
+
} catch {
|
|
1271
|
+
}
|
|
1192
1272
|
return { pending_path: pendingPath, stable_id: stableId };
|
|
1193
1273
|
} catch (err) {
|
|
1194
1274
|
if (writtenTarget && targetAbs !== void 0 && existsSync3(targetAbs)) {
|
|
@@ -1210,6 +1290,17 @@ async function approveOne(projectRoot, pendingPath, allocator) {
|
|
|
1210
1290
|
async function rejectAll(projectRoot, pendingPaths, reason) {
|
|
1211
1291
|
const rejected = [];
|
|
1212
1292
|
for (const pendingPath of pendingPaths) {
|
|
1293
|
+
try {
|
|
1294
|
+
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
1295
|
+
if (existsSync3(sandboxed.abs)) {
|
|
1296
|
+
const content = await readFile3(sandboxed.abs, "utf8");
|
|
1297
|
+
const merged = rewriteFrontmatterMerge(content, { status: "rejected" });
|
|
1298
|
+
if (merged !== content) {
|
|
1299
|
+
await atomicWriteText(sandboxed.abs, merged);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
} catch {
|
|
1303
|
+
}
|
|
1213
1304
|
await emitEventBestEffort2(projectRoot, {
|
|
1214
1305
|
event_type: "knowledge_rejected",
|
|
1215
1306
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1268,6 +1359,11 @@ function extractSlug(path) {
|
|
|
1268
1359
|
return file.replace(/^K[PT]-(MOD|DEC|GLD|PIT|PRO)-\d+--/u, "");
|
|
1269
1360
|
}
|
|
1270
1361
|
async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
1362
|
+
if (target.absPath.includes("/pending/")) {
|
|
1363
|
+
throw new Error(
|
|
1364
|
+
"layer-flip not allowed on pending entries; approve first, then modify the canonical entry's layer"
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1271
1367
|
const fromLayer = fm.layer ?? "team";
|
|
1272
1368
|
const toLayer = changes.layer;
|
|
1273
1369
|
const pluralType = fm.type ?? target.inferredType;
|
|
@@ -1334,6 +1430,10 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
1334
1430
|
});
|
|
1335
1431
|
}
|
|
1336
1432
|
const responsePath = toLayer === "team" ? relative2(projectRoot, toAbs) : `~/${relative2(resolvePersonalRoot2(), toAbs)}`;
|
|
1433
|
+
try {
|
|
1434
|
+
await reconcileKnowledge(projectRoot, { trigger: "post-modify" });
|
|
1435
|
+
} catch {
|
|
1436
|
+
}
|
|
1337
1437
|
return {
|
|
1338
1438
|
action: "modify",
|
|
1339
1439
|
pending_path: responsePath,
|
|
@@ -1390,17 +1490,25 @@ async function searchEntries(projectRoot, query, filters) {
|
|
|
1390
1490
|
continue;
|
|
1391
1491
|
}
|
|
1392
1492
|
}
|
|
1493
|
+
if (!isVisibleByLifecycle(fm, filters)) {
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
const bodyForSearch = filters?.include_body === true ? extractBody(content) : "";
|
|
1393
1497
|
const haystacks = [
|
|
1394
1498
|
fm.title ?? "",
|
|
1395
1499
|
fm.summary ?? "",
|
|
1396
1500
|
...fm.tags ?? [],
|
|
1397
|
-
name
|
|
1501
|
+
name,
|
|
1502
|
+
bodyForSearch
|
|
1398
1503
|
].map((s) => s.toLowerCase());
|
|
1399
1504
|
const matches = haystacks.some((h) => h.includes(lowerQuery));
|
|
1400
1505
|
if (!matches) continue;
|
|
1401
1506
|
const reportedPath = source.isPersonal ? `~/${relative2(resolvePersonalRoot2(), absolutePath)}` : relative2(projectRoot, absolutePath);
|
|
1402
1507
|
items.push({
|
|
1403
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 } : {},
|
|
1404
1512
|
type,
|
|
1405
1513
|
layer,
|
|
1406
1514
|
maturity,
|
|
@@ -1409,7 +1517,14 @@ async function searchEntries(projectRoot, query, filters) {
|
|
|
1409
1517
|
...source.isPending ? { origin: source.isPersonal ? "personal" : "team" } : {},
|
|
1410
1518
|
...fm.tags !== void 0 && fm.tags.length > 0 ? { tags: fm.tags } : {},
|
|
1411
1519
|
...fm.title !== void 0 ? { title: fm.title } : {},
|
|
1412
|
-
...fm.summary !== void 0 ? { summary: fm.summary } : {}
|
|
1520
|
+
...fm.summary !== void 0 ? { summary: fm.summary } : {},
|
|
1521
|
+
...fm.status !== void 0 ? { status: fm.status } : {},
|
|
1522
|
+
...fm.deferred_until !== void 0 ? { deferred_until: fm.deferred_until } : {},
|
|
1523
|
+
// 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 } : {}
|
|
1413
1528
|
});
|
|
1414
1529
|
}
|
|
1415
1530
|
}
|
|
@@ -1419,6 +1534,21 @@ async function searchEntries(projectRoot, query, filters) {
|
|
|
1419
1534
|
async function deferAll(projectRoot, pendingPaths, until, reason) {
|
|
1420
1535
|
const deferred = [];
|
|
1421
1536
|
for (const pendingPath of pendingPaths) {
|
|
1537
|
+
try {
|
|
1538
|
+
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
1539
|
+
if (existsSync3(sandboxed.abs)) {
|
|
1540
|
+
const content = await readFile3(sandboxed.abs, "utf8");
|
|
1541
|
+
const patch = {
|
|
1542
|
+
status: "deferred",
|
|
1543
|
+
...until !== void 0 ? { deferred_until: until } : {}
|
|
1544
|
+
};
|
|
1545
|
+
const merged = rewriteFrontmatterMerge(content, patch);
|
|
1546
|
+
if (merged !== content) {
|
|
1547
|
+
await atomicWriteText(sandboxed.abs, merged);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
} catch {
|
|
1551
|
+
}
|
|
1422
1552
|
await emitEventBestEffort2(projectRoot, {
|
|
1423
1553
|
event_type: "knowledge_deferred",
|
|
1424
1554
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1488,6 +1618,14 @@ function parseFrontmatter(content) {
|
|
|
1488
1618
|
case "relevance_paths":
|
|
1489
1619
|
out.relevance_paths = parseFlowArray(value);
|
|
1490
1620
|
break;
|
|
1621
|
+
case "status":
|
|
1622
|
+
if (value === "active" || value === "rejected" || value === "deferred") {
|
|
1623
|
+
out.status = value;
|
|
1624
|
+
}
|
|
1625
|
+
break;
|
|
1626
|
+
case "deferred_until":
|
|
1627
|
+
out.deferred_until = stripQuotes(value);
|
|
1628
|
+
break;
|
|
1491
1629
|
default:
|
|
1492
1630
|
break;
|
|
1493
1631
|
}
|
|
@@ -1549,6 +1687,8 @@ ${content}`;
|
|
|
1549
1687
|
if (patch.tags !== void 0) updates.tags = `tags: [${patch.tags.join(", ")}]`;
|
|
1550
1688
|
if (patch.relevance_scope !== void 0) updates.relevance_scope = `relevance_scope: ${patch.relevance_scope}`;
|
|
1551
1689
|
if (patch.relevance_paths !== void 0) updates.relevance_paths = `relevance_paths: [${patch.relevance_paths.join(", ")}]`;
|
|
1690
|
+
if (patch.status !== void 0) updates.status = `status: ${patch.status}`;
|
|
1691
|
+
if (patch.deferred_until !== void 0) updates.deferred_until = `deferred_until: ${quoteIfNeeded(patch.deferred_until)}`;
|
|
1552
1692
|
const lines = block.split(/\r?\n/u);
|
|
1553
1693
|
const seen = /* @__PURE__ */ new Set();
|
|
1554
1694
|
const newLines = [];
|
|
@@ -1582,6 +1722,8 @@ function appendPatchLines(lines, patch) {
|
|
|
1582
1722
|
if (patch.tags !== void 0) lines.push(`tags: [${patch.tags.join(", ")}]`);
|
|
1583
1723
|
if (patch.relevance_scope !== void 0) lines.push(`relevance_scope: ${patch.relevance_scope}`);
|
|
1584
1724
|
if (patch.relevance_paths !== void 0) lines.push(`relevance_paths: [${patch.relevance_paths.join(", ")}]`);
|
|
1725
|
+
if (patch.status !== void 0) lines.push(`status: ${patch.status}`);
|
|
1726
|
+
if (patch.deferred_until !== void 0) lines.push(`deferred_until: ${quoteIfNeeded(patch.deferred_until)}`);
|
|
1585
1727
|
}
|
|
1586
1728
|
function quoteIfNeeded(value) {
|
|
1587
1729
|
if (/[\n\r]/u.test(value)) {
|
|
@@ -1661,7 +1803,7 @@ var PRIORITY_ORDER = {
|
|
|
1661
1803
|
medium: 1,
|
|
1662
1804
|
low: 2
|
|
1663
1805
|
};
|
|
1664
|
-
function
|
|
1806
|
+
function extractBody2(content) {
|
|
1665
1807
|
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/u.exec(content);
|
|
1666
1808
|
if (match === null) {
|
|
1667
1809
|
return content.replace(/^\uFEFF/u, "");
|
|
@@ -1681,7 +1823,7 @@ async function getKnowledgeSections(projectRoot, input) {
|
|
|
1681
1823
|
const rules = [];
|
|
1682
1824
|
for (const rule of selectedRules) {
|
|
1683
1825
|
const content = await readFile4(resolveRuleSourcePath(projectRoot, rule.path), "utf8");
|
|
1684
|
-
const body =
|
|
1826
|
+
const body = extractBody2(content);
|
|
1685
1827
|
const description = rule.node.description;
|
|
1686
1828
|
if (description !== void 0 && description.knowledge_type === void 0 && description.knowledge_layer === void 0) {
|
|
1687
1829
|
diagnostics.push({
|
|
@@ -1894,7 +2036,7 @@ function formatPreexistingRootMessage(projectRoot) {
|
|
|
1894
2036
|
function createFabricServer(tracker) {
|
|
1895
2037
|
const server = new McpServer({
|
|
1896
2038
|
name: "fabric-knowledge-server",
|
|
1897
|
-
version: "2.0.0-rc.
|
|
2039
|
+
version: "2.0.0-rc.27"
|
|
1898
2040
|
});
|
|
1899
2041
|
registerPlanContext(server, tracker);
|
|
1900
2042
|
registerKnowledgeSections(server, tracker);
|
|
@@ -2002,7 +2144,7 @@ function createShutdownHandler(deps) {
|
|
|
2002
2144
|
};
|
|
2003
2145
|
}
|
|
2004
2146
|
async function startHttpServer(options) {
|
|
2005
|
-
const { createFabricHttpApp } = await import("./http-
|
|
2147
|
+
const { createFabricHttpApp } = await import("./http-3WADEK3O.js");
|
|
2006
2148
|
const { port, projectRoot, host = "127.0.0.1", authToken } = options;
|
|
2007
2149
|
const app = createFabricHttpApp({ projectRoot, host, authToken });
|
|
2008
2150
|
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.27",
|
|
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.27"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/express": "^5.0.6",
|