@telorun/analyzer 0.60.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/analyzer.js +130 -9
  3. package/dist/artifact-layer-index.d.ts +13 -5
  4. package/dist/artifact-layer-index.d.ts.map +1 -1
  5. package/dist/artifact-layer-index.js +37 -13
  6. package/dist/artifact-selector.d.ts +10 -3
  7. package/dist/artifact-selector.d.ts.map +1 -1
  8. package/dist/artifact-selector.js +10 -1
  9. package/dist/builtins.d.ts.map +1 -1
  10. package/dist/builtins.js +97 -13
  11. package/dist/cel-bindings.d.ts +0 -6
  12. package/dist/cel-bindings.d.ts.map +1 -1
  13. package/dist/cel-bindings.js +3 -28
  14. package/dist/definition-registry.d.ts +17 -0
  15. package/dist/definition-registry.d.ts.map +1 -1
  16. package/dist/definition-registry.js +31 -2
  17. package/dist/identifier-name.d.ts +114 -0
  18. package/dist/identifier-name.d.ts.map +1 -0
  19. package/dist/identifier-name.js +183 -0
  20. package/dist/index.d.ts +14 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +11 -2
  23. package/dist/manifest-schemas.d.ts +81 -0
  24. package/dist/manifest-schemas.d.ts.map +1 -1
  25. package/dist/manifest-schemas.js +208 -6
  26. package/dist/module-file-claims.d.ts +7 -0
  27. package/dist/module-file-claims.d.ts.map +1 -1
  28. package/dist/module-file-claims.js +22 -2
  29. package/dist/module-library.d.ts +97 -0
  30. package/dist/module-library.d.ts.map +1 -0
  31. package/dist/module-library.js +162 -0
  32. package/dist/requires-block.d.ts +125 -0
  33. package/dist/requires-block.d.ts.map +1 -0
  34. package/dist/requires-block.js +182 -0
  35. package/dist/schema-keywords.d.ts +68 -0
  36. package/dist/schema-keywords.d.ts.map +1 -0
  37. package/dist/schema-keywords.js +324 -0
  38. package/dist/schema-region.d.ts +12 -1
  39. package/dist/schema-region.d.ts.map +1 -1
  40. package/dist/schema-region.js +12 -1
  41. package/dist/telo-version.d.ts +3 -0
  42. package/dist/telo-version.d.ts.map +1 -0
  43. package/dist/telo-version.js +8 -0
  44. package/dist/types.d.ts +31 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/validate-identifier-names.d.ts +31 -0
  47. package/dist/validate-identifier-names.d.ts.map +1 -0
  48. package/dist/validate-identifier-names.js +144 -0
  49. package/dist/validate-module-artifact.d.ts.map +1 -1
  50. package/dist/validate-module-artifact.js +53 -1
  51. package/dist/validate-observed-state.d.ts +9 -2
  52. package/dist/validate-observed-state.d.ts.map +1 -1
  53. package/dist/validate-observed-state.js +9 -2
  54. package/dist/validate-references.d.ts.map +1 -1
  55. package/dist/validate-references.js +5 -26
  56. package/dist/validate-requires.d.ts +49 -0
  57. package/dist/validate-requires.d.ts.map +1 -0
  58. package/dist/validate-requires.js +99 -0
  59. package/dist/value-type-keyword.d.ts +1 -1
  60. package/dist/value-type-keyword.d.ts.map +1 -1
  61. package/dist/value-type-keyword.js +1 -0
  62. package/dist/version-range.d.ts +88 -0
  63. package/dist/version-range.d.ts.map +1 -0
  64. package/dist/version-range.js +173 -0
  65. package/package.json +2 -2
  66. package/src/analyzer.ts +146 -10
  67. package/src/artifact-layer-index.ts +47 -14
  68. package/src/artifact-selector.ts +15 -4
  69. package/src/builtins.ts +102 -13
  70. package/src/cel-bindings.ts +3 -28
  71. package/src/definition-registry.ts +30 -2
  72. package/src/identifier-name.ts +228 -0
  73. package/src/index.ts +44 -1
  74. package/src/manifest-schemas.ts +223 -4
  75. package/src/module-file-claims.ts +32 -2
  76. package/src/module-library.ts +208 -0
  77. package/src/requires-block.ts +253 -0
  78. package/src/schema-keywords.ts +359 -0
  79. package/src/schema-region.ts +12 -1
  80. package/src/telo-version.ts +9 -0
  81. package/src/types.ts +32 -0
  82. package/src/validate-identifier-names.ts +173 -0
  83. package/src/validate-module-artifact.ts +56 -0
  84. package/src/validate-observed-state.ts +9 -2
  85. package/src/validate-references.ts +5 -26
  86. package/src/validate-requires.ts +129 -0
  87. package/src/value-type-keyword.ts +1 -0
  88. package/src/version-range.ts +238 -0
