dna-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -0
  3. package/dist/adapters/embedding/onnx.d.ts +32 -0
  4. package/dist/adapters/embedding/onnx.js +51 -0
  5. package/dist/adapters/filesystem/cache.d.ts +16 -0
  6. package/dist/adapters/filesystem/cache.js +113 -0
  7. package/dist/adapters/filesystem/index.d.ts +2 -0
  8. package/dist/adapters/filesystem/index.js +2 -0
  9. package/dist/adapters/filesystem/source.d.ts +82 -0
  10. package/dist/adapters/filesystem/source.js +357 -0
  11. package/dist/adapters/postgres/index.d.ts +2 -0
  12. package/dist/adapters/postgres/index.js +2 -0
  13. package/dist/adapters/postgres/migrations.d.ts +14 -0
  14. package/dist/adapters/postgres/migrations.js +88 -0
  15. package/dist/adapters/postgres/source.d.ts +172 -0
  16. package/dist/adapters/postgres/source.js +569 -0
  17. package/dist/adapters/resolvers/github.d.ts +12 -0
  18. package/dist/adapters/resolvers/github.js +47 -0
  19. package/dist/adapters/resolvers/http.d.ts +22 -0
  20. package/dist/adapters/resolvers/http.js +145 -0
  21. package/dist/adapters/resolvers/index.d.ts +3 -0
  22. package/dist/adapters/resolvers/index.js +3 -0
  23. package/dist/adapters/resolvers/local.d.ts +24 -0
  24. package/dist/adapters/resolvers/local.js +132 -0
  25. package/dist/adapters/search/driver.d.ts +45 -0
  26. package/dist/adapters/search/driver.js +138 -0
  27. package/dist/adapters/search/migrations.d.ts +26 -0
  28. package/dist/adapters/search/migrations.js +58 -0
  29. package/dist/adapters/search/rrf.d.ts +23 -0
  30. package/dist/adapters/search/rrf.js +42 -0
  31. package/dist/adapters/search/sqlite-vec.d.ts +66 -0
  32. package/dist/adapters/search/sqlite-vec.js +294 -0
  33. package/dist/bootstrap.d.ts +45 -0
  34. package/dist/bootstrap.js +134 -0
  35. package/dist/extensions/agentskills/DOCS.md +18 -0
  36. package/dist/extensions/agentskills.d.ts +28 -0
  37. package/dist/extensions/agentskills.js +252 -0
  38. package/dist/extensions/agentsmd/DOCS.md +18 -0
  39. package/dist/extensions/agentsmd.d.ts +22 -0
  40. package/dist/extensions/agentsmd.js +170 -0
  41. package/dist/extensions/audit/kinds/audit-log.kind.yaml +148 -0
  42. package/dist/extensions/audit.d.ts +25 -0
  43. package/dist/extensions/audit.js +62 -0
  44. package/dist/extensions/automation/kinds/automation.kind.yaml +296 -0
  45. package/dist/extensions/automation/query.d.ts +49 -0
  46. package/dist/extensions/automation/query.js +77 -0
  47. package/dist/extensions/automation/write-guards.d.ts +59 -0
  48. package/dist/extensions/automation/write-guards.js +153 -0
  49. package/dist/extensions/automation.d.ts +41 -0
  50. package/dist/extensions/automation.js +54 -0
  51. package/dist/extensions/collab.d.ts +16 -0
  52. package/dist/extensions/collab.js +79 -0
  53. package/dist/extensions/doc/kinds/doc.kind.yaml +118 -0
  54. package/dist/extensions/doc.d.ts +25 -0
  55. package/dist/extensions/doc.js +10 -0
  56. package/dist/extensions/eval/kinds/eval-baseline.kind.yaml +65 -0
  57. package/dist/extensions/eval/kinds/eval-case.kind.yaml +140 -0
  58. package/dist/extensions/eval/kinds/eval-run.kind.yaml +134 -0
  59. package/dist/extensions/eval/kinds/eval-suite.kind.yaml +87 -0
  60. package/dist/extensions/eval.d.ts +24 -0
  61. package/dist/extensions/eval.js +31 -0
  62. package/dist/extensions/evidence/kinds/evidence.kind.yaml +92 -0
  63. package/dist/extensions/evidence.d.ts +17 -0
  64. package/dist/extensions/evidence.js +94 -0
  65. package/dist/extensions/federation.d.ts +22 -0
  66. package/dist/extensions/federation.js +173 -0
  67. package/dist/extensions/guardrails/DOCS.md +20 -0
  68. package/dist/extensions/guardrails.d.ts +31 -0
  69. package/dist/extensions/guardrails.js +228 -0
  70. package/dist/extensions/helix/DOCS-Actor.md +20 -0
  71. package/dist/extensions/helix/DOCS-Agent.md +20 -0
  72. package/dist/extensions/helix/DOCS-UseCase.md +20 -0
  73. package/dist/extensions/helix/layers.d.ts +12 -0
  74. package/dist/extensions/helix/layers.js +18 -0
  75. package/dist/extensions/helix/write-guards.d.ts +70 -0
  76. package/dist/extensions/helix/write-guards.js +172 -0
  77. package/dist/extensions/helix.d.ts +29 -0
  78. package/dist/extensions/helix.js +963 -0
  79. package/dist/extensions/helix_extras.d.ts +124 -0
  80. package/dist/extensions/helix_extras.js +328 -0
  81. package/dist/extensions/hooks.d.ts +31 -0
  82. package/dist/extensions/hooks.js +247 -0
  83. package/dist/extensions/kinddef/DOCS.md +24 -0
  84. package/dist/extensions/kinddef.d.ts +40 -0
  85. package/dist/extensions/kinddef.js +171 -0
  86. package/dist/extensions/lesson.d.ts +6 -0
  87. package/dist/extensions/lesson.js +194 -0
  88. package/dist/extensions/modelreg/kinds/model-profile.kind.yaml +155 -0
  89. package/dist/extensions/modelreg.d.ts +30 -0
  90. package/dist/extensions/modelreg.js +38 -0
  91. package/dist/extensions/recognizer.d.ts +34 -0
  92. package/dist/extensions/recognizer.js +272 -0
  93. package/dist/extensions/research.d.ts +32 -0
  94. package/dist/extensions/research.js +292 -0
  95. package/dist/extensions/safety.d.ts +33 -0
  96. package/dist/extensions/safety.js +230 -0
  97. package/dist/extensions/sdlc/kinds/adr.kind.yaml +136 -0
  98. package/dist/extensions/sdlc/kinds/archive-proposal.kind.yaml +107 -0
  99. package/dist/extensions/sdlc/kinds/changelog.kind.yaml +108 -0
  100. package/dist/extensions/sdlc/kinds/cognitive-policy.kind.yaml +470 -0
  101. package/dist/extensions/sdlc/kinds/forecast.kind.yaml +125 -0
  102. package/dist/extensions/sdlc/kinds/insight.kind.yaml +87 -0
  103. package/dist/extensions/sdlc/kinds/kaizen.kind.yaml +106 -0
  104. package/dist/extensions/sdlc/kinds/lesson-learned.kind.yaml +270 -0
  105. package/dist/extensions/sdlc/kinds/narrative.kind.yaml +189 -0
  106. package/dist/extensions/sdlc/kinds/pattern-insight.kind.yaml +204 -0
  107. package/dist/extensions/sdlc/kinds/postmortem.kind.yaml +168 -0
  108. package/dist/extensions/sdlc/kinds/pre-mortem.kind.yaml +145 -0
  109. package/dist/extensions/sdlc/kinds/prompt-template.kind.yaml +109 -0
  110. package/dist/extensions/sdlc/kinds/retrospective.kind.yaml +186 -0
  111. package/dist/extensions/sdlc/kinds/risk-register.kind.yaml +178 -0
  112. package/dist/extensions/sdlc/kinds/saved-view.kind.yaml +127 -0
  113. package/dist/extensions/sdlc/kinds/status-report.kind.yaml +129 -0
  114. package/dist/extensions/sdlc/kinds/synthesis-run.kind.yaml +279 -0
  115. package/dist/extensions/sdlc/kinds/synthesizer-state.kind.yaml +196 -0
  116. package/dist/extensions/sdlc/kinds/workflow-event.kind.yaml +195 -0
  117. package/dist/extensions/sdlc.d.ts +32 -0
  118. package/dist/extensions/sdlc.js +1318 -0
  119. package/dist/extensions/soulspec/DOCS.md +19 -0
  120. package/dist/extensions/soulspec.d.ts +22 -0
  121. package/dist/extensions/soulspec.js +287 -0
  122. package/dist/extensions/tenant.d.ts +6 -0
  123. package/dist/extensions/tenant.js +291 -0
  124. package/dist/extensions/testkit.d.ts +24 -0
  125. package/dist/extensions/testkit.js +211 -0
  126. package/dist/index.d.ts +80 -0
  127. package/dist/index.js +102 -0
  128. package/dist/kernel/_text.d.ts +28 -0
  129. package/dist/kernel/_text.js +42 -0
  130. package/dist/kernel/bundle-handle.d.ts +83 -0
  131. package/dist/kernel/bundle-handle.js +184 -0
  132. package/dist/kernel/capabilities.d.ts +157 -0
  133. package/dist/kernel/capabilities.js +109 -0
  134. package/dist/kernel/collaborator-ports.d.ts +91 -0
  135. package/dist/kernel/collaborator-ports.js +1 -0
  136. package/dist/kernel/composition-resolver.d.ts +215 -0
  137. package/dist/kernel/composition-resolver.js +558 -0
  138. package/dist/kernel/composition.d.ts +10 -0
  139. package/dist/kernel/composition.js +1 -0
  140. package/dist/kernel/descriptor-loader.d.ts +13 -0
  141. package/dist/kernel/descriptor-loader.js +48 -0
  142. package/dist/kernel/document.d.ts +43 -0
  143. package/dist/kernel/document.js +71 -0
  144. package/dist/kernel/embedding.d.ts +28 -0
  145. package/dist/kernel/embedding.js +78 -0
  146. package/dist/kernel/errors.d.ts +62 -0
  147. package/dist/kernel/errors.js +82 -0
  148. package/dist/kernel/events.d.ts +6 -0
  149. package/dist/kernel/events.js +21 -0
  150. package/dist/kernel/evidence-capture.d.ts +69 -0
  151. package/dist/kernel/evidence-capture.js +138 -0
  152. package/dist/kernel/fs.d.ts +20 -0
  153. package/dist/kernel/fs.js +128 -0
  154. package/dist/kernel/generic-rw.d.ts +33 -0
  155. package/dist/kernel/generic-rw.js +235 -0
  156. package/dist/kernel/hooks.d.ts +172 -0
  157. package/dist/kernel/hooks.js +256 -0
  158. package/dist/kernel/index.d.ts +624 -0
  159. package/dist/kernel/index.js +1638 -0
  160. package/dist/kernel/instance.d.ts +211 -0
  161. package/dist/kernel/instance.js +487 -0
  162. package/dist/kernel/kind-registry.d.ts +183 -0
  163. package/dist/kernel/kind-registry.js +710 -0
  164. package/dist/kernel/kind_base.d.ts +106 -0
  165. package/dist/kernel/kind_base.js +244 -0
  166. package/dist/kernel/layer-resolver.d.ts +48 -0
  167. package/dist/kernel/layer-resolver.js +284 -0
  168. package/dist/kernel/lock-manager.d.ts +21 -0
  169. package/dist/kernel/lock-manager.js +43 -0
  170. package/dist/kernel/lock.d.ts +38 -0
  171. package/dist/kernel/lock.js +115 -0
  172. package/dist/kernel/meta.d.ts +135 -0
  173. package/dist/kernel/meta.js +817 -0
  174. package/dist/kernel/models.d.ts +3089 -0
  175. package/dist/kernel/models.js +764 -0
  176. package/dist/kernel/navigator.d.ts +36 -0
  177. package/dist/kernel/navigator.js +132 -0
  178. package/dist/kernel/port-surface.d.ts +33 -0
  179. package/dist/kernel/port-surface.js +180 -0
  180. package/dist/kernel/preview.d.ts +60 -0
  181. package/dist/kernel/preview.js +54 -0
  182. package/dist/kernel/prompt-budget.d.ts +39 -0
  183. package/dist/kernel/prompt-budget.js +43 -0
  184. package/dist/kernel/prompt-builder.d.ts +25 -0
  185. package/dist/kernel/prompt-builder.js +237 -0
  186. package/dist/kernel/protocols.d.ts +751 -0
  187. package/dist/kernel/protocols.js +493 -0
  188. package/dist/kernel/reports.d.ts +26 -0
  189. package/dist/kernel/reports.js +215 -0
  190. package/dist/kernel/resolver.d.ts +146 -0
  191. package/dist/kernel/resolver.js +232 -0
  192. package/dist/kernel/resource.d.ts +57 -0
  193. package/dist/kernel/resource.js +97 -0
  194. package/dist/kernel/runtime.d.ts +16 -0
  195. package/dist/kernel/runtime.js +18 -0
  196. package/dist/kernel/safety-scanner.d.ts +38 -0
  197. package/dist/kernel/safety-scanner.js +228 -0
  198. package/dist/kernel/serialize-to-files.d.ts +29 -0
  199. package/dist/kernel/serialize-to-files.js +23 -0
  200. package/dist/kernel/spec-access.d.ts +23 -0
  201. package/dist/kernel/spec-access.js +40 -0
  202. package/dist/kernel/studio_ui.d.ts +88 -0
  203. package/dist/kernel/studio_ui.js +135 -0
  204. package/dist/kernel/templates.d.ts +77 -0
  205. package/dist/kernel/templates.js +116 -0
  206. package/dist/kernel/tool-registry.d.ts +52 -0
  207. package/dist/kernel/tool-registry.js +89 -0
  208. package/dist/kernel/write-pipeline.d.ts +65 -0
  209. package/dist/kernel/write-pipeline.js +271 -0
  210. package/dist/kernel/writer-helpers.d.ts +24 -0
  211. package/dist/kernel/writer-helpers.js +42 -0
  212. package/dist/memory/decay.d.ts +31 -0
  213. package/dist/memory/decay.js +97 -0
  214. package/dist/memory/ecphory.d.ts +25 -0
  215. package/dist/memory/ecphory.js +178 -0
  216. package/dist/memory/encodingContext.d.ts +21 -0
  217. package/dist/memory/encodingContext.js +62 -0
  218. package/dist/memory/index.d.ts +23 -0
  219. package/dist/memory/index.js +23 -0
  220. package/dist/memory/memoryType.d.ts +7 -0
  221. package/dist/memory/memoryType.js +24 -0
  222. package/dist/memory/policy.d.ts +35 -0
  223. package/dist/memory/policy.js +35 -0
  224. package/dist/memory/retrieval.d.ts +21 -0
  225. package/dist/memory/retrieval.js +145 -0
  226. package/dist/testing/index.d.ts +8 -0
  227. package/dist/testing/index.js +8 -0
  228. package/dist/testing/recordSearchConformance.d.ts +43 -0
  229. package/dist/testing/recordSearchConformance.js +147 -0
  230. package/dist/viz/ascii.d.ts +7 -0
  231. package/dist/viz/ascii.js +60 -0
  232. package/dist/viz/health.d.ts +8 -0
  233. package/dist/viz/health.js +161 -0
  234. package/dist/viz/index.d.ts +10 -0
  235. package/dist/viz/index.js +10 -0
  236. package/dist/viz/matrix.d.ts +8 -0
  237. package/dist/viz/matrix.js +84 -0
  238. package/dist/viz/mermaid.d.ts +38 -0
  239. package/dist/viz/mermaid.js +706 -0
  240. package/package.json +82 -0
@@ -0,0 +1,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";