Compare commits

..

28 Commits

Author SHA1 Message Date
bigmerge 1dc49b1923 Fix engram search latency: pin embed model, cache query embeddings, bound activate BFS
El SDK CI - dev / build-and-test (pull_request) Failing after 10m1s
Pins the Ollama embed model resident (keep_alive:-1) to avoid multi-second
cold reloads whenever a larger generation model evicts it under unified-
memory pressure (measured cold reload up to ~2.2s vs ~0.02-0.05s warm).

Adds a direct-mapped query-embedding cache (FNV-1a keyed, full strcmp to
reject collisions) so a repeated query costs zero Ollama round-trips —
directly serves the curiosity loop, which reseeds the same query terms
repeatedly.

Replaces engram_activate's unbounded FIFO frontier BFS with a beam-capped,
level-synchronous BFS (default beam 128, tunable via
ENGRAM_ACTIVATE_BEAM) to bound per-hop hub-node explosion that could
previously reach multi-second/crash territory at depth 2-3.

Excludes an inert engram_prune_telemetry build-enabler stub that was only
needed to link this checkout against a newer integration branch — not part
of the fix.
2026-08-15 14:25:12 -05:00
will.anderson d71fc4c1c0 Merge pull request 'promote stage -> main: reconciled el runtime (engram search + natives + durable truncation fix + Windows port)' (#82) from stage into main
El SDK Release / build-and-release (push) Successful in 8m31s
El SDK CI - dev / build-and-test (pull_request) Successful in 8m41s
2026-07-22 21:44:01 +00:00
will.anderson a118d19393 Merge pull request 'promote dev -> stage: el cluster (#66 engram + #79 truncation fix + release-runtime Windows port)' (#81) from dev into stage
El SDK CI - stage / build-and-test (push) Successful in 7m58s
El SDK Release / build-and-release (pull_request) Successful in 4m16s
2026-07-22 21:20:17 +00:00
will.anderson c6aa1e5c53 Merge pull request 'Land el cluster: #66 engram search + natives, #79 truncation fix, + release-runtime Windows port (reconciled)' (#80) from reconcile/el-cluster-windows-runtime into dev
El SDK CI - dev / build-and-test (push) Successful in 8m3s
El SDK CI - stage / build-and-test (pull_request) Successful in 4m25s
Land el cluster (#66 + #79 + release-runtime Windows-port reconciliation) into dev
2026-07-22 21:06:36 +00:00
will.anderson ff577391f2 reconcile(release-runtime): Windows-port + complete v1.0.0 release runtime so the desktop soul cross-compiles
El SDK CI - dev / build-and-test (pull_request) Successful in 7m7s
The desktop soul (neuron/dist) compiles against the v1.0.0-20260501 release
runtime. After #66 landed the engram natives (tokenized/ranked search,
engram_prune_telemetry) and #79 the durable truncation fix into this runtime,
two gaps remained before it could cross-compile the Windows brain:

1. Windows OS boundary: the release runtime had no Win32 path. Ported the same
   _WIN32-guarded shim the mainline runtime carries (#69): #ifdef _WIN32 ->
   el_platform_win.h (winsock/dlsym/popen + WSAStartup ctor), SOCKET fd guards
   and el_closesocket() at every socket site, CreateProcessA for exec_bg, the
   tm_zone/mingw guard, an el_setsockopt optval wrapper (GCC14), and curl-less
   libcurl stubs. Every change is _WIN32/HAVE_CURL-gated — the POSIX build is
   byte-identical (gcc -fsyntax-only clean; native behaviour unchanged).

2. Header exports: the release el_runtime.h omitted symbols the soul dist calls
   that are defined in this runtime's .c — the http_handler_fn/http_handler4_fn
   typedefs and el_arena_push/pop, engram_prune_telemetry, engram_get_node_by_label.
   Declaration-only, POSIX-neutral; fixes implicit-declaration/unknown-type
   errors under the C11 mingw build.

Result: x86_64-w64-mingw32-gcc compiles el_runtime.c + all 48 soul modules
clean; POSIX gcc -fsyntax-only clean. This is the Windows-port PR the runtime
needed on main (the release-runtime counterpart to #69), landed via stage.
2026-07-22 15:56:08 -05:00
will.anderson ee0d5f9b97 Merge #79: durable HTTP response-truncation fix, both runtimes (via stage) 2026-07-22 15:46:02 -05:00
will.anderson 391bd818ea Merge #66: tokenized+ranked engram lexical search + engram natives (via stage) 2026-07-22 15:45:54 -05:00
will.anderson 43636aed99 runtime: pair fs_read length hint with its buffer in BOTH runtimes — kill response truncation for good
El SDK Release / build-and-release (pull_request) Failing after 7s
The binary-safe fs_read length (_tl_fs_read_len) was consumed by the HTTP
response path for ANY body, even when a handler wrapped a smaller file into a
larger reply. Content-Length then lied AND the send stopped short: the
safety-contact (988) routes returned 178 of 208/218 bytes, cut mid-'set_at' —
unparseable JSON. The desktop app read that as failure. On Windows the shipped
brain is an OLD build without even the per-handler workaround, so EVERY reply
truncated: the app can't read confirmations and refuses the new user.

Durable fix: pair the length hint with the exact buffer pointer it describes
(_tl_fs_read_buf). Apply the raw byte count ONLY when the response IS that
buffer (binary file serving stays correct); every wrapped/enveloped/derived
body is measured with strlen. Reset both at request start and in fs_read /
json_get_raw. This also closes the stale-hint heap over-read (a length larger
than a later body would read past it out the socket) that a plain max() leaves
open — so this class of bug dies on every platform, not just where a handler
happened to be patched.

Applied identically to the mainline runtime (lang/el-compiler/runtime) AND the
frozen release runtime (lang/releases/v1.0.0-20260501) the desktop souls
compile against — the release copy still carried the raw leak, which is why the
Windows brain kept truncating. Same proven approach as PR #78 (Tim Lingo),
extended to cover the release runtime and rebased onto current main.

Both runtimes: gcc -fsyntax-only clean.
2026-07-22 15:04:25 -05:00
will.anderson 2baa0b9a41 Merge pull request 'release: promote stage -> main (ci publish hardening for sdk-release)' (#77) from stage into main
El SDK Release / build-and-release (push) Successful in 7m55s
2026-07-15 21:21:28 +00:00
will.anderson 6a8b2461cd Merge pull request 'release: promote dev -> stage (ci publish hardening for stage/main)' (#76) from dev into stage
El SDK CI - stage / build-and-test (push) Successful in 8m19s
El SDK Release / build-and-release (pull_request) Failing after 13m1s
2026-07-15 21:16:11 +00:00
will.anderson bcb356fe69 Merge pull request 'ci(stage,main): decouple ci-base rebuild, make SDK publish fail loudly' (#75) from hotfix/ci-stage-main-publish-hardening into dev
El SDK CI - stage / build-and-test (pull_request) Successful in 4m27s
El SDK CI - dev / build-and-test (push) Failing after 14m3s
2026-07-15 21:15:27 +00:00
will.anderson dd7827059a ci(stage,main): decouple ci-base rebuild, make SDK publish fail loudly
El SDK CI - dev / build-and-test (pull_request) Failing after 14m30s
Mirror the PR #72 fix (applied to ci-dev.yaml) onto ci-stage.yaml and
sdk-release.yaml. The stage and prod release jobs reported FAILURE even
when the el-runtime-c/-h publish SUCCEEDED, because the ancillary ci-base
Docker rebuild (a CI-cache optimization on the fragile host-mode GCE
runner) reddened the whole job.

- Rebuild ci-base step: continue-on-error: true — never blocks/reddens
  the job; the SDK publish is the deliverable.
- Publish step: set -euo pipefail + empty-key guard + active-account echo
  so a real publish failure still fails loud and is diagnosable.
2026-07-15 16:14:50 -05:00
will.anderson 208e36c899 Merge pull request 'release: promote stage -> main (tokenized search, get_node_by_label, epm fix, win portability)' (#74) from stage into main
El SDK Release / build-and-release (push) Successful in 8m23s
2026-07-15 18:24:39 +00:00
will.anderson b97ce74d1f Merge pull request 'release: promote dev -> stage (tokenized search, get_node_by_label, epm fix)' (#73) from dev into stage
El SDK CI - stage / build-and-test (push) Failing after 8m45s
El SDK Release / build-and-release (pull_request) Successful in 4m1s
2026-07-15 17:20:11 +00:00
will.anderson 155a449c4e Merge pull request 'ci(dev): make SDK publish fail loudly, decouple ci-base rebuild' (#72) from hotfix/ci-dev-publish-hardening into dev
El SDK CI - dev / build-and-test (push) Successful in 8m56s
El SDK CI - stage / build-and-test (pull_request) Successful in 4m10s
2026-07-15 16:34:14 +00:00
will.anderson 4696fd6833 ci(dev): make SDK publish fail loudly, decouple ci-base rebuild
El SDK CI - dev / build-and-test (pull_request) Successful in 8m51s
The dev push build went green-then-red while nothing published: the
Publish step had no set -e, so an auth/upload failure exited 0 (silent
no-publish), while the ci-base rebuild (set -euo pipefail + Docker on the
host-mode runner) hard-failed the job. Add set -euo pipefail + an empty-key
guard + active-account echo to the Publish step so failures surface with a
retrievable log, and mark the ci-base cache rebuild continue-on-error so
the fragile Docker step can never block the actual SDK artifact publish.
2026-07-15 11:33:37 -05:00
will.anderson 581a351fb1 Merge pull request 'integrate: stack PRs #65–#69 (elc OOM guard, tokenized+semantic engram search, get_node_by_label, win portability) for green CI' (#71) from hotfix/stage-elc-engram-integration into dev
El SDK CI - dev / build-and-test (push) Failing after 14m31s
2026-07-15 15:49:57 +00:00
will.anderson 8ce8656de2 epm: declare cross-module callees as extern fn so strict compilers accept generated C
El SDK CI - dev / build-and-test (pull_request) Successful in 7m33s
epm's sibling modules (registry/install/update) call functions defined in other
modules and in the El runtime (config, read_installed, registry_find,
manifest_deps, manifest_name, registry_latest_version, registry_token,
install_vessel, installed_version) without importing them, so elc emits no C
prototype for those calls. gcc<=13 treated the resulting implicit declarations
as warnings; gcc>=14 and clang reject them as hard errors, which is why the
"Build epm" CI step fails and blocks the whole dev/stage pipeline.

Add `extern fn` forward declarations -- El's own separate-compilation mechanism
-- for each cross-module callee at the top of registry/install/update. This
gives elc the correct C prototype in every generated translation unit, so the
calls compile cleanly and still resolve at link time. Simply suppressing
-Wimplicit-function-declaration would be unsafe: an implicit int return
truncates the 64-bit pointer returns of config/registry_find into a latent
crash, so declaring the true signatures is the correct fix. Localized to epm;
touches neither elc nor the runtime.
2026-07-15 10:14:43 -05:00
will.anderson 1e49560f1f Merge remote-tracking branch 'origin/feat/engram-semantic-search' into hotfix/stage-elc-engram-integration
El SDK CI - dev / build-and-test (pull_request) Failing after 14m39s
# Conflicts:
#	lang/el-compiler/runtime/el_runtime.c
2026-07-15 09:33:05 -05:00
will.anderson e8f0b5a9de Merge remote-tracking branch 'origin/fix/engram-lexical-tokenized-search' into hotfix/stage-elc-engram-integration 2026-07-15 09:28:44 -05:00
will.anderson 40287c4cfc Merge remote-tracking branch 'origin/hotfix/win-runtime-portability' into hotfix/stage-elc-engram-integration 2026-07-15 09:28:44 -05:00
will.anderson 0481bea44d Merge remote-tracking branch 'origin/hotfix/runtime-engram-get-node-by-label' into hotfix/stage-elc-engram-integration 2026-07-15 09:28:44 -05:00
will.anderson 9d565ca080 Merge remote-tracking branch 'origin/hotfix/elc-fixes' into hotfix/stage-elc-engram-integration 2026-07-15 09:28:44 -05:00
will.anderson 4773dd0aa2 runtime: make Windows soul reproducible from a clean el checkout
El SDK Release / build-and-release (pull_request) Failing after 16s
Two el_runtime portability defects only ever lived in staged local copies
used to hand-build neuron-ui PR #136's curl-enabled Windows neuron.exe.
gcc 15 promotes both to hard errors, so a clean el checkout cannot rebuild
that soul. Upstream the minimal fixes so the build is reproducible:

- http_serve_async: cast setsockopt optval to (const char*). Win32/mingw
  setsockopt wants const char*, not int*; the cast is a no-op on POSIX and
  matches the four already-cast sites elsewhere in this file.
- engram_save persist path: map fsync -> _commit in the _WIN32-only
  el_platform_win.h (io.h already included). Windows has no fsync(); the
  POSIX path is untouched.
2026-07-15 04:24:08 -05:00
will.anderson 6b9d9e6c4a Add engram_get_node_by_label runtime native to unblock soul link
El SDK Release / build-and-release (pull_request) Failing after 22s
chat.el calls the runtime native engram_get_node_by_label to fetch
well-known nodes (conv:history, session:summary) by stable label rather
than by ID — immune to vector-index drift across restarts. The current
runtime never defined it, so the regenerated dist/soul.c fails to link.

Backport the function verbatim (idiom-adapted to jb_finish) from release
runtime v1.0.0-20260501 and register it as an EL builtin exactly like its
siblings: runtime definition + prototype, __-prefixed seed wrapper +
prototype, and codegen arity entry. No search-site code is touched.
2026-07-15 04:07:33 -05:00
will.anderson b4967af13e feat(engram): semantic search layer via nomic-embed-text (cosine ∪ lexical)
Lexical istr_contains alone can't surface a node whose words don't appear
in the query. This adds an optional dense-vector layer: node content and the
query are embedded through Ollama (nomic-embed-text), and nodes are ranked by
cosine similarity unioned with lexical hits, so a paraphrase query reaches the
right node.

Wired into all three query entry points in el_runtime.c:
  - engram_search_json (HTTP /api/search): collect lexical ∪ semantic
    candidates, score (lexical base 1.0 + cosine; pure-semantic = cosine),
    rank, emit top-N. Stable sort preserves old order when semantic is off.
  - engram_search (internal el_val twin): lexical ∪ semantic union.
  - engram_activate seed loop (HTTP /api/activate): a node seeds if it
    lexically matches OR clears the cosine threshold; pure-semantic seeds
    enter scaled by cosine so paraphrase spreads without overpowering.

Degradable by design: the whole layer is gated on HAVE_CURL plus a one-shot
runtime probe. If curl is compiled out, Ollama is unreachable, or
ENGRAM_SEMANTIC=0, every entry point yields zero semantic signal and callers
fall back byte-for-byte to the pre-existing lexical search.

Node embeddings are cached in process memory keyed by node id with an FNV-1a
content hash for invalidation; the query is embedded once per call — so the
graph is not re-embedded on every query. nomic task prefixes
(search_query:/search_document:) are applied for retrieval separation.

Build steps gain -DHAVE_CURL so the engram artifact compiles the layer in
(-lcurl was already linked). Env: ENGRAM_SEMANTIC, ENGRAM_EMBED_URL,
ENGRAM_EMBED_MODEL, ENGRAM_SEMANTIC_MIN (cosine threshold, default 0.6).
2026-07-14 18:48:16 -05:00
will.anderson 2b2a1246e7 Merge pull request 'runtime: fix the memory-leak + write-corruption pair in el_runtime.c' (#64) from hotfix/el-runtime-leak-and-persist into main
El SDK Release / build-and-release (push) Failing after 10m52s
2026-07-13 21:23:31 +00:00
will.anderson 5c41c66a0f Merge pull request 'fix(windows): guard el_mem_check with _WIN32 — rusage is POSIX-only' (#60) from fix/windows-rusage-guard into stage
El SDK CI - stage / build-and-test (push) Failing after 13m21s
fix(windows): guard el_mem_check with _WIN32 — rusage is POSIX-only
2026-06-25 16:48:13 +00:00
27 changed files with 1022 additions and 6525 deletions
+15
View File
@@ -214,9 +214,18 @@ jobs:
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
# Fail loudly: previously this step had no `set -e`, so an auth or
# upload failure was swallowed (step exited 0 on the trailing echo)
# and the SDK silently never published. Surface failures now.
set -euo pipefail
if [ -z "${GCP_SA_KEY:-}" ]; then
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
exit 1
fi
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
VERSION="${GITHUB_SHA:0:8}"
@@ -268,6 +277,12 @@ jobs:
# Patches ci-base:dev in-place: pulls the existing image (which has all
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
#
# continue-on-error: this is a CI-cache optimization, NOT the release
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
# runner where DinD/Docker availability is fragile. A failure here must
# never block or redden the job — the SDK publish above is the deliverable.
continue-on-error: true
if: github.event_name == 'push'
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
+15
View File
@@ -212,12 +212,21 @@ jobs:
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
# Fail loudly: previously this step had no `set -e`, so an auth or
# upload failure was swallowed (step exited 0 on the trailing echo)
# and the SDK silently never published. Surface failures now.
set -euo pipefail
if [ -z "${GCP_SA_KEY:-}" ]; then
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
exit 1
fi
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates curl
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
VERSION="${GITHUB_SHA:0:8}"
@@ -253,6 +262,12 @@ jobs:
# Patches ci-base:stage in-place: pulls the existing image (which has all
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
#
# continue-on-error: this is a CI-cache optimization, NOT the release
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
# runner where DinD/Docker availability is fragile. A failure here must
# never block or redden the job — the SDK publish above is the deliverable.
continue-on-error: true
if: github.event_name == 'push'
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
+15
View File
@@ -288,12 +288,21 @@ jobs:
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
# Fail loudly: previously this step had no `set -e`, so an auth or
# upload failure was swallowed (step exited 0 on the trailing echo)
# and the SDK silently never published. Surface failures now.
set -euo pipefail
if [ -z "${GCP_SA_KEY:-}" ]; then
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
exit 1
fi
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates curl
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
VERSION="${GITHUB_SHA:0:8}"
@@ -345,6 +354,12 @@ jobs:
# Patches ci-base:latest in-place: pulls the existing image (which has all
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
#
# continue-on-error: this is a CI-cache optimization, NOT the release
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
# runner where DinD/Docker availability is fragile. A failure here must
# never block or redden the job — the SDK publish above is the deliverable.
continue-on-error: true
if: github.event_name == 'push'
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
-45
View File
@@ -1,45 +0,0 @@
;;; lang_profile_es.el — Spanish language profile for ELP.
;;; Keys the realizer's construction switches. Mirrors lang_profile_en / _pt.
(lang_profile_es
(language "Spanish")
(iso639 "es")
(family "Romance")
;; -- core typology flags -------------------------------------------------
(pro-drop yes) ; subjects routinely dropped; agreement carries person
(obligatory-subject no)
(grammatical-gender yes) ; m/f on every noun; article+adjective AGREE
(gender-source lexicon); REAL per-noun gender from UniMorph — NOT a heuristic
(do-support no)
(subject-aux-inversion no) ; questions by intonation/punctuation, not inversion
(question-strategy intonation)
(article-selection "el/la/los/las un/una/unos/unas")
(stressed-a-rule yes) ; fem sg noun in stressed a-/ha- takes el/un (el agua)
(adjective-position postnominal) ; default post; a few prenominal + apocope
(adjective-agreement "gender+number")
(question-punct inverted) ; opening ¿ ¡ required
;; -- MANDATORY CONTRACTIONS (coordinator quality bar) --------------------
(contractions ((de el "del") (a el "al")))
(contraction-mandatory yes) ; 'de el'/'a el' MUST surface as del/al
;; -- verb / aspect system ------------------------------------------------
(verb-classes (ar er ir))
(tenses (present preterite imperfect future conditional))
(moods (ind sbjv imp))
(finite-agreement "person+number (6 slots)")
(perfect-aux "haber") ; haber + past participle (invariant -o)
(progressive-aux "estar") ; estar + gerund
(passive-aux "ser") ; ser + participle (agrees) + por-agent
(copula-split "ser/estar") ; permanent vs stage-level
(future "infinitive + é/ás/á/emos/éis/án")
;; -- clitics / government ------------------------------------------------
(object-clitics yes) ; me te lo la le nos os los las; proclisis/enclisis
(clitic-order "se II I III (le+lo -> se lo)")
(enclisis "imperative/infinitive/gerund + accent repair (dá+me+lo->dámelo)")
(verb-prep-government yes) ; verbs select prep (protestar+contra, escapar+de)
;; -- SACRED safety bar (shared with en/pt) -------------------------------
(negation-faithful yes)) ; polarity never dropped/inverted; unplaceable -> FLAG
+10 -171
View File
@@ -54,16 +54,6 @@ fn es_str_last3(s: String) -> String {
// Spanish verbs fall into three conjugation classes defined by the infinitive
// ending: -ar, -er, -ir. The stem is the infinitive minus those two characters.
// Strong-vowel-final test (a/e/o) used for orthographic y-insertion and the
// accented -ído participle (caer->caído/cayó; but ui/iu diphthongs stay plain).
fn es_strong_vowel_final(s: String) -> Bool {
let c: String = es_str_last_char(s)
if str_eq(c, "a") { return true }
if str_eq(c, "e") { return true }
if str_eq(c, "o") { return true }
return false
}
fn es_verb_class(base: String) -> String {
if es_str_ends(base, "ar") { return "ar" }
if es_str_ends(base, "er") { return "er" }
@@ -463,18 +453,12 @@ fn es_regular_preterite(stem: String, vclass: String, slot: Int) -> String {
if slot == 4 { return stem + "asteis" }
return stem + "aron"
}
// -er and -ir share the same preterite endings.
// Orthographic rule: a vowel-final stem takes -yó/-yeron (caer->cayó,
// leer->leyó, creer->creyó) since -ió after a vowel becomes -yó.
// -er and -ir share the same preterite endings
if slot == 0 { return stem + "í" }
if slot == 1 { return stem + "iste" }
if slot == 2 {
if es_strong_vowel_final(stem) { return stem + "" }
return stem + ""
}
if slot == 2 { return stem + "" }
if slot == 3 { return stem + "imos" }
if slot == 4 { return stem + "isteis" }
if es_strong_vowel_final(stem) { return stem + "yeron" }
return stem + "ieron"
}
@@ -656,35 +640,17 @@ fn es_pluralize(noun: String) -> String {
if !str_eq(inv, "") {
return inv
}
// Oxytone nouns ending accented-vowel + n LOSE the written accent in the
// plural (canción->canciones, razón->razones, jardín->jardines).
// NOTE El strings are byte-indexed; "ón"/"án"/... are 3 bytes (accent=2 +n).
if es_str_ends(noun, "ón") { return es_str_drop_last(noun, 3) + "ones" }
if es_str_ends(noun, "án") { return es_str_drop_last(noun, 3) + "anes" }
if es_str_ends(noun, "én") { return es_str_drop_last(noun, 3) + "enes" }
if es_str_ends(noun, "ín") { return es_str_drop_last(noun, 3) + "ines" }
if es_str_ends(noun, "ún") { return es_str_drop_last(noun, 3) + "unes" }
// Oxytone accented-vowel + s also loses the accent (francés->franceses,
// inglés->ingleses). í/ú stay (país->países via the consonant rule).
if es_str_ends(noun, "és") { return es_str_drop_last(noun, 3) + "eses" }
if es_str_ends(noun, "ás") { return es_str_drop_last(noun, 3) + "ases" }
if es_str_ends(noun, "ós") { return es_str_drop_last(noun, 3) + "oses" }
let last: String = es_str_last_char(noun)
// Ends in -z: replace with -ces
if es_str_ends(noun, "z") {
if str_eq(last, "z") {
return es_str_drop_last(noun, 1) + "ces"
}
// Stressed final vowel: á/é/ó -> +s (café->cafés); í/ú -> +es (rubí->rubíes)
if es_str_ends(noun, "á") { return noun + "s" }
if es_str_ends(noun, "é") { return noun + "s" }
if es_str_ends(noun, "ó") { return noun + "s" }
if es_str_ends(noun, "í") { return noun + "es" }
if es_str_ends(noun, "ú") { return noun + "es" }
// Plain final vowel: add -s
if es_str_ends(noun, "a") { return noun + "s" }
if es_str_ends(noun, "e") { return noun + "s" }
if es_str_ends(noun, "i") { return noun + "s" }
if es_str_ends(noun, "o") { return noun + "s" }
if es_str_ends(noun, "u") { return noun + "s" }
// Ends in a vowel: add -s
if str_eq(last, "a") { return noun + "s" }
if str_eq(last, "e") { return noun + "s" }
if str_eq(last, "i") { return noun + "s" }
if str_eq(last, "o") { return noun + "s" }
if str_eq(last, "u") { return noun + "s" }
// Ends in consonant (including -s for stressed words like autobús): add -es
return noun + "es"
}
@@ -721,37 +687,6 @@ fn es_starts_with_stressed_a(noun: String) -> Bool {
return false
}
// es_article_for_gender: the article logic, given gender EXPLICITLY.
// The realizer should call this with the REAL per-noun gender from
// vocabulary-es.el (form2), NOT the es_gender heuristic that is what
// eliminates the 'el mano' / 'la día' masculine-default error class.
fn es_article_for_gender(gender: String, noun: String, definite: String, number: String) -> String {
let is_plural: Bool = str_eq(number, "plural")
let is_def: Bool = str_eq(definite, "true")
if is_def {
if is_plural {
if str_eq(gender, "f") { return "las" }
return "los"
}
if str_eq(gender, "f") {
if es_starts_with_stressed_a(noun) { return "el" }
return "la"
}
return "el"
}
if is_plural {
if str_eq(gender, "f") { return "unas" }
return "unos"
}
if str_eq(gender, "f") {
if es_starts_with_stressed_a(noun) { return "un" }
return "una"
}
return "un"
}
fn es_agree_article(noun: String, definite: String, number: String) -> String {
let gender: String = es_gender(noun)
let is_plural: Bool = str_eq(number, "plural")
@@ -779,99 +714,3 @@ fn es_agree_article(noun: String, definite: String, number: String) -> String {
if str_eq(gender, "f") { return "una" }
return "un"
}
// Past participle (compound tenses: haber + participle)
//
// Irregular participle table (transcribed from morphology_es_full._IRREG_PART),
// then the regular rule: -ar -> -ado, -er/-ir -> -ido.
fn es_participle(verb: String) -> String {
if str_eq(verb, "escribir") { return "escrito" }
if str_eq(verb, "describir") { return "descrito" }
if str_eq(verb, "abrir") { return "abierto" }
if str_eq(verb, "cubrir") { return "cubierto" }
if str_eq(verb, "descubrir") { return "descubierto" }
if str_eq(verb, "morir") { return "muerto" }
if str_eq(verb, "poner") { return "puesto" }
if str_eq(verb, "ver") { return "visto" }
if str_eq(verb, "volver") { return "vuelto" }
if str_eq(verb, "devolver") { return "devuelto" }
if str_eq(verb, "hacer") { return "hecho" }
if str_eq(verb, "deshacer") { return "deshecho" }
if str_eq(verb, "decir") { return "dicho" }
if str_eq(verb, "romper") { return "roto" }
if str_eq(verb, "resolver") { return "resuelto" }
if str_eq(verb, "freír") { return "frito" }
if str_eq(verb, "imprimir") { return "impreso" }
if str_eq(verb, "satisfacer") { return "satisfecho" }
if str_eq(verb, "prever") { return "previsto" }
if str_eq(verb, "revolver") { return "revuelto" }
// Accented -ír infinitives (oír->oído, sonreír->sonreído): "ír" is 3 bytes.
if es_str_ends(verb, "ír") { return es_str_drop_last(verb, 3) + "ído" }
if es_str_ends(verb, "ar") { return es_str_drop_last(verb, 2) + "ado" }
// -er/-ir: a strong-vowel stem takes the accented -ído (caer->caído,
// leer->leído, poseer->poseído); consonant stems stay -ido; ui/iu
// diphthongs (construir->construido) stay plain.
if es_str_ends(verb, "er") {
let st: String = es_str_drop_last(verb, 2)
if es_strong_vowel_final(st) { return st + "ído" }
return st + "ido"
}
if es_str_ends(verb, "ir") {
let st: String = es_str_drop_last(verb, 2)
if es_strong_vowel_final(st) { return st + "ído" }
return st + "ido"
}
return verb
}
// Adjective agreement (gender + number)
//
// Rule-based agreement mirroring morphology_es_full.inflect_adj (rule path):
// feminine: -o -> -a; nationality/-dor exceptions add -a; else invariant
// plural: reuse es_pluralize on the agreed singular
// (Lexicon-backed adjectives in Python may differ; those divergences are what
// the parity check surfaces.)
fn es_adj_feminine(lemma: String) -> String {
if str_eq(lemma, "español") { return "española" }
if str_eq(lemma, "francés") { return "francesa" }
if str_eq(lemma, "inglés") { return "inglesa" }
if str_eq(lemma, "alemán") { return "alemana" }
if str_eq(lemma, "trabajador") { return "trabajadora" }
if str_eq(lemma, "hablador") { return "habladora" }
if str_eq(lemma, "encantador") { return "encantadora" }
if es_str_ends(lemma, "o") { return es_str_drop_last(lemma, 1) + "a" }
return lemma
}
fn es_inflect_adj(lemma: String, gender: String, number: String) -> String {
if str_eq(gender, "f") {
let fem: String = es_adj_feminine(lemma)
if str_eq(number, "plural") { return es_pluralize(fem) }
return fem
}
// masculine (citation)
if str_eq(number, "plural") { return es_pluralize(lemma) }
return lemma
}
// Mandatory preposition + article contraction (de+el->del, a+el->al)
//
// Transcribed from realizer_es._contract. np_text is the already-realized NP
// beginning with "el " when the contraction fires.
fn es_starts_el(np_text: String) -> Bool {
let n: Int = str_len(np_text)
if n < 3 { return false }
return str_eq(str_slice(np_text, 0, 3), "el ")
}
fn es_contract(prep: String, np_text: String) -> String {
if es_starts_el(np_text) {
let rest: String = str_slice(np_text, 3, str_len(np_text))
if str_eq(prep, "de") { return "del " + rest }
if str_eq(prep, "a") { return "al " + rest }
}
return prep + " " + np_text
}
-362
View File
@@ -1,362 +0,0 @@
;;; vocabulary-es.el — Spanish vocabulary for ELP surface realization.
;;; Schema: (lemma pos form0 form1 form2 en_translation semantic_hint)
;;; Source: UniMorph Spanish (github.com/unimorph/spa, CC-BY-SA 3.0),
;;; generated by gen_elp_es.py via morphology_es_full (real forms).
;;; Verbs: form0=present-ind-3sg form1=preterite-3sg form2=past-participle
;;; Nouns: form0=singular form1=plural form2=REAL gender (m/f, from lexicon —
;;; NOT an ending heuristic; this is what kills 'el mano'/'la día' errors)
;;; Adjs : form0=masc-sg form1=fem-sg form2=masc-pl
(vocabulary-es
;; -- function / closed class (incl. mandatory contractions del/al) --------
("el" "det" "el" "los" "m" "the" "definite article m.sg")
("la" "det" "la" "las" "f" "the" "definite article f.sg")
("un" "det" "un" "unos" "m" "a" "indefinite article m.sg")
("una" "det" "una" "unas" "f" "a" "indefinite article f.sg")
("del" "contraction" "del" "" "" "of the" "de + el (mandatory contraction)")
("al" "contraction" "al" "" "" "to the" "a + el (mandatory contraction)")
("este" "dem" "este" "estos" "m" "this" "proximal dem m")
("esta" "dem" "esta" "estas" "f" "this" "proximal dem f")
("no" "neg" "no" "" "" "not/no" "sentential negator (preverbal)")
("ninguno" "det" "ningún" "ninguna" "" "none" "negative determiner (apocope ningún m.sg)")
("y" "conj" "y" "e" "" "and" "coordinator (e before i-/hi-)")
("o" "conj" "o" "u" "" "or" "coordinator (u before o-/ho-)")
("pero" "conj" "pero" "" "" "but" "adversative coordinator")
("que" "conj" "que" "" "" "that" "complementizer / relative")
("si" "conj" "si" "" "" "if" "conditional subordinator")
("porque" "conj" "porque" "" "" "because" "causal subordinator")
("cuando" "conj" "cuando" "" "" "when" "temporal subordinator")
("a" "prep" "a" "" "" "to" "dir-obj (personal a) / dative / allative; a+el=al")
("de" "prep" "de" "" "" "of/from" "genitive/ablative government; de+el=del")
("en" "prep" "en" "" "" "in/on" "locative")
("con" "prep" "con" "" "" "with" "comitative")
("por" "prep" "por" "" "" "by/for" "passive agent / cause")
("para" "prep" "para" "" "" "for" "purpose/benefactive")
("contra" "prep" "contra" "" "" "against" "adversative government (protestar contra)")
("sin" "prep" "sin" "" "" "without" "privative")
("yo" "pron" "yo" "me" "mi" "I" "1sg subj/obj/poss")
("" "pron" "" "te" "tu" "you" "2sg informal")
("usted" "pron" "usted" "lo" "su" "you" "2sg formal (3sg agreement)")
("él" "pron" "él" "lo" "su" "he" "3sg m subj/DO-clitic/poss")
("ella" "pron" "ella" "la" "su" "she" "3sg f subj/DO-clitic/poss")
("nosotros" "pron" "nosotros" "nos" "nuestro" "we" "1pl")
("vosotros" "pron" "vosotros" "os" "vuestro" "you" "2pl informal")
("ellos" "pron" "ellos" "los" "su" "they" "3pl m")
("ellas" "pron" "ellas" "las" "su" "they" "3pl f")
("le" "clitic" "le" "les" "" "to-him/her" "dative clitic 3sg/3pl (->se before lo/la)")
("se" "clitic" "se" "se" "" "himself/-self" "reflexive / spurious-se (le+lo->se lo)")
;; -- verbs (form0=pres-3sg form1=pret-3sg form2=past-participle) -----------
("abrazar" "verb" "abraza" "abrazó" "abrazado" "abrazar" "ar/regular")
("abrir" "verb" "abre" "abrió" "abierto" "abrir" "ir/irregular")
("aceptar" "verb" "acepta" "aceptó" "aceptado" "aceptar" "ar/regular")
("acordar" "verb" "acuerda" "acordó" "acordado" "acordar" "ar/regular")
("amar" "verb" "ama" "amó" "amado" "amar" "ar/regular")
("anunciar" "verb" "anuncia" "anunció" "anunciado" "anunciar" "ar/regular")
("aprobar" "verb" "aprueba" "aprobó" "aprobado" "aprobar" "ar/regular")
("aumentar" "verb" "aumenta" "aumentó" "aumentado" "aumentar" "ar/regular")
("ayudar" "verb" "ayuda" "ayudó" "ayudado" "ayudar" "ar/regular")
("bajar" "verb" "baja" "bajó" "bajado" "bajar" "ar/regular")
("besar" "verb" "besa" "besó" "besado" "besar" "ar/regular")
("buscar" "verb" "busca" "buscó" "buscado" "buscar" "ar/regular")
("caer" "verb" "cae" "cayó" "caído" "caer" "er/regular")
("cambiar" "verb" "cambia" "cambió" "cambiado" "cambiar" "ar/regular")
("caminar" "verb" "camina" "caminó" "caminado" "caminar" "ar/regular")
("cantar" "verb" "canta" "cantó" "cantado" "cantar" "ar/regular")
("celebrar" "verb" "celebra" "celebró" "celebrado" "celebrar" "ar/regular")
("cerrar" "verb" "cierra" "cerró" "cerrado" "cerrar" "ar/regular")
("comenzar" "verb" "comienza" "comenzó" "comenzado" "comenzar" "ar/regular")
("comer" "verb" "come" "comió" "comido" "comer" "er/regular")
("condenar" "verb" "condena" "condenó" "condenado" "condenar" "ar/regular")
("confirmar" "verb" "confirma" "confirmó" "confirmado" "confirmar" "ar/regular")
("conocer" "verb" "conoce" "conoció" "conocido" "conocer" "er/regular")
("contar" "verb" "cuenta" "contó" "contado" "contar" "ar/regular")
("contratar" "verb" "contrata" "contrató" "contratado" "contratar" "ar/regular")
("costar" "verb" "cuesta" "costó" "costado" "costar" "ar/regular")
("crear" "verb" "crea" "creó" "creado" "crear" "ar/regular")
("crecer" "verb" "crece" "creció" "crecido" "crecer" "er/regular")
("creer" "verb" "cree" "creyó" "creído" "creer" "er/regular")
("dar" "verb" "da" "dio" "dado" "dar" "ar/irregular")
("deber" "verb" "debe" "debió" "debido" "deber" "er/regular")
("decir" "verb" "dice" "dijo" "dicho" "decir" "ir/irregular")
("dejar" "verb" "deja" "dejó" "dejado" "dejar" "ar/regular")
("desaparecer" "verb" "desaparece" "desapareció" "desaparecido" "desaparecer" "er/regular")
("descubrir" "verb" "descubre" "descubrió" "descubierto" "descubrir" "ir/irregular")
("despedir" "verb" "despide" "despidió" "despedido" "despedir" "ir/regular")
("detener" "verb" "detiene" "detuvo" "detenido" "detener" "er/regular")
("dimitir" "verb" "dimite" "dimitió" "dimitido" "dimitir" "ir/regular")
("doler" "verb" "duele" "dolió" "dolido" "doler" "er/regular")
("dormir" "verb" "duerme" "durmió" "dormido" "dormir" "ir/regular")
("durar" "verb" "dura" "duró" "durado" "durar" "ar/regular")
("empezar" "verb" "empieza" "empezó" "empezado" "empezar" "ar/regular")
("encontrar" "verb" "encuentra" "encontró" "encontrado" "encontrar" "ar/regular")
("entender" "verb" "entiende" "entendió" "entendido" "entender" "er/regular")
("entrar" "verb" "entra" "entró" "entrado" "entrar" "ar/regular")
("entregar" "verb" "entrega" "entregó" "entregado" "entregar" "ar/regular")
("escapar" "verb" "escapa" "escapó" "escapado" "escapar" "ar/regular")
("escribir" "verb" "escribe" "escribió" "escrito" "escribir" "ir/regular")
("escuchar" "verb" "escucha" "escuchó" "escuchado" "escuchar" "ar/regular")
("esperar" "verb" "espera" "esperó" "esperado" "esperar" "ar/regular")
("estar" "verb" "está" "estuvo" "estado" "estar" "ar/irregular")
("estudiar" "verb" "estudia" "estudió" "estudiado" "estudiar" "ar/regular")
("existir" "verb" "existe" "existió" "existido" "existir" "ir/regular")
("explicar" "verb" "explica" "explicó" "explicado" "explicar" "ar/regular")
("firmar" "verb" "firma" "firmó" "firmado" "firmar" "ar/regular")
("ganar" "verb" "gana" "ganó" "ganado" "ganar" "ar/regular")
("gritar" "verb" "grita" "gritó" "gritado" "gritar" "ar/regular")
("guardar" "verb" "guarda" "guardó" "guardado" "guardar" "ar/regular")
("gustar" "verb" "gusta" "gustó" "gustado" "gustar" "ar/regular")
("haber" "verb" "ha" "hubo" "habido" "haber" "er/irregular")
("hablar" "verb" "habla" "habló" "hablado" "hablar" "ar/regular")
("hacer" "verb" "hace" "hizo" "hecho" "hacer" "er/irregular")
("ir" "verb" "va" "fue" "ido" "ir" "ir/irregular")
("jugar" "verb" "juega" "jugó" "jugado" "jugar" "ar/regular")
("lavar" "verb" "lava" "lavó" "lavado" "lavar" "ar/regular")
("leer" "verb" "lee" "leyó" "leído" "leer" "er/regular")
("llamar" "verb" "llama" "llamó" "llamado" "llamar" "ar/regular")
("llegar" "verb" "llega" "llegó" "llegado" "llegar" "ar/regular")
("llevar" "verb" "lleva" "llevó" "llevado" "llevar" "ar/regular")
("llover" "verb" "llueve" "llovió" "llovido" "llover" "er/regular")
("mirar" "verb" "mira" "miró" "mirado" "mirar" "ar/regular")
("morir" "verb" "muere" "murió" "muerto" "morir" "ir/irregular")
("mostrar" "verb" "muestra" "mostró" "mostrado" "mostrar" "ar/regular")
("nacer" "verb" "nace" "nació" "nacido" "nacer" "er/regular")
("ocultar" "verb" "oculta" "ocultó" "ocultado" "ocultar" "ar/regular")
("ofrecer" "verb" "ofrece" "ofreció" "ofrecido" "ofrecer" "er/regular")
("olvidar" "verb" "olvida" "olvidó" "olvidado" "olvidar" "ar/regular")
("oír" "verb" "oye" "oyó" "oído" "oír" "ar/regular")
("parecer" "verb" "parece" "pareció" "parecido" "parecer" "er/regular")
("pasar" "verb" "pasa" "pasó" "pasado" "pasar" "ar/regular")
("pedir" "verb" "pide" "pidió" "pedido" "pedir" "ir/regular")
("pensar" "verb" "piensa" "pensó" "pensado" "pensar" "ar/regular")
("perder" "verb" "pierde" "perdió" "perdido" "perder" "er/regular")
("perdonar" "verb" "perdona" "perdonó" "perdonado" "perdonar" "ar/regular")
("poder" "verb" "puede" "pudo" "podido" "poder" "er/irregular")
("poner" "verb" "pone" "puso" "puesto" "poner" "er/irregular")
("preocupar" "verb" "preocupa" "preocupó" "preocupado" "preocupar" "ar/regular")
("producir" "verb" "produce" "produjo" "producido" "producir" "ir/regular")
("prometer" "verb" "promete" "prometió" "prometido" "prometer" "er/regular")
("proponer" "verb" "propone" "propuso" "propuesto" "proponer" "er/regular")
("prosperar" "verb" "prospera" "prosperó" "prosperado" "prosperar" "ar/regular")
("protestar" "verb" "protesta" "protestó" "protestado" "protestar" "ar/regular")
("quedar" "verb" "queda" "quedó" "quedado" "quedar" "ar/regular")
("querer" "verb" "quiere" "quiso" "querido" "querer" "er/irregular")
("recordar" "verb" "recuerda" "recordó" "recordado" "recordar" "ar/regular")
("recorrer" "verb" "recorre" "recorrió" "recorrido" "recorrer" "er/regular")
("regresar" "verb" "regresa" "regresó" "regresado" "regresar" "ar/regular")
("renacer" "verb" "renace" "renació" "renacido" "renacer" "er/regular")
("saber" "verb" "sabe" "supo" "sabido" "saber" "er/irregular")
("salir" "verb" "sale" "salió" "salido" "salir" "ir/irregular")
("sentar" "verb" "sienta" "sentó" "sentado" "sentar" "ar/regular")
("sentir" "verb" "siente" "sintió" "sentido" "sentir" "ir/regular")
("separar" "verb" "separa" "separó" "separado" "separar" "ar/regular")
("ser" "verb" "es" "fue" "sido" "ser" "er/irregular")
("sonreír" "verb" "sonríe" "sonrió" "sonreído" "sonreír" "ar/regular")
("soplar" "verb" "sopla" "sopló" "soplado" "soplar" "ar/regular")
("sorprender" "verb" "sorprende" "sorprendió" "sorprendido" "sorprender" "er/regular")
("soñar" "verb" "sueña" "soñó" "soñado" "soñar" "ar/regular")
("subir" "verb" "sube" "subió" "subido" "subir" "ir/regular")
("tener" "verb" "tiene" "tuvo" "tenido" "tener" "er/irregular")
("terminar" "verb" "termina" "terminó" "terminado" "terminar" "ar/regular")
("tocar" "verb" "toca" "tocó" "tocado" "tocar" "ar/regular")
("tomar" "verb" "toma" "tomó" "tomado" "tomar" "ar/regular")
("trabajar" "verb" "trabaja" "trabajó" "trabajado" "trabajar" "ar/regular")
("vender" "verb" "vende" "vendió" "vendido" "vender" "er/regular")
("venir" "verb" "viene" "vino" "venido" "venir" "ir/irregular")
("ver" "verb" "ve" "vio" "visto" "ver" "er/irregular")
("viajar" "verb" "viaja" "viajó" "viajado" "viajar" "ar/regular")
("vivir" "verb" "vive" "vivió" "vivido" "vivir" "ir/regular")
("volver" "verb" "vuelve" "volvió" "vuelto" "volver" "er/irregular")
;; -- nouns (form0=sg form1=pl form2=REAL gender m/f) ----------------------
("Barcelona" "noun" "barcelona" "barcelonas" "f" "Barcelona" "gender:heuristic")
("Madrid" "noun" "madrid" "madrides" "m" "Madrid" "gender:heuristic")
("María" "noun" "maría" "marías" "f" "María" "gender:heuristic")
("acuerdo" "noun" "acuerdo" "acuerdos" "m" "acuerdo" "gender:lexicon")
("acusado" "noun" "acusado" "acusados" "m" "acusado" "gender:lexicon")
("agua" "noun" "agua" "aguas" "f" "agua" "gender:lexicon")
("amigo" "noun" "amigo" "amigos" "m" "amigo" "gender:lexicon")
("amor" "noun" "amor" "amores" "m" "amor" "gender:lexicon")
("anciano" "noun" "anciano" "ancianos" "m" "anciano" "gender:lexicon")
("autor" "noun" "autor" "autores" "m" "autor" "gender:lexicon")
("ayuda" "noun" "ayuda" "ayudas" "f" "ayuda" "gender:lexicon")
("año" "noun" "año" "años" "m" "año" "gender:lexicon")
("banco" "noun" "banco" "bancos" "m" "banco" "gender:lexicon")
("barco" "noun" "barco" "barcos" "m" "barco" "gender:lexicon")
("baño" "noun" "baño" "baños" "m" "baño" "gender:lexicon")
("beneficio" "noun" "beneficio" "beneficios" "m" "beneficio" "gender:lexicon")
("billete" "noun" "billete" "billetes" "m" "billete" "gender:lexicon")
("café" "noun" "café" "cafés" "m" "café" "gender:lexicon")
("calma" "noun" "calma" "calmas" "f" "calma" "gender:lexicon")
("camino" "noun" "camino" "caminos" "m" "camino" "gender:lexicon")
("canción" "noun" "canción" "canciones" "f" "canción" "gender:lexicon")
("candidato" "noun" "candidato" "candidatos" "m" "candidato" "gender:lexicon")
("casa" "noun" "casa" "casas" "f" "casa" "gender:lexicon")
("cena" "noun" "cena" "cenas" "f" "cena" "gender:lexicon")
("ciudad" "noun" "ciudad" "ciudades" "f" "ciudad" "gender:lexicon")
("ciudadano" "noun" "ciudadano" "ciudadanos" "m" "ciudadano" "gender:lexicon")
("color" "noun" "color" "colores" "m" "color" "gender:lexicon")
("condición" "noun" "condición" "condiciones" "f" "condición" "gender:lexicon")
("corazón" "noun" "corazón" "corazones" "m" "corazón" "gender:lexicon")
("costa" "noun" "costa" "costas" "f" "costa" "gender:lexicon")
("crisis" "noun" "crisis" "crisis" "f" "crisis" "gender:lexicon")
("crédito" "noun" "crédito" "créditos" "m" "crédito" "gender:lexicon")
("culpa" "noun" "culpa" "culpas" "f" "culpa" "gender:lexicon")
("damnificado" "noun" "damnificado" "damnificados" "m" "damnificado" "gender:lexicon")
("demás" "noun" "demás" "demás" "m" "demás" "gender:heuristic")
("dinero" "noun" "dinero" "dineros" "m" "dinero" "gender:lexicon")
("día" "noun" "día" "días" "m" "día" "gender:lexicon")
("economía" "noun" "economía" "economías" "f" "economía" "gender:lexicon")
("elección" "noun" "elección" "elecciones" "f" "elección" "gender:lexicon")
("empleo" "noun" "empleo" "empleos" "m" "empleo" "gender:lexicon")
("empresa" "noun" "empresa" "empresas" "f" "empresa" "gender:lexicon")
("equipo" "noun" "equipo" "equipos" "m" "equipo" "gender:lexicon")
("estación" "noun" "estación" "estaciones" "f" "estación" "gender:lexicon")
("estudiante" "noun" "estudiante" "estudiantes" "m" "estudiante" "gender:lexicon")
("experto" "noun" "experto" "expertos" "m" "experto" "gender:lexicon")
("fiesta" "noun" "fiesta" "fiestas" "f" "fiesta" "gender:lexicon")
("flor" "noun" "flor" "flores" "f" "flor" "gender:lexicon")
("foto" "noun" "foto" "fotos" "f" "foto" "gender:lexicon")
("frío" "noun" "frío" "fríos" "m" "frío" "gender:lexicon")
("fuego" "noun" "fuego" "fuegos" "m" "fuego" "gender:lexicon")
("fuerza" "noun" "fuerza" "fuerzas" "f" "fuerza" "gender:lexicon")
("gato" "noun" "gato" "gatos" "m" "gato" "gender:lexicon")
("gobierno" "noun" "gobierno" "gobiernos" "m" "gobierno" "gender:lexicon")
("gusto" "noun" "gusto" "gustos" "m" "gusto" "gender:lexicon")
("hermano" "noun" "hermano" "hermanos" "m" "hermano" "gender:lexicon")
("hombre" "noun" "hombre" "hombres" "m" "hombre" "gender:lexicon")
("jardín" "noun" "jardín" "jardines" "m" "jardín" "gender:lexicon")
("juez" "noun" "juez" "jueces" "m" "juez" "gender:lexicon")
("juicio" "noun" "juicio" "juicios" "m" "juicio" "gender:lexicon")
("lentitud" "noun" "lentitud" "lentitudes" "f" "lentitud" "gender:lexicon")
("ley" "noun" "ley" "leyes" "f" "ley" "gender:lexicon")
("libertad" "noun" "libertad" "libertades" "f" "libertad" "gender:lexicon")
("libro" "noun" "libro" "libros" "m" "libro" "gender:lexicon")
("llave" "noun" "llave" "llaves" "f" "llave" "gender:lexicon")
("lluvia" "noun" "lluvia" "lluvias" "f" "lluvia" "gender:lexicon")
("luna" "noun" "luna" "lunas" "f" "luna" "gender:lexicon")
("luz" "noun" "luz" "luces" "f" "luz" "gender:lexicon")
("madre" "noun" "madre" "madres" "f" "madre" "gender:lexicon")
("mano" "noun" "mano" "manos" "f" "mano" "gender:lexicon")
("mapa" "noun" "mapa" "mapas" "m" "mapa" "gender:lexicon")
("mar" "noun" "mar" "mares" "m" "mar" "gender:lexicon")
("marea" "noun" "marea" "mareas" "f" "marea" "gender:lexicon")
("memoria" "noun" "memoria" "memorias" "f" "memoria" "gender:lexicon")
("mesa" "noun" "mesa" "mesas" "f" "mesa" "gender:lexicon")
("ministro" "noun" "ministro" "ministros" "m" "ministro" "gender:lexicon")
("montaña" "noun" "montaña" "montañas" "f" "montaña" "gender:lexicon")
("moto" "noun" "moto" "motos" "f" "moto" "gender:lexicon")
("mujer" "noun" "mujer" "mujeres" "f" "mujer" "gender:lexicon")
("mundo" "noun" "mundo" "mundos" "m" "mundo" "gender:lexicon")
("música" "noun" "música" "músicas" "f" "música" "gender:lexicon")
("nación" "noun" "nación" "naciones" "f" "nación" "gender:lexicon")
("niebla" "noun" "niebla" "nieblas" "f" "niebla" "gender:lexicon")
("nieve" "noun" "nieve" "nieves" "f" "nieve" "gender:lexicon")
("niña" "noun" "niña" "niñas" "f" "niña" "gender:lexicon")
("niño" "noun" "niño" "niños" "m" "niño" "gender:lexicon")
("noche" "noun" "noche" "noches" "f" "noche" "gender:lexicon")
("nombre" "noun" "nombre" "nombres" "m" "nombre" "gender:lexicon")
("ojo" "noun" "ojo" "ojos" "m" "ojo" "gender:heuristic")
("orilla" "noun" "orilla" "orillas" "f" "orilla" "gender:lexicon")
("paisaje" "noun" "paisaje" "paisajes" "m" "paisaje" "gender:lexicon")
("parlamento" "noun" "parlamento" "parlamentos" "m" "parlamento" "gender:lexicon")
("parte" "noun" "parte" "partes" "f" "parte" "gender:lexicon")
("pasillo" "noun" "pasillo" "pasillos" "m" "pasillo" "gender:lexicon")
("país" "noun" "país" "países" "m" "país" "gender:lexicon")
("película" "noun" "película" "películas" "f" "película" "gender:lexicon")
("perro" "noun" "perro" "perros" "m" "perro" "gender:lexicon")
("persona" "noun" "persona" "personas" "f" "persona" "gender:lexicon")
("petróleo" "noun" "petróleo" "petróleos" "m" "petróleo" "gender:lexicon")
("pez" "noun" "pez" "peces" "f" "pez" "gender:lexicon")
("plan" "noun" "plan" "planes" "m" "plan" "gender:lexicon")
("policía" "noun" "policía" "policías" "f" "policía" "gender:lexicon")
("portavoz" "noun" "portavoz" "portavoces" "m" "portavoz" "gender:lexicon")
("portero" "noun" "portero" "porteros" "m" "portero" "gender:lexicon")
("precio" "noun" "precio" "precios" "m" "precio" "gender:lexicon")
("presidente" "noun" "presidente" "presidentes" "m" "presidente" "gender:lexicon")
("problema" "noun" "problema" "problemas" "m" "problema" "gender:lexicon")
("programa" "noun" "programa" "programas" "m" "programa" "gender:lexicon")
("proyecto" "noun" "proyecto" "proyectos" "m" "proyecto" "gender:lexicon")
("puerta" "noun" "puerta" "puertas" "f" "puerta" "gender:lexicon")
("pájaro" "noun" "pájaro" "pájaros" "m" "pájaro" "gender:lexicon")
("razón" "noun" "razón" "razones" "f" "razón" "gender:lexicon")
("raíz" "noun" "raíz" "raíces" "f" "raíz" "gender:lexicon")
("recuerdo" "noun" "recuerdo" "recuerdos" "m" "recuerdo" "gender:lexicon")
("reforma" "noun" "reforma" "reformas" "f" "reforma" "gender:lexicon")
("región" "noun" "región" "regiones" "f" "región" "gender:lexicon")
("río" "noun" "río" "ríos" "m" "río" "gender:lexicon")
("semilla" "noun" "semilla" "semillas" "f" "semilla" "gender:lexicon")
("sendero" "noun" "sendero" "senderos" "m" "sendero" "gender:lexicon")
("señor" "noun" "señor" "señores" "m" "señor" "gender:lexicon")
("silencio" "noun" "silencio" "silencios" "m" "silencio" "gender:lexicon")
("silla" "noun" "silla" "sillas" "f" "silla" "gender:lexicon")
("sol" "noun" "sol" "soles" "m" "sol" "gender:lexicon")
("tarea" "noun" "tarea" "tareas" "f" "tarea" "gender:lexicon")
("tema" "noun" "tema" "temas" "m" "tema" "gender:lexicon")
("ti" "noun" "ti" "tis" "m" "ti" "gender:heuristic")
("tiempo" "noun" "tiempo" "tiempos" "m" "tiempo" "gender:lexicon")
("tipo" "noun" "tipo" "tipos" "m" "tipo" "gender:lexicon")
("todo" "noun" "todo" "todos" "m" "todo" "gender:heuristic")
("tormenta" "noun" "tormenta" "tormentas" "f" "tormenta" "gender:lexicon")
("trabajador" "noun" "trabajador" "trabajadores" "m" "trabajador" "gender:lexicon")
("tristeza" "noun" "tristeza" "tristezas" "f" "tristeza" "gender:lexicon")
("ventana" "noun" "ventana" "ventanas" "f" "ventana" "gender:lexicon")
("verdad" "noun" "verdad" "verdades" "f" "verdad" "gender:lexicon")
("viaje" "noun" "viaje" "viajes" "m" "viaje" "gender:lexicon")
("victoria" "noun" "victoria" "victorias" "f" "victoria" "gender:lexicon")
("vida" "noun" "vida" "vidas" "f" "vida" "gender:lexicon")
("viento" "noun" "viento" "vientos" "m" "viento" "gender:lexicon")
("voz" "noun" "voz" "voces" "f" "voz" "gender:lexicon")
("vuelta" "noun" "vuelta" "vueltas" "f" "vuelta" "gender:lexicon")
("árbol" "noun" "árbol" "árboles" "m" "árbol" "gender:lexicon")
;; -- adjectives (form0=masc-sg form1=fem-sg form2=masc-pl) ----------------
("alto" "adj" "alto" "alta" "altos" "alto" "lexicon")
("ambos" "adj" "ambos" "ambos" "ambos" "ambos" "rule")
("antiguo" "adj" "antiguo" "antigua" "antiguos" "antiguo" "lexicon")
("azul" "adj" "azul" "azul" "azules" "azul" "lexicon")
("bajo" "adj" "bajo" "baja" "bajos" "bajo" "lexicon")
("blanco" "adj" "blanco" "blanca" "blancos" "blanco" "lexicon")
("bondadoso" "adj" "bondadoso" "bondadosa" "bondadosos" "bondadoso" "lexicon")
("bonito" "adj" "bonito" "bonita" "bonitos" "bonito" "lexicon")
("bueno" "adj" "bueno" "buena" "buenos" "bueno" "lexicon")
("cansado" "adj" "cansado" "cansada" "cansados" "cansado" "lexicon")
("corto" "adj" "corto" "corta" "cortos" "corto" "lexicon")
("difícil" "adj" "difícil" "difícil" "difíciles" "difícil" "lexicon")
("dos" "adj" "dos" "dos" "dos" "dos" "rule")
("económico" "adj" "económico" "económica" "económicos" "económico" "lexicon")
("español" "adj" "español" "española" "españoles" "español" "lexicon")
("estrecho" "adj" "estrecho" "estrecha" "estrechos" "estrecho" "lexicon")
("fascinante" "adj" "fascinante" "fascinante" "fascinantes" "fascinante" "rule")
("feliz" "adj" "feliz" "feliz" "felices" "feliz" "lexicon")
("francés" "adj" "francés" "francesa" "franceses" "francés" "lexicon")
("frío" "adj" "frío" "fría" "fríos" "frío" "lexicon")
("fácil" "adj" "fácil" "fácil" "fáciles" "fácil" "lexicon")
("grande" "adj" "grande" "grande" "grandes" "grande" "lexicon")
("hermoso" "adj" "hermoso" "hermosa" "hermosos" "hermoso" "lexicon")
("importante" "adj" "importante" "importante" "importantes" "importante" "lexicon")
("inglés" "adj" "inglés" "inglesa" "ingleses" "inglés" "lexicon")
("largo" "adj" "largo" "larga" "largos" "largo" "lexicon")
("lento" "adj" "lento" "lenta" "lentos" "lento" "lexicon")
("malo" "adj" "malo" "mala" "malos" "malo" "lexicon")
("mucho" "adj" "mucho" "mucha" "muchos" "mucho" "lexicon")
("necesario" "adj" "necesario" "necesaria" "necesarios" "necesario" "lexicon")
("negro" "adj" "negro" "negra" "negros" "negro" "lexicon")
("nuevo" "adj" "nuevo" "nueva" "nuevos" "nuevo" "lexicon")
("olvidado" "adj" "olvidado" "olvidada" "olvidados" "olvidado" "lexicon")
("oscuro" "adj" "oscuro" "oscura" "oscuros" "oscuro" "lexicon")
("pequeño" "adj" "pequeño" "pequeña" "pequeños" "pequeño" "lexicon")
("político" "adj" "político" "política" "políticos" "político" "lexicon")
("posible" "adj" "posible" "posible" "posibles" "posible" "lexicon")
("rojo" "adj" "rojo" "roja" "rojos" "rojo" "lexicon")
("rápido" "adj" "rápido" "rápida" "rápidos" "rápido" "lexicon")
("sabio" "adj" "sabio" "sabia" "sabios" "sabio" "lexicon")
("silencioso" "adj" "silencioso" "silenciosa" "silenciosos" "silencioso" "lexicon")
("social" "adj" "social" "social" "sociales" "social" "lexicon")
("trabajador" "adj" "trabajador" "trabajadora" "trabajadores" "trabajador" "lexicon")
("triste" "adj" "triste" "triste" "tristes" "triste" "rule")
("verde" "adj" "verde" "verde" "verdes" "verde" "lexicon")
("viejo" "adj" "viejo" "vieja" "viejos" "viejo" "lexicon")
)
-262
View File
@@ -1,262 +0,0 @@
# -*- coding: utf-8 -*-
"""gen_elp_es.py — emit the ELP (.el) port artifacts for Spanish.
Mirrors gen_elp_en.py. Produces:
vocabulary-es.el real generated vocabulary in the established schema
[lemma, pos, form0, form1, form2, en_translation, semantic_hint]
(same schema as vocabulary-got.el / vocabulary-en.el;
UniMorph spa lineage).
Verbs : form0=present-ind-3sg form1=preterite-3sg form2=past-participle
Nouns : form0=singular form1=plural form2=REAL gender (m/f, lexicon)
Adjs : form0=masc-sg form1=fem-sg form2=masc-pl
lang_profile_es.el the Spanish profile with the flags the realizer keys on.
Every form is generated by morphology_es_full (real UniMorph lexicon, not
hand-typed), so the .el vocabulary is honest and reproduces the forms the
realizer used. CRITICAL (coordinator quality bar): noun gender in form2 is the
REAL per-lemma lexicon gender (N;FEM/MASC), NOT an ending heuristic — this is
what kills the 'el mano / la día' masculine-default error class.
"""
import morphology_es_full as M
from test_set_es import TESTS
from held_out_es import HELD
# ── core closed class + common content lemmas so the vocab is usable beyond the
# validated sentences ──────────────────────────────────────────────────────
_CORE_VERBS = ["ser", "estar", "haber", "tener", "hacer", "ir", "ver", "dar",
"saber", "poder", "querer", "venir", "decir", "poner", "salir",
"hablar", "comer", "vivir", "trabajar", "estudiar", "llegar",
"pasar", "deber", "parecer", "quedar", "creer", "dejar", "llevar",
"encontrar", "llamar", "pensar", "volver", "conocer", "sentir",
"contar", "empezar", "buscar", "esperar", "existir", "entrar",
"escribir", "perder", "producir", "recordar", "morir", "nacer",
"abrir", "escapar", "soñar", "amar", "caer", "leer", "oír"]
_CORE_NOUNS = ["tiempo", "persona", "año", "día", "mano", "mundo", "vida",
"hombre", "mujer", "parte", "casa", "país", "problema", "programa",
"tema", "mapa", "agua", "foto", "moto", "ciudad", "libertad",
"canción", "nación", "flor", "color", "amor", "señor", "viaje",
"paisaje", "gato", "perro", "libro", "mesa", "silla", "noche",
"luz", "voz", "pez", "raíz", "crisis", "sol", "luna", "mar",
"corazón", "flor", "árbol", "camino", "puerta", "ventana"]
_CORE_ADJS = ["bueno", "malo", "nuevo", "viejo", "grande", "pequeño", "alto",
"bajo", "largo", "corto", "feliz", "triste", "fácil", "difícil",
"rápido", "lento", "hermoso", "económico", "político", "social",
"azul", "rojo", "verde", "blanco", "negro", "español", "francés",
"inglés", "importante", "posible", "necesario", "trabajador"]
# closed-class function words. Contractions (del/al) and the government notes
# are the coordinator's quality bar (mandatory contraction; verb-prep govt).
_FUNCTION = [
# articles (gender/number agreement is in morphology; these are citation)
("el", "det", "el", "los", "m", "the", "definite article m.sg"),
("la", "det", "la", "las", "f", "the", "definite article f.sg"),
("un", "det", "un", "unos","m", "a", "indefinite article m.sg"),
("una", "det", "una", "unas","f", "a", "indefinite article f.sg"),
# MANDATORY CONTRACTIONS (prep + el) — del / al
("del", "contraction", "del", "", "", "of the", "de + el (mandatory contraction)"),
("al", "contraction", "al", "", "", "to the", "a + el (mandatory contraction)"),
# demonstratives
("este", "dem", "este", "estos", "m", "this", "proximal dem m"),
("esta", "dem", "esta", "estas", "f", "this", "proximal dem f"),
# negation (SACRED — polarity never dropped)
("no", "neg", "no", "", "", "not/no", "sentential negator (preverbal)"),
("ninguno", "det", "ningún", "ninguna", "", "none", "negative determiner (apocope ningún m.sg)"),
# conjunctions
("y", "conj", "y", "e", "", "and", "coordinator (e before i-/hi-)"),
("o", "conj", "o", "u", "", "or", "coordinator (u before o-/ho-)"),
("pero", "conj", "pero", "", "", "but", "adversative coordinator"),
("que", "conj", "que", "", "", "that", "complementizer / relative"),
("si", "conj", "si", "", "", "if", "conditional subordinator"),
("porque", "conj", "porque", "", "", "because", "causal subordinator"),
("cuando", "conj", "cuando", "", "", "when", "temporal subordinator"),
# prepositions (government: verbs select these; contraction with el applies to a/de)
("a", "prep", "a", "", "", "to", "dir-obj (personal a) / dative / allative; a+el=al"),
("de", "prep", "de", "", "", "of/from", "genitive/ablative government; de+el=del"),
("en", "prep", "en", "", "", "in/on", "locative"),
("con", "prep", "con", "", "", "with", "comitative"),
("por", "prep", "por", "", "", "by/for", "passive agent / cause"),
("para", "prep", "para", "", "", "for", "purpose/benefactive"),
("contra", "prep", "contra", "", "", "against", "adversative government (protestar contra)"),
("sin", "prep", "sin", "", "", "without", "privative"),
# subject pronouns
("yo", "pron", "yo", "me", "mi", "I", "1sg subj/obj/poss"),
("", "pron", "", "te", "tu", "you", "2sg informal"),
("usted", "pron", "usted", "lo", "su", "you", "2sg formal (3sg agreement)"),
("él", "pron", "él", "lo", "su", "he", "3sg m subj/DO-clitic/poss"),
("ella", "pron", "ella", "la", "su", "she", "3sg f subj/DO-clitic/poss"),
("nosotros", "pron", "nosotros", "nos", "nuestro", "we", "1pl"),
("vosotros", "pron", "vosotros", "os", "vuestro", "you", "2pl informal"),
("ellos", "pron", "ellos", "los", "su", "they", "3pl m"),
("ellas", "pron", "ellas", "las", "su", "they", "3pl f"),
# indirect-object clitics
("le", "clitic", "le", "les", "", "to-him/her", "dative clitic 3sg/3pl (->se before lo/la)"),
("se", "clitic", "se", "se", "", "himself/-self", "reflexive / spurious-se (le+lo->se lo)"),
]
def _walk_collect(spec, verbs, nouns, adjs):
"""Recursively collect verb/noun/adj lemmas from a semantic spec."""
if isinstance(spec, dict):
if spec.get("pred"):
verbs.add(spec["pred"])
if spec.get("noun"):
nouns.add(spec["noun"])
if spec.get("adj"):
adjs.add(spec["adj"])
if spec.get("superlative"):
adjs.add(spec["superlative"])
if spec.get("from_adj"):
adjs.add(spec["from_adj"])
# adjs: [{"lemma":..,"pos":..}] | ["lemma", ..]
for a in spec.get("adjs", []) or []:
adjs.add(a["lemma"] if isinstance(a, dict) else a)
for a in spec.get("adj_coord", []) or []:
adjs.add(a["lemma"] if isinstance(a, dict) else a)
if isinstance(spec.get("pcomp"), dict):
pc = spec["pcomp"]
if pc.get("adj"):
adjs.add(pc["adj"])
for a in pc.get("adj_coord", []) or []:
adjs.add(a["lemma"] if isinstance(a, dict) else a)
for v in spec.values():
_walk_collect(v, verbs, nouns, adjs)
elif isinstance(spec, list):
for it in spec:
_walk_collect(it, verbs, nouns, adjs)
def _collect_from_specs():
verbs, nouns, adjs = set(), set(), set()
for t in TESTS + HELD:
_walk_collect(t["spec"], verbs, nouns, adjs)
return verbs, nouns, adjs
def _esc(s):
return str(s).replace('"', '\\"')
def _row(fields):
return " (" + " ".join(f'"{_esc(f)}"' for f in fields) + ")"
def emit_vocabulary(path):
v_specs, n_specs, a_specs = _collect_from_specs()
verbs = sorted(set(_CORE_VERBS) | v_specs)
nouns = sorted(set(_CORE_NOUNS) | n_specs)
adjs = sorted(set(_CORE_ADJS) | a_specs)
lines = [
";;; vocabulary-es.el — Spanish vocabulary for ELP surface realization.",
";;; Schema: (lemma pos form0 form1 form2 en_translation semantic_hint)",
";;; Source: UniMorph Spanish (github.com/unimorph/spa, CC-BY-SA 3.0),",
";;; generated by gen_elp_es.py via morphology_es_full (real forms).",
";;; Verbs: form0=present-ind-3sg form1=preterite-3sg form2=past-participle",
";;; Nouns: form0=singular form1=plural form2=REAL gender (m/f, from lexicon —",
";;; NOT an ending heuristic; this is what kills 'el mano'/'la día' errors)",
";;; Adjs : form0=masc-sg form1=fem-sg form2=masc-pl",
"",
"(vocabulary-es",
"",
" ;; -- function / closed class (incl. mandatory contractions del/al) --------",
]
for f in _FUNCTION:
lines.append(_row(f))
lines.append("")
lines.append(" ;; -- verbs (form0=pres-3sg form1=pret-3sg form2=past-participle) -----------")
for lem in verbs:
f0, c0 = M.conjugate(lem, "ind", "present", "third", "singular")
f1, c1 = M.conjugate(lem, "ind", "preterite", "third", "singular")
pp, cp = M.participle(lem)
vclass = lem[-2:] if lem[-2:] in ("ar", "er", "ir") else "ar"
irr = "irregular" if (c0 == "lexicon" and pp in M._IRREG_PART.values()) or \
lem in ("ser", "estar", "ir", "haber", "tener", "hacer", "ver",
"dar", "saber", "poder", "querer", "venir", "decir",
"poner", "salir") else "regular"
lines.append(_row([lem, "verb", f0, f1, pp, lem, vclass + "/" + irr]))
lines.append("")
lines.append(" ;; -- nouns (form0=sg form1=pl form2=REAL gender m/f) ----------------------")
for lem in nouns:
sg, _ = M.inflect_noun(lem, "singular")
pl, _ = M.inflect_noun(lem, "plural")
g = M.noun_gender(lem)
# honesty flag: did gender come from the lexicon, or a heuristic fallback?
src = "lexicon" if (lem in M._NOUNS and M._NOUNS[lem].get("g")) else "heuristic"
lines.append(_row([lem, "noun", sg, pl, g, lem, "gender:" + src]))
lines.append("")
lines.append(" ;; -- adjectives (form0=masc-sg form1=fem-sg form2=masc-pl) ----------------")
for lem in adjs:
m_sg, _ = M.inflect_adj(lem, "m", "singular")
f_sg, _ = M.inflect_adj(lem, "f", "singular")
m_pl, _ = M.inflect_adj(lem, "m", "plural")
src = "lexicon" if lem in M._ADJS else "rule"
lines.append(_row([lem, "adj", m_sg, f_sg, m_pl, lem, src]))
lines.append("")
lines.append(")")
with open(path, "w", encoding="utf-8") as fh:
fh.write("\n".join(lines) + "\n")
return len(_FUNCTION) + len(verbs) + len(nouns) + len(adjs), len(verbs), len(nouns), len(adjs)
LANG_PROFILE = ''';;; lang_profile_es.el — Spanish language profile for ELP.
;;; Keys the realizer's construction switches. Mirrors lang_profile_en / _pt.
(lang_profile_es
(language "Spanish")
(iso639 "es")
(family "Romance")
;; -- core typology flags -------------------------------------------------
(pro-drop yes) ; subjects routinely dropped; agreement carries person
(obligatory-subject no)
(grammatical-gender yes) ; m/f on every noun; article+adjective AGREE
(gender-source lexicon); REAL per-noun gender from UniMorph — NOT a heuristic
(do-support no)
(subject-aux-inversion no) ; questions by intonation/punctuation, not inversion
(question-strategy intonation)
(article-selection "el/la/los/las un/una/unos/unas")
(stressed-a-rule yes) ; fem sg noun in stressed a-/ha- takes el/un (el agua)
(adjective-position postnominal) ; default post; a few prenominal + apocope
(adjective-agreement "gender+number")
(question-punct inverted) ; opening ¿ ¡ required
;; -- MANDATORY CONTRACTIONS (coordinator quality bar) --------------------
(contractions ((de el "del") (a el "al")))
(contraction-mandatory yes) ; 'de el'/'a el' MUST surface as del/al
;; -- verb / aspect system ------------------------------------------------
(verb-classes (ar er ir))
(tenses (present preterite imperfect future conditional))
(moods (ind sbjv imp))
(finite-agreement "person+number (6 slots)")
(perfect-aux "haber") ; haber + past participle (invariant -o)
(progressive-aux "estar") ; estar + gerund
(passive-aux "ser") ; ser + participle (agrees) + por-agent
(copula-split "ser/estar") ; permanent vs stage-level
(future "infinitive + é/ás/á/emos/éis/án")
;; -- clitics / government ------------------------------------------------
(object-clitics yes) ; me te lo la le nos os los las; proclisis/enclisis
(clitic-order "se II I III (le+lo -> se lo)")
(enclisis "imperative/infinitive/gerund + accent repair (dá+me+lo->dámelo)")
(verb-prep-government yes) ; verbs select prep (protestar+contra, escapar+de)
;; -- SACRED safety bar (shared with en/pt) -------------------------------
(negation-faithful yes)) ; polarity never dropped/inverted; unplaceable -> FLAG
'''
if __name__ == "__main__":
import sys
voc_path = sys.argv[1] if len(sys.argv) > 1 else "vocabulary-es.el"
lp_path = sys.argv[2] if len(sys.argv) > 2 else "lang_profile_es.el"
total, nv, nn, na = emit_vocabulary(voc_path)
with open(lp_path, "w", encoding="utf-8") as fh:
fh.write(LANG_PROFILE)
print(f"wrote {voc_path} ({total} entries: {len(_FUNCTION)} fn, {nv} verbs, {nn} nouns, {na} adjs)")
print(f"wrote {lp_path}")
print("lexicon:", M.lexicon_stats())
-100
View File
@@ -1,100 +0,0 @@
# -*- coding: utf-8 -*-
"""gen_parity_es.py — emit an El parity program that checks the .el Spanish
morphology against the validated Python (UniMorph-backed) realizer.
Python is the ORACLE. For every lemma in the held-out inventory we embed the
Python-produced form, call the corresponding .el function, and the El program
prints PASS/FAIL per category. Aggregation is done in bash (grep -c), so no
El-side mutable counters are needed.
Categories:
Vpres verb present-ind-3sg es_conjugate(v,present,third,singular)
Vpret verb preterite-3sg es_conjugate(v,past,third,singular)
Vpart past participle es_participle(v)
Npl noun plural es_pluralize(n)
Gheur noun gender HEURISTIC es_gender(n) [exposes the bug]
Aheur def article via heuristic es_agree_article(n,true,sg) [inherits the bug]
Avocab def article via REAL g es_article_for_gender(realg,...) [the fix]
Ampl adj masc-plural es_inflect_adj(a,m,plural)
Afsg adj fem-singular es_inflect_adj(a,f,singular)
Ctr contraction del/al es_contract(prep, np)
"""
import morphology_es_full as M
import realizer_es as R
from gen_elp_es import _collect_from_specs, _CORE_VERBS, _CORE_NOUNS, _CORE_ADJS
def _esc(s):
return str(s).replace('\\', '\\\\').replace('"', '\\"')
def _check(cat, lemma, el_call, expected):
return (f' es_check("{cat}", "{_esc(lemma)}", {el_call}, "{_esc(expected)}")')
def main(out_path):
v_specs, n_specs, a_specs = _collect_from_specs()
verbs = sorted(set(_CORE_VERBS) | v_specs)
nouns = sorted(set(_CORE_NOUNS) | n_specs)
adjs = sorted(set(_CORE_ADJS) | a_specs)
lines = []
lines.append("// gen'd parity checks — Python oracle embedded, El functions called.")
lines.append("fn es_check(cat: String, lemma: String, got: String, exp: String) {")
lines.append(' if str_eq(got, exp) {')
lines.append(' println("PASS " + cat)')
lines.append(' } else {')
lines.append(' println("FAIL " + cat + " " + lemma + " got=" + got + " exp=" + exp)')
lines.append(' }')
lines.append("}")
lines.append("")
lines.append("fn es_parity() {")
# verbs
for v in verbs:
p0 = M.conjugate(v, "ind", "present", "third", "singular")[0]
p1 = M.conjugate(v, "ind", "preterite", "third", "singular")[0]
pp = M.participle(v)[0]
lines.append(_check("Vpres", v, f'es_conjugate("{_esc(v)}", "present", "third", "singular")', p0))
lines.append(_check("Vpret", v, f'es_conjugate("{_esc(v)}", "past", "third", "singular")', p1))
lines.append(_check("Vpart", v, f'es_participle("{_esc(v)}")', pp))
# nouns
for n in nouns:
pl = M.inflect_noun(n, "plural")[0]
g = M.noun_gender(n) # REAL lexicon gender
art = R._article(g, "singular", "def", n) # oracle article from real gender
lines.append(_check("Npl", n, f'es_pluralize("{_esc(n)}")', pl))
lines.append(_check("Gheur", n, f'es_gender("{_esc(n)}")', g))
lines.append(_check("Aheur", n, f'es_agree_article("{_esc(n)}", "true", "singular")', art))
lines.append(_check("Avocab", n, f'es_article_for_gender("{_esc(g)}", "{_esc(n)}", "true", "singular")', art))
# adjectives
for a in adjs:
mpl = M.inflect_adj(a, "m", "plural")[0]
fsg = M.inflect_adj(a, "f", "singular")[0]
lines.append(_check("Ampl", a, f'es_inflect_adj("{_esc(a)}", "m", "plural")', mpl))
lines.append(_check("Afsg", a, f'es_inflect_adj("{_esc(a)}", "f", "singular")', fsg))
# contractions (mandatory)
ctr_cases = [("de", "el día"), ("a", "el hombre"), ("de", "el mundo"),
("a", "el país"), ("en", "el mar"), ("de", "el año"),
("a", "la casa"), ("de", "la ciudad")]
for prep, np in ctr_cases:
exp = R._contract(prep, np)
lines.append(_check("Ctr", prep + "+" + np, f'es_contract("{_esc(prep)}", "{_esc(np)}")', exp))
lines.append("}")
lines.append("")
lines.append("fn main() {")
lines.append(" es_parity()")
lines.append("}")
with open(out_path, "w", encoding="utf-8") as fh:
fh.write("\n".join(lines) + "\n")
print(f"wrote {out_path} ({len(verbs)} verbs, {len(nouns)} nouns, {len(adjs)} adjs)")
if __name__ == "__main__":
import sys
main(sys.argv[1] if len(sys.argv) > 1 else "parity_es_checks.el")
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -81,7 +81,7 @@ jobs:
# Link to produce the engram binary
- name: Link engram binary
run: |
cc -std=c11 -O2 \
cc -std=c11 -O2 -DHAVE_CURL \
-I /usr/local/lib/el \
-o dist/engram \
dist/engram.c \
+1 -1
View File
@@ -88,7 +88,7 @@ jobs:
# Link to produce the engram binary
- name: Link engram binary
run: |
cc -std=c11 -O2 \
cc -std=c11 -O2 -DHAVE_CURL \
-I /usr/local/lib/el \
-o dist/engram \
dist/engram.c \
+1 -1
View File
@@ -62,7 +62,7 @@ jobs:
# Link to produce the engram binary
- name: Link engram binary
run: |
cc -std=c11 -O2 \
cc -std=c11 -O2 -DHAVE_CURL \
-I /usr/local/lib/el \
-o dist/engram \
dist/engram.c \
BIN
View File
Binary file not shown.
+35 -137
View File
@@ -10,8 +10,6 @@ el_val_t query_param(el_val_t path, el_val_t key);
el_val_t query_int(el_val_t path, el_val_t key, el_val_t default_val);
el_val_t extract_id(el_val_t path, el_val_t prefix);
el_val_t route_stats(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_act_stats(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_text_health(el_val_t method, el_val_t path, el_val_t body);
el_val_t persist_canonical(void);
el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_get_node(el_val_t method, el_val_t path, el_val_t body);
@@ -20,19 +18,16 @@ el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_search(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_create_edges_batch(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_neighbors(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_strengthen(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_save(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_load(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_health(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_embed_backfill(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_load_merge(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_capture_knowledge(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_similarity(el_val_t method, el_val_t path, el_val_t body);
el_val_t check_auth_ok(el_val_t method, el_val_t body);
el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body);
@@ -121,20 +116,11 @@ el_val_t route_stats(el_val_t method, el_val_t path, el_val_t body) {
return 0;
}
el_val_t route_act_stats(el_val_t method, el_val_t path, el_val_t body) {
return engram_act_stats_json();
return 0;
}
el_val_t route_text_health(el_val_t method, el_val_t path, el_val_t body) {
return engram_text_health_json();
return 0;
}
el_val_t persist_canonical(void) {
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_1 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_1 = (EL_STR("/tmp/engram")); } else { _if_result_1 = (dir_raw); } _if_result_1; });
return engram_save(el_str_concat(dir, EL_STR("/snapshot.json")));
engram_save(el_str_concat(dir, EL_STR("/snapshot.json")));
return 1;
return 0;
}
@@ -142,18 +128,9 @@ el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body) {
el_val_t content = json_get_string(body, EL_STR("content"));
el_val_t nt_raw = json_get_string(body, EL_STR("node_type"));
el_val_t node_type = ({ el_val_t _if_result_2 = 0; if (str_eq(nt_raw, EL_STR(""))) { _if_result_2 = (EL_STR("Memory")); } else { _if_result_2 = (nt_raw); } _if_result_2; });
el_val_t sal_present = json_get_raw(body, EL_STR("salience"));
el_val_t salience = ({ el_val_t _if_result_3 = 0; if (str_eq(sal_present, EL_STR(""))) { _if_result_3 = (el_from_float(0.5)); } else { _if_result_3 = (json_get_float(body, EL_STR("salience"))); } _if_result_3; });
el_val_t label_raw = json_get_string(body, EL_STR("label"));
el_val_t label = ({ el_val_t _if_result_4 = 0; if (str_eq(label_raw, EL_STR(""))) { _if_result_4 = (content); } else { _if_result_4 = (label_raw); } _if_result_4; });
el_val_t imp_present = json_get_raw(body, EL_STR("importance"));
el_val_t importance = ({ el_val_t _if_result_5 = 0; if (str_eq(imp_present, EL_STR(""))) { _if_result_5 = (el_from_float(0.5)); } else { _if_result_5 = (json_get_float(body, EL_STR("importance"))); } _if_result_5; });
el_val_t conf_present = json_get_raw(body, EL_STR("confidence"));
el_val_t confidence = ({ el_val_t _if_result_6 = 0; if (str_eq(conf_present, EL_STR(""))) { _if_result_6 = (el_from_float(1.0)); } else { _if_result_6 = (json_get_float(body, EL_STR("confidence"))); } _if_result_6; });
el_val_t tier_raw = json_get_string(body, EL_STR("tier"));
el_val_t tier = ({ el_val_t _if_result_7 = 0; if (str_eq(tier_raw, EL_STR(""))) { _if_result_7 = (EL_STR("Working")); } else { _if_result_7 = (tier_raw); } _if_result_7; });
el_val_t tags = json_get_string(body, EL_STR("tags"));
el_val_t id = engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags);
el_val_t sal_raw = json_get_float(body, EL_STR("salience"));
el_val_t salience = ({ el_val_t _if_result_3 = 0; if ((sal_raw == el_from_float(0.0))) { _if_result_3 = (el_from_float(0.5)); } else { _if_result_3 = (sal_raw); } _if_result_3; });
el_val_t id = engram_node(content, node_type, salience);
el_val_t saved = persist_canonical();
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"id\":\""), id), EL_STR("\",\"content\":\"")), content), EL_STR("\",\"node_type\":\"")), node_type), EL_STR("\"}"));
return 0;
@@ -181,7 +158,7 @@ el_val_t route_scan_nodes(el_val_t method, el_val_t path, el_val_t body) {
el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body) {
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_8 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_8 = (EL_STR("/tmp/engram")); } else { _if_result_8 = (dir_raw); } _if_result_8; });
el_val_t dir = ({ el_val_t _if_result_4 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_4 = (EL_STR("/tmp/engram")); } else { _if_result_4 = (dir_raw); } _if_result_4; });
el_val_t snap_path = el_str_concat(dir, EL_STR("/.scan-export.json"));
engram_save(snap_path);
el_val_t snap = fs_read(snap_path);
@@ -197,22 +174,22 @@ el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body) {
}
el_val_t route_search(el_val_t method, el_val_t path, el_val_t body) {
el_val_t q = ({ el_val_t _if_result_9 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_9 = (query_param(path, EL_STR("q"))); } else { _if_result_9 = (json_get_string(body, EL_STR("query"))); } _if_result_9; });
el_val_t q = ({ el_val_t _if_result_5 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_5 = (query_param(path, EL_STR("q"))); } else { _if_result_5 = (json_get_string(body, EL_STR("query"))); } _if_result_5; });
el_val_t lim_url = query_int(path, EL_STR("limit"), 0);
el_val_t lim_body = json_get_int(body, EL_STR("limit"));
el_val_t lim_either = ({ el_val_t _if_result_10 = 0; if ((lim_url > 0)) { _if_result_10 = (lim_url); } else { _if_result_10 = (lim_body); } _if_result_10; });
el_val_t limit = ({ el_val_t _if_result_11 = 0; if ((lim_either > 0)) { _if_result_11 = (lim_either); } else { _if_result_11 = (20); } _if_result_11; });
el_val_t lim_either = ({ el_val_t _if_result_6 = 0; if ((lim_url > 0)) { _if_result_6 = (lim_url); } else { _if_result_6 = (lim_body); } _if_result_6; });
el_val_t limit = ({ el_val_t _if_result_7 = 0; if ((lim_either > 0)) { _if_result_7 = (lim_either); } else { _if_result_7 = (20); } _if_result_7; });
return engram_search_json(q, limit);
return 0;
}
el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body) {
el_val_t q = ({ el_val_t _if_result_12 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_12 = (query_param(path, EL_STR("q"))); } else { _if_result_12 = (json_get_string(body, EL_STR("query"))); } _if_result_12; });
el_val_t q = ({ el_val_t _if_result_8 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_8 = (query_param(path, EL_STR("q"))); } else { _if_result_8 = (json_get_string(body, EL_STR("query"))); } _if_result_8; });
if (str_eq(q, EL_STR(""))) {
return err_json(EL_STR("missing query"));
}
el_val_t d_raw = ({ el_val_t _if_result_13 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_13 = (query_int(path, EL_STR("depth"), 3)); } else { _if_result_13 = (json_get_int(body, EL_STR("depth"))); } _if_result_13; });
el_val_t depth = ({ el_val_t _if_result_14 = 0; if ((d_raw > 0)) { _if_result_14 = (d_raw); } else { _if_result_14 = (3); } _if_result_14; });
el_val_t d_raw = ({ el_val_t _if_result_9 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_9 = (query_int(path, EL_STR("depth"), 3)); } else { _if_result_9 = (json_get_int(body, EL_STR("depth"))); } _if_result_9; });
el_val_t depth = ({ el_val_t _if_result_10 = 0; if ((d_raw > 0)) { _if_result_10 = (d_raw); } else { _if_result_10 = (3); } _if_result_10; });
return el_str_concat(el_str_concat(EL_STR("{\"results\":"), engram_activate_json(q, depth)), EL_STR("}"));
return 0;
}
@@ -221,50 +198,15 @@ el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body) {
el_val_t from_id = json_get_string(body, EL_STR("from_id"));
el_val_t to_id = json_get_string(body, EL_STR("to_id"));
el_val_t rel_raw = json_get_string(body, EL_STR("relation"));
el_val_t relation = ({ el_val_t _if_result_15 = 0; if (str_eq(rel_raw, EL_STR(""))) { _if_result_15 = (EL_STR("associates")); } else { _if_result_15 = (rel_raw); } _if_result_15; });
el_val_t w_present = json_get_raw(body, EL_STR("weight"));
el_val_t weight = ({ el_val_t _if_result_16 = 0; if (str_eq(w_present, EL_STR(""))) { _if_result_16 = (el_from_float(0.5)); } else { _if_result_16 = (json_get_float(body, EL_STR("weight"))); } _if_result_16; });
el_val_t relation = ({ el_val_t _if_result_11 = 0; if (str_eq(rel_raw, EL_STR(""))) { _if_result_11 = (EL_STR("associates")); } else { _if_result_11 = (rel_raw); } _if_result_11; });
el_val_t w_raw = json_get_float(body, EL_STR("weight"));
el_val_t weight = ({ el_val_t _if_result_12 = 0; if ((w_raw == el_from_float(0.0))) { _if_result_12 = (el_from_float(0.5)); } else { _if_result_12 = (w_raw); } _if_result_12; });
engram_connect(from_id, to_id, weight, relation);
el_val_t saved = persist_canonical();
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"from_id\":\""), from_id), EL_STR("\",\"to_id\":\"")), to_id), EL_STR("\",\"relation\":\"")), relation), EL_STR("\"}"));
return 0;
}
el_val_t route_create_edges_batch(el_val_t method, el_val_t path, el_val_t body) {
el_val_t arr = json_get_raw(body, EL_STR("edges"));
if (str_eq(arr, EL_STR(""))) {
return err_json(EL_STR("missing edges array"));
}
el_val_t n = json_array_len(arr);
if (n == 0) {
return EL_STR("{\"ok\":true,\"accepted\":0,\"skipped\":0}");
}
el_val_t i = 0;
el_val_t accepted = 0;
el_val_t skipped = 0;
while (i < n) {
el_val_t item = json_array_get(arr, i);
el_val_t from_id = json_get_string(item, EL_STR("from_id"));
el_val_t to_id = json_get_string(item, EL_STR("to_id"));
if (str_eq(from_id, EL_STR("")) || str_eq(to_id, EL_STR(""))) {
skipped = (skipped + 1);
} else {
el_val_t rel_raw = json_get_string(item, EL_STR("relation"));
el_val_t relation = ({ el_val_t _if_result_17 = 0; if (str_eq(rel_raw, EL_STR(""))) { _if_result_17 = (EL_STR("associates")); } else { _if_result_17 = (rel_raw); } _if_result_17; });
el_val_t w_present = json_get_raw(item, EL_STR("weight"));
el_val_t weight = ({ el_val_t _if_result_18 = 0; if (str_eq(w_present, EL_STR(""))) { _if_result_18 = (el_from_float(0.5)); } else { _if_result_18 = (json_get_float(item, EL_STR("weight"))); } _if_result_18; });
engram_connect(from_id, to_id, weight, relation);
accepted = (accepted + 1);
}
i = (i + 1);
}
if (accepted > 0) {
el_val_t saved = persist_canonical();
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"accepted\":"), int_to_str(accepted)), EL_STR(",\"skipped\":")), int_to_str(skipped)), EL_STR("}"));
return 0;
}
el_val_t route_neighbors(el_val_t method, el_val_t path, el_val_t body) {
el_val_t id = extract_id(path, EL_STR("/api/neighbors/"));
if (str_eq(id, EL_STR(""))) {
@@ -300,51 +242,36 @@ el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body) {
el_val_t route_save(el_val_t method, el_val_t path, el_val_t body) {
el_val_t p_raw = json_get_string(body, EL_STR("path"));
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_19 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_19 = (EL_STR("/tmp/engram")); } else { _if_result_19 = (dir_raw); } _if_result_19; });
el_val_t p = ({ el_val_t _if_result_20 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_20 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_20 = (p_raw); } _if_result_20; });
el_val_t sv = engram_save(p);
el_val_t sv_ok = ({ el_val_t _if_result_21 = 0; if ((sv == 0)) { _if_result_21 = (EL_STR("false")); } else { _if_result_21 = (EL_STR("true")); } _if_result_21; });
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":"), sv_ok), EL_STR(",\"path\":\"")), p), EL_STR("\",\"node_count\":")), int_to_str(engram_node_count())), EL_STR(",\"edge_count\":")), int_to_str(engram_edge_count())), EL_STR("}"));
el_val_t dir = ({ el_val_t _if_result_13 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_13 = (EL_STR("/tmp/engram")); } else { _if_result_13 = (dir_raw); } _if_result_13; });
el_val_t p = ({ el_val_t _if_result_14 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_14 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_14 = (p_raw); } _if_result_14; });
engram_save(p);
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"path\":\""), p), EL_STR("\"}"));
return 0;
}
el_val_t route_load(el_val_t method, el_val_t path, el_val_t body) {
el_val_t p_raw = json_get_string(body, EL_STR("path"));
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_22 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_22 = (EL_STR("/tmp/engram")); } else { _if_result_22 = (dir_raw); } _if_result_22; });
el_val_t p = ({ el_val_t _if_result_23 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_23 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_23 = (p_raw); } _if_result_23; });
el_val_t ld = engram_load(p);
el_val_t ld_ok = ({ el_val_t _if_result_24 = 0; if ((ld == 0)) { _if_result_24 = (EL_STR("false")); } else { _if_result_24 = (EL_STR("true")); } _if_result_24; });
el_val_t nc_after = engram_node_count();
el_val_t hollow = ({ el_val_t _if_result_25 = 0; if ((nc_after == 0)) { _if_result_25 = (EL_STR("true")); } else { _if_result_25 = (EL_STR("false")); } _if_result_25; });
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":"), ld_ok), EL_STR(",\"path\":\"")), p), EL_STR("\",\"node_count\":")), int_to_str(nc_after)), EL_STR(",\"edge_count\":")), int_to_str(engram_edge_count())), EL_STR(",\"hollow\":")), hollow), EL_STR("}"));
el_val_t dir = ({ el_val_t _if_result_15 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_15 = (EL_STR("/tmp/engram")); } else { _if_result_15 = (dir_raw); } _if_result_15; });
el_val_t p = ({ el_val_t _if_result_16 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_16 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_16 = (p_raw); } _if_result_16; });
engram_load(p);
return ok_json();
return 0;
}
el_val_t route_health(el_val_t method, el_val_t path, el_val_t body) {
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"status\":\"ok\",\"engine\":\"engram-runtime-native\",\"node_count\":"), int_to_str(engram_node_count())), EL_STR(",\"edge_count\":")), int_to_str(engram_edge_count())), EL_STR("}"));
return 0;
}
el_val_t route_embed_backfill(el_val_t method, el_val_t path, el_val_t body) {
el_val_t n = query_int(path, EL_STR("n"), 32);
el_val_t result = engram_embed_backfill(n);
el_val_t done = json_get_float(result, EL_STR("embedded"));
if (done > el_from_float(0.0)) {
el_val_t saved = persist_canonical();
}
return result;
return EL_STR("{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}");
return 0;
}
el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body) {
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_26 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_26 = (EL_STR("/tmp/engram")); } else { _if_result_26 = (dir_raw); } _if_result_26; });
el_val_t dir = ({ el_val_t _if_result_17 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_17 = (EL_STR("/tmp/engram")); } else { _if_result_17 = (dir_raw); } _if_result_17; });
el_val_t snap_path = el_str_concat(dir, EL_STR("/.sync-export.json"));
engram_save(snap_path);
el_val_t snap = fs_read(snap_path);
if (str_eq(snap, EL_STR(""))) {
return err_json(EL_STR("sync export failed: snapshot unreadable"));
return EL_STR("{\"nodes\":[],\"edges\":[]}");
}
return snap;
return 0;
@@ -378,7 +305,7 @@ el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body) {
el_val_t conf = el_from_float(0.8);
el_val_t id = engram_node_full(content, EL_STR("InternalStateEvent"), EL_STR("state-event"), sal, imp, conf, EL_STR("Episodic"), EL_STR("[\"internal-state\",\"InternalStateEvent\"]"));
el_val_t ret_raw = env(EL_STR("ENGRAM_ISE_RETENTION_MS"));
el_val_t ret_ms = ({ el_val_t _if_result_27 = 0; if (str_eq(ret_raw, EL_STR(""))) { _if_result_27 = (172800000); } else { _if_result_27 = (str_to_int(ret_raw)); } _if_result_27; });
el_val_t ret_ms = ({ el_val_t _if_result_18 = 0; if (str_eq(ret_raw, EL_STR(""))) { _if_result_18 = (172800000); } else { _if_result_18 = (str_to_int(ret_raw)); } _if_result_18; });
el_val_t pruned = engram_prune_telemetry(ret_ms);
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\",\"pruned\":")), int_to_str(pruned)), EL_STR("}"));
return 0;
@@ -390,21 +317,21 @@ el_val_t route_capture_knowledge(el_val_t method, el_val_t path, el_val_t body)
return err_json(EL_STR("missing content"));
}
el_val_t title = json_get_string(body, EL_STR("title"));
el_val_t label = ({ el_val_t _if_result_28 = 0; if (str_eq(title, EL_STR(""))) { _if_result_28 = (str_slice(content, 0, 60)); } else { _if_result_28 = (title); } _if_result_28; });
el_val_t label = ({ el_val_t _if_result_19 = 0; if (str_eq(title, EL_STR(""))) { _if_result_19 = (str_slice(content, 0, 60)); } else { _if_result_19 = (title); } _if_result_19; });
el_val_t category_raw = json_get_string(body, EL_STR("category"));
el_val_t category = ({ el_val_t _if_result_29 = 0; if (str_eq(category_raw, EL_STR(""))) { _if_result_29 = (EL_STR("other")); } else { _if_result_29 = (category_raw); } _if_result_29; });
el_val_t category = ({ el_val_t _if_result_20 = 0; if (str_eq(category_raw, EL_STR(""))) { _if_result_20 = (EL_STR("other")); } else { _if_result_20 = (category_raw); } _if_result_20; });
el_val_t ktier_raw = json_get_string(body, EL_STR("tier"));
el_val_t ktier = ({ el_val_t _if_result_30 = 0; if (str_eq(ktier_raw, EL_STR(""))) { _if_result_30 = (EL_STR("note")); } else { _if_result_30 = (ktier_raw); } _if_result_30; });
el_val_t ktier = ({ el_val_t _if_result_21 = 0; if (str_eq(ktier_raw, EL_STR(""))) { _if_result_21 = (EL_STR("note")); } else { _if_result_21 = (ktier_raw); } _if_result_21; });
el_val_t project = json_get_string(body, EL_STR("project"));
el_val_t tags_raw = json_get_raw(body, EL_STR("tags"));
el_val_t tags_base = ({ el_val_t _if_result_31 = 0; if (str_eq(tags_raw, EL_STR(""))) { _if_result_31 = (EL_STR("[]")); } else { _if_result_31 = (tags_raw); } _if_result_31; });
el_val_t tags_base = ({ el_val_t _if_result_22 = 0; if (str_eq(tags_raw, EL_STR(""))) { _if_result_22 = (EL_STR("[]")); } else { _if_result_22 = (tags_raw); } _if_result_22; });
el_val_t base_len = str_len(tags_base);
el_val_t head = str_slice(tags_base, 0, (base_len - 1));
el_val_t sep = ({ el_val_t _if_result_32 = 0; if (str_eq(head, EL_STR("["))) { _if_result_32 = (EL_STR("")); } else { _if_result_32 = (EL_STR(",")); } _if_result_32; });
el_val_t sep = ({ el_val_t _if_result_23 = 0; if (str_eq(head, EL_STR("["))) { _if_result_23 = (EL_STR("")); } else { _if_result_23 = (EL_STR(",")); } _if_result_23; });
el_val_t safe_cat = str_replace(category, EL_STR("\""), EL_STR("'"));
el_val_t safe_tier = str_replace(ktier, EL_STR("\""), EL_STR("'"));
el_val_t safe_proj = str_replace(project, EL_STR("\""), EL_STR("'"));
el_val_t proj_tag = ({ el_val_t _if_result_33 = 0; if (str_eq(safe_proj, EL_STR(""))) { _if_result_33 = (EL_STR("")); } else { _if_result_33 = (el_str_concat(el_str_concat(EL_STR(",\"project:"), safe_proj), EL_STR("\""))); } _if_result_33; });
el_val_t proj_tag = ({ el_val_t _if_result_24 = 0; if (str_eq(safe_proj, EL_STR(""))) { _if_result_24 = (EL_STR("")); } else { _if_result_24 = (el_str_concat(el_str_concat(EL_STR(",\"project:"), safe_proj), EL_STR("\""))); } _if_result_24; });
el_val_t tags = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(head, sep), EL_STR("\"category:")), safe_cat), EL_STR("\",\"tier:")), safe_tier), EL_STR("\"")), proj_tag), EL_STR("]"));
el_val_t sal = el_from_float(0.5);
el_val_t imp = el_from_float(0.5);
@@ -415,20 +342,6 @@ el_val_t route_capture_knowledge(el_val_t method, el_val_t path, el_val_t body)
return 0;
}
el_val_t route_similarity(el_val_t method, el_val_t path, el_val_t body) {
el_val_t a = query_param(path, EL_STR("a"));
el_val_t b = query_param(path, EL_STR("b"));
if (str_eq(a, EL_STR(""))) {
return err_json(EL_STR("missing a"));
}
if (str_eq(b, EL_STR(""))) {
return err_json(EL_STR("missing b"));
}
el_val_t sim = engram_cosine_sim(a, b);
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"a\":\""), a), EL_STR("\",\"b\":\"")), b), EL_STR("\",\"cosine\":")), float_to_str(sim)), EL_STR("}"));
return 0;
}
el_val_t check_auth_ok(el_val_t method, el_val_t body) {
el_val_t key = env(EL_STR("ENGRAM_API_KEY"));
if (str_eq(key, EL_STR(""))) {
@@ -464,12 +377,6 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/stats")) || str_eq(clean, EL_STR("/stats")))) {
return route_stats(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/act-stats")) || str_eq(clean, EL_STR("/act-stats")))) {
return route_act_stats(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/text-health")) || str_eq(clean, EL_STR("/text-health")))) {
return route_text_health(method, path, body);
}
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/nodes")) || str_eq(clean, EL_STR("/nodes")))) {
return route_create_node(method, path, body);
}
@@ -488,9 +395,6 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/edges")) || str_eq(clean, EL_STR("/edges")))) {
return route_create_edge(method, path, body);
}
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/edges/batch")) || str_eq(clean, EL_STR("/edges/batch")))) {
return route_create_edges_batch(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/neighbors/"))) {
return route_neighbors(method, path, body);
}
@@ -521,12 +425,6 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
if (str_eq(method, EL_STR("GET")) && str_eq(clean, EL_STR("/api/sync"))) {
return route_sync(method, path, body);
}
if (str_eq(clean, EL_STR("/api/embed-backfill"))) {
return route_embed_backfill(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/similarity"))) {
return route_similarity(method, path, body);
}
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"not found\",\"path\":\""), clean), EL_STR("\"}"));
return 0;
}
@@ -534,10 +432,10 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
int main(int _argc, char** _argv) {
el_runtime_init_args(_argc, _argv);
bind_raw = env(EL_STR("ENGRAM_BIND"));
bind_str = ({ el_val_t _if_result_34 = 0; if (str_eq(bind_raw, EL_STR(""))) { _if_result_34 = (EL_STR(":8742")); } else { _if_result_34 = (bind_raw); } _if_result_34; });
bind_str = ({ el_val_t _if_result_25 = 0; if (str_eq(bind_raw, EL_STR(""))) { _if_result_25 = (EL_STR(":8742")); } else { _if_result_25 = (bind_raw); } _if_result_25; });
port = parse_port(bind_str);
data_dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
data_dir = ({ el_val_t _if_result_35 = 0; if (str_eq(data_dir_raw, EL_STR(""))) { _if_result_35 = (EL_STR("/tmp/engram")); } else { _if_result_35 = (data_dir_raw); } _if_result_35; });
data_dir = ({ el_val_t _if_result_26 = 0; if (str_eq(data_dir_raw, EL_STR(""))) { _if_result_26 = (EL_STR("/tmp/engram")); } else { _if_result_26 = (data_dir_raw); } _if_result_26; });
snapshot_path = el_str_concat(data_dir, EL_STR("/snapshot.json"));
engram_load(snapshot_path);
boot_snap = fs_read(snapshot_path);
+13 -238
View File
@@ -76,38 +76,6 @@ fn route_stats(method: String, path: String, body: String) -> String {
engram_stats_json()
}
// route_act_stats GET /api/act-stats
// (2026-08-04 self-review) engram_act_stats_json() has existed since the
// 2026-07-27 review but was reachable ONLY through the soul daemon's heartbeat
// binding. Every activation-layer gauge WM evictions, breakthroughs, embedder
// breaker state, context drift, and now the Hebbian counters was therefore
// invisible unless the soul happened to be running and its ISEs were read back
// out of the store. Diagnosing the activation layer required a working soul,
// which is exactly backwards: the lower layer should be observable on its own.
// This review needed it to verify link formation and could not get at it. One
// line of plumbing, and the whole activation layer becomes directly diagnosable.
fn route_act_stats(method: String, path: String, body: String) -> String {
engram_act_stats_json()
}
// route_text_health GET /api/text-health
// (2026-08-08 self-review) The daily census half of the text-integrity gauge.
// Today's review found that the JSON parser had been replacing every \uXXXX
// escape with a literal '?' for at least two months: 3,119 of 4,081
// non-telemetry nodes (76%) were damaged, including the self traversal root
// and every values node, and NOTHING detected it because every gauge in the
// system measured whether the machinery was running, and none measured whether
// the text it carried was intact. No snapshot on disk predates the damage, so
// it cannot be undone; it can only be made impossible to repeat quietly.
//
// The parser is fixed. This route is the standing check: `damaged` should now
// hold flat at its historical floor and never climb. `write_damaged` (also on
// the heartbeat as txt_damaged) is the live regression signal non-zero means
// a write path is mangling text right now.
fn route_text_health(method: String, path: String, body: String) -> String {
engram_text_health_json()
}
// (2026-07-18 self-review) Scoping sweep: `let` inside an if-block creates an
// inner scope only it does NOT mutate the outer binding (documented with
// evidence in awareness.el, 2026-05-25). Every default/reassignment below used
@@ -133,54 +101,17 @@ fn route_text_health(method: String, path: String, body: String) -> String {
fn persist_canonical() -> Int {
let dir_raw: String = env("ENGRAM_DATA_DIR")
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
// (2026-08-10 self-review) This returned a hardcoded 1, which made every
// caller's `let saved: Int = persist_canonical()` a dead variable six
// durable write paths each believed they had confirmation of a successful
// canonical persist and none of them had any. Propagate the real result.
return engram_save(dir + "/snapshot.json")
engram_save(dir + "/snapshot.json")
return 1
}
// INCOMPLETE-ROUTE FIX (2026-07-24 self-review): this route silently dropped
// label, importance, tier, and tags engram_node() defaults label to content
// and importance to 0.5, so every node created over HTTP lost its metadata.
// Observed live: the soul's boot-counter write-back landed with
// label="soul:boot_count:99" (content), importance 0.5, no tags. Honor the
// full field set via engram_node_full when any of them is supplied.
// PRESENCE-AWARE DEFAULTS (2026-08-01 self-review): the old pattern
// `if x == 0.0 { default }` made a legitimate 0.0 unrepresentable a caller
// setting salience/importance/weight to zero silently got 0.5. json_get_raw
// returns "" when the key is ABSENT and the raw token when present, so
// absence and zero are now distinguishable. Also: confidence was hardcoded
// to 1.0 regardless of input every HTTP-created node claimed full
// epistemic confidence. Now honored from the payload (default 1.0).
fn route_create_node(method: String, path: String, body: String) -> String {
let content: String = json_get_string(body, "content")
let nt_raw: String = json_get_string(body, "node_type")
let node_type: String = if str_eq(nt_raw, "") { "Memory" } else { nt_raw }
let sal_present: String = json_get_raw(body, "salience")
let salience: Float = if str_eq(sal_present, "") { 0.5 } else { json_get_float(body, "salience") }
let label_raw: String = json_get_string(body, "label")
let label: String = if str_eq(label_raw, "") { content } else { label_raw }
let imp_present: String = json_get_raw(body, "importance")
let importance: Float = if str_eq(imp_present, "") { 0.5 } else { json_get_float(body, "importance") }
let conf_present: String = json_get_raw(body, "confidence")
let confidence: Float = if str_eq(conf_present, "") { 1.0 } else { json_get_float(body, "confidence") }
let tier_raw: String = json_get_string(body, "tier")
let tier: String = if str_eq(tier_raw, "") { "Working" } else { tier_raw }
let tags: String = json_get_string(body, "tags")
// NO el_from_float WRAPPER (2026-08-01 self-review): salience/importance/
// confidence are already Float (el_val_t) values json_get_float and
// Float literals both encode. Wrapping them in el_from_float AGAIN
// reinterpreted the boxed bits as a raw double, producing garbage that
// failed engram_decode_score's range check and clamped every HTTP-created
// node to defaults (salience 0.9 in 0.5 stored; confidence 0.6 in → 1.0
// stored verified live). route_emit_ise always passed Floats bare and
// its 0.3/0.3/0.8 stored correctly; this call now does the same.
let id: String = engram_node_full(
content, node_type, label,
salience, importance, confidence,
tier, tags
)
let sal_raw: Float = json_get_float(body, "salience")
let salience: Float = if sal_raw == 0.0 { 0.5 } else { sal_raw }
let id: String = engram_node(content, node_type, salience)
let saved: Int = persist_canonical()
"{\"id\":\"" + id + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\"}"
}
@@ -246,64 +177,13 @@ fn route_create_edge(method: String, path: String, body: String) -> String {
let to_id: String = json_get_string(body, "to_id")
let rel_raw: String = json_get_string(body, "relation")
let relation: String = if str_eq(rel_raw, "") { "associates" } else { rel_raw }
// Presence-aware (2026-08-01): weight 0.0 is a legitimate edge weight
// (dormant association); only default when the key is absent.
let w_present: String = json_get_raw(body, "weight")
let weight: Float = if str_eq(w_present, "") { 0.5 } else { json_get_float(body, "weight") }
let w_raw: Float = json_get_float(body, "weight")
let weight: Float = if w_raw == 0.0 { 0.5 } else { w_raw }
engram_connect(from_id, to_id, weight, relation)
let saved: Int = persist_canonical()
"{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + relation + "\"}"
}
// route_create_edges_batch POST /api/edges/batch {"edges":[{from_id,to_id,relation,weight}, ...]}
//
// WHY THIS EXISTS (2026-08-07 self-review). persist_canonical() writes the
// FULL canonical snapshot 60MB at current graph size and route_create_edge
// calls it once per edge. That is correct for the interactive one-edge case and
// ruinous for any bulk write: the soul's Hebbian consolidation path delivers
// ~14 associations per 8-minute heartbeat, which through the single-edge route
// would be ~840MB of disk writes per beat, ~150GB/day, to persist 14 edges.
//
// The fix is not to weaken durability it is to make the unit of durability
// the BATCH. Connect every edge, then snapshot exactly once. Same guarantee
// (nothing acknowledged is lost to a restart), 1/N the writes. Empty or
// malformed entries are skipped rather than aborting the batch: a consolidation
// payload is best-effort by design, and one bad id should not cost the other 13.
//
// Returns the accepted count so the caller can tell delivery from silence.
fn route_create_edges_batch(method: String, path: String, body: String) -> String {
let arr: String = json_get_raw(body, "edges")
if str_eq(arr, "") { return err_json("missing edges array") }
let n: Int = json_array_len(arr)
if n == 0 { return "{\"ok\":true,\"accepted\":0,\"skipped\":0}" }
let i: Int = 0
let accepted: Int = 0
let skipped: Int = 0
while i < n {
let item: String = json_array_get(arr, i)
let from_id: String = json_get_string(item, "from_id")
let to_id: String = json_get_string(item, "to_id")
if str_eq(from_id, "") || str_eq(to_id, "") {
let skipped = skipped + 1
} else {
let rel_raw: String = json_get_string(item, "relation")
let relation: String = if str_eq(rel_raw, "") { "associates" } else { rel_raw }
let w_present: String = json_get_raw(item, "weight")
let weight: Float = if str_eq(w_present, "") { 0.5 } else { json_get_float(item, "weight") }
engram_connect(from_id, to_id, weight, relation)
let accepted = accepted + 1
}
let i = i + 1
}
// ONE snapshot for the whole batch the entire point of this route.
// Skip it when nothing was accepted: an all-malformed payload must not
// trigger a 60MB write.
if accepted > 0 {
let saved: Int = persist_canonical()
}
return "{\"ok\":true,\"accepted\":" + int_to_str(accepted) + ",\"skipped\":" + int_to_str(skipped) + "}"
}
fn route_neighbors(method: String, path: String, body: String) -> String {
let id: String = extract_id(path, "/api/neighbors/")
if str_eq(id, "") { return err_json("missing id") }
@@ -332,15 +212,8 @@ fn route_save(method: String, path: String, body: String) -> String {
let dir_raw: String = env("ENGRAM_DATA_DIR")
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw }
// (2026-08-10 self-review) engram_save returns 0 on an empty path and the
// route discarded it, so the response was a literal "ok":true regardless
// of whether anything was written. Report the actual result AND the counts
// that were supposed to have been written the same move that made
// route_health honest on 2026-08-01. A caller can now tell "saved 13k
// nodes" from "saved nothing and said ok".
let sv: Int = engram_save(p)
let sv_ok: String = if sv == 0 { "false" } else { "true" }
"{\"ok\":" + sv_ok + ",\"path\":\"" + p + "\",\"node_count\":" + int_to_str(engram_node_count()) + ",\"edge_count\":" + int_to_str(engram_edge_count()) + "}"
engram_save(p)
"{\"ok\":true,\"path\":\"" + p + "\"}"
}
fn route_load(method: String, path: String, body: String) -> String {
@@ -348,59 +221,12 @@ fn route_load(method: String, path: String, body: String) -> String {
let dir_raw: String = env("ENGRAM_DATA_DIR")
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw }
// (2026-08-10 self-review) This was a stub response over the single most
// destructive operation in the server. engram_load returns 0 on an empty
// path, an unopenable file, a zero-length file, or malloc failure and
// this route answered ok_json() in every one of those cases.
//
// Precise failure shape (el_runtime.c:9890): the fopen guard runs BEFORE
// the store reset, so a MISSING path is genuinely safe it returns 0 with
// the graph intact. The dangerous case is a readable-but-malformed file:
// the reset loop frees every node and edge FIRST, then parses, so a
// truncated or non-snapshot JSON leaves a hollow store and the caller
// was told "ok":true. With 37 GB of stale dated snapshots sitting in the
// data dir as tempting restore targets, "restore reported success and
// silently emptied the graph" is a live risk, not a hypothetical one.
//
// Fix: surface the return value AND the resulting counts. node_count=0
// after a load is the unambiguous hollow-store signal (same convention
// route_health adopted 2026-08-01). Callers can now verify a restore
// instead of trusting it.
let ld: Int = engram_load(p)
let ld_ok: String = if ld == 0 { "false" } else { "true" }
let nc_after: Int = engram_node_count()
let hollow: String = if nc_after == 0 { "true" } else { "false" }
"{\"ok\":" + ld_ok + ",\"path\":\"" + p + "\",\"node_count\":" + int_to_str(nc_after) + ",\"edge_count\":" + int_to_str(engram_edge_count()) + ",\"hollow\":" + hollow + "}"
engram_load(p)
ok_json()
}
// (2026-08-01 self-review) Health previously returned a hardcoded literal
// it reported "ok" even when the snapshot failed to load and the store was
// empty. Now reports live counts so a monitor can distinguish "up and
// loaded" from "up and hollow" (node_count=0 after boot = failed load).
fn route_health(method: String, path: String, body: String) -> String {
"{\"status\":\"ok\",\"engine\":\"engram-runtime-native\",\"node_count\":" + int_to_str(engram_node_count()) + ",\"edge_count\":" + int_to_str(engram_edge_count()) + "}"
}
// route_embed_backfill GET/POST /api/embed-backfill?n=48
//
// (2026-07-25 self-review) The lazy embedding backfill runs only inside
// engram_activate, and nothing in production calls /api/activate on this
// store the soul's curiosity loop activates its own in-process graph.
// After a restart from a snapshot without vectors, embedded_count stalled
// at 93/12175 and would never recover. This route lets the soul's
// heartbeat pump the backfill explicitly (48/min clears a 12k backlog in
// ~4h). Persists the canonical snapshot whenever new vectors were
// generated the 2026-07-25 regression happened precisely because 3747
// in-RAM embeddings were never snapshotted before a restart. Self-
// limiting: once coverage is full, embedded=0 and no save occurs.
fn route_embed_backfill(method: String, path: String, body: String) -> String {
let n: Int = query_int(path, "n", 32)
let result: String = engram_embed_backfill(n)
let done: Float = json_get_float(result, "embedded")
if done > 0.0 {
let saved: Int = persist_canonical()
}
return result
"{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}"
}
// route_sync return a snapshot of non-ISE/non-Working nodes for the soul daemon
@@ -423,16 +249,7 @@ fn route_sync(method: String, path: String, body: String) -> String {
let snap_path: String = dir + "/.sync-export.json"
engram_save(snap_path)
let snap: String = fs_read(snap_path)
// 2026-08-02 self-review: this used to return {"nodes":[],"edges":[]} when
// the export/read failed. The soul's sync_ok test (awareness.el) only
// checks for "" and "{}", so that placeholder PASSED as a healthy sync:
// soul.last_sync_ok_ts got stamped, sync_age_ms stayed green, the
// sync_empty warn ISE never fired, and engram_sync reported added:0
// forever. A totally broken sync was indistinguishable from a quiet
// healthy one the exact failure class this route was added to fix in
// the first place (see 2026-06-27 note above). Return a real error so the
// failure is loud on both sides.
if str_eq(snap, "") { return err_json("sync export failed: snapshot unreadable") }
if str_eq(snap, "") { return "{\"nodes\":[],\"edges\":[]}" }
return snap
}
@@ -554,25 +371,6 @@ fn route_capture_knowledge(method: String, path: String, body: String) -> String
"{\"ok\":true,\"id\":\"" + id + "\"}"
}
// route_similarity GET /api/similarity?a=<id>&b=<id>
//
// (2026-08-01 self-review) engram_cosine_sim was added 2026-07-24
// (bl-b2d1c944) with the stated purpose of exposing semantic distance to
// "EL code and the introspection API" but it had ZERO callers anywhere:
// no route, no soul-daemon use. The activation path uses embeddings
// internally (semantic seeding, Pass-2 additive term), but there was no way
// to probe pairwise node similarity from outside. This closes that: cosine
// in [-1,1], or -2 when either node is missing or not yet embedded (so
// "not comparable" is distinguishable from "genuinely orthogonal" 0.0).
fn route_similarity(method: String, path: String, body: String) -> String {
let a: String = query_param(path, "a")
let b: String = query_param(path, "b")
if str_eq(a, "") { return err_json("missing a") }
if str_eq(b, "") { return err_json("missing b") }
let sim: Float = engram_cosine_sim(a, b)
"{\"a\":\"" + a + "\",\"b\":\"" + b + "\",\"cosine\":" + float_to_str(sim) + "}"
}
// Auth
fn check_auth_ok(method: String, body: String) -> Bool {
@@ -619,12 +417,6 @@ fn handle_request(method: String, path: String, body: String) -> String {
if str_eq(method, "GET") && (str_eq(clean, "/api/stats") || str_eq(clean, "/stats")) {
return route_stats(method, path, body)
}
if str_eq(method, "GET") && (str_eq(clean, "/api/act-stats") || str_eq(clean, "/act-stats")) {
return route_act_stats(method, path, body)
}
if str_eq(method, "GET") && (str_eq(clean, "/api/text-health") || str_eq(clean, "/text-health")) {
return route_text_health(method, path, body)
}
// Nodes
if str_eq(method, "POST") && (str_eq(clean, "/api/nodes") || str_eq(clean, "/nodes")) {
@@ -647,13 +439,6 @@ fn handle_request(method: String, path: String, body: String) -> String {
if str_eq(method, "POST") && (str_eq(clean, "/api/edges") || str_eq(clean, "/edges")) {
return route_create_edge(method, path, body)
}
// Batch edge write one snapshot for the whole payload. Must be tested
// BEFORE nothing else claims it; the exact-match on "/api/edges" above
// does not catch "/api/edges/batch", so order is not load-bearing here,
// but keeping the two adjacent keeps them from drifting apart.
if str_eq(method, "POST") && (str_eq(clean, "/api/edges/batch") || str_eq(clean, "/edges/batch")) {
return route_create_edges_batch(method, path, body)
}
if str_eq(method, "GET") && str_starts_with(clean, "/api/neighbors/") {
return route_neighbors(method, path, body)
}
@@ -693,16 +478,6 @@ fn handle_request(method: String, path: String, body: String) -> String {
return route_sync(method, path, body)
}
// Embedding backfill pumped by the soul heartbeat (2026-07-25)
if str_eq(clean, "/api/embed-backfill") {
return route_embed_backfill(method, path, body)
}
// Semantic similarity probe (2026-08-01)
if str_eq(method, "GET") && str_starts_with(clean, "/api/similarity") {
return route_similarity(method, path, body)
}
"{\"error\":\"not found\",\"path\":\"" + clean + "\"}"
}
+10
View File
@@ -17,6 +17,16 @@
// 4. Append dep to order after all its transitive deps
// 5. Deduplicate: skip already-ordered vessels
// Cross-module forward declarations
// Defined in sibling epm modules; resolved at link time. The `extern fn` decls
// give elc the C prototypes so generated install.c compiles cleanly under strict
// compilers (gcc>=14 / clang) that reject implicit function declarations.
extern fn manifest_name(src: String) -> String // manifest.el
extern fn manifest_deps(src: String) -> String // manifest.el
extern fn registry_token() -> String // registry.el
extern fn registry_find(name: String, version: String) -> String // registry.el
extern fn registry_latest_version(name: String) -> String // registry.el
// Install paths
// packages_dir returns the root directory for installed vessels.
+9
View File
@@ -14,6 +14,15 @@
// EPM_REGISTRY_ORG org name that hosts vessel repos (default: neuron-technologies)
// EPM_TOKEN Gitea personal access token (required for publish)
// Cross-module forward declarations
// These symbols are defined in sibling epm modules or the El runtime and are
// resolved at link time. The `extern fn` decls give elc the C prototype so the
// generated registry.c compiles cleanly under strict compilers (gcc>=14 / clang)
// that reject implicit function declarations. Signature arity must match the
// definition; return/param types are informational (all lower to el_val_t).
extern fn config(key: String) -> String // El runtime builtin
extern fn read_installed() -> String // install.el
// Config helpers
// registry_api_url returns the Gitea API base URL with no trailing slash.
+9
View File
@@ -6,6 +6,15 @@
// Depends on: registry.el (registry_latest_version, registry_find),
// install.el (read_installed, install_vessel, installed_version)
// Cross-module forward declarations
// Defined in sibling epm modules; resolved at link time. The `extern fn` decls
// give elc the C prototypes so generated update.c compiles cleanly under strict
// compilers (gcc>=14 / clang) that reject implicit function declarations.
extern fn read_installed() -> String // install.el
extern fn installed_version(name: String) -> String // install.el
extern fn install_vessel(name: String, version: String) -> Bool // install.el
extern fn registry_latest_version(name: String) -> String // registry.el
// Semver helpers
// semver_part extracts the Nth dot-separated component from a semver string.
@@ -75,6 +75,7 @@ static inline void* el_win_dlsym(void* handle, const char* name) {
#include <direct.h> /* _mkdir */
#define mkdir(path, mode) _mkdir(path) /* POSIX mkdir(path,mode) → _mkdir(path) */
#define timegm _mkgmtime /* UTC tm → time_t */
#define fsync(fd) _commit(fd) /* no fsync() on Windows; _commit() (<io.h>) is the equiv */
/* setenv/unsetenv: not in the Windows CRT; map to _putenv_s / SetEnvironmentVariable. */
static inline int setenv(const char* name, const char* value, int overwrite) {
File diff suppressed because it is too large Load Diff
+1
View File
@@ -1072,6 +1072,7 @@ el_val_t __engram_save(el_val_t path) { return engram_save
el_val_t __engram_load(el_val_t path) { return engram_load(path); }
el_val_t __engram_get_node_json(el_val_t id) { return engram_get_node_json(id); }
el_val_t __engram_get_node_by_label(el_val_t label) { return engram_get_node_by_label(label); }
el_val_t __engram_search_json(el_val_t query, el_val_t limit) {
return engram_search_json(query, limit);
+1
View File
@@ -226,6 +226,7 @@ el_val_t __engram_activate(el_val_t query, el_val_t depth);
el_val_t __engram_save(el_val_t path);
el_val_t __engram_load(el_val_t path);
el_val_t __engram_get_node_json(el_val_t id);
el_val_t __engram_get_node_by_label(el_val_t label);
el_val_t __engram_search_json(el_val_t query, el_val_t limit);
el_val_t __engram_scan_nodes_json(el_val_t limit, el_val_t offset);
el_val_t __engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
+1
View File
@@ -2670,6 +2670,7 @@ fn builtin_arity(name: String) -> Int {
if str_eq(name, "engram_save") { return 1 }
if str_eq(name, "engram_load") { return 1 }
if str_eq(name, "engram_get_node_json") { return 1 }
if str_eq(name, "engram_get_node_by_label") { return 1 }
if str_eq(name, "engram_search_json") { return 2 }
if str_eq(name, "engram_scan_nodes_json") { return 2 }
if str_eq(name, "engram_neighbors_json") { return 3 }
@@ -0,0 +1,186 @@
#ifndef EL_PLATFORM_WIN_H
#define EL_PLATFORM_WIN_H
/*
* el_platform_win.h Windows OS-boundary shim for el_runtime.c.
*
* Branch: feat/windows-el-runtime. Included ONLY when _WIN32 is defined; the POSIX build is
* untouched. Goal: let el_runtime.c (a BSD-sockets / dlfcn / fork host) compile and link with
* mingw-w64 into a native neuron.exe, with no behavioural change to the Linux/macOS build.
*
* What it maps:
* - sockets : winsock2 (same call names: socket/bind/listen/accept/recv/send/setsockopt).
* Sockets close with closesocket() (see el_closesocket), and the stack must be
* started once with WSAStartup done automatically via a load-time constructor.
* - dlsym : el_runtime.c uses dlsym(RTLD_DEFAULT, name) to resolve callback/tool symbols
* exported by the main module. Windows equivalent: GetProcAddress on the process
* module. Link the soul with -Wl,--export-all-symbols so the symbols are findable.
* - popen : mapped to _popen/_pclose.
* - threads : UNCHANGED. mingw-w64 ships winpthreads, so <pthread.h> + -lpthread just work.
*/
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <io.h>
#include <process.h>
/* Portable headers mingw-w64 provides (verified present). */
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h> /* strcasecmp */
#include <ctype.h>
#include <math.h>
#include <time.h>
#include <sys/time.h> /* mingw-w64 provides gettimeofday here */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <errno.h>
#include <pthread.h>
/* ── socket close ─────────────────────────────────────────────────────────── */
/* Winsock closes sockets with closesocket(), not close() (close() is for file fds). The POSIX
build defines the same helper as close() so the call sites are identical across platforms. */
static inline int el_closesocket(SOCKET s) { return closesocket(s); }
/* ── setsockopt optval type ───────────────────────────────────────────────── */
/* Winsock's setsockopt takes optval as (const char*); POSIX takes (const void*), so el_runtime.c
passes &int directly. GCC 14+ makes that an error under -Wincompatible-pointer-types. Wrap it so
the runtime's POSIX-style call sites compile unchanged (defined before the macro so the wrapper
itself resolves to the real winsock setsockopt). */
static inline int el_setsockopt(SOCKET s, int level, int optname, const void* optval, int optlen) {
return setsockopt(s, level, optname, (const char*)optval, optlen);
}
#define setsockopt(s, l, o, v, n) el_setsockopt((s), (l), (o), (v), (int)(n))
/* ── winsock init (once, at load) ─────────────────────────────────────────── */
static void el__win_net_init(void) {
static int inited = 0;
if (!inited) { WSADATA w; WSAStartup(MAKEWORD(2, 2), &w); inited = 1; }
}
__attribute__((constructor)) static void el__win_ctor(void) { el__win_net_init(); }
/* ── dlsym → GetProcAddress ───────────────────────────────────────────────── */
#ifndef RTLD_DEFAULT
#define RTLD_DEFAULT ((void*)0)
#endif
static inline void* el_win_dlsym(void* handle, const char* name) {
(void)handle;
return (void*)(uintptr_t)GetProcAddress(GetModuleHandleA(NULL), name);
}
#define dlsym(h, n) el_win_dlsym((h), (n))
/* ── popen / pclose ───────────────────────────────────────────────────────── */
#define popen _popen
#define pclose _pclose
/* ── misc POSIX → Win32 shims ─────────────────────────────────────────────── */
#include <direct.h> /* _mkdir */
#define mkdir(path, mode) _mkdir(path) /* POSIX mkdir(path,mode) → _mkdir(path) */
#define timegm _mkgmtime /* UTC tm → time_t */
/* setenv/unsetenv: not in the Windows CRT; map to _putenv_s / SetEnvironmentVariable. */
static inline int setenv(const char* name, const char* value, int overwrite) {
(void)overwrite;
return _putenv_s(name, value ? value : "");
}
static inline int unsetenv(const char* name) {
/* _putenv_s(name, "") sets VAR="" rather than removing it.
* SetEnvironmentVariableA(name, NULL) truly deletes it from the Win32
* env block; then we sync the CRT cache with _putenv("NAME="). */
SetEnvironmentVariableA(name, NULL);
size_t len = strlen(name);
char *buf = (char*)malloc(len + 2);
if (!buf) return -1;
memcpy(buf, name, len);
buf[len] = '=';
buf[len + 1] = '\0';
_putenv(buf);
free(buf);
return 0;
}
/* nanosleep — not available in MSVC/UCRT; approximate with Sleep(). */
static inline int el_nanosleep(const struct timespec *req, struct timespec *rem) {
(void)rem;
DWORD ms = (DWORD)((req->tv_sec * 1000ULL) + (req->tv_nsec / 1000000ULL));
Sleep(ms ? ms : 1);
return 0;
}
#define nanosleep(req, rem) el_nanosleep((req), (rem))
/* localtime_r/gmtime_r: Windows offers localtime_s/gmtime_s with reversed arg order. */
static inline struct tm* localtime_r(const time_t* t, struct tm* out) {
return localtime_s(out, t) == 0 ? out : (struct tm*)0;
}
static inline struct tm* gmtime_r(const time_t* t, struct tm* out) {
return gmtime_s(out, t) == 0 ? out : (struct tm*)0;
}
/* ── libcurl: degradable stubs for the curl-less Windows build ─────────────── */
/* The curl-less validation build (WITH_CURL=0) links no libcurl. el_runtime.c uses libcurl
* unconditionally for its HTTP client / LLM layer; these stubs let it compile and link so the
* runtime, HTTP *server*, graph and memory work natively on Windows. Live outbound HTTP/LLM calls
* degrade to a runtime error (curl_easy_perform returns an error) matching the documented
* curl-less contract. When HAVE_CURL is defined (WITH_CURL=1) the real <curl/curl.h> is used and
* this whole block is compiled out. POSIX never sees this header, so the POSIX build is untouched. */
#ifndef HAVE_CURL
typedef void CURL;
typedef int CURLcode;
#define CURLE_OK 0
#define CURLE_HTTP_RETURNED_ERROR 22
#define CURL_ERROR_SIZE 256
/* Option ids: values are irrelevant to the no-op setopt below; kept distinct for readability. */
#define CURLOPT_URL 10002
#define CURLOPT_WRITEFUNCTION 20011
#define CURLOPT_WRITEDATA 10001
#define CURLOPT_POSTFIELDS 10015
#define CURLOPT_POSTFIELDSIZE 120
#define CURLOPT_POST 47
#define CURLOPT_HTTPHEADER 10023
#define CURLOPT_TIMEOUT_MS 155
#define CURLOPT_NOSIGNAL 99
#define CURLOPT_USERAGENT 10018
#define CURLOPT_FOLLOWLOCATION 52
#define CURLOPT_ERRORBUFFER 10010
#define CURLOPT_CUSTOMREQUEST 10036
#define CURLOPT_FAILONERROR 45
struct curl_slist { char* data; struct curl_slist* next; };
static inline struct curl_slist* curl_slist_append(struct curl_slist* list, const char* s) {
struct curl_slist* node = (struct curl_slist*)malloc(sizeof(struct curl_slist));
if (!node) return list;
node->data = s ? strdup(s) : NULL;
node->next = NULL;
if (!list) return node;
struct curl_slist* p = list;
while (p->next) p = p->next;
p->next = node;
return list;
}
static inline void curl_slist_free_all(struct curl_slist* list) {
while (list) { struct curl_slist* n = list->next; free(list->data); free(list); list = n; }
}
static inline CURL* curl_easy_init(void) { return (CURL*)malloc(1); }
static inline CURLcode curl_easy_setopt(CURL* h, int opt, ...) { (void)h; (void)opt; return CURLE_OK; }
static inline CURLcode curl_easy_perform(CURL* h) { (void)h; return 7 /* CURLE_COULDNT_CONNECT */; }
static inline void curl_easy_cleanup(CURL* h) { free(h); }
static inline const char* curl_easy_strerror(CURLcode c) {
(void)c; return "libcurl not built in (curl-less build)";
}
#endif /* !HAVE_CURL */
#endif /* EL_PLATFORM_WIN_H */
File diff suppressed because it is too large Load Diff
+12 -36
View File
@@ -117,15 +117,6 @@ el_val_t el_min(el_val_t a, el_val_t b);
void el_retain(el_val_t v);
void el_release(el_val_t v);
/* ── Arena scoping ────────────────────────────────────────────────────────────
* el_arena_push() activates the string arena (if not already active) and
* returns a mark; el_arena_pop(mark) frees all strings allocated since that
* mark. Used by codegen for per-function/statement scoping and by long-running
* EL loops (e.g. the soul daemon's awareness tick) to reclaim per-iteration
* allocations. */
el_val_t el_arena_push(void);
el_val_t el_arena_pop(el_val_t mark);
/* ── List ────────────────────────────────────────────────────────────────── */
el_val_t el_list_new(el_val_t count, ...);
@@ -151,7 +142,6 @@ el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
el_val_t http_delete(el_val_t url);
el_val_t http_delete_json(el_val_t url, el_val_t json_body);
void http_serve(el_val_t port, el_val_t handler);
void http_set_handler(el_val_t name);
@@ -177,11 +167,6 @@ void http_set_handler(el_val_t name);
void http_serve_v2(el_val_t port, el_val_t handler);
void http_set_handler_v2(el_val_t name);
/* Non-blocking variant of http_serve: runs the accept loop in a background
* pthread and returns immediately so the caller can continue (used by the
* soul daemon to run awareness_run() after starting its HTTP API). */
void http_serve_async(el_val_t port, el_val_t handler);
/* Build an HTTP response envelope. `headers_json` should be a JSON object
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
* returned string carries the discriminator `{"el_http_response":1,...}`
@@ -591,7 +576,6 @@ el_val_t engram_list_layers(void);
el_val_t engram_get_node(el_val_t id);
void engram_strengthen(el_val_t node_id);
void engram_forget(el_val_t node_id);
el_val_t engram_prune_telemetry(el_val_t older_than_ms);
el_val_t engram_node_count(void);
el_val_t engram_search(el_val_t query, el_val_t limit);
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset);
@@ -610,32 +594,12 @@ el_val_t engram_load(el_val_t path);
* can pass results straight through without round-tripping ElList/ElMap
* through json_stringify. */
el_val_t engram_get_node_json(el_val_t id);
el_val_t engram_get_node_by_label(el_val_t label);
el_val_t engram_search_json(el_val_t query, el_val_t limit);
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
el_val_t engram_stats_json(void);
el_val_t engram_act_stats_json(void);
el_val_t engram_text_health_json(void);
el_val_t engram_cosine_sim(el_val_t id_a, el_val_t id_b);
/* Destructively pop up to `max` newly-formed Hebbian associations as a JSON
* array of {from_id,to_id,weight,hebb}. The learning process (soul daemon) is
* not the process that owns persistence (engram HTTP server); this is how a
* self-formed association crosses that boundary. (2026-08-07 self-review.) */
el_val_t engram_hebb_drain_json(el_val_t max);
/* Document frequency of a term across node labels — term-specificity signal
* for curiosity seed selection. (2026-08-03 self-review.) */
el_val_t engram_label_df(el_val_t term);
/* Best curiosity seed from one node: argmax over idf·position·casing across
* the candidate tokens of its label, falling back to its content when the
* label is a sentinel. Excludes pipe-delimited tabu terms during selection
* and gates candidates to the df band [min_df, max_df]. Returns "" when
* nothing qualifies. (2026-08-13 self-review.) */
el_val_t engram_salient_term(el_val_t node_id, el_val_t max_df,
el_val_t min_df, el_val_t tabu);
el_val_t engram_embed_backfill(el_val_t count);
el_val_t engram_list_layers_json(void);
/* Working memory introspection — count, mean weight, and top-N snapshot.
* Ported from el-compiler/runtime on 2026-06-30 self-review. */
@@ -794,6 +758,18 @@ el_val_t trace_span_start(el_val_t name);
el_val_t trace_span_end(el_val_t span_handle);
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
/* ── Runtime symbols required by the soul modules ──────────────────────────── */
/* All implemented in el_runtime.c but omitted from this release header; the soul dist modules
* reference them directly, so the public header must export them. Declarations only mirrors the
* mainline el_runtime.h and is platform-independent (no behavioural change to the POSIX build). */
typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body);
typedef el_val_t (*http_handler4_fn)(el_val_t method, el_val_t path, el_val_t body, el_val_t headers);
el_val_t el_arena_push(void);
el_val_t el_arena_pop(el_val_t mark);
void http_serve_async(el_val_t port, el_val_t handler);
el_val_t engram_get_node_by_label(el_val_t label);
el_val_t engram_prune_telemetry(el_val_t older_than_ms);
#ifdef __cplusplus
}
#endif