@@ -236,6 +236,64 @@ export declare const InvokeStepSchema: {
236
236
  };
237
237
  };
238
238
  };
239
+ /**
240
+ * A JSON Schema an author writes as a manifest VALUE — an `inputType:`, an
241
+ * `outputType:`, a `status:` block, an API route's `request.schema`.
242
+ *
243
+ * Declared so the surfaces that read a slot's schema learn what lives there. A
244
+ * slot spelled `type: object` told them "some object": completion offered
245
+ * nothing from the first key down, hover had nothing to show, and a misspelled
246
+ * keyword travelled to a runtime failure that named the wrong field.
247
+ *
248
+ * OPEN, deliberately (`additionalProperties: true`, `type` admitting the
249
+ * boolean form a nested `additionalProperties: false` takes). The keyword set
250
+ * below is draft-07's — the dialect AJV actually runs — and everything outside
251
+ * it, every `x-telo-*` annotation included, passes through untouched. Closing
252
+ * this would reject the next annotation the moment a module invented one, for a
253
+ * check nobody asked for; what it buys as it stands is the value of a keyword
254
+ * an author DID write (`required: "name"`, `type: 5`) being wrong at
255
+ * `telo check` rather than at dispatch.
256
+ *
257
+ * RECURSIVE, which is why it is not expanded in place like the fragments above
258
+ * it: a schema's properties hold schemas. `expandManifestFragments` rewrites a
259
+ * reference to this to the document-local `#/$defs/JsonSchema7` and hoists one
260
+ * copy to the enclosing schema's root, so the pointer resolves inside whatever
261
+ * AJV compiles, and the editor's local-only `$ref` resolver keeps working.
262
+ */
263
+ export declare const JsonSchema7Schema: {
264
+ title: string;
265
+ description: string;
266
+ type: string[];
267
+ properties: import("./schema-keywords.js").SchemaKeywords;
268
+ additionalProperties: boolean;
269
+ };
270
+ /**
271
+ * The schema a KIND declares for its own configuration — a `Telo.Definition`'s
272
+ * `schema:` block.
273
+ *
274
+ * The same body as {@link JsonSchema7Schema}, under its own name because the
275
+ * name is the discriminator: a kind's schema is where the `x-telo-*` vocabulary
276
+ * belongs (`x-telo-eval`, `x-telo-ref`, `x-telo-scope`, …) and a plain data
277
+ * schema is where it does not. Completion reads the `x-telo-fragment` stamp to
278
+ * decide which vocabulary to offer, exactly as a retry-budget consumer reads
279
+ * which of `RetryPolicy` / `RetryAttempts` a slot pointed at.
280
+ *
281
+ * The annotations are NOT properties here — see `schema-keywords.ts` for why a
282
+ * literal `x-telo-*` key inside a hoisted `properties` map would read to the
283
+ * annotation walkers as a slot the author never wrote.
284
+ */
285
+ export declare const KindSchemaSchema: {
286
+ title: string;
287
+ description: string;
288
+ type: string[];
289
+ properties: import("./schema-keywords.js").SchemaKeywords;
290
+ additionalProperties: boolean;
291
+ };
292
+ /** True when a slot's `x-telo-fragment` stamp says it holds author-written JSON
293
+ * Schema — a kind's `schema:`, a `status:` block, a `Telo.JsonSchema`'s own
294
+ * `schema`. The one accessor every consumer asks, so no surface re-spells the
295
+ * set. */
296
+ export declare function isSchemaFragment(name: string | undefined): boolean;
239
297
  /** Root schema registered with AJV under {@link MANIFEST_SCHEMA_URI}. Carries
240
298
  * `$defs` only — it isn't validated against directly. */
241
299
  export declare const ManifestRootSchema: {
@@ -371,6 +429,20 @@ export declare const ManifestRootSchema: {
371
429
  };
372
430
  };
373
431
  };
432
+ JsonSchema7: {
433
+ title: string;
434
+ description: string;
435
+ type: string[];
436
+ properties: import("./schema-keywords.js").SchemaKeywords;
437
+ additionalProperties: boolean;
438
+ };
439
+ KindSchema: {
440
+ title: string;
441
+ description: string;
442
+ type: string[];
443
+ properties: import("./schema-keywords.js").SchemaKeywords;
444
+ additionalProperties: boolean;
445
+ };
374
446
  };
