dna-sdk 0.1.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 (240) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -0
  3. package/dist/adapters/embedding/onnx.d.ts +32 -0
  4. package/dist/adapters/embedding/onnx.js +51 -0
  5. package/dist/adapters/filesystem/cache.d.ts +16 -0
  6. package/dist/adapters/filesystem/cache.js +113 -0
  7. package/dist/adapters/filesystem/index.d.ts +2 -0
  8. package/dist/adapters/filesystem/index.js +2 -0
  9. package/dist/adapters/filesystem/source.d.ts +82 -0
  10. package/dist/adapters/filesystem/source.js +357 -0
  11. package/dist/adapters/postgres/index.d.ts +2 -0
  12. package/dist/adapters/postgres/index.js +2 -0
  13. package/dist/adapters/postgres/migrations.d.ts +14 -0
  14. package/dist/adapters/postgres/migrations.js +88 -0
  15. package/dist/adapters/postgres/source.d.ts +172 -0
  16. package/dist/adapters/postgres/source.js +569 -0
  17. package/dist/adapters/resolvers/github.d.ts +12 -0
  18. package/dist/adapters/resolvers/github.js +47 -0
  19. package/dist/adapters/resolvers/http.d.ts +22 -0
  20. package/dist/adapters/resolvers/http.js +145 -0
  21. package/dist/adapters/resolvers/index.d.ts +3 -0
  22. package/dist/adapters/resolvers/index.js +3 -0
  23. package/dist/adapters/resolvers/local.d.ts +24 -0
  24. package/dist/adapters/resolvers/local.js +132 -0
  25. package/dist/adapters/search/driver.d.ts +45 -0
  26. package/dist/adapters/search/driver.js +138 -0
  27. package/dist/adapters/search/migrations.d.ts +26 -0
  28. package/dist/adapters/search/migrations.js +58 -0
  29. package/dist/adapters/search/rrf.d.ts +23 -0
  30. package/dist/adapters/search/rrf.js +42 -0
  31. package/dist/adapters/search/sqlite-vec.d.ts +66 -0
  32. package/dist/adapters/search/sqlite-vec.js +294 -0
  33. package/dist/bootstrap.d.ts +45 -0
  34. package/dist/bootstrap.js +134 -0
  35. package/dist/extensions/agentskills/DOCS.md +18 -0
  36. package/dist/extensions/agentskills.d.ts +28 -0
  37. package/dist/extensions/agentskills.js +252 -0
  38. package/dist/extensions/agentsmd/DOCS.md +18 -0
  39. package/dist/extensions/agentsmd.d.ts +22 -0
  40. package/dist/extensions/agentsmd.js +170 -0
  41. package/dist/extensions/audit/kinds/audit-log.kind.yaml +148 -0
  42. package/dist/extensions/audit.d.ts +25 -0
  43. package/dist/extensions/audit.js +62 -0
  44. package/dist/extensions/automation/kinds/automation.kind.yaml +296 -0
  45. package/dist/extensions/automation/query.d.ts +49 -0
  46. package/dist/extensions/automation/query.js +77 -0
  47. package/dist/extensions/automation/write-guards.d.ts +59 -0
  48. package/dist/extensions/automation/write-guards.js +153 -0
  49. package/dist/extensions/automation.d.ts +41 -0
  50. package/dist/extensions/automation.js +54 -0
  51. package/dist/extensions/collab.d.ts +16 -0
  52. package/dist/extensions/collab.js +79 -0
  53. package/dist/extensions/doc/kinds/doc.kind.yaml +118 -0
  54. package/dist/extensions/doc.d.ts +25 -0
  55. package/dist/extensions/doc.js +10 -0
  56. package/dist/extensions/eval/kinds/eval-baseline.kind.yaml +65 -0
  57. package/dist/extensions/eval/kinds/eval-case.kind.yaml +140 -0
  58. package/dist/extensions/eval/kinds/eval-run.kind.yaml +134 -0
  59. package/dist/extensions/eval/kinds/eval-suite.kind.yaml +87 -0
  60. package/dist/extensions/eval.d.ts +24 -0
  61. package/dist/extensions/eval.js +31 -0
  62. package/dist/extensions/evidence/kinds/evidence.kind.yaml +92 -0
  63. package/dist/extensions/evidence.d.ts +17 -0
  64. package/dist/extensions/evidence.js +94 -0
  65. package/dist/extensions/federation.d.ts +22 -0
  66. package/dist/extensions/federation.js +173 -0
  67. package/dist/extensions/guardrails/DOCS.md +20 -0
  68. package/dist/extensions/guardrails.d.ts +31 -0
  69. package/dist/extensions/guardrails.js +228 -0
  70. package/dist/extensions/helix/DOCS-Actor.md +20 -0
  71. package/dist/extensions/helix/DOCS-Agent.md +20 -0
  72. package/dist/extensions/helix/DOCS-UseCase.md +20 -0
  73. package/dist/extensions/helix/layers.d.ts +12 -0
  74. package/dist/extensions/helix/layers.js +18 -0
  75. package/dist/extensions/helix/write-guards.d.ts +70 -0
  76. package/dist/extensions/helix/write-guards.js +172 -0
  77. package/dist/extensions/helix.d.ts +29 -0
  78. package/dist/extensions/helix.js +963 -0
  79. package/dist/extensions/helix_extras.d.ts +124 -0
  80. package/dist/extensions/helix_extras.js +328 -0
  81. package/dist/extensions/hooks.d.ts +31 -0
  82. package/dist/extensions/hooks.js +247 -0
  83. package/dist/extensions/kinddef/DOCS.md +24 -0
  84. package/dist/extensions/kinddef.d.ts +40 -0
  85. package/dist/extensions/kinddef.js +171 -0
  86. package/dist/extensions/lesson.d.ts +6 -0
  87. package/dist/extensions/lesson.js +194 -0
  88. package/dist/extensions/modelreg/kinds/model-profile.kind.yaml +155 -0
  89. package/dist/extensions/modelreg.d.ts +30 -0
  90. package/dist/extensions/modelreg.js +38 -0
  91. package/dist/extensions/recognizer.d.ts +34 -0
  92. package/dist/extensions/recognizer.js +272 -0
  93. package/dist/extensions/research.d.ts +32 -0
  94. package/dist/extensions/research.js +292 -0
  95. package/dist/extensions/safety.d.ts +33 -0
  96. package/dist/extensions/safety.js +230 -0
  97. package/dist/extensions/sdlc/kinds/adr.kind.yaml +136 -0
  98. package/dist/extensions/sdlc/kinds/archive-proposal.kind.yaml +107 -0
  99. package/dist/extensions/sdlc/kinds/changelog.kind.yaml +108 -0
  100. package/dist/extensions/sdlc/kinds/cognitive-policy.kind.yaml +470 -0
  101. package/dist/extensions/sdlc/kinds/forecast.kind.yaml +125 -0
  102. package/dist/extensions/sdlc/kinds/insight.kind.yaml +87 -0
  103. package/dist/extensions/sdlc/kinds/kaizen.kind.yaml +106 -0
  104. package/dist/extensions/sdlc/kinds/lesson-learned.kind.yaml +270 -0
  105. package/dist/extensions/sdlc/kinds/narrative.kind.yaml +189 -0
  106. package/dist/extensions/sdlc/kinds/pattern-insight.kind.yaml +204 -0
  107. package/dist/extensions/sdlc/kinds/postmortem.kind.yaml +168 -0
  108. package/dist/extensions/sdlc/kinds/pre-mortem.kind.yaml +145 -0
  109. package/dist/extensions/sdlc/kinds/prompt-template.kind.yaml +109 -0
  110. package/dist/extensions/sdlc/kinds/retrospective.kind.yaml +186 -0
  111. package/dist/extensions/sdlc/kinds/risk-register.kind.yaml +178 -0
  112. package/dist/extensions/sdlc/kinds/saved-view.kind.yaml +127 -0
  113. package/dist/extensions/sdlc/kinds/status-report.kind.yaml +129 -0
  114. package/dist/extensions/sdlc/kinds/synthesis-run.kind.yaml +279 -0
  115. package/dist/extensions/sdlc/kinds/synthesizer-state.kind.yaml +196 -0
  116. package/dist/extensions/sdlc/kinds/workflow-event.kind.yaml +195 -0
  117. package/dist/extensions/sdlc.d.ts +32 -0
  118. package/dist/extensions/sdlc.js +1318 -0
  119. package/dist/extensions/soulspec/DOCS.md +19 -0
  120. package/dist/extensions/soulspec.d.ts +22 -0
  121. package/dist/extensions/soulspec.js +287 -0
  122. package/dist/extensions/tenant.d.ts +6 -0
  123. package/dist/extensions/tenant.js +291 -0
  124. package/dist/extensions/testkit.d.ts +24 -0
  125. package/dist/extensions/testkit.js +211 -0
  126. package/dist/index.d.ts +80 -0
  127. package/dist/index.js +102 -0
  128. package/dist/kernel/_text.d.ts +28 -0
  129. package/dist/kernel/_text.js +42 -0
  130. package/dist/kernel/bundle-handle.d.ts +83 -0
  131. package/dist/kernel/bundle-handle.js +184 -0
  132. package/dist/kernel/capabilities.d.ts +157 -0
  133. package/dist/kernel/capabilities.js +109 -0
  134. package/dist/kernel/collaborator-ports.d.ts +91 -0
  135. package/dist/kernel/collaborator-ports.js +1 -0
  136. package/dist/kernel/composition-resolver.d.ts +215 -0
  137. package/dist/kernel/composition-resolver.js +558 -0
  138. package/dist/kernel/composition.d.ts +10 -0
  139. package/dist/kernel/composition.js +1 -0
  140. package/dist/kernel/descriptor-loader.d.ts +13 -0
  141. package/dist/kernel/descriptor-loader.js +48 -0
  142. package/dist/kernel/document.d.ts +43 -0
  143. package/dist/kernel/document.js +71 -0
  144. package/dist/kernel/embedding.d.ts +28 -0
  145. package/dist/kernel/embedding.js +78 -0
  146. package/dist/kernel/errors.d.ts +62 -0
  147. package/dist/kernel/errors.js +82 -0
  148. package/dist/kernel/events.d.ts +6 -0
  149. package/dist/kernel/events.js +21 -0
  150. package/dist/kernel/evidence-capture.d.ts +69 -0
  151. package/dist/kernel/evidence-capture.js +138 -0
  152. package/dist/kernel/fs.d.ts +20 -0
  153. package/dist/kernel/fs.js +128 -0
  154. package/dist/kernel/generic-rw.d.ts +33 -0
  155. package/dist/kernel/generic-rw.js +235 -0
  156. package/dist/kernel/hooks.d.ts +172 -0
  157. package/dist/kernel/hooks.js +256 -0
  158. package/dist/kernel/index.d.ts +624 -0
  159. package/dist/kernel/index.js +1638 -0
  160. package/dist/kernel/instance.d.ts +211 -0
  161. package/dist/kernel/instance.js +487 -0
  162. package/dist/kernel/kind-registry.d.ts +183 -0
  163. package/dist/kernel/kind-registry.js +710 -0
  164. package/dist/kernel/kind_base.d.ts +106 -0
  165. package/dist/kernel/kind_base.js +244 -0
  166. package/dist/kernel/layer-resolver.d.ts +48 -0
  167. package/dist/kernel/layer-resolver.js +284 -0
  168. package/dist/kernel/lock-manager.d.ts +21 -0
  169. package/dist/kernel/lock-manager.js +43 -0
  170. package/dist/kernel/lock.d.ts +38 -0
  171. package/dist/kernel/lock.js +115 -0
  172. package/dist/kernel/meta.d.ts +135 -0
  173. package/dist/kernel/meta.js +817 -0
  174. package/dist/kernel/models.d.ts +3089 -0
  175. package/dist/kernel/models.js +764 -0
  176. package/dist/kernel/navigator.d.ts +36 -0
  177. package/dist/kernel/navigator.js +132 -0
  178. package/dist/kernel/port-surface.d.ts +33 -0
  179. package/dist/kernel/port-surface.js +180 -0
  180. package/dist/kernel/preview.d.ts +60 -0
  181. package/dist/kernel/preview.js +54 -0
  182. package/dist/kernel/prompt-budget.d.ts +39 -0
  183. package/dist/kernel/prompt-budget.js +43 -0
  184. package/dist/kernel/prompt-builder.d.ts +25 -0
  185. package/dist/kernel/prompt-builder.js +237 -0
  186. package/dist/kernel/protocols.d.ts +751 -0
  187. package/dist/kernel/protocols.js +493 -0
  188. package/dist/kernel/reports.d.ts +26 -0
  189. package/dist/kernel/reports.js +215 -0
  190. package/dist/kernel/resolver.d.ts +146 -0
  191. package/dist/kernel/resolver.js +232 -0
  192. package/dist/kernel/resource.d.ts +57 -0
  193. package/dist/kernel/resource.js +97 -0
  194. package/dist/kernel/runtime.d.ts +16 -0
  195. package/dist/kernel/runtime.js +18 -0
  196. package/dist/kernel/safety-scanner.d.ts +38 -0
  197. package/dist/kernel/safety-scanner.js +228 -0
  198. package/dist/kernel/serialize-to-files.d.ts +29 -0
  199. package/dist/kernel/serialize-to-files.js +23 -0
  200. package/dist/kernel/spec-access.d.ts +23 -0
  201. package/dist/kernel/spec-access.js +40 -0
  202. package/dist/kernel/studio_ui.d.ts +88 -0
  203. package/dist/kernel/studio_ui.js +135 -0
  204. package/dist/kernel/templates.d.ts +77 -0
  205. package/dist/kernel/templates.js +116 -0
  206. package/dist/kernel/tool-registry.d.ts +52 -0
  207. package/dist/kernel/tool-registry.js +89 -0
  208. package/dist/kernel/write-pipeline.d.ts +65 -0
  209. package/dist/kernel/write-pipeline.js +271 -0
  210. package/dist/kernel/writer-helpers.d.ts +24 -0
  211. package/dist/kernel/writer-helpers.js +42 -0
  212. package/dist/memory/decay.d.ts +31 -0
  213. package/dist/memory/decay.js +97 -0
  214. package/dist/memory/ecphory.d.ts +25 -0
  215. package/dist/memory/ecphory.js +178 -0
  216. package/dist/memory/encodingContext.d.ts +21 -0
  217. package/dist/memory/encodingContext.js +62 -0
  218. package/dist/memory/index.d.ts +23 -0
  219. package/dist/memory/index.js +23 -0
  220. package/dist/memory/memoryType.d.ts +7 -0
  221. package/dist/memory/memoryType.js +24 -0
  222. package/dist/memory/policy.d.ts +35 -0
  223. package/dist/memory/policy.js +35 -0
  224. package/dist/memory/retrieval.d.ts +21 -0
  225. package/dist/memory/retrieval.js +145 -0
  226. package/dist/testing/index.d.ts +8 -0
  227. package/dist/testing/index.js +8 -0
  228. package/dist/testing/recordSearchConformance.d.ts +43 -0
  229. package/dist/testing/recordSearchConformance.js +147 -0
  230. package/dist/viz/ascii.d.ts +7 -0
  231. package/dist/viz/ascii.js +60 -0
  232. package/dist/viz/health.d.ts +8 -0
  233. package/dist/viz/health.js +161 -0
  234. package/dist/viz/index.d.ts +10 -0
  235. package/dist/viz/index.js +10 -0
  236. package/dist/viz/matrix.d.ts +8 -0
  237. package/dist/viz/matrix.js +84 -0
  238. package/dist/viz/mermaid.d.ts +38 -0
  239. package/dist/viz/mermaid.js +706 -0
  240. package/package.json +82 -0
