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,1638 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel v3 — Mediator connecting 5 ports.
|
|
3
|
+
*
|
|
4
|
+
* 1:1 parity with Python dna.v3.kernel.__init__.
|
|
5
|
+
*/
|
|
6
|
+
import { Document } from "./document.js";
|
|
7
|
+
import { deriveFirstLine } from "./_text.js";
|
|
8
|
+
import { ExtensionLoadError, KindRegistrationError, ReaderRegistrationError, WriterRegistrationError, } from "./errors.js";
|
|
9
|
+
import { HookRegistry } from "./hooks.js";
|
|
10
|
+
import { WritePipeline } from "./write-pipeline.js";
|
|
11
|
+
import { FakeEmbeddingProvider } from "./embedding.js";
|
|
12
|
+
import { ManifestInstance } from "./instance.js";
|
|
13
|
+
import { ResolveError, LayerPolicyViolationError, validateTenantSlug, } from "./protocols.js";
|
|
14
|
+
import { ToolRegistry } from "./tool-registry.js";
|
|
15
|
+
import { materialize, } from "./templates.js";
|
|
16
|
+
import yaml from "js-yaml";
|
|
17
|
+
// s-invert-layer-resolver-dep (2026-07-07): the resolver is kernel-owned —
|
|
18
|
+
// the kernel imports NO extension modules (guarded by
|
|
19
|
+
// tests/kernel-extension-boundary.test.ts).
|
|
20
|
+
import { DefaultLayerResolver } from "./layer-resolver.js";
|
|
21
|
+
import { CompositionResolver } from "./composition-resolver.js";
|
|
22
|
+
import { KindRegistry } from "./kind-registry.js";
|
|
23
|
+
import { sourceCapabilities } from "./capabilities.js";
|
|
24
|
+
import { DEFAULT_INHERITABLE_KINDS_V1 } from "./resolver.js";
|
|
25
|
+
import { GenericBundleReader, GenericBundleWriter } from "./generic-rw.js";
|
|
26
|
+
import { nodeFS } from "./fs.js";
|
|
27
|
+
// s-kernel-decomp-ts-parity — alias generation + the KIND_NAME collision
|
|
28
|
+
// ratchet moved into the KindRegistry module (Py twin: kind_registry.py).
|
|
29
|
+
// Re-exported here so the historical `from "../src/kernel"` import path (tests,
|
|
30
|
+
// tooling) keeps resolving them.
|
|
31
|
+
export { kebabKindName, generateAlias, EXPLICIT_ALIAS_ALLOWLIST, KIND_NAME_COLLISION_ALLOWLIST, } from "./kind-registry.js";
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Write-facade public types — 1:1 parity with Python dna.kernel.
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
/** Raised when writeDocument/deleteDocument is called but no
|
|
36
|
+
* WritableSourcePort is registered on the Kernel. */
|
|
37
|
+
export class NotWritableError extends Error {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "NotWritableError";
|
|
41
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** i-112 Phase 1 — single source of truth para os special-scope names.
|
|
45
|
+
* Dois papéis distintos (paridade 1:1 com Python protocols.py):
|
|
46
|
+
* DEFAULT_BASE_SCOPE = fallback de herança; SYSTEM_SCOPE = casa dos lookups globais.
|
|
47
|
+
* Ambos "_lib" hoje; nomeados separados pra Fases 2-3 divergirem. */
|
|
48
|
+
export const DEFAULT_BASE_SCOPE = "_lib";
|
|
49
|
+
export const SYSTEM_SCOPE = "_lib";
|
|
50
|
+
/**
|
|
51
|
+
* The scope that owns the canonical ModelProfile registry.
|
|
52
|
+
* Queried directly by `modelProfile()` — regardless of the caller's scope.
|
|
53
|
+
* 1:1 parity with Python `Kernel._MODEL_REGISTRY_SCOPE`.
|
|
54
|
+
*/
|
|
55
|
+
export const MODEL_REGISTRY_SCOPE = SYSTEM_SCOPE;
|
|
56
|
+
/**
|
|
57
|
+
* The scope that owns the canonical VoicePolicy registry. GLOBAL like
|
|
58
|
+
* ModelProfile — queried directly regardless of the caller's scope.
|
|
59
|
+
* 1:1 parity with Python `Kernel._VOICE_POLICY_SCOPE`.
|
|
60
|
+
*/
|
|
61
|
+
export const VOICE_POLICY_SCOPE = SYSTEM_SCOPE;
|
|
62
|
+
export class Kernel {
|
|
63
|
+
// ── Kind classification — DERIVED from KindPort attributes ──────────────
|
|
64
|
+
// s-kernel-kindport-classification-attrs: the kernel no longer hardcodes
|
|
65
|
+
// Kind-name sets; it reads each registered Kind's declared attribute
|
|
66
|
+
// (isOverlayable / scopeInheritable). The membership API (`k.X.has(kind)`) is
|
|
67
|
+
// unchanged — these became instance getters — but the source of truth is now
|
|
68
|
+
// the Kind, not a literal list. 1:1 parity with the Python kernel.
|
|
69
|
+
//
|
|
70
|
+
// Structural bootstrap Kinds (scope identity / schema / policy) are
|
|
71
|
+
// non-overlayable AND non-inheritable BY DEFINITION (mirrors
|
|
72
|
+
// resolver BOOTSTRAP_KINDS); the legacy ledger names have no registered
|
|
73
|
+
// KindPort to carry an attribute. Both are unioned in so the classification
|
|
74
|
+
// is identical even on a kernel that hasn't registered those Kinds.
|
|
75
|
+
static _BOOTSTRAP_KINDS = new Set([
|
|
76
|
+
"Genome", "KindDefinition", "LayerPolicy",
|
|
77
|
+
]);
|
|
78
|
+
static _LEGACY_NON_INHERITABLE = new Set([
|
|
79
|
+
"Milestone", "VibeSession",
|
|
80
|
+
]);
|
|
81
|
+
static INHERIT_PARENT_SCOPE = DEFAULT_BASE_SCOPE;
|
|
82
|
+
_classifyKinds(pred) {
|
|
83
|
+
const out = new Set();
|
|
84
|
+
for (const kp of this._kinds.values()) {
|
|
85
|
+
if (kp.kind && pred(kp))
|
|
86
|
+
out.add(kp.kind);
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
/** Kinds structurally never overlayable. Derived from KindPort.isOverlayable
|
|
91
|
+
* (s-kernel-kindport-classification-attrs). 1:1 with Python
|
|
92
|
+
* Kernel._NON_OVERLAYABLE_KINDS. */
|
|
93
|
+
get NON_OVERLAYABLE_KINDS() {
|
|
94
|
+
const s = this._classifyKinds((kp) => kp.isOverlayable === false);
|
|
95
|
+
for (const k of Kernel._BOOTSTRAP_KINDS)
|
|
96
|
+
s.add(k);
|
|
97
|
+
return s;
|
|
98
|
+
}
|
|
99
|
+
/** Per-scope ledger + structural Kinds that do NOT inherit across scopes.
|
|
100
|
+
* Derived from KindPort.scopeInheritable. 1:1 with Python
|
|
101
|
+
* Kernel._NON_INHERITABLE_KINDS. */
|
|
102
|
+
get NON_INHERITABLE_KINDS() {
|
|
103
|
+
const s = this._classifyKinds((kp) => kp.scopeInheritable === false);
|
|
104
|
+
for (const k of Kernel._BOOTSTRAP_KINDS)
|
|
105
|
+
s.add(k);
|
|
106
|
+
for (const k of Kernel._LEGACY_NON_INHERITABLE)
|
|
107
|
+
s.add(k);
|
|
108
|
+
return s;
|
|
109
|
+
}
|
|
110
|
+
/** `k.INHERITABLE_KINDS.has(kind)` — denylist-backed membership (everything
|
|
111
|
+
* inherits EXCEPT NON_INHERITABLE_KINDS). 1:1 with Python _INHERITABLE_KINDS. */
|
|
112
|
+
get INHERITABLE_KINDS() {
|
|
113
|
+
const deny = this.NON_INHERITABLE_KINDS;
|
|
114
|
+
return { has: (kind) => !deny.has(kind) };
|
|
115
|
+
}
|
|
116
|
+
_source = null;
|
|
117
|
+
_cache = null;
|
|
118
|
+
_resolvers = new Map();
|
|
119
|
+
/** @internal — public so the KindRegistry `RegistryHost` slice reaches the
|
|
120
|
+
* rescan gate. Convention `_`-private; not a supported API surface. */
|
|
121
|
+
_readers = [];
|
|
122
|
+
_writers = [];
|
|
123
|
+
_writableSource = null;
|
|
124
|
+
_profiles = [];
|
|
125
|
+
/** Registered-Kind identity map + the registration funnel (Fase 3,
|
|
126
|
+
* s-kernel-decomp-ts-parity). OWNS the `_kinds` Map; the kernel's `_kinds`
|
|
127
|
+
* getter proxies to it, and `kind()`/`kindFromDescriptor()`/
|
|
128
|
+
* `_registerKindDefinitions()`/`_registerCustomKinds()`/`validateDepFilters()`
|
|
129
|
+
* are thin facades. Shared across withTenant copies (Object.assign copies the
|
|
130
|
+
* ref) — Kinds are global, registered once at boot. Py twin:
|
|
131
|
+
* `Kernel._kindreg`. */
|
|
132
|
+
_kindreg = new KindRegistry(undefined, this);
|
|
133
|
+
/** The registered-Kind dict — proxied to `this._kindreg` so the ~20 inline
|
|
134
|
+
* `this._kinds` read sites across the kernel keep working after the Fase 3
|
|
135
|
+
* extraction. Key: "apiVersion\0kind". Py twin: `Kernel._kinds` property. */
|
|
136
|
+
get _kinds() {
|
|
137
|
+
return this._kindreg.kinds;
|
|
138
|
+
}
|
|
139
|
+
/** Extensions that successfully registered, in load() order. Used by
|
|
140
|
+
* listTemplates() to aggregate scaffolds from every loaded extension. */
|
|
141
|
+
_extensions = [];
|
|
142
|
+
/** Tool-definition registry (s-dna-port-surface-parity — TS twin of the
|
|
143
|
+
* Py `Kernel._toolreg`). Tools are global (not tenant-scoped), so one
|
|
144
|
+
* registry is safely shared across withTenant shallow copies. */
|
|
145
|
+
_toolreg = new ToolRegistry();
|
|
146
|
+
/** Composition-V2 engine (Phase 17 — s-ts-composition-v2-port). Holds a
|
|
147
|
+
* back-ref to this kernel; the kernel keeps the public methods
|
|
148
|
+
* (resolveDocument / computeResolutionChain / personalizeDocument /
|
|
149
|
+
* compositionSummary) as thin delegators, 1:1 with the Python layout. */
|
|
150
|
+
_composition = new CompositionResolver(this);
|
|
151
|
+
/** Document write/delete execution (tenant resolve, layer-policy check,
|
|
152
|
+
* pre_save veto, persist, post_save/post_delete) — extracted collaborator
|
|
153
|
+
* (Fase 2, s-kernel-decomp-f2-writepipeline). The kernel keeps
|
|
154
|
+
* writeDocument/deleteDocument as thin facades. Non-readonly: withTenant
|
|
155
|
+
* re-instantiates it against the copy so a tenant-bound kernel resolves its
|
|
156
|
+
* own tenant. Py twin: `Kernel._write_pipeline`. */
|
|
157
|
+
_writePipeline = new WritePipeline(this);
|
|
158
|
+
hooks = new HookRegistry();
|
|
159
|
+
/** @internal — public for the KindRegistry `RegistryHost` slice. */
|
|
160
|
+
_genericsResolved = false;
|
|
161
|
+
_fs = nodeFS;
|
|
162
|
+
/** s-alias-generated-not-typed — owner context for alias generation of
|
|
163
|
+
* the Kinds registered by the Extension currently inside load().
|
|
164
|
+
* @internal — public for the KindRegistry `RegistryHost` slice.
|
|
165
|
+
* Py twin: Kernel._loading_ext_owner. */
|
|
166
|
+
_loadingExtOwner = null;
|
|
167
|
+
/** Two-planes F2 — pluggable semantic-search provider (see search()). */
|
|
168
|
+
_searchProvider = null;
|
|
169
|
+
/** Damper: warn once per provider-failure episode, then debug. */
|
|
170
|
+
_searchProviderWarned = false;
|
|
171
|
+
/**
|
|
172
|
+
* rec-embedding-port — pluggable embedding provider (see embed()). null = no
|
|
173
|
+
* real provider → the deterministic zero-dep FakeEmbeddingProvider (offline/CI
|
|
174
|
+
* floor) is constructed lazily on first use.
|
|
175
|
+
*/
|
|
176
|
+
_embeddingProvider = null;
|
|
177
|
+
/**
|
|
178
|
+
* Tenant binding (Phase 1 — tenant-as-first-class).
|
|
179
|
+
*
|
|
180
|
+
* `null` means unbound — only GLOBAL kinds may be written; TENANTED
|
|
181
|
+
* kinds raise `TenantRequired`. Set via the constructor (Sanity
|
|
182
|
+
* `withConfig` pattern) or per-call via `withTenant(other)` (Stripe
|
|
183
|
+
* Connect pattern).
|
|
184
|
+
*/
|
|
185
|
+
tenant = null;
|
|
186
|
+
constructor(opts) {
|
|
187
|
+
if (opts?.tenant !== undefined) {
|
|
188
|
+
validateTenantSlug(opts.tenant);
|
|
189
|
+
this.tenant = opts.tenant;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Return a shallow-copy Kernel bound to `tenant`. Original Kernel is
|
|
194
|
+
* unchanged — call sites can hand off the copy to per-request
|
|
195
|
+
* handlers without mutating shared state (Sanity `client.withConfig`
|
|
196
|
+
* pattern).
|
|
197
|
+
*
|
|
198
|
+
* Pass `tenant=null` to obtain an unbound kernel (writes only allowed
|
|
199
|
+
* for GLOBAL kinds).
|
|
200
|
+
*/
|
|
201
|
+
withTenant(tenant) {
|
|
202
|
+
validateTenantSlug(tenant);
|
|
203
|
+
// Shallow object copy — shares source/cache/extensions/hooks
|
|
204
|
+
const copy = Object.create(Object.getPrototypeOf(this));
|
|
205
|
+
Object.assign(copy, this);
|
|
206
|
+
copy.tenant = tenant;
|
|
207
|
+
// Re-point the write pipeline at the copy so a tenant-bound kernel
|
|
208
|
+
// resolves its OWN tenant in writeDocument/deleteDocument (Fase 2).
|
|
209
|
+
copy._writePipeline = new WritePipeline(copy);
|
|
210
|
+
return copy;
|
|
211
|
+
}
|
|
212
|
+
/** Return the registered KindPort for a kind name (case-sensitive).
|
|
213
|
+
* With apiVersion the lookup is EXACT on (apiVersion, kind) — bare
|
|
214
|
+
* lookups on a name shared by multiple apiVersions resolve
|
|
215
|
+
* extension-first then registration order (i-195, Py twin:
|
|
216
|
+
* KindRegistry.port_for). */
|
|
217
|
+
_kindPortFor(kind, apiVersion) {
|
|
218
|
+
return this._kindreg.portFor(kind, apiVersion);
|
|
219
|
+
}
|
|
220
|
+
/** Public lookup for a registered KindPort by kind name. Use from
|
|
221
|
+
* tooling that needs to consult Kind metadata (isRuntimeArtifact,
|
|
222
|
+
* scope, storage, ...) without reaching into Kernel internals. Pass
|
|
223
|
+
* apiVersion for exact resolution on ambiguous names (i-195). */
|
|
224
|
+
kindPortFor(kind, apiVersion) {
|
|
225
|
+
return this._kindPortFor(kind, apiVersion);
|
|
226
|
+
}
|
|
227
|
+
/** Validate a Kind-Writer Agent's slot↔schema contract
|
|
228
|
+
* (feat/kind-writer-pilot, Task 2). Called from writeDocument only when
|
|
229
|
+
* spec.writes_kind is set — fail early so a malformed Kind-Writer is
|
|
230
|
+
* rejected before runtime emission. Twin of Python _validate_kind_writer.
|
|
231
|
+
*
|
|
232
|
+
* Contract:
|
|
233
|
+
* - writes_kind must resolve to a registered KindPort whose schema() is an
|
|
234
|
+
* object (schema-bearing); unknown / schema-less → Error (mentions schema).
|
|
235
|
+
* - every creative_slots name must be a property in the schema.
|
|
236
|
+
* - every required schema field must be covered by
|
|
237
|
+
* creative_slots ∪ Object.keys(system_slots); uncovered → "unmapped". */
|
|
238
|
+
/** @internal — called by the Helix extension's Kind-Writer pre_save
|
|
239
|
+
* guard (s-write-path-despecialize). Thin delegator to
|
|
240
|
+
* `WritePipeline.validateKindWriter` (Fase 2,
|
|
241
|
+
* s-kernel-decomp-f2-writepipeline). */
|
|
242
|
+
_validateKindWriter(spec) {
|
|
243
|
+
this._writePipeline.validateKindWriter(spec);
|
|
244
|
+
}
|
|
245
|
+
/** All registered KindPorts. Order matches registration. Facade over
|
|
246
|
+
* `this._kindreg.allPorts()` (Fase 3). */
|
|
247
|
+
kindPorts() {
|
|
248
|
+
return this._kindreg.allPorts();
|
|
249
|
+
}
|
|
250
|
+
/** F3 D4 (spec 2026-06-10-kinds-descriptor-f3): kind names whose port
|
|
251
|
+
* declares `embedFields` — via descriptor `embed:` or a class-level
|
|
252
|
+
* `embedFields` (the KindBase parity hook for not-yet-migrated
|
|
253
|
+
* classes). Py twin: `Kernel.embeddable_kinds()` (frozenset). */
|
|
254
|
+
embeddableKinds() {
|
|
255
|
+
const out = new Set();
|
|
256
|
+
for (const kp of this.kindPorts()) {
|
|
257
|
+
const ef = kp.embedFields;
|
|
258
|
+
if (ef !== null && ef !== undefined)
|
|
259
|
+
out.add(kp.kind);
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
/** Return the TenantScope for a registered kind, or null if unset.
|
|
264
|
+
* Phase 1 keeps undeclared kinds permissive (back-compat). @internal —
|
|
265
|
+
* read by WritePipeline.resolveTenantArg via the back-ref. */
|
|
266
|
+
_kindScope(kind) {
|
|
267
|
+
const kp = this._kindPortFor(kind);
|
|
268
|
+
if (!kp)
|
|
269
|
+
return null;
|
|
270
|
+
return kp.scope ?? null;
|
|
271
|
+
}
|
|
272
|
+
// `_resolveTenantArg` moved to `WritePipeline.resolveTenantArg` (Fase 2,
|
|
273
|
+
// s-kernel-decomp-f2-writepipeline) — it was only ever used by the two
|
|
274
|
+
// write/delete bodies, which now live in the pipeline.
|
|
275
|
+
// -- Registration ---------------------------------------------------------
|
|
276
|
+
// Hook names are typed (`HookName` vocabulary + string back-compat,
|
|
277
|
+
// s-dna-typed-hook-names); the HookRegistry warns once per (registry,
|
|
278
|
+
// name) on names outside the vocabulary.
|
|
279
|
+
use(hook, fn) {
|
|
280
|
+
this.hooks.use(hook, fn);
|
|
281
|
+
}
|
|
282
|
+
on(hook, fn) {
|
|
283
|
+
this.hooks.on(hook, fn);
|
|
284
|
+
}
|
|
285
|
+
/** Register a veto listener (e.g. 'pre_save') — throwing vetoes the
|
|
286
|
+
* operation. See HookRegistry.onVeto for priority/key semantics. */
|
|
287
|
+
onVeto(hook, fn, opts) {
|
|
288
|
+
this.hooks.onVeto(hook, fn, opts);
|
|
289
|
+
}
|
|
290
|
+
source(s) {
|
|
291
|
+
this._source = s;
|
|
292
|
+
}
|
|
293
|
+
/** The SourcePort registered via source(), or null. Read-only getter.
|
|
294
|
+
* The setter method is `source(src)`. Named `activeSource` to avoid
|
|
295
|
+
* collision between a method and a property of the same name.
|
|
296
|
+
* Parity: python Kernel.active_source. */
|
|
297
|
+
get activeSource() {
|
|
298
|
+
return this._source;
|
|
299
|
+
}
|
|
300
|
+
// ── Two-planes F2 — record-plane public surface (query/count/search) ─────
|
|
301
|
+
/**
|
|
302
|
+
* Kernel-level record query — push-down delegated to `source.query`
|
|
303
|
+
* (two-planes F2; TS twin of the Py `kernel.query`).
|
|
304
|
+
*
|
|
305
|
+
* Adds on top of the source:
|
|
306
|
+
* - Tenant binding auto-stamp: `opts.tenant` > `Kernel.tenant` > unset
|
|
307
|
+
* (Stripe Connect pattern, same as writeDocument).
|
|
308
|
+
* - Cross-scope `scopes` (F2.4): iterates the scopes with per-scope
|
|
309
|
+
* queries and CONCATENATES without dedup — records from distinct
|
|
310
|
+
* scopes are distinct docs. Mutually exclusive with a diverging
|
|
311
|
+
* positional `scope`: `scopes` wins (the positional is ignored).
|
|
312
|
+
* `limit`/`offset` apply PER scope.
|
|
313
|
+
*
|
|
314
|
+
* Divergence from Py (documented): the TS kernel has NO
|
|
315
|
+
* origin/inheritance machinery (no `origin=` param, no scope-inheritance
|
|
316
|
+
* chain, no catalog pass) — those live in the Py QueryEngine only.
|
|
317
|
+
* Records are per-scope, so the record plane loses nothing.
|
|
318
|
+
*
|
|
319
|
+
* Sources without the optional `query` capability (e.g. PostgresSource
|
|
320
|
+
* TS, no push-down this phase) raise a clear capability error.
|
|
321
|
+
*/
|
|
322
|
+
async *query(scope, kind, opts = {}) {
|
|
323
|
+
if (!this._source) {
|
|
324
|
+
throw new Error("No source registered. Call kernel.source() first.");
|
|
325
|
+
}
|
|
326
|
+
const { scopes, ...rest } = opts;
|
|
327
|
+
if (scopes != null) {
|
|
328
|
+
// F2.4 cross-scope: per-scope queries, concat no dedup. Recursion
|
|
329
|
+
// rebinds tenant per call (passes the raw opt through).
|
|
330
|
+
for (const sc of scopes)
|
|
331
|
+
yield* this.query(sc, kind, rest);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const src = this._source;
|
|
335
|
+
// s-sourceport-contract-cleanup: consult the DECLARED capabilities
|
|
336
|
+
// instead of a typeof feature-test.
|
|
337
|
+
if (!sourceCapabilities(src).queryPushdown || typeof src.query !== "function") {
|
|
338
|
+
throw new Error("source does not implement query — use FilesystemSource or a query-capable adapter");
|
|
339
|
+
}
|
|
340
|
+
// Tenant binding: opt > Kernel.tenant > unset.
|
|
341
|
+
const effectiveTenant = rest.tenant ?? this.tenant ?? undefined;
|
|
342
|
+
yield* src.query(scope, kind, {
|
|
343
|
+
filter: rest.filter,
|
|
344
|
+
limit: rest.limit,
|
|
345
|
+
offset: rest.offset,
|
|
346
|
+
orderBy: rest.orderBy,
|
|
347
|
+
tenant: effectiveTenant,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* F2 D2 — public aggregation count alongside `query` (TS twin of the
|
|
352
|
+
* Py `kernel.count`). Push-down to `source.count` (FS: in-memory core).
|
|
353
|
+
*
|
|
354
|
+
* Returns `CountResult`: `{ total, groups }` — groups by count DESC,
|
|
355
|
+
* key ASC with `null` last; `groups` is `null` without `groupBy`.
|
|
356
|
+
*
|
|
357
|
+
* NO origin/inheritance on purpose — records are per-scope (spec D5:
|
|
358
|
+
* derived views build on top of `kernel.query` in code). Cross-scope
|
|
359
|
+
* via `scopes` (totals SUMMED, groups MERGED by key and re-sorted;
|
|
360
|
+
* `scopes` wins over a diverging positional `scope`).
|
|
361
|
+
*
|
|
362
|
+
* Example (Studio velocity):
|
|
363
|
+
* const res = await kernel.count("dna-development", "Story", {
|
|
364
|
+
* groupBy: "spec.status",
|
|
365
|
+
* });
|
|
366
|
+
* // { total: 950, groups: [{ key: "done", count: 700 }, …] }
|
|
367
|
+
*/
|
|
368
|
+
async count(scope, kind, opts = {}) {
|
|
369
|
+
if (!this._source) {
|
|
370
|
+
throw new Error("No source registered. Call kernel.source() first.");
|
|
371
|
+
}
|
|
372
|
+
const src = this._source;
|
|
373
|
+
// s-sourceport-contract-cleanup: declared capabilities, not typeof.
|
|
374
|
+
if (!sourceCapabilities(src).queryPushdown || typeof src.count !== "function") {
|
|
375
|
+
throw new Error("source does not implement count — use FilesystemSource or a count-capable adapter");
|
|
376
|
+
}
|
|
377
|
+
const effectiveTenant = opts.tenant ?? this.tenant ?? undefined;
|
|
378
|
+
const targetScopes = opts.scopes ?? [scope];
|
|
379
|
+
let total = 0;
|
|
380
|
+
const merged = new Map();
|
|
381
|
+
for (const sc of targetScopes) {
|
|
382
|
+
const res = await src.count(sc, kind, {
|
|
383
|
+
filter: opts.filter,
|
|
384
|
+
groupBy: opts.groupBy,
|
|
385
|
+
tenant: effectiveTenant,
|
|
386
|
+
});
|
|
387
|
+
total += res.total ?? 0;
|
|
388
|
+
for (const g of res.groups ?? []) {
|
|
389
|
+
merged.set(g.key, (merged.get(g.key) ?? 0) + (g.count ?? 0));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
let groups = null;
|
|
393
|
+
if (opts.groupBy != null) {
|
|
394
|
+
// Same tie-break as the in-memory core + the Py merge:
|
|
395
|
+
// count DESC, then key ASC with null LAST (i-121 spirit).
|
|
396
|
+
groups = [...merged.entries()]
|
|
397
|
+
.map(([key, count]) => ({ key, count }))
|
|
398
|
+
.sort((a, b) => {
|
|
399
|
+
if (a.count !== b.count)
|
|
400
|
+
return b.count - a.count;
|
|
401
|
+
if ((a.key === null) !== (b.key === null))
|
|
402
|
+
return a.key === null ? 1 : -1;
|
|
403
|
+
const sa = String(a.key);
|
|
404
|
+
const sb = String(b.key);
|
|
405
|
+
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return { total, groups };
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Register the embedding provider (rec-embedding-port). One per kernel; later
|
|
412
|
+
* registration replaces (boot-time wiring). Sibling to
|
|
413
|
+
* `recordSearchProvider` — a real provider (ONNX all-MiniLM-L6-v2) registers
|
|
414
|
+
* itself at app boot; without one, `embed` uses the deterministic
|
|
415
|
+
* `FakeEmbeddingProvider` floor.
|
|
416
|
+
*/
|
|
417
|
+
embeddingProvider(provider) {
|
|
418
|
+
this._embeddingProvider = provider;
|
|
419
|
+
}
|
|
420
|
+
/** The registered embedding provider, or a lazily-constructed fake (cached).
|
|
421
|
+
* The fake is zero-dep core (js-sha256, already a dependency) so it is a
|
|
422
|
+
* static import — no ML deps enter the graph. */
|
|
423
|
+
_resolveEmbeddingProvider() {
|
|
424
|
+
if (this._embeddingProvider === null) {
|
|
425
|
+
this._embeddingProvider = new FakeEmbeddingProvider();
|
|
426
|
+
}
|
|
427
|
+
return this._embeddingProvider;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Embed texts into dense vectors (rec-embedding-port; TS twin of the Py
|
|
431
|
+
* `kernel.embed`). Uses the registered `EmbeddingPort` when present, else the
|
|
432
|
+
* deterministic zero-dep fake floor. Returns one `dims`-length vector per
|
|
433
|
+
* input, in order; empty input → empty array. Read the space via
|
|
434
|
+
* `embeddingDims` / `embeddingModelId`.
|
|
435
|
+
*/
|
|
436
|
+
async embed(texts) {
|
|
437
|
+
if (texts.length === 0)
|
|
438
|
+
return [];
|
|
439
|
+
return this._resolveEmbeddingProvider().embed([...texts]);
|
|
440
|
+
}
|
|
441
|
+
/** Output dimensionality of the active embedding provider. */
|
|
442
|
+
get embeddingDims() {
|
|
443
|
+
return this._resolveEmbeddingProvider().dims;
|
|
444
|
+
}
|
|
445
|
+
/** Identity of the active embedding space (vectors from different `modelId`s
|
|
446
|
+
* are NOT comparable). */
|
|
447
|
+
get embeddingModelId() {
|
|
448
|
+
return this._resolveEmbeddingProvider().modelId;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Register the semantic-search provider (two-planes F2). One per
|
|
452
|
+
* kernel; later registration replaces (boot-time wiring) and resets
|
|
453
|
+
* the failure-warning damper (new provider → fresh episode).
|
|
454
|
+
*/
|
|
455
|
+
recordSearchProvider(provider) {
|
|
456
|
+
this._searchProvider = provider;
|
|
457
|
+
this._searchProviderWarned = false;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Public record search (F2 D2; TS twin of the Py `kernel.search`).
|
|
461
|
+
* Provider registered → semantic (degraded=false). No provider OR
|
|
462
|
+
* provider error → lexical token-match fallback over `query()`
|
|
463
|
+
* (degraded=true; requires `kind` — without it returns empty
|
|
464
|
+
* degraded). Tenant binding same as `query()`.
|
|
465
|
+
*/
|
|
466
|
+
async search(scope, queryText, opts = {}) {
|
|
467
|
+
const k = opts.k ?? 10;
|
|
468
|
+
const effectiveTenant = opts.tenant ?? this.tenant ?? "";
|
|
469
|
+
const prov = this._searchProvider;
|
|
470
|
+
if (prov !== null) {
|
|
471
|
+
try {
|
|
472
|
+
const hits = await prov.search({
|
|
473
|
+
scope,
|
|
474
|
+
queryText,
|
|
475
|
+
kind: opts.kind ?? null,
|
|
476
|
+
k,
|
|
477
|
+
tenant: effectiveTenant || "",
|
|
478
|
+
});
|
|
479
|
+
this._searchProviderWarned = false; // episode over
|
|
480
|
+
return { hits, degraded: false };
|
|
481
|
+
}
|
|
482
|
+
catch (err) {
|
|
483
|
+
// Search is a read — degrade, never crash. Damped: warn ONCE per
|
|
484
|
+
// failure episode, then debug until a successful call resets.
|
|
485
|
+
if (!this._searchProviderWarned) {
|
|
486
|
+
this._searchProviderWarned = true;
|
|
487
|
+
console.warn("[kernel] search provider failed; lexical fallback "
|
|
488
|
+
+ "(further failures logged at debug until recovery)", err);
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
console.debug("[kernel] search provider still failing; lexical fallback", err);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
hits: await this._lexicalSearch(scope, queryText, {
|
|
497
|
+
kind: opts.kind ?? null,
|
|
498
|
+
k,
|
|
499
|
+
tenant: effectiveTenant || undefined,
|
|
500
|
+
}),
|
|
501
|
+
degraded: true,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Degraded fallback for `search()` — honest DEV lexical scan, NOT
|
|
506
|
+
* similarity (two-planes F2; 1:1 with the Py `_lexical_search`).
|
|
507
|
+
*
|
|
508
|
+
* Matches by token-set over the STRING VALUES of each doc's spec
|
|
509
|
+
* (recursive walk; never substring over serialized JSON —
|
|
510
|
+
* `json.dumps` Py and `JSON.stringify` TS diverge in separators and
|
|
511
|
+
* would break parity). Requires `kind` (records are scanned per-kind);
|
|
512
|
+
* without it there is nothing safe to scan → empty.
|
|
513
|
+
* Score = query tokens present ÷ total query tokens.
|
|
514
|
+
*/
|
|
515
|
+
async _lexicalSearch(scope, queryText, opts) {
|
|
516
|
+
if (!opts.kind)
|
|
517
|
+
return [];
|
|
518
|
+
const qTokens = queryText.toLowerCase().split(/\s+/).filter(Boolean);
|
|
519
|
+
if (qTokens.length === 0)
|
|
520
|
+
return [];
|
|
521
|
+
const specTokens = (node, out) => {
|
|
522
|
+
if (typeof node === "string") {
|
|
523
|
+
for (const t of node.toLowerCase().split(/\s+/))
|
|
524
|
+
if (t)
|
|
525
|
+
out.add(t);
|
|
526
|
+
}
|
|
527
|
+
else if (Array.isArray(node)) {
|
|
528
|
+
for (const v of node)
|
|
529
|
+
specTokens(v, out);
|
|
530
|
+
}
|
|
531
|
+
else if (typeof node === "object" && node !== null) {
|
|
532
|
+
for (const v of Object.values(node))
|
|
533
|
+
specTokens(v, out);
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
const hits = [];
|
|
537
|
+
for await (const row of this.query(scope, opts.kind, {
|
|
538
|
+
tenant: opts.tenant,
|
|
539
|
+
limit: 500,
|
|
540
|
+
})) {
|
|
541
|
+
const tokens = new Set();
|
|
542
|
+
specTokens(row.spec ?? {}, tokens);
|
|
543
|
+
const score = qTokens.filter((t) => tokens.has(t)).length / qTokens.length;
|
|
544
|
+
if (score > 0) {
|
|
545
|
+
const meta = row.metadata;
|
|
546
|
+
const name = meta?.name ?? row.name ?? "";
|
|
547
|
+
hits.push({ scope, kind: opts.kind, name, score });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
hits.sort((a, b) => b.score - a.score); // stable — input order on ties
|
|
551
|
+
return hits.slice(0, opts.k);
|
|
552
|
+
}
|
|
553
|
+
// ────────────────────────────────────────────────────────────────
|
|
554
|
+
// Composition Engine V2 (Phase 17 — s-ts-composition-v2-port, TS twin
|
|
555
|
+
// of the Py Kernel composition surface) — declarative cross-scope +
|
|
556
|
+
// tenant overlay resolution with provenance. Orchestration lives in
|
|
557
|
+
// `kernel/composition-resolver.ts`; these are thin delegators (same
|
|
558
|
+
// layout as Python: kernel keeps the public methods, the engine holds
|
|
559
|
+
// a back-ref). Behavioral gate: tests/parity-fixtures/composition/.
|
|
560
|
+
// ────────────────────────────────────────────────────────────────
|
|
561
|
+
/**
|
|
562
|
+
* Internal — one raw doc for a `(scope, kind, name, tenant)` layer key
|
|
563
|
+
* (tenant is "" for the base layer). TS twin of the Py
|
|
564
|
+
* `_granular_doc_cached` MINUS the cache: the TS kernel has no
|
|
565
|
+
* kernel-level doc cache, so this is a direct source read on every
|
|
566
|
+
* call. PERF divergence only — same inputs, same outputs (see
|
|
567
|
+
* composition-resolver.ts module docstring, divergence #1).
|
|
568
|
+
*/
|
|
569
|
+
async _granularDoc(key) {
|
|
570
|
+
const [scope, kind, name, tenantOrEmpty] = key;
|
|
571
|
+
const tenant = tenantOrEmpty || null;
|
|
572
|
+
const src = this._source;
|
|
573
|
+
if (!src)
|
|
574
|
+
throw new Error("No source registered. Call kernel.source() first.");
|
|
575
|
+
// s-sourceport-contract-cleanup: declared capabilities, not typeof.
|
|
576
|
+
if (sourceCapabilities(src).granularOne && typeof src.loadOne === "function") {
|
|
577
|
+
return await src.loadOne(scope, kind, name, { readers: this._readers, tenant });
|
|
578
|
+
}
|
|
579
|
+
// Legacy adapter — fall back to loadAll + find. Mirrors the Py legacy
|
|
580
|
+
// fallback exactly, INCLUDING ignoring tenant (base layer only).
|
|
581
|
+
const docs = await src.loadAll(scope, this._readers);
|
|
582
|
+
for (const d of docs) {
|
|
583
|
+
if (d.kind !== kind)
|
|
584
|
+
continue;
|
|
585
|
+
const meta = d.metadata;
|
|
586
|
+
const n = meta?.name ?? d.name ?? "";
|
|
587
|
+
if (n === name)
|
|
588
|
+
return d;
|
|
589
|
+
}
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Internal — the ordered Catalog scope set for `tenant` (Phase 3b ch1,
|
|
594
|
+
* i-112 on the Py side). The TS kernel has NO catalog machinery yet
|
|
595
|
+
* (Genome scan + tenant lockfile — TS parity tracked as `i-185`), so
|
|
596
|
+
* this hook returns `[]`: the resolver's Catalog splice is fully
|
|
597
|
+
* implemented but contributes no layers on TS today (see
|
|
598
|
+
* composition-resolver.ts module docstring, divergence #3).
|
|
599
|
+
*/
|
|
600
|
+
async _catalogScopes(tenant, opts) {
|
|
601
|
+
void tenant;
|
|
602
|
+
void opts;
|
|
603
|
+
return [];
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Walk `Genome.spec.parent_scope` transitively → ordered resolution
|
|
607
|
+
* chain of `[scope, tenant]` pairs, HIGHEST priority first:
|
|
608
|
+
* [[scope, tenant], [scope, null], [parent, tenant], [parent, null], …]
|
|
609
|
+
* When `tenant` is null, only base layers are emitted per scope.
|
|
610
|
+
* Cycle detection via visited set; depth capped at MAX_RESOLUTION_DEPTH;
|
|
611
|
+
* missing Genome / missing parent_scope terminates the walk (with the
|
|
612
|
+
* V1 back-compat escalation to `_lib`).
|
|
613
|
+
* Py twin: `Kernel._compute_resolution_chain`.
|
|
614
|
+
*/
|
|
615
|
+
async computeResolutionChain(scope, tenant = null) {
|
|
616
|
+
return await this._composition.computeResolutionChain(scope, tenant);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Resolve the composition rule `[scope_inheritance, merge_strategy,
|
|
620
|
+
* tenant_overlay]` for (scope, kind) — the scope's
|
|
621
|
+
* `LayerPolicy.composition_rules[kind]`, else the inherit-by-default
|
|
622
|
+
* denylist. Py twin: `Kernel._get_composition_rule`.
|
|
623
|
+
*/
|
|
624
|
+
async getCompositionRule(scope, kind) {
|
|
625
|
+
return await this._composition.getCompositionRule(scope, kind);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Resolve a doc through the composition chain — Phase 17 primitive.
|
|
629
|
+
* Returns `ResolvedDocument` with merged doc + full provenance.
|
|
630
|
+
* Bootstrap Kinds (Genome, LayerPolicy, KindDefinition) bypass
|
|
631
|
+
* inheritance entirely (local-only, single-layer provenance).
|
|
632
|
+
* Py twin: `Kernel.resolve_document`.
|
|
633
|
+
*/
|
|
634
|
+
async resolveDocument(scope, kind, name, opts) {
|
|
635
|
+
return await this._composition.resolveDocument(scope, kind, name, opts);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Clone an inherited doc into `targetScope` as a local override.
|
|
639
|
+
* Throws when the doc isn't inherited or the target already exists
|
|
640
|
+
* (without `overwrite`). Py twin: `Kernel.personalize_document`
|
|
641
|
+
* (bundle-entry payload cloning is Py-only — divergence #4 in
|
|
642
|
+
* composition-resolver.ts).
|
|
643
|
+
*/
|
|
644
|
+
async personalizeDocument(targetScope, kind, name, opts) {
|
|
645
|
+
return await this._composition.personalizeDocument(targetScope, kind, name, opts);
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Cheap aggregate of the scope's parent chain + per-Kind local /
|
|
649
|
+
* inherited / installed counts (Py twin: `Kernel.composition_summary`;
|
|
650
|
+
* same snake_case wire shape:
|
|
651
|
+
* `{scope, parent_chain, resources: {Kind: {local, inherited, installed,
|
|
652
|
+
* total}}}`). The Py twin rides its QueryEngine origin filters; the TS
|
|
653
|
+
* kernel has no origin machinery, so the three passes are computed here
|
|
654
|
+
* directly with the SAME dedup semantics (local names shadow catalog +
|
|
655
|
+
* parent names; catalog names do NOT shadow inherited — mirroring the
|
|
656
|
+
* three independent origin-filtered queries Python makes). `installed`
|
|
657
|
+
* is always 0 until the TS catalog surface lands (i-185).
|
|
658
|
+
*/
|
|
659
|
+
async compositionSummary(scope, opts) {
|
|
660
|
+
const tenant = opts?.tenant ?? null;
|
|
661
|
+
// Parent chain: derived from Genome.parent_scope + V1 fallback,
|
|
662
|
+
// deduped (chain has (scope, null) pairs; collapse to unique parents).
|
|
663
|
+
const chain = await this.computeResolutionChain(scope, null);
|
|
664
|
+
const parentChain = [];
|
|
665
|
+
for (const [s] of chain) {
|
|
666
|
+
if (s !== scope && !parentChain.includes(s))
|
|
667
|
+
parentChain.push(s);
|
|
668
|
+
}
|
|
669
|
+
const resources = {};
|
|
670
|
+
const src = this._source;
|
|
671
|
+
// s-sourceport-contract-cleanup: declared capabilities, not typeof.
|
|
672
|
+
if (!src || !sourceCapabilities(src).queryPushdown || typeof src.query !== "function") {
|
|
673
|
+
// No record-plane source → every per-Kind count is skipped
|
|
674
|
+
// (mirrors Python's best-effort per-Kind `except: continue`).
|
|
675
|
+
return { scope, parent_chain: parentChain, resources };
|
|
676
|
+
}
|
|
677
|
+
// Tenant binding mirrors the Py QueryEngine: kwarg > Kernel.tenant.
|
|
678
|
+
const effectiveTenant = tenant ?? this.tenant ?? undefined;
|
|
679
|
+
const nameOf = (row) => {
|
|
680
|
+
const meta = row.metadata;
|
|
681
|
+
return String(meta?.name ?? row.name ?? "");
|
|
682
|
+
};
|
|
683
|
+
for (const kind of [...DEFAULT_INHERITABLE_KINDS_V1].sort()) {
|
|
684
|
+
try {
|
|
685
|
+
// Local pass — always collects names (they shadow later passes).
|
|
686
|
+
const seenLocal = new Set();
|
|
687
|
+
let localCount = 0;
|
|
688
|
+
for await (const row of src.query(scope, kind, { tenant: effectiveTenant })) {
|
|
689
|
+
const n = nameOf(row);
|
|
690
|
+
if (n)
|
|
691
|
+
seenLocal.add(n);
|
|
692
|
+
localCount += 1;
|
|
693
|
+
}
|
|
694
|
+
// Catalog/installed pass — dedup vs local (Local wins). [] until
|
|
695
|
+
// i-185; loop kept so the semantics are already wired.
|
|
696
|
+
let installedCount = 0;
|
|
697
|
+
const catalogSeen = new Set(seenLocal);
|
|
698
|
+
if (this.INHERITABLE_KINDS.has(kind) && scope !== Kernel.INHERIT_PARENT_SCOPE) {
|
|
699
|
+
let catalogScopes;
|
|
700
|
+
try {
|
|
701
|
+
catalogScopes = await this._catalogScopes(tenant, { exclude: new Set([scope]) });
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
catalogScopes = [];
|
|
705
|
+
}
|
|
706
|
+
for (const [catScope, catTenant] of catalogScopes) {
|
|
707
|
+
try {
|
|
708
|
+
for await (const row of src.query(catScope, kind, {
|
|
709
|
+
tenant: catTenant ?? undefined,
|
|
710
|
+
})) {
|
|
711
|
+
const n = nameOf(row);
|
|
712
|
+
if (n && catalogSeen.has(n))
|
|
713
|
+
continue;
|
|
714
|
+
if (n)
|
|
715
|
+
catalogSeen.add(n);
|
|
716
|
+
installedCount += 1;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
// Parent/inherited pass — dedup vs LOCAL names only (catalog names
|
|
725
|
+
// intentionally excluded — Python's origin="inherited" query never
|
|
726
|
+
// runs its catalog pass). Same gate as the Py QueryEngine.
|
|
727
|
+
let inheritedCount = 0;
|
|
728
|
+
if (this.INHERITABLE_KINDS.has(kind) && scope !== Kernel.INHERIT_PARENT_SCOPE) {
|
|
729
|
+
const parentSeen = new Set(seenLocal);
|
|
730
|
+
for (const parent of parentChain) {
|
|
731
|
+
try {
|
|
732
|
+
for await (const row of src.query(parent, kind, { tenant: effectiveTenant })) {
|
|
733
|
+
const n = nameOf(row);
|
|
734
|
+
if (n && parentSeen.has(n))
|
|
735
|
+
continue;
|
|
736
|
+
if (n)
|
|
737
|
+
parentSeen.add(n);
|
|
738
|
+
inheritedCount += 1;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
continue; // fail-soft per parent scope, exactly like Py
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (localCount || inheritedCount || installedCount) {
|
|
747
|
+
resources[kind] = {
|
|
748
|
+
local: localCount,
|
|
749
|
+
inherited: inheritedCount,
|
|
750
|
+
installed: installedCount,
|
|
751
|
+
total: localCount + inheritedCount + installedCount,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
continue; // best-effort — skip Kind if source errors (parity w/ Py)
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return { scope, parent_chain: parentChain, resources };
|
|
760
|
+
}
|
|
761
|
+
/** WriterPorts registered via writer(w). Returns a FROZEN snapshot of
|
|
762
|
+
* the internal writer list — mutating the returned array throws in
|
|
763
|
+
* strict mode and never affects the Kernel's internal state.
|
|
764
|
+
* Parity: python Kernel.active_writers (which returns a tuple). */
|
|
765
|
+
get activeWriters() {
|
|
766
|
+
return Object.freeze([...this._writers]);
|
|
767
|
+
}
|
|
768
|
+
/** ReaderPorts registered via reader(r). Frozen snapshot — mirror of
|
|
769
|
+
* activeWriters (s-dna-rw-roundtrip-suite: the round-trip conformance
|
|
770
|
+
* suite enumerates registered pairs through this surface).
|
|
771
|
+
* Parity: python Kernel.active_readers. */
|
|
772
|
+
get activeReaders() {
|
|
773
|
+
return Object.freeze([...this._readers]);
|
|
774
|
+
}
|
|
775
|
+
cache(c) {
|
|
776
|
+
this._cache = c;
|
|
777
|
+
}
|
|
778
|
+
resolver(scheme, r) {
|
|
779
|
+
this._resolvers.set(scheme, r);
|
|
780
|
+
}
|
|
781
|
+
reader(r) {
|
|
782
|
+
// H1 — structural conformance check. TypeScript can't use
|
|
783
|
+
// runtime_checkable Protocol like Python, so we test method
|
|
784
|
+
// presence + arity manually. Catches the typo-on-detect bug at
|
|
785
|
+
// registration time instead of in production scans.
|
|
786
|
+
if (typeof r?.detect !== "function" ||
|
|
787
|
+
typeof r?.read !== "function") {
|
|
788
|
+
throw new ReaderRegistrationError(`Reader ${r?.constructor?.name ?? typeof r} ` +
|
|
789
|
+
`does not satisfy ReaderPort interface (missing detect/read ` +
|
|
790
|
+
`methods). See typescript/src/kernel/protocols.ts.`);
|
|
791
|
+
}
|
|
792
|
+
// Idempotent re-registration — same class is a no-op
|
|
793
|
+
if (this._readers.some((existing) => existing.constructor === r.constructor)) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
this._readers.push(r);
|
|
797
|
+
}
|
|
798
|
+
writer(w) {
|
|
799
|
+
// H1 — structural conformance check (mirror of reader()).
|
|
800
|
+
// serialize is part of the contract since s-dna-rw-roundtrip-suite.
|
|
801
|
+
if (typeof w?.canWrite !== "function" ||
|
|
802
|
+
typeof w?.write !== "function" ||
|
|
803
|
+
typeof w?.serialize !== "function") {
|
|
804
|
+
throw new WriterRegistrationError(`Writer ${w?.constructor?.name ?? typeof w} ` +
|
|
805
|
+
`does not satisfy WriterPort interface (missing canWrite/write/` +
|
|
806
|
+
`serialize methods — serialize is part of the contract since ` +
|
|
807
|
+
`s-dna-rw-roundtrip-suite). See typescript/src/kernel/protocols.ts.`);
|
|
808
|
+
}
|
|
809
|
+
if (this._writers.some((existing) => existing.constructor === w.constructor)) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
this._writers.push(w);
|
|
813
|
+
}
|
|
814
|
+
writableSource(ws) {
|
|
815
|
+
this._writableSource = ws;
|
|
816
|
+
}
|
|
817
|
+
fs(f) {
|
|
818
|
+
this._fs = f;
|
|
819
|
+
}
|
|
820
|
+
/** Register a Kind (H1 validation funnel). Thin facade over
|
|
821
|
+
* `this._kindreg.registerKind()` (Fase 3, s-kernel-decomp-ts-parity —
|
|
822
|
+
* the funnel moved into the KindRegistry; Py twin: `Kernel.kind()`
|
|
823
|
+
* delegating to `self._kindreg.register_kind`). */
|
|
824
|
+
kind(k) {
|
|
825
|
+
this._kindreg.registerKind(k);
|
|
826
|
+
}
|
|
827
|
+
/** F3 (spec D3): register a BUILTIN Kind from a KindDefinition
|
|
828
|
+
* descriptor (`kinds/*.kind.yaml` package data). Thin facade over the
|
|
829
|
+
* KindRegistry funnel (Fase 3). Py twin: `Kernel.kind_from_descriptor`. */
|
|
830
|
+
kindFromDescriptor(raw) {
|
|
831
|
+
return this._kindreg.registerFromDescriptor(raw);
|
|
832
|
+
}
|
|
833
|
+
/** Summary dict for a registered kind, including resolved docs. Facade
|
|
834
|
+
* over `this._kindreg.describe()` (Fase 3). */
|
|
835
|
+
describeKind(kindName) {
|
|
836
|
+
return this._kindreg.describe(kindName);
|
|
837
|
+
}
|
|
838
|
+
/** Register a composition profile that declares how an orchestrator
|
|
839
|
+
* kind connects to other kinds. Called by extensions (e.g.
|
|
840
|
+
* HelixExtension) during register(). */
|
|
841
|
+
compositionProfile(profile) {
|
|
842
|
+
this._profiles.push(profile);
|
|
843
|
+
}
|
|
844
|
+
// -- Tools (s-dna-port-surface-parity — Py twin: s-dna-tool-decorator) ----
|
|
845
|
+
// Analogous to `.kind()` — extensions register tool definitions via
|
|
846
|
+
// `kernel.tool(td)` inside register(); consumers query via
|
|
847
|
+
// `kernel.getTools({ group })`. Pure metadata layer — the execution path
|
|
848
|
+
// stays framework-native (`td.getCallable()`).
|
|
849
|
+
/** Register a tool definition (delegates to the ToolRegistry;
|
|
850
|
+
* last-write-wins on same name). Py twin: `Kernel.tool`. */
|
|
851
|
+
tool(td) {
|
|
852
|
+
this._toolreg.register(td);
|
|
853
|
+
}
|
|
854
|
+
/** Return a tool definition by name, or `null` if unknown.
|
|
855
|
+
* Py twin: `Kernel.get_tool`. */
|
|
856
|
+
getTool(name) {
|
|
857
|
+
return this._toolreg.get(name);
|
|
858
|
+
}
|
|
859
|
+
/** Return registered tool definitions, optionally filtered by group(s)
|
|
860
|
+
* (`groups: ["read"]` expands the umbrella alias).
|
|
861
|
+
* Py twin: `Kernel.get_tools`. */
|
|
862
|
+
getTools(opts = {}) {
|
|
863
|
+
return this._toolreg.getMany(opts);
|
|
864
|
+
}
|
|
865
|
+
/** Reverse-build `{group: [toolNames…]}` from the registry.
|
|
866
|
+
* Py twin: `Kernel.list_tool_groups`. */
|
|
867
|
+
listToolGroups() {
|
|
868
|
+
return this._toolreg.groups();
|
|
869
|
+
}
|
|
870
|
+
/** Canonical dep_filter target resolution (s-alias-generated-not-typed).
|
|
871
|
+
*
|
|
872
|
+
* The CONTRACT is alias-valued dep_filters (`"soulspec-soul"`). The
|
|
873
|
+
* legacy `"kind=<Name>"` format resolves through a DEPRECATED shim so
|
|
874
|
+
* per-scope KindDefinition docs keep working. Builtin extensions must
|
|
875
|
+
* be alias-pure (validateDepFilters rejects `kind=` there). Delegates
|
|
876
|
+
* to the shared `resolveDepFilterTargetOver` — since
|
|
877
|
+
* s-unify-composition-subsystems the ONE resolver every dep_filter
|
|
878
|
+
* reader (`validateRefs` / `mi.composition` / the Kernel) consumes.
|
|
879
|
+
* Facade over `this._kindreg.resolveDepFilterTarget()` (Fase 3).
|
|
880
|
+
* Py twin: KindRegistry.resolve_dep_filter_target. */
|
|
881
|
+
resolveDepFilterTarget(value) {
|
|
882
|
+
return this._kindreg.resolveDepFilterTarget(value);
|
|
883
|
+
}
|
|
884
|
+
/** s-alias-generated-not-typed — every dep_filter target of an
|
|
885
|
+
* EXTENSION-registered Kind must resolve to a registered alias.
|
|
886
|
+
*
|
|
887
|
+
* Aliases are the wire key of dep_filters / Mustache sections /
|
|
888
|
+
* LayerPolicy — a typo used to degrade the prompt SILENTLY (the dep
|
|
889
|
+
* just vanished from the context, warning buried in logs). Called at
|
|
890
|
+
* the end of `loadBuiltins()` (the TS twin of `Kernel.auto()`).
|
|
891
|
+
*
|
|
892
|
+
* - Extension/builtin port with an unknown alias OR the legacy
|
|
893
|
+
* `kind=` format → `KindRegistrationError` (boot fails loud).
|
|
894
|
+
* - Per-scope declarative ports (user KindDefinition docs) only WARN
|
|
895
|
+
* — user docs never take the boot down (same posture as the
|
|
896
|
+
* parse_error / plane-lint funnels).
|
|
897
|
+
* Facade over `this._kindreg.validateDepFilters()` (Fase 3).
|
|
898
|
+
* Py twin: Kernel.validate_dep_filters. */
|
|
899
|
+
validateDepFilters() {
|
|
900
|
+
this._kindreg.validateDepFilters();
|
|
901
|
+
}
|
|
902
|
+
load(ext) {
|
|
903
|
+
// H1 — structural check before calling register(). Catches
|
|
904
|
+
// "loaded an instance of the wrong class" — invisible in
|
|
905
|
+
// entry-point discovery (e.g. registering a Kind class instead
|
|
906
|
+
// of an Extension class as the entry-point target).
|
|
907
|
+
if (typeof ext?.register !== "function") {
|
|
908
|
+
throw new ExtensionLoadError(`Extension ${ext?.constructor?.name ?? typeof ext} ` +
|
|
909
|
+
`has no callable register() method. Extensions must implement ` +
|
|
910
|
+
`\`register(kernel)\` per the Extension interface.`);
|
|
911
|
+
}
|
|
912
|
+
// s-dna-extension-host-contract — validate the WHOLE Extension
|
|
913
|
+
// contract fail-loud, not just register(). name identifies the
|
|
914
|
+
// extension in logs / alias-owner generation; version identifies it
|
|
915
|
+
// in diagnostics. Py twin: Kernel.load().
|
|
916
|
+
const extName = ext.name;
|
|
917
|
+
if (typeof extName !== "string" || extName.trim() === "") {
|
|
918
|
+
throw new ExtensionLoadError(`Extension ${ext?.constructor?.name ?? typeof ext} ` +
|
|
919
|
+
`has no valid \`name\` (got ${JSON.stringify(extName)}). Extensions ` +
|
|
920
|
+
`must declare \`name: string\` (non-empty) per the Extension interface.`);
|
|
921
|
+
}
|
|
922
|
+
const extVersion = ext.version;
|
|
923
|
+
if (typeof extVersion !== "string" || extVersion.trim() === "") {
|
|
924
|
+
throw new ExtensionLoadError(`Extension ${JSON.stringify(extName)} has no valid \`version\` ` +
|
|
925
|
+
`(got ${JSON.stringify(extVersion)}). Extensions must declare ` +
|
|
926
|
+
`\`version: string\` per the Extension interface.`);
|
|
927
|
+
}
|
|
928
|
+
try {
|
|
929
|
+
// s-alias-generated-not-typed — owner context p/ geração de
|
|
930
|
+
// alias dos Kinds registrados por esta Extension (declarado
|
|
931
|
+
// 1× por extension, não por Kind). Py twin: Kernel.load().
|
|
932
|
+
this._loadingExtOwner =
|
|
933
|
+
ext.aliasOwner ??
|
|
934
|
+
ext.name ??
|
|
935
|
+
null;
|
|
936
|
+
try {
|
|
937
|
+
ext.register(this);
|
|
938
|
+
}
|
|
939
|
+
finally {
|
|
940
|
+
this._loadingExtOwner = null;
|
|
941
|
+
}
|
|
942
|
+
this._extensions.push(ext);
|
|
943
|
+
}
|
|
944
|
+
catch (e) {
|
|
945
|
+
// H1 — registration validation errors propagate cleanly, not
|
|
946
|
+
// swallowed by the hook path. They represent a *configuration*
|
|
947
|
+
// problem the operator must fix before boot can continue.
|
|
948
|
+
if (e instanceof KindRegistrationError ||
|
|
949
|
+
e instanceof ReaderRegistrationError ||
|
|
950
|
+
e instanceof WriterRegistrationError) {
|
|
951
|
+
const name = ext.name ?? String(ext);
|
|
952
|
+
console.error(`Extension ${name} failed registration validation: ${e}`);
|
|
953
|
+
throw e;
|
|
954
|
+
}
|
|
955
|
+
const name = ext.name ?? String(ext);
|
|
956
|
+
console.error(`Extension ${name} failed to register: ${e}`);
|
|
957
|
+
if (this.hooks.has("extension_error")) {
|
|
958
|
+
this.hooks.emit("extension_error", {
|
|
959
|
+
scope: name,
|
|
960
|
+
kind: "Extension",
|
|
961
|
+
name,
|
|
962
|
+
data: { error: String(e) },
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
throw e;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
// -- Templates (Phase 0 contract) -----------------------------------------
|
|
971
|
+
/**
|
|
972
|
+
* Aggregate `templates()` from every loaded extension.
|
|
973
|
+
*
|
|
974
|
+
* The `templates()` method is feature-tested via
|
|
975
|
+
* `typeof ext.templates === "function"` so extensions that predate
|
|
976
|
+
* Phase 0 (and don't declare the method) still work. A misbehaving
|
|
977
|
+
* extension that throws inside its `templates()` is logged to
|
|
978
|
+
* `console.warn` but never breaks discovery for the other
|
|
979
|
+
* extensions.
|
|
980
|
+
*/
|
|
981
|
+
listTemplates() {
|
|
982
|
+
const out = [];
|
|
983
|
+
for (const ext of this._extensions) {
|
|
984
|
+
if (typeof ext.templates !== "function")
|
|
985
|
+
continue;
|
|
986
|
+
try {
|
|
987
|
+
out.push(...ext.templates());
|
|
988
|
+
}
|
|
989
|
+
catch (e) {
|
|
990
|
+
const name = ext.name ?? String(ext);
|
|
991
|
+
console.warn(`extension ${name}.templates() raised: ${e}`);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return out;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Materialize a template by id into `opts.targetRoot`.
|
|
998
|
+
*
|
|
999
|
+
* Throws `Error("template not found: <id>")` if no loaded extension
|
|
1000
|
+
* advertises a template with the given id (the TS equivalent of
|
|
1001
|
+
* Python's `KeyError`). `opts.onConflict` is passed through to
|
|
1002
|
+
* {@link materialize}.
|
|
1003
|
+
*/
|
|
1004
|
+
scaffold(templateId, opts) {
|
|
1005
|
+
for (const t of this.listTemplates()) {
|
|
1006
|
+
if (t.id === templateId) {
|
|
1007
|
+
return materialize(t, opts);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
throw new Error(`template not found: ${templateId}`);
|
|
1011
|
+
}
|
|
1012
|
+
// -- Generic reader/writer auto-registration ------------------------------
|
|
1013
|
+
/** @internal — public for the KindRegistry `RegistryHost` slice (the
|
|
1014
|
+
* 2-phase-load rescan re-resolves generic BUNDLE readers/writers here). */
|
|
1015
|
+
_ensureGenericReadersWriters() {
|
|
1016
|
+
if (this._genericsResolved)
|
|
1017
|
+
return;
|
|
1018
|
+
this._genericsResolved = true;
|
|
1019
|
+
for (const kp of this._kinds.values()) {
|
|
1020
|
+
const sd = kp.storage;
|
|
1021
|
+
if (!sd || sd.pattern !== "bundle")
|
|
1022
|
+
continue;
|
|
1023
|
+
// Check if any existing reader already handles this marker
|
|
1024
|
+
const hasReader = this._readers.some((r) => r._marker === sd.marker);
|
|
1025
|
+
if (!hasReader) {
|
|
1026
|
+
this._readers.push(new GenericBundleReader(sd, kp.apiVersion, kp.kind, this._fs));
|
|
1027
|
+
}
|
|
1028
|
+
// Check if any existing writer already handles this kind
|
|
1029
|
+
const hasWriter = this._writers.some((w) => w._kind === kp.kind);
|
|
1030
|
+
if (!hasWriter) {
|
|
1031
|
+
this._writers.push(new GenericBundleWriter(sd, kp.kind, this._fs));
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
// -- Kernel storage helpers (facades over this._kindreg — Fase 3) ---------
|
|
1036
|
+
containerForKind(kindName) {
|
|
1037
|
+
return this._kindreg.containerFor(kindName);
|
|
1038
|
+
}
|
|
1039
|
+
storageForKind(kindName) {
|
|
1040
|
+
return this._kindreg.storageFor(kindName);
|
|
1041
|
+
}
|
|
1042
|
+
kindByContainer(container) {
|
|
1043
|
+
return this._kindreg.byContainer(container);
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Stable human-readable locator for a document.
|
|
1047
|
+
*
|
|
1048
|
+
* - Filesystem sources (detected by the presence of a `baseDir`
|
|
1049
|
+
* property) → "<baseDir>/<scope>/<kindSubdir>/<name>"
|
|
1050
|
+
* - Other sources → "<scheme>://<scope>/<kind>/<name>" where scheme
|
|
1051
|
+
* comes from source.urlScheme, falling back to the class name with
|
|
1052
|
+
* the trailing "source" suffix stripped.
|
|
1053
|
+
*
|
|
1054
|
+
* Parity: python Kernel._target_locator.
|
|
1055
|
+
*/
|
|
1056
|
+
_targetLocator(scope, kind, name) {
|
|
1057
|
+
const src = this._source;
|
|
1058
|
+
if (src && typeof src.baseDir === "string") {
|
|
1059
|
+
const sd = this.storageForKind(kind);
|
|
1060
|
+
const subdir = sd?.container ? sd.container : `${kind.toLowerCase()}s`;
|
|
1061
|
+
return `${src.baseDir}/${scope}/${subdir}/${name}`;
|
|
1062
|
+
}
|
|
1063
|
+
const scheme = src?.urlScheme ??
|
|
1064
|
+
(src
|
|
1065
|
+
? src.constructor.name.toLowerCase().replace(/source$/, "")
|
|
1066
|
+
: "unknown");
|
|
1067
|
+
return `${scheme}://${scope}/${kind}/${name}`;
|
|
1068
|
+
}
|
|
1069
|
+
// -- Write path -----------------------------------------------------------
|
|
1070
|
+
serializeDocument(_scope, kind, name, raw) {
|
|
1071
|
+
this._ensureGenericReadersWriters();
|
|
1072
|
+
// Find KindPort
|
|
1073
|
+
let kp;
|
|
1074
|
+
for (const k of this._kinds.values()) {
|
|
1075
|
+
if (k.kind === kind) {
|
|
1076
|
+
kp = k;
|
|
1077
|
+
break;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (!kp)
|
|
1081
|
+
throw new Error(`Unknown kind: ${kind}`);
|
|
1082
|
+
const sd = kp.storage;
|
|
1083
|
+
// serialize() is part of the WriterPort contract (enforced at
|
|
1084
|
+
// registration since s-dna-rw-roundtrip-suite) — the first writer
|
|
1085
|
+
// that claims the kind serializes.
|
|
1086
|
+
const writer = this._writers.find(w => w.canWrite(raw));
|
|
1087
|
+
let rawFiles;
|
|
1088
|
+
if (writer) {
|
|
1089
|
+
rawFiles = writer.serialize(raw);
|
|
1090
|
+
}
|
|
1091
|
+
else if (sd.pattern === "yaml") {
|
|
1092
|
+
rawFiles = [{ relativePath: `${name}.yaml`, content: yaml.dump(raw, { flowLevel: -1, sortKeys: false }) }];
|
|
1093
|
+
return { files: rawFiles.map(f => ({
|
|
1094
|
+
relativePath: sd.container ? `${sd.container}/${f.relativePath}` : f.relativePath,
|
|
1095
|
+
content: f.content,
|
|
1096
|
+
})) };
|
|
1097
|
+
}
|
|
1098
|
+
else if (sd.pattern === "root") {
|
|
1099
|
+
return { files: [{ relativePath: sd.marker, content: yaml.dump(raw, { flowLevel: -1, sortKeys: false }) }] };
|
|
1100
|
+
}
|
|
1101
|
+
else if (sd.pattern === "standalone") {
|
|
1102
|
+
const spec = (raw.spec ?? {});
|
|
1103
|
+
const content = sd.bodyField ? String(spec[sd.bodyField] ?? "") : yaml.dump(raw, { flowLevel: -1 });
|
|
1104
|
+
return { files: [{ relativePath: sd.marker, content }] };
|
|
1105
|
+
}
|
|
1106
|
+
else {
|
|
1107
|
+
// BUNDLE without serialize() — fallback to YAML dump of raw
|
|
1108
|
+
rawFiles = [{ relativePath: sd.marker ?? `${name}.yaml`, content: yaml.dump(raw, { flowLevel: -1 }) }];
|
|
1109
|
+
}
|
|
1110
|
+
// Prefix paths for BUNDLE: container/name/file
|
|
1111
|
+
const prefix = sd.pattern === "bundle"
|
|
1112
|
+
? (sd.container ? `${sd.container}/${name}/` : `${name}/`)
|
|
1113
|
+
: "";
|
|
1114
|
+
// Preserve the entry payload as-is: text entries carry `content`,
|
|
1115
|
+
// binary ones `contentBytes` (the WriterPort serialize contract).
|
|
1116
|
+
return {
|
|
1117
|
+
files: rawFiles.map(f => ({
|
|
1118
|
+
relativePath: `${prefix}${f.relativePath}`,
|
|
1119
|
+
...(f.contentBytes !== undefined
|
|
1120
|
+
? { contentBytes: f.contentBytes }
|
|
1121
|
+
: { content: f.content }),
|
|
1122
|
+
})),
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Pure preview — returns target, serialized files, existsAlready.
|
|
1127
|
+
*
|
|
1128
|
+
* Does NOT touch disk. ``existsAlready`` is a UI hint so callers can
|
|
1129
|
+
* render "create" vs "overwrite" affordances. Parity: Python
|
|
1130
|
+
* Kernel.preview_document.
|
|
1131
|
+
*/
|
|
1132
|
+
async previewDocument(scope, kind, name, raw) {
|
|
1133
|
+
const payload = this.serializeDocument(scope, kind, name, raw);
|
|
1134
|
+
const target = this._targetLocator(scope, kind, name);
|
|
1135
|
+
const existsAlready = await this._targetExists(scope, kind, name);
|
|
1136
|
+
return { target, files: payload.files, existsAlready };
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Best-effort probe: is the target document already present?
|
|
1140
|
+
*
|
|
1141
|
+
* Uses the writable source's `listVersions` when available (non-empty
|
|
1142
|
+
* = exists). Returns false on any adapter failure — this is a UI hint,
|
|
1143
|
+
* not a correctness gate. Parity: Python Kernel._target_exists.
|
|
1144
|
+
*/
|
|
1145
|
+
async _targetExists(scope, kind, name) {
|
|
1146
|
+
const src = this._writableSource ?? this._source;
|
|
1147
|
+
if (!src)
|
|
1148
|
+
return false;
|
|
1149
|
+
const listVersions = src.listVersions;
|
|
1150
|
+
if (typeof listVersions !== "function")
|
|
1151
|
+
return false;
|
|
1152
|
+
try {
|
|
1153
|
+
const versions = await listVersions.call(src, scope, kind, name);
|
|
1154
|
+
return versions.length > 0;
|
|
1155
|
+
}
|
|
1156
|
+
catch {
|
|
1157
|
+
return false;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
async writeDocument(scope, kind, name, raw, options) {
|
|
1161
|
+
// Thin facade (Fase 2, s-kernel-decomp-f2-writepipeline) — the fat body
|
|
1162
|
+
// (tenant resolve, layer-policy check, pre_save veto, persist,
|
|
1163
|
+
// post_save) lives in WritePipeline.write.
|
|
1164
|
+
return this._writePipeline.write(scope, kind, name, raw, options);
|
|
1165
|
+
}
|
|
1166
|
+
async deleteDocument(scope, kind, name, options) {
|
|
1167
|
+
// Thin facade (Fase 2) — delegates to WritePipeline.delete. Deletes have
|
|
1168
|
+
// NO pre_save veto (only writes do).
|
|
1169
|
+
return this._writePipeline.delete(scope, kind, name, options);
|
|
1170
|
+
}
|
|
1171
|
+
/** @internal — resolve the registered writable source or throw
|
|
1172
|
+
* NotWritableError. Used by WritePipeline (Py twin:
|
|
1173
|
+
* `Kernel._require_writable_source`). */
|
|
1174
|
+
_requireWritableSource() {
|
|
1175
|
+
if (!this._writableSource) {
|
|
1176
|
+
throw new NotWritableError("No writable source registered. Call kernel.writableSource() first.");
|
|
1177
|
+
}
|
|
1178
|
+
return this._writableSource;
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Per-scope cache of the base (no-layer) ManifestInstance. Used by the
|
|
1182
|
+
* layer-policy check so it does not re-resolve layers per call. Mirrors
|
|
1183
|
+
* Python KernelCache._base (the kernel's extracted cache collaborator).
|
|
1184
|
+
*/
|
|
1185
|
+
_baseInstanceCache;
|
|
1186
|
+
/** LRU bound on _baseInstanceCache (scopes). Mirrors Py _BASE_INSTANCE_MAX (i-036). */
|
|
1187
|
+
static BASE_INSTANCE_MAX = 64;
|
|
1188
|
+
async _ensureBaseInstance(scope) {
|
|
1189
|
+
if (!this._baseInstanceCache)
|
|
1190
|
+
this._baseInstanceCache = new Map();
|
|
1191
|
+
const cache = this._baseInstanceCache;
|
|
1192
|
+
if (cache.has(scope)) {
|
|
1193
|
+
// LRU touch — move to the MRU end so a hot scope survives eviction.
|
|
1194
|
+
const v = cache.get(scope) ?? null;
|
|
1195
|
+
cache.delete(scope);
|
|
1196
|
+
cache.set(scope, v);
|
|
1197
|
+
return v;
|
|
1198
|
+
}
|
|
1199
|
+
let mi;
|
|
1200
|
+
try {
|
|
1201
|
+
mi = await this.instance(scope);
|
|
1202
|
+
}
|
|
1203
|
+
catch {
|
|
1204
|
+
mi = null;
|
|
1205
|
+
}
|
|
1206
|
+
cache.set(scope, mi);
|
|
1207
|
+
// Evict least-recently-used (oldest insertion) over the bound.
|
|
1208
|
+
while (cache.size > Kernel.BASE_INSTANCE_MAX) {
|
|
1209
|
+
const oldest = cache.keys().next().value;
|
|
1210
|
+
cache.delete(oldest);
|
|
1211
|
+
}
|
|
1212
|
+
return mi;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Resolve a kind name to its globally-unique alias (`<owner>-<kind>`).
|
|
1216
|
+
* Falls back to `kind.toLowerCase()` when no registered port provides one.
|
|
1217
|
+
* Facade over `this._kindreg.aliasFor()` (Fase 3). Parity: Python
|
|
1218
|
+
* Kernel._alias_for delegating to `self._kindreg.alias_for`.
|
|
1219
|
+
*/
|
|
1220
|
+
_aliasFor(kind) {
|
|
1221
|
+
return this._kindreg.aliasFor(kind);
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Validate that writing/deleting `kind/name` in `scope` against the given
|
|
1225
|
+
* layer overlay satisfies the Module's LayerPolicy (OPEN / RESTRICTED /
|
|
1226
|
+
* LOCKED). Parity: Python Kernel._check_layer_policy.
|
|
1227
|
+
*
|
|
1228
|
+
* Policy modes (resolved via alias `<owner>-<kind>`):
|
|
1229
|
+
* - LOCKED — any write throws.
|
|
1230
|
+
* - RESTRICTED — adding a new doc (not present in base) throws; adding a
|
|
1231
|
+
* *new top-level spec key* on an existing doc throws; overriding
|
|
1232
|
+
* existing top-level spec keys is allowed.
|
|
1233
|
+
* - OPEN (default) — never throws.
|
|
1234
|
+
*
|
|
1235
|
+
* When the scope has no Module doc, policy defaults to OPEN (no-op).
|
|
1236
|
+
*/
|
|
1237
|
+
/** @internal — read by WritePipeline (write/delete policy gate). */
|
|
1238
|
+
async _checkLayerPolicyAsync(scope, kind, name, raw, layer) {
|
|
1239
|
+
const [layerId] = layer;
|
|
1240
|
+
// Phase 16 — hardcoded allowlist: Genome / KindDefinition /
|
|
1241
|
+
// LayerPolicy can never be written to a layer overlay regardless
|
|
1242
|
+
// of declared policy. Identity, schema-bootstrap and policy Kinds
|
|
1243
|
+
// are structurally non-overlayable (a tenant must not be able to
|
|
1244
|
+
// redefine its own visibility, version, or the policy that
|
|
1245
|
+
// constrains its overlay).
|
|
1246
|
+
if (this.NON_OVERLAYABLE_KINDS.has(kind)) {
|
|
1247
|
+
throw new LayerPolicyViolationError(`${kind} is structurally non-overlayable; ` +
|
|
1248
|
+
`cannot write to layer '${layerId}'`);
|
|
1249
|
+
}
|
|
1250
|
+
const mi = await this._ensureBaseInstance(scope);
|
|
1251
|
+
if (!mi)
|
|
1252
|
+
return; // no manifest / unreadable → no policy to enforce
|
|
1253
|
+
const alias = this._aliasFor(kind);
|
|
1254
|
+
// Phase 16 commit 4 — policies come exclusively from LayerPolicy
|
|
1255
|
+
// docs in the scope. Module.spec.layers legacy path is GONE.
|
|
1256
|
+
// Iterate; the last doc whose spec.layer_id matches wins.
|
|
1257
|
+
let policyStr = "open";
|
|
1258
|
+
let layerPolicyDocs = [];
|
|
1259
|
+
try {
|
|
1260
|
+
layerPolicyDocs = mi._all("LayerPolicy");
|
|
1261
|
+
}
|
|
1262
|
+
catch {
|
|
1263
|
+
layerPolicyDocs = [];
|
|
1264
|
+
}
|
|
1265
|
+
for (const lpDoc of layerPolicyDocs) {
|
|
1266
|
+
const lpSpec = (lpDoc.spec ?? {});
|
|
1267
|
+
if (lpSpec.layer_id !== layerId)
|
|
1268
|
+
continue;
|
|
1269
|
+
const lpPolicies = (lpSpec.policies ?? {});
|
|
1270
|
+
const value = lpPolicies[alias];
|
|
1271
|
+
if (typeof value === "string" && value) {
|
|
1272
|
+
policyStr = value.toLowerCase();
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (policyStr === "locked") {
|
|
1276
|
+
throw new LayerPolicyViolationError(`${alias} is LOCKED in layer '${layerId}' per LayerPolicy docs`);
|
|
1277
|
+
}
|
|
1278
|
+
if (policyStr === "restricted") {
|
|
1279
|
+
const existing = mi._one(kind, name);
|
|
1280
|
+
if (!existing) {
|
|
1281
|
+
throw new LayerPolicyViolationError(`${alias} in layer '${layerId}' is RESTRICTED — ` +
|
|
1282
|
+
`cannot add new document '${name}' not present in base`);
|
|
1283
|
+
}
|
|
1284
|
+
// Compare against the doc's RAW spec (what was actually authored), not
|
|
1285
|
+
// the typed spec (which exposes every field defined on the model —
|
|
1286
|
+
// including unset defaults). Fall back to typed spec only when raw is
|
|
1287
|
+
// unavailable. Mirrors Python Task 5's fix.
|
|
1288
|
+
const existingRaw = (existing.raw ?? {});
|
|
1289
|
+
const existingRawSpec = existingRaw.spec;
|
|
1290
|
+
let existingKeys;
|
|
1291
|
+
if (existingRawSpec && typeof existingRawSpec === "object") {
|
|
1292
|
+
existingKeys = new Set(Object.keys(existingRawSpec));
|
|
1293
|
+
}
|
|
1294
|
+
else {
|
|
1295
|
+
const existingSpec = (existing.spec ?? {});
|
|
1296
|
+
existingKeys = new Set(Object.keys(existingSpec));
|
|
1297
|
+
}
|
|
1298
|
+
const newSpecVal = raw?.spec;
|
|
1299
|
+
const newSpec = newSpecVal && typeof newSpecVal === "object"
|
|
1300
|
+
? newSpecVal
|
|
1301
|
+
: {};
|
|
1302
|
+
const added = Object.keys(newSpec)
|
|
1303
|
+
.filter((k) => !existingKeys.has(k))
|
|
1304
|
+
.sort();
|
|
1305
|
+
if (added.length > 0) {
|
|
1306
|
+
throw new LayerPolicyViolationError(`${alias} in layer '${layerId}' is RESTRICTED — ` +
|
|
1307
|
+
`cannot add new top-level spec keys [${added.join(", ")}]; ` +
|
|
1308
|
+
`may only override existing`);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
// OPEN: allow
|
|
1312
|
+
}
|
|
1313
|
+
// -- Instance creation ----------------------------------------------------
|
|
1314
|
+
async instance(scope, layers) {
|
|
1315
|
+
this._ensureGenericReadersWriters();
|
|
1316
|
+
if (!this._source) {
|
|
1317
|
+
throw new Error("No source registered. Call kernel.source() first.");
|
|
1318
|
+
}
|
|
1319
|
+
if (!this._cache) {
|
|
1320
|
+
throw new Error("No cache registered. Call kernel.cache() first.");
|
|
1321
|
+
}
|
|
1322
|
+
// 1. Load bootstrap docs (Phase 16 — Genome + KindDefinition +
|
|
1323
|
+
// LayerPolicy in one shot). Replaces the legacy ``loadManifest``
|
|
1324
|
+
// cardinality-1 contract.
|
|
1325
|
+
const bootstrapDocs = await this._source.loadBootstrapDocs(scope);
|
|
1326
|
+
let manifest = {};
|
|
1327
|
+
for (const d of bootstrapDocs) {
|
|
1328
|
+
if (d.kind === "Genome") {
|
|
1329
|
+
manifest = d;
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
// 1b. Register custom_kinds from manifest (legacy ``Module.spec
|
|
1334
|
+
// .custom_kinds`` field). KindDefinition docs from bootstrapDocs
|
|
1335
|
+
// get registered later via the existing 2-phase loader path.
|
|
1336
|
+
this._registerCustomKinds(manifest);
|
|
1337
|
+
// 2. Resolve deps (auto on cache miss)
|
|
1338
|
+
const resolveErrors = [];
|
|
1339
|
+
const spec = manifest.spec ?? {};
|
|
1340
|
+
const deps = spec.dependencies ?? [];
|
|
1341
|
+
for (const dep of deps) {
|
|
1342
|
+
const uri = dep.source ?? "";
|
|
1343
|
+
const scheme = uri.includes(":") ? uri.split(":")[0] : "";
|
|
1344
|
+
const resolver = this._resolvers.get(scheme);
|
|
1345
|
+
if (!resolver) {
|
|
1346
|
+
resolveErrors.push(`No resolver for scheme: ${scheme}`);
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
const key = resolver.cacheKey(uri);
|
|
1350
|
+
if (!this._cache.has(scope, key)) {
|
|
1351
|
+
try {
|
|
1352
|
+
const resolved = await resolver.resolve(uri, dep);
|
|
1353
|
+
const cacheItems = resolved.map((r) => ({
|
|
1354
|
+
name: r.name,
|
|
1355
|
+
kind: r.kind,
|
|
1356
|
+
contentPath: r.sourcePath,
|
|
1357
|
+
}));
|
|
1358
|
+
this._cache.store(scope, key, cacheItems);
|
|
1359
|
+
}
|
|
1360
|
+
catch (e) {
|
|
1361
|
+
if (e instanceof ResolveError) {
|
|
1362
|
+
resolveErrors.push(String(e));
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
throw e;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
// 3. Load local + cache docs (per-key for correct origin tagging)
|
|
1371
|
+
const localRaws = await this._source.loadAll(scope, this._readers);
|
|
1372
|
+
const allRaws = [];
|
|
1373
|
+
for (const raw of localRaws) {
|
|
1374
|
+
allRaws.push({ raw, origin: "local" });
|
|
1375
|
+
}
|
|
1376
|
+
// Scope-level inheritance — Story s-platform-agent-fallback (2026-05-28).
|
|
1377
|
+
// Eager MI: carrega docs do INHERIT_PARENT_SCOPE filtrados pra kinds
|
|
1378
|
+
// em INHERITABLE_KINDS, merge com local (local ganha por (kind, name)).
|
|
1379
|
+
// 1:1 parity com Python Kernel.instance_async.
|
|
1380
|
+
if (scope !== Kernel.INHERIT_PARENT_SCOPE) {
|
|
1381
|
+
let parentRaws = [];
|
|
1382
|
+
try {
|
|
1383
|
+
parentRaws = await this._source.loadAll(Kernel.INHERIT_PARENT_SCOPE, this._readers);
|
|
1384
|
+
}
|
|
1385
|
+
catch {
|
|
1386
|
+
// defensive — parent scope inacessível, scope local segue normal
|
|
1387
|
+
}
|
|
1388
|
+
const localKeys = new Set(localRaws.map((r) => {
|
|
1389
|
+
const meta = r.metadata;
|
|
1390
|
+
const name = meta?.name ?? r.name;
|
|
1391
|
+
return `${r.kind ?? ""}\0${name ?? ""}`;
|
|
1392
|
+
}));
|
|
1393
|
+
for (const praw of parentRaws) {
|
|
1394
|
+
const pkind = praw.kind;
|
|
1395
|
+
if (!pkind || !this.INHERITABLE_KINDS.has(pkind))
|
|
1396
|
+
continue;
|
|
1397
|
+
const pmeta = praw.metadata;
|
|
1398
|
+
const pname = pmeta?.name ?? praw.name;
|
|
1399
|
+
const pkey = `${pkind}\0${pname ?? ""}`;
|
|
1400
|
+
if (localKeys.has(pkey))
|
|
1401
|
+
continue;
|
|
1402
|
+
allRaws.push({ raw: praw, origin: `inherited:${Kernel.INHERIT_PARENT_SCOPE}` });
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
for (const dep of deps) {
|
|
1406
|
+
const uri = dep.source ?? "";
|
|
1407
|
+
const scheme = uri.includes(":") ? uri.split(":")[0] : "";
|
|
1408
|
+
const resolver = this._resolvers.get(scheme);
|
|
1409
|
+
if (!resolver)
|
|
1410
|
+
continue;
|
|
1411
|
+
const key = resolver.cacheKey(uri);
|
|
1412
|
+
const keyRaws = await this._cache.loadKey(scope, key, this._readers);
|
|
1413
|
+
for (const raw of keyRaws) {
|
|
1414
|
+
allRaws.push({ raw, origin: uri });
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
// 5. Apply layers (if requested)
|
|
1418
|
+
let finalRaws = allRaws;
|
|
1419
|
+
if (layers && Object.keys(layers).length > 0) {
|
|
1420
|
+
finalRaws = await this._applyLayers(scope, allRaws, layers);
|
|
1421
|
+
}
|
|
1422
|
+
// ── Phase 1: parse + register KindDefinitions ──
|
|
1423
|
+
const addedReaders = this._registerKindDefinitions(finalRaws.map((r) => r.raw));
|
|
1424
|
+
// If new declarative kinds introduced new generic readers, re-scan the
|
|
1425
|
+
// source so instance documents of those new kinds are picked up.
|
|
1426
|
+
if (addedReaders && this._source) {
|
|
1427
|
+
try {
|
|
1428
|
+
const extra = await this._source.loadAll(scope, this._readers);
|
|
1429
|
+
const seen = new Set(finalRaws.map(({ raw }) => `${raw.apiVersion ?? ""}\0${raw.kind ?? ""}\0${(raw.metadata?.name) ?? ""}`));
|
|
1430
|
+
for (const r of extra) {
|
|
1431
|
+
const key = `${r.apiVersion ?? ""}\0${r.kind ?? ""}\0${(r.metadata?.name) ?? ""}`;
|
|
1432
|
+
if (!seen.has(key)) {
|
|
1433
|
+
finalRaws.push({ raw: r, origin: "local" });
|
|
1434
|
+
seen.add(key);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
catch {
|
|
1439
|
+
// defensive
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
// ── Phase 2: parse all docs via KindPorts ──
|
|
1443
|
+
const documents = [];
|
|
1444
|
+
for (const { raw, origin } of finalRaws) {
|
|
1445
|
+
documents.push(this._parseDoc(raw, origin));
|
|
1446
|
+
}
|
|
1447
|
+
return new ManifestInstance({
|
|
1448
|
+
scope,
|
|
1449
|
+
documents,
|
|
1450
|
+
kinds: this._kinds,
|
|
1451
|
+
source: this._source,
|
|
1452
|
+
resolveErrors,
|
|
1453
|
+
kernel: this,
|
|
1454
|
+
profiles: this._profiles,
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
async resolveLayers(mi, layers) {
|
|
1458
|
+
return this.instance(mi.scope, layers);
|
|
1459
|
+
}
|
|
1460
|
+
// -- Layer application ----------------------------------------------------
|
|
1461
|
+
async _applyLayers(scope, baseRaws, layers) {
|
|
1462
|
+
// Get policies from root document
|
|
1463
|
+
const policies = {};
|
|
1464
|
+
for (const { raw } of baseRaws) {
|
|
1465
|
+
const av = raw.apiVersion ?? "";
|
|
1466
|
+
const kn = raw.kind ?? "";
|
|
1467
|
+
const key = `${av}\0${kn}`;
|
|
1468
|
+
const kp = this._kinds.get(key);
|
|
1469
|
+
if (kp?.isRoot) {
|
|
1470
|
+
const doc = this._parseDoc(raw);
|
|
1471
|
+
const rawPolicies = kp.getLayerPolicies(doc);
|
|
1472
|
+
if (rawPolicies) {
|
|
1473
|
+
for (const [alias, ps] of Object.entries(rawPolicies)) {
|
|
1474
|
+
policies[alias] = String(ps);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
break;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
// Load overlay docs — source uses readers to detect bundles (SKILL.md etc)
|
|
1481
|
+
const overlayRaws = [];
|
|
1482
|
+
for (const [layerId, value] of Object.entries(layers)) {
|
|
1483
|
+
const loaded = await this._source.loadLayer(scope, layerId, value, this._readers);
|
|
1484
|
+
overlayRaws.push(...loaded);
|
|
1485
|
+
}
|
|
1486
|
+
if (overlayRaws.length === 0)
|
|
1487
|
+
return baseRaws;
|
|
1488
|
+
// Merge using DefaultLayerResolver with a direct adapter
|
|
1489
|
+
// so the resolver doesn't need to hit disk again
|
|
1490
|
+
const resolver = new DefaultLayerResolver();
|
|
1491
|
+
const directSource = {
|
|
1492
|
+
loadLayer: () => overlayRaws,
|
|
1493
|
+
};
|
|
1494
|
+
const rawsOnly = baseRaws.map((r) => r.raw);
|
|
1495
|
+
const merged = resolver.resolve(rawsOnly, layers, directSource, scope, policies);
|
|
1496
|
+
return merged.map((raw) => ({ raw, origin: "local" }));
|
|
1497
|
+
}
|
|
1498
|
+
// -- Parsing --------------------------------------------------------------
|
|
1499
|
+
/**
|
|
1500
|
+
* If a kind declares `descriptionFallbackField` and metadata.description
|
|
1501
|
+
* is missing/empty, derive it from the named spec field. Mutates `raw`.
|
|
1502
|
+
*/
|
|
1503
|
+
static _fillDerivedDescription(raw, kindPort) {
|
|
1504
|
+
const field = kindPort
|
|
1505
|
+
.descriptionFallbackField;
|
|
1506
|
+
if (!field)
|
|
1507
|
+
return;
|
|
1508
|
+
let meta = raw.metadata;
|
|
1509
|
+
if (!meta) {
|
|
1510
|
+
meta = {};
|
|
1511
|
+
raw.metadata = meta;
|
|
1512
|
+
}
|
|
1513
|
+
if (meta.description)
|
|
1514
|
+
return;
|
|
1515
|
+
const text = (raw.spec ?? {})[field];
|
|
1516
|
+
const derived = deriveFirstLine(text);
|
|
1517
|
+
if (derived)
|
|
1518
|
+
meta.description = derived;
|
|
1519
|
+
}
|
|
1520
|
+
_parseDoc(raw, origin = "local") {
|
|
1521
|
+
const av = raw.apiVersion ?? "";
|
|
1522
|
+
const kn = raw.kind ?? "";
|
|
1523
|
+
const name = raw.metadata?.name ?? "";
|
|
1524
|
+
const key = `${av}\0${kn}`;
|
|
1525
|
+
const kindPort = this._kinds.get(key);
|
|
1526
|
+
let typed = null;
|
|
1527
|
+
if (kindPort) {
|
|
1528
|
+
try {
|
|
1529
|
+
Kernel._fillDerivedDescription(raw, kindPort);
|
|
1530
|
+
typed = kindPort.parse(raw);
|
|
1531
|
+
}
|
|
1532
|
+
catch (e) {
|
|
1533
|
+
console.warn(`Parse error for ${av}/${kn}: ${e}`);
|
|
1534
|
+
if (this.hooks.has("parse_error")) {
|
|
1535
|
+
this.hooks.emit("parse_error", {
|
|
1536
|
+
scope: "",
|
|
1537
|
+
kind: kn,
|
|
1538
|
+
name,
|
|
1539
|
+
data: { error: String(e), apiVersion: av, raw },
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return Document.fromRaw(raw, typed ?? undefined, origin);
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Phase 1 of 2-phase loading: parse KindDefinition docs + register
|
|
1548
|
+
* synthetic DeclarativeKindPorts. Thin facade over the KindRegistry
|
|
1549
|
+
* funnel (Fase 3, s-kernel-decomp-ts-parity). Returns true iff new
|
|
1550
|
+
* BUNDLE readers were added (the rescan gate). Py twin:
|
|
1551
|
+
* `Kernel._register_kind_definitions`.
|
|
1552
|
+
*/
|
|
1553
|
+
_registerKindDefinitions(rawDocs) {
|
|
1554
|
+
return this._kindreg.registerKindDefinitions(rawDocs);
|
|
1555
|
+
}
|
|
1556
|
+
// -- Custom kinds ---------------------------------------------------------
|
|
1557
|
+
/** Register dynamic `Module.spec.custom_kinds`. Thin facade over the
|
|
1558
|
+
* KindRegistry funnel (Fase 3). Py twin: `Kernel._register_custom_kinds`. */
|
|
1559
|
+
_registerCustomKinds(manifest) {
|
|
1560
|
+
this._kindreg.registerCustomKinds(manifest);
|
|
1561
|
+
}
|
|
1562
|
+
// Quick-start helpers (quick / auto) have been moved to
|
|
1563
|
+
// typescript/src/bootstrap.ts — use createKernelWithBuiltins() or
|
|
1564
|
+
// quickInstance() instead. The kernel itself should NOT import from
|
|
1565
|
+
// extensions/ (microkernel principle).
|
|
1566
|
+
// -- Model registry helpers -----------------------------------------------
|
|
1567
|
+
/** s-realtime-model-single-default — 1:1 parity with the Python kernel:
|
|
1568
|
+
* the realtime fallback the prompt-budget guard caps against resolves
|
|
1569
|
+
* through ONE env (DNA_VOICE_REALTIME_MODEL), read at ACCESS time so a
|
|
1570
|
+
* pin set after construction still applies. */
|
|
1571
|
+
_DEFAULT_REALTIME_MODEL_FALLBACK = "gpt-realtime-2";
|
|
1572
|
+
get _DEFAULT_REALTIME_MODEL() {
|
|
1573
|
+
return (process.env.DNA_VOICE_REALTIME_MODEL || this._DEFAULT_REALTIME_MODEL_FALLBACK);
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Resolve a ModelProfile from the `_lib` scope by `model_id` (pass 1)
|
|
1577
|
+
* or `aliases[]` (pass 2). Returns the matching Document or null on miss.
|
|
1578
|
+
*
|
|
1579
|
+
* Always queries `MODEL_REGISTRY_SCOPE` ("_lib") directly — ModelProfile
|
|
1580
|
+
* is GLOBAL and NOT in `INHERITABLE_KINDS`; any caller scope is irrelevant.
|
|
1581
|
+
*
|
|
1582
|
+
* 1:1 parity with Python `Kernel.model_profile(model_id_or_alias)`.
|
|
1583
|
+
*
|
|
1584
|
+
* @param modelIdOrAlias - The model_id string or an alias declared in the profile.
|
|
1585
|
+
* @returns The matching Document, or null on miss or error.
|
|
1586
|
+
*/
|
|
1587
|
+
async modelProfile(modelIdOrAlias) {
|
|
1588
|
+
let rows;
|
|
1589
|
+
try {
|
|
1590
|
+
const mi = await this.instance(MODEL_REGISTRY_SCOPE);
|
|
1591
|
+
rows = mi._all("ModelProfile");
|
|
1592
|
+
}
|
|
1593
|
+
catch {
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1596
|
+
// Pass 1: exact model_id match
|
|
1597
|
+
for (const row of rows) {
|
|
1598
|
+
const spec = (row.spec ?? {});
|
|
1599
|
+
if (spec.model_id === modelIdOrAlias) {
|
|
1600
|
+
return row;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
// Pass 2: aliases[]
|
|
1604
|
+
for (const row of rows) {
|
|
1605
|
+
const spec = (row.spec ?? {});
|
|
1606
|
+
const aliases = (spec.aliases ?? []);
|
|
1607
|
+
if (aliases.includes(modelIdOrAlias)) {
|
|
1608
|
+
return row;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Resolve a VoicePolicy from the `_lib` scope by metadata name.
|
|
1615
|
+
* Returns the matching Document, the first policy as a fallback, or null
|
|
1616
|
+
* on miss/error. Always queries `VOICE_POLICY_SCOPE` ("_lib")
|
|
1617
|
+
* directly — VoicePolicy is GLOBAL and NOT in `INHERITABLE_KINDS`.
|
|
1618
|
+
*
|
|
1619
|
+
* 1:1 parity with Python `Kernel.voice_policy(name)`.
|
|
1620
|
+
*/
|
|
1621
|
+
async voicePolicy(name = "default") {
|
|
1622
|
+
let rows;
|
|
1623
|
+
try {
|
|
1624
|
+
const mi = await this.instance(VOICE_POLICY_SCOPE);
|
|
1625
|
+
rows = mi._all("VoicePolicy");
|
|
1626
|
+
}
|
|
1627
|
+
catch {
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
for (const row of rows) {
|
|
1631
|
+
if (row.name === name) {
|
|
1632
|
+
return row;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
return rows.length > 0 ? rows[0] : null;
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
export { ManifestInstance } from "./instance.js";
|