375
447
  };
376
448
  /** A private, expanded copy of a fragment, for a consumer that must EMBED one
@@ -398,6 +470,15 @@ export declare function manifestFragment(name: string): Record<string, unknown>;
398
470
  * loaded manifest see the same expanded shape.
399
471
  */
400
472
  export declare function expandManifestFragments(node: unknown, seen?: Set<object>): void;
473
+ /**
474
+ * Expand fragments in a schema that never passes through the loader —
475
+ * `builtins.ts` is not a manifest, so its slots have to arrive already
476
+ * localized, with the hoisted `$defs` at the root of the schema AJV compiles.
477
+ *
478
+ * Returns the same object, expanded in place, for use as a declaration
479
+ * initializer.
480
+ */
481
+ export declare function withSchemaFragments<T extends Record<string, unknown>>(schema: T): T;
401
482
  /** Stamped by {@link expandManifestFragments} with the name of the shared
402
483
  * fragment a slot pointed at. Derived, never author-written. */
403
484
  export declare const X_TELO_FRAGMENT = "x-telo-fragment";
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-schemas.d.ts","sourceRoot":"","sources":["../src/manifest-schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD,sEAAsE;AACtE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yEA8ByE;AACzE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoB7B,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwD7B,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB;;;;;;CAM/B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8C5B,CAAC;AAiBF;0DAC0D;AAC1D,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAQ9B,CAAC;AAIF;;;0EAG0E;AAC1E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAStE;AAID;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,cAAoB,GAAG,IAAI,CAoBrF;AA+DD;iEACiE;AACjE,eAAO,MAAM,eAAe,oBAAoB,CAAC;AAEjD;uDACuD;AACvD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAItE"}
1
+ {"version":3,"file":"manifest-schemas.d.ts","sourceRoot":"","sources":["../src/manifest-schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAKH,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD,sEAAsE;AACtE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yEA8ByE;AACzE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoB7B,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwD7B,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB;;;;;;CAM/B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8C5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,iBAAiB;;;;;;CAM7B,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB;;;;;;CAO5B,CAAC;AAUF;;;WAGW;AACX,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAElE;AAiCD;0DAC0D;AAC1D,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAU9B,CAAC;AAIF;;;0EAG0E;AAC1E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmBtE;AAID;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,cAAoB,GAAG,IAAI,CAGrF;AA0HD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAGnF;AA+DD;iEACiE;AACjE,eAAO,MAAM,eAAe,oBAAoB,CAAC;AAEjD;uDACuD;AACvD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAItE"}
@@ -27,6 +27,8 @@
27
27
  * Adding a fragment means putting it under `$defs` in {@link ManifestRootSchema}
28
28
  * and `$ref`-ing it from module schemas. Browser-safe: no Node built-ins.
29
29
  */
30
+ import { jsonSchemaKeywords } from "./schema-keywords.js";
31
+ import { SCHEMA_REGION_KEYS } from "./schema-region.js";
30
32
  export const MANIFEST_SCHEMA_URI = "telo://manifest";
31
33
  /** `$ref` to a fragment in this set, as a module schema writes it. */
32
34
  export function manifestFragmentRef(name) {
@@ -236,6 +238,88 @@ export const InvokeStepSchema = {
236
238
  },
237
239
  },
238
240
  };
241
+ /**
242
+ * A JSON Schema an author writes as a manifest VALUE — an `inputType:`, an
243
+ * `outputType:`, a `status:` block, an API route's `request.schema`.
244
+ *
245
+ * Declared so the surfaces that read a slot's schema learn what lives there. A
246
+ * slot spelled `type: object` told them "some object": completion offered
247
+ * nothing from the first key down, hover had nothing to show, and a misspelled
248
+ * keyword travelled to a runtime failure that named the wrong field.
249
+ *
250
+ * OPEN, deliberately (`additionalProperties: true`, `type` admitting the
251
+ * boolean form a nested `additionalProperties: false` takes). The keyword set
252
+ * below is draft-07's — the dialect AJV actually runs — and everything outside
253
+ * it, every `x-telo-*` annotation included, passes through untouched. Closing
254
+ * this would reject the next annotation the moment a module invented one, for a
255
+ * check nobody asked for; what it buys as it stands is the value of a keyword
256
+ * an author DID write (`required: "name"`, `type: 5`) being wrong at
257
+ * `telo check` rather than at dispatch.
258
+ *
259
+ * RECURSIVE, which is why it is not expanded in place like the fragments above
260
+ * it: a schema's properties hold schemas. `expandManifestFragments` rewrites a
261
+ * reference to this to the document-local `#/$defs/JsonSchema7` and hoists one
262
+ * copy to the enclosing schema's root, so the pointer resolves inside whatever
263
+ * AJV compiles, and the editor's local-only `$ref` resolver keeps working.
264
+ */
265
+ export const JsonSchema7Schema = {
266
+ title: "JSON Schema",
267
+ description: "The shape of a value, as JSON Schema (draft-07).",
268
+ type: ["object", "boolean"],
269
+ properties: jsonSchemaKeywords(hoistedDefKey("JsonSchema7")),
270
+ additionalProperties: true,
271
+ };
272
+ /**
273
+ * The schema a KIND declares for its own configuration — a `Telo.Definition`'s
274
+ * `schema:` block.
275
+ *
276
+ * The same body as {@link JsonSchema7Schema}, under its own name because the
277
+ * name is the discriminator: a kind's schema is where the `x-telo-*` vocabulary
278
+ * belongs (`x-telo-eval`, `x-telo-ref`, `x-telo-scope`, …) and a plain data
279
+ * schema is where it does not. Completion reads the `x-telo-fragment` stamp to
280
+ * decide which vocabulary to offer, exactly as a retry-budget consumer reads
281
+ * which of `RetryPolicy` / `RetryAttempts` a slot pointed at.
282
+ *
283
+ * The annotations are NOT properties here — see `schema-keywords.ts` for why a
284
+ * literal `x-telo-*` key inside a hoisted `properties` map would read to the
285
+ * annotation walkers as a slot the author never wrote.
286
+ */
287
+ export const KindSchemaSchema = {
288
+ title: "Kind schema",
289
+ description: "The configuration a resource of this kind accepts, as JSON Schema plus the `x-telo-*` annotations that say how each field behaves.",
290
+ type: ["object", "boolean"],
291
+ properties: jsonSchemaKeywords(hoistedDefKey("KindSchema")),
292
+ additionalProperties: true,
293
+ };
294
+ /** The fragments that describe author-written JSON Schema, whichever vocabulary
295
+ * they admit. They are also exactly the RECURSIVE ones — a schema is the only
296
+ * shape in this set that contains itself — so a reference to one is localized
297
+ * and hoisted rather than expanded in place ({@link localizeRecursiveFragment});
298
+ * should a self-containing fragment that is not a schema ever land, the two
299
+ * ideas split and this set stays the one about schemas. */
300
+ const SCHEMA_FRAGMENTS = new Set(["JsonSchema7", "KindSchema"]);
301
+ /** True when a slot's `x-telo-fragment` stamp says it holds author-written JSON
302
+ * Schema — a kind's `schema:`, a `status:` block, a `Telo.JsonSchema`'s own
303
+ * `schema`. The one accessor every consumer asks, so no surface re-spells the
304
+ * set. */
305
+ export function isSchemaFragment(name) {
306
+ return name !== undefined && SCHEMA_FRAGMENTS.has(name);
307
+ }
308
+ /**
309
+ * The `$defs` key a hoisted fragment is written under, and the one its own
310
+ * self-reference points at.
311
+ *
312
+ * NAMESPACED so a collision is unrepresentable rather than diagnosable: `$defs`
313
+ * is the author's namespace, and a kind declaring its own `$defs: { KindSchema:
314
+ * … }` beside a slot pointing at the fragment would otherwise have its shape
315
+ * silently validate every such slot — a wrong-but-plausible validation, which is
316
+ * worse than a loud failure and impossible to see. With a reserved key the two
317
+ * coexist, and "already present" can only mean a previous hoist of the same
318
+ * fragment, which is what makes {@link hoistFragmentDef}'s skip provably safe.
319
+ */
320
+ function hoistedDefKey(name) {
321
+ return `telo:${name}`;
322
+ }
239
323
  /** Recursively freeze, so the fragment set cannot be edited through any of the
240
324
  * references handed out. `fragmentFor` clones precisely because downstream
241
325
  * passes rewrite schemas in place — `resolveSchemaRefKinds` rewrites the very
@@ -260,6 +344,8 @@ export const ManifestRootSchema = {
260
344
  RetryPolicy: RetryPolicySchema,
261
345
  RetryAttempts: RetryAttemptsSchema,
262
346
  InvokeStep: InvokeStepSchema,
347
+ JsonSchema7: JsonSchema7Schema,
348
+ KindSchema: KindSchemaSchema,
263
349
  },
264
350
  };
265
351
  deepFreeze(ManifestRootSchema);
@@ -272,6 +358,14 @@ export function manifestFragment(name) {
272
358
  if (!fragment || typeof fragment !== "object") {
273
359
  throw new Error(`Unknown manifest fragment '${name}'`);
274
360
  }
361
+ if (SCHEMA_FRAGMENTS.has(name)) {
362
+ // A recursive fragment has no expanded form — that is the whole reason it is
363
+ // localized instead. Embedding one would hand the consumer a body whose
364
+ // `#/$defs` pointers resolve against nothing.
365
+ throw new Error(`Manifest fragment '${name}' is recursive and cannot be embedded — ` +
366
+ `point a slot at it with $ref: "${manifestFragmentRef(name)}" and pass the ` +
367
+ `enclosing schema through withSchemaFragments().`);
368
+ }
275
369
  const copy = structuredClone(fragment);
276
370
  expandManifestFragments(copy);
277
371
  copy[X_TELO_FRAGMENT] = name;
@@ -298,6 +392,26 @@ const FRAGMENT_PREFIX = `${MANIFEST_SCHEMA_URI}#/$defs/`;
298
392
  * loaded manifest see the same expanded shape.
299
393
  */