@@ -0,0 +1,817 @@
1
+ /**
2
+ * Meta-kind machinery: DeclarativeKindPort synthesized from a
3
+ * TypedKindDefinition. 1:1 parity with Python dna.kernel.meta.
4
+ */
5
+ /** Convert HSL (h: 0-360, s: 0-100, l: 0-100) to a hex color string.
6
+ * Mermaid classDef doesn't support hsl(), so we need hex. */
7
+ function hslToHex(h, s, l) {
8
+ const sn = s / 100;
9
+ const ln = l / 100;
10
+ const c = (1 - Math.abs(2 * ln - 1)) * sn;
11
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
12
+ const m = ln - c / 2;
13
+ let r = 0, g = 0, b = 0;
14
+ if (h < 60) {
15
+ r = c;
16
+ g = x;
17
+ }
18
+ else if (h < 120) {
19
+ r = x;
20
+ g = c;
21
+ }
22
+ else if (h < 180) {
23
+ g = c;
24
+ b = x;
25
+ }
26
+ else if (h < 240) {
27
+ g = x;
28
+ b = c;
29
+ }
30
+ else if (h < 300) {
31
+ r = x;
32
+ b = c;
33
+ }
34
+ else {
35
+ r = c;
36
+ b = x;
37
+ }
38
+ const toHex = (v) => Math.round((v + m) * 255).toString(16).padStart(2, "0");
39
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
40
+ }
41
+ import { DEFAULT_VOLATILE_SPEC_FIELDS, createAjv } from "./kind_base.js";
42
+ import { StudioUIMetadata } from "./studio_ui.js";
43
+ import { SD, } from "./protocols.js";
44
+ /** Convert a literal YAML storage dict into a StorageDescriptor. */
45
+ export function storageDictToDescriptor(storage) {
46
+ if (storage == null || typeof storage !== "object") {
47
+ throw new Error(`storage must be a dict, got ${typeof storage}`);
48
+ }
49
+ const stype = storage.type;
50
+ if (!stype) {
51
+ throw new Error("storage dict must have a 'type' field");
52
+ }
53
+ const bodyAs = (val) => {
54
+ if (val == null)
55
+ return "text";
56
+ if (val === "text" || val === "list" || val === "sections")
57
+ return val;
58
+ throw new Error(`unknown body_as=${String(val)} (expected text|list|sections)`);
59
+ };
60
+ if (stype === "bundle") {
61
+ const container = storage.container ?? storage.dir;
62
+ const marker = storage.marker;
63
+ if (!container || !marker) {
64
+ throw new Error("storage type=bundle requires 'container' (or 'dir') and 'marker'");
65
+ }
66
+ return SD.bundle(container, marker, bodyAs(storage.body_as), storage.body_field ?? "instruction");
67
+ }
68
+ if (stype === "yaml") {
69
+ const container = storage.container ?? storage.dir;
70
+ if (!container)
71
+ throw new Error("storage type=yaml requires 'container'");
72
+ return SD.yaml(container);
73
+ }
74
+ if (stype === "standalone") {
75
+ const filename = storage.path ??
76
+ storage.filename ??
77
+ storage.marker;
78
+ if (!filename)
79
+ throw new Error("storage type=standalone requires 'path' (or 'filename')");
80
+ return SD.standalone(filename, bodyAs(storage.body_as), storage.body_field ?? "content");
81
+ }
82
+ if (stype === "root") {
83
+ return SD.root(storage.marker ?? "manifest.yaml");
84
+ }
85
+ throw new Error(`unknown storage type=${stype} (expected: bundle, yaml, standalone, root)`);
86
+ }
87
+ export class DeclarativeKindPort {
88
+ __declarative__ = true;
89
+ apiVersion;
90
+ kind;
91
+ alias;
92
+ origin;
93
+ docs;
94
+ isRoot;
95
+ isPromptTarget;
96
+ // F3 (spec D2): was hardcoded 5 — now declarable, default 5 preserved.
97
+ promptTargetPriority;
98
+ flattenInContext;
99
+ storage;
100
+ // F3 (spec D2): KindDefinitionSpec now populates is_runtime_artifact.
101
+ isRuntimeArtifact;
102
+ // ---- F3 descriptor fields (spec 2026-06-10-kinds-descriptor-f3, D2) --
103
+ // `plane`: mirrors KindBase.plane ("composition" default).
104
+ plane;
105
+ // `scope`: mirrors the class attribute (e.g. KaizenKind declares
106
+ // `scope = TenantScope.GLOBAL`). Only set when tenant_scope was
107
+ // EXPLICITLY declared — undeclared kinds stay permissive (Phase 1
108
+ // back-compat: the Kernel reads kp.scope ?? undefined).
109
+ scope;
110
+ // Kernel classification flags — mirror KindBase defaults.
111
+ scopeInheritable;
112
+ isOverlayable;
113
+ // `embedFields`: source fields for embedding text (D4 derivation).
114
+ embedFields;
115
+ // `summary`: declarative list-endpoint projection {field: default}.
116
+ _summary;
117
+ // Declared volatile fields union the KindBase defaults so the
118
+ // canonical digest contract matches hand-written record Kinds.
119
+ volatileSpecFields;
120
+ // -- Rendering hints (auto-derived from KindDefinition spec) ---
121
+ graphStyle;
122
+ asciiIcon;
123
+ displayLabel;
124
+ _depFilters;
125
+ _defaultAgent;
126
+ /** Public read-only view of the JSON Schema authored in the KindDefinition.
127
+ * Consumers like the Studio's NewDocumentWizard use this to pre-populate
128
+ * required fields with empty-but-valid stubs. */
129
+ jsonSchema;
130
+ _validate;
131
+ _ajv;
132
+ uiSchema;
133
+ // ---- Descriptor expressiveness fields (spec 2026-06-11, D1/D3-D7) ----
134
+ // D1 `ui`: reconstructed StudioUIMetadata (or undefined) — byte-identical
135
+ // /kinds/manifest output vs the deleted class.
136
+ ui;
137
+ // D3 `describe`: template string OR {path} projection.
138
+ _describe;
139
+ // D5 `spec_defaults`: shallow-merge map applied in parse() before validation.
140
+ _specDefaults;
141
+ // D6 `default_agent_field`: spec field returned VERBATIM by getDefaultAgentName.
142
+ _defaultAgentField;
143
+ // D7 `description_fallback_field`: pass-through string attr for Studio.
144
+ descriptionFallbackField;
145
+ constructor(typedDef) {
146
+ const spec = typedDef.spec;
147
+ this.apiVersion = spec.target_api_version;
148
+ this.kind = spec.target_kind;
149
+ this.alias = spec.alias;
150
+ this.origin = spec.origin;
151
+ this.isRoot = spec.is_root;
152
+ this.isPromptTarget = spec.prompt_target;
153
+ this.flattenInContext = spec.flatten_in_context;
154
+ this.isRuntimeArtifact = Boolean(spec.is_runtime_artifact ?? false);
155
+ // F3 fields read defensively (Python getattr twin) — specs are
156
+ // duck-typed in tests (hand-built objects), so stay defensive with
157
+ // the same defaults KindDefinitionSpecSchema declares.
158
+ const f3 = spec;
159
+ // F3 (spec D2): was hardcoded 5 — default 5 preserved.
160
+ this.promptTargetPriority = Math.trunc(f3.prompt_target_priority ?? 5);
161
+ this.plane = f3.plane ?? "composition";
162
+ if (f3.tenant_scope_declared) {
163
+ this.scope = f3.tenant_scope;
164
+ }
165
+ this.scopeInheritable = Boolean(f3.scope_inheritable ?? true);
166
+ this.isOverlayable = Boolean(f3.is_overlayable ?? true);
167
+ this.embedFields = f3.embed ?? null;
168
+ this._summary = f3.summary ?? null;
169
+ // Values MAY be projection objects (spec D2); lint them at load so a bad
170
+ // descriptor fails fast (unknown key / exclusivity). Twin of Python.
171
+ DeclarativeKindPort._lintSummary(this._summary);
172
+ // Declared ∪ KindBase defaults — read the defaults FROM kind_base
173
+ // (never re-hardcode; C1 review carry-over, twin of Python meta.py).
174
+ this.volatileSpecFields = new Set([
175
+ ...DEFAULT_VOLATILE_SPEC_FIELDS,
176
+ ...(f3.volatile_spec_fields ?? []),
177
+ ]);
178
+ this.docs = spec.docs ?? undefined;
179
+ this._depFilters = spec.dep_filters ?? null;
180
+ this._defaultAgent = spec.default_agent ?? null;
181
+ this.jsonSchema = spec.schema ?? {};
182
+ this.storage = storageDictToDescriptor(spec.storage);
183
+ // ---- Descriptor expressiveness fields (spec 2026-06-11, D1/D3-D7) ----
184
+ const exprSpec = spec;
185
+ // D1: reconstruct the real StudioUIMetadata from the validated mapping so
186
+ // /kinds/manifest output is byte-identical to the deleted class version.
187
+ this.ui =
188
+ exprSpec.ui != null
189
+ ? new StudioUIMetadata(exprSpec.ui)
190
+ : undefined;
191
+ // D3 describe (template string OR {path} mapping).
192
+ this._describe = exprSpec.describe ?? null;
193
+ // D5 spec_defaults — lint NOW (load time), fail fast on a bad descriptor.
194
+ this._specDefaults = exprSpec.spec_defaults ?? null;
195
+ if (this._specDefaults != null && Object.keys(this._specDefaults).length > 0) {
196
+ this._lintSpecDefaults(this._specDefaults, this.jsonSchema);
197
+ }
198
+ // D6 default_agent_field.
199
+ this._defaultAgentField = exprSpec.default_agent_field ?? null;
200
+ // D7 description_fallback_field pass-through.
201
+ this.descriptionFallbackField = exprSpec.description_fallback_field ?? undefined;
202
+ // Rendering hints — read from KindDefinition spec if user-provided,
203
+ // otherwise auto-derive. Custom kinds get a deterministic color from
204
+ // their origin hash so they're visually distinct in diagrams.
205
+ const rawSpec = spec;
206
+ const userStyle = rawSpec.graph_style;
207
+ // F3 parity fix: YAML descriptors are byte-identical Py↔TS, so the
208
+ // canonical key is snake_case `text_color` (what Python reads);
209
+ // `textColor` stays accepted for back-compat. Gate on fill+stroke only
210
+ // (mirrors Python — text_color defaults to "#fff").
211
+ const userTextColor = userStyle?.text_color ?? userStyle?.textColor;
212
+ if (userStyle?.fill && userStyle?.stroke) {
213
+ this.graphStyle = {
214
+ fill: userStyle.fill,
215
+ stroke: userStyle.stroke,
216
+ textColor: userTextColor ?? "#fff",
217
+ };
218
+ }
219
+ else {
220
+ // Deterministic color from origin hash — output as HEX because
221
+ // mermaid classDef doesn't support hsl() syntax.
222
+ let h = 0;
223
+ for (let i = 0; i < this.origin.length; i++) {
224
+ h = (h * 31 + this.origin.charCodeAt(i)) | 0;
225
+ }
226
+ const hue = Math.abs(h) % 360;
227
+ this.graphStyle = {
228
+ fill: hslToHex(hue, 55, 55),
229
+ stroke: hslToHex(hue, 55, 40),
230
+ textColor: "#fff",
231
+ };
232
+ }
233
+ this.asciiIcon = (typeof rawSpec.ascii_icon === "string" ? rawSpec.ascii_icon : null) ?? "📄";
234
+ this.displayLabel = (typeof rawSpec.display_label === "string" ? rawSpec.display_label : null) ?? (this.kind + "s");
235
+ if (Object.keys(this.jsonSchema).length > 0) {
236
+ // createAjv: strict:false to accept lenient JSON Schema shapes users
237
+ // author in YAML + annotation-only formats (Py jsonschema parity).
238
+ this._ajv = createAjv();
239
+ this._validate = this._ajv.compile(this.jsonSchema);
240
+ }
241
+ else {
242
+ this._ajv = null;
243
+ this._validate = null;
244
+ }
245
+ // Derive a uiSchema from the JSON Schema so the Studio's
246
+ // SchemaDrivenEditor can render a form for this custom kind with zero
247
+ // per-kind code. Users can still override individual fields via a
248
+ // `ui_schema:` block in the KindDefinition spec (takes precedence).
249
+ const derived = deriveUiSchemaFromJsonSchema(this.jsonSchema);
250
+ const userHints = spec.ui_schema ?? {};
251
+ const merged = { ...derived };
252
+ for (const [field, hint] of Object.entries(userHints)) {
253
+ merged[field] = { ...(merged[field] ?? {}), ...hint };
254
+ }
255
+ this.uiSchema = Object.keys(merged).length > 0 ? merged : undefined;
256
+ }
257
+ /** Load-time lint for `spec_defaults` (spec D5). Each default KEY must
258
+ * exist in schema.properties and its VALUE must validate against THAT
259
+ * property's subschema. `required` is intentionally IGNORED — defaults are
260
+ * a *partial* spec (autolab's real _DEFAULTS doesn't satisfy required).
261
+ * Twin of Python DeclarativeKindPort._lint_spec_defaults. */
262
+ _lintSpecDefaults(specDefaults, jsonSchema) {
263
+ const props = jsonSchema.properties ?? {};
264
+ const ajv = createAjv();
265
+ for (const [key, value] of Object.entries(specDefaults)) {
266
+ if (!(key in props)) {
267
+ throw new Error(`KindDefinition spec_defaults key '${key}' is not a property in schema.properties`);
268
+ }
269
+ const subschema = props[key];
270
+ if (subschema != null && typeof subschema === "object") {
271
+ const validate = ajv.compile(subschema);
272
+ if (!validate(value)) {
273
+ throw new Error(`KindDefinition spec_defaults['${key}']=${JSON.stringify(value)} ` +
274
+ `fails its property subschema: ${ajv.errorsText(validate.errors)}`);
275
+ }
276
+ }
277
+ }
278
+ }
279
+ depFilters() {
280
+ return this._depFilters;
281
+ }
282
+ getDefaultAgentName(doc) {
283
+ // D6: when default_agent_field is declared, return the spec field VERBATIM
284
+ // (no `?? null` coercion — "" stays "", mirroring the eval-evalexperiment
285
+ // class). Otherwise fall back to the static default_agent.
286
+ if (this._defaultAgentField != null) {
287
+ const rawSpec = doc.spec;
288
+ const spec = rawSpec != null && typeof rawSpec === "object" && !Array.isArray(rawSpec)
289
+ ? rawSpec
290
+ : {};
291
+ return this._defaultAgentField in spec
292
+ ? spec[this._defaultAgentField]
293
+ : null;
294
+ }
295
+ return this._defaultAgent;
296
+ }
297
+ getLayerPolicies(_doc) {
298
+ return null;
299
+ }
300
+ parse(raw) {
301
+ let spec = raw.spec ?? {};
302
+ // D5: shallow-merge {...spec_defaults, ...spec} BEFORE validation —
303
+ // exactly autolab-run's class behavior (defaults fill, spec overrides).
304
+ if (this._specDefaults != null && Object.keys(this._specDefaults).length > 0) {
305
+ spec = { ...this._specDefaults, ...spec };
306
+ raw = { ...raw, spec };
307
+ }
308
+ if (this._validate) {
309
+ const ok = this._validate(spec);
310
+ if (!ok) {
311
+ const errs = this._ajv?.errorsText(this._validate.errors);
312
+ throw new Error(`DeclarativeKind '${this.kind}' spec validation failed: ${errs}`);
313
+ }
314
+ }
315
+ return raw;
316
+ }
317
+ /** Display string for a doc (spec D3).
318
+ * - Template form (string): substitute `{field}` placeholders from the
319
+ * spec top level; a missing/None field renders as "".
320
+ * - Projection form ({path: field}): return the spec field verbatim (or
321
+ * null if absent).
322
+ * - No `describe` declared → null (today's behavior).
323
+ * Twin of Python DeclarativeKindPort.describe. */
324
+ describe(doc) {
325
+ if (this._describe == null)
326
+ return null;
327
+ const rawSpec = doc.spec;
328
+ const spec = rawSpec != null && typeof rawSpec === "object" && !Array.isArray(rawSpec)
329
+ ? rawSpec
330
+ : {};
331
+ if (typeof this._describe === "object") {
332
+ const fieldName = this._describe.path;
333
+ if (fieldName == null)
334
+ return null;
335
+ const val = spec[fieldName];
336
+ return val != null ? val : null;
337
+ }
338
+ // Template form: replace {field} with the spec value (missing/null → "").
339
+ return this._describe.replace(/\{([^}]+)\}/g, (_m, key) => {
340
+ const v = spec[key];
341
+ return v == null ? "" : String(v);
342
+ });
343
+ }
344
+ // -- Summary projection vocabulary (spec D2) -----------------------------
345
+ // A value in `summary:` MAY be a projection object. Plain values keep
346
+ // today's meaning (the projected default). The closed vocabulary + FIXED
347
+ // combinator order: resolve (`path`|`count_of`) → `default` → `round` →
348
+ // `truncate`. `format` is exclusive of the others. Twin of Python meta.py.
349
+ static _PROJECTION_KEYS = new Set([
350
+ "count_of",
351
+ "path",
352
+ "paths",
353
+ "format",
354
+ "truncate",
355
+ "round",
356
+ "default",
357
+ "filter_falsy",
358
+ "all_or_empty",
359
+ "placeholder_defaults",
360
+ ]);
361
+ // A dict VALUE is a projection only if it carries one of these markers
362
+ // (else it's a plain default that happens to be a dict).
363
+ static _PROJECTION_MARKERS = new Set([
364
+ "count_of",
365
+ "path",
366
+ "paths",
367
+ "format",
368
+ ]);
369
+ static _isProjection(value) {
370
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
371
+ return false;
372
+ }
373
+ for (const k of Object.keys(value)) {
374
+ if (DeclarativeKindPort._PROJECTION_MARKERS.has(k))
375
+ return true;
376
+ }
377
+ return false;
378
+ }
379
+ /** Load-time lint for projection objects in `summary:` (spec D2). Unknown
380
+ * key → throw; exactly one resolver; `format`/`paths` exclusivity. Twin of
381
+ * Python `_lint_summary`. */
382
+ static _lintSummary(summary) {
383
+ if (summary == null || typeof summary !== "object")
384
+ return;
385
+ for (const [field, value] of Object.entries(summary)) {
386
+ if (!DeclarativeKindPort._isProjection(value))
387
+ continue;
388
+ const keys = new Set(Object.keys(value));
389
+ const unknown = [...keys].filter((k) => !DeclarativeKindPort._PROJECTION_KEYS.has(k));
390
+ if (unknown.length > 0) {
391
+ throw new Error(`KindDefinition summary['${field}'] projection has unknown key(s) ` +
392
+ `${JSON.stringify(unknown.sort())}; allowed: ` +
393
+ `${JSON.stringify([...DeclarativeKindPort._PROJECTION_KEYS].sort())}`);
394
+ }
395
+ const resolvers = [...keys].filter((k) => ["count_of", "path", "paths", "format"].includes(k));
396
+ if (resolvers.length !== 1) {
397
+ throw new Error(`KindDefinition summary['${field}'] projection must have exactly ` +
398
+ `one of count_of/path/paths/format, got ${JSON.stringify(resolvers.sort())}`);
399
+ }
400
+ if (keys.has("format") &&
401
+ [...keys].some((k) => !["format", "all_or_empty", "placeholder_defaults"].includes(k))) {
402
+ throw new Error(`KindDefinition summary['${field}'] format projection is exclusive ` +
403
+ `of path/count_of/round/truncate/default`);
404
+ }
405
+ if (keys.has("paths") &&
406
+ [...keys].some((k) => !["paths", "filter_falsy"].includes(k))) {
407
+ throw new Error(`KindDefinition summary['${field}'] paths projection only supports ` +
408
+ `filter_falsy`);
409
+ }
410
+ }
411
+ }
412
+ /** Dict-only walk over a dotted `a.b.c` path. Missing → null. */
413
+ static _walkPath(spec, path) {
414
+ let cur = spec;
415
+ for (const seg of path.split(".")) {
416
+ if (cur == null ||
417
+ typeof cur !== "object" ||
418
+ Array.isArray(cur) ||
419
+ !(seg in cur)) {
420
+ return null;
421
+ }
422
+ cur = cur[seg];
423
+ }
424
+ return cur;
425
+ }
426
+ /** Decimal rounding that is byte-behavior-identical to CPython's built-in
427
+ * `round(value, ndigits)` — i.e. a faithful port of CPython `double_round`.
428
+ *
429
+ * CPython rounds the EXACT stored IEEE-754 double to `ndigits` decimal
430
+ * places, ties-to-even, on the *true binary* value. A naive
431
+ * `Math.round(value * 10^n) / 10^n` (round-half-away, and on the *scaled*
432
+ * double) diverges on the genuine half cases — e.g. `0.125@2` is exactly
433
+ * 1/8 so CPython ties to even → `0.12`, whereas `Math.round` gives `0.13`;
434
+ * and `2.675@2` stores as `2.67499…` so CPython → `2.67`. To match all of
435
+ * these we decompose the double into its exact rational `mantissa·2^exp2`,
436
+ * scale by `10^ndigits` with exact BigInt arithmetic, and round-half-to-even
437
+ * on the exact remainder. Verified against CPython `round` over a 20k-case
438
+ * Monte-Carlo (n∈{1,2,3,4}) plus the pinned hard cases: 0 mismatches.
439
+ *
440
+ * Real consumers only use round:4 (avg_score/cost); this keeps the deeper
441
+ * depths (round:2, the spec's #1 parity risk) identical too. */
442
+ static _bankersRound(value, ndigits) {
443
+ if (!Number.isFinite(value))
444
+ return value;
445
+ if (value === 0)
446
+ return value;
447
+ const neg = value < 0 || Object.is(value, -0);
448
+ const v = Math.abs(value);
449
+ // Decompose v exactly into mantissa * 2^exp2 via its IEEE-754 bits.
450
+ const buf = new ArrayBuffer(8);
451
+ const dv = new DataView(buf);
452
+ dv.setFloat64(0, v);
453
+ const hi = dv.getUint32(0);
454
+ const lo = dv.getUint32(4);
455
+ const rawExp = (hi >>> 20) & 0x7ff;
456
+ let mantissa = ((BigInt(hi & 0xfffff) << 32n) | BigInt(lo >>> 0));
457
+ let exp2;
458
+ if (rawExp === 0) {
459
+ exp2 = -1074; // subnormal
460
+ }
461
+ else {
462
+ mantissa |= 1n << 52n;
463
+ exp2 = rawExp - 1075;
464
+ }
465
+ // We want round(value * 10^ndigits) to nearest integer, ties-to-even,
466
+ // computed exactly as a rational num/den.
467
+ const ten = 10n ** BigInt(Math.abs(ndigits));
468
+ let num = mantissa;
469
+ let den = 1n;
470
+ if (exp2 >= 0)
471
+ num <<= BigInt(exp2);
472
+ else
473
+ den <<= BigInt(-exp2);
474
+ if (ndigits >= 0)
475
+ num *= ten;
476
+ else
477
+ den *= ten;
478
+ let q = num / den;
479
+ const r = num - q * den; // 0 <= r < den
480
+ const twice = r * 2n;
481
+ if (twice > den)
482
+ q += 1n;
483
+ else if (twice === den && q % 2n === 1n)
484
+ q += 1n; // tie → even
485
+ // Reassemble q / 10^ndigits as the exact decimal string, then parse — this
486
+ // lands on the same double CPython produces (no scaled-division ULP drift).
487
+ let result;
488
+ if (ndigits > 0) {
489
+ const s = q.toString().padStart(ndigits + 1, "0");
490
+ const cut = s.length - ndigits;
491
+ result = Number(`${s.slice(0, cut)}.${s.slice(cut)}`);
492
+ }
493
+ else if (ndigits === 0) {
494
+ result = Number(q);
495
+ }
496
+ else {
497
+ result = Number(q) * Number(ten);
498
+ }
499
+ return neg ? -result : result;
500
+ }
501
+ static _resolveProjection(spec, proj) {
502
+ // -- format (exclusive) -------------------------------------------------
503
+ if ("format" in proj) {
504
+ const template = proj.format;
505
+ const allOrEmpty = Boolean(proj.all_or_empty);
506
+ const phDefaults = proj.placeholder_defaults ?? {};
507
+ const names = [...template.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]);
508
+ const resolved = {};
509
+ for (const name of names) {
510
+ const present = name in spec && spec[name] != null;
511
+ if (present) {
512
+ resolved[name] = spec[name];
513
+ }
514
+ else if (allOrEmpty) {
515
+ return "";
516
+ }
517
+ else if (name in phDefaults) {
518
+ resolved[name] = phDefaults[name];
519
+ }
520
+ else {
521
+ resolved[name] = "";
522
+ }
523
+ }
524
+ return template.replace(/\{([^}]+)\}/g, (_m, key) => {
525
+ const v = resolved[key];
526
+ return v == null ? "" : String(v);
527
+ });
528
+ }
529
+ // -- paths + filter_falsy (leaf-keyed) ----------------------------------
530
+ if ("paths" in proj) {
531
+ const out = {};
532
+ const filterFalsy = Boolean(proj.filter_falsy);
533
+ for (const path of proj.paths) {
534
+ const leaf = path.split(".").pop();
535
+ const val = DeclarativeKindPort._walkPath(spec, path);
536
+ if (filterFalsy && !val)
537
+ continue;
538
+ out[leaf] = val;
539
+ }
540
+ return out;
541
+ }
542
+ // -- resolve: count_of | path -------------------------------------------
543
+ let value;
544
+ if ("count_of" in proj) {
545
+ const target = spec[proj.count_of];
546
+ value =
547
+ target == null
548
+ ? 0
549
+ : typeof target === "string" || Array.isArray(target)
550
+ ? target.length
551
+ : 0;
552
+ }
553
+ else {
554
+ value = DeclarativeKindPort._walkPath(spec, proj.path);
555
+ }
556
+ // -- default (fires on missing OR None, post-resolve) -------------------
557
+ if ("default" in proj && value == null) {
558
+ value = proj.default;
559
+ }
560
+ // -- round (numeric; null passes through) -------------------------------
561
+ if ("round" in proj &&
562
+ typeof value === "number" &&
563
+ Number.isFinite(value)) {
564
+ value = DeclarativeKindPort._bankersRound(value, proj.round);
565
+ }
566
+ // -- truncate (string[:N]) ----------------------------------------------
567
+ if ("truncate" in proj && typeof value === "string") {
568
+ value = value.slice(0, proj.truncate);
569
+ }
570
+ return value;
571
+ }
572
+ /** Declarative projection (F3 spec D2): when the KindDefinition declares
573
+ * `summary: {field: <plain default | projection object>}`, project the
574
+ * doc's spec. A PLAIN value keeps today's meaning (present field from
575
+ * spec, else the declared default). A PROJECTION object runs the closed
576
+ * vocabulary (count_of/path/paths/format + combinators). No declaration →
577
+ * null (today's behavior). Twin of Python DeclarativeKindPort.summary. */
578
+ summary(doc) {
579
+ if (this._summary == null)
580
+ return null;
581
+ const rawSpec = doc.spec;
582
+ const spec = rawSpec != null && typeof rawSpec === "object" && !Array.isArray(rawSpec)
583
+ ? rawSpec
584
+ : {};
585
+ const out = {};
586
+ for (const [c, d] of Object.entries(this._summary)) {
587
+ if (DeclarativeKindPort._isProjection(d)) {
588
+ out[c] = DeclarativeKindPort._resolveProjection(spec, d);
589
+ }
590
+ else {
591
+ out[c] = c in spec ? spec[c] : d;
592
+ }
593
+ }
594
+ return out;
595
+ }
596
+ promptTemplate() {
597
+ return null;
598
+ }
599
+ dependencies() {
600
+ return this.depFilters();
601
+ }
602
+ schema() {
603
+ return Object.keys(this.jsonSchema).length > 0 ? this.jsonSchema : null;
604
+ }
605
+ /**
606
+ * Preview blocks derived from the kind's JSON schema. Walks the
607
+ * top-level `properties` of `jsonSchema` and renders each one based on
608
+ * its declared type:
609
+ * - string with format=markdown OR maxLength>=400 → markdown block
610
+ * - string → fields entry
611
+ * - integer / number / boolean → fields entry
612
+ * - array of strings → fields entry (bullets)
613
+ * - array of objects / object → code block (json)
614
+ * - enum → fields entry
615
+ *
616
+ * Required fields surface first, optional after.
617
+ */
618
+ preview(doc) {
619
+ const spec = (doc.spec ?? {});
620
+ const props = this.jsonSchema.properties;
621
+ if (!props || Object.keys(props).length === 0) {
622
+ // No schema info — just dump the spec
623
+ if (!spec || Object.keys(spec).length === 0) {
624
+ return [{ kind: "empty", title: `${this.kind} (empty)` }];
625
+ }
626
+ return [
627
+ {
628
+ kind: "code",
629
+ title: `${this.kind} spec`,
630
+ body: JSON.stringify(spec, null, 2),
631
+ language: "json",
632
+ },
633
+ ];
634
+ }
635
+ const required = new Set(this.jsonSchema.required ?? []);
636
+ const ordered = Object.entries(props).sort(([a], [b]) => {
637
+ const ar = required.has(a) ? 0 : 1;
638
+ const br = required.has(b) ? 0 : 1;
639
+ if (ar !== br)
640
+ return ar - br;
641
+ return a.localeCompare(b);
642
+ });
643
+ const blocks = [];
644
+ const fields = [];
645
+ for (const [field, propSchema] of ordered) {
646
+ const value = spec[field];
647
+ if (value == null || value === "")
648
+ continue;
649
+ const type = propSchema.type;
650
+ const format = propSchema.format;
651
+ const maxLength = propSchema.maxLength ?? 0;
652
+ const isEnum = Array.isArray(propSchema.enum);
653
+ // Long markdown / multi-line strings → standalone markdown block
654
+ if (type === "string" &&
655
+ typeof value === "string" &&
656
+ (format === "markdown" || maxLength >= 400 || value.length > 200)) {
657
+ blocks.push({
658
+ kind: "markdown",
659
+ title: propSchema.title ?? field,
660
+ body: value,
661
+ });
662
+ continue;
663
+ }
664
+ if (type === "string" && typeof value === "string") {
665
+ fields.push({
666
+ label: propSchema.title ?? field,
667
+ value: isEnum ? value : value,
668
+ });
669
+ continue;
670
+ }
671
+ if (type === "integer" || type === "number") {
672
+ fields.push({ label: field, value: String(value) });
673
+ continue;
674
+ }
675
+ if (type === "boolean") {
676
+ fields.push({ label: field, value: value ? "true" : "false" });
677
+ continue;
678
+ }
679
+ if (type === "array" && Array.isArray(value)) {
680
+ const items = propSchema.items ?? {};
681
+ if (items.type === "string" && !items.enum) {
682
+ fields.push({
683
+ label: propSchema.title ?? field,
684
+ value: value.map((v) => `• ${String(v)}`).join("\n"),
685
+ });
686
+ }
687
+ else {
688
+ blocks.push({
689
+ kind: "code",
690
+ title: propSchema.title ?? field,
691
+ body: JSON.stringify(value, null, 2),
692
+ language: "json",
693
+ });
694
+ }
695
+ continue;
696
+ }
697
+ if (type === "object" || (typeof value === "object" && value != null)) {
698
+ blocks.push({
699
+ kind: "code",
700
+ title: propSchema.title ?? field,
701
+ body: JSON.stringify(value, null, 2),
702
+ language: "json",
703
+ });
704
+ continue;
705
+ }
706
+ // Fallback
707
+ fields.push({ label: field, value: String(value) });
708
+ }
709
+ if (fields.length > 0) {
710
+ blocks.unshift({ kind: "fields", title: this.kind, fields });
711
+ }
712
+ if (blocks.length === 0) {
713
+ return [{ kind: "empty", title: `${this.kind} (empty)` }];
714
+ }
715
+ return blocks;
716
+ }
717
+ static fromTyped(typedDef) {
718
+ return new DeclarativeKindPort(typedDef);
719
+ }
720
+ }
721
+ // ---------------------------------------------------------------------------
722
+ // JSON Schema → uiSchema derivation
723
+ // ---------------------------------------------------------------------------
724
+ /**
725
+ * Map a single JSON Schema property node to a `SchemaDrivenEditor` field hint.
726
+ * Conservative by default — unknown shapes fall back to a YAML editor so
727
+ * nothing is ever lost.
728
+ */
729
+ function fieldHintForJsonSchemaProp(prop) {
730
+ const type = prop.type;
731
+ const format = prop.format;
732
+ const title = prop.title;
733
+ const description = prop.description;
734
+ const enumVals = prop.enum;
735
+ const hint = {};
736
+ if (title)
737
+ hint.label = title;
738
+ if (description)
739
+ hint.help = description;
740
+ // Enum → select (dropdown)
741
+ if (enumVals && enumVals.length > 0) {
742
+ hint.widget = "select";
743
+ hint.options = enumVals;
744
+ return hint;
745
+ }
746
+ if (type === "string") {
747
+ if (format === "markdown" || format === "md") {
748
+ hint.widget = "markdown";
749
+ hint.height = 300;
750
+ }
751
+ else if (format === "yaml") {
752
+ hint.widget = "code";
753
+ hint.language = "yaml";
754
+ hint.height = 240;
755
+ }
756
+ else if (format === "code") {
757
+ hint.widget = "code";
758
+ hint.height = 240;
759
+ }
760
+ else if (format === "textarea" || (prop.maxLength ?? 0) > 200) {
761
+ hint.widget = "textarea";
762
+ }
763
+ else {
764
+ hint.widget = "input";
765
+ }
766
+ return hint;
767
+ }
768
+ if (type === "integer" || type === "number") {
769
+ hint.widget = "number";
770
+ return hint;
771
+ }
772
+ if (type === "boolean") {
773
+ hint.widget = "checkbox";
774
+ return hint;
775
+ }
776
+ if (type === "array") {
777
+ const items = prop.items ?? {};
778
+ if (items.type === "string" && !items.enum) {
779
+ hint.widget = "tags";
780
+ return hint;
781
+ }
782
+ // arrays of objects or anything richer → fallback to yaml editor
783
+ hint.widget = "yaml";
784
+ hint.height = 200;
785
+ return hint;
786
+ }
787
+ if (type === "object") {
788
+ hint.widget = "yaml";
789
+ hint.height = 240;
790
+ return hint;
791
+ }
792
+ // Unknown / polymorphic → safe yaml fallback
793
+ hint.widget = "yaml";
794
+ hint.height = 200;
795
+ return hint;
796
+ }
797
+ /**
798
+ * Walk a JSON Schema's top-level `properties` and produce a uiSchema map
799
+ * keyed by field name. Required fields get `required: true` so the
800
+ * SchemaDrivenEditor can mark them visually if it chooses to.
801
+ */
802
+ export function deriveUiSchemaFromJsonSchema(jsonSchema) {
803
+ const out = {};
804
+ const props = jsonSchema.properties;
805
+ if (!props)
806
+ return out;
807
+ const required = new Set(jsonSchema.required ?? []);
808
+ let order = 0;
809
+ for (const [field, prop] of Object.entries(props)) {
810
+ const hint = fieldHintForJsonSchemaProp(prop);
811
+ hint.order = order++;
812
+ if (required.has(field))
813
+ hint.required = true;
814
+ out[field] = hint;
815
+ }
816
+ return out;
817
+ }