Skip to content

feat(inputlayer-js): add guarded programs, claim() and any()/NOT(any()) existence checks - #240

Merged
jsam merged 5 commits into
mainfrom
fm/sdk-p1-j
Oct 4, 2026
Merged

jsam merged 5 commits into
mainfrom
fm/sdk-p1-j

Conversation

@jsam

@jsam jsam commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Intent

So fix the SDKs by improving them. (Context: the captain asked to rework the SDKs so agents and agentic loops use InputLayer without writing Datalog, and to fix the SDK release blockers by improving the SDKs rather than patching. The agreed design, signed off by an independent challenger after three rounds, is ~/firstmate/data/sdk-design/report.md; this item is its phase-1 PR P1-J P1-J (JS).)

What that phase-1 item P1-J of the agreed design asks for, in the JS SDK (packages/inputlayer-js), on today's relation()/from() API:

  • any(Relation, {col: value or column}) and NOT(any(...)): an existence check and its negation, usable in rule bodies (from().where), queries and write guards. A negated atom must share a variable with a positive atom (engine range restriction); a negation bound only to constants is rewritten only when that preserves meaning (a positive column already equal to the constant), otherwise bound through an SDK-owned constant relation: a program-local session fact il_const_ on the read path, a persistent row staged before the guard and deleted last on the write path (session facts are invisible to update bodies); rules refuse with a CompileError. Canonical body order: positive atoms, negated atoms, comparisons.
  • kg.program(): statements committed as one request and one transaction; .when() makes the whole program conditional with the guard evaluated once through a transaction token (-il_txn(""), +il_txn("t-") <- guard; every fact write conditioned on il_txn("t-"); token removed last), so it applies whole or not at all. strict: true (default, and mandatory for programs holding rules or schema) uses the abort form: +il_txn_pending, the token statement, an assertion that inserts a string into the int relation il_assert when the token is absent (the engine's type error rolls back everything, rules and schema included), and PreconditionFailed is read structurally from errors[0].index; strict: false returns applied=false. il_txn, il_txn_pending, il_assert are declared by define(). Guarded inserts use the update form with a never-present ghost delete row; a single-head +r(...) <- body is never emitted for a base relation (the engine treats it as a rule).
  • kg.claim(Relation, row, {when, unless, key}) -> {won, holder}: the guarded insert with unless as a negated atom, followed by a query on the key in the same program, so one of many racing claims wins and the others learn the holder.
  • Errors per the design: CompileError, PreconditionFailed, ConflictError. Counts are read from the engine's per-statement reply messages with a pinned grammar.
  • Same semantics as the Python counterpart item. Unit tests on compiled text plus live tests against a real engine that run in CI (make js-test-live / the js-sdk-live full-suite job), including guard_cursor_advance_applies_whole, guard_first_terminal_outcome, guard_note_replacement, claim_twenty_way_race, strict_guard_raises_precondition_failed_from_error_index, stale_deploy_is_refused_including_rules, deploy_applies_whole_when_guard_holds, and a guarded_program_is_all_or_nothing property test. Millisecond latency must not regress. Other phase-1 items (subscriptions, connection core, docs/README hero) and later phases are out of scope.

What Changed

  • Added any(Relation, {...}) and NOT(any(...)) existence checks to the JS SDK. They work in from().where rule bodies, queries and write guards. The compiler puts body atoms in a fixed order: positive atoms, then negated atoms, then comparisons. When a negation is bound only to constants, the compiler either rewrites it, binds it through an SDK-owned constant relation (il_const_<type>, a session fact on reads and a staged persistent row on writes), or refuses it with CompileError. That refusal also applies in debug()/why().
  • Added kg.program() (new Program in program.ts). It sends all its statements as one request and one transaction. .when(guard) makes the whole program conditional through an il_txn token, so it applies whole or not at all. strict: true uses an il_assert abort form and raises PreconditionFailed, read from errors[0].index. strict: false returns applied=false. Guarded inserts use the update form anchored on a ghost delete row. Write counts come from per-statement reply messages using a shared, pinned grammar.
  • Added kg.claim(Relation, row, {when, unless, key}), which returns {won, holder}. Added and exported the CompileError, PreconditionFailed and ConflictError error classes, and documented them in the JS SDK guide. Added unit tests on compiled text (guards.test.ts) and live engine tests (guards.integration.test.ts); make js-test-live now runs the live tests.

Risk Assessment

✅ Low: The round-3 fix routes debug() and why() through explainable(). These are the only consumers of plan.debug and plan.why. A query whose NOT(any()) binds only constants is now refused with a CompileError before anything is sent to the engine. The new unit test checks that refusal through the public KnowledgeGraph API and checks that nothing reaches the connection. Re-checking the earlier fixes (the il_ghost(0) anchor and the shared parseWriteMessage grammar) and the rest of the diff turned up no new defects.

Testing

I built the engine from this commit and ran it against a disposable data dir. Against it I ran the targeted guard/claim/compiler vitest files (all passed, including guard_cursor_advance_applies_whole, guard_first_terminal_outcome, guard_note_replacement, claim_twenty_way_race, strict_guard_raises_precondition_failed_from_error_index, stale_deploy_is_refused_including_rules, deploy_applies_whole_when_guard_holds and the guarded_program_is_all_or_nothing property test). I also ran a separate end-user script through the public SDK. It confirmed: guarded writes anchored on il_ghost(0) leave existing paused(false)/counter(-1) rows untouched; strict aborts roll back rules, schema and facts and raise PreconditionFailed with no il_txn/il_assert leftovers; strict:false returns applied=false; NOT(any()) guards bound only to constants work and leave no il_txn_const rows behind; debug()/why() and rules refuse constant-only negations with CompileError; a 20-way claim race across 20 connections has exactly one winner and every caller sees the same holder. Insert/query latency matched the base SDK (about 1.6 ms and 0.8 ms medians). Only the ConflictError mapping could not be provoked live. All scratch files, the build and the server were torn down; the worktree is clean.

  • Live validation: ✅ go - 8 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Named live tests from the intent (cursor advance, first terminal outcome, note replacement, claim race, strict PreconditionFailed, stale deploy refused incl. rules, deploy applies whole, all-or-nothin… ✅ pass live guards-live-vitest.txt
Adversarial ghost: a guarded insert into a relation that already holds paused(false)/counter(-1) keeps those rows (anchor is -il_ghost(0)) ✅ pass live p1j-end-user-drive.txt S1: ?paused -> [[false],[true]], ?counter -> [[-1],[5]], deleted=0
Strict program with schema+rule+fact and a failing guard raises PreconditionFailed and leaves rules, schema, facts and guard relations untouched ✅ pass live p1j-end-user-drive.txt S3
strict:false with a failing guard returns applied=false and writes nothing; strict with a holding guard applies the rule and facts whole ✅ pass live p1j-end-user-drive.txt S2, S4
NOT(any()) bound only to constants works as a write guard (staged il_txn_const row deleted last) and as a query filter (session fact), and debug()/why() refuse it with CompileError ✅ pass live p1j-end-user-drive.txt S5, S6
A rule whose NOT(any()) binds only constants is refused with CompileError ✅ pass live p1j-end-user-drive.txt S7
20 claims racing on 20 separate connections: exactly one wins and all learn the same holder; claim with when/unless reports won=false and the holder for the loser ✅ pass live p1j-end-user-drive.txt S8
Millisecond latency of plain insert/query does not regress compared with the base SDK ✅ pass live latency-base-vs-change.txt
An engine 'conflict' failure on a conditional write is raised as ConflictError ⏸️ untested no The engine only returns a 'conflict' code when concurrent commits keep changing what a guard reads. Neither the 20-way claim race nor the randomized property test produced one against the local engine…
Evidence: Live vitest run: guards integration + unit compiled-text tests

 RUN  v3.2.7 ~/.no-mistakes/worktrees/9eea230c3aae/01M43CYT2B9Z2Y51YB1D1M8WFW/packages/inputlayer-js

 ✓ tests/compiler.test.ts > compileSchema > compiles a relation schema 1ms
 ✓ tests/compiler.test.ts > compileSchema timestamp > declares a timestamp column as int 0ms
 ✓ tests/compiler.test.ts > compileSchema timestamp > writes Timestamp and Date values as Unix milliseconds 0ms
 ✓ tests/compiler.test.ts > compileInsert > compiles a single insert 0ms
 ✓ tests/compiler.test.ts > compileInsert > compiles a session (ephemeral) insert 0ms
 ✓ tests/compiler.test.ts > compileBulkInsert > compiles a bulk insert 0ms
 ✓ tests/compiler.test.ts > compileDelete > compiles a single delete 0ms
 ✓ tests/compiler.test.ts > compileConditionalDelete > compiles a conditional delete 1ms
 ✓ tests/compiler.test.ts > compileQuery > compiles a simple full-relation query 1ms
 ✓ tests/compiler.test.ts > compileQuery > compiles a query with column selection and a where filter 1ms
 ✓ tests/compiler.test.ts > compileQuery > compiles a query with join 0ms
 ✓ tests/compiler.test.ts > compileQuery > keeps non-equality join conditions as filters 1ms
 ✓ tests/compiler.test.ts > compileQuery > binds computed columns in the body 0ms
 ✓ tests/compiler.test.ts > compileQuery > compiles a query with limit 0ms
 ✓ tests/compiler.test.ts > compileQuery > compiles a query with limit and offset 0ms
 ✓ tests/compiler.test.ts > compileQuery > compiles a query with order by as a sort annotation in the first atom 0ms
 ✓ tests/compiler.test.ts > compileQuery > moves the ordered column's relation first 0ms
 ✓ tests/compiler.test.ts > compileQuery > splits OR into branches merged client-side 2ms
 ✓ tests/compiler.test.ts > compileQuery > compiles an aggregation query to a program-local rule 1ms
 ✓ tests/compiler.test.ts > compileQuery > gives each aggregate of the same column its own result column 0ms
 ✓ tests/compiler.test.ts > compileQuery > expands topK into its passthrough and ordered columns 0ms
 ✓ tests/compiler.test.ts > compileQuery > aggregates over the union of OR branches 0ms
 ✓ tests/compiler.test.ts > compileQuery > rejects a query without a relation 1ms
 ✓ tests/compiler.test.ts > count() without a column > counts the first variable of the first atom 0ms
 ✓ tests/compiler.test.ts > count() without a column > counts the first column of the first joined relation 0ms
 ✓ tests/compiler.test.ts > count() without a column > never emits count<> in a rule head 0ms
 ✓ tests/compiler.test.ts > resultColumnIndexes > maps schema-named columns by position in the first atom 0ms
 ✓ tests/compiler.test.ts > resultColumnIndexes > maps variable-named columns by name 0ms
 ✓ tests/compiler.test.ts > compileRule > compiles a simple rule 0ms
 ✓ tests/compiler.test.ts > compileRule > compiles a session rule 0ms
 ✓ tests/compiler.test.ts > in / notIn > compiles in() to an atom of the target relation 0ms
 ✓ tests/compiler.test.ts > in / notIn > compiles notIn() to a negated atom 0ms
 ✓ tests/compiler.test.ts > in / notIn > places the variable at the target column and works through refs() 0ms
 ✓ tests/compiler.test.ts > in / notIn > binds the source column in a rule when it is not selected 0ms
 ✓ tests/compiler.test.ts > in / notIn > compiles in() on columns from a from().where() callback 0ms
 ✓ tests/compiler.test.ts > in / notIn > compiles in() in a query to a positive atom 0ms
 ✓ tests/compiler.test.ts > in / notIn > rejects a target column the target relation does not have 0ms
 ✓ tests/compiler.test.ts > in / notIn > rejects a target column with no relation definition 0ms
 ✓ tests/compiler.test.ts > AND/OR with three or more conditions > compiles in a query 0ms
 ✓ tests/compiler.test.ts > AND/OR with three or more conditions > compiles AND in a rule 0ms
 ✓ tests/guards.test.ts > any() in rules > compiles the hero rule: NOT(any()) bound to a joined column, after the positive atoms 3ms
 ✓ tests/guards.test.ts > any() in rules > shares a variable with an equality already pinning the constant 1ms
 ✓ tests/guards.test.ts > any() in rules > refuses a constant-only negation it cannot rewrite soundly in a rule 0ms
 ✓ tests/guards.test.ts > any() in rules > refuses a negation bound to a relation the body does not join 1ms
 ✓ tests/guards.test.ts > any() in rules > compiles a positive any() as an existence atom with _ for unbound columns 0ms
 ✓ tests/guards.test.ts > any() in rules > refuses an unknown column 0ms
 ✓ tests/guards.test.ts > any() in rules > works as RelationDef.any() 0ms
 ✓ tests/guards.test.ts > any() in queries > binds a constant-only negation through a program-local session fact 1ms
 ✓ tests/guards.test.ts > any() in queries > adds a positive any() atom without new result columns 0ms
 ✓ tests/guards.test.ts > debug() and why() > refuse a query whose NOT(any()) binds only constants, without contacting the engine 1ms
 ✓ tests/guards.test.ts > Program > commits plain statements as one program 1ms
 ✓ tests/guards.test.ts > Program > compiles .when(strict: false) to the token form 0ms
 ✓ tests/guards.test.ts > Program > compiles .when() with strict (the default) to the abort form 0ms
 ✓ tests/guards.test.ts > Program > carries the constant of a negation through a positive atom carrying it 0ms
 ✓ tests/guards.test.ts > Program > binds a negation-only guard through a staged il_txn_const row, deleted last 0ms
 ✓ tests/guards.test.ts > Program > anchors a guarded insert on the SDK ghost row, never a row of the written relation 0ms
 ✓ tests/guards.test.ts > Program > refuses strict: false for a program holding rules or schema 0ms
 ✓ tests/guards.test.ts > Program > refuses a guard condition over a relation the guard does not bind 0ms
 ✓ tests/guards.test.ts > Program > refuses a retract that names no column 0ms
 ✓ tests/guards.test.ts > Program > refuses an empty program and a missing column 0ms
 ✓ tests/guards.test.ts > Program > gives each program its own token 1ms
 ✓ tests/guards.test.ts > claim > compiles the hero claim: guarded insert, then the query on the key 0ms
 ✓ tests/guards.test.ts > claim > derives unless from key, and binds a lone negation through il_txn_const 0ms
 ✓ tests/guards.test.ts > claim > without a guard is a plain insert read back by every column 0ms
 ✓ tests/guards.test.ts > write reply grammar > reads every count reply 0ms
 ✓ tests/guards.test.ts > write reply grammar > refuses an unknown reply rather than guessing 0ms
 ✓ tests/guards.integration.test.ts > guards and claims (live) > count_reply_grammar: every write reply the SDK counts from parses 3ms
 ✓ tests/guards.integration.test.ts > guards and claims (live) > hero: a rule with NOT(any()), a claim once per need, cancel on the kill switch, re-claim when lifted 62ms
 ✓ tests/guards.integration.test.ts > guards and claims (live) > read path: a constant-only NOT(any()) binds through a session fact that does not outlive the query 7ms
 ✓ tests/guards.integration.test.ts > guards and claims (live) > guard_cursor_advance_applies_whole: a write gate that advances the cursor it guards on 38ms
 ✓ tests/guards.integration.test.ts > guards and claims (live) > guard_first_terminal_outcome: a negation-only guard binds through il_txn_const 10ms
 ✓ tests/guards.integration.test.ts > guards and claims (live) > guard_note_replacement: a program that deletes the note its guard reads applies whole 11ms
 ✓ tests/guards.integration.test.ts > guards and claims (live) > guarded writes keep a stored row equal to a typed placeholder 13ms
 ✓ tests/guards.integration.test.ts > guards and claims (live) > strict_guard_raises_precondition_failed_from_error_index 5ms
 ✓ tests/guards.integration.test.ts > guards and claims (live) > stale_deploy_is_refused_including_rules 25ms
 ✓ tests/guards.integration.test.ts > guards and claims (live) > deploy_applies_whole_when_guard_holds 17ms
 ✓ tests/guards.integration.test.ts > guards and claims (live) > claim_twenty_way_race: one winner, nineteen see its row 91ms
 ✓ tests/guards.integration.test.ts > guards and claims (live) > guarded_program_is_all_or_nothing: random programs apply whole or not at all 2672ms

 Test Files  3 passed (3)
      Tests  78 passed (78)
   Start at  12:53:49
   Duration  3.65s (transform 322ms, setup 0ms, collect 649ms, tests 3.17s, environment 1ms, prepare 204ms)
Evidence: End-user SDK drive transcript (S1-S9) with compiled IQL and engine state

== S1 adversarial ghost: rows equal to old typed placeholders survive guarded writes
guarded commit -> {"applied":true,"inserted":2,"deleted":0,"iql":"+il_txn_pending(\"t-6de80410-81ca-4b56-a2f2-de59a0856d8d\")\n-il_txn(\"\"), +il_txn(\"t-6de80410-81ca-4b56-a2f2-de59a0856d8d\") <- gate(\"open\")\n-il_assert(0), +il_assert(\"precondition_failed:t-6de80410-81ca-4b56-a2f2-de59a0856d8d\") <- il_txn_pending(K), K = \"t-6de80410-81ca-4b56-a2f2-de59a0856d8d\", !il_txn(K)\n-il_ghost(0), +paused(true) <- il_txn(\"t-6de80410-81ca-4b56-a2f2-de59a0856d8d\")\n-il_ghost(0), +counter(5) <- il_txn(\"t-6de80410-81ca-4b56-a2f2-de59a0856d8d\")\n-il_txn(\"t-6de80410-81ca-4b56-a2f2-de59a0856d8d\") <- il_txn(\"t-6de80410-81ca-4b56-a2f2-de59a0856d8d\")\n-il_txn_pending(\"t-6de80410-81ca-4b56-a2f2-de59a0856d8d\")"}
?paused(X) -> [[false],[true]]  ?counter(X) -> [[-1],[5]]
guarded insert of row equal to old ghost (paused(false)) -> {"applied":true,"inserted":0,"deleted":0,"iql":"+il_txn_pending(\"t-7ee46b33-bdb6-47c4-8087-565bc5bd7802\")\n-il_txn(\"\"), +il_txn(\"t-7ee46b33-bdb6-47c4-8087-565bc5bd7802\") <- gate(\"open\")\n-il_assert(0), +il_assert(\"precondition_failed:t-7ee46b33-bdb6-47c4-8087-565bc5bd7802\") <- il_txn_pending(K), K = \"t-7ee46b33-bdb6-47c4-8087-565bc5bd7802\", !il_txn(K)\n-il_ghost(0), +paused(false) <- il_txn(\"t-7ee46b33-bdb6-47c4-8087-565bc5bd7802\")\n-il_txn(\"t-7ee46b33-bdb6-47c4-8087-565bc5bd7802\") <- il_txn(\"t-7ee46b33-bdb6-47c4-8087-565bc5bd7802\")\n-il_txn_pending(\"t-7ee46b33-bdb6-47c4-8087-565bc5bd7802\")"}  ?paused -> [[false],[true]]

== S2 strict:false with failing guard -> applied=false, nothing written
result -> {"applied":false,"inserted":0,"deleted":0,"iql":"-il_txn(\"\"), +il_txn(\"t-b8ee816f-730c-4009-abb6-95b894aafe5b\") <- gate(\"closed\")\n-il_ghost(0), +job(\"j1\", \"x\") <- il_txn(\"t-b8ee816f-730c-4009-abb6-95b894aafe5b\")\n-il_txn(\"t-b8ee816f-730c-4009-abb6-95b894aafe5b\") <- il_txn(\"t-b8ee816f-730c-4009-abb6-95b894aafe5b\")"}  ?job -> []

== S3 strict (default) failing guard on a program with rule + schema -> PreconditionFailed, rule & schema & facts untouched
threw PreconditionFailed - Precondition failed: the program guard did not hold, nothing was applied
?item -> []  rule list has ready? false  .rel unchanged? true
leftovers il_txn/il_txn_pending/il_assert -> [[],[],[]]

== S4 strict guard holds -> whole program applied incl. rule
result -> {"applied":true,"inserted":1,"deleted":0,"iql":"+il_txn_pending(\"t-d235df24-9f40-4856-a133-582c2d990369\")\n-il_txn(\"\"), +il_txn(\"t-d235df24-9f40-4856-a133-582c2d990369\") <- gate(\"open\")\n-il_assert(0), +il_assert(\"precondition_failed:t-d235df24-9f40-4856-a133-582c2d990369\") <- il_txn_pending(K), K = \"t-d235df24-9f40-4856-a133-582c2d990369\", !il_txn(K)\n+ready(Name) <- item(Name)\n-il_ghost(0), +item(\"i2\") <- il_txn(\"t-d235df24-9f40-4856-a133-582c2d990369\")\n-il_txn(\"t-d235df24-9f40-4856-a133-582c2d990369\") <- il_txn(\"t-d235df24-9f40-4856-a133-582c2d990369\")\n-il_txn_pending(\"t-d235df24-9f40-4856-a133-582c2d990369\")"}  ?ready -> [["i2"]]

== S5 negation-only guard: NOT(any()) bound to constants on the write path
while blocked -> {"applied":false,"inserted":0,"deleted":0,"iql":"+il_txn_const_s(\"b\")\n-il_txn(\"\"), +il_txn(\"t-3abe8b49-7f18-4ae5-9a08-d4e566ae403e\") <- il_txn_const_s(K), !block(K), K = \"b\"\n-il_ghost(0), +item(\"blocked\") <- il_txn(\"t-3abe8b49-7f18-4ae5-9a08-d4e566ae403e\")\n-il_txn(\"t-3abe8b49-7f18-4ae5-9a08-d4e566ae403e\") <- il_txn(\"t-3abe8b49-7f18-4ae5-9a08-d4e566ae403e\")\n-il_txn_const_s(\"b\")"}  after lift -> {"applied":true,"inserted":1,"deleted":0,"iql":"+il_txn_const_s(\"b\")\n-il_txn(\"\"), +il_txn(\"t-64a0e01f-904b-47a6-9c2f-df73e7e9d624\") <- il_txn_const_s(K), !block(K), K = \"b\"\n-il_ghost(0), +item(\"unblocked\") <- il_txn(\"t-64a0e01f-904b-47a6-9c2f-df73e7e9d624\")\n-il_txn(\"t-64a0e01f-904b-47a6-9c2f-df73e7e9d624\") <- il_txn(\"t-64a0e01f-904b-47a6-9c2f-df73e7e9d624\")\n-il_txn_const_s(\"b\")"}  ?item -> [["i2"],["unblocked"]]  il_txn_const_s -> []

== S6 query NOT(any()) constant-only (read path) and debug()/why() refusal
query rows -> [["i2"],["unblocked"]]
debug -> CompileError true
why -> CompileError true

== S7 rule with constant-only NOT(any()) that cannot be rewritten -> CompileError
-> CompileError - NOT(any(block)) binds only constants, so it shares no variable with a positive atom. Bind the negated column to a column of a joined relation, e.g. NOT(any(block, { col: r.col("col") })) with r.col("col").eq(value)

== S8 claim: 20 racing claims across separate connections, one winner, all learn holder
winners: 1  distinct holders seen: 1  holder: {"id":"race","owner":"w0"}  stored -> [["race","w0"]]
claim with unless/when:  {"won":true,"holder":{"id":"j9","owner":"me"}} {"won":false,"holder":{"id":"j9","owner":"me"}}

== S9 latency (ms, median of 50) on this SDK
plain insert 2.20  plain query 0.83  guarded program 4.01  claim 3.06
Evidence: Latency base vs change

base-a4be4a2e insert 1.57/1.61/1.61 query 0.79/0.80/0.82 change-3bfce994 insert 1.82/1.56/1.62 query 0.81/0.79/0.75 (median ms over 200)

base-a4be4a2e median ms over 200: insert 1.57  query 0.79
change-3bfce994 median ms over 200: insert 1.82  query 0.81
base-a4be4a2e median ms over 200: insert 1.61  query 0.80
change-3bfce994 median ms over 200: insert 1.56  query 0.79
base-a4be4a2e median ms over 200: insert 1.61  query 0.82
change-3bfce994 median ms over 200: insert 1.62  query 0.75
Evidence: Engine server log
InputLayer Server
=================

�[2m2026-10-04T12:53:45.242166Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m data_dir_lock_acquired �[3mdata_dir�[0m�[2m=�[0m/tmp/il-nm-lMKu �[3melapsed_us�[0m�[2m=�[0m86
�[2m2026-10-04T12:53:45.245034Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_load_complete �[3mknowledge_graphs�[0m�[2m=�[0m0 �[3melapsed_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:53:45.247169Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m save_metadata_complete �[3melapsed_ms�[0m�[2m=�[0m1
�[2m2026-10-04T12:53:45.247181Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_create_complete �[3mkg�[0m�[2m=�[0mdefault �[3melapsed_ms�[0m�[2m=�[0m2
�[2m2026-10-04T12:53:45.249742Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m save_metadata_complete �[3melapsed_ms�[0m�[2m=�[0m2
�[2m2026-10-04T12:53:45.249753Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_create_complete �[3mkg�[0m�[2m=�[0m_internal �[3melapsed_ms�[0m�[2m=�[0m2
�[2m2026-10-04T12:53:45.274130Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m Auth bootstrap: reusing credentials from /tmp/il-nm-lMKu/credentials.toml
�[2m2026-10-04T12:53:45.278446Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine::program_commit�[0m�[2m:�[0m program_commit_persisted �[3mkg�[0m�[2m=�[0m_internal �[3mrelations�[0m�[2m=�[0m1 �[3mstatements�[0m�[2m=�[0m1 �[3mtime�[0m�[2m=�[0m1 �[3mpersist_ms�[0m�[2m=�[0m4
�[2m2026-10-04T12:53:45.278488Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m snapshot_publish_complete �[3mrelations�[0m�[2m=�[0m1 �[3mrules�[0m�[2m=�[0m0 �[3msnapshot_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:53:45.278496Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m Auth bootstrap: admin user created
�[2m2026-10-04T12:53:45.285950Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine::program_commit�[0m�[2m:�[0m program_commit_persisted �[3mkg�[0m�[2m=�[0m_internal �[3mrelations�[0m�[2m=�[0m3 �[3mstatements�[0m�[2m=�[0m1 �[3mtime�[0m�[2m=�[0m2 �[3mpersist_ms�[0m�[2m=�[0m7
�[2m2026-10-04T12:53:45.285992Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m snapshot_publish_complete �[3mrelations�[0m�[2m=�[0m4 �[3mrules�[0m�[2m=�[0m0 �[3msnapshot_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:53:45.285997Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m Auth bootstrap: API key 'bootstrap' created for admin
�[2m2026-10-04T12:53:45.287316Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m Auth bootstrap: credentials saved to /tmp/il-nm-lMKu/credentials.toml

=== INITIAL ADMIN CREDENTIALS CREATED ===
Admin API key: ****1a40
==========================================

Generated credentials saved to: /tmp/il-nm-lMKu/credentials.toml
Retrieve them with:  cat /tmp/il-nm-lMKu/credentials.toml
Delete this file to generate new credentials on next boot.

Storage engine initialized

HTTP Server
-----------
Address: 127.0.0.1:8197
GUI:     enabled

HTTP server listening on: http://127.0.0.1:8197
GUI dashboard available at: http://127.0.0.1:8197/
WebSocket API docs at: http://127.0.0.1:8197/api/ws-docs
�[2m2026-10-04T12:53:49.962709Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest::handlers::ws�[0m�[2m:�[0m ws_connection_start �[3mkg�[0m�[2m=�[0mdefault
�[2m2026-10-04T12:53:50.002617Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m audit_auth_login_success �[3musername�[0m�[2m=�[0madmin �[3mcredential�[0m�[2m=�[0mpassword:admin#2 �[3mpeer�[0m�[2m=�[0m127.0.0.1
�[2m2026-10-04T12:53:50.002657Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest::handlers::ws�[0m�[2m:�[0m ws_authenticated �[3mkg�[0m�[2m=�[0mdefault �[3musername�[0m�[2m=�[0madmin �[3mrole�[0m�[2m=�[0madmin �[3mcredential�[0m�[2m=�[0mpassword:admin#2
�[2m2026-10-04T12:53:50.002664Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_ensure_exists �[3mkg�[0m�[2m=�[0mdefault �[3melapsed_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:53:50.002679Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest::handlers::ws�[0m�[2m:�[0m ws_session_created �[3mkg�[0m�[2m=�[0mdefault �[3mactive_sessions�[0m�[2m=�[0m1
�[2m2026-10-04T12:53:50.005013Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest::handlers::ws�[0m�[2m:�[0m ws_connection_start �[3mkg�[0m�[2m=�[0mdefault
�[2m2026-10-04T12:53:50.018951Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m audit_auth_login_success �[3musername�[0m�[2m=�[0madmin �[3mcredential�[0m�[2m=�[0mpassword:admin#2 �[3mpeer�[0m�[2m=�[0m127.0.0.1
�[2m2026-10-04T12:53:50.018974Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest::handlers::ws�[0m�[2m:�[0m ws_authenticated �[3mkg�[0m�[2m=�[0mdefault �[3musername�[0m�[2m=�[0madmin �[3mrole�[0m�[2m=�[0madmin �[3mcredential�[0m�[2m=�[0mpassword:admin#2
�[2m2026-10-04T12:53:50.018981Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_ensure_exists �[3mkg�[0m�[2m=�[0mdefault �[3melapsed_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:53:50.018989Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest::handlers::ws�[0m�[2m:�[0m ws_session_created �[3mkg�[0m�[2m=�[0mdefault �[3mactive_sessions�[0m�[2m=�[0m2
�[2m2026-10-04T12:53:50.019609Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::rest::handlers::ws::execute�[0m�[2m:�[0m ws_execute_start �[3mprogram_len�[0m�[2m=�[0m15 �[3mprogram_preview�[0m�[2m=�[0m.kg use default �[2m�[3mrequest_id�[0m�[2m=�[0m1 �[3mmsg_bytes�[0m�[2m=�[0m75�[0m
�[2m2026-10-04T12:53:50.019681Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_job_start �[3mprogram_len�[0m�[2m=�[0m15
�[2m2026-10-04T12:53:50.019702Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_ensure_exists �[3mkg�[0m�[2m=�[0mdefault �[3melapsed_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:53:50.019710Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_parse_complete �[3mprogram_len�[0m�[2m=�[0m15 �[3mstatements�[0m�[2m=�[0m1 �[3mparse_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:53:50.019715Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m meta_kg_use_start �[3mkg�[0m�[2m=�[0mdefault
�[2m2026-10-04T12:53:50.019718Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_ensure_exists �[3mkg�[0m�[2m=�[0mdefault �[3melapsed_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:53:50.019720Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m meta_kg_use_ok �[3mkg�[0m�[2m=�[0mdefault
�[2m2026-10-04T12:53:50.019724Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_job_complete_messages �[3mprogram_len�[0m�[2m=�[0m15 �[3mtotal_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:53:50.019774Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_complete �[3mprogram_len�[0m�[2m=�[0m15 �[3mcompute_ms�[0m�[2m=�[0m0 �[3mok�[0m�[2m=�[0mtrue �[2m�[3mrequest_id�[0m�[2m=�[0m1 �[3mmsg_bytes�[0m�[2m=�[0m75�[0m
�[2m2026-10-04T12:53:50.019786Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::rest::handlers::ws::execute�[0m�[2m:�[0m ws_execute_end �[3mprogram_len�[0m�[2m=�[0m15 �[3melapsed_ms�[0m�[2m=�[0m0 �[3mok�[0m�[2m=�[0mtrue �[2m�[3mrequest_id�[0m�[2m=�[0m1 �[3mmsg_bytes�[0m�[2m=�[0m75�[0m
�[2m2026-10-04T12:53:50.020492Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::rest::handlers::ws::execute�[0m�[2m:�[0m ws_execute_start �[3mprogram_len�[0m�[2m=�[0m23 �[3mprogram_preview�[0m�[2m=�[0m.kg drop test_guards_js �[2m�[3mrequest_id�[0m�[2m=�[0m1 �[3mmsg_bytes�[0m�[2m=�[0m83�[0m
�[2m2026-10-04T12:53:50.020548Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_job_start �[3mprogram_len�[0m�[2m=�[0m23
�[2m2026-10-04T12:53:50.020557Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_ensure_exists �[3mkg�[0m�[2m=�[0mdefault �[3melapsed_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:53:50.020562Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_parse_complete �[3mprogram_len�[0m�[2m=�[0m23 �[3mstatements�[0m�[2m=�[0m1 �[3mparse_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:53:50.020568Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m meta_kg_drop_start �[3mkg�[0m�[2m=�[0mtest_guards_js
�[2m2026-10-04T12:5

... [13149590 bytes truncated] ...

.012343Z�[0m �[32m INFO�[0m �[2minputlayer�[0m�[2m:�[0m engine_execute_complete �[3msource_len�[0m�[2m=�[0m1 �[3mtotal_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:55:35.012346Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine::snapshot�[0m�[2m:�[0m snapshot_execute �[3mprogram_len�[0m�[2m=�[0m29 �[3mrules�[0m�[2m=�[0m1 �[3msession_facts�[0m�[2m=�[0m0 �[3melapsed_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:55:35.012352Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_engine_exec_complete �[3mprogram_len�[0m�[2m=�[0m11 �[3mkg�[0m�[2m=�[0mnm_lat �[3mquery_exec_ms�[0m�[2m=�[0m0 �[3mhas_session_facts�[0m�[2m=�[0mfalse
�[2m2026-10-04T12:55:35.012383Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_job_complete �[3mprogram_len�[0m�[2m=�[0m11 �[3mtotal_ms�[0m�[2m=�[0m0 �[3mrow_count�[0m�[2m=�[0m200 �[3mtotal_count�[0m�[2m=�[0m200 �[3mtruncated�[0m�[2m=�[0mfalse
�[2m2026-10-04T12:55:35.012423Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_complete �[3mprogram_len�[0m�[2m=�[0m11 �[3mcompute_ms�[0m�[2m=�[0m0 �[3mok�[0m�[2m=�[0mtrue �[2m�[3mrequest_id�[0m�[2m=�[0m405 �[3mmsg_bytes�[0m�[2m=�[0m73�[0m
�[2m2026-10-04T12:55:35.012433Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::rest::handlers::ws::execute�[0m�[2m:�[0m ws_execute_end �[3mprogram_len�[0m�[2m=�[0m11 �[3melapsed_ms�[0m�[2m=�[0m0 �[3mok�[0m�[2m=�[0mtrue �[2m�[3mrequest_id�[0m�[2m=�[0m405 �[3mmsg_bytes�[0m�[2m=�[0m73�[0m
�[2m2026-10-04T12:55:35.012929Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::rest::handlers::ws::execute�[0m�[2m:�[0m ws_execute_start �[3mprogram_len�[0m�[2m=�[0m15 �[3mprogram_preview�[0m�[2m=�[0m.kg use default �[2m�[3mrequest_id�[0m�[2m=�[0m2 �[3mmsg_bytes�[0m�[2m=�[0m75�[0m
�[2m2026-10-04T12:55:35.012974Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_job_start �[3mprogram_len�[0m�[2m=�[0m15
�[2m2026-10-04T12:55:35.012980Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_ensure_exists �[3mkg�[0m�[2m=�[0mdefault �[3melapsed_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:55:35.012984Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_parse_complete �[3mprogram_len�[0m�[2m=�[0m15 �[3mstatements�[0m�[2m=�[0m1 �[3mparse_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:55:35.012988Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m meta_kg_use_start �[3mkg�[0m�[2m=�[0mdefault
�[2m2026-10-04T12:55:35.012990Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_ensure_exists �[3mkg�[0m�[2m=�[0mdefault �[3melapsed_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:55:35.012992Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m meta_kg_use_ok �[3mkg�[0m�[2m=�[0mdefault
�[2m2026-10-04T12:55:35.013000Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_job_complete_messages �[3mprogram_len�[0m�[2m=�[0m15 �[3mtotal_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:55:35.013031Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_complete �[3mprogram_len�[0m�[2m=�[0m15 �[3mcompute_ms�[0m�[2m=�[0m0 �[3mok�[0m�[2m=�[0mtrue �[2m�[3mrequest_id�[0m�[2m=�[0m2 �[3mmsg_bytes�[0m�[2m=�[0m75�[0m
�[2m2026-10-04T12:55:35.013041Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::rest::handlers::ws::execute�[0m�[2m:�[0m ws_execute_end �[3mprogram_len�[0m�[2m=�[0m15 �[3melapsed_ms�[0m�[2m=�[0m0 �[3mok�[0m�[2m=�[0mtrue �[2m�[3mrequest_id�[0m�[2m=�[0m2 �[3mmsg_bytes�[0m�[2m=�[0m75�[0m
�[2m2026-10-04T12:55:35.014761Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::rest::handlers::ws::execute�[0m�[2m:�[0m ws_execute_start �[3mprogram_len�[0m�[2m=�[0m15 �[3mprogram_preview�[0m�[2m=�[0m.kg drop nm_lat �[2m�[3mrequest_id�[0m�[2m=�[0m4 �[3mmsg_bytes�[0m�[2m=�[0m75�[0m
�[2m2026-10-04T12:55:35.014805Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_job_start �[3mprogram_len�[0m�[2m=�[0m15
�[2m2026-10-04T12:55:35.014815Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_ensure_exists �[3mkg�[0m�[2m=�[0mdefault �[3melapsed_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:55:35.014822Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_parse_complete �[3mprogram_len�[0m�[2m=�[0m15 �[3mstatements�[0m�[2m=�[0m1 �[3mparse_ms�[0m�[2m=�[0m0
�[2m2026-10-04T12:55:35.014827Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m meta_kg_drop_start �[3mkg�[0m�[2m=�[0mnm_lat
�[2m2026-10-04T12:55:35.027029Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m save_metadata_complete �[3melapsed_ms�[0m�[2m=�[0m2
�[2m2026-10-04T12:55:35.027040Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_drop_prepare_complete �[3mkg�[0m�[2m=�[0mnm_lat �[3melapsed_ms�[0m�[2m=�[0m12
�[2m2026-10-04T12:55:35.027044Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m meta_kg_drop_prepare_ok �[3mkg�[0m�[2m=�[0mnm_lat
�[2m2026-10-04T12:55:35.035940Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m save_metadata_complete �[3melapsed_ms�[0m�[2m=�[0m2
�[2m2026-10-04T12:55:35.038235Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m kg_drop_finish_complete �[3mkg�[0m�[2m=�[0mnm_lat �[3melapsed_ms�[0m�[2m=�[0m11
�[2m2026-10-04T12:55:35.038245Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m meta_kg_drop_finish_ok �[3mkg�[0m�[2m=�[0mnm_lat
�[2m2026-10-04T12:55:35.038261Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_statement_exec_complete �[3mprogram_len�[0m�[2m=�[0m15 �[3mstmt_exec_ms�[0m�[2m=�[0m23 �[3msession_facts�[0m�[2m=�[0m0 �[3msession_rules�[0m�[2m=�[0m0
�[2m2026-10-04T12:55:35.038268Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_job_complete_messages �[3mprogram_len�[0m�[2m=�[0m15 �[3mtotal_ms�[0m�[2m=�[0m23
�[2m2026-10-04T12:55:35.038398Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::handler�[0m�[2m:�[0m query_complete �[3mprogram_len�[0m�[2m=�[0m15 �[3mcompute_ms�[0m�[2m=�[0m23 �[3mok�[0m�[2m=�[0mtrue �[2m�[3mrequest_id�[0m�[2m=�[0m4 �[3mmsg_bytes�[0m�[2m=�[0m75�[0m
�[2m2026-10-04T12:55:35.038411Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::session�[0m�[2m:�[0m sessions_closed_for_dropped_kg �[3mkg�[0m�[2m=�[0m"nm_lat" �[3mclosed�[0m�[2m=�[0m1 �[2m�[3mrequest_id�[0m�[2m=�[0m4 �[3mmsg_bytes�[0m�[2m=�[0m75�[0m
�[2m2026-10-04T12:55:35.038421Z�[0m �[32m INFO�[0m �[1mws_request�[0m: �[2minputlayer::protocol::rest::handlers::ws::execute�[0m�[2m:�[0m ws_execute_end �[3mprogram_len�[0m�[2m=�[0m15 �[3melapsed_ms�[0m�[2m=�[0m23 �[3mok�[0m�[2m=�[0mtrue �[2m�[3mrequest_id�[0m�[2m=�[0m4 �[3mmsg_bytes�[0m�[2m=�[0m75�[0m
�[2m2026-10-04T12:55:35.039028Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest::handlers::ws�[0m�[2m:�[0m ws_session_disconnecting �[3mactive_sessions�[0m�[2m=�[0m2
�[2m2026-10-04T12:55:35.039050Z�[0m �[33m WARN�[0m �[2minputlayer::protocol::rest::handlers::ws�[0m�[2m:�[0m session_cleanup_failed �[3merror�[0m�[2m=�[0mSession a1f84505-117f-41a4-a27f-af1e63f6b318 not found
�[2m2026-10-04T12:55:35.039163Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest::handlers::ws�[0m�[2m:�[0m ws_session_disconnecting �[3mactive_sessions�[0m�[2m=�[0m2
�[2m2026-10-04T12:55:35.039263Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest::handlers::ws�[0m�[2m:�[0m ws_session_disconnecting �[3mactive_sessions�[0m�[2m=�[0m1
�[2m2026-10-04T12:55:45.103771Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest�[0m�[2m:�[0m Received SIGTERM, shutting down...
�[2m2026-10-04T12:55:45.103951Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest�[0m�[2m:�[0m session_reaper_shutdown
�[2m2026-10-04T12:55:45.103964Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::rest�[0m�[2m:�[0m auto_compact_shutdown
�[2m2026-10-04T12:55:45.104013Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m Flushing WAL and saving metadata...
�[2m2026-10-04T12:55:45.131859Z�[0m �[32m INFO�[0m �[2minputlayer::storage_engine�[0m�[2m:�[0m save_metadata_complete �[3melapsed_ms�[0m�[2m=�[0m2
�[2m2026-10-04T12:55:45.131881Z�[0m �[32m INFO�[0m �[2minputlayer::protocol::handler�[0m�[2m:�[0m Shutdown complete.
- Outcome: ⚠️ 0 issues across 3 runs (26m40s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 5 issues found → auto-fixed (2) ✅
  • ⚠️ packages/inputlayer-js/src/program.ts:64 - The ghost delete row of a guarded insert isn't guaranteed to be absent: ghost() builds it from ordinary values in the user's own relation (&#34;&#34;, -1, -1.0, false, []). The intent asks for a "never-present ghost delete row", but these are valid values that can be stored. Two concrete cases: (1) A relation Paused(on: bool) holds paused(false), or Counter(id: int) holds counter(-1). Any guarded insert into that relation compiles to -paused(false), +paused(true) &lt;- il_txn(&#34;t-…&#34;), which silently deletes the user's existing row and counts it in deleted. (2) Inserting a row equal to the ghost, for example a guarded insert(Paused, {on: false}), gives -paused(false), +paused(false), whose result depends on engine delete/insert ordering. The same ghost is emitted by compileClaim (program.ts:375), so kg.claim() has the same problem. Fix: anchor the update form on a row of an SDK-owned relation that is never written, such as -il_txn(&#34;&#34;). The token statement already uses that form, and the live grammar test already runs -il_ghost(0), +flag_grammar(&#34;b&#34;) &lt;- …, showing the engine accepts a delete on a different relation. A test with a pre-existing row equal to the old ghost would pin this.
  • ⚠️ packages/inputlayer-js/src/knowledge-graph.ts:327 - KnowledgeGraph.retract() is a new top-level public write method. The P1-J intent covers any()/NOT(any()), kg.program().when(), kg.claim() and the error types, not a standalone retract on the knowledge graph. Program.retract already does this, and the tests could use kg.program().retract(...).commit(). Recommend removing it unless the design calls for it.
  • ⚠️ packages/inputlayer-js/src/relation.ts:79 - RelationDef.any() is a second spelling of the free function any(Relation, {...}) that the intent specifies. No requirement needs the method form. Recommend removing it so the public API has one way to write an existence check.
  • ℹ️ packages/inputlayer-js/src/proxy.ts:172 - AND/OR now accept any number of arguments instead of exactly two. This is a public API change the P1-J intent doesn't ask for; the hero rule could nest binary ANDs. It's backward-compatible and small, but it widens scope. Confirm it's wanted, otherwise remove it.
  • ℹ️ packages/inputlayer-js/src/knowledge-graph.ts:866 - The insert-reply regex is now defined twice: INSERTED in knowledge-graph.ts:866 (used by insertedCount) and INSERTED in program.ts as part of the reply grammar the live test pins. If the engine's message changes, one copy can be updated and the other missed. Make insertedCount use parseWriteMessage so there is a single owner of the grammar.

🔧 Fix applied.
3 issues (2 warnings, 1 info) still open:

  • ⚠️ packages/inputlayer-js/src/knowledge-graph.ts:327 - KnowledgeGraph.retract() is a new top-level public write method. The P1-J intent covers any()/NOT(any()), kg.program().when(), kg.claim() and the error types, not a standalone retract on the knowledge graph. Program.retract already does this, and the tests could use kg.program().retract(...).commit(). Recommend removing it unless the design calls for it.

  • ⚠️ packages/inputlayer-js/src/relation.ts:79 - RelationDef.any() is a second spelling of the free function any(Relation, {...}) that the intent specifies. No requirement needs the method form. Recommend removing it so the public API has one way to write an existence check.

  • ℹ️ packages/inputlayer-js/src/proxy.ts:172 - AND/OR now accept any number of arguments instead of exactly two. This is a public API change the P1-J intent doesn't ask for; the hero rule could nest binary ANDs. It's backward-compatible and small, but it widens scope. Confirm it's wanted, otherwise remove it.

  • ⚠️ packages/inputlayer-js/src/knowledge-graph.ts:816 - explainable() is defined but never called, so kg.debug() and kg.why() give wrong results without failing for a query whose NOT(any()) binds only constants. In that case compileQueryPlan binds the constant through an il_const_&lt;type&gt; session fact, but it adds that fact only to plan.programs (compiler.ts:895), not to plan.debug or plan.why. debug() (knowledge-graph.ts:694) and why() (knowledge-graph.ts:707) send the bare statement. It reads il_const_s(K), which has no rows, so for example kg.why({select:[ToolPolicy], where: NOT(any(KillSwitch,{tool:&#39;x&#39;}))}) explains an empty result or hits an unknown-relation error, while kg.query() with the same options returns rows. Fix: wrap the plan in explainable(plan, &#39;debug&#39;) / explainable(plan, &#39;why&#39;) in both methods. That is the CompileError refusal the helper was written for, and it was introduced in 0ad8571.

🔧 Fix applied.
✅ Re-checked - no issues remain.

⚠️ **Test** - 0 issues

✅ No issues found.

  • Live validation: ✅ go - 17 of 17 scenarios driven live against the product
Scenario Result Live Evidence
guard_cursor_advance_applies_whole: guarded cursor advance applies whole, a replay applies nothing, and a two-connection race applies exactly once ✅ pass live guards-live.txt (guard_cursor_advance_applies_whole)
guard_first_terminal_outcome: a negation-only guard records the first terminal outcome and refuses the second ✅ pass live guards-live.txt (guard_first_terminal_outcome)
guard_note_replacement: a program that deletes the note its guard reads applies whole ✅ pass live guards-live.txt (guard_note_replacement)
claim_twenty_way_race: 20 connections claim the same key; exactly one wins and all 20 see the winner as holder ✅ pass live guards-live.txt (claim_twenty_way_race)
strict_guard_raises_precondition_failed_from_error_index: a strict guard that does not hold raises PreconditionFailed and writes nothing ✅ pass live guards-live.txt; adhoc-transcript.txt (strict on false guard -> PreconditionFailed)
stale_deploy_is_refused_including_rules: a deploy guarded on the wrong version leaves the rule definition and data unchanged ✅ pass live guards-live.txt (stale_deploy_is_refused_including_rules)
deploy_applies_whole_when_guard_holds: rule replacement plus version bump apply together ✅ pass live guards-live.txt (deploy_applies_whole_when_guard_holds)
guarded_program_is_all_or_nothing: 25 random strict and non-strict programs either apply fully or leave state unchanged, with no il_txn/il_assert leftovers ✅ pass live guards-live.txt (guarded_program_is_all_or_nothing)
strict:false on a false guard returns applied=false with 0 inserted and 0 deleted ✅ pass live adhoc-transcript.txt (strict:false on false guard)
Adversarial ghost anchor: stored rows paused(false) and counter(-1) survive guarded inserts and claims, and deleted stays 0 ✅ pass live adhoc-transcript.txt (paused rows [[false],[true]] counter rows [[-1],[5]]); guards-live.txt (guarded writes keep a stored row equal to a typed placeholder)
Hero rule with NOT(any(KillSwitch)): need appears, claim once, kill switch cancels, lifting it re-enables the claim ✅ pass live guards-live.txt (hero test)
Read path: a query with a constant-only NOT(any()) binds through a session fact that is gone afterwards ✅ pass live guards-live.txt (read path test)
Adversarial: a rule with a constant-only NOT(any()) and a guard containing OR are refused with CompileError before anything is sent; a rewritable negation (positive column pinned to the same constant)… ✅ pass live adhoc-transcript.txt (CompileError lines; unlocked before lock [[j9]] after lock [])
claim with when/unless: the first claimant wins, a rival learns the holder, and a claim whose when guard fails returns holder null ✅ pass live adhoc-transcript.txt (claim A/B/when-false)
Pinned count-reply grammar parses every write reply the engine sends, including the il_ghost(0) update form ✅ pass live guards-live.txt (count_reply_grammar)
Millisecond latency kept: guarded program and claim medians stay in single-digit ms ✅ pass live adhoc-transcript.txt latency line: insert 2.25, guarded 3.55, strict:false 3.14, claim 2.73 ms
Existing live JS SDK behaviour (docs queries, integration) still works after the insert/query changes ✅ pass live other-live.txt (81 passed)
  • CARGO_TARGET_DIR=/tmp/nm-target-01M43CYT cargo build --release --bin inputlayer-server, then started the server on 127.0.0.1:18791 with a throwaway --data-dir (removed afterwards)

  • npx vitest run tests/guards.test.ts tests/compiler.test.ts (unit tests on compiled text, 65 passed)

  • INPUTLAYER_TEST_SERVER=ws://127.0.0.1:18791/ws npx vitest run --reporter=verbose tests/guards.integration.test.ts (12 live guard/claim tests passed)

  • npx vitest run tests/docs-queries.integration.test.ts tests/integration.test.ts live (81 passed; these are the other suites make js-test-live runs)

  • Temporary live driver tests/zz-nm-adhoc.integration.test.ts (copied to evidence, then deleted from the worktree), covering: the ghost anchor with stored paused(false)/counter(-1) rows; strict:false vs strict on a false guard; CompileError for a constant-only NOT(any()) in a rule and for OR in a guard; the rule rewrite when a positive column is pinned to the same constant; claim winner/loser/when-false; median latency over 30 runs

  • 🚨 packages/inputlayer-js/tests/guards.integration.test.ts:326 - claim_twenty_way_race failed every time with 'Unexpected server response: 429'. Since the connection core (feat(inputlayer-js): routed connection core with request ids, deadlines and reconnect #237), each KnowledgeGraph handle opens its own lazily-bound socket. The test bound all 20 handles with Promise.all, which opened 19 sockets at once and went over the engine's per-IP limit on unauthenticated sockets (ws_max_preauth_per_ip = 16; the server log shows ws_preauth_limit_exceeded). Fixed in the test by binding the handles one at a time. The 20 claims still go out together, and the test now passes reliably.

  • ⚠️ packages/inputlayer-js/tests/integration.test.ts:979 - 'Integration: Engine failures > dropping the current knowledge graph rejects without switching it' fails in the js-test-live file set (dropKnowledgeGraph resolves instead of rejecting with QueryError). It fails the same way on the base commit a4be4a2 against the same server, so this change didn't cause it, and it's outside P1-J. But it will make the js-sdk-live CI job fail, and the intent relies on that job.

  • Live validation: ✅ go - 13 of 13 scenarios driven live against the product

Scenario Result Live Evidence
A rule body using NOT(any(Rel, {col: column})) drops a row once the blocking fact is inserted ✅ pass live demo-transcript.txt §1; guards-live.txt hero test
Adversarial: a rule whose NOT(any()) binds only constants is refused with CompileError ✅ pass live demo-transcript.txt §2
A query with a constant-only NOT(any()) returns correct rows through the il_const session fact, which does not outlive the query ✅ pass live demo-transcript.txt §3; guards-live.txt 'read path'
Adversarial: kg.debug()/kg.why() refuse a constant-only NOT(any()) query with CompileError ✅ pass live demo-transcript.txt §3
program().when() with strict:false applies whole once (applied, inserted=2, deleted=1); a replay applies nothing ✅ pass live demo-transcript.txt §4; guards-live.txt guard_cursor_advance_applies_whole, guard_first_terminal_outcome, guard_note_replacement
A strict guard that fails raises PreconditionFailed (read from the error index) and writes nothing ✅ pass live demo-transcript.txt §5; guards-live.txt strict_guard_raises_precondition_failed_from_error_index
A stale deploy with rules and schema is refused whole; a deploy whose guard holds applies whole ✅ pass live guards-live.txt stale_deploy_is_refused_including_rules, deploy_applies_whole_when_guard_holds; demo §6
Adversarial ghost row: a stored Paused(false) / typed-placeholder row survives guarded inserts and claims (il_ghost(0) anchor) ✅ pass live demo-transcript.txt §7; guards-live.txt 'guarded writes keep a stored row equal to a typed placeholder'
claim() race: 20 (and 10) concurrent claimers on separate connections produce exactly one winner, everyone agrees on the holder, and no il_txn rows are left ✅ pass live guards-live.txt claim_twenty_way_race (after test fix), race-repeat.txt, demo §8
Property: random guarded programs apply whole or not at all ✅ pass live guards-live.txt guarded_program_is_all_or_nothing
Write-reply count grammar parses every reply the engine sends for counted writes ✅ pass live guards-live.txt count_reply_grammar
Variadic AND/OR with 3+ arguments compiles and runs in a query ✅ pass live demo-transcript.txt §9; tests/guards.test.ts unit tests
Latency stays in milliseconds: guarded program vs plain insert ✅ pass live demo-transcript.txt §10 (plain median 1.90 ms, guarded median 3.21 ms)
  • cargo build --release --bin inputlayer-server and an isolated server on 127.0.0.1:8197 with a throwaway --data-dir (removed afterwards)
  • INPUTLAYER_TEST_SERVER=ws://127.0.0.1:8197/ws npx vitest run --reporter=verbose tests/guards.integration.test.ts (before fix: 11/12, race test 429; after fix: 12/12)
  • npx vitest run tests/guards.integration.test.ts -t twenty, 5 more runs, all pass
  • npx vitest run tests/guards.test.ts tests/compiler.test.ts (unit tests on compiled text, including 3+-argument AND/OR and the debug/why CompileError)
  • js-test-live file set: npx vitest run tests/docs-queries.integration.test.ts tests/integration.test.ts tests/connection.integration.test.ts tests/guards.integration.test.ts (99/100; the one failure also happens on base)
  • Base-commit check: ran a4be4a2e's tests/integration.test.ts against the same server (same failure)
  • End-user script npx vite-node demo.ts against the live server: rule NOT(any()), constant-only refusals, read-path il_const, when() strict:false apply/replay, strict PreconditionFailed, stale deploy with rule, ghost anchor keeping Paused(false), 10-way claim race, OR with 3 arguments, latency comparison

🔧 Fix applied.
✅ Re-checked - no issues remain.

  • Live validation: ✅ go - 8 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Named live tests from the intent (cursor advance, first terminal outcome, note replacement, claim race, strict PreconditionFailed, stale deploy refused incl. rules, deploy applies whole, all-or-nothin… ✅ pass live guards-live-vitest.txt
Adversarial ghost: a guarded insert into a relation that already holds paused(false)/counter(-1) keeps those rows (anchor is -il_ghost(0)) ✅ pass live p1j-end-user-drive.txt S1: ?paused -> [[false],[true]], ?counter -> [[-1],[5]], deleted=0
Strict program with schema+rule+fact and a failing guard raises PreconditionFailed and leaves rules, schema, facts and guard relations untouched ✅ pass live p1j-end-user-drive.txt S3
strict:false with a failing guard returns applied=false and writes nothing; strict with a holding guard applies the rule and facts whole ✅ pass live p1j-end-user-drive.txt S2, S4
NOT(any()) bound only to constants works as a write guard (staged il_txn_const row deleted last) and as a query filter (session fact), and debug()/why() refuse it with CompileError ✅ pass live p1j-end-user-drive.txt S5, S6
A rule whose NOT(any()) binds only constants is refused with CompileError ✅ pass live p1j-end-user-drive.txt S7
20 claims racing on 20 separate connections: exactly one wins and all learn the same holder; claim with when/unless reports won=false and the holder for the loser ✅ pass live p1j-end-user-drive.txt S8
Millisecond latency of plain insert/query does not regress compared with the base SDK ✅ pass live latency-base-vs-change.txt
An engine 'conflict' failure on a conditional write is raised as ConflictError ⏸️ untested no The engine only returns a 'conflict' code when concurrent commits keep changing what a guard reads. Neither the 20-way claim race nor the randomized property test produced one against the local engine…
  • CARGO_TARGET_DIR=/tmp/nm-il-target cargo build --release --bin inputlayer-server, then the server started with a throwaway --data-dir on 127.0.0.1:8197
  • INPUTLAYER_TEST_SERVER=ws://127.0.0.1:8197/ws npx vitest run tests/guards.integration.test.ts tests/guards.test.ts tests/compiler.test.ts --reporter=verbose (78/78 passed, including all 8 named live tests from the intent)
  • End-user script run with npx vite-node that imports the SDK and checks: ghost-collision rows, strict:false applied=false, strict abort rolling back rule+schema+facts, strict apply-whole with a rule, NOT(any()) guard bound only to constants, read-path NOT(any()) plus debug()/why() refusal, rule CompileError, 20-connection claim race, and claim with when/unless
  • Latency comparison: the base-commit SDK (git archive a4be4a2e) against this change on the same server, median of 200 insert and query calls, 3 rounds
🔧 **Document** - 1 issue found ✅
  • ℹ️ docs/content/docs/guides/js-sdk.mdx - The JS SDK guide does not yet cover how to use any()/NOT(any()), kg.program().when()/strict, kg.claim() or kg.retract(). The intent leaves docs out of scope for P1-J (they belong to the separate phase-1 docs/README hero item), so this pass only added CompileError, PreconditionFailed and ConflictError to the guide's error-class list. Follow-up: write the usage section for guards and claims in that docs item.

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

✅ No issues found.

jsam added 5 commits October 4, 2026 12:37
…n relation()/from()

Guarded programs compile to the transaction-token form (strict: false) or
the abort form (strict, the default): the guard is evaluated once, every
fact write is conditioned on the token, and a false guard rolls the whole
program back, rules and schema included, raising PreconditionFailed from
the assertion's error index. claim() is the guarded insert with unless as a
negated atom plus a query on the key in the same program. any() and
NOT(any()) work in rules, queries, conditional deletes and guards; a
negated atom bound only to constants shares a variable with an equal
positive column, else binds through il_const_<t> (queries) or a staged
il_txn_const_<t> row (writes). kg.define() declares il_txn,
il_txn_pending and il_assert in one program. Unit tests pin the compiled
text; live tests (guard_cursor_advance_applies_whole,
guard_first_terminal_outcome, guard_note_replacement,
claim_twenty_way_race, strict_guard_raises_precondition_failed_from_error_index,
stale_deploy_is_refused_including_rules, deploy_applies_whole_when_guard_holds,
guarded_program_is_all_or_nothing, count-reply grammar, the hero flow)
join make js-test-live.
@jsam
jsam merged commit 2a74841 into main Oct 4, 2026
20 checks passed
@jsam
jsam deleted the fm/sdk-p1-j branch October 4, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant