@metaobjectsdev/metadata 0.6.0 → 0.7.0-rc.10

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 (141) hide show
  1. package/README.md +54 -3
  2. package/dist/attr-schema-validate.js +7 -7
  3. package/dist/attr-schema-validate.js.map +1 -1
  4. package/dist/core/export-json.d.ts +6 -7
  5. package/dist/core/export-json.d.ts.map +1 -1
  6. package/dist/core/export-json.js +15 -17
  7. package/dist/core/export-json.js.map +1 -1
  8. package/dist/core/index.d.ts +4 -2
  9. package/dist/core/index.d.ts.map +1 -1
  10. package/dist/core/index.js +6 -2
  11. package/dist/core/index.js.map +1 -1
  12. package/dist/core/parser-yaml.d.ts.map +1 -1
  13. package/dist/core/parser-yaml.js +36 -11
  14. package/dist/core/parser-yaml.js.map +1 -1
  15. package/dist/core/yaml-desugar.d.ts.map +1 -1
  16. package/dist/core/yaml-desugar.js +54 -5
  17. package/dist/core/yaml-desugar.js.map +1 -1
  18. package/dist/core/yaml-positions-walker.d.ts +21 -0
  19. package/dist/core/yaml-positions-walker.d.ts.map +1 -0
  20. package/dist/core/yaml-positions-walker.js +75 -0
  21. package/dist/core/yaml-positions-walker.js.map +1 -0
  22. package/dist/core/yaml-positions.d.ts +19 -0
  23. package/dist/core/yaml-positions.d.ts.map +1 -0
  24. package/dist/core/yaml-positions.js +60 -0
  25. package/dist/core/yaml-positions.js.map +1 -0
  26. package/dist/core-types.d.ts.map +1 -1
  27. package/dist/core-types.js +7 -4
  28. package/dist/core-types.js.map +1 -1
  29. package/dist/errors.d.ts +32 -9
  30. package/dist/errors.d.ts.map +1 -1
  31. package/dist/errors.js +44 -5
  32. package/dist/errors.js.map +1 -1
  33. package/dist/index.d.ts +6 -3
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +9 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/json-path.d.ts +8 -0
  38. package/dist/json-path.d.ts.map +1 -0
  39. package/dist/json-path.js +39 -0
  40. package/dist/json-path.js.map +1 -0
  41. package/dist/loader/meta-data-loader.d.ts +47 -6
  42. package/dist/loader/meta-data-loader.d.ts.map +1 -1
  43. package/dist/loader/meta-data-loader.js +126 -8
  44. package/dist/loader/meta-data-loader.js.map +1 -1
  45. package/dist/loader/meta-data-source.d.ts +6 -2
  46. package/dist/loader/meta-data-source.d.ts.map +1 -1
  47. package/dist/loader/meta-data-source.js +10 -6
  48. package/dist/loader/meta-data-source.js.map +1 -1
  49. package/dist/loader/shortcuts.d.ts +9 -0
  50. package/dist/loader/shortcuts.d.ts.map +1 -0
  51. package/dist/loader/shortcuts.js +19 -0
  52. package/dist/loader/shortcuts.js.map +1 -0
  53. package/dist/loader/sources/directory-source.d.ts +15 -0
  54. package/dist/loader/sources/directory-source.d.ts.map +1 -0
  55. package/dist/loader/sources/directory-source.js +80 -0
  56. package/dist/loader/sources/directory-source.js.map +1 -0
  57. package/dist/loader/sources/file-source.d.ts +12 -0
  58. package/dist/loader/sources/file-source.d.ts.map +1 -0
  59. package/dist/loader/sources/file-source.js +46 -0
  60. package/dist/loader/sources/file-source.js.map +1 -0
  61. package/dist/loader/sources/index.d.ts +5 -0
  62. package/dist/loader/sources/index.d.ts.map +1 -0
  63. package/dist/loader/sources/index.js +5 -0
  64. package/dist/loader/sources/index.js.map +1 -0
  65. package/dist/loader/sources/uri-source.d.ts +9 -0
  66. package/dist/loader/sources/uri-source.d.ts.map +1 -0
  67. package/dist/loader/sources/uri-source.js +42 -0
  68. package/dist/loader/sources/uri-source.js.map +1 -0
  69. package/dist/loader/validation-passes.d.ts.map +1 -1
  70. package/dist/loader/validation-passes.js +92 -28
  71. package/dist/loader/validation-passes.js.map +1 -1
  72. package/dist/naming.d.ts +15 -2
  73. package/dist/naming.d.ts.map +1 -1
  74. package/dist/naming.js +20 -6
  75. package/dist/naming.js.map +1 -1
  76. package/dist/parser-core.d.ts +17 -4
  77. package/dist/parser-core.d.ts.map +1 -1
  78. package/dist/parser-core.js +371 -44
  79. package/dist/parser-core.js.map +1 -1
  80. package/dist/parser-json.d.ts.map +1 -1
  81. package/dist/parser-json.js +10 -2
  82. package/dist/parser-json.js.map +1 -1
  83. package/dist/persistence/source/validate-source-roles.js +2 -2
  84. package/dist/persistence/source/validate-source-roles.js.map +1 -1
  85. package/dist/semantic-diff.d.ts +5 -0
  86. package/dist/semantic-diff.d.ts.map +1 -0
  87. package/dist/semantic-diff.js +49 -0
  88. package/dist/semantic-diff.js.map +1 -0
  89. package/dist/shared/meta-data.d.ts +10 -0
  90. package/dist/shared/meta-data.d.ts.map +1 -1
  91. package/dist/shared/meta-data.js +23 -0
  92. package/dist/shared/meta-data.js.map +1 -1
  93. package/dist/source.d.ts +96 -0
  94. package/dist/source.d.ts.map +1 -0
  95. package/dist/source.js +38 -0
  96. package/dist/source.js.map +1 -0
  97. package/dist/subtype-rules.js +1 -1
  98. package/dist/subtype-rules.js.map +1 -1
  99. package/dist/super-resolve.d.ts +2 -0
  100. package/dist/super-resolve.d.ts.map +1 -1
  101. package/dist/super-resolve.js +1 -1
  102. package/dist/super-resolve.js.map +1 -1
  103. package/dist/template/template-constants.d.ts +3 -1
  104. package/dist/template/template-constants.d.ts.map +1 -1
  105. package/dist/template/template-constants.js +22 -6
  106. package/dist/template/template-constants.js.map +1 -1
  107. package/dist/template/template-schema.d.ts.map +1 -1
  108. package/dist/template/template-schema.js +41 -1
  109. package/dist/template/template-schema.js.map +1 -1
  110. package/package.json +1 -1
  111. package/src/attr-schema-validate.ts +7 -7
  112. package/src/core/export-json.ts +15 -18
  113. package/src/core/index.ts +8 -2
  114. package/src/core/parser-yaml.ts +38 -11
  115. package/src/core/yaml-desugar.ts +58 -4
  116. package/src/core/yaml-positions-walker.ts +101 -0
  117. package/src/core/yaml-positions.ts +80 -0
  118. package/src/core-types.ts +7 -4
  119. package/src/errors.ts +57 -8
  120. package/src/index.ts +28 -3
  121. package/src/json-path.ts +46 -0
  122. package/src/loader/meta-data-loader.ts +168 -10
  123. package/src/loader/meta-data-source.ts +10 -6
  124. package/src/loader/shortcuts.ts +31 -0
  125. package/src/loader/sources/directory-source.ts +90 -0
  126. package/src/{core → loader/sources}/file-source.ts +3 -3
  127. package/src/loader/sources/index.ts +6 -0
  128. package/src/loader/sources/uri-source.ts +44 -0
  129. package/src/loader/validation-passes.ts +96 -29
  130. package/src/naming.ts +39 -7
  131. package/src/parser-core.ts +412 -46
  132. package/src/parser-json.ts +11 -2
  133. package/src/persistence/source/validate-source-roles.ts +2 -2
  134. package/src/semantic-diff.ts +48 -0
  135. package/src/shared/meta-data.ts +28 -0
  136. package/src/source.ts +99 -0
  137. package/src/subtype-rules.ts +1 -1
  138. package/src/super-resolve.ts +3 -1
  139. package/src/template/template-constants.ts +23 -6
  140. package/src/template/template-schema.ts +43 -0
  141. package/src/core/file-meta-data-loader.ts +0 -89
