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,271 @@
1
+ import { deriveEventType } from "./events.js";
2
+ import { createAjv } from "./kind_base.js";
3
+ import { SpecValidationError, TenantScope, TenantRequired, TenantNotAllowed, validateTenantSlug, } from "./protocols.js";
4
+ /** Compiled-validator cache for the generic write-path schema check
5
+ * (s-write-path-validation, i-008) — keyed by KindPort identity so each
6
+ * port's schema compiles once per process, not once per write. `null`
7
+ * marks a schema-less (permissive) port. */
8
+ const _writeValidators = new WeakMap();
9
+ export class WritePipeline {
10
+ _k;
11
+ constructor(kernel) {
12
+ this._k = kernel;
13
+ }
14
+ /** Read the write-validation mode knob. `enforce` (default) vetoes an
15
+ * invalid write; `warn` logs and persists; `off` skips the step. Read
16
+ * per-write (not memoized) so tests / operators can flip it live. */
17
+ static _validationMode() {
18
+ const mode = (process.env.DNA_WRITE_VALIDATION ?? "enforce").trim().toLowerCase();
19
+ return mode === "warn" || mode === "off" ? mode : "enforce";
20
+ }
21
+ /**
22
+ * Validate `raw.spec` against the Kind's declared JSON Schema at WRITE
23
+ * time (s-write-path-validation, i-008 — the systemic gap found on the
24
+ * Automation work: the kernel only schema-validated at scan/read via the
25
+ * fail-soft `parse_error` channel, so a shape-broken doc persisted and
26
+ * exploded later, far from the author). Twin of Python
27
+ * `WritePipeline._validate_spec_schema`.
28
+ *
29
+ * Contract:
30
+ * - Kinds without a schema (`schema()` null/empty, or throwing) stay
31
+ * PERMISSIVE — validation is opt-in by data, as always.
32
+ * - `spec_defaults` (descriptor D5) are shallow-merged into the spec
33
+ * BEFORE validating, mirroring `DeclarativeKindPort.parse`.
34
+ * - Runs AFTER the `pre_save` veto hooks (Kind-owned cures mutate
35
+ * ctx.raw first), BEFORE persistence.
36
+ * - Didactic failure (install #26 pattern): names the field, the
37
+ * violation, and points at `dna kind show <Kind>`.
38
+ */
39
+ _validateSpecSchema(scope, kind, name, raw, port) {
40
+ const mode = WritePipeline._validationMode();
41
+ if (mode === "off" || port === null || typeof raw !== "object" || raw === null) {
42
+ return;
43
+ }
44
+ let validate = _writeValidators.get(port);
45
+ if (validate === undefined) {
46
+ let schema = null;
47
+ try {
48
+ schema = port.schema?.() ?? null;
49
+ }
50
+ catch {
51
+ schema = null; // a Kind whose schema errors stays permissive
52
+ }
53
+ validate =
54
+ schema !== null && typeof schema === "object" && Object.keys(schema).length > 0
55
+ ? createAjv().compile(schema)
56
+ : null;
57
+ _writeValidators.set(port, validate);
58
+ }
59
+ if (validate === null)
60
+ return;
61
+ const rawSpec = raw.spec;
62
+ let spec = rawSpec !== null && typeof rawSpec === "object" && !Array.isArray(rawSpec)
63
+ ? rawSpec
64
+ : {};
65
+ // Descriptor D5: defaults fill, spec overrides — exactly what the
66
+ // validating parse sees.
67
+ const defaults = port
68
+ ._specDefaults;
69
+ if (defaults != null && Object.keys(defaults).length > 0) {
70
+ spec = { ...defaults, ...spec };
71
+ }
72
+ if (validate(spec))
73
+ return;
74
+ const first = validate.errors?.[0];
75
+ const path = (first?.instancePath ?? "").replace(/^\//, "").replace(/\//g, ".");
76
+ const loc = path ? `spec.${path}` : "spec";
77
+ const detail = first?.message ?? "spec does not match the Kind's schema";
78
+ const msg = `write vetoed for ${scope}/${kind}/${name}: schema validation failed ` +
79
+ `at ${loc}: ${detail} — see \`dna kind show ${kind}\` for the expected shape`;
80
+ if (mode === "warn") {
81
+ // eslint-disable-next-line no-console
82
+ console.warn(`${msg} (DNA_WRITE_VALIDATION=warn — persisted anyway)`);
83
+ return;
84
+ }
85
+ throw new SpecValidationError(msg);
86
+ }
87
+ /**
88
+ * Reconcile tenant + layer args + Kernel.tenant + KindPort.scope.
89
+ * Returns `[effectiveTenant, residualLayer]`. Back-compat:
90
+ * `layer=("tenant", X)` is rewritten to `tenant=X` with a deprecation
91
+ * console.warn. Validation: TENANTED kind requires a tenant; GLOBAL kind
92
+ * forbids it. Moved verbatim from `Kernel._resolveTenantArg`.
93
+ */
94
+ resolveTenantArg(kind, tenant, layer) {
95
+ const k = this._k;
96
+ let residualLayer = layer;
97
+ let explicitTenant = tenant;
98
+ // Back-compat: layer=("tenant", X) → tenant=X
99
+ if (layer !== undefined && layer[0] === "tenant") {
100
+ // eslint-disable-next-line no-console
101
+ console.warn("[DEPRECATION] layer=('tenant', X) is deprecated — pass tenant=X to writeDocument/deleteDocument instead");
102
+ if (explicitTenant === null || explicitTenant === undefined) {
103
+ explicitTenant = layer[1];
104
+ }
105
+ residualLayer = undefined;
106
+ }
107
+ // Effective tenant: explicit per-call > Kernel.tenant binding
108
+ const effective = explicitTenant !== null && explicitTenant !== undefined
109
+ ? explicitTenant
110
+ : k.tenant;
111
+ validateTenantSlug(effective);
112
+ // Validate against KindPort.scope when EXPLICITLY declared.
113
+ const scopeDecl = k._kindScope(kind);
114
+ if (scopeDecl === TenantScope.TENANTED && effective === null) {
115
+ throw new TenantRequired(`Kind ${JSON.stringify(kind)} is TENANTED — pass tenant=<slug> to ` +
116
+ `writeDocument() or bind one via new Kernel({tenant: ...}) / ` +
117
+ `kernel.withTenant(...)`);
118
+ }
119
+ if (scopeDecl === TenantScope.GLOBAL && effective !== null) {
120
+ throw new TenantNotAllowed(`Kind ${JSON.stringify(kind)} is GLOBAL — must NOT pass a tenant. ` +
121
+ `Use the unbound kernel (new Kernel() with no tenant) or ` +
122
+ `kernel.withTenant(null) for global writes.`);
123
+ }
124
+ return [effective, residualLayer];
125
+ }
126
+ /** Real writeDocument body — the facade (`Kernel.writeDocument`) is a thin
127
+ * delegator. Persists through the writable source with the `pre_save` veto
128
+ * gate + `post_save` emission. */
129
+ async write(scope, kind, name, raw, options) {
130
+ const k = this._k;
131
+ const src = k._requireWritableSource();
132
+ // Resolve tenant + validate against KindPort.scope (back-compat for
133
+ // layer=("tenant", X) → tenant=X with deprecation warning)
134
+ const [effectiveTenant, residualLayer] = this.resolveTenantArg(kind, options?.tenant, options?.layer);
135
+ const policyCheckLayer = effectiveTenant !== null ? ["tenant", effectiveTenant] : residualLayer;
136
+ if (policyCheckLayer !== undefined) {
137
+ await k._checkLayerPolicyAsync(scope, kind, name, raw, policyCheckLayer);
138
+ }
139
+ // pre_save veto hooks (s-write-path-despecialize) — Kind-specific write
140
+ // rules (platform-agent fork guard, Kind-Writer contract, ...) live in
141
+ // the extension that OWNS the Kind and register here via
142
+ // `kernel.onVeto("pre_save", fn, {priority})`. A throw vetoes the
143
+ // write; listeners may mutate `ctx.raw` in place. Fires regardless of
144
+ // `skipHooks` — these are integrity gates, not notifications
145
+ // (`skipHooks` only silences post_save).
146
+ if (k.hooks.hasVeto("pre_save")) {
147
+ await k.hooks.emitVeto("pre_save", {
148
+ scope, kind, name, raw,
149
+ tenant: effectiveTenant,
150
+ ...(policyCheckLayer !== undefined ? { layer: policyCheckLayer } : {}),
151
+ kernel: k,
152
+ });
153
+ }
154
+ // --- generic spec↔schema validation (s-write-path-validation, i-008) ---
155
+ // AFTER the veto hooks (Kind-owned cures mutate ctx.raw first), BEFORE
156
+ // persistence: what gets validated is the exact shape that would be
157
+ // saved. i-195: resolve the port by the doc's own apiVersion.
158
+ this._validateSpecSchema(scope, kind, name, raw, k.kindPortFor(kind, raw?.apiVersion));
159
+ const version = await src.saveDocument(scope, kind, name, raw, {
160
+ author: options?.author,
161
+ tenant: effectiveTenant ?? undefined,
162
+ layer: residualLayer,
163
+ });
164
+ // Cache invalidation only for true base writes (no tenant + no overlay)
165
+ if (effectiveTenant === null && residualLayer === undefined) {
166
+ // Base instance cache lives in Python; TS kernel doesn't cache here.
167
+ }
168
+ if (!options?.skipHooks) {
169
+ // Hook layer keeps legacy shape for back-compat with subscribers.
170
+ const hookLayer = policyCheckLayer;
171
+ await this.emitPostSave(scope, kind, name, raw, options?.author, false, hookLayer);
172
+ }
173
+ return version;
174
+ }
175
+ /** Real deleteDocument body — the facade is a thin delegator. NOTE: deletes
176
+ * have NO pre_save veto gate (only writes do). */
177
+ async delete(scope, kind, name, options) {
178
+ const k = this._k;
179
+ const src = k._requireWritableSource();
180
+ // Resolve tenant + validate against KindPort.scope.
181
+ const [effectiveTenant, residualLayer] = this.resolveTenantArg(kind, options?.tenant, options?.layer);
182
+ const policyCheckLayer = effectiveTenant !== null ? ["tenant", effectiveTenant] : residualLayer;
183
+ if (policyCheckLayer !== undefined) {
184
+ await k._checkLayerPolicyAsync(scope, kind, name, null, policyCheckLayer);
185
+ }
186
+ await src.deleteDocument(scope, kind, name, {
187
+ author: options?.author,
188
+ tenant: effectiveTenant ?? undefined,
189
+ layer: residualLayer,
190
+ });
191
+ if (!options?.skipHooks) {
192
+ await this.emitPostDelete(scope, kind, name, policyCheckLayer);
193
+ }
194
+ }
195
+ /** Emit the post_save hook (shared by the new and legacy write paths).
196
+ * Async so async listeners (event bus publishers etc.) can await I/O. */
197
+ async emitPostSave(scope, kind, name, raw, author, isUpdate, layer) {
198
+ const k = this._k;
199
+ if (!k.hooks.has("post_save"))
200
+ return;
201
+ await k.hooks.emitAsync("post_save", {
202
+ scope,
203
+ kind,
204
+ name,
205
+ data: {
206
+ event_type: deriveEventType(kind, isUpdate ?? false),
207
+ author: author ?? "sdk",
208
+ is_update: isUpdate ?? false,
209
+ spec: raw,
210
+ },
211
+ ...(layer !== undefined ? { layer } : {}),
212
+ });
213
+ }
214
+ /** Emit the post_delete hook (only from the new delegation path). */
215
+ async emitPostDelete(scope, kind, name, layer) {
216
+ const k = this._k;
217
+ if (!k.hooks.has("post_delete"))
218
+ return;
219
+ await k.hooks.emitAsync("post_delete", {
220
+ scope,
221
+ kind,
222
+ name,
223
+ data: {},
224
+ ...(layer !== undefined ? { layer } : {}),
225
+ });
226
+ }
227
+ /** Validate a SINGLE Kind-Writer target's slot↔schema contract. Moved from
228
+ * `Kernel._validateOneKindWriterEntry`. */
229
+ validateOneKindWriterEntry(target, creativeSlots, systemSlots) {
230
+ const port = this._k.kindPortFor(target);
231
+ const schema = port?.schema?.() ?? null;
232
+ if (schema === null || typeof schema !== "object") {
233
+ throw new Error(`Kind-Writer Agent writes_kind=${JSON.stringify(target)} has no ` +
234
+ `schema (Kind is unknown or schema-less); a Kind-Writer must target a ` +
235
+ `schema-bearing Kind.`);
236
+ }
237
+ const properties = schema.properties ?? {};
238
+ for (const slot of creativeSlots) {
239
+ if (!(slot in properties)) {
240
+ throw new Error(`Kind-Writer Agent creative_slot ${JSON.stringify(slot)} is not ` +
241
+ `a property of Kind ${JSON.stringify(target)}'s schema.`);
242
+ }
243
+ }
244
+ const covered = new Set([...creativeSlots, ...Object.keys(systemSlots ?? {})]);
245
+ const required = schema.required ?? [];
246
+ for (const req of required) {
247
+ if (!covered.has(req)) {
248
+ throw new Error(`Kind-Writer Agent: required field ${JSON.stringify(req)} of ` +
249
+ `Kind ${JSON.stringify(target)} is unmapped — cover it via ` +
250
+ `creative_slots or system_slots.`);
251
+ }
252
+ }
253
+ }
254
+ /** Validate a Kind-Writer Agent's slot↔schema contract. Fired by the
255
+ * Helix extension's Kind-Writer `pre_save` guard via the
256
+ * `kernel._validateKindWriter` shim. Moved from `Kernel._validateKindWriter`. */
257
+ validateKindWriter(spec) {
258
+ // Multi-Kind (writes_kinds): validate EACH {kind: {creative_slots,
259
+ // system_slots}} entry the same way. An agent uses EITHER writes_kind
260
+ // (single) OR writes_kinds (multi). Twin of Python validate_kind_writer.
261
+ const writesKinds = spec.writes_kinds;
262
+ if (writesKinds && Object.keys(writesKinds).length > 0) {
263
+ for (const [target, entry] of Object.entries(writesKinds)) {
264
+ const e = entry ?? {};
265
+ this.validateOneKindWriterEntry(target, e.creative_slots ?? [], e.system_slots ?? {});
266
+ }
267
+ return;
268
+ }
269
+ this.validateOneKindWriterEntry(spec.writes_kind, spec.creative_slots ?? [], spec.system_slots ?? {});
270
+ }
271
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shared helpers for KindPort writers — TS twin of
3
+ * `dna.kernel.writer_helpers` (Python).
4
+ *
5
+ * L3 (s-writer-binary-entries 2026-05-25): user-content bundle Kinds
6
+ * (HtmlArtifact, ImagePrompt, Pictogram, Asset) share the
7
+ * convention `spec.source_files: Record<string, string | Uint8Array>`
8
+ * which becomes sibling bundle entries alongside the writer's primary
9
+ * marker. Centralising the pop+convert+write logic keeps writers honest
10
+ * and lets adapter-level enhancements land in one place.
11
+ */
12
+ import type { BundleHandle } from "./bundle-handle.js";
13
+ import type { SerializedFile } from "./protocols.js";
14
+ /**
15
+ * Pop `spec.source_files` from spec (mutates) and convert each file
16
+ * to a bundle-entry dict. Returns SerializedFile[] using `content`
17
+ * for strings and `contentBytes` for binary.
18
+ */
19
+ export declare function popSourceFilesAsEntries(spec: Record<string, unknown>, kindName: string): SerializedFile[];
20
+ /**
21
+ * Write a list of bundle entries (mixed text/binary) to a BundleHandle.
22
+ * Dispatches each entry to bundle.writeText or bundle.writeBytes.
23
+ */
24
+ export declare function writeEntriesToHandle(bundle: BundleHandle, entries: SerializedFile[]): Promise<void>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Pop `spec.source_files` from spec (mutates) and convert each file
3
+ * to a bundle-entry dict. Returns SerializedFile[] using `content`
4
+ * for strings and `contentBytes` for binary.
5
+ */
6
+ export function popSourceFilesAsEntries(spec, kindName) {
7
+ const extra = spec.source_files;
8
+ delete spec.source_files;
9
+ if (extra == null)
10
+ return [];
11
+ if (typeof extra !== "object" || Array.isArray(extra)) {
12
+ throw new TypeError(`${kindName}.spec.source_files must be a Record<string, string|Uint8Array>, got ${typeof extra}`);
13
+ }
14
+ const out = [];
15
+ for (const [relPath, payload] of Object.entries(extra)) {
16
+ if (payload instanceof Uint8Array) {
17
+ out.push({ relativePath: relPath, contentBytes: payload });
18
+ }
19
+ else if (typeof payload === "string") {
20
+ out.push({ relativePath: relPath, content: payload });
21
+ }
22
+ else {
23
+ throw new TypeError(`${kindName}.spec.source_files[${JSON.stringify(relPath)}] must be ` +
24
+ `string or Uint8Array, got ${typeof payload}`);
25
+ }
26
+ }
27
+ return out;
28
+ }
29
+ /**
30
+ * Write a list of bundle entries (mixed text/binary) to a BundleHandle.
31
+ * Dispatches each entry to bundle.writeText or bundle.writeBytes.
32
+ */
33
+ export async function writeEntriesToHandle(bundle, entries) {
34
+ for (const f of entries) {
35
+ if (f.contentBytes !== undefined) {
36
+ await bundle.writeBytes(f.relativePath, f.contentBytes);
37
+ }
38
+ else if (f.content !== undefined) {
39
+ await bundle.writeText(f.relativePath, f.content);
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Ebbinghaus forgetting curve + bi-temporal validity — pure math (TS twin of
3
+ * `dna/memory/decay.py`). R(t) = e^(-t/S). Bit-identical arithmetic to Python.
4
+ *
5
+ * s-memory-verbs (2026-07-09).
6
+ */
7
+ import { DecayPolicy } from "./policy.js";
8
+ type Spec = Record<string, unknown> | null | undefined;
9
+ /** `confidence_score` → number. Numeric passthrough or tier (faint=1, firm=3, burning=5). */
10
+ export declare function confidenceScoreNumeric(spec: Spec, def?: number): number;
11
+ /** Resolve stability (days) from a memory spec — explicit → tier → legacy numeric → default. */
12
+ export declare function stabilityFromSpec(spec: Spec, policy?: DecayPolicy): number;
13
+ /** Days elapsed since `ts` (ISO). null on unparseable input. `now` in epoch ms. */
14
+ export declare function daysSince(ts: unknown, now?: number): number | null;
15
+ /** R(t) = e^(-t/S). 1.0 when never recalled (`daysSinceRecall` null/≤0). */
16
+ export declare function ebbinghausRetention(stabilityDays: number, daysSinceRecall: number | null): number;
17
+ /** Spacing-effect bump: S_new = S_old × (1 + 0.5 × R(t)); capped. */
18
+ export declare function recallBump(currentStability: number, daysSinceRecall: number | null, policy?: DecayPolicy): number;
19
+ /** Multiply a ranking score by current retention. Returns `[adjusted, retention]`. */
20
+ export declare function decayAdjustedScore(baseScore: number, spec: Spec, opts?: {
21
+ floor?: number;
22
+ now?: number;
23
+ policy?: DecayPolicy;
24
+ }): [number, number];
25
+ /**
26
+ * Bi-temporal filter (Zep valid_from/valid_to). True when the memory is still
27
+ * current — `valid_to` unset OR in the future. A memory invalidated in the past
28
+ * never surfaces. Unparseable `valid_to` fails OPEN (never hide on a bad stamp).
29
+ */
30
+ export declare function currentlyValid(validTo: unknown, now?: number): boolean;
31
+ export {};
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Ebbinghaus forgetting curve + bi-temporal validity — pure math (TS twin of
3
+ * `dna/memory/decay.py`). R(t) = e^(-t/S). Bit-identical arithmetic to Python.
4
+ *
5
+ * s-memory-verbs (2026-07-09).
6
+ */
7
+ import { DEFAULT_DECAY_POLICY, decayTiers } from "./policy.js";
8
+ function parseIso(ts) {
9
+ if (typeof ts !== "string" || !ts)
10
+ return null;
11
+ const ms = Date.parse(ts.replace("Z", "+00:00"));
12
+ return Number.isNaN(ms) ? null : ms;
13
+ }
14
+ /** `confidence_score` → number. Numeric passthrough or tier (faint=1, firm=3, burning=5). */
15
+ export function confidenceScoreNumeric(spec, def = 1.0) {
16
+ if (!spec || typeof spec !== "object")
17
+ return def;
18
+ const raw = spec.confidence_score;
19
+ if (raw == null)
20
+ return def;
21
+ if (typeof raw === "number")
22
+ return raw;
23
+ if (typeof raw === "string") {
24
+ const map = { faint: 1.0, firm: 3.0, burning: 5.0 };
25
+ return map[raw.toLowerCase()] ?? def;
26
+ }
27
+ return def;
28
+ }
29
+ /** Resolve stability (days) from a memory spec — explicit → tier → legacy numeric → default. */
30
+ export function stabilityFromSpec(spec, policy = DEFAULT_DECAY_POLICY) {
31
+ const tiers = decayTiers(policy);
32
+ if (!spec || typeof spec !== "object" || Object.keys(spec).length === 0) {
33
+ return policy.defaultStabilityDays;
34
+ }
35
+ const s = spec;
36
+ const explicit = s.engram_stability_days;
37
+ if (typeof explicit === "number" && explicit > 0)
38
+ return explicit;
39
+ const strength = s.confidence_score;
40
+ if (typeof strength === "string") {
41
+ return tiers[strength.toLowerCase()] ?? policy.defaultStabilityDays;
42
+ }
43
+ if (typeof strength === "number") {
44
+ const v = strength;
45
+ if (v <= 1.0)
46
+ return tiers.faint;
47
+ if (v <= 5.0)
48
+ return tiers.faint + ((v - 1.0) / 4.0) * (tiers.firm - tiers.faint);
49
+ if (v <= 10.0)
50
+ return tiers.firm + ((v - 5.0) / 5.0) * (tiers.burning - tiers.firm);
51
+ return tiers.burning;
52
+ }
53
+ return policy.defaultStabilityDays;
54
+ }
55
+ /** Days elapsed since `ts` (ISO). null on unparseable input. `now` in epoch ms. */
56
+ export function daysSince(ts, now = Date.now()) {
57
+ const ms = parseIso(ts);
58
+ if (ms === null)
59
+ return null;
60
+ return Math.max((now - ms) / 86_400_000, 0.0);
61
+ }
62
+ /** R(t) = e^(-t/S). 1.0 when never recalled (`daysSinceRecall` null/≤0). */
63
+ export function ebbinghausRetention(stabilityDays, daysSinceRecall) {
64
+ if (daysSinceRecall === null || daysSinceRecall <= 0)
65
+ return 1.0;
66
+ const s = Math.max(0.1, stabilityDays);
67
+ return Math.exp(-daysSinceRecall / s);
68
+ }
69
+ /** Spacing-effect bump: S_new = S_old × (1 + 0.5 × R(t)); capped. */
70
+ export function recallBump(currentStability, daysSinceRecall, policy = DEFAULT_DECAY_POLICY) {
71
+ const s = Math.max(0.1, currentStability);
72
+ const r = ebbinghausRetention(s, daysSinceRecall);
73
+ return Math.min(s * (1.0 + 0.5 * r), policy.maxStabilityDays);
74
+ }
75
+ /** Multiply a ranking score by current retention. Returns `[adjusted, retention]`. */
76
+ export function decayAdjustedScore(baseScore, spec, opts = {}) {
77
+ const { floor = 0.05, now = Date.now(), policy = DEFAULT_DECAY_POLICY } = opts;
78
+ const s = stabilityFromSpec(spec, policy);
79
+ const last = spec?.last_surfaced
80
+ ?? spec?.last_recall_at;
81
+ const days = daysSince(last, now);
82
+ const retention = ebbinghausRetention(s, days);
83
+ return [baseScore * Math.max(floor, retention), retention];
84
+ }
85
+ /**
86
+ * Bi-temporal filter (Zep valid_from/valid_to). True when the memory is still
87
+ * current — `valid_to` unset OR in the future. A memory invalidated in the past
88
+ * never surfaces. Unparseable `valid_to` fails OPEN (never hide on a bad stamp).
89
+ */
90
+ export function currentlyValid(validTo, now = Date.now()) {
91
+ if (!validTo)
92
+ return true;
93
+ const ms = parseIso(validTo);
94
+ if (ms === null)
95
+ return true;
96
+ return ms > now;
97
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Deterministic ecphory — Semon's Law of Ecphory as a pure function (TS twin of
3
+ * `dna/memory/ecphory.py`). Partial-match between a cue context and an engram's
4
+ * encoding_context, weighted overlap, saturation/novelty/recency adjustments,
5
+ * homophony propagation. No LLM, no kernel — the verb layer owns persistence.
6
+ *
7
+ * s-memory-verbs (2026-07-09).
8
+ */
9
+ import { RecallPolicy } from "./policy.js";
10
+ export interface EngramRef {
11
+ name: string;
12
+ spec: Record<string, unknown>;
13
+ }
14
+ export interface EcphoryScore {
15
+ engram: EngramRef;
16
+ score: number;
17
+ matchedDims: string[];
18
+ reasonTags: string[];
19
+ }
20
+ /** Partial-match score between an engram's encoding_context and a cue. */
21
+ export declare function scoreEngram(engram: EngramRef, cueCtx: Record<string, unknown>, semanticScores?: Record<string, number>, policy?: RecallPolicy): EcphoryScore;
22
+ /** Saturation / novelty / recency / high-fidelity modifiers. */
23
+ export declare function applySemonAdjustments(s: EcphoryScore, now: number, policy?: RecallPolicy): EcphoryScore;
24
+ /** Surface homophonic neighbors via `homophonic_links`. Score = direct × 0.7 × resonance. */
25
+ export declare function expandHomophony(directs: EcphoryScore[], engramByName: Map<string, EngramRef>): EcphoryScore[];
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Deterministic ecphory — Semon's Law of Ecphory as a pure function (TS twin of
3
+ * `dna/memory/ecphory.py`). Partial-match between a cue context and an engram's
4
+ * encoding_context, weighted overlap, saturation/novelty/recency adjustments,
5
+ * homophony propagation. No LLM, no kernel — the verb layer owns persistence.
6
+ *
7
+ * s-memory-verbs (2026-07-09).
8
+ */
9
+ import { DEFAULT_RECALL_POLICY } from "./policy.js";
10
+ function tokenize(s) {
11
+ return new Set(s.toLowerCase().split(/[\s/_-]+/).filter(Boolean));
12
+ }
13
+ function withinWindow(atIso, now, hours) {
14
+ if (typeof atIso !== "string" || !atIso)
15
+ return false;
16
+ const ms = Date.parse(atIso.replace("Z", "+00:00"));
17
+ if (Number.isNaN(ms))
18
+ return false;
19
+ return now - ms <= hours * 3_600_000;
20
+ }
21
+ /** Partial-match score between an engram's encoding_context and a cue. */
22
+ export function scoreEngram(engram, cueCtx, semanticScores, policy = DEFAULT_RECALL_POLICY) {
23
+ const spec = engram.spec;
24
+ const ec = spec.encoding_context ?? {};
25
+ const matched = [];
26
+ let score = 0.0;
27
+ const ecArea = String(ec.area ?? spec.area ?? "").trim();
28
+ const cueArea = String(cueCtx.area_inferred ?? cueCtx.query ?? "").trim();
29
+ const cueQ = cueArea.toLowerCase();
30
+ // Path 1: area token overlap.
31
+ let areaScore = 0.0;
32
+ let areaLabel = "";
33
+ if (ecArea && cueArea) {
34
+ const ecTokens = tokenize(ecArea);
35
+ const cueTokens = tokenize(cueArea);
36
+ if (ecTokens.size && cueTokens.size) {
37
+ const common = new Set([...ecTokens].filter((t) => cueTokens.has(t)));
38
+ const union = new Set([...ecTokens, ...cueTokens]);
39
+ const isSuperset = common.size === ecTokens.size || common.size === cueTokens.size;
40
+ if (isSuperset) {
41
+ areaScore = policy.contentWeight;
42
+ areaLabel = "area";
43
+ }
44
+ else if (common.size) {
45
+ const jaccard = common.size / union.size;
46
+ areaScore = policy.contentWeight * jaccard;
47
+ areaLabel = `area~${jaccard.toFixed(2)}`;
48
+ }
49
+ }
50
+ }
51
+ // Path 2: query phrase in summary/title/body.
52
+ let summaryScore = 0.0;
53
+ let summaryLabel = "";
54
+ if (cueQ && cueQ.length >= 3) {
55
+ const haystack = ["summary", "title", "body"]
56
+ .map((f) => String(spec[f] ?? ""))
57
+ .join(" ")
58
+ .toLowerCase();
59
+ if (haystack && haystack.includes(cueQ)) {
60
+ summaryScore = policy.contentWeight;
61
+ summaryLabel = "summary";
62
+ }
63
+ else if (haystack) {
64
+ const qTokens = tokenize(cueQ);
65
+ const contentTokens = tokenize(haystack);
66
+ if (qTokens.size && [...qTokens].every((t) => contentTokens.has(t))) {
67
+ summaryScore = policy.summaryPartialWeight;
68
+ summaryLabel = "summary-tokens";
69
+ }
70
+ }
71
+ }
72
+ // Path 3: embedding cosine (only when caller feeds semanticScores).
73
+ let semanticScore = 0.0;
74
+ let semanticLabel = "";
75
+ if (semanticScores) {
76
+ const cos = Number(semanticScores[engram.name] ?? 0.0);
77
+ if (cos > 0.0) {
78
+ semanticScore = policy.cosineWeight * cos;
79
+ semanticLabel = `semantic~${cos.toFixed(2)}`;
80
+ }
81
+ }
82
+ const primary = Math.max(areaScore, summaryScore, semanticScore);
83
+ if (primary > 0) {
84
+ score += primary;
85
+ if (primary === areaScore && areaLabel)
86
+ matched.push(areaLabel);
87
+ else if (primary === summaryScore && summaryLabel)
88
+ matched.push(summaryLabel);
89
+ else if (semanticLabel)
90
+ matched.push(semanticLabel);
91
+ }
92
+ // co_topics jaccard.
93
+ const ecTopics = new Set(ec.co_topics ?? []);
94
+ const cueTopics = new Set(cueCtx.co_topics ?? []);
95
+ if (ecTopics.size && cueTopics.size) {
96
+ const overlap = new Set([...ecTopics].filter((t) => cueTopics.has(t)));
97
+ const union = new Set([...ecTopics, ...cueTopics]);
98
+ if (union.size) {
99
+ score += policy.coTopicsWeight * (overlap.size / union.size);
100
+ if (overlap.size)
101
+ matched.push(`co_topics(${overlap.size})`);
102
+ }
103
+ }
104
+ // source_refs distinctiveness.
105
+ const ecRefs = new Set((ec.source_refs ?? spec.source_refs) ?? []);
106
+ const cueRefs = new Set(cueCtx.source_refs ?? []);
107
+ if (ecRefs.size && cueRefs.size && [...ecRefs].some((r) => cueRefs.has(r))) {
108
+ score += policy.sourceRefsWeight;
109
+ matched.push("source_refs");
110
+ }
111
+ // affect mood (marginal boost).
112
+ const ecAffect = ec.affect ?? spec.affect;
113
+ const cueAffect = cueCtx.affect_mood;
114
+ if (ecAffect && cueAffect && ecAffect !== "neutral" && cueAffect !== "neutral" && ecAffect === cueAffect) {
115
+ score += policy.affectWeight;
116
+ matched.push("affect");
117
+ }
118
+ // time_of_day (perceptual, marginal).
119
+ if (ec.time_of_day && cueCtx.time_of_day && ec.time_of_day === cueCtx.time_of_day) {
120
+ score += policy.timeWeight;
121
+ matched.push("time");
122
+ }
123
+ return { engram, score, matchedDims: matched, reasonTags: [] };
124
+ }
125
+ /** Saturation / novelty / recency / high-fidelity modifiers. */
126
+ export function applySemonAdjustments(s, now, policy = DEFAULT_RECALL_POLICY) {
127
+ const spec = s.engram.spec;
128
+ const cues = spec.cues_history ?? [];
129
+ const recent24h = cues.filter((c) => c && typeof c === "object" && withinWindow(c.at, now, 24)).length;
130
+ if (recent24h >= policy.saturationThreshold) {
131
+ s.score *= policy.saturationDecay;
132
+ s.reasonTags.push("saturation_decay");
133
+ }
134
+ if (cues.length === 0) {
135
+ s.score += policy.noveltyBoost;
136
+ s.reasonTags.push("novelty_boost");
137
+ }
138
+ if (withinWindow(spec.created_at, now, 24)) {
139
+ s.score += policy.recencyBoost;
140
+ s.reasonTags.push("recency_boost");
141
+ }
142
+ const strength = spec.confidence_score;
143
+ if (typeof strength === "number" && strength > 1.1) {
144
+ s.score += 0.05;
145
+ s.reasonTags.push("high_fidelity");
146
+ }
147
+ return s;
148
+ }
149
+ /** Surface homophonic neighbors via `homophonic_links`. Score = direct × 0.7 × resonance. */
150
+ export function expandHomophony(directs, engramByName) {
151
+ const homo = new Map();
152
+ for (const d of directs) {
153
+ const links = d.engram.spec.homophonic_links ?? [];
154
+ for (const link of links) {
155
+ if (!link || typeof link !== "object")
156
+ continue;
157
+ const l = link;
158
+ const targetName = String(l.target_name ?? l.engram_name ?? "").trim();
159
+ if (!targetName || targetName === d.engram.name)
160
+ continue;
161
+ const target = engramByName.get(targetName);
162
+ if (!target)
163
+ continue;
164
+ const resonance = Number(l.resonance_score ?? 0.5);
165
+ const newScore = d.score * 0.7 * resonance;
166
+ const existing = homo.get(targetName);
167
+ if (!existing || newScore > existing.score) {
168
+ homo.set(targetName, {
169
+ engram: target,
170
+ score: newScore,
171
+ matchedDims: ["homophonic"],
172
+ reasonTags: [`via=${d.engram.name}`, `basis=${l.basis ?? "co-area"}`],
173
+ });
174
+ }
175
+ }
176
+ }
177
+ return [...homo.values()].sort((a, b) => b.score - a.score);
178
+ }