@duckcodeailabs/dql-core 1.6.21 → 1.6.22
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/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/registry.d.ts +25 -1
- package/dist/contracts/registry.d.ts.map +1 -1
- package/dist/contracts/registry.js +104 -12
- package/dist/contracts/registry.js.map +1 -1
- package/dist/contracts/types.d.ts +55 -0
- package/dist/contracts/types.d.ts.map +1 -1
- package/dist/contracts/types.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lineage/builder.d.ts.map +1 -1
- package/dist/lineage/builder.js +12 -0
- package/dist/lineage/builder.js.map +1 -1
- package/dist/lineage/column-lineage.d.ts +27 -0
- package/dist/lineage/column-lineage.d.ts.map +1 -1
- package/dist/lineage/column-lineage.js +147 -0
- package/dist/lineage/column-lineage.js.map +1 -1
- package/dist/lineage/derivation.d.ts +111 -0
- package/dist/lineage/derivation.d.ts.map +1 -0
- package/dist/lineage/derivation.js +177 -0
- package/dist/lineage/derivation.js.map +1 -0
- package/dist/lineage/impact.d.ts +127 -0
- package/dist/lineage/impact.d.ts.map +1 -0
- package/dist/lineage/impact.js +305 -0
- package/dist/lineage/impact.js.map +1 -0
- package/dist/lineage/index.d.ts +3 -1
- package/dist/lineage/index.d.ts.map +1 -1
- package/dist/lineage/index.js +3 -1
- package/dist/lineage/index.js.map +1 -1
- package/dist/manifest/builder.d.ts +16 -0
- package/dist/manifest/builder.d.ts.map +1 -1
- package/dist/manifest/builder.js +117 -10
- package/dist/manifest/builder.js.map +1 -1
- package/dist/manifest/dbt-freshness.d.ts +65 -0
- package/dist/manifest/dbt-freshness.d.ts.map +1 -0
- package/dist/manifest/dbt-freshness.js +264 -0
- package/dist/manifest/dbt-freshness.js.map +1 -0
- package/dist/manifest/domain-writer.d.ts +66 -0
- package/dist/manifest/domain-writer.d.ts.map +1 -0
- package/dist/manifest/domain-writer.js +114 -0
- package/dist/manifest/domain-writer.js.map +1 -0
- package/dist/manifest/index.d.ts +4 -1
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/index.js +3 -0
- package/dist/manifest/index.js.map +1 -1
- package/dist/manifest/output-drift.d.ts +31 -0
- package/dist/manifest/output-drift.d.ts.map +1 -0
- package/dist/manifest/output-drift.js +97 -0
- package/dist/manifest/output-drift.js.map +1 -0
- package/dist/manifest/types.d.ts +156 -1
- package/dist/manifest/types.d.ts.map +1 -1
- package/dist/parser/parser.d.ts.map +1 -1
- package/dist/parser/parser.js +8 -0
- package/dist/parser/parser.js.map +1 -1
- package/dist/semantic/analyzer.d.ts +8 -0
- package/dist/semantic/analyzer.d.ts.map +1 -1
- package/dist/semantic/analyzer.js +8 -0
- package/dist/semantic/analyzer.js.map +1 -1
- package/dist/semantic/conflicts.d.ts +33 -0
- package/dist/semantic/conflicts.d.ts.map +1 -0
- package/dist/semantic/conflicts.js +270 -0
- package/dist/semantic/conflicts.js.map +1 -0
- package/dist/semantic/index.d.ts +1 -1
- package/dist/semantic/index.d.ts.map +1 -1
- package/dist/semantic/index.js +1 -1
- package/dist/semantic/index.js.map +1 -1
- package/dist/trust/index.d.ts +3 -0
- package/dist/trust/index.d.ts.map +1 -0
- package/dist/trust/index.js +2 -0
- package/dist/trust/index.js.map +1 -0
- package/dist/trust/labels.d.ts +122 -0
- package/dist/trust/labels.d.ts.map +1 -0
- package/dist/trust/labels.js +185 -0
- package/dist/trust/labels.js.map +1 -0
- package/package.json +5 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freshness-aware trust — read dbt run artifacts (READ-ONLY) and fold data
|
|
3
|
+
* health into the effective trust of certified blocks.
|
|
4
|
+
*
|
|
5
|
+
* "Certified" means the *logic* was reviewed. It says nothing about whether the
|
|
6
|
+
* *data* behind a block is fresh: an upstream dbt model may have failed its last
|
|
7
|
+
* run, or be past its freshness window. This module reads dbt's own artifacts —
|
|
8
|
+
* `run_results.json` (last-run status/time per model) and, when present, the
|
|
9
|
+
* source-freshness output — and derives a {@link DbtDataState} per dbt node. It
|
|
10
|
+
* then rolls those up to each certified block's transitive dbt upstreams so a
|
|
11
|
+
* surface can render "Certified · stale data" / "Certified · upstream failed".
|
|
12
|
+
*
|
|
13
|
+
* We never run dbt or query the warehouse — we only parse files dbt produced.
|
|
14
|
+
* Everything degrades to `unknown` (which surfaces as the plain "Certified"
|
|
15
|
+
* label) when an artifact is missing or unparseable, so this is fully additive.
|
|
16
|
+
*
|
|
17
|
+
* Kept in a dedicated module so `manifest/builder.ts` only needs a couple of
|
|
18
|
+
* call sites, keeping its diff minimal and merge-clean with the sibling
|
|
19
|
+
* `outputContract`/`drift` change.
|
|
20
|
+
*/
|
|
21
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
/** Worst-first ordering used when rolling many upstream states into one. */
|
|
24
|
+
const STATE_SEVERITY = {
|
|
25
|
+
failed: 3,
|
|
26
|
+
stale: 2,
|
|
27
|
+
unknown: 1,
|
|
28
|
+
fresh: 0,
|
|
29
|
+
};
|
|
30
|
+
/** Return the worse (higher-severity) of two data states. */
|
|
31
|
+
export function worseDataState(a, b) {
|
|
32
|
+
return STATE_SEVERITY[a] >= STATE_SEVERITY[b] ? a : b;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve and read dbt's `run_results.json` (and source-freshness output, when
|
|
36
|
+
* a separate `sources.json` sits beside it) from the directory holding the dbt
|
|
37
|
+
* `manifest.json`. Returns an empty index — never throws — when nothing is
|
|
38
|
+
* found or the file cannot be parsed.
|
|
39
|
+
*
|
|
40
|
+
* dbt writes both `manifest.json` and `run_results.json` into the same
|
|
41
|
+
* `target/` directory, so we look beside `manifestPath`.
|
|
42
|
+
*/
|
|
43
|
+
export function loadDbtRunState(manifestPath) {
|
|
44
|
+
const targetDir = dirname(manifestPath);
|
|
45
|
+
const byUniqueId = new Map();
|
|
46
|
+
const runResultsPath = join(targetDir, 'run_results.json');
|
|
47
|
+
let foundPath;
|
|
48
|
+
if (existsSync(runResultsPath)) {
|
|
49
|
+
try {
|
|
50
|
+
const raw = JSON.parse(readFileSync(runResultsPath, 'utf-8'));
|
|
51
|
+
const generatedAt = raw?.metadata?.generated_at;
|
|
52
|
+
const results = Array.isArray(raw?.results) ? raw.results : [];
|
|
53
|
+
for (const result of results) {
|
|
54
|
+
const uniqueId = result?.unique_id;
|
|
55
|
+
if (!uniqueId)
|
|
56
|
+
continue;
|
|
57
|
+
byUniqueId.set(uniqueId, runStateFromRunResult(result, generatedAt));
|
|
58
|
+
}
|
|
59
|
+
if (results.length > 0)
|
|
60
|
+
foundPath = runResultsPath;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Unparseable artifact — degrade silently to "unknown" everywhere.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Optional source-freshness output. dbt's `source freshness` command writes a
|
|
67
|
+
// `sources.json` beside the manifest; fold any stale/error verdicts in. This
|
|
68
|
+
// overrides/augments run-result state for source nodes only.
|
|
69
|
+
const sourcesPath = join(targetDir, 'sources.json');
|
|
70
|
+
if (existsSync(sourcesPath)) {
|
|
71
|
+
try {
|
|
72
|
+
const raw = JSON.parse(readFileSync(sourcesPath, 'utf-8'));
|
|
73
|
+
const results = Array.isArray(raw?.results) ? raw.results : [];
|
|
74
|
+
for (const result of results) {
|
|
75
|
+
const uniqueId = result?.unique_id;
|
|
76
|
+
if (!uniqueId)
|
|
77
|
+
continue;
|
|
78
|
+
const freshness = freshnessFromSourceResult(result);
|
|
79
|
+
if (!freshness)
|
|
80
|
+
continue;
|
|
81
|
+
const existing = byUniqueId.get(uniqueId);
|
|
82
|
+
byUniqueId.set(uniqueId, {
|
|
83
|
+
dataState: existing
|
|
84
|
+
? worseDataState(existing.dataState, freshness.dataState)
|
|
85
|
+
: freshness.dataState,
|
|
86
|
+
lastRunStatus: existing?.lastRunStatus,
|
|
87
|
+
lastRunCompletedAt: existing?.lastRunCompletedAt,
|
|
88
|
+
freshnessStatus: freshness.freshnessStatus,
|
|
89
|
+
maxLoadedAt: freshness.maxLoadedAt,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (results.length > 0)
|
|
93
|
+
foundPath = foundPath ?? sourcesPath;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Ignore — freshness is best-effort.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { byUniqueId, runResultsPath: foundPath };
|
|
100
|
+
}
|
|
101
|
+
/** Map one `run_results.json` entry to a {@link DbtRunState}. */
|
|
102
|
+
function runStateFromRunResult(result, generatedAt) {
|
|
103
|
+
const status = result?.status;
|
|
104
|
+
// Prefer the precise per-node execute completion time from `timing`; fall back
|
|
105
|
+
// to the artifact's `generated_at` so we always carry *some* "as of" time.
|
|
106
|
+
const timing = Array.isArray(result?.timing) ? result.timing : [];
|
|
107
|
+
const executeCompletedAt = timing.find((t) => t?.name === 'execute')?.completed_at;
|
|
108
|
+
return {
|
|
109
|
+
dataState: dataStateFromRunStatus(status),
|
|
110
|
+
lastRunStatus: status,
|
|
111
|
+
lastRunCompletedAt: executeCompletedAt ?? generatedAt,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* dbt run statuses: model runs report `success` | `error` | `skipped`; tests
|
|
116
|
+
* report `pass` | `fail` | `warn` | `error`. A failed or skipped upstream means
|
|
117
|
+
* the data may be missing or wrong → `failed`. Success → `fresh`. Anything
|
|
118
|
+
* unrecognized → `unknown` (no opinion).
|
|
119
|
+
*/
|
|
120
|
+
function dataStateFromRunStatus(status) {
|
|
121
|
+
switch (status) {
|
|
122
|
+
case 'success':
|
|
123
|
+
case 'pass':
|
|
124
|
+
return 'fresh';
|
|
125
|
+
case 'error':
|
|
126
|
+
case 'fail':
|
|
127
|
+
case 'skipped':
|
|
128
|
+
case 'runtime error':
|
|
129
|
+
return 'failed';
|
|
130
|
+
default:
|
|
131
|
+
return 'unknown';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Map one source-freshness result to a freshness state. */
|
|
135
|
+
function freshnessFromSourceResult(result) {
|
|
136
|
+
const status = result?.status ?? result?.criteria?.status;
|
|
137
|
+
const maxLoadedAt = result?.max_loaded_at ?? result?.criteria?.max_loaded_at;
|
|
138
|
+
switch (status) {
|
|
139
|
+
case 'pass':
|
|
140
|
+
return { dataState: 'fresh', freshnessStatus: status, maxLoadedAt };
|
|
141
|
+
case 'warn':
|
|
142
|
+
case 'error':
|
|
143
|
+
// A freshness warn/error means the data is past its window: stale, not a
|
|
144
|
+
// hard run failure.
|
|
145
|
+
return { dataState: 'stale', freshnessStatus: status, maxLoadedAt };
|
|
146
|
+
case 'runtime error':
|
|
147
|
+
return { dataState: 'failed', freshnessStatus: status, maxLoadedAt };
|
|
148
|
+
default:
|
|
149
|
+
return status ? { dataState: 'unknown', freshnessStatus: status, maxLoadedAt } : undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Compute each certified block's effective `dataState` from the health of its
|
|
154
|
+
* transitive dbt upstreams, mutating blocks additively in place.
|
|
155
|
+
*
|
|
156
|
+
* Resolution: a block's `tableDependencies` name dbt models/sources; we resolve
|
|
157
|
+
* each to a `sources[...]` entry that carries a `dbtModel.uniqueId`, then walk
|
|
158
|
+
* upstream through the dbt DAG (`edges`: source → target uniqueId) collecting
|
|
159
|
+
* every reachable node's run state. The block's `dataState` is the worst state
|
|
160
|
+
* among them. Blocks with no resolvable dbt upstream are left untouched.
|
|
161
|
+
*
|
|
162
|
+
* Only acts when a run-results artifact was actually read — without it every
|
|
163
|
+
* node is `unknown` and we leave blocks alone so nothing regresses to a noisy
|
|
164
|
+
* "· data freshness unknown" qualifier.
|
|
165
|
+
*/
|
|
166
|
+
export function applyBlockDataState(blocks, sources, dbtDag, runState) {
|
|
167
|
+
if (runState.byUniqueId.size === 0)
|
|
168
|
+
return;
|
|
169
|
+
// uniqueId -> upstream uniqueIds (reverse of edges' source→target direction;
|
|
170
|
+
// an edge {source, target} means target depends on source).
|
|
171
|
+
const upstream = new Map();
|
|
172
|
+
for (const edge of dbtDag?.edges ?? []) {
|
|
173
|
+
const list = upstream.get(edge.target) ?? [];
|
|
174
|
+
list.push(edge.source);
|
|
175
|
+
upstream.set(edge.target, list);
|
|
176
|
+
}
|
|
177
|
+
// table name -> dbt uniqueId, via the sources map. Block table refs may be
|
|
178
|
+
// schema-qualified (`dev.customers`) while the dbt-model source is named bare
|
|
179
|
+
// (`customers`), so index every alias: the source name plus the model's
|
|
180
|
+
// bare / schema-qualified / db-qualified names derived from its dbt metadata.
|
|
181
|
+
const tableToUniqueId = new Map();
|
|
182
|
+
const addTableKey = (key, uid) => {
|
|
183
|
+
if (!key)
|
|
184
|
+
return;
|
|
185
|
+
const k = key.toLowerCase();
|
|
186
|
+
if (!tableToUniqueId.has(k))
|
|
187
|
+
tableToUniqueId.set(k, uid);
|
|
188
|
+
};
|
|
189
|
+
for (const source of Object.values(sources)) {
|
|
190
|
+
const dm = source.dbtModel;
|
|
191
|
+
if (!dm?.uniqueId)
|
|
192
|
+
continue;
|
|
193
|
+
addTableKey(source.name, dm.uniqueId);
|
|
194
|
+
const bare = dm.uniqueId.split('.').pop();
|
|
195
|
+
addTableKey(bare, dm.uniqueId);
|
|
196
|
+
if (dm.schema && bare)
|
|
197
|
+
addTableKey(`${dm.schema}.${bare}`, dm.uniqueId);
|
|
198
|
+
if (dm.database && dm.schema && bare)
|
|
199
|
+
addTableKey(`${dm.database}.${dm.schema}.${bare}`, dm.uniqueId);
|
|
200
|
+
}
|
|
201
|
+
// A node's own state, or `undefined` when dbt produced no run/freshness result
|
|
202
|
+
// for it (e.g. raw sources never "run"). Unstated nodes are NEUTRAL — being in
|
|
203
|
+
// a block's lineage must not drag it to "unknown" just because a source has no
|
|
204
|
+
// run record.
|
|
205
|
+
const stateFor = (uid) => runState.byUniqueId.get(uid)?.dataState;
|
|
206
|
+
// Memoised worst KNOWN state over the transitive upstream closure of a node.
|
|
207
|
+
const closureCache = new Map();
|
|
208
|
+
const worstUpstream = (uid, seen = new Set()) => {
|
|
209
|
+
if (closureCache.has(uid))
|
|
210
|
+
return closureCache.get(uid);
|
|
211
|
+
if (seen.has(uid))
|
|
212
|
+
return stateFor(uid);
|
|
213
|
+
seen.add(uid);
|
|
214
|
+
let worst = stateFor(uid);
|
|
215
|
+
for (const up of upstream.get(uid) ?? []) {
|
|
216
|
+
const s = worstUpstream(up, seen);
|
|
217
|
+
if (s !== undefined)
|
|
218
|
+
worst = worst === undefined ? s : worseDataState(worst, s);
|
|
219
|
+
}
|
|
220
|
+
closureCache.set(uid, worst);
|
|
221
|
+
return worst;
|
|
222
|
+
};
|
|
223
|
+
for (const block of Object.values(blocks)) {
|
|
224
|
+
const upstreamUids = [];
|
|
225
|
+
for (const table of block.tableDependencies ?? []) {
|
|
226
|
+
const lower = table.toLowerCase();
|
|
227
|
+
const uid = tableToUniqueId.get(lower) ?? tableToUniqueId.get(lower.split('.').pop() ?? lower);
|
|
228
|
+
if (uid)
|
|
229
|
+
upstreamUids.push(uid);
|
|
230
|
+
}
|
|
231
|
+
if (upstreamUids.length === 0)
|
|
232
|
+
continue;
|
|
233
|
+
let worst;
|
|
234
|
+
let worstUid;
|
|
235
|
+
for (const uid of upstreamUids) {
|
|
236
|
+
const state = worstUpstream(uid);
|
|
237
|
+
if (state === undefined)
|
|
238
|
+
continue;
|
|
239
|
+
if (worst === undefined || STATE_SEVERITY[state] > STATE_SEVERITY[worst]) {
|
|
240
|
+
worst = state;
|
|
241
|
+
worstUid = uid;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (worst === undefined)
|
|
245
|
+
continue;
|
|
246
|
+
block.dataState = worst;
|
|
247
|
+
block.dataStateDetail = describeBlockDataState(worst, worstUid, runState);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function describeBlockDataState(state, worstUid, runState) {
|
|
251
|
+
if (state === 'fresh')
|
|
252
|
+
return 'All upstream dbt models are fresh (last run succeeded).';
|
|
253
|
+
const node = worstUid ? runState.byUniqueId.get(worstUid) : undefined;
|
|
254
|
+
const shortName = worstUid?.split('.').pop();
|
|
255
|
+
switch (state) {
|
|
256
|
+
case 'failed':
|
|
257
|
+
return `Upstream dbt model${shortName ? ` "${shortName}"` : ''} last run failed${node?.lastRunStatus ? ` (status: ${node.lastRunStatus})` : ''}.`;
|
|
258
|
+
case 'stale':
|
|
259
|
+
return `Upstream data${shortName ? ` from "${shortName}"` : ''} is past its freshness window${node?.freshnessStatus ? ` (freshness: ${node.freshnessStatus})` : ''}.`;
|
|
260
|
+
default:
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=dbt-freshness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dbt-freshness.js","sourceRoot":"","sources":["../../src/manifest/dbt-freshness.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain declaration writer (spec 17, part B).
|
|
3
|
+
*
|
|
4
|
+
* `scanDomains` reads first-class `domain "<Name>" { ... }` declarations and
|
|
5
|
+
* `dql doctor` warns when a used domain has no such declaration — but until now
|
|
6
|
+
* there was no way to AUTHOR one. This module emits a `domain` declaration in
|
|
7
|
+
* the EXACT format the parser (`parseDomainDecl`) + `scanDomains` accept, and
|
|
8
|
+
* resolves the canonical on-disk location for it.
|
|
9
|
+
*
|
|
10
|
+
* Convention (matches `scanDomains` + the "domain-first folders" test):
|
|
11
|
+
* domains/<slug>/domain.dql ← when a domain folder exists or is chosen
|
|
12
|
+
* domains/<slug>/domain.dql ← created if absent (the writer makes the dir)
|
|
13
|
+
*
|
|
14
|
+
* A domain authored here is `name`, `owner`, `boundedContext`, `sourceSystems`,
|
|
15
|
+
* `description`. `reviewCadence` is included with a sensible default so the
|
|
16
|
+
* doctor's "missing reviewCadence" warning is also satisfied.
|
|
17
|
+
*/
|
|
18
|
+
/** A domain authored through the writer (spec 17, part B). */
|
|
19
|
+
export interface DomainInput {
|
|
20
|
+
name: string;
|
|
21
|
+
owner?: string;
|
|
22
|
+
boundedContext?: string;
|
|
23
|
+
sourceSystems?: string[];
|
|
24
|
+
description?: string;
|
|
25
|
+
/** Optional review cadence; defaults to "quarterly" so doctor stays quiet. */
|
|
26
|
+
reviewCadence?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface WrittenDomain {
|
|
29
|
+
/** Project-relative path of the written `domain.dql`. */
|
|
30
|
+
path: string;
|
|
31
|
+
/** Absolute path on disk. */
|
|
32
|
+
absPath: string;
|
|
33
|
+
/** The folder slug under `domains/`. */
|
|
34
|
+
slug: string;
|
|
35
|
+
}
|
|
36
|
+
/** Normalize a domain name/id into a folder slug under `domains/`. */
|
|
37
|
+
export declare function domainFolderSlug(value: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Render a first-class `domain` declaration that `scanDomains` reads back. The
|
|
40
|
+
* field order + syntax mirror the parser's accepted set so a round-trip is
|
|
41
|
+
* lossless for the authored fields.
|
|
42
|
+
*/
|
|
43
|
+
export declare function renderDomainDeclaration(domain: DomainInput): string;
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the canonical `domains/<slug>/domain.dql` path for a domain. Honors an
|
|
46
|
+
* EXISTING `domain.dql` anywhere under `domains/<slug>/` (so a PUT/DELETE finds
|
|
47
|
+
* the file `scanDomains` actually read), else falls back to the canonical path.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveDomainDeclPath(projectRoot: string, nameOrSlug: string): {
|
|
50
|
+
absPath: string;
|
|
51
|
+
relativePath: string;
|
|
52
|
+
slug: string;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Write (create or overwrite) a first-class domain declaration. Returns the path
|
|
56
|
+
* written. The folder is created so subsequent block authoring can place blocks
|
|
57
|
+
* under `domains/<slug>/blocks/`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function writeDomainDeclaration(projectRoot: string, domain: DomainInput): WrittenDomain;
|
|
60
|
+
/**
|
|
61
|
+
* Delete a domain declaration file. Returns true when a file was removed. Only
|
|
62
|
+
* the declaration `.dql` is removed — authored blocks/terms under the folder are
|
|
63
|
+
* left untouched.
|
|
64
|
+
*/
|
|
65
|
+
export declare function deleteDomainDeclaration(projectRoot: string, nameOrSlug: string): boolean;
|
|
66
|
+
//# sourceMappingURL=domain-writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-writer.d.ts","sourceRoot":"","sources":["../../src/manifest/domain-writer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,8DAA8D;AAC9D,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;CACd;AAMD,sEAAsE;AACtE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQtD;AAOD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAWnE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,GACjB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAOzD;AAkBD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,aAAa,CAQ9F;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAKxF"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain declaration writer (spec 17, part B).
|
|
3
|
+
*
|
|
4
|
+
* `scanDomains` reads first-class `domain "<Name>" { ... }` declarations and
|
|
5
|
+
* `dql doctor` warns when a used domain has no such declaration — but until now
|
|
6
|
+
* there was no way to AUTHOR one. This module emits a `domain` declaration in
|
|
7
|
+
* the EXACT format the parser (`parseDomainDecl`) + `scanDomains` accept, and
|
|
8
|
+
* resolves the canonical on-disk location for it.
|
|
9
|
+
*
|
|
10
|
+
* Convention (matches `scanDomains` + the "domain-first folders" test):
|
|
11
|
+
* domains/<slug>/domain.dql ← when a domain folder exists or is chosen
|
|
12
|
+
* domains/<slug>/domain.dql ← created if absent (the writer makes the dir)
|
|
13
|
+
*
|
|
14
|
+
* A domain authored here is `name`, `owner`, `boundedContext`, `sourceSystems`,
|
|
15
|
+
* `description`. `reviewCadence` is included with a sensible default so the
|
|
16
|
+
* doctor's "missing reviewCadence" warning is also satisfied.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { dirname, join, relative } from 'node:path';
|
|
20
|
+
function escapeString(value) {
|
|
21
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
22
|
+
}
|
|
23
|
+
/** Normalize a domain name/id into a folder slug under `domains/`. */
|
|
24
|
+
export function domainFolderSlug(value) {
|
|
25
|
+
return (value
|
|
26
|
+
.trim()
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
29
|
+
.replace(/^-+|-+$/g, '') || 'domain');
|
|
30
|
+
}
|
|
31
|
+
function stringArrayField(name, values) {
|
|
32
|
+
if (!values || values.length === 0)
|
|
33
|
+
return '';
|
|
34
|
+
return `\n ${name} = [${values.map((v) => `"${escapeString(v)}"`).join(', ')}]`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Render a first-class `domain` declaration that `scanDomains` reads back. The
|
|
38
|
+
* field order + syntax mirror the parser's accepted set so a round-trip is
|
|
39
|
+
* lossless for the authored fields.
|
|
40
|
+
*/
|
|
41
|
+
export function renderDomainDeclaration(domain) {
|
|
42
|
+
const reviewCadence = domain.reviewCadence?.trim() || 'quarterly';
|
|
43
|
+
const lines = [`domain "${escapeString(domain.name)}" {`];
|
|
44
|
+
if (domain.owner)
|
|
45
|
+
lines.push(` owner = "${escapeString(domain.owner)}"`);
|
|
46
|
+
if (domain.boundedContext)
|
|
47
|
+
lines.push(` boundedContext = "${escapeString(domain.boundedContext)}"`);
|
|
48
|
+
lines.push(` reviewCadence = "${escapeString(reviewCadence)}"`);
|
|
49
|
+
let body = lines.join('\n');
|
|
50
|
+
body += stringArrayField('sourceSystems', domain.sourceSystems);
|
|
51
|
+
if (domain.description)
|
|
52
|
+
body += `\n description = "${escapeString(domain.description)}"`;
|
|
53
|
+
body += '\n}\n';
|
|
54
|
+
return body;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the canonical `domains/<slug>/domain.dql` path for a domain. Honors an
|
|
58
|
+
* EXISTING `domain.dql` anywhere under `domains/<slug>/` (so a PUT/DELETE finds
|
|
59
|
+
* the file `scanDomains` actually read), else falls back to the canonical path.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveDomainDeclPath(projectRoot, nameOrSlug) {
|
|
62
|
+
const slug = domainFolderSlug(nameOrSlug);
|
|
63
|
+
const domainDir = join(projectRoot, 'domains', slug);
|
|
64
|
+
// Prefer an existing declaration file (any `*.dql` that declares this domain).
|
|
65
|
+
const existing = findExistingDomainFile(domainDir);
|
|
66
|
+
const absPath = existing ?? join(domainDir, 'domain.dql');
|
|
67
|
+
return { absPath, relativePath: relative(projectRoot, absPath), slug };
|
|
68
|
+
}
|
|
69
|
+
/** Find an existing `.dql` file under a domain folder that declares a domain. */
|
|
70
|
+
function findExistingDomainFile(domainDir) {
|
|
71
|
+
if (!existsSync(domainDir))
|
|
72
|
+
return undefined;
|
|
73
|
+
try {
|
|
74
|
+
for (const entry of readdirSync(domainDir, { withFileTypes: true })) {
|
|
75
|
+
if (!entry.isFile() || !entry.name.endsWith('.dql'))
|
|
76
|
+
continue;
|
|
77
|
+
const file = join(domainDir, entry.name);
|
|
78
|
+
const source = readFileSync(file, 'utf-8');
|
|
79
|
+
if (/(^|\n)\s*domain\s+"/.test(source))
|
|
80
|
+
return file;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Best-effort.
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Write (create or overwrite) a first-class domain declaration. Returns the path
|
|
90
|
+
* written. The folder is created so subsequent block authoring can place blocks
|
|
91
|
+
* under `domains/<slug>/blocks/`.
|
|
92
|
+
*/
|
|
93
|
+
export function writeDomainDeclaration(projectRoot, domain) {
|
|
94
|
+
if (!domain.name || !domain.name.trim()) {
|
|
95
|
+
throw new Error('writeDomainDeclaration requires a non-empty domain name.');
|
|
96
|
+
}
|
|
97
|
+
const { absPath, relativePath, slug } = resolveDomainDeclPath(projectRoot, domain.name);
|
|
98
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
99
|
+
writeFileSync(absPath, renderDomainDeclaration(domain), 'utf-8');
|
|
100
|
+
return { path: relativePath, absPath, slug };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Delete a domain declaration file. Returns true when a file was removed. Only
|
|
104
|
+
* the declaration `.dql` is removed — authored blocks/terms under the folder are
|
|
105
|
+
* left untouched.
|
|
106
|
+
*/
|
|
107
|
+
export function deleteDomainDeclaration(projectRoot, nameOrSlug) {
|
|
108
|
+
const { absPath } = resolveDomainDeclPath(projectRoot, nameOrSlug);
|
|
109
|
+
if (!existsSync(absPath))
|
|
110
|
+
return false;
|
|
111
|
+
rmSync(absPath, { force: true });
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=domain-writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-writer.js","sourceRoot":"","sources":["../../src/manifest/domain-writer.ts"],"names":[],"mappings":"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"}
|
package/dist/manifest/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
export { buildManifest, collectInputFiles, loadProjectConfig, resolveDataLexManifestPath, resolveDbtManifestPath, type ManifestBuildOptions, type DbtImportFilters, } from './builder.js';
|
|
2
|
-
export
|
|
2
|
+
export { detectOutputDrift } from './output-drift.js';
|
|
3
|
+
export { writeDomainDeclaration, deleteDomainDeclaration, renderDomainDeclaration, resolveDomainDeclPath, domainFolderSlug, type DomainInput, type WrittenDomain, } from './domain-writer.js';
|
|
4
|
+
export { loadDbtRunState, applyBlockDataState, worseDataState, type DbtRunStateIndex, } from './dbt-freshness.js';
|
|
5
|
+
export type { DQLManifest, ManifestBlock, ManifestDomain, ManifestTerm, ManifestBusinessView, ManifestNotebook, ManifestNotebookCell, ManifestMetric, ManifestDimension, ManifestSource, ManifestLineage, ManifestLineageNode, ManifestLineageEdge, ManifestDbtImport, ManifestDiagnostic, ManifestConflictDetail, ManifestConflictSide, ManifestDriftDetail, ManifestApp, ManifestDashboard, DbtDataState, DbtRunState, } from './types.js';
|
|
3
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/manifest/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,0BAA0B,EAC1B,sBAAsB,EACtB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,GACtB,MAAM,cAAc,CAAC;AAEtB,YAAY,EACV,WAAW,EACX,aAAa,EACb,YAAY,EACZ,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,iBAAiB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/manifest/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,0BAA0B,EAC1B,sBAAsB,EACtB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,GACtB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,WAAW,EAChB,KAAK,aAAa,GACnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC;AAE5B,YAAY,EACV,WAAW,EACX,aAAa,EACb,cAAc,EACd,YAAY,EACZ,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,kBAAkB,EAClB,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,EACnB,WAAW,EACX,iBAAiB,EACjB,YAAY,EACZ,WAAW,GACZ,MAAM,YAAY,CAAC"}
|
package/dist/manifest/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
// Manifest — DQL project compilation artifact
|
|
2
2
|
export { buildManifest, collectInputFiles, loadProjectConfig, resolveDataLexManifestPath, resolveDbtManifestPath, } from './builder.js';
|
|
3
|
+
export { detectOutputDrift } from './output-drift.js';
|
|
4
|
+
export { writeDomainDeclaration, deleteDomainDeclaration, renderDomainDeclaration, resolveDomainDeclPath, domainFolderSlug, } from './domain-writer.js';
|
|
5
|
+
export { loadDbtRunState, applyBlockDataState, worseDataState, } from './dbt-freshness.js';
|
|
3
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/manifest/index.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAE9C,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,0BAA0B,EAC1B,sBAAsB,GAGvB,MAAM,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/manifest/index.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAE9C,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,0BAA0B,EAC1B,sBAAsB,GAGvB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,qBAAqB,EACrB,gBAAgB,GAGjB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,cAAc,GAEf,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output-contract drift detection.
|
|
3
|
+
*
|
|
4
|
+
* DQL composition is intentionally unbounded freeform `ref()` between blocks.
|
|
5
|
+
* The cost of that freedom: when a child block changes its output columns, a
|
|
6
|
+
* parent that `ref()`s it can break *silently*. We do not restrict composition
|
|
7
|
+
* — instead we make drift *visible*.
|
|
8
|
+
*
|
|
9
|
+
* This pass compares the columns a parent block references on a `ref()`'d child
|
|
10
|
+
* (extracted from the parent's SQL) against the child's current `outputContract`
|
|
11
|
+
* (its actual output schema). A referenced column that is missing/renamed
|
|
12
|
+
* produces a `kind: 'drift'`, `severity: 'warning'` diagnostic naming the
|
|
13
|
+
* parent, the child, and the drifted column. It never fails the build, and
|
|
14
|
+
* additive changes (the child gaining new columns) produce no warning.
|
|
15
|
+
*
|
|
16
|
+
* Non-goals (explicitly rejected — see the feature spec):
|
|
17
|
+
* - Hard compile-time parent→child binding / failing the build.
|
|
18
|
+
* - Runtime enforcement.
|
|
19
|
+
*/
|
|
20
|
+
import type { ManifestBlock, ManifestDiagnostic } from './types.js';
|
|
21
|
+
/**
|
|
22
|
+
* Detect output-contract drift across all blocks. For each parent block that
|
|
23
|
+
* `ref()`s a child block and references one of its columns, warn when that
|
|
24
|
+
* column is no longer in the child's `outputContract`.
|
|
25
|
+
*
|
|
26
|
+
* Conservative on every axis: unknown child schemas, unparseable parent SQL,
|
|
27
|
+
* and ambiguous (multi-source, unqualified) column references all yield no
|
|
28
|
+
* warning rather than a false positive — see `extractRefColumnUsage`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function detectOutputDrift(blocks: Record<string, ManifestBlock>): ManifestDiagnostic[];
|
|
31
|
+
//# sourceMappingURL=output-drift.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-drift.d.ts","sourceRoot":"","sources":["../../src/manifest/output-drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AA4BpE;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,GAAG,kBAAkB,EAAE,CA0C7F"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output-contract drift detection.
|
|
3
|
+
*
|
|
4
|
+
* DQL composition is intentionally unbounded freeform `ref()` between blocks.
|
|
5
|
+
* The cost of that freedom: when a child block changes its output columns, a
|
|
6
|
+
* parent that `ref()`s it can break *silently*. We do not restrict composition
|
|
7
|
+
* — instead we make drift *visible*.
|
|
8
|
+
*
|
|
9
|
+
* This pass compares the columns a parent block references on a `ref()`'d child
|
|
10
|
+
* (extracted from the parent's SQL) against the child's current `outputContract`
|
|
11
|
+
* (its actual output schema). A referenced column that is missing/renamed
|
|
12
|
+
* produces a `kind: 'drift'`, `severity: 'warning'` diagnostic naming the
|
|
13
|
+
* parent, the child, and the drifted column. It never fails the build, and
|
|
14
|
+
* additive changes (the child gaining new columns) produce no warning.
|
|
15
|
+
*
|
|
16
|
+
* Non-goals (explicitly rejected — see the feature spec):
|
|
17
|
+
* - Hard compile-time parent→child binding / failing the build.
|
|
18
|
+
* - Runtime enforcement.
|
|
19
|
+
*/
|
|
20
|
+
import { extractRefColumnUsage } from '../lineage/column-lineage.js';
|
|
21
|
+
/** Case-insensitive column-name key for comparing referenced vs. declared columns. */
|
|
22
|
+
function columnKey(name) {
|
|
23
|
+
return name.trim().toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The set of columns a child block guarantees, derived from its
|
|
27
|
+
* `outputContract`. Returns `null` when the child's output schema is unknown
|
|
28
|
+
* (no contract, or a contract that includes a star/unresolved entry) — in that
|
|
29
|
+
* case we cannot tell a real rename from an unparsed projection, so we stay
|
|
30
|
+
* silent rather than emit a false positive.
|
|
31
|
+
*/
|
|
32
|
+
function knownChildColumns(child) {
|
|
33
|
+
const contract = child.outputContract;
|
|
34
|
+
if (!contract || contract.length === 0)
|
|
35
|
+
return null;
|
|
36
|
+
const names = new Set();
|
|
37
|
+
for (const entry of contract) {
|
|
38
|
+
const name = entry.name?.trim();
|
|
39
|
+
// A `*` / wildcard contract entry means the schema is open — we can't
|
|
40
|
+
// distinguish a renamed column from a still-present one.
|
|
41
|
+
if (!name || name === '*' || name.endsWith('.*'))
|
|
42
|
+
return null;
|
|
43
|
+
names.add(columnKey(name));
|
|
44
|
+
}
|
|
45
|
+
return names;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Detect output-contract drift across all blocks. For each parent block that
|
|
49
|
+
* `ref()`s a child block and references one of its columns, warn when that
|
|
50
|
+
* column is no longer in the child's `outputContract`.
|
|
51
|
+
*
|
|
52
|
+
* Conservative on every axis: unknown child schemas, unparseable parent SQL,
|
|
53
|
+
* and ambiguous (multi-source, unqualified) column references all yield no
|
|
54
|
+
* warning rather than a false positive — see `extractRefColumnUsage`.
|
|
55
|
+
*/
|
|
56
|
+
export function detectOutputDrift(blocks) {
|
|
57
|
+
const diagnostics = [];
|
|
58
|
+
for (const parent of Object.values(blocks)) {
|
|
59
|
+
if (!parent.sql || !(parent.refDependencies?.length))
|
|
60
|
+
continue;
|
|
61
|
+
const usages = extractRefColumnUsage(parent.sql);
|
|
62
|
+
if (usages.length === 0)
|
|
63
|
+
continue;
|
|
64
|
+
for (const usage of usages) {
|
|
65
|
+
const child = blocks[usage.block];
|
|
66
|
+
if (!child)
|
|
67
|
+
continue; // unresolved ref — handled elsewhere as a resolve diagnostic
|
|
68
|
+
if (child.name === parent.name)
|
|
69
|
+
continue; // self-reference, ignore
|
|
70
|
+
const childColumns = knownChildColumns(child);
|
|
71
|
+
if (!childColumns)
|
|
72
|
+
continue; // child schema unknown — cannot judge drift
|
|
73
|
+
const available = (child.outputContract ?? []).map((c) => c.name);
|
|
74
|
+
for (const referenced of usage.columns) {
|
|
75
|
+
if (childColumns.has(columnKey(referenced)))
|
|
76
|
+
continue;
|
|
77
|
+
diagnostics.push({
|
|
78
|
+
kind: 'drift',
|
|
79
|
+
filePath: parent.filePath,
|
|
80
|
+
severity: 'warning',
|
|
81
|
+
message: `block "${parent.name}" references column "${referenced}" on ref("${child.name}"), ` +
|
|
82
|
+
`but "${child.name}" no longer outputs it` +
|
|
83
|
+
(available.length > 0 ? ` (current outputs: ${available.join(', ')})` : '') +
|
|
84
|
+
`. The child's output contract drifted — update the reference or restore the column.`,
|
|
85
|
+
drift: {
|
|
86
|
+
parentBlock: parent.name,
|
|
87
|
+
childBlock: child.name,
|
|
88
|
+
column: referenced,
|
|
89
|
+
availableColumns: available,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return diagnostics;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=output-drift.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-drift.js","sourceRoot":"","sources":["../../src/manifest/output-drift.ts"],"names":[],"mappings":"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"}
|