300
394
  export function expandManifestFragments(node, seen = new Set()) {
395
+ if (!node || typeof node !== "object")
396
+ return;
397
+ walkFragments(node, seen, { hoistTarget: node, depth: 0 });
398
+ }
399
+ /**
400
+ * Descend one key. A TOP-LEVEL schema-valued key (`schema:`, `inputType:`,
401
+ * `status:`, …) opens a new hoist target, because that node is what a validator
402
+ * compiles: a `#/$defs/…` pointer written below it resolves against IT, not
403
+ * against the document. Depth-anchored for the reason migrations anchor `under`
404
+ * at top-level keys — a slot DESCRIBING a field named `schema` is not itself a
405
+ * schema region, and treating it as one would scatter `$defs` blocks into the
406
+ * middle of a kind's property map.
407
+ */
408
+ function childContext(ctx, key, value) {
409
+ if (ctx.depth === 0 && SCHEMA_REGION_KEYS.includes(key) && isPlainObject(value)) {
410
+ return { hoistTarget: value, depth: 1 };
411
+ }
412
+ return { hoistTarget: ctx.hoistTarget, depth: ctx.depth + 1 };
413
+ }
414
+ function walkFragments(node, seen, ctx) {
301
415
  if (!node || typeof node !== "object")
302
416
  return;
303
417
  if (seen.has(node))
@@ -305,23 +419,111 @@ export function expandManifestFragments(node, seen = new Set()) {
305
419
  seen.add(node);
306
420
  if (Array.isArray(node)) {
307
421
  for (let i = 0; i < node.length; i++) {
308
- const fragment = fragmentFor(node[i]);
309
- if (fragment)
310
- node[i] = fragment;
311
- else
312
- expandManifestFragments(node[i], seen);
422
+ const localized = localizeRecursiveFragment(node[i], ctx);
423
+ if (localized)
424
+ node[i] = localized;
425
+ else {
426
+ const fragment = fragmentFor(node[i]);
427
+ if (fragment)
428
+ node[i] = fragment;
429
+ else
430
+ walkFragments(node[i], seen, { ...ctx, depth: ctx.depth + 1 });
431
+ }
313
432
  }
314
433
  return;
315
434
  }
316
435
  const obj = node;
317
436
  for (const [key, value] of Object.entries(obj)) {
437
+ const localized = localizeRecursiveFragment(value, ctx);
438
+ if (localized) {
439
+ obj[key] = localized;
440
+ continue;
441
+ }
318
442
  const fragment = fragmentFor(value);
319
443
  if (fragment)
320
444
  obj[key] = fragment;
321
445
  else
322
- expandManifestFragments(value, seen);
446
+ walkFragments(value, seen, childContext(ctx, key, value));
447
+ }
448
+ }
449
+ /**
450
+ * Rewrite a reference to a RECURSIVE fragment to the document-local pointer,
451
+ * hoisting one copy of the fragment into the enclosing schema's `$defs`.
452
+ *
453
+ * Localized rather than inlined because the shape contains itself: inlining
454
+ * cannot terminate, which is what the "closed, non-recursive set" caveat above
455
+ * says. Localized rather than left pointing at `telo://manifest` because two
456
+ * consumers cannot follow a foreign URI — the editor's resolver throws on any
457
+ * `$ref` that does not start with `#/`, and that throw takes the whole canvas
458
+ * down, exactly as gating fragment expansion once did.
459
+ *
460
+ * Siblings written beside the `$ref` are kept, but draft-07 makes `$ref`
461
+ * exclusive, so they reach the human surfaces (completion, hover, the editor)
462
+ * and not AJV. A slot adds a `title` / `description` that way; a slot narrowing
463
+ * the shape would be silently ignored and should declare its own schema.
464
+ */
465
+ function localizeRecursiveFragment(value, ctx) {
466
+ if (!isPlainObject(value))
467
+ return undefined;
468
+ const ref = value.$ref;
469
+ if (typeof ref !== "string" || !ref.startsWith(FRAGMENT_PREFIX))
470
+ return undefined;
471
+ const name = ref.slice(FRAGMENT_PREFIX.length);
472
+ if (!SCHEMA_FRAGMENTS.has(name))
473
+ return undefined;
474
+ hoistFragmentDef(ctx.hoistTarget, name);
475
+ return { ...value, $ref: `#/$defs/${hoistedDefKey(name)}`, [X_TELO_FRAGMENT]: name };
476
+ }
477
+ /** Add `name` (and anything it references in turn) to `target.$defs`, cloned —
478
+ * the fragment set is frozen, and later passes rewrite schemas in place. */
479
+ function hoistFragmentDef(target, name) {
480
+ const defs = (target.$defs ??= {});
481
+ const pending = [name];
482
+ while (pending.length > 0) {
483
+ const next = pending.pop();
484
+ const key = hoistedDefKey(next);
485
+ if (defs[key])
486
+ continue;
487
+ const fragment = ManifestRootSchema.$defs[next];
488
+ if (!fragment || typeof fragment !== "object")
489
+ continue;
490
+ // Stamped on the hoisted body, not only on the slot that pointed at it: a
491
+ // schema nests, so the node a consumer resolves two levels down is the
492
+ // fragment reached through its own self-reference, with no slot in sight.
493
+ // That node is exactly where an annotation like `x-telo-eval` is written,
494
+ // so it is where the stamp has to be readable.
495
+ defs[key] = { ...structuredClone(fragment), [X_TELO_FRAGMENT]: next };
496
+ for (const nested of SCHEMA_FRAGMENTS) {
497
+ if (nested !== next && referencesLocalDef(defs[key], nested))
498
+ pending.push(nested);
499
+ }
323
500
  }
324
501
  }
502
+ /** Does a hoisted fragment point at another one? Cheap and structural: the set
503
+ * is small and a missed edge would leave an unresolvable pointer, which AJV
504
+ * reports loudly rather than silently skipping. */
505
+ function referencesLocalDef(node, name) {
506
+ if (!node || typeof node !== "object")
507
+ return false;
508
+ if (Array.isArray(node))
509
+ return node.some((item) => referencesLocalDef(item, name));
510
+ const obj = node;
511
+ if (obj.$ref === `#/$defs/${hoistedDefKey(name)}`)
512
+ return true;
513
+ return Object.values(obj).some((value) => referencesLocalDef(value, name));
514
+ }
515
+ /**
516
+ * Expand fragments in a schema that never passes through the loader —
517
+ * `builtins.ts` is not a manifest, so its slots have to arrive already
518
+ * localized, with the hoisted `$defs` at the root of the schema AJV compiles.
519
+ *
520
+ * Returns the same object, expanded in place, for use as a declaration
521
+ * initializer.
522
+ */
523
+ export function withSchemaFragments(schema) {
524
+ expandManifestFragments(schema);
525
+ return schema;
526
+ }
325
527
  /**
326
528
  * The fragment a node references, expanded and merged with whatever the node
327
529
  * declared beside the `$ref`, or undefined when it references none.
@@ -42,6 +42,13 @@ interface ClaimBase {
42
42
  * controller claim with no selector that nothing would reject.
43
43
  */
44
44
  export type ModuleFileClaim = (ClaimBase & {
45
+ readonly role: "library";
46
+ readonly selector: ArtifactSelector;
47
+ /** The bare specifier a consumer's bundle imports this entry point by. */
48
+ readonly specifier: string;
49
+ /** The source `path` was built from, as on a controller claim. */
50
+ readonly localPath?: string;
51
+ }) | (ClaimBase & {
45
52
  readonly role: "controller";
46
53
  readonly selector: ArtifactSelector;
47
54
  /** Extra payload patterns that belong in the same layer as this claim —
@@ -1 +1 @@
1
- {"version":3,"file":"module-file-claims.d.ts","sourceRoot":"","sources":["../src/module-file-claims.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,wBAAwB,EAC9B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAuC,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAEpG;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,UAAU,SAAS;IACjB;;;uEAGmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;6CACyC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GACvB,CAAC,SAAS,GAAG;IACX,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC;;kEAE8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;mDAI+C;IAC/C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC,GACF,CAAC,SAAS,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,CAAC;AAqF9C;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,MAAM,EACpB,QAAQ,GAAE,wBAA4C,GACrD,eAAe,EAAE,CAanB"}
1
+ {"version":3,"file":"module-file-claims.d.ts","sourceRoot":"","sources":["../src/module-file-claims.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,wBAAwB,EAC9B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAuC,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAGpG;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,UAAU,SAAS;IACjB;;;uEAGmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;6CACyC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GACvB,CAAC,SAAS,GAAG;IACX,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,kEAAkE;IAClE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC,GACF,CAAC,SAAS,GAAG;IACX,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC;;kEAE8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;mDAI+C;IAC/C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC,GACF,CAAC,SAAS,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,CAAC;AAsG9C;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,MAAM,EACpB,QAAQ,GAAE,wBAA4C,GACrD,eAAe,EAAE,CAiBnB"}
@@ -2,6 +2,7 @@ import { defaultCustomTags, defaultRegistry, walkCelExpressions, } from "@teloru
2
2
  import { PackageURL } from "packageurl-js";
3
3
  import { parseAllDocuments } from "yaml";
4
4
  import { selectorFromQualifiers, selectorKey } from "./artifact-selector.js";
5
+ import { readLibraryCandidates } from "./module-library.js";
5
6
  /** `pkg:telo/local/<format>?path=…` — the bundled-controller delivery mode.
6
7
  * Anything else (`pkg:npm`, `pkg:cargo`) fetches from its own ecosystem and
7
8
  * contributes no layer. */
@@ -57,6 +58,21 @@ function controllerClaims(json) {
57
58
  }
58
59
  return claims;
59
60
  }
61
+ /** The library entry points one document's `library:` block names. Unlike a
62
+ * controller entry — reached only when this module's own kinds instantiate —
63
+ * this one is what a *sibling* resolves a bare specifier to, which is why it
64
+ * gets its own layer rather than riding in the controller layer: a consumer
65
+ * must reach it without loading this module's controllers. */
66
+ function libraryClaims(json) {
67
+ return readLibraryCandidates(json).candidates.map((candidate) => ({
68
+ role: "library",
69
+ path: candidate.path,
70
+ selector: candidate.selector,
71
+ specifier: candidate.specifier,
72
+ ...(candidate.localPath ? { localPath: candidate.localPath } : {}),
73
+ origin: candidate.origin,
74
+ }));
75
+ }
60
76
  /** Claims contributed by tagged values, asked of the engine that owns each tag.
61
77
  * The walk reaches every tagged scalar in the document, so an engine that
62
78
  * embeds files is discovered wherever its tag was written.
@@ -81,7 +97,7 @@ function taggedClaims(json, registry) {
81
97
  * their layers — dropping one would leave a platform's layer short a file it
82
98
  * declared it needs. */
83
99
  function claimKey(claim) {
84
- const selector = claim.role === "controller" ? selectorKey(claim.selector) : "";
100
+ const selector = claim.role === "controller" || claim.role === "library" ? selectorKey(claim.selector) : "";
85
101
  return `${claim.role}\0${selector}\0${claim.path}`;
86
102
  }
87
103
  /**
@@ -98,7 +114,11 @@ export function collectModuleFileClaims(manifestText, registry = defaultRegistry
98
114
  const claims = [];
99
115
  for (const doc of parseAllDocuments(manifestText, { customTags: defaultCustomTags() })) {
100
116
  const json = doc.toJSON();
101
- for (const claim of [...controllerClaims(json), ...taggedClaims(json, registry)]) {
117
+ for (const claim of [
118
+ ...libraryClaims(json),
119
+ ...controllerClaims(json),
120
+ ...taggedClaims(json, registry),
121
+ ]) {
102
122
  const key = claimKey(claim);
103
123
  if (seen.has(key))
104
124
  continue;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * A module's **exported code** — the `exports.code:` block on a `Telo.Library`
3
+ * doc, which names the entry point a *sibling module's* controller bundle
4
+ * resolves this module's bare specifier to.
5
+ *
6
+ * ```yaml
7
+ * exports:
8
+ * kinds:
9
+ * - Store
10
+ * code:
11
+ * - specifier: "@telorun/kv-store"
12
+ * format: js
13
+ * path: ./nodejs/kv-store.mjs
14
+ * source: ./nodejs/src/index.ts
15
+ * ```
16
+ *
17
+ * ## Why it sits under `exports:`
18
+ *
19
+ * A library already declares what crosses its boundary — the kinds importers may
20
+ * name, the resource instances they may `!ref`. This is the same statement about
21
+ * its *code*, and it gates the same way: a specifier nobody declares resolves to
22
+ * nothing. Putting it beside them keeps one block for "reachable from outside"
23
+ * rather than a second top-level key whose name (`library:` on a `Telo.Library`)
24
+ * meant a different thing from the kind one line above it.
25
+ *
26
+ * ## Why it is not a package URL
27
+ *
28
+ * `controllers:` names a PURL because it must be able to say `pkg:npm/…` or
29
+ * `pkg:cargo/…` — an ecosystem fetch. This entry never fetches: it names a file
30
+ * the module already ships, so `pkg:telo/local/` would be three constant segments
31
+ * before the first real datum. What is left after removing them is exactly these
32
+ * fields, and as data they are visually editable, where a query string is one
33
+ * opaque text box.
34
+ *
35
+ * The **model** is unchanged: `format` plus the optional platform axes build the
36
+ * same `ArtifactSelector` a controller candidate does, so layer matching, platform
37
+ * fallthrough and lazy materialization are inherited whole.
38
+ *
39
+ * ## Why the specifier is declared here
40
+ *
41
+ * A bundle imports the bare specifier `@telorun/sql`; the consumer's manifest
42
+ * declares the dependency as `Sql: ../sql`. Something has to connect the two, and
43
+ * it is the *library* that says so, once, rather than each of its consumers:
44
+ *
45
+ * - the specifier is a property of the library — its name in a host language's
46
+ * ecosystem — not of the relationship, so N consumers cannot disagree about it
47
+ * and adding a consumer restates nothing;
48
+ * - it sits beside the format, which keeps runtime **derived, never declared**:
49
+ * the entry says `format: js`, and a Rust entry carries `specifier:
50
+ * telorun-sql` with no runtime-keyed map anywhere.
51
+ *
52
+ * **One specifier, one entry point.** Subpaths are deliberately not
53
+ * representable: reproducing npm's `exports` map inside the artifact would pull a
54
+ * package manager's resolution semantics into Telo, which is what the "only
55
+ * workspace modules are de-inlined" rule refuses on `kysely`'s behalf.
56
+ *
57
+ * `Telo.Application` has no `exports:` block at all — an application is a root
58
+ * with no importer, so nothing could resolve a specifier to it.
59
+ *
60
+ * Browser-safe: string work only. Whether the named file EXISTS is a separate
61
+ * question, asked by the Node-side caller that has a directory.
62
+ */
63
+ import { type ArtifactSelector } from "./artifact-selector.js";
64
+ export interface LibraryCandidate {
65
+ /** The bare specifier a sibling's controller bundle imports this library by. */
66
+ readonly specifier: string;
67
+ /** Module-root-relative path of the built entry point. */
68
+ readonly path: string;
69
+ /** Module-root-relative TypeScript source it is built from (`source:`), when
70
+ * the entry names one. Present only while the module is a working copy; a
71
+ * published artifact ships no `src/`. */
72
+ readonly localPath?: string;
73
+ readonly selector: ArtifactSelector;
74
+ /** Where the entry was written, for diagnostics. */
75
+ readonly origin: string;
76
+ }
77
+ /** Why an `exports.code` entry could not be read. Returned rather than thrown so
78
+ * the analyzer can report every entry of a block, and so a reader on the load
79
+ * path can carry on with the entries that are well-formed. */
80
+ export interface LibraryCandidateProblem {
81
+ readonly origin: string;
82
+ readonly detail: string;
83
+ }
84
+ export interface LibraryCandidates {
85
+ readonly candidates: LibraryCandidate[];
86
+ readonly problems: LibraryCandidateProblem[];
87
+ }
88
+ /**
89
+ * Read the `exports.code:` block off an owner document's JSON projection.
90
+ *
91
+ * Everything malformed is a problem rather than a silent skip: an entry that
92
+ * cannot be read names no entry point, so a consumer's bundle falls back to
93
+ * *inlining* the library — the duplicated module scope this whole mechanism
94
+ * exists to remove — and it does so on someone else's machine.
95
+ */
96
+ export declare function readLibraryCandidates(ownerJson: unknown): LibraryCandidates;
97
+ //# sourceMappingURL=module-library.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module-library.d.ts","sourceRoot":"","sources":["../src/module-library.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAEH,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,wBAAwB,CAAC;AAKhC,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;8CAE0C;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,oDAAoD;IACpD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;+DAE+D;AAC/D,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,UAAU,EAAE,gBAAgB,EAAE,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,uBAAuB,EAAE,CAAC;CAC9C;AAmBD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,OAAO,GAAG,iBAAiB,CAkF3E"}