@fenglimg/fabric-server 2.1.0-rc.2 → 2.2.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +483 -161
- package/dist/index.js +5380 -3603
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { AgentsMeta, KnowledgeTestIndex, AgentsLayer, AgentsTopologyType, KnowledgeType, Layer, StableId, AgentsMetaCounters, EventLedgerEventInput, EventLedgerEvent, RuleDescriptionIndexItem } from '@fenglimg/fabric-shared';
|
|
2
|
+
import { AgentsMeta, KnowledgeTestIndex, AgentsLayer, AgentsTopologyType, KnowledgeType, Layer, StableId, AgentsMetaCounters, EventLedgerEventInput, EventLedgerEvent, RuleDescriptionIndexItem, LedgerEntry } from '@fenglimg/fabric-shared';
|
|
3
3
|
import { FabExtractKnowledgeInput, FabExtractKnowledgeOutput, FabReviewInput, FabReviewOutput } from '@fenglimg/fabric-shared/schemas/api-contracts';
|
|
4
4
|
|
|
5
5
|
interface InFlightTracker {
|
|
@@ -22,90 +22,79 @@ declare function getLegacyLedgerPath(projectRoot: string): string;
|
|
|
22
22
|
declare function getEventLedgerPath(projectRoot: string): string;
|
|
23
23
|
declare function getMetricsLedgerPath(projectRoot: string): string;
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
changed: boolean;
|
|
83
|
-
fixed: DoctorIssue[];
|
|
84
|
-
remaining_manual_errors: DoctorIssue[];
|
|
85
|
-
warnings: DoctorIssue[];
|
|
86
|
-
message: string;
|
|
87
|
-
report: DoctorReport;
|
|
88
|
-
};
|
|
89
|
-
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";
|
|
90
|
-
type DoctorApplyLintMutation = {
|
|
91
|
-
kind: DoctorApplyLintMutationKind;
|
|
92
|
-
path: string;
|
|
93
|
-
detail: string;
|
|
94
|
-
applied: boolean;
|
|
95
|
-
error?: string;
|
|
25
|
+
/**
|
|
26
|
+
* O(1) in-memory increment for a named counter. Safe to call from any MCP
|
|
27
|
+
* tool handler; no I/O happens until the next `flushMetrics()` call.
|
|
28
|
+
*
|
|
29
|
+
* `delta` defaults to 1; callers can pass a positive integer (e.g. fetched
|
|
30
|
+
* N stable_ids in a single sections call) to fold N bumps into one.
|
|
31
|
+
*/
|
|
32
|
+
declare function bumpCounter(projectRoot: string, name: string, delta?: number): void;
|
|
33
|
+
/**
|
|
34
|
+
* Snapshot the current counter accumulator and reset it. Returned map is a
|
|
35
|
+
* frozen copy; the live accumulator starts fresh from zero. Exposed so
|
|
36
|
+
* flushMetrics + tests + a future fab_metrics manual-flush CLI hook can all
|
|
37
|
+
* use the same primitive without racing.
|
|
38
|
+
*/
|
|
39
|
+
declare function drainCounters(projectRoot: string): Record<string, number>;
|
|
40
|
+
/**
|
|
41
|
+
* Drain the current accumulator and append one JSONL row to
|
|
42
|
+
* `.fabric/metrics.jsonl`. Returns the appended row (or `null` when the
|
|
43
|
+
* accumulator was empty — no spurious zero rows). fs failures degrade
|
|
44
|
+
* silently; the next flush will carry the union of the failed-write
|
|
45
|
+
* interval + the current one.
|
|
46
|
+
*/
|
|
47
|
+
declare function flushMetrics(projectRoot: string, options?: {
|
|
48
|
+
windowMs?: number;
|
|
49
|
+
now?: Date;
|
|
50
|
+
}): Promise<MetricsRow | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Start the background flush timer for a project root. Idempotent — calling
|
|
53
|
+
* twice on the same root replaces the prior interval. Returns a stop handle
|
|
54
|
+
* the caller can invoke at shutdown to flush + clear the timer.
|
|
55
|
+
*
|
|
56
|
+
* The flush is fire-and-forget (no await on the setInterval callback) so
|
|
57
|
+
* the timer cadence stays accurate even when fs is slow.
|
|
58
|
+
*/
|
|
59
|
+
declare function startMetricsFlush(projectRoot: string, options?: {
|
|
60
|
+
intervalMs?: number;
|
|
61
|
+
}): () => Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Cancel the background flush timer for a project root if one is running.
|
|
64
|
+
* Does NOT drain the accumulator — callers that want a final flush should
|
|
65
|
+
* await flushMetrics(projectRoot) afterward.
|
|
66
|
+
*/
|
|
67
|
+
declare function stopMetricsFlush(projectRoot: string): void;
|
|
68
|
+
/**
|
|
69
|
+
* Read accumulated metrics rows from `.fabric/metrics.jsonl`. Missing file
|
|
70
|
+
* returns []. Malformed rows are dropped silently (the sidecar is best-
|
|
71
|
+
* effort observability; a corrupt row never blocks a reader).
|
|
72
|
+
*
|
|
73
|
+
* Exposed for the NEW-34 `fab metrics` CLI dashboard + future doctor lints
|
|
74
|
+
* (e.g. cite-goodhart pattern replay) that need counter trends without
|
|
75
|
+
* walking events.jsonl.
|
|
76
|
+
*/
|
|
77
|
+
declare function readMetrics(projectRoot: string): Promise<MetricsRow[]>;
|
|
78
|
+
type MetricsRow = {
|
|
79
|
+
timestamp: string;
|
|
80
|
+
window: string;
|
|
81
|
+
counters: Record<string, number>;
|
|
96
82
|
};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Canonical metric counter names used by the high-frequency emitters that
|
|
85
|
+
* left events.jsonl as part of the rc.37 Wave B clean-slate. Centralized
|
|
86
|
+
* here so the B5 hard-gate (`metric_event_in_jsonl`) can grep for these
|
|
87
|
+
* exact strings and fail when one accidentally re-appears in the audit
|
|
88
|
+
* ledger emit path.
|
|
89
|
+
*/
|
|
90
|
+
declare const METRIC_COUNTER_NAMES: {
|
|
91
|
+
readonly knowledge_consumed: "knowledge_consumed";
|
|
92
|
+
readonly edit_intent_checked: "edit_intent_checked";
|
|
93
|
+
readonly knowledge_context_planned: "knowledge_context_planned";
|
|
94
|
+
readonly knowledge_sections_fetched: "knowledge_sections_fetched";
|
|
105
95
|
};
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
declare function runDoctorApplyLint(target: string): Promise<DoctorApplyLintReport>;
|
|
96
|
+
type MetricCounterName = (typeof METRIC_COUNTER_NAMES)[keyof typeof METRIC_COUNTER_NAMES];
|
|
97
|
+
|
|
109
98
|
type CiteContractMetrics = {
|
|
110
99
|
decisions_cited: number;
|
|
111
100
|
pitfalls_cited: number;
|
|
@@ -136,6 +125,25 @@ type CiteCoverageReport = {
|
|
|
136
125
|
compliant_cites?: number;
|
|
137
126
|
noncompliant_cites?: number;
|
|
138
127
|
uncorrelatable_edits?: number;
|
|
128
|
+
recall_backed_edits?: number;
|
|
129
|
+
recall_coverage_rate?: number | null;
|
|
130
|
+
exposed_and_mutated?: {
|
|
131
|
+
count: number;
|
|
132
|
+
ids?: string[];
|
|
133
|
+
};
|
|
134
|
+
mutations_observed?: {
|
|
135
|
+
count: number;
|
|
136
|
+
};
|
|
137
|
+
mutation_pool?: {
|
|
138
|
+
attributed: number;
|
|
139
|
+
unattributed_workspace_dirty: number;
|
|
140
|
+
};
|
|
141
|
+
sessions_closed?: {
|
|
142
|
+
count: number;
|
|
143
|
+
};
|
|
144
|
+
by_store?: Record<string, {
|
|
145
|
+
qualifying_cites: number;
|
|
146
|
+
}>;
|
|
139
147
|
};
|
|
140
148
|
per_client?: Record<string, Partial<CiteCoverageReport["metrics"]>>;
|
|
141
149
|
dismissed_reason_histogram?: Record<string, number>;
|
|
@@ -157,6 +165,7 @@ declare function runDoctorCiteCoverage(projectRoot: string, options: {
|
|
|
157
165
|
client: "cc" | "codex" | "cursor" | "all";
|
|
158
166
|
layer?: "team" | "personal" | "all";
|
|
159
167
|
until?: number;
|
|
168
|
+
recallWindowMs?: number;
|
|
160
169
|
}): Promise<CiteCoverageReport>;
|
|
161
170
|
type ArchiveHistoryEntry = {
|
|
162
171
|
session_id_short: string;
|
|
@@ -192,6 +201,101 @@ type HistoryAllReport = {
|
|
|
192
201
|
declare function runDoctorHistoryAll(projectRoot: string, options: {
|
|
193
202
|
since: number;
|
|
194
203
|
}): Promise<HistoryAllReport>;
|
|
204
|
+
|
|
205
|
+
type DoctorStatus = "ok" | "warn" | "error";
|
|
206
|
+
type DoctorIssueKind = "fixable_error" | "manual_error" | "warning" | "info";
|
|
207
|
+
type DoctorCheck = {
|
|
208
|
+
name: string;
|
|
209
|
+
status: DoctorStatus;
|
|
210
|
+
message: string;
|
|
211
|
+
kind?: DoctorIssueKind;
|
|
212
|
+
code?: string;
|
|
213
|
+
fixable?: boolean;
|
|
214
|
+
actionHint?: string;
|
|
215
|
+
audience?: "user" | "maintainer";
|
|
216
|
+
};
|
|
217
|
+
type DoctorIssue = {
|
|
218
|
+
code: string;
|
|
219
|
+
name: string;
|
|
220
|
+
message: string;
|
|
221
|
+
path?: string;
|
|
222
|
+
actionHint?: string;
|
|
223
|
+
audience?: "user" | "maintainer";
|
|
224
|
+
};
|
|
225
|
+
type DoctorPayloadLimits = {
|
|
226
|
+
warn_bytes: number;
|
|
227
|
+
hard_bytes: number;
|
|
228
|
+
source: "default" | "config";
|
|
229
|
+
};
|
|
230
|
+
type DoctorSummary = {
|
|
231
|
+
target: string;
|
|
232
|
+
framework: {
|
|
233
|
+
kind: string;
|
|
234
|
+
version: string;
|
|
235
|
+
subkind: string;
|
|
236
|
+
};
|
|
237
|
+
entryPoints: Array<{
|
|
238
|
+
path: string;
|
|
239
|
+
reason: string;
|
|
240
|
+
}>;
|
|
241
|
+
metaRevision: string | null;
|
|
242
|
+
computedMetaRevision: string | null;
|
|
243
|
+
ruleCount: number;
|
|
244
|
+
eventLedgerPath: string;
|
|
245
|
+
fixableErrorCount: number;
|
|
246
|
+
manualErrorCount: number;
|
|
247
|
+
warningCount: number;
|
|
248
|
+
infoCount: number;
|
|
249
|
+
targetFiles: Record<string, boolean>;
|
|
250
|
+
payload_limits: DoctorPayloadLimits;
|
|
251
|
+
health: DoctorHealth;
|
|
252
|
+
};
|
|
253
|
+
type DoctorHealth = {
|
|
254
|
+
score: number;
|
|
255
|
+
grade: "A" | "B" | "C" | "D" | "F";
|
|
256
|
+
penalties: {
|
|
257
|
+
manual_errors: number;
|
|
258
|
+
fixable_errors: number;
|
|
259
|
+
warnings: number;
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
type DoctorReport = {
|
|
263
|
+
status: DoctorStatus;
|
|
264
|
+
checks: DoctorCheck[];
|
|
265
|
+
fixable_errors: DoctorIssue[];
|
|
266
|
+
manual_errors: DoctorIssue[];
|
|
267
|
+
warnings: DoctorIssue[];
|
|
268
|
+
infos: DoctorIssue[];
|
|
269
|
+
summary: DoctorSummary;
|
|
270
|
+
};
|
|
271
|
+
type DoctorFixReport = {
|
|
272
|
+
changed: boolean;
|
|
273
|
+
fixed: DoctorIssue[];
|
|
274
|
+
remaining_manual_errors: DoctorIssue[];
|
|
275
|
+
warnings: DoctorIssue[];
|
|
276
|
+
message: string;
|
|
277
|
+
report: DoctorReport;
|
|
278
|
+
};
|
|
279
|
+
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";
|
|
280
|
+
type DoctorApplyLintMutation = {
|
|
281
|
+
kind: DoctorApplyLintMutationKind;
|
|
282
|
+
path: string;
|
|
283
|
+
detail: string;
|
|
284
|
+
applied: boolean;
|
|
285
|
+
error?: string;
|
|
286
|
+
};
|
|
287
|
+
type DoctorApplyLintReport = {
|
|
288
|
+
changed: boolean;
|
|
289
|
+
mutations: DoctorApplyLintMutation[];
|
|
290
|
+
manual_errors: DoctorIssue[];
|
|
291
|
+
aborted: boolean;
|
|
292
|
+
abort_reason?: string;
|
|
293
|
+
message: string;
|
|
294
|
+
report: DoctorReport;
|
|
295
|
+
};
|
|
296
|
+
declare function runDoctorReport(target: string): Promise<DoctorReport>;
|
|
297
|
+
declare function runDoctorFix(target: string): Promise<DoctorFixReport>;
|
|
298
|
+
declare function runDoctorApplyLint(target: string): Promise<DoctorApplyLintReport>;
|
|
195
299
|
type EnrichDescriptionsMode = "auto" | "preview" | "readonly" | "interactive";
|
|
196
300
|
type EnrichDescriptionsCandidate = {
|
|
197
301
|
path: string;
|
|
@@ -213,6 +317,130 @@ declare function enrichDescriptions(projectRoot: string, opts?: {
|
|
|
213
317
|
dryRun?: boolean;
|
|
214
318
|
}): Promise<EnrichDescriptionsReport>;
|
|
215
319
|
|
|
320
|
+
interface Bm25Document {
|
|
321
|
+
id: string;
|
|
322
|
+
tokens: string[];
|
|
323
|
+
}
|
|
324
|
+
interface Bm25Model {
|
|
325
|
+
/**
|
|
326
|
+
* BM25 score of document `id` against the (pre-tokenized) query terms.
|
|
327
|
+
* Returns 0 for an unknown id, an empty document, no query terms, or no
|
|
328
|
+
* term overlap. Query-term duplicates are collapsed — repeating a term in
|
|
329
|
+
* the query does not inflate the score (term frequency is a document
|
|
330
|
+
* property, not a query property).
|
|
331
|
+
*/
|
|
332
|
+
scoreDoc(id: string, queryTerms: string[]): number;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Build a BM25 model over `docs`. The corpus statistics (document frequency,
|
|
336
|
+
* average document length) are computed once here; `scoreDoc` is then O(query
|
|
337
|
+
* terms) per call.
|
|
338
|
+
*/
|
|
339
|
+
declare function buildBm25Model(docs: Bm25Document[]): Bm25Model;
|
|
340
|
+
|
|
341
|
+
interface ConflictEntry {
|
|
342
|
+
stable_id: string;
|
|
343
|
+
/** Plural knowledge_type ("decisions" | "pitfalls" | "guidelines" | "models" | "processes"). */
|
|
344
|
+
knowledge_type: string;
|
|
345
|
+
/** "team" | "personal". Conflicts are only meaningful within one layer. */
|
|
346
|
+
layer: string;
|
|
347
|
+
/** Title + body (or any text used for similarity). */
|
|
348
|
+
text: string;
|
|
349
|
+
}
|
|
350
|
+
type ConflictVerdict = "conflict" | "similar" | "unknown";
|
|
351
|
+
interface ConflictPair {
|
|
352
|
+
a: string;
|
|
353
|
+
b: string;
|
|
354
|
+
knowledge_type: string;
|
|
355
|
+
layer: string;
|
|
356
|
+
/** Symmetric normalized bm25 similarity in [0,1]. */
|
|
357
|
+
similarity: number;
|
|
358
|
+
/**
|
|
359
|
+
* "unknown" until a judge classifies it (cheap pass leaves it unknown =
|
|
360
|
+
* needs-human-review). "conflict" = the judge ruled a real contradiction
|
|
361
|
+
* (escalates to error). "similar" = judged not-a-conflict (stays a warn /
|
|
362
|
+
* possible duplicate).
|
|
363
|
+
*/
|
|
364
|
+
verdict: ConflictVerdict;
|
|
365
|
+
rationale?: string;
|
|
366
|
+
}
|
|
367
|
+
type ConflictJudge = (a: ConflictEntry, b: ConflictEntry) => Promise<{
|
|
368
|
+
isConflict: boolean;
|
|
369
|
+
rationale: string;
|
|
370
|
+
}>;
|
|
371
|
+
declare const DEFAULT_CONFLICT_SIMILARITY_THRESHOLD = 0.5;
|
|
372
|
+
/**
|
|
373
|
+
* Symmetric, [0,1]-normalized bm25 similarity between two docs sharing a model.
|
|
374
|
+
* For each direction we measure how much of the target doc's self-relevance the
|
|
375
|
+
* other doc's terms recover (scoreDoc(target, otherTokens) / scoreDoc(target,
|
|
376
|
+
* targetTokens)), then take the MIN of both directions — conservative: both
|
|
377
|
+
* entries must strongly overlap, so a short entry incidentally contained in a
|
|
378
|
+
* long one does not over-fire.
|
|
379
|
+
*/
|
|
380
|
+
declare function pairSimilarity(model: ReturnType<typeof buildBm25Model>, a: {
|
|
381
|
+
id: string;
|
|
382
|
+
tokens: string[];
|
|
383
|
+
}, b: {
|
|
384
|
+
id: string;
|
|
385
|
+
tokens: string[];
|
|
386
|
+
}): number;
|
|
387
|
+
/**
|
|
388
|
+
* Cheap deterministic pass: candidate conflicting/duplicate pairs. Compares
|
|
389
|
+
* only entries sharing (type, layer), via bm25 similarity ≥ threshold. Returns
|
|
390
|
+
* pairs sorted by similarity descending (stable_id-tie-broken for determinism).
|
|
391
|
+
* Every returned pair has verdict "unknown" — the cheap pass cannot tell a
|
|
392
|
+
* conflict from a benign duplicate; that is the judge's job.
|
|
393
|
+
*
|
|
394
|
+
* O(G · n_g²) where G = groups and n_g = entries per group. Conflicts only make
|
|
395
|
+
* sense within one (type, layer), so the quadratic stays bounded to small
|
|
396
|
+
* same-bucket sets, not the whole corpus.
|
|
397
|
+
*/
|
|
398
|
+
declare function findConflictCandidates(entries: ConflictEntry[], opts?: {
|
|
399
|
+
threshold?: number;
|
|
400
|
+
}): ConflictPair[];
|
|
401
|
+
/**
|
|
402
|
+
* Full lint. Always runs the cheap pass; when `judge` is supplied (deep mode)
|
|
403
|
+
* each candidate is classified into conflict/similar. A judge throw leaves the
|
|
404
|
+
* pair as "unknown" (degrade-safe — a flaky judge never crashes doctor).
|
|
405
|
+
*
|
|
406
|
+
* Entry lookup for the judge is by stable_id from the same `entries` array.
|
|
407
|
+
*/
|
|
408
|
+
declare function lintConflicts(entries: ConflictEntry[], opts?: {
|
|
409
|
+
threshold?: number;
|
|
410
|
+
judge?: ConflictJudge;
|
|
411
|
+
}): Promise<ConflictPair[]>;
|
|
412
|
+
|
|
413
|
+
interface ConflictLintReport {
|
|
414
|
+
status: "ok";
|
|
415
|
+
threshold: number;
|
|
416
|
+
deep: boolean;
|
|
417
|
+
/** Total candidate pairs (similarity ≥ threshold). */
|
|
418
|
+
candidate_count: number;
|
|
419
|
+
/** Subset judged a real contradiction (deep mode only). */
|
|
420
|
+
conflict_count: number;
|
|
421
|
+
pairs: ConflictPair[];
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Load canonical knowledge entries with bodies from the agents.meta node index.
|
|
425
|
+
* Skips pending/draft staging entries (conflict detection targets the curated
|
|
426
|
+
* corpus) and any node missing a stable_id or knowledge_type. Bodies are read
|
|
427
|
+
* best-effort — an unreadable file contributes no entry.
|
|
428
|
+
*/
|
|
429
|
+
declare function loadConflictEntries(projectRoot: string): Promise<ConflictEntry[]>;
|
|
430
|
+
/**
|
|
431
|
+
* Run the knowledge-conflict lint. Always runs the cheap bm25 candidate pass;
|
|
432
|
+
* when `deep` is set AND a judge is supplied, escalates candidates to
|
|
433
|
+
* conflict/similar verdicts via the injected LLM judge.
|
|
434
|
+
*
|
|
435
|
+
* threshold resolution: opts.threshold → fabric-config
|
|
436
|
+
* conflict_lint_similarity_threshold → DEFAULT_CONFLICT_SIMILARITY_THRESHOLD.
|
|
437
|
+
*/
|
|
438
|
+
declare function runDoctorConflictLint(projectRoot: string, opts?: {
|
|
439
|
+
threshold?: number;
|
|
440
|
+
deep?: boolean;
|
|
441
|
+
judge?: ConflictJudge;
|
|
442
|
+
}): Promise<ConflictLintReport>;
|
|
443
|
+
|
|
216
444
|
type KnowledgeMetaBuildSource = "doctor_fix" | "sync_meta";
|
|
217
445
|
type KnowledgeMetaBuildResult = {
|
|
218
446
|
meta: AgentsMeta;
|
|
@@ -329,7 +557,34 @@ declare function extractKnowledge(projectRoot: string, input: FabExtractKnowledg
|
|
|
329
557
|
declare function reviewKnowledge(projectRoot: string, input: FabReviewInput): Promise<FabReviewOutput>;
|
|
330
558
|
|
|
331
559
|
type StoredEventLedgerEvent = EventLedgerEvent;
|
|
560
|
+
type ReadEventLedgerOptions = {
|
|
561
|
+
event_type?: EventLedgerEvent["event_type"];
|
|
562
|
+
since?: number;
|
|
563
|
+
correlation_id?: string;
|
|
564
|
+
session_id?: string;
|
|
565
|
+
};
|
|
566
|
+
type LedgerWarning = {
|
|
567
|
+
kind: "partial_write_at_tail";
|
|
568
|
+
byte_offset: number;
|
|
569
|
+
byte_length: number;
|
|
570
|
+
snippet_first_120: string;
|
|
571
|
+
} | {
|
|
572
|
+
kind: "schema_version_unsupported";
|
|
573
|
+
line_index: number;
|
|
574
|
+
schema_version: unknown;
|
|
575
|
+
snippet_first_120: string;
|
|
576
|
+
} | {
|
|
577
|
+
kind: "event_type_unknown";
|
|
578
|
+
line_index: number;
|
|
579
|
+
event_type: unknown;
|
|
580
|
+
snippet_first_120: string;
|
|
581
|
+
};
|
|
582
|
+
type ReadEventLedgerResult = {
|
|
583
|
+
events: StoredEventLedgerEvent[];
|
|
584
|
+
warnings: LedgerWarning[];
|
|
585
|
+
};
|
|
332
586
|
declare function appendEventLedgerEvent(projectRoot: string, event: EventLedgerEventInput): Promise<StoredEventLedgerEvent>;
|
|
587
|
+
declare function readEventLedger(projectRoot: string, options?: ReadEventLedgerOptions): Promise<ReadEventLedgerResult>;
|
|
333
588
|
/**
|
|
334
589
|
* Synchronously fsync the event ledger file to ensure OS page-cache buffers are
|
|
335
590
|
* flushed to durable storage. Must be called AFTER in-flight drain but BEFORE
|
|
@@ -349,6 +604,8 @@ type PlanContextInput = {
|
|
|
349
604
|
correlation_id?: string;
|
|
350
605
|
session_id?: string;
|
|
351
606
|
target_paths?: string[];
|
|
607
|
+
layer_filter?: "team" | "personal" | "both";
|
|
608
|
+
include_related?: boolean;
|
|
352
609
|
};
|
|
353
610
|
type RequirementProfile = {
|
|
354
611
|
target_path: string;
|
|
@@ -373,10 +630,12 @@ type PlanContextResult = {
|
|
|
373
630
|
selection_token: string;
|
|
374
631
|
entries: PlanContextEntry[];
|
|
375
632
|
candidates: RuleDescriptionIndexItem[];
|
|
633
|
+
omitted_candidate_count?: number;
|
|
376
634
|
preflight_diagnostics: PreflightDiagnostic[];
|
|
377
635
|
auto_healed?: boolean;
|
|
378
636
|
previous_revision_hash?: string;
|
|
379
637
|
redirects?: Record<string, string>;
|
|
638
|
+
related_appended?: Record<string, string>;
|
|
380
639
|
};
|
|
381
640
|
type SelectionTokenState = {
|
|
382
641
|
token: string;
|
|
@@ -398,6 +657,18 @@ type RecallInput = PlanContextInput & {
|
|
|
398
657
|
* provided, filters the fetched body set to this intersection.
|
|
399
658
|
*/
|
|
400
659
|
ids?: string[];
|
|
660
|
+
/**
|
|
661
|
+
* v2.2 MC1-recall-pack (W2-T4): when true, expand the fetched set with the
|
|
662
|
+
* one-hop `related` graph neighbours (H2) of the selected entries that are
|
|
663
|
+
* also present in the candidate index. Lets a scoped `ids` recall pull in the
|
|
664
|
+
* connected knowledge without a second round-trip. No-op when `ids` is omitted
|
|
665
|
+
* (every candidate is already fetched) or no related edges resolve in-corpus.
|
|
666
|
+
*/
|
|
667
|
+
include_related?: boolean;
|
|
668
|
+
};
|
|
669
|
+
type RecallTruncation = {
|
|
670
|
+
omitted_candidate_count: number;
|
|
671
|
+
returned_candidate_count: number;
|
|
401
672
|
};
|
|
402
673
|
type RecallResult = PlanContextResult & {
|
|
403
674
|
rules: Array<{
|
|
@@ -405,90 +676,23 @@ type RecallResult = PlanContextResult & {
|
|
|
405
676
|
level: "L0" | "L1" | "L2";
|
|
406
677
|
path: string;
|
|
407
678
|
body: string;
|
|
679
|
+
store?: {
|
|
680
|
+
alias: string;
|
|
681
|
+
};
|
|
408
682
|
}>;
|
|
409
683
|
selected_stable_ids: string[];
|
|
410
684
|
diagnostics: Array<{
|
|
411
|
-
code: "missing_knowledge_metadata";
|
|
685
|
+
code: "missing_knowledge_metadata" | "unresolved_selected_id";
|
|
412
686
|
severity: "warn";
|
|
413
687
|
stable_id: string;
|
|
414
688
|
message: string;
|
|
415
689
|
}>;
|
|
690
|
+
directive: string;
|
|
691
|
+
next_steps?: string[];
|
|
692
|
+
truncation?: RecallTruncation;
|
|
416
693
|
};
|
|
417
694
|
declare function recall(projectRoot: string, input: RecallInput): Promise<RecallResult>;
|
|
418
695
|
|
|
419
|
-
/**
|
|
420
|
-
* O(1) in-memory increment for a named counter. Safe to call from any MCP
|
|
421
|
-
* tool handler; no I/O happens until the next `flushMetrics()` call.
|
|
422
|
-
*
|
|
423
|
-
* `delta` defaults to 1; callers can pass a positive integer (e.g. fetched
|
|
424
|
-
* N stable_ids in a single sections call) to fold N bumps into one.
|
|
425
|
-
*/
|
|
426
|
-
declare function bumpCounter(projectRoot: string, name: string, delta?: number): void;
|
|
427
|
-
/**
|
|
428
|
-
* Snapshot the current counter accumulator and reset it. Returned map is a
|
|
429
|
-
* frozen copy; the live accumulator starts fresh from zero. Exposed so
|
|
430
|
-
* flushMetrics + tests + a future fab_metrics manual-flush CLI hook can all
|
|
431
|
-
* use the same primitive without racing.
|
|
432
|
-
*/
|
|
433
|
-
declare function drainCounters(projectRoot: string): Record<string, number>;
|
|
434
|
-
/**
|
|
435
|
-
* Drain the current accumulator and append one JSONL row to
|
|
436
|
-
* `.fabric/metrics.jsonl`. Returns the appended row (or `null` when the
|
|
437
|
-
* accumulator was empty — no spurious zero rows). fs failures degrade
|
|
438
|
-
* silently; the next flush will carry the union of the failed-write
|
|
439
|
-
* interval + the current one.
|
|
440
|
-
*/
|
|
441
|
-
declare function flushMetrics(projectRoot: string, options?: {
|
|
442
|
-
windowMs?: number;
|
|
443
|
-
now?: Date;
|
|
444
|
-
}): Promise<MetricsRow | null>;
|
|
445
|
-
/**
|
|
446
|
-
* Start the background flush timer for a project root. Idempotent — calling
|
|
447
|
-
* twice on the same root replaces the prior interval. Returns a stop handle
|
|
448
|
-
* the caller can invoke at shutdown to flush + clear the timer.
|
|
449
|
-
*
|
|
450
|
-
* The flush is fire-and-forget (no await on the setInterval callback) so
|
|
451
|
-
* the timer cadence stays accurate even when fs is slow.
|
|
452
|
-
*/
|
|
453
|
-
declare function startMetricsFlush(projectRoot: string, options?: {
|
|
454
|
-
intervalMs?: number;
|
|
455
|
-
}): () => Promise<void>;
|
|
456
|
-
/**
|
|
457
|
-
* Cancel the background flush timer for a project root if one is running.
|
|
458
|
-
* Does NOT drain the accumulator — callers that want a final flush should
|
|
459
|
-
* await flushMetrics(projectRoot) afterward.
|
|
460
|
-
*/
|
|
461
|
-
declare function stopMetricsFlush(projectRoot: string): void;
|
|
462
|
-
/**
|
|
463
|
-
* Read accumulated metrics rows from `.fabric/metrics.jsonl`. Missing file
|
|
464
|
-
* returns []. Malformed rows are dropped silently (the sidecar is best-
|
|
465
|
-
* effort observability; a corrupt row never blocks a reader).
|
|
466
|
-
*
|
|
467
|
-
* Exposed for the NEW-34 `fab metrics` CLI dashboard + future doctor lints
|
|
468
|
-
* (e.g. cite-goodhart pattern replay) that need counter trends without
|
|
469
|
-
* walking events.jsonl.
|
|
470
|
-
*/
|
|
471
|
-
declare function readMetrics(projectRoot: string): Promise<MetricsRow[]>;
|
|
472
|
-
type MetricsRow = {
|
|
473
|
-
timestamp: string;
|
|
474
|
-
window: string;
|
|
475
|
-
counters: Record<string, number>;
|
|
476
|
-
};
|
|
477
|
-
/**
|
|
478
|
-
* Canonical metric counter names used by the high-frequency emitters that
|
|
479
|
-
* left events.jsonl as part of the rc.37 Wave B clean-slate. Centralized
|
|
480
|
-
* here so the B5 hard-gate (`metric_event_in_jsonl`) can grep for these
|
|
481
|
-
* exact strings and fail when one accidentally re-appears in the audit
|
|
482
|
-
* ledger emit path.
|
|
483
|
-
*/
|
|
484
|
-
declare const METRIC_COUNTER_NAMES: {
|
|
485
|
-
readonly knowledge_consumed: "knowledge_consumed";
|
|
486
|
-
readonly edit_intent_checked: "edit_intent_checked";
|
|
487
|
-
readonly knowledge_context_planned: "knowledge_context_planned";
|
|
488
|
-
readonly knowledge_sections_fetched: "knowledge_sections_fetched";
|
|
489
|
-
};
|
|
490
|
-
type MetricCounterName = (typeof METRIC_COUNTER_NAMES)[keyof typeof METRIC_COUNTER_NAMES];
|
|
491
|
-
|
|
492
696
|
/**
|
|
493
697
|
* Start the background rotation timer for a project root. Idempotent —
|
|
494
698
|
* calling twice on the same root replaces the prior interval. Returns a
|
|
@@ -511,10 +715,47 @@ declare function startRotationTick(projectRoot: string, options?: {
|
|
|
511
715
|
declare function stopRotationTick(projectRoot: string): void;
|
|
512
716
|
|
|
513
717
|
/**
|
|
514
|
-
*
|
|
718
|
+
* ContextCache — unified hot-path cache for the Fabric server.
|
|
719
|
+
*
|
|
720
|
+
* Three logical slots:
|
|
721
|
+
* 1. "meta" — agents.meta.json content (TTL-based, default 5 s)
|
|
722
|
+
* 2. "context" — GetKnowledgeContext per projectRoot (TTL-based, default 5 s)
|
|
723
|
+
* 3. "audit" — sliding-window byte-offset cursor for audit.jsonl reads
|
|
724
|
+
*
|
|
725
|
+
* Invalidation reasons:
|
|
726
|
+
* - "meta_write" — eager invalidation when a write service mutates agents.meta.json
|
|
727
|
+
* - "file_watch" — chokidar detected an on-disk change
|
|
515
728
|
*/
|
|
516
|
-
|
|
517
|
-
|
|
729
|
+
type InvalidationReason = "meta_write" | "file_watch";
|
|
730
|
+
type AuditCursor = {
|
|
731
|
+
offset: number;
|
|
732
|
+
remainder: string;
|
|
733
|
+
windowEntries: Array<{
|
|
734
|
+
ts: number;
|
|
735
|
+
}>;
|
|
736
|
+
};
|
|
737
|
+
declare class ContextCache {
|
|
738
|
+
private readonly defaultTtlMs;
|
|
739
|
+
private readonly metaSlot;
|
|
740
|
+
private readonly contextSlot;
|
|
741
|
+
private readonly auditSlot;
|
|
742
|
+
constructor(defaultTtlMs?: number);
|
|
743
|
+
get<T>(slot: "meta" | "context", key: string): T | undefined;
|
|
744
|
+
set<T>(slot: "meta" | "context", key: string, value: T, ttlMs?: number): void;
|
|
745
|
+
getAuditCursor(projectRoot: string): AuditCursor | undefined;
|
|
746
|
+
setAuditCursor(projectRoot: string, cursor: AuditCursor): void;
|
|
747
|
+
resetAuditCursor(projectRoot: string): void;
|
|
748
|
+
/**
|
|
749
|
+
* Invalidate cache slots based on what changed.
|
|
750
|
+
*
|
|
751
|
+
* @param reason "meta_write" — only the meta slot for this projectRoot
|
|
752
|
+
* "file_watch" — meta + context slots (AGENTS.md may have changed)
|
|
753
|
+
* @param projectRoot Optional; if omitted, clears ALL keys in affected slots.
|
|
754
|
+
*/
|
|
755
|
+
invalidate(reason: InvalidationReason, projectRoot?: string): void;
|
|
756
|
+
private slotStore;
|
|
757
|
+
}
|
|
758
|
+
declare const contextCache: ContextCache;
|
|
518
759
|
|
|
519
760
|
interface KnowledgeSyncOptions {
|
|
520
761
|
mode?: "incremental" | "full";
|
|
@@ -563,6 +804,12 @@ interface KnowledgeSyncReport {
|
|
|
563
804
|
warnings: StructuredWarning[];
|
|
564
805
|
reconciled_files?: string[];
|
|
565
806
|
}
|
|
807
|
+
/**
|
|
808
|
+
* Clear the knowledge-sync cooldown for a projectRoot so the next ensureKnowledgeFresh
|
|
809
|
+
* call performs a real I/O scan. Called by the chokidar watcher when a rule
|
|
810
|
+
* file changes (see http.ts handleCacheWatcherEvent).
|
|
811
|
+
*/
|
|
812
|
+
declare function invalidateKnowledgeSyncCooldown(projectRoot: string): void;
|
|
566
813
|
/**
|
|
567
814
|
* Detects drift between disk and agents.meta.json, emits ledger events, and
|
|
568
815
|
* invalidates the cache. Does NOT rewrite agents.meta.json. Optimised for
|
|
@@ -596,6 +843,80 @@ interface ReconcileKnowledgeOptions {
|
|
|
596
843
|
*/
|
|
597
844
|
declare function reconcileKnowledge(projectRoot: string, opts?: ReconcileKnowledgeOptions): Promise<KnowledgeSyncReport>;
|
|
598
845
|
|
|
846
|
+
type LedgerSourceFilter = "ai" | "human";
|
|
847
|
+
type StoredLedgerEntry = LedgerEntry & {
|
|
848
|
+
id: string;
|
|
849
|
+
};
|
|
850
|
+
type ReadLedgerOptions = {
|
|
851
|
+
source?: LedgerSourceFilter;
|
|
852
|
+
since?: number;
|
|
853
|
+
};
|
|
854
|
+
type ResolvedLedgerPaths = {
|
|
855
|
+
primaryPath: string;
|
|
856
|
+
legacyPath: string;
|
|
857
|
+
readPath: string;
|
|
858
|
+
usingLegacy: boolean;
|
|
859
|
+
};
|
|
860
|
+
declare function resolveLedgerPaths(projectRoot: string): Promise<ResolvedLedgerPaths>;
|
|
861
|
+
declare function readLedger(projectRoot: string, options?: ReadLedgerOptions): Promise<StoredLedgerEntry[]>;
|
|
862
|
+
|
|
863
|
+
type RehydrateTarget = {
|
|
864
|
+
ledgerEntryId: string;
|
|
865
|
+
} | {
|
|
866
|
+
timestamp: number;
|
|
867
|
+
};
|
|
868
|
+
type RehydratedAgentsMetaSnapshot = {
|
|
869
|
+
meta: AgentsMeta;
|
|
870
|
+
metadata: {
|
|
871
|
+
at_ledger_id: string;
|
|
872
|
+
at_commit: string | null;
|
|
873
|
+
replayed_count: number;
|
|
874
|
+
mode: "git-show" | "ledger-fallback";
|
|
875
|
+
};
|
|
876
|
+
entries: StoredLedgerEntry[];
|
|
877
|
+
};
|
|
878
|
+
declare function rehydrateAgentsMetaAt(projectRoot: string, target: RehydrateTarget): Promise<RehydratedAgentsMetaSnapshot>;
|
|
879
|
+
|
|
880
|
+
declare function readAgentsMeta(projectRoot: string): Promise<AgentsMeta>;
|
|
881
|
+
|
|
882
|
+
type KnowledgeEntryItem = {
|
|
883
|
+
path: string;
|
|
884
|
+
content: string;
|
|
885
|
+
};
|
|
886
|
+
type DescriptionStub = {
|
|
887
|
+
path: string;
|
|
888
|
+
description: string;
|
|
889
|
+
};
|
|
890
|
+
type HumanLockedNearby = {
|
|
891
|
+
file: string;
|
|
892
|
+
excerpt: string;
|
|
893
|
+
};
|
|
894
|
+
type KnowledgePayload = {
|
|
895
|
+
L0: string;
|
|
896
|
+
L1: KnowledgeEntryItem[];
|
|
897
|
+
L2: KnowledgeEntryItem[];
|
|
898
|
+
human_locked_nearby: HumanLockedNearby[];
|
|
899
|
+
description_stubs?: DescriptionStub[];
|
|
900
|
+
};
|
|
901
|
+
type GetKnowledgeInput = {
|
|
902
|
+
path: string;
|
|
903
|
+
client_hash?: string;
|
|
904
|
+
correlation_id?: string;
|
|
905
|
+
session_id?: string;
|
|
906
|
+
};
|
|
907
|
+
type GetKnowledgeResult = {
|
|
908
|
+
revision_hash: string;
|
|
909
|
+
stale: boolean;
|
|
910
|
+
rules: KnowledgePayload;
|
|
911
|
+
};
|
|
912
|
+
declare function getKnowledge(projectRoot: string, input: GetKnowledgeInput): Promise<GetKnowledgeResult>;
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Shared constants used across the server package.
|
|
916
|
+
*/
|
|
917
|
+
/** MCP resource URI for the project's bootstrap README (L0 rules) file. */
|
|
918
|
+
declare const AGENTS_MD_RESOURCE_URI = "fabric://bootstrap-readme";
|
|
919
|
+
|
|
599
920
|
/**
|
|
600
921
|
* Returns an info-level startup message when CLAUDE.md or AGENTS.md exist at
|
|
601
922
|
* the project root, or null when neither is present.
|
|
@@ -605,6 +926,7 @@ declare function reconcileKnowledge(projectRoot: string, opts?: ReconcileKnowled
|
|
|
605
926
|
*/
|
|
606
927
|
declare function formatPreexistingRootMessage(projectRoot: string): string | null;
|
|
607
928
|
|
|
929
|
+
declare const FABRIC_SERVER_INSTRUCTIONS: string;
|
|
608
930
|
declare function createFabricServer(tracker?: InFlightTracker): McpServer;
|
|
609
931
|
declare function startStdioServer(): Promise<void>;
|
|
610
932
|
/**
|
|
@@ -631,4 +953,4 @@ interface ShutdownHandlerDeps {
|
|
|
631
953
|
*/
|
|
632
954
|
declare function createShutdownHandler(deps: ShutdownHandlerDeps): () => void;
|
|
633
955
|
|
|
634
|
-
export { AGENTS_MD_RESOURCE_URI, 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 HistoryAllReport, type HistoryDayRow, type InFlightTracker, KnowledgeIdAllocator, type KnowledgeMetaBuildResult, type KnowledgeMetaBuildSource, type KnowledgeSyncLedgerEvent, type KnowledgeSyncOptions, type KnowledgeSyncReport, LEDGER_PATH, LEGACY_LEDGER_PATH, type LedgerEvent, METRICS_LEDGER_PATH, METRIC_COUNTER_NAMES, type MetricCounterName, type MetricsRow, type PlanContextInput, type PlanContextResult, type RecallInput, type RecallResult, type ReconcileKnowledgeOptions, type RequirementProfile, type SelectionTokenState, type ShutdownHandlerDeps, type StructuredWarning, type WriteKnowledgeMetaOptions, appendEventLedgerEvent, buildKnowledgeMeta, bumpCounter, computeKnowledgeBasedAgentsMeta, computeKnowledgeTestIndex, createFabricServer, createInFlightTracker, createShutdownHandler, deriveKnowledgeMetaLayer, deriveKnowledgeMetaTopologyType, drainCounters, enrichDescriptions, ensureKnowledgeFresh, extractKnowledge, flushAndSyncEventLedger, flushMetrics, formatPreexistingRootMessage, getEventLedgerPath, getLedgerPath, getLegacyLedgerPath, getMetricsLedgerPath, isSameKnowledgeTestIndex, loadKbIdTypeMap, planContext, readMetrics, readSelectionToken, recall, reconcileKnowledge, reviewKnowledge, runDoctorApplyLint, runDoctorArchiveHistory, runDoctorCiteCoverage, runDoctorFix, runDoctorHistoryAll, runDoctorReport, stableStringify, startMetricsFlush, startRotationTick, startStdioServer, stopMetricsFlush, stopRotationTick, writeKnowledgeMeta };
|
|
956
|
+
export { AGENTS_MD_RESOURCE_URI, type ArchiveHistoryEntry, type ArchiveHistoryReport, type CiteCoverageReport, type ConflictEntry, type ConflictJudge, type ConflictLintReport, type ConflictPair, type ConflictVerdict, DEFAULT_CONFLICT_SIMILARITY_THRESHOLD, type DoctorApplyLintMutation, type DoctorApplyLintMutationKind, type DoctorApplyLintReport, type DoctorFixReport, type DoctorIssue, type DoctorReport, EVENT_LEDGER_PATH, type EnrichDescriptionsCandidate, type EnrichDescriptionsMode, type EnrichDescriptionsReport, FABRIC_SERVER_INSTRUCTIONS, type HistoryAllReport, type HistoryDayRow, type InFlightTracker, KnowledgeIdAllocator, type KnowledgeMetaBuildResult, type KnowledgeMetaBuildSource, type KnowledgeSyncLedgerEvent, type KnowledgeSyncOptions, type KnowledgeSyncReport, LEDGER_PATH, LEGACY_LEDGER_PATH, type LedgerEvent, METRICS_LEDGER_PATH, METRIC_COUNTER_NAMES, type MetricCounterName, type MetricsRow, type PlanContextInput, type PlanContextResult, type RecallInput, type RecallResult, type ReconcileKnowledgeOptions, type RequirementProfile, type SelectionTokenState, type ShutdownHandlerDeps, type StructuredWarning, type WriteKnowledgeMetaOptions, appendEventLedgerEvent, buildKnowledgeMeta, bumpCounter, computeKnowledgeBasedAgentsMeta, computeKnowledgeTestIndex, contextCache, createFabricServer, createInFlightTracker, createShutdownHandler, deriveKnowledgeMetaLayer, deriveKnowledgeMetaTopologyType, drainCounters, enrichDescriptions, ensureKnowledgeFresh, extractKnowledge, findConflictCandidates, flushAndSyncEventLedger, flushMetrics, formatPreexistingRootMessage, getEventLedgerPath, getKnowledge, getLedgerPath, getLegacyLedgerPath, getMetricsLedgerPath, invalidateKnowledgeSyncCooldown, isSameKnowledgeTestIndex, lintConflicts, loadConflictEntries, loadKbIdTypeMap, pairSimilarity, planContext, readAgentsMeta, readEventLedger, readLedger, readMetrics, readSelectionToken, recall, reconcileKnowledge, rehydrateAgentsMetaAt, resolveLedgerPaths, reviewKnowledge, runDoctorApplyLint, runDoctorArchiveHistory, runDoctorCiteCoverage, runDoctorConflictLint, runDoctorFix, runDoctorHistoryAll, runDoctorReport, stableStringify, startMetricsFlush, startRotationTick, startStdioServer, stopMetricsFlush, stopRotationTick, writeKnowledgeMeta };
|