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.
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/dist/adapters/embedding/onnx.d.ts +32 -0
- package/dist/adapters/embedding/onnx.js +51 -0
- package/dist/adapters/filesystem/cache.d.ts +16 -0
- package/dist/adapters/filesystem/cache.js +113 -0
- package/dist/adapters/filesystem/index.d.ts +2 -0
- package/dist/adapters/filesystem/index.js +2 -0
- package/dist/adapters/filesystem/source.d.ts +82 -0
- package/dist/adapters/filesystem/source.js +357 -0
- package/dist/adapters/postgres/index.d.ts +2 -0
- package/dist/adapters/postgres/index.js +2 -0
- package/dist/adapters/postgres/migrations.d.ts +14 -0
- package/dist/adapters/postgres/migrations.js +88 -0
- package/dist/adapters/postgres/source.d.ts +172 -0
- package/dist/adapters/postgres/source.js +569 -0
- package/dist/adapters/resolvers/github.d.ts +12 -0
- package/dist/adapters/resolvers/github.js +47 -0
- package/dist/adapters/resolvers/http.d.ts +22 -0
- package/dist/adapters/resolvers/http.js +145 -0
- package/dist/adapters/resolvers/index.d.ts +3 -0
- package/dist/adapters/resolvers/index.js +3 -0
- package/dist/adapters/resolvers/local.d.ts +24 -0
- package/dist/adapters/resolvers/local.js +132 -0
- package/dist/adapters/search/driver.d.ts +45 -0
- package/dist/adapters/search/driver.js +138 -0
- package/dist/adapters/search/migrations.d.ts +26 -0
- package/dist/adapters/search/migrations.js +58 -0
- package/dist/adapters/search/rrf.d.ts +23 -0
- package/dist/adapters/search/rrf.js +42 -0
- package/dist/adapters/search/sqlite-vec.d.ts +66 -0
- package/dist/adapters/search/sqlite-vec.js +294 -0
- package/dist/bootstrap.d.ts +45 -0
- package/dist/bootstrap.js +134 -0
- package/dist/extensions/agentskills/DOCS.md +18 -0
- package/dist/extensions/agentskills.d.ts +28 -0
- package/dist/extensions/agentskills.js +252 -0
- package/dist/extensions/agentsmd/DOCS.md +18 -0
- package/dist/extensions/agentsmd.d.ts +22 -0
- package/dist/extensions/agentsmd.js +170 -0
- package/dist/extensions/audit/kinds/audit-log.kind.yaml +148 -0
- package/dist/extensions/audit.d.ts +25 -0
- package/dist/extensions/audit.js +62 -0
- package/dist/extensions/automation/kinds/automation.kind.yaml +296 -0
- package/dist/extensions/automation/query.d.ts +49 -0
- package/dist/extensions/automation/query.js +77 -0
- package/dist/extensions/automation/write-guards.d.ts +59 -0
- package/dist/extensions/automation/write-guards.js +153 -0
- package/dist/extensions/automation.d.ts +41 -0
- package/dist/extensions/automation.js +54 -0
- package/dist/extensions/collab.d.ts +16 -0
- package/dist/extensions/collab.js +79 -0
- package/dist/extensions/doc/kinds/doc.kind.yaml +118 -0
- package/dist/extensions/doc.d.ts +25 -0
- package/dist/extensions/doc.js +10 -0
- package/dist/extensions/eval/kinds/eval-baseline.kind.yaml +65 -0
- package/dist/extensions/eval/kinds/eval-case.kind.yaml +140 -0
- package/dist/extensions/eval/kinds/eval-run.kind.yaml +134 -0
- package/dist/extensions/eval/kinds/eval-suite.kind.yaml +87 -0
- package/dist/extensions/eval.d.ts +24 -0
- package/dist/extensions/eval.js +31 -0
- package/dist/extensions/evidence/kinds/evidence.kind.yaml +92 -0
- package/dist/extensions/evidence.d.ts +17 -0
- package/dist/extensions/evidence.js +94 -0
- package/dist/extensions/federation.d.ts +22 -0
- package/dist/extensions/federation.js +173 -0
- package/dist/extensions/guardrails/DOCS.md +20 -0
- package/dist/extensions/guardrails.d.ts +31 -0
- package/dist/extensions/guardrails.js +228 -0
- package/dist/extensions/helix/DOCS-Actor.md +20 -0
- package/dist/extensions/helix/DOCS-Agent.md +20 -0
- package/dist/extensions/helix/DOCS-UseCase.md +20 -0
- package/dist/extensions/helix/layers.d.ts +12 -0
- package/dist/extensions/helix/layers.js +18 -0
- package/dist/extensions/helix/write-guards.d.ts +70 -0
- package/dist/extensions/helix/write-guards.js +172 -0
- package/dist/extensions/helix.d.ts +29 -0
- package/dist/extensions/helix.js +963 -0
- package/dist/extensions/helix_extras.d.ts +124 -0
- package/dist/extensions/helix_extras.js +328 -0
- package/dist/extensions/hooks.d.ts +31 -0
- package/dist/extensions/hooks.js +247 -0
- package/dist/extensions/kinddef/DOCS.md +24 -0
- package/dist/extensions/kinddef.d.ts +40 -0
- package/dist/extensions/kinddef.js +171 -0
- package/dist/extensions/lesson.d.ts +6 -0
- package/dist/extensions/lesson.js +194 -0
- package/dist/extensions/modelreg/kinds/model-profile.kind.yaml +155 -0
- package/dist/extensions/modelreg.d.ts +30 -0
- package/dist/extensions/modelreg.js +38 -0
- package/dist/extensions/recognizer.d.ts +34 -0
- package/dist/extensions/recognizer.js +272 -0
- package/dist/extensions/research.d.ts +32 -0
- package/dist/extensions/research.js +292 -0
- package/dist/extensions/safety.d.ts +33 -0
- package/dist/extensions/safety.js +230 -0
- package/dist/extensions/sdlc/kinds/adr.kind.yaml +136 -0
- package/dist/extensions/sdlc/kinds/archive-proposal.kind.yaml +107 -0
- package/dist/extensions/sdlc/kinds/changelog.kind.yaml +108 -0
- package/dist/extensions/sdlc/kinds/cognitive-policy.kind.yaml +470 -0
- package/dist/extensions/sdlc/kinds/forecast.kind.yaml +125 -0
- package/dist/extensions/sdlc/kinds/insight.kind.yaml +87 -0
- package/dist/extensions/sdlc/kinds/kaizen.kind.yaml +106 -0
- package/dist/extensions/sdlc/kinds/lesson-learned.kind.yaml +270 -0
- package/dist/extensions/sdlc/kinds/narrative.kind.yaml +189 -0
- package/dist/extensions/sdlc/kinds/pattern-insight.kind.yaml +204 -0
- package/dist/extensions/sdlc/kinds/postmortem.kind.yaml +168 -0
- package/dist/extensions/sdlc/kinds/pre-mortem.kind.yaml +145 -0
- package/dist/extensions/sdlc/kinds/prompt-template.kind.yaml +109 -0
- package/dist/extensions/sdlc/kinds/retrospective.kind.yaml +186 -0
- package/dist/extensions/sdlc/kinds/risk-register.kind.yaml +178 -0
- package/dist/extensions/sdlc/kinds/saved-view.kind.yaml +127 -0
- package/dist/extensions/sdlc/kinds/status-report.kind.yaml +129 -0
- package/dist/extensions/sdlc/kinds/synthesis-run.kind.yaml +279 -0
- package/dist/extensions/sdlc/kinds/synthesizer-state.kind.yaml +196 -0
- package/dist/extensions/sdlc/kinds/workflow-event.kind.yaml +195 -0
- package/dist/extensions/sdlc.d.ts +32 -0
- package/dist/extensions/sdlc.js +1318 -0
- package/dist/extensions/soulspec/DOCS.md +19 -0
- package/dist/extensions/soulspec.d.ts +22 -0
- package/dist/extensions/soulspec.js +287 -0
- package/dist/extensions/tenant.d.ts +6 -0
- package/dist/extensions/tenant.js +291 -0
- package/dist/extensions/testkit.d.ts +24 -0
- package/dist/extensions/testkit.js +211 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +102 -0
- package/dist/kernel/_text.d.ts +28 -0
- package/dist/kernel/_text.js +42 -0
- package/dist/kernel/bundle-handle.d.ts +83 -0
- package/dist/kernel/bundle-handle.js +184 -0
- package/dist/kernel/capabilities.d.ts +157 -0
- package/dist/kernel/capabilities.js +109 -0
- package/dist/kernel/collaborator-ports.d.ts +91 -0
- package/dist/kernel/collaborator-ports.js +1 -0
- package/dist/kernel/composition-resolver.d.ts +215 -0
- package/dist/kernel/composition-resolver.js +558 -0
- package/dist/kernel/composition.d.ts +10 -0
- package/dist/kernel/composition.js +1 -0
- package/dist/kernel/descriptor-loader.d.ts +13 -0
- package/dist/kernel/descriptor-loader.js +48 -0
- package/dist/kernel/document.d.ts +43 -0
- package/dist/kernel/document.js +71 -0
- package/dist/kernel/embedding.d.ts +28 -0
- package/dist/kernel/embedding.js +78 -0
- package/dist/kernel/errors.d.ts +62 -0
- package/dist/kernel/errors.js +82 -0
- package/dist/kernel/events.d.ts +6 -0
- package/dist/kernel/events.js +21 -0
- package/dist/kernel/evidence-capture.d.ts +69 -0
- package/dist/kernel/evidence-capture.js +138 -0
- package/dist/kernel/fs.d.ts +20 -0
- package/dist/kernel/fs.js +128 -0
- package/dist/kernel/generic-rw.d.ts +33 -0
- package/dist/kernel/generic-rw.js +235 -0
- package/dist/kernel/hooks.d.ts +172 -0
- package/dist/kernel/hooks.js +256 -0
- package/dist/kernel/index.d.ts +624 -0
- package/dist/kernel/index.js +1638 -0
- package/dist/kernel/instance.d.ts +211 -0
- package/dist/kernel/instance.js +487 -0
- package/dist/kernel/kind-registry.d.ts +183 -0
- package/dist/kernel/kind-registry.js +710 -0
- package/dist/kernel/kind_base.d.ts +106 -0
- package/dist/kernel/kind_base.js +244 -0
- package/dist/kernel/layer-resolver.d.ts +48 -0
- package/dist/kernel/layer-resolver.js +284 -0
- package/dist/kernel/lock-manager.d.ts +21 -0
- package/dist/kernel/lock-manager.js +43 -0
- package/dist/kernel/lock.d.ts +38 -0
- package/dist/kernel/lock.js +115 -0
- package/dist/kernel/meta.d.ts +135 -0
- package/dist/kernel/meta.js +817 -0
- package/dist/kernel/models.d.ts +3089 -0
- package/dist/kernel/models.js +764 -0
- package/dist/kernel/navigator.d.ts +36 -0
- package/dist/kernel/navigator.js +132 -0
- package/dist/kernel/port-surface.d.ts +33 -0
- package/dist/kernel/port-surface.js +180 -0
- package/dist/kernel/preview.d.ts +60 -0
- package/dist/kernel/preview.js +54 -0
- package/dist/kernel/prompt-budget.d.ts +39 -0
- package/dist/kernel/prompt-budget.js +43 -0
- package/dist/kernel/prompt-builder.d.ts +25 -0
- package/dist/kernel/prompt-builder.js +237 -0
- package/dist/kernel/protocols.d.ts +751 -0
- package/dist/kernel/protocols.js +493 -0
- package/dist/kernel/reports.d.ts +26 -0
- package/dist/kernel/reports.js +215 -0
- package/dist/kernel/resolver.d.ts +146 -0
- package/dist/kernel/resolver.js +232 -0
- package/dist/kernel/resource.d.ts +57 -0
- package/dist/kernel/resource.js +97 -0
- package/dist/kernel/runtime.d.ts +16 -0
- package/dist/kernel/runtime.js +18 -0
- package/dist/kernel/safety-scanner.d.ts +38 -0
- package/dist/kernel/safety-scanner.js +228 -0
- package/dist/kernel/serialize-to-files.d.ts +29 -0
- package/dist/kernel/serialize-to-files.js +23 -0
- package/dist/kernel/spec-access.d.ts +23 -0
- package/dist/kernel/spec-access.js +40 -0
- package/dist/kernel/studio_ui.d.ts +88 -0
- package/dist/kernel/studio_ui.js +135 -0
- package/dist/kernel/templates.d.ts +77 -0
- package/dist/kernel/templates.js +116 -0
- package/dist/kernel/tool-registry.d.ts +52 -0
- package/dist/kernel/tool-registry.js +89 -0
- package/dist/kernel/write-pipeline.d.ts +65 -0
- package/dist/kernel/write-pipeline.js +271 -0
- package/dist/kernel/writer-helpers.d.ts +24 -0
- package/dist/kernel/writer-helpers.js +42 -0
- package/dist/memory/decay.d.ts +31 -0
- package/dist/memory/decay.js +97 -0
- package/dist/memory/ecphory.d.ts +25 -0
- package/dist/memory/ecphory.js +178 -0
- package/dist/memory/encodingContext.d.ts +21 -0
- package/dist/memory/encodingContext.js +62 -0
- package/dist/memory/index.d.ts +23 -0
- package/dist/memory/index.js +23 -0
- package/dist/memory/memoryType.d.ts +7 -0
- package/dist/memory/memoryType.js +24 -0
- package/dist/memory/policy.d.ts +35 -0
- package/dist/memory/policy.js +35 -0
- package/dist/memory/retrieval.d.ts +21 -0
- package/dist/memory/retrieval.js +145 -0
- package/dist/testing/index.d.ts +8 -0
- package/dist/testing/index.js +8 -0
- package/dist/testing/recordSearchConformance.d.ts +43 -0
- package/dist/testing/recordSearchConformance.js +147 -0
- package/dist/viz/ascii.d.ts +7 -0
- package/dist/viz/ascii.js +60 -0
- package/dist/viz/health.d.ts +8 -0
- package/dist/viz/health.js +161 -0
- package/dist/viz/index.d.ts +10 -0
- package/dist/viz/index.js +10 -0
- package/dist/viz/matrix.d.ts +8 -0
- package/dist/viz/matrix.js +84 -0
- package/dist/viz/mermaid.d.ts +38 -0
- package/dist/viz/mermaid.js +706 -0
- 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
|
+
}
|