@kaelio/ktx 0.1.0-rc.5 → 0.1.0
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/assets/python/kaelio_ktx-0.1.0-py3-none-any.whl +0 -0
- package/assets/python/manifest.json +2 -2
- package/dist/clack.d.ts +6 -0
- package/dist/clack.js +23 -0
- package/dist/cli-program.js +5 -2
- package/dist/cli-program.test.js +7 -1
- package/dist/cli-runtime.d.ts +4 -0
- package/dist/cli-runtime.js +8 -1
- package/dist/command-schemas.d.ts +1 -1
- package/dist/commands/ingest-commands.js +1 -0
- package/dist/commands/knowledge-commands.js +5 -0
- package/dist/commands/mcp-commands.js +11 -3
- package/dist/commands/mcp-commands.test.js +30 -1
- package/dist/commands/sql-commands.d.ts +3 -0
- package/dist/commands/sql-commands.js +43 -0
- package/dist/commands/sql-commands.test.d.ts +1 -0
- package/dist/commands/sql-commands.test.js +68 -0
- package/dist/context-build-view.js +5 -1
- package/dist/dev.test.js +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.test.js +56 -21
- package/dist/ingest.js +123 -18
- package/dist/ingest.test.js +206 -0
- package/dist/io/print-list.d.ts +2 -1
- package/dist/io/print-list.js +7 -0
- package/dist/io/print-list.test.js +13 -11
- package/dist/io/symbols.d.ts +2 -2
- package/dist/knowledge.d.ts +1 -0
- package/dist/knowledge.js +34 -16
- package/dist/knowledge.test.js +27 -0
- package/dist/managed-python-command.d.ts +2 -0
- package/dist/managed-python-command.js +17 -9
- package/dist/managed-python-command.test.js +59 -4
- package/dist/next-steps.js +1 -1
- package/dist/next-steps.test.js +2 -0
- package/dist/print-command-tree.js +7 -1
- package/dist/public-ingest.d.ts +9 -1
- package/dist/public-ingest.js +50 -7
- package/dist/public-ingest.test.js +69 -2
- package/dist/release-version.d.ts +5 -0
- package/dist/release-version.js +44 -0
- package/dist/runtime-requirements.d.ts +23 -0
- package/dist/runtime-requirements.js +99 -0
- package/dist/runtime-requirements.test.d.ts +1 -0
- package/dist/runtime-requirements.test.js +63 -0
- package/dist/setup-agents.d.ts +11 -3
- package/dist/setup-agents.js +397 -134
- package/dist/setup-agents.test.js +359 -61
- package/dist/setup-embeddings.js +3 -6
- package/dist/setup-embeddings.test.js +18 -2
- package/dist/setup-models.js +2 -2
- package/dist/setup-models.test.js +5 -3
- package/dist/setup-ready-menu.d.ts +1 -1
- package/dist/setup-ready-menu.js +2 -0
- package/dist/setup-ready-menu.test.js +3 -0
- package/dist/setup-runtime.d.ts +45 -0
- package/dist/setup-runtime.js +47 -0
- package/dist/setup-runtime.test.d.ts +1 -0
- package/dist/setup-runtime.test.js +110 -0
- package/dist/setup-sources-notion.test.d.ts +1 -0
- package/dist/setup-sources-notion.test.js +107 -0
- package/dist/setup-sources.js +5 -2
- package/dist/setup.d.ts +19 -1
- package/dist/setup.js +104 -29
- package/dist/setup.test.js +221 -57
- package/dist/sl.js +2 -2
- package/dist/sl.test.js +10 -0
- package/dist/source-mapping.js +9 -1
- package/dist/source-mapping.test.d.ts +1 -0
- package/dist/source-mapping.test.js +65 -0
- package/dist/sql.d.ts +22 -0
- package/dist/sql.js +125 -0
- package/dist/sql.test.d.ts +1 -0
- package/dist/sql.test.js +226 -0
- package/node_modules/@ktx/connector-clickhouse/dist/package-exports.test.js +1 -1
- package/node_modules/@ktx/context/dist/connections/connection-type.d.ts +4 -4
- package/node_modules/@ktx/context/dist/core/git.service.d.ts +3 -0
- package/node_modules/@ktx/context/dist/core/git.service.js +47 -1
- package/node_modules/@ktx/context/dist/core/git.service.patch.test.d.ts +1 -0
- package/node_modules/@ktx/context/dist/core/git.service.patch.test.js +40 -0
- package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/types.d.ts +5 -5
- package/node_modules/@ktx/context/dist/ingest/adapters/looker/looker.adapter.d.ts +2 -2
- package/node_modules/@ktx/context/dist/ingest/adapters/looker/tools/looker-query-to-sl.tool.d.ts +2 -2
- package/node_modules/@ktx/context/dist/ingest/adapters/looker/types.d.ts +16 -16
- package/node_modules/@ktx/context/dist/ingest/adapters/lookml/pull-config.d.ts +1 -1
- package/node_modules/@ktx/context/dist/ingest/adapters/metabase/fetch.js +16 -0
- package/node_modules/@ktx/context/dist/ingest/adapters/metabase/fetch.test.js +41 -0
- package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/metricflow.adapter.d.ts +2 -1
- package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/metricflow.adapter.js +40 -0
- package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/metricflow.adapter.test.js +116 -1
- package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/projection-config.d.ts +29 -0
- package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/projection-config.js +40 -0
- package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/pull-config.d.ts +1 -1
- package/node_modules/@ktx/context/dist/ingest/artifact-gates.d.ts +25 -0
- package/node_modules/@ktx/context/dist/ingest/artifact-gates.js +149 -0
- package/node_modules/@ktx/context/dist/ingest/artifact-gates.test.d.ts +1 -0
- package/node_modules/@ktx/context/dist/ingest/artifact-gates.test.js +167 -0
- package/node_modules/@ktx/context/dist/ingest/final-gate-repair.d.ts +29 -0
- package/node_modules/@ktx/context/dist/ingest/final-gate-repair.js +178 -0
- package/node_modules/@ktx/context/dist/ingest/final-gate-repair.test.d.ts +1 -0
- package/node_modules/@ktx/context/dist/ingest/final-gate-repair.test.js +109 -0
- package/node_modules/@ktx/context/dist/ingest/index.d.ts +8 -1
- package/node_modules/@ktx/context/dist/ingest/index.js +7 -0
- package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.d.ts +18 -2
- package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.isolated-diff.test.d.ts +1 -0
- package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.isolated-diff.test.js +1761 -0
- package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.js +1695 -901
- package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.test.js +135 -118
- package/node_modules/@ktx/context/dist/ingest/ingest-trace.d.ts +50 -0
- package/node_modules/@ktx/context/dist/ingest/ingest-trace.js +88 -0
- package/node_modules/@ktx/context/dist/ingest/ingest-trace.test.d.ts +1 -0
- package/node_modules/@ktx/context/dist/ingest/ingest-trace.test.js +76 -0
- package/node_modules/@ktx/context/dist/ingest/isolated-diff/git-patch.d.ts +16 -0
- package/node_modules/@ktx/context/dist/ingest/isolated-diff/git-patch.js +78 -0
- package/node_modules/@ktx/context/dist/ingest/isolated-diff/git-patch.test.d.ts +1 -0
- package/node_modules/@ktx/context/dist/ingest/isolated-diff/git-patch.test.js +76 -0
- package/node_modules/@ktx/context/dist/ingest/isolated-diff/patch-integrator.d.ts +58 -0
- package/node_modules/@ktx/context/dist/ingest/isolated-diff/patch-integrator.js +223 -0
- package/node_modules/@ktx/context/dist/ingest/isolated-diff/patch-integrator.test.d.ts +1 -0
- package/node_modules/@ktx/context/dist/ingest/isolated-diff/patch-integrator.test.js +369 -0
- package/node_modules/@ktx/context/dist/ingest/isolated-diff/textual-conflict-resolver.d.ts +23 -0
- package/node_modules/@ktx/context/dist/ingest/isolated-diff/textual-conflict-resolver.js +190 -0
- package/node_modules/@ktx/context/dist/ingest/isolated-diff/textual-conflict-resolver.test.d.ts +1 -0
- package/node_modules/@ktx/context/dist/ingest/isolated-diff/textual-conflict-resolver.test.js +101 -0
- package/node_modules/@ktx/context/dist/ingest/isolated-diff/work-unit-executor.d.ts +15 -0
- package/node_modules/@ktx/context/dist/ingest/isolated-diff/work-unit-executor.js +61 -0
- package/node_modules/@ktx/context/dist/ingest/isolated-diff/work-unit-executor.test.d.ts +1 -0
- package/node_modules/@ktx/context/dist/ingest/isolated-diff/work-unit-executor.test.js +137 -0
- package/node_modules/@ktx/context/dist/ingest/local-bundle-ingest.test.js +7 -0
- package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.js +54 -10
- package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.test.js +65 -0
- package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.d.ts +23 -5
- package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.js +17 -0
- package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.test.js +1 -0
- package/node_modules/@ktx/context/dist/ingest/memory-flow/types.d.ts +6 -0
- package/node_modules/@ktx/context/dist/ingest/parsed-target-table.d.ts +1 -1
- package/node_modules/@ktx/context/dist/ingest/ports.d.ts +3 -0
- package/node_modules/@ktx/context/dist/ingest/report-snapshot.d.ts +32 -7
- package/node_modules/@ktx/context/dist/ingest/report-snapshot.js +25 -0
- package/node_modules/@ktx/context/dist/ingest/report-snapshot.test.js +124 -0
- package/node_modules/@ktx/context/dist/ingest/reports.d.ts +23 -0
- package/node_modules/@ktx/context/dist/ingest/semantic-layer-target-policy.d.ts +11 -0
- package/node_modules/@ktx/context/dist/ingest/semantic-layer-target-policy.js +26 -0
- package/node_modules/@ktx/context/dist/ingest/semantic-layer-target-policy.test.d.ts +1 -0
- package/node_modules/@ktx/context/dist/ingest/semantic-layer-target-policy.test.js +25 -0
- package/node_modules/@ktx/context/dist/ingest/stages/stage-3-work-units.d.ts +4 -0
- package/node_modules/@ktx/context/dist/ingest/stages/stage-3-work-units.js +4 -0
- package/node_modules/@ktx/context/dist/ingest/stages/stage-3-work-units.test.js +29 -0
- package/node_modules/@ktx/context/dist/ingest/tools/emit-unmapped-fallback.tool.d.ts +1 -1
- package/node_modules/@ktx/context/dist/ingest/types.d.ts +24 -0
- package/node_modules/@ktx/context/dist/ingest/wiki-body-refs.d.ts +24 -0
- package/node_modules/@ktx/context/dist/ingest/wiki-body-refs.js +111 -0
- package/node_modules/@ktx/context/dist/ingest/wiki-body-refs.test.d.ts +1 -0
- package/node_modules/@ktx/context/dist/ingest/wiki-body-refs.test.js +138 -0
- package/node_modules/@ktx/context/dist/llm/claude-code-runtime.js +19 -2
- package/node_modules/@ktx/context/dist/llm/claude-code-runtime.test.js +33 -0
- package/node_modules/@ktx/context/dist/project/setup-config.d.ts +1 -1
- package/node_modules/@ktx/context/dist/project/setup-config.js +10 -1
- package/node_modules/@ktx/context/dist/project/setup-config.test.js +3 -2
- package/node_modules/@ktx/context/dist/sl/tools/sl-edit-source.tool.js +5 -1
- package/node_modules/@ktx/context/dist/sl/tools/sl-edit-source.tool.test.js +15 -0
- package/node_modules/@ktx/context/dist/sl/tools/sl-write-source.tool.js +5 -1
- package/node_modules/@ktx/context/dist/sl/tools/sl-write-source.tool.test.js +22 -0
- package/node_modules/@ktx/context/dist/tools/action-target-connection.d.ts +9 -0
- package/node_modules/@ktx/context/dist/tools/action-target-connection.js +14 -0
- package/node_modules/@ktx/context/dist/tools/context-candidate-write.tool.d.ts +4 -4
- package/node_modules/@ktx/context/dist/tools/index.d.ts +1 -0
- package/node_modules/@ktx/context/dist/tools/index.js +1 -0
- package/node_modules/@ktx/context/dist/wiki/local-knowledge.js +4 -1
- package/node_modules/@ktx/context/dist/wiki/local-knowledge.test.js +44 -0
- package/node_modules/@ktx/context/dist/wiki/tools/wiki-write.tool.js +3 -48
- package/node_modules/@ktx/context/dist/wiki/tools/wiki-write.tool.test.js +28 -0
- package/node_modules/@ktx/context/dist/wiki/wiki-ref-validation.d.ts +17 -0
- package/node_modules/@ktx/context/dist/wiki/wiki-ref-validation.js +79 -0
- package/node_modules/@ktx/context/dist/wiki/wiki-ref-validation.test.d.ts +1 -0
- package/node_modules/@ktx/context/dist/wiki/wiki-ref-validation.test.js +64 -0
- package/node_modules/@ktx/context/prompts/memory_agent_bundle_ingest_work_unit.md +23 -4
- package/node_modules/@ktx/context/skills/ingest_triage/SKILL.md +7 -3
- package/package.json +4 -4
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { findInvalidWikiBodyRefs, parseWikiBodyRefs } from './wiki-body-refs.js';
|
|
3
|
+
const sources = [
|
|
4
|
+
{
|
|
5
|
+
name: 'mart_account_segments',
|
|
6
|
+
grain: ['account_id'],
|
|
7
|
+
columns: [
|
|
8
|
+
{ name: 'account_id', type: 'string' },
|
|
9
|
+
{ name: 'segment', type: 'string' },
|
|
10
|
+
],
|
|
11
|
+
joins: [],
|
|
12
|
+
measures: [{ name: 'total_contract_arr', expr: 'sum(contract_arr)' }],
|
|
13
|
+
segments: [{ name: 'enterprise', expr: "segment = 'enterprise'" }],
|
|
14
|
+
table: 'analytics.mart_account_segments',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
describe('wiki body refs', () => {
|
|
18
|
+
it('parses only explicit inline-code body references outside fenced blocks', () => {
|
|
19
|
+
const body = [
|
|
20
|
+
'Valid `mart_account_segments.total_contract_arr` and `source:mart_account_segments`.',
|
|
21
|
+
'Also `warehouse/mart_account_segments.segment` and `table:analytics.mart_account_segments`.',
|
|
22
|
+
'Ignore prose mart_account_segments.total_contract_arr_cents.',
|
|
23
|
+
'Ignore `single_token`.',
|
|
24
|
+
'Ignore wildcard pattern `mart_nrr_quarterly.*_arr_cents`.',
|
|
25
|
+
'Ignore condition `users.is_internal = false`.',
|
|
26
|
+
'```sql',
|
|
27
|
+
'select `mart_account_segments.total_contract_arr_cents`',
|
|
28
|
+
'```',
|
|
29
|
+
].join('\n');
|
|
30
|
+
expect(parseWikiBodyRefs(body)).toEqual([
|
|
31
|
+
{ kind: 'sl_entity', connectionId: null, sourceName: 'mart_account_segments', entityName: 'total_contract_arr' },
|
|
32
|
+
{ kind: 'sl_source', connectionId: null, sourceName: 'mart_account_segments' },
|
|
33
|
+
{ kind: 'sl_entity', connectionId: 'warehouse', sourceName: 'mart_account_segments', entityName: 'segment' },
|
|
34
|
+
{ kind: 'table', connectionId: null, tableRef: 'analytics.mart_account_segments' },
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
it('rejects stale inline-code semantic-layer references', async () => {
|
|
38
|
+
const invalid = await findInvalidWikiBodyRefs({
|
|
39
|
+
pageKey: 'account-segments',
|
|
40
|
+
body: 'ARR is documented as `mart_account_segments.total_contract_arr_cents`.',
|
|
41
|
+
visibleConnectionIds: ['warehouse'],
|
|
42
|
+
loadSources: async () => sources,
|
|
43
|
+
tableExists: async () => true,
|
|
44
|
+
});
|
|
45
|
+
expect(invalid).toEqual([
|
|
46
|
+
'account-segments: unknown semantic-layer entity mart_account_segments.total_contract_arr_cents',
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
it('does not treat wildcard inline-code patterns as exact semantic-layer entity references', async () => {
|
|
50
|
+
const invalid = await findInvalidWikiBodyRefs({
|
|
51
|
+
pageKey: 'revenue-metrics-encoding',
|
|
52
|
+
body: 'Cents columns include `mart_nrr_quarterly.*_arr_cents` and `mart_retention_movement_breakout.*_arr_cents`.',
|
|
53
|
+
visibleConnectionIds: ['warehouse'],
|
|
54
|
+
loadSources: async () => [
|
|
55
|
+
{ name: 'mart_nrr_quarterly', grain: [], columns: [], joins: [], measures: [], table: 'analytics.mart_nrr_quarterly' },
|
|
56
|
+
{
|
|
57
|
+
name: 'mart_retention_movement_breakout',
|
|
58
|
+
grain: [],
|
|
59
|
+
columns: [],
|
|
60
|
+
joins: [],
|
|
61
|
+
measures: [],
|
|
62
|
+
table: 'analytics.mart_retention_movement_breakout',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
tableExists: async () => true,
|
|
66
|
+
});
|
|
67
|
+
expect(invalid).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
it('does not treat inline-code SQL predicates as exact semantic-layer entity references', async () => {
|
|
70
|
+
const invalid = await findInvalidWikiBodyRefs({
|
|
71
|
+
pageKey: 'account-reporting-exclusions',
|
|
72
|
+
body: 'Exclude internal users with `users.is_internal = false` and test users with `users.is_test = false`.',
|
|
73
|
+
visibleConnectionIds: ['warehouse'],
|
|
74
|
+
loadSources: async () => [
|
|
75
|
+
{
|
|
76
|
+
name: 'users',
|
|
77
|
+
grain: [],
|
|
78
|
+
columns: [
|
|
79
|
+
{ name: 'is_internal', type: 'boolean' },
|
|
80
|
+
{ name: 'is_test', type: 'boolean' },
|
|
81
|
+
],
|
|
82
|
+
joins: [],
|
|
83
|
+
measures: [],
|
|
84
|
+
table: 'analytics.users',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
tableExists: async () => true,
|
|
88
|
+
});
|
|
89
|
+
expect(invalid).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
it('validates source, dimension, segment, measure, and table references', async () => {
|
|
92
|
+
const invalid = await findInvalidWikiBodyRefs({
|
|
93
|
+
pageKey: 'account-segments',
|
|
94
|
+
body: [
|
|
95
|
+
'`mart_account_segments.total_contract_arr`',
|
|
96
|
+
'`mart_account_segments.segment`',
|
|
97
|
+
'`mart_account_segments.enterprise`',
|
|
98
|
+
'`source:mart_account_segments`',
|
|
99
|
+
'`table:analytics.mart_account_segments`',
|
|
100
|
+
].join('\n'),
|
|
101
|
+
visibleConnectionIds: ['warehouse'],
|
|
102
|
+
loadSources: async () => sources,
|
|
103
|
+
tableExists: async (_connectionId, tableRef) => tableRef === 'analytics.mart_account_segments',
|
|
104
|
+
});
|
|
105
|
+
expect(invalid).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
it('ignores two-part inline code when the source is not visible', async () => {
|
|
108
|
+
const invalid = await findInvalidWikiBodyRefs({
|
|
109
|
+
pageKey: 'engineering-notes',
|
|
110
|
+
body: [
|
|
111
|
+
'A version token like `node.v22` is not a semantic-layer reference.',
|
|
112
|
+
'A raw table must use `table:analytics.mart_account_segments`.',
|
|
113
|
+
].join('\n'),
|
|
114
|
+
visibleConnectionIds: ['warehouse'],
|
|
115
|
+
loadSources: async () => sources,
|
|
116
|
+
tableExists: async (_connectionId, tableRef) => tableRef === 'analytics.mart_account_segments',
|
|
117
|
+
});
|
|
118
|
+
expect(invalid).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
it('still rejects explicit missing source and table references', async () => {
|
|
121
|
+
const invalid = await findInvalidWikiBodyRefs({
|
|
122
|
+
pageKey: 'account-segments',
|
|
123
|
+
body: [
|
|
124
|
+
'`source:missing_source`',
|
|
125
|
+
'`warehouse/source:missing_source`',
|
|
126
|
+
'`table:analytics.missing_table`',
|
|
127
|
+
].join('\n'),
|
|
128
|
+
visibleConnectionIds: ['warehouse'],
|
|
129
|
+
loadSources: async () => sources,
|
|
130
|
+
tableExists: async () => false,
|
|
131
|
+
});
|
|
132
|
+
expect(invalid).toEqual([
|
|
133
|
+
'account-segments: unknown semantic-layer source missing_source',
|
|
134
|
+
'account-segments: unknown semantic-layer source warehouse/missing_source',
|
|
135
|
+
'account-segments: unknown raw table analytics.missing_table',
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -18,6 +18,7 @@ const BUILTIN_TOOLS = [
|
|
|
18
18
|
'WebSearch',
|
|
19
19
|
'TodoWrite',
|
|
20
20
|
];
|
|
21
|
+
const KTX_MCP_SERVER_NAME = 'ktx';
|
|
21
22
|
function isResult(message) {
|
|
22
23
|
return message.type === 'result';
|
|
23
24
|
}
|
|
@@ -66,11 +67,18 @@ function assertInitIsolation(message, allowedToolIds, expectedMcpServerNames) {
|
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
function expectedMcpServerNames(tools) {
|
|
69
|
-
return tools && Object.keys(tools).length > 0 ? new Set([
|
|
70
|
+
return tools && Object.keys(tools).length > 0 ? new Set([KTX_MCP_SERVER_NAME]) : new Set();
|
|
71
|
+
}
|
|
72
|
+
function managedMcpSettings(serverNames) {
|
|
73
|
+
return {
|
|
74
|
+
allowManagedMcpServersOnly: true,
|
|
75
|
+
allowedMcpServers: serverNames.map((serverName) => ({ serverName })),
|
|
76
|
+
};
|
|
70
77
|
}
|
|
71
78
|
function baseOptions(input) {
|
|
72
79
|
const toolIds = mcpToolIds(input.tools ?? {});
|
|
73
80
|
const allowedToolIds = new Set(toolIds);
|
|
81
|
+
const expectedServerNames = [...expectedMcpServerNames(input.tools)];
|
|
74
82
|
return {
|
|
75
83
|
cwd: input.projectDir,
|
|
76
84
|
model: input.model,
|
|
@@ -79,6 +87,8 @@ function baseOptions(input) {
|
|
|
79
87
|
skills: [],
|
|
80
88
|
plugins: [],
|
|
81
89
|
tools: [],
|
|
90
|
+
managedSettings: managedMcpSettings(expectedServerNames),
|
|
91
|
+
strictMcpConfig: true,
|
|
82
92
|
allowedTools: toolIds,
|
|
83
93
|
disallowedTools: BUILTIN_TOOLS,
|
|
84
94
|
canUseTool: async (toolName, _toolInput, options) => allowedToolIds.has(toolName)
|
|
@@ -92,7 +102,14 @@ function baseOptions(input) {
|
|
|
92
102
|
persistSession: false,
|
|
93
103
|
env: createKtxClaudeCodeEnv(input.env),
|
|
94
104
|
...(input.tools && Object.keys(input.tools).length > 0
|
|
95
|
-
? {
|
|
105
|
+
? {
|
|
106
|
+
mcpServers: {
|
|
107
|
+
[KTX_MCP_SERVER_NAME]: createSdkMcpServer({
|
|
108
|
+
name: KTX_MCP_SERVER_NAME,
|
|
109
|
+
tools: createClaudeSdkTools(input.tools),
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
112
|
+
}
|
|
96
113
|
: {}),
|
|
97
114
|
};
|
|
98
115
|
}
|
|
@@ -66,6 +66,11 @@ describe('ClaudeCodeKtxLlmRuntime', () => {
|
|
|
66
66
|
skills: [],
|
|
67
67
|
plugins: [],
|
|
68
68
|
tools: [],
|
|
69
|
+
managedSettings: {
|
|
70
|
+
allowManagedMcpServersOnly: true,
|
|
71
|
+
allowedMcpServers: [],
|
|
72
|
+
},
|
|
73
|
+
strictMcpConfig: true,
|
|
69
74
|
allowedTools: [],
|
|
70
75
|
permissionMode: 'dontAsk',
|
|
71
76
|
persistSession: false,
|
|
@@ -125,6 +130,11 @@ describe('ClaudeCodeKtxLlmRuntime', () => {
|
|
|
125
130
|
});
|
|
126
131
|
const options = query.mock.calls[0][0].options;
|
|
127
132
|
expect(options.allowedTools).toEqual(['mcp__ktx__load_skill']);
|
|
133
|
+
expect(options.managedSettings).toEqual({
|
|
134
|
+
allowManagedMcpServersOnly: true,
|
|
135
|
+
allowedMcpServers: [{ serverName: 'ktx' }],
|
|
136
|
+
});
|
|
137
|
+
expect(options.strictMcpConfig).toBe(true);
|
|
128
138
|
expect(await options.canUseTool('mcp__ktx__load_skill', {}, { signal: new AbortController().signal, toolUseID: '1' })).toEqual({
|
|
129
139
|
behavior: 'allow',
|
|
130
140
|
toolUseID: '1',
|
|
@@ -155,6 +165,11 @@ describe('ClaudeCodeKtxLlmRuntime', () => {
|
|
|
155
165
|
skills: [],
|
|
156
166
|
plugins: [],
|
|
157
167
|
tools: [],
|
|
168
|
+
managedSettings: {
|
|
169
|
+
allowManagedMcpServersOnly: true,
|
|
170
|
+
allowedMcpServers: [],
|
|
171
|
+
},
|
|
172
|
+
strictMcpConfig: true,
|
|
158
173
|
allowedTools: [],
|
|
159
174
|
permissionMode: 'dontAsk',
|
|
160
175
|
persistSession: false,
|
|
@@ -233,6 +248,11 @@ describe('ClaudeCodeKtxLlmRuntime', () => {
|
|
|
233
248
|
})).resolves.toEqual({ stopReason: 'budget' });
|
|
234
249
|
const options = query.mock.calls[0][0].options;
|
|
235
250
|
expect(options.allowedTools).toEqual(['mcp__ktx__load_skill']);
|
|
251
|
+
expect(options.managedSettings).toEqual({
|
|
252
|
+
allowManagedMcpServersOnly: true,
|
|
253
|
+
allowedMcpServers: [{ serverName: 'ktx' }],
|
|
254
|
+
});
|
|
255
|
+
expect(options.strictMcpConfig).toBe(true);
|
|
236
256
|
expect(await options.canUseTool('mcp__ktx__load_skill', {}, { signal: new AbortController().signal, toolUseID: '1' })).toEqual({
|
|
237
257
|
behavior: 'allow',
|
|
238
258
|
toolUseID: '1',
|
|
@@ -287,6 +307,10 @@ describe('ClaudeCodeKtxLlmRuntime', () => {
|
|
|
287
307
|
answer: 'yes',
|
|
288
308
|
});
|
|
289
309
|
expect(objectQuery.mock.calls[0][0].options.env).toEqual(expect.objectContaining({ PATH: '/usr/bin' }));
|
|
310
|
+
expect(objectQuery.mock.calls[0][0].options.managedSettings).toEqual({
|
|
311
|
+
allowManagedMcpServersOnly: true,
|
|
312
|
+
allowedMcpServers: [],
|
|
313
|
+
});
|
|
290
314
|
expect(objectQuery.mock.calls[0][0].options.env).not.toEqual(expect.objectContaining({ ANTHROPIC_API_KEY: 'sk-ant-test', AWS_PROFILE: 'prod' }));
|
|
291
315
|
const agentQuery = vi.fn((_input) => stream([
|
|
292
316
|
initMessage({ tools: ['mcp__ktx__load_skill'], mcp_servers: [{ name: 'ktx', status: 'connected' }] }),
|
|
@@ -321,6 +345,10 @@ describe('ClaudeCodeKtxLlmRuntime', () => {
|
|
|
321
345
|
telemetryTags: { operationName: 'test' },
|
|
322
346
|
});
|
|
323
347
|
expect(agentQuery.mock.calls[0][0].options.env).toEqual(expect.objectContaining({ HOME: '/Users/test' }));
|
|
348
|
+
expect(agentQuery.mock.calls[0][0].options.managedSettings).toEqual({
|
|
349
|
+
allowManagedMcpServersOnly: true,
|
|
350
|
+
allowedMcpServers: [{ serverName: 'ktx' }],
|
|
351
|
+
});
|
|
324
352
|
expect(agentQuery.mock.calls[0][0].options.env).not.toEqual(expect.objectContaining({ ANTHROPIC_AUTH_TOKEN: 'token', CLAUDE_CODE_USE_VERTEX: '1' }));
|
|
325
353
|
});
|
|
326
354
|
it('logs and ignores onStepFinish callback errors', async () => {
|
|
@@ -376,6 +404,11 @@ describe('ClaudeCodeKtxLlmRuntime', () => {
|
|
|
376
404
|
skills: [],
|
|
377
405
|
plugins: [],
|
|
378
406
|
tools: [],
|
|
407
|
+
managedSettings: {
|
|
408
|
+
allowManagedMcpServersOnly: true,
|
|
409
|
+
allowedMcpServers: [],
|
|
410
|
+
},
|
|
411
|
+
strictMcpConfig: true,
|
|
379
412
|
allowedTools: [],
|
|
380
413
|
persistSession: false,
|
|
381
414
|
env: expect.not.objectContaining({ ANTHROPIC_API_KEY: 'sk-ant-test' }),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { KtxProjectConfig } from './config.js';
|
|
2
|
-
export declare const KTX_SETUP_STEPS: readonly ["project", "llm", "embeddings", "databases", "sources", "context", "agents"];
|
|
2
|
+
export declare const KTX_SETUP_STEPS: readonly ["project", "llm", "embeddings", "databases", "sources", "runtime", "context", "agents"];
|
|
3
3
|
export type KtxSetupStep = (typeof KTX_SETUP_STEPS)[number];
|
|
4
4
|
export interface KtxSetupState {
|
|
5
5
|
completed_steps: KtxSetupStep[];
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
export const KTX_SETUP_STEPS = [
|
|
3
|
+
export const KTX_SETUP_STEPS = [
|
|
4
|
+
'project',
|
|
5
|
+
'llm',
|
|
6
|
+
'embeddings',
|
|
7
|
+
'databases',
|
|
8
|
+
'sources',
|
|
9
|
+
'runtime',
|
|
10
|
+
'context',
|
|
11
|
+
'agents',
|
|
12
|
+
];
|
|
4
13
|
const SETUP_GITIGNORE_ENTRIES = [
|
|
5
14
|
'cache/',
|
|
6
15
|
'db.sqlite',
|
|
@@ -16,11 +16,12 @@ describe('KTX setup config helpers', () => {
|
|
|
16
16
|
await markKtxSetupStateStepComplete(tempDir, 'project');
|
|
17
17
|
await markKtxSetupStateStepComplete(tempDir, 'project');
|
|
18
18
|
await markKtxSetupStateStepComplete(tempDir, 'llm');
|
|
19
|
+
await markKtxSetupStateStepComplete(tempDir, 'runtime');
|
|
19
20
|
await markKtxSetupStateStepComplete(tempDir, 'context');
|
|
20
21
|
expect(await readKtxSetupState(tempDir)).toEqual({
|
|
21
|
-
completed_steps: ['project', 'llm', 'context'],
|
|
22
|
+
completed_steps: ['project', 'llm', 'runtime', 'context'],
|
|
22
23
|
});
|
|
23
|
-
await expect(readFile(join(tempDir, '.ktx', 'setup', 'state.json'), 'utf-8')).resolves.toBe(`${JSON.stringify({ completed_steps: ['project', 'llm', 'context'] }, null, 2)}\n`);
|
|
24
|
+
await expect(readFile(join(tempDir, '.ktx', 'setup', 'state.json'), 'utf-8')).resolves.toBe(`${JSON.stringify({ completed_steps: ['project', 'llm', 'runtime', 'context'] }, null, 2)}\n`);
|
|
24
25
|
});
|
|
25
26
|
it('sets setup database connection ids without duplicates', () => {
|
|
26
27
|
const config = buildDefaultKtxProjectConfig();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import YAML from 'yaml';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { addTouchedSlSource, validateActionRawPaths } from '../../tools/index.js';
|
|
3
|
+
import { addTouchedSlSource, validateActionRawPaths, validateActionTargetConnection, } from '../../tools/index.js';
|
|
4
4
|
import { applySqlEdits } from '../../tools/sql-edit-replacer.js';
|
|
5
5
|
import { normalizeSemanticLayerDescriptions } from '../description-normalization.js';
|
|
6
6
|
import { BaseSemanticLayerTool, } from './base-semantic-layer.tool.js';
|
|
@@ -59,6 +59,10 @@ If no source exists yet, use sl_write_source instead — this tool will reject t
|
|
|
59
59
|
const { name: author, email: authorEmail } = await this.authorResolver.resolve(context.userId);
|
|
60
60
|
const semanticLayerService = context.session?.semanticLayerService ?? this.semanticLayerService;
|
|
61
61
|
const skipIndex = context.session?.isWorktreeScoped === true;
|
|
62
|
+
const targetConnectionValidation = validateActionTargetConnection(context.session, connectionId);
|
|
63
|
+
if (!targetConnectionValidation.ok) {
|
|
64
|
+
return this.buildOutput(false, [targetConnectionValidation.error], sourceName);
|
|
65
|
+
}
|
|
62
66
|
const rawPathValidation = validateActionRawPaths(context.session, input.rawPaths);
|
|
63
67
|
if (!rawPathValidation.ok) {
|
|
64
68
|
return this.buildOutput(false, [rawPathValidation.error], sourceName);
|
|
@@ -80,6 +80,21 @@ describe('SlEditSourceTool — session gating', () => {
|
|
|
80
80
|
targetConnectionId: warehouseConnectionId,
|
|
81
81
|
}));
|
|
82
82
|
});
|
|
83
|
+
it('rejects session-scoped edits outside allowed target connections', async () => {
|
|
84
|
+
const { tool } = makeTool();
|
|
85
|
+
const session = makeSession({
|
|
86
|
+
allowedConnectionNames: new Set(['warehouse']),
|
|
87
|
+
});
|
|
88
|
+
const context = { ...baseContext, session };
|
|
89
|
+
const result = await tool.call({
|
|
90
|
+
connectionId: 'finance',
|
|
91
|
+
sourceName: 'orders',
|
|
92
|
+
yaml_edits: [{ oldText: 'measures: []', newText: 'measures: []' }],
|
|
93
|
+
}, context);
|
|
94
|
+
expect(result.structured.success).toBe(false);
|
|
95
|
+
expect(result.markdown).toContain('connectionId "finance" is outside this ingest session');
|
|
96
|
+
expect(session.actions).toEqual([]);
|
|
97
|
+
});
|
|
83
98
|
it('indexes normally when no session is present', async () => {
|
|
84
99
|
const { tool, slSearchService } = makeTool();
|
|
85
100
|
const result = await tool.call({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import YAML from 'yaml';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { addTouchedSlSource, validateActionRawPaths } from '../../tools/index.js';
|
|
3
|
+
import { addTouchedSlSource, validateActionRawPaths, validateActionTargetConnection, } from '../../tools/index.js';
|
|
4
4
|
import { sourceOverlaySchema } from '../schemas.js';
|
|
5
5
|
import { BaseSemanticLayerTool, sourceDefinitionSchema, } from './base-semantic-layer.tool.js';
|
|
6
6
|
import { normalizeSemanticLayerDescriptions } from '../description-normalization.js';
|
|
@@ -83,6 +83,10 @@ Do NOT join back to a table that the SQL already aggregates from if the grain co
|
|
|
83
83
|
const { name: author, email: authorEmail } = await this.authorResolver.resolve(context.userId);
|
|
84
84
|
const semanticLayerService = context.session?.semanticLayerService ?? this.semanticLayerService;
|
|
85
85
|
const skipIndex = context.session?.isWorktreeScoped === true;
|
|
86
|
+
const targetConnectionValidation = validateActionTargetConnection(context.session, connectionId);
|
|
87
|
+
if (!targetConnectionValidation.ok) {
|
|
88
|
+
return this.buildOutput(false, [targetConnectionValidation.error], sourceName);
|
|
89
|
+
}
|
|
86
90
|
const rawPathValidation = validateActionRawPaths(context.session, input.rawPaths);
|
|
87
91
|
if (!rawPathValidation.ok) {
|
|
88
92
|
return this.buildOutput(false, [rawPathValidation.error], sourceName);
|
|
@@ -112,6 +112,28 @@ describe('SlWriteSourceTool — session gating', () => {
|
|
|
112
112
|
targetConnectionId: warehouseConnectionId,
|
|
113
113
|
}));
|
|
114
114
|
});
|
|
115
|
+
it('rejects session-scoped writes outside allowed target connections', async () => {
|
|
116
|
+
const { tool } = makeTool();
|
|
117
|
+
const session = makeSession({
|
|
118
|
+
allowedConnectionNames: new Set(['warehouse']),
|
|
119
|
+
});
|
|
120
|
+
const context = { ...baseContext, session };
|
|
121
|
+
const result = await tool.call({
|
|
122
|
+
connectionId: 'finance',
|
|
123
|
+
sourceName: 'finance_orders',
|
|
124
|
+
source: {
|
|
125
|
+
name: 'finance_orders',
|
|
126
|
+
table: 'public.orders',
|
|
127
|
+
grain: ['id'],
|
|
128
|
+
columns: [{ name: 'id', type: 'string' }],
|
|
129
|
+
measures: [],
|
|
130
|
+
joins: [],
|
|
131
|
+
},
|
|
132
|
+
}, context);
|
|
133
|
+
expect(result.structured.success).toBe(false);
|
|
134
|
+
expect(result.markdown).toContain('connectionId "finance" is outside this ingest session');
|
|
135
|
+
expect(session.actions).toEqual([]);
|
|
136
|
+
});
|
|
115
137
|
it('indexes normally when no session is present', async () => {
|
|
116
138
|
const { tool, slSearchService } = makeTool();
|
|
117
139
|
const result = await tool.call({
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ToolSession } from './tool-session.js';
|
|
2
|
+
type ActionTargetConnectionValidation = {
|
|
3
|
+
ok: true;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
error: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function validateActionTargetConnection(session: ToolSession | undefined, connectionId: string): ActionTargetConnectionValidation;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function validateActionTargetConnection(session, connectionId) {
|
|
2
|
+
const allowed = session?.allowedConnectionNames;
|
|
3
|
+
if (!allowed) {
|
|
4
|
+
return { ok: true };
|
|
5
|
+
}
|
|
6
|
+
if (allowed.has(connectionId)) {
|
|
7
|
+
return { ok: true };
|
|
8
|
+
}
|
|
9
|
+
const allowedList = [...allowed].sort();
|
|
10
|
+
return {
|
|
11
|
+
ok: false,
|
|
12
|
+
error: `connectionId "${connectionId}" is outside this ingest session's allowed target connections: ${allowedList.length > 0 ? allowedList.join(', ') : '(none)'}`,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -13,9 +13,9 @@ declare const contextCandidateWriteInputSchema: z.ZodObject<{
|
|
|
13
13
|
actionHint: z.ZodEnum<{
|
|
14
14
|
merge: "merge";
|
|
15
15
|
conflict: "conflict";
|
|
16
|
-
skip: "skip";
|
|
17
|
-
update: "update";
|
|
18
16
|
create: "create";
|
|
17
|
+
update: "update";
|
|
18
|
+
skip: "skip";
|
|
19
19
|
}>;
|
|
20
20
|
durabilityScore: z.ZodNumber;
|
|
21
21
|
authorityScore: z.ZodNumber;
|
|
@@ -48,9 +48,9 @@ export declare class ContextCandidateWriteTool extends BaseTool<typeof contextCa
|
|
|
48
48
|
actionHint: z.ZodEnum<{
|
|
49
49
|
merge: "merge";
|
|
50
50
|
conflict: "conflict";
|
|
51
|
-
skip: "skip";
|
|
52
|
-
update: "update";
|
|
53
51
|
create: "create";
|
|
52
|
+
update: "update";
|
|
53
|
+
skip: "skip";
|
|
54
54
|
}>;
|
|
55
55
|
durabilityScore: z.ZodNumber;
|
|
56
56
|
authorityScore: z.ZodNumber;
|
|
@@ -14,5 +14,6 @@ export type { SqlEdit } from './sql-edit-replacer.js';
|
|
|
14
14
|
export { applySqlEdits } from './sql-edit-replacer.js';
|
|
15
15
|
export type { IngestToolMetadata, MemoryAction, ToolSession } from './tool-session.js';
|
|
16
16
|
export { validateActionRawPaths } from './action-raw-paths.js';
|
|
17
|
+
export { validateActionTargetConnection } from './action-target-connection.js';
|
|
17
18
|
export type { TouchedSlSource, TouchedSlSourceSet } from './touched-sl-sources.js';
|
|
18
19
|
export { addTouchedSlSource, createTouchedSlSources, deleteTouchedSlSource, hasTouchedSlSource, listTouchedSlSources, touchedSlSourceCount, touchedSlSourceNamesForConnection, } from './touched-sl-sources.js';
|
|
@@ -8,4 +8,5 @@ export { ContextEvidenceSearchTool } from './context-evidence-search.tool.js';
|
|
|
8
8
|
export { ingestMetadataRequired, resolveIngestMetadata } from './context-ingest-metadata.js';
|
|
9
9
|
export { applySqlEdits } from './sql-edit-replacer.js';
|
|
10
10
|
export { validateActionRawPaths } from './action-raw-paths.js';
|
|
11
|
+
export { validateActionTargetConnection } from './action-target-connection.js';
|
|
11
12
|
export { addTouchedSlSource, createTouchedSlSources, deleteTouchedSlSource, hasTouchedSlSource, listTouchedSlSources, touchedSlSourceCount, touchedSlSourceNamesForConnection, } from './touched-sl-sources.js';
|
|
@@ -211,6 +211,7 @@ async function searchLocalKnowledgePagesWithSqlite(project, input) {
|
|
|
211
211
|
},
|
|
212
212
|
{
|
|
213
213
|
lane: 'semantic',
|
|
214
|
+
weight: 3,
|
|
214
215
|
async generate(args) {
|
|
215
216
|
if (!embeddingService) {
|
|
216
217
|
return { status: 'skipped', candidates: [], reason: 'embedding_unconfigured' };
|
|
@@ -222,7 +223,9 @@ async function searchLocalKnowledgePagesWithSqlite(project, input) {
|
|
|
222
223
|
limit: args.laneCandidatePoolLimit,
|
|
223
224
|
});
|
|
224
225
|
return {
|
|
225
|
-
candidates: rows
|
|
226
|
+
candidates: rows
|
|
227
|
+
.filter((row) => row.rawScore > 0)
|
|
228
|
+
.map((row, index) => ({ id: row.id, rank: index + 1, rawScore: row.rawScore })),
|
|
226
229
|
};
|
|
227
230
|
}
|
|
228
231
|
catch (error) {
|
|
@@ -13,6 +13,22 @@ class FakeEmbeddingPort {
|
|
|
13
13
|
return Promise.all(texts.map((text) => this.computeEmbedding(text)));
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
class ArrSynonymEmbeddingPort {
|
|
17
|
+
maxBatchSize = 16;
|
|
18
|
+
async computeEmbedding(text) {
|
|
19
|
+
const lower = text.toLowerCase();
|
|
20
|
+
if (lower.trim() === 'annual recurring revenue' || lower.includes('arr') || lower.includes('contract-first')) {
|
|
21
|
+
return [1, 0];
|
|
22
|
+
}
|
|
23
|
+
if (lower.includes('net revenue') || lower.includes('gross') || lower.includes('refund')) {
|
|
24
|
+
return [0, 1];
|
|
25
|
+
}
|
|
26
|
+
return [0.5, 0.5];
|
|
27
|
+
}
|
|
28
|
+
async computeEmbeddingsBulk(texts) {
|
|
29
|
+
return Promise.all(texts.map((text) => this.computeEmbedding(text)));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
16
32
|
describe('local knowledge helpers', () => {
|
|
17
33
|
let tempDir;
|
|
18
34
|
let project;
|
|
@@ -108,6 +124,34 @@ describe('local knowledge helpers', () => {
|
|
|
108
124
|
lanes: expect.arrayContaining([expect.objectContaining({ lane: 'semantic', status: 'available' })]),
|
|
109
125
|
});
|
|
110
126
|
});
|
|
127
|
+
it('ranks ARR synonym queries by semantic page embeddings over stronger lexical revenue matches', async () => {
|
|
128
|
+
await writeLocalKnowledgePage(project, {
|
|
129
|
+
key: 'arr-definition',
|
|
130
|
+
scope: 'GLOBAL',
|
|
131
|
+
summary: 'ARR is calculated contract-first for active customer contracts.',
|
|
132
|
+
content: 'Contract-first active contract value takes precedence over subscription values.',
|
|
133
|
+
tags: ['arr', 'contracts', 'finance'],
|
|
134
|
+
});
|
|
135
|
+
await writeLocalKnowledgePage(project, {
|
|
136
|
+
key: 'net-revenue-definition',
|
|
137
|
+
scope: 'GLOBAL',
|
|
138
|
+
summary: 'Net revenue definition',
|
|
139
|
+
content: 'Annual revenue is gross invoice revenue minus credits and refunds.',
|
|
140
|
+
tags: ['revenue', 'finance'],
|
|
141
|
+
});
|
|
142
|
+
const search = await searchLocalKnowledgePages(project, {
|
|
143
|
+
query: 'annual recurring revenue',
|
|
144
|
+
userId: 'local',
|
|
145
|
+
limit: 2,
|
|
146
|
+
embeddingService: new ArrSynonymEmbeddingPort(),
|
|
147
|
+
});
|
|
148
|
+
expect(search.map((result) => result.key)).toEqual(['arr-definition', 'net-revenue-definition']);
|
|
149
|
+
expect(search[0]).toMatchObject({
|
|
150
|
+
key: 'arr-definition',
|
|
151
|
+
matchReasons: expect.arrayContaining(['semantic']),
|
|
152
|
+
lanes: expect.arrayContaining([expect.objectContaining({ lane: 'semantic', status: 'available' })]),
|
|
153
|
+
});
|
|
154
|
+
});
|
|
111
155
|
it('reports semantic lane as skipped when wiki embeddings are not configured', async () => {
|
|
112
156
|
await writeLocalKnowledgePage(project, {
|
|
113
157
|
key: 'metrics-revenue',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { validateFlatWikiKey } from '../keys.js';
|
|
3
|
+
import { findMissingWikiRefs } from '../wiki-ref-validation.js';
|
|
3
4
|
import { applySqlEdits } from '../../tools/sql-edit-replacer.js';
|
|
4
5
|
import { BaseTool, validateActionRawPaths } from '../../tools/index.js';
|
|
5
6
|
const MAX_USER_BLOCKS = 100;
|
|
@@ -51,53 +52,6 @@ function normalizeAccidentalEscapedMarkdownNewlines(content) {
|
|
|
51
52
|
return content;
|
|
52
53
|
return content.replace(/\\r\\n/g, '\n').replace(/\\n/g, '\n').replace(/\\r/g, '\n');
|
|
53
54
|
}
|
|
54
|
-
function isWikiPageKeyRef(ref) {
|
|
55
|
-
return /^[a-z0-9][a-z0-9_-]*(?:-[a-z0-9_]+)*$/.test(ref);
|
|
56
|
-
}
|
|
57
|
-
function extractInlineWikiRefs(content) {
|
|
58
|
-
const refs = new Set();
|
|
59
|
-
const re = /\[\[([^\]\n]+)\]\]/g;
|
|
60
|
-
for (const match of content.matchAll(re)) {
|
|
61
|
-
const target = match[1]?.split('|', 1)[0]?.trim();
|
|
62
|
-
if (target && isWikiPageKeyRef(target)) {
|
|
63
|
-
refs.add(target);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return [...refs].sort();
|
|
67
|
-
}
|
|
68
|
-
async function visibleWikiPageKeys(wikiService, scope, scopeId) {
|
|
69
|
-
const keys = new Set();
|
|
70
|
-
if (scope === 'USER') {
|
|
71
|
-
for (const key of await wikiService.listPageKeys('GLOBAL', null)) {
|
|
72
|
-
keys.add(key);
|
|
73
|
-
}
|
|
74
|
-
for (const key of await wikiService.listPageKeys('USER', scopeId)) {
|
|
75
|
-
keys.add(key);
|
|
76
|
-
}
|
|
77
|
-
return keys;
|
|
78
|
-
}
|
|
79
|
-
for (const key of await wikiService.listPageKeys('GLOBAL', null)) {
|
|
80
|
-
keys.add(key);
|
|
81
|
-
}
|
|
82
|
-
return keys;
|
|
83
|
-
}
|
|
84
|
-
async function findMissingWikiRefs(input) {
|
|
85
|
-
const candidates = new Set();
|
|
86
|
-
for (const ref of input.refs ?? []) {
|
|
87
|
-
if (isWikiPageKeyRef(ref)) {
|
|
88
|
-
candidates.add(ref);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
for (const ref of extractInlineWikiRefs(input.content)) {
|
|
92
|
-
candidates.add(ref);
|
|
93
|
-
}
|
|
94
|
-
if (candidates.size === 0) {
|
|
95
|
-
return [];
|
|
96
|
-
}
|
|
97
|
-
const available = await visibleWikiPageKeys(input.wikiService, input.scope, input.scopeId);
|
|
98
|
-
available.add(input.pageKey);
|
|
99
|
-
return [...candidates].filter((ref) => !available.has(ref)).sort();
|
|
100
|
-
}
|
|
101
55
|
export class WikiWriteTool extends BaseTool {
|
|
102
56
|
wikiService;
|
|
103
57
|
pagesRepository;
|
|
@@ -208,7 +162,8 @@ Keys must be flat file names, not directory paths. Use tags/source frontmatter f
|
|
|
208
162
|
refs: finalFm.refs,
|
|
209
163
|
content: finalContent,
|
|
210
164
|
});
|
|
211
|
-
|
|
165
|
+
const deferMissingRefs = !!context.session?.ingest;
|
|
166
|
+
if (!deferMissingRefs && missingRefs.length > 0) {
|
|
212
167
|
return {
|
|
213
168
|
markdown: `Error: wiki references target missing page(s): ${missingRefs.join(', ')}. ` +
|
|
214
169
|
'Create those pages first, retarget the links, or remove the refs.',
|
|
@@ -233,4 +233,32 @@ describe('WikiWriteTool', () => {
|
|
|
233
233
|
expect(result.markdown).toMatch(/orbit-team-lanes-detail/);
|
|
234
234
|
expect(wikiService.writePage).not.toHaveBeenCalled();
|
|
235
235
|
});
|
|
236
|
+
it('accepts forward refs during ingest sessions for post-pass validation', async () => {
|
|
237
|
+
const { tool, wikiService } = makeTool({
|
|
238
|
+
wikiService: {
|
|
239
|
+
listPageKeys: vi.fn().mockResolvedValue(['orbit-company-overview']),
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
const session = {
|
|
243
|
+
connectionId: 'conn-1',
|
|
244
|
+
isWorktreeScoped: true,
|
|
245
|
+
preHead: null,
|
|
246
|
+
touchedSlSources: createTouchedSlSources(),
|
|
247
|
+
actions: [],
|
|
248
|
+
semanticLayerService: {},
|
|
249
|
+
wikiService: wikiService,
|
|
250
|
+
configService: {},
|
|
251
|
+
gitService: {},
|
|
252
|
+
ingest: { runId: 'run-1', jobId: 'job-1', syncId: 'sync-1', sourceKey: 'notion' },
|
|
253
|
+
};
|
|
254
|
+
const result = await tool.call({
|
|
255
|
+
key: 'orbit-how-we-work',
|
|
256
|
+
summary: 'Operating norms',
|
|
257
|
+
content: 'See [[orbit-team-lanes-detail]].',
|
|
258
|
+
refs: ['orbit-company-overview', 'orbit-team-lanes-detail'],
|
|
259
|
+
}, { ...baseContext, session });
|
|
260
|
+
expect(result.structured).toMatchObject({ success: true, key: 'orbit-how-we-work', action: 'created' });
|
|
261
|
+
expect(wikiService.writePage).toHaveBeenCalledTimes(1);
|
|
262
|
+
expect(session.actions).toContainEqual(expect.objectContaining({ target: 'wiki', type: 'created', key: 'orbit-how-we-work' }));
|
|
263
|
+
});
|
|
236
264
|
});
|