@telorun/kernel 0.60.0 → 0.62.0
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/bundle/module-artifact.d.ts +28 -2
- package/dist/bundle/module-artifact.d.ts.map +1 -1
- package/dist/bundle/module-artifact.js +36 -9
- package/dist/bundle/module-artifact.js.map +1 -1
- package/dist/controller-loader.d.ts +7 -4
- package/dist/controller-loader.d.ts.map +1 -1
- package/dist/controller-loader.js.map +1 -1
- package/dist/controller-loaders/bundle-loader.d.ts.map +1 -1
- package/dist/controller-loaders/bundle-loader.js +9 -4
- package/dist/controller-loaders/bundle-loader.js.map +1 -1
- package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
- package/dist/controller-loaders/npm-loader.js +62 -10
- package/dist/controller-loaders/npm-loader.js.map +1 -1
- package/dist/controllers/resource-definition/resource-inherited-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-inherited-controller.js +57 -10
- package/dist/controllers/resource-definition/resource-inherited-controller.js.map +1 -1
- package/dist/controllers/type/json-schema-controller.d.ts +8 -0
- package/dist/controllers/type/json-schema-controller.d.ts.map +1 -0
- package/dist/controllers/type/json-schema-controller.js +91 -0
- package/dist/controllers/type/json-schema-controller.js.map +1 -0
- package/dist/evaluation-context.d.ts +5 -0
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/evaluation-context.js +63 -33
- package/dist/evaluation-context.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/init-failure-diagnostics.d.ts +61 -0
- package/dist/init-failure-diagnostics.d.ts.map +1 -0
- package/dist/init-failure-diagnostics.js +141 -0
- package/dist/init-failure-diagnostics.js.map +1 -0
- package/dist/invocation-contract-binding.d.ts +105 -0
- package/dist/invocation-contract-binding.d.ts.map +1 -0
- package/dist/invocation-contract-binding.js +296 -0
- package/dist/invocation-contract-binding.js.map +1 -0
- package/dist/kernel.d.ts +17 -0
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +76 -25
- package/dist/kernel.js.map +1 -1
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +21 -0
- package/dist/module-context.js.map +1 -1
- package/dist/resource-context.d.ts +45 -0
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +79 -0
- package/dist/resource-context.js.map +1 -1
- package/dist/schema-compiled-values.d.ts +9 -1
- package/dist/schema-compiled-values.d.ts.map +1 -1
- package/dist/schema-compiled-values.js +55 -16
- package/dist/schema-compiled-values.js.map +1 -1
- package/dist/schema-validator.d.ts.map +1 -1
- package/dist/schema-validator.js +15 -1
- package/dist/schema-validator.js.map +1 -1
- package/dist/static-analysis-diagnostics.d.ts +19 -0
- package/dist/static-analysis-diagnostics.d.ts.map +1 -0
- package/dist/static-analysis-diagnostics.js +52 -0
- package/dist/static-analysis-diagnostics.js.map +1 -0
- package/dist/transports/oci/oci-transport.d.ts +17 -0
- package/dist/transports/oci/oci-transport.d.ts.map +1 -1
- package/dist/transports/oci/oci-transport.js +33 -8
- package/dist/transports/oci/oci-transport.js.map +1 -1
- package/dist/transports/transport-registry.d.ts +6 -4
- package/dist/transports/transport-registry.d.ts.map +1 -1
- package/dist/transports/transport-registry.js +6 -4
- package/dist/transports/transport-registry.js.map +1 -1
- package/package.json +4 -4
- package/src/bundle/module-artifact.ts +54 -12
- package/src/controller-loader.ts +7 -4
- package/src/controller-loaders/bundle-loader.ts +9 -4
- package/src/controller-loaders/npm-loader.ts +75 -13
- package/src/controllers/resource-definition/resource-inherited-controller.ts +69 -10
- package/src/controllers/type/json-schema-controller.ts +114 -0
- package/src/evaluation-context.ts +81 -32
- package/src/index.ts +3 -1
- package/src/init-failure-diagnostics.ts +169 -0
- package/src/invocation-contract-binding.ts +392 -0
- package/src/kernel.ts +95 -23
- package/src/module-context.ts +27 -0
- package/src/resource-context.ts +81 -0
- package/src/schema-compiled-values.ts +55 -15
- package/src/schema-validator.ts +15 -1
- package/src/static-analysis-diagnostics.ts +51 -0
- package/src/transports/oci/oci-transport.ts +34 -8
- package/src/transports/transport-registry.ts +6 -4
|
@@ -26,6 +26,12 @@ import {
|
|
|
26
26
|
} from "@telorun/sdk";
|
|
27
27
|
import { RuntimeError } from "@telorun/sdk";
|
|
28
28
|
import { evalPathCovers } from "@telorun/analyzer";
|
|
29
|
+
import {
|
|
30
|
+
classifyInitFailures,
|
|
31
|
+
renderInitFailureText,
|
|
32
|
+
summarizeInitFailures,
|
|
33
|
+
type FailedResource,
|
|
34
|
+
} from "./init-failure-diagnostics.js";
|
|
29
35
|
import {
|
|
30
36
|
acceptReportedStatus,
|
|
31
37
|
buildPublishedProps,
|
|
@@ -93,6 +99,16 @@ function collectResourceRefs(resource: ResourceManifest): ResourceRef[] {
|
|
|
93
99
|
return [...found.values()];
|
|
94
100
|
}
|
|
95
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Project resource refs onto the names they depend on IN THIS CONTEXT. A local
|
|
104
|
+
* ref is its own name; a cross-module `Alias.name` ref depends on the local
|
|
105
|
+
* `Telo.Import` resource named by the alias, since that is the resource whose
|
|
106
|
+
* failure would strand it. Used to attribute an init failure to its cause.
|
|
107
|
+
*/
|
|
108
|
+
function localDependencyNames(refs: ResourceRef[]): string[] {
|
|
109
|
+
return refs.map((r) => (r.alias && r.alias !== "Self" ? r.alias : r.name));
|
|
110
|
+
}
|
|
111
|
+
|
|
96
112
|
/**
|
|
97
113
|
* Build a resource's resolved properties for the debug stream — its config "after
|
|
98
114
|
* templating", with `${{ }}` / `!cel` reduced to concrete values. The manifest is
|
|
@@ -343,6 +359,12 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
343
359
|
/** Resources queued for initialization on this context node. */
|
|
344
360
|
private pendingResources: ResourceManifest[] = [];
|
|
345
361
|
|
|
362
|
+
/** Per-resource dependency names, captured at create() time — BEFORE Phase-5
|
|
363
|
+
* injection swaps refs for live instances, so the walk sees plain objects and
|
|
364
|
+
* cannot wander into a controller's (possibly cyclic) object graph. Read only
|
|
365
|
+
* when init fails, to attribute each failure to its cause. */
|
|
366
|
+
private readonly resourceDependencies = new Map<string, string[]>();
|
|
367
|
+
|
|
346
368
|
/**
|
|
347
369
|
* Optional hook called between create() and init() for each resource.
|
|
348
370
|
* Set by the kernel to inject live instances into reference fields.
|
|
@@ -616,7 +638,10 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
616
638
|
*/
|
|
617
639
|
async initializeResources(): Promise<void> {
|
|
618
640
|
const MAX_PASSES = 10;
|
|
619
|
-
const errors = new Map<
|
|
641
|
+
const errors = new Map<
|
|
642
|
+
string,
|
|
643
|
+
{ message: string; code?: string; details?: string; children?: RuntimeDiagnostic[] }
|
|
644
|
+
>();
|
|
620
645
|
|
|
621
646
|
let pass = 1;
|
|
622
647
|
do {
|
|
@@ -641,6 +666,8 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
641
666
|
errors.delete(name);
|
|
642
667
|
progress = true;
|
|
643
668
|
const createdRes = created.resource;
|
|
669
|
+
const refs = collectResourceRefs(createdRes);
|
|
670
|
+
this.resourceDependencies.set(name, localDependencyNames(refs));
|
|
644
671
|
const payload: Record<string, unknown> = {
|
|
645
672
|
resource: {
|
|
646
673
|
kind: createdRes.kind,
|
|
@@ -649,7 +676,7 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
649
676
|
id: this.resourceId(createdRes.kind, createdRes.metadata.name),
|
|
650
677
|
},
|
|
651
678
|
...(this.owner ? { owner: this.owner } : {}),
|
|
652
|
-
dependencies: this.qualifyDeps(
|
|
679
|
+
dependencies: this.qualifyDeps(refs),
|
|
653
680
|
};
|
|
654
681
|
// `properties` (the resolved config) is a second full config walk plus
|
|
655
682
|
// a secret scrub. Build it lazily: the EventBus short-circuits when
|
|
@@ -682,6 +709,7 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
682
709
|
? this.resolveImportedInstance(alias, n)
|
|
683
710
|
: this.resourceInstances.get(n)?.instance,
|
|
684
711
|
(n) => this.hasManifest(n) && !this.resourceInstances.has(n),
|
|
712
|
+
this,
|
|
685
713
|
);
|
|
686
714
|
}
|
|
687
715
|
if (instance.init) await instance.init(ctx);
|
|
@@ -694,6 +722,9 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
694
722
|
await this.publishSnapshot(name);
|
|
695
723
|
this.resourceInstances.set(name, { resource, instance });
|
|
696
724
|
this.createdInstances.delete(name);
|
|
725
|
+
// Read only on failure, and this one succeeded — drop it rather than
|
|
726
|
+
// holding a dep-name array per resource for the context's lifetime.
|
|
727
|
+
this.resourceDependencies.delete(name);
|
|
697
728
|
errors.delete(name);
|
|
698
729
|
progress = true;
|
|
699
730
|
await this.emit(`${resource.kind}.${resource.metadata.name}.Initialized`, {
|
|
@@ -715,38 +746,33 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
715
746
|
} while (pass <= MAX_PASSES);
|
|
716
747
|
|
|
717
748
|
if (this.pendingResources.length > 0 || this.createdInstances.size > 0) {
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
749
|
+
const toFailure = (name: string, kind: string, deps: string[]): FailedResource => {
|
|
750
|
+
const info = errors.get(name) ?? { message: "Unknown error" };
|
|
751
|
+
return {
|
|
752
|
+
resource: name,
|
|
753
|
+
kind,
|
|
754
|
+
message: info.message,
|
|
755
|
+
details: info.details,
|
|
756
|
+
code: info.code,
|
|
757
|
+
children: info.children,
|
|
758
|
+
deps,
|
|
759
|
+
};
|
|
760
|
+
};
|
|
761
|
+
const failures: FailedResource[] = [
|
|
762
|
+
// A resource that never got created was never injected either, so its
|
|
763
|
+
// manifest still carries plain `{kind, name}` refs — walk it here rather
|
|
764
|
+
// than relying on the create-time capture it never reached.
|
|
765
|
+
...this.pendingResources.map((r) =>
|
|
766
|
+
toFailure(r.metadata.name, r.kind, localDependencyNames(collectResourceRefs(r))),
|
|
767
|
+
),
|
|
768
|
+
...[...this.createdInstances].map(([name, { resource }]) =>
|
|
769
|
+
toFailure(name, resource.kind, this.resourceDependencies.get(name) ?? []),
|
|
770
|
+
),
|
|
739
771
|
];
|
|
740
|
-
const
|
|
741
|
-
.map((d) => {
|
|
742
|
-
const head = ` ${d.kind ? `${d.kind} ` : ""}${d.resource}: ${d.message}${d.code ? ` [${d.code}]` : ""}`;
|
|
743
|
-
const extra = d.details ? "\n" + d.details.split("\n").map((l) => ` ${l}`).join("\n") : "";
|
|
744
|
-
return head + extra;
|
|
745
|
-
})
|
|
746
|
-
.join("\n");
|
|
772
|
+
const diagnostics = classifyInitFailures(failures);
|
|
747
773
|
throw new RuntimeError(
|
|
748
774
|
"ERR_RESOURCE_INITIALIZATION_FAILED",
|
|
749
|
-
|
|
775
|
+
`${summarizeInitFailures(diagnostics)}:\n${renderInitFailureText(diagnostics)}`,
|
|
750
776
|
diagnostics,
|
|
751
777
|
);
|
|
752
778
|
}
|
|
@@ -815,7 +841,7 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
815
841
|
// Propagate injection hook: extend getInstance to also resolve parent singleton instances.
|
|
816
842
|
if (parent.preInitHook) {
|
|
817
843
|
const parentHook = parent.preInitHook;
|
|
818
|
-
child.preInitHook = (resource, childGetInstance, childIsPending) => {
|
|
844
|
+
child.preInitHook = (resource, childGetInstance, childIsPending, owner) => {
|
|
819
845
|
parentHook(
|
|
820
846
|
resource,
|
|
821
847
|
(name, alias) => {
|
|
@@ -836,6 +862,9 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
836
862
|
// Only a scope-local dependency can still be pending — an outer resource
|
|
837
863
|
// is live by the time a scope opens — so the child's own predicate suffices.
|
|
838
864
|
childIsPending,
|
|
865
|
+
// Forwarded, not replaced by `child`: a `with:` nested inside a scoped
|
|
866
|
+
// resource must still resolve kinds against the scope it opened in.
|
|
867
|
+
owner,
|
|
839
868
|
);
|
|
840
869
|
};
|
|
841
870
|
}
|
|
@@ -1761,11 +1790,31 @@ function formatErrorForDiagnostic(err: unknown): {
|
|
|
1761
1790
|
message: string;
|
|
1762
1791
|
code?: string;
|
|
1763
1792
|
details?: string;
|
|
1793
|
+
children?: RuntimeDiagnostic[];
|
|
1764
1794
|
} {
|
|
1765
1795
|
if (!(err instanceof Error)) {
|
|
1766
1796
|
return { message: String(err) };
|
|
1767
1797
|
}
|
|
1768
1798
|
|
|
1799
|
+
// A nested context's aggregate (an import initializing its library's
|
|
1800
|
+
// resources) already carries a classified diagnostic list. Keep it structured
|
|
1801
|
+
// instead of letting the cause-chain walk flatten it into this entry's
|
|
1802
|
+
// message: the child's root causes stay distinguishable from the child's own
|
|
1803
|
+
// cascade, and the error count sees the real leaves rather than one import.
|
|
1804
|
+
// The headline is re-derived from the diagnostics, never recovered by parsing
|
|
1805
|
+
// the message the child already rendered from them.
|
|
1806
|
+
if (
|
|
1807
|
+
err instanceof RuntimeError &&
|
|
1808
|
+
err.code === "ERR_RESOURCE_INITIALIZATION_FAILED" &&
|
|
1809
|
+
err.diagnostics?.length
|
|
1810
|
+
) {
|
|
1811
|
+
return {
|
|
1812
|
+
message: summarizeInitFailures(err.diagnostics),
|
|
1813
|
+
code: err.code,
|
|
1814
|
+
children: err.diagnostics,
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1769
1818
|
const detailLines: string[] = [];
|
|
1770
1819
|
const seen = new Set<unknown>();
|
|
1771
1820
|
let current: unknown = err;
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ export {
|
|
|
32
32
|
moduleDirectoryFor,
|
|
33
33
|
hostPlatformTarget,
|
|
34
34
|
type MaterializedLayer,
|
|
35
|
+
type ResolvedControllerLayer,
|
|
35
36
|
} from "./bundle/module-artifact.js";
|
|
36
37
|
export { readOwnerManifest, type OwnerManifest } from "./bundle/module-manifest.js";
|
|
37
38
|
export type {
|
|
@@ -46,7 +47,8 @@ export { nodeCelHandlers } from "./cel-handlers.js";
|
|
|
46
47
|
export { ModuleContext } from "./module-context.js";
|
|
47
48
|
export { ManifestRegistry as Registry } from "./registry.js";
|
|
48
49
|
export { ResourceURI } from "./resource-uri.js";
|
|
49
|
-
export type { RuntimeDiagnostic } from "@telorun/sdk";
|
|
50
|
+
export type { DiagnosticOrigin, RuntimeDiagnostic } from "@telorun/sdk";
|
|
51
|
+
export { describeBlockedGroup, groupBlockedResources } from "./init-failure-diagnostics.js";
|
|
50
52
|
|
|
51
53
|
// Structured logging — the runtime half of kernel/specs/logging.md. The record
|
|
52
54
|
// model, severity scale, and `Logger` surface live in `@telorun/sdk`; these are
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { RuntimeDiagnostic } from "@telorun/sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Codes that mean "this resource never got its turn": the multi-pass init loop
|
|
5
|
+
* deferred it because a dependency had not initialized, so it never produced a
|
|
6
|
+
* failure of its own. This is the ONLY signal that an entry may be collapsed —
|
|
7
|
+
* see {@link classifyInitFailures}.
|
|
8
|
+
*/
|
|
9
|
+
const DEPENDENCY_PENDING_CODES = new Set([
|
|
10
|
+
"ERR_LOCAL_REF_PENDING",
|
|
11
|
+
"ERR_CROSS_MODULE_REF_PENDING",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
/** One resource that did not reach the `Initialized` state, with the outbound
|
|
15
|
+
* edges (names of resources in the SAME context) captured for it. */
|
|
16
|
+
export interface FailedResource {
|
|
17
|
+
resource: string;
|
|
18
|
+
kind?: string;
|
|
19
|
+
message: string;
|
|
20
|
+
details?: string;
|
|
21
|
+
code?: string;
|
|
22
|
+
children?: RuntimeDiagnostic[];
|
|
23
|
+
deps: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Split a failed-resource set into the ones that failed on their own (root
|
|
28
|
+
* causes) and the ones that only failed because something else in the set did.
|
|
29
|
+
*
|
|
30
|
+
* A dependency chain of any length produces one real error and N shadows of it,
|
|
31
|
+
* and the shadows outnumber the cause — reporting them flat buries the only
|
|
32
|
+
* line a reader can act on.
|
|
33
|
+
*
|
|
34
|
+
* **What makes an entry derived is its CODE, never its edges.** Only a
|
|
35
|
+
* {@link DEPENDENCY_PENDING_CODES} deferral says "this resource never ran, so
|
|
36
|
+
* it has nothing of its own to report". A reference edge into the failure set
|
|
37
|
+
* proves an edge exists, not that this entry's failure came from it: a resource
|
|
38
|
+
* can reference a failed dependency AND fail its own schema validation, and
|
|
39
|
+
* collapsing it there would swallow a real, independent error the author has to
|
|
40
|
+
* fix — the second half of a two-error session they would only discover on the
|
|
41
|
+
* next run. (Nor is the edge trustworthy on its own terms: `collectResourceRefs`
|
|
42
|
+
* walks `with:`-scoped inline declarations, whose names resolve scope-locally,
|
|
43
|
+
* so a scoped `!ref Db` can collide with a failed module-level `Db`.)
|
|
44
|
+
*
|
|
45
|
+
* Edges are used for ATTRIBUTION only — they name which failure a deferred
|
|
46
|
+
* entry is waiting on. `blockedBy` is the ROOT of the chain, not the immediate
|
|
47
|
+
* blocker, since that is the name a reader has to go fix; the walk stops at the
|
|
48
|
+
* first entry that is not itself derived. A deferral with no visible edge (a
|
|
49
|
+
* `${{ resources.X }}` read the ref walk cannot see) is still derived, just
|
|
50
|
+
* unattributed.
|
|
51
|
+
*
|
|
52
|
+
* Classification never hides everything: if no entry survives as a root (every
|
|
53
|
+
* failure is a deferral), the whole set is reported unclassified.
|
|
54
|
+
*
|
|
55
|
+
* Returns diagnostics ordered root causes first, then the derived entries.
|
|
56
|
+
*/
|
|
57
|
+
export function classifyInitFailures(failures: FailedResource[]): RuntimeDiagnostic[] {
|
|
58
|
+
const failed = new Set(failures.map((f) => f.resource));
|
|
59
|
+
|
|
60
|
+
const derived = new Set(
|
|
61
|
+
failures.filter((f) => f.code && DEPENDENCY_PENDING_CODES.has(f.code)).map((f) => f.resource),
|
|
62
|
+
);
|
|
63
|
+
// Attribution edges are collected for EVERY entry, derived or not: a chain
|
|
64
|
+
// walk has to pass through an entry to reach the root beyond it.
|
|
65
|
+
const edgeBlocker = new Map<string, string>();
|
|
66
|
+
for (const f of failures) {
|
|
67
|
+
const dep = f.deps.find((n) => n !== f.resource && failed.has(n));
|
|
68
|
+
if (dep !== undefined) edgeBlocker.set(f.resource, dep);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rootCauseOf = (name: string): string | undefined => {
|
|
72
|
+
const seen = new Set<string>([name]);
|
|
73
|
+
let current = edgeBlocker.get(name);
|
|
74
|
+
while (current !== undefined && !seen.has(current)) {
|
|
75
|
+
if (!derived.has(current)) return current;
|
|
76
|
+
seen.add(current);
|
|
77
|
+
current = edgeBlocker.get(current);
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const toDiagnostic = (f: FailedResource, isDerived: boolean): RuntimeDiagnostic => {
|
|
83
|
+
const blockedBy = isDerived ? rootCauseOf(f.resource) : undefined;
|
|
84
|
+
return {
|
|
85
|
+
resource: f.resource,
|
|
86
|
+
kind: f.kind,
|
|
87
|
+
message: f.message,
|
|
88
|
+
details: f.details,
|
|
89
|
+
code: f.code,
|
|
90
|
+
...(f.children?.length ? { children: f.children } : {}),
|
|
91
|
+
...(isDerived ? { derived: true, ...(blockedBy ? { blockedBy } : {}) } : {}),
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const roots = failures.filter((f) => !derived.has(f.resource));
|
|
96
|
+
if (roots.length === 0) return failures.map((f) => toDiagnostic(f, false));
|
|
97
|
+
|
|
98
|
+
return [
|
|
99
|
+
...roots.map((f) => toDiagnostic(f, false)),
|
|
100
|
+
...failures.filter((f) => derived.has(f.resource)).map((f) => toDiagnostic(f, true)),
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Group the derived entries by the root cause they hang off, so a renderer can
|
|
105
|
+
* collapse each chain to a single line instead of repeating one failure N
|
|
106
|
+
* times. Entries whose blocker could not be named group under `undefined`. */
|
|
107
|
+
export function groupBlockedResources(
|
|
108
|
+
diagnostics: RuntimeDiagnostic[],
|
|
109
|
+
): Map<string | undefined, string[]> {
|
|
110
|
+
const groups = new Map<string | undefined, string[]>();
|
|
111
|
+
for (const d of diagnostics) {
|
|
112
|
+
if (!d.derived) continue;
|
|
113
|
+
const key = d.blockedBy;
|
|
114
|
+
const names = groups.get(key) ?? [];
|
|
115
|
+
names.push(d.resource ?? "(unnamed)");
|
|
116
|
+
groups.set(key, names);
|
|
117
|
+
}
|
|
118
|
+
return groups;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** One collapsed line per blocked chain, e.g.
|
|
122
|
+
* `9 resources blocked by GrantDb: GrantStore, GoogleTokens, ...`. */
|
|
123
|
+
export function describeBlockedGroup(blockedBy: string | undefined, names: string[]): string {
|
|
124
|
+
const subject = `${names.length} resource${names.length !== 1 ? "s" : ""}`;
|
|
125
|
+
const blocker = blockedBy ?? "an uninitialized dependency";
|
|
126
|
+
return `${subject} blocked by ${blocker}: ${names.join(", ")}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The headline for a classified failure set. Shared by the aggregate error's
|
|
130
|
+
* own message and by the entry an importing context builds for it, so the two
|
|
131
|
+
* are never recovered by re-parsing each other's rendered text. */
|
|
132
|
+
export function summarizeInitFailures(diagnostics: RuntimeDiagnostic[]): string {
|
|
133
|
+
const total = diagnostics.length;
|
|
134
|
+
const roots = diagnostics.filter((d) => !d.derived).length;
|
|
135
|
+
const blocked = total - roots;
|
|
136
|
+
return (
|
|
137
|
+
`${total} resource${total !== 1 ? "s" : ""} failed to initialize` +
|
|
138
|
+
(blocked > 0 ? ` (${roots} root cause${roots !== 1 ? "s" : ""}, rest blocked)` : "")
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Render a classified failure set as the text body of the aggregate error
|
|
143
|
+
* message — root causes in full, each blocked chain collapsed to one line.
|
|
144
|
+
* Nested children recurse through this same function so a child list is
|
|
145
|
+
* traversed exactly once, groups included. */
|
|
146
|
+
export function renderInitFailureText(diagnostics: RuntimeDiagnostic[]): string {
|
|
147
|
+
const lines: string[] = [];
|
|
148
|
+
for (const d of diagnostics) {
|
|
149
|
+
if (!d.derived) {
|
|
150
|
+
lines.push(
|
|
151
|
+
` ${d.kind ? `${d.kind} ` : ""}${d.resource}: ${d.message}${d.code ? ` [${d.code}]` : ""}`,
|
|
152
|
+
);
|
|
153
|
+
if (d.details) lines.push(...d.details.split("\n").map((l) => ` ${l}`));
|
|
154
|
+
}
|
|
155
|
+
// A derived entry contributes no line of its own, but a nested context's
|
|
156
|
+
// root causes are not shadows of THIS context's failure — they still report.
|
|
157
|
+
if (d.children?.length) {
|
|
158
|
+
lines.push(
|
|
159
|
+
...renderInitFailureText(d.children)
|
|
160
|
+
.split("\n")
|
|
161
|
+
.map((l) => ` ${l}`),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const [blockedBy, names] of groupBlockedResources(diagnostics)) {
|
|
166
|
+
lines.push(` ${describeBlockedGroup(blockedBy, names)}`);
|
|
167
|
+
}
|
|
168
|
+
return lines.join("\n");
|
|
169
|
+
}
|