@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
|
@@ -93,6 +93,13 @@
|
|
|
93
93
|
"$ref": "#/$defs/edge"
|
|
94
94
|
}
|
|
95
95
|
},
|
|
96
|
+
"dangling": {
|
|
97
|
+
"description": "Added in contract 1 by #3922. The members' references whose target is no node of their graph (#3910): a job `needs` a job no workflow declares, a `uses:` names another repository's workflow, a GitLab `include:` names a file chant did not generate. Each member's `ir.dangling` entry with `from` prefixed by its member and a `member` field, sorted by `from`, `path` and `value`. Absent when no member has one.",
|
|
98
|
+
"type": "array",
|
|
99
|
+
"items": {
|
|
100
|
+
"$ref": "#/$defs/dangling"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
96
103
|
"groups": {
|
|
97
104
|
"type": "object",
|
|
98
105
|
"required": [
|
|
@@ -158,6 +165,16 @@
|
|
|
158
165
|
}
|
|
159
166
|
},
|
|
160
167
|
"description": "Keys prefixed with the member."
|
|
168
|
+
},
|
|
169
|
+
"byPipeline": {
|
|
170
|
+
"type": "object",
|
|
171
|
+
"additionalProperties": {
|
|
172
|
+
"type": "array",
|
|
173
|
+
"items": {
|
|
174
|
+
"type": "string"
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
"description": "Added in contract 1 by #3922. CI pipeline membership (#3910): a workflow or pipeline the member's CI lexicon reports, with the node ids it runs. Keys prefixed with the member, like byStack. Edges with `crossPipeline` run between these groups."
|
|
161
178
|
}
|
|
162
179
|
}
|
|
163
180
|
},
|
|
@@ -181,6 +198,13 @@
|
|
|
181
198
|
},
|
|
182
199
|
"member": {
|
|
183
200
|
"type": "string"
|
|
201
|
+
},
|
|
202
|
+
"previously": {
|
|
203
|
+
"type": "array",
|
|
204
|
+
"items": {
|
|
205
|
+
"type": "string"
|
|
206
|
+
},
|
|
207
|
+
"description": "Names this output had before, from a rename hint a lexicon read in the member's source (a sql `-- previously:` comment). A link to one of them is told the new name."
|
|
184
208
|
}
|
|
185
209
|
}
|
|
186
210
|
}
|
|
@@ -516,7 +540,15 @@
|
|
|
516
540
|
"type": "string"
|
|
517
541
|
},
|
|
518
542
|
"kind": {
|
|
519
|
-
"
|
|
543
|
+
"enum": [
|
|
544
|
+
"ref",
|
|
545
|
+
"needs",
|
|
546
|
+
"calls",
|
|
547
|
+
"triggers",
|
|
548
|
+
"includes",
|
|
549
|
+
"extends"
|
|
550
|
+
],
|
|
551
|
+
"description": "`ref` for an edge from a typed reference. The CI kinds (#3910): `needs` (a job waits on another), `calls` (a reusable workflow or action is used), `triggers` (a pipeline starts another), `includes` (a file is pulled in), `extends` (a GitLab job inherits another). The list grows within contract 1; a reader ignores a kind it does not know."
|
|
520
552
|
},
|
|
521
553
|
"viaAttr": {
|
|
522
554
|
"type": "string"
|
|
@@ -524,8 +556,50 @@
|
|
|
524
556
|
"toAttr": {
|
|
525
557
|
"type": "string"
|
|
526
558
|
},
|
|
559
|
+
"crossPipeline": {
|
|
560
|
+
"type": "boolean",
|
|
561
|
+
"description": "Added in contract 1 by #3922. True when the edge leaves the pipeline `from` belongs to (#3910): a reusable-workflow call, a `workflow_run` trigger, a GitLab child or multi-project trigger, an include. Absent on every in-pipeline edge."
|
|
562
|
+
},
|
|
563
|
+
"member": {
|
|
564
|
+
"type": "string"
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
"dangling": {
|
|
569
|
+
"type": "object",
|
|
570
|
+
"required": [
|
|
571
|
+
"from",
|
|
572
|
+
"kind",
|
|
573
|
+
"path",
|
|
574
|
+
"value",
|
|
575
|
+
"member"
|
|
576
|
+
],
|
|
577
|
+
"properties": {
|
|
578
|
+
"from": {
|
|
579
|
+
"type": "string",
|
|
580
|
+
"description": "The node holding the reference, prefixed with its member."
|
|
581
|
+
},
|
|
582
|
+
"kind": {
|
|
583
|
+
"$ref": "#/$defs/edge/properties/kind"
|
|
584
|
+
},
|
|
585
|
+
"path": {
|
|
586
|
+
"type": "string",
|
|
587
|
+
"description": "Where on `from` it was written, such as `needs`, `uses` or `trigger.include`."
|
|
588
|
+
},
|
|
589
|
+
"value": {
|
|
590
|
+
"type": "string",
|
|
591
|
+
"description": "The reference exactly as written."
|
|
592
|
+
},
|
|
593
|
+
"crossPipeline": {
|
|
594
|
+
"type": "boolean",
|
|
595
|
+
"description": "As on an edge."
|
|
596
|
+
},
|
|
527
597
|
"member": {
|
|
528
598
|
"type": "string"
|
|
599
|
+
},
|
|
600
|
+
"nested": {
|
|
601
|
+
"type": "string",
|
|
602
|
+
"description": "For a member of kind `workspace`: the nested workspace's member the reference was read through, as on a node."
|
|
529
603
|
}
|
|
530
604
|
}
|
|
531
605
|
},
|
|
@@ -636,8 +710,23 @@
|
|
|
636
710
|
"origin": {
|
|
637
711
|
"enum": [
|
|
638
712
|
"declared",
|
|
713
|
+
"kind-reader",
|
|
639
714
|
"inferred:joinKey"
|
|
640
|
-
]
|
|
715
|
+
],
|
|
716
|
+
"description": "How the row was found: declared in chant.workspace.json, read from the member's own files by its kind's link reader (#3932), or inferred by joinKey()."
|
|
717
|
+
},
|
|
718
|
+
"reader": {
|
|
719
|
+
"type": "string",
|
|
720
|
+
"description": "A kind-reader row (#3932): the link reader that found it, such as dbt-project."
|
|
721
|
+
},
|
|
722
|
+
"file": {
|
|
723
|
+
"type": "string",
|
|
724
|
+
"description": "A kind-reader row (#3932): the file that states the link, from the workspace root."
|
|
725
|
+
},
|
|
726
|
+
"line": {
|
|
727
|
+
"type": "integer",
|
|
728
|
+
"minimum": 1,
|
|
729
|
+
"description": "A kind-reader row (#3932): the line of file that states the link."
|
|
641
730
|
},
|
|
642
731
|
"resolves": {
|
|
643
732
|
"const": "source"
|
|
@@ -728,8 +817,23 @@
|
|
|
728
817
|
"origin": {
|
|
729
818
|
"enum": [
|
|
730
819
|
"declared",
|
|
820
|
+
"kind-reader",
|
|
731
821
|
"inferred:joinKey"
|
|
732
|
-
]
|
|
822
|
+
],
|
|
823
|
+
"description": "How the row was found: declared in chant.workspace.json, read from the member's own files by its kind's link reader (#3932), or inferred by joinKey()."
|
|
824
|
+
},
|
|
825
|
+
"reader": {
|
|
826
|
+
"type": "string",
|
|
827
|
+
"description": "A kind-reader row (#3932): the link reader that found it, such as dbt-project."
|
|
828
|
+
},
|
|
829
|
+
"file": {
|
|
830
|
+
"type": "string",
|
|
831
|
+
"description": "A kind-reader row (#3932): the file that states the link, from the workspace root."
|
|
832
|
+
},
|
|
833
|
+
"line": {
|
|
834
|
+
"type": "integer",
|
|
835
|
+
"minimum": 1,
|
|
836
|
+
"description": "A kind-reader row (#3932): the line of file that states the link."
|
|
733
837
|
},
|
|
734
838
|
"resolves": {
|
|
735
839
|
"const": "source"
|
|
@@ -746,7 +850,8 @@
|
|
|
746
850
|
"description": "The consumer's parameter node, when known."
|
|
747
851
|
},
|
|
748
852
|
"input": {
|
|
749
|
-
"type": "string"
|
|
853
|
+
"type": "string",
|
|
854
|
+
"description": "The consumer's parameter the join would feed; for a kind-reader row (#3932), the output its file reads, which no single member was found to produce."
|
|
750
855
|
},
|
|
751
856
|
"status": {
|
|
752
857
|
"const": "ambiguous"
|
package/src/workspace/intent.ts
CHANGED
|
@@ -77,7 +77,7 @@ import { followSquashes, squashLineOrigins, type Forge, type SquashFollow } from
|
|
|
77
77
|
import { memberDirOnDisk, memberSymbolResolvers, type SymbolPluginLoader } from "./lexicon-symbols";
|
|
78
78
|
import { resolveSymbol, symbolResolverFor, type SymbolResolver } from "./symbols";
|
|
79
79
|
import { readTrailerJoins, type CommitTrailerJoins } from "./trailer-joins";
|
|
80
|
-
import { joinPath, skippedDir, type WorkspaceTree } from "./tree";
|
|
80
|
+
import { joinPath, skippedDir, workingTree, type WorkspaceTree } from "./tree";
|
|
81
81
|
import { activeAttestors, type ProvenanceLevel } from "./trust/attestor";
|
|
82
82
|
import { commitProvenance, policyAtBase, resolveBase } from "./trust/provenance";
|
|
83
83
|
import { locateWorkspace, type LocatedWorkspace } from "./which-chant";
|
|
@@ -1722,7 +1722,12 @@ async function walk(query: IntentQuery, head: Head): Promise<IntentResult> {
|
|
|
1722
1722
|
try {
|
|
1723
1723
|
const groups = resolveGroups(declaration, located.tree);
|
|
1724
1724
|
const kindsRegistry = loadKindRegistry(declaration.pins, located.rootOnDisk).registry;
|
|
1725
|
-
|
|
1725
|
+
const { gatherLexiconOutputs } = await import("./lexicon-outputs");
|
|
1726
|
+
const lexiconOutputs = await gatherLexiconOutputs(located.rootOnDisk, declaration, located.at ? workingTree(located.rootOnDisk) : located.tree, groups, kindsRegistry, {
|
|
1727
|
+
atRevision: located.at !== null,
|
|
1728
|
+
...(query.loadLexicon ? { load: query.loadLexicon as unknown as import("./lexicon-outputs").OutputsPluginLoader } : {}),
|
|
1729
|
+
});
|
|
1730
|
+
rows = resolveLinks(declaration, sourceMemberHandles(declaration, located.tree, groups, kindsRegistry, lexiconOutputs)).filter((r) => r.origin === "declared");
|
|
1726
1731
|
} catch (err) {
|
|
1727
1732
|
if (!(err instanceof WorkspaceReadError)) throw err;
|
|
1728
1733
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Links a kind reads from its member's own files (#3932).
|
|
3
|
+
*
|
|
4
|
+
* A member's links usually come from its entry in `chant.workspace.json`, or
|
|
5
|
+
* are inferred from a chant member's parameters by `joinKey()`. Some members
|
|
6
|
+
* already say what they read in files of their own: a dbt project names the
|
|
7
|
+
* tables it reads in its `sources:` YAML. A kind can name a link reader in its
|
|
8
|
+
* kinds file (`reader`), and chant runs that reader over the member's
|
|
9
|
+
* directory to derive the member's links.
|
|
10
|
+
*
|
|
11
|
+
* The readers are chant's code, listed in {@link KIND_LINK_READERS}: a kinds
|
|
12
|
+
* file stays data, and chant never imports the package that supplies it. A
|
|
13
|
+
* reader runs offline and parses files as data. It never invokes the tool
|
|
14
|
+
* whose files it reads, so `chant workspace check` keeps its promise of no
|
|
15
|
+
* network and no member code.
|
|
16
|
+
*
|
|
17
|
+
* A reader returns links with the file and line that state them, and gaps:
|
|
18
|
+
* the places it could not read without guessing, such as a templated value.
|
|
19
|
+
* `resolveLinks` (`./links.ts`) resolves the links beside the declared ones,
|
|
20
|
+
* with origin `kind-reader`; WSP093 and WSP094 name the reader's file and line,
|
|
21
|
+
* and WSP099 reports the gaps.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { Declaration, Member } from "./declaration";
|
|
25
|
+
import { resolveMemberFields, type FieldValue, type KindLinkReaderName, type KindRegistry } from "./kinds";
|
|
26
|
+
import type { WorkspaceTree } from "./tree";
|
|
27
|
+
import { readDbtProject } from "./dbt-project-reader";
|
|
28
|
+
|
|
29
|
+
/** One link a reader found in the member's files. */
|
|
30
|
+
export interface KindReadLink {
|
|
31
|
+
/**
|
|
32
|
+
* The producer the link names, or null when the member's files don't say
|
|
33
|
+
* which member produces it. A null producer is found among the members
|
|
34
|
+
* whose outputs include {@link anchor}; see `resolveLinks`.
|
|
35
|
+
*/
|
|
36
|
+
member: string | null;
|
|
37
|
+
/** The producer's output the link reads. */
|
|
38
|
+
output: string;
|
|
39
|
+
/**
|
|
40
|
+
* The output that picks the producer when `member` is null; unset, the
|
|
41
|
+
* output itself. A dbt source column is anchored on its table, so a dropped
|
|
42
|
+
* column is missing from the member that still has the table.
|
|
43
|
+
*/
|
|
44
|
+
anchor?: string;
|
|
45
|
+
/** The file that states the link, relative to the workspace root. */
|
|
46
|
+
file: string;
|
|
47
|
+
/** Its line, from 1. */
|
|
48
|
+
line: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A place the reader could not read without guessing. The links it would have stated are left out. */
|
|
52
|
+
export interface KindReadGap {
|
|
53
|
+
/** Relative to the workspace root. */
|
|
54
|
+
file: string;
|
|
55
|
+
line: number;
|
|
56
|
+
message: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface KindLinkRead {
|
|
60
|
+
/** The reader that ran. */
|
|
61
|
+
reader: KindLinkReaderName;
|
|
62
|
+
links: KindReadLink[];
|
|
63
|
+
gaps: KindReadGap[];
|
|
64
|
+
/**
|
|
65
|
+
* The member field that names the producer, for messages that ask the
|
|
66
|
+
* author to set it when the files don't say (`sqlMember` for dbt).
|
|
67
|
+
*/
|
|
68
|
+
producerField?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** What a reader gets: the member, its directory and resolved fields, and the tree read. */
|
|
72
|
+
export interface KindReaderInput {
|
|
73
|
+
member: Member;
|
|
74
|
+
/** The member's directory, tree-relative; "" for the workspace root. */
|
|
75
|
+
dir: string;
|
|
76
|
+
/** The member's fields, defaults filled in (#3151); null when its kind declares none. */
|
|
77
|
+
fields: Record<string, FieldValue | null | Record<string, FieldValue | null>> | null;
|
|
78
|
+
tree: WorkspaceTree;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type KindLinkReader = (input: KindReaderInput) => KindLinkRead;
|
|
82
|
+
|
|
83
|
+
/** Every reader chant ships, by the name a kinds file uses. */
|
|
84
|
+
export const LINK_READERS: Record<KindLinkReaderName, KindLinkReader> = {
|
|
85
|
+
"dbt-project": readDbtProject,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Run the link reader of every member whose kind has one. A member whose
|
|
90
|
+
* directory is missing is left out. A reader that throws is reported as a
|
|
91
|
+
* gap on the member's directory, so one unreadable project never stops
|
|
92
|
+
* the check.
|
|
93
|
+
*/
|
|
94
|
+
export function readKindLinks(declaration: Declaration, tree: WorkspaceTree, kinds: KindRegistry): Map<string, KindLinkRead> {
|
|
95
|
+
const out = new Map<string, KindLinkRead>();
|
|
96
|
+
for (const m of declaration.members) {
|
|
97
|
+
const kind = kinds.get(m.kind);
|
|
98
|
+
if (!kind?.reader) continue;
|
|
99
|
+
const dir = m.dir === "." ? "" : m.dir;
|
|
100
|
+
if (tree.stat(dir) !== "dir") continue;
|
|
101
|
+
const { fields } = resolveMemberFields(kind, m.fields);
|
|
102
|
+
try {
|
|
103
|
+
out.set(m.name, LINK_READERS[kind.reader]({ member: m, dir, fields, tree }));
|
|
104
|
+
} catch (err) {
|
|
105
|
+
out.set(m.name, { reader: kind.reader, links: [], gaps: [{ file: dir || ".", line: 1, message: `the ${kind.reader} reader failed: ${(err as Error).message}` }] });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
package/src/workspace/kinds.ts
CHANGED
|
@@ -128,8 +128,24 @@ export interface MemberKind {
|
|
|
128
128
|
graph?: KindGraph;
|
|
129
129
|
/** The fields a member entry of this kind may set, with their defaults (#3151); unset, the kind declares none. */
|
|
130
130
|
fields?: Record<string, KindField>;
|
|
131
|
+
/**
|
|
132
|
+
* The link reader chant runs over a member's own files (#3932), by name, from
|
|
133
|
+
* {@link KIND_LINK_READERS}. It derives the member's links offline, from
|
|
134
|
+
* files it parses as data; unset, the member's links are only those its
|
|
135
|
+
* entry states.
|
|
136
|
+
*/
|
|
137
|
+
reader?: KindLinkReaderName;
|
|
131
138
|
}
|
|
132
139
|
|
|
140
|
+
/**
|
|
141
|
+
* The link readers chant ships (#3932). A kind names one in its kinds file as
|
|
142
|
+
* `reader`. The readers are chant's own code, not the package's: a kinds file
|
|
143
|
+
* stays data, and chant never imports the package that supplies it. Closed:
|
|
144
|
+
* a kind naming another reader is kept without one, and the load reports it.
|
|
145
|
+
*/
|
|
146
|
+
export const KIND_LINK_READERS = ["dbt-project"] as const;
|
|
147
|
+
export type KindLinkReaderName = (typeof KIND_LINK_READERS)[number];
|
|
148
|
+
|
|
133
149
|
export interface KindRegistry {
|
|
134
150
|
get(name: string): MemberKind | undefined;
|
|
135
151
|
/** Every member kind's name, sorted, for "unknown kind" messages. */
|
|
@@ -384,7 +400,7 @@ export function parseKindData(text: string, source: string): KindData {
|
|
|
384
400
|
const problems: string[] = [];
|
|
385
401
|
const kinds: MemberKind[] = [];
|
|
386
402
|
const seen = new Set<string>();
|
|
387
|
-
for (const k of (raw as { kinds: { name: string; description: string; precedence: number; probe: FileProbe; outputs?: string[]; graph?: KindGraph; fields?: Record<string, KindField
|
|
403
|
+
for (const k of (raw as { kinds: { name: string; description: string; precedence: number; probe: FileProbe; outputs?: string[]; graph?: KindGraph; fields?: Record<string, KindField>; reader?: string }[] }).kinds) {
|
|
388
404
|
if (BUILTIN_KIND_NAMES.includes(k.name)) {
|
|
389
405
|
problems.push(`${source}: kind ${k.name} is built in and can't be supplied by a package`);
|
|
390
406
|
continue;
|
|
@@ -408,7 +424,11 @@ export function parseKindData(text: string, source: string): KindData {
|
|
|
408
424
|
source,
|
|
409
425
|
...(k.graph ? { graph: { lexicon: k.graph.lexicon, config: structuredClone(k.graph.config) } } : {}),
|
|
410
426
|
...(k.fields ? { fields: structuredClone(k.fields) } : {}),
|
|
427
|
+
...(k.reader && (KIND_LINK_READERS as readonly string[]).includes(k.reader) ? { reader: k.reader as KindLinkReaderName } : {}),
|
|
411
428
|
});
|
|
429
|
+
if (k.reader && !(KIND_LINK_READERS as readonly string[]).includes(k.reader)) {
|
|
430
|
+
problems.push(`${source}: kind ${k.name} names link reader ${k.reader}, which this chant does not have (it has ${KIND_LINK_READERS.join(", ")}); the kind is kept, and its members' links are only those their entries state`);
|
|
431
|
+
}
|
|
412
432
|
for (const problem of fieldDefaultProblems(k.fields ?? {})) problems.push(`${source}: kind ${k.name} ${problem}`);
|
|
413
433
|
}
|
|
414
434
|
return { kinds, problems };
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The outputs a member's lexicons report about their own declarations
|
|
3
|
+
* (#3931), read offline for `chant workspace check`.
|
|
4
|
+
*
|
|
5
|
+
* A sql member exposes each table and each column as a workspace output
|
|
6
|
+
* (`public.orders`, `public.orders.customer_id`) without anyone writing
|
|
7
|
+
* `output(...)`. The names are the lexicon's to give, so the lexicon answers
|
|
8
|
+
* through its `workspaceOutputs` hook, the way a CI lexicon answers
|
|
9
|
+
* `graphEdges` (#3910).
|
|
10
|
+
*
|
|
11
|
+
* The check never runs member code. The member's source files are folded
|
|
12
|
+
* with `foldProject` under `sandbox`, which evaluates the fold subset and
|
|
13
|
+
* imports nothing from the project: a sql `table` template becomes the real
|
|
14
|
+
* entity because the tag is the lexicon's own function, called with the
|
|
15
|
+
* folded parts. A file that does not fold is not run either. It is handed to
|
|
16
|
+
* the hook as unread, and when it is one of that lexicon's files (it uses the
|
|
17
|
+
* lexicon's tags), the hook records a gap: the member's outputs are then
|
|
18
|
+
* incomplete, and a link to a name not in them is unresolved rather than
|
|
19
|
+
* missing.
|
|
20
|
+
*
|
|
21
|
+
* Folding is asynchronous and the checks are not, so the outputs are
|
|
22
|
+
* gathered before the checks run, only for the members some declared link
|
|
23
|
+
* targets, and only for the kinds whose outputs are read from source.
|
|
24
|
+
*
|
|
25
|
+
* Under `--at` the revision's files are not on disk to fold, so a member
|
|
26
|
+
* whose lexicons report outputs gets a gap instead.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { join, posix } from "node:path";
|
|
30
|
+
import type { StaticFieldsRead } from "../config-static";
|
|
31
|
+
import { isDeclarable, type Declarable } from "../declarable";
|
|
32
|
+
import type { LexiconUnreadSource, LexiconWorkspaceOutput } from "../graph-ir";
|
|
33
|
+
import type { BuildRootContext, BuildRootContribution, IntrinsicDef } from "../lexicon";
|
|
34
|
+
import type { Declaration, ResolvedGroup } from "./declaration";
|
|
35
|
+
import type { KindRegistry } from "./kinds";
|
|
36
|
+
import { kindHandles } from "./links";
|
|
37
|
+
import { readKindLinks } from "./kind-link-readers";
|
|
38
|
+
import { memberExcludes, sourceFiles } from "./member-handles";
|
|
39
|
+
import { memberDirOnDisk } from "./lexicon-symbols";
|
|
40
|
+
import type { WorkspaceTree } from "./tree";
|
|
41
|
+
|
|
42
|
+
/** What one member's lexicons report: its outputs, and why the list may be incomplete. */
|
|
43
|
+
export interface MemberLexiconOutputs {
|
|
44
|
+
outputs: Pick<LexiconWorkspaceOutput, "name" | "previously">[];
|
|
45
|
+
gaps: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The part of a lexicon plugin this reader uses. */
|
|
49
|
+
export interface OutputsPlugin {
|
|
50
|
+
name?: string;
|
|
51
|
+
intrinsics?: () => IntrinsicDef[];
|
|
52
|
+
buildRoots?: (ctx: BuildRootContext) => Promise<BuildRootContribution>;
|
|
53
|
+
workspaceOutputs?: (entities: ReadonlyMap<string, Declarable>, unread?: readonly LexiconUnreadSource[]) => { outputs: LexiconWorkspaceOutput[]; gaps?: string[] } | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Loads a lexicon plugin by name; `loadPlugin` from the CLI unless a test swaps it. */
|
|
57
|
+
export type OutputsPluginLoader = (name: string) => Promise<OutputsPlugin>;
|
|
58
|
+
|
|
59
|
+
export interface GatherLexiconOutputsOptions {
|
|
60
|
+
/** Read under `--at`: the member's files are a revision's, not on disk, so nothing is folded. */
|
|
61
|
+
atRevision?: boolean;
|
|
62
|
+
load?: OutputsPluginLoader;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const firstLine = (err: unknown): string => (err instanceof Error ? err.message : String(err)).split("\n")[0];
|
|
66
|
+
|
|
67
|
+
const memberDir = (dir: string) => (dir === "." ? "" : dir);
|
|
68
|
+
|
|
69
|
+
/** The `sourceDir` a member's config sets, read without running it. */
|
|
70
|
+
function staticSourceDir(read: StaticFieldsRead): string | undefined {
|
|
71
|
+
const value = read.status === "read" ? read.fields.sourceDir : undefined;
|
|
72
|
+
if (typeof value !== "string") return undefined;
|
|
73
|
+
const dir = posix.normalize(value.replace(/\\/g, "/")).replace(/\/+$/, "");
|
|
74
|
+
return dir === "." || dir === "" ? undefined : dir;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Only the files under the member's `sourceDir`, when its config sets one, as
|
|
79
|
+
* a build reads them (#3941). Without one, every source file of the member.
|
|
80
|
+
*/
|
|
81
|
+
function withinSourceDir(files: string[], member: string, sourceDir: string | undefined): string[] {
|
|
82
|
+
if (!sourceDir) return files;
|
|
83
|
+
const prefix = `${member ? `${member}/` : ""}${sourceDir}/`;
|
|
84
|
+
return files.filter((f) => f.startsWith(prefix));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A folded file's exports, less the entities of lexicons the member doesn't
|
|
89
|
+
* declare (#3941). An Op is a `chant` entity, and several files exporting
|
|
90
|
+
* `op` would otherwise collide and leave the whole member unread, though no
|
|
91
|
+
* hook reads Ops. Values that are not entities (a composite) are kept.
|
|
92
|
+
*/
|
|
93
|
+
function ownExports(exports: ReadonlyMap<string, unknown>, lexicons: readonly string[]): Record<string, unknown> {
|
|
94
|
+
const own = new Set(lexicons);
|
|
95
|
+
const out: Record<string, unknown> = {};
|
|
96
|
+
for (const [name, value] of exports) {
|
|
97
|
+
if (isDeclarable(value) && !own.has(value.lexicon)) continue;
|
|
98
|
+
out[name] = value;
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The lexicon names a member's config declares, read without running it.
|
|
105
|
+
* Unlike the intent walk, a config that can't be read statically is not
|
|
106
|
+
* loaded: the check runs no member code, so it is a gap.
|
|
107
|
+
*/
|
|
108
|
+
async function staticMemberLexicons(dir: string): Promise<string[]> {
|
|
109
|
+
const { readLexiconDeclarationsStatically } = await import("../config-static");
|
|
110
|
+
const read = readLexiconDeclarationsStatically(dir);
|
|
111
|
+
if (read.status === "no-config") return [];
|
|
112
|
+
if (read.status !== "read") throw new Error(read.reason);
|
|
113
|
+
const { lexiconNames, registerLexiconDeclarations } = await import("../lexicon-module");
|
|
114
|
+
const { dirname } = await import("node:path");
|
|
115
|
+
registerLexiconDeclarations(read.entries, dirname(read.configPath));
|
|
116
|
+
return lexiconNames(read.entries);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The lexicon outputs of every member a declared link, or a link a kind's
|
|
121
|
+
* reader found (#3932), targets, keyed by
|
|
122
|
+
* member name. A member whose lexicons report nothing is left out.
|
|
123
|
+
* `tree` is the working tree under `root`.
|
|
124
|
+
*/
|
|
125
|
+
export async function gatherLexiconOutputs(
|
|
126
|
+
root: string,
|
|
127
|
+
declaration: Declaration,
|
|
128
|
+
tree: WorkspaceTree,
|
|
129
|
+
groups: readonly ResolvedGroup[],
|
|
130
|
+
kinds: KindRegistry,
|
|
131
|
+
options: GatherLexiconOutputsOptions = {},
|
|
132
|
+
): Promise<Map<string, MemberLexiconOutputs>> {
|
|
133
|
+
const out = new Map<string, MemberLexiconOutputs>();
|
|
134
|
+
const targets = new Set(declaration.members.flatMap((m) => m.links.filter((l) => l.member !== m.name).map((l) => l.member)));
|
|
135
|
+
// Links a kind's reader finds in a member's files target members too (#3932). One that names no
|
|
136
|
+
// producer may be any other member's, so every other member is read.
|
|
137
|
+
for (const [consumer, read] of readKindLinks(declaration, tree, kinds)) {
|
|
138
|
+
for (const link of read.links) {
|
|
139
|
+
if (link.member !== null) targets.add(link.member);
|
|
140
|
+
else for (const m of declaration.members) if (m.name !== consumer) targets.add(m.name);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const m of declaration.members) {
|
|
144
|
+
if (!targets.has(m.name)) continue;
|
|
145
|
+
const kind = kinds.get(m.kind);
|
|
146
|
+
if (!kind || kindHandles(m, kind) || tree.stat(memberDir(m.dir)) !== "dir") continue;
|
|
147
|
+
const read = await readMemberLexiconOutputs(root, declaration, tree, groups, m, options);
|
|
148
|
+
if (read) out.set(m.name, read);
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The lexicons the files import, by the package name chant gives a lexicon. */
|
|
154
|
+
function importedLexicons(tree: WorkspaceTree, files: readonly string[]): string[] {
|
|
155
|
+
const names = new Set<string>();
|
|
156
|
+
for (const file of files) {
|
|
157
|
+
let text: string;
|
|
158
|
+
try {
|
|
159
|
+
text = tree.read(file);
|
|
160
|
+
} catch {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
for (const m of text.matchAll(/from\s+["']@intentius\/chant-lexicon-([a-z0-9-]+)(?:\/[^"']*)?["']/g)) names.add(m[1]!);
|
|
164
|
+
}
|
|
165
|
+
return [...names].sort();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function readMemberLexiconOutputs(
|
|
169
|
+
root: string,
|
|
170
|
+
declaration: Declaration,
|
|
171
|
+
tree: WorkspaceTree,
|
|
172
|
+
groups: readonly ResolvedGroup[],
|
|
173
|
+
m: Declaration["members"][number],
|
|
174
|
+
options: GatherLexiconOutputsOptions,
|
|
175
|
+
): Promise<MemberLexiconOutputs | undefined> {
|
|
176
|
+
const dir = memberDirOnDisk(root, m.dir);
|
|
177
|
+
const { readConfigFieldsStatically } = await import("../config-static");
|
|
178
|
+
const files = withinSourceDir(
|
|
179
|
+
sourceFiles(tree, memberDir(m.dir), memberExcludes(declaration, groups, m)),
|
|
180
|
+
memberDir(m.dir),
|
|
181
|
+
staticSourceDir(readConfigFieldsStatically(dir, ["sourceDir"])),
|
|
182
|
+
);
|
|
183
|
+
let names: string[];
|
|
184
|
+
try {
|
|
185
|
+
names = await staticMemberLexicons(dir);
|
|
186
|
+
} catch {
|
|
187
|
+
// A config that can't be read statically is not run. The lexicons the
|
|
188
|
+
// source imports stand in for the ones it names.
|
|
189
|
+
names = importedLexicons(tree, files);
|
|
190
|
+
}
|
|
191
|
+
if (names.length === 0) return undefined;
|
|
192
|
+
const load = options.load ?? ((await import("../cli/plugins")).loadPlugin as unknown as OutputsPluginLoader);
|
|
193
|
+
const plugins: { name: string; plugin: OutputsPlugin }[] = [];
|
|
194
|
+
const gaps: string[] = [];
|
|
195
|
+
for (const name of names) {
|
|
196
|
+
try {
|
|
197
|
+
plugins.push({ name, plugin: await load(name) });
|
|
198
|
+
} catch (err) {
|
|
199
|
+
gaps.push(`lexicon "${name}" could not be loaded, so the outputs it reads were not (${firstLine(err)})`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const reporting = plugins.filter((p) => typeof p.plugin.workspaceOutputs === "function");
|
|
203
|
+
if (reporting.length === 0) return gaps.length ? { outputs: [], gaps } : undefined;
|
|
204
|
+
if (options.atRevision) {
|
|
205
|
+
return { outputs: [], gaps: [`the outputs of lexicon ${reporting.map((p) => p.name).join(", ")} are read from the working tree's files, not a revision's`] };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Fold the member's source, never run it.
|
|
209
|
+
const abs = (file: string) => join(root, ...file.split("/"));
|
|
210
|
+
const intrinsics = plugins.flatMap((p) => {
|
|
211
|
+
try {
|
|
212
|
+
return p.plugin.intrinsics?.() ?? [];
|
|
213
|
+
} catch {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
let entities: Map<string, Declarable> = new Map();
|
|
218
|
+
const unread: LexiconUnreadSource[] = [];
|
|
219
|
+
try {
|
|
220
|
+
const { foldProject } = await import("../discovery/fold-import");
|
|
221
|
+
const { collectEntities } = await import("../discovery/collect");
|
|
222
|
+
const verdicts = await foldProject(files.map(abs), intrinsics, { lexicons: names, sandbox: true });
|
|
223
|
+
const folded: { file: string; exports: Record<string, unknown> }[] = [];
|
|
224
|
+
for (const file of files) {
|
|
225
|
+
const v = verdicts.get(abs(file));
|
|
226
|
+
if (v?.verdict === "fold" && v.exports) {
|
|
227
|
+
folded.push({ file: abs(file), exports: ownExports(v.exports, names) });
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
let text = "";
|
|
231
|
+
try {
|
|
232
|
+
text = tree.read(file);
|
|
233
|
+
} catch {
|
|
234
|
+
// An unreadable file is still unread; the hook decides whether it matters.
|
|
235
|
+
}
|
|
236
|
+
const reason = v?.reason ?? (v?.taintedBy ? `it imports ${v.taintedBy.from}, which does not fold` : "it does not fold");
|
|
237
|
+
unread.push({ file, text, reason });
|
|
238
|
+
}
|
|
239
|
+
entities = collectEntities(folded, dir);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
return { outputs: [], gaps: [...gaps, `${m.dir}: its source could not be folded, so its lexicons' outputs were not read (${firstLine(err)})`] };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// A reporting lexicon's own build roots (the sql lexicon's `.sql` files) join
|
|
245
|
+
// the folded entities, as they join a build's. Only these lexicons' hooks
|
|
246
|
+
// run: they read files, and start nothing.
|
|
247
|
+
for (const { name, plugin } of reporting) {
|
|
248
|
+
if (typeof plugin.buildRoots !== "function") continue;
|
|
249
|
+
const read = readConfigFieldsStatically(dir, [name, "sourceDir"]);
|
|
250
|
+
const config = read.status === "read" ? read.fields : {};
|
|
251
|
+
const sourceDir = typeof config.sourceDir === "string" ? join(dir, config.sourceDir) : undefined;
|
|
252
|
+
try {
|
|
253
|
+
const contributed = await plugin.buildRoots({ projectRoot: dir, config, entities, ...(sourceDir ? { sourceDir } : {}) });
|
|
254
|
+
for (const [key, entity] of contributed.entities) if (!entities.has(key)) entities.set(key, entity);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
gaps.push(`lexicon "${name}" could not read the member's other source files, so their declarations were not read (${firstLine(err)})`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const outputs: MemberLexiconOutputs["outputs"] = [];
|
|
261
|
+
for (const { name, plugin } of reporting) {
|
|
262
|
+
try {
|
|
263
|
+
const report = plugin.workspaceOutputs!(entities, unread);
|
|
264
|
+
if (!report) continue;
|
|
265
|
+
for (const o of report.outputs) outputs.push({ name: o.name, ...(o.previously?.length ? { previously: o.previously } : {}) });
|
|
266
|
+
for (const g of report.gaps ?? []) gaps.push(g);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
gaps.push(`lexicon "${name}" workspaceOutputs threw, so its outputs were not read (${firstLine(err)})`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { outputs, gaps };
|
|
272
|
+
}
|
|
@@ -308,6 +308,28 @@ describe("the graph's links section", () => {
|
|
|
308
308
|
test("the linked member pairs, for the pipeline checks' environment rule", async () => {
|
|
309
309
|
expect(declaredMemberLinks(decl)).toEqual([{ consumer: "web", producer: "shared" }]);
|
|
310
310
|
});
|
|
311
|
+
|
|
312
|
+
test("a link to an output's previous name says what it was renamed to (#3931)", () => {
|
|
313
|
+
const d = parseDeclaration(
|
|
314
|
+
declaration([
|
|
315
|
+
{ name: "db", dir: "db", kind: "chant" },
|
|
316
|
+
{ name: "web", dir: "web", kind: "chant", links: [{ member: "db", output: "public.orders.customer_id" }] },
|
|
317
|
+
]),
|
|
318
|
+
"chant.workspace.json",
|
|
319
|
+
);
|
|
320
|
+
const [row] = graphLinks(d, {
|
|
321
|
+
composed: ["db", "web"],
|
|
322
|
+
exports: [
|
|
323
|
+
{ member: "db", name: "public.orders", node: "db/orders" },
|
|
324
|
+
{ member: "db", name: "public.orders.buyer_id", node: "db/orders", previously: ["public.orders.customer_id"] },
|
|
325
|
+
],
|
|
326
|
+
imports: [],
|
|
327
|
+
});
|
|
328
|
+
expect(row).toMatchObject({ status: "missing" });
|
|
329
|
+
expect(row!.reason).toBe(
|
|
330
|
+
"db has no output public.orders.customer_id; it was renamed to public.orders.buyer_id, so link to that instead; its outputs: public.orders, public.orders.buyer_id",
|
|
331
|
+
);
|
|
332
|
+
});
|
|
311
333
|
});
|
|
312
334
|
|
|
313
335
|
describe("the declaration's link fields", () => {
|