@intentius/chant 0.124.44 → 0.124.46
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/capabilities/aws.paths +148 -0
- package/dist/capabilities/base-mappings.json +122 -0
- package/dist/capabilities/catalog.d.ts +89 -0
- package/dist/capabilities/catalog.d.ts.map +1 -0
- package/dist/capabilities/catalog.js +204 -0
- package/dist/capabilities/language.d.ts +103 -0
- package/dist/capabilities/language.d.ts.map +1 -0
- package/dist/capabilities/language.js +335 -0
- package/dist/capabilities/matcher.d.ts +65 -0
- package/dist/capabilities/matcher.d.ts.map +1 -0
- package/dist/capabilities/matcher.js +299 -0
- package/dist/capabilities/view.d.ts +207 -0
- package/dist/capabilities/view.d.ts.map +1 -0
- package/dist/capabilities/view.js +484 -0
- package/dist/ci-identity.d.ts +183 -12
- package/dist/ci-identity.d.ts.map +1 -1
- package/dist/ci-identity.js +362 -53
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/lint.js +1 -0
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +17 -1
- package/dist/cli/handlers/declared-graph.d.ts +44 -0
- package/dist/cli/handlers/declared-graph.d.ts.map +1 -0
- package/dist/cli/handlers/declared-graph.js +87 -0
- package/dist/cli/handlers/graph.d.ts +25 -0
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/graph.js +93 -87
- package/dist/cli/handlers/pr-env.d.ts +8 -2
- package/dist/cli/handlers/pr-env.d.ts.map +1 -1
- package/dist/cli/handlers/pr-env.js +127 -225
- package/dist/cli/handlers/pr.d.ts +3 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +1 -0
- package/dist/cli/handlers/search-count.d.ts +89 -0
- package/dist/cli/handlers/search-count.d.ts.map +1 -0
- package/dist/cli/handlers/search-count.js +132 -0
- package/dist/cli/handlers/search.d.ts +358 -6
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/handlers/search.js +1065 -155
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +23 -3
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/server.js +3 -0
- package/dist/cli/mcp/tools/capabilities.d.ts +7 -0
- package/dist/cli/mcp/tools/capabilities.d.ts.map +1 -0
- package/dist/cli/mcp/tools/capabilities.js +27 -0
- package/dist/cli/mcp/workspace-tools.d.ts +3 -1
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.js +47 -2
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +64 -17
- package/dist/cli/registry.d.ts +22 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +2 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +4 -2
- package/dist/components/fan-out-support.d.ts +3 -0
- package/dist/components/fan-out-support.d.ts.map +1 -1
- package/dist/components/fan-out-support.js +1 -1
- package/dist/components/pr-env-ports.d.ts +29 -0
- package/dist/components/pr-env-ports.d.ts.map +1 -0
- package/dist/components/pr-env-ports.js +77 -0
- package/dist/components/pr-env-run.d.ts +254 -0
- package/dist/components/pr-env-run.d.ts.map +1 -0
- package/dist/components/pr-env-run.js +376 -0
- package/dist/components/pr-environment.d.ts +91 -15
- package/dist/components/pr-environment.d.ts.map +1 -1
- package/dist/components/pr-environment.js +95 -9
- package/dist/components/pr-pipeline.d.ts +10 -1
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/config-environments.d.ts +92 -0
- package/dist/config-environments.d.ts.map +1 -0
- package/dist/config-environments.js +102 -0
- package/dist/config.d.ts +24 -84
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -96
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +15 -3
- package/dist/graph-effective.d.ts +33 -2
- package/dist/graph-effective.d.ts.map +1 -1
- package/dist/graph-effective.js +215 -68
- package/dist/graph-ir.d.ts +23 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ir.js +130 -0
- package/dist/graph-sub-template.d.ts +11 -0
- package/dist/graph-sub-template.d.ts.map +1 -0
- package/dist/graph-sub-template.js +19 -0
- package/dist/lexicon.d.ts +9 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +28 -0
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/observe.js +23 -1
- package/dist/lint/rule.d.ts +6 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/live-endpoint.d.ts +1 -1
- package/dist/live-endpoint.d.ts.map +1 -1
- package/dist/live-endpoint.js +1 -1
- package/dist/observation.d.ts +48 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +21 -5
- package/dist/op/activities/activity-contracts.d.ts +4 -4
- package/dist/op/op-waves.d.ts +17 -0
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +23 -1
- package/dist/pr-forge.d.ts +15 -1
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +28 -16
- package/dist/pr-requires.d.ts +5 -1
- package/dist/pr-requires.d.ts.map +1 -1
- package/dist/wave-runtime.mjs +251 -250
- package/dist/workspace/capabilities-contract.d.ts +66 -0
- package/dist/workspace/capabilities-contract.d.ts.map +1 -0
- package/dist/workspace/capabilities-contract.js +107 -0
- package/dist/workspace/capabilities.schema.json +229 -0
- package/dist/workspace/ci-cloud-identity.d.ts +3 -1
- package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
- package/dist/workspace/ci-cloud-identity.js +16 -1
- package/dist/workspace/ci-run.d.ts +35 -8
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +95 -21
- package/dist/workspace/ci-secrets.d.ts.map +1 -1
- package/dist/workspace/ci-secrets.js +4 -3
- package/dist/workspace/compose-graph.d.ts +9 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/compose-graph.js +3 -1
- package/dist/workspace/conformance/index.d.ts +1 -1
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/conformance/index.js +39 -2
- package/dist/workspace/declaration.d.ts +10 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +17 -1
- package/dist/workspace/declaration.schema.json +36 -1
- package/dist/workspace/graph-cache.d.ts +8 -0
- package/dist/workspace/graph-cache.d.ts.map +1 -1
- package/dist/workspace/graph-cache.js +27 -3
- package/dist/workspace/graph-cli.d.ts +39 -2
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/graph-cli.js +66 -24
- package/dist/workspace/graph.schema.json +148 -0
- package/dist/workspace/kind-readers.d.ts +3 -0
- package/dist/workspace/kind-readers.d.ts.map +1 -1
- package/dist/workspace/kind-readers.js +7 -1
- package/dist/workspace/member-commands.d.ts +10 -1
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/member-commands.js +17 -1
- package/dist/workspace/nested-graph.d.ts +2 -0
- package/dist/workspace/nested-graph.d.ts.map +1 -1
- package/dist/workspace/nested-graph.js +32 -1
- package/dist/workspace/reason-codes.d.ts +10 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +12 -0
- package/dist/workspace/search-cli.d.ts +204 -0
- package/dist/workspace/search-cli.d.ts.map +1 -0
- package/dist/workspace/search-cli.js +466 -0
- package/dist/workspace/search-contract.d.ts +56 -0
- package/dist/workspace/search-contract.d.ts.map +1 -0
- package/dist/workspace/search-contract.js +70 -0
- package/dist/workspace/search.schema.json +822 -0
- package/package.json +1 -1
- package/src/__snapshots__/okf.test.ts.snap +10 -0
- package/src/capabilities/aws.paths +148 -0
- package/src/capabilities/base-mappings.json +122 -0
- package/src/capabilities/catalog.ts +230 -0
- package/src/capabilities/language.test.ts +191 -0
- package/src/capabilities/language.ts +373 -0
- package/src/capabilities/matcher.ts +315 -0
- package/src/capabilities/view-compose.test.ts +76 -0
- package/src/capabilities/view.ts +631 -0
- package/src/ci-identity-targets.test.ts +275 -0
- package/src/ci-identity.ts +394 -61
- package/src/cli/commands/lint.ts +1 -0
- package/src/cli/handlers/build.ts +15 -1
- package/src/cli/handlers/declared-graph.ts +99 -0
- package/src/cli/handlers/graph.test.ts +94 -3
- package/src/cli/handlers/graph.ts +123 -96
- package/src/cli/handlers/pr-env.ts +135 -227
- package/src/cli/handlers/pr.ts +4 -0
- package/src/cli/handlers/search-build-roots.test.ts +144 -0
- package/src/cli/handlers/search-count.test.ts +191 -0
- package/src/cli/handlers/search-count.ts +165 -0
- package/src/cli/handlers/search-json.test.ts +333 -0
- package/src/cli/handlers/search-live.test.ts +387 -0
- package/src/cli/handlers/search-paths.test.ts +235 -0
- package/src/cli/handlers/search.ts +1312 -166
- package/src/cli/main.test.ts +28 -3
- package/src/cli/main.ts +23 -3
- package/src/cli/mcp/server.test.ts +3 -3
- package/src/cli/mcp/server.ts +3 -0
- package/src/cli/mcp/tools/capabilities.ts +32 -0
- package/src/cli/mcp/workspace-tools.e2e.test.ts +21 -1
- package/src/cli/mcp/workspace-tools.ts +47 -2
- package/src/cli/parse-args.ts +52 -13
- package/src/cli/registry.ts +22 -0
- package/src/components/cli-support.ts +6 -1
- package/src/components/fan-out-support.ts +4 -1
- package/src/components/pr-env-ports.ts +89 -0
- package/src/components/pr-env-run-bundle.test.ts +54 -0
- package/src/components/pr-env-run.test.ts +290 -0
- package/src/components/pr-env-run.ts +563 -0
- package/src/components/pr-environment.ts +161 -13
- package/src/components/pr-pipeline.ts +10 -1
- package/src/config-environments.ts +133 -0
- package/src/config.ts +37 -124
- package/src/graph-detail.ts +15 -3
- package/src/graph-effective.ts +226 -75
- package/src/graph-ir.test.ts +163 -0
- package/src/graph-ir.ts +147 -0
- package/src/graph-sub-template.ts +22 -0
- package/src/lexicon.ts +10 -0
- package/src/lifecycle/observe.test.ts +27 -1
- package/src/lifecycle/observe.ts +43 -1
- package/src/lint/rule.ts +6 -0
- package/src/live-endpoint.ts +1 -1
- package/src/observation.test.ts +25 -0
- package/src/observation.ts +69 -5
- package/src/op/op-waves.ts +38 -1
- package/src/pr-forge-bundle.test.ts +1 -1
- package/src/pr-forge.ts +44 -17
- package/src/pr-requires.test.ts +43 -6
- package/src/pr-requires.ts +5 -1
- package/src/workspace/capabilities-contract.test.ts +88 -0
- package/src/workspace/capabilities-contract.ts +132 -0
- package/src/workspace/capabilities.schema.json +229 -0
- package/src/workspace/ci-cloud-identity.test.ts +20 -0
- package/src/workspace/ci-cloud-identity.ts +14 -1
- package/src/workspace/ci-run.test.ts +74 -1
- package/src/workspace/ci-run.ts +122 -29
- package/src/workspace/ci-secrets.test.ts +1 -1
- package/src/workspace/ci-secrets.ts +4 -3
- package/src/workspace/compose-graph.ts +10 -1
- package/src/workspace/conformance/conformance.e2e.test.ts +14 -1
- package/src/workspace/conformance/index.ts +31 -2
- package/src/workspace/declaration.schema.json +36 -1
- package/src/workspace/declaration.test.ts +7 -0
- package/src/workspace/declaration.ts +24 -2
- package/src/workspace/graph-cache.ts +27 -3
- package/src/workspace/graph-cli.ts +96 -24
- package/src/workspace/graph.schema.json +148 -0
- package/src/workspace/kind-readers.e2e.test.ts +28 -0
- package/src/workspace/kind-readers.test.ts +1 -1
- package/src/workspace/kind-readers.ts +7 -1
- package/src/workspace/member-commands.ts +22 -1
- package/src/workspace/nested-graph.ts +32 -1
- package/src/workspace/reason-codes.test.ts +7 -0
- package/src/workspace/reason-codes.ts +12 -0
- package/src/workspace/search-cli.e2e.test.ts +305 -0
- package/src/workspace/search-cli.ts +591 -0
- package/src/workspace/search-contract.ts +95 -0
- package/src/workspace/search.schema.json +822 -0
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lexicon-neutral view capability rules read (#4375, ruled on #4361).
|
|
3
|
+
*
|
|
4
|
+
* chant's graph IR can't serve as a rule surface as it stands: an IAM
|
|
5
|
+
* statement or an inline security-group rule is a property tree inside one
|
|
6
|
+
* node, a Terraform policy is HCL text in a string, Terraform has link
|
|
7
|
+
* resources where CloudFormation has a direct reference, and every Terraform
|
|
8
|
+
* resource is `Terraform::Resource`. So rules read this view instead:
|
|
9
|
+
*
|
|
10
|
+
* - labels (`Instance`, `Role`, `SecurityGroup`) from each lexicon's kind
|
|
11
|
+
* mapping, and edge names (`subnet`, `sg`) from the IR edge's `viaAttr`;
|
|
12
|
+
* - a node per policy statement and per inline block (an ingress rule, an
|
|
13
|
+
* inline route), joined to its holder by a named edge;
|
|
14
|
+
* - one edge per link resource (a Terraform policy attachment);
|
|
15
|
+
* - an edge for an ARN string that names a declared role or bucket.
|
|
16
|
+
*
|
|
17
|
+
* The mapping is data, shipped by each lexicon (`CapabilityMapping`). The
|
|
18
|
+
* adapter below is the code that reads it. What a lexicon can't say as data
|
|
19
|
+
* (reading a Terraform `jsonencode(...)` policy) it supplies as a hook.
|
|
20
|
+
*/
|
|
21
|
+
import type { GraphIR, IRNode, IREdge } from "../graph-ir";
|
|
22
|
+
import { terraformType } from "../graph-effective";
|
|
23
|
+
|
|
24
|
+
/** A path into a node's attributes: `A.B` follows objects, `A[]` spreads a list. */
|
|
25
|
+
export type AttrPath = string;
|
|
26
|
+
|
|
27
|
+
/** Where a neutral attribute is read from: one path, the first present of several, or every value of several. */
|
|
28
|
+
export type AttrSource = AttrPath | AttrPath[] | { all: AttrPath[] } | { const: unknown };
|
|
29
|
+
|
|
30
|
+
/** A label for a kind, optionally only when the node's attributes match. */
|
|
31
|
+
export type LabelSpec = string | { label: string; when?: Record<string, unknown>; style?: string; set?: Record<string, unknown> };
|
|
32
|
+
|
|
33
|
+
/** An inline block exploded into nodes of its own. */
|
|
34
|
+
export interface BlockSpec {
|
|
35
|
+
/** The attribute holding the block or list of blocks. */
|
|
36
|
+
attr: AttrPath;
|
|
37
|
+
label: string;
|
|
38
|
+
/** The edge from the holder to the block; a leading `-` points it from the block to the holder. */
|
|
39
|
+
edge: string;
|
|
40
|
+
attrs?: Record<string, AttrSource>;
|
|
41
|
+
/** Attributes of the block that reference other nodes, and the edge name each makes from the block. */
|
|
42
|
+
refs?: Record<string, string>;
|
|
43
|
+
/** Only blocks whose attributes match. */
|
|
44
|
+
when?: Record<string, unknown>;
|
|
45
|
+
/** Recorded as the block node's `style` attribute. */
|
|
46
|
+
style?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A link resource turned into one edge between the two nodes it links. */
|
|
50
|
+
export interface LinkSpec {
|
|
51
|
+
/** The view edge (from the link node) whose target is the new edge's source. */
|
|
52
|
+
from: string;
|
|
53
|
+
/** The view edge (from the link node) whose target is the new edge's target. */
|
|
54
|
+
to: string;
|
|
55
|
+
edge: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Why a lexicon can't supply a capability. */
|
|
59
|
+
export interface Unsupported {
|
|
60
|
+
code: string;
|
|
61
|
+
message: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A lexicon's mapping onto the view, as data (`<lexicon>/src/capabilities/mapping.json`).
|
|
66
|
+
* Every field is optional, so a mapping can extend another.
|
|
67
|
+
*/
|
|
68
|
+
export interface CapabilityMapping {
|
|
69
|
+
lexicon: string;
|
|
70
|
+
/** How a node's type is read: its IR kind, or its Terraform resource type. */
|
|
71
|
+
types?: "kind" | "terraform";
|
|
72
|
+
/** The attribute holding the block body, when it isn't the attributes themselves. */
|
|
73
|
+
body?: string;
|
|
74
|
+
/** How references inside attribute values are spelled. */
|
|
75
|
+
refs?: "cloudformation" | "terraform";
|
|
76
|
+
labels?: Record<string, LabelSpec>;
|
|
77
|
+
/**
|
|
78
|
+
* Per label: IR `viaAttr` -> view edge name. A leading `-` reverses the
|
|
79
|
+
* edge. Where one attribute references several kinds (a launch template's
|
|
80
|
+
* `LaunchTemplateData`), the name can be given per target label.
|
|
81
|
+
*/
|
|
82
|
+
edges?: Record<string, Record<string, string | Record<string, string>>>;
|
|
83
|
+
/** Per label: neutral attribute -> where it is read from. */
|
|
84
|
+
attrs?: Record<string, Record<string, AttrSource>>;
|
|
85
|
+
blocks?: Record<string, BlockSpec[]>;
|
|
86
|
+
links?: Record<string, LinkSpec>;
|
|
87
|
+
/** Per label: attribute path of a policy document -> its kind (`permission`, `trust`, `resource`). */
|
|
88
|
+
documents?: Record<string, Record<AttrPath, string>>;
|
|
89
|
+
/** Where tags are read from, and which tag keys give a data store its classification. */
|
|
90
|
+
tags?: AttrPath[];
|
|
91
|
+
classificationTags?: string[];
|
|
92
|
+
/** Capabilities this lexicon can't supply, and why. */
|
|
93
|
+
unsupported?: Record<string, Unsupported>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** What a lexicon supplies as code, for what its mapping can't say as data. */
|
|
97
|
+
export interface CapabilityAdapter {
|
|
98
|
+
/**
|
|
99
|
+
* Read a policy document held in an attribute. Return the document as a
|
|
100
|
+
* JSON object; a value position holding an expression comes back as a
|
|
101
|
+
* string with the expression in `${...}`. Undefined when it can't be read.
|
|
102
|
+
*/
|
|
103
|
+
readDocument?(value: unknown): unknown;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** One lexicon's contribution: its mapping (data) and its adapter (code). */
|
|
107
|
+
export interface CapabilityPack {
|
|
108
|
+
mapping: CapabilityMapping;
|
|
109
|
+
adapter?: CapabilityAdapter;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* One edge of the IR a view edge stands for, in the IR's own direction:
|
|
114
|
+
* `from` holds the reference, `to` is what it names. #4359's `PathHop`, with
|
|
115
|
+
* the view edge's name.
|
|
116
|
+
*/
|
|
117
|
+
export interface ViewHop {
|
|
118
|
+
from: string;
|
|
119
|
+
fromAttr?: string;
|
|
120
|
+
to: string;
|
|
121
|
+
toAttr?: string;
|
|
122
|
+
edge: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface ViewNode {
|
|
126
|
+
id: string;
|
|
127
|
+
label: string;
|
|
128
|
+
/** The IR node this view node is, or sits inside (a statement's holder). */
|
|
129
|
+
irId: string;
|
|
130
|
+
lexicon: string;
|
|
131
|
+
attrs: Record<string, unknown>;
|
|
132
|
+
/** What the node was read from: the resource's attributes (its block body), or the inline block. */
|
|
133
|
+
body?: Record<string, unknown>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface ViewEdge {
|
|
137
|
+
from: string;
|
|
138
|
+
to: string;
|
|
139
|
+
name: string;
|
|
140
|
+
/** The IR hops this edge stands for, in walk order from `from`. */
|
|
141
|
+
hops: ViewHop[];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface View {
|
|
145
|
+
nodes: Map<string, ViewNode>;
|
|
146
|
+
edges: ViewEdge[];
|
|
147
|
+
/** Lexicons whose nodes the view read through a mapping. */
|
|
148
|
+
lexicons: Set<string>;
|
|
149
|
+
/**
|
|
150
|
+
* ARN strings that name a role, bucket or table by name, where this view
|
|
151
|
+
* declares nothing of that name (#4378). A workspace resolves them over
|
|
152
|
+
* every member's view: the thing may be declared in another member.
|
|
153
|
+
*/
|
|
154
|
+
unresolved?: UnresolvedArn[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** An ARN a view could not resolve, kept so a composed view can (#4378). */
|
|
158
|
+
export interface UnresolvedArn {
|
|
159
|
+
/** The view node holding the reference: a statement or an inline block. */
|
|
160
|
+
from: string;
|
|
161
|
+
fromAttr: string;
|
|
162
|
+
/** The view edge it makes once resolved (`resource`, `principal`, or a block's ref edge). */
|
|
163
|
+
edge: string;
|
|
164
|
+
value: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Merge mappings for the same lexicon: objects merge key by key at every
|
|
169
|
+
* depth, lists concatenate, and anything else in a later mapping wins.
|
|
170
|
+
*/
|
|
171
|
+
export function mergeMappings(...ms: CapabilityMapping[]): CapabilityMapping {
|
|
172
|
+
const merge = (a: unknown, b: unknown): unknown => {
|
|
173
|
+
if (b === undefined) return a;
|
|
174
|
+
if (Array.isArray(a) && Array.isArray(b)) return [...a, ...b.filter((x) => typeof x === "object" || !a.includes(x))];
|
|
175
|
+
const isObj = (x: unknown): x is Record<string, unknown> => !!x && typeof x === "object" && !Array.isArray(x);
|
|
176
|
+
if (isObj(a) && isObj(b)) {
|
|
177
|
+
const out: Record<string, unknown> = { ...a };
|
|
178
|
+
for (const [k, v] of Object.entries(b)) out[k] = merge(a[k], v);
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
return b;
|
|
182
|
+
};
|
|
183
|
+
return ms.reduce((acc, m) => merge(acc, m) as CapabilityMapping, { lexicon: ms[0]?.lexicon ?? "" } as CapabilityMapping);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const arr = (v: unknown): unknown[] => (v == null ? [] : Array.isArray(v) ? v : [v]);
|
|
187
|
+
|
|
188
|
+
/** Every value at `path` (`A.B`, `A[].B`), flattened. */
|
|
189
|
+
function readPath(obj: unknown, path: AttrPath): unknown[] {
|
|
190
|
+
let cur: unknown[] = [obj];
|
|
191
|
+
for (const seg of path.split(".")) {
|
|
192
|
+
const spread = seg.endsWith("[]");
|
|
193
|
+
const key = spread ? seg.slice(0, -2) : seg;
|
|
194
|
+
const next: unknown[] = [];
|
|
195
|
+
for (const c of cur) {
|
|
196
|
+
if (!c || typeof c !== "object") continue;
|
|
197
|
+
const v = (c as Record<string, unknown>)[key];
|
|
198
|
+
if (v === undefined) continue;
|
|
199
|
+
if (spread) next.push(...arr(v));
|
|
200
|
+
else next.push(v);
|
|
201
|
+
}
|
|
202
|
+
cur = next;
|
|
203
|
+
}
|
|
204
|
+
return cur;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function readSource(obj: unknown, src: AttrSource): unknown {
|
|
208
|
+
if (typeof src === "string") return readPath(obj, src)[0];
|
|
209
|
+
if (Array.isArray(src)) {
|
|
210
|
+
for (const p of src) {
|
|
211
|
+
const v = readPath(obj, p)[0];
|
|
212
|
+
if (v !== undefined && v !== null) return v;
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
if ("const" in src) return src.const;
|
|
217
|
+
return src.all.flatMap((p) => readPath(obj, p).flatMap(arr));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function matchesWhen(obj: unknown, when?: Record<string, unknown>): boolean {
|
|
221
|
+
if (!when) return true;
|
|
222
|
+
return Object.entries(when).every(([k, v]) => readPath(obj, k)[0] === v);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The bare name a row prints: no stack qualification, no root directory. */
|
|
226
|
+
export function displayName(id: string): string {
|
|
227
|
+
const s = id.includes("::") ? id.slice(id.lastIndexOf("::") + 2) : id;
|
|
228
|
+
return s.slice(s.lastIndexOf("/") + 1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const ARN_NAMES: { label: string; re: RegExp }[] = [
|
|
232
|
+
{ label: "Role", re: /^arn:aws[\w-]*:iam::[^:]*:role\/(?:[^*]*\/)?([^/*]+)$/ },
|
|
233
|
+
{ label: "Bucket", re: /^arn:aws[\w-]*:s3:::([^/*]+)(?:\/.*)?$/ },
|
|
234
|
+
{ label: "Table", re: /^arn:aws[\w-]*:dynamodb:[^:]*:[^:]*:table\/([^/*]+)$/ },
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Normalize what each lexicon spells its own way, for labels whose meaning is
|
|
239
|
+
* core vocabulary: an ingress rule's protocol and port range.
|
|
240
|
+
*/
|
|
241
|
+
function normalize(label: string, attrs: Record<string, unknown>): void {
|
|
242
|
+
if (label === "IngressRule") {
|
|
243
|
+
const raw = attrs.proto == null ? "-1" : String(attrs.proto).toLowerCase();
|
|
244
|
+
const proto = raw === "all" || raw === "-1" ? "-1" : raw;
|
|
245
|
+
attrs.proto = proto;
|
|
246
|
+
if (proto === "-1" || attrs.from == null) {
|
|
247
|
+
attrs.from = 0;
|
|
248
|
+
attrs.to = 65535;
|
|
249
|
+
} else {
|
|
250
|
+
attrs.from = Number(attrs.from);
|
|
251
|
+
attrs.to = attrs.to == null ? attrs.from : Number(attrs.to);
|
|
252
|
+
}
|
|
253
|
+
attrs.cidrs = arr(attrs.cidrs).map(String);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Build the view of an IR through the packs given. Nodes of a kind no
|
|
259
|
+
* mapping labels are left out, as are edges no mapping names.
|
|
260
|
+
*/
|
|
261
|
+
export function buildView(ir: GraphIR, packs: CapabilityPack[]): View {
|
|
262
|
+
const byLexicon = new Map<string, CapabilityPack>();
|
|
263
|
+
for (const p of packs) {
|
|
264
|
+
const prev = byLexicon.get(p.mapping.lexicon);
|
|
265
|
+
byLexicon.set(
|
|
266
|
+
p.mapping.lexicon,
|
|
267
|
+
prev ? { mapping: mergeMappings(prev.mapping, p.mapping), adapter: { ...prev.adapter, ...p.adapter } } : p,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
const terraformPack = [...byLexicon.values()].find((p) => p.mapping.types === "terraform");
|
|
271
|
+
const kindPacks = [...byLexicon.values()].filter((p) => p.mapping.types !== "terraform");
|
|
272
|
+
|
|
273
|
+
const nodes = new Map<string, ViewNode>();
|
|
274
|
+
const edges: ViewEdge[] = [];
|
|
275
|
+
const lexicons = new Set<string>();
|
|
276
|
+
const irById = new Map(ir.nodes.map((n) => [n.id, n]));
|
|
277
|
+
const packOf = new Map<string, CapabilityPack>();
|
|
278
|
+
|
|
279
|
+
const typeAndPack = (n: IRNode): { type: string; pack: CapabilityPack } | undefined => {
|
|
280
|
+
const tf = terraformType(n);
|
|
281
|
+
if (tf !== undefined) return terraformPack ? { type: tf, pack: terraformPack } : undefined;
|
|
282
|
+
const kind = n.kind ?? "";
|
|
283
|
+
const pack = kindPacks.find((p) => p.mapping.labels?.[kind] !== undefined);
|
|
284
|
+
return pack ? { type: kind, pack } : undefined;
|
|
285
|
+
};
|
|
286
|
+
const bodyOf = (n: IRNode, pack: CapabilityPack): Record<string, unknown> => {
|
|
287
|
+
const attrs = (n.attrs ?? {}) as Record<string, unknown>;
|
|
288
|
+
if (!pack.mapping.body) return attrs;
|
|
289
|
+
const b = attrs[pack.mapping.body];
|
|
290
|
+
return b && typeof b === "object" && !Array.isArray(b) ? (b as Record<string, unknown>) : {};
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Terraform locals, per scope, so `tags = local.restricted` reads as a map.
|
|
294
|
+
const locals = new Map<string, Record<string, unknown>>();
|
|
295
|
+
const scopeOf = (id: string): string => id.slice(0, id.lastIndexOf("/") + 1);
|
|
296
|
+
for (const n of ir.nodes) {
|
|
297
|
+
if (n.kind !== "Terraform::Locals") continue;
|
|
298
|
+
const body = ((n.attrs as Record<string, unknown>)?.body ?? {}) as Record<string, unknown>;
|
|
299
|
+
locals.set(scopeOf(n.id), { ...locals.get(scopeOf(n.id)), ...body });
|
|
300
|
+
}
|
|
301
|
+
const resolveLocal = (from: string, v: unknown): unknown => {
|
|
302
|
+
const m = typeof v === "string" ? /^\$\{local\.([\w-]+)\}$/.exec(v) : null;
|
|
303
|
+
return m ? locals.get(scopeOf(from))?.[m[1]] : v;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// ---- nodes
|
|
307
|
+
for (const n of ir.nodes) {
|
|
308
|
+
const tp = typeAndPack(n);
|
|
309
|
+
if (!tp) continue;
|
|
310
|
+
const { type, pack } = tp;
|
|
311
|
+
const m = pack.mapping;
|
|
312
|
+
const spec = m.labels?.[type];
|
|
313
|
+
if (spec === undefined) continue;
|
|
314
|
+
const label = typeof spec === "string" ? spec : spec.label;
|
|
315
|
+
const body = bodyOf(n, pack);
|
|
316
|
+
if (typeof spec !== "string" && !matchesWhen(body, spec.when)) continue;
|
|
317
|
+
const attrs: Record<string, unknown> = { type };
|
|
318
|
+
if (typeof spec !== "string" && spec.style) attrs.style = spec.style;
|
|
319
|
+
if (typeof spec !== "string" && spec.set) Object.assign(attrs, spec.set);
|
|
320
|
+
for (const [neutral, src] of Object.entries(m.attrs?.[label] ?? {})) {
|
|
321
|
+
const v = readSource(body, src);
|
|
322
|
+
if (v !== undefined) attrs[neutral] = resolveLocal(n.id, v);
|
|
323
|
+
}
|
|
324
|
+
// Carry the IR's own derived attributes (a live read's `internetFacing`).
|
|
325
|
+
for (const [k, v] of Object.entries((n.attrs ?? {}) as Record<string, unknown>)) {
|
|
326
|
+
if (k === "internetFacing" && attrs[k] === undefined) attrs[k] = v;
|
|
327
|
+
}
|
|
328
|
+
if (m.classificationTags?.length) {
|
|
329
|
+
for (const path of m.tags ?? []) {
|
|
330
|
+
const tags = resolveLocal(n.id, readPath(body, path)[0]);
|
|
331
|
+
const map: Record<string, unknown> = Array.isArray(tags)
|
|
332
|
+
? Object.fromEntries(tags.map((t) => [(t as { Key?: unknown }).Key, (t as { Value?: unknown }).Value]))
|
|
333
|
+
: tags && typeof tags === "object"
|
|
334
|
+
? (tags as Record<string, unknown>)
|
|
335
|
+
: {};
|
|
336
|
+
for (const key of m.classificationTags) {
|
|
337
|
+
if (map[key] !== undefined && attrs.classification === undefined) attrs.classification = map[key];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
normalize(label, attrs);
|
|
342
|
+
nodes.set(n.id, { id: n.id, label, irId: n.id, lexicon: m.lexicon, attrs, body });
|
|
343
|
+
packOf.set(n.id, pack);
|
|
344
|
+
lexicons.add(m.lexicon);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ---- references inside attribute values
|
|
348
|
+
const byName = new Map<string, Map<string, string>>();
|
|
349
|
+
for (const v of nodes.values()) {
|
|
350
|
+
const name = v.attrs.name;
|
|
351
|
+
if (typeof name !== "string" || !name) continue;
|
|
352
|
+
if (!byName.has(v.label)) byName.set(v.label, new Map());
|
|
353
|
+
byName.get(v.label)!.set(name, v.id);
|
|
354
|
+
}
|
|
355
|
+
const refTarget = (from: string, value: unknown): { id: string; attr?: string } | undefined => {
|
|
356
|
+
const pack = packOf.get(from);
|
|
357
|
+
if (value && typeof value === "object") {
|
|
358
|
+
const ref = (value as { $ref?: unknown }).$ref;
|
|
359
|
+
if (typeof ref === "string") {
|
|
360
|
+
if (nodes.has(ref)) return { id: ref };
|
|
361
|
+
const dot = ref.lastIndexOf(".");
|
|
362
|
+
if (dot > 0 && nodes.has(ref.slice(0, dot))) return { id: ref.slice(0, dot), attr: ref.slice(dot + 1) };
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
const sub = (value as { $sub?: unknown }).$sub;
|
|
366
|
+
if (typeof sub === "string") {
|
|
367
|
+
const m = /\$\{([^.}]+)(?:\.([^}]+))?\}/.exec(sub);
|
|
368
|
+
if (m && nodes.has(m[1])) return { id: m[1], attr: m[2] };
|
|
369
|
+
}
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
if (typeof value !== "string") return undefined;
|
|
373
|
+
if (pack?.mapping.refs === "terraform") {
|
|
374
|
+
const m = /(?:^|[^\w.])((?:data\.)?[a-z][a-z0-9]*_[a-z0-9_]+\.[A-Za-z0-9_-]+)(?:\[[^\]]*\])?\.([A-Za-z_]+)/.exec(value);
|
|
375
|
+
if (m) {
|
|
376
|
+
const id = scopeOf(from) + m[1];
|
|
377
|
+
if (nodes.has(id)) return { id, attr: m[2] };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
for (const { label, re } of ARN_NAMES) {
|
|
381
|
+
const m = re.exec(value);
|
|
382
|
+
const id = m ? byName.get(label)?.get(m[1]) : undefined;
|
|
383
|
+
if (id) return { id };
|
|
384
|
+
}
|
|
385
|
+
return undefined;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const unresolved: UnresolvedArn[] = [];
|
|
389
|
+
// An ARN that names nothing declared here is kept for a workspace's composer (#4378).
|
|
390
|
+
const keepArn = (from: string, fromAttr: string, edge: string, value: unknown): void => {
|
|
391
|
+
if (typeof value === "string" && ARN_NAMES.some(({ re }) => re.test(value))) unresolved.push({ from, fromAttr, edge, value });
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const blockEdges: ViewEdge[] = [];
|
|
395
|
+
// ---- inline blocks: ingress rules, inline routes
|
|
396
|
+
for (const v of [...nodes.values()]) {
|
|
397
|
+
const pack = packOf.get(v.id)!;
|
|
398
|
+
const specs = pack.mapping.blocks?.[v.label];
|
|
399
|
+
if (!specs) continue;
|
|
400
|
+
const body = bodyOf(irById.get(v.id)!, pack);
|
|
401
|
+
for (const spec of specs) {
|
|
402
|
+
readPath(body, spec.attr)
|
|
403
|
+
.flatMap(arr)
|
|
404
|
+
.forEach((raw, i) => {
|
|
405
|
+
if (!raw || typeof raw !== "object" || !matchesWhen(raw, spec.when)) return;
|
|
406
|
+
const id = `${v.id}#${spec.attr.replace(/\[\]/g, "")}[${i}]`;
|
|
407
|
+
const attrs: Record<string, unknown> = { block: true };
|
|
408
|
+
if (spec.style) attrs.style = spec.style;
|
|
409
|
+
for (const [neutral, src] of Object.entries(spec.attrs ?? {})) {
|
|
410
|
+
const val = readSource(raw, src);
|
|
411
|
+
if (val !== undefined) attrs[neutral] = val;
|
|
412
|
+
}
|
|
413
|
+
normalize(spec.label, attrs);
|
|
414
|
+
nodes.set(id, { id, label: spec.label, irId: v.id, lexicon: v.lexicon, attrs, body: raw as Record<string, unknown> });
|
|
415
|
+
packOf.set(id, pack);
|
|
416
|
+
const back = spec.edge.startsWith("-");
|
|
417
|
+
const name = back ? spec.edge.slice(1) : spec.edge;
|
|
418
|
+
const hop: ViewHop = { from: v.id, fromAttr: spec.attr.replace(/\[\]/g, ""), to: id, edge: name };
|
|
419
|
+
blockEdges.push(back ? { from: id, to: v.id, name, hops: [hop] } : { from: v.id, to: id, name, hops: [hop] });
|
|
420
|
+
for (const [attr, edge] of Object.entries(spec.refs ?? {})) {
|
|
421
|
+
for (const val of arr((raw as Record<string, unknown>)[attr])) {
|
|
422
|
+
const t = refTarget(v.id, val);
|
|
423
|
+
if (!t) keepArn(id, attr, edge, val);
|
|
424
|
+
if (t) blockEdges.push({ from: id, to: t.id, name: edge, hops: [{ from: id, fromAttr: attr, to: t.id, ...(t.attr ? { toAttr: t.attr } : {}), edge }] });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Blocks first: an inline route is read before a separate route resource,
|
|
431
|
+
// the order the folds always used.
|
|
432
|
+
edges.push(...blockEdges);
|
|
433
|
+
|
|
434
|
+
// ---- IR edges, named per label
|
|
435
|
+
for (const e of ir.edges ?? []) {
|
|
436
|
+
const from = nodes.get(e.from);
|
|
437
|
+
const to = nodes.get(e.to);
|
|
438
|
+
if (!from || !to) continue;
|
|
439
|
+
const pack = packOf.get(e.from)!;
|
|
440
|
+
const via = e.viaAttr ?? e.kind ?? "";
|
|
441
|
+
const spec = pack.mapping.edges?.[from.label]?.[via];
|
|
442
|
+
const name = typeof spec === "string" ? spec : spec?.[to.label];
|
|
443
|
+
if (!name) continue;
|
|
444
|
+
const back = name.startsWith("-");
|
|
445
|
+
const n = back ? name.slice(1) : name;
|
|
446
|
+
const hop = irHop(e, n);
|
|
447
|
+
edges.push(back ? { from: e.to, to: e.from, name: n, hops: [hop] } : { from: e.from, to: e.to, name: n, hops: [hop] });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ---- link resources -> one edge
|
|
451
|
+
for (const v of [...nodes.values()]) {
|
|
452
|
+
const link = packOf.get(v.id)?.mapping.links?.[v.label];
|
|
453
|
+
if (!link) continue;
|
|
454
|
+
const a = edges.find((e) => e.from === v.id && e.name === link.from);
|
|
455
|
+
const b = edges.find((e) => e.from === v.id && e.name === link.to);
|
|
456
|
+
if (a && b) edges.push({ from: a.to, to: b.to, name: link.edge, hops: [...a.hops, ...b.hops] });
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ---- policy documents -> statements
|
|
460
|
+
for (const holder of [...nodes.values()]) {
|
|
461
|
+
if (holder.attrs.block) continue;
|
|
462
|
+
const pack = packOf.get(holder.id)!;
|
|
463
|
+
const docs = pack.mapping.documents?.[holder.label];
|
|
464
|
+
if (!docs) continue;
|
|
465
|
+
const body = bodyOf(irById.get(holder.id)!, pack);
|
|
466
|
+
for (const [path, docKind] of Object.entries(docs)) {
|
|
467
|
+
readPath(body, path).forEach((raw, di) => {
|
|
468
|
+
const doc = readDocument(raw, pack);
|
|
469
|
+
if (!doc) return;
|
|
470
|
+
const base = path.replace(/\[\]/g, "");
|
|
471
|
+
arr((doc as Record<string, unknown>).Statement).forEach((s0, si) => {
|
|
472
|
+
if (!s0 || typeof s0 !== "object") return;
|
|
473
|
+
const s = s0 as Record<string, unknown>;
|
|
474
|
+
const id = `${holder.id}#${base}[${di}].Statement[${si}]`;
|
|
475
|
+
const principal = s.Principal;
|
|
476
|
+
const principals =
|
|
477
|
+
principal === "*"
|
|
478
|
+
? ["*"]
|
|
479
|
+
: principal && typeof principal === "object"
|
|
480
|
+
? Object.values(principal as Record<string, unknown>).flatMap(arr).map((p) => (typeof p === "string" ? p : "ref"))
|
|
481
|
+
: [];
|
|
482
|
+
const resources = arr(s.Resource);
|
|
483
|
+
const attrs: Record<string, unknown> = {
|
|
484
|
+
doc: docKind,
|
|
485
|
+
effect: s.Effect,
|
|
486
|
+
actions: arr(s.Action).map(String),
|
|
487
|
+
...(s.NotAction !== undefined ? { notActions: arr(s.NotAction).map(String) } : {}),
|
|
488
|
+
...(s.NotResource !== undefined ? { notResource: true } : {}),
|
|
489
|
+
...(s.Condition !== undefined ? { condition: true } : {}),
|
|
490
|
+
...(resources.some((r) => r === "*") ? { anyResource: true } : {}),
|
|
491
|
+
principals,
|
|
492
|
+
};
|
|
493
|
+
nodes.set(id, { id, label: "Statement", irId: holder.id, lexicon: holder.lexicon, attrs });
|
|
494
|
+
packOf.set(id, pack);
|
|
495
|
+
edges.push({ from: holder.id, to: id, name: "statement", hops: [{ from: holder.id, fromAttr: base, to: id, edge: "statement" }] });
|
|
496
|
+
let unresolved = 0;
|
|
497
|
+
resources.forEach((r, ri) => {
|
|
498
|
+
const t = refTarget(holder.id, r);
|
|
499
|
+
// A reference the graph holds but can't name (an intrinsic with
|
|
500
|
+
// no target, a Terraform expression to an undeclared resource) is
|
|
501
|
+
// counted, so an answer can say what it skipped. A `Sub` is read
|
|
502
|
+
// through its `$sub` template (chant#4373).
|
|
503
|
+
if (!t && ((r && typeof r === "object") || (typeof r === "string" && r.includes("${")))) unresolved++;
|
|
504
|
+
if (!t) keepArn(id, `Resource[${ri}]`, "resource", r);
|
|
505
|
+
if (t) edges.push({ from: id, to: t.id, name: "resource", hops: [{ from: id, fromAttr: `Resource[${ri}]`, to: t.id, ...(t.attr ? { toAttr: t.attr } : {}), edge: "resource" }] });
|
|
506
|
+
});
|
|
507
|
+
if (unresolved > 0) attrs.unresolved = unresolved;
|
|
508
|
+
if (principal && typeof principal === "object") {
|
|
509
|
+
for (const p of arr((principal as Record<string, unknown>).AWS)) {
|
|
510
|
+
const t = refTarget(holder.id, p);
|
|
511
|
+
if (!t) keepArn(id, "Principal.AWS", "principal", p);
|
|
512
|
+
if (t) edges.push({ from: id, to: t.id, name: "principal", hops: [{ from: id, fromAttr: "Principal.AWS", to: t.id, ...(t.attr ? { toAttr: t.attr } : {}), edge: "principal" }] });
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return { nodes, edges, lexicons, unresolved };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** The version of the serialized view a member's `chant graph --capability-view` prints (#4378). */
|
|
523
|
+
export const VIEW_DOCUMENT_VERSION = 1;
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* The view as a member's `chant graph --format ir --capability-view` prints
|
|
527
|
+
* it (#4378): built by the member's own chant, through the mappings and
|
|
528
|
+
* adapters of the lexicons that member loads, so a workspace's rules read
|
|
529
|
+
* every member as the member's own toolchain maps it. Node bodies are left
|
|
530
|
+
* out: rules read labels, attributes and edges only.
|
|
531
|
+
*/
|
|
532
|
+
export interface ViewDocument {
|
|
533
|
+
version: number;
|
|
534
|
+
lexicons: string[];
|
|
535
|
+
nodes: { id: string; label: string; irId: string; lexicon: string; attrs: Record<string, unknown> }[];
|
|
536
|
+
edges: ViewEdge[];
|
|
537
|
+
unresolved: UnresolvedArn[];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export function serializeView(view: View): ViewDocument {
|
|
541
|
+
return {
|
|
542
|
+
version: VIEW_DOCUMENT_VERSION,
|
|
543
|
+
lexicons: [...view.lexicons].sort(),
|
|
544
|
+
nodes: [...view.nodes.values()].map((n) => ({ id: n.id, label: n.label, irId: n.irId, lexicon: n.lexicon, attrs: n.attrs })),
|
|
545
|
+
edges: view.edges,
|
|
546
|
+
unresolved: view.unresolved ?? [],
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Whether a member printed a view this chant reads; anything else is treated as no view. */
|
|
551
|
+
export function isViewDocument(v: unknown): v is ViewDocument {
|
|
552
|
+
if (!v || typeof v !== "object") return false;
|
|
553
|
+
const d = v as Record<string, unknown>;
|
|
554
|
+
return d.version === VIEW_DOCUMENT_VERSION && Array.isArray(d.nodes) && Array.isArray(d.edges) && Array.isArray(d.lexicons);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** A view document with every id it holds given `prefix` (`<member>/`), as `workspace graph` composes ids. */
|
|
558
|
+
export function prefixView(doc: ViewDocument, prefix: string): ViewDocument {
|
|
559
|
+
const p = (id: string): string => `${prefix}${id}`;
|
|
560
|
+
const hop = (h: ViewHop): ViewHop => ({ ...h, from: p(h.from), to: p(h.to) });
|
|
561
|
+
return {
|
|
562
|
+
version: doc.version,
|
|
563
|
+
lexicons: doc.lexicons,
|
|
564
|
+
nodes: doc.nodes.map((n) => ({ ...n, id: p(n.id), irId: p(n.irId) })),
|
|
565
|
+
edges: doc.edges.map((e) => ({ ...e, from: p(e.from), to: p(e.to), hops: e.hops.map(hop) })),
|
|
566
|
+
unresolved: (doc.unresolved ?? []).map((u) => ({ ...u, from: p(u.from) })),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Compose member views into one (#4378), the matcher's input over a
|
|
572
|
+
* workspace. Each member's view was mapped by its own chant; the one thing
|
|
573
|
+
* left to do here is resolve the ARNs a member couldn't, by name over every
|
|
574
|
+
* member's nodes. Where several members declare the name, the last one in
|
|
575
|
+
* member order wins, as within one view.
|
|
576
|
+
*/
|
|
577
|
+
export function composeViews(docs: ViewDocument[]): View {
|
|
578
|
+
const nodes = new Map<string, ViewNode>();
|
|
579
|
+
const edges: ViewEdge[] = [];
|
|
580
|
+
const lexicons = new Set<string>();
|
|
581
|
+
for (const d of docs) {
|
|
582
|
+
for (const n of d.nodes) nodes.set(n.id, { ...n });
|
|
583
|
+
edges.push(...d.edges);
|
|
584
|
+
for (const l of d.lexicons) lexicons.add(l);
|
|
585
|
+
}
|
|
586
|
+
const byName = new Map<string, Map<string, string>>();
|
|
587
|
+
for (const v of nodes.values()) {
|
|
588
|
+
const name = v.attrs.name;
|
|
589
|
+
if (typeof name !== "string" || !name) continue;
|
|
590
|
+
if (!byName.has(v.label)) byName.set(v.label, new Map());
|
|
591
|
+
byName.get(v.label)!.set(name, v.id);
|
|
592
|
+
}
|
|
593
|
+
const unresolved: UnresolvedArn[] = [];
|
|
594
|
+
for (const u of docs.flatMap((d) => d.unresolved ?? [])) {
|
|
595
|
+
let to: string | undefined;
|
|
596
|
+
for (const { label, re } of ARN_NAMES) {
|
|
597
|
+
const m = re.exec(u.value);
|
|
598
|
+
to = m ? byName.get(label)?.get(m[1]) : undefined;
|
|
599
|
+
if (to) break;
|
|
600
|
+
}
|
|
601
|
+
if (to) edges.push({ from: u.from, to, name: u.edge, hops: [{ from: u.from, fromAttr: u.fromAttr, to, edge: u.edge }] });
|
|
602
|
+
else unresolved.push(u);
|
|
603
|
+
}
|
|
604
|
+
return { nodes, edges, lexicons, unresolved };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function irHop(e: IREdge, name: string): ViewHop {
|
|
608
|
+
return {
|
|
609
|
+
from: e.from,
|
|
610
|
+
...(e.viaAttr ? { fromAttr: e.viaAttr } : {}),
|
|
611
|
+
to: e.to,
|
|
612
|
+
...(e.toAttr ? { toAttr: e.toAttr } : {}),
|
|
613
|
+
edge: name,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function readDocument(raw: unknown, pack: CapabilityPack): unknown {
|
|
618
|
+
if (pack.adapter?.readDocument) return pack.adapter.readDocument(raw);
|
|
619
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
620
|
+
const inner = (raw as Record<string, unknown>).PolicyDocument;
|
|
621
|
+
return inner && typeof inner === "object" ? inner : raw;
|
|
622
|
+
}
|
|
623
|
+
if (typeof raw === "string") {
|
|
624
|
+
try {
|
|
625
|
+
return JSON.parse(raw);
|
|
626
|
+
} catch {
|
|
627
|
+
return undefined;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return undefined;
|
|
631
|
+
}
|