@telorun/analyzer 0.62.1 → 0.64.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/analysis-registry.d.ts +24 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +41 -0
- package/dist/analyzer.d.ts +3 -37
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +396 -476
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +2 -7
- package/dist/cel-scope-query.d.ts +109 -0
- package/dist/cel-scope-query.d.ts.map +1 -0
- package/dist/cel-scope-query.js +270 -0
- package/dist/cel-scope.d.ts +166 -0
- package/dist/cel-scope.d.ts.map +1 -0
- package/dist/cel-scope.js +377 -0
- package/dist/definition-registry.d.ts +15 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +25 -9
- package/dist/find-manifest.d.ts +10 -0
- package/dist/find-manifest.d.ts.map +1 -0
- package/dist/find-manifest.js +12 -0
- package/dist/flatten-for-analyzer.js +1 -1
- package/dist/import-resolution-diagnostics.d.ts +9 -2
- package/dist/import-resolution-diagnostics.d.ts.map +1 -1
- package/dist/import-resolution-diagnostics.js +26 -7
- package/dist/index.d.ts +22 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -1
- package/dist/invocation-contract.d.ts +17 -1
- package/dist/invocation-contract.d.ts.map +1 -1
- package/dist/invocation-contract.js +42 -4
- package/dist/loaded-types.d.ts +12 -3
- package/dist/loaded-types.d.ts.map +1 -1
- package/dist/manifest-analysis.d.ts +73 -0
- package/dist/manifest-analysis.d.ts.map +1 -0
- package/dist/manifest-analysis.js +78 -0
- package/dist/manifest-loader.d.ts +0 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +62 -41
- package/dist/manifest-path.d.ts +18 -0
- package/dist/manifest-path.d.ts.map +1 -0
- package/dist/manifest-path.js +37 -0
- package/dist/manifest-schemas.d.ts +699 -0
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +306 -14
- package/dist/module-compatibility.d.ts +40 -0
- package/dist/module-compatibility.d.ts.map +1 -0
- package/dist/module-compatibility.js +52 -0
- package/dist/ref-slot-reading.d.ts +30 -0
- package/dist/ref-slot-reading.d.ts.map +1 -0
- package/dist/ref-slot-reading.js +139 -0
- package/dist/referrer-rule.d.ts +67 -0
- package/dist/referrer-rule.d.ts.map +1 -0
- package/dist/referrer-rule.js +96 -0
- package/dist/release/index.d.ts +1 -1
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +1 -1
- package/dist/release/version-stamp.d.ts +16 -0
- package/dist/release/version-stamp.d.ts.map +1 -1
- package/dist/release/version-stamp.js +52 -0
- package/dist/release/workspace-config.d.ts +10 -6
- package/dist/release/workspace-config.d.ts.map +1 -1
- package/dist/release/workspace-config.js +10 -6
- package/dist/resolve-schema-ref-kinds.d.ts +4 -3
- package/dist/resolve-schema-ref-kinds.d.ts.map +1 -1
- package/dist/resolve-schema-ref-kinds.js +16 -0
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +4 -3
- package/dist/resolve-zone-containment.d.ts +99 -0
- package/dist/resolve-zone-containment.d.ts.map +1 -0
- package/dist/resolve-zone-containment.js +212 -0
- package/dist/resolve-zone-requirements.d.ts +5 -0
- package/dist/resolve-zone-requirements.d.ts.map +1 -1
- package/dist/resolve-zone-requirements.js +48 -7
- package/dist/resource-rule.d.ts +106 -0
- package/dist/resource-rule.d.ts.map +1 -0
- package/dist/resource-rule.js +236 -0
- package/dist/rule-condition.d.ts +50 -0
- package/dist/rule-condition.d.ts.map +1 -0
- package/dist/rule-condition.js +108 -0
- package/dist/schema-keywords.d.ts.map +1 -1
- package/dist/schema-keywords.js +20 -0
- package/dist/schema-projection.d.ts +171 -0
- package/dist/schema-projection.d.ts.map +1 -0
- package/dist/schema-projection.js +317 -0
- package/dist/schema-walk.d.ts +25 -0
- package/dist/schema-walk.d.ts.map +1 -0
- package/dist/schema-walk.js +126 -0
- package/dist/step-slot.d.ts +39 -0
- package/dist/step-slot.d.ts.map +1 -0
- package/dist/step-slot.js +49 -0
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +17 -3
- package/dist/validate-durable-regions.d.ts +15 -0
- package/dist/validate-durable-regions.d.ts.map +1 -0
- package/dist/validate-durable-regions.js +336 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +44 -2
- package/dist/validate-referrer-rules.d.ts +120 -0
- package/dist/validate-referrer-rules.d.ts.map +1 -0
- package/dist/validate-referrer-rules.js +255 -0
- package/dist/validate-resource-rules.d.ts +114 -0
- package/dist/validate-resource-rules.d.ts.map +1 -0
- package/dist/validate-resource-rules.js +283 -0
- package/dist/validate-schema-projection.d.ts +32 -0
- package/dist/validate-schema-projection.d.ts.map +1 -0
- package/dist/validate-schema-projection.js +110 -0
- package/dist/validate-step-inputs.d.ts +19 -1
- package/dist/validate-step-inputs.d.ts.map +1 -1
- package/dist/validate-step-inputs.js +102 -9
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +11 -5
- package/dist/validate-zone-slots.d.ts +2 -2
- package/dist/validate-zone-slots.d.ts.map +1 -1
- package/dist/validate-zone-slots.js +202 -6
- package/dist/validate-zone-violations.d.ts +12 -0
- package/dist/validate-zone-violations.d.ts.map +1 -0
- package/dist/validate-zone-violations.js +169 -0
- package/dist/value-type-keyword.d.ts +1 -1
- package/dist/value-type-keyword.d.ts.map +1 -1
- package/dist/value-type-keyword.js +4 -0
- package/dist/zone-slot.d.ts +61 -0
- package/dist/zone-slot.d.ts.map +1 -1
- package/dist/zone-slot.js +86 -5
- package/package.json +2 -2
- package/src/analysis-registry.ts +43 -0
- package/src/analyzer.ts +452 -595
- package/src/call-graph.ts +2 -8
- package/src/cel-scope-query.ts +337 -0
- package/src/cel-scope.ts +570 -0
- package/src/definition-registry.ts +31 -13
- package/src/find-manifest.ts +19 -0
- package/src/flatten-for-analyzer.ts +1 -1
- package/src/import-resolution-diagnostics.ts +25 -7
- package/src/index.ts +86 -0
- package/src/invocation-contract.ts +67 -2
- package/src/loaded-types.ts +12 -3
- package/src/manifest-analysis.ts +132 -0
- package/src/manifest-loader.ts +87 -55
- package/src/manifest-path.ts +34 -0
- package/src/manifest-schemas.ts +328 -14
- package/src/module-compatibility.ts +72 -0
- package/src/ref-slot-reading.ts +156 -0
- package/src/referrer-rule.ts +116 -0
- package/src/release/index.ts +1 -0
- package/src/release/version-stamp.ts +58 -0
- package/src/release/workspace-config.ts +10 -6
- package/src/resolve-schema-ref-kinds.ts +28 -4
- package/src/resolve-throws-union.ts +4 -3
- package/src/resolve-zone-containment.ts +329 -0
- package/src/resolve-zone-requirements.ts +67 -7
- package/src/resource-rule.ts +252 -0
- package/src/rule-condition.ts +126 -0
- package/src/schema-keywords.ts +24 -0
- package/src/schema-projection.ts +403 -0
- package/src/schema-walk.ts +144 -0
- package/src/step-slot.ts +64 -0
- package/src/telo-version.ts +1 -1
- package/src/validate-cel-context.ts +17 -3
- package/src/validate-durable-regions.ts +401 -0
- package/src/validate-references.ts +57 -2
- package/src/validate-referrer-rules.ts +373 -0
- package/src/validate-resource-rules.ts +401 -0
- package/src/validate-schema-projection.ts +184 -0
- package/src/validate-step-inputs.ts +148 -10
- package/src/validate-throws-coverage.ts +12 -5
- package/src/validate-zone-slots.ts +192 -8
- package/src/validate-zone-violations.ts +190 -0
- package/src/value-type-keyword.ts +4 -0
- package/src/zone-slot.ts +114 -5
|
@@ -20,25 +20,43 @@ function isRecognizedSourceShape(source: string): boolean {
|
|
|
20
20
|
);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/** Which of the three failures this is. Kept as one function so the code and
|
|
24
|
+
* the sentence can never disagree about which cause was established. */
|
|
25
|
+
function classify(e: GraphLoadError): "INVALID_IMPORT_TARGET" | "INVALID_IMPORT_SOURCE" | "IMPORT_UNRESOLVED" {
|
|
26
|
+
if (e.reason === "unusable-target") return "INVALID_IMPORT_TARGET";
|
|
27
|
+
return isRecognizedSourceShape(e.source ?? e.url) ? "IMPORT_UNRESOLVED" : "INVALID_IMPORT_SOURCE";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function messageFor(e: GraphLoadError, code: ReturnType<typeof classify>): string {
|
|
24
31
|
const authored = e.source ?? e.url;
|
|
25
32
|
const via = e.alias ? `import '${e.alias}' → '${authored}'` : `'${authored}'`;
|
|
26
|
-
if (
|
|
33
|
+
if (code === "INVALID_IMPORT_SOURCE") {
|
|
27
34
|
return (
|
|
28
35
|
`Cannot resolve ${via}: not a recognized module reference. Expected ` +
|
|
29
36
|
`'namespace/name@version', 'oci://host/repo@tag', 'https://…', or a relative path.`
|
|
30
37
|
);
|
|
31
38
|
}
|
|
39
|
+
// The target WAS obtained, so "cannot resolve" would name the wrong problem —
|
|
40
|
+
// and the wrong person to fix it. The loader's message already says what was
|
|
41
|
+
// fetched and why it is not importable.
|
|
42
|
+
if (code === "INVALID_IMPORT_TARGET") return `Cannot use ${via}: ${e.error.message}`;
|
|
32
43
|
return `Cannot resolve ${via}: ${e.error.message}`;
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
/**
|
|
36
|
-
* Convert a graph's import
|
|
37
|
-
*
|
|
47
|
+
* Convert a graph's import failures (`graph.errors`) into structured, coded
|
|
48
|
+
* diagnostics. This is the single source of truth for surfacing a broken
|
|
38
49
|
* import — every host (CLI, VS Code, telo-editor) routes these instead of each
|
|
39
50
|
* re-deriving the channel and drifting (the VS Code extension used to drop it
|
|
40
51
|
* entirely, showing nothing for a broken import).
|
|
41
52
|
*
|
|
53
|
+
* Three codes, because three different people fix them:
|
|
54
|
+
* `INVALID_IMPORT_SOURCE` (the ref is not a module reference at all),
|
|
55
|
+
* `IMPORT_UNRESOLVED` (a well-formed ref that could not be obtained), and
|
|
56
|
+
* `INVALID_IMPORT_TARGET` (obtained, but not an importable library — an
|
|
57
|
+
* application, no library document, or one that names no module). Collapsing
|
|
58
|
+
* the last into the second would tell an author to fix a ref that is correct.
|
|
59
|
+
*
|
|
42
60
|
* The analyzer owns only this raw channel conversion; the *presentation* policy
|
|
43
61
|
* — which analysis cascade to hold back for a compromised file — lives in
|
|
44
62
|
* `@telorun/ide-support`'s `assembleGraphDiagnostics`.
|
|
@@ -51,15 +69,15 @@ function messageFor(e: GraphLoadError, malformed: boolean): string {
|
|
|
51
69
|
export function importResolutionDiagnostics(graph: LoadedGraph): AnalysisDiagnostic[] {
|
|
52
70
|
return graph.errors.map((e) => {
|
|
53
71
|
const filePath = e.fromSource ?? graph.entry.owner.source;
|
|
54
|
-
const
|
|
72
|
+
const code = classify(e);
|
|
55
73
|
const data: { filePath: string; path?: string; sourceLine?: number } = { filePath };
|
|
56
74
|
if (e.alias) data.path = `imports.${e.alias}`;
|
|
57
75
|
if (e.sourceLine !== undefined) data.sourceLine = e.sourceLine;
|
|
58
76
|
return {
|
|
59
77
|
severity: DiagnosticSeverity.Error,
|
|
60
|
-
code
|
|
78
|
+
code,
|
|
61
79
|
source: SOURCE,
|
|
62
|
-
message: messageFor(e,
|
|
80
|
+
message: messageFor(e, code),
|
|
63
81
|
data,
|
|
64
82
|
};
|
|
65
83
|
});
|
package/src/index.ts
CHANGED
|
@@ -110,6 +110,8 @@ export {
|
|
|
110
110
|
transfersControl,
|
|
111
111
|
} from "./ref-slot.js";
|
|
112
112
|
export type { RefSlot, RefUse, RefUseCases } from "./ref-slot.js";
|
|
113
|
+
export { isStepSlot, readStepSlot, STEP_FRAGMENT } from "./step-slot.js";
|
|
114
|
+
export type { StepSlot } from "./step-slot.js";
|
|
113
115
|
export {
|
|
114
116
|
ANNOTATION_KEYWORDS,
|
|
115
117
|
registerTeloKeywords,
|
|
@@ -131,6 +133,8 @@ export type {
|
|
|
131
133
|
RequiresIssue,
|
|
132
134
|
RequiresVerdict,
|
|
133
135
|
} from "./requires-block.js";
|
|
136
|
+
export { manifestCompatibility } from "./module-compatibility.js";
|
|
137
|
+
export type { ModuleCompatibility } from "./module-compatibility.js";
|
|
134
138
|
export { TELO_SURFACE_VERSION } from "./telo-version.js";
|
|
135
139
|
export { validateRequires } from "./validate-requires.js";
|
|
136
140
|
export type { ValidateRequiresOptions } from "./validate-requires.js";
|
|
@@ -150,8 +154,10 @@ export type {
|
|
|
150
154
|
export {
|
|
151
155
|
hasProvidesZone,
|
|
152
156
|
hasRequiresZone,
|
|
157
|
+
providedZoneAttributes,
|
|
153
158
|
readProvidesZone,
|
|
154
159
|
readRequiresZone,
|
|
160
|
+
readViolatesZone,
|
|
155
161
|
rewriteRequiresZoneKind,
|
|
156
162
|
} from "./zone-slot.js";
|
|
157
163
|
export type { ProvidesZoneSlot, RequiresZoneSlot } from "./zone-slot.js";
|
|
@@ -169,6 +175,73 @@ export type {
|
|
|
169
175
|
} from "./resolve-zone-requirements.js";
|
|
170
176
|
export type { ZoneModuleDocuments } from "./zone-module-documents.js";
|
|
171
177
|
export { validateZoneSlotDeclarations } from "./validate-zone-slots.js";
|
|
178
|
+
export {
|
|
179
|
+
RESOURCE_RULES_ANNOTATION,
|
|
180
|
+
readResourceRules,
|
|
181
|
+
readRawResourceRules,
|
|
182
|
+
resolveRuleSubjects,
|
|
183
|
+
findDynamicLeaf,
|
|
184
|
+
type ResourceRule,
|
|
185
|
+
type ResourceRuleSeverity,
|
|
186
|
+
type RuleSubject,
|
|
187
|
+
} from "./resource-rule.js";
|
|
188
|
+
export {
|
|
189
|
+
validateResourceRuleDeclarations,
|
|
190
|
+
evaluateResourceRules,
|
|
191
|
+
ruleExercised,
|
|
192
|
+
RESOURCE_RULE_BUDGET_MS,
|
|
193
|
+
type ResourceRuleIssue,
|
|
194
|
+
type ResourceRuleFinding,
|
|
195
|
+
} from "./validate-resource-rules.js";
|
|
196
|
+
export {
|
|
197
|
+
REFERRER_RULES_ANNOTATION,
|
|
198
|
+
readReferrerRules,
|
|
199
|
+
readRawReferrerRules,
|
|
200
|
+
hasReferrerRules,
|
|
201
|
+
rewriteReferrerRuleKinds,
|
|
202
|
+
type ReferrerRule,
|
|
203
|
+
} from "./referrer-rule.js";
|
|
204
|
+
export {
|
|
205
|
+
validateReferrerRuleDeclarations,
|
|
206
|
+
evaluateReferrerRules,
|
|
207
|
+
referrerRuleExercised,
|
|
208
|
+
reportReferrerRules,
|
|
209
|
+
reportUnexercisedReferrerRule,
|
|
210
|
+
type Referrer,
|
|
211
|
+
type ReferrerRuleIssue,
|
|
212
|
+
type ReferrerRuleFinding,
|
|
213
|
+
type ReferrerRuleDiagnostic,
|
|
214
|
+
} from "./validate-referrer-rules.js";
|
|
215
|
+
export { RULE_BUDGET_MS } from "./rule-condition.js";
|
|
216
|
+
export {
|
|
217
|
+
readSchemaProjection,
|
|
218
|
+
readSchemaMap,
|
|
219
|
+
readProjectionFrom,
|
|
220
|
+
readProjectionRef,
|
|
221
|
+
manifestListScope,
|
|
222
|
+
projectionKeyMap,
|
|
223
|
+
projectEntries,
|
|
224
|
+
resolveSchemaProjections,
|
|
225
|
+
describeProjectionFailure,
|
|
226
|
+
type SchemaProjection,
|
|
227
|
+
type SchemaMap,
|
|
228
|
+
type ProjectionRef,
|
|
229
|
+
type ProjectionLookup,
|
|
230
|
+
type ProjectionScope,
|
|
231
|
+
type ProjectionFailure,
|
|
232
|
+
} from "./schema-projection.js";
|
|
233
|
+
export { validateSchemaProjection, type SchemaProjectionIssue } from "./validate-schema-projection.js";
|
|
234
|
+
export { validateDurableRegions } from "./validate-durable-regions.js";
|
|
235
|
+
export { validateZoneViolations } from "./validate-zone-violations.js";
|
|
236
|
+
export {
|
|
237
|
+
containmentIndex,
|
|
238
|
+
findZoneRegions,
|
|
239
|
+
regionManifests,
|
|
240
|
+
type ContainedNode,
|
|
241
|
+
type DefinitionLookup,
|
|
242
|
+
type RegionBoundary,
|
|
243
|
+
type ZoneRegion,
|
|
244
|
+
} from "./resolve-zone-containment.js";
|
|
172
245
|
export type { ZoneSlotIssue } from "./validate-zone-slots.js";
|
|
173
246
|
export { validateDynamicSelectors, validateRefSlotDeclarations } from "./validate-ref-slots.js";
|
|
174
247
|
export type { RefSlotIssue } from "./validate-ref-slots.js";
|
|
@@ -297,6 +370,19 @@ export { documentToAst, parseToAst } from "./yaml-ast.js";
|
|
|
297
370
|
export type { AstDocument, AstMap, AstNode, AstPair, AstScalar, AstSeq } from "./yaml-ast.js";
|
|
298
371
|
export { CelParseError, buildCelSegments, wrapCelAst } from "./cel-ast.js";
|
|
299
372
|
export type { CelNode, CelSegment } from "./cel-ast.js";
|
|
373
|
+
|
|
374
|
+
// The CEL scope rule, and the way into it from outside the analysis pass. What
|
|
375
|
+
// completes, what hovers and what type-checks are one answer because they are
|
|
376
|
+
// one function.
|
|
377
|
+
export { CelScopeResolver } from "./cel-scope.js";
|
|
378
|
+
export type { CelScope, CelScopeInputs, CelSiteRef } from "./cel-scope.js";
|
|
379
|
+
export { CelScopeQuery } from "./cel-scope-query.js";
|
|
380
|
+
export type { CelScopeQueryContext, ContextDeclarationSite } from "./cel-scope-query.js";
|
|
381
|
+
// The pairing of a registry and the manifests it analyzed — the one seam a host
|
|
382
|
+
// threads for every question that needs both.
|
|
383
|
+
export { navigateConcretePath } from "./manifest-path.js";
|
|
384
|
+
export { ManifestAnalysis } from "./manifest-analysis.js";
|
|
385
|
+
export type { ManifestRef } from "./manifest-analysis.js";
|
|
300
386
|
export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity, diagnosticFix } from "./types.js";
|
|
301
387
|
export type {
|
|
302
388
|
AnalysisDiagnostic,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { isLiveSlot, type ResourceDefinition, valueTypeOf } from "@telorun/sdk";
|
|
2
|
+
import { AliasResolver, moduleScopedDefResolver, type ModuleScopes } from "./alias-resolver.js";
|
|
3
|
+
import { DefinitionRegistry } from "./definition-registry.js";
|
|
2
4
|
import {
|
|
3
5
|
type ContractDirection,
|
|
4
6
|
contractDeclarer,
|
|
@@ -6,9 +8,34 @@ import {
|
|
|
6
8
|
effectiveContractField,
|
|
7
9
|
} from "./extends-resolution.js";
|
|
8
10
|
import { resolveTypeFieldToSchema } from "./validate-cel-context.js";
|
|
11
|
+
import {
|
|
12
|
+
manifestListScope,
|
|
13
|
+
resolveSchemaProjections,
|
|
14
|
+
type ProjectionFailure,
|
|
15
|
+
} from "./schema-projection.js";
|
|
9
16
|
|
|
10
17
|
export type { ContractDirection };
|
|
11
18
|
|
|
19
|
+
/** The {@link ContractScope} the analyzer resolves invocation contracts in: kinds
|
|
20
|
+
* resolve in the module that declared the definition they were read off (so an
|
|
21
|
+
* `extends` chain crossing module boundaries re-scopes at every hop), and named
|
|
22
|
+
* `telo#Type` references resolve against the flattened manifest list. `resolveIn`
|
|
23
|
+
* is the top-level entry point, where the kind was written by the READING
|
|
24
|
+
* module and there is no declaring definition yet. */
|
|
25
|
+
export function analyzerContractScope(
|
|
26
|
+
defs: DefinitionRegistry,
|
|
27
|
+
aliases: AliasResolver,
|
|
28
|
+
scopes: ModuleScopes,
|
|
29
|
+
allManifests: Record<string, any>[],
|
|
30
|
+
): ContractScope & { resolveIn(kind: string, module?: string): ResourceDefinition | undefined } {
|
|
31
|
+
const resolve = moduleScopedDefResolver<ResourceDefinition>(defs, aliases, scopes);
|
|
32
|
+
return {
|
|
33
|
+
resolveDefinition: resolve,
|
|
34
|
+
resolveIn: resolve.in,
|
|
35
|
+
typeManifestsFor: () => allManifests,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
12
39
|
/**
|
|
13
40
|
* The one answer to "what is this target's input / output schema".
|
|
14
41
|
*
|
|
@@ -80,11 +107,20 @@ export function resolveContract(
|
|
|
80
107
|
manifest: Record<string, any> | undefined,
|
|
81
108
|
definition: ResourceDefinition | undefined,
|
|
82
109
|
scope: ContractScope,
|
|
110
|
+
/** Collects declaration-derived slots that could not be projected. Omitted by
|
|
111
|
+
* every caller that only wants the schema; the one pass that REPORTS them
|
|
112
|
+
* passes an array. */
|
|
113
|
+
projectionFailures?: ProjectionFailure[],
|
|
83
114
|
): ResolvedContract | undefined {
|
|
84
115
|
const own = manifest?.[direction];
|
|
85
116
|
if (own !== undefined && own !== null) {
|
|
86
117
|
const schema = resolveTypeFieldToSchema(own, scope.typeManifestsFor(definition));
|
|
87
|
-
if (schema)
|
|
118
|
+
if (schema) {
|
|
119
|
+
return {
|
|
120
|
+
schema: projectionResolved(schema, manifest, scope, definition, projectionFailures),
|
|
121
|
+
origin: "instance",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
88
124
|
}
|
|
89
125
|
|
|
90
126
|
const declared = effectiveContractField(definition, scope.resolveDefinition, direction);
|
|
@@ -92,7 +128,36 @@ export function resolveContract(
|
|
|
92
128
|
const declarer = contractDeclarer(definition, scope.resolveDefinition, direction);
|
|
93
129
|
const schema = resolveTypeFieldToSchema(declared, scope.typeManifestsFor(declarer));
|
|
94
130
|
if (!schema) return undefined;
|
|
95
|
-
return {
|
|
131
|
+
return {
|
|
132
|
+
schema: projectionResolved(schema, manifest, scope, declarer, projectionFailures),
|
|
133
|
+
origin: "kind",
|
|
134
|
+
declaredBy: declarer,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A contract may be DECLARATION-derived: a kind whose `outputType` projects the
|
|
140
|
+
* declaration of a resource it references (a repository typing its rows from
|
|
141
|
+
* the table it reads). The manifest is in hand here and nowhere later, so this
|
|
142
|
+
* is where the projection is resolved — and it is resolved for a kind-declared
|
|
143
|
+
* contract and an instance-declared one alike, since either may carry one.
|
|
144
|
+
*/
|
|
145
|
+
function projectionResolved(
|
|
146
|
+
schema: Record<string, any>,
|
|
147
|
+
manifest: Record<string, any> | undefined,
|
|
148
|
+
scope: ContractScope,
|
|
149
|
+
declarer: ResourceDefinition | undefined,
|
|
150
|
+
failures?: ProjectionFailure[],
|
|
151
|
+
): Record<string, any> {
|
|
152
|
+
return resolveSchemaProjections(
|
|
153
|
+
schema,
|
|
154
|
+
manifest,
|
|
155
|
+
manifestListScope(
|
|
156
|
+
scope.typeManifestsFor(declarer),
|
|
157
|
+
(kind) => scope.resolveDefinition(kind) as Record<string, any> | undefined,
|
|
158
|
+
),
|
|
159
|
+
failures,
|
|
160
|
+
) as Record<string, any>;
|
|
96
161
|
}
|
|
97
162
|
|
|
98
163
|
/** {@link resolveContract}, falling back to {@link PERMISSIVE_CONTRACT}. For
|
package/src/loaded-types.ts
CHANGED
|
@@ -70,9 +70,11 @@ export interface ImportEdge {
|
|
|
70
70
|
* this (minus its version), since it names the module's location
|
|
71
71
|
* independently of what the module declares about itself. */
|
|
72
72
|
targetRef: string;
|
|
73
|
-
/** Target library's `metadata.name
|
|
74
|
-
*
|
|
75
|
-
|
|
73
|
+
/** Target library's `metadata.name`. Never absent: an import whose target
|
|
74
|
+
* names no library identity registers no edge at all and is reported in
|
|
75
|
+
* `LoadedGraph.errors`, so an edge that exists is one every consumer can
|
|
76
|
+
* resolve a kind through. */
|
|
77
|
+
targetModuleName: string;
|
|
76
78
|
/** DEPRECATED. Target library's `metadata.namespace`, or `null` when it
|
|
77
79
|
* declares none. Feeds only the legacy `<namespace>/<module>#<Kind>` form
|
|
78
80
|
* of `x-telo-ref`; nothing else reads it. */
|
|
@@ -137,5 +139,12 @@ export interface GraphLoadError {
|
|
|
137
139
|
alias?: string;
|
|
138
140
|
/** Line of the `Telo.Import` doc in `fromSource`, for position fallback. */
|
|
139
141
|
sourceLine?: number;
|
|
142
|
+
/** Why this import is unusable. `"unresolved"` (the default) means the target
|
|
143
|
+
* was never obtained — an unreachable or unrecognizable source, which the
|
|
144
|
+
* author fixes in their own file. `"unusable-target"` means it WAS obtained
|
|
145
|
+
* and is not an importable library, which is a defect in the target module.
|
|
146
|
+
* Kept apart because the two call for different actions; consumers phrase
|
|
147
|
+
* them differently rather than asserting one cause for both. */
|
|
148
|
+
reason?: "unresolved" | "unusable-target";
|
|
140
149
|
error: Error;
|
|
141
150
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **One analyzed manifest set, and the questions asked of it.**
|
|
3
|
+
*
|
|
4
|
+
* Several answers a host needs require the SAME two things: the registry's
|
|
5
|
+
* definitions and aliases, and the manifest set they were resolved against. The
|
|
6
|
+
* registry deliberately holds no manifests — it is populated per analysis and
|
|
7
|
+
* reused across them — so each such answer would otherwise become another
|
|
8
|
+
* factory on the registry and another optional parameter on every IDE entry
|
|
9
|
+
* point. Four of those arrived in short order (CEL scope, step declarations,
|
|
10
|
+
* context-binding declarations, invocation contracts) and the next one is not
|
|
11
|
+
* hypothetical.
|
|
12
|
+
*
|
|
13
|
+
* So the pairing is named once and the questions hang off it. A host threads ONE
|
|
14
|
+
* object and gains later questions for free; each facet keeps its own honest
|
|
15
|
+
* name rather than accreting onto whichever one happened to exist first.
|
|
16
|
+
*
|
|
17
|
+
* Nothing here re-implements an answer. `contractFor` is the shared
|
|
18
|
+
* {@link resolveContract} — the one `telo check` runs and the kernel binds at
|
|
19
|
+
* dispatch — given the scope to run in; `celScope` is the same
|
|
20
|
+
* {@link CelScopeQuery} the analysis pass's rule is built from. That is the
|
|
21
|
+
* whole point: a completion list is a claim about what the checker accepts, and
|
|
22
|
+
* a second implementation of any of these could not be held to it.
|
|
23
|
+
*/
|
|
24
|
+
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
25
|
+
import { AliasResolver, type ModuleScopes } from "./alias-resolver.js";
|
|
26
|
+
import { CelScopeQuery, type CelScopeQueryContext } from "./cel-scope-query.js";
|
|
27
|
+
import { DefinitionRegistry } from "./definition-registry.js";
|
|
28
|
+
import type { ContractDirection } from "./extends-resolution.js";
|
|
29
|
+
import { analyzerContractScope, resolveContract } from "./invocation-contract.js";
|
|
30
|
+
import { findManifest } from "./find-manifest.js";
|
|
31
|
+
import { isModuleKind } from "./module-kinds.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A reference as the loader leaves it — the internal `{kind, name, alias?}`
|
|
35
|
+
* shape `resolveRefSentinels` rewrites `!ref` into.
|
|
36
|
+
*/
|
|
37
|
+
export interface ManifestRef {
|
|
38
|
+
kind?: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
alias?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class ManifestAnalysis {
|
|
44
|
+
private readonly scopes: ModuleScopes;
|
|
45
|
+
private celScopeQuery: CelScopeQuery | undefined;
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
readonly manifests: ResourceManifest[],
|
|
49
|
+
private readonly ctx: CelScopeQueryContext,
|
|
50
|
+
) {
|
|
51
|
+
const rootModules = new Set<string>();
|
|
52
|
+
for (const m of manifests) {
|
|
53
|
+
if (isModuleKind(m.kind) && m.metadata?.name) rootModules.add(m.metadata.name as string);
|
|
54
|
+
}
|
|
55
|
+
this.scopes = { aliasesByModule: ctx.aliasesByModule, rootModules };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** What CEL sees, per site. Built on first use — its indices are a function of
|
|
59
|
+
* the whole set, and a host that never opens a CEL body should not pay for
|
|
60
|
+
* them. */
|
|
61
|
+
get celScope(): CelScopeQuery {
|
|
62
|
+
return (this.celScopeQuery ??= new CelScopeQuery(this.manifests, this.ctx));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The manifest a `(kind, name)` pair addresses. */
|
|
66
|
+
resourceFor(kind: string | undefined, name: string | undefined): ResourceManifest | undefined {
|
|
67
|
+
return findManifest(this.manifests, kind, name);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The invocation contract of the resource a reference names.
|
|
72
|
+
*
|
|
73
|
+
* The shared resolver, so an editor offering a target's input keys is offering
|
|
74
|
+
* exactly what `telo check` validates that call site against and what the
|
|
75
|
+
* kernel binds at dispatch. Layered instance-first: a resource declaring its
|
|
76
|
+
* own `inputType:` narrows the kind's, which is the common case for a
|
|
77
|
+
* `Run.Sequence` used as a handler.
|
|
78
|
+
*/
|
|
79
|
+
contractFor(ref: ManifestRef, direction: ContractDirection): Record<string, any> | undefined {
|
|
80
|
+
const target = this.resolveRef(ref);
|
|
81
|
+
const definition = ref.kind ? this.definitionFor(ref.kind) : undefined;
|
|
82
|
+
if (!target && !definition) return undefined;
|
|
83
|
+
return resolveContract(
|
|
84
|
+
direction,
|
|
85
|
+
target as Record<string, any> | undefined,
|
|
86
|
+
definition,
|
|
87
|
+
analyzerContractScope(
|
|
88
|
+
this.ctx.defs,
|
|
89
|
+
this.ctx.aliases,
|
|
90
|
+
this.scopes,
|
|
91
|
+
this.manifests as Record<string, any>[],
|
|
92
|
+
),
|
|
93
|
+
)?.schema;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The manifest a reference names.
|
|
98
|
+
*
|
|
99
|
+
* An ALIAS narrows before the name does: a flattened set carries every
|
|
100
|
+
* imported library's exported instances, so two libraries exporting a `store`
|
|
101
|
+
* are two manifests with one name. Matching the alias to its target module
|
|
102
|
+
* picks the right one; where the alias resolves to nothing the name alone is
|
|
103
|
+
* used, which is what a local reference needs anyway.
|
|
104
|
+
*/
|
|
105
|
+
private resolveRef(ref: ManifestRef): ResourceManifest | undefined {
|
|
106
|
+
if (!ref.name) return undefined;
|
|
107
|
+
const byName = this.manifests.filter(
|
|
108
|
+
(m) => (m.metadata as { name?: string } | undefined)?.name === ref.name,
|
|
109
|
+
);
|
|
110
|
+
if (byName.length === 0) return undefined;
|
|
111
|
+
if (byName.length === 1) return byName[0];
|
|
112
|
+
|
|
113
|
+
const targetModule = ref.alias ? this.ctx.aliases.moduleForAlias?.(ref.alias) : undefined;
|
|
114
|
+
if (targetModule) {
|
|
115
|
+
const scoped = byName.find(
|
|
116
|
+
(m) => (m.metadata as { module?: string } | undefined)?.module === targetModule,
|
|
117
|
+
);
|
|
118
|
+
if (scoped) return scoped;
|
|
119
|
+
}
|
|
120
|
+
// Several candidates and nothing to choose between them: refusing is the
|
|
121
|
+
// honest answer, since typing a call site against the wrong resource's
|
|
122
|
+
// contract is worse than typing it against none.
|
|
123
|
+
return ref.kind ? byName.find((m) => m.kind === ref.kind) : undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private definitionFor(kind: string): ResourceDefinition | undefined {
|
|
127
|
+
const canonical = this.ctx.aliases.resolveKind(kind);
|
|
128
|
+
return this.ctx.defs.resolve(kind) ?? (canonical ? this.ctx.defs.resolve(canonical) : undefined);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type { CelScopeQueryContext, AliasResolver, DefinitionRegistry };
|
package/src/manifest-loader.ts
CHANGED
|
@@ -57,6 +57,62 @@ function collectMigrationDiagnostics(entry: LoadedModule): AnalysisDiagnostic[]
|
|
|
57
57
|
return diagnostics;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** The identity an import target contributes to its importer, or the reason it
|
|
61
|
+
* contributes none.
|
|
62
|
+
*
|
|
63
|
+
* Every way an import can be unusable answers here, in one place and without
|
|
64
|
+
* throwing: the target is an application, it carries no library document at
|
|
65
|
+
* all, or it carries one that never names itself. A name is not decoration —
|
|
66
|
+
* it is the kind prefix every `Alias.Kind` in the importer resolves through —
|
|
67
|
+
* so a target that has none is as unusable as one that was never fetched, and
|
|
68
|
+
* is reported the same way rather than left to surface later as an invented
|
|
69
|
+
* module nobody declared. */
|
|
70
|
+
type ImportTargetIdentity =
|
|
71
|
+
| { name: string; namespace: string | null }
|
|
72
|
+
| { unusable: string };
|
|
73
|
+
|
|
74
|
+
function importTargetIdentity(target: LoadedModule, importSource: string): ImportTargetIdentity {
|
|
75
|
+
const library = target.owner.manifests.find((m) => m?.kind === "Telo.Library");
|
|
76
|
+
const application = target.owner.manifests.find((m) => m?.kind === "Telo.Application");
|
|
77
|
+
|
|
78
|
+
if (!library && application) {
|
|
79
|
+
return {
|
|
80
|
+
unusable:
|
|
81
|
+
`Telo.Import target '${importSource}' is a Telo.Application. ` +
|
|
82
|
+
`Only Telo.Library modules may be imported. Applications are run directly, not imported. ` +
|
|
83
|
+
`Point this import at a library, or drop it and run that application on its own.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!library) {
|
|
88
|
+
const kinds = target.owner.manifests
|
|
89
|
+
.map((m) => m?.kind)
|
|
90
|
+
.filter((k): k is string => typeof k === "string");
|
|
91
|
+
const detail = kinds.length
|
|
92
|
+
? `Fetched ${target.owner.manifests.length} document(s) with kinds [${kinds.join(", ")}].`
|
|
93
|
+
: `Fetched manifest contained no recognizable Telo documents — check that the source ` +
|
|
94
|
+
`serves a Telo.Library manifest and not an upstream error page.`;
|
|
95
|
+
return {
|
|
96
|
+
unusable:
|
|
97
|
+
`Telo.Import target '${importSource}' did not resolve to a Telo.Library. ` +
|
|
98
|
+
`Fetched from: ${target.owner.source}. ${detail}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const name = library.metadata?.name;
|
|
103
|
+
if (typeof name !== "string" || name === "") {
|
|
104
|
+
return {
|
|
105
|
+
unusable:
|
|
106
|
+
`Telo.Import target '${importSource}' resolved to a Telo.Library that declares no ` +
|
|
107
|
+
`'metadata.name'. Fetched from: ${target.owner.source}. A library's name is the kind ` +
|
|
108
|
+
`prefix its importers reference, so it cannot be imported until the target declares one.`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const namespace = (library.metadata as { namespace?: string | null } | undefined)?.namespace;
|
|
113
|
+
return { name, namespace: typeof namespace === "string" ? namespace : null };
|
|
114
|
+
}
|
|
115
|
+
|
|
60
116
|
const SYSTEM_KINDS = new Set([
|
|
61
117
|
"Telo.Application",
|
|
62
118
|
"Telo.Library",
|
|
@@ -320,10 +376,10 @@ export class Loader {
|
|
|
320
376
|
// Resolve the file we'll fetch through the source chain to get the
|
|
321
377
|
// canonical `source` URL — same identity used as the modules-map key.
|
|
322
378
|
let targetCanonical: string;
|
|
323
|
-
let targetModule: LoadedModule
|
|
379
|
+
let targetModule: LoadedModule;
|
|
324
380
|
if (modules.has(resolvedTarget)) {
|
|
325
381
|
targetCanonical = resolvedTarget;
|
|
326
|
-
targetModule = modules.get(resolvedTarget)
|
|
382
|
+
targetModule = modules.get(resolvedTarget)!;
|
|
327
383
|
} else {
|
|
328
384
|
try {
|
|
329
385
|
const loaded = await this.loadModule(resolvedTarget, options);
|
|
@@ -332,7 +388,7 @@ export class Loader {
|
|
|
332
388
|
modules.set(targetCanonical, loaded);
|
|
333
389
|
targetModule = loaded;
|
|
334
390
|
} else {
|
|
335
|
-
targetModule = modules.get(targetCanonical)
|
|
391
|
+
targetModule = modules.get(targetCanonical)!;
|
|
336
392
|
}
|
|
337
393
|
} catch (err) {
|
|
338
394
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
@@ -349,32 +405,42 @@ export class Loader {
|
|
|
349
405
|
}
|
|
350
406
|
}
|
|
351
407
|
|
|
352
|
-
// Resolve target identity from its Telo.Library doc and stamp it
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
408
|
+
// Resolve target identity from its Telo.Library doc and stamp it on
|
|
409
|
+
// the edge — flattenForAnalyzer reads from the edge directly, never
|
|
410
|
+
// re-deriving from manifest.metadata.
|
|
411
|
+
//
|
|
412
|
+
// Checked per IMPORT rather than per distinct target: an import
|
|
413
|
+
// pointing at a module something else already reached (the entry
|
|
414
|
+
// application above all) would otherwise skip the check entirely and
|
|
415
|
+
// register a nameless edge. An unusable target is recorded exactly
|
|
416
|
+
// like an unfetchable one — an error against this import, no edge,
|
|
417
|
+
// and the rest of the graph still loads — so no consumer has to know
|
|
418
|
+
// the difference between the two ways an import can fail.
|
|
419
|
+
const identity = importTargetIdentity(targetModule, importSource);
|
|
420
|
+
if ("unusable" in identity) {
|
|
421
|
+
const e = new Error(identity.unusable);
|
|
422
|
+
(e as { sourceLine?: number }).sourceLine = sourceLine;
|
|
423
|
+
errors.push({
|
|
424
|
+
url: targetCanonical,
|
|
425
|
+
source: importSource,
|
|
426
|
+
fromSource: file.source,
|
|
427
|
+
alias,
|
|
428
|
+
sourceLine,
|
|
429
|
+
reason: "unusable-target",
|
|
430
|
+
error: e,
|
|
431
|
+
});
|
|
432
|
+
continue;
|
|
366
433
|
}
|
|
367
434
|
|
|
368
435
|
aliases.set(alias, {
|
|
369
436
|
targetSource: targetCanonical,
|
|
370
437
|
targetRef: importSource,
|
|
371
|
-
targetModuleName,
|
|
372
|
-
targetNamespace,
|
|
438
|
+
targetModuleName: identity.name,
|
|
439
|
+
targetNamespace: identity.namespace,
|
|
373
440
|
});
|
|
374
441
|
|
|
375
|
-
if (
|
|
442
|
+
if (!visited.has(targetCanonical)) {
|
|
376
443
|
visited.add(targetCanonical);
|
|
377
|
-
this.assertImportTargetIsLibrary(targetModule, importSource, sourceLine);
|
|
378
444
|
queue.push(targetModule);
|
|
379
445
|
}
|
|
380
446
|
}
|
|
@@ -444,40 +510,6 @@ export class Loader {
|
|
|
444
510
|
}
|
|
445
511
|
}
|
|
446
512
|
|
|
447
|
-
private assertImportTargetIsLibrary(
|
|
448
|
-
target: LoadedModule,
|
|
449
|
-
importSource: string,
|
|
450
|
-
sourceLine: number,
|
|
451
|
-
): void {
|
|
452
|
-
const importedLibrary = target.owner.manifests.find((m) => m?.kind === "Telo.Library");
|
|
453
|
-
const importedApplication = target.owner.manifests.find(
|
|
454
|
-
(m) => m?.kind === "Telo.Application",
|
|
455
|
-
);
|
|
456
|
-
if (importedApplication) {
|
|
457
|
-
const e = new Error(
|
|
458
|
-
`Telo.Import target '${importSource}' is a Telo.Application. ` +
|
|
459
|
-
`Only Telo.Library modules may be imported. Applications are run directly, not imported.`,
|
|
460
|
-
);
|
|
461
|
-
(e as { sourceLine?: number }).sourceLine = sourceLine;
|
|
462
|
-
throw e;
|
|
463
|
-
}
|
|
464
|
-
if (!importedLibrary) {
|
|
465
|
-
const kinds = target.owner.manifests
|
|
466
|
-
.map((m) => m?.kind)
|
|
467
|
-
.filter((k): k is string => typeof k === "string");
|
|
468
|
-
const detail = kinds.length
|
|
469
|
-
? `Fetched ${target.owner.manifests.length} document(s) with kinds [${kinds.join(", ")}].`
|
|
470
|
-
: `Fetched manifest contained no recognizable Telo documents — check that the source ` +
|
|
471
|
-
`serves a Telo.Library manifest and not an upstream error page.`;
|
|
472
|
-
const e = new Error(
|
|
473
|
-
`Telo.Import target '${importSource}' did not resolve to a Telo.Library. ` +
|
|
474
|
-
`Fetched from: ${target.owner.source}. ${detail}`,
|
|
475
|
-
);
|
|
476
|
-
(e as { sourceLine?: number }).sourceLine = sourceLine;
|
|
477
|
-
throw e;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
513
|
private async resolveIncludes(
|
|
482
514
|
ownerSource: string,
|
|
483
515
|
patterns: string[],
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigating a MANIFEST by a concrete path (`routes[0].request.schema.query`).
|
|
3
|
+
*
|
|
4
|
+
* The counterpart to `schema-walk.ts`, which navigates a schema: this addresses
|
|
5
|
+
* the author's own document, indices and all. Its own module because three
|
|
6
|
+
* places needed it independently — the CEL scope query resolving a context
|
|
7
|
+
* binding's declaration, the call-site checker resolving an argument map, and
|
|
8
|
+
* the IDE resolving the same map for completion — and three copies of one
|
|
9
|
+
* traversal is exactly what the shared-answer rule exists to prevent.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The value at `path`, or `undefined` when any segment is absent.
|
|
14
|
+
*
|
|
15
|
+
* Absence is what makes a candidate path a CHECK rather than a guess: a caller
|
|
16
|
+
* offering several possible shapes can try each and know a hit is a real node.
|
|
17
|
+
*/
|
|
18
|
+
export function navigateConcretePath(root: Record<string, any>, path: string): unknown {
|
|
19
|
+
let current: unknown = root;
|
|
20
|
+
for (const segment of path.split(".")) {
|
|
21
|
+
if (!segment) continue;
|
|
22
|
+
const match = segment.match(/^([^[]*)((?:\[\d+\])*)$/);
|
|
23
|
+
if (!match) return undefined;
|
|
24
|
+
if (match[1]) {
|
|
25
|
+
if (current === null || typeof current !== "object") return undefined;
|
|
26
|
+
current = (current as Record<string, unknown>)[match[1]];
|
|
27
|
+
}
|
|
28
|
+
for (const index of match[2].matchAll(/\[(\d+)\]/g)) {
|
|
29
|
+
if (!Array.isArray(current)) return undefined;
|
|
30
|
+
current = current[Number(index[1])];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return current;
|
|
34
|
+
}
|