Commit Graph

606 Commits

Author SHA1 Message Date
bigmerge f23cb2b948 answer the module question: the partition is a path, and there is no namespacing 2026-08-17 09:54:23 -05:00
bigmerge 79f6cb7985 ANSWER: if the partition is a neighbourhood, does linking survive?
The question is premature, and measuring says why. El's partition is a
FILESYSTEM PATH, not a neighbourhood, and there is no namespacing at all.

MEASURED
  import is textual inlining (resolve_imports), guarded against double
  inclusion by a __elc_imp__:<path> state key
  when a .elh header exists the header is inlined instead and the .el is marked
  seen, so symbols resolve at C link time -- so linking IS real, delegated to C
  two modules defining `helper` emit two C functions into one translation unit

So linking barely survives the PATH partition. Whether it survives a
neighbourhood partition cannot be asked yet.

A DIAGNOSTIC REGRESSION I CAUSED, found by asking this question. cc does catch
the collision, but reports:

    error: redefinition of '__el_body_helper'
    error: redefinition of '__env_helper'
    error: redefinition of '__thunk_helper'
    error: redefinition of 'helper'

The user's own function is FOURTH. The first three are generated symbols
introduced by the unconditional-wrapper pass earlier today -- before it, there
was one clear message. Repaired by catching the collision at El level instead:

    duplicate definition: 'helper' is defined 2 times — El has no namespacing,
    so imported modules share one global scope

LIMIT, stated rather than hidden: textual inlining destroys file provenance. By
the time codegen runs there is one source string, so the message can say WHICH
name collides but not which files. Naming a.el and b.el needs provenance
threaded through resolve_imports.

104/104 native, 4/4 definitions_query.sh, the compiler itself reports clean,
fixpoint ok.
2026-08-17 09:54:23 -05:00
bigmerge bb040ad2c8 record the numeric literals answer: a bare number has no axis 2026-08-17 09:50:06 -05:00
bigmerge 97f741e9c2 answer the numeric literals question: a bare number is a magnitude with no axis 2026-08-17 09:49:51 -05:00
bigmerge c48db6c2a8 ANSWER: is 3 a position, or a convention we agreed on?
Both, at different layers, and the split is the same as everywhere else. The
NUMERAL is convention -- int_to_str was already form 1, because no position
determines that twelve is written 1 then 2 in base ten. The NUMBER is a
position: three things are three things regardless of notation.

But the sharper answer follows from `love = 0`. A bare `3` is a MAGNITUDE WITH
NO AXIS. It is not a position until something gives it a direction, which is
exactly why 3.days needs a calendar and why time_add(t, n, "min") had to carry
its axis as a string.

PREDICTIONS AND RESULTS
  P1 numeral = convention, number = position                TRUE
  P2 a bare literal is dimensionless until context types it TRUE
  P3 there is a measurable place where El guesses           TRUE
  P4 Instant + Int is not caught though Duration + Int is   TRUE
  P5 the rule catches it                                    TRUE
  P6 nothing legitimate in the tree relies on it            TRUE

P3/P4 IS THE DEFECT, and it was found by reasoning from the philosophy and then
measured. Duration + Int was refused -- "an Int carries no unit" -- while

    let t: Instant = now()
    let u: Instant = t + 3

compiled to raw (t + 3) and reported CLEAN. Adding a dimensionless number to a
point is worse than adding it to a displacement: it silently moves the instant
by an unspecified amount. 3 of what? Whatever the representation happens to be,
which is the leak itself. The asymmetry had no justification; the rule was
simply never written.

P6 MATTERED. Two calendar tests looked like Instant + Int:

    let later: Instant = i + 1.hour
    let later: Instant = base + 15.hours

They are not. `1.hour` lexes to a Duration -- el_duration_from_nanos(1LL *
3600000000000LL) -- and both stay clean. That is the whole answer demonstrated
in one line: t + 3 is refused because 3 has no axis; t + 1.hour is accepted
because .hour supplies one.

104/104 native + 2 new, integration green, fixpoint ok.
2026-08-17 09:49:50 -05:00
bigmerge 93aa96cfaf record the parsing answer: a grammar is a basis, and the should gate refused the obvious move 2026-08-17 09:44:14 -05:00
bigmerge 067dd40317 answer the parsing question: a grammar is a basis, and five keywords reserved nothing 2026-08-17 09:43:58 -05:00
bigmerge 0143cc458a ANSWER: is a grammar a convention, or a region?
Both, at different layers -- and it is the same split as serialization: the
convention is the BASIS, never the ACT.

  lexeme -> token      `fn` means function-start because someone said so   CONVENTION
  shape recognition    given tokens, which construct is this               REGION
  source -> structure  parsing is transduction onto that basis             GEOMETRY
  byte traversal       something must read them in order                   IRREDUCIBLE

Three things push the ACT toward region rather than convention: ambiguity
(a * b needs context; a grammar resolves it with the lexer hack, a region by
neighbourhood), error recovery (nearest-region is free), and precedence, which
is ordering along an axis with a conventional parameter.

AND THE SHOULD GATE SAYS NO TO THE OBVIOUS MOVE

Every other table this session moved to data. This one stays code. The keyword
set is CLOSED by the language definition -- it does not leak the way an
allowlist does -- and the lexer runs before the program is understood, so a
program can never declare its own keywords. Externalising it costs file I/O on
every compile and buys nothing. Same verdict as is_digit in ASCII.

WHAT WAS ACTUALLY WRONG: five of 46 keywords were consumed by no parser or
codegen path. sealed, activate, seed, protocol, impl. Each stole an identifier
from users for nothing.

SECOND SILENT MISCOMPILATION OF THE DAY. Using one did not fail to parse:

    let seed = 42
    let impl = seed + 1

compiled CLEAN -- zero cc errors -- and printed 0 instead of 44. No diagnostic
at any layer. Fixed by removing the five.

A DEFECT IN MY OWN MEASUREMENT, caught before it did damage: my first pass
checked only parser.el and reported `test` as inert too. codegen consumes it at
4135 for --test mode, and the tree has 408 uses. Removing it would have broken
every test in the suite. The measurement was re-run across all four consumers.

100/100 native + 2 new, 31/31 integration, fixpoint ok.
2026-08-17 09:43:58 -05:00
bigmerge 15d4352bac make the capability table match its own status note
The note said serialization, text encoding, storage, network, concurrency and
emission had collapsed; the table still listed all six as live capabilities.
A document that contradicts itself one screen apart is worse than one that is
merely out of date.

Also renames 27 from Secrecy to Concealment. 'Secrecy' covered one of the three
things in that row and got the other two backwards: a hash is public and a
signature exists to be read. Integrity and authenticity are grounding under
adversarial conditions, which is row 16. Only concealment stands alone.
2026-08-17 09:37:35 -05:00
bigmerge 2fcc1c287c correct the architecture docs against what was measured
capabilities.md cited '== lowering to str_eq unless both operand names are in a
hardcoded int-name set' as the paradigm defect. That is wrong: __int_names comes
from type annotations, which is legitimate propagation. The real defect was 35
hardcoded builtin return types one layer down, and mislocating it hid a live
miscompilation of unannotated lets.

