@kaelio/ktx 0.11.0 → 0.12.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.11.0-py3-none-any.whl → kaelio_ktx-0.12.0-py3-none-any.whl} +0 -0
- package/assets/python/manifest.json +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/admin.js +1 -1
- package/dist/clack.d.ts +16 -0
- package/dist/clack.js +37 -6
- package/dist/claude-code-prompt-caching.js +1 -1
- package/dist/cli-program.js +3 -3
- package/dist/cli-runtime.js +2 -2
- package/dist/commands/connection-commands.js +1 -1
- package/dist/commands/ingest-commands.js +4 -4
- package/dist/commands/mcp-commands.js +12 -12
- package/dist/commands/runtime-commands.js +4 -4
- package/dist/commands/setup-commands.js +6 -5
- package/dist/commands/sl-commands.js +1 -1
- package/dist/commands/sql-commands.js +1 -1
- package/dist/commands/status-commands.js +1 -1
- package/dist/connection.js +1 -1
- package/dist/connectors/clickhouse/connector.js +1 -1
- package/dist/connectors/mysql/connector.js +1 -1
- package/dist/connectors/snowflake/connector.d.ts +1 -1
- package/dist/connectors/sqlite/connector.js +2 -25
- package/dist/connectors/sqlserver/connector.js +3 -3
- package/dist/context/connections/connection-type.d.ts +1 -1
- package/dist/context/connections/read-only-sql.d.ts +1 -0
- package/dist/context/connections/read-only-sql.js +116 -2
- package/dist/context/core/git.service.d.ts +23 -0
- package/dist/context/core/git.service.js +71 -8
- package/dist/context/ingest/adapters/historic-sql/projection.js +2 -1
- package/dist/context/ingest/adapters/looker/client.js +7 -2
- package/dist/context/ingest/adapters/looker/factory.d.ts +8 -1
- package/dist/context/ingest/adapters/looker/factory.js +9 -0
- package/dist/context/ingest/adapters/looker/mapping.js +1 -1
- package/dist/context/ingest/adapters/looker/types.d.ts +1 -1
- package/dist/context/ingest/adapters/metabase/client.d.ts +1 -1
- package/dist/context/ingest/adapters/metabase/client.js +1 -1
- package/dist/context/ingest/adapters/metabase/local-metabase.adapter.js +1 -1
- package/dist/context/ingest/adapters/metabase/mapping.js +6 -6
- package/dist/context/ingest/artifact-gates.d.ts +2 -6
- package/dist/context/ingest/artifact-gates.js +5 -47
- package/dist/context/ingest/constrained-repair.d.ts +55 -0
- package/dist/context/ingest/constrained-repair.js +167 -0
- package/dist/context/ingest/final-gate-repair.d.ts +9 -11
- package/dist/context/ingest/final-gate-repair.js +40 -128
- package/dist/context/ingest/finalization-scope.d.ts +1 -1
- package/dist/context/ingest/finalization-scope.js +15 -15
- package/dist/context/ingest/ingest-bundle.runner.d.ts +1 -0
- package/dist/context/ingest/ingest-bundle.runner.js +101 -67
- package/dist/context/ingest/isolated-diff/patch-integrator.d.ts +6 -13
- package/dist/context/ingest/isolated-diff/patch-integrator.js +32 -109
- package/dist/context/ingest/isolated-diff/textual-conflict-resolver.d.ts +8 -9
- package/dist/context/ingest/isolated-diff/textual-conflict-resolver.js +63 -141
- package/dist/context/ingest/local-bundle-runtime.d.ts +2 -0
- package/dist/context/ingest/local-bundle-runtime.js +9 -10
- package/dist/context/ingest/local-ingest.d.ts +2 -0
- package/dist/context/ingest/local-ingest.js +2 -0
- package/dist/context/ingest/memory-flow/view-model.js +1 -1
- package/dist/context/ingest/stages/stage-3-work-units.d.ts +2 -6
- package/dist/context/ingest/stages/stage-3-work-units.js +2 -1
- package/dist/context/ingest/stages/validate-wu-sources.d.ts +7 -1
- package/dist/context/ingest/stages/validate-wu-sources.js +109 -4
- package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.d.ts +2 -0
- package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.js +1 -1
- package/dist/context/ingest/tools/warehouse-verification/discover-data.tool.js +3 -3
- package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.d.ts +3 -1
- package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.js +15 -1
- package/dist/context/llm/ai-sdk-runtime.js +2 -2
- package/dist/context/llm/claude-code-runtime.js +1 -1
- package/dist/context/llm/local-config.js +1 -1
- package/dist/context/llm/runtime-tools.js +2 -2
- package/dist/context/mcp/context-tools.js +7 -7
- package/dist/context/mcp/local-project-ports.js +23 -54
- package/dist/context/memory/local-memory.js +4 -1
- package/dist/context/memory/memory-agent.service.js +1 -1
- package/dist/context/project/config.d.ts +11 -4
- package/dist/context/project/config.js +85 -30
- package/dist/context/project/driver-schemas.js +1 -1
- package/dist/context/project/mappings-yaml-schema.js +2 -2
- package/dist/context/project/project.js +12 -4
- package/dist/context/scan/description-generation.js +4 -4
- package/dist/context/scan/local-enrichment-artifacts.js +2 -1
- package/dist/context/scan/local-scan.js +2 -2
- package/dist/context/scan/local-structural-artifacts.js +5 -5
- package/dist/context/scan/relationship-benchmark-report.js +1 -1
- package/dist/context/scan/relationship-discovery.js +3 -3
- package/dist/context/scan/relationship-llm-proposal.js +3 -3
- package/dist/context/sl/local-query.js +3 -33
- package/dist/context/sl/local-sl.d.ts +0 -8
- package/dist/context/sl/local-sl.js +44 -69
- package/dist/context/sl/semantic-layer.service.d.ts +25 -8
- package/dist/context/sl/semantic-layer.service.js +109 -56
- package/dist/context/sl/source-files.d.ts +46 -0
- package/dist/context/sl/source-files.js +131 -0
- package/dist/context/sl/tools/base-semantic-layer.tool.d.ts +2 -2
- package/dist/context/sl/tools/base-semantic-layer.tool.js +2 -7
- package/dist/context/sl/tools/sl-edit-source.tool.js +10 -8
- package/dist/context/sl/tools/sl-warehouse-validation.js +55 -27
- package/dist/context/sl/tools/sl-write-source.tool.js +12 -9
- package/dist/context/sql-analysis/dialect.d.ts +2 -0
- package/dist/context/sql-analysis/dialect.js +20 -0
- package/dist/context/tools/base-tool.d.ts +6 -19
- package/dist/context/tools/base-tool.js +0 -14
- package/dist/context-build-view.js +5 -5
- package/dist/database-tree-picker.js +18 -3
- package/dist/demo-assets.js +0 -1
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +31 -23
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +44 -0
- package/dist/ingest.d.ts +1 -1
- package/dist/ingest.js +8 -2
- package/dist/io/symbols.d.ts +2 -0
- package/dist/io/symbols.js +2 -0
- package/dist/io/tty.d.ts +8 -0
- package/dist/io/tty.js +16 -0
- package/dist/llm/embedding-health.js +1 -1
- package/dist/llm/embedding-provider.js +3 -3
- package/dist/llm/model-provider.js +1 -1
- package/dist/local-adapters.d.ts +1 -0
- package/dist/local-adapters.js +2 -2
- package/dist/local-scan-connectors.js +1 -1
- package/dist/managed-local-embeddings.js +17 -8
- package/dist/managed-mcp-daemon.js +3 -3
- package/dist/managed-python-command.d.ts +7 -0
- package/dist/managed-python-command.js +34 -8
- package/dist/managed-python-daemon.js +2 -2
- package/dist/managed-python-http.js +3 -3
- package/dist/managed-python-runtime.d.ts +30 -1
- package/dist/managed-python-runtime.js +134 -18
- package/dist/managed-uv-release.d.ts +7 -0
- package/dist/managed-uv-release.js +11 -0
- package/dist/mcp-http-server.js +4 -4
- package/dist/mcp-server-factory.js +3 -3
- package/dist/mcp-stdio-server.js +1 -1
- package/dist/memory-flow-hud.js +2 -2
- package/dist/next-steps.js +2 -2
- package/dist/prompt-navigation.d.ts +17 -0
- package/dist/prompt-navigation.js +49 -3
- package/dist/prompts/memory_agent_bundle_ingest_work_unit.md +2 -2
- package/dist/prompts/memory_agent_external_ingest.md +2 -2
- package/dist/public-ingest-copy.js +1 -1
- package/dist/public-ingest.js +3 -3
- package/dist/release-version.js +1 -1
- package/dist/runtime-requirements.js +1 -1
- package/dist/runtime.js +9 -9
- package/dist/scan.js +1 -1
- package/dist/setup-agents.js +21 -30
- package/dist/setup-banner.d.ts +20 -0
- package/dist/setup-banner.js +39 -0
- package/dist/setup-context.js +24 -15
- package/dist/setup-databases.js +31 -59
- package/dist/setup-demo-tour.js +12 -8
- package/dist/setup-embeddings.js +9 -9
- package/dist/setup-interrupt.js +1 -1
- package/dist/setup-models.d.ts +4 -1
- package/dist/setup-models.js +54 -28
- package/dist/setup-project.js +29 -5
- package/dist/setup-prompts.js +16 -1
- package/dist/setup-ready-menu.js +1 -1
- package/dist/setup-sources.js +27 -7
- package/dist/setup.js +13 -13
- package/dist/skills/analytics/SKILL.md +3 -3
- package/dist/skills/dbt_ingest/SKILL.md +3 -3
- package/dist/skills/looker_ingest/SKILL.md +3 -3
- package/dist/skills/lookml_ingest/SKILL.md +7 -7
- package/dist/skills/metabase_ingest/SKILL.md +4 -4
- package/dist/skills/metricflow_ingest/SKILL.md +15 -15
- package/dist/skills/notion_synthesize/SKILL.md +1 -1
- package/dist/skills/sl/SKILL.md +3 -3
- package/dist/skills/sl_capture/SKILL.md +1 -1
- package/dist/skills/wiki_capture/SKILL.md +1 -1
- package/dist/source-mapping.js +1 -1
- package/dist/startup-profile.js +1 -1
- package/dist/status-project.d.ts +0 -2
- package/dist/status-project.js +4 -6
- package/dist/telemetry/events.d.ts +1 -1
- package/dist/telemetry/exception.js +14 -0
- package/dist/text-ingest.js +1 -1
- package/dist/tree-picker-tui.d.ts +0 -1
- package/dist/tree-picker-tui.js +2 -3
- package/package.json +1 -1
|
@@ -7,48 +7,12 @@ import { normalizeSemanticLayerDescriptions } from './description-normalization.
|
|
|
7
7
|
import { sourceDefinitionSchema, sourceOverlaySchema } from './schemas.js';
|
|
8
8
|
import { composeOverlay, projectManifestEntry, SemanticLayerService, toResolvedWire, } from './semantic-layer.service.js';
|
|
9
9
|
import { loadLatestSlDictionaryEntries } from './sl-dictionary-profile.js';
|
|
10
|
+
import { assertSafeConnectionId, isSafeConnectionId, isSlYamlPath, slSourceNameForFile, sourceNameFromPath, } from './source-files.js';
|
|
10
11
|
import { buildSemanticLayerSourceSearchText, SlSearchService } from './sl-search.service.js';
|
|
11
12
|
import { SqliteSlSourcesIndex } from './sqlite-sl-sources-index.js';
|
|
12
|
-
const LOCAL_AUTHOR = 'ktx';
|
|
13
|
-
const LOCAL_AUTHOR_EMAIL = 'ktx@example.com';
|
|
14
|
-
function assertSafePathToken(kind, value) {
|
|
15
|
-
if (value.trim().length === 0 ||
|
|
16
|
-
value.includes('..') ||
|
|
17
|
-
value.includes('\\') ||
|
|
18
|
-
value.startsWith('/') ||
|
|
19
|
-
value.startsWith('.') ||
|
|
20
|
-
value.includes('//')) {
|
|
21
|
-
throw new Error(`Unsafe ${kind}: ${value}`);
|
|
22
|
-
}
|
|
23
|
-
return value;
|
|
24
|
-
}
|
|
25
|
-
function assertSafeConnectionId(connectionId) {
|
|
26
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(connectionId)) {
|
|
27
|
-
throw new Error(`Unsafe connection id: ${connectionId}`);
|
|
28
|
-
}
|
|
29
|
-
return assertSafePathToken('connection id', connectionId);
|
|
30
|
-
}
|
|
31
|
-
function isSafeConnectionId(connectionId) {
|
|
32
|
-
return typeof connectionId === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(connectionId);
|
|
33
|
-
}
|
|
34
|
-
function assertSafeSourceName(sourceName) {
|
|
35
|
-
if (!/^[a-z0-9][a-z0-9_]*$/.test(sourceName)) {
|
|
36
|
-
throw new Error(`Unsafe semantic-layer source name: ${sourceName}`);
|
|
37
|
-
}
|
|
38
|
-
return assertSafePathToken('semantic-layer source name', sourceName);
|
|
39
|
-
}
|
|
40
13
|
function isRecord(value) {
|
|
41
14
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
42
15
|
}
|
|
43
|
-
function slPath(connectionId, sourceName) {
|
|
44
|
-
return `semantic-layer/${assertSafeConnectionId(connectionId)}/${assertSafeSourceName(sourceName)}.yaml`;
|
|
45
|
-
}
|
|
46
|
-
function sourceNameFromPath(path) {
|
|
47
|
-
return (path
|
|
48
|
-
.split('/')
|
|
49
|
-
.at(-1)
|
|
50
|
-
?.replace(/\.ya?ml$/, '') ?? path);
|
|
51
|
-
}
|
|
52
16
|
function parseYamlRecord(raw) {
|
|
53
17
|
const parsed = YAML.parse(raw);
|
|
54
18
|
if (!isRecord(parsed)) {
|
|
@@ -126,11 +90,17 @@ export async function loadLocalSlSourceRecords(project, input) {
|
|
|
126
90
|
const dir = `semantic-layer/${connectionId}`;
|
|
127
91
|
const schemaDir = `${dir}/_schema`;
|
|
128
92
|
const listed = await project.fileStore.listFiles(dir);
|
|
129
|
-
const paths = listed.files.filter(
|
|
93
|
+
const paths = listed.files.filter(isSlYamlPath).sort();
|
|
130
94
|
const sources = new Map();
|
|
131
95
|
for (const path of paths.filter((file) => file.startsWith(`${schemaDir}/`))) {
|
|
132
96
|
const raw = await project.fileStore.readFile(path);
|
|
133
|
-
|
|
97
|
+
let tables;
|
|
98
|
+
try {
|
|
99
|
+
tables = manifestTables(parseYamlRecord(raw.content));
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
throw new Error(`${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
103
|
+
}
|
|
134
104
|
if (!tables) {
|
|
135
105
|
continue;
|
|
136
106
|
}
|
|
@@ -146,7 +116,30 @@ export async function loadLocalSlSourceRecords(project, input) {
|
|
|
146
116
|
}
|
|
147
117
|
for (const path of paths.filter((file) => !file.startsWith(`${schemaDir}/`))) {
|
|
148
118
|
const raw = await project.fileStore.readFile(path);
|
|
149
|
-
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = parseYamlRecord(raw.content);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// A source mid-edit (e.g. an agent saved half-written YAML) must not take
|
|
125
|
+
// down reads, listings, or search for its siblings. Key it by the same
|
|
126
|
+
// name the writer side uses (the intact top-level `name:`, recovered even
|
|
127
|
+
// when the YAML is broken below it; filename only as a last resort) so a
|
|
128
|
+
// broken uppercase/hashed/human-renamed source stays reachable under its
|
|
129
|
+
// real name, and surface the raw content for repair.
|
|
130
|
+
const brokenName = slSourceNameForFile(path, raw.content);
|
|
131
|
+
sources.set(brokenName, {
|
|
132
|
+
connectionId,
|
|
133
|
+
name: brokenName,
|
|
134
|
+
path,
|
|
135
|
+
columnCount: 0,
|
|
136
|
+
measureCount: 0,
|
|
137
|
+
joinCount: 0,
|
|
138
|
+
yaml: raw.content,
|
|
139
|
+
source: { name: brokenName, grain: [], columns: [], joins: [], measures: [] },
|
|
140
|
+
});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
150
143
|
const name = typeof parsed.name === 'string' && parsed.name.length > 0 ? parsed.name : sourceNameFromPath(path);
|
|
151
144
|
if (parsed.table || parsed.sql) {
|
|
152
145
|
const source = parsedStandaloneSource(parsed, name);
|
|
@@ -191,36 +184,18 @@ export async function validateLocalSlSource(rawYaml, options) {
|
|
|
191
184
|
return { valid: false, errors: validationErrors(error) };
|
|
192
185
|
}
|
|
193
186
|
}
|
|
194
|
-
/** @internal */
|
|
195
|
-
export async function writeLocalSlSource(project, input) {
|
|
196
|
-
const validation = await validateLocalSlSource(input.yaml, { project, connectionId: input.connectionId });
|
|
197
|
-
if (!validation.valid) {
|
|
198
|
-
throw new Error(`Invalid semantic-layer source: ${validation.errors.join('; ')}`);
|
|
199
|
-
}
|
|
200
|
-
const parsed = parseYamlRecord(input.yaml);
|
|
201
|
-
if (typeof parsed.name === 'string' && parsed.name !== input.sourceName) {
|
|
202
|
-
throw new Error(`Semantic-layer source name "${parsed.name}" does not match requested path "${input.sourceName}"`);
|
|
203
|
-
}
|
|
204
|
-
const path = slPath(input.connectionId, input.sourceName);
|
|
205
|
-
return project.fileStore.writeFile(path, input.yaml.endsWith('\n') ? input.yaml : `${input.yaml}\n`, LOCAL_AUTHOR, LOCAL_AUTHOR_EMAIL, `Write semantic-layer source: ${input.connectionId}/${input.sourceName}`);
|
|
206
|
-
}
|
|
207
|
-
/** @internal */
|
|
208
187
|
export async function readLocalSlSource(project, input) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
});
|
|
221
|
-
const record = records.find((source) => source.name === input.sourceName);
|
|
222
|
-
return record ? { ...record } : null;
|
|
223
|
-
}
|
|
188
|
+
// Source identity is the in-file `name:` (mirroring the warehouse identifier
|
|
189
|
+
// verbatim, e.g. Snowflake's uppercase `WIDGET_SALES`), never the filename. The
|
|
190
|
+
// record loader resolves standalone files, overlays, manifest-backed sources,
|
|
191
|
+
// and mid-edit files whose YAML no longer parses — so readers — `ktx sl read`,
|
|
192
|
+
// `ktx sl validate`, and the `sl_read_source` MCP tool — can surface broken
|
|
193
|
+
// content for repair instead of failing on it.
|
|
194
|
+
const records = await loadLocalSlSourceRecords(project, {
|
|
195
|
+
connectionId: input.connectionId,
|
|
196
|
+
});
|
|
197
|
+
const record = records.find((source) => source.name === input.sourceName);
|
|
198
|
+
return record ? { ...record } : null;
|
|
224
199
|
}
|
|
225
200
|
export async function resolveLocalSlSource(project, input) {
|
|
226
201
|
if (input.connectionId !== undefined) {
|
|
@@ -38,17 +38,23 @@ export declare class SemanticLayerService {
|
|
|
38
38
|
name: string;
|
|
39
39
|
connectionType: string;
|
|
40
40
|
}>>;
|
|
41
|
-
private
|
|
41
|
+
private resolveWritePath;
|
|
42
42
|
writeSource(connectionId: string, source: SemanticLayerSource, author: string, authorEmail: string, commitMessage?: string, options?: WriteSourceOptions & {
|
|
43
43
|
skipLock?: boolean;
|
|
44
44
|
}): Promise<{
|
|
45
|
+
path: string;
|
|
45
46
|
warnings: string[];
|
|
46
47
|
commitHash?: string | null;
|
|
47
48
|
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Raw standalone/overlay file for a source, resolved by its in-file `name:`.
|
|
51
|
+
* Returns null when no file declares the name (the source may still exist as
|
|
52
|
+
* a manifest entry under `_schema/`).
|
|
53
|
+
*/
|
|
48
54
|
readSourceFile(connectionId: string, sourceName: string): Promise<{
|
|
49
55
|
content: string;
|
|
50
56
|
path: string;
|
|
51
|
-
}>;
|
|
57
|
+
} | null>;
|
|
52
58
|
loadSource(connectionId: string, sourceName: string): Promise<SemanticLayerSource | null>;
|
|
53
59
|
loadAllSources(connectionId: string): Promise<LoadAllSourcesResult>;
|
|
54
60
|
/**
|
|
@@ -87,14 +93,8 @@ export declare class SemanticLayerService {
|
|
|
87
93
|
} | null>;
|
|
88
94
|
validatePhysicalTableReferences(connectionId: string, sources: SemanticLayerSource[]): Promise<string[]>;
|
|
89
95
|
getDialectForConnection(connectionId: string): Promise<string>;
|
|
90
|
-
listSourceNames(connectionId: string): Promise<string[]>;
|
|
91
96
|
listFilesForConnection(connectionId: string): Promise<string[]>;
|
|
92
|
-
readFileByPath(connectionId: string, relativePath: string): Promise<{
|
|
93
|
-
content: string;
|
|
94
|
-
readOnly: boolean;
|
|
95
|
-
}>;
|
|
96
97
|
deleteSource(connectionId: string, sourceName: string, author: string, authorEmail: string): Promise<import("../../context/core/file-store.js").KtxFileWriteResult | null>;
|
|
97
|
-
getSourceHistory(connectionId: string, sourceName: string): Promise<unknown>;
|
|
98
98
|
/**
|
|
99
99
|
* Validate the semantic layer state that *would* exist if `proposedSource`
|
|
100
100
|
* were written, without persisting anything. Used by write/edit tools to
|
|
@@ -203,6 +203,23 @@ export interface ManifestTableEntry {
|
|
|
203
203
|
usage?: TableUsageOutput;
|
|
204
204
|
}
|
|
205
205
|
export declare function projectManifestEntry(name: string, entry: ManifestTableEntry): SemanticLayerSource;
|
|
206
|
+
export interface MissingJoinTarget {
|
|
207
|
+
to: string;
|
|
208
|
+
/** Source whose name matches only case-insensitively, if any — the usual authoring mistake. */
|
|
209
|
+
caseMismatch: string | null;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Join targets that do not exactly match a known source name. The Python
|
|
213
|
+
* engine resolves `joins[].to` by exact name within one connection's source
|
|
214
|
+
* set (`engine._collect_orphan_join_target_errors`) and `query()` raises on a
|
|
215
|
+
* miss, so anything looser here — case-insensitive matches, table refs,
|
|
216
|
+
* sources in other connections — would pass this gate and then fail
|
|
217
|
+
* query/validation as an orphan join target.
|
|
218
|
+
*/
|
|
219
|
+
export declare function findMissingJoinTargets(joins: Array<{
|
|
220
|
+
to: string;
|
|
221
|
+
}> | undefined, knownSourceNames: Iterable<string>): MissingJoinTarget[];
|
|
222
|
+
export declare function formatMissingJoinTarget(missing: MissingJoinTarget): string;
|
|
206
223
|
/**
|
|
207
224
|
* Returns one message per measure-level segment reference that doesn't resolve to
|
|
208
225
|
* a segment defined on the source. Array is empty when every reference checks out.
|
|
@@ -2,6 +2,7 @@ import YAML from 'yaml';
|
|
|
2
2
|
import { noopLogger } from '../../context/core/config.js';
|
|
3
3
|
import { normalizeSemanticLayerDescriptions } from './description-normalization.js';
|
|
4
4
|
import { isOverlaySource, resolvedSourceSchema, sourceDefinitionSchema, sourceOverlaySchema } from './schemas.js';
|
|
5
|
+
import { isSlYamlPath, resolveSlSourceFile, slDeclaredSourceName, slSourceFilePath } from './source-files.js';
|
|
5
6
|
const SL_DIR_PREFIX = 'semantic-layer';
|
|
6
7
|
const CONNECTION_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
|
|
7
8
|
/** @internal */
|
|
@@ -85,7 +86,7 @@ export class SemanticLayerService {
|
|
|
85
86
|
async listConnectionIds() {
|
|
86
87
|
try {
|
|
87
88
|
const result = await this.configService.listFiles(SL_DIR_PREFIX);
|
|
88
|
-
// Directories under semantic-layer/ are connectionIds. Local
|
|
89
|
+
// Directories under semantic-layer/ are connectionIds. Local ktx projects use
|
|
89
90
|
// readable ids like "warehouse" and "dbt-main", not only UUIDs.
|
|
90
91
|
return result.files
|
|
91
92
|
.map((f) => f.replace(`${SL_DIR_PREFIX}/`, '').split('/')[0])
|
|
@@ -104,8 +105,31 @@ export class SemanticLayerService {
|
|
|
104
105
|
return this.connections.listEnabledConnections(ids);
|
|
105
106
|
}
|
|
106
107
|
// ── YAML File Operations ────────────────────────────────
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
// The in-file `name:` is the source's identity; the filename is only a derived
|
|
109
|
+
// label. Rewrites land on the file that already declares the name (humans may
|
|
110
|
+
// rename files freely); new sources get a derived filename. A file already
|
|
111
|
+
// sitting at the derived path that declares a name declares a *different* one
|
|
112
|
+
// (the resolver would have matched it otherwise) — fail instead of clobbering
|
|
113
|
+
// it. A nameless/unparseable file there is the broken remains of this very
|
|
114
|
+
// source (the derived path is a function of the name), so overwriting it is
|
|
115
|
+
// the repair path, not data loss.
|
|
116
|
+
async resolveWritePath(connectionId, sourceName) {
|
|
117
|
+
const existing = await resolveSlSourceFile(this.configService, connectionId, sourceName);
|
|
118
|
+
if (existing) {
|
|
119
|
+
return existing.path;
|
|
120
|
+
}
|
|
121
|
+
const path = slSourceFilePath(connectionId, sourceName);
|
|
122
|
+
let occupant = null;
|
|
123
|
+
try {
|
|
124
|
+
occupant = slDeclaredSourceName((await this.configService.readFile(path)).content);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return path;
|
|
128
|
+
}
|
|
129
|
+
if (occupant !== null) {
|
|
130
|
+
throw new Error(`Cannot write source '${sourceName}': ${path} already defines source '${occupant}'`);
|
|
131
|
+
}
|
|
132
|
+
return path;
|
|
109
133
|
}
|
|
110
134
|
async writeSource(connectionId, source, author, authorEmail, commitMessage, options) {
|
|
111
135
|
// Writes are intentionally permissive — the agent must be able to save broken files so
|
|
@@ -140,38 +164,40 @@ export class SemanticLayerService {
|
|
|
140
164
|
this.logger.warn(`[writeSource] '${source.name}': ${danglingRefs.join('; ')}. Saving anyway.`);
|
|
141
165
|
}
|
|
142
166
|
}
|
|
143
|
-
const path = this.
|
|
167
|
+
const path = await this.resolveWritePath(connectionId, source.name);
|
|
144
168
|
const normalizedSource = normalizeSemanticLayerDescriptions(source);
|
|
145
169
|
const content = YAML.stringify(normalizedSource, { indent: 2, lineWidth: 0, version: '1.1' });
|
|
146
170
|
const message = commitMessage ?? `Update semantic layer source: ${source.name}`;
|
|
147
171
|
const result = await this.configService.writeFile(path, content, author, authorEmail, message, {
|
|
148
172
|
skipLock: options?.skipLock,
|
|
149
173
|
});
|
|
150
|
-
|
|
174
|
+
// The filename is derived from (or resolved by) the source name — surface
|
|
175
|
+
// the actual path so callers don't have to re-resolve it.
|
|
176
|
+
return { ...result, path, warnings };
|
|
151
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Raw standalone/overlay file for a source, resolved by its in-file `name:`.
|
|
180
|
+
* Returns null when no file declares the name (the source may still exist as
|
|
181
|
+
* a manifest entry under `_schema/`).
|
|
182
|
+
*/
|
|
152
183
|
async readSourceFile(connectionId, sourceName) {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
return { content: result.content, path };
|
|
184
|
+
const file = await resolveSlSourceFile(this.configService, connectionId, sourceName);
|
|
185
|
+
return file ? { content: file.content, path: file.path } : null;
|
|
156
186
|
}
|
|
157
187
|
async loadSource(connectionId, sourceName) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const result = await this.readSourceFile(connectionId, sourceName);
|
|
161
|
-
content = result.content;
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
188
|
+
const file = await this.readSourceFile(connectionId, sourceName);
|
|
189
|
+
if (!file) {
|
|
164
190
|
return null;
|
|
165
191
|
}
|
|
166
192
|
try {
|
|
167
|
-
return YAML.parse(content);
|
|
193
|
+
return YAML.parse(file.content);
|
|
168
194
|
}
|
|
169
195
|
catch (error) {
|
|
170
196
|
// Distinguish a YAML parse failure from a missing file. The file exists but
|
|
171
197
|
// its contents are unparseable — callers that treat null as "does not exist"
|
|
172
198
|
// could otherwise overwrite the broken file. Surface the parse failure via
|
|
173
199
|
// the service logger so the broken source is at least visible.
|
|
174
|
-
this.logger.warn(`[loadSource] ${
|
|
200
|
+
this.logger.warn(`[loadSource] ${file.path}: YAML parse failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
175
201
|
return null;
|
|
176
202
|
}
|
|
177
203
|
}
|
|
@@ -182,7 +208,7 @@ export class SemanticLayerService {
|
|
|
182
208
|
let allFiles;
|
|
183
209
|
try {
|
|
184
210
|
const result = await this.configService.listFiles(dir);
|
|
185
|
-
allFiles = result.files.filter((f) => f
|
|
211
|
+
allFiles = result.files.filter((f) => isSlYamlPath(f));
|
|
186
212
|
}
|
|
187
213
|
catch (e) {
|
|
188
214
|
const message = `Failed to list semantic-layer files under ${dir}: ${e instanceof Error ? e.message : String(e)}`;
|
|
@@ -283,7 +309,7 @@ export class SemanticLayerService {
|
|
|
283
309
|
let allFiles;
|
|
284
310
|
try {
|
|
285
311
|
const listing = await this.configService.listFiles(dir);
|
|
286
|
-
allFiles = listing.files.filter((f) => f
|
|
312
|
+
allFiles = listing.files.filter((f) => isSlYamlPath(f));
|
|
287
313
|
}
|
|
288
314
|
catch {
|
|
289
315
|
return result;
|
|
@@ -350,7 +376,7 @@ export class SemanticLayerService {
|
|
|
350
376
|
const schemaDir = `${SL_DIR_PREFIX}/${connectionId}/_schema`;
|
|
351
377
|
try {
|
|
352
378
|
const result = await this.configService.listFiles(schemaDir);
|
|
353
|
-
const yamlFiles = result.files.filter((f) => f
|
|
379
|
+
const yamlFiles = result.files.filter((f) => isSlYamlPath(f));
|
|
354
380
|
for (const filePath of yamlFiles) {
|
|
355
381
|
try {
|
|
356
382
|
const { content } = await this.configService.readFile(filePath);
|
|
@@ -390,7 +416,7 @@ export class SemanticLayerService {
|
|
|
390
416
|
let yamlFiles;
|
|
391
417
|
try {
|
|
392
418
|
const result = await this.configService.listFiles(schemaDir);
|
|
393
|
-
yamlFiles = result.files.filter((f) => f
|
|
419
|
+
yamlFiles = result.files.filter((f) => isSlYamlPath(f));
|
|
394
420
|
}
|
|
395
421
|
catch {
|
|
396
422
|
return null;
|
|
@@ -457,7 +483,7 @@ export class SemanticLayerService {
|
|
|
457
483
|
.filter((c) => !c.expr && !manifestColumns.has(c.name.toLowerCase()))
|
|
458
484
|
.map((c) => c.name);
|
|
459
485
|
if (absentDeclaredColumns.length > 0) {
|
|
460
|
-
errors.push(`${source.name}
|
|
486
|
+
errors.push(`${source.name}: table "${source.table}" matched manifest ${manifestLabel}, ` +
|
|
461
487
|
`but declared column(s) absent from physical table: ${absentDeclaredColumns.join(', ')}. ` +
|
|
462
488
|
`Available columns: ${[...manifestColumns.values()].join(', ')}`);
|
|
463
489
|
}
|
|
@@ -466,7 +492,7 @@ export class SemanticLayerService {
|
|
|
466
492
|
return !declared || (!declared.expr && !manifestColumns.has(grain.toLowerCase()));
|
|
467
493
|
});
|
|
468
494
|
if (missingGrainColumns.length > 0) {
|
|
469
|
-
errors.push(`${source.name}
|
|
495
|
+
errors.push(`${source.name}: grain column(s) absent from physical table "${source.table}": ${missingGrainColumns.join(', ')}`);
|
|
470
496
|
}
|
|
471
497
|
for (const column of declaredColumns) {
|
|
472
498
|
if (!column.expr) {
|
|
@@ -480,7 +506,7 @@ export class SemanticLayerService {
|
|
|
480
506
|
validMeasures: new Set(),
|
|
481
507
|
});
|
|
482
508
|
if (missing.length > 0) {
|
|
483
|
-
errors.push(`${source.name}
|
|
509
|
+
errors.push(`${source.name}: computed column "${column.name}" references unknown column(s): ${missing.join(', ')}`);
|
|
484
510
|
}
|
|
485
511
|
}
|
|
486
512
|
for (const segment of source.segments ?? []) {
|
|
@@ -492,7 +518,7 @@ export class SemanticLayerService {
|
|
|
492
518
|
validMeasures: new Set(),
|
|
493
519
|
});
|
|
494
520
|
if (missing.length > 0) {
|
|
495
|
-
errors.push(`${source.name}
|
|
521
|
+
errors.push(`${source.name}: segment "${segment.name}" references unknown column(s): ${missing.join(', ')}`);
|
|
496
522
|
}
|
|
497
523
|
}
|
|
498
524
|
for (const measure of source.measures ?? []) {
|
|
@@ -504,7 +530,7 @@ export class SemanticLayerService {
|
|
|
504
530
|
validMeasures: measureNames,
|
|
505
531
|
});
|
|
506
532
|
if (exprMissing.length > 0) {
|
|
507
|
-
errors.push(`${source.name}
|
|
533
|
+
errors.push(`${source.name}: measure "${measure.name}" references unknown column(s): ${exprMissing.join(', ')}`);
|
|
508
534
|
}
|
|
509
535
|
if (measure.filter) {
|
|
510
536
|
const filterMissing = missingLocalExpressionRefs({
|
|
@@ -515,7 +541,7 @@ export class SemanticLayerService {
|
|
|
515
541
|
validMeasures: new Set(),
|
|
516
542
|
});
|
|
517
543
|
if (filterMissing.length > 0) {
|
|
518
|
-
errors.push(`${source.name}
|
|
544
|
+
errors.push(`${source.name}: measure "${measure.name}" filter references unknown column(s): ${filterMissing.join(', ')}`);
|
|
519
545
|
}
|
|
520
546
|
}
|
|
521
547
|
}
|
|
@@ -525,7 +551,7 @@ export class SemanticLayerService {
|
|
|
525
551
|
continue;
|
|
526
552
|
}
|
|
527
553
|
if (!validOutputColumns.has(parsed.localColumn.toLowerCase())) {
|
|
528
|
-
errors.push(`${source.name}
|
|
554
|
+
errors.push(`${source.name}: join to "${join.to}" references local column ` +
|
|
529
555
|
`"${parsed.localColumn}" that is not a valid output column`);
|
|
530
556
|
}
|
|
531
557
|
const targetSource = sourcesByName.get(join.to.toLowerCase()) ??
|
|
@@ -533,7 +559,7 @@ export class SemanticLayerService {
|
|
|
533
559
|
if (targetSource) {
|
|
534
560
|
const targetColumns = new Set(targetSource.columns.map((c) => c.name.toLowerCase()));
|
|
535
561
|
if (!targetColumns.has(parsed.targetColumn.toLowerCase())) {
|
|
536
|
-
errors.push(`${source.name}
|
|
562
|
+
errors.push(`${source.name}: join to "${join.to}" references target column ` +
|
|
537
563
|
`"${parsed.targetColumn}" that does not exist on the target source`);
|
|
538
564
|
}
|
|
539
565
|
}
|
|
@@ -548,41 +574,28 @@ export class SemanticLayerService {
|
|
|
548
574
|
}
|
|
549
575
|
return SemanticLayerService.mapDialect(connection.connectionType);
|
|
550
576
|
}
|
|
551
|
-
async listSourceNames(connectionId) {
|
|
552
|
-
const dir = `${SL_DIR_PREFIX}/${connectionId}`;
|
|
553
|
-
try {
|
|
554
|
-
const result = await this.configService.listFiles(dir);
|
|
555
|
-
return result.files.filter((f) => f.endsWith('.yaml')).map((f) => f.replace(`${dir}/`, '').replace('.yaml', ''));
|
|
556
|
-
}
|
|
557
|
-
catch {
|
|
558
|
-
return [];
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
577
|
async listFilesForConnection(connectionId) {
|
|
562
578
|
const dir = `${SL_DIR_PREFIX}/${connectionId}`;
|
|
563
579
|
try {
|
|
564
580
|
const result = await this.configService.listFiles(dir, true);
|
|
565
|
-
return result.files.filter((f) => f
|
|
581
|
+
return result.files.filter((f) => isSlYamlPath(f));
|
|
566
582
|
}
|
|
567
583
|
catch {
|
|
568
584
|
return [];
|
|
569
585
|
}
|
|
570
586
|
}
|
|
571
|
-
async readFileByPath(connectionId, relativePath) {
|
|
572
|
-
const fullPath = `${SL_DIR_PREFIX}/${connectionId}/${relativePath}`;
|
|
573
|
-
const result = await this.configService.readFile(fullPath);
|
|
574
|
-
return {
|
|
575
|
-
content: result.content,
|
|
576
|
-
readOnly: relativePath.startsWith('_schema/'),
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
587
|
async deleteSource(connectionId, sourceName, author, authorEmail) {
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
588
|
+
const file = await resolveSlSourceFile(this.configService, connectionId, sourceName);
|
|
589
|
+
if (!file) {
|
|
590
|
+
// `deleteFile` returns null for a missing path, which would let a no-op
|
|
591
|
+
// delete read as success. Distinguish the two real cases instead.
|
|
592
|
+
if (await this.isManifestBacked(connectionId, sourceName)) {
|
|
593
|
+
throw new Error(`Source '${sourceName}' is defined by the scan manifest (_schema/) and has no overlay file to delete. ` +
|
|
594
|
+
`Rescan the connection to remove it from the manifest.`);
|
|
595
|
+
}
|
|
596
|
+
throw new Error(`Semantic-layer source not found: ${connectionId}/${sourceName}`);
|
|
597
|
+
}
|
|
598
|
+
return this.configService.deleteFile(file.path, author, authorEmail, `Delete semantic layer source: ${sourceName}`);
|
|
586
599
|
}
|
|
587
600
|
/**
|
|
588
601
|
* Validate the semantic layer state that *would* exist if `proposedSource`
|
|
@@ -639,6 +652,16 @@ export class SemanticLayerService {
|
|
|
639
652
|
// any column-without-type errors via the warehouse probe.
|
|
640
653
|
}
|
|
641
654
|
merged.push(toPush);
|
|
655
|
+
// A join target the engine cannot resolve fails every downstream gate and
|
|
656
|
+
// query with the error attributed to the phantom target. Reject it here,
|
|
657
|
+
// on the source that declares it, while the writing agent can still fix it.
|
|
658
|
+
const missingJoinTargets = findMissingJoinTargets(toPush.joins, merged.map((s) => s.name));
|
|
659
|
+
const joinTargetErrors = missingJoinTargets.map((missing) => `${toPush.name}: ${formatMissingJoinTarget(missing)}. Declare joins only to existing ` +
|
|
660
|
+
`semantic-layer sources in this connection, or drop the join and keep the relationship ` +
|
|
661
|
+
`in a column description.`);
|
|
662
|
+
if (joinTargetErrors.length > 0) {
|
|
663
|
+
return { errors: [...loadErrors, ...joinTargetErrors], warnings: [], perSourceWarnings: {} };
|
|
664
|
+
}
|
|
642
665
|
const validatable = merged.filter((s) => s.table != null || s.sql != null);
|
|
643
666
|
if (validatable.length === 0) {
|
|
644
667
|
return { errors: loadErrors, warnings: [], perSourceWarnings: {} };
|
|
@@ -704,7 +727,7 @@ export class SemanticLayerService {
|
|
|
704
727
|
catch {
|
|
705
728
|
return [];
|
|
706
729
|
}
|
|
707
|
-
const schemaFiles = files.filter((file) => /^semantic-layer\/[^/]+\/_schema
|
|
730
|
+
const schemaFiles = files.filter((file) => /^semantic-layer\/[^/]+\/_schema\//.test(file) && isSlYamlPath(file));
|
|
708
731
|
const entries = [];
|
|
709
732
|
for (const filePath of schemaFiles) {
|
|
710
733
|
const connectionId = filePath.split('/')[1];
|
|
@@ -732,7 +755,7 @@ export class SemanticLayerService {
|
|
|
732
755
|
let allFiles;
|
|
733
756
|
try {
|
|
734
757
|
const result = await this.configService.listFiles(dir);
|
|
735
|
-
allFiles = result.files.filter((f) => f
|
|
758
|
+
allFiles = result.files.filter((f) => isSlYamlPath(f));
|
|
736
759
|
}
|
|
737
760
|
catch {
|
|
738
761
|
return warnings;
|
|
@@ -883,7 +906,7 @@ export class SemanticLayerService {
|
|
|
883
906
|
const tables = new Map();
|
|
884
907
|
try {
|
|
885
908
|
const result = await this.configService.listFiles(dir);
|
|
886
|
-
const yamlFiles = result.files.filter((f) => f
|
|
909
|
+
const yamlFiles = result.files.filter((f) => isSlYamlPath(f));
|
|
887
910
|
for (const filePath of yamlFiles) {
|
|
888
911
|
try {
|
|
889
912
|
const { content } = await this.configService.readFile(filePath);
|
|
@@ -1182,6 +1205,36 @@ function parseJoinColumns(on, sourceName, targetName) {
|
|
|
1182
1205
|
}
|
|
1183
1206
|
return { localColumn: left.column, targetColumn: right.column };
|
|
1184
1207
|
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Join targets that do not exactly match a known source name. The Python
|
|
1210
|
+
* engine resolves `joins[].to` by exact name within one connection's source
|
|
1211
|
+
* set (`engine._collect_orphan_join_target_errors`) and `query()` raises on a
|
|
1212
|
+
* miss, so anything looser here — case-insensitive matches, table refs,
|
|
1213
|
+
* sources in other connections — would pass this gate and then fail
|
|
1214
|
+
* query/validation as an orphan join target.
|
|
1215
|
+
*/
|
|
1216
|
+
export function findMissingJoinTargets(joins, knownSourceNames) {
|
|
1217
|
+
const known = new Set();
|
|
1218
|
+
const canonicalByLower = new Map();
|
|
1219
|
+
for (const name of knownSourceNames) {
|
|
1220
|
+
known.add(name);
|
|
1221
|
+
canonicalByLower.set(name.toLowerCase(), name);
|
|
1222
|
+
}
|
|
1223
|
+
const missing = [];
|
|
1224
|
+
for (const join of joins ?? []) {
|
|
1225
|
+
if (known.has(join.to)) {
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
missing.push({ to: join.to, caseMismatch: canonicalByLower.get(join.to.toLowerCase()) ?? null });
|
|
1229
|
+
}
|
|
1230
|
+
return missing;
|
|
1231
|
+
}
|
|
1232
|
+
export function formatMissingJoinTarget(missing) {
|
|
1233
|
+
const hint = missing.caseMismatch
|
|
1234
|
+
? `; join targets are case-sensitive — the source is named "${missing.caseMismatch}"`
|
|
1235
|
+
: '';
|
|
1236
|
+
return `join target "${missing.to}" does not exist${hint}`;
|
|
1237
|
+
}
|
|
1185
1238
|
/**
|
|
1186
1239
|
* Returns one message per measure-level segment reference that doesn't resolve to
|
|
1187
1240
|
* a segment defined on the source. Array is empty when every reference checks out.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { KtxFileStorePort } from '../../context/core/file-store.js';
|
|
2
|
+
export declare function assertSafeConnectionId(connectionId: string): string;
|
|
3
|
+
export declare function isSafeConnectionId(connectionId: string | undefined): connectionId is string;
|
|
4
|
+
export declare function sourceNameFromPath(path: string): string;
|
|
5
|
+
export declare function isSlYamlPath(path: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Derive the filename for a semantic-layer source. Total over all possible
|
|
8
|
+
* source names — never throws.
|
|
9
|
+
*
|
|
10
|
+
* Names that are already safe lowercase snake_case become `<name>.yaml`;
|
|
11
|
+
* anything else becomes `<slug>-<8 hex of sha256(name)>.yaml`. The two ranges
|
|
12
|
+
* are disjoint and the mapping is injective: safe filenames contain no `-`,
|
|
13
|
+
* hashed filenames always end in `-<8 hex>`, and slugs are lowercased so names
|
|
14
|
+
* differing only by case get distinct hashes instead of colliding paths on
|
|
15
|
+
* case-insensitive filesystems (macOS APFS, Windows).
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export declare function slSourceFileName(sourceName: string): string;
|
|
20
|
+
export declare function slSourceFilePath(connectionId: string, sourceName: string): string;
|
|
21
|
+
export interface SlSourceFile {
|
|
22
|
+
path: string;
|
|
23
|
+
content: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function slSourceNameForFile(path: string, content: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* The `name:` a semantic-layer YAML file declares, or null when the file is
|
|
28
|
+
* nameless or so broken even the name is unrecoverable. Null is how
|
|
29
|
+
* `writeSource` tells a genuine name conflict at a derived path apart from the
|
|
30
|
+
* broken remains of the source being written, which a rewrite must repair
|
|
31
|
+
* rather than refuse.
|
|
32
|
+
*
|
|
33
|
+
* Uses `parseDocument`, not `parse`: a file with a syntax error below the
|
|
34
|
+
* `name:` line still parses into a partial tree whose top-level `name:` is
|
|
35
|
+
* intact. `parse` would throw on the same input and drop the source to its
|
|
36
|
+
* filename — wrong for human-renamed files, whose filename is not the name.
|
|
37
|
+
*/
|
|
38
|
+
export declare function slDeclaredSourceName(content: string): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Find the standalone/overlay file that defines `sourceName` for a connection.
|
|
41
|
+
* Returns null when no file declares the name (the source may still exist as a
|
|
42
|
+
* manifest entry under `_schema/`). Throws when more than one file declares the
|
|
43
|
+
* same name — that breaks the one-file-per-name invariant and must be repaired
|
|
44
|
+
* by hand rather than silently picking one.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveSlSourceFile(fileStore: Pick<KtxFileStorePort, 'listFiles' | 'readFile'>, connectionId: string, sourceName: string): Promise<SlSourceFile | null>;
|