@intentius/chant 0.124.45 → 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/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/lint.js +1 -0
- 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.map +1 -1
- package/dist/cli/handlers/pr-env.js +2 -1
- 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/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 +5 -21
- package/dist/components/pr-env-run.d.ts.map +1 -1
- package/dist/components/pr-env-run.js +6 -70
- package/dist/components/pr-environment.d.ts +2 -1
- package/dist/components/pr-environment.d.ts.map +1 -1
- package/dist/components/pr-environment.js +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/wave-runtime.mjs +250 -249
- 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/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/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/cli/commands/lint.ts +1 -0
- 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 +1 -2
- 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/pr-env-ports.ts +89 -0
- package/src/components/pr-env-run-bundle.test.ts +54 -0
- package/src/components/pr-env-run.ts +5 -76
- package/src/components/pr-environment.ts +2 -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/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/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/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
package/src/graph-ir.ts
CHANGED
|
@@ -5,9 +5,12 @@ import { type Declarable, isDeclarable } from "./declarable";
|
|
|
5
5
|
import { isLexiconOutput, type LexiconOutput } from "./lexicon-output";
|
|
6
6
|
import { getProvenance } from "./provenance";
|
|
7
7
|
import { INTRINSIC_MARKER } from "./intrinsic";
|
|
8
|
+
import { subTemplateRefs } from "./graph-sub-template";
|
|
8
9
|
import type { ResourceMetadata } from "./lexicon";
|
|
9
10
|
import type { UnobservedEntity } from "./observation";
|
|
10
11
|
|
|
12
|
+
export { subTemplateRefs };
|
|
13
|
+
|
|
11
14
|
/**
|
|
12
15
|
* Graph IR — the engine-neutral, lint-gated representation of a project's
|
|
13
16
|
* resolved infrastructure graph. Painters (mermaid, graphviz, custom SVG) and
|
|
@@ -35,6 +38,21 @@ export interface AttrRefEnvelope {
|
|
|
35
38
|
$ref: string;
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
/**
|
|
42
|
+
* A CloudFormation `Fn::Sub` in an attribute projection (#4373), in place of
|
|
43
|
+
* `{ $intrinsic: true }`. `$sub` is the template exactly as it serializes, so
|
|
44
|
+
* `${assets.Arn}/*` names the producer and attribute a reader would otherwise
|
|
45
|
+
* have to guess. `$vars` is the two-argument form's variable map, each value
|
|
46
|
+
* projected like any other attr. Every `${Name}` or `${Name.Attr}` in the
|
|
47
|
+
* template that names a node, and every reference in `$vars`, is also an edge.
|
|
48
|
+
* `${!Literal}` (CloudFormation's escape), pseudo-parameters (`${AWS::Region}`)
|
|
49
|
+
* and names `$vars` binds are not.
|
|
50
|
+
*/
|
|
51
|
+
export interface SubEnvelope {
|
|
52
|
+
$sub: string;
|
|
53
|
+
$vars?: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
|
|
38
56
|
/** One resource in the graph. */
|
|
39
57
|
export interface IRNode {
|
|
40
58
|
/** Logical name (the export name, or composite-expanded name). */
|
|
@@ -465,6 +483,13 @@ export interface GraphIR {
|
|
|
465
483
|
* the passes produced, not a list maintained by hand.
|
|
466
484
|
*/
|
|
467
485
|
derivedAttrs?: Record<string, string[]>;
|
|
486
|
+
/**
|
|
487
|
+
* With `chant graph --format ir --capability-view` (#4378): the
|
|
488
|
+
* lexicon-neutral view capability rules read, built by this chant through
|
|
489
|
+
* the project's own lexicon mappings and adapters. A `ViewDocument`
|
|
490
|
+
* (`capabilities/view.ts`). Absent unless asked for.
|
|
491
|
+
*/
|
|
492
|
+
capabilityView?: unknown;
|
|
468
493
|
}
|
|
469
494
|
|
|
470
495
|
/** A node is anything that serializes to a resource — not a property or output. */
|
|
@@ -499,6 +524,80 @@ function refIntrinsicTarget(value: unknown): string | undefined {
|
|
|
499
524
|
return undefined;
|
|
500
525
|
}
|
|
501
526
|
|
|
527
|
+
/** A CloudFormation `Fn::Sub`: its template, and the variable map of the two-argument form. */
|
|
528
|
+
interface SubForm {
|
|
529
|
+
template: string;
|
|
530
|
+
vars?: Record<string, unknown>;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Read `{ "Fn::Sub": "..." }` or `{ "Fn::Sub": ["...", { ... }] }`, or undefined. */
|
|
534
|
+
function subForm(json: unknown): SubForm | undefined {
|
|
535
|
+
if (json === null || typeof json !== "object" || Array.isArray(json)) return undefined;
|
|
536
|
+
const keys = Object.keys(json);
|
|
537
|
+
if (keys.length !== 1 || keys[0] !== "Fn::Sub") return undefined;
|
|
538
|
+
const arg = (json as Record<string, unknown>)["Fn::Sub"];
|
|
539
|
+
if (typeof arg === "string") return { template: arg };
|
|
540
|
+
if (Array.isArray(arg) && typeof arg[0] === "string") {
|
|
541
|
+
const vars = arg[1];
|
|
542
|
+
return vars !== null && typeof vars === "object" && !Array.isArray(vars)
|
|
543
|
+
? { template: arg[0], vars: vars as Record<string, unknown> }
|
|
544
|
+
: { template: arg[0] };
|
|
545
|
+
}
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** The `Fn::Sub` an intrinsic serializes to (the aws `Sub` tag), or undefined. */
|
|
550
|
+
function subIntrinsicForm(value: unknown): SubForm | undefined {
|
|
551
|
+
try {
|
|
552
|
+
return subForm((value as { toJSON?: () => unknown }).toJSON?.());
|
|
553
|
+
} catch {
|
|
554
|
+
// An AttrRef inside whose logical name is not set yet: no template to read.
|
|
555
|
+
return undefined;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** False when an intrinsic's `toJSON()` throws (an AttrRef inside it has no logical name yet). */
|
|
560
|
+
function serializes(value: unknown): boolean {
|
|
561
|
+
try {
|
|
562
|
+
(value as { toJSON?: () => unknown }).toJSON?.();
|
|
563
|
+
return true;
|
|
564
|
+
} catch {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* The logical names serialized CloudFormation JSON references: `{ Ref }`,
|
|
571
|
+
* `{ "Fn::GetAtt" }` in either encoding, and a nested `Fn::Sub`. Reads a
|
|
572
|
+
* two-argument `Sub`'s variable map after `toJSON()` has turned its live
|
|
573
|
+
* references into JSON.
|
|
574
|
+
*/
|
|
575
|
+
function cfnJsonRefNames(json: unknown, out: string[]): void {
|
|
576
|
+
if (json === null || typeof json !== "object") return;
|
|
577
|
+
if (Array.isArray(json)) {
|
|
578
|
+
for (const item of json) cfnJsonRefNames(item, out);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const obj = json as Record<string, unknown>;
|
|
582
|
+
if (typeof obj.Ref === "string") {
|
|
583
|
+
out.push(obj.Ref);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const getAtt = obj["Fn::GetAtt"];
|
|
587
|
+
if (getAtt !== undefined) {
|
|
588
|
+
const id = Array.isArray(getAtt) ? getAtt[0] : typeof getAtt === "string" ? getAtt.split(".")[0] : undefined;
|
|
589
|
+
if (typeof id === "string") out.push(id);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const sub = subForm(obj);
|
|
593
|
+
if (sub) {
|
|
594
|
+
for (const r of subTemplateRefs(sub.template, new Set(Object.keys(sub.vars ?? {})))) out.push(r.name);
|
|
595
|
+
if (sub.vars) cfnJsonRefNames(Object.values(sub.vars), out);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
for (const v of Object.values(obj)) cfnJsonRefNames(v, out);
|
|
599
|
+
}
|
|
600
|
+
|
|
502
601
|
/** Resolve the producer (logical name) an AttrRef points at. */
|
|
503
602
|
function refTarget(ref: AttrRef, reverse: Map<object, string>): string | undefined {
|
|
504
603
|
// Nested refs (under `props`) aren't assigned a logical name by resolveAttrRefs,
|
|
@@ -545,6 +644,9 @@ function project(value: unknown, seen: Set<unknown>, reverse: Map<object, string
|
|
|
545
644
|
// Ref to a non-node) stay opaque.
|
|
546
645
|
const to = refIntrinsicTarget(value);
|
|
547
646
|
if (to && nodeIds.has(to)) return { $ref: to } satisfies AttrRefEnvelope;
|
|
647
|
+
// A Sub keeps its template, so a reader sees which node and attribute it names (#4373).
|
|
648
|
+
const sub = subIntrinsicForm(value);
|
|
649
|
+
if (sub) return subEnvelope(sub, seen, reverse, nodeIds);
|
|
548
650
|
return { $intrinsic: true };
|
|
549
651
|
}
|
|
550
652
|
|
|
@@ -568,6 +670,14 @@ function project(value: unknown, seen: Set<unknown>, reverse: Map<object, string
|
|
|
568
670
|
return out;
|
|
569
671
|
}
|
|
570
672
|
|
|
673
|
+
// A Sub written as plain CloudFormation JSON reads the same as the `Sub` tag's.
|
|
674
|
+
const plainSub = subForm(value);
|
|
675
|
+
if (plainSub) {
|
|
676
|
+
const out = subEnvelope(plainSub, seen, reverse, nodeIds);
|
|
677
|
+
seen.delete(value);
|
|
678
|
+
return out;
|
|
679
|
+
}
|
|
680
|
+
|
|
571
681
|
const out: Record<string, unknown> = {};
|
|
572
682
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
573
683
|
const p = project(v, seen, reverse, nodeIds);
|
|
@@ -577,6 +687,17 @@ function project(value: unknown, seen: Set<unknown>, reverse: Map<object, string
|
|
|
577
687
|
return out;
|
|
578
688
|
}
|
|
579
689
|
|
|
690
|
+
/** Project a {@link SubForm} into its {@link SubEnvelope}. */
|
|
691
|
+
function subEnvelope(sub: SubForm, seen: Set<unknown>, reverse: Map<object, string>, nodeIds: Set<string>): SubEnvelope {
|
|
692
|
+
if (!sub.vars) return { $sub: sub.template };
|
|
693
|
+
const vars: Record<string, unknown> = {};
|
|
694
|
+
for (const [k, v] of Object.entries(sub.vars)) {
|
|
695
|
+
const p = project(v, seen, reverse, nodeIds);
|
|
696
|
+
if (p !== undefined) vars[k] = p;
|
|
697
|
+
}
|
|
698
|
+
return { $sub: sub.template, $vars: vars };
|
|
699
|
+
}
|
|
700
|
+
|
|
580
701
|
// `references` and `stack` are the two duck-typed channels above
|
|
581
702
|
// ({@link entityReferences}, {@link entityStack}). They are metadata about the
|
|
582
703
|
// entity's place in the graph, not declared configuration, and both are read
|
|
@@ -645,6 +766,9 @@ function collectEdges(
|
|
|
645
766
|
});
|
|
646
767
|
}
|
|
647
768
|
const seen = new Set<unknown>();
|
|
769
|
+
const edgeTo = (to: string, viaAttr: string): void => {
|
|
770
|
+
if (to !== from && nodeIds.has(to)) edges.push({ from, to, kind: "ref", viaAttr });
|
|
771
|
+
};
|
|
648
772
|
const visit = (value: unknown, viaAttr: string): void => {
|
|
649
773
|
if (value === null || typeof value !== "object") return;
|
|
650
774
|
if (isAttrRefLike(value)) {
|
|
@@ -659,8 +783,31 @@ function collectEdges(
|
|
|
659
783
|
// — resolve it to an edge instead of dropping it as an opaque intrinsic (#513).
|
|
660
784
|
const to = refIntrinsicTarget(value);
|
|
661
785
|
if (to && to !== from && nodeIds.has(to)) edges.push({ from, to, kind: "ref", viaAttr });
|
|
786
|
+
// A Sub's interpolations are references too (#4373): each `${Name}` or
|
|
787
|
+
// `${Name.Attr}` naming a node, and each reference in a variable map.
|
|
788
|
+
const sub = subIntrinsicForm(value);
|
|
789
|
+
if (sub) {
|
|
790
|
+
const names = subTemplateRefs(sub.template, new Set(Object.keys(sub.vars ?? {}))).map((r) => r.name);
|
|
791
|
+
if (sub.vars) cfnJsonRefNames(Object.values(sub.vars), names);
|
|
792
|
+
for (const name of names) edgeTo(name, viaAttr);
|
|
793
|
+
}
|
|
794
|
+
// The live values the aws `Sub` tag interpolated. Covers an AttrRef whose
|
|
795
|
+
// logical name was not set, where `toJSON()` throws and leaves no
|
|
796
|
+
// template to read above. Any other intrinsic stays opaque.
|
|
797
|
+
const interpolated = (value as { values?: unknown }).values;
|
|
798
|
+
if (Array.isArray(interpolated) && (sub || !serializes(value))) {
|
|
799
|
+
for (const v of interpolated) visit(v, viaAttr);
|
|
800
|
+
}
|
|
662
801
|
return;
|
|
663
802
|
}
|
|
803
|
+
// A Sub written as plain CloudFormation JSON in props: the template's names
|
|
804
|
+
// here, then the variable map's live values through the walk below.
|
|
805
|
+
const plainSub = subForm(value);
|
|
806
|
+
if (plainSub) {
|
|
807
|
+
for (const r of subTemplateRefs(plainSub.template, new Set(Object.keys(plainSub.vars ?? {})))) {
|
|
808
|
+
edgeTo(r.name, viaAttr);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
664
811
|
// A whole node held in props or an enumerable field (the sql lexicon's
|
|
665
812
|
// `dependsOn`) is a reference to it: draw the edge, and do not walk its
|
|
666
813
|
// config, whose refs belong to that node's own edges (#3257). Checked
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Kept apart from ./graph-ir so ./graph-detail, which runs in the browser
|
|
2
|
+
// bundle, can read a Sub template without pulling in node:path.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The names a `Sub` template interpolates (#4373): `${Name}` and
|
|
6
|
+
* `${Name.Attr}`, in order. Skips CloudFormation's `${!Literal}` escape,
|
|
7
|
+
* pseudo-parameters (`${AWS::Region}`, anything with `::`), and names the
|
|
8
|
+
* two-argument form's variable map binds, which are not resources.
|
|
9
|
+
*/
|
|
10
|
+
export function subTemplateRefs(
|
|
11
|
+
template: string,
|
|
12
|
+
bound: ReadonlySet<string> = new Set(),
|
|
13
|
+
): Array<{ name: string; attr?: string }> {
|
|
14
|
+
const out: Array<{ name: string; attr?: string }> = [];
|
|
15
|
+
for (const match of template.matchAll(/\$\{([^}]*)\}/g)) {
|
|
16
|
+
const token = match[1].trim();
|
|
17
|
+
if (!token || token.startsWith("!") || token.includes("::") || bound.has(token)) continue;
|
|
18
|
+
const dot = token.indexOf(".");
|
|
19
|
+
out.push(dot > 0 ? { name: token.slice(0, dot), attr: token.slice(dot + 1) } : { name: token });
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
package/src/lexicon.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CapabilityPack } from "./capabilities/view";
|
|
1
2
|
import type { Serializer } from "./serializer";
|
|
2
3
|
import type { Declarable } from "./declarable";
|
|
3
4
|
import type { LintRule } from "./lint/rule";
|
|
@@ -2082,6 +2083,15 @@ export interface LexiconPlugin {
|
|
|
2082
2083
|
*/
|
|
2083
2084
|
referenceCatalog?: ReferenceCatalog;
|
|
2084
2085
|
|
|
2086
|
+
/**
|
|
2087
|
+
* This lexicon's capability pack (#4375): its mapping onto the
|
|
2088
|
+
* lexicon-neutral view the capability rules read (labels, edge names,
|
|
2089
|
+
* attributes, policy documents, inline blocks, link resources), as data,
|
|
2090
|
+
* plus an adapter for what the mapping can't say as data. `chant search`'s
|
|
2091
|
+
* `cap:` terms and `--capabilities` read it. Opt-in.
|
|
2092
|
+
*/
|
|
2093
|
+
capabilityPack?: CapabilityPack;
|
|
2094
|
+
|
|
2085
2095
|
/**
|
|
2086
2096
|
* Enrich live IR node attributes for edge reconstruction (#784). Returns
|
|
2087
2097
|
* `nodeId → attributes` with cross-resource references resolved to the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { observeResources, collectAmbient } from "./observe";
|
|
2
|
+
import { observeResources, collectAmbient, carryAnnotations } from "./observe";
|
|
3
3
|
import { observation } from "../observation";
|
|
4
4
|
import type { ObservationLexicon, ResourceMetadata } from "../lexicon";
|
|
5
5
|
import type { BuildResult } from "../build";
|
|
@@ -59,6 +59,32 @@ describe("observeResources", () => {
|
|
|
59
59
|
expect(Object.keys(observations[0].resources)).toEqual(["web-vpc"]);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
+
it("returns what a lexicon learned about an absent entity as annotations, outside the observations (#4372)", async () => {
|
|
63
|
+
const plugins = [
|
|
64
|
+
{
|
|
65
|
+
name: "aws",
|
|
66
|
+
serializer: {} as ObservationLexicon["serializer"],
|
|
67
|
+
describeResources: async () =>
|
|
68
|
+
observation({}, undefined, undefined, undefined, undefined, undefined, { "web-vpc": { planAction: "create" } }),
|
|
69
|
+
} as unknown as ObservationLexicon,
|
|
70
|
+
];
|
|
71
|
+
const { observations, annotations } = await observeResources("prod", plugins, mockBuild());
|
|
72
|
+
// Absent: no observation row anywhere, so nothing that classifies sees it.
|
|
73
|
+
expect(observations.flatMap((o) => Object.keys(o.resources))).toEqual([]);
|
|
74
|
+
expect(annotations).toEqual({ "web-vpc": { planAction: "create" } });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("carryAnnotations sets attributes by id and touches nothing else (#4372)", () => {
|
|
78
|
+
const nodes = [
|
|
79
|
+
{ id: "a", kind: "K", lexicon: "x", attrs: { _status: "accent", name: "a" } },
|
|
80
|
+
{ id: "b", kind: "K", lexicon: "x", attrs: { _status: "good" } },
|
|
81
|
+
];
|
|
82
|
+
const out = carryAnnotations(nodes, { a: { planAction: "create" } });
|
|
83
|
+
expect(out[0]).toEqual({ id: "a", kind: "K", lexicon: "x", attrs: { _status: "accent", name: "a", planAction: "create" } });
|
|
84
|
+
expect(out[1]).toBe(nodes[1]);
|
|
85
|
+
expect(carryAnnotations(nodes, undefined)).toBe(nodes);
|
|
86
|
+
});
|
|
87
|
+
|
|
62
88
|
it("passes the declared entity names for scoping", async () => {
|
|
63
89
|
let names: string[] = [];
|
|
64
90
|
const plugins = [awsPlugin(({ entityNames }) => { names = entityNames; return {}; })];
|
package/src/lifecycle/observe.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
unobservedAll,
|
|
23
23
|
formatUnobserved,
|
|
24
24
|
type NormalizedObservation,
|
|
25
|
+
type RootRead,
|
|
25
26
|
} from "../observation";
|
|
26
27
|
import { zeroResourcesWarning } from "../live-endpoint";
|
|
27
28
|
import { unqualifiedKey } from "./identity";
|
|
@@ -39,6 +40,40 @@ export interface ObserveResult {
|
|
|
39
40
|
* answer, not ahead of it.
|
|
40
41
|
*/
|
|
41
42
|
notes: string[];
|
|
43
|
+
/**
|
|
44
|
+
* Facts about declared entities beside their verdict (#4372), keyed like
|
|
45
|
+
* the observations' resources (stack-qualified where those are). A planned
|
|
46
|
+
* create is the case: absent, with the plan's attributes. Never part of
|
|
47
|
+
* `observations`, so nothing that classifies or paints presence sees it;
|
|
48
|
+
* {@link carryAnnotations} puts it on the declared nodes of an overlay.
|
|
49
|
+
*/
|
|
50
|
+
annotations?: Record<string, Record<string, unknown>>;
|
|
51
|
+
/**
|
|
52
|
+
* Which read answered each declared entity (#2267), keyed like the
|
|
53
|
+
* observations' resources: `"state"`, `"live"`, `"plan"` or `"live+plan"`
|
|
54
|
+
* from the terraform lexicon. Present only for lexicons that report it.
|
|
55
|
+
*/
|
|
56
|
+
sources?: Record<string, string>;
|
|
57
|
+
/** Which read answered each root, and why it fell back (structured, from the lexicon). */
|
|
58
|
+
rootReads?: Record<string, RootRead>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Put each entity's annotations (#4372) on its node, by id. Only attributes
|
|
63
|
+
* are touched: `_status`, `physicalId` and `ownership` stay what the overlay
|
|
64
|
+
* decided, so a planned create is still painted pending and still absent to
|
|
65
|
+
* every consumer that asks. An annotation wins a collision with a declared
|
|
66
|
+
* attribute, the same precedence an observed attribute has in the overlay.
|
|
67
|
+
*/
|
|
68
|
+
export function carryAnnotations<N extends { id: string; attrs: Record<string, unknown> }>(
|
|
69
|
+
nodes: N[],
|
|
70
|
+
annotations: Record<string, Record<string, unknown>> | undefined,
|
|
71
|
+
): N[] {
|
|
72
|
+
if (!annotations || Object.keys(annotations).length === 0) return nodes;
|
|
73
|
+
return nodes.map((n) => {
|
|
74
|
+
const a = annotations[n.id];
|
|
75
|
+
return a && Object.keys(a).length > 0 ? { ...n, attrs: { ...n.attrs, ...a } } : n;
|
|
76
|
+
});
|
|
42
77
|
}
|
|
43
78
|
|
|
44
79
|
/**
|
|
@@ -57,6 +92,7 @@ function qualifyObservation(obs: NormalizedObservation, stackName: string): Norm
|
|
|
57
92
|
queried: q(obs.queried),
|
|
58
93
|
// Which read answered (#2267) is per entity, so it re-keys with them.
|
|
59
94
|
sources: q(obs.sources),
|
|
95
|
+
...(obs.annotations ? { annotations: q(obs.annotations) } : {}),
|
|
60
96
|
notes: obs.notes,
|
|
61
97
|
// Exports are already keyed by stack (#1279); nothing to qualify.
|
|
62
98
|
...(obs.stackExports ? { stackExports: obs.stackExports } : {}),
|
|
@@ -131,6 +167,9 @@ export async function observeResources(
|
|
|
131
167
|
const warnings: string[] = [];
|
|
132
168
|
const errors: string[] = [];
|
|
133
169
|
const notes: string[] = [];
|
|
170
|
+
const annotations: Record<string, Record<string, unknown>> = {};
|
|
171
|
+
const sources: Record<string, string> = {};
|
|
172
|
+
const rootReads: Record<string, RootRead> = {};
|
|
134
173
|
|
|
135
174
|
for (const plugin of plugins) {
|
|
136
175
|
if (!plugin.describeResources) continue;
|
|
@@ -230,6 +269,9 @@ export async function observeResources(
|
|
|
230
269
|
const line = `[${plugin.name}] ${note}`;
|
|
231
270
|
if (!notes.includes(line)) notes.push(line);
|
|
232
271
|
}
|
|
272
|
+
Object.assign(annotations, observed.annotations ?? {});
|
|
273
|
+
Object.assign(sources, observed.sources);
|
|
274
|
+
Object.assign(rootReads, observed.rootReads ?? {});
|
|
233
275
|
// What the estate depends on but does not declare (#1273). Read after the
|
|
234
276
|
// managed resources, because the declared observation is the closure's
|
|
235
277
|
// roots — there is nothing to reference out from until it exists.
|
|
@@ -299,7 +341,7 @@ export async function observeResources(
|
|
|
299
341
|
}
|
|
300
342
|
}
|
|
301
343
|
|
|
302
|
-
return { observations, warnings, errors, notes };
|
|
344
|
+
return { observations, warnings, errors, notes, annotations, ...(Object.keys(sources).length > 0 ? { sources } : {}), ...(Object.keys(rootReads).length > 0 ? { rootReads } : {}) };
|
|
303
345
|
}
|
|
304
346
|
|
|
305
347
|
/**
|
package/src/lint/rule.ts
CHANGED
|
@@ -45,6 +45,12 @@ export interface LintDiagnostic {
|
|
|
45
45
|
severity: Severity;
|
|
46
46
|
/** Human-readable message */
|
|
47
47
|
message: string;
|
|
48
|
+
/**
|
|
49
|
+
* The entity a post-synth check reported against, when it named one: the
|
|
50
|
+
* same name the message leads with in brackets, and the graph node's id
|
|
51
|
+
* (behold#919). Absent for a source-file rule.
|
|
52
|
+
*/
|
|
53
|
+
entity?: string;
|
|
48
54
|
/** Optional fix that can be applied */
|
|
49
55
|
fix?: LintFix;
|
|
50
56
|
}
|
package/src/live-endpoint.ts
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* `--live --env floci` read against azure silently went to real Azure.
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
-
import { environmentEndpoint, type EnvironmentDeclaration } from "./config";
|
|
38
|
+
import { environmentEndpoint, type EnvironmentDeclaration } from "./config-environments";
|
|
39
39
|
import { emulatorsOf, endpointEnvVars, type EmulatorDeclaration } from "./op/emulator-lifecycle";
|
|
40
40
|
|
|
41
41
|
/**
|
package/src/observation.test.ts
CHANGED
|
@@ -221,6 +221,31 @@ describe("observeEntities harness (#1201)", () => {
|
|
|
221
221
|
});
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
+
test("an absent entity's annotations are collected, and it stays in neither map (#4372)", async () => {
|
|
225
|
+
const result = await observeEntities(
|
|
226
|
+
[entity("a"), entity("b"), entity("c")],
|
|
227
|
+
adapterOf({
|
|
228
|
+
a: { present: meta() },
|
|
229
|
+
b: { absent: true, annotations: { planAction: "create", planAfterUnknown: ["id"] } },
|
|
230
|
+
c: { absent: true, annotations: {} },
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
expect(Object.keys(result.resources)).toEqual(["a"]);
|
|
234
|
+
expect(result.unobserved).toBeUndefined();
|
|
235
|
+
expect(result.annotations).toEqual({ b: { planAction: "create", planAfterUnknown: ["id"] } });
|
|
236
|
+
// Through the normalizer and a merge with another part, unchanged.
|
|
237
|
+
const merged = mergeObservations([normalizeObservation(result), normalizeObservation({ other: meta() })]);
|
|
238
|
+
expect(merged.annotations).toEqual({ b: { planAction: "create", planAfterUnknown: ["id"] } });
|
|
239
|
+
expect(Object.keys(merged.resources).sort()).toEqual(["a", "other"]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("no annotations means no annotations key, on the envelope and the normalized form", async () => {
|
|
243
|
+
const result = await observeEntities([entity("b")], adapterOf({ b: { absent: true } }));
|
|
244
|
+
expect("annotations" in result).toBe(false);
|
|
245
|
+
expect("annotations" in normalizeObservation(result)).toBe(false);
|
|
246
|
+
expect("annotations" in mergeObservations([normalizeObservation(result)])).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
|
|
224
249
|
test("collects the queried address from every variant — the absent one especially (#1620)", async () => {
|
|
225
250
|
const result = await observeEntities(
|
|
226
251
|
[entity("a"), entity("b"), entity("c")],
|
package/src/observation.ts
CHANGED
|
@@ -149,6 +149,31 @@ export interface ObservationResult {
|
|
|
149
149
|
* before this field an observation gave it no way to tell them apart.
|
|
150
150
|
*/
|
|
151
151
|
sources?: Record<string, string>;
|
|
152
|
+
/**
|
|
153
|
+
* Facts the read learned about an entity beside its verdict (#4372), keyed
|
|
154
|
+
* by chant entity name, as attribute name -> value. The case it exists for
|
|
155
|
+
* is a Terraform plan's planned create: the plan says the block will be
|
|
156
|
+
* created and which of its values stay unknown until apply, but the block
|
|
157
|
+
* has no row in the plan's prior state, so it is OBSERVED-ABSENT, and
|
|
158
|
+
* absence is spelled "in neither map". Without this map those facts had
|
|
159
|
+
* nowhere to go.
|
|
160
|
+
*
|
|
161
|
+
* Additive metadata over the tri-state, like {@link queried} and
|
|
162
|
+
* {@link sources}: classification never reads it, ownership never comes
|
|
163
|
+
* from it, and an entity with annotations and no `resources` row is still
|
|
164
|
+
* absent. A renderer or search carries the attributes onto the declared
|
|
165
|
+
* node (`carryAnnotations` in ./lifecycle/observe.ts), where they read like
|
|
166
|
+
* any other attribute without making the node present.
|
|
167
|
+
*/
|
|
168
|
+
annotations?: Record<string, Record<string, unknown>>;
|
|
169
|
+
/**
|
|
170
|
+
* Which read answered each root, and why it fell back when it did (#4384
|
|
171
|
+
* follow-up), keyed by a lexicon-chosen root key (terraform uses
|
|
172
|
+
* `terraform.roots.<root>`). Structured beside {@link notes}, which say the
|
|
173
|
+
* same thing in prose: a consumer reads this field and never matches note
|
|
174
|
+
* text, so rewording a note cannot change what it reports.
|
|
175
|
+
*/
|
|
176
|
+
rootReads?: Record<string, RootRead>;
|
|
152
177
|
/**
|
|
153
178
|
* Notices about the read as a whole, not about any one entity (#1265) —
|
|
154
179
|
* "the ownership filter could not be applied on this surface" is the
|
|
@@ -169,6 +194,20 @@ export interface ObservationResult {
|
|
|
169
194
|
stackExports?: Record<string, Record<string, unknown>>;
|
|
170
195
|
}
|
|
171
196
|
|
|
197
|
+
/**
|
|
198
|
+
* The read a root was asked for and the one that answered. `depth` is the read
|
|
199
|
+
* asked for (`"plan"` for a plan-depth root, `"live+plan"` for a live root at
|
|
200
|
+
* the plan depth). `fellBackTo` is set only when that read failed: the read that
|
|
201
|
+
* answered instead (`"live"` is live-plan alone, for a live root), or
|
|
202
|
+
* `"declared"` when none did.
|
|
203
|
+
* `reason` is a short machine-stable cause, not prose to match on.
|
|
204
|
+
*/
|
|
205
|
+
export interface RootRead {
|
|
206
|
+
depth: string;
|
|
207
|
+
fellBackTo?: "state" | "live" | "declared";
|
|
208
|
+
reason?: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
172
211
|
/**
|
|
173
212
|
* What `describeResources()` may return: the bare map (pre-#1089, still valid —
|
|
174
213
|
* "I looked at everything") or the {@link ObservationResult} envelope.
|
|
@@ -183,6 +222,10 @@ export interface NormalizedObservation {
|
|
|
183
222
|
queried: Record<string, string>;
|
|
184
223
|
/** Which read answered, per entity name (#2267). Empty when the lexicon reported none. */
|
|
185
224
|
sources: Record<string, string>;
|
|
225
|
+
/** Facts beside the verdict, per entity name (#4372). Absent when the lexicon reported none. */
|
|
226
|
+
annotations?: Record<string, Record<string, unknown>>;
|
|
227
|
+
/** Which read answered each root, structured. Absent when the lexicon reported none. */
|
|
228
|
+
rootReads?: Record<string, RootRead>;
|
|
186
229
|
/** Run-level notices (#1265), distinct. Empty when the lexicon reported none. */
|
|
187
230
|
notes: string[];
|
|
188
231
|
/** Per-stack exports (#1279), keyed by stack name. Absent when the lexicon reported none. */
|
|
@@ -209,6 +252,8 @@ export function observation(
|
|
|
209
252
|
notes?: string[],
|
|
210
253
|
stackExports?: Record<string, Record<string, unknown>>,
|
|
211
254
|
sources?: Record<string, string>,
|
|
255
|
+
annotations?: Record<string, Record<string, unknown>>,
|
|
256
|
+
rootReads?: Record<string, RootRead>,
|
|
212
257
|
): ObservationResult {
|
|
213
258
|
return {
|
|
214
259
|
observation: "v1",
|
|
@@ -216,6 +261,8 @@ export function observation(
|
|
|
216
261
|
...(unobserved && Object.keys(unobserved).length > 0 ? { unobserved } : {}),
|
|
217
262
|
...(queried && Object.keys(queried).length > 0 ? { queried } : {}),
|
|
218
263
|
...(sources && Object.keys(sources).length > 0 ? { sources } : {}),
|
|
264
|
+
...(annotations && Object.keys(annotations).length > 0 ? { annotations } : {}),
|
|
265
|
+
...(rootReads && Object.keys(rootReads).length > 0 ? { rootReads } : {}),
|
|
219
266
|
...(notes && notes.length > 0 ? { notes } : {}),
|
|
220
267
|
...(stackExports && Object.keys(stackExports).length > 0 ? { stackExports } : {}),
|
|
221
268
|
};
|
|
@@ -236,6 +283,8 @@ export function normalizeObservation(value: DescribeResourcesResult | undefined)
|
|
|
236
283
|
queried: value.queried ?? {},
|
|
237
284
|
sources: value.sources ?? {},
|
|
238
285
|
notes: [...new Set(value.notes ?? [])],
|
|
286
|
+
...(value.annotations && Object.keys(value.annotations).length > 0 ? { annotations: value.annotations } : {}),
|
|
287
|
+
...(value.rootReads && Object.keys(value.rootReads).length > 0 ? { rootReads: value.rootReads } : {}),
|
|
239
288
|
...(value.stackExports && Object.keys(value.stackExports).length > 0 ? { stackExports: value.stackExports } : {}),
|
|
240
289
|
};
|
|
241
290
|
}
|
|
@@ -287,11 +336,16 @@ export function mergeObservations(parts: Iterable<NormalizedObservation>): Norma
|
|
|
287
336
|
// is one note (#1265).
|
|
288
337
|
const notes = new Set<string>();
|
|
289
338
|
const stackExports: Record<string, Record<string, unknown>> = {};
|
|
339
|
+
// Annotations (#4372) merge like `sources`: per entity, disjoint parts.
|
|
340
|
+
const annotations: Record<string, Record<string, unknown>> = {};
|
|
341
|
+
const rootReads: Record<string, RootRead> = {};
|
|
290
342
|
for (const part of parts) {
|
|
291
343
|
Object.assign(resources, part.resources);
|
|
292
344
|
Object.assign(unobserved, part.unobserved);
|
|
293
345
|
Object.assign(queried, part.queried);
|
|
294
346
|
Object.assign(sources, part.sources);
|
|
347
|
+
Object.assign(annotations, part.annotations ?? {});
|
|
348
|
+
Object.assign(rootReads, part.rootReads ?? {});
|
|
295
349
|
for (const n of part.notes) notes.add(n);
|
|
296
350
|
Object.assign(stackExports, part.stackExports ?? {});
|
|
297
351
|
}
|
|
@@ -304,6 +358,8 @@ export function mergeObservations(parts: Iterable<NormalizedObservation>): Norma
|
|
|
304
358
|
queried,
|
|
305
359
|
sources,
|
|
306
360
|
notes: [...notes],
|
|
361
|
+
...(Object.keys(annotations).length > 0 ? { annotations } : {}),
|
|
362
|
+
...(Object.keys(rootReads).length > 0 ? { rootReads } : {}),
|
|
307
363
|
...(Object.keys(stackExports).length > 0 ? { stackExports } : {}),
|
|
308
364
|
};
|
|
309
365
|
}
|
|
@@ -370,10 +426,14 @@ export interface DeclaredEntity {
|
|
|
370
426
|
* issued against. The harness collects it into the result's `queried` map (and
|
|
371
427
|
* onto the unobserved entry), so even an absent verdict — which records
|
|
372
428
|
* nothing else — says where the provider was asked.
|
|
429
|
+
*
|
|
430
|
+
* An absent verdict may also carry `annotations` (#4372): attributes the read
|
|
431
|
+
* learned about the entity without finding it, collected into the result's
|
|
432
|
+
* `annotations` map. They never make it present.
|
|
373
433
|
*/
|
|
374
434
|
export type EntityObservation =
|
|
375
435
|
| { present: ResourceMetadata; queried?: string }
|
|
376
|
-
| { absent: true; queried?: string }
|
|
436
|
+
| { absent: true; queried?: string; annotations?: Record<string, unknown> }
|
|
377
437
|
| { unobserved: { reason: UnobservedReason; detail?: string }; queried?: string };
|
|
378
438
|
|
|
379
439
|
/** What a lexicon supplies to drive the harness. `Client` is its transport handle. */
|
|
@@ -463,6 +523,7 @@ export async function observeEntities<Client>(
|
|
|
463
523
|
const resources: Record<string, ResourceMetadata> = {};
|
|
464
524
|
const unobserved: Record<string, UnobservedEntity> = {};
|
|
465
525
|
const queried: Record<string, string> = {};
|
|
526
|
+
const annotations: Record<string, Record<string, unknown>> = {};
|
|
466
527
|
const run = adapter.concurrently ?? ((items, fn) => boundedConcurrently(items, fn));
|
|
467
528
|
|
|
468
529
|
await run(declared, async (entity) => {
|
|
@@ -486,11 +547,14 @@ export async function observeEntities<Client>(
|
|
|
486
547
|
...result.unobserved,
|
|
487
548
|
...(result.queried ? { queried: result.queried } : {}),
|
|
488
549
|
};
|
|
550
|
+
} else if (result.annotations && Object.keys(result.annotations).length > 0) {
|
|
551
|
+
// `absent`: the verdict records nothing — in neither map is how the
|
|
552
|
+
// contract spells absence — but its address, when the adapter supplied
|
|
553
|
+
// one, lands in `queried` (#1620) so the absence can explain itself, and
|
|
554
|
+
// what the read learned about it lands in `annotations` (#4372).
|
|
555
|
+
annotations[entity.name] = result.annotations;
|
|
489
556
|
}
|
|
490
|
-
// `absent`: the verdict records nothing — in neither map is how the
|
|
491
|
-
// contract spells absence — but its address, when the adapter supplied
|
|
492
|
-
// one, lands in `queried` (#1620) so the absence can explain itself.
|
|
493
557
|
});
|
|
494
558
|
|
|
495
|
-
return observation(resources, unobserved, queried);
|
|
559
|
+
return observation(resources, unobserved, queried, undefined, undefined, undefined, annotations);
|
|
496
560
|
}
|