@telorun/analyzer 0.57.0 → 0.58.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/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +91 -9
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +6 -3
- package/dist/definition-registry.d.ts +5 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +9 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/invocation-contract.d.ts +21 -13
- package/dist/invocation-contract.d.ts.map +1 -1
- package/dist/invocation-contract.js +43 -46
- package/dist/loaded-types.d.ts +13 -0
- package/dist/loaded-types.d.ts.map +1 -1
- package/dist/manifest-loader.d.ts +8 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +33 -6
- package/dist/migrations/driver.d.ts +69 -0
- package/dist/migrations/driver.d.ts.map +1 -0
- package/dist/migrations/driver.js +200 -0
- package/dist/migrations/entries/index.d.ts +3 -0
- package/dist/migrations/entries/index.d.ts.map +1 -0
- package/dist/migrations/entries/index.js +9 -0
- package/dist/migrations/entries/normalize-value-types.json +48 -0
- package/dist/migrations/entries/ref-slot-scalar-type.json +20 -0
- package/dist/migrations/entry-data.d.ts +25 -0
- package/dist/migrations/entry-data.d.ts.map +1 -0
- package/dist/migrations/entry-data.js +171 -0
- package/dist/migrations/index.d.ts +22 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +19 -0
- package/dist/migrations/match.d.ts +162 -0
- package/dist/migrations/match.d.ts.map +1 -0
- package/dist/migrations/match.js +295 -0
- package/dist/migrations/patch.d.ts +68 -0
- package/dist/migrations/patch.d.ts.map +1 -0
- package/dist/migrations/patch.js +236 -0
- package/dist/migrations/provenance.d.ts +25 -0
- package/dist/migrations/provenance.d.ts.map +1 -0
- package/dist/migrations/provenance.js +118 -0
- package/dist/migrations/registry.d.ts +35 -0
- package/dist/migrations/registry.d.ts.map +1 -0
- package/dist/migrations/registry.js +35 -0
- package/dist/migrations/report.d.ts +28 -0
- package/dist/migrations/report.d.ts.map +1 -0
- package/dist/migrations/report.js +144 -0
- package/dist/migrations/types.d.ts +115 -0
- package/dist/migrations/types.d.ts.map +1 -0
- package/dist/migrations/types.js +24 -0
- package/dist/migrations/yaml-edit.d.ts +21 -0
- package/dist/migrations/yaml-edit.d.ts.map +1 -0
- package/dist/migrations/yaml-edit.js +245 -0
- package/dist/parse-loaded-file.d.ts +8 -0
- package/dist/parse-loaded-file.d.ts.map +1 -1
- package/dist/parse-loaded-file.js +22 -13
- package/dist/resolve-schema-type-refs.d.ts +29 -12
- package/dist/resolve-schema-type-refs.d.ts.map +1 -1
- package/dist/resolve-schema-type-refs.js +89 -16
- package/dist/schema-compat.d.ts +35 -9
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +180 -64
- package/dist/schema-region.d.ts +33 -0
- package/dist/schema-region.d.ts.map +1 -0
- package/dist/schema-region.js +45 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +29 -3
- package/dist/validate-step-inputs.d.ts +8 -1
- package/dist/validate-step-inputs.d.ts.map +1 -1
- package/dist/validate-step-inputs.js +47 -2
- package/dist/validate-value-type-slots.d.ts +43 -0
- package/dist/validate-value-type-slots.d.ts.map +1 -0
- package/dist/validate-value-type-slots.js +142 -0
- package/dist/value-type-keyword.d.ts +60 -0
- package/dist/value-type-keyword.d.ts.map +1 -0
- package/dist/value-type-keyword.js +119 -0
- package/dist/yaml-source-edit.d.ts +56 -0
- package/dist/yaml-source-edit.d.ts.map +1 -0
- package/dist/yaml-source-edit.js +93 -0
- package/package.json +3 -3
- package/src/analyzer.ts +99 -6
- package/src/cel-environment.ts +6 -3
- package/src/definition-registry.ts +10 -0
- package/src/index.ts +26 -2
- package/src/invocation-contract.ts +43 -48
- package/src/loaded-types.ts +13 -0
- package/src/manifest-loader.ts +37 -6
- package/src/migrations/driver.ts +262 -0
- package/src/migrations/entries/index.ts +10 -0
- package/src/migrations/entries/normalize-value-types.json +48 -0
- package/src/migrations/entries/ref-slot-scalar-type.json +20 -0
- package/src/migrations/entry-data.ts +207 -0
- package/src/migrations/index.ts +22 -0
- package/src/migrations/match.ts +382 -0
- package/src/migrations/patch.ts +265 -0
- package/src/migrations/provenance.ts +167 -0
- package/src/migrations/registry.ts +40 -0
- package/src/migrations/report.ts +169 -0
- package/src/migrations/types.ts +144 -0
- package/src/migrations/yaml-edit.ts +260 -0
- package/src/parse-loaded-file.ts +31 -12
- package/src/resolve-schema-type-refs.ts +93 -16
- package/src/schema-compat.ts +212 -74
- package/src/schema-region.ts +46 -0
- package/src/types.ts +17 -0
- package/src/validate-cel-context.ts +36 -3
- package/src/validate-step-inputs.ts +56 -1
- package/src/validate-value-type-slots.ts +182 -0
- package/src/value-type-keyword.ts +135 -0
- package/src/yaml-source-edit.ts +106 -0
- package/dist/binary-slot.d.ts +0 -39
- package/dist/binary-slot.d.ts.map +0 -1
- package/dist/binary-slot.js +0 -58
- package/src/binary-slot.ts +0 -71
package/src/analyzer.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
defaultRegistry,
|
|
6
6
|
isRefSentinel,
|
|
7
7
|
isTaggedSentinel,
|
|
8
|
+
plainChainOf,
|
|
8
9
|
type CelSurface,
|
|
9
10
|
} from "@telorun/templating";
|
|
10
11
|
import type { DiagnosticFix } from "./types.js";
|
|
@@ -56,11 +57,17 @@ import {
|
|
|
56
57
|
validateRefSlotDeclarations,
|
|
57
58
|
type RefSlotIssue,
|
|
58
59
|
} from "./validate-ref-slots.js";
|
|
60
|
+
import {
|
|
61
|
+
validateValueTypeSlots,
|
|
62
|
+
type ValueTypeSlotIssue,
|
|
63
|
+
} from "./validate-value-type-slots.js";
|
|
59
64
|
import { resolveSchemaTypeRefs } from "./resolve-schema-type-refs.js";
|
|
60
65
|
import { validateSchemaTypeRefs } from "./validate-schema-type-refs.js";
|
|
61
66
|
import { rewriteSyntheticOrigins } from "./rewrite-synthetic-origins.js";
|
|
62
67
|
import {
|
|
63
68
|
celTypeSatisfiesJsonSchema,
|
|
69
|
+
checkSchemaCompatibility,
|
|
70
|
+
navigateSchemaToExprPath,
|
|
64
71
|
substituteCelFields,
|
|
65
72
|
validateAgainstSchema,
|
|
66
73
|
type SchemaIssue,
|
|
@@ -1201,6 +1208,18 @@ export class StaticAnalyzer {
|
|
|
1201
1208
|
const refConstraintIssues: RefConstraintIssue[] = [];
|
|
1202
1209
|
const refSlotIssues: RefSlotIssue[] = [];
|
|
1203
1210
|
const zoneSlotIssues: ZoneSlotIssue[] = [];
|
|
1211
|
+
// `x-telo-type` is checked on EVERY manifest, not only on definition docs: a
|
|
1212
|
+
// schema fragment is written wherever a kind declares a schema-valued field,
|
|
1213
|
+
// so an inline `inputType:` on an ordinary resource carries one just as a
|
|
1214
|
+
// definition's `schema:` does. Same scoping as every other schema issue —
|
|
1215
|
+
// the entry's own modules, since a dependency is not the consumer's to fix.
|
|
1216
|
+
const valueTypeSlotIssues: ValueTypeSlotIssue[] = [];
|
|
1217
|
+
for (const m of manifests) {
|
|
1218
|
+
const declaringModule = (m.metadata as { module?: string } | undefined)?.module;
|
|
1219
|
+
if (!declaringModule || rootModules.has(declaringModule)) {
|
|
1220
|
+
valueTypeSlotIssues.push(...validateValueTypeSlots(m as unknown as ResourceManifest));
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1204
1223
|
for (const m of manifests) {
|
|
1205
1224
|
if (m.kind !== "Telo.Definition" && m.kind !== "Telo.Abstract") continue;
|
|
1206
1225
|
const def = m as unknown as ResourceDefinition;
|
|
@@ -1340,6 +1359,26 @@ export class StaticAnalyzer {
|
|
|
1340
1359
|
// accessor split; `readRefSlot` stays lenient so surfaces keep working
|
|
1341
1360
|
// mid-migration, and this reports what leniency would silently absorb.
|
|
1342
1361
|
for (const issue of refSlotIssues) diagnostics.push(refSlotIssueDiagnostic(issue));
|
|
1362
|
+
// The same split for `x-telo-type`. Its reader returns a slot with no
|
|
1363
|
+
// entry for a name it does not know, which is what an unrecognized brand
|
|
1364
|
+
// used to do SILENTLY — the slot simply lost its identity.
|
|
1365
|
+
for (const issue of valueTypeSlotIssues) {
|
|
1366
|
+
diagnostics.push({
|
|
1367
|
+
severity: DiagnosticSeverity.Error,
|
|
1368
|
+
code: issue.code,
|
|
1369
|
+
source: SOURCE,
|
|
1370
|
+
message: issue.message,
|
|
1371
|
+
data: {
|
|
1372
|
+
resource: {
|
|
1373
|
+
kind: issue.manifest.kind,
|
|
1374
|
+
name: issue.manifest.metadata?.name as string,
|
|
1375
|
+
},
|
|
1376
|
+
filePath: (issue.manifest.metadata as { source?: string } | undefined)?.source,
|
|
1377
|
+
path: issue.path,
|
|
1378
|
+
...(issue.fix ? { fix: issue.fix } : {}),
|
|
1379
|
+
},
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1343
1382
|
// Same split for the two zone annotations. Unreadable ones fail in
|
|
1344
1383
|
// OPPOSITE directions — a dropped requirement is silently unenforced, a
|
|
1345
1384
|
// dropped provision invents failures — so neither can be left to
|
|
@@ -1625,6 +1664,11 @@ export class StaticAnalyzer {
|
|
|
1625
1664
|
filePath?: string;
|
|
1626
1665
|
})[] = [];
|
|
1627
1666
|
const celTypeByPath = new Map<ResourceManifest, Map<string, string>>();
|
|
1667
|
+
// The schema an expression RESOLVES TO, beside the CEL type it carries. Both
|
|
1668
|
+
// are needed and neither replaces the other: the CEL type answers "does this
|
|
1669
|
+
// fit the slot at all", the schema answers "do their type arguments agree",
|
|
1670
|
+
// which cel-js cannot express because it types by constructor identity.
|
|
1671
|
+
const celSourceSchemaByPath = new Map<ResourceManifest, Map<string, Record<string, any>>>();
|
|
1628
1672
|
// Context-free typed environments, one per manifest. Reused across every
|
|
1629
1673
|
// expression in it — see the build site for why a matched context opts out.
|
|
1630
1674
|
const typedEnvByManifest = new Map<ResourceManifest, Environment>();
|
|
@@ -1962,12 +2006,15 @@ export class StaticAnalyzer {
|
|
|
1962
2006
|
defs,
|
|
1963
2007
|
aliases,
|
|
1964
2008
|
{ aliasesByModule, rootModules },
|
|
2009
|
+
celStepContextSchema,
|
|
1965
2010
|
)) {
|
|
1966
2011
|
diagnostics.push({
|
|
1967
2012
|
severity: DiagnosticSeverity.Error,
|
|
1968
|
-
code: "CONTRACT_INPUTS_MISMATCH",
|
|
2013
|
+
code: issue.code ?? "CONTRACT_INPUTS_MISMATCH",
|
|
1969
2014
|
source: SOURCE,
|
|
1970
|
-
message:
|
|
2015
|
+
message: issue.code
|
|
2016
|
+
? `${m.kind}/${stepName}: inputs at '${issue.path}' flow into ${issue.targetLabel} with disagreeing type arguments: ${issue.message}`
|
|
2017
|
+
: `${m.kind}/${stepName}: inputs at '${issue.path}' do not satisfy ${issue.targetLabel}'s declared inputType: ${issue.message}`,
|
|
1971
2018
|
data: {
|
|
1972
2019
|
resource: { kind: m.kind, name: stepName ?? "" },
|
|
1973
2020
|
filePath: stepFile,
|
|
@@ -2260,6 +2307,30 @@ export class StaticAnalyzer {
|
|
|
2260
2307
|
byPath.set(path, result.type);
|
|
2261
2308
|
}
|
|
2262
2309
|
|
|
2310
|
+
// The producer half of the type-argument check. A CEL type is a bare
|
|
2311
|
+
// name — cel-js types by constructor identity, so a byte stream and a
|
|
2312
|
+
// string stream are one CEL type and always will be — so the argument
|
|
2313
|
+
// comparison is a parallel pass over the SCHEMAS the analyzer already
|
|
2314
|
+
// walks. This is where the producer's schema is in hand: navigating
|
|
2315
|
+
// the expression's chain into its context schema is exactly "the
|
|
2316
|
+
// producer's outputType at the expression's tail".
|
|
2317
|
+
//
|
|
2318
|
+
// Only a PLAIN CHAIN is navigated — `steps.encode.result.output`, the
|
|
2319
|
+
// shape a wiring site actually takes. An expression that computes
|
|
2320
|
+
// rather than names has no schema to read off the context, so it
|
|
2321
|
+
// records nothing and no argument check fires: silence where the
|
|
2322
|
+
// analyzer knows least is the conservative direction, and the same one
|
|
2323
|
+
// `x-telo-step-context`'s pure-`value` typing takes.
|
|
2324
|
+
const chain = effectiveContext ? plainChainOf(`\${{${expr}}}`) : undefined;
|
|
2325
|
+
if (chain) {
|
|
2326
|
+
const produced = navigateSchemaToExprPath(effectiveContext!, chain);
|
|
2327
|
+
if (produced) {
|
|
2328
|
+
let byPath = celSourceSchemaByPath.get(m);
|
|
2329
|
+
if (!byPath) celSourceSchemaByPath.set(m, (byPath = new Map()));
|
|
2330
|
+
byPath.set(path, produced);
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2263
2334
|
// A non-deterministic call in a compile-eval field is baked once at
|
|
2264
2335
|
// load: `nowIso()` there freezes at boot. Sometimes that is the
|
|
2265
2336
|
// intent (a boot timestamp, a run id), so it warns rather than
|
|
@@ -2331,13 +2402,35 @@ export class StaticAnalyzer {
|
|
|
2331
2402
|
for (const slot of celReturnSlots) {
|
|
2332
2403
|
const type = celTypeByPath.get(slot.manifest)?.get(slot.path);
|
|
2333
2404
|
if (type === undefined) continue;
|
|
2334
|
-
if (celTypeSatisfiesJsonSchema(type.split("<")[0]!, slot.schema))
|
|
2335
|
-
|
|
2405
|
+
if (!celTypeSatisfiesJsonSchema(type.split("<")[0]!, slot.schema)) {
|
|
2406
|
+
const expected = slot.schema["x-telo-type"] ?? slot.schema.type ?? "unknown";
|
|
2407
|
+
diagnostics.push({
|
|
2408
|
+
severity: DiagnosticSeverity.Error,
|
|
2409
|
+
code: "CEL_TYPE_ERROR",
|
|
2410
|
+
source: SOURCE,
|
|
2411
|
+
message: `${slot.resource.kind}/${slot.resource.name}: CEL at '${slot.path}' returns '${type}' but the field expects '${expected}'.`,
|
|
2412
|
+
data: { resource: slot.resource, filePath: slot.filePath, path: slot.path },
|
|
2413
|
+
});
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
// The type fits; do its ARGUMENTS agree? Covariant and gradual — an
|
|
2417
|
+
// omitted argument is *any* in both directions, so an unmigrated producer
|
|
2418
|
+
// or consumer is never reported, and only a definite conflict is.
|
|
2419
|
+
const produced = celSourceSchemaByPath.get(slot.manifest)?.get(slot.path);
|
|
2420
|
+
if (!produced) continue;
|
|
2421
|
+
const { compatible, issues } = checkSchemaCompatibility(
|
|
2422
|
+
produced,
|
|
2423
|
+
slot.schema,
|
|
2424
|
+
(ref: string) => defs.schemaForId(ref),
|
|
2425
|
+
);
|
|
2426
|
+
if (compatible) continue;
|
|
2336
2427
|
diagnostics.push({
|
|
2337
2428
|
severity: DiagnosticSeverity.Error,
|
|
2338
|
-
code: "
|
|
2429
|
+
code: "CEL_TYPE_ARGUMENT_MISMATCH",
|
|
2339
2430
|
source: SOURCE,
|
|
2340
|
-
message:
|
|
2431
|
+
message:
|
|
2432
|
+
`${slot.resource.kind}/${slot.resource.name}: CEL at '${slot.path}' produces a value ` +
|
|
2433
|
+
`whose type arguments disagree with the field's: ${issues.join("; ")}.`,
|
|
2341
2434
|
data: { resource: slot.resource, filePath: slot.filePath, path: slot.path },
|
|
2342
2435
|
});
|
|
2343
2436
|
}
|
package/src/cel-environment.ts
CHANGED
|
@@ -3,10 +3,13 @@ import type { ResourceManifest } from "@telorun/sdk";
|
|
|
3
3
|
import { jsonSchemaToCelType, VALUE_BRAND_BASE } from "./schema-compat.js";
|
|
4
4
|
|
|
5
5
|
/** Transport protocol on a `ports` entry → the nominal CEL brand its resolved
|
|
6
|
-
* value carries. Mirrors the `protocol` enum in the Application schema
|
|
6
|
+
* value carries. Mirrors the `protocol` enum in the Application schema, and
|
|
7
|
+
* names the value types by their canonical `Telo.`-qualified names — the same
|
|
8
|
+
* spelling an author writes at `x-telo-type`, so a branded port and a branded
|
|
9
|
+
* field are comparable by name with nothing in between to translate. */
|
|
7
10
|
const PORT_PROTOCOL_BRAND: Record<string, string> = {
|
|
8
|
-
tcp: "TcpPort",
|
|
9
|
-
udp: "UdpPort",
|
|
11
|
+
tcp: "Telo.TcpPort",
|
|
12
|
+
udp: "Telo.UdpPort",
|
|
10
13
|
};
|
|
11
14
|
|
|
12
15
|
export { buildCelEnvironment } from "@telorun/templating";
|
|
@@ -127,6 +127,16 @@ export class DefinitionRegistry {
|
|
|
127
127
|
return this.registeredSchemaIds.has(id) || this.ajv.getSchema(id) !== undefined;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/** The schema registered under `id`, for a structural comparison that must see
|
|
131
|
+
* THROUGH a named shape. Declaring a shape once and referencing it is the
|
|
132
|
+
* sanctioned way to reuse one, so a comparator that cannot follow the
|
|
133
|
+
* reference judges two opaque nodes and learns nothing. */
|
|
134
|
+
schemaForId(id: string): Record<string, any> | undefined {
|
|
135
|
+
const compiled = this.ajv.getSchema(id);
|
|
136
|
+
const schema = compiled?.schema;
|
|
137
|
+
return schema && typeof schema === "object" ? (schema as Record<string, any>) : undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
130
140
|
/** Validates data against a schema using this registry's AJV instance, which has all
|
|
131
141
|
* registered definition schemas loaded — enabling cross-module $ref resolution.
|
|
132
142
|
* A compile failure returns `[]` here; it is surfaced loudly (once, on the
|
package/src/index.ts
CHANGED
|
@@ -65,7 +65,7 @@ export {
|
|
|
65
65
|
PERMISSIVE_CONTRACT,
|
|
66
66
|
resolveContract,
|
|
67
67
|
resolveContractSchema,
|
|
68
|
-
|
|
68
|
+
withLiveValuesSkipped,
|
|
69
69
|
} from "./invocation-contract.js";
|
|
70
70
|
export type { ContractOrigin, ContractScope, ResolvedContract } from "./invocation-contract.js";
|
|
71
71
|
export {
|
|
@@ -97,7 +97,18 @@ export {
|
|
|
97
97
|
transfersControl,
|
|
98
98
|
} from "./ref-slot.js";
|
|
99
99
|
export type { RefSlot, RefUse, RefUseCases } from "./ref-slot.js";
|
|
100
|
-
export {
|
|
100
|
+
export {
|
|
101
|
+
ANNOTATION_KEYWORDS,
|
|
102
|
+
registerTeloKeywords,
|
|
103
|
+
valueTypeKeyword,
|
|
104
|
+
} from "./value-type-keyword.js";
|
|
105
|
+
export {
|
|
106
|
+
applyTextEdits,
|
|
107
|
+
isPlainSafe,
|
|
108
|
+
quoteStyleOf,
|
|
109
|
+
renderFixReplacement,
|
|
110
|
+
} from "./yaml-source-edit.js";
|
|
111
|
+
export type { QuoteStyle, TextEdit } from "./yaml-source-edit.js";
|
|
101
112
|
export {
|
|
102
113
|
hasProvidesZone,
|
|
103
114
|
hasRequiresZone,
|
|
@@ -123,6 +134,10 @@ export { validateZoneSlotDeclarations } from "./validate-zone-slots.js";
|
|
|
123
134
|
export type { ZoneSlotIssue } from "./validate-zone-slots.js";
|
|
124
135
|
export { validateDynamicSelectors, validateRefSlotDeclarations } from "./validate-ref-slots.js";
|
|
125
136
|
export type { RefSlotIssue } from "./validate-ref-slots.js";
|
|
137
|
+
export { validateValueTypeSlots } from "./validate-value-type-slots.js";
|
|
138
|
+
export type { ValueTypeSlotIssue } from "./validate-value-type-slots.js";
|
|
139
|
+
export { checkSchemaCompatibility } from "./schema-compat.js";
|
|
140
|
+
export type { CompatibilityResult } from "./schema-compat.js";
|
|
126
141
|
export { visitManifest } from "./manifest-visitor.js";
|
|
127
142
|
export type {
|
|
128
143
|
CelSiteEvent,
|
|
@@ -139,6 +154,15 @@ export { isModuleKind, MODULE_KINDS } from "./module-kinds.js";
|
|
|
139
154
|
export type { ModuleKind } from "./module-kinds.js";
|
|
140
155
|
export { parseLoadedFile } from "./parse-loaded-file.js";
|
|
141
156
|
export type { ParseOptions } from "./parse-loaded-file.js";
|
|
157
|
+
export {
|
|
158
|
+
CORE_MIGRATIONS,
|
|
159
|
+
migrateFileText,
|
|
160
|
+
migrateManifests,
|
|
161
|
+
NO_MIGRATIONS,
|
|
162
|
+
parseMigrationEntry,
|
|
163
|
+
remapMigratedPaths,
|
|
164
|
+
} from "./migrations/index.js";
|
|
165
|
+
export type { FileMigrations, MigrationEntry, MigrationRewrite } from "./migrations/index.js";
|
|
142
166
|
export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
|
|
143
167
|
export type { SyntheticImport } from "./inline-imports.js";
|
|
144
168
|
export {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { isLiveSlot, type ResourceDefinition } from "@telorun/sdk";
|
|
2
2
|
import {
|
|
3
3
|
type ContractDirection,
|
|
4
4
|
contractDeclarer,
|
|
@@ -108,33 +108,41 @@ export function resolveContractSchema(
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
|
-
* A copy of `schema` with every `
|
|
112
|
-
*
|
|
111
|
+
* A copy of `schema` with every `live`-typed node left unconstrained, for
|
|
112
|
+
* validating a runtime value against.
|
|
113
113
|
*
|
|
114
|
-
*
|
|
115
|
-
* `inputType` and lists it in `required`, and `Record.Stream`, `Ai`, `Tar`
|
|
116
|
-
* `Console` do the same — so a one-directional skip would walk a live
|
|
117
|
-
* with AJV on the hottest path in the runtime. That is the same defect
|
|
118
|
-
* `stripCompiledValues` walking a live `ResourceInstance` in a ref slot: a
|
|
119
|
-
* object in a declared slot is not data to be traversed.
|
|
120
|
-
*
|
|
114
|
+
* Live values travel in BOTH directions — `Codec.Encoder` declares a stream on
|
|
115
|
+
* its `inputType` and lists it in `required`, and `Record.Stream`, `Ai`, `Tar`
|
|
116
|
+
* and `Console` do the same — so a one-directional skip would walk a live
|
|
117
|
+
* `Stream` with AJV on the hottest path in the runtime. That is the same defect
|
|
118
|
+
* as `stripCompiledValues` walking a live `ResourceInstance` in a ref slot: a
|
|
119
|
+
* live object in a declared slot is not data to be traversed.
|
|
120
|
+
*
|
|
121
|
+
* EXEMPTION IS A PROPERTY OF THE TYPE, not of a position. This used to neutralize
|
|
122
|
+
* only a key it found in a `properties` map, so an array-OF-streams element was
|
|
123
|
+
* reached and left constrained even though the walk descended into `items`.
|
|
124
|
+
* Reading the exemption off the declared value type makes an item, a union branch
|
|
125
|
+
* and a property the same case, and it is one rule instead of three.
|
|
126
|
+
*
|
|
127
|
+
* The exemption is from VALIDATION, never from TYPING: a live type's declared
|
|
128
|
+
* arguments still travel through every schema-typing walk the analyzer performs.
|
|
121
129
|
*
|
|
122
130
|
* Structural (returns a new object, never mutates), and shared so the analyzer
|
|
123
131
|
* and the kernel exempt the same set.
|
|
124
132
|
*/
|
|
125
|
-
export function
|
|
133
|
+
export function withLiveValuesSkipped(
|
|
126
134
|
schema: Record<string, any>,
|
|
127
135
|
/** Resolves a `$ref` to the schema it names. Required to see through the
|
|
128
136
|
* reference form the runtime deliberately KEEPS intact for its validator: a
|
|
129
137
|
* contract written as `{ $ref: "telo:mod/Type" }` has none of its own
|
|
130
138
|
* properties, so a walk that cannot follow the reference exempts nothing and
|
|
131
|
-
* the
|
|
139
|
+
* the live value is traversed after all. */
|
|
132
140
|
resolveRef?: (ref: string) => Record<string, any> | undefined,
|
|
133
141
|
): Record<string, any> {
|
|
134
|
-
return
|
|
142
|
+
return stripLive(schema, [], resolveRef);
|
|
135
143
|
}
|
|
136
144
|
|
|
137
|
-
function
|
|
145
|
+
function stripLive(
|
|
138
146
|
node: unknown,
|
|
139
147
|
// A PATH-scoped guard, not a global memo: a schema object reached twice from
|
|
140
148
|
// different parents must be stripped twice (a global `seen` would hand the
|
|
@@ -145,7 +153,7 @@ function stripStreams(
|
|
|
145
153
|
if (Array.isArray(node)) {
|
|
146
154
|
let changed = false;
|
|
147
155
|
const items = node.map((item) => {
|
|
148
|
-
const next =
|
|
156
|
+
const next = stripLive(item, path, resolveRef);
|
|
149
157
|
if (next !== item) changed = true;
|
|
150
158
|
return next;
|
|
151
159
|
});
|
|
@@ -155,6 +163,14 @@ function stripStreams(
|
|
|
155
163
|
let schema = node as Record<string, any>;
|
|
156
164
|
if (path.includes(schema)) return schema;
|
|
157
165
|
|
|
166
|
+
// The one rule. An empty schema leaves the node DECLARED but unconstrained,
|
|
167
|
+
// rather than deleted: deleting a property would force `additionalProperties:
|
|
168
|
+
// false` open, and a closed contract would stop rejecting unknown keys the
|
|
169
|
+
// moment it grew a stream — trading one exemption for a hole across the whole
|
|
170
|
+
// shape. `required` is untouched for the same reason: a live value IS present,
|
|
171
|
+
// it is only not walked.
|
|
172
|
+
if (isLiveSlot(schema)) return {};
|
|
173
|
+
|
|
158
174
|
// Follow a whole-document reference to SEE the annotations behind it, but
|
|
159
175
|
// return the original node when nothing behind it was stripped. Substituting
|
|
160
176
|
// the resolved target unconditionally would break schema identity — the
|
|
@@ -163,7 +179,7 @@ function stripStreams(
|
|
|
163
179
|
if (resolveRef && typeof schema.$ref === "string") {
|
|
164
180
|
const target = resolveRef(schema.$ref);
|
|
165
181
|
if (target && !path.includes(target)) {
|
|
166
|
-
const stripped =
|
|
182
|
+
const stripped = stripLive(target, [...path, schema], resolveRef);
|
|
167
183
|
if (stripped === target) return node;
|
|
168
184
|
const { $ref: _ref, ...siblings } = schema;
|
|
169
185
|
return Object.keys(siblings).length > 0 ? { ...stripped, ...siblings } : stripped;
|
|
@@ -171,44 +187,23 @@ function stripStreams(
|
|
|
171
187
|
}
|
|
172
188
|
const here = [...path, schema];
|
|
173
189
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// is expressed before the branches are merged), so the marked set is read
|
|
179
|
-
// from the folded view while the removal is applied here.
|
|
180
|
-
const streamed = Object.keys(properties).filter(
|
|
181
|
-
(key) => (properties[key] as Record<string, any> | undefined)?.["x-telo-stream"],
|
|
182
|
-
);
|
|
183
|
-
if (streamed.length > 0) {
|
|
184
|
-
const kept: Record<string, any> = {};
|
|
185
|
-
for (const [key, value] of Object.entries(properties)) {
|
|
186
|
-
if (!streamed.includes(key)) kept[key] = value;
|
|
187
|
-
}
|
|
188
|
-
// The key stays DECLARED but unconstrained, rather than being deleted.
|
|
189
|
-
// Deleting it would force `additionalProperties: false` open, and a closed
|
|
190
|
-
// contract would stop rejecting unknown keys the moment it grew a stream —
|
|
191
|
-
// trading one exemption for a hole across the whole shape.
|
|
192
|
-
for (const key of streamed) kept[key] = {};
|
|
193
|
-
out = { ...schema, properties: kept };
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Recurse: a stream one level down (an item, a branch, a nested object) is as
|
|
198
|
-
// live as one at the root, and walking it with AJV is the same defect.
|
|
190
|
+
// Recurse: a live value one level down (a property, an item, a branch, a
|
|
191
|
+
// nested object) is as live as one at the root, and walking it with AJV is the
|
|
192
|
+
// same defect. Each is neutralized by the single rule above when the walk
|
|
193
|
+
// reaches it.
|
|
199
194
|
//
|
|
200
195
|
// `properties` and `$defs` are MAPS of schemas, not schemas — descending into
|
|
201
196
|
// them as if they were would visit nothing, since a map has none of the
|
|
202
197
|
// keywords this walk looks for.
|
|
203
|
-
let changed =
|
|
204
|
-
const result: Record<string, any> = { ...
|
|
198
|
+
let changed = false;
|
|
199
|
+
const result: Record<string, any> = { ...schema };
|
|
205
200
|
for (const key of ["properties", "$defs"] as const) {
|
|
206
|
-
const map =
|
|
201
|
+
const map = schema[key] as Record<string, any> | undefined;
|
|
207
202
|
if (!map || typeof map !== "object") continue;
|
|
208
203
|
let mapChanged = false;
|
|
209
204
|
const next: Record<string, any> = {};
|
|
210
205
|
for (const [name, child] of Object.entries(map)) {
|
|
211
|
-
const stripped =
|
|
206
|
+
const stripped = stripLive(child, here, resolveRef);
|
|
212
207
|
if (stripped !== child) mapChanged = true;
|
|
213
208
|
next[name] = stripped;
|
|
214
209
|
}
|
|
@@ -218,9 +213,9 @@ function stripStreams(
|
|
|
218
213
|
}
|
|
219
214
|
}
|
|
220
215
|
for (const key of ["items", "allOf", "anyOf", "oneOf"] as const) {
|
|
221
|
-
const child =
|
|
216
|
+
const child = schema[key];
|
|
222
217
|
if (child === undefined) continue;
|
|
223
|
-
const next =
|
|
218
|
+
const next = stripLive(child, here, resolveRef);
|
|
224
219
|
if (next !== child) {
|
|
225
220
|
result[key] = next;
|
|
226
221
|
changed = true;
|
|
@@ -237,7 +232,7 @@ function stripStreams(
|
|
|
237
232
|
* schema's defaults rather than by the size of the payload. */
|
|
238
233
|
export function defaultBearingPaths(
|
|
239
234
|
schema: Record<string, any>,
|
|
240
|
-
/** See {@link
|
|
235
|
+
/** See {@link withLiveValuesSkipped} — a contract kept in `$ref` form
|
|
241
236
|
* declares its defaults behind the reference, and a walk that cannot follow
|
|
242
237
|
* it would report none, leaving the caller's data shared where a fill lands. */
|
|
243
238
|
resolveRef?: (ref: string) => Record<string, any> | undefined,
|
package/src/loaded-types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import type { Document } from "yaml";
|
|
3
|
+
import type { FileMigrations } from "./migrations/driver.js";
|
|
3
4
|
import type { DocumentPosition } from "./position-metadata.js";
|
|
4
5
|
import type { AnalysisDiagnostic, Range } from "./types.js";
|
|
5
6
|
import type { AstDocument } from "./yaml-ast.js";
|
|
@@ -31,6 +32,12 @@ export interface LoadedFile {
|
|
|
31
32
|
positions: DocumentPosition[];
|
|
32
33
|
/** Document-level parse errors aggregated from `yaml.Document.errors`. */
|
|
33
34
|
parseErrors: ParseError[];
|
|
35
|
+
/** What the migration phase rewrote in this file, and the diagnostics for it.
|
|
36
|
+
* Empty unless the load opted into `migrate` AND a legacy spelling matched.
|
|
37
|
+
* `manifests` above are the POST-migration tree; `documents` / `astDocuments`
|
|
38
|
+
* / `positions` / `text` are always the author's file untouched, which is
|
|
39
|
+
* what the recorded `legacyPath` resolves against. */
|
|
40
|
+
migrations: FileMigrations;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
export interface ParseError {
|
|
@@ -93,6 +100,12 @@ export interface LoadedGraph {
|
|
|
93
100
|
* it independently re-resolves an import (the analyzer already sees repointed
|
|
94
101
|
* `importEdges`). Empty when no module identity appeared at two sources. */
|
|
95
102
|
overrides: Map<string, string>;
|
|
103
|
+
/** Deprecation diagnostics from the loader's migration phase, scoped to the
|
|
104
|
+
* ENTRY's own module (owner + its `include:` partials). A migration rewrites
|
|
105
|
+
* always — the runtime must read artifacts published years ago — but reports
|
|
106
|
+
* only here, because a published dependency is not the consumer's to fix.
|
|
107
|
+
* Empty unless the load opted into `migrate`. */
|
|
108
|
+
migrationDiagnostics: AnalysisDiagnostic[];
|
|
96
109
|
/** Diagnostics produced while reconciling module versions — one per import
|
|
97
110
|
* edge redirected to a different version (warning for a same-major hoist,
|
|
98
111
|
* error for a major mismatch). Surfaced alongside `analyze()` diagnostics by
|
package/src/manifest-loader.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
LoadedModule,
|
|
10
10
|
} from "./loaded-types.js";
|
|
11
11
|
import { desugarLoadedFile } from "./inline-imports.js";
|
|
12
|
+
import type { MigrationEntry } from "./migrations/types.js";
|
|
12
13
|
import { isModuleKind } from "./module-kinds.js";
|
|
13
14
|
import { parseLoadedFile } from "./parse-loaded-file.js";
|
|
14
15
|
import { reconcileModuleVersions } from "./reconcile-module-versions.js";
|
|
@@ -44,6 +45,18 @@ function collectParseDiagnostics(
|
|
|
44
45
|
return diagnostics;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/** Rewrite always, report locally. Every file in the graph was migrated, but a
|
|
49
|
+
* published dependency's manifest is not the consumer's to fix and its author
|
|
50
|
+
* is the only person who can — so only the entry module's own files (owner +
|
|
51
|
+
* its `include:` partials) report. Same rule `X_TELO_REF_UNRESOLVED` follows. */
|
|
52
|
+
function collectMigrationDiagnostics(entry: LoadedModule): AnalysisDiagnostic[] {
|
|
53
|
+
const diagnostics: AnalysisDiagnostic[] = [];
|
|
54
|
+
for (const file of [entry.owner, ...entry.partials]) {
|
|
55
|
+
diagnostics.push(...file.migrations.diagnostics);
|
|
56
|
+
}
|
|
57
|
+
return diagnostics;
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
const SYSTEM_KINDS = new Set([
|
|
48
61
|
"Telo.Application",
|
|
49
62
|
"Telo.Library",
|
|
@@ -51,12 +64,19 @@ const SYSTEM_KINDS = new Set([
|
|
|
51
64
|
"Telo.Definition",
|
|
52
65
|
]);
|
|
53
66
|
|
|
54
|
-
/** File cache variant tags: compile (c/r) × desugarImports (d/n)
|
|
55
|
-
* and a raw load of the same file are distinct entries so
|
|
56
|
-
* wrong manifest tree
|
|
57
|
-
|
|
67
|
+
/** File cache variant tags: compile (c/r) × desugarImports (d/n) × migrate
|
|
68
|
+
* (m/x). A desugared and a raw load of the same file are distinct entries so
|
|
69
|
+
* neither sees the wrong manifest tree, and the migration axis is there for
|
|
70
|
+
* the same reason — the editor's round-trip view and `telo migrate` must see
|
|
71
|
+
* the author's spelling, everything else the current one. */
|
|
72
|
+
const CACHE_VARIANTS = [
|
|
73
|
+
"rnx", "rdx", "cnx", "cdx",
|
|
74
|
+
"rnm", "rdm", "cnm", "cdm",
|
|
75
|
+
] as const;
|
|
58
76
|
function variantKey(options?: LoadOptions): string {
|
|
59
|
-
return `${options?.compile ? "c" : "r"}${options?.desugarImports ? "d" : "n"}
|
|
77
|
+
return `${options?.compile ? "c" : "r"}${options?.desugarImports ? "d" : "n"}${
|
|
78
|
+
options?.migrate ? "m" : "x"
|
|
79
|
+
}`;
|
|
60
80
|
}
|
|
61
81
|
|
|
62
82
|
export class Loader {
|
|
@@ -76,6 +96,7 @@ export class Loader {
|
|
|
76
96
|
|
|
77
97
|
protected sources: ManifestSource[];
|
|
78
98
|
private readonly celEnv: Environment;
|
|
99
|
+
private readonly migrations?: readonly MigrationEntry[];
|
|
79
100
|
|
|
80
101
|
/** Sources are resolved in order — the first whose `supports(url)` matches
|
|
81
102
|
* wins. The caller (composition root) decides which concrete sources exist
|
|
@@ -85,6 +106,7 @@ export class Loader {
|
|
|
85
106
|
constructor(sources: ManifestSource[] = [], options: LoaderInitOptions = {}) {
|
|
86
107
|
this.sources = [...sources];
|
|
87
108
|
this.celEnv = buildCelEnvironment(options.celHandlers);
|
|
109
|
+
this.migrations = options.migrations;
|
|
88
110
|
}
|
|
89
111
|
|
|
90
112
|
register(source: ManifestSource): this {
|
|
@@ -188,7 +210,13 @@ export class Loader {
|
|
|
188
210
|
* caller opted in. Desugaring lives here, not in the pure `parseLoadedFile`,
|
|
189
211
|
* so round-trip consumers (the editor) keep a raw manifest/AST/position
|
|
190
212
|
* triple they can pair by index; only resolved consumers that pass
|
|
191
|
-
* `desugarImports` see synthetic Telo.Import manifests.
|
|
213
|
+
* `desugarImports` see synthetic Telo.Import manifests.
|
|
214
|
+
*
|
|
215
|
+
* The migration phase runs inside `parseLoadedFile`, i.e. before desugaring —
|
|
216
|
+
* a rule must only ever match author-written nodes, and the position is what
|
|
217
|
+
* makes that structural rather than a convention. Nothing needs the later
|
|
218
|
+
* position: the `imports:` map is read straight off the module manifest and is
|
|
219
|
+
* equally available before desugaring. */
|
|
192
220
|
private parseAndMaybeDesugar(
|
|
193
221
|
source: string,
|
|
194
222
|
requestedUrl: string,
|
|
@@ -198,6 +226,8 @@ export class Loader {
|
|
|
198
226
|
const loaded = parseLoadedFile(source, requestedUrl, text, {
|
|
199
227
|
compile: options?.compile,
|
|
200
228
|
celEnv: this.celEnv,
|
|
229
|
+
migrate: options?.migrate,
|
|
230
|
+
migrations: this.migrations,
|
|
201
231
|
});
|
|
202
232
|
return options?.desugarImports ? desugarLoadedFile(loaded) : loaded;
|
|
203
233
|
}
|
|
@@ -364,6 +394,7 @@ export class Loader {
|
|
|
364
394
|
modules,
|
|
365
395
|
importEdges,
|
|
366
396
|
overrides,
|
|
397
|
+
migrationDiagnostics: collectMigrationDiagnostics(entry),
|
|
367
398
|
versionDiagnostics: diagnostics,
|
|
368
399
|
parseDiagnostics: collectParseDiagnostics(modules),
|
|
369
400
|
errors,
|