@@ -4,8 +4,9 @@
4
4
  // the canonical object to the shared buildTree (parser-core.ts).
5
5
 
6
6
  import { ParseError } from "./errors.js";
7
- import { buildTree, errOpts } from "./parser-core.js";
7
+ import { buildTree } from "./parser-core.js";
8
8
  import type { ParseOptions, ParseResult } from "./parser-core.js";
9
+ import type { ErrorSource } from "./source.js";
9
10
 
10
11
  export type { ParseOptions, ParseResult } from "./parser-core.js";
11
12
 
@@ -18,9 +19,17 @@ export function parseJson(content: string, opts: ParseOptions): ParseResult {
18
19
  try {
19
20
  parsed = JSON.parse(normalizedContent);
20
21
  } catch (err) {
22
+ // FR5a / ADR-0009 — pre-buildTree errors can't reach the parser's
23
+ // module-level JsonPathBuilder; build a minimal json-source envelope
24
+ // rooted at "$" so cross-port callers see a consistent shape.
25
+ const source: ErrorSource = {
26
+ format: "json",
27
+ files: [opts.sourceName ?? "<unknown>"],
28
+ jsonPath: "$",
29
+ };
21
30
  throw new ParseError(
22
31
  `Invalid JSON: ${(err as Error).message}`,
23
- { ...errOpts(opts.sourceName), code: "ERR_MALFORMED_JSON" },
32
+ { code: "ERR_MALFORMED_JSON", source },
24
33
  );
25
34
  }
26
35
 
@@ -36,14 +36,14 @@ export function validateSourceRoles(root: MetaData): ParseError[] {
36
36
  errors.push(
37
37
  new ParseError(
38
38
  `object "${obj.name}" declares ${sources.length} source(s) but none has role "${SOURCE_ROLE_PRIMARY}"`,
39
- { code: "ERR_SOURCE_NO_PRIMARY" },
39
+ { code: "ERR_SOURCE_NO_PRIMARY", source: obj.source },
40
40
  ),
41
41
  );
42
42
  } else if (primaryCount > 1) {
43
43
  errors.push(
44
44
  new ParseError(
45
45
  `object "${obj.name}" declares ${primaryCount} sources with role "${SOURCE_ROLE_PRIMARY}"; exactly one is required`,
46
- { code: "ERR_SOURCE_MULTIPLE_PRIMARY" },
46
+ { code: "ERR_SOURCE_MULTIPLE_PRIMARY", source: obj.source },
47
47
  ),
48
48
  );
49
49
  }
@@ -0,0 +1,48 @@
1
+ // server/typescript/packages/metadata/src/semantic-diff.ts
2
+ //
3
+ // FR5a / ADR-0009 — Cross-port-aligned semantic-equality compare for metadata
4
+ // trees. Returns `true` if the two inputs differ in any semantically-meaningful
5
+ // way (excluding `source`, which is loader output).
6
+ //
7
+ // Algorithm (ADR-0009 §semantic_diff):
8
+ // 1. Sort attrs lexicographically; compare attr-by-attr; values by canonical
9
+ // structural equality (key-order independent, whitespace-insensitive).
10
+ // 2. Children are compared as ordered sequences.
11
+ // 3. Reserved structural keys (name, package, extends, abstract, overlay,
12
+ // isArray, value) participate like attrs.
13
+ // 4. `source` excluded from the diff.
14
+
15
+ type Tree = Record<string, unknown>;
16
+
17
+ const EXCLUDED = new Set(["source"]);
18
+
19
+ function isObject(v: unknown): v is Tree {
20
+ return typeof v === "object" && v !== null && !Array.isArray(v);
21
+ }
22
+
23
+ function equal(a: unknown, b: unknown): boolean {
24
+ if (a === b) return true;
25
+ if (Array.isArray(a) && Array.isArray(b)) {
26
+ if (a.length !== b.length) return false;
27
+ for (let i = 0; i < a.length; i++) {
28
+ if (!equal(a[i], b[i])) return false;
29
+ }
30
+ return true;
31
+ }
32
+ if (isObject(a) && isObject(b)) {
33
+ const aKeys = Object.keys(a).filter((k) => !EXCLUDED.has(k)).sort();
34
+ const bKeys = Object.keys(b).filter((k) => !EXCLUDED.has(k)).sort();
35
+ if (aKeys.length !== bKeys.length) return false;
36
+ for (let i = 0; i < aKeys.length; i++) {
37
+ if (aKeys[i] !== bKeys[i]) return false;
38
+ if (!equal(a[aKeys[i]!], b[bKeys[i]!])) return false;
39
+ }
40
+ return true;
41
+ }
42
+ return false;
43
+ }
44
+
45
+ /** Returns `true` if the inputs differ in any semantically-meaningful way. */
46
+ export function semanticDiff(a: Tree, b: Tree): boolean {
47
+ return !equal(a, b);
48
+ }
@@ -5,6 +5,7 @@ import type { DataType } from "../data-type.js";
5
5
  import type { MetaAttr } from "../core/attr/meta-attr.js";
6
6
  import { inferAttrSubType } from "../serializer-json.js";
7
7
  import { attrClassFor } from "../attr-class-map.js";
8
+ import type { ErrorSource } from "../source.js";
8
9
 
9
10
  export type AttrValue = string | number | boolean | string[] | AttrObject;
10
11
 
@@ -57,6 +58,14 @@ export abstract class MetaData {
57
58
  // construction (for field/attr nodes). Read via MetaField/MetaAttr.dataType.
58
59
  protected _dataType?: DataType;
59
60
 
61
+ // ADR-0009 provenance. Always populated; defaults to `{ format: "code" }`
62
+ // for programmatic construction (tests, factories, in-code builders). The
63
+ // JSON parser overwrites via setSource() during tree walk; future phases
64
+ // (overlay merge, super resolution) may overwrite with merged/resolved
65
+ // variants. Excluded from canonical JSON serialization — loader-derived
66
+ // state, not metadata.
67
+ private _source: ErrorSource = { format: "code" };
68
+
60
69
  // Per-instance read cache: only populated once the node is frozen.
61
70
  private readonly _cache = new Map<string, unknown>();
62
71
 
@@ -171,6 +180,25 @@ export abstract class MetaData {
171
180
  this._dataType = dt;
172
181
  }
173
182
 
183
+ // ---------------------------------------------------------------------------
184
+ // source (ADR-0009 provenance)
185
+ // ---------------------------------------------------------------------------
186
+
187
+ /** Provenance envelope for this node. Always populated. See ADR-0009. */
188
+ get source(): ErrorSource {
189
+ return this._source;
190
+ }
191
+
192
+ /**
193
+ * Loader-internal: assign provenance. Called by parser and merge phases as
194
+ * they build the tree. Honors the frozen-guard like other setters.
195
+ * @internal
196
+ */
197
+ setSource(s: ErrorSource): void {
198
+ this._assertNotFrozen();
199
+ this._source = s;
200
+ }
201
+
174
202
  // ---------------------------------------------------------------------------
175
203
  // isAbstract
176
204
  // ---------------------------------------------------------------------------
package/src/source.ts ADDED
@@ -0,0 +1,99 @@
1
+ // server/typescript/packages/metadata/src/source.ts
2
+ //
3
+ // FR5a — Loader error envelope + source-on-node (ADR-0009).
4
+ //
5
+ // Cross-port-aligned types: every metaobjects port emits the same envelope
6
+ // shape so a tool consuming errors from multiple language ports can compare
7
+ // them byte-identically.
8
+
9
+ /** Discriminated union over the provenance variants a metadata node or error
10
+ * can carry. See ADR-0009 §Decision for the canonical shape.
11
+ *
12
+ * FR5b note (finalized 2026-05-27, all four ports): buildTree-emitted errors
13
+ * from a YAML input emit `format: "yaml"` and carry the optional
14
+ * `yamlPosition` (line/col from the desugar's position map). The
15
+ * `yamlPosition` field is also declared optionally on the `format: "json"`
16
+ * variant for backward shape-compat with any FR5b-interim envelopes still
17
+ * serialized to disk; new YAML errors no longer use that variant. */
18
+ export type ErrorSource =
19
+ | { format: "json"; files: [string]; jsonPath: string;
20
+ yamlPosition?: { line: number; col: number } }
21
+ | { format: "yaml"; files: [string]; jsonPath: string;
22
+ yamlPosition?: { line: number; col: number } }
23
+ | { format: "merged"; files: string[]; jsonPath: string;
24
+ contributors: Contributor[] }
25
+ | { format: "resolved"; files: string[]; jsonPath?: string;
26
+ referrer?: string; target?: string }
27
+ | { format: "database"; dbLocation: { table: string; id: string };
28
+ jsonPath?: string }
29
+ | { format: "code"; caller?: string };
30
+
31
+ export interface Contributor {
32
+ file: string;
33
+ role: "overlay-base" | "overlay-extension" | "extends-base" | "extends-extension";
34
+ }
35
+
36
+ export interface NodeContext {
37
+ type?: string;
38
+ subtype?: string;
39
+ name?: string;
40
+ fqn?: string;
41
+ }
42
+
43
+ /** Envelope shape every loader error conforms to. */
44
+ export interface LoaderError {
45
+ // REQUIRED — conformance-enforced.
46
+ code: string;
47
+ message: string;
48
+ source: ErrorSource;
49
+ // RECOMMENDED — optional per ADR-0009 §What ports are NOT required to do.
50
+ suggestions?: string[];
51
+ fixture?: string;
52
+ node?: NodeContext;
53
+ }
54
+
55
+ /** Warning envelope — same shape as LoaderError but a `WARN_*` code. */
56
+ export interface LoaderWarning {
57
+ code: string;
58
+ message: string;
59
+ source: ErrorSource;
60
+ suggestions?: string[];
61
+ fixture?: string;
62
+ node?: NodeContext;
63
+ }
64
+
65
+ /** Canonical synthetic envelope for programmatic / test-constructed nodes.
66
+ * `caller` is an optional human label (e.g. "QueriesTest.makePost"). */
67
+ export function codeSource(caller?: string): ErrorSource {
68
+ return caller ? { format: "code", caller } : { format: "code" };
69
+ }
70
+
71
+ /** FR5d — build a `format: "resolved"` envelope from a referrer node's source
72
+ * envelope (typically a `format: "json"` or `format: "yaml"` parse-time
73
+ * source) plus the referrer FQN and unresolved target string.
74
+ *
75
+ * The resolved envelope carries:
76
+ * - `files`: the referrer's source files (so editors can jump to it),
77
+ * - `jsonPath`: the referrer's jsonPath when known (the location of the
78
+ * broken reference on disk),
79
+ * - `referrer`: the FQN of the metadata node that declared the broken
80
+ * reference (e.g. "myapp::content::Video"),
81
+ * - `target`: the unresolved reference string itself (e.g. "BaseEntity",
82
+ * "Program.weeks.invalid", "DoesNotExist").
83
+ *
84
+ * `referrerSource` may be any FR5a variant — we read its files/jsonPath
85
+ * best-effort and fall back to an empty `files: []` when the referrer's
86
+ * source is itself a `code`/`database` envelope. */
87
+ export function resolvedSource(
88
+ referrerSource: ErrorSource,
89
+ referrer: string,
90
+ target: string,
91
+ ): ErrorSource {
92
+ const files = "files" in referrerSource ? [...referrerSource.files] : [];
93
+ const jsonPath = "jsonPath" in referrerSource ? referrerSource.jsonPath : undefined;
94
+ const out: ErrorSource = { format: "resolved", files, referrer, target };
95
+ if (jsonPath !== undefined) {
96
+ (out as { jsonPath?: string }).jsonPath = jsonPath;
97
+ }
98
+ return out;
99
+ }
@@ -35,7 +35,7 @@ function walk(model: MetaData, errors: ParseError[], warnings: string[]): void {
35
35
  new ParseError(
36
36
  `value object '${model.fqn()}' must not have a primary identity ` +
37
37
  `(use subType: "entity" for records with identity)`,
38
- { code: "ERR_SUBTYPE_RULE_VIOLATION" },
38
+ { code: "ERR_SUBTYPE_RULE_VIOLATION", source: model.source },
39
39
  ),
40
40
  );
41
41
  } else if (
@@ -100,6 +100,8 @@ export interface DeferredSuperFailure {
100
100
  nodeFqn: string;
101
101
  /** The raw super ref string that didn't resolve. */
102
102
  ref: string;
103
+ /** ADR-0009 provenance envelope of the referencing node (FR5a). */
104
+ source: import("./source.js").ErrorSource;
103
105
  }
104
106
 
105
107
  /**
@@ -128,7 +130,7 @@ export function resolveDeferredSupers(root: MetaData): DeferredSuperFailure[] {
128
130
  // Frozen — ignore; the loader should resolve before freeze.
129
131
  }
130
132
  } else {
131
- failures.push({ nodeFqn: node.fqn(), ref: node.superRef });
133
+ failures.push({ nodeFqn: node.fqn(), ref: node.superRef, source: node.source });
132
134
  }
133
135
  });
134
136
  return failures;
@@ -1,10 +1,14 @@
1
- // template.* subtype vocabulary + reserved attribute names (FR-004, R1).
1
+ // template.* subtype vocabulary + reserved attribute names (FR-004, R1; ADR-0011).
2
2
  //
3
- // `template` is the fourth-pillar base type: a renderable text artifact bound to
4
- // a typed payload. Two subtypes (by audience/structure, NOT by format):
5
- // - prompt: LLM-targeted; carries the prompt-overlay attrs and is the home for
6
- // future structured-prompt (role/turn/tool) divergence.
3
+ // `template` is the fourth-pillar base type a typed payload bound to either
4
+ // a rendered text artifact (prompt/output) or to a tool-call envelope.
5
+ // Three subtypes:
6
+ // - prompt: LLM-targeted renderable text. Carries the prompt-overlay attrs.
7
+ // Renderable body required via @textRef.
7
8
  // - output: every other rendered artifact (email, export, docs, config).
9
+ // Renderable body required via @textRef.
10
+ // - toolcall: LLM tool-call envelope (no renderable body — the body IS the
11
+ // structured output schema resolved via @payloadRef). Per ADR-0011.
8
12
  //
9
13
  // Format is the @format ATTRIBUTE (closed set below), never a subtype — the
10
14
  // render engine keys its escaper off @format, so a new format costs one escaper
@@ -14,15 +18,18 @@ import { SUBTYPE_BASE } from "../shared/base-types.js";
14
18
 
15
19
  export const TEMPLATE_SUBTYPE_PROMPT = "prompt";
16
20
  export const TEMPLATE_SUBTYPE_OUTPUT = "output";
21
+ export const TEMPLATE_SUBTYPE_TOOLCALL = "toolcall";
17
22
 
18
23
  export const TEMPLATE_SUBTYPES = [
19
24
  SUBTYPE_BASE,
20
25
  TEMPLATE_SUBTYPE_PROMPT,
21
26
  TEMPLATE_SUBTYPE_OUTPUT,
27
+ TEMPLATE_SUBTYPE_TOOLCALL,
22
28
  ] as const;
23
29
  export type TemplateSubType = (typeof TEMPLATE_SUBTYPES)[number];
24
30
 
25
- // Generic reserved attrs (both subtypes). The "@" is applied at wire time.
31
+ // Generic reserved attrs (prompt + output). The "@" is applied at wire time.
32
+ // NOT inherited by toolcall — toolcall has no renderable body.
26
33
  export const TEMPLATE_ATTR_PAYLOAD_REF = "payloadRef";
27
34
  export const TEMPLATE_ATTR_TEXT_REF = "textRef";
28
35
  export const TEMPLATE_ATTR_FORMAT = "format";
@@ -39,6 +46,16 @@ export const TEMPLATE_ATTR_MAX_TOKENS = "maxTokens";
39
46
  export const TEMPLATE_ATTR_REQUIRED_SLOTS = "requiredSlots";
40
47
  export const TEMPLATE_ATTR_MODEL = "model";
41
48
 
49
+ // Toolcall-specific attrs (template.toolcall only). Vendor-agnostic; vendor
50
+ // wire details (retry semantics, fallback shapes, etc.) are added by consumer
51
+ // providers via registry.extend per ADR-0011.
52
+ //
53
+ // @description is intentionally NOT a toolcall-specific constant — every type
54
+ // gets @description via the documentation common-attrs provider. Tool
55
+ // descriptions surfaced to the LLM use the same @description common attr
56
+ // doc-gen uses.
57
+ export const TEMPLATE_ATTR_TOOL_NAME = "toolName";
58
+
42
59
  // Closed format set — escaping/whitespace behavior is keyed off this in the
43
60
  // render engine's escaper registry (FR-004 R7).
44
61
  export const TEMPLATE_FORMATS = [
@@ -16,6 +16,7 @@ import { SUBTYPE_BASE } from "../shared/base-types.js";
16
16
  import {
17
17
  TEMPLATE_SUBTYPE_PROMPT,
18
18
  TEMPLATE_SUBTYPE_OUTPUT,
19
+ TEMPLATE_SUBTYPE_TOOLCALL,
19
20
  TEMPLATE_ATTR_PAYLOAD_REF,
20
21
  TEMPLATE_ATTR_TEXT_REF,
21
22
  TEMPLATE_ATTR_FORMAT,
@@ -26,6 +27,7 @@ import {
26
27
  TEMPLATE_ATTR_MAX_TOKENS,
27
28
  TEMPLATE_ATTR_REQUIRED_SLOTS,
28
29
  TEMPLATE_ATTR_MODEL,
30
+ TEMPLATE_ATTR_TOOL_NAME,
29
31
  TEMPLATE_FORMATS,
30
32
  } from "./template-constants.js";
31
33
 
@@ -99,8 +101,49 @@ const promptOverlayAttrs: AttrSchema[] = [
99
101
  },
100
102
  ];
101
103
 
104
+ // Toolcall attrs (template.toolcall only — does NOT inherit genericAttrs).
105
+ // Per ADR-0011: vendor-agnostic in core; vendor wire details (retry semantics,
106
+ // fallback shapes, parallel invocation, cache hints) added by consumer
107
+ // providers via registry.extend(TYPE_TEMPLATE, "toolcall", { attributes: [...] }).
108
+ //
109
+ // Critical: @textRef is intentionally NOT required here. A tool-call has no
110
+ // renderable text body — the body IS the structured output schema resolved
111
+ // via @payloadRef. This is the design rationale for toolcall being its own
112
+ // subtype rather than template.output + @toolName.
113
+ //
114
+ // @description is intentionally NOT declared here — it's already a documentation
115
+ // common attr added to every type by docProvider. Tool descriptions surfaced to
116
+ // the LLM read the same @description common attr that doc-gen uses.
117
+ const toolcallAttrs: AttrSchema[] = [
118
+ {
119
+ name: TEMPLATE_ATTR_TOOL_NAME,
120
+ valueType: ATTR_SUBTYPE_STRING,
121
+ required: true,
122
+ description: "Wire tool name surfaced to the LLM (vendor-specific format).",
123
+ },
124
+ {
125
+ name: TEMPLATE_ATTR_PAYLOAD_REF,
126
+ valueType: ATTR_SUBTYPE_STRING,
127
+ required: true,
128
+ description: "Output value-object the tool produces (resolved against the metamodel).",
129
+ },
130
+ {
131
+ name: TEMPLATE_ATTR_OWNER,
132
+ valueType: ATTR_SUBTYPE_STRING,
133
+ required: false,
134
+ description: "Governance: the owner of this toolcall.",
135
+ },
136
+ {
137
+ name: TEMPLATE_ATTR_SINCE,
138
+ valueType: ATTR_SUBTYPE_STRING,
139
+ required: false,
140
+ description: "Governance: the version this toolcall was introduced in.",
141
+ },
142
+ ];
143
+
102
144
  export const TEMPLATE_ATTRS_MAP = new Map<string, AttrSchema[]>([
103
145
  [SUBTYPE_BASE, []],
104
146
  [TEMPLATE_SUBTYPE_PROMPT, [...genericAttrs, ...promptOverlayAttrs]],
105
147
  [TEMPLATE_SUBTYPE_OUTPUT, [...genericAttrs]],
148
+ [TEMPLATE_SUBTYPE_TOOLCALL, [...toolcallAttrs]],
106
149
  ]);
@@ -1,89 +0,0 @@
1
- // FileMetaDataLoader — discovers file-backed MetaDataSources and runs the
2
- // MetaDataLoader pipeline over them. A UrlMetaDataLoader will slot in the
3
- // same way later.
4
-
5
- import type { Stats } from "node:fs";
6
- import { readdir, stat } from "node:fs/promises";
7
- import { join } from "node:path";
8
- import { MetaDataLoader, type LoadResult } from "../loader/meta-data-loader.js";
9
- import { FileSource } from "./file-source.js";
10
- import { parseYaml } from "./parser-yaml.js";
11
- import type { ParseOptions, ParseResult } from "../parser-core.js";
12
- import type { MetaDataSource } from "../loader/meta-data-source.js";
13
-
14
- /** Minimal glob matcher supporting `*` (any chars except `/`) and `**` (any chars). */
15
- function matchSimpleGlob(pattern: string, value: string): boolean {
16
- const regexStr = pattern
17
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
18
- .replace(/\*\*/g, "::DOUBLESTAR::")
19
- .replace(/\*/g, "[^/]*")
20
- .replace(/::DOUBLESTAR::/g, ".*");
21
- return new RegExp(`^${regexStr}$`).test(value);
22
- }
23
-
24
- export class FileMetaDataLoader extends MetaDataLoader {
25
- /** Adds YAML parsing on top of the base loader's JSON-only `parseSource`. */
26
- protected override parseSource(
27
- content: string,
28
- source: MetaDataSource,
29
- parseOpts: ParseOptions,
30
- ): ParseResult {
31
- if (source.format === "yaml") {
32
- return parseYaml(content, parseOpts);
33
- }
34
- return super.parseSource(content, source, parseOpts);
35
- }
36
-
37
- /**
38
- * Load every `.json` / `.yaml` / `.yml` file in a directory (non-recursive),
39
- * in deterministic ordinal filename order.
40
- * @param opts.exclude glob patterns (relative to dir) to skip — `*` / `**`.
41
- */
42
- async loadDirectory(dir: string, opts?: { exclude?: string[] }): Promise<LoadResult> {
43
- let entries: string[];
44
- try {
45
- // Sorted for deterministic multi-file load order: the overlay merge is
46
- // order-sensitive (last-writer-wins on attr conflicts), so the scan must
47
- // not depend on filesystem readdir order. Ordinal filename sort is
48
- // reproducible across the Java/Python/C# ports.
49
- entries = (await readdir(dir)).sort();
50
- } catch (err) {
51
- // Surface the I/O failure as a collected error via the empty-source path.
52
- const emptyResult = await this.load([]);
53
- return {
54
- ...emptyResult,
55
- errors: [
56
- new Error(`loadDirectory: cannot read ${dir}: ${(err as Error).message}`),
57
- ...emptyResult.errors,
58
- ],
59
- };
60
- }
61
-
62
- const excludes = opts?.exclude ?? [];
63
- const paths: string[] = [];
64
- for (const entry of entries) {
65
- const lower = entry.toLowerCase();
66
- if (!lower.endsWith(".json") && !lower.endsWith(".yaml") && !lower.endsWith(".yml")) {
67
- continue;
68
- }
69
- const filePath = join(dir, entry);
70
- let statResult: Stats;
71
- try {
72
- statResult = await stat(filePath);
73
- } catch {
74
- // Entry vanished between readdir and stat (TOCTOU) or is not accessible.
75
- // Skip it rather than breaking the no-throw contract of loadDirectory.
76
- continue;
77
- }
78
- if (!statResult.isFile()) continue;
79
- if (excludes.some((p) => matchSimpleGlob(p, entry))) continue;
80
- paths.push(filePath);
81
- }
82
- return this.loadFiles(paths);
83
- }
84
-
85
- /** Load an explicit list of file paths, in order. */
86
- async loadFiles(paths: string[]): Promise<LoadResult> {
87
- return this.load(paths.map((p) => new FileSource(p)));
88
- }
89
- }