geometry-vs-code.md listed concurrency and error handling as open. Both are
answered: ordering is a partial order and coordination is the price of
forgetting; standing is signed, so not-known and known-false are opposite
directions rather than one boolean. Added the fourth proof form (adversarial
exactness) and recorded that form 1 no longer survives as a verdict -- every row
it justified was a basis, not a capability.

Also marked cross-cutting concerns as implemented rather than predicted.
2026-08-17 09:37:13 -05:00
bigmerge 505e5e74d9 land int signatures, and repair a silent miscompilation they exposed 2026-08-17 09:32:13 -05:00
bigmerge cbef1c1ebb EXPERIMENT: Int return types as data — and the bug that fell out
PREDICTIONS AND RESULTS
  P1 is_int_call's 35 hardcoded names move to data        TRUE
  P2 is_int_name stays -- it is annotation propagation    TRUE
  P3 the dispatch stays -- it is emission                 TRUE
  P4 codegen shrinks ~40 lines                            TRUE  4507 -> 4469
  P5 the design doc's characterisation is WRONG           TRUE
  P6 the moved data also fixes the bug it exposed         TRUE

P5 CORRECTS THE RECORD. el-language-design.md and geometry-vs-code.md both cite
"== lowering to str_eq unless both operand names are in a hardcoded int-name
set -- a literal list of variable names treated as integers" as the paradigm
defect. It is not one. __int_names is populated from TYPE ANNOTATIONS
(param["type"] == "Int"), which is primitive but legitimate type propagation.
The actual defect was is_int_call: 35 hardcoded builtin return types, the same
shape as the temporal 19.

P6 IS A LIVE CORRECTNESS BUG, PRE-EXISTING, NOW FIXED

    let a = str_len("hello")     // no annotation
    let b = str_len("hi")
    let c = a + b                // -> el_str_concat(a, b) on two integers

Verified identical on the pre-change compiler, so not a regression. It compiled
clean, ran, and printed NOTHING where it should print 7. No error at any layer.

The repair is three lines: an unannotated let takes its type from what the
initialiser returns. The return types were already required for dispatch and
were simply never consulted at the binding site. Moving them into data is what
made the gap visible -- reading the code for eight hours did not.

98/98 native + 2 new, 31/31 integration, fixpoint ok.
2026-08-17 09:32:13 -05:00
bigmerge 50425f375d land temporal adjudication as a query: the emitter records, the rules are data 2026-08-17 09:27:42 -05:00
bigmerge e8e25a07b4 EXPERIMENT: temporal adjudication moves out; the placeholder stays
The previous pass moved the type DATA and left the judgment inline, which I
stated rather than hid. This finishes it.

PREDICTIONS AND RESULTS
  P1 codegen can emit operand-type relations               TRUE
                                                           "main calls temporal:instant_plus_instant"
  P2 the affine rules are a small closed set as data       TRUE  6 rules
  P3 violations still caught at build time                 TRUE  exit=1
  P4 the reporter leaves codegen                           TRUE  4538 -> 4507
  P5 the TIME_TYPE_ERROR placeholder must STAY             TRUE

P5 is the boundary of this whole approach. The emitter has to emit SOMETHING
for an illegal expression -- it cannot emit nothing and it cannot decide what
the program meant. So the placeholder is irreducible in the same way the AST
traversal was: what moved is the judgment and the wording, not the fact that
something must be written.

The rules are affine algebra and the set is closed because there are only two
kinds of thing. An Instant is a POINT, a Duration is a DISPLACEMENT: add a
displacement to a point, subtract two points for a displacement, combine
displacements. Nothing else is meaningful, which is why the enumeration in
temporal.rel cannot grow the way an allowlist does.

A defect in my own checker, found by running it: the .rel file uses aligned
columns and my awk assumed a single space, so the message came out with the
rule key still prefixed. Same class as the multi-line header parse in the arity
pass -- formatting assumptions that only fail when you look at the output.

98/98 native, 6/6 temporal_query.sh, fixpoint ok.
2026-08-17 09:27:42 -05:00
bigmerge e01e079bda land temporal signatures as data: the type table leaves, the dispatch stays 2026-08-17 09:25:18 -05:00
bigmerge d2d89fcb60 EXPERIMENT: temporal types as data — and the pass that GREW the compiler
This block is structurally unlike the previous four. It does not only
adjudicate, it DISPATCHES: Instant + Duration must become el_instant_add_dur,
LocalDate + Duration must become el_local_date_add_dur. The emitted C depends on
the type answer, so it cannot move to a post-hoc query. Selecting which call to
emit is an emitter's actual job.

PREDICTIONS AND RESULTS
  P1 the block conflates dispatch with adjudication      TRUE
  P2 adjudication can move, dispatch cannot              TRUE
  P3 this pass shrinks codegen far less than the last    TRUE, and worse:
                                                         4513 -> 4537, it GREW
                                                         by 24 lines
  P4 the rules are affine algebra, closed by construction TRUE
  P5 no type propagation -- name tracking plus a
     hardcoded list of which builtins return which type   TRUE, 19 names

P3 is the honest result and it is not spun: moving 19 names into a data file
cost more lines than it saved, because a generic loader is larger than the
enumeration it replaces. The win is not line count. It is that adding a 20th
temporal builtin is now a one-line edit to signatures.rel instead of a compiler
change, and that the data is inspectable.

WHY THE HEADER CANNOT SUPPLY THIS, unlike arity: el_runtime.h declares every
builtin as returning el_val_t, because El has ONE type. That single type is why
the whole seam is cheap and it is exactly why the C boundary cannot say that
now() returns an Instant while unix_seconds() returns an Int. The El-level type
is real and the boundary erases it.

INCOMPLETE, and stated rather than hidden: P2 said adjudication could move to a
query. It has NOT. Violations still emit TIME_TYPE_ERROR inline from the
emitter. Only the type DATA moved. Moving the adjudication needs the operand
types recorded as relations, which is a further pass.

98/98 native, 4/4 temporal_signatures.sh, fixpoint ok.
2026-08-17 09:25:18 -05:00
bigmerge d9e301be6d land arity-from-header: the runtime declares its own surface 2026-08-17 09:21:10 -05:00
bigmerge 9cc6040df2 EXPERIMENT: derive arity from the runtime's own declarations
codegen.el carried builtin_arity(): 344 lines, 300 entries, a hand-maintained
second copy of el_runtime.h.

PREDICTIONS AND RESULTS
  P1 the table duplicates the header                     TRUE   243 shared names
  P2 they have already drifted                           FALSE  ZERO drift. The
                                                                duplicate had been
                                                                maintained correctly.
  P3 codegen can emit call-arity relations               TRUE
  P4 the check becomes a query against the header        TRUE
  P5 codegen drops to roughly baseline                   TRUE   4903 -> 4512,
                                                                149 BELOW the 4661
                                                                it started at

