mirror of
https://github.com/Kaelio/ktx.git
synced 2026-07-13 11:22:11 +02:00
feat: rename project wiki directory
This commit is contained in:
parent
e1e9c4af91
commit
7ca96ce316
111 changed files with 423 additions and 423 deletions
|
|
@ -10,7 +10,7 @@ Capture only when the signal is unambiguous: a metric definition stated plainly,
|
|||
|
||||
<workflow>
|
||||
1. Read the wiki and SL indexes to avoid creating duplicates.
|
||||
2. If the content has wiki-style signal, load the `knowledge_capture` skill and follow its workflow.
|
||||
2. If the content has wiki-style signal, load the `wiki_capture` skill and follow its workflow.
|
||||
3. If the content has SL-style signal, load the `sl` skill and follow its Part 3 workflow.
|
||||
4. Prefer updating existing entries over creating new ones — backfills often duplicate existing knowledge.
|
||||
5. When done, exit the loop.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ Parsimonious. Stage 3 WUs already loaded `ingest_triage` and handled conflicts t
|
|||
</stance>
|
||||
|
||||
<workflow>
|
||||
1. Load `ingest_triage`, then `sl_capture` + `knowledge_capture`.
|
||||
1. Load `ingest_triage`, then `sl_capture` + `wiki_capture`.
|
||||
2. Call `stage_list()` for the full index of this job's writes. If it is empty AND you have no evictions, exit — the runner short-circuits this case but the skill still teaches you to bail fast.
|
||||
3. If the system prompt includes `<canonical_pins>`, apply those pins before flagging a same-name or near-duplicate conflict. A pinned `canonicalArtifactKey` keeps the contested name when it is present in the Stage Index; competing variants keep or receive disambiguated names.
|
||||
4. Sweep both exact-key conflicts and near-duplicate writes. Compare WUs that wrote overlapping SL source names, overlapping wiki keys, the same `tables:` or `sl_refs:` action details, or obviously equivalent topic titles under different wiki keys. Call `stage_diff` to see the actual difference, and use `wiki_read`/`sl_read_source` when two different keys appear to describe the same table, metric, or source-of-truth mapping. If they're the same content, leave one canonical artifact and record the duplicate as subsumed. If they differ per `ingest_triage` rules, apply the correct resolution (rename + capture; election of canonical; silent replace for expression-only re-ingest change; or pinned canonical), then call `emit_conflict_resolution` with the artifact key and decision.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ Assertive. The bundle was explicitly submitted for ingest. Default to capturing
|
|||
|
||||
<workflow>
|
||||
1. Read this WorkUnit's section at the end of the user prompt. It lists your `rawFiles`, any unchanged `dependencyPaths` you may need to resolve references, the `peerFileIndex` (paths only; you CANNOT read them), the source's `skillNames`, and any `priorProvenance` rows telling you what earlier syncs produced from these files.
|
||||
2. Load the per-source review skill first (e.g. `lookml_ingest`, `metricflow_ingest`, `dbt_ingest`), then `sl_capture` and `knowledge_capture`, and `ingest_triage` last. The triage skill tells you how to react when `discover_data` reveals that a prior WU already wrote something overlapping.
|
||||
2. Load the per-source review skill first (e.g. `lookml_ingest`, `metricflow_ingest`, `dbt_ingest`), then `sl_capture` and `wiki_capture`, and `ingest_triage` last. The triage skill tells you how to react when `discover_data` reveals that a prior WU already wrote something overlapping.
|
||||
3. If the system prompt includes `<canonical_pins>`, read those pins before choosing artifact keys. A pin's `canonicalArtifactKey` is the preferred artifact for its `contestedKey`: prefer editing the pinned canonical artifact when it already exists or when this raw file clearly updates it. Do not create a duplicate contested artifact when a pin says another artifact is canonical; use a specific disambiguated key only when the raw file describes a genuinely different domain.
|
||||
4. For each raw file: call `read_raw_file` (or `read_raw_span` for slicing large files) to load content. Before writing a new SL source or wiki page, call `discover_data` for each candidate source, table, metric, or topic name to find prior-WU writes, existing wiki pages, SL sources, and raw warehouse matches; apply `ingest_triage` when you hit one, and apply any matching canonical pin before deciding whether to edit, rename, or skip.
|
||||
5. For every `wiki_write`, `wiki_remove`, `sl_write_source`, or `sl_edit_source` call, include `rawPaths` with only the raw file paths that directly support that action. If one artifact synthesizes several files, list each contributing raw file. Do not include unrelated files from the same WorkUnit.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ A single artifact typically produces multiple actions: one SL source per table/v
|
|||
|
||||
<workflow>
|
||||
1. Review the wiki and SL indexes in the prompt. Prefer updating existing entries over creating duplicates.
|
||||
2. Load the `sl` skill for SL-writes and `knowledge_capture` for wiki-writes. Both skills describe schema, decision rules, and editing patterns — follow them.
|
||||
2. Load the `sl` skill for SL-writes and `wiki_capture` for wiki-writes. Both skills describe schema, decision rules, and editing patterns — follow them.
|
||||
3. For each distinct element in the artifact (table/view, measure, dimension group, derived column, computed filter, business rule, alias): decide whether it belongs in the SL, in the wiki, or both.
|
||||
4. Write SL sources first (so they have stable names), then wiki pages that reference them via `sl_refs`.
|
||||
5. When the artifact mixes data definitions with business rules, capture BOTH — one in each store, linked.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ Skip:
|
|||
<workflow>
|
||||
1. Read the wiki index and the SL sources index in the prompt below.
|
||||
2. Identify durable knowledge OR reusable data patterns in the turn.
|
||||
3. If the turn has wiki-style signal (preferences, definitions, conventions), load the `knowledge_capture` skill and follow its workflow.
|
||||
3. If the turn has wiki-style signal (preferences, definitions, conventions), load the `wiki_capture` skill and follow its workflow.
|
||||
4. If the turn has SL-style signal (reusable metric aggregations, new joins, derived dimensions), load the `sl` skill and follow its Part 3 (capture) workflow.
|
||||
5. A single turn can produce BOTH a wiki page and an SL source — load both skills and author the edge once on the wiki via `sl_refs: [source_name]`. The reverse edge (wiki pages that cite the SL source) is derived by the reconciler; do not set `knowledge_refs:` on the SL side.
|
||||
6. When you're done, exit the loop without calling any more tools. Do NOT emit a final text summary.
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ metrics:
|
|||
```
|
||||
|
||||
Do NOT emit SL for this. Instead:
|
||||
- Write a wiki page at `knowledge/global/<metric_name>-intent.md` quoting the full YAML body and a one-line explanation of the intended semantics (base event → conversion event within window).
|
||||
- Write a wiki page at `wiki/global/<metric_name>-intent.md` quoting the full YAML body and a one-line explanation of the intended semantics (base event → conversion event within window).
|
||||
- Call `emit_unmapped_fallback` with `rawPath` set to the MetricFlow file path, `reason: "conversion_metric_unsupported"`, and `fallback: "flagged"`.
|
||||
|
||||
When KTX SL gains conversion primitives, re-ingesting will find the prior wiki note (via `priorProvenance`) and replace it with an SL source.
|
||||
|
|
@ -288,7 +288,7 @@ measures:
|
|||
- {name: margin, expr: "sum(revenue_cents) - sum(cost_cents)"}
|
||||
```
|
||||
|
||||
Also write a wiki page at `knowledge/global/margin-metric.md` explaining the cross-source origin.
|
||||
Also write a wiki page at `wiki/global/margin-metric.md` explaining the cross-source origin.
|
||||
|
||||
## Example 4 — filtered metric creates a new measure
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
---
|
||||
name: knowledge_capture
|
||||
description: KTX's knowledge base — wiki pages for durable, reusable business knowledge. Covers capture workflow for user preferences, metric definitions, organizational conventions, and cross-references between knowledge pages and semantic-layer sources. Loaded by the post-turn memory-agent only. The research agent reads wiki via `wiki_read`/`wiki_search` but does not write it.
|
||||
name: wiki_capture
|
||||
description: KTX's knowledge base — wiki pages for durable, reusable business knowledge. Covers capture workflow for user preferences, metric definitions, organizational conventions, and cross-references between wiki pages and semantic-layer sources. Loaded by the post-turn memory-agent only. The research agent reads wiki via `wiki_read`/`wiki_search` but does not write it.
|
||||
callers: [memory_agent]
|
||||
---
|
||||
|
||||
# Knowledge Capture
|
||||
# Wiki Capture
|
||||
|
||||
## Role
|
||||
|
||||
|
|
@ -19,13 +19,13 @@ describe('memory action target identity', () => {
|
|||
{
|
||||
target: 'wiki',
|
||||
type: 'created',
|
||||
key: 'knowledge/global/orders.md',
|
||||
key: 'wiki/global/orders.md',
|
||||
detail: '',
|
||||
targetConnectionId: 'ignored',
|
||||
},
|
||||
'looker-run',
|
||||
),
|
||||
).toBe('wiki:looker-run:knowledge/global/orders.md');
|
||||
).toBe('wiki:looker-run:wiki/global/orders.md');
|
||||
});
|
||||
|
||||
it('resolves action target connection only for SL actions', () => {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ interface DbtSourceAdapterOptions {
|
|||
|
||||
export class DbtSourceAdapter implements SourceAdapter {
|
||||
readonly source = 'dbt' as const;
|
||||
/** Runner merges: ingest_triage, sl_capture, knowledge_capture (see ingest-bundle.runner.ts) */
|
||||
/** Runner merges: ingest_triage, sl_capture, wiki_capture (see ingest-bundle.runner.ts) */
|
||||
readonly skillNames: string[] = ['dbt_ingest'];
|
||||
|
||||
constructor(private readonly options: DbtSourceAdapterOptions = {}) {}
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ describe('historic-SQL local ingest retrieval acceptance', () => {
|
|||
|
||||
await expect(readFile(join(project.projectDir, 'semantic-layer/warehouse/_schema/public.yaml'), 'utf-8')).resolves
|
||||
.toContain('Analysts repeatedly inspect paid order lifecycle by customer segment.');
|
||||
await expect(readFile(join(project.projectDir, 'knowledge/global/historic-sql-paid-order-lifecycle.md'), 'utf-8'))
|
||||
await expect(readFile(join(project.projectDir, 'wiki/global/historic-sql-paid-order-lifecycle.md'), 'utf-8'))
|
||||
.resolves.toContain('Paid Order Lifecycle');
|
||||
|
||||
const reloaded = await loadKtxProject({ projectDir: project.projectDir });
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ async function commitProjectionChanges(workdir: string): Promise<void> {
|
|||
const status = await git.status();
|
||||
const paths = status.files
|
||||
.map((file) => file.path)
|
||||
.filter((path) => path.startsWith('semantic-layer/') || path.startsWith('knowledge/global/historic-sql'));
|
||||
.filter((path) => path.startsWith('semantic-layer/') || path.startsWith('wiki/global/historic-sql'));
|
||||
if (paths.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ describe('projectHistoricSqlEvidence', () => {
|
|||
await writeJson(workdir, 'raw-sources/warehouse/historic-sql/sync-1/tables/public.customers.json', { table: 'public.customers' });
|
||||
await writeText(
|
||||
workdir,
|
||||
'knowledge/global/historic-sql-old-order-lifecycle.md',
|
||||
'wiki/global/historic-sql-old-order-lifecycle.md',
|
||||
[
|
||||
'---',
|
||||
YAML.stringify({
|
||||
|
|
@ -127,7 +127,7 @@ describe('projectHistoricSqlEvidence', () => {
|
|||
);
|
||||
await writeText(
|
||||
workdir,
|
||||
'knowledge/global/historic-sql-retired-pattern.md',
|
||||
'wiki/global/historic-sql-retired-pattern.md',
|
||||
[
|
||||
'---',
|
||||
YAML.stringify({
|
||||
|
|
@ -164,10 +164,10 @@ describe('projectHistoricSqlEvidence', () => {
|
|||
const result = await projectHistoricSqlEvidence({ workdir, connectionId: 'warehouse', syncId: 'sync-1', runId: 'run-1' });
|
||||
|
||||
expect(result.patternPagesWritten).toBe(1);
|
||||
await expect(readFile(join(workdir, 'knowledge/global/historic-sql-old-order-lifecycle.md'), 'utf-8')).resolves.toContain(
|
||||
await expect(readFile(join(workdir, 'wiki/global/historic-sql-old-order-lifecycle.md'), 'utf-8')).resolves.toContain(
|
||||
'Order Lifecycle Analysis',
|
||||
);
|
||||
await expect(readFile(join(workdir, 'knowledge/global/historic-sql-retired-pattern.md'), 'utf-8')).resolves.toContain(
|
||||
await expect(readFile(join(workdir, 'wiki/global/historic-sql-retired-pattern.md'), 'utf-8')).resolves.toContain(
|
||||
'stale_since: "2026-05-11T00:00:00.000Z"',
|
||||
);
|
||||
});
|
||||
|
|
@ -192,7 +192,7 @@ describe('projectHistoricSqlEvidence', () => {
|
|||
await writeJson(workdir, 'raw-sources/warehouse/historic-sql/sync-1/tables/public.customers.json', { table: 'public.customers' });
|
||||
await writeText(
|
||||
workdir,
|
||||
'knowledge/global/historic-sql-order-lifecycle-analysis.md',
|
||||
'wiki/global/historic-sql-order-lifecycle-analysis.md',
|
||||
[
|
||||
'---',
|
||||
YAML.stringify({
|
||||
|
|
@ -230,7 +230,7 @@ describe('projectHistoricSqlEvidence', () => {
|
|||
const result = await projectHistoricSqlEvidence({ workdir, connectionId: 'warehouse', syncId: 'sync-1', runId: 'run-1' });
|
||||
|
||||
expect(result.patternPagesWritten).toBe(1);
|
||||
const page = await readFile(join(workdir, 'knowledge/global/historic-sql-order-lifecycle-analysis.md'), 'utf-8');
|
||||
const page = await readFile(join(workdir, 'wiki/global/historic-sql-order-lifecycle-analysis.md'), 'utf-8');
|
||||
expect(page).toContain('Analysts compare order status with customer segment again.');
|
||||
expect(page).not.toContain('Archived body');
|
||||
expect(page).not.toContain('archived');
|
||||
|
|
@ -254,7 +254,7 @@ describe('projectHistoricSqlEvidence', () => {
|
|||
});
|
||||
await writeText(
|
||||
workdir,
|
||||
'knowledge/global/historic-sql-retired-pattern.md',
|
||||
'wiki/global/historic-sql-retired-pattern.md',
|
||||
[
|
||||
'---',
|
||||
YAML.stringify({
|
||||
|
|
@ -279,7 +279,7 @@ describe('projectHistoricSqlEvidence', () => {
|
|||
|
||||
expect(result.archivedPatternPages).toBe(0);
|
||||
expect(result.stalePatternPagesMarked).toBe(0);
|
||||
await expect(readFile(join(workdir, 'knowledge/global/historic-sql-retired-pattern.md'), 'utf-8')).resolves.toContain(
|
||||
await expect(readFile(join(workdir, 'wiki/global/historic-sql-retired-pattern.md'), 'utf-8')).resolves.toContain(
|
||||
'Archived retired body',
|
||||
);
|
||||
});
|
||||
|
|
@ -322,7 +322,7 @@ describe('projectHistoricSqlEvidence', () => {
|
|||
});
|
||||
await writeText(
|
||||
workdir,
|
||||
'knowledge/global/historic-sql-legacy-template.md',
|
||||
'wiki/global/historic-sql-legacy-template.md',
|
||||
[
|
||||
'---',
|
||||
YAML.stringify({
|
||||
|
|
@ -357,7 +357,7 @@ describe('projectHistoricSqlEvidence', () => {
|
|||
commonJoins: [],
|
||||
staleSince: '2026-05-11T00:00:00.000Z',
|
||||
});
|
||||
await expect(readFile(join(workdir, 'knowledge/global/historic-sql-legacy-template.md'), 'utf-8')).rejects.toMatchObject({
|
||||
await expect(readFile(join(workdir, 'wiki/global/historic-sql-legacy-template.md'), 'utf-8')).rejects.toMatchObject({
|
||||
code: 'ENOENT',
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ export async function projectHistoricSqlEvidence(input: HistoricSqlProjectionInp
|
|||
}
|
||||
}
|
||||
|
||||
const wikiRoot = join(input.workdir, 'knowledge/global');
|
||||
const wikiRoot = join(input.workdir, 'wiki/global');
|
||||
await mkdir(wikiRoot, { recursive: true });
|
||||
const allPages = await loadPatternPages(wikiRoot);
|
||||
const activePages = allPages.filter((page) => !isArchivedPatternPage(page));
|
||||
|
|
|
|||
|
|
@ -599,7 +599,7 @@ describe('IngestBundleRunner — Stages 1 → 7', () => {
|
|||
currentToolSession.actions.push({
|
||||
target: 'wiki',
|
||||
type: 'created',
|
||||
key: 'knowledge/orders.md',
|
||||
key: 'wiki/orders.md',
|
||||
detail: 'captured order context',
|
||||
});
|
||||
}
|
||||
|
|
@ -638,7 +638,7 @@ describe('IngestBundleRunner — Stages 1 → 7', () => {
|
|||
expect.objectContaining({
|
||||
type: 'work_unit_started',
|
||||
unitKey: 'u1',
|
||||
skills: ['ingest_triage', 'sl_capture', 'knowledge_capture'],
|
||||
skills: ['ingest_triage', 'sl_capture', 'wiki_capture'],
|
||||
stepBudget: 40,
|
||||
}),
|
||||
expect.objectContaining({ type: 'work_unit_step', unitKey: 'u1', stepIndex: 1, stepBudget: 40 }),
|
||||
|
|
@ -647,7 +647,7 @@ describe('IngestBundleRunner — Stages 1 → 7', () => {
|
|||
unitKey: 'u1',
|
||||
target: 'wiki',
|
||||
action: 'created',
|
||||
key: 'knowledge/orders.md',
|
||||
key: 'wiki/orders.md',
|
||||
}),
|
||||
expect.objectContaining({ type: 'work_unit_finished', unitKey: 'u1', status: 'success' }),
|
||||
]),
|
||||
|
|
@ -860,7 +860,7 @@ describe('IngestBundleRunner — Stages 1 → 7', () => {
|
|||
{ toolCallId: 'ledger-1', messages: [] },
|
||||
);
|
||||
await params.toolSet.wiki_write.execute(
|
||||
{ key: 'knowledge/a.md', content: 'safe summary' },
|
||||
{ key: 'wiki/a.md', content: 'safe summary' },
|
||||
{ toolCallId: 'wiki-1', messages: [] },
|
||||
);
|
||||
}
|
||||
|
|
@ -1351,7 +1351,7 @@ describe('IngestBundleRunner — Stages 1 → 7', () => {
|
|||
{
|
||||
target: 'wiki',
|
||||
type: 'created',
|
||||
key: 'knowledge/global/pipeline.md',
|
||||
key: 'wiki/global/pipeline.md',
|
||||
detail: 'Pipeline article',
|
||||
},
|
||||
{
|
||||
|
|
@ -1391,7 +1391,7 @@ describe('IngestBundleRunner — Stages 1 → 7', () => {
|
|||
});
|
||||
|
||||
expect(deps.knowledgeSlRefs.syncFromWiki).toHaveBeenCalledWith({
|
||||
wikiPageKey: 'knowledge/global/pipeline.md',
|
||||
wikiPageKey: 'wiki/global/pipeline.md',
|
||||
wikiScope: 'GLOBAL',
|
||||
wikiScopeId: null,
|
||||
refs: [{ connectionId: 'warehouse-2', sourceName: 'looker__b2b__sales_pipeline' }],
|
||||
|
|
@ -1410,7 +1410,7 @@ describe('IngestBundleRunner — Stages 1 → 7', () => {
|
|||
connectionId: 'looker-run',
|
||||
targetConnectionId: null,
|
||||
artifactKind: 'wiki',
|
||||
artifactKey: 'knowledge/global/pipeline.md',
|
||||
artifactKey: 'wiki/global/pipeline.md',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
|
@ -1617,7 +1617,7 @@ describe('IngestBundleRunner — Stages 1 → 7', () => {
|
|||
const workUnitCall = deps.agentRunner.runLoop.mock.calls.find(
|
||||
([params]: any[]) => params.telemetryTags.operationName === 'ingest-bundle-wu',
|
||||
);
|
||||
expect(workUnitCall?.[0].userPrompt).toContain('## Knowledge Pages');
|
||||
expect(workUnitCall?.[0].userPrompt).toContain('## Wiki Pages');
|
||||
expect(workUnitCall?.[0].userPrompt).toContain(
|
||||
'- revenue-recognition: Recognize revenue net of refunds after fulfillment.',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -293,7 +293,7 @@ export class IngestBundleRunner {
|
|||
return '(empty)';
|
||||
}
|
||||
|
||||
return `## Knowledge Pages\n${pages.map((page) => `- ${page.page_key}: ${page.summary}`).join('\n')}`;
|
||||
return `## Wiki Pages\n${pages.map((page) => `- ${page.page_key}: ${page.summary}`).join('\n')}`;
|
||||
}
|
||||
|
||||
private async buildSlIndex(connectionIds: string[]): Promise<string> {
|
||||
|
|
@ -596,7 +596,7 @@ export class IngestBundleRunner {
|
|||
|
||||
const baseFraming = await this.deps.promptService.loadPrompt('memory_agent_bundle_ingest_work_unit');
|
||||
const wuSkillNames = Array.from(
|
||||
new Set<string>([...adapter.skillNames, 'ingest_triage', 'sl_capture', 'knowledge_capture']),
|
||||
new Set<string>([...adapter.skillNames, 'ingest_triage', 'sl_capture', 'wiki_capture']),
|
||||
);
|
||||
const wuSkills = await this.deps.skillsRegistry.listSkills(wuSkillNames, 'memory_agent');
|
||||
const skillsPrompt = this.deps.skillsRegistry.buildSkillsPrompt(wuSkills, 'memory_agent');
|
||||
|
|
@ -973,7 +973,7 @@ export class IngestBundleRunner {
|
|||
const reconcileBaseFraming = await this.deps.promptService.loadPrompt('memory_agent_bundle_ingest_reconcile');
|
||||
const reconcileSkills = await this.deps.skillsRegistry.listSkills(
|
||||
Array.from(
|
||||
new Set(['ingest_triage', 'sl_capture', 'knowledge_capture', ...(adapter.reconcileSkillNames ?? [])]),
|
||||
new Set(['ingest_triage', 'sl_capture', 'wiki_capture', ...(adapter.reconcileSkillNames ?? [])]),
|
||||
),
|
||||
'memory_agent',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@ const adapterSkillNames = [
|
|||
'historic_sql_table_digest',
|
||||
'historic_sql_patterns',
|
||||
'ingest_triage',
|
||||
'knowledge_capture',
|
||||
'wiki_capture',
|
||||
'sl_capture',
|
||||
] as const;
|
||||
|
||||
const adapterReconcileSkillNames = [
|
||||
'ingest_triage',
|
||||
'knowledge_capture',
|
||||
'wiki_capture',
|
||||
'sl_capture',
|
||||
] as const;
|
||||
|
||||
|
|
|
|||
|
|
@ -314,7 +314,7 @@ class LocalKnowledgeIndex implements KnowledgeIndexPort {
|
|||
scope: string,
|
||||
scopeId: string | null,
|
||||
): Promise<Map<string, { searchText: string; hasEmbedding: boolean }>> {
|
||||
const prefix = scope === 'GLOBAL' ? 'knowledge/global/' : `knowledge/user/${scopeId}/`;
|
||||
const prefix = scope === 'GLOBAL' ? 'wiki/global/' : `wiki/user/${scopeId}/`;
|
||||
const result = new Map<string, { searchText: string; hasEmbedding: boolean }>();
|
||||
for (const [path, page] of this.sqlite.getExistingPages()) {
|
||||
if (!path.startsWith(prefix)) {
|
||||
|
|
@ -341,7 +341,7 @@ class LocalKnowledgeIndex implements KnowledgeIndexPort {
|
|||
}
|
||||
|
||||
async findPageByKey(scope: string, scopeId: string | null, pageKey: string) {
|
||||
const path = scope === 'GLOBAL' ? `knowledge/global/${pageKey}.md` : `knowledge/user/${scopeId}/${pageKey}.md`;
|
||||
const path = scope === 'GLOBAL' ? `wiki/global/${pageKey}.md` : `wiki/user/${scopeId}/${pageKey}.md`;
|
||||
try {
|
||||
await this.project.fileStore.readFile(path);
|
||||
return { page_key: pageKey };
|
||||
|
|
@ -355,12 +355,12 @@ class LocalKnowledgeIndex implements KnowledgeIndexPort {
|
|||
): Promise<KnowledgeIndexPageListing[]> {
|
||||
const pages: KnowledgeIndexPageListing[] = [];
|
||||
for (const scope of [
|
||||
{ scope: 'GLOBAL', scopeId: null, dir: 'knowledge/global' },
|
||||
{ scope: 'USER', scopeId: userId, dir: `knowledge/user/${userId}` },
|
||||
{ scope: 'GLOBAL', scopeId: null, dir: 'wiki/global' },
|
||||
{ scope: 'USER', scopeId: userId, dir: `wiki/user/${userId}` },
|
||||
]) {
|
||||
const listed = await this.project.fileStore.listFiles(scope.dir, true);
|
||||
for (const file of listed.files.filter((entry) => entry.endsWith('.md'))) {
|
||||
const parsedPath = parseKnowledgeIndexPath(file.startsWith('global/') || file.startsWith('user/') ? file : `${scope.dir.replace('knowledge/', '')}/${file}`);
|
||||
const parsedPath = parseKnowledgeIndexPath(file.startsWith('global/') || file.startsWith('user/') ? file : `${scope.dir.replace('wiki/', '')}/${file}`);
|
||||
if (!parsedPath || parsedPath.scope !== scope.scope) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -404,7 +404,7 @@ class LocalKnowledgeIndex implements KnowledgeIndexPort {
|
|||
}
|
||||
|
||||
private async syncAllPagesFromDisk(): Promise<void> {
|
||||
const listed = await this.project.fileStore.listFiles('knowledge', true);
|
||||
const listed = await this.project.fileStore.listFiles('wiki', true);
|
||||
const existingPages = this.sqlite.getExistingPages();
|
||||
const pages: SqliteKnowledgeIndexPage[] = [];
|
||||
for (const file of listed.files.filter((entry) => entry.endsWith('.md'))) {
|
||||
|
|
@ -412,7 +412,7 @@ class LocalKnowledgeIndex implements KnowledgeIndexPort {
|
|||
if (!parsedPath) {
|
||||
continue;
|
||||
}
|
||||
const path = `knowledge/${file}`;
|
||||
const path = `wiki/${file}`;
|
||||
const raw = await this.project.fileStore.readFile(path);
|
||||
const parsed = parseWiki(raw.content);
|
||||
const tags = parseWikiTags(raw.content);
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ function baseScenario(overrides: Partial<MemoryFlowReplayInput> = {}): MemoryFlo
|
|||
{ type: 'raw_snapshot_written', syncId: 'sync-success', rawFileCount: 4 },
|
||||
{ type: 'diff_computed', added: 2, modified: 1, deleted: 0, unchanged: 1 },
|
||||
{ type: 'chunks_planned', chunkCount: 2, workUnitCount: 2, evictionCount: 0 },
|
||||
{ type: 'work_unit_started', unitKey: 'orders', skills: ['knowledge_capture'], stepBudget: 40 },
|
||||
{ type: 'candidate_action', unitKey: 'orders', target: 'wiki', action: 'created', key: 'knowledge/global/orders.md' },
|
||||
{ type: 'work_unit_started', unitKey: 'orders', skills: ['wiki_capture'], stepBudget: 40 },
|
||||
{ type: 'candidate_action', unitKey: 'orders', target: 'wiki', action: 'created', key: 'wiki/global/orders.md' },
|
||||
{ type: 'candidate_action', unitKey: 'orders', target: 'sl', action: 'updated', key: 'warehouse.orders' },
|
||||
{ type: 'work_unit_finished', unitKey: 'orders', status: 'success' },
|
||||
{ type: 'work_unit_started', unitKey: 'revenue', skills: ['knowledge_capture'], stepBudget: 40 },
|
||||
{ type: 'candidate_action', unitKey: 'revenue', target: 'wiki', action: 'updated', key: 'knowledge/global/revenue.md' },
|
||||
{ type: 'work_unit_started', unitKey: 'revenue', skills: ['wiki_capture'], stepBudget: 40 },
|
||||
{ type: 'candidate_action', unitKey: 'revenue', target: 'wiki', action: 'updated', key: 'wiki/global/revenue.md' },
|
||||
{ type: 'work_unit_finished', unitKey: 'revenue', status: 'success' },
|
||||
{ type: 'reconciliation_finished', conflictCount: 0, fallbackCount: 0 },
|
||||
{ type: 'saved', commitSha: 'abc123456789', wikiCount: 2, slCount: 1 }, // pragma: allowlist secret
|
||||
|
|
@ -38,7 +38,7 @@ function baseScenario(overrides: Partial<MemoryFlowReplayInput> = {}): MemoryFlo
|
|||
unitKey: 'orders',
|
||||
target: 'wiki',
|
||||
action: 'created',
|
||||
key: 'knowledge/global/orders.md',
|
||||
key: 'wiki/global/orders.md',
|
||||
summary: 'Captured order definitions',
|
||||
rawFiles: ['models/orders.yml'],
|
||||
status: 'success',
|
||||
|
|
@ -56,7 +56,7 @@ function baseScenario(overrides: Partial<MemoryFlowReplayInput> = {}): MemoryFlo
|
|||
unitKey: 'revenue',
|
||||
target: 'wiki',
|
||||
action: 'updated',
|
||||
key: 'knowledge/global/revenue.md',
|
||||
key: 'wiki/global/revenue.md',
|
||||
summary: 'Updated revenue notes',
|
||||
rawFiles: ['docs/revenue.md'],
|
||||
status: 'success',
|
||||
|
|
@ -66,7 +66,7 @@ function baseScenario(overrides: Partial<MemoryFlowReplayInput> = {}): MemoryFlo
|
|||
{
|
||||
rawPath: 'models/orders.yml',
|
||||
artifactKind: 'wiki',
|
||||
artifactKey: 'knowledge/global/orders.md',
|
||||
artifactKey: 'wiki/global/orders.md',
|
||||
actionType: 'created',
|
||||
},
|
||||
{ rawPath: 'models/orders.yml', artifactKind: 'sl', artifactKey: 'warehouse.orders', actionType: 'updated' },
|
||||
|
|
@ -111,7 +111,7 @@ export function validationRevertScenario(): MemoryFlowReplayInput {
|
|||
{ type: 'raw_snapshot_written', syncId: 'sync-validation', rawFileCount: 1 },
|
||||
{ type: 'diff_computed', added: 1, modified: 0, deleted: 0, unchanged: 0 },
|
||||
{ type: 'chunks_planned', chunkCount: 1, workUnitCount: 1, evictionCount: 0 },
|
||||
{ type: 'work_unit_started', unitKey: 'orders', skills: ['knowledge_capture'], stepBudget: 40 },
|
||||
{ type: 'work_unit_started', unitKey: 'orders', skills: ['wiki_capture'], stepBudget: 40 },
|
||||
{ type: 'candidate_action', unitKey: 'orders', target: 'sl', action: 'updated', key: 'warehouse.orders' },
|
||||
{
|
||||
type: 'work_unit_finished',
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ function reportSnapshot(): IngestReportSnapshot {
|
|||
{
|
||||
rawPath: 'views/orders.view.lkml',
|
||||
artifactKind: 'wiki',
|
||||
artifactKey: 'knowledge/global/orders.md',
|
||||
artifactKey: 'wiki/global/orders.md',
|
||||
actionType: 'wiki_written',
|
||||
},
|
||||
{
|
||||
|
|
@ -115,7 +115,7 @@ function reportSnapshot(): IngestReportSnapshot {
|
|||
rawFiles: ['views/orders.view.lkml'],
|
||||
status: 'success',
|
||||
actions: [
|
||||
{ target: 'wiki', type: 'created', key: 'knowledge/global/orders.md', detail: 'order facts' },
|
||||
{ target: 'wiki', type: 'created', key: 'wiki/global/orders.md', detail: 'order facts' },
|
||||
{ target: 'sl', type: 'updated', key: 'warehouse.orders', detail: 'order measures' },
|
||||
],
|
||||
touchedSlSources: [{ connectionId: 'warehouse', sourceName: 'warehouse.orders' }],
|
||||
|
|
@ -180,7 +180,7 @@ describe('memory-flow event mapping', () => {
|
|||
unitKey: 'orders',
|
||||
target: 'wiki',
|
||||
action: 'created',
|
||||
key: 'knowledge/global/orders.md',
|
||||
key: 'wiki/global/orders.md',
|
||||
});
|
||||
expect(replay.events).toContainEqual({
|
||||
type: 'work_unit_finished',
|
||||
|
|
@ -197,7 +197,7 @@ describe('memory-flow event mapping', () => {
|
|||
unitKey: 'orders',
|
||||
target: 'wiki',
|
||||
action: 'created',
|
||||
key: 'knowledge/global/orders.md',
|
||||
key: 'wiki/global/orders.md',
|
||||
summary: 'order facts',
|
||||
rawFiles: ['views/orders.view.lkml'],
|
||||
status: 'success',
|
||||
|
|
@ -225,7 +225,7 @@ describe('memory-flow event mapping', () => {
|
|||
{
|
||||
rawPath: 'views/orders.view.lkml',
|
||||
artifactKind: 'wiki',
|
||||
artifactKey: 'knowledge/global/orders.md',
|
||||
artifactKey: 'wiki/global/orders.md',
|
||||
actionType: 'wiki_written',
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ function view(): MemoryFlowViewModel {
|
|||
unitKey: 'orders',
|
||||
target: 'wiki',
|
||||
action: 'created',
|
||||
key: 'knowledge/orders.md',
|
||||
key: 'wiki/orders.md',
|
||||
summary: 'order facts',
|
||||
rawFiles: ['orders.yml'],
|
||||
status: 'success',
|
||||
|
|
@ -53,7 +53,7 @@ function view(): MemoryFlowViewModel {
|
|||
{
|
||||
rawPath: 'orders.yml',
|
||||
artifactKind: 'wiki',
|
||||
artifactKey: 'knowledge/orders.md',
|
||||
artifactKey: 'wiki/orders.md',
|
||||
actionType: 'wiki_written',
|
||||
},
|
||||
],
|
||||
|
|
@ -104,8 +104,8 @@ function view(): MemoryFlowViewModel {
|
|||
status: 'complete',
|
||||
headline: '2 candidates',
|
||||
counters: ['1 wiki', '1 SL'],
|
||||
chips: [{ label: 'knowledge/orders.md', status: 'complete' }],
|
||||
details: ['wiki created: knowledge/orders.md', 'sl updated: warehouse.orders'],
|
||||
chips: [{ label: 'wiki/orders.md', status: 'complete' }],
|
||||
details: ['wiki created: wiki/orders.md', 'sl updated: warehouse.orders'],
|
||||
},
|
||||
{
|
||||
id: 'gates',
|
||||
|
|
@ -173,7 +173,7 @@ describe('memory-flow interaction reducer', () => {
|
|||
shouldQuit: false,
|
||||
});
|
||||
expect(selectedMemoryFlowColumn(view(), selected).title).toBe('ACTIONS');
|
||||
expect(selectedMemoryFlowDetails(view(), selected)).toContain('wiki created: knowledge/orders.md');
|
||||
expect(selectedMemoryFlowDetails(view(), selected)).toContain('wiki created: wiki/orders.md');
|
||||
});
|
||||
|
||||
it('selects and clamps a chip directly for mouse-driven renderers', () => {
|
||||
|
|
@ -226,7 +226,7 @@ describe('memory-flow interaction reducer', () => {
|
|||
state = reduceMemoryFlowInteractionState(state, 'tab', view());
|
||||
expect(state.pane).toBe('provenance');
|
||||
expect(selectedMemoryFlowDetails(view(), state)).toContain(
|
||||
'orders.yml -> wiki:knowledge/orders.md (wiki_written)',
|
||||
'orders.yml -> wiki:wiki/orders.md (wiki_written)',
|
||||
);
|
||||
|
||||
state = reduceMemoryFlowInteractionState(state, 'tab', view());
|
||||
|
|
@ -241,7 +241,7 @@ describe('memory-flow interaction reducer', () => {
|
|||
state = reduceMemoryFlowInteractionState(state, 'provenance', view());
|
||||
expect(state.pane).toBe('provenance');
|
||||
expect(selectedMemoryFlowDetails(view(), state)).toContain(
|
||||
'orders.yml -> wiki:knowledge/orders.md (wiki_written)',
|
||||
'orders.yml -> wiki:wiki/orders.md (wiki_written)',
|
||||
);
|
||||
|
||||
state = reduceMemoryFlowInteractionState(state, 'transcript', view());
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ function view(): MemoryFlowViewModel {
|
|||
unitKey: 'orders',
|
||||
target: 'wiki',
|
||||
action: 'created',
|
||||
key: 'knowledge/orders.md',
|
||||
key: 'wiki/orders.md',
|
||||
summary: 'order facts',
|
||||
rawFiles: ['orders.yml'],
|
||||
status: 'success',
|
||||
|
|
@ -46,7 +46,7 @@ function view(): MemoryFlowViewModel {
|
|||
{
|
||||
rawPath: 'orders.yml',
|
||||
artifactKind: 'wiki',
|
||||
artifactKey: 'knowledge/orders.md',
|
||||
artifactKey: 'wiki/orders.md',
|
||||
actionType: 'wiki_written',
|
||||
},
|
||||
],
|
||||
|
|
@ -97,8 +97,8 @@ function view(): MemoryFlowViewModel {
|
|||
status: 'complete',
|
||||
headline: '2 candidates',
|
||||
counters: ['1 wiki', '1 SL'],
|
||||
chips: [{ label: 'knowledge/orders.md', status: 'complete' }],
|
||||
details: ['wiki created: knowledge/orders.md', 'sl updated: warehouse.orders'],
|
||||
chips: [{ label: 'wiki/orders.md', status: 'complete' }],
|
||||
details: ['wiki created: wiki/orders.md', 'sl updated: warehouse.orders'],
|
||||
},
|
||||
{
|
||||
id: 'gates',
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ function view(): MemoryFlowViewModel {
|
|||
status: 'complete',
|
||||
headline: '2 candidates',
|
||||
counters: ['1 wiki', '1 SL'],
|
||||
chips: [{ label: 'knowledge/orders.md', status: 'complete' }],
|
||||
details: ['wiki created: knowledge/orders.md'],
|
||||
chips: [{ label: 'wiki/orders.md', status: 'complete' }],
|
||||
details: ['wiki created: wiki/orders.md'],
|
||||
},
|
||||
{
|
||||
id: 'gates',
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ function snapshot(overrides: Partial<MemoryFlowReplayInput> = {}): MemoryFlowRep
|
|||
{ type: 'raw_snapshot_written', syncId: 'sync-1', rawFileCount: 2 },
|
||||
{ type: 'diff_computed', added: 1, modified: 1, deleted: 0, unchanged: 0 },
|
||||
{ type: 'chunks_planned', chunkCount: 1, workUnitCount: 1, evictionCount: 0 },
|
||||
{ type: 'work_unit_started', unitKey: 'orders', skills: ['knowledge_capture'], stepBudget: 40 },
|
||||
{ type: 'work_unit_started', unitKey: 'orders', skills: ['wiki_capture'], stepBudget: 40 },
|
||||
{ type: 'work_unit_step', unitKey: 'orders', stepIndex: 1, stepBudget: 40 },
|
||||
{ type: 'candidate_action', unitKey: 'orders', target: 'wiki', action: 'created', key: 'knowledge/orders.md' },
|
||||
{ type: 'candidate_action', unitKey: 'orders', target: 'wiki', action: 'created', key: 'wiki/orders.md' },
|
||||
{ type: 'work_unit_finished', unitKey: 'orders', status: 'success' },
|
||||
{ type: 'reconciliation_finished', conflictCount: 0, fallbackCount: 0 },
|
||||
{ type: 'saved', commitSha: 'abc12345', wikiCount: 1, slCount: 0 },
|
||||
|
|
@ -37,7 +37,7 @@ function snapshot(overrides: Partial<MemoryFlowReplayInput> = {}): MemoryFlowRep
|
|||
unitKey: 'orders',
|
||||
target: 'wiki',
|
||||
action: 'created',
|
||||
key: 'knowledge/orders.md',
|
||||
key: 'wiki/orders.md',
|
||||
summary: 'Created orders page',
|
||||
rawFiles: ['orders.md'],
|
||||
status: 'success',
|
||||
|
|
@ -47,7 +47,7 @@ function snapshot(overrides: Partial<MemoryFlowReplayInput> = {}): MemoryFlowRep
|
|||
{
|
||||
rawPath: 'orders.md',
|
||||
artifactKind: 'wiki',
|
||||
artifactKey: 'knowledge/orders.md',
|
||||
artifactKey: 'wiki/orders.md',
|
||||
actionType: 'wiki_written',
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ function replayInput(): MemoryFlowReplayInput {
|
|||
unitKey: 'orders',
|
||||
target: 'wiki',
|
||||
action: 'created',
|
||||
key: 'knowledge/orders.md',
|
||||
key: 'wiki/orders.md',
|
||||
summary: 'order facts',
|
||||
rawFiles: ['orders.yml'],
|
||||
status: 'success',
|
||||
|
|
@ -40,7 +40,7 @@ function replayInput(): MemoryFlowReplayInput {
|
|||
{
|
||||
rawPath: 'orders.yml',
|
||||
artifactKind: 'wiki',
|
||||
artifactKey: 'knowledge/orders.md',
|
||||
artifactKey: 'wiki/orders.md',
|
||||
actionType: 'wiki_written',
|
||||
},
|
||||
],
|
||||
|
|
@ -60,8 +60,8 @@ function replayInput(): MemoryFlowReplayInput {
|
|||
{ type: 'raw_snapshot_written', syncId: 'sync-1', rawFileCount: 2 },
|
||||
{ type: 'diff_computed', added: 1, modified: 1, deleted: 0, unchanged: 3 },
|
||||
{ type: 'chunks_planned', chunkCount: 2, workUnitCount: 2, evictionCount: 0 },
|
||||
{ type: 'work_unit_started', unitKey: 'orders', skills: ['knowledge_capture'], stepBudget: 40 },
|
||||
{ type: 'candidate_action', unitKey: 'orders', target: 'wiki', action: 'created', key: 'knowledge/orders.md' },
|
||||
{ type: 'work_unit_started', unitKey: 'orders', skills: ['wiki_capture'], stepBudget: 40 },
|
||||
{ type: 'candidate_action', unitKey: 'orders', target: 'wiki', action: 'created', key: 'wiki/orders.md' },
|
||||
{ type: 'candidate_action', unitKey: 'orders', target: 'sl', action: 'updated', key: 'warehouse.orders' },
|
||||
{ type: 'work_unit_finished', unitKey: 'orders', status: 'success' },
|
||||
{ type: 'work_unit_finished', unitKey: 'revenue', status: 'failed', reason: 'validation failed' },
|
||||
|
|
@ -122,7 +122,7 @@ describe('buildMemoryFlowViewModel', () => {
|
|||
{
|
||||
rawPath: 'orders.yml',
|
||||
artifactKind: 'wiki',
|
||||
artifactKey: 'knowledge/orders.md',
|
||||
artifactKey: 'wiki/orders.md',
|
||||
actionType: 'wiki_written',
|
||||
},
|
||||
]);
|
||||
|
|
@ -136,7 +136,7 @@ describe('buildMemoryFlowViewModel', () => {
|
|||
},
|
||||
]);
|
||||
expect(view.columns.find((column) => column.id === 'actions')?.details).toContain(
|
||||
'orders wiki created knowledge/orders.md: order facts',
|
||||
'orders wiki created wiki/orders.md: order facts',
|
||||
);
|
||||
expect(view.columns.find((column) => column.id === 'saved')?.details).toContain('Commit: abc12345');
|
||||
expect(view.completionLine).toBe(
|
||||
|
|
@ -159,13 +159,13 @@ describe('buildMemoryFlowViewModel', () => {
|
|||
{ type: 'source_acquired', adapter: 'looker', trigger: 'demo_seeded', fileCount: 7 },
|
||||
{ type: 'source_acquired', adapter: 'notion', trigger: 'demo_seeded', fileCount: 8 },
|
||||
{ type: 'chunks_planned', chunkCount: 1, workUnitCount: 1, evictionCount: 0 },
|
||||
{ type: 'work_unit_started', unitKey: 'revenue-and-contracts', skills: ['knowledge_capture'], stepBudget: 40 },
|
||||
{ type: 'work_unit_started', unitKey: 'revenue-and-contracts', skills: ['wiki_capture'], stepBudget: 40 },
|
||||
{
|
||||
type: 'candidate_action',
|
||||
unitKey: 'revenue-and-contracts',
|
||||
target: 'wiki',
|
||||
action: 'created',
|
||||
key: 'knowledge/global/arr-contract-first.md',
|
||||
key: 'wiki/global/arr-contract-first.md',
|
||||
},
|
||||
{ type: 'work_unit_finished', unitKey: 'revenue-and-contracts', status: 'success' },
|
||||
{ type: 'reconciliation_finished', conflictCount: 0, fallbackCount: 0 },
|
||||
|
|
@ -376,7 +376,7 @@ describe('buildMemoryFlowViewModel', () => {
|
|||
{ type: 'raw_snapshot_written', syncId: 'sync-errors', rawFileCount: 2 },
|
||||
{ type: 'diff_computed', added: 2, modified: 0, deleted: 0, unchanged: 0 },
|
||||
{ type: 'chunks_planned', chunkCount: 1, workUnitCount: 1, evictionCount: 0 },
|
||||
{ type: 'work_unit_started', unitKey: 'orders', skills: ['knowledge_capture'], stepBudget: 40 },
|
||||
{ type: 'work_unit_started', unitKey: 'orders', skills: ['wiki_capture'], stepBudget: 40 },
|
||||
{ type: 'candidate_action', unitKey: 'orders', target: 'sl', action: 'updated', key: 'warehouse.orders' },
|
||||
{
|
||||
type: 'work_unit_finished',
|
||||
|
|
@ -402,7 +402,7 @@ describe('buildMemoryFlowViewModel', () => {
|
|||
events: [
|
||||
{ type: 'source_acquired', adapter: 'metricflow', trigger: 'manual_resync', fileCount: 1 },
|
||||
{ type: 'chunks_planned', chunkCount: 1, workUnitCount: 1, evictionCount: 0 },
|
||||
{ type: 'work_unit_started', unitKey: 'docs', skills: ['knowledge_capture'], stepBudget: 40 },
|
||||
{ type: 'work_unit_started', unitKey: 'docs', skills: ['wiki_capture'], stepBudget: 40 },
|
||||
{ type: 'work_unit_finished', unitKey: 'docs', status: 'failed', reason: 'agent step budget exhausted' },
|
||||
],
|
||||
plannedWorkUnits: [{ unitKey: 'docs', rawFiles: ['docs.md'], peerFileCount: 0, dependencyCount: 0 }],
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ function validReportSnapshot() {
|
|||
rawFiles: ['cards/1.json', 'cards/2.json'],
|
||||
status: 'success',
|
||||
actions: [
|
||||
{ target: 'wiki', type: 'created', key: 'knowledge/global/revenue.md', detail: 'Revenue overview' },
|
||||
{ target: 'wiki', type: 'created', key: 'wiki/global/revenue.md', detail: 'Revenue overview' },
|
||||
{ target: 'sl', type: 'updated', key: 'warehouse.orders', detail: 'Added order amount measure' },
|
||||
],
|
||||
touchedSlSources: ['warehouse.orders'],
|
||||
|
|
@ -38,7 +38,7 @@ function validReportSnapshot() {
|
|||
{
|
||||
rawPath: 'cards/1.json',
|
||||
artifactKind: 'wiki',
|
||||
artifactKey: 'knowledge/global/revenue.md',
|
||||
artifactKey: 'wiki/global/revenue.md',
|
||||
actionType: 'wiki_written',
|
||||
},
|
||||
],
|
||||
|
|
@ -48,7 +48,7 @@ function validReportSnapshot() {
|
|||
path: 'tool-transcripts/cards.jsonl',
|
||||
toolCallCount: 3,
|
||||
errorCount: 0,
|
||||
toolNames: ['knowledge_capture'],
|
||||
toolNames: ['wiki_capture'],
|
||||
},
|
||||
],
|
||||
reconciliationActions: [],
|
||||
|
|
@ -90,7 +90,7 @@ describe('parseIngestReportSnapshot', () => {
|
|||
{
|
||||
target: 'wiki',
|
||||
type: 'created',
|
||||
key: 'knowledge/global/revenue.md',
|
||||
key: 'wiki/global/revenue.md',
|
||||
detail: 'Revenue overview',
|
||||
targetConnectionId: null,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ describe('SqliteBundleIngestStore', () => {
|
|||
rawPath: 'pages/revenue.md',
|
||||
rawContentHash: 'hash-old',
|
||||
artifactKind: 'wiki',
|
||||
artifactKey: 'knowledge/global/revenue.md',
|
||||
artifactKey: 'wiki/global/revenue.md',
|
||||
artifactContentHash: null,
|
||||
actionType: 'wiki_written',
|
||||
},
|
||||
|
|
@ -191,7 +191,7 @@ describe('SqliteBundleIngestStore', () => {
|
|||
rawPath: 'pages/revenue.md',
|
||||
rawContentHash: 'hash-new',
|
||||
artifactKind: 'wiki',
|
||||
artifactKey: 'knowledge/global/revenue.md',
|
||||
artifactKey: 'wiki/global/revenue.md',
|
||||
artifactContentHash: 'artifact-hash-new',
|
||||
actionType: 'wiki_written',
|
||||
},
|
||||
|
|
@ -234,7 +234,7 @@ describe('SqliteBundleIngestStore', () => {
|
|||
sync_id: 'sync-new',
|
||||
raw_content_hash: 'hash-new',
|
||||
artifact_kind: 'wiki',
|
||||
artifact_key: 'knowledge/global/revenue.md',
|
||||
artifact_key: 'wiki/global/revenue.md',
|
||||
action_type: 'wiki_written',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
|
|
@ -381,7 +381,7 @@ describe('SqliteBundleIngestStore', () => {
|
|||
rawPath: 'pages/success/page.md',
|
||||
rawContentHash: 'hash-success',
|
||||
artifactKind: 'wiki',
|
||||
artifactKey: 'knowledge/notion/success.md',
|
||||
artifactKey: 'wiki/notion/success.md',
|
||||
artifactContentHash: 'artifact-success',
|
||||
actionType: 'wiki_written',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ export async function repairWikiSlRefs(input: {
|
|||
warnings: [...warnings, 'Skipped wiki sl_refs repair: config service cannot list wiki files.'],
|
||||
};
|
||||
}
|
||||
const listed = await listFiles('knowledge', true);
|
||||
const listed = await listFiles('wiki', true);
|
||||
const repairs: WikiSlRefRepair[] = [];
|
||||
|
||||
for (const file of listed.files.sort()) {
|
||||
|
|
|
|||
|
|
@ -208,10 +208,10 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void
|
|||
const knowledge = ports.knowledge;
|
||||
registerParsedTool(
|
||||
server,
|
||||
'knowledge_search',
|
||||
'wiki_search',
|
||||
{
|
||||
title: 'Knowledge Search',
|
||||
description: 'Search KTX knowledge pages and return ranked summaries.',
|
||||
title: 'Wiki Search',
|
||||
description: 'Search KTX wiki pages and return ranked summaries.',
|
||||
inputSchema: knowledgeSearchSchema.shape,
|
||||
},
|
||||
knowledgeSearchSchema,
|
||||
|
|
@ -227,25 +227,25 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void
|
|||
|
||||
registerParsedTool(
|
||||
server,
|
||||
'knowledge_read',
|
||||
'wiki_read',
|
||||
{
|
||||
title: 'Knowledge Read',
|
||||
description: 'Read a KTX knowledge page by key.',
|
||||
title: 'Wiki Read',
|
||||
description: 'Read a KTX wiki page by key.',
|
||||
inputSchema: knowledgeReadSchema.shape,
|
||||
},
|
||||
knowledgeReadSchema,
|
||||
async (input) => {
|
||||
const page = await knowledge.read({ userId: userContext.userId, key: input.key });
|
||||
return page ? jsonToolResult(page) : jsonErrorToolResult(`Knowledge page "${input.key}" was not found.`);
|
||||
return page ? jsonToolResult(page) : jsonErrorToolResult(`Wiki page "${input.key}" was not found.`);
|
||||
},
|
||||
);
|
||||
|
||||
registerParsedTool(
|
||||
server,
|
||||
'knowledge_write',
|
||||
'wiki_write',
|
||||
{
|
||||
title: 'Knowledge Write',
|
||||
description: 'Create or replace a KTX knowledge page and its SL references.',
|
||||
title: 'Wiki Write',
|
||||
description: 'Create or replace a KTX wiki page and its SL references.',
|
||||
inputSchema: knowledgeWriteSchema.shape,
|
||||
},
|
||||
knowledgeWriteSchema,
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('writes, reads, and searches global knowledge pages', async () => {
|
||||
it('writes, reads, and searches global wiki pages', async () => {
|
||||
const project = await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
const ports = createLocalProjectMcpContextPorts(project);
|
||||
|
||||
|
|
@ -372,7 +372,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
|||
results: [
|
||||
expect.objectContaining({
|
||||
key: 'revenue',
|
||||
path: 'knowledge/global/revenue.md',
|
||||
path: 'wiki/global/revenue.md',
|
||||
scope: 'GLOBAL',
|
||||
summary: 'Revenue definition',
|
||||
score: expect.any(Number),
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ describe('createKtxMcpServer', () => {
|
|||
captured: { wiki: ['revenue'], sl: [], xrefs: [] },
|
||||
error: null,
|
||||
commitHash: 'abc123',
|
||||
skillsLoaded: ['knowledge_capture'],
|
||||
skillsLoaded: ['wiki_capture'],
|
||||
signalDetected: true,
|
||||
}),
|
||||
};
|
||||
|
|
@ -123,7 +123,7 @@ describe('createKtxMcpServer', () => {
|
|||
captured: { wiki: ['revenue'], sl: [], xrefs: [] },
|
||||
error: null,
|
||||
commitHash: 'abc123',
|
||||
skillsLoaded: ['knowledge_capture'],
|
||||
skillsLoaded: ['wiki_capture'],
|
||||
signalDetected: true,
|
||||
},
|
||||
null,
|
||||
|
|
@ -139,7 +139,7 @@ describe('createKtxMcpServer', () => {
|
|||
captured: { wiki: ['revenue'], sl: [], xrefs: [] },
|
||||
error: null,
|
||||
commitHash: 'abc123',
|
||||
skillsLoaded: ['knowledge_capture'],
|
||||
skillsLoaded: ['wiki_capture'],
|
||||
signalDetected: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -175,7 +175,7 @@ describe('createKtxMcpServer', () => {
|
|||
}: {
|
||||
toolSet: Record<string, { execute: (input: unknown, options?: { toolCallId?: string }) => Promise<unknown> }>;
|
||||
}) => {
|
||||
await toolSet.load_skill.execute({ name: 'knowledge_capture' });
|
||||
await toolSet.load_skill.execute({ name: 'wiki_capture' });
|
||||
await toolSet.wiki_write.execute(
|
||||
{
|
||||
key: 'arr',
|
||||
|
|
@ -220,7 +220,7 @@ describe('createKtxMcpServer', () => {
|
|||
});
|
||||
await expect(access(join(project.projectDir, '.ktx/db.sqlite'))).resolves.toBeUndefined();
|
||||
await expect(access(join(project.projectDir, '.ktx/memory-runs/memory-run-mcp.json'))).rejects.toThrow();
|
||||
await expect(readFile(join(project.projectDir, 'knowledge/global/arr.md'), 'utf-8')).resolves.toContain(
|
||||
await expect(readFile(join(project.projectDir, 'wiki/global/arr.md'), 'utf-8')).resolves.toContain(
|
||||
'ARR means annual recurring revenue.',
|
||||
);
|
||||
} finally {
|
||||
|
|
@ -257,7 +257,7 @@ describe('createKtxMcpServer', () => {
|
|||
results: [
|
||||
{
|
||||
key: 'revenue',
|
||||
path: 'knowledge/global/revenue.md',
|
||||
path: 'wiki/global/revenue.md',
|
||||
scope: 'GLOBAL',
|
||||
summary: 'Paid order value',
|
||||
score: 0.42,
|
||||
|
|
@ -519,9 +519,6 @@ describe('createKtxMcpServer', () => {
|
|||
'ingest_report',
|
||||
'ingest_status',
|
||||
'ingest_trigger',
|
||||
'knowledge_read',
|
||||
'knowledge_search',
|
||||
'knowledge_write',
|
||||
'memory_capture',
|
||||
'memory_capture_status',
|
||||
'scan_list_artifacts',
|
||||
|
|
@ -534,6 +531,9 @@ describe('createKtxMcpServer', () => {
|
|||
'sl_read_source',
|
||||
'sl_validate',
|
||||
'sl_write_source',
|
||||
'wiki_read',
|
||||
'wiki_search',
|
||||
'wiki_write',
|
||||
]);
|
||||
|
||||
await expect(getTool(fake.tools, 'connection_list').handler({})).resolves.toEqual({
|
||||
|
|
@ -595,20 +595,20 @@ describe('createKtxMcpServer', () => {
|
|||
});
|
||||
expect(contextTools.connections?.test).toHaveBeenCalledWith({ connectionId: 'warehouse' });
|
||||
|
||||
await getTool(fake.tools, 'knowledge_search').handler({ query: 'revenue', limit: 5 });
|
||||
await getTool(fake.tools, 'wiki_search').handler({ query: 'revenue', limit: 5 });
|
||||
expect(contextTools.knowledge?.search).toHaveBeenCalledWith({
|
||||
userId: 'mcp-user',
|
||||
query: 'revenue',
|
||||
limit: 5,
|
||||
});
|
||||
|
||||
await getTool(fake.tools, 'knowledge_read').handler({ key: 'revenue' });
|
||||
await getTool(fake.tools, 'wiki_read').handler({ key: 'revenue' });
|
||||
expect(contextTools.knowledge?.read).toHaveBeenCalledWith({
|
||||
userId: 'mcp-user',
|
||||
key: 'revenue',
|
||||
});
|
||||
|
||||
await getTool(fake.tools, 'knowledge_write').handler({
|
||||
await getTool(fake.tools, 'wiki_write').handler({
|
||||
key: 'revenue',
|
||||
summary: 'Paid order value',
|
||||
content: '# Revenue',
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const LOOKML_STRUCTURAL_PATTERN = /^\s*(view|explore|model|include)\s*:\s*[\w"`]
|
|||
const LOOKML_FIELDS_PATTERN =
|
||||
/^\s*(measure|dimension|dimension_group|sql_table_name|derived_table|sql_always_where|drill_fields|join)\s*:/m;
|
||||
|
||||
export const DEFAULT_SKILL_NAMES = ['sl', 'sl_capture', 'knowledge_capture'] as const;
|
||||
export const DEFAULT_SKILL_NAMES = ['sl', 'sl_capture', 'wiki_capture'] as const;
|
||||
|
||||
export function detectCaptureSignals(input: MemoryAgentInput): CaptureSignals {
|
||||
const userMessage = input.userMessage?.trim() ?? '';
|
||||
|
|
@ -56,7 +56,7 @@ export function buildRequiredSkillsBlock(signals: CaptureSignals): string {
|
|||
const reason =
|
||||
signals.reasons.find((r) => r.includes('definition keyword') || r.includes('definition table')) ??
|
||||
'wiki signal detected';
|
||||
required.push({ name: 'knowledge_capture', reason });
|
||||
required.push({ name: 'wiki_capture', reason });
|
||||
}
|
||||
if (signals.sl) {
|
||||
const reason =
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ describe('LocalMemoryRunStore', () => {
|
|||
await store.markDone('memory-run-1', {
|
||||
signalDetected: true,
|
||||
actions: [{ target: 'wiki', type: 'created', key: 'revenue', detail: 'Revenue definition' }],
|
||||
skillsLoaded: ['knowledge_capture'],
|
||||
skillsLoaded: ['wiki_capture'],
|
||||
commitHash: 'abc123',
|
||||
});
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ describe('LocalMemoryRunStore', () => {
|
|||
chatId: 'chat-1',
|
||||
outputSummary: {
|
||||
actions: [{ target: 'wiki', type: 'created', key: 'revenue', detail: 'Revenue definition' }],
|
||||
skillsLoaded: ['knowledge_capture'],
|
||||
skillsLoaded: ['wiki_capture'],
|
||||
signalDetected: true,
|
||||
commitHash: 'abc123',
|
||||
},
|
||||
|
|
@ -96,7 +96,7 @@ describe('createLocalProjectMemoryCapture', () => {
|
|||
}: {
|
||||
toolSet: Record<string, { execute: (input: unknown, options?: { toolCallId?: string }) => Promise<unknown> }>;
|
||||
}) => {
|
||||
await toolSet.load_skill.execute({ name: 'knowledge_capture' });
|
||||
await toolSet.load_skill.execute({ name: 'wiki_capture' });
|
||||
await toolSet.wiki_write.execute(
|
||||
{
|
||||
key: 'revenue',
|
||||
|
|
@ -134,11 +134,11 @@ describe('createLocalProjectMemoryCapture', () => {
|
|||
status: 'done',
|
||||
done: true,
|
||||
captured: { wiki: ['revenue'], sl: [], xrefs: [] },
|
||||
skillsLoaded: ['knowledge_capture'],
|
||||
skillsLoaded: ['wiki_capture'],
|
||||
signalDetected: true,
|
||||
});
|
||||
|
||||
await expect(readFile(join(project.projectDir, 'knowledge/global/revenue.md'), 'utf-8')).resolves.toContain(
|
||||
await expect(readFile(join(project.projectDir, 'wiki/global/revenue.md'), 'utf-8')).resolves.toContain(
|
||||
'Revenue means paid order value net of refunds.',
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -222,8 +222,8 @@ class LocalKnowledgeIndex implements KnowledgeIndexPort {
|
|||
async listPagesForUser(userId: string) {
|
||||
const pages: KnowledgeIndexPageListing[] = [];
|
||||
for (const scope of [
|
||||
{ scope: 'GLOBAL', scopeId: null, dir: 'knowledge/global' },
|
||||
{ scope: 'USER', scopeId: userId, dir: `knowledge/user/${userId}` },
|
||||
{ scope: 'GLOBAL', scopeId: null, dir: 'wiki/global' },
|
||||
{ scope: 'USER', scopeId: userId, dir: `wiki/user/${userId}` },
|
||||
]) {
|
||||
const listed = await this.project.fileStore.listFiles(scope.dir, true);
|
||||
for (const file of listed.files.filter((entry) => entry.endsWith('.md'))) {
|
||||
|
|
@ -262,7 +262,7 @@ class LocalKnowledgeIndex implements KnowledgeIndexPort {
|
|||
}
|
||||
|
||||
private pagePath(scope: string, scopeId: string | null, pageKey: string): string {
|
||||
return scope === 'GLOBAL' ? `knowledge/global/${pageKey}.md` : `knowledge/user/${scopeId}/${pageKey}.md`;
|
||||
return scope === 'GLOBAL' ? `wiki/global/${pageKey}.md` : `wiki/user/${scopeId}/${pageKey}.md`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -318,7 +318,7 @@ export class MemoryAgentService {
|
|||
}
|
||||
|
||||
const signalsActedOn: string[] = [];
|
||||
if (signals.knowledge && skillsLoaded.includes('knowledge_capture')) {
|
||||
if (signals.knowledge && skillsLoaded.includes('wiki_capture')) {
|
||||
signalsActedOn.push('knowledge');
|
||||
}
|
||||
if (signals.sl && skillsLoaded.includes('sl')) {
|
||||
|
|
@ -580,12 +580,12 @@ export class MemoryAgentService {
|
|||
private async buildWikiIndex(userId: string, userScopedEnabled: boolean): Promise<string> {
|
||||
const pages = await this.deps.knowledgeIndex.listPagesForUser(userId);
|
||||
if (pages.length === 0) {
|
||||
return '(empty — no knowledge pages exist yet)';
|
||||
return '(empty — no wiki pages exist yet)';
|
||||
}
|
||||
|
||||
const formatEntry = (p: { page_key: string; summary: string }) => `- ${p.page_key}: ${p.summary}`;
|
||||
if (!userScopedEnabled) {
|
||||
return `## Knowledge Pages\n${pages.map(formatEntry).join('\n')}`;
|
||||
return `## Wiki Pages\n${pages.map(formatEntry).join('\n')}`;
|
||||
}
|
||||
|
||||
const globalEntries: string[] = [];
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ describe('MemoryCaptureService', () => {
|
|||
const result: MemoryAgentResult = {
|
||||
signalDetected: true,
|
||||
actions: [{ target: 'wiki', type: 'created', key: 'revenue', detail: 'captured revenue definition' }],
|
||||
skillsLoaded: ['knowledge_capture'],
|
||||
skillsLoaded: ['wiki_capture'],
|
||||
commitHash: 'abc123',
|
||||
};
|
||||
const { capture, store, ingest, run } = buildService();
|
||||
|
|
@ -136,7 +136,7 @@ describe('MemoryCaptureService', () => {
|
|||
},
|
||||
error: null,
|
||||
commitHash: 'abc123',
|
||||
skillsLoaded: ['knowledge_capture'],
|
||||
skillsLoaded: ['wiki_capture'],
|
||||
signalDetected: true,
|
||||
});
|
||||
expect(store.rows.get('run-1')?.inputHash).toHaveLength(64);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const promptsDir = fileURLToPath(new URL('../../prompts', import.meta.url));
|
|||
const skillsDir = fileURLToPath(new URL('../../skills', import.meta.url));
|
||||
const memorySourceTypes: MemoryAgentSourceType[] = ['research', 'external_ingest', 'backfill'];
|
||||
const expectedSkillHeadings: Record<string, string> = {
|
||||
knowledge_capture: '# Knowledge Capture',
|
||||
wiki_capture: '# Wiki Capture',
|
||||
sl: '# Semantic Layer',
|
||||
sl_capture: '# Semantic Layer',
|
||||
};
|
||||
|
|
@ -33,7 +33,7 @@ const verificationWriterSkills = [
|
|||
'live_database_ingest',
|
||||
'historic_sql_table_digest',
|
||||
'historic_sql_patterns',
|
||||
'knowledge_capture',
|
||||
'wiki_capture',
|
||||
'sl_capture',
|
||||
] as const;
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ describe('memory runtime assets', () => {
|
|||
const registry = new SkillsRegistryService({ skillsDir });
|
||||
const skills = await registry.listSkills([...DEFAULT_SKILL_NAMES], 'memory_agent');
|
||||
|
||||
expect(skills.map((skill) => skill.name).sort()).toEqual(['knowledge_capture', 'sl', 'sl_capture']);
|
||||
expect(skills.map((skill) => skill.name).sort()).toEqual(['wiki_capture', 'sl', 'sl_capture']);
|
||||
|
||||
for (const skill of skills) {
|
||||
const body = await readFile(join(skill.path, 'SKILL.md'), 'utf-8');
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ describe('KTX project config', () => {
|
|||
run_research: {
|
||||
enabled: false,
|
||||
max_iterations: 20,
|
||||
default_toolset: ['sl_query', 'knowledge_search', 'sl_read_source'],
|
||||
default_toolset: ['sl_query', 'wiki_search', 'sl_read_source'],
|
||||
},
|
||||
},
|
||||
memory: {
|
||||
|
|
|
|||
|
|
@ -423,7 +423,7 @@ export function buildDefaultKtxProjectConfig(projectName = 'ktx-project'): KtxPr
|
|||
run_research: {
|
||||
enabled: false,
|
||||
max_iterations: 20,
|
||||
default_toolset: ['sl_query', 'knowledge_search', 'sl_read_source'],
|
||||
default_toolset: ['sl_query', 'wiki_search', 'sl_read_source'],
|
||||
},
|
||||
},
|
||||
memory: {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ describe('LocalGitFileStore', () => {
|
|||
|
||||
it('writes, commits, and reads a project file', async () => {
|
||||
const write = await store.writeFile(
|
||||
'knowledge/global/revenue.md',
|
||||
'wiki/global/revenue.md',
|
||||
'# Revenue\n',
|
||||
'Agent',
|
||||
'agent@example.com',
|
||||
|
|
@ -40,20 +40,20 @@ describe('LocalGitFileStore', () => {
|
|||
);
|
||||
|
||||
expect(write.commitHash).toMatch(/^[0-9a-f]{40}$/);
|
||||
await expect(readFile(join(tempDir, 'knowledge/global/revenue.md'), 'utf-8')).resolves.toBe('# Revenue\n');
|
||||
await expect(store.readFile('knowledge/global/revenue.md')).resolves.toMatchObject({
|
||||
await expect(readFile(join(tempDir, 'wiki/global/revenue.md'), 'utf-8')).resolves.toBe('# Revenue\n');
|
||||
await expect(store.readFile('wiki/global/revenue.md')).resolves.toMatchObject({
|
||||
content: '# Revenue\n',
|
||||
});
|
||||
});
|
||||
|
||||
it('lists files recursively and can strip the requested prefix', async () => {
|
||||
await store.writeFile('knowledge/global/a.md', 'a', 'Agent', 'agent@example.com', 'Add a');
|
||||
await store.writeFile('knowledge/global/nested/b.md', 'b', 'Agent', 'agent@example.com', 'Add b');
|
||||
await store.writeFile('wiki/global/a.md', 'a', 'Agent', 'agent@example.com', 'Add a');
|
||||
await store.writeFile('wiki/global/nested/b.md', 'b', 'Agent', 'agent@example.com', 'Add b');
|
||||
|
||||
await expect(store.listFiles('knowledge')).resolves.toEqual({
|
||||
files: ['knowledge/global/a.md', 'knowledge/global/nested/b.md'],
|
||||
await expect(store.listFiles('wiki')).resolves.toEqual({
|
||||
files: ['wiki/global/a.md', 'wiki/global/nested/b.md'],
|
||||
});
|
||||
await expect(store.listFiles('knowledge/global', true)).resolves.toEqual({
|
||||
await expect(store.listFiles('wiki/global', true)).resolves.toEqual({
|
||||
files: ['a.md', 'nested/b.md'],
|
||||
});
|
||||
});
|
||||
|
|
@ -77,10 +77,10 @@ describe('LocalGitFileStore', () => {
|
|||
});
|
||||
|
||||
it('exposes Git history for a file', async () => {
|
||||
await store.writeFile('knowledge/global/history.md', 'v1', 'Agent', 'agent@example.com', 'Add history');
|
||||
await store.writeFile('knowledge/global/history.md', 'v2', 'Agent', 'agent@example.com', 'Update history');
|
||||
await store.writeFile('wiki/global/history.md', 'v1', 'Agent', 'agent@example.com', 'Add history');
|
||||
await store.writeFile('wiki/global/history.md', 'v2', 'Agent', 'agent@example.com', 'Update history');
|
||||
|
||||
const history = await store.getFileHistory('knowledge/global/history.md');
|
||||
const history = await store.getFileHistory('wiki/global/history.md');
|
||||
|
||||
expect(Array.isArray(history)).toBe(true);
|
||||
expect(history[0]).toMatchObject({ message: 'Update history' });
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ describe('KTX local project runtime', () => {
|
|||
expect(gitignore).toContain('secrets/');
|
||||
expect(gitignore).toContain('setup/');
|
||||
expect(gitignore).toContain('agents/');
|
||||
await expect(stat(join(projectDir, 'knowledge/global/.gitkeep'))).resolves.toBeDefined();
|
||||
await expect(stat(join(projectDir, 'wiki/global/.gitkeep'))).resolves.toBeDefined();
|
||||
await expect(stat(join(projectDir, 'semantic-layer/.gitkeep'))).resolves.toBeDefined();
|
||||
await expect(stat(join(projectDir, '_schema/.gitkeep'))).rejects.toMatchObject({ code: 'ENOENT' });
|
||||
await expect(stat(join(projectDir, 'raw-sources/.gitkeep'))).resolves.toBeDefined();
|
||||
|
|
@ -50,7 +50,7 @@ describe('KTX local project runtime', () => {
|
|||
|
||||
const loaded = await loadKtxProject({ projectDir });
|
||||
await loaded.fileStore.writeFile(
|
||||
'knowledge/global/revenue.md',
|
||||
'wiki/global/revenue.md',
|
||||
'# Revenue\n',
|
||||
'Agent',
|
||||
'agent@example.com',
|
||||
|
|
@ -58,7 +58,7 @@ describe('KTX local project runtime', () => {
|
|||
);
|
||||
|
||||
expect(loaded.config.project).toBe('warehouse');
|
||||
await expect(loaded.fileStore.readFile('knowledge/global/revenue.md')).resolves.toMatchObject({
|
||||
await expect(loaded.fileStore.readFile('wiki/global/revenue.md')).resolves.toMatchObject({
|
||||
content: '# Revenue\n',
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const TRACKED_SCAFFOLD_FILES: Array<{ path: string; content: string }> = [
|
|||
},
|
||||
{ path: '.ktx/prompts/.gitkeep', content: '' },
|
||||
{ path: '.ktx/skills/.gitkeep', content: '' },
|
||||
{ path: 'knowledge/global/.gitkeep', content: '' },
|
||||
{ path: 'wiki/global/.gitkeep', content: '' },
|
||||
{ path: 'semantic-layer/.gitkeep', content: '' },
|
||||
{ path: 'raw-sources/.gitkeep', content: '' },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -64,14 +64,14 @@ describe('SkillsRegistryService', () => {
|
|||
|
||||
it('discovers valid skills and skips invalid ones', async () => {
|
||||
await writeSkill('sl', '---\nname: sl\ndescription: Semantic layer.\n---\n\n# SL');
|
||||
await writeSkill('knowledge_capture', '---\nname: knowledge_capture\ndescription: Wiki capture.\n---\n\n# KC');
|
||||
await writeSkill('wiki_capture', '---\nname: wiki_capture\ndescription: Wiki capture.\n---\n\n# KC');
|
||||
await writeSkill('broken', '# no frontmatter at all');
|
||||
await mkdir(join(tempDir, 'not_a_skill'), { recursive: true });
|
||||
|
||||
const catalog = await service.discoverSkills(tempDir);
|
||||
expect(catalog.size).toBe(2);
|
||||
expect(catalog.get('sl')?.name).toBe('sl');
|
||||
expect(catalog.get('knowledge_capture')?.description).toContain('Wiki capture');
|
||||
expect(catalog.get('wiki_capture')?.description).toContain('Wiki capture');
|
||||
expect(catalog.has('broken')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -80,10 +80,10 @@ describe('SkillsRegistryService', () => {
|
|||
it('formats bullet list with name and description', () => {
|
||||
const output = service.buildSkillsPrompt([
|
||||
{ name: 'sl', description: 'Semantic layer.', path: '/tmp/sl' },
|
||||
{ name: 'knowledge_capture', description: 'Wiki capture.', path: '/tmp/kc' },
|
||||
{ name: 'wiki_capture', description: 'Wiki capture.', path: '/tmp/kc' },
|
||||
]);
|
||||
expect(output).toContain('- sl: Semantic layer.');
|
||||
expect(output).toContain('- knowledge_capture: Wiki capture.');
|
||||
expect(output).toContain('- wiki_capture: Wiki capture.');
|
||||
expect(output).toContain('Use the `load_skill` tool');
|
||||
});
|
||||
|
||||
|
|
@ -144,8 +144,8 @@ describe('SkillsRegistryService', () => {
|
|||
'---\nname: sl_capture\ndescription: Memory-only capture skill.\ncallers: [memory_agent]\n---\n\n# Capture',
|
||||
);
|
||||
await writeSkill(
|
||||
'knowledge_capture',
|
||||
'---\nname: knowledge_capture\ndescription: Wiki capture.\ncallers: [memory_agent]\n---\n\n# KC',
|
||||
'wiki_capture',
|
||||
'---\nname: wiki_capture\ndescription: Wiki capture.\ncallers: [memory_agent]\n---\n\n# KC',
|
||||
);
|
||||
service = new SkillsRegistryService({ skillsDir: tempDir });
|
||||
});
|
||||
|
|
@ -157,7 +157,7 @@ describe('SkillsRegistryService', () => {
|
|||
|
||||
it('memory_agent caller sees memory-only and open skills', async () => {
|
||||
const skills = await service.listSkills('memory_agent');
|
||||
expect(skills.map((skill) => skill.name).sort()).toEqual(['knowledge_capture', 'sl', 'sl_capture']);
|
||||
expect(skills.map((skill) => skill.name).sort()).toEqual(['wiki_capture', 'sl', 'sl_capture']);
|
||||
});
|
||||
|
||||
it('listSkills with names and caller intersects both filters', async () => {
|
||||
|
|
@ -185,26 +185,26 @@ describe('SkillsRegistryService', () => {
|
|||
it('discovers skills from additional directories when the primary directory misses', async () => {
|
||||
const extraDir = await mkdtemp(join(tmpdir(), 'skills-registry-extra-'));
|
||||
try {
|
||||
await mkdir(join(extraDir, 'knowledge_capture'), { recursive: true });
|
||||
await mkdir(join(extraDir, 'wiki_capture'), { recursive: true });
|
||||
await writeFile(
|
||||
join(extraDir, 'knowledge_capture', 'SKILL.md'),
|
||||
join(extraDir, 'wiki_capture', 'SKILL.md'),
|
||||
[
|
||||
'---',
|
||||
'name: knowledge_capture',
|
||||
'name: wiki_capture',
|
||||
'description: Packaged knowledge capture skill.',
|
||||
'callers: [memory_agent]',
|
||||
'---',
|
||||
'',
|
||||
'# Knowledge Capture',
|
||||
'# Wiki Capture',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
service = new SkillsRegistryService({ skillsDir: tempDir, additionalSkillDirs: [extraDir] });
|
||||
|
||||
const skills = await service.listSkills(['knowledge_capture'], 'memory_agent');
|
||||
const skills = await service.listSkills(['wiki_capture'], 'memory_agent');
|
||||
|
||||
expect(skills.map((skill) => skill.name)).toEqual(['knowledge_capture']);
|
||||
expect(skills[0]?.path).toBe(join(extraDir, 'knowledge_capture'));
|
||||
expect(skills.map((skill) => skill.name)).toEqual(['wiki_capture']);
|
||||
expect(skills[0]?.path).toBe(join(extraDir, 'wiki_capture'));
|
||||
} finally {
|
||||
await rm(extraDir, { recursive: true, force: true });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ export class SkillsRegistryService {
|
|||
const list = skills.map((skill) => `- ${skill.name}: ${skill.description}`).join('\n');
|
||||
const captureNote =
|
||||
caller === 'research'
|
||||
? '\n\nKnowledge pages and semantic-layer sources are captured automatically by a post-turn memory agent. Focus on answering, not on saving. Use `knowledge_read`/`knowledge_search` and `sl_read_source` to consult what already exists; the memory agent will write any new conventions or measures the turn surfaces.'
|
||||
? '\n\nWiki pages and semantic-layer sources are captured automatically by a post-turn memory agent. Focus on answering, not on saving. Use `wiki_read`/`wiki_search` and `sl_read_source` to consult what already exists; the memory agent will write any new conventions or measures the turn surfaces.'
|
||||
: '';
|
||||
return `\n## Skills\n\nUse the \`load_skill\` tool to load a skill when the task benefits from specialized instructions.${captureNote}\n\nAvailable skills:\n${list}\n`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,9 +84,9 @@ describe('KnowledgeWikiService.syncFromCommit', () => {
|
|||
const { service, pagesRepository, gitService } = makeService();
|
||||
|
||||
gitService.diffNameStatus.mockResolvedValue([
|
||||
{ status: 'A', path: 'knowledge/global/new-page.md' },
|
||||
{ status: 'M', path: 'knowledge/global/changed-page.md' },
|
||||
{ status: 'D', path: 'knowledge/global/gone-page.md' },
|
||||
{ status: 'A', path: 'wiki/global/new-page.md' },
|
||||
{ status: 'M', path: 'wiki/global/changed-page.md' },
|
||||
{ status: 'D', path: 'wiki/global/gone-page.md' },
|
||||
]);
|
||||
gitService.getFileAtCommit.mockImplementation((path: string) => {
|
||||
if (path.endsWith('new-page.md')) {
|
||||
|
|
@ -117,10 +117,10 @@ describe('KnowledgeWikiService.syncFromCommit', () => {
|
|||
const { service, pagesRepository, gitService, logger } = makeService();
|
||||
|
||||
gitService.diffNameStatus.mockResolvedValue([
|
||||
{ status: 'A', path: 'knowledge/global/revenue-policy.md' },
|
||||
{ status: 'A', path: 'knowledge/global/historic-sql-order-lifecycle.md' },
|
||||
{ status: 'A', path: 'knowledge/global/historic-sql/order-lifecycle.md' },
|
||||
{ status: 'A', path: 'knowledge/global/orbit/company-overview.md' },
|
||||
{ status: 'A', path: 'wiki/global/revenue-policy.md' },
|
||||
{ status: 'A', path: 'wiki/global/historic-sql-order-lifecycle.md' },
|
||||
{ status: 'A', path: 'wiki/global/historic-sql/order-lifecycle.md' },
|
||||
{ status: 'A', path: 'wiki/global/orbit/company-overview.md' },
|
||||
]);
|
||||
gitService.getFileAtCommit.mockImplementation((path: string) => {
|
||||
if (path.endsWith('revenue-policy.md')) {
|
||||
|
|
@ -137,13 +137,13 @@ describe('KnowledgeWikiService.syncFromCommit', () => {
|
|||
|
||||
await service.syncFromCommit('sha-before', 'sha-after', 'run-uuid');
|
||||
|
||||
expect(gitService.getFileAtCommit).not.toHaveBeenCalledWith('knowledge/global/orbit/company-overview.md', 'sha-after');
|
||||
expect(gitService.getFileAtCommit).not.toHaveBeenCalledWith('knowledge/global/historic-sql/order-lifecycle.md', 'sha-after');
|
||||
expect(gitService.getFileAtCommit).not.toHaveBeenCalledWith('wiki/global/orbit/company-overview.md', 'sha-after');
|
||||
expect(gitService.getFileAtCommit).not.toHaveBeenCalledWith('wiki/global/historic-sql/order-lifecycle.md', 'sha-after');
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
'[knowledge.sync] skipping unparseable path: knowledge/global/orbit/company-overview.md',
|
||||
'[wiki.sync] skipping unparseable path: wiki/global/orbit/company-overview.md',
|
||||
);
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
'[knowledge.sync] skipping unparseable path: knowledge/global/historic-sql/order-lifecycle.md',
|
||||
'[wiki.sync] skipping unparseable path: wiki/global/historic-sql/order-lifecycle.md',
|
||||
);
|
||||
const call = pagesRepository.applyDiffTransactional.mock.calls[0][0];
|
||||
expect(call.upserts).toEqual(
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { buildKnowledgeSearchText } from './knowledge-search-text.js';
|
|||
import type { KnowledgeGitDiffPort, KnowledgeIndexPort, UpsertPageParams } from './ports.js';
|
||||
import type { WikiFrontmatter, WikiPage, WikiPageWithScope } from './types.js';
|
||||
|
||||
const WIKI_PREFIX = 'knowledge';
|
||||
const WIKI_PREFIX = 'wiki';
|
||||
|
||||
export type { WikiFrontmatter };
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ export class KnowledgeWikiService {
|
|||
) {
|
||||
const path = this.pagePath(scope, scopeId, pageKey);
|
||||
const serialized = this.serializePage(frontmatter, content);
|
||||
const message = commitMessage ?? `Update knowledge page: ${pageKey}`;
|
||||
const message = commitMessage ?? `Update wiki page: ${pageKey}`;
|
||||
return this.configService.writeFile(path, serialized, author, authorEmail, message, {
|
||||
skipLock: options?.skipLock,
|
||||
});
|
||||
|
|
@ -115,7 +115,7 @@ export class KnowledgeWikiService {
|
|||
) {
|
||||
const path = this.pagePath(scope, scopeId, pageKey);
|
||||
try {
|
||||
return await this.configService.deleteFile(path, author, authorEmail, `Remove knowledge page: ${pageKey}`);
|
||||
return await this.configService.deleteFile(path, author, authorEmail, `Remove wiki page: ${pageKey}`);
|
||||
} catch (error) {
|
||||
// Check if the file actually exists — if not, deletion is a no-op
|
||||
try {
|
||||
|
|
@ -196,7 +196,7 @@ export class KnowledgeWikiService {
|
|||
rawContent,
|
||||
author,
|
||||
authorEmail,
|
||||
commitMessage ?? `Update knowledge page (raw): ${pageKey}`,
|
||||
commitMessage ?? `Update wiki page (raw): ${pageKey}`,
|
||||
);
|
||||
await this.syncSinglePage(scope, scopeId, pageKey, parsed.frontmatter, parsed.content);
|
||||
return parsed;
|
||||
|
|
@ -352,9 +352,9 @@ export class KnowledgeWikiService {
|
|||
|
||||
/**
|
||||
* Apply the diff between two commits on the config repo to the shared
|
||||
* `knowledge` index in a single transaction. Called by the ingest runner
|
||||
* wiki index in a single transaction. Called by the ingest runner
|
||||
* after Stage 6 squashes the session branch into main: the pre-squash main
|
||||
* SHA and the post-squash SHA bracket exactly the set of knowledge-file
|
||||
* SHA and the post-squash SHA bracket exactly the set of wiki-file
|
||||
* changes this run produced.
|
||||
*
|
||||
* Any added/modified file becomes an upsert (tagged with `source_run_id`),
|
||||
|
|
@ -362,7 +362,7 @@ export class KnowledgeWikiService {
|
|||
* transaction so the shared table stays consistent.
|
||||
*/
|
||||
async syncFromCommit(fromSha: string, toSha: string, runId: string): Promise<void> {
|
||||
const diff = await this.gitService.diffNameStatus(fromSha, toSha, 'knowledge/');
|
||||
const diff = await this.gitService.diffNameStatus(fromSha, toSha, 'wiki/');
|
||||
if (diff.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -372,7 +372,7 @@ export class KnowledgeWikiService {
|
|||
for (const entry of diff) {
|
||||
const parsedPath = parseKnowledgePath(entry.path);
|
||||
if (!parsedPath) {
|
||||
this.logger.warn(`[knowledge.sync] skipping unparseable path: ${entry.path}`);
|
||||
this.logger.warn(`[wiki.sync] skipping unparseable path: ${entry.path}`);
|
||||
continue;
|
||||
}
|
||||
if (entry.status === 'D') {
|
||||
|
|
@ -392,7 +392,7 @@ export class KnowledgeWikiService {
|
|||
embedding = await this.embeddingService.computeEmbedding(searchText);
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`[knowledge.sync] embedding failed for ${parsedPath.pageKey}: ${err instanceof Error ? err.message : String(err)}`,
|
||||
`[wiki.sync] embedding failed for ${parsedPath.pageKey}: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
const contentHash = createHash('sha256').update(content).digest('hex');
|
||||
|
|
@ -410,21 +410,21 @@ export class KnowledgeWikiService {
|
|||
}
|
||||
|
||||
await this.pagesRepository.applyDiffTransactional({ runId, upserts, deletes });
|
||||
this.logger.log(`[knowledge.sync] run=${runId} applied ${upserts.length} upsert(s), ${deletes.length} delete(s)`);
|
||||
this.logger.log(`[wiki.sync] run=${runId} applied ${upserts.length} upsert(s), ${deletes.length} delete(s)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a `knowledge/<scope>/...` file path into its scope and page key.
|
||||
* `knowledge/global/foo.md` → { scope: 'GLOBAL', scopeId: null, pageKey: 'foo' }
|
||||
* `knowledge/user/<id>/bar.md` → { scope: 'USER', scopeId: '<id>', pageKey: 'bar' }
|
||||
* Parse a `wiki/<scope>/...` file path into its scope and page key.
|
||||
* `wiki/global/foo.md` → { scope: 'GLOBAL', scopeId: null, pageKey: 'foo' }
|
||||
* `wiki/user/<id>/bar.md` → { scope: 'USER', scopeId: '<id>', pageKey: 'bar' }
|
||||
*/
|
||||
function parseKnowledgePath(path: string): { scope: string; scopeId: string | null; pageKey: string } | null {
|
||||
if (!path.endsWith('.md')) {
|
||||
return null;
|
||||
}
|
||||
const segments = path.split('/');
|
||||
if (segments[0] !== 'knowledge') {
|
||||
if (segments[0] !== 'wiki') {
|
||||
return null;
|
||||
}
|
||||
const rest = segments.slice(1);
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ describe('local knowledge helpers', () => {
|
|||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('writes, reads, lists, and searches global knowledge pages', async () => {
|
||||
it('writes, reads, lists, and searches global wiki pages', async () => {
|
||||
const write = await writeLocalKnowledgePage(project, {
|
||||
key: 'metrics-revenue',
|
||||
scope: 'GLOBAL',
|
||||
|
|
@ -46,7 +46,7 @@ describe('local knowledge helpers', () => {
|
|||
slRefs: ['orders'],
|
||||
});
|
||||
|
||||
expect(write.path).toBe('knowledge/global/metrics-revenue.md');
|
||||
expect(write.path).toBe('wiki/global/metrics-revenue.md');
|
||||
expect(write.operation).toBe('write');
|
||||
|
||||
await expect(readLocalKnowledgePage(project, { key: 'metrics-revenue', userId: 'local' })).resolves.toMatchObject({
|
||||
|
|
@ -62,7 +62,7 @@ describe('local knowledge helpers', () => {
|
|||
await expect(listLocalKnowledgePages(project, { userId: 'local' })).resolves.toEqual([
|
||||
{
|
||||
key: 'metrics-revenue',
|
||||
path: 'knowledge/global/metrics-revenue.md',
|
||||
path: 'wiki/global/metrics-revenue.md',
|
||||
scope: 'GLOBAL',
|
||||
summary: 'Revenue metric definition',
|
||||
},
|
||||
|
|
@ -72,7 +72,7 @@ describe('local knowledge helpers', () => {
|
|||
expect(search).toEqual([
|
||||
expect.objectContaining({
|
||||
key: 'metrics-revenue',
|
||||
path: 'knowledge/global/metrics-revenue.md',
|
||||
path: 'wiki/global/metrics-revenue.md',
|
||||
scope: 'GLOBAL',
|
||||
score: expect.any(Number),
|
||||
matchReasons: expect.arrayContaining(['lexical']),
|
||||
|
|
@ -195,7 +195,7 @@ describe('local knowledge helpers', () => {
|
|||
fingerprints: ['fp_paid_orders'],
|
||||
});
|
||||
|
||||
const raw = await project.fileStore.readFile('knowledge/global/monthly-paid-orders.md');
|
||||
const raw = await project.fileStore.readFile('wiki/global/monthly-paid-orders.md');
|
||||
expect(raw.content).toContain('source: historic-sql');
|
||||
expect(raw.content).toContain('intent: Monthly paid order count');
|
||||
expect(raw.content).toContain(['tables:', ' - analytics.orders'].join('\n'));
|
||||
|
|
@ -245,7 +245,7 @@ describe('local knowledge helpers', () => {
|
|||
).rejects.toThrow('Invalid wiki key "orbit/company-overview". Wiki keys must be flat; use "orbit-company-overview".');
|
||||
});
|
||||
|
||||
it('ignores nested historic-SQL legacy paths when listing local knowledge pages', async () => {
|
||||
it('ignores nested historic-SQL legacy paths when listing local wiki pages', async () => {
|
||||
await writeLocalKnowledgePage(project, {
|
||||
key: 'historic-sql-paid-orders',
|
||||
scope: 'GLOBAL',
|
||||
|
|
@ -254,7 +254,7 @@ describe('local knowledge helpers', () => {
|
|||
tags: ['historic-sql'],
|
||||
});
|
||||
await project.fileStore.writeFile(
|
||||
'knowledge/global/historic-sql/paid-orders.md',
|
||||
'wiki/global/historic-sql/paid-orders.md',
|
||||
'---\nsummary: Nested historic SQL page\nusage_mode: auto\n---\n\nNested body\n',
|
||||
'Test',
|
||||
'test@example.com',
|
||||
|
|
@ -264,7 +264,7 @@ describe('local knowledge helpers', () => {
|
|||
await expect(listLocalKnowledgePages(project, { userId: 'local' })).resolves.toEqual([
|
||||
{
|
||||
key: 'historic-sql-paid-orders',
|
||||
path: 'knowledge/global/historic-sql-paid-orders.md',
|
||||
path: 'wiki/global/historic-sql-paid-orders.md',
|
||||
scope: 'GLOBAL',
|
||||
summary: 'Flat historic SQL page',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -75,13 +75,13 @@ function stringArray(value: unknown): string[] {
|
|||
function knowledgePath(scope: LocalKnowledgeScope, userId: string | undefined, key: string): string {
|
||||
const safeKey = assertFlatWikiKey(key);
|
||||
if (scope === 'GLOBAL') {
|
||||
return `knowledge/global/${safeKey}.md`;
|
||||
return `wiki/global/${safeKey}.md`;
|
||||
}
|
||||
return `knowledge/user/${assertSafePathToken('user id', userId ?? 'local')}/${safeKey}.md`;
|
||||
return `wiki/user/${assertSafePathToken('user id', userId ?? 'local')}/${safeKey}.md`;
|
||||
}
|
||||
|
||||
function keyFromKnowledgePath(path: string, scope: LocalKnowledgeScope, userId: string): string | null {
|
||||
const prefix = scope === 'GLOBAL' ? 'knowledge/global/' : `knowledge/user/${assertSafePathToken('user id', userId)}/`;
|
||||
const prefix = scope === 'GLOBAL' ? 'wiki/global/' : `wiki/user/${assertSafePathToken('user id', userId)}/`;
|
||||
const key = path.slice(prefix.length).replace(/\.md$/, '');
|
||||
if (isFlatWikiKey(key)) {
|
||||
return key;
|
||||
|
|
@ -158,7 +158,7 @@ export async function writeLocalKnowledgePage(
|
|||
serializeKnowledgePage(input),
|
||||
LOCAL_AUTHOR,
|
||||
LOCAL_AUTHOR_EMAIL,
|
||||
`Write knowledge page: ${input.key}`,
|
||||
`Write wiki page: ${input.key}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -181,7 +181,7 @@ export async function listLocalKnowledgePages(
|
|||
const userId = input.userId ?? 'local';
|
||||
const pages: LocalKnowledgeSummary[] = [];
|
||||
for (const scope of ['GLOBAL', 'USER'] as const) {
|
||||
const root = scope === 'GLOBAL' ? 'knowledge/global' : `knowledge/user/${assertSafePathToken('user id', userId)}`;
|
||||
const root = scope === 'GLOBAL' ? 'wiki/global' : `wiki/user/${assertSafePathToken('user id', userId)}`;
|
||||
const listed = await project.fileStore.listFiles(root);
|
||||
for (const path of listed.files.filter((file) => file.endsWith('.md')).sort()) {
|
||||
const key = keyFromKnowledgePath(path, scope, userId);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ describe('SqliteKnowledgeIndex', () => {
|
|||
|
||||
function page(overrides: Partial<SqliteKnowledgeIndexPage> = {}): SqliteKnowledgeIndexPage {
|
||||
return {
|
||||
path: 'knowledge/global/revenue.md',
|
||||
path: 'wiki/global/revenue.md',
|
||||
key: 'revenue',
|
||||
scope: 'GLOBAL',
|
||||
summary: 'Revenue definition',
|
||||
|
|
@ -36,7 +36,7 @@ describe('SqliteKnowledgeIndex', () => {
|
|||
index.sync([
|
||||
page(),
|
||||
page({
|
||||
path: 'knowledge/global/support.md',
|
||||
path: 'wiki/global/support.md',
|
||||
key: 'support',
|
||||
summary: 'Support queue',
|
||||
content: 'Tickets are grouped by priority.',
|
||||
|
|
@ -47,8 +47,8 @@ describe('SqliteKnowledgeIndex', () => {
|
|||
await expect(access(dbPath)).resolves.toBeUndefined();
|
||||
expect(index.searchLexicalCandidates({ queryText: 'paid order', limit: 10 })).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'knowledge/global/revenue.md',
|
||||
path: 'knowledge/global/revenue.md',
|
||||
id: 'wiki/global/revenue.md',
|
||||
path: 'wiki/global/revenue.md',
|
||||
rank: 1,
|
||||
rawScore: expect.any(Number),
|
||||
}),
|
||||
|
|
@ -57,7 +57,7 @@ describe('SqliteKnowledgeIndex', () => {
|
|||
|
||||
it('removes stale rows when the Markdown source list changes', () => {
|
||||
const index = new SqliteKnowledgeIndex({ dbPath });
|
||||
index.rebuild([page(), page({ path: 'knowledge/global/churn.md', key: 'churn', content: 'Churn risk.' })]);
|
||||
index.rebuild([page(), page({ path: 'wiki/global/churn.md', key: 'churn', content: 'Churn risk.' })]);
|
||||
expect(index.search('churn', 10)).toHaveLength(1);
|
||||
|
||||
index.rebuild([page()]);
|
||||
|
|
@ -67,12 +67,12 @@ describe('SqliteKnowledgeIndex', () => {
|
|||
|
||||
it('exposes existing search text and embedding state for incremental refresh', () => {
|
||||
const index = new SqliteKnowledgeIndex({ dbPath });
|
||||
index.sync([page({ path: 'knowledge/global/revenue.md', key: 'revenue', embedding: [1, 0] })]);
|
||||
index.sync([page({ path: 'wiki/global/revenue.md', key: 'revenue', embedding: [1, 0] })]);
|
||||
|
||||
expect(index.getExistingPages()).toEqual(
|
||||
new Map([
|
||||
[
|
||||
'knowledge/global/revenue.md',
|
||||
'wiki/global/revenue.md',
|
||||
expect.objectContaining({
|
||||
searchText: expect.stringContaining('Revenue definition'),
|
||||
embedding: [1, 0],
|
||||
|
|
@ -84,29 +84,29 @@ describe('SqliteKnowledgeIndex', () => {
|
|||
|
||||
it('does not treat empty embeddings as indexed semantic vectors', () => {
|
||||
const index = new SqliteKnowledgeIndex({ dbPath });
|
||||
index.sync([page({ path: 'knowledge/global/revenue.md', key: 'revenue', embedding: [] })]);
|
||||
index.sync([page({ path: 'wiki/global/revenue.md', key: 'revenue', embedding: [] })]);
|
||||
|
||||
expect(index.getExistingPages().get('knowledge/global/revenue.md')?.embedding).toBeNull();
|
||||
expect(index.getExistingPages().get('wiki/global/revenue.md')?.embedding).toBeNull();
|
||||
expect(index.searchSemanticCandidates({ queryEmbedding: [1, 0], limit: 10 })).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns semantic lane candidates from stored page embeddings', () => {
|
||||
const index = new SqliteKnowledgeIndex({ dbPath });
|
||||
index.sync([
|
||||
page({ path: 'knowledge/global/revenue.md', key: 'revenue', embedding: [1, 0] }),
|
||||
page({ path: 'knowledge/global/support.md', key: 'support', summary: 'Support queue', embedding: [0, 1] }),
|
||||
page({ path: 'wiki/global/revenue.md', key: 'revenue', embedding: [1, 0] }),
|
||||
page({ path: 'wiki/global/support.md', key: 'support', summary: 'Support queue', embedding: [0, 1] }),
|
||||
]);
|
||||
|
||||
expect(index.searchSemanticCandidates({ queryEmbedding: [1, 0], limit: 10 })).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'knowledge/global/revenue.md',
|
||||
path: 'knowledge/global/revenue.md',
|
||||
id: 'wiki/global/revenue.md',
|
||||
path: 'wiki/global/revenue.md',
|
||||
rank: 1,
|
||||
rawScore: 1,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'knowledge/global/support.md',
|
||||
path: 'knowledge/global/support.md',
|
||||
id: 'wiki/global/support.md',
|
||||
path: 'wiki/global/support.md',
|
||||
rank: 2,
|
||||
rawScore: 0,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export class WikiRemoveTool extends BaseTool<typeof wikiRemoveInputSchema> {
|
|||
}
|
||||
|
||||
get description(): string {
|
||||
return `<purpose>Remove a knowledge page that is no longer relevant.</purpose>`;
|
||||
return `<purpose>Remove a wiki page that is no longer relevant.</purpose>`;
|
||||
}
|
||||
|
||||
get inputSchema() {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ describe('WikiSearchTool', () => {
|
|||
results: [
|
||||
{
|
||||
key: 'metrics-revenue',
|
||||
path: 'knowledge/global/metrics-revenue.md',
|
||||
path: 'wiki/global/metrics-revenue.md',
|
||||
scope: 'GLOBAL' as const,
|
||||
summary: 'Revenue metric definition',
|
||||
score: 0.02459016393442623,
|
||||
|
|
@ -28,7 +28,7 @@ describe('WikiSearchTool', () => {
|
|||
results: [
|
||||
{
|
||||
blockKey: 'metrics-revenue',
|
||||
path: 'knowledge/global/metrics-revenue.md',
|
||||
path: 'wiki/global/metrics-revenue.md',
|
||||
summary: 'Revenue metric definition',
|
||||
score: 0.02459016393442623,
|
||||
matchReasons: ['lexical', 'token'],
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ export class WikiWriteTool extends BaseTool<typeof wikiWriteInputSchema> {
|
|||
|
||||
get description(): string {
|
||||
return `<purpose>
|
||||
Create or update a knowledge page. Provide content for create/rewrite, or replacements for targeted edits.
|
||||
Create or update a wiki page. Provide content for create/rewrite, or replacements for targeted edits.
|
||||
For existing pages, you may provide only frontmatter fields such as summary, tags, refs, or sl_refs to update metadata while preserving content.
|
||||
tags/refs/sl_refs use REPLACE semantics: omit to keep existing on update, [] to clear, [values] to set.
|
||||
Keys must be flat file names, not directory paths. Use tags/source frontmatter for grouping.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue