@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,149 @@
|
|
|
1
|
+
import { findMissingWikiRefs } from '../wiki/wiki-ref-validation.js';
|
|
2
|
+
import { findInvalidWikiBodyRefs } from './wiki-body-refs.js';
|
|
3
|
+
function parseSlRef(ref) {
|
|
4
|
+
const withoutConnection = ref.includes('/') ? ref.slice(ref.indexOf('/') + 1) : ref;
|
|
5
|
+
const connectionId = ref.includes('/') ? ref.slice(0, ref.indexOf('/')) : null;
|
|
6
|
+
const [sourceName = '', entityName = null] = withoutConnection.split('.', 2);
|
|
7
|
+
return { connectionId, sourceName, entityName };
|
|
8
|
+
}
|
|
9
|
+
function slEntityNames(source) {
|
|
10
|
+
return new Set([
|
|
11
|
+
...(source.measures ?? []).map((measure) => measure.name),
|
|
12
|
+
...(source.columns ?? []).map((column) => column.name),
|
|
13
|
+
...(source.segments ?? []).map((segment) => segment.name),
|
|
14
|
+
]);
|
|
15
|
+
}
|
|
16
|
+
function uniqueTouchedSources(sources) {
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
const unique = [];
|
|
19
|
+
for (const source of sources) {
|
|
20
|
+
const key = `${source.connectionId}:${source.sourceName}`;
|
|
21
|
+
if (seen.has(key)) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
seen.add(key);
|
|
25
|
+
unique.push(source);
|
|
26
|
+
}
|
|
27
|
+
return unique.sort((left, right) => {
|
|
28
|
+
const byConnection = left.connectionId.localeCompare(right.connectionId);
|
|
29
|
+
return byConnection === 0 ? left.sourceName.localeCompare(right.sourceName) : byConnection;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async function expandTouchedSlSourcesWithDirectJoinNeighbors(input) {
|
|
33
|
+
const expanded = [...input.touchedSlSources];
|
|
34
|
+
const touchedByConnection = new Map();
|
|
35
|
+
for (const source of input.touchedSlSources) {
|
|
36
|
+
const bucket = touchedByConnection.get(source.connectionId) ?? new Set();
|
|
37
|
+
bucket.add(source.sourceName);
|
|
38
|
+
touchedByConnection.set(source.connectionId, bucket);
|
|
39
|
+
}
|
|
40
|
+
for (const connectionId of input.connectionIds) {
|
|
41
|
+
const touched = touchedByConnection.get(connectionId);
|
|
42
|
+
if (!touched || touched.size === 0) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const { sources } = await input.semanticLayerService.loadAllSources(connectionId);
|
|
46
|
+
for (const source of sources) {
|
|
47
|
+
const sourceIsTouched = touched.has(source.name);
|
|
48
|
+
if (sourceIsTouched) {
|
|
49
|
+
for (const join of source.joins ?? []) {
|
|
50
|
+
expanded.push({ connectionId, sourceName: join.to });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if ((source.joins ?? []).some((join) => touched.has(join.to))) {
|
|
54
|
+
expanded.push({ connectionId, sourceName: source.name });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return uniqueTouchedSources(expanded);
|
|
59
|
+
}
|
|
60
|
+
async function validateWikiSlRefs(input) {
|
|
61
|
+
const errors = [];
|
|
62
|
+
const sourcesByConnection = new Map();
|
|
63
|
+
for (const connectionId of input.connectionIds) {
|
|
64
|
+
const { sources } = await input.semanticLayerService.loadAllSources(connectionId);
|
|
65
|
+
sourcesByConnection.set(connectionId, sources);
|
|
66
|
+
}
|
|
67
|
+
for (const pageKey of input.changedWikiPageKeys) {
|
|
68
|
+
const page = await input.wikiService.readPage('GLOBAL', null, pageKey);
|
|
69
|
+
if (!page) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
for (const ref of page.frontmatter.sl_refs ?? []) {
|
|
73
|
+
const parsed = parseSlRef(ref);
|
|
74
|
+
const candidateConnections = parsed.connectionId ? [parsed.connectionId] : input.connectionIds;
|
|
75
|
+
let source;
|
|
76
|
+
for (const connectionId of candidateConnections) {
|
|
77
|
+
source = sourcesByConnection.get(connectionId)?.find((candidate) => candidate.name === parsed.sourceName);
|
|
78
|
+
if (source) {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!source) {
|
|
83
|
+
errors.push(`${pageKey}: unknown sl_refs entry ${ref}`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (parsed.entityName && !slEntityNames(source).has(parsed.entityName)) {
|
|
87
|
+
errors.push(`${pageKey}: unknown sl_refs entity ${ref}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return errors;
|
|
92
|
+
}
|
|
93
|
+
async function validateWikiRefs(input) {
|
|
94
|
+
const dangling = [];
|
|
95
|
+
for (const pageKey of input.changedWikiPageKeys) {
|
|
96
|
+
const page = await input.wikiService.readPage('GLOBAL', null, pageKey);
|
|
97
|
+
if (!page) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const missingRefs = await findMissingWikiRefs({
|
|
101
|
+
wikiService: input.wikiService,
|
|
102
|
+
scope: 'GLOBAL',
|
|
103
|
+
scopeId: null,
|
|
104
|
+
pageKey,
|
|
105
|
+
refs: page.frontmatter.refs,
|
|
106
|
+
content: page.content,
|
|
107
|
+
});
|
|
108
|
+
for (const missingRef of missingRefs) {
|
|
109
|
+
dangling.push(`${pageKey} -> ${missingRef}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return dangling;
|
|
113
|
+
}
|
|
114
|
+
export async function validateFinalIngestArtifacts(input) {
|
|
115
|
+
const touchedWithDependencies = await expandTouchedSlSourcesWithDirectJoinNeighbors(input);
|
|
116
|
+
const validation = await input.validateTouchedSources(touchedWithDependencies);
|
|
117
|
+
const errors = validation.invalidSources.map((source) => `semantic-layer validation failed for ${source}`);
|
|
118
|
+
errors.push(...(await validateWikiSlRefs(input)));
|
|
119
|
+
const danglingWikiRefs = await validateWikiRefs(input);
|
|
120
|
+
if (danglingWikiRefs.length > 0) {
|
|
121
|
+
errors.push(`wiki references target missing page(s): ${danglingWikiRefs.join(', ')}`);
|
|
122
|
+
}
|
|
123
|
+
for (const pageKey of input.changedWikiPageKeys) {
|
|
124
|
+
const page = await input.wikiService.readPage('GLOBAL', null, pageKey);
|
|
125
|
+
if (!page) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
errors.push(...(await findInvalidWikiBodyRefs({
|
|
129
|
+
pageKey,
|
|
130
|
+
body: page.content,
|
|
131
|
+
visibleConnectionIds: input.connectionIds,
|
|
132
|
+
loadSources: async (connectionId) => {
|
|
133
|
+
const { sources } = await input.semanticLayerService.loadAllSources(connectionId);
|
|
134
|
+
return sources;
|
|
135
|
+
},
|
|
136
|
+
tableExists: input.tableExists,
|
|
137
|
+
})));
|
|
138
|
+
}
|
|
139
|
+
if (errors.length > 0) {
|
|
140
|
+
throw new Error(`final artifact gates failed:\n${errors.join('\n')}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export function validateProvenanceRawPaths(input) {
|
|
144
|
+
for (const row of input.rows) {
|
|
145
|
+
if (!input.currentRawPaths.has(row.rawPath) && !input.deletedRawPaths.has(row.rawPath)) {
|
|
146
|
+
throw new Error(`provenance row references raw path outside this snapshot: ${row.rawPath}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { validateFinalIngestArtifacts, validateProvenanceRawPaths } from './artifact-gates.js';
|
|
3
|
+
function wikiServiceWithPages(pages) {
|
|
4
|
+
return {
|
|
5
|
+
listPageKeys: vi.fn().mockResolvedValue(Object.keys(pages)),
|
|
6
|
+
readPage: vi.fn().mockImplementation((_scope, _scopeId, pageKey) => {
|
|
7
|
+
const page = pages[pageKey];
|
|
8
|
+
if (!page) {
|
|
9
|
+
return Promise.resolve(null);
|
|
10
|
+
}
|
|
11
|
+
return Promise.resolve({
|
|
12
|
+
pageKey,
|
|
13
|
+
frontmatter: {
|
|
14
|
+
summary: pageKey,
|
|
15
|
+
usage_mode: 'auto',
|
|
16
|
+
refs: page.refs,
|
|
17
|
+
sl_refs: page.slRefs,
|
|
18
|
+
},
|
|
19
|
+
content: page.content ?? '',
|
|
20
|
+
});
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
describe('artifact gates', () => {
|
|
25
|
+
it('fails the final tree when wiki body references a stale semantic-layer measure', async () => {
|
|
26
|
+
const wikiService = wikiServiceWithPages({
|
|
27
|
+
'account-segments': {
|
|
28
|
+
slRefs: ['mart_account_segments'],
|
|
29
|
+
content: 'ARR is `mart_account_segments.total_contract_arr_cents`.',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const semanticLayerService = {
|
|
33
|
+
loadAllSources: vi.fn().mockResolvedValue({
|
|
34
|
+
sources: [
|
|
35
|
+
{
|
|
36
|
+
name: 'mart_account_segments',
|
|
37
|
+
grain: ['account_id'],
|
|
38
|
+
columns: [{ name: 'account_id', type: 'string' }],
|
|
39
|
+
joins: [],
|
|
40
|
+
measures: [{ name: 'total_contract_arr', expr: 'sum(contract_arr)' }],
|
|
41
|
+
table: 'analytics.mart_account_segments',
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
loadErrors: [],
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
await expect(validateFinalIngestArtifacts({
|
|
48
|
+
connectionIds: ['warehouse'],
|
|
49
|
+
changedWikiPageKeys: ['account-segments'],
|
|
50
|
+
touchedSlSources: [{ connectionId: 'warehouse', sourceName: 'mart_account_segments' }],
|
|
51
|
+
wikiService: wikiService,
|
|
52
|
+
semanticLayerService: semanticLayerService,
|
|
53
|
+
validateTouchedSources: async () => ({ invalidSources: [], validSources: ['mart_account_segments'] }),
|
|
54
|
+
tableExists: async () => true,
|
|
55
|
+
})).rejects.toThrow(/unknown semantic-layer entity mart_account_segments\.total_contract_arr_cents/);
|
|
56
|
+
});
|
|
57
|
+
it('fails before provenance insertion when a raw path cannot be tied to the current snapshot or eviction set', () => {
|
|
58
|
+
expect(() => validateProvenanceRawPaths({
|
|
59
|
+
rows: [{ rawPath: 'cards/missing.json' }],
|
|
60
|
+
currentRawPaths: new Set(['cards/present.json']),
|
|
61
|
+
deletedRawPaths: new Set(['cards/deleted.json']),
|
|
62
|
+
})).toThrow(/provenance row references raw path outside this snapshot: cards\/missing\.json/);
|
|
63
|
+
});
|
|
64
|
+
it('fails measure-level wiki frontmatter sl_refs that point at missing entities', async () => {
|
|
65
|
+
const wikiService = wikiServiceWithPages({
|
|
66
|
+
'account-segments': {
|
|
67
|
+
slRefs: ['mart_account_segments.total_contract_arr_cents'],
|
|
68
|
+
content: 'ARR uses a renamed measure.',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const semanticLayerService = {
|
|
72
|
+
loadAllSources: vi.fn().mockResolvedValue({
|
|
73
|
+
sources: [
|
|
74
|
+
{
|
|
75
|
+
name: 'mart_account_segments',
|
|
76
|
+
grain: ['account_id'],
|
|
77
|
+
columns: [{ name: 'account_id', type: 'string' }],
|
|
78
|
+
joins: [],
|
|
79
|
+
measures: [{ name: 'total_contract_arr', expr: 'sum(contract_arr)' }],
|
|
80
|
+
table: 'analytics.mart_account_segments',
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
loadErrors: [],
|
|
84
|
+
}),
|
|
85
|
+
};
|
|
86
|
+
await expect(validateFinalIngestArtifacts({
|
|
87
|
+
connectionIds: ['warehouse'],
|
|
88
|
+
changedWikiPageKeys: ['account-segments'],
|
|
89
|
+
touchedSlSources: [{ connectionId: 'warehouse', sourceName: 'mart_account_segments' }],
|
|
90
|
+
wikiService: wikiService,
|
|
91
|
+
semanticLayerService: semanticLayerService,
|
|
92
|
+
validateTouchedSources: async () => ({ invalidSources: [], validSources: ['warehouse:mart_account_segments'] }),
|
|
93
|
+
tableExists: async () => true,
|
|
94
|
+
})).rejects.toThrow(/unknown sl_refs entity mart_account_segments\.total_contract_arr_cents/);
|
|
95
|
+
});
|
|
96
|
+
it('validates direct declared-join neighbors of touched semantic-layer sources', async () => {
|
|
97
|
+
const semanticLayerService = {
|
|
98
|
+
loadAllSources: vi.fn().mockResolvedValue({
|
|
99
|
+
sources: [
|
|
100
|
+
{
|
|
101
|
+
name: 'orders',
|
|
102
|
+
grain: ['order_id'],
|
|
103
|
+
columns: [
|
|
104
|
+
{ name: 'order_id', type: 'string' },
|
|
105
|
+
{ name: 'account_id', type: 'string' },
|
|
106
|
+
],
|
|
107
|
+
joins: [{ to: 'accounts', on: 'orders.account_id = accounts.account_id', relationship: 'many_to_one' }],
|
|
108
|
+
measures: [{ name: 'order_count', expr: 'count(*)' }],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'accounts',
|
|
112
|
+
grain: ['account_id'],
|
|
113
|
+
columns: [{ name: 'account_id', type: 'string' }],
|
|
114
|
+
joins: [],
|
|
115
|
+
measures: [{ name: 'account_count', expr: 'count(*)' }],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'segments',
|
|
119
|
+
grain: ['segment_id'],
|
|
120
|
+
columns: [
|
|
121
|
+
{ name: 'segment_id', type: 'string' },
|
|
122
|
+
{ name: 'account_id', type: 'string' },
|
|
123
|
+
],
|
|
124
|
+
joins: [{ to: 'accounts', on: 'segments.account_id = accounts.account_id', relationship: 'many_to_one' }],
|
|
125
|
+
measures: [],
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
loadErrors: [],
|
|
129
|
+
}),
|
|
130
|
+
};
|
|
131
|
+
const validateTouchedSources = vi.fn().mockResolvedValue({ invalidSources: [], validSources: [] });
|
|
132
|
+
await validateFinalIngestArtifacts({
|
|
133
|
+
connectionIds: ['warehouse'],
|
|
134
|
+
changedWikiPageKeys: [],
|
|
135
|
+
touchedSlSources: [{ connectionId: 'warehouse', sourceName: 'accounts' }],
|
|
136
|
+
wikiService: { readPage: vi.fn() },
|
|
137
|
+
semanticLayerService: semanticLayerService,
|
|
138
|
+
validateTouchedSources,
|
|
139
|
+
tableExists: async () => true,
|
|
140
|
+
});
|
|
141
|
+
expect(validateTouchedSources).toHaveBeenCalledWith([
|
|
142
|
+
{ connectionId: 'warehouse', sourceName: 'accounts' },
|
|
143
|
+
{ connectionId: 'warehouse', sourceName: 'orders' },
|
|
144
|
+
{ connectionId: 'warehouse', sourceName: 'segments' },
|
|
145
|
+
]);
|
|
146
|
+
});
|
|
147
|
+
it('fails final gates when a changed wiki page references a missing wiki page', async () => {
|
|
148
|
+
const wikiService = wikiServiceWithPages({
|
|
149
|
+
'account-segments': {
|
|
150
|
+
refs: ['missing-frontmatter-page'],
|
|
151
|
+
content: 'See [[missing-inline-page]] for the related process.',
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
const semanticLayerService = {
|
|
155
|
+
loadAllSources: vi.fn().mockResolvedValue({ sources: [], loadErrors: [] }),
|
|
156
|
+
};
|
|
157
|
+
await expect(validateFinalIngestArtifacts({
|
|
158
|
+
connectionIds: ['warehouse'],
|
|
159
|
+
changedWikiPageKeys: ['account-segments'],
|
|
160
|
+
touchedSlSources: [],
|
|
161
|
+
wikiService: wikiService,
|
|
162
|
+
semanticLayerService: semanticLayerService,
|
|
163
|
+
validateTouchedSources: async () => ({ invalidSources: [], validSources: [] }),
|
|
164
|
+
tableExists: async () => true,
|
|
165
|
+
})).rejects.toThrow(/wiki references target missing page\(s\): account-segments -> missing-frontmatter-page, account-segments -> missing-inline-page/);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AgentRunnerPort } from '../llm/index.js';
|
|
2
|
+
import type { TouchedSlSource } from '../tools/index.js';
|
|
3
|
+
import type { IngestTraceWriter } from './ingest-trace.js';
|
|
4
|
+
type FinalGateRepairKind = 'patch_semantic_gate' | 'final_artifact_gate';
|
|
5
|
+
export type FinalGateRepairResult = {
|
|
6
|
+
status: 'repaired';
|
|
7
|
+
attempts: number;
|
|
8
|
+
changedPaths: string[];
|
|
9
|
+
} | {
|
|
10
|
+
status: 'failed';
|
|
11
|
+
attempts: number;
|
|
12
|
+
reason: string;
|
|
13
|
+
};
|
|
14
|
+
export interface RepairFinalGateFailureInput {
|
|
15
|
+
agentRunner: AgentRunnerPort;
|
|
16
|
+
workdir: string;
|
|
17
|
+
gateError: string;
|
|
18
|
+
allowedPaths: string[];
|
|
19
|
+
trace: IngestTraceWriter;
|
|
20
|
+
repairKind: FinalGateRepairKind;
|
|
21
|
+
maxAttempts?: number;
|
|
22
|
+
stepBudget?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function finalGateRepairPaths(input: {
|
|
25
|
+
changedWikiPageKeys: string[];
|
|
26
|
+
touchedSlSources: TouchedSlSource[];
|
|
27
|
+
}): string[];
|
|
28
|
+
export declare function repairFinalGateFailure(input: RepairFinalGateFailureInput): Promise<FinalGateRepairResult>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { traceTimed } from './ingest-trace.js';
|
|
5
|
+
const readRepairFileSchema = z.object({
|
|
6
|
+
path: z.string().min(1),
|
|
7
|
+
});
|
|
8
|
+
const writeRepairFileSchema = z.object({
|
|
9
|
+
path: z.string().min(1),
|
|
10
|
+
content: z.string(),
|
|
11
|
+
});
|
|
12
|
+
function normalizeRepoPath(path) {
|
|
13
|
+
const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
14
|
+
const parts = normalized.split('/').filter((part) => part.length > 0);
|
|
15
|
+
if (parts.length === 0 || parts.some((part) => part === '.' || part === '..')) {
|
|
16
|
+
throw new Error(`gate repair path must be a repository-relative path: ${path}`);
|
|
17
|
+
}
|
|
18
|
+
return parts.join('/');
|
|
19
|
+
}
|
|
20
|
+
function assertAllowedPath(path, allowedPaths) {
|
|
21
|
+
const normalized = normalizeRepoPath(path);
|
|
22
|
+
if (!allowedPaths.has(normalized)) {
|
|
23
|
+
throw new Error(`gate repair path not allowed: ${normalized}`);
|
|
24
|
+
}
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
27
|
+
async function readOptionalFile(path) {
|
|
28
|
+
try {
|
|
29
|
+
return { exists: true, content: await readFile(path, 'utf-8') };
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
33
|
+
return { exists: false, content: '' };
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function buildGateRepairSystemPrompt() {
|
|
39
|
+
return `<role>
|
|
40
|
+
You repair one KTX isolated-diff artifact gate failure inside the integration worktree.
|
|
41
|
+
</role>
|
|
42
|
+
|
|
43
|
+
<rules>
|
|
44
|
+
- Use read_gate_error first.
|
|
45
|
+
- Read only files exposed by read_repair_file.
|
|
46
|
+
- Edit only paths exposed by write_repair_file.
|
|
47
|
+
- Prefer the smallest text edit that makes the gate pass.
|
|
48
|
+
- Preserve accepted work-unit, reconciliation, and deterministic projection content.
|
|
49
|
+
- Do not invent warehouse facts, business definitions, or semantic-layer entities.
|
|
50
|
+
- If the gate error requires choosing between conflicting facts without evidence, stop without editing.
|
|
51
|
+
</rules>`;
|
|
52
|
+
}
|
|
53
|
+
function buildGateRepairUserPrompt(input) {
|
|
54
|
+
return `Repair isolated-diff artifact gates.
|
|
55
|
+
|
|
56
|
+
Repair kind: ${input.repairKind}
|
|
57
|
+
Attempt: ${input.attempt} of ${input.maxAttempts}
|
|
58
|
+
|
|
59
|
+
Allowed files:
|
|
60
|
+
${input.allowedPaths.map((path) => `- ${path}`).join('\n')}
|
|
61
|
+
|
|
62
|
+
Gate error:
|
|
63
|
+
${input.gateError}
|
|
64
|
+
|
|
65
|
+
Use read_gate_error first. Then inspect only the allowed files, write the
|
|
66
|
+
minimal repaired content, and stop.`;
|
|
67
|
+
}
|
|
68
|
+
function buildToolSet(input) {
|
|
69
|
+
return {
|
|
70
|
+
read_gate_error: {
|
|
71
|
+
name: 'read_gate_error',
|
|
72
|
+
description: 'Read the artifact gate failure that must be repaired.',
|
|
73
|
+
inputSchema: z.object({}),
|
|
74
|
+
execute: async () => ({
|
|
75
|
+
markdown: input.gateError,
|
|
76
|
+
structured: { gateError: input.gateError },
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
read_repair_file: {
|
|
80
|
+
name: 'read_repair_file',
|
|
81
|
+
description: 'Read one allowed file from the integration worktree.',
|
|
82
|
+
inputSchema: readRepairFileSchema,
|
|
83
|
+
execute: async ({ path }) => {
|
|
84
|
+
const normalized = assertAllowedPath(path, input.allowedPaths);
|
|
85
|
+
const file = await readOptionalFile(join(input.workdir, normalized));
|
|
86
|
+
return {
|
|
87
|
+
markdown: file.exists ? file.content : `(missing file: ${normalized})`,
|
|
88
|
+
structured: { path: normalized, exists: file.exists },
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
write_repair_file: {
|
|
93
|
+
name: 'write_repair_file',
|
|
94
|
+
description: 'Replace one allowed integration worktree file with repaired text content.',
|
|
95
|
+
inputSchema: writeRepairFileSchema,
|
|
96
|
+
execute: async ({ path, content }) => {
|
|
97
|
+
const normalized = assertAllowedPath(path, input.allowedPaths);
|
|
98
|
+
const fullPath = join(input.workdir, normalized);
|
|
99
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
100
|
+
await writeFile(fullPath, content, 'utf-8');
|
|
101
|
+
input.editedPaths.add(normalized);
|
|
102
|
+
return {
|
|
103
|
+
markdown: `Wrote ${normalized}`,
|
|
104
|
+
structured: { path: normalized, bytes: Buffer.byteLength(content) },
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export function finalGateRepairPaths(input) {
|
|
111
|
+
return [
|
|
112
|
+
...new Set([
|
|
113
|
+
...input.touchedSlSources.map((source) => `semantic-layer/${source.connectionId}/${source.sourceName}.yaml`),
|
|
114
|
+
...input.changedWikiPageKeys.map((pageKey) => `wiki/global/${pageKey}.md`),
|
|
115
|
+
]),
|
|
116
|
+
].sort();
|
|
117
|
+
}
|
|
118
|
+
export async function repairFinalGateFailure(input) {
|
|
119
|
+
const allowedPaths = new Set(input.allowedPaths.map(normalizeRepoPath));
|
|
120
|
+
const maxAttempts = input.maxAttempts ?? 1;
|
|
121
|
+
const stepBudget = input.stepBudget ?? 16;
|
|
122
|
+
let lastFailure = 'gate repair did not run';
|
|
123
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
124
|
+
const editedPaths = new Set();
|
|
125
|
+
const sortedAllowedPaths = [...allowedPaths].sort();
|
|
126
|
+
const traceData = {
|
|
127
|
+
repairKind: input.repairKind,
|
|
128
|
+
attempt,
|
|
129
|
+
maxAttempts,
|
|
130
|
+
allowedPaths: sortedAllowedPaths,
|
|
131
|
+
gateError: input.gateError,
|
|
132
|
+
};
|
|
133
|
+
const result = await traceTimed(input.trace, 'gate_repair', 'gate_repair', traceData, async () => input.agentRunner.runLoop({
|
|
134
|
+
modelRole: 'repair',
|
|
135
|
+
systemPrompt: buildGateRepairSystemPrompt(),
|
|
136
|
+
userPrompt: buildGateRepairUserPrompt({
|
|
137
|
+
gateError: input.gateError,
|
|
138
|
+
allowedPaths: sortedAllowedPaths,
|
|
139
|
+
repairKind: input.repairKind,
|
|
140
|
+
attempt,
|
|
141
|
+
maxAttempts,
|
|
142
|
+
}),
|
|
143
|
+
toolSet: buildToolSet({
|
|
144
|
+
workdir: input.workdir,
|
|
145
|
+
gateError: input.gateError,
|
|
146
|
+
allowedPaths,
|
|
147
|
+
editedPaths,
|
|
148
|
+
}),
|
|
149
|
+
stepBudget,
|
|
150
|
+
telemetryTags: {
|
|
151
|
+
operationName: 'ingest-isolated-diff-gate-repair',
|
|
152
|
+
source: input.trace.context.sourceKey,
|
|
153
|
+
jobId: input.trace.context.jobId,
|
|
154
|
+
repairKind: input.repairKind,
|
|
155
|
+
},
|
|
156
|
+
}));
|
|
157
|
+
if (result.stopReason === 'error') {
|
|
158
|
+
lastFailure = result.error?.message ?? 'gate repair agent loop errored';
|
|
159
|
+
await input.trace.event('error', 'gate_repair', 'gate_repair_failed', traceData, result.error);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const changedPaths = [...editedPaths].sort();
|
|
163
|
+
if (changedPaths.length === 0) {
|
|
164
|
+
lastFailure = 'gate repair completed without editing an allowed path';
|
|
165
|
+
await input.trace.event('error', 'gate_repair', 'gate_repair_failed', {
|
|
166
|
+
...traceData,
|
|
167
|
+
reason: lastFailure,
|
|
168
|
+
});
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
await input.trace.event('debug', 'gate_repair', 'gate_repair_repaired', {
|
|
172
|
+
...traceData,
|
|
173
|
+
changedPaths,
|
|
174
|
+
});
|
|
175
|
+
return { status: 'repaired', attempts: attempt, changedPaths };
|
|
176
|
+
}
|
|
177
|
+
return { status: 'failed', attempts: maxAttempts, reason: lastFailure };
|
|
178
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { finalGateRepairPaths, repairFinalGateFailure } from './final-gate-repair.js';
|
|
6
|
+
import { FileIngestTraceWriter } from './ingest-trace.js';
|
|
7
|
+
async function makeHarness() {
|
|
8
|
+
const root = await mkdtemp(join(tmpdir(), 'ktx-final-gate-repair-'));
|
|
9
|
+
const workdir = join(root, 'workdir');
|
|
10
|
+
await mkdir(join(workdir, 'wiki/global'), { recursive: true });
|
|
11
|
+
await mkdir(join(workdir, 'semantic-layer/warehouse'), { recursive: true });
|
|
12
|
+
await writeFile(join(workdir, 'wiki/global/account-segments.md'), '---\nsummary: Account segments\nusage_mode: auto\n---\n\nARR uses `mart_account_segments.total_contract_arr_cents`.\n', 'utf-8');
|
|
13
|
+
await writeFile(join(workdir, 'semantic-layer/warehouse/mart_account_segments.yaml'), 'name: mart_account_segments\ncolumns: [{name: account_id, type: string}]\njoins: []\nmeasures:\n - name: total_contract_arr\n expr: sum(contract_arr)\n', 'utf-8');
|
|
14
|
+
const trace = new FileIngestTraceWriter({
|
|
15
|
+
tracePath: join(root, 'trace.jsonl'),
|
|
16
|
+
jobId: 'job-1',
|
|
17
|
+
connectionId: 'warehouse',
|
|
18
|
+
sourceKey: 'metabase',
|
|
19
|
+
runId: 'run-1',
|
|
20
|
+
syncId: 'sync-1',
|
|
21
|
+
level: 'trace',
|
|
22
|
+
});
|
|
23
|
+
return { root, workdir, trace };
|
|
24
|
+
}
|
|
25
|
+
describe('finalGateRepairPaths', () => {
|
|
26
|
+
it('derives sorted wiki and semantic-layer file paths', () => {
|
|
27
|
+
expect(finalGateRepairPaths({
|
|
28
|
+
changedWikiPageKeys: ['account-segments', 'overview', 'account-segments'],
|
|
29
|
+
touchedSlSources: [
|
|
30
|
+
{ connectionId: 'warehouse', sourceName: 'mart_account_segments' },
|
|
31
|
+
{ connectionId: 'warehouse', sourceName: 'orders' },
|
|
32
|
+
{ connectionId: 'warehouse', sourceName: 'orders' },
|
|
33
|
+
],
|
|
34
|
+
})).toEqual([
|
|
35
|
+
'semantic-layer/warehouse/mart_account_segments.yaml',
|
|
36
|
+
'semantic-layer/warehouse/orders.yaml',
|
|
37
|
+
'wiki/global/account-segments.md',
|
|
38
|
+
'wiki/global/overview.md',
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('repairFinalGateFailure', () => {
|
|
43
|
+
it('lets the repair agent read gate errors and edit only allowed files', async () => {
|
|
44
|
+
const { workdir, trace } = await makeHarness();
|
|
45
|
+
const agentRunner = {
|
|
46
|
+
runLoop: vi.fn(async (params) => {
|
|
47
|
+
const error = await params.toolSet.read_gate_error.execute({});
|
|
48
|
+
expect(error.markdown).toContain('total_contract_arr_cents');
|
|
49
|
+
const page = await params.toolSet.read_repair_file.execute({
|
|
50
|
+
path: 'wiki/global/account-segments.md',
|
|
51
|
+
});
|
|
52
|
+
expect(page.markdown).toContain('total_contract_arr_cents');
|
|
53
|
+
await expect(params.toolSet.write_repair_file.execute({
|
|
54
|
+
path: 'wiki/global/other.md',
|
|
55
|
+
content: 'not allowed',
|
|
56
|
+
})).rejects.toThrow(/gate repair path not allowed/);
|
|
57
|
+
await params.toolSet.write_repair_file.execute({
|
|
58
|
+
path: 'wiki/global/account-segments.md',
|
|
59
|
+
content: page.markdown.replace('total_contract_arr_cents', 'total_contract_arr'),
|
|
60
|
+
});
|
|
61
|
+
return { stopReason: 'natural' };
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
const result = await repairFinalGateFailure({
|
|
65
|
+
agentRunner,
|
|
66
|
+
workdir,
|
|
67
|
+
gateError: 'final artifact gates failed:\naccount-segments: unknown semantic-layer entity mart_account_segments.total_contract_arr_cents',
|
|
68
|
+
allowedPaths: ['wiki/global/account-segments.md'],
|
|
69
|
+
trace,
|
|
70
|
+
repairKind: 'final_artifact_gate',
|
|
71
|
+
maxAttempts: 1,
|
|
72
|
+
stepBudget: 8,
|
|
73
|
+
});
|
|
74
|
+
expect(result).toEqual({
|
|
75
|
+
status: 'repaired',
|
|
76
|
+
attempts: 1,
|
|
77
|
+
changedPaths: ['wiki/global/account-segments.md'],
|
|
78
|
+
});
|
|
79
|
+
await expect(readFile(join(workdir, 'wiki/global/account-segments.md'), 'utf-8')).resolves.toContain('total_contract_arr');
|
|
80
|
+
await expect(readFile(trace.tracePath, 'utf-8')).resolves.toContain('gate_repair_repaired');
|
|
81
|
+
expect(agentRunner.runLoop).toHaveBeenCalledWith(expect.objectContaining({
|
|
82
|
+
modelRole: 'repair',
|
|
83
|
+
stepBudget: 8,
|
|
84
|
+
telemetryTags: expect.objectContaining({
|
|
85
|
+
operationName: 'ingest-isolated-diff-gate-repair',
|
|
86
|
+
repairKind: 'final_artifact_gate',
|
|
87
|
+
}),
|
|
88
|
+
}));
|
|
89
|
+
});
|
|
90
|
+
it('returns failed when the repair agent edits no allowed file', async () => {
|
|
91
|
+
const { workdir, trace } = await makeHarness();
|
|
92
|
+
const result = await repairFinalGateFailure({
|
|
93
|
+
agentRunner: { runLoop: vi.fn(async () => ({ stopReason: 'natural' })) },
|
|
94
|
+
workdir,
|
|
95
|
+
gateError: 'final artifact gates failed:\naccount-segments: unknown semantic-layer entity',
|
|
96
|
+
allowedPaths: ['wiki/global/account-segments.md'],
|
|
97
|
+
trace,
|
|
98
|
+
repairKind: 'final_artifact_gate',
|
|
99
|
+
maxAttempts: 1,
|
|
100
|
+
stepBudget: 8,
|
|
101
|
+
});
|
|
102
|
+
expect(result).toEqual({
|
|
103
|
+
status: 'failed',
|
|
104
|
+
attempts: 1,
|
|
105
|
+
reason: 'gate repair completed without editing an allowed path',
|
|
106
|
+
});
|
|
107
|
+
await expect(readFile(trace.tracePath, 'utf-8')).resolves.toContain('gate_repair_failed');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -4,6 +4,7 @@ export type { DaemonLiveDatabaseIntrospectionOptions, KtxDaemonDatabaseHttpJsonR
|
|
|
4
4
|
export { createDaemonLiveDatabaseIntrospection } from './adapters/live-database/daemon-introspection.js';
|
|
5
5
|
export type { LiveDatabaseExtractedColumn, LiveDatabaseExtractedForeignKey, LiveDatabaseExtractedSchema, LiveDatabaseExtractedTable, } from './adapters/live-database/extracted-schema.js';
|
|
6
6
|
export { buildLiveDatabaseTableNaturalKey, ktxSchemaSnapshotToExtractedSchema, } from './adapters/live-database/extracted-schema.js';
|
|
7
|
+
export { assertSemanticLayerTargetPathsAllowed, findDisallowedSemanticLayerTargetPaths, semanticLayerConnectionIdFromPath, } from './semantic-layer-target-policy.js';
|
|
7
8
|
export { LiveDatabaseSourceAdapter } from './adapters/live-database/live-database.adapter.js';
|
|
8
9
|
export type { BuildLiveDatabaseManifestShardsInput, BuildLiveDatabaseManifestShardsResult, LiveDatabaseManifestColumn, LiveDatabaseManifestExistingDescriptions, LiveDatabaseManifestJoinData, LiveDatabaseManifestJoinEntry, LiveDatabaseManifestShard, LiveDatabaseManifestTableData, LiveDatabaseManifestTableEntry, } from './adapters/live-database/manifest.js';
|
|
9
10
|
export { buildLiveDatabaseManifestShards } from './adapters/live-database/manifest.js';
|
|
@@ -134,6 +135,11 @@ export type { CandidateDedupPort, CandidateDedupResult, ContextCandidateCarryfor
|
|
|
134
135
|
export { buildSyncId, provenanceMarker, rawSourcesDirForSync, rawSourcesRoot, } from './raw-sources-paths.js';
|
|
135
136
|
export { ingestReportSnapshotSchema, parseIngestReportSnapshot } from './report-snapshot.js';
|
|
136
137
|
export type { IngestReportBody, IngestReportSnapshot } from './reports.js';
|
|
138
|
+
export * from './artifact-gates.js';
|
|
139
|
+
export * from './ingest-trace.js';
|
|
140
|
+
export * from './isolated-diff/git-patch.js';
|
|
141
|
+
export * from './isolated-diff/patch-integrator.js';
|
|
142
|
+
export * from './isolated-diff/work-unit-executor.js';
|
|
137
143
|
export * from './reports.js';
|
|
138
144
|
export { SourceAdapterRegistry } from './source-adapter-registry.js';
|
|
139
145
|
export type { SqliteBundleIngestStoreOptions } from './sqlite-bundle-ingest-store.js';
|
|
@@ -145,4 +151,5 @@ export { buildReconcileSystemPrompt, buildReconcileToolSet, buildReconcileUserPr
|
|
|
145
151
|
export type { ReconciliationOutcome } from './stages/stage-4-reconciliation.js';
|
|
146
152
|
export { runReconciliationStage4 } from './stages/stage-4-reconciliation.js';
|
|
147
153
|
export type { StageIndex } from './stages/stage-index.types.js';
|
|
148
|
-
export type { ChunkResult, DiffSet, EvictionUnit, FetchContext, IngestBundleJob, IngestBundleRef, IngestBundleResult, IngestDiffSummary, IngestJobContext, IngestJobPhase, IngestTrigger, ScopeDescriptor, SourceAdapter, SourceFetchIssue, SourceFetchReport, TriageLane, TriageSignals, UnresolvedCardInfo, WorkUnit, } from './types.js';
|
|
154
|
+
export type { ChunkResult, DiffSet, EvictionUnit, FetchContext, IngestBundleJob, IngestBundleRef, IngestBundleResult, IngestDiffSummary, IngestJobContext, IngestJobPhase, IngestTrigger, ScopeDescriptor, SourceAdapter, SourceFetchIssue, SourceFetchReport, TriageLane, TriageSignals, UnresolvedCardInfo, WorkUnit, DeterministicProjectionContext, ProjectionResult, } from './types.js';
|
|
155
|
+
export * from './wiki-body-refs.js';
|