P2 being false is the better result: the table was not WRONG, it was
INCOMPLETE. 110 functions the runtime declares had no entry, so calling them
with the wrong argument count produced no El-level diagnostic at all. Measured:
the old compiler reports 0 arity errors for __http_do_map_to_file(1); the query
reports "takes 5 arguments, called with 1".

Deriving from the header fixes coverage AND makes drift impossible by
construction. 503 signatures, versus 300 entries maintained by hand.

THREE DEFECTS IN MY OWN CHECKER, each found by running it rather than reading it
  1. El names and C names differ -- `println` is `__println`. 60 of 500 decls
     carry the prefix and codegen owns the mapping; the old table carried both
     keys. One rule covers all 60.
  2. Multi-line declarations parsed as zero params, so the checker reported
     "takes 0" for a function taking 5. A diagnostic with the wrong number in it
     is worse than none -- the same shape as the stale caller attribution in the
     previous pass.
  3. Fixing (2) by joining lines dropped 500 signatures to 334, because a
     declaration preceded by a comment no longer started its record. Comments
     are stripped first now.

98/98 native, 5/5 arity_query.sh, fixpoint ok.
2026-08-17 09:21:10 -05:00
bigmerge 29f78f9f67 land capability-as-policy: eighteen literals become a data file 2026-08-17 09:16:36 -05:00
bigmerge c2d9596e76 EXPERIMENT: the capability tier becomes shipped policy plus a query
Capability differs from prohibits_outside in one way that matters: a utility
program cannot be trusted to declare its own restrictions, because it would
declare none. So the policy comes from OUTSIDE the program -- it ships with the
language as data, editable without a compiler release.

  tools/check/capabilities.rel   18 names that were string literals in codegen
  tools/check/capabilities.sh    the query that decides

PREDICTIONS AND RESULTS
  P1 codegen emits kind + call graph, drops the 4 name tests   TRUE  zero #errors
  P2 the 18 literals become a data file                        TRUE
  P3 the checker catches capability violations                 TRUE  exit=1
  P4 codegen drops ~76 lines                                   TRUE  4963 -> 4881
  P5 below the 4661 baseline                                   FALSE ~+230

TWO DEFECTS THE HARNESS FOUND THAT READING WOULD NOT HAVE

1. Calls inside main became invisible. cg_fn returns early for main -- C
   provides its own -- so hooking the recording there left every call in main
   unrecorded: a blind spot exactly where a program does its work. The old
   cap_check_call ran from cg_expr and did see main. Moved the recording to
   cg_expr.

2. Caller attribution was stale. __cg_current_fn kept whatever cg_fn set last,
   so a violation in main was reported against the previously emitted function.
   The test still PASSED, because the violation was detected -- only the name
   was wrong, and a diagnostic naming the wrong fn is worse than none. Fixed at
   all three main-emission sites; the first patch missed two because the live
   path is codegen_streaming.

98/98 native, 7/7 + 4/4 + 5/5 integration, fixpoint ok.
2026-08-17 09:16:36 -05:00
bigmerge 60c07ad784 land prohibition-as-query: the emitter records, it no longer adjudicates 2026-08-17 09:11:48 -05:00
bigmerge c741cfe928 EXPERIMENT: prohibition becomes a query over emitted relations
I said prohibition could not move because "a #error has no runtime". That
conflated two separable things: WHEN a violation is detected (build time --
correct, and unchanged) and WHERE the rule and the checker live (the compiler
-- assumed).

A prohibition is a containment relation over the call graph. So codegen now
records what it saw:

    sneaky   calls raw_sql
    allowed  calls raw_sql
    allowed  calls @repository
    repository calls prohibits:raw_sql

and tools/check/prohibitions.sh decides, at build time, outside the compiler.

PREDICTIONS AND RESULTS
  P1 codegen can emit the call graph it already walks   TRUE
  P2 the check becomes a query outside the compiler     TRUE
  P3 all prohibition decisions leave codegen            TRUE  zero #errors now
  P4 violations still caught at build time              TRUE  exit=1
  P5 codegen drops below the 4661 baseline              FALSE 4962, +301

P5 is the finding. The TRAVERSAL is irreducible -- you must walk the AST to
find calls, and those ~120 lines do not move no matter who decides. What is not
irreducible is the rule (which names) or the decision (#error). Those left. I
predicted the whole 223 lines would go because I had not separated walking from
adjudicating.

Still compiled, and measured rather than assumed: the capability-tier system
(cap_check_call, is_self_formation_call, is_dharma_call, is_llm_call,
cap_record_violation, emit_cap_violations) is 76 lines of the same shape --
prohibits_WITHIN rather than prohibits_outside, so the checker needs the
opposite polarity to absorb it.

98/98 native, 4/4 prohibition_query.sh, 7/7 seam_binding.sh, fixpoint ok.
2026-08-17 09:11:48 -05:00
bigmerge c04d68f9ce land runtime invocation control: only prohibition remains compiled 2026-08-17 09:06:16 -05:00
bigmerge bc2f26ddfc EXPERIMENT: invocation control resolves at runtime
ISHIKAWA: why did wraps_body need compile-time knowledge? Because the wrapper
called the target directly. If the wrapper calls through the seam instead, the
seam can call the body itself, and a construct bound after the build decides
how and whether to invoke it.

PREDICTIONS AND RESULTS
  P1 wrap becomes runtime-bindable                  TRUE   body x3 -> 21,
                                                           never invoked -> 111
  P2 codegen shrinks                                TRUE   5042 -> 4977
  P3 cost 5-10% from an indirect call on every fn   TRUE   0.36s -> 0.39s, ~8%
  P4 zero-param fns break on the empty struct       TRUE   empty struct is a GNU
                                                           extension, empty init
                                                           is C23. Fixed with a
                                                           char field.
  P5 fixpoint holds                                 TRUE

PROCESS FAILURE worth recording: my first patch silently did not apply because
I dropped the assert on the string replacement. The build then failed with
"undeclared identifier __thunk_noargs", which I nearly attributed to the
empty-struct prediction. The guard that would have caught it existed and I
removed it -- the same shape as every other defect found tonight.

Removed: declare_wrap, decorator_wrap, cg_wrap_target, cg_wrap_construct,
params_to_call_args, and the wraps_body scanner branch.

prohibits_outside is now the ONLY construct kind left at compile time, and it
cannot move: a #error has no runtime.
2026-08-17 09:06:05 -05:00
bigmerge b40754f07b land unconditional wrapper: exit crossings resolve at runtime 2026-08-17 09:01:55 -05:00
bigmerge 285166c25c EXPERIMENT: emit the wrapper unconditionally, so exit binds at runtime too
ISHIKAWA: why did exit injection still need compile-time knowledge? Because the
body-helper wrapper was only emitted when codegen already knew an exit
construct existed. The wrapper being conditional was the cause, not the wrapper
being necessary.

PREDICTIONS AND RESULTS
  P1 exit becomes runtime-bindable                    TRUE  returns 14, bound
                                                            after the build
  P2 codegen shrinks                                  TRUE  5094 -> 5044
  P3 cost 5-15% from a call frame on every fn         FALSE 0.37s -> 0.38s, ~3%
  P4 fixpoint holds                                   TRUE

Every fn now gets a body helper and a wrapper. It has to be unconditional:
early returns must route through something for an exit construct to observe
them, and codegen cannot know which fns will be bound after the binary exists.

Removed with the machinery: declare_exit, decorator_exit, cg_exit_target,
cg_exit_construct, and the injects_at_exit scanner branch.

Two controls failed and were rewritten rather than repaired --
no-exit-construct-emits-no-wrapper asserted the optimisation this removes, so
it is now inverted. The integration harness gained a seventh assertion: an exit
construct declared after the build replaces the result.

99/99 native, 7/7 integration, fixpoint gen2==gen3.
2026-08-17 09:01:55 -05:00
bigmerge 24f7fb5143 land the runtime seam: resolve the crossing at execution
Five compile-time passes added 491 lines to the thing that was supposed to stop
growing. The seam is ~55 lines of C and one line of emission, and it does at
runtime what three of those five kinds did at compile time -- for programs that
are already built.

  a construct declared AFTER the binary exists applies to it
  free when unused: 0.36s vs 0.37s baseline across 267 indirections
  dlsym was the cost, not the table scan; resolve-once recovered 3.5x
  refusal works, composition works, unlinked targets are skipped not fatal

injects_at_exit and wraps_body do NOT collapse: early returns must route
through the body-helper wrapper regardless of when the target is resolved. The
wrapper is structural, which I had wrong. prohibits_outside cannot move at all
-- a #error has no runtime.

Controls: 99/99 native compiler tests, plus tests/integration/seam_binding.sh
(6/6) for the claim compile_capture structurally cannot see.
2026-08-17 08:56:41 -05:00
bigmerge 8bbb750c2c control the claim that cannot be unit tested
The seam's whole claim is that a construct declared AFTER a binary exists
applies to that already-built program. compile_capture only sees emitted text,
so it structurally cannot check this: it needs a built binary, a linked target,
and an environment. Verified by hand until now, which is the standing problem
this session has been about.

tests/integration/seam_binding.sh builds a probe from El source containing no
construct at all, links a target that El never references, and asserts:

  ok  unbound program is unaffected
  ok  a construct declared AFTER the build applies
  ok  a construct declared after the build can REFUSE
  ok  an unlinked target is skipped, not fatal
  ok  a binding for a different fn does not fire
  ok  two constructs compose on one crossing

  6 assertions, 6 passed, 0 failed

The eight controls that failed after the strip were replaced, not repaired.
They asserted compile-time emission of capability that moved to runtime;
contorting them would have kept an assertion whose subject no longer exists.
Three took their place, asserting the emitted shape, and the behaviour they
used to cover is now the integration harness's job -- which is the honest
division, since the shape and the behaviour are no longer the same fact.

99/99 native compiler tests pass. Fixpoint holds.
2026-08-17 08:48:57 -05:00
bigmerge 28d19da7f1 strip the compile-time machinery the seam replaces
PREDICTION: codegen.el drops below 4661, its size before any of these passes.
RESULT: FALSE. 5157 -> 5096. Still +435 over baseline.

  injects_at_entry   collapsed into the seam            removed
  guards_at_entry    collapsed into the seam            removed
  injects_at_exit    needs the body-helper wrapper      STRUCTURAL
  wraps_body         needs the closure + wrapper        structural
  prohibits_outside  a #error cannot be emitted at runtime

The wrapper is not a consequence of compile-time resolution. Early returns must
be routed through something no matter when the target is resolved, so exit
injection was never going to collapse. I predicted it would because I had
conflated "resolved late" with "emitted less".

What did collapse is entry injection and refusal -- 61 lines of compiler
replaced by one refusable indirection, with the capability now bindable after
the binary exists.

8 tests fail, and they are exactly the 8 controls for compile-time entry
injection and guards. No unrelated breakage: the controls reported precisely
what moved. They assert emission of something that now happens at runtime, so
they need rewriting as integration tests -- which the framework does not
currently support, because runtime binding needs a built binary and an
environment, not compile_capture.

Verified after the strip: fixpoint gen2==gen3, observation and refusal both
work through the seam with the compiler knowing nothing about either.
2026-08-17 08:43:57 -05:00
bigmerge 886626a64e seam refusal + control tests: a runtime binding can short-circuit
Prediction 3 was FALSE. I expected refusal to be impossible through the seam
because the entry indirection discarded its return. One line:

    { el_val_t __s = el_seam_run(EL_STR(f), 0, 0); if (__s) return __s; }

work() returns 7; bound to a refusing construct AFTER the build it returns 42.
So three of the five compile-time kinds are runtime-bindable: entry injection,
exit injection, and refusal. wraps_body needs invocation control and
prohibits_outside is compile-time by nature.

104/104 native compiler tests pass.
2026-08-17 08:40:10 -05:00
bigmerge 82e998273b self-review 2026-08-17: bound the off-graph ISE log — moving telemetry off-graph moved the leak, it did not close it
The 2026-07-16 review fixed telemetry growth in the GRAPH by calling
engram_prune_telemetry(48h) on every ISE insert. The 2026-08-xx move to
ENGRAM_ISE_OFFGRAPH=1 then routed every state event to a flat append-only
log instead — and that path had no retention of any kind. The prune call
still exists in server.el, but it now sits in the branch that production
never takes, so the fix reads as present while being inert.

Measured on the live store: 17.1 MB / 14,305 events over 3.56 days =
4.81 MB/day, unbounded (~1.76 GB/year).

engram_ise_log_append now compacts to a byte bound after append. Byte- and
not time-bounded on purpose: this is a flat file with no index, so size is
the property that has to be bounded, and ftell on the handle already held
is O(1) versus an O(file) timestamp scan per append. Default 64 MB retains
~13 days at the measured rate — more history than the 48h the on-graph path
kept. Override with ENGRAM_ISE_LOG_MAX_BYTES.

Compaction keeps the TAIL, never the head: engram_dreams_json reads the
last ~2 MB of this file for dream-recall, so the recent end is the end with
a reader, and KEEP (16 MB) stays well clear of that window. Resumes at the
first line boundary so the tail never starts mid-record, and only renames
over the live log when the tail was written in full — a short write must
not destroy history.

The honesty rail is unchanged: rotated-out remains "I don't remember",
never a synthesized dream. This only makes the forgetting bounded and
explicit instead of deferred forever.

Verified against a 4,000-event harness at a 200 KB cap: file bounded,
newest record retained, oldest dropped, 883 lines with zero malformed
records, tail contiguous, no .tmp residue.
2026-08-17 08:39:48 -05:00
bigmerge 35b07bade2 EXPERIMENT: resolve the crossing at execution, not at emission
HYPOTHESIS (Will's): a compiler whose one compiled mechanism is extending the
LANGUAGE — not the compiler — can compose without recompilation.

ISHIKAWA — why does a construct require a recompile today?
  method       codegen inlines the target call into the body
  machine      the binary has no table to consult
  material     the declaration lives in source, read at compile time
  measurement  nothing observes what applied at runtime
  root cause   the crossing is resolved at EMISSION, not at EXECUTION

CHANGE: codegen emits one unconditional indirection per fn. Which constructs
apply is read from a table that can be written AFTER the binary exists;
targets resolve through dlsym against the running image.

PREDICTIONS AND RESULTS
  P1 a construct declared after the build applies       TRUE
  P2 an unlinked target is skipped, not fatal           TRUE
  P3 emitting on every fn is measurably slower          FALSE — 0.37s -> 0.36s
                                                        with 267 indirections and
                                                        no bindings. Free unused.
  P4 the compiler still self-hosts                      TRUE (see note)

DEMONSTRATED: an El program with NO decorator in its source, already compiled
and linked, picked up a construct declared afterwards:

    $ /tmp/seamrun                       -> 7
    $ echo 'work audited entry audit_entry' > constructs.txt
    $ EL_CONSTRUCTS=constructs.txt /tmp/seamrun
      AUDIT: work applied by audited
      7

P4 note: my first fixpoint test was wrong, not the code. I compared gen1 to
gen2, which must differ whenever codegen's output changes. gen2 == gen3, 267
seam sites, stable.

MEASURED COST, and the root cause was not where I looked
  0 bindings                    0.36s vs 0.37s baseline   free
  2 bindings, dlsym per call    2.45s                     6.6x
  2 bindings, resolved once     0.69s                     3.5x recovered
The table scan was never the cost. dlsym walks the dynamic symbol table on
every call. Resolve once and cache — which is the smallest form of what
salience does for memory: what is hot stays resolved. The 0.69s residual is
audit_entry's own printf on two of the compiler's hottest functions, not seam
overhead.

CONSEQUENCE: the five compile-time declaration kinds on iteration-1 are a
compile-time specialisation of something that resolves at runtime. They are not
wrong, but they are not the mechanism — the mechanism is one indirection, and a
kind is data.
2026-08-17 08:37:47 -05:00
bigmerge 1b324a071f let a construct declare what may not cross it
The other half of a boundary: not what runs when something crosses, but what
may not cross at all. It was two string literals in vbd_is_restricted_name and
one #error in cg_fn — one prohibition, uneditable without a compiler release.

    @decorator("prohibits_outside", "raw_sql")
    fn repository() {}

    fn sneaky() -> Int { raw_sql("DROP") }
    // #error "boundary violation: raw_sql may only be called from an
    //          @repository fn, but 'sneaky' is not one"

The recursive matcher is parameterised through a state key rather than by
threading an argument through every branch of the walk — the mechanism codegen
already uses for __match_counter and __if_expr_counter. Each prohibition is
checked in its own turn, so the owning construct is known by construction and
the diagnostic names it instead of hardcoding one rule's wording.

PREDICTIONS AND RESULTS
  1 the 3 duplicated uniqueness rules are textually identical    TRUE
  2 a declared prohibition reproduces @manager's #error          TRUE
  3 existing output byte-identical                               TRUE
  4 a program can declare its own prohibition                    TRUE
  5 fixpoint holds                                               TRUE

I misread result 2 on first pass: a @manager fn calling dharma_emit still
emitted one #error, which looked like a failure. It is the CAPABILITY-tier rule
at codegen.el:2578, a separate prohibition system, and it fires identically on
the pre-change compiler.

MEASURED DEFECTS STILL OPEN
  - two independent prohibition systems (VBD constructs, capability tiers);
    only the first is declarable
  - 3 uniqueness rules written 6 times, once per codegen path, kept in sync by
    hand and identical today

102/102 native compiler tests pass, compiler self-hosts byte-identically.
2026-08-17 08:15:27 -05:00
bigmerge 7d01608a9d land wraps_body: a construct controls invocation
Proven on experiment/wraps-body (2bed848): base(5) wrapped by a target that
invokes the body twice returns 10; a target that never invokes it returns 999.
Neither is expressible by deciding whether to repeat.

Root cause it corrected: 'C has no closures' is a fact about one grammar, not
about what can be emitted. And El's single type (el_val_t = int64_t) cannot
describe a callable, so codegen emits the calling convention rather than asking
El's type system for something it structurally cannot say.
2026-08-17 08:12:16 -05:00
bigmerge 2bed8483f7 EXPERIMENT: hand the construct the body as a real closure
ROOT CAUSE of the weaker design: "C has no closures" was taken as a fact about
what is possible. It is a fact about one grammar. Every C++ lambda, every Go
closure, every Rust closure compiles to a struct of captured values plus a
function pointer -- which is what is emitted here. Codegen emits C; it is not
written in C's syntax, and the distinction is the whole difference between a
construct that can only decide whether to repeat and one that controls
invocation.

It would also have crippled the JS backend, which has closures natively, for a
limit that applies only to the C one.

PREDICTIONS AND RESULTS
  1 env struct + thunk taking void*                        TRUE
  2 fails to compile: struct redefinition                  FALSE -- C allows the
    inner declaration to shadow. Prediction wrong; C is more permissive than
    assumed. A different real defect surfaced instead: a wrap with no exit
    construct emitted `(EL_STR("f"), EL_STR(""), __r);` -- a call to an empty
    target -- because has_exit was reused as "needs a wrapper" and the exit line
    was emitted unconditionally. Fixed.
  3 compiles when the target is declared in El             FALSE -- and this is
    the root cause worth keeping: El has ONE type, el_val_t = int64_t. El's type
    system cannot describe a callable, so `extern fn` and the real signature
    cannot be made to agree in El's own vocabulary. The fix is not a cast:
    codegen DEFINES the wrap calling convention, so codegen emits the extern
    declaration. The convention is not El-expressible; it is emitted.
  4 target controls invocation, 0..N times                 TRUE
  5 existing @manager output byte-identical                TRUE
  6 compiler fixpoint holds                                TRUE
  7 emitting the convention makes it compile               TRUE

MEASURED
  base(5) wrapped by a target that invokes the body twice and sums -> 10
  never_runs(5) wrapped by a target that never invokes it        -> 999

Neither is expressible by "decide whether to repeat". This supersedes the
repeats_body experiment on experiment/repeats-body, which was built around the
mistaken limit.
2026-08-17 08:08:37 -05:00
bigmerge 4f7568b07f give a construct its after-crossing face, and let constructs compose
§6 records 62 persist-after-mutate sites, 10 auth-per-route, and
index-after-append that failed at 9 of 9 — every one an obligation at a
crossing that decayed into "remember to do this afterwards." An obligation a
human must remember is not an obligation, and the 9-of-9 figure is what that
costs.

    @decorator("injects_at_exit", "persist_now")
    fn durable() {}

The body moves into a static helper and the visible fn becomes a wrapper, so
EARLY RETURNS pass through the exit injection. Emitting it only before the
fall-through return would have silently missed every early return — the exact
failure class this seam exists to remove. Fns with no exit construct emit
byte-identically to before.

Three independent constructs now compose on one fn, none known to the compiler:

    el_val_t mutate(el_val_t k) {
      { el_val_t __g = my_auth(EL_STR("mutate"), EL_STR("authenticate")); if (__g) return __g; }
      engram_boundary_beat(EL_STR("mutate"), EL_STR("manager"));
      el_val_t __r = __el_body_mutate(k);
      persist_now(EL_STR("mutate"), EL_STR("durable"), __r);
      return __r;
    }

Guard, then entry, then body, then exit. §5.2 asked whether `hold` is one
construct or two; the implementation answers one construct with two faces,
selected by declared kind rather than by two mechanisms.

Verified: existing output byte-identical, compiler self-hosts byte-identically,
early returns pass through the exit, ordering holds under composition. 98/98
native compiler tests pass.
2026-08-17 07:56:28 -05:00
bigmerge 60737b0305 let a construct refuse, not only observe
@authenticate (6 uses), @authorize (3), @rate_limit (3) and @validate (2)
parsed, attached, and compiled to nothing. Fourteen applications that read as
protection and emitted no instruction — a function decorated @authenticate
compiled byte-identically to an undecorated one.

The missing capability was not authentication. It was that a construct could
observe a boundary but never refuse one. injects_at_entry discards the target's
result; there was no form in which a construct could say no.

    @decorator("guards_at_entry", "my_auth")
    fn authenticate() {}

    @authenticate
    @authorize
    fn handler() -> String { ... }

emits, at entry:

    { el_val_t __g = my_auth(EL_STR("handler"), EL_STR("authenticate")); if (__g) return __g; }
    { el_val_t __g = my_roles(EL_STR("handler"), EL_STR("authorize")); if (__g) return __g; }

Guards precede injections because a refused call must not report a crossing,
and every guard runs where the topmost injecting construct wins — refusal is
not a role, so it does not follow the role convention.

The compiler still knows nothing about auth. The program points the construct
at its own function, which is where that decision belongs.

Verified: existing @manager/@accessor output byte-identical, compiler
self-hosts byte-identically, guards stack in declaration order and emit before
the beat. 94/94 native compiler tests pass.
2026-08-17 07:53:10 -05:00
bigmerge 5718943f2e let a construct declare its own meaning instead of the emitter knowing it
codegen called fn_has_decorator for exactly three names — manager, accessor,
route. Twelve others parsed, attached as {name,args}, and compiled to nothing,
including four that look like protection: @authenticate (6 uses), @authorize
(3), @rate_limit (3), @validate (2). The cause was not that the branches were
untidy. A construct had nothing to BE, so its meaning had nowhere to live
except the emitter, and every construct was therefore a compiler edit.

A name -> injection table would have moved the enumeration twenty lines up
without removing it. So the construct now carries its own meaning:

    @decorator("injects_at_entry", "engram_boundary_beat")
    fn audited() {}

    @audited
    fn risky_op() -> Int { ... }   // gets the beat, attributed to "audited"

scan_declared_decorators is a token-level pre-pass beside scan_routes, forced
by streaming codegen having no whole-program AST. manager and accessor are
seeded as the compiled-in core — the fixedSelf shape from substrate.go: a
complete fallback exists, declaration is enrichment.

This is the injection half of the seam only. The prohibition half (@manager's
#error on dharma_emit) stays hardcoded, because "which calls may appear inside
this boundary" is a query over program structure and there is nothing yet to
ask.

Verified three ways: emitted C for existing @manager/@accessor code is
byte-identical to the hardcoded path; a construct with a name the compiler has
never heard of injects correctly; the compiler self-hosts byte-identically.
90/90 native compiler tests pass.
2026-08-17 07:50:56 -05:00
bigmerge dcaa77d77b make boundary crossings attributable to the construct that caused them
The beat reported which function crossed a boundary, never which decorator
put the beat there. So the graph accumulated boundary events with no
attribution, and no construct could be measured — "is this decorator
earning its keep" stayed an argument instead of a traversal.

engram_boundary_beat now takes the construct and carries it on the bus as
{"construct":"..."}. The injection point, the beat, and the accumulation
already existed; only the attribution was missing.

Also pins a known defect as a test: codegen calls fn_has_decorator for
exactly three names (manager, accessor, route). Twelve others parse, attach,
and compile to nothing — including @authenticate (6 uses), @authorize (3),
@rate_limit (3) and @validate (2), which look like protection and are not.
decorator-authenticate-compiles-to-nothing asserts that @authenticate emits
byte-identical C to no decorator at all, so fixing it will be a visible flip.

Verified: compiler self-hosts byte-identically, 86/86 native compiler tests
pass, emitted C carries the construct for both @manager and @accessor.
2026-08-17 04:53:40 -05:00
bigmerge 409bf57341 track the architecture docs
They were written outside git, so the reasoning that produces the design
had no history and no way to be superseded. capabilities.md and
geometry-vs-code.md are both known stale at this commit; they are tracked
as-is so the corrections are visible as movement rather than as a rewrite.
2026-08-17 04:34:32 -05:00
bigmerge addd51209f runtime: extract engram_text.c, and repair 10 harnesses that could not link
El SDK CI - dev / build-and-test (pull_request) Failing after 13m39s
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
bigmerge 9a13547fe2 runtime: put el_runtime.c on a ratchet, and actually run the guards
scripts/check-single-runtime.sh guards against el_runtime.c being COPIED — it
was written after a lagging fork shipped to prod and dropped learned hebb edges.
Nothing guarded against it GROWING. So it grew: 10,607 -> 20,527 lines, 94% in
3.5 months, the whole time under an explicit commit-message promise that it was
a temporary shim about to be deleted.

Worse, the copy guard was never wired in. Its own footer described the CI
wire-in as a TODO, and the TODO had never been done — the script existed but ran
nowhere, in no workflow and in no hook, so it had caught nothing for as long as
it has been in the tree. A guard that does not run is a comment.

This adds the missing guard and runs both.

  * lang/runtime/BUDGET — a RATCHET, not a limit. max_lines is set at the
    current 20,527 with NO headroom: the file cannot grow by one line. A second
    cap, max_engram_fns (279), counts top-level engram_/eg_/cog_ definitions in
    it — ~47.5% of the file is engram code and engram already owns six sibling
    .c files, so this is the scoreboard for moving it out. Both may only go DOWN.

  * scripts/check-runtime-growth.sh — enforces the ratchet, and three
    invariants that keep the multi-file runtime honest: every .c in
    lang/runtime/ is either in SOURCES or explicitly platform-optional (an
    unaccounted .c is compiled by nothing and is silently dead); install.sh's
    hardcoded download list matches SOURCES (it cannot call the helper — it
    runs where there is no checkout — so that copy is checked, not trusted);
    and an advisory nudge to lower the budget when you have earned it.

  * Both guards now run as early steps in ci-dev.yaml, ci-stage.yaml and
    sdk-release.yaml, and in .githooks/pre-commit.

The failure message is the point. The guard that existed said what was wrong but
not where the code should go, which makes it easy to "fix" by arguing with the
guard. This one names the destination: the concern-owning .c, or a new .c plus
one line in SOURCES, or c_source in a program's manifest.el — and it prints the
`nm` command that proves placement is link-time and that the shipped compiler
already links from ten translation units. Every runtime file except el_runtime.c
is deliberately uncapped, because that is where code is supposed to go.

Proven with negative controls, per lang/AGENTS.md step 5 — each shown FAILING:
  * +1 line to el_runtime.c                  -> FAIL (20528/20527)
  * +1 engram fn, net-zero lines             -> FAIL (280/279)
  * a new unaccounted lang/runtime/*.c       -> FAIL
  * engram_store.c removed from install.sh   -> FAIL, names the missing file
  * el_runtime.c truncated to 20,000 lines   -> PASS + "lower max_lines to 20000"
  * baseline, tree unmodified                -> OK, and both guards green

el_runtime.c is byte-identical after the controls; this commit changes zero
lines of it.
2026-08-16 19:59:43 -05:00
will.anderson 481badf1d1 Merge pull request 'organ: el speaks — the peripheral becomes a capability of the language' (#159) from feat/el-speaks into dev
El SDK CI - dev / build-and-test (push) Failing after 10m40s
2026-08-17 00:58:20 +00:00
will.anderson b92ec92c48 Merge pull request 'engram: intake realizes a signal into a manifold, it does not assume a node' (#158) from wire/write-realizes-signal into dev
El SDK CI - dev / build-and-test (push) Failing after 10m58s
2026-08-17 00:58:04 +00:00
will.anderson e1bc6fe944 Merge pull request 'singleton: guard the state, not the program's name' (#157) from fix/singleton-guards-the-state into dev
El SDK CI - dev / build-and-test (push) Failing after 11m15s
2026-08-17 00:57:49 +00:00
will.anderson eb13dace9c Merge pull request 'runtime: the link set is multi-file — name it once, ship all of it' (#160) from fix/runtime-shim-retire into dev
El SDK CI - dev / build-and-test (push) Failing after 12m41s
2026-08-17 00:56:23 +00:00
bigmerge 8c2406ff6b runtime: the link set is multi-file — name it once, ship all of it
El SDK CI - dev / build-and-test (pull_request) Failing after 5m39s
el_runtime.c was created 2026-05-03 as an explicitly temporary build shim. It
was deleted that afternoon ("runtime is 100% native El") and restored 25 minutes
later "UNTIL the compiler is updated to emit #include el_seed.h". The `until`
never came. 3.5 months on it is 20,527 lines, and nothing was ever set up to
notice — a file scheduled for deletion gets no owner, no budget, no boundary.

What kept it growing is not inertia, it is an instruction. lang/AGENTS.md said
el_runtime.c "is the authoritative single-file link target ... THIS IS WHERE A
NEW C BUILTIN'S IMPLEMENTATION MUST CURRENTLY LIVE TO BE LINKABLE", and made it
step 1 of the add-a-builtin recipe. That is false. Placement is a link-time
concern: builtin_arity maps NAME -> ARITY INT only, the El name is emitted as
the exact C symbol, and `ld` resolves it — the compiler cannot tell which .c a
symbol came from. `nm lang/dist/platform/elc` on the shipped compiler already
shows T _engram_geo_reify_index_new, T _vindex_insert, T _engram_think,
T _engram_reason_abduce: it is linked from ten translation units today. In a
repo where agents write most of the code, a false instruction in the instruction
file is the forcing function. The file grew because the recipe said to grow it.

The multi-file runtime is therefore already real, and the docs and the
distribution never caught up — which left a live, shipped bug:

  * Linking el_runtime.c alone FAILS at `ld` (undefined engram_ground_json,
    engram_activate_inner, eg_find_relation, cog_assert_two_axis, ...) because
    el_runtime.c #includes six engram headers and calls into all six siblings.
  * sdk-release.yaml shipped el_runtime.c/.h + engram_store.c/.h and none of the
    other five required .c files, so downstream consumers of the el-runtime-c
    Artifact Registry package and of install.sh got a lib/ that cannot link.
  * .githooks/pre-commit linked el_runtime.c alone with stderr to /dev/null, so
    it reported all 13 native suites as FAILED with the real ld error invisible.
  * AGENTS.md's self-host recipe compiled el-compiler/runtime/el_runtime.c — a
    path the same file's "DO NOT EDIT" list names as a lagging fork.

The root fix is to stop writing the list down eight times:

  * lang/runtime/SOURCES — the canonical link set, in one place, in link order.
  * scripts/el-runtime-sources.sh — prints it, optionally prefixed; --check
    fails loudly on a missing file, --headers for the shipped headers.
  * Every link line in AGENTS.md, lang/AGENTS.md, DESIGN.md, lang/spec/language.md,
    the three workflows and the pre-commit hook now reads that one list.
  * Adding a concern's .c is one line in SOURCES, so a new builtin no longer has
    to be appended to el_runtime.c just because appending was the cheaper edit.

Distribution: ship the siblings rather than amalgamate. Amalgamation needs a new
tool and contradicts DESIGN.md's compile-once-link-many; the siblings are already
independently authored and independently tested (engram/test/*.sh link subsets
directly), and engram_store.c was already shipped, so this completes a mechanism
that existed rather than inventing one. Source is also a superset: a consumer
that wants one file can concatenate, one that wants separate TUs cannot undo an
amalgamation. el-runtime-c/-h stay for backward compatibility; el-runtime-src is
added carrying the complete set plus SOURCES.

lang/AGENTS.md now points new C builtins at the concern-owning .c and states
plainly that the compiler cannot tell which .c a symbol came from, with the nm
evidence. AGENTS.md's "reconcile which is canonical (verify)" note is resolved:
neither file supersedes the other, the canonical unit is the set.

Verified locally (the bar; not CI):
  * engram/src/server.el compiles and links against the SOURCES set.
  * Compile-once-link-many into libel.a links the same program.
  * elb builds from the corrected recipe.
  * Self-host fixpoint byte-identical (11,110 lines, stage2 == stage3) built
    with the SOURCES-driven link line.
  * pre-commit hook: 0 of 13 native suites passing -> 8 of 13.

The 5 still-failing suites are PRE-EXISTING and untouched here: test_fs
(fs_list_json undeclared), test_state (state_has, state_get_or undeclared),
test_json (json_build_array/json_build_object/json_escape_string undefined),
test_time (now_ns undefined), test_env (1 assertion). Builtins registered in
builtin_arity with no implementation or no declaration anywhere — the same
recipe defect, now visible because the linker error is no longer suppressed.

Not attempted: making elc emit #include el_seed.h and dropping elb's hardcoded
runtime path. That is the correct long-term fix and finishes the 2026-05-03
migration, but it touches codegen and self-hosting and belongs in its own change.
2026-08-16 16:44:26 -05:00
Neuron c26b6aac82 organ: the rest of the peripheral moves into El
El SDK CI - dev / build-and-test (pull_request) Failing after 4m18s
The speaker and the voice-fetch landed in the previous commit. This is the
remainder of the 939-line Swift program, ported, and the line it draws is
between DEVICE and ARITHMETIC rather than between languages.

Two things stay realizers, because they are the two things El cannot express
as arithmetic: handing a buffer to the DAC and waiting for it to drain
(el_audio_darwin.m), and asking the OS for samples off a mic or frames off a
camera (el_capture_darwin.m). Both are their own translation units declared
in el_runtime.h, never patches to el_runtime.c.

Everything else is El. WAV decode, LPC autocorrelation, Levinson-Durbin at
order 16, formant extraction off the all-pole envelope, source-filter
resynthesis, and the three descriptors are organ_dsp.el. Consent, disclosure
and the scene descriptor are organ.el. Barge-in, yield-or-hold, backchannel
and resume are organ_converse.el.

The organ never learns a word. Codes and phoneme geometry arrive from the
language side; the organ turns them into samples and gets the samples out the
speaker, and runs the same trip in reverse for the senses. No lexicon, no
grapheme-to-phoneme, by design.

Barge-in needed pause/resume and a real DAC position rather than a tick
counter, because "finish the buffer" is not barge-in and a queue holding
three buffers is a third of a second wrong about where it is. An injected
barge also had to fire once rather than stay true, which is otherwise a
livelock the moment a backchannel resumes.

Measured against the Swift on out/mic_room.wav: seconds, rms, peak, zcr,
centroid and F0 agree to every printed digit; formants F1-F5 and bandwidths
B1-B5 are identical. imitate cannot match bit-for-bit because the Swift
excites unvoiced frames with Double.random — two Swift runs correlate 0.957
with each other and El correlates 0.958 with Swift, so the port is as close
to the original as the original is to itself.

Verified end to end: consent fails closed on both locks, real mic capture
(16000 frames), real camera frame (1920x1080 -> 15 numbers), voiceprint,
imitate, hear-imitate, a voice learned by ear and fetched back out of the
engram, and all five converse paths with real audio. The binary contains
zero afplay/Swift strings and spawns no child process while speaking.
2026-08-16 16:42:41 -05:00
bigmerge 99ef855b98 engram: intake realizes a signal into a manifold, it does not assume a node
El SDK CI - dev / build-and-test (pull_request) Failing after 13m16s
There is no write node. What arrives at /api/write is a SIGNAL; a node is an
OUTPUT of realization, never an INPUT to it. route_write asserted otherwise in
one line:

    let manifold: String = "[" + body + "]"   // the body IS a valid manifold node object

A request body is not a manifold, and that assertion is the whole defect. It is
why every written signal landed as one flat node with zero edges, measured on a
clone: {"inserted":1,"nodes_added":1,"edges_added":0} and GET /api/neighbors on
the new id returning [].

PR #155 corrected transduce(signal, modality) to return a Manifold — components
plus relations — but touched only ingest, the runtime and its tests. Nothing
downstream called it: grep 'transduce|realize|Manifold|decompos' over
engram/src/server.el returned exactly one line, a comment. The primitive was
fixed and the engram's entire HTTP surface never reached for it.

This wires the intake seam to the primitive that already exists. It decomposes
nothing itself and must never: transduce dispatches through the dlsym realizer
registry, so adding a modality is registering a realizer, not editing this file
and not patching the runtime. intake_signal only carries what the primitive
returns into the store — components become nodes carrying their OWN geometry
via node_attach_geometry, relations become edges at the weight the realizer
stated, and manifold_member still wires the set into one connected sub-graph
exactly as insert_manifold_json already did.

Built general rather than special-cased: five of the six intake doors (write,
supersede, nodes, knowledge/capture, state-events) are the same hand-written
"content -> engram_node_full -> one flat node", differing only in the
node_type/tier/tags they hardcode. Those are parameters here so each door can
move onto this one function. Only /api/write rides it in this pass.

When no organ is registered the signal is stored flat exactly as before, but
the response now says so ("realized":false,"organ":false,"components":0).
Silent flattening was the real defect — a caller could not tell "nothing
decomposed me" from "I decomposed into one component". el_runtime.c draws the
same line between an absent organ and a broken one, for the same reason.

No realizer is authored here and none is registered, so production behaviour is
unchanged. The mechanism is what landed.
2026-08-16 16:34:36 -05:00
Neuron 5503e1d9a4 organ: el gets a speaker, and fetches the voice from the engram
El could turn meaning into samples and could not make a sound. Every path
from those samples to the air ran outside the language, through a 939-line
Swift program that shelled out to afplay, so the voice was not a capability
of El or of Neuron but a separate binary standing next to them.

Two things land here.

The speaker. el_audio_darwin.m is a CoreAudio AudioQueue realizer in its own
translation unit, declared in el_runtime.h, deliberately not a patch to
el_runtime.c — acquiring a device must not mean editing the middle of the
language, the same rule the realizer registry follows for modalities. It
takes samples straight out of memory, so nothing is written to disk and no
process is spawned between the intent to speak and the sound. The async half
(play/stop/playing/played_frames) exists because barge-in means stopping on
the spot, and a blocking play cannot be interrupted. el_peripheral_null.c is
the same entry points everywhere else, so El that speaks links anywhere and
truthfully reports having no speaker.

The voice. organ_voice_fetch asks the engram for a voice region by query and
reads the geometry off the node that comes back. A voice is not a JSON file
next to the code; it is a memory, and the organ retrieves it the way anything
retrieves a memory. An absent region returns empty rather than a plausible
default, because a caller must be able to tell 'this is how they sound' from
'I never heard them'.

Underneath both: __str_set_char bounds-checked writes against strlen(), which
is 0 for the zero-filled buffer __str_alloc hands back, so every write was
rejected and every El-authored WAV in this repo was 55,244 bytes of silence
that reported ok=true. Byte buffers now carry their capacity in a side table;
text keeps the exact strlen behaviour it had. This is why nobody noticed El
was mute.

Measured: voice fetched from the engram reads f0=137 f0_end=116 kf=1269
f1=500 f2=2093 f3=3531, matching the 30s LPC measurement; render is 20160
samples at 16 kHz; both the rendered utterance and an own-core tone played
aloud through CoreAudio with no Swift and no afplay in the chain.
2026-08-16 16:27:30 -05:00