@intentius/chant 0.124.22 → 0.124.24
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/audit/rules-doc.js +1 -1
- package/dist/ci-jobs.d.ts +41 -1
- package/dist/ci-jobs.d.ts.map +1 -1
- package/dist/ci-jobs.js +48 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +14 -0
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +9 -0
- package/dist/cli/handlers/emulator.d.ts +13 -2
- package/dist/cli/handlers/emulator.d.ts.map +1 -1
- package/dist/cli/handlers/emulator.js +63 -3
- package/dist/cli/handlers/graph.js +24 -1
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +7 -1
- package/dist/cli/handlers/run-wave.d.ts.map +1 -1
- package/dist/cli/handlers/run-wave.js +5 -4
- package/dist/cli/handlers/run.d.ts +4 -3
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +73 -9
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +17 -4
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +31 -0
- package/dist/cli/registry.d.ts +12 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +45 -6
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +97 -22
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +25 -10
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +24 -2
- package/dist/graph-ir.d.ts +43 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ir.js +27 -0
- package/dist/lexicon.d.ts +25 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +11 -0
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/git.js +62 -6
- package/dist/lifecycle/no-record.d.ts +59 -0
- package/dist/lifecycle/no-record.d.ts.map +1 -0
- package/dist/lifecycle/no-record.js +88 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +30 -9
- package/dist/op/emulator-lifecycle.d.ts +23 -0
- package/dist/op/emulator-lifecycle.d.ts.map +1 -1
- package/dist/op/emulator-lifecycle.js +39 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +3 -0
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +1 -1
- package/dist/op/local-executor.d.ts +10 -0
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +7 -2
- package/dist/op/op-waves-notify.d.ts +2 -0
- package/dist/op/op-waves-notify.d.ts.map +1 -1
- package/dist/op/op-waves-notify.js +9 -3
- package/dist/op/op-waves-run.d.ts +8 -2
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +36 -11
- package/dist/op/op-waves.d.ts +85 -1
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +153 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/runtimes/local.js +6 -0
- package/dist/op/step-console.d.ts +65 -0
- package/dist/op/step-console.d.ts.map +1 -0
- package/dist/op/step-console.js +140 -0
- package/dist/point-decision.d.ts.map +1 -1
- package/dist/point-decision.js +3 -0
- package/dist/wave-runtime.mjs +240 -224
- package/dist/workspace/checks/links.d.ts +5 -0
- package/dist/workspace/checks/links.d.ts.map +1 -1
- package/dist/workspace/checks/links.js +54 -8
- package/dist/workspace/checks.d.ts +4 -1
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +6 -2
- package/dist/workspace/compose-graph.d.ts +26 -4
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/compose-graph.js +21 -7
- package/dist/workspace/dbt-project-reader.d.ts +36 -0
- package/dist/workspace/dbt-project-reader.d.ts.map +1 -0
- package/dist/workspace/dbt-project-reader.js +290 -0
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/graph-cli.js +3 -1
- package/dist/workspace/graph.schema.json +109 -4
- package/dist/workspace/intent.js +7 -2
- package/dist/workspace/kind-link-readers.d.ts +84 -0
- package/dist/workspace/kind-link-readers.d.ts.map +1 -0
- package/dist/workspace/kind-link-readers.js +53 -0
- package/dist/workspace/kinds.d.ts +15 -0
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/kinds.js +11 -0
- package/dist/workspace/lexicon-outputs.d.ts +63 -0
- package/dist/workspace/lexicon-outputs.d.ts.map +1 -0
- package/dist/workspace/lexicon-outputs.js +241 -0
- package/dist/workspace/links.d.ts +29 -7
- package/dist/workspace/links.d.ts.map +1 -1
- package/dist/workspace/links.js +109 -34
- package/dist/workspace/member-handles.d.ts +18 -1
- package/dist/workspace/member-handles.d.ts.map +1 -1
- package/dist/workspace/member-handles.js +31 -14
- package/dist/workspace/nested-graph.d.ts +1 -1
- package/dist/workspace/nested-graph.d.ts.map +1 -1
- package/dist/workspace/nested-graph.js +5 -1
- package/dist/workspace/records-checkout.d.ts +11 -1
- package/dist/workspace/records-checkout.d.ts.map +1 -1
- package/dist/workspace/records-checkout.js +38 -2
- package/dist/workspace/runs.d.ts.map +1 -1
- package/dist/workspace/runs.js +4 -2
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +3 -2
- package/dist/workspace/workspace-kinds.schema.json +5 -0
- package/package.json +1 -1
- package/src/audit/rules-doc.ts +1 -1
- package/src/ci-jobs.ts +58 -1
- package/src/cli/commands/build.ts +18 -0
- package/src/cli/handlers/approve.ts +9 -0
- package/src/cli/handlers/emulator.test.ts +61 -1
- package/src/cli/handlers/emulator.ts +65 -4
- package/src/cli/handlers/graph.ts +22 -1
- package/src/cli/handlers/promote.test.ts +7 -0
- package/src/cli/handlers/promote.ts +7 -1
- package/src/cli/handlers/run-no-record.e2e.test.ts +87 -0
- package/src/cli/handlers/run-wave.ts +5 -4
- package/src/cli/handlers/run.test.ts +126 -0
- package/src/cli/handlers/run.ts +69 -9
- package/src/cli/main.test.ts +24 -0
- package/src/cli/main.ts +17 -4
- package/src/cli/parse-args.ts +21 -0
- package/src/cli/registry.ts +12 -0
- package/src/components/cli-support.test.ts +75 -0
- package/src/components/cli-support.ts +130 -25
- package/src/components/driver.test.ts +28 -0
- package/src/components/driver.ts +23 -10
- package/src/components/generate-disabled.test.ts +56 -0
- package/src/graph-ci-edges.test.ts +50 -0
- package/src/graph-detail.ts +21 -2
- package/src/graph-ir.test.ts +23 -1
- package/src/graph-ir.ts +65 -0
- package/src/lexicon.ts +26 -1
- package/src/lifecycle/git.ts +61 -6
- package/src/lifecycle/no-record.test.ts +175 -0
- package/src/lifecycle/no-record.ts +108 -0
- package/src/op/activities/shell.ts +28 -6
- package/src/op/emulator-lifecycle.test.ts +24 -1
- package/src/op/emulator-lifecycle.ts +58 -2
- package/src/op/gate.ts +3 -0
- package/src/op/index.ts +1 -1
- package/src/op/local-executor.ts +20 -3
- package/src/op/op-waves-jobs.test.ts +213 -0
- package/src/op/op-waves-notify.ts +11 -5
- package/src/op/op-waves-run.ts +42 -11
- package/src/op/op-waves.ts +202 -2
- package/src/op/runtimes/local.ts +6 -0
- package/src/op/step-console.test.ts +150 -0
- package/src/op/step-console.ts +155 -0
- package/src/point-decision-bundle.test.ts +1 -0
- package/src/point-decision.ts +3 -0
- package/src/wave-loop-golden.test.ts +3 -0
- package/src/workspace/checks/links.ts +57 -8
- package/src/workspace/checks.e2e.test.ts +1 -1
- package/src/workspace/checks.ts +9 -2
- package/src/workspace/compose-graph.test.ts +55 -0
- package/src/workspace/compose-graph.ts +33 -9
- package/src/workspace/dbt-kind.e2e.test.ts +95 -0
- package/src/workspace/dbt-kind.test.ts +283 -0
- package/src/workspace/dbt-project-reader.ts +291 -0
- package/src/workspace/graph-cli.ts +3 -1
- package/src/workspace/graph-contract.test.ts +26 -1
- package/src/workspace/graph.schema.json +109 -4
- package/src/workspace/intent.ts +7 -2
- package/src/workspace/kind-link-readers.ts +109 -0
- package/src/workspace/kinds.ts +21 -1
- package/src/workspace/lexicon-outputs.ts +272 -0
- package/src/workspace/links.test.ts +22 -0
- package/src/workspace/links.ts +129 -34
- package/src/workspace/member-handles.ts +28 -8
- package/src/workspace/nested-graph.test.ts +14 -0
- package/src/workspace/nested-graph.ts +7 -2
- package/src/workspace/records-checkout.ts +36 -2
- package/src/workspace/records-uncommitted.test.ts +5 -1
- package/src/workspace/runs.ts +4 -2
- package/src/workspace/status.ts +3 -2
- package/src/workspace/workspace-kinds.schema.json +5 -0
package/src/workspace/links.ts
CHANGED
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
* that parameter give way to the declared one, which is how an inferred edge
|
|
16
16
|
* is promoted.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
18
|
+
* A member whose kind has a link reader (#3932, `./kind-link-readers.ts`) also
|
|
19
|
+
* states links in its own files, such as a dbt project's sources. Those rows
|
|
20
|
+
* have origin `kind-reader` and carry the file and line that state them.
|
|
21
|
+
*
|
|
22
|
+
* Every row says how it was found (`declared`, `kind-reader` or `inferred:joinKey`), where it
|
|
19
23
|
* resolved (`source`, or `live` for the rows of `check --live`, #2549) and its status. A link that
|
|
20
24
|
* can't be resolved is kept, with the reason.
|
|
21
25
|
*
|
|
@@ -30,6 +34,7 @@ import { joinKey, joinLabel, type JoinLabel } from "../join-key";
|
|
|
30
34
|
import type { Declaration, LinkDeclaration, Member } from "./declaration";
|
|
31
35
|
import type { KindRegistry, MemberKind } from "./kinds";
|
|
32
36
|
import type { MemberCollector } from "./compose-graph";
|
|
37
|
+
import type { KindLinkRead, KindReadLink } from "./kind-link-readers";
|
|
33
38
|
|
|
34
39
|
/**
|
|
35
40
|
* The link kinds chant knows. Closed: a link of any other kind fails closed.
|
|
@@ -46,14 +51,20 @@ export const TELEMETRY_LINK_KIND = "telemetry";
|
|
|
46
51
|
/** The OTLP protocols a telemetry link may state, as `OTEL_EXPORTER_OTLP_PROTOCOL` names them. */
|
|
47
52
|
export const TELEMETRY_PROTOCOLS = ["grpc", "http/protobuf", "http/json"] as const;
|
|
48
53
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
54
|
+
/**
|
|
55
|
+
* How a row was found: written down in the declaration, read from the
|
|
56
|
+
* member's own files by its kind's link reader (#3932), or inferred by the
|
|
57
|
+
* named rule.
|
|
58
|
+
*/
|
|
59
|
+
export type LinkOrigin = "declared" | "kind-reader" | "inferred:joinKey";
|
|
51
60
|
|
|
52
61
|
/** One output or parameter of a member, as a join sees it. */
|
|
53
62
|
export interface JoinHandle {
|
|
54
63
|
name: string;
|
|
55
64
|
/** The node that holds it, when the reader knows one (a graph IR id). */
|
|
56
65
|
node?: string;
|
|
66
|
+
/** Names it had before, from a rename hint in the producer's source (#3931). */
|
|
67
|
+
previously?: readonly string[];
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
/** What a member offers links, and what it reads. */
|
|
@@ -80,6 +91,10 @@ interface RowBase {
|
|
|
80
91
|
resolves: "source" | "live";
|
|
81
92
|
/** Why the row is not `resolved`, or null. */
|
|
82
93
|
reason: string | null;
|
|
94
|
+
/** A `kind-reader` row (#3932): the reader, and the file (from the workspace root) and line that state the link. */
|
|
95
|
+
reader?: string;
|
|
96
|
+
file?: string;
|
|
97
|
+
line?: number;
|
|
83
98
|
}
|
|
84
99
|
|
|
85
100
|
/** A declared link, or an inferred join with one producer. */
|
|
@@ -113,7 +128,11 @@ export interface TelemetryTargets {
|
|
|
113
128
|
composed: ReadonlySet<string>;
|
|
114
129
|
}
|
|
115
130
|
|
|
116
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* A parameter that joins outputs of two or more producers (or two outputs of
|
|
133
|
+
* one). For a `kind-reader` row whose file names no producer, `input` is the
|
|
134
|
+
* output it reads, and the candidates are the members that could produce it.
|
|
135
|
+
*/
|
|
117
136
|
export interface AmbiguousRow extends RowBase {
|
|
118
137
|
status: "ambiguous";
|
|
119
138
|
input: string;
|
|
@@ -170,9 +189,16 @@ function nearNames(name: string, outputs: JoinHandle[]): string[] {
|
|
|
170
189
|
* Resolve every declared link and infer the joins nobody wrote down.
|
|
171
190
|
* `handles` holds one entry per member whose handles were read; a member
|
|
172
191
|
* missing from it is unresolved. Declared rows come first, in declaration
|
|
173
|
-
* order;
|
|
192
|
+
* order; then the rows each member's kind reader found (`readLinks`, #3932),
|
|
193
|
+
* in member and file order; inferred and ambiguous rows follow, sorted by
|
|
194
|
+
* consumer and parameter.
|
|
174
195
|
*/
|
|
175
|
-
export function resolveLinks(
|
|
196
|
+
export function resolveLinks(
|
|
197
|
+
declaration: Declaration,
|
|
198
|
+
handles: ReadonlyMap<string, MemberHandles>,
|
|
199
|
+
telemetry?: TelemetryTargets,
|
|
200
|
+
readLinks?: ReadonlyMap<string, KindLinkRead>,
|
|
201
|
+
): LinkTableRow[] {
|
|
176
202
|
const rows: LinkTableRow[] = [];
|
|
177
203
|
const declaredTargets = new Map<string, Set<string>>();
|
|
178
204
|
|
|
@@ -215,30 +241,18 @@ export function resolveLinks(declaration: Declaration, handles: ReadonlyMap<stri
|
|
|
215
241
|
row.reason = `protocol applies to telemetry links; this link has kind ${kind}`;
|
|
216
242
|
continue;
|
|
217
243
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
} else if (!producer.complete) {
|
|
231
|
-
row.status = "unresolved";
|
|
232
|
-
row.reason = `${link.output} was not found in ${link.member}, whose outputs could not all be read in source${producer.why ? ` (${producer.why})` : ""}`;
|
|
233
|
-
} else {
|
|
234
|
-
const near = nearNames(link.output, producer.outputs);
|
|
235
|
-
const known = producer.outputs.map((o) => o.name);
|
|
236
|
-
row.status = "missing";
|
|
237
|
-
row.reason =
|
|
238
|
-
`${link.member} has no output ${link.output}` +
|
|
239
|
-
(near.length ? `; did you mean ${near.join(" or ")}? Links match exactly` : "") +
|
|
240
|
-
(known.length ? `; its outputs: ${[...new Set(known)].sort(cmp).join(", ")}` : "; it exposes no outputs");
|
|
241
|
-
}
|
|
244
|
+
resolveOutput(row, handles);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Links a kind's reader found in the member's own files (#3932).
|
|
249
|
+
for (const consumer of declaration.members) {
|
|
250
|
+
const read = readLinks?.get(consumer.name);
|
|
251
|
+
if (!read) continue;
|
|
252
|
+
for (const link of read.links) {
|
|
253
|
+
const row = readerRow(declaration, consumer, read, link, handles);
|
|
254
|
+
rows.push(row);
|
|
255
|
+
if (row.status !== "ambiguous") (declaredTargets.get(consumer.name) ?? declaredTargets.set(consumer.name, new Set()).get(consumer.name)!).add(`${row.producer}\0${row.output}`);
|
|
242
256
|
}
|
|
243
257
|
}
|
|
244
258
|
|
|
@@ -297,6 +311,85 @@ export function resolveLinks(declaration: Declaration, handles: ReadonlyMap<stri
|
|
|
297
311
|
return [...rows, ...inferred];
|
|
298
312
|
}
|
|
299
313
|
|
|
314
|
+
/** Match a declared or reader's output row against its producer's handles, setting its status and reason. */
|
|
315
|
+
function resolveOutput(row: LinkRow, handles: ReadonlyMap<string, MemberHandles>): void {
|
|
316
|
+
const producer = handles.get(row.producer);
|
|
317
|
+
const found = producer?.outputs.find((o) => o.name === row.output);
|
|
318
|
+
if (found) {
|
|
319
|
+
if (found.node) row.to = found.node;
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (!producer) {
|
|
323
|
+
row.status = "unresolved";
|
|
324
|
+
row.reason = `the outputs of ${row.producer} were not read`;
|
|
325
|
+
} else if (producer.exposesNone) {
|
|
326
|
+
row.status = "missing";
|
|
327
|
+
row.reason = producer.why ?? `${row.producer} exposes no outputs`;
|
|
328
|
+
} else if (!producer.complete) {
|
|
329
|
+
row.status = "unresolved";
|
|
330
|
+
row.reason = `${row.output} was not found in ${row.producer}, whose outputs could not all be read in source${producer.why ? ` (${producer.why})` : ""}`;
|
|
331
|
+
} else {
|
|
332
|
+
const renamed = [...new Set(producer.outputs.filter((o) => o.previously?.includes(row.output)).map((o) => o.name))].sort(cmp);
|
|
333
|
+
const near = renamed.length ? [] : nearNames(row.output, producer.outputs);
|
|
334
|
+
const known = producer.outputs.map((o) => o.name);
|
|
335
|
+
row.status = "missing";
|
|
336
|
+
row.reason =
|
|
337
|
+
`${row.producer} has no output ${row.output}` +
|
|
338
|
+
(renamed.length ? `; it was renamed to ${renamed.join(" or ")}, so link to that instead` : "") +
|
|
339
|
+
(near.length ? `; did you mean ${near.join(" or ")}? Links match exactly` : "") +
|
|
340
|
+
(known.length ? `; its outputs: ${[...new Set(known)].sort(cmp).join(", ")}` : "; it exposes no outputs");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* The row for one link a kind's reader found (#3932). A link whose file names
|
|
346
|
+
* its producer resolves like a declared one. One that doesn't is given the
|
|
347
|
+
* producer that exposes its anchor (a dbt column's table); when none does,
|
|
348
|
+
* and exactly one other member exposes outputs at all, that member, so a
|
|
349
|
+
* dropped table is missing from the only place it could be. Otherwise the
|
|
350
|
+
* row is ambiguous, and the message names the field that settles it.
|
|
351
|
+
*/
|
|
352
|
+
function readerRow(declaration: Declaration, consumer: Member, read: KindLinkRead, link: KindReadLink, handles: ReadonlyMap<string, MemberHandles>): LinkTableRow {
|
|
353
|
+
const base = { consumer: consumer.name, kind: DEFAULT_LINK_KIND, origin: "kind-reader" as const, resolves: "source" as const, reader: read.reader, file: link.file, line: link.line };
|
|
354
|
+
let producer = link.member;
|
|
355
|
+
if (producer === null) {
|
|
356
|
+
const anchor = link.anchor ?? link.output;
|
|
357
|
+
const others = declaration.members.filter((m) => m.name !== consumer.name);
|
|
358
|
+
const exposing = others.filter((m) => handles.get(m.name)?.outputs.some((o) => o.name === anchor));
|
|
359
|
+
const possible = others.filter((m) => {
|
|
360
|
+
const h = handles.get(m.name);
|
|
361
|
+
return h !== undefined && !h.exposesNone;
|
|
362
|
+
});
|
|
363
|
+
if (exposing.length === 1) producer = exposing[0].name;
|
|
364
|
+
else if (exposing.length === 0 && possible.length === 1) producer = possible[0].name;
|
|
365
|
+
else {
|
|
366
|
+
const candidates = (exposing.length > 0 ? exposing : possible).map((m) => ({ producer: m.name, output: link.output, label: "exact" as const }));
|
|
367
|
+
const field = read.producerField ? `; set fields.${read.producerField} on ${consumer.name} to name it` : "";
|
|
368
|
+
return {
|
|
369
|
+
...base,
|
|
370
|
+
status: "ambiguous",
|
|
371
|
+
input: link.output,
|
|
372
|
+
candidates,
|
|
373
|
+
reason:
|
|
374
|
+
exposing.length > 1
|
|
375
|
+
? `${link.file}:${link.line} reads ${anchor}, which ${exposing.map((m) => m.name).join(" and ")} all expose${field}`
|
|
376
|
+
: candidates.length > 0
|
|
377
|
+
? `${link.file}:${link.line} reads ${anchor}, which no member exposes, and ${consumer.name} does not say which of ${candidates.map((c) => c.producer).join(", ")} produces it${field}`
|
|
378
|
+
: `${link.file}:${link.line} reads ${anchor}, and no other member exposes outputs`,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const row: LinkRow = { ...base, producer, output: link.output, label: "exact", input: null, status: "resolved", reason: null };
|
|
383
|
+
const problem = linkTargetProblem(declaration, consumer, { member: producer } as LinkDeclaration);
|
|
384
|
+
if (problem) {
|
|
385
|
+
row.status = "invalid";
|
|
386
|
+
row.reason = read.producerField ? `fields.${read.producerField}: ${problem}` : problem;
|
|
387
|
+
return row;
|
|
388
|
+
}
|
|
389
|
+
resolveOutput(row, handles);
|
|
390
|
+
return row;
|
|
391
|
+
}
|
|
392
|
+
|
|
300
393
|
/**
|
|
301
394
|
* Resolve a telemetry link against the producer's collector (#2558): the
|
|
302
395
|
* target exists (presence), and when the link states a protocol, the target
|
|
@@ -369,7 +462,7 @@ export interface ComposedHandles {
|
|
|
369
462
|
composed: readonly string[];
|
|
370
463
|
/** The collectors those members report (#2558); a telemetry link resolves against them. */
|
|
371
464
|
collectors?: readonly MemberCollector[];
|
|
372
|
-
exports: readonly { member: string; name: string; node?: string }[];
|
|
465
|
+
exports: readonly { member: string; name: string; node?: string; previously?: readonly string[] }[];
|
|
373
466
|
imports: readonly { member: string; name: string; node: string }[];
|
|
374
467
|
}
|
|
375
468
|
|
|
@@ -384,7 +477,9 @@ export function irMemberHandles(graph: ComposedHandles): Map<string, MemberHandl
|
|
|
384
477
|
for (const member of graph.composed) {
|
|
385
478
|
out.set(member, {
|
|
386
479
|
member,
|
|
387
|
-
outputs: graph.exports
|
|
480
|
+
outputs: graph.exports
|
|
481
|
+
.filter((e) => e.member === member)
|
|
482
|
+
.map((e) => ({ name: e.name, ...(e.node ? { node: e.node } : {}), ...(e.previously?.length ? { previously: e.previously } : {}) })),
|
|
388
483
|
inputs: graph.imports.filter((i) => i.member === member).map((i) => ({ name: i.name, node: i.node })),
|
|
389
484
|
complete: true,
|
|
390
485
|
why: null,
|
|
@@ -401,7 +496,7 @@ export function irMemberHandles(graph: ComposedHandles): Map<string, MemberHandl
|
|
|
401
496
|
* With `kinds`, a member that isn't composed but whose kind lists its outputs
|
|
402
497
|
* (`other`, kinds from a package) is a link target too.
|
|
403
498
|
*/
|
|
404
|
-
export function graphLinks(declaration: Declaration, graph: ComposedHandles, kinds?: KindRegistry): LinkTableRow[] {
|
|
499
|
+
export function graphLinks(declaration: Declaration, graph: ComposedHandles, kinds?: KindRegistry, readLinks?: ReadonlyMap<string, KindLinkRead>): LinkTableRow[] {
|
|
405
500
|
const handles = irMemberHandles(graph);
|
|
406
501
|
for (const m of declaration.members) {
|
|
407
502
|
const kind = kinds?.get(m.kind);
|
|
@@ -413,7 +508,7 @@ export function graphLinks(declaration: Declaration, graph: ComposedHandles, kin
|
|
|
413
508
|
collectors: new Map((graph.collectors ?? []).map((c) => [c.member, c])),
|
|
414
509
|
composed: new Set(graph.composed),
|
|
415
510
|
};
|
|
416
|
-
return resolveLinks(declaration, handles, telemetry).map((row) => {
|
|
511
|
+
return resolveLinks(declaration, handles, telemetry, readLinks).map((row) => {
|
|
417
512
|
if (row.status === "ambiguous" || row.origin !== "declared") return row;
|
|
418
513
|
// The declaration's pointer means nothing to a graph reader.
|
|
419
514
|
const { pointer: _pointer, ...rest } = row;
|
|
@@ -25,6 +25,7 @@ import { isInside, type Declaration, type ResolvedGroup } from "./declaration";
|
|
|
25
25
|
import type { KindRegistry } from "./kinds";
|
|
26
26
|
import { kindHandles, type MemberHandles } from "./links";
|
|
27
27
|
import { joinPath, skippedDir, type WorkspaceTree } from "./tree";
|
|
28
|
+
import type { MemberLexiconOutputs } from "./lexicon-outputs";
|
|
28
29
|
|
|
29
30
|
const memberDir = (dir: string) => (dir === "." ? "" : dir);
|
|
30
31
|
|
|
@@ -60,7 +61,7 @@ function isSourceFile(name: string): boolean {
|
|
|
60
61
|
* directories (other members, group matches), `node_modules`, dot
|
|
61
62
|
* directories and build output.
|
|
62
63
|
*/
|
|
63
|
-
function sourceFiles(tree: WorkspaceTree, dir: string, exclude: ReadonlySet<string>): string[] {
|
|
64
|
+
export function sourceFiles(tree: WorkspaceTree, dir: string, exclude: ReadonlySet<string>): string[] {
|
|
64
65
|
const out: string[] = [];
|
|
65
66
|
const walk = (at: string) => {
|
|
66
67
|
for (const e of tree.list(at) ?? []) {
|
|
@@ -188,18 +189,35 @@ export function readSourceHandles(tree: WorkspaceTree, dir: string, exclude: Rea
|
|
|
188
189
|
return out;
|
|
189
190
|
}
|
|
190
191
|
|
|
192
|
+
/**
|
|
193
|
+
* The directories inside member `m` that are not its source: other members'
|
|
194
|
+
* directories and example-group matches nested in it.
|
|
195
|
+
*/
|
|
196
|
+
export function memberExcludes(declaration: Declaration, groups: readonly ResolvedGroup[], m: Declaration["members"][number]): Set<string> {
|
|
197
|
+
const exclude = new Set<string>();
|
|
198
|
+
for (const o of declaration.members) if (o !== m && o.dir !== "." && isInside(o.dir, m.dir)) exclude.add(o.dir);
|
|
199
|
+
for (const g of groups) for (const dir of g.matches) if (isInside(dir, m.dir)) exclude.add(dir);
|
|
200
|
+
return exclude;
|
|
201
|
+
}
|
|
202
|
+
|
|
191
203
|
/**
|
|
192
204
|
* Every member's handles, read in source for `chant workspace check`. A
|
|
193
205
|
* `chant` member's outputs and parameters are parsed from its TypeScript
|
|
194
206
|
* without running it; other kinds expose what the kind and the entry list.
|
|
195
207
|
* A member whose directory is missing or whose kind is unknown is left out,
|
|
196
208
|
* so links to it are unresolved.
|
|
209
|
+
*
|
|
210
|
+
* `lexiconOutputs` adds what the member's lexicons report about their own
|
|
211
|
+
* declarations (#3931, `./lexicon-outputs.ts`), gathered beforehand because
|
|
212
|
+
* reading them folds the member's files, which is asynchronous. Its gaps
|
|
213
|
+
* count as the member's.
|
|
197
214
|
*/
|
|
198
215
|
export function sourceMemberHandles(
|
|
199
216
|
declaration: Declaration,
|
|
200
217
|
tree: WorkspaceTree,
|
|
201
218
|
groups: readonly ResolvedGroup[],
|
|
202
219
|
kinds: KindRegistry,
|
|
220
|
+
lexiconOutputs?: ReadonlyMap<string, MemberLexiconOutputs>,
|
|
203
221
|
): Map<string, MemberHandles> {
|
|
204
222
|
const out = new Map<string, MemberHandles>();
|
|
205
223
|
for (const m of declaration.members) {
|
|
@@ -210,16 +228,18 @@ export function sourceMemberHandles(
|
|
|
210
228
|
out.set(m.name, listed);
|
|
211
229
|
} else {
|
|
212
230
|
// Other members' directories and group matches inside this one are not its source.
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const read = readSourceHandles(tree, memberDir(m.dir), exclude);
|
|
231
|
+
const read = readSourceHandles(tree, memberDir(m.dir), memberExcludes(declaration, groups, m));
|
|
232
|
+
const lexicon = lexiconOutputs?.get(m.name);
|
|
233
|
+
const gaps = [...read.gaps, ...(lexicon?.gaps ?? [])];
|
|
217
234
|
out.set(m.name, {
|
|
218
235
|
member: m.name,
|
|
219
|
-
outputs:
|
|
236
|
+
outputs: [
|
|
237
|
+
...read.outputs.map((o) => ({ name: o.name })),
|
|
238
|
+
...(lexicon?.outputs ?? []).map((o) => ({ name: o.name, ...(o.previously?.length ? { previously: o.previously } : {}) })),
|
|
239
|
+
],
|
|
220
240
|
inputs: read.inputs.map((i) => ({ name: i.name })),
|
|
221
|
-
complete:
|
|
222
|
-
why:
|
|
241
|
+
complete: gaps.length === 0,
|
|
242
|
+
why: gaps.length === 0 ? null : gaps.length === 1 ? gaps[0] : `${gaps[0]}, and ${gaps.length - 1} more`,
|
|
223
243
|
exposesNone: false,
|
|
224
244
|
});
|
|
225
245
|
}
|
|
@@ -97,6 +97,20 @@ describe("foldNested", () => {
|
|
|
97
97
|
});
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
+
describe("foldNested with CI pipelines and dangling references (#3922)", () => {
|
|
101
|
+
test("prefixes byPipeline and dangling with the outer member, and keeps the nested member on each reference", () => {
|
|
102
|
+
const outer = composeWorkspaceGraph({ name: "outer", root: "." }, [
|
|
103
|
+
{ member: member("platform", { kind: "workspace", status: "skipped", reason: { code: "kind-not-run", message: "nested" }, chant: null, irVersion: null }) },
|
|
104
|
+
]);
|
|
105
|
+
const inner = innerGraph();
|
|
106
|
+
inner.groups.byPipeline = { "api/ci": ["api/Queue"] };
|
|
107
|
+
inner.dangling = [{ from: "api/Queue", kind: "needs", path: "needs", value: "lint", member: "api" }];
|
|
108
|
+
foldNested(outer, outer.members[0], inner);
|
|
109
|
+
expect(outer.groups.byPipeline).toEqual({ "platform/api/ci": ["platform/api/Queue"] });
|
|
110
|
+
expect(outer.dangling).toEqual([{ from: "platform/api/Queue", kind: "needs", path: "needs", value: "lint", member: "platform", nested: "api" }]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
100
114
|
describe("the outer workspace never writes inside a nested one", () => {
|
|
101
115
|
let root: string;
|
|
102
116
|
afterEach(() => rmSync(root, { recursive: true, force: true }));
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { spawn } from "node:child_process";
|
|
24
24
|
import type { ParsedArgs } from "../cli/registry";
|
|
25
|
-
import type
|
|
25
|
+
import { danglingKey, SCOPED_GROUPS, type ComposedMember, type ComposedNode, type WorkspaceGraph } from "./compose-graph";
|
|
26
26
|
|
|
27
27
|
export type { NestedWorkspace } from "./compose-graph";
|
|
28
28
|
import { readerToolchain, type Toolchain } from "./member-commands";
|
|
@@ -148,12 +148,17 @@ export function foldNested(graph: WorkspaceGraph, member: ComposedMember, inner:
|
|
|
148
148
|
into[k] = [...(into[k] ?? []), ...v.map((id) => prefixed(outer, id))].sort();
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
for (const key of
|
|
151
|
+
for (const key of SCOPED_GROUPS) {
|
|
152
152
|
for (const [k, v] of Object.entries(innerGroups[key] ?? {})) {
|
|
153
153
|
const into = (groups[key] ??= {});
|
|
154
154
|
into[prefixed(outer, k)] = v.map((id) => prefixed(outer, id)).sort();
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
+
if (inner.dangling?.length) {
|
|
158
|
+
graph.dangling = [...(graph.dangling ?? []), ...inner.dangling.map((d) => ({ ...d, from: prefixed(outer, d.from), member: outer, nested: d.member }))].sort((a, b) =>
|
|
159
|
+
danglingKey(a).localeCompare(danglingKey(b)),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
157
162
|
for (const c of inner.collectors ?? []) graph.collectors.push({ ...c, member: prefixed(outer, c.member) });
|
|
158
163
|
for (const [kind, attrs] of Object.entries(inner.derivedAttrs ?? {})) {
|
|
159
164
|
const all = new Set([...(graph.derivedAttrs?.[kind] ?? []), ...attrs]);
|
|
@@ -11,10 +11,15 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Every git call here reads. None takes the index lock or refreshes the
|
|
13
13
|
* index (`--no-optional-locks`), so a read leaves the checkout as it was,
|
|
14
|
-
* `.git` included, which the reader conformance suite checks.
|
|
14
|
+
* `.git` included, which the reader conformance suite checks. A diff against
|
|
15
|
+
* the working tree is the exception git makes: it refreshes the index even
|
|
16
|
+
* then, so {@link worktreeChanges} runs it on a private copy.
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
import { execFileSync } from "node:child_process";
|
|
20
|
+
import { copyFileSync, existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
21
|
+
import { tmpdir } from "node:os";
|
|
22
|
+
import { isAbsolute, join } from "node:path";
|
|
18
23
|
import { resolveBase, type BaseSource } from "./trust/provenance";
|
|
19
24
|
|
|
20
25
|
/** Where a record's file stands against `HEAD`. */
|
|
@@ -55,6 +60,35 @@ function tryGit(top: string, args: string[]): string | undefined {
|
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The paths under `spec` whose working-tree content differs from `rev`, or
|
|
65
|
+
* undefined when git can't say. `git diff <rev>` refreshes the index's stat
|
|
66
|
+
* data for a file touched without a change, and writes the index even under
|
|
67
|
+
* `--no-optional-locks` (git 2.54), so it runs against a copy of the index
|
|
68
|
+
* (`GIT_INDEX_FILE`) and the checkout's own index is never written.
|
|
69
|
+
*/
|
|
70
|
+
export function worktreeChanges(top: string, rev: string, spec: string): string[] | undefined {
|
|
71
|
+
const scratch = mkdtempSync(join(tmpdir(), "chant-index-"));
|
|
72
|
+
try {
|
|
73
|
+
const at = tryGit(top, ["rev-parse", "--git-path", "index"])?.trim();
|
|
74
|
+
const index = at ? (isAbsolute(at) ? at : join(top, at)) : undefined;
|
|
75
|
+
const copy = join(scratch, "index");
|
|
76
|
+
if (index && existsSync(index)) copyFileSync(index, copy);
|
|
77
|
+
const out = execFileSync("git", ["--no-optional-locks", "--literal-pathspecs", "diff", "--name-only", "-z", "--no-renames", rev, "--", spec], {
|
|
78
|
+
cwd: top,
|
|
79
|
+
encoding: "utf-8",
|
|
80
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
81
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
82
|
+
env: { ...process.env, GIT_INDEX_FILE: copy },
|
|
83
|
+
});
|
|
84
|
+
return out.split("\0").filter(Boolean);
|
|
85
|
+
} catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
} finally {
|
|
88
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
58
92
|
/**
|
|
59
93
|
* The branch, head and base of the checkout at `top`. `explicitBase` is
|
|
60
94
|
* `--base`, as the trust policy takes it; otherwise the target branch is the
|
|
@@ -98,7 +132,7 @@ export function worktreeStates(
|
|
|
98
132
|
const direct = (p: string) => p.startsWith(prefix) && !p.slice(prefix.length).includes("/");
|
|
99
133
|
const spec = dir === "." ? "." : dir;
|
|
100
134
|
const atHead = new Set(zList(tryGit(top, ["ls-tree", "-r", "-z", "--name-only", head, "--", spec])).filter(direct));
|
|
101
|
-
const changed = new Set(
|
|
135
|
+
const changed = new Set(worktreeChanges(top, head, spec) ?? []);
|
|
102
136
|
for (const p of paths) states.set(p, !atHead.has(p) ? "new" : changed.has(p) ? "modified" : "committed");
|
|
103
137
|
const present = new Set(paths);
|
|
104
138
|
const deleted = [...atHead].filter((p) => !present.has(p) && match.test(p.slice(prefix.length))).sort();
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { execFileSync } from "node:child_process";
|
|
9
|
-
import { cpSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { cpSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import Ajv2020 from "ajv/dist/2020";
|
|
@@ -101,6 +101,10 @@ describe("records in the working tree (#3160)", () => {
|
|
|
101
101
|
const { root } = repository();
|
|
102
102
|
const path = join(root, file("001"));
|
|
103
103
|
writeFileSync(path, decided("001"));
|
|
104
|
+
// Its stat data no longer matches the index on any clock, so a read that
|
|
105
|
+
// refreshes the index would write it (a CI runner was slow enough to).
|
|
106
|
+
const later = new Date(Date.now() + 5_000);
|
|
107
|
+
utimesSync(path, later, later);
|
|
104
108
|
const index = join(root, ".git", "index");
|
|
105
109
|
const before = { bytes: readFileSync(index), mtime: statSync(index).mtimeMs };
|
|
106
110
|
const doc = result(await queryRecords({ kind: KIND, cwd: root, uncommitted: true }));
|
package/src/workspace/runs.ts
CHANGED
|
@@ -55,7 +55,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
55
55
|
import { readFileSync, statSync } from "node:fs";
|
|
56
56
|
import { resolve } from "node:path";
|
|
57
57
|
import { z } from "zod";
|
|
58
|
-
import { describeLifecyclePush, fetchLifecycleStatus, type LifecyclePushStatus, pushLifecycleStatus, readBlobBySha, RefCASConflictError, writeLedgerFiles } from "../lifecycle/git";
|
|
58
|
+
import { describeLifecyclePush, fetchLifecycleStatus, ledgerRevision, type LifecyclePushStatus, pushLifecycleStatus, readBlobBySha, RefCASConflictError, writeLedgerFiles } from "../lifecycle/git";
|
|
59
59
|
import { resolveMemberLedger } from "../lifecycle/member-ledger";
|
|
60
60
|
import { sortedJsonReplacer } from "../utils";
|
|
61
61
|
import { followSquashes, type SquashFollow } from "./squash";
|
|
@@ -509,7 +509,9 @@ async function appendRun(top: string, rootOnDisk: string, id: string, make: (exi
|
|
|
509
509
|
let written: RunLine[] = [];
|
|
510
510
|
let lastErr: unknown;
|
|
511
511
|
for (let attempt = 1; attempt <= APPEND_RETRY_ATTEMPTS && commit === undefined; attempt++) {
|
|
512
|
-
|
|
512
|
+
// #3955: in a run that records nothing, the run's own unrecorded tip.
|
|
513
|
+
const rev = await ledgerRevision(top);
|
|
514
|
+
const prior = git(top, ["rev-parse", "--verify", "--quiet", `${rev === LEDGER_BRANCH ? `refs/heads/${LEDGER_BRANCH}` : rev}:${path}`])?.trim() || null;
|
|
513
515
|
const text = prior ? ((await readBlobBySha(prior, { cwd: top })) ?? "") : "";
|
|
514
516
|
const existing = parseRunFile(text, id).lines;
|
|
515
517
|
const lines = make(existing);
|
package/src/workspace/status.ts
CHANGED
|
@@ -41,7 +41,7 @@ import { readPathSha } from "../lifecycle/git";
|
|
|
41
41
|
import { latestPerComponent, readReleaseLedger, type ReleaseRecord } from "../lifecycle/release-ledger";
|
|
42
42
|
import { readReleasePlan, type ReleasePlan } from "../lifecycle/plan-ledger";
|
|
43
43
|
import { listWorkLeases, type WorkLeaseState } from "../lifecycle/work-lease";
|
|
44
|
-
import { readCheckoutHead, type CheckoutHead } from "./records-checkout";
|
|
44
|
+
import { readCheckoutHead, worktreeChanges, type CheckoutHead } from "./records-checkout";
|
|
45
45
|
import { policyView, replicationState, type ReplicationState } from "./wip";
|
|
46
46
|
import { findWorkspaceRoot } from "../project-root";
|
|
47
47
|
import { resolveBoxes, type ResolvedIsolation } from "./box-isolation";
|
|
@@ -400,7 +400,8 @@ async function shipView(member: Member, cwd: string, read: LedgerReader): Promis
|
|
|
400
400
|
};
|
|
401
401
|
// The commit must be here: a release from a commit this checkout never had says nothing about it.
|
|
402
402
|
if (lines(["cat-file", "-e", `${last.gitSha}^{commit}`]) === null) return view;
|
|
403
|
-
|
|
403
|
+
// A diff against the working tree writes the index; run it on a copy.
|
|
404
|
+
const changed = worktreeChanges(top, last.gitSha, ".") ?? null;
|
|
404
405
|
const untracked = lines(["ls-files", "--others", "--exclude-standard", "-z"]);
|
|
405
406
|
if (changed === null || untracked === null) return view;
|
|
406
407
|
const bookkeeping = (path: string) => ship.bookkeeping.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
|
|
@@ -78,6 +78,11 @@
|
|
|
78
78
|
},
|
|
79
79
|
"fields": {
|
|
80
80
|
"$ref": "#/$defs/fields"
|
|
81
|
+
},
|
|
82
|
+
"reader": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"pattern": "^[a-z][a-z0-9-]{0,39}$",
|
|
85
|
+
"description": "A link reader chant ships, by name, that derives a member's links from the member's own files (#3932): dbt-project reads a dbt project's sources. The reader is chant's code and runs offline, parsing files as data; the package supplies only the name. A name this chant doesn't have keeps the kind without a reader, and the load reports it. A chant older than this field fails a kinds file that uses it."
|
|
81
86
|
}
|
|
82
87
|
},
|
|
83
88
|
"patternProperties": {
|