@telorun/analyzer 0.56.1 → 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.
Files changed (133) hide show
  1. package/dist/analyzer.d.ts +5 -0
  2. package/dist/analyzer.d.ts.map +1 -1
  3. package/dist/analyzer.js +232 -94
  4. package/dist/cel-environment.d.ts.map +1 -1
  5. package/dist/cel-environment.js +6 -3
  6. package/dist/definition-registry.d.ts +5 -0
  7. package/dist/definition-registry.d.ts.map +1 -1
  8. package/dist/definition-registry.js +9 -0
  9. package/dist/index.d.ts +14 -4
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +8 -3
  12. package/dist/invocation-contract.d.ts +21 -13
  13. package/dist/invocation-contract.d.ts.map +1 -1
  14. package/dist/invocation-contract.js +43 -46
  15. package/dist/loaded-types.d.ts +13 -0
  16. package/dist/loaded-types.d.ts.map +1 -1
  17. package/dist/manifest-loader.d.ts +8 -1
  18. package/dist/manifest-loader.d.ts.map +1 -1
  19. package/dist/manifest-loader.js +33 -6
  20. package/dist/manifest-visitor.d.ts +4 -0
  21. package/dist/manifest-visitor.d.ts.map +1 -1
  22. package/dist/manifest-visitor.js +3 -3
  23. package/dist/migrations/driver.d.ts +69 -0
  24. package/dist/migrations/driver.d.ts.map +1 -0
  25. package/dist/migrations/driver.js +200 -0
  26. package/dist/migrations/entries/index.d.ts +3 -0
  27. package/dist/migrations/entries/index.d.ts.map +1 -0
  28. package/dist/migrations/entries/index.js +9 -0
  29. package/dist/migrations/entries/normalize-value-types.json +48 -0
  30. package/dist/migrations/entries/ref-slot-scalar-type.json +20 -0
  31. package/dist/migrations/entry-data.d.ts +25 -0
  32. package/dist/migrations/entry-data.d.ts.map +1 -0
  33. package/dist/migrations/entry-data.js +171 -0
  34. package/dist/migrations/index.d.ts +22 -0
  35. package/dist/migrations/index.d.ts.map +1 -0
  36. package/dist/migrations/index.js +19 -0
  37. package/dist/migrations/match.d.ts +162 -0
  38. package/dist/migrations/match.d.ts.map +1 -0
  39. package/dist/migrations/match.js +295 -0
  40. package/dist/migrations/patch.d.ts +68 -0
  41. package/dist/migrations/patch.d.ts.map +1 -0
  42. package/dist/migrations/patch.js +236 -0
  43. package/dist/migrations/provenance.d.ts +25 -0
  44. package/dist/migrations/provenance.d.ts.map +1 -0
  45. package/dist/migrations/provenance.js +118 -0
  46. package/dist/migrations/registry.d.ts +35 -0
  47. package/dist/migrations/registry.d.ts.map +1 -0
  48. package/dist/migrations/registry.js +35 -0
  49. package/dist/migrations/report.d.ts +28 -0
  50. package/dist/migrations/report.d.ts.map +1 -0
  51. package/dist/migrations/report.js +144 -0
  52. package/dist/migrations/types.d.ts +115 -0
  53. package/dist/migrations/types.d.ts.map +1 -0
  54. package/dist/migrations/types.js +24 -0
  55. package/dist/migrations/yaml-edit.d.ts +21 -0
  56. package/dist/migrations/yaml-edit.d.ts.map +1 -0
  57. package/dist/migrations/yaml-edit.js +245 -0
  58. package/dist/module-file-claims.d.ts +65 -0
  59. package/dist/module-file-claims.d.ts.map +1 -0
  60. package/dist/module-file-claims.js +106 -0
  61. package/dist/parse-loaded-file.d.ts +8 -0
  62. package/dist/parse-loaded-file.d.ts.map +1 -1
  63. package/dist/parse-loaded-file.js +22 -13
  64. package/dist/resolve-schema-type-refs.d.ts +29 -12
  65. package/dist/resolve-schema-type-refs.d.ts.map +1 -1
  66. package/dist/resolve-schema-type-refs.js +89 -16
  67. package/dist/schema-compat.d.ts +35 -9
  68. package/dist/schema-compat.d.ts.map +1 -1
  69. package/dist/schema-compat.js +181 -54
  70. package/dist/schema-region.d.ts +33 -0
  71. package/dist/schema-region.d.ts.map +1 -0
  72. package/dist/schema-region.js +45 -0
  73. package/dist/types.d.ts +51 -0
  74. package/dist/types.d.ts.map +1 -1
  75. package/dist/types.js +6 -0
  76. package/dist/validate-cel-context.d.ts.map +1 -1
  77. package/dist/validate-cel-context.js +29 -3
  78. package/dist/validate-include-placement.d.ts +26 -0
  79. package/dist/validate-include-placement.d.ts.map +1 -0
  80. package/dist/validate-include-placement.js +67 -0
  81. package/dist/validate-step-inputs.d.ts +8 -1
  82. package/dist/validate-step-inputs.d.ts.map +1 -1
  83. package/dist/validate-step-inputs.js +47 -2
  84. package/dist/validate-throws-coverage.d.ts.map +1 -1
  85. package/dist/validate-throws-coverage.js +15 -12
  86. package/dist/validate-value-type-slots.d.ts +43 -0
  87. package/dist/validate-value-type-slots.d.ts.map +1 -0
  88. package/dist/validate-value-type-slots.js +142 -0
  89. package/dist/value-type-keyword.d.ts +60 -0
  90. package/dist/value-type-keyword.d.ts.map +1 -0
  91. package/dist/value-type-keyword.js +119 -0
  92. package/dist/yaml-source-edit.d.ts +56 -0
  93. package/dist/yaml-source-edit.d.ts.map +1 -0
  94. package/dist/yaml-source-edit.js +93 -0
  95. package/package.json +4 -3
  96. package/src/analyzer.ts +276 -129
  97. package/src/cel-environment.ts +6 -3
  98. package/src/definition-registry.ts +10 -0
  99. package/src/index.ts +31 -3
  100. package/src/invocation-contract.ts +43 -48
  101. package/src/loaded-types.ts +13 -0
  102. package/src/manifest-loader.ts +37 -6
  103. package/src/manifest-visitor.ts +11 -3
  104. package/src/migrations/driver.ts +262 -0
  105. package/src/migrations/entries/index.ts +10 -0
  106. package/src/migrations/entries/normalize-value-types.json +48 -0
  107. package/src/migrations/entries/ref-slot-scalar-type.json +20 -0
  108. package/src/migrations/entry-data.ts +207 -0
  109. package/src/migrations/index.ts +22 -0
  110. package/src/migrations/match.ts +382 -0
  111. package/src/migrations/patch.ts +265 -0
  112. package/src/migrations/provenance.ts +167 -0
  113. package/src/migrations/registry.ts +40 -0
  114. package/src/migrations/report.ts +169 -0
  115. package/src/migrations/types.ts +144 -0
  116. package/src/migrations/yaml-edit.ts +260 -0
  117. package/src/module-file-claims.ts +168 -0
  118. package/src/parse-loaded-file.ts +31 -12
  119. package/src/resolve-schema-type-refs.ts +93 -16
  120. package/src/schema-compat.ts +218 -62
  121. package/src/schema-region.ts +46 -0
  122. package/src/types.ts +54 -0
  123. package/src/validate-cel-context.ts +36 -3
  124. package/src/validate-include-placement.ts +70 -0
  125. package/src/validate-step-inputs.ts +56 -1
  126. package/src/validate-throws-coverage.ts +16 -11
  127. package/src/validate-value-type-slots.ts +182 -0
  128. package/src/value-type-keyword.ts +135 -0
  129. package/src/yaml-source-edit.ts +106 -0
  130. package/dist/binary-slot.d.ts +0 -39
  131. package/dist/binary-slot.d.ts.map +0 -1
  132. package/dist/binary-slot.js +0 -58
  133. package/src/binary-slot.ts +0 -71
