Files
el/lang/runtime/engram_text.c
T
bigmerge addd51209f
El SDK CI - dev / build-and-test (pull_request) Failing after 13m39s
runtime: extract engram_text.c, and repair 10 harnesses that could not link
First concern moved out of el_runtime.c under the ratchet, and the move is
deliberately small: it exists to prove the mechanism end to end before anything
large depends on it.

engram_text.{c,h} — query tokenization, candidate-token hygiene, word-boundary
matching, and the text-damage signature. Four functions, moved verbatim; only
`static` was dropped and each doc comment travelled with the code. They touch no
EL value type and no engram store type: plain C over <ctype.h>/<string.h> over
char buffers. They were never el_runtime.c's business.

  el_runtime.c   20,527 -> 20,427 lines   (BUDGET max_lines ratcheted down)
  engram fns        279 -> 275            (BUDGET max_engram_fns ratcheted down)

The Stage 1 extension point worked as designed: adding the file to
lang/runtime/SOURCES was one line, and every build path picked it up. The
Stage 2 drift guard then caught that I had NOT added it to install.sh's
standalone list — the exact class of drift it was written for, on its first
real change, before the commit rather than after a broken SDK shipped.

WHY ONLY 100 LINES, AND WHAT ACTUALLY BLOCKS THE REST

Measured, not estimated: of 273 engram-domain functions in el_runtime.c
(~9,700 lines), only 75 (~1,058 lines) can move today, and they are scattered
rather than clustered. The blocker is a single fact:

  EngramNode, EngramEdge, EngramStore, EngramLayer, EngramWal and EngramIdSlot
  are typedef'd INSIDE el_runtime.c. No sibling can see them. engram_store.h
  defines a SEPARATE serializable "node view" struct and maps between the two.

So every engram function that takes an EngramNode* — which is most of them, 109
of 273 by direct type reference — cannot compile in engram_store.c until those
types move to a shared header. That extraction is the real Stage 3 enabler and
it deserves its own change: it touches the most load-bearing struct in the
system, and doing it in the same commit as a code move would make a regression
impossible to bisect.

REPAIRED: 10 engram harnesses that had silently stopped linking

Not new breakage from this move — verified against unmodified dev, where
el_runtime.c + engram_store.c alone already failed with undefined symbols.
They had been dead for as long as el_runtime.c has been calling into the
siblings, and nothing noticed because nothing ran them.

  run_m3_parity, run_m7_traversal, run_m35_hebb_persist,
  run_interoception_p0..p5   — now build from $(scripts/el-runtime-sources.sh)
  run_wal_tests              — its two TUs #include "el_runtime.c" directly, so
                               it links the SIBLINGS ONLY; adding el_runtime.c
                               to that link line would define every symbol twice

(That #include'd .c is worth recording: the runtime does have one, in
engram/test/test_wal.c and the generated test_failloud.c.)

Verified locally — every one of these was run, not assumed:
  * m3_parity ............ PASS, incl. ASan+UBSan clean across seed/on/reboot
  * m7_traversal ......... PASS
  * m35_hebb_persist ..... PASS   (the gate over the original prod hebb bug)
  * interoception p0..p5 . PASS   (all six)
  * wal_tests ............ 66 passed, 0 failed, + fail-loud exit check
  * self-host fixpoint ... byte-identical, AND the emitted C is byte-identical
                           to the pre-move compiler output — the move changes
                           nothing the compiler produces
  * engram/src/server.el . compiles and links
  * native suites ........ 8 of 13, unchanged from before the move; the same 5
                           pre-existing failures, no regression
  * both runtime guards .. green at the new, lower budget

Also fixes a block comment left unterminated by the extraction (the deleted
range carried its closing */), restoring the compile to its single pre-existing
-Wcomment warning.
2026-08-16 19:59:43 -05:00

123 lines
5.2 KiB
C

