diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index 2c10bae..7603b36 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -3223,8 +3223,17 @@ fn cg_fn(stmt: Map) -> Void { // strengthen (self-activity) + a dharma bus event — so a decorated op // self-reports with ZERO hand-written instrumentation in its body. (VBD role // = the topmost decorator; write it topmost when stacking with @route.) - if fn_has_decorator(stmt, "manager") || fn_has_decorator(stmt, "accessor") { - emit_line(" engram_boundary_beat(EL_STR(" + c_str_lit(fn_name) + "));") + // The beat carries the CONSTRUCT that caused it, not only the fn that beat. + // Without it the graph accumulates boundary events with no way to attribute + // them to the decorator responsible — so no construct can ever be measured, + // and "is this decorator earning its keep" stays an argument instead of a + // query. One parameter is the whole difference. + if fn_has_decorator(stmt, "manager") { + emit_line(" engram_boundary_beat(EL_STR(" + c_str_lit(fn_name) + "), EL_STR(" + c_str_lit("manager") + "));") + } else { + if fn_has_decorator(stmt, "accessor") { + emit_line(" engram_boundary_beat(EL_STR(" + c_str_lit(fn_name) + "), EL_STR(" + c_str_lit("accessor") + "));") + } } // Seed declared with parameter names so reassignment works let decl = native_list_empty() diff --git a/lang/runtime/el_runtime.c b/lang/runtime/el_runtime.c index 441be58..021f3f9 100644 --- a/lang/runtime/el_runtime.c +++ b/lang/runtime/el_runtime.c @@ -16991,24 +16991,40 @@ void dharma_emit(el_val_t event_type, el_val_t payload) { free(b.buf); } -/* engram_boundary_beat(op_name) — the decorated-fn boundary AUTO-EMIT (VBD seam). - * codegen injects a single call to this at the entry of every @manager/@accessor - * decorated fn, so a decorated op self-reports with ZERO hand-written - * instrumentation in its body: +/* engram_boundary_beat(op_name, construct) — decorated-fn boundary AUTO-EMIT + * (VBD seam). codegen injects a single call to this at the entry of every + * @manager/@accessor decorated fn, so a decorated op self-reports with ZERO + * hand-written instrumentation in its body: * (1) afferent counter++ — the boundary was crossed * (2) engram_chrono_tick() — interoception: the mind senses its own op firing * (3) engram_strengthen(self-anchor)— reinforce the self-activity anchor (an * activation-count/salience bump, NOT a content/edge write — identity * write-protection is untouched) * (4) dharma_emit(neuron.op.) — provenance on the shared bus transport - * (same bus the swarm peers field on); bumps _eg_dharma_emits. */ -el_val_t engram_boundary_beat(el_val_t op_name) { + * (same bus the swarm peers field on); bumps _eg_dharma_emits. + * + * `construct` is the DECORATOR that caused the beat ("manager" / "accessor"), + * carried in the payload. Before it, the beat reported which fn crossed a + * boundary but never which construct put the beat there — so boundary events + * accumulated in the graph with no attribution, and no decorator could ever be + * measured. "Is this construct earning its keep" was an argument; with the + * attribution it is a traversal. The payload is built here rather than at the + * call site so the format has exactly one author. + * + * The construct name comes from a closed set codegen controls, so the + * unescaped snprintf below cannot be injected through. That is NOT true of + * dharma_emit generally — its payloads are hand-concatenated at 39 call sites + * with no escaping, and a value containing a quote silently corrupts the + * event. Fixing that is a separate change; this one does not add to it. */ +el_val_t engram_boundary_beat(el_val_t op_name, el_val_t construct) { _eg_aff_boundary_ops++; engram_chrono_tick(); engram_strengthen(EL_STR("kn-efeb4a5b-5aff-4759-8a97-7233099be6ee")); - const char* nm = EL_CSTR(op_name); if (!nm) nm = ""; + const char* nm = EL_CSTR(op_name); if (!nm) nm = ""; + const char* ct = EL_CSTR(construct); if (!ct) ct = ""; char ev[160]; snprintf(ev, sizeof ev, "neuron.op.%s", nm); - dharma_emit(el_wrap_str(el_strdup(ev)), EL_STR("")); + char pl[192]; snprintf(pl, sizeof pl, "{\"construct\":\"%s\"}", ct); + dharma_emit(el_wrap_str(el_strdup(ev)), el_wrap_str(el_strdup(pl))); return (el_val_t)0; } diff --git a/lang/runtime/el_runtime.h b/lang/runtime/el_runtime.h index 716e147..b3e0c9d 100644 --- a/lang/runtime/el_runtime.h +++ b/lang/runtime/el_runtime.h @@ -887,7 +887,7 @@ el_val_t engram_age_field(el_val_t delta_ms); el_val_t engram_age_field_catchup(void); el_val_t engram_chrono_persist_tick(void); el_val_t engram_chrono_tick(void); -el_val_t engram_boundary_beat(el_val_t op_name); /* API-reshape decorator-seam auto-emit */ +el_val_t engram_boundary_beat(el_val_t op_name, el_val_t construct); /* API-reshape decorator-seam auto-emit; construct = the decorator that caused the beat */ el_val_t engram_self_anchor_capture(void); el_val_t engram_self_drift_json(void); el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction); diff --git a/lang/spec/language.md b/lang/spec/language.md index df79455..bdf82e5 100644 --- a/lang/spec/language.md +++ b/lang/spec/language.md @@ -460,7 +460,7 @@ The `@` token followed by an identifier attaches a decorator to the next `FnDef` | Decorator | Structural effect | |---|---| | `@manager` | Permits calls to `dharma_emit` / `dharma_field`. Calling either from a non-`@manager` fn emits a `#error` into the generated C — a compile-time failure, not a lint. | -| `@manager`, `@accessor` | Codegen injects one call to `engram_boundary_beat()` at function entry. The decorated op self-reports (chrono tick, afferent counter, self-activity strengthen, dharma bus event) with **zero** hand-written instrumentation in its body. | +| `@manager`, `@accessor` | Codegen injects one call to `engram_boundary_beat(, )` at function entry, where `` is the decorator that caused the beat. The decorated op self-reports (chrono tick, afferent counter, self-activity strengthen, dharma bus event carrying `{"construct":"..."}`) with **zero** hand-written instrumentation in its body. Without the construct argument the graph accumulates boundary events with no attribution, so no construct can be measured. | | `@route(path, method, …)` | Records a route into a generated dispatch table. | Decorators with no registered meaning are accepted and ignored. diff --git a/lang/tests/native/test_compiler.el b/lang/tests/native/test_compiler.el index 84ad5c4..4f68f78 100644 --- a/lang/tests/native/test_compiler.el +++ b/lang/tests/native/test_compiler.el @@ -726,3 +726,49 @@ test "compiler-stdint-include" { let out: String = compile_capture(src) assert str_contains(out, "stdint.h"), "output includes stdint.h" } + +// ── Decorator seam: boundary-beat attribution ──────────────────────────────── +// +// The beat carries the CONSTRUCT that caused it, not only the fn that beat. +// Without the second argument the graph accumulates boundary events with no +// way to attribute them to the decorator responsible, so no construct can ever +// be measured and "is this decorator earning its keep" stays an argument +// instead of a query. + +test "decorator-manager-beat-carries-construct" { + let src: String = "@manager\nfn f() -> Int { return 1 }" + let out: String = compile_capture(src) + assert str_contains(out, "engram_boundary_beat"), "@manager injects the beat" + assert str_contains(out, "EL_STR(\"manager\")"), "beat carries the construct that caused it" +} + +test "decorator-accessor-beat-carries-construct" { + let src: String = "@accessor\nfn f() -> Int { return 1 }" + let out: String = compile_capture(src) + assert str_contains(out, "engram_boundary_beat"), "@accessor injects the beat" + assert str_contains(out, "EL_STR(\"accessor\")"), "beat carries the construct that caused it" +} + +test "decorator-undecorated-fn-has-no-beat" { + let src: String = "fn f() -> Int { return 1 }" + let out: String = compile_capture(src) + assert !str_contains(out, "engram_boundary_beat"), "an undecorated fn does not beat" +} + +// ── Decorator seam: the twelve inert names ─────────────────────────────────── +// +// PINS A KNOWN DEFECT. codegen calls fn_has_decorator for exactly three names +// (manager, accessor, route). Twelve others parse, attach as {name,args}, and +// compile to nothing — including four that look like protection: +// @authenticate (6 uses), @authorize (3), @rate_limit (3), @validate (2). +// +// This test asserts the CURRENT behaviour so that fixing it is a visible +// change rather than a silent one. When a pass wires or rejects these, this +// test flips and that flip is the proof. + +test "decorator-authenticate-compiles-to-nothing" { + let src: String = "@authenticate\nfn f() -> Int { return 1 }" + let out: String = compile_capture(src) + let bare: String = compile_capture("fn f() -> Int { return 1 }") + assert str_eq(out, bare), "KNOWN DEFECT: @authenticate emits identical C to no decorator at all" +}