@@ -0,0 +1,162 @@
1
+ /** The selector half of a migration: what a rule MATCHES, as data.
2
+ *
3
+ * A patch addresses a known location; a migration has to find *every*
4
+ * occurrence of a legacy spelling, so selection is the half a plain patch
5
+ * format has none of. It is declarative for the same reason the operations
6
+ * are: an entry travels to a Rust and a Go kernel, and a predicate expressed
7
+ * in one language would mean one artifact is read two ways — invisibly, since
8
+ * a migration that succeeds is silent.
9
+ *
10
+ * **Containment is POSITIVE and required.** A rule states which document kinds
11
+ * it may touch (`inKind`) and which region of those documents it may reach
12
+ * into (`under`); nothing outside is reachable. The alternative — walk
13
+ * everything and subtract — cannot be made sound, because the set to subtract
14
+ * is unbounded: a `Run.Value` value, an `Assert.Equals` expected, any kind
15
+ * whose config carries a user JSON blob can hold something shaped like the
16
+ * node a rule looks for, and forgetting one corrupts a manifest with no
17
+ * diagnostic. A denylist also cannot express the guarantee the module surface
18
+ * is promised to carry — *a dependency can rename its own field and provably
19
+ * nothing else* — which is a statement about what a rule may reach, so it has
20
+ * to be said positively. `notUnder` remains for subtracting inside a region a
21
+ * rule legitimately reaches, which is a narrowing, not the containment itself.
22
+ *
23
+ * **`under` is ANCHORED at the document root**, not a set of key names to look
24
+ * for anywhere on the path. Anchoring is what makes the containment claim
25
+ * true: a `Telo.Definition`'s `resources:` template body carries other kinds'
26
+ * configuration, and any of it may hold a key spelled `schema` over data that
27
+ * merely looks like a schema — so "some segment of the path is `schema`" would
28
+ * reach the very user JSON blob the positive form exists to keep out, and
29
+ * would delete from it silently. Anchored, `under` names top-level document
30
+ * keys and a region is a genuine subtree.
31
+ *
32
+ * **`inSchema` is the one region a kind list cannot name.** An annotation
33
+ * keyword occurs in author-written JSON Schema, and schema fragments are not
34
+ * confined to kind documents: an inline `inputType:` / `outputType:` on ANY kind
35
+ * that declares one, an API route's `request.schema.body`, a `Telo.JsonSchema`'s
36
+ * `schema`. That set of kinds is open — any kind may declare a schema-valued
37
+ * field — and enumerating the standard library's would put resource-kind
38
+ * knowledge into the analyzer, against the topology-driven constraint. So a rule
39
+ * may instead state that it reaches only inside a SCHEMA REGION, named by the
40
+ * KERNEL's own schema-valued keys, which no kind owns.
41
+ *
42
+ * With `inSchema`, and only with it, `inKind` / `under` may be `["*"]` — and
43
+ * only for a rule keyed on a reserved `x-telo-*` annotation. That pairing IS the
44
+ * containment: the region gate bounds where the walk may go, and the reserved-key
45
+ * rule bounds what it may touch, since an `x-telo-*` key is Telo vocabulary
46
+ * wherever it appears and cannot mean something else in someone's config. Both
47
+ * halves are refused at entry-read time, so a module-shipped entry can no more
48
+ * spell `"*"` than it can name another module's kind.
49
+ *
50
+ * The residue is stated rather than claimed away: a manifest that asserts ABOUT
51
+ * a schema — a schema literal under a key spelled `schema` inside an assertion's
52
+ * expected value — is reachable, and would be rewritten into its own synonym.
53
+ * That cannot be closed in a data-only matcher without naming kinds. It is
54
+ * accepted because the sites the wildcards reach are exactly the ones no
55
+ * enumeration covers, and the alternative leaves an author reading a deprecation
56
+ * `telo migrate` refuses to act on.
57
+ *
58
+ * The vocabulary is closed, which is what makes it a trust boundary once
59
+ * module-shipped entries are aggregated beside core ones. An unrecognized key
60
+ * is refused rather than ignored — a selector that silently matches wider than
61
+ * it reads is the one failure this cannot tolerate. */
62
+ import type { MigrationPath } from "./types.js";
63
+ /** The wildcard `inKind` / `under` value. Legal only alongside `inSchema`, and
64
+ * only for a rule keyed on an `x-telo-*` annotation. */
65
+ export declare const MATCH_ANY = "*";
66
+ export interface MigrationMatch {
67
+ /** The mapping key this rule rewrites. */
68
+ readonly key: string;
69
+ /** Document `kind:` values this rule may match in. Required and non-empty:
70
+ * a rule that does not say which documents it touches cannot be reasoned
71
+ * about, and is exactly the rule that reaches into a resource's config. */
72
+ readonly inKind: readonly string[];
73
+ /** The region of the document this rule may reach, named by TOP-LEVEL
74
+ * document keys: the matched node must be AT or BELOW one of them. Required
75
+ * and non-empty, for the same reason as `inKind`.
76
+ *
77
+ * Anchored at the root rather than matched anywhere on the path — see the
78
+ * file header; an unanchored `under` reaches into a nested resource's own
79
+ * configuration and is not containment at all.
80
+ *
81
+ * At-or-below rather than strictly-below so the vocabulary is complete — a
82
+ * rule that rewrites a top-level key names that key, instead of the region
83
+ * being unreachable and the grammar needing a second spelling for the
84
+ * document root. */
85
+ readonly under: readonly string[];
86
+ /** The value must deep-equal this. Mutually exclusive with `valueOneOf`. */
87
+ readonly value?: unknown;
88
+ /** The value must be one of these. Matching against the KNOWN legacy values
89
+ * rather than any value is what leaves an unrecognized one alone for the
90
+ * ordinary validator to report, instead of silently rewriting it. */
91
+ readonly valueOneOf?: readonly unknown[];
92
+ /** A key that must be present in the same mapping. The matched key is often
93
+ * stale only *because* of what sits beside it. */
94
+ readonly withSibling?: string;
95
+ /** Ancestor keys that disqualify a match inside the region `under` allows.
96
+ * The data-bearing JSON Schema keywords (`const`, `default`, `enum`,
97
+ * `examples`) hold values that may look like schemas. */
98
+ readonly notUnder?: readonly string[];
99
+ /** Narrow to a JSON Schema region: the match must be at or below a node
100
+ * reached through one of {@link SCHEMA_REGION_KEYS}.
101
+ *
102
+ * A second bound, never a replacement for `inKind` / `under` — it is what
103
+ * makes their wildcard forms safe, and it narrows an enumerated region just as
104
+ * usefully. See the file header for why an annotation rename needs it. */
105
+ readonly inSchema?: boolean;
106
+ }
107
+ export declare const MATCH_KEYS: readonly ["key", "inKind", "under", "value", "valueOneOf", "withSibling", "notUnder", "inSchema"];
108
+ /** Structural equality over JSON values — what `value` / `valueOneOf` compare
109
+ * with, so a match on `true` never also matches `"true"`.
110
+ *
111
+ * Exported because the PATCHER asks the same question in the other direction:
112
+ * "is the value already what this would write". Both answers have to come from
113
+ * one rule, or a rule could match a spelling the patch then declares current. */
114
+ export declare function deepEquals(a: unknown, b: unknown): boolean;
115
+ /** Read a rule's `match` block, refusing anything the vocabulary does not
116
+ * define. `describe` names the entry and rule so a failure says which. */
117
+ export declare function readMigrationMatch(describe: string, raw: unknown): MigrationMatch;
118
+ /** One candidate site: a mapping entry whose key some rule is interested in. */
119
+ interface MatchSite {
120
+ readonly path: MigrationPath;
121
+ readonly value: unknown;
122
+ readonly parent: Record<string, unknown>;
123
+ }
124
+ /** Every candidate site in one document, keyed by mapping key.
125
+ *
126
+ * Built ONCE per document and shared by every rule that can apply to it,
127
+ * because the walk is the expensive part and it does not depend on the rule —
128
+ * this is on the kernel's boot path for every file in the graph, so a walk per
129
+ * rule would scale the cost of loading any manifest with the size of the
130
+ * migration set.
131
+ *
132
+ * The walk is bounded by the same containment the rules declare: `roots` is
133
+ * the union of the applicable rules' `under`, so a region no rule can reach is
134
+ * never descended into and a document no rule can match is never walked at
135
+ * all. Only keys some rule asked for allocate a path array. */
136
+ export type MatchIndex = ReadonlyMap<string, readonly MatchSite[]>;
137
+ /**
138
+ * Index `document`'s candidate sites for `keys`, descending only into the
139
+ * top-level regions named by `roots`.
140
+ *
141
+ * The caller has already gated on `inKind` — see `selectMatches`, which repeats
142
+ * the check because it holds the individual rule.
143
+ */
144
+ export declare function buildMatchIndex(document: unknown, keys: ReadonlySet<string>, roots: ReadonlySet<string>): MatchIndex;
145
+ /** The rules in `entries` that may match a document of `kind`, with the key and
146
+ * root sets their shared index needs. Empty rules mean the document is not
147
+ * walked at all. */
148
+ export declare function applicableRules<T extends {
149
+ readonly match: MigrationMatch;
150
+ }>(rules: Iterable<T>, kind: unknown): {
151
+ rules: T[];
152
+ keys: Set<string>;
153
+ roots: Set<string>;
154
+ };
155
+ /** The sites in `index` this match selects, in document order. */
156
+ export declare function selectMatches(index: MatchIndex, document: unknown, match: MigrationMatch): MigrationPath[];
157
+ /** Convenience for a single match against a document — builds a one-rule index.
158
+ * The driver uses `applicableRules` + `buildMatchIndex` + `selectMatches` so
159
+ * one walk serves every rule that can reach the document. */
160
+ export declare function findMatches(document: unknown, match: MigrationMatch): MigrationPath[];
161
+ export {};
162
+ //# sourceMappingURL=match.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../../src/migrations/match.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wDA4DwD;AAGxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;yDACyD;AACzD,eAAO,MAAM,SAAS,MAAM,CAAC;AAE7B,MAAM,WAAW,cAAc;IAC7B,0CAA0C;IAC1C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;gFAE4E;IAC5E,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;;;;;;;;yBAWqB;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB;;0EAEsE;IACtE,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IACzC;uDACmD;IACnD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;8DAE0D;IAC1D,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;;+EAK2E;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,eAAO,MAAM,UAAU,mGASb,CAAC;AAMX;;;;;kFAKkF;AAClF,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAY1D;AAUD;2EAC2E;AAC3E,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,cAAc,CA6EjF;AAED,gFAAgF;AAChF,UAAU,SAAS;IACjB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED;;;;;;;;;;;gEAWgE;AAChE,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC,CAAC;AAEnE;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,OAAO,EACjB,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,EACzB,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GACzB,UAAU,CA6CZ;AAED;;qBAEqB;AACrB,wBAAgB,eAAe,CAAC,CAAC,SAAS;IAAE,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAA;CAAE,EAC1E,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,EAClB,IAAI,EAAE,OAAO,GACZ;IAAE,KAAK,EAAE,CAAC,EAAE,CAAC;IAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CAYvD;AAQD,kEAAkE;AAClE,wBAAgB,aAAa,CAC3B,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,OAAO,EACjB,KAAK,EAAE,cAAc,GACpB,aAAa,EAAE,CA4BjB;AAED;;8DAE8D;AAC9D,wBAAgB,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,cAAc,GAAG,aAAa,EAAE,CAGrF"}
@@ -0,0 +1,295 @@
1
+ /** The selector half of a migration: what a rule MATCHES, as data.
2
+ *
3
+ * A patch addresses a known location; a migration has to find *every*
4
+ * occurrence of a legacy spelling, so selection is the half a plain patch
5
+ * format has none of. It is declarative for the same reason the operations
6
+ * are: an entry travels to a Rust and a Go kernel, and a predicate expressed
7
+ * in one language would mean one artifact is read two ways — invisibly, since
8
+ * a migration that succeeds is silent.
9
+ *
10
+ * **Containment is POSITIVE and required.** A rule states which document kinds
11
+ * it may touch (`inKind`) and which region of those documents it may reach
12
+ * into (`under`); nothing outside is reachable. The alternative — walk
13
+ * everything and subtract — cannot be made sound, because the set to subtract
14
+ * is unbounded: a `Run.Value` value, an `Assert.Equals` expected, any kind
15
+ * whose config carries a user JSON blob can hold something shaped like the
16
+ * node a rule looks for, and forgetting one corrupts a manifest with no
17
+ * diagnostic. A denylist also cannot express the guarantee the module surface
18
+ * is promised to carry — *a dependency can rename its own field and provably
19
+ * nothing else* — which is a statement about what a rule may reach, so it has
20
+ * to be said positively. `notUnder` remains for subtracting inside a region a
21
+ * rule legitimately reaches, which is a narrowing, not the containment itself.
22
+ *
23
+ * **`under` is ANCHORED at the document root**, not a set of key names to look
24
+ * for anywhere on the path. Anchoring is what makes the containment claim
25
+ * true: a `Telo.Definition`'s `resources:` template body carries other kinds'
26
+ * configuration, and any of it may hold a key spelled `schema` over data that
27
+ * merely looks like a schema — so "some segment of the path is `schema`" would
28
+ * reach the very user JSON blob the positive form exists to keep out, and
29
+ * would delete from it silently. Anchored, `under` names top-level document
30
+ * keys and a region is a genuine subtree.
31
+ *
32
+ * **`inSchema` is the one region a kind list cannot name.** An annotation
33
+ * keyword occurs in author-written JSON Schema, and schema fragments are not
34
+ * confined to kind documents: an inline `inputType:` / `outputType:` on ANY kind
35
+ * that declares one, an API route's `request.schema.body`, a `Telo.JsonSchema`'s
36
+ * `schema`. That set of kinds is open — any kind may declare a schema-valued
37
+ * field — and enumerating the standard library's would put resource-kind
38
+ * knowledge into the analyzer, against the topology-driven constraint. So a rule
39
+ * may instead state that it reaches only inside a SCHEMA REGION, named by the
40
+ * KERNEL's own schema-valued keys, which no kind owns.
41
+ *
42
+ * With `inSchema`, and only with it, `inKind` / `under` may be `["*"]` — and
43
+ * only for a rule keyed on a reserved `x-telo-*` annotation. That pairing IS the
44
+ * containment: the region gate bounds where the walk may go, and the reserved-key
45
+ * rule bounds what it may touch, since an `x-telo-*` key is Telo vocabulary
46
+ * wherever it appears and cannot mean something else in someone's config. Both
47
+ * halves are refused at entry-read time, so a module-shipped entry can no more
48
+ * spell `"*"` than it can name another module's kind.
49
+ *
50
+ * The residue is stated rather than claimed away: a manifest that asserts ABOUT
51
+ * a schema — a schema literal under a key spelled `schema` inside an assertion's
52
+ * expected value — is reachable, and would be rewritten into its own synonym.
53
+ * That cannot be closed in a data-only matcher without naming kinds. It is
54
+ * accepted because the sites the wildcards reach are exactly the ones no
55
+ * enumeration covers, and the alternative leaves an author reading a deprecation
56
+ * `telo migrate` refuses to act on.
57
+ *
58
+ * The vocabulary is closed, which is what makes it a trust boundary once
59
+ * module-shipped entries are aggregated beside core ones. An unrecognized key
60
+ * is refused rather than ignored — a selector that silently matches wider than
61
+ * it reads is the one failure this cannot tolerate. */
62
+ import { isInSchemaRegion } from "../schema-region.js";
63
+ /** The wildcard `inKind` / `under` value. Legal only alongside `inSchema`, and
64
+ * only for a rule keyed on an `x-telo-*` annotation. */
65
+ export const MATCH_ANY = "*";
66
+ export const MATCH_KEYS = [
67
+ "key",
68
+ "inKind",
69
+ "under",
70
+ "value",
71
+ "valueOneOf",
72
+ "withSibling",
73
+ "notUnder",
74
+ "inSchema",
75
+ ];
76
+ function isPlainObject(value) {
77
+ return typeof value === "object" && value !== null && !Array.isArray(value);
78
+ }
79
+ /** Structural equality over JSON values — what `value` / `valueOneOf` compare
80
+ * with, so a match on `true` never also matches `"true"`.
81
+ *
82
+ * Exported because the PATCHER asks the same question in the other direction:
83
+ * "is the value already what this would write". Both answers have to come from
84
+ * one rule, or a rule could match a spelling the patch then declares current. */
85
+ export function deepEquals(a, b) {
86
+ if (a === b)
87
+ return true;
88
+ if (Array.isArray(a) || Array.isArray(b)) {
89
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
90
+ return false;
91
+ return a.every((item, i) => deepEquals(item, b[i]));
92
+ }
93
+ if (isPlainObject(a) && isPlainObject(b)) {
94
+ const aKeys = Object.keys(a);
95
+ if (aKeys.length !== Object.keys(b).length)
96
+ return false;
97
+ return aKeys.every((k) => Object.hasOwn(b, k) && deepEquals(a[k], b[k]));
98
+ }
99
+ return false;
100
+ }
101
+ function requireStringList(describe, raw, key) {
102
+ const value = raw[key];
103
+ if (!Array.isArray(value) || value.length === 0 || value.some((v) => typeof v !== "string")) {
104
+ throw new Error(`${describe}: 'match.${key}' must be a non-empty sequence of strings`);
105
+ }
106
+ return value;
107
+ }
108
+ /** Read a rule's `match` block, refusing anything the vocabulary does not
109
+ * define. `describe` names the entry and rule so a failure says which. */
110
+ export function readMigrationMatch(describe, raw) {
111
+ if (!isPlainObject(raw))
112
+ throw new Error(`${describe}: 'match' must be a mapping`);
113
+ for (const key of Object.keys(raw)) {
114
+ if (!MATCH_KEYS.includes(key)) {
115
+ throw new Error(`${describe}: 'match' has no key '${key}'. Known keys: ${MATCH_KEYS.join(", ")}.`);
116
+ }
117
+ }
118
+ if (typeof raw.key !== "string" || raw.key.length === 0) {
119
+ throw new Error(`${describe}: 'match.key' must be a non-empty string`);
120
+ }
121
+ if (Object.hasOwn(raw, "value") && Object.hasOwn(raw, "valueOneOf")) {
122
+ throw new Error(`${describe}: 'match' takes at most one of 'value' or 'valueOneOf'`);
123
+ }
124
+ if (Object.hasOwn(raw, "valueOneOf") && !Array.isArray(raw.valueOneOf)) {
125
+ throw new Error(`${describe}: 'match.valueOneOf' must be a sequence`);
126
+ }
127
+ if (Object.hasOwn(raw, "withSibling") && typeof raw.withSibling !== "string") {
128
+ throw new Error(`${describe}: 'match.withSibling' must be a string`);
129
+ }
130
+ if (Object.hasOwn(raw, "notUnder") &&
131
+ (!Array.isArray(raw.notUnder) || raw.notUnder.some((k) => typeof k !== "string"))) {
132
+ throw new Error(`${describe}: 'match.notUnder' must be a sequence of strings`);
133
+ }
134
+ if (Object.hasOwn(raw, "inSchema") && typeof raw.inSchema !== "boolean") {
135
+ throw new Error(`${describe}: 'match.inSchema' must be a boolean`);
136
+ }
137
+ // The two conditions that make a wildcard safe, refused here rather than
138
+ // narrowed later: a rule the reader accepts is a rule the driver will run, and
139
+ // a selector that matches wider than it reads is exactly what this vocabulary
140
+ // exists to prevent. A module-shipped entry therefore cannot spell `"*"` at
141
+ // all, because it cannot satisfy both halves for a keyword it does not own.
142
+ const wildcards = [
143
+ ...(Array.isArray(raw.inKind) ? raw.inKind : []),
144
+ ...(Array.isArray(raw.under) ? raw.under : []),
145
+ ].filter((value) => value === MATCH_ANY);
146
+ if (wildcards.length > 0) {
147
+ if (raw.inSchema !== true) {
148
+ throw new Error(`${describe}: 'match.inKind' / 'match.under' may only be '${MATCH_ANY}' together with ` +
149
+ `'inSchema: true' — the schema region is what bounds a walk the wildcard unbounds`);
150
+ }
151
+ if (!raw.key.startsWith("x-telo-")) {
152
+ throw new Error(`${describe}: '${MATCH_ANY}' is legal only for a rule keyed on an 'x-telo-*' annotation, ` +
153
+ `which is Telo vocabulary wherever it appears — '${raw.key}' could mean something else ` +
154
+ `inside a resource's own configuration`);
155
+ }
156
+ }
157
+ const match = {
158
+ key: raw.key,
159
+ inKind: requireStringList(describe, raw, "inKind"),
160
+ under: requireStringList(describe, raw, "under"),
161
+ };
162
+ if (Object.hasOwn(raw, "value"))
163
+ match.value = raw.value;
164
+ if (Object.hasOwn(raw, "valueOneOf"))
165
+ match.valueOneOf = raw.valueOneOf;
166
+ if (Object.hasOwn(raw, "withSibling"))
167
+ match.withSibling = raw.withSibling;
168
+ if (Object.hasOwn(raw, "notUnder"))
169
+ match.notUnder = raw.notUnder;
170
+ if (raw.inSchema === true)
171
+ match.inSchema = true;
172
+ return match;
173
+ }
174
+ /**
175
+ * Index `document`'s candidate sites for `keys`, descending only into the
176
+ * top-level regions named by `roots`.
177
+ *
178
+ * The caller has already gated on `inKind` — see `selectMatches`, which repeats
179
+ * the check because it holds the individual rule.
180
+ */
181
+ export function buildMatchIndex(document, keys, roots) {
182
+ const index = new Map();
183
+ if (keys.size === 0 || roots.size === 0 || !isPlainObject(document))
184
+ return index;
185
+ // A mutable stack, materialized into an array only when a site is recorded.
186
+ const stack = [];
187
+ const record = (key, value, parent) => {
188
+ const bucket = index.get(key) ?? [];
189
+ bucket.push({ path: [...stack], value, parent });
190
+ index.set(key, bucket);
191
+ };
192
+ const walk = (node) => {
193
+ if (Array.isArray(node)) {
194
+ for (let i = 0; i < node.length; i++) {
195
+ stack.push(i);
196
+ walk(node[i]);
197
+ stack.pop();
198
+ }
199
+ return;
200
+ }
201
+ if (!isPlainObject(node))
202
+ return;
203
+ for (const [key, value] of Object.entries(node)) {
204
+ stack.push(key);
205
+ if (keys.has(key))
206
+ record(key, value, node);
207
+ walk(value);
208
+ stack.pop();
209
+ }
210
+ };
211
+ // Anchored containment, enforced by where the walk STARTS: everything the
212
+ // index holds is at or below a top-level key some rule named. A wildcard root
213
+ // starts everywhere instead — bounded not by where the walk begins but by the
214
+ // schema region and the reserved key its rule had to declare to earn it.
215
+ const everywhere = roots.has(MATCH_ANY);
216
+ for (const [key, value] of Object.entries(document)) {
217
+ if (!everywhere && !roots.has(key))
218
+ continue;
219
+ stack.push(key);
220
+ if (keys.has(key))
221
+ record(key, value, document);
222
+ walk(value);
223
+ stack.pop();
224
+ }
225
+ return index;
226
+ }
227
+ /** The rules in `entries` that may match a document of `kind`, with the key and
228
+ * root sets their shared index needs. Empty rules mean the document is not
229
+ * walked at all. */
230
+ export function applicableRules(rules, kind) {
231
+ const applicable = [];
232
+ const keys = new Set();
233
+ const roots = new Set();
234
+ if (typeof kind !== "string")
235
+ return { rules: applicable, keys, roots };
236
+ for (const rule of rules) {
237
+ if (!rule.match.inKind.includes(kind) && !rule.match.inKind.includes(MATCH_ANY))
238
+ continue;
239
+ applicable.push(rule);
240
+ keys.add(rule.match.key);
241
+ for (const root of rule.match.under)
242
+ roots.add(root);
243
+ }
244
+ return { rules: applicable, keys, roots };
245
+ }
246
+ function valueMatches(match, value) {
247
+ if (Object.hasOwn(match, "value"))
248
+ return deepEquals(match.value, value);
249
+ if (match.valueOneOf)
250
+ return match.valueOneOf.some((candidate) => deepEquals(candidate, value));
251
+ return true;
252
+ }
253
+ /** The sites in `index` this match selects, in document order. */
254
+ export function selectMatches(index, document, match) {
255
+ const kind = isPlainObject(document) ? document.kind : undefined;
256
+ if (typeof kind !== "string")
257
+ return [];
258
+ if (!match.inKind.includes(kind) && !match.inKind.includes(MATCH_ANY))
259
+ return [];
260
+ const sites = index.get(match.key);
261
+ if (!sites)
262
+ return [];
263
+ const anyRoot = match.under.includes(MATCH_ANY);
264
+ const out = [];
265
+ for (const site of sites) {
266
+ // The index may be shared with rules naming other regions, so the anchor is
267
+ // re-checked per rule. A numeric first segment cannot occur — a document is
268
+ // a mapping — but the guard keeps the containment claim independent of that.
269
+ const anchor = site.path[0];
270
+ if (!anyRoot && (typeof anchor !== "string" || !match.under.includes(anchor)))
271
+ continue;
272
+ // The schema region — the same ancestry rule every schema-bounded surface
273
+ // reads, so the migration and the validators cannot disagree about where a
274
+ // schema is.
275
+ if (match.inSchema && !isInSchemaRegion(site.path))
276
+ continue;
277
+ // `notUnder` subtracts within the region, so it reads the whole path. A
278
+ // numeric segment never equals a key name, so the raw path is enough.
279
+ if (match.notUnder?.some((segment) => site.path.includes(segment)))
280
+ continue;
281
+ if (!valueMatches(match, site.value))
282
+ continue;
283
+ if (match.withSibling !== undefined && !Object.hasOwn(site.parent, match.withSibling))
284
+ continue;
285
+ out.push(site.path);
286
+ }
287
+ return out;
288
+ }
289
+ /** Convenience for a single match against a document — builds a one-rule index.
290
+ * The driver uses `applicableRules` + `buildMatchIndex` + `selectMatches` so
291
+ * one walk serves every rule that can reach the document. */
292
+ export function findMatches(document, match) {
293
+ const index = buildMatchIndex(document, new Set([match.key]), new Set(match.under));
294
+ return selectMatches(index, document, match);
295
+ }
@@ -0,0 +1,68 @@
1
+ /** Planning and applying a migration patch.
2
+ *
3
+ * Planning is separate from applying because a patch is ALL-OR-NOTHING at each
4
+ * match: a rule whose second operation cannot apply must not leave the first
5
+ * one's edit behind. `planPatch` resolves every operation against the tree
6
+ * first and returns either a concrete effect list or one refusal; only then
7
+ * does an applier run. The same plan drives both appliers — the JSON tree the
8
+ * loader hands the analyzer, and the YAML document `telo migrate` writes — so
9
+ * the in-memory rewrite and the on-disk repair cannot disagree about what an
10
+ * operation means. */
11
+ import type { MigrationOperation, MigrationPath, MigrationRefusal } from "./types.js";
12
+ /** A resolved, unconditionally applicable edit. */
13
+ export type MigrationEffect = {
14
+ readonly kind: "rename-key";
15
+ readonly parent: MigrationPath;
16
+ readonly from: string;
17
+ readonly to: string;
18
+ } | {
19
+ readonly kind: "set-value";
20
+ readonly path: MigrationPath;
21
+ readonly value: unknown;
22
+ } | {
23
+ readonly kind: "set-tag";
24
+ readonly path: MigrationPath;
25
+ readonly tag: string;
26
+ readonly source: string;
27
+ } | {
28
+ readonly kind: "insert-item";
29
+ readonly path: MigrationPath;
30
+ readonly index: number;
31
+ readonly value: unknown;
32
+ } | {
33
+ readonly kind: "remove-entry";
34
+ readonly path: MigrationPath;
35
+ };
36
+ export interface PatchPlan {
37
+ readonly effects: readonly MigrationEffect[];
38
+ /** Where the matched node lives after the patch. Differs from the matched
39
+ * path only when a `rename-key` moved it. */
40
+ readonly finalPath: MigrationPath;
41
+ /** The value at the match before anything was applied — the "before" half of
42
+ * the generated diagnostic sentence. */
43
+ readonly before: unknown;
44
+ /** The value at the match after the patch, or `undefined` for a removal. */
45
+ readonly after: unknown;
46
+ }
47
+ export type PatchPlanResult = {
48
+ readonly ok: true;
49
+ readonly plan: PatchPlan;
50
+ } | {
51
+ readonly ok: false;
52
+ readonly refusal: MigrationRefusal;
53
+ };
54
+ /**
55
+ * Resolve `ops` at `path` against `root`, or refuse.
56
+ *
57
+ * Refusing is the design's safety valve: the node is left exactly as the author
58
+ * wrote it and the ordinary validator reports it with an accurate message,
59
+ * rather than the migration guessing or dropping a value.
60
+ */
61
+ export declare function planPatch(root: unknown, path: MigrationPath, ops: readonly MigrationOperation[]): PatchPlanResult;
62
+ /** Apply a plan to the in-memory manifest tree. Mutates in place — the tree is
63
+ * the loader's own projection of the document, never the author's file. */
64
+ export declare function applyEffectsToTree(root: unknown, effects: readonly MigrationEffect[]): void;
65
+ /** Dotted rendering of a path, in the form the position index and every
66
+ * diagnostic's `data.path` use: `routes[0].handler`. */
67
+ export declare function formatMigrationPath(path: MigrationPath): string;
68
+ //# sourceMappingURL=patch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patch.d.ts","sourceRoot":"","sources":["../../src/migrations/patch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;uBASuB;AAIvB,OAAO,KAAK,EACV,kBAAkB,EAClB,aAAa,EACb,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB,mDAAmD;AACnD,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC3G;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACrF;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACzG;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC/G;IAAE,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC;AAEpE,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C;kDAC8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC;6CACyC;IACzC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC/C;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAoD/D;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,aAAa,EACnB,GAAG,EAAE,SAAS,kBAAkB,EAAE,GACjC,eAAe,CA4FjB;AAED;4EAC4E;AAC5E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,eAAe,EAAE,GAAG,IAAI,CA6C3F;AAUD;yDACyD;AACzD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAO/D"}