/* engram_text.c — see engram_text.h.
*
* Moved verbatim out of el_runtime.c (2026-08-16). Bodies are unchanged; only
* `static` was dropped so they link from this translation unit, and each
* function's doc comment travelled with it.
*/
#include "engram_text.h"
#include <ctype.h>
#include <string.h>
/* Split q on whitespace into up to ENGRAM_MAX_QTOKENS distinct
* (case-insensitive) tokens. Returns the token count. Over-long tokens are
* truncated to ENGRAM_QTOK_LEN-1; over-count tokens are ignored. */
int engram_tokenize_query(const char* q,
char toks[][ENGRAM_QTOK_LEN], int maxtok) {
int n = 0;
if (!q) return 0;
const char* p = q;
while (*p && n < maxtok) {
while (*p && isspace((unsigned char)*p)) p++;
if (!*p) break;
char buf[ENGRAM_QTOK_LEN];
size_t tl = 0;
while (*p && !isspace((unsigned char)*p)) {
if (tl < sizeof(buf) - 1) buf[tl++] = *p;
p++;
}
buf[tl] = '\0';
if (tl == 0) continue;
int dup = 0;
for (int s = 0; s < n; s++) {
if (strcasecmp(toks[s], buf) == 0) { dup = 1; break; }
}
if (dup) continue;
memcpy(toks[n], buf, tl + 1);
n++;
}
return n;
}
/* Trim leading/trailing non-alphanumerics, then accept only tokens whose core
* is alphanumeric plus '-' and '_' with at least 3 letters. This subsumes the
* quoted-title guard (2026-07-25) and the "<!--" flood (2026-08-03)
* structurally: markup and punctuation-bearing tokens never become
* candidates, rather than being blocklisted after the fact. */
int eg_st_clean_token(const char* raw, size_t rawlen,
char* out, size_t outcap) {
size_t s = 0, e = rawlen;
while (s < e && !isalnum((unsigned char)raw[s])) s++;
while (e > s && !isalnum((unsigned char)raw[e - 1])) e--;
size_t len = e - s;
if (len < 4 || len >= outcap) return 0;
int alpha = 0;
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)raw[s + i];
if (isalpha(c)) alpha++;
else if (!isdigit(c) && c != '-' && c != '_') return 0;
}
if (alpha < 3) return 0;
memcpy(out, raw + s, len);
out[len] = '\0';
return 1;
}
/* Word-boundary document frequency. engram_label_df uses istr_contains, i.e.
* SUBSTRING matching, and that is the wrong estimator for term specificity on
* short tokens: "them" hits inside "theme" and "anthem", "about" and "whole"
* come back with df 2 and 1 rather than 0. That matters here specifically
* because the min_df floor is what rejects English function words, and it can
* only do that job if their df is honestly zero. Substring df quietly handed
* them a survival ticket. Measured on the live store before this fix, "whole"
* (df=1, idf=8.76) and "about" (df=2, idf=8.36) were outscoring real topical
* terms and losing only on position — one node whose text happened to open
* with a function word would have seeded on it.
*
* engram_label_df keeps substring semantics: it is a separate published
* measure with existing callers, and changing it underneath them is not this
* change's business. */
int eg_st_label_has_word(const char* hay, const char* word) {
size_t wl = strlen(word);
for (const char* p = hay; *p; p++) {
if (strncasecmp(p, word, wl) != 0) continue;
char before = (p == hay) ? '\0' : p[-1];
char after = p[wl];
if (before && (isalnum((unsigned char)before) || before == '_')) continue;
if (after && (isalnum((unsigned char)after) || after == '_')) continue;
return 1;
}
return 0;
}
/* Text-damage signature. Extracted with the function from el_runtime.c's
* "Text-integrity instrumentation" block; the stock/flow gauges that use it
* (engram_text_health_json, _eg_txt_write_damaged) stay there because they
* touch store and EL value types.
*
* SIGNATURE. Conservative on purpose — a false alarm that cries corruption
* over ordinary punctuation is worse than useless. Two patterns, both of
* which are essentially absent from well-formed English prose:
* (a) alnum '?' alnum — "na?ve", "caf?s", "don?t". A real question mark
* never sits between two word characters.
* (b) ' ? ' followed by a lowercase letter — a lost em/en dash. A real
* question mark is not preceded by a space, and
* what follows one starts a new sentence.
* Deliberately NOT flagged: a trailing '?' after a word, '? ' before a
* capital, or '?' at end of string — all legitimate. This under-counts (it
* cannot see a mangled 'café ' where the '?' landed before a space), so the
* census is a floor on the damage, never an exaggeration of it. */
int eg_text_loss_signature(const char* s) {
if (!s) return 0;
for (const char* p = s; *p; p++) {
if (*p != '?') continue;
unsigned char prev = (p == s) ? 0 : (unsigned char)p[-1];
unsigned char next = (unsigned char)p[1];
/* (a) sandwiched between word characters. */
if (isalnum(prev) && isalnum(next)) return 1;
/* (b) spaced, with lowercase continuation — a lost dash. */
if (prev == ' ' && next == ' ' && islower((unsigned char)p[2])) return 1;
}
return 0;
}