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