@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
|
@@ -9,6 +9,7 @@ import type { SemanticLayerService, SlConnectionCatalogPort, SlSearchService, Sl
|
|
|
9
9
|
import type { ToolContext, ToolSession, TouchedSlSource } from '../tools/index.js';
|
|
10
10
|
import type { KnowledgeIndexPort, KnowledgeWikiService } from '../wiki/index.js';
|
|
11
11
|
import type { CanonicalPin } from './canonical-pins.js';
|
|
12
|
+
import type { IngestTraceLevel } from './ingest-trace.js';
|
|
12
13
|
import type { IngestReportSnapshot } from './reports.js';
|
|
13
14
|
import type { ReconcileCandidateForPrompt, ReconcileCandidateSummary, ReconcilePromptRunState } from './stages/build-reconcile-context.js';
|
|
14
15
|
import type { ReconciliationOutcome } from './stages/stage-4-reconciliation.js';
|
|
@@ -101,6 +102,7 @@ export interface IngestSettingsPort {
|
|
|
101
102
|
workUnitMaxConcurrency?: number;
|
|
102
103
|
workUnitStepBudget?: number;
|
|
103
104
|
workUnitFailureMode?: 'abort' | 'continue';
|
|
105
|
+
ingestTraceLevel?: IngestTraceLevel;
|
|
104
106
|
}
|
|
105
107
|
export interface IngestGitAuthor {
|
|
106
108
|
name: string;
|
|
@@ -112,6 +114,7 @@ export interface IngestStoragePort {
|
|
|
112
114
|
resolveUploadDir(uploadId: string): string;
|
|
113
115
|
resolvePullDir(jobId: string): string;
|
|
114
116
|
resolveTranscriptDir(jobId: string): string;
|
|
117
|
+
resolveTracePath(jobId: string): string;
|
|
115
118
|
}
|
|
116
119
|
export interface IngestCommitMessagePort {
|
|
117
120
|
enqueueForExternalCommit(args: {
|
|
@@ -8,6 +8,10 @@ export declare const ingestReportSnapshotSchema: z.ZodObject<{
|
|
|
8
8
|
sourceKey: z.ZodString;
|
|
9
9
|
createdAt: z.ZodString;
|
|
10
10
|
body: z.ZodObject<{
|
|
11
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
12
|
+
failed: "failed";
|
|
13
|
+
completed: "completed";
|
|
14
|
+
}>>;
|
|
11
15
|
syncId: z.ZodString;
|
|
12
16
|
diffSummary: z.ZodObject<{
|
|
13
17
|
added: z.ZodNumber;
|
|
@@ -34,10 +38,10 @@ export declare const ingestReportSnapshotSchema: z.ZodObject<{
|
|
|
34
38
|
retryRecommended: z.ZodBoolean;
|
|
35
39
|
kind: z.ZodOptional<z.ZodEnum<{
|
|
36
40
|
lookml_connection_mismatch: "lookml_connection_mismatch";
|
|
37
|
-
unmapped_looker_connection: "unmapped_looker_connection";
|
|
38
|
-
unparseable_sql_table_name: "unparseable_sql_table_name";
|
|
39
41
|
looker_template_unresolved: "looker_template_unresolved";
|
|
40
42
|
derived_table_not_supported: "derived_table_not_supported";
|
|
43
|
+
unmapped_looker_connection: "unmapped_looker_connection";
|
|
44
|
+
unparseable_sql_table_name: "unparseable_sql_table_name";
|
|
41
45
|
}>>;
|
|
42
46
|
details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
43
47
|
}, z.core.$strip>>>;
|
|
@@ -54,15 +58,36 @@ export declare const ingestReportSnapshotSchema: z.ZodObject<{
|
|
|
54
58
|
retryRecommended: z.ZodBoolean;
|
|
55
59
|
kind: z.ZodOptional<z.ZodEnum<{
|
|
56
60
|
lookml_connection_mismatch: "lookml_connection_mismatch";
|
|
57
|
-
unmapped_looker_connection: "unmapped_looker_connection";
|
|
58
|
-
unparseable_sql_table_name: "unparseable_sql_table_name";
|
|
59
61
|
looker_template_unresolved: "looker_template_unresolved";
|
|
60
62
|
derived_table_not_supported: "derived_table_not_supported";
|
|
63
|
+
unmapped_looker_connection: "unmapped_looker_connection";
|
|
64
|
+
unparseable_sql_table_name: "unparseable_sql_table_name";
|
|
61
65
|
}>>;
|
|
62
66
|
details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
63
67
|
}, z.core.$strip>>>;
|
|
64
68
|
}, z.core.$strip>>;
|
|
65
69
|
commitSha: z.ZodNullable<z.ZodString>;
|
|
70
|
+
tracePath: z.ZodOptional<z.ZodString>;
|
|
71
|
+
failure: z.ZodOptional<z.ZodObject<{
|
|
72
|
+
phase: z.ZodString;
|
|
73
|
+
message: z.ZodString;
|
|
74
|
+
details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
75
|
+
}, z.core.$strip>>;
|
|
76
|
+
isolatedDiff: z.ZodOptional<z.ZodObject<{
|
|
77
|
+
enabled: z.ZodBoolean;
|
|
78
|
+
integrationWorktreePath: z.ZodOptional<z.ZodString>;
|
|
79
|
+
ingestionBaseSha: z.ZodOptional<z.ZodString>;
|
|
80
|
+
projectionSha: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
81
|
+
acceptedPatches: z.ZodNumber;
|
|
82
|
+
textualConflicts: z.ZodNumber;
|
|
83
|
+
semanticConflicts: z.ZodNumber;
|
|
84
|
+
resolverAttempts: z.ZodDefault<z.ZodNumber>;
|
|
85
|
+
resolverRepairs: z.ZodDefault<z.ZodNumber>;
|
|
86
|
+
resolverFailures: z.ZodDefault<z.ZodNumber>;
|
|
87
|
+
gateRepairAttempts: z.ZodDefault<z.ZodNumber>;
|
|
88
|
+
gateRepairs: z.ZodDefault<z.ZodNumber>;
|
|
89
|
+
gateRepairFailures: z.ZodDefault<z.ZodNumber>;
|
|
90
|
+
}, z.core.$strip>>;
|
|
66
91
|
workUnits: z.ZodArray<z.ZodObject<{
|
|
67
92
|
unitKey: z.ZodString;
|
|
68
93
|
rawFiles: z.ZodArray<z.ZodString>;
|
|
@@ -78,8 +103,8 @@ export declare const ingestReportSnapshotSchema: z.ZodObject<{
|
|
|
78
103
|
}>;
|
|
79
104
|
type: z.ZodEnum<{
|
|
80
105
|
created: "created";
|
|
81
|
-
updated: "updated";
|
|
82
106
|
removed: "removed";
|
|
107
|
+
updated: "updated";
|
|
83
108
|
}>;
|
|
84
109
|
key: z.ZodString;
|
|
85
110
|
detail: z.ZodString;
|
|
@@ -104,8 +129,8 @@ export declare const ingestReportSnapshotSchema: z.ZodObject<{
|
|
|
104
129
|
}>;
|
|
105
130
|
type: z.ZodEnum<{
|
|
106
131
|
created: "created";
|
|
107
|
-
updated: "updated";
|
|
108
132
|
removed: "removed";
|
|
133
|
+
updated: "updated";
|
|
109
134
|
}>;
|
|
110
135
|
key: z.ZodString;
|
|
111
136
|
detail: z.ZodString;
|
|
@@ -138,9 +163,9 @@ export declare const ingestReportSnapshotSchema: z.ZodObject<{
|
|
|
138
163
|
unmappedFallbacks: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
139
164
|
rawPath: z.ZodString;
|
|
140
165
|
reason: z.ZodEnum<{
|
|
166
|
+
no_connection_mapping: "no_connection_mapping";
|
|
141
167
|
looker_template_unresolved: "looker_template_unresolved";
|
|
142
168
|
derived_table_not_supported: "derived_table_not_supported";
|
|
143
|
-
no_connection_mapping: "no_connection_mapping";
|
|
144
169
|
no_physical_table: "no_physical_table";
|
|
145
170
|
multiple_table_references: "multiple_table_references";
|
|
146
171
|
unsupported_dialect: "unsupported_dialect";
|
|
@@ -109,6 +109,11 @@ const sourceFetchReportSchema = z.object({
|
|
|
109
109
|
skipped: z.array(sourceFetchIssueSchema).default([]),
|
|
110
110
|
warnings: z.array(sourceFetchIssueSchema).default([]),
|
|
111
111
|
});
|
|
112
|
+
const ingestReportFailureSchema = z.object({
|
|
113
|
+
phase: z.string().min(1),
|
|
114
|
+
message: z.string().min(1),
|
|
115
|
+
details: z.record(z.string(), z.unknown()).optional(),
|
|
116
|
+
});
|
|
112
117
|
export const ingestReportSnapshotSchema = z
|
|
113
118
|
.object({
|
|
114
119
|
id: z.string().min(1),
|
|
@@ -119,10 +124,30 @@ export const ingestReportSnapshotSchema = z
|
|
|
119
124
|
createdAt: z.string().min(1),
|
|
120
125
|
body: z
|
|
121
126
|
.object({
|
|
127
|
+
status: z.enum(['completed', 'failed']).optional(),
|
|
122
128
|
syncId: z.string().min(1),
|
|
123
129
|
diffSummary: ingestDiffSummarySchema,
|
|
124
130
|
fetch: sourceFetchReportSchema.optional(),
|
|
125
131
|
commitSha: z.string().nullable(),
|
|
132
|
+
tracePath: z.string().optional(),
|
|
133
|
+
failure: ingestReportFailureSchema.optional(),
|
|
134
|
+
isolatedDiff: z
|
|
135
|
+
.object({
|
|
136
|
+
enabled: z.boolean(),
|
|
137
|
+
integrationWorktreePath: z.string().optional(),
|
|
138
|
+
ingestionBaseSha: z.string().optional(),
|
|
139
|
+
projectionSha: z.string().nullable().optional(),
|
|
140
|
+
acceptedPatches: z.number().int().min(0),
|
|
141
|
+
textualConflicts: z.number().int().min(0),
|
|
142
|
+
semanticConflicts: z.number().int().min(0),
|
|
143
|
+
resolverAttempts: z.number().int().min(0).default(0),
|
|
144
|
+
resolverRepairs: z.number().int().min(0).default(0),
|
|
145
|
+
resolverFailures: z.number().int().min(0).default(0),
|
|
146
|
+
gateRepairAttempts: z.number().int().min(0).default(0),
|
|
147
|
+
gateRepairs: z.number().int().min(0).default(0),
|
|
148
|
+
gateRepairFailures: z.number().int().min(0).default(0),
|
|
149
|
+
})
|
|
150
|
+
.optional(),
|
|
126
151
|
workUnits: z.array(z.object({
|
|
127
152
|
unitKey: z.string().min(1),
|
|
128
153
|
rawFiles: z.array(z.string()),
|
|
@@ -194,6 +194,45 @@ describe('parseIngestReportSnapshot', () => {
|
|
|
194
194
|
expect(snapshot.body.provenanceRows).toEqual([]);
|
|
195
195
|
expect(snapshot.body.toolTranscripts).toEqual([]);
|
|
196
196
|
});
|
|
197
|
+
it('parses failed ingest reports with trace and failure details', () => {
|
|
198
|
+
const snapshot = parseIngestReportSnapshot({
|
|
199
|
+
id: 'report-failed',
|
|
200
|
+
runId: 'run-failed',
|
|
201
|
+
jobId: 'job-failed',
|
|
202
|
+
connectionId: 'warehouse',
|
|
203
|
+
sourceKey: 'metabase',
|
|
204
|
+
createdAt: '2026-05-17T12:00:00.000Z',
|
|
205
|
+
body: {
|
|
206
|
+
status: 'failed',
|
|
207
|
+
syncId: 'sync-failed',
|
|
208
|
+
diffSummary: { added: 1, modified: 0, deleted: 0, unchanged: 0 },
|
|
209
|
+
commitSha: null,
|
|
210
|
+
tracePath: '/project/.ktx/ingest-traces/job-failed/trace.jsonl',
|
|
211
|
+
failure: {
|
|
212
|
+
phase: 'final_gates',
|
|
213
|
+
message: 'final artifact gates failed',
|
|
214
|
+
},
|
|
215
|
+
workUnits: [],
|
|
216
|
+
failedWorkUnits: [],
|
|
217
|
+
reconciliationSkipped: true,
|
|
218
|
+
conflictsResolved: [],
|
|
219
|
+
evictionsApplied: [],
|
|
220
|
+
unmappedFallbacks: [],
|
|
221
|
+
evictionInputs: [],
|
|
222
|
+
unresolvedCards: [],
|
|
223
|
+
supersededBy: null,
|
|
224
|
+
overrideOf: null,
|
|
225
|
+
provenanceRows: [],
|
|
226
|
+
toolTranscripts: [],
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
expect(snapshot.body.status).toBe('failed');
|
|
230
|
+
expect(snapshot.body.failure).toEqual({
|
|
231
|
+
phase: 'final_gates',
|
|
232
|
+
message: 'final artifact gates failed',
|
|
233
|
+
});
|
|
234
|
+
expect(snapshot.body.tracePath).toContain('trace.jsonl');
|
|
235
|
+
});
|
|
197
236
|
it('rejects malformed report snapshots with a concise message', () => {
|
|
198
237
|
const report = validReportSnapshot();
|
|
199
238
|
report.body.workUnits[0] = {
|
|
@@ -202,4 +241,89 @@ describe('parseIngestReportSnapshot', () => {
|
|
|
202
241
|
};
|
|
203
242
|
expect(() => parseIngestReportSnapshot(report)).toThrow('Invalid ingest report snapshot');
|
|
204
243
|
});
|
|
244
|
+
it('parses isolated-diff textual resolver counters', () => {
|
|
245
|
+
const snapshot = parseIngestReportSnapshot({
|
|
246
|
+
id: 'report-1',
|
|
247
|
+
runId: 'run-1',
|
|
248
|
+
jobId: 'job-1',
|
|
249
|
+
connectionId: 'warehouse',
|
|
250
|
+
sourceKey: 'metabase',
|
|
251
|
+
createdAt: '2026-05-18T00:00:00.000Z',
|
|
252
|
+
body: {
|
|
253
|
+
status: 'completed',
|
|
254
|
+
syncId: 'sync-1',
|
|
255
|
+
diffSummary: { added: 0, modified: 1, deleted: 0, unchanged: 0 },
|
|
256
|
+
commitSha: 'abc123',
|
|
257
|
+
isolatedDiff: {
|
|
258
|
+
enabled: true,
|
|
259
|
+
acceptedPatches: 2,
|
|
260
|
+
textualConflicts: 1,
|
|
261
|
+
semanticConflicts: 0,
|
|
262
|
+
resolverAttempts: 1,
|
|
263
|
+
resolverRepairs: 1,
|
|
264
|
+
resolverFailures: 0,
|
|
265
|
+
},
|
|
266
|
+
workUnits: [],
|
|
267
|
+
failedWorkUnits: [],
|
|
268
|
+
reconciliationSkipped: true,
|
|
269
|
+
conflictsResolved: [],
|
|
270
|
+
evictionsApplied: [],
|
|
271
|
+
unmappedFallbacks: [],
|
|
272
|
+
artifactResolutions: [],
|
|
273
|
+
evictionInputs: [],
|
|
274
|
+
unresolvedCards: [],
|
|
275
|
+
supersededBy: null,
|
|
276
|
+
overrideOf: null,
|
|
277
|
+
provenanceRows: [],
|
|
278
|
+
toolTranscripts: [],
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
expect(snapshot.body.isolatedDiff).toMatchObject({
|
|
282
|
+
resolverAttempts: 1,
|
|
283
|
+
resolverRepairs: 1,
|
|
284
|
+
resolverFailures: 0,
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
it('parses isolated-diff gate repair counters', () => {
|
|
288
|
+
const snapshot = parseIngestReportSnapshot({
|
|
289
|
+
id: 'report-1',
|
|
290
|
+
runId: 'run-1',
|
|
291
|
+
jobId: 'job-1',
|
|
292
|
+
connectionId: 'warehouse',
|
|
293
|
+
sourceKey: 'metabase',
|
|
294
|
+
createdAt: '2026-05-18T00:00:00.000Z',
|
|
295
|
+
body: {
|
|
296
|
+
status: 'completed',
|
|
297
|
+
syncId: 'sync-1',
|
|
298
|
+
diffSummary: { added: 1, modified: 0, deleted: 0, unchanged: 0 },
|
|
299
|
+
commitSha: 'abc123',
|
|
300
|
+
isolatedDiff: {
|
|
301
|
+
enabled: true,
|
|
302
|
+
acceptedPatches: 1,
|
|
303
|
+
textualConflicts: 0,
|
|
304
|
+
semanticConflicts: 1,
|
|
305
|
+
gateRepairAttempts: 1,
|
|
306
|
+
gateRepairs: 1,
|
|
307
|
+
gateRepairFailures: 0,
|
|
308
|
+
},
|
|
309
|
+
workUnits: [],
|
|
310
|
+
failedWorkUnits: [],
|
|
311
|
+
reconciliationSkipped: true,
|
|
312
|
+
conflictsResolved: [],
|
|
313
|
+
evictionsApplied: [],
|
|
314
|
+
unmappedFallbacks: [],
|
|
315
|
+
evictionInputs: [],
|
|
316
|
+
unresolvedCards: [],
|
|
317
|
+
supersededBy: null,
|
|
318
|
+
overrideOf: null,
|
|
319
|
+
provenanceRows: [],
|
|
320
|
+
toolTranscripts: [],
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
expect(snapshot.body.isolatedDiff).toMatchObject({
|
|
324
|
+
gateRepairAttempts: 1,
|
|
325
|
+
gateRepairs: 1,
|
|
326
|
+
gateRepairFailures: 0,
|
|
327
|
+
});
|
|
328
|
+
});
|
|
205
329
|
});
|
|
@@ -37,11 +37,34 @@ export interface IngestReportPostProcessorOutcome {
|
|
|
37
37
|
warnings: string[];
|
|
38
38
|
touchedSources: TouchedSlSource[];
|
|
39
39
|
}
|
|
40
|
+
export interface IngestReportFailure {
|
|
41
|
+
phase: string;
|
|
42
|
+
message: string;
|
|
43
|
+
details?: Record<string, unknown>;
|
|
44
|
+
}
|
|
40
45
|
export interface IngestReportBody {
|
|
46
|
+
status?: 'completed' | 'failed';
|
|
41
47
|
syncId: string;
|
|
42
48
|
diffSummary: IngestDiffSummary;
|
|
43
49
|
fetch?: SourceFetchReport;
|
|
44
50
|
commitSha: string | null;
|
|
51
|
+
tracePath?: string;
|
|
52
|
+
failure?: IngestReportFailure;
|
|
53
|
+
isolatedDiff?: {
|
|
54
|
+
enabled: boolean;
|
|
55
|
+
integrationWorktreePath?: string;
|
|
56
|
+
ingestionBaseSha?: string;
|
|
57
|
+
projectionSha?: string | null;
|
|
58
|
+
acceptedPatches: number;
|
|
59
|
+
textualConflicts: number;
|
|
60
|
+
semanticConflicts: number;
|
|
61
|
+
resolverAttempts?: number;
|
|
62
|
+
resolverRepairs?: number;
|
|
63
|
+
resolverFailures?: number;
|
|
64
|
+
gateRepairAttempts?: number;
|
|
65
|
+
gateRepairs?: number;
|
|
66
|
+
gateRepairFailures?: number;
|
|
67
|
+
};
|
|
45
68
|
workUnits: IngestReportWorkUnit[];
|
|
46
69
|
failedWorkUnits: string[];
|
|
47
70
|
reconciliationSkipped: boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface SemanticLayerTargetPolicyInput {
|
|
2
|
+
paths: readonly string[];
|
|
3
|
+
allowedConnectionIds: ReadonlySet<string>;
|
|
4
|
+
}
|
|
5
|
+
export interface SemanticLayerTargetPolicyViolation {
|
|
6
|
+
path: string;
|
|
7
|
+
connectionId: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function semanticLayerConnectionIdFromPath(path: string): string | null;
|
|
10
|
+
export declare function findDisallowedSemanticLayerTargetPaths(input: SemanticLayerTargetPolicyInput): SemanticLayerTargetPolicyViolation[];
|
|
11
|
+
export declare function assertSemanticLayerTargetPathsAllowed(input: SemanticLayerTargetPolicyInput): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function semanticLayerConnectionIdFromPath(path) {
|
|
2
|
+
const normalized = path.replace(/^[ab]\//, '');
|
|
3
|
+
const match = /^semantic-layer\/([^/]+)\//.exec(normalized);
|
|
4
|
+
return match?.[1] ?? null;
|
|
5
|
+
}
|
|
6
|
+
export function findDisallowedSemanticLayerTargetPaths(input) {
|
|
7
|
+
return input.paths
|
|
8
|
+
.map((path) => ({ path, connectionId: semanticLayerConnectionIdFromPath(path) }))
|
|
9
|
+
.filter((entry) => {
|
|
10
|
+
return entry.connectionId !== null && !input.allowedConnectionIds.has(entry.connectionId);
|
|
11
|
+
})
|
|
12
|
+
.sort((left, right) => {
|
|
13
|
+
const byConnection = left.connectionId.localeCompare(right.connectionId);
|
|
14
|
+
return byConnection === 0 ? left.path.localeCompare(right.path) : byConnection;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function assertSemanticLayerTargetPathsAllowed(input) {
|
|
18
|
+
const violations = findDisallowedSemanticLayerTargetPaths(input);
|
|
19
|
+
if (violations.length === 0) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const allowed = [...input.allowedConnectionIds].sort();
|
|
23
|
+
throw new Error(`semantic-layer target connection not allowed: ${violations
|
|
24
|
+
.map((violation) => `${violation.path} (${violation.connectionId})`)
|
|
25
|
+
.join(', ')}; allowed: ${allowed.length > 0 ? allowed.join(', ') : '(none)'}`);
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { assertSemanticLayerTargetPathsAllowed, findDisallowedSemanticLayerTargetPaths, semanticLayerConnectionIdFromPath, } from './semantic-layer-target-policy.js';
|
|
3
|
+
describe('semantic-layer target policy', () => {
|
|
4
|
+
it('extracts connection ids from semantic-layer paths', () => {
|
|
5
|
+
expect(semanticLayerConnectionIdFromPath('semantic-layer/warehouse/orders.yaml')).toBe('warehouse');
|
|
6
|
+
expect(semanticLayerConnectionIdFromPath('a/semantic-layer/finance/orders.yaml')).toBe('finance');
|
|
7
|
+
expect(semanticLayerConnectionIdFromPath('wiki/global/orders.md')).toBeNull();
|
|
8
|
+
});
|
|
9
|
+
it('finds semantic-layer paths outside the allowed target connections', () => {
|
|
10
|
+
expect(findDisallowedSemanticLayerTargetPaths({
|
|
11
|
+
paths: [
|
|
12
|
+
'semantic-layer/warehouse/orders.yaml',
|
|
13
|
+
'semantic-layer/finance/orders.yaml',
|
|
14
|
+
'wiki/global/orders.md',
|
|
15
|
+
],
|
|
16
|
+
allowedConnectionIds: new Set(['warehouse']),
|
|
17
|
+
})).toEqual([{ path: 'semantic-layer/finance/orders.yaml', connectionId: 'finance' }]);
|
|
18
|
+
});
|
|
19
|
+
it('throws a deterministic error for unauthorized semantic-layer targets', () => {
|
|
20
|
+
expect(() => assertSemanticLayerTargetPathsAllowed({
|
|
21
|
+
paths: ['semantic-layer/finance/orders.yaml', 'semantic-layer/marketing/accounts.yaml'],
|
|
22
|
+
allowedConnectionIds: new Set(['warehouse']),
|
|
23
|
+
})).toThrow(/semantic-layer target connection not allowed: semantic-layer\/finance\/orders\.yaml \(finance\), semantic-layer\/marketing\/accounts\.yaml \(marketing\); allowed: warehouse/);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -12,6 +12,7 @@ export interface WorkUnitExecutionDeps {
|
|
|
12
12
|
revParseHead(): Promise<string | null>;
|
|
13
13
|
};
|
|
14
14
|
agentRunner: AgentRunnerPort;
|
|
15
|
+
validateWikiRefs?: (actions: MemoryAction[]) => Promise<string[]>;
|
|
15
16
|
validateTouchedSources: (touched: TouchedSlSource[]) => Promise<TouchedValidationResult>;
|
|
16
17
|
resetHardTo: (targetSha: string) => Promise<void>;
|
|
17
18
|
buildSystemPrompt: (wu: WorkUnit) => string;
|
|
@@ -40,5 +41,8 @@ export interface WorkUnitOutcome {
|
|
|
40
41
|
touchedSlSources: TouchedSlSource[];
|
|
41
42
|
slDisallowed?: boolean;
|
|
42
43
|
slDisallowedReason?: 'lookml_connection_mismatch';
|
|
44
|
+
patchPath?: string;
|
|
45
|
+
patchTouchedPaths?: string[];
|
|
46
|
+
childWorktreePath?: string;
|
|
43
47
|
}
|
|
44
48
|
export declare function executeWorkUnit(deps: WorkUnitExecutionDeps, wu: WorkUnit): Promise<WorkUnitOutcome>;
|
|
@@ -81,6 +81,10 @@ export async function executeWorkUnit(deps, wu) {
|
|
|
81
81
|
if (toolFailureCount > 0) {
|
|
82
82
|
return failWithReset(`${toolFailureCount} tool call(s) failed during WorkUnit ${wu.unitKey}`);
|
|
83
83
|
}
|
|
84
|
+
const danglingWikiRefs = (await deps.validateWikiRefs?.(deps.sessionActions)) ?? [];
|
|
85
|
+
if (danglingWikiRefs.length > 0) {
|
|
86
|
+
return failWithReset(`wiki references target missing page(s): ${danglingWikiRefs.join(', ')}`);
|
|
87
|
+
}
|
|
84
88
|
const touched = listTouchedSlSources(deps.captureSession.touchedSlSources);
|
|
85
89
|
if (touched.length > 0) {
|
|
86
90
|
const validation = await deps.validateTouchedSources(touched);
|
|
@@ -108,6 +108,35 @@ describe('Stage 3 — executeWorkUnit', () => {
|
|
|
108
108
|
expect(outcome.touchedSlSources).toEqual([]);
|
|
109
109
|
expect(deps.resetHardTo).toHaveBeenCalledWith('pre');
|
|
110
110
|
});
|
|
111
|
+
it('dangling wiki refs reset to the pre-WU SHA and mark WU failed after the agent loop', async () => {
|
|
112
|
+
const deps = makeDeps();
|
|
113
|
+
deps.sessionWorktreeGit.revParseHead = vi.fn().mockResolvedValueOnce('pre').mockResolvedValueOnce('post');
|
|
114
|
+
deps.agentRunner.runLoop = vi.fn().mockImplementation(() => {
|
|
115
|
+
deps.sessionActions.push({ target: 'wiki', type: 'created', key: 'page-a', detail: 'Page A' });
|
|
116
|
+
return Promise.resolve({ stopReason: 'natural' });
|
|
117
|
+
});
|
|
118
|
+
deps.validateWikiRefs = vi.fn().mockResolvedValue(['page-a -> page-b']);
|
|
119
|
+
const outcome = await executeWorkUnit(deps, makeWu());
|
|
120
|
+
expect(outcome.status).toBe('failed');
|
|
121
|
+
expect(outcome.reason).toContain('wiki references target missing page(s): page-a -> page-b');
|
|
122
|
+
expect(outcome.actions).toEqual([]);
|
|
123
|
+
expect(outcome.touchedSlSources).toEqual([]);
|
|
124
|
+
expect(deps.resetHardTo).toHaveBeenCalledWith('pre');
|
|
125
|
+
});
|
|
126
|
+
it('resolved wiki refs pass post-WU validation and preserve actions', async () => {
|
|
127
|
+
const deps = makeDeps();
|
|
128
|
+
deps.sessionWorktreeGit.revParseHead = vi.fn().mockResolvedValueOnce('pre').mockResolvedValueOnce('post');
|
|
129
|
+
deps.agentRunner.runLoop = vi.fn().mockImplementation(() => {
|
|
130
|
+
deps.sessionActions.push({ target: 'wiki', type: 'created', key: 'page-a', detail: 'Page A' });
|
|
131
|
+
deps.sessionActions.push({ target: 'wiki', type: 'created', key: 'page-b', detail: 'Page B' });
|
|
132
|
+
return Promise.resolve({ stopReason: 'natural' });
|
|
133
|
+
});
|
|
134
|
+
deps.validateWikiRefs = vi.fn().mockResolvedValue([]);
|
|
135
|
+
const outcome = await executeWorkUnit(deps, makeWu());
|
|
136
|
+
expect(outcome.status).toBe('success');
|
|
137
|
+
expect(outcome.actions.map((action) => action.key)).toEqual(['page-a', 'page-b']);
|
|
138
|
+
expect(deps.resetHardTo).not.toHaveBeenCalled();
|
|
139
|
+
});
|
|
111
140
|
it('runner loop thrown exception resets to the pre-WU SHA and marks WU failed', async () => {
|
|
112
141
|
const deps = makeDeps();
|
|
113
142
|
deps.sessionWorktreeGit.revParseHead = vi.fn().mockResolvedValueOnce('pre').mockResolvedValueOnce('post');
|
|
@@ -6,7 +6,7 @@ interface EmitUnmappedFallbackDeps {
|
|
|
6
6
|
}
|
|
7
7
|
export declare function createEmitUnmappedFallbackTool(deps: EmitUnmappedFallbackDeps): import("ai").Tool<{
|
|
8
8
|
rawPath: string;
|
|
9
|
-
reason: "
|
|
9
|
+
reason: "no_connection_mapping" | "looker_template_unresolved" | "derived_table_not_supported" | "no_physical_table" | "multiple_table_references" | "unsupported_dialect" | "parse_error" | "missing_target_table" | "cumulative_metric_unsupported" | "conversion_metric_unsupported";
|
|
10
10
|
fallback: "sql_standalone" | "wiki_only" | "flagged";
|
|
11
11
|
tableRef?: string | undefined;
|
|
12
12
|
clarification?: string | undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { KtxEmbeddingPort } from '../core/embedding.js';
|
|
2
|
+
import type { SemanticLayerService } from '../sl/index.js';
|
|
2
3
|
import type { MemoryFlowEventSink } from './memory-flow/types.js';
|
|
3
4
|
export type IngestTrigger = 'upload' | 'scheduled_pull' | 'manual_resync' | 'manual_override';
|
|
4
5
|
export interface DiffSet {
|
|
@@ -40,6 +41,7 @@ export interface ChunkResult {
|
|
|
40
41
|
export interface FetchContext {
|
|
41
42
|
connectionId: string;
|
|
42
43
|
sourceKey: string;
|
|
44
|
+
memoryFlow?: MemoryFlowEventSink;
|
|
43
45
|
}
|
|
44
46
|
type SourceFetchIssueKind = 'unmapped_looker_connection' | 'unparseable_sql_table_name' | 'looker_template_unresolved' | 'derived_table_not_supported' | 'lookml_connection_mismatch';
|
|
45
47
|
export interface SourceFetchIssue {
|
|
@@ -76,6 +78,27 @@ export interface ClusterWorkUnitsContext {
|
|
|
76
78
|
stagedDir: string;
|
|
77
79
|
embedding: KtxEmbeddingPort;
|
|
78
80
|
}
|
|
81
|
+
export interface DeterministicProjectionContext {
|
|
82
|
+
connectionId: string;
|
|
83
|
+
sourceKey: string;
|
|
84
|
+
syncId: string;
|
|
85
|
+
jobId: string;
|
|
86
|
+
runId: string;
|
|
87
|
+
stagedDir: string;
|
|
88
|
+
workdir: string;
|
|
89
|
+
parseArtifacts?: unknown;
|
|
90
|
+
semanticLayerService: SemanticLayerService;
|
|
91
|
+
}
|
|
92
|
+
export interface ProjectionResult {
|
|
93
|
+
warnings: string[];
|
|
94
|
+
errors: string[];
|
|
95
|
+
touchedSources: Array<{
|
|
96
|
+
connectionId: string;
|
|
97
|
+
sourceName: string;
|
|
98
|
+
}>;
|
|
99
|
+
changedWikiPageKeys: string[];
|
|
100
|
+
result?: unknown;
|
|
101
|
+
}
|
|
79
102
|
export interface SourceAdapter {
|
|
80
103
|
readonly source: string;
|
|
81
104
|
readonly skillNames: string[];
|
|
@@ -89,6 +112,7 @@ export interface SourceAdapter {
|
|
|
89
112
|
listTargetConnectionIds?(stagedDir: string): Promise<string[]>;
|
|
90
113
|
chunk(stagedDir: string, diffSet?: DiffSet): Promise<ChunkResult>;
|
|
91
114
|
clusterWorkUnits?(ctx: ClusterWorkUnitsContext): Promise<WorkUnit[]>;
|
|
115
|
+
project?(ctx: DeterministicProjectionContext): Promise<ProjectionResult>;
|
|
92
116
|
describeScope?(stagedDir: string): Promise<ScopeDescriptor>;
|
|
93
117
|
onPullSucceeded?(ctx: {
|
|
94
118
|
connectionId: string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SemanticLayerSource } from '../sl/index.js';
|
|
2
|
+
export type WikiBodyRef = {
|
|
3
|
+
kind: 'sl_entity';
|
|
4
|
+
connectionId: string | null;
|
|
5
|
+
sourceName: string;
|
|
6
|
+
entityName: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'sl_source';
|
|
9
|
+
connectionId: string | null;
|
|
10
|
+
sourceName: string;
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'table';
|
|
13
|
+
connectionId: string | null;
|
|
14
|
+
tableRef: string;
|
|
15
|
+
};
|
|
16
|
+
export interface WikiBodyRefValidationInput {
|
|
17
|
+
pageKey: string;
|
|
18
|
+
body: string;
|
|
19
|
+
visibleConnectionIds: string[];
|
|
20
|
+
loadSources(connectionId: string): Promise<SemanticLayerSource[]>;
|
|
21
|
+
tableExists(connectionId: string, tableRef: string): Promise<boolean>;
|
|
22
|
+
}
|
|
23
|
+
export declare function parseWikiBodyRefs(body: string): WikiBodyRef[];
|
|
24
|
+
export declare function findInvalidWikiBodyRefs(input: WikiBodyRefValidationInput): Promise<string[]>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const inlineCodePattern = /`([^`\n]+)`/g;
|
|
2
|
+
function visibleLinesOutsideFences(body) {
|
|
3
|
+
const lines = [];
|
|
4
|
+
let fenced = false;
|
|
5
|
+
for (const line of body.split('\n')) {
|
|
6
|
+
if (/^\s*```/.test(line)) {
|
|
7
|
+
fenced = !fenced;
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
if (!fenced) {
|
|
11
|
+
lines.push(line);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return lines;
|
|
15
|
+
}
|
|
16
|
+
function parseConnectionScoped(value) {
|
|
17
|
+
const slash = value.indexOf('/');
|
|
18
|
+
if (slash <= 0) {
|
|
19
|
+
return { connectionId: null, body: value };
|
|
20
|
+
}
|
|
21
|
+
return { connectionId: value.slice(0, slash), body: value.slice(slash + 1) };
|
|
22
|
+
}
|
|
23
|
+
function isIdentifierToken(value) {
|
|
24
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
|
25
|
+
}
|
|
26
|
+
export function parseWikiBodyRefs(body) {
|
|
27
|
+
const refs = [];
|
|
28
|
+
for (const line of visibleLinesOutsideFences(body)) {
|
|
29
|
+
for (const match of line.matchAll(inlineCodePattern)) {
|
|
30
|
+
const token = (match[1] ?? '').trim();
|
|
31
|
+
if (!token) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const scoped = parseConnectionScoped(token);
|
|
35
|
+
if (scoped.body.startsWith('source:')) {
|
|
36
|
+
const sourceName = scoped.body.slice('source:'.length).trim();
|
|
37
|
+
if (sourceName) {
|
|
38
|
+
refs.push({ kind: 'sl_source', connectionId: scoped.connectionId, sourceName });
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (scoped.body.startsWith('table:')) {
|
|
43
|
+
const tableRef = scoped.body.slice('table:'.length).trim();
|
|
44
|
+
if (tableRef) {
|
|
45
|
+
refs.push({ kind: 'table', connectionId: scoped.connectionId, tableRef });
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const parts = scoped.body.split('.');
|
|
50
|
+
if (parts.length === 2 && isIdentifierToken(parts[0] ?? '') && isIdentifierToken(parts[1] ?? '')) {
|
|
51
|
+
refs.push({
|
|
52
|
+
kind: 'sl_entity',
|
|
53
|
+
connectionId: scoped.connectionId,
|
|
54
|
+
sourceName: parts[0],
|
|
55
|
+
entityName: parts[1],
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return refs;
|
|
61
|
+
}
|
|
62
|
+
function entityNames(source) {
|
|
63
|
+
return new Set([
|
|
64
|
+
...(source.measures ?? []).map((measure) => measure.name),
|
|
65
|
+
...(source.columns ?? []).map((column) => column.name),
|
|
66
|
+
...(source.segments ?? []).map((segment) => segment.name),
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
export async function findInvalidWikiBodyRefs(input) {
|
|
70
|
+
const errors = [];
|
|
71
|
+
const sourceCache = new Map();
|
|
72
|
+
const loadSources = async (connectionId) => {
|
|
73
|
+
const cached = sourceCache.get(connectionId);
|
|
74
|
+
if (cached) {
|
|
75
|
+
return cached;
|
|
76
|
+
}
|
|
77
|
+
const sources = await input.loadSources(connectionId);
|
|
78
|
+
sourceCache.set(connectionId, sources);
|
|
79
|
+
return sources;
|
|
80
|
+
};
|
|
81
|
+
const findSource = async (connectionIds, sourceName) => {
|
|
82
|
+
for (const connectionId of connectionIds) {
|
|
83
|
+
const source = (await loadSources(connectionId)).find((candidate) => candidate.name === sourceName);
|
|
84
|
+
if (source) {
|
|
85
|
+
return { connectionId, source };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
};
|
|
90
|
+
for (const ref of parseWikiBodyRefs(input.body)) {
|
|
91
|
+
const connectionIds = ref.connectionId ? [ref.connectionId] : input.visibleConnectionIds;
|
|
92
|
+
if (ref.kind === 'table') {
|
|
93
|
+
const found = await Promise.all(connectionIds.map((connectionId) => input.tableExists(connectionId, ref.tableRef)));
|
|
94
|
+
if (!found.some(Boolean)) {
|
|
95
|
+
errors.push(`${input.pageKey}: unknown raw table ${ref.connectionId ? `${ref.connectionId}/` : ''}${ref.tableRef}`);
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const found = await findSource(connectionIds, ref.sourceName);
|
|
100
|
+
if (!found) {
|
|
101
|
+
if (ref.kind === 'sl_source') {
|
|
102
|
+
errors.push(`${input.pageKey}: unknown semantic-layer source ${ref.connectionId ? `${ref.connectionId}/` : ''}${ref.sourceName}`);
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (ref.kind === 'sl_entity' && !entityNames(found.source).has(ref.entityName)) {
|
|
107
|
+
errors.push(`${input.pageKey}: unknown semantic-layer entity ${ref.sourceName}.${ref.entityName}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return errors;
|
|
111
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|