Skip to content

feat(inputlayer-py): add guarded programs, claim() and R.any()/~R.any() existence checks - #247

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

jsam merged 4 commits into
mainfrom
fm/sdk-p1-i

Conversation

@jsam

@jsam jsam commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Intent

So fix the SDKs by improving them. (Context: agreed SDK design ~/firstmate/data/sdk-design/report.md; this is phase-1 PR P1-I P1-I (Py). Its JS twin merged as #240 (claim(), program().when(), any(), retract, variadic AND/OR); keep semantics identical. Python compiler v2 (#235, #239) and connection core (#236) are merged.)

Substance of the referenced design item P1-I (Python SDK): kg.claim(), kg.program().when() in both guard forms (token and abort) and the il_txn_const write-path binding, R.any() on today's Relation/Derived/From; il_txn, il_txn_pending, il_assert declared by define(); count-grammar and abort-form conformance tests; live tests guard_cursor_advance_applies_whole, guard_first_terminal_outcome (negation-only shape), 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, property test guarded_program_is_all_or_nothing.

Design semantics referenced (sections 5, 9, 11 and the JS twin #240): kg.program() collects statements committed as one request and one transaction. .when() makes the whole program conditional and the guard is evaluated once: the token form (strict=False, fact-only programs) is statement 1 -il_txn(""), +il_txn("t-<id>") <- guard, every insert becomes -il_ghost(0), +r(...) <- il_txn("t-<id>") (a single-head +r(...) <- body is never emitted for a base relation because the engine registers it as a rule), every conditional delete gets , il_txn("t-<id>") appended, and the last statement removes the token; applied is read from the token statement's count and strict=False returns applied=False instead of raising. The abort form (strict=True, the default, mandatory for programs holding rules or schema, which a token cannot condition) adds +il_txn_pending("t-") before the token statement and the assertion -il_assert(0), +il_assert("precondition_failed:t-<id>") <- il_txn_pending(K), K = "t-<id>", !il_txn(K) after it, with il_assert(v: int), so a false guard fails the program at the assertion index and the engine rolls back everything, rules and schema included; PreconditionFailed is raised structurally from errors[0].index == assertion index, never from message text. strict=False with rules or schema is refused with a CompileError. claim(row, when=, unless=, key=) is the guarded insert with unless as a negated atom in the guard plus a query on the key in the same program; it returns Claim(won, holder) with holder None when the when-guard failed; of 20 concurrent claims exactly one wins. R.any(**cols) / ~R.any(...) are existence checks usable in rules, queries, conditional deletes and guards; a negated atom must share a variable with a positive atom: when a positive atom already carries the constant (an equality or the same constant in a positive any()) the compiler shares a variable there, otherwise it binds the constant through an SDK-owned typed constant relation: a program-local session fact il_const_ on the read path, persistent il_const_ rows for views, and on the write path a staged persistent il_txn_const_ row inserted before the guard and deleted last (session facts are invisible to update bodies). Write counts are read from the pinned per-statement reply grammar (Inserted N fact(s) into ''., Update: N deleted, M inserted., Conditional delete: N fact(s) deleted from ''., Deleted N facts from ''.) and a live conformance test pins that grammar and the abort form against every engine build in CI. A conflict on a conditional write raises Conflict (retry with backoff). Live tests run against a real engine in CI (make python-sdk-live).

What Changed

  • Python SDK guarded writes: added kg.program(), which commits its statements as one request and one transaction. .when() makes the whole program conditional. Both guard forms exist: the token form (strict=False, fact-only, returns applied=False) and the abort form (strict=True, the default, which also works for rules and schema). Also added kg.claim(row, when=, unless=, key=), which returns Claim(won, holder), plus kg.retract(). The sync client has matching ProgramSync, program(), claim() and retract(). New exports: Program, ProgramResult, Claim, and the PreconditionFailed and Conflict exceptions. PreconditionFailed is raised from the error index of the assertion statement, never from message text.
  • Compiler: added R.any(**cols) / ~R.any(...) existence checks for rules, queries, conditional deletes and guards. A negated atom shares a variable with a positive atom when one already carries the constant. Otherwise the compiler binds the constant through SDK-owned il_const_<t> relations (read path and views) or staged il_txn_const_<t> rows (write path). define() now declares il_txn, il_txn_pending and il_assert. Write counts are parsed from the pinned per-statement reply grammar.
  • Tests and docs: added unit tests (test_guards.py) and live tests (guards_live.py) covering the count-reply grammar and abort-form conformance, the 20-way claim race, stale deploys, and the all-or-nothing property. The live tests are wired into make python-sdk-live. Added guarded-write and claim sections to the Python and JS SDK guides, and updated the READMEs to say subscribe()/claim() are merged on main but not yet in a release.

Risk Assessment

⚠️ Medium: The round-1 fix holds. define_rules now stores each constant row and each clause as its own _Plain statement, so the unguarded and guarded write_indexes match the engine's per-statement reply rows. The new test asserts the correct indexes, and those indexes would be off by one without the fix. The risk stays medium only because the change as a whole adds a lot of new transactional write-path machinery whose correctness the live tests must still confirm against the engine.

Testing

Built the release server all the way through and started it on an isolated port with a throwaway data dir. Ran the make python-sdk-live suites against it: docs queries, compile rules, subscriptions and guards, 70 tests, all passing. That covers every guard and claim live test, including the count-reply grammar conformance test, the 20-way claim race and the all-or-nothing property test. Also ran an extra driver for the constant-row index fix, rule handling under guards, the strict=False refusal and guard leftovers; every check passed. The same driver fails with InternalError on the pre-fix SDK, so the regression is reproduced and shown fixed. A first attempt on port 8197 hit a port collision, so I discarded it and re-ran on a port I had confirmed was free. Afterwards I removed all transient state and ran cargo clean.

  • Live validation: ✅ go - 11 of 11 scenarios driven live against the product
Scenario Result Live Evidence
Program with define_rules (negation bound via +il_const_s row) plus insert/retract commits with correct inserted/deleted counts and no InternalError ✅ pass live extra_live_scenarios.txt S1/S1b: (applied, inserted, deleted) = (True, 1, 1); allowed rows were [a, b], then [] after kill_switch('t'). The pre-fix SDK raised InternalError against the same server (ex…
Guarded strict deploy with a constant-bound rule plus writes applies whole with correct counts ✅ pass live extra_live_scenarios.txt S2: (True, 1, 1); kill_switch rows ['y']
guard_cursor_advance_applies_whole: a cursor-advancing guarded program applies whole once, a replay applies nothing, and a 2-way race has one winner ✅ pass live python_sdk_live.txt tests/guards_live.py::test_guard_cursor_advance_applies_whole PASSED
guard_first_terminal_outcome: a negation-only guard through a staged il_txn_const row lets only the first outcome land ✅ pass live python_sdk_live.txt test_guard_first_terminal_outcome PASSED
guard_note_replacement: a program deleting the note its guard reads applies whole ✅ pass live python_sdk_live.txt test_guard_note_replacement PASSED
A strict guard raises PreconditionFailed from the error index and nothing is applied ✅ pass live python_sdk_live.txt test_strict_guard_raises_precondition_failed_from_error_index PASSED; extra_live_scenarios.txt S3 PreconditionFailed raised with kill_switch rows unchanged
A stale deploy is refused, including rule and schema changes; a deploy applies whole when its guard holds ✅ pass live python_sdk_live.txt test_stale_deploy_is_refused_including_rules PASSED, test_deploy_applies_whole_when_guard_holds PASSED
claim_twenty_way_race: exactly one of 20 concurrent claims wins and all see the same holder ✅ pass live python_sdk_live.txt test_claim_twenty_way_race PASSED (also test_hero_claim_once_per_need... PASSED)
Property: random guarded programs are all-or-nothing in both strict modes ✅ pass live python_sdk_live.txt test_guarded_program_is_all_or_nothing PASSED
Count-reply grammar conformance, and no guard leftovers (il_txn, il_txn_pending, il_assert, il_txn_const_s are empty) ✅ pass live python_sdk_live.txt test_count_reply_grammar PASSED; extra_live_scenarios.txt S5 shows all four relations empty
Adversarial: strict=False on a program holding rules is refused with CompileError ✅ pass live extra_live_scenarios.txt S4: CompileError 'strict=False cannot guard a program holding rules or schema...'
Evidence: Live SDK suites (make python-sdk-live equivalent) transcript
============================= test session starts ==============================
platform linux -- Python 3.10.22, pytest-9.1.1, pluggy-1.6.0 -- ~/.no-mistakes/worktrees/9eea230c3aae/01M43JW4WPWEQA59H7XZKEQ0QK/packages/inputlayer-py/.venv/bin/python
cachedir: .pytest_cache
rootdir: ~/.no-mistakes/worktrees/9eea230c3aae/01M43JW4WPWEQA59H7XZKEQ0QK/packages/inputlayer-py
configfile: pyproject.toml
plugins: langsmith-0.12.1, asyncio-1.3.0, anyio-4.13.0
asyncio: mode=auto, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collecting ... collected 70 items

tests/docs_queries_live.py::test_basic_queries_all_rows PASSED           [  1%]
tests/docs_queries_live.py::test_basic_queries_with_a_filter PASSED      [  2%]
tests/docs_queries_live.py::test_selecting_specific_columns PASSED       [  4%]
tests/docs_queries_live.py::test_a_projection_is_a_set PASSED            [  5%]
tests/docs_queries_live.py::test_joins PASSED                            [  7%]
tests/docs_queries_live.py::test_self_joins PASSED                       [  8%]
tests/docs_queries_live.py::test_computed_columns PASSED                 [ 10%]
tests/docs_queries_live.py::test_ordering_and_pagination PASSED          [ 11%]
tests/docs_queries_live.py::test_sort_and_limit_run_in_the_engine PASSED [ 12%]
tests/docs_queries_live.py::test_aggregations_group_by_department_with_stats PASSED [ 14%]
tests/docs_queries_live.py::test_agg_two_measures_same_column PASSED     [ 15%]
tests/docs_queries_live.py::test_aggregates_leave_no_session_rule PASSED [ 17%]
tests/docs_queries_live.py::test_count_without_a_column PASSED           [ 18%]
tests/docs_queries_live.py::test_aggregations_top_k_per_group PASSED     [ 20%]
tests/docs_queries_live.py::test_aggregate_ordered_and_limited PASSED    [ 21%]
tests/docs_queries_live.py::test_working_with_results PASSED             [ 22%]
tests/docs_queries_live.py::test_query_plans PASSED                      [ 24%]
tests/docs_queries_live.py::test_debug_takes_the_arguments_of_query PASSED [ 25%]
tests/docs_queries_live.py::test_debug_shows_the_plan_of_an_aggregate_query PASSED [ 27%]
tests/docs_queries_live.py::test_why_returns_the_selected_columns_with_a_proof_per_row PASSED [ 28%]
tests/docs_queries_live.py::test_why_explains_an_aggregate_query PASSED  [ 30%]
tests/docs_queries_live.py::test_why_returns_a_column_of_the_second_joined_relation PASSED [ 31%]
tests/docs_queries_live.py::test_why_returns_a_computed_column PASSED    [ 32%]
tests/docs_queries_live.py::test_an_offset_without_a_limit_pages_in_the_engine PASSED [ 34%]
tests/docs_queries_live.py::test_why_orders_and_paginates_like_query PASSED [ 35%]
tests/docs_queries_live.py::test_why_returns_the_distinct_rows_of_a_projection_like_query PASSED [ 37%]
tests/docs_queries_live.py::test_paging_a_projection_ordered_by_an_unselected_column_is_refused PASSED [ 38%]
tests/docs_queries_live.py::test_why_orders_and_limits_an_aggregate_query_like_query PASSED [ 40%]
tests/docs_queries_live.py::test_raw_iql PASSED                          [ 41%]
tests/docs_queries_live.py::test_derived_relations_querying_a_recursive_rule PASSED [ 42%]
tests/docs_queries_live.py::test_sessions_session_facts_mix_with_persistent_data PASSED [ 44%]
tests/docs_queries_live.py::test_session_insert_keeps_every_fact PASSED  [ 45%]
tests/docs_queries_live.py::test_distance_functions PASSED               [ 47%]
tests/docs_queries_live.py::test_temporal_functions PASSED               [ 48%]
tests/docs_queries_live.py::test_or_conditions_merge_their_branches PASSED [ 50%]
tests/compile_rules_live.py::test_every_fixture_command_runs_and_rejected_spellings_fail PASSED [ 51%]
tests/compile_rules_live.py::test_rule_definition_reads_the_clauses PASSED [ 52%]
tests/compile_rules_live.py::test_session_rules_list_and_drop_by_clause PASSED [ 54%]
tests/compile_rules_live.py::test_datetime_columns_define_insert_and_read_back PASSED [ 55%]
tests/compile_rules_live.py::test_in_and_not_in_queries PASSED           [ 57%]
tests/compile_rules_live.py::test_in_in_a_rule_and_a_conditional_delete PASSED [ 58%]
tests/compile_rules_live.py::test_ground_negation_binds_a_constant_for_the_request_only PASSED [ 60%]
tests/compile_rules_live.py::test_negation_sharing_no_variable_is_refused_before_sending PASSED [ 61%]
tests/compile_rules_live.py::test_view_with_ground_negation PASSED       [ 62%]
tests/compile_rules_live.py::test_session_view_with_ground_negation PASSED [ 64%]
tests/compile_rules_live.py::test_negated_comparison PASSED              [ 65%]
tests/compile_rules_live.py::test_literals_round_trip_through_the_engine PASSED [ 67%]
tests/compile_rules_live.py::test_unrepresentable_values_are_refused_before_sending PASSED [ 68%]
tests/subscriptions_live.py::test_history_matches_the_oracle_generator PASSED [ 70%]
tests/subscriptions_live.py::test_snapshot_plus_deltas_equals_a_fresh_query_over_random_histories PASSED [ 71%]
tests/subscriptions_live.py::test_projection_two_supporting_tuples PASSED [ 72%]
tests/subscriptions_live.py::test_streamed_snapshot_and_streamed_deltas_are_assembled PASSED [ 74%]
tests/subscriptions_live.py::test_a_seq_gap_and_a_reset_end_in_unverified_then_an_exact_resync PASSED [ 75%]
tests/subscriptions_live.py::test_a_slow_consumer_gets_unverified_then_a_resync_once_it_has_read PASSED [ 77%]
tests/subscriptions_live.py::test_reconnect_gives_unverified_at_once_then_only_what_changed PASSED [ 78%]
tests/subscriptions_live.py::test_an_acl_revoke_ends_the_subscription_with_access_denied PASSED [ 80%]
tests/subscriptions_live.py::test_refused_before_or_at_the_engine PASSED [ 81%]
tests/subscriptions_live.py::test_watch_yields_the_whole_result_and_on_calls_back PASSED [ 82%]
tests/guards_live.py::test_count_reply_grammar PASSED                    [ 84%]
tests/guards_live.py::test_hero_claim_once_per_need_cancel_on_kill_switch_reclaim_when_lifted PASSED [ 85%]
tests/guards_live.py::test_read_path_constant_only_negation_does_not_outlive_the_query PASSED [ 87%]
tests/guards_live.py::test_guard_cursor_advance_applies_whole PASSED     [ 88%]
tests/guards_live.py::test_guard_first_terminal_outcome PASSED           [ 90%]
tests/guards_live.py::test_guard_note_replacement PASSED                 [ 91%]
tests/guards_live.py::test_guarded_writes_keep_a_stored_row_equal_to_a_typed_placeholder PASSED [ 92%]
tests/guards_live.py::test_strict_guard_raises_precondition_failed_from_error_index PASSED [ 94%]
tests/guards_live.py::test_stale_deploy_is_refused_including_rules PASSED [ 95%]
tests/guards_live.py::test_deploy_applies_whole_when_guard_holds PASSED  [ 97%]
tests/guards_live.py::test_claim_twenty_way_race PASSED                  [ 98%]
tests/guards_live.py::test_guarded_program_is_all_or_nothing PASSED      [100%]

============================= 70 passed in 29.97s ==============================
Evidence: Extra live scenarios driver
"""Extra live scenarios driven against a real inputlayer-server (not part of the repo)."""
from __future__ import annotations
import asyncio, contextlib, os, sys
from typing import ClassVar
from inputlayer import Derived, From, InputLayer, Relation, PreconditionFailed, CompileError

URL = os.environ["INPUTLAYER_TEST_SERVER"]
KG = "nm_extra_live"

class ToolPolicy(Relation):
    tool: str
    mode: str
class KillSwitch(Relation):
    tool: str
class PackVersion(Relation):
    name: str
    version: str
class Allowed(Derived):
    tool: str
    rules: ClassVar[list] = []
Allowed.rules = [From(ToolPolicy).where(~KillSwitch.any(tool="t")).select(tool=ToolPolicy.tool)]

def show(label, v):
    print(f"[{label}] {v}")

async def main() -> int:
    il = InputLayer(URL, username="admin", password="admin")
    await il.connect()
    with contextlib.suppress(Exception):
        await il.drop_knowledge_graph(KG)
    kg = il.knowledge_graph(KG)
    await kg.define(ToolPolicy, KillSwitch, PackVersion)
    await kg.insert([ToolPolicy(tool="a", mode="auto"), ToolPolicy(tool="b", mode="auto")])
    await kg.insert(KillSwitch(tool="old"))
    ok = True

    # S1: regression - unguarded program: rule whose negation binds via +il_const_s row, then writes
    p = kg.program().define_rules(Allowed).insert(KillSwitch(tool="x")).retract(KillSwitch, tool="old")
    show("S1 iql", "\n" + p.iql())
    r = await p.commit()
    show("S1 result", (r.applied, r.inserted, r.deleted))
    ok &= (r.applied, r.inserted, r.deleted) == (True, 1, 1)
    q = sorted(x[0] for x in (await kg.query(Allowed)).rows)
    show("S1 allowed rows (kill_switch('t') absent)", q)
    ok &= q == ["a", "b"]

    # S1b: the rule reacts to its constant
    await kg.insert(KillSwitch(tool="t"))
    q2 = (await kg.query(Allowed)).rows
    show("S1b allowed rows after kill_switch('t')", q2)
    ok &= q2 == []
    await kg.retract(KillSwitch, tool="t")

    # S2: regression - guarded strict program with the same rule plus writes
    await kg.insert(PackVersion(name="delivery", version="v2"))
    g = (kg.program().clear_rule("allowed").define_rules(Allowed)
         .insert(KillSwitch(tool="y")).retract(KillSwitch, tool="x")
         .when(PackVersion.any(name="delivery", version="v2")))
    r2 = await g.commit()
    show("S2 guarded result", (r2.applied, r2.inserted, r2.deleted))
    ok &= (r2.applied, r2.inserted, r2.deleted) == (True, 1, 1)
    ks = sorted(x[0] for x in await_rows(await kg.execute("?kill_switch(T)")))
    show("S2 kill_switch rows", ks)
    ok &= ks == ["y"]

    # S3: guarded strict program with false guard -> PreconditionFailed, nothing applied
    g3 = (kg.program().define_rules(Allowed).insert(KillSwitch(tool="z"))
          .when(PackVersion.any(name="delivery", version="v9")))
    try:
        await g3.commit()
        show("S3", "NO EXCEPTION (fail)"); ok = False
    except PreconditionFailed as e:
        show("S3 raised", f"PreconditionFailed: {e}")
    ks = sorted(x[0] for x in await_rows(await kg.execute("?kill_switch(T)")))
    show("S3 kill_switch rows (unchanged)", ks)
    ok &= ks == ["y"]

    # S4: strict=False with rules is refused with CompileError
    try:
        await kg.program().define_rules(Allowed).insert(KillSwitch(tool="w")).when(
            PackVersion.any(name="delivery")).commit(strict=False)
        show("S4", "NO EXCEPTION (fail)"); ok = False
    except CompileError as e:
        show("S4 raised", f"CompileError: {e}")

    # S5: leftovers
    for qq in ("?il_txn(T)", "?il_txn_pending(T)", "?il_assert(V)", "?il_txn_const_s(K)"):
        rr = (await kg.execute(qq)).rows
        show(f"S5 {qq}", rr)
        ok &= rr == []

    await il.drop_knowledge_graph(KG)
    await il.close()
    print("OVERALL:", "PASS" if ok else "FAIL")
    return 0 if ok else 1

def await_rows(res):
    return res.rows

sys.exit(asyncio.run(main()))
Evidence: Extra live scenarios transcript (branch SDK, PASS)
[S1 iql] 
+il_const_s("t")
+allowed(Tool) <- tool_policy(Tool, _), il_const_s(K), !kill_switch(K), K = "t"
+kill_switch("x")
-kill_switch("old")
[S1 result] (True, 1, 1)
[S1 allowed rows (kill_switch('t') absent)] ['a', 'b']
[S1b allowed rows after kill_switch('t')] []
[S2 guarded result] (True, 1, 1)
[S2 kill_switch rows] ['y']
[S3 raised] PreconditionFailed: Precondition failed: the program guard did not hold, nothing was applied
  query: +il_txn_pending("t-b11a794e-763c-4c85-9fd4-acebc0d67cec")
-il_txn(""), +il_txn("t-b11a794e-763c-4c85-9fd4-acebc0d67cec") <- pack_version("delivery", "v9")
-il_assert(0), +il_assert("precondition_faile...
[S3 kill_switch rows (unchanged)] ['y']
[S4 raised] CompileError: strict=False cannot guard a program holding rules or schema: a guard can only stop them by aborting the program (commit it with strict=True (the default))
[S5 ?il_txn(T)] []
[S5 ?il_txn_pending(T)] []
[S5 ?il_assert(V)] []
[S5 ?il_txn_const_s(K)] []
OVERALL: PASS
Evidence: Same driver on pre-fix SDK e3c62df (reproduces InternalError)
[S1 iql] 
+il_const_s("t")
+allowed(Tool) <- tool_policy(Tool, _), il_const_s(K), !kill_switch(K), K = "t"
+kill_switch("x")
-kill_switch("old")
Traceback (most recent call last):
  File "~/.no-mistakes/evidence/01M43JW4WPWEQA59H7XZKEQ0QK/extra_live_scenarios.py", line 100, in <module>
    sys.exit(asyncio.run(main()))
  File "~/.local/share/uv/python/cpython-3.10-linux-x86_64-gnu/lib/python3.10/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "~/.local/share/uv/python/cpython-3.10-linux-x86_64-gnu/lib/python3.10/asyncio/base_events.py", line 649, in run_until_complete
    return future.result()
  File "~/.no-mistakes/evidence/01M43JW4WPWEQA59H7XZKEQ0QK/extra_live_scenarios.py", line 40, in main
    r = await p.commit()
  File "/tmp/nm_prefix/packages/inputlayer-py/src/inputlayer/program.py", line 223, in commit
    return await self._runner(self, strict)
  File "/tmp/nm_prefix/packages/inputlayer-py/src/inputlayer/knowledge_graph.py", line 461, in _commit_program
    counts = parse_write_message(message(i))
  File "/tmp/nm_prefix/packages/inputlayer-py/src/inputlayer/program.py", line 485, in parse_write_message
    raise InternalError(f"Unexpected write reply from the engine: {message!r}")
inputlayer.exceptions.InternalError: Unexpected write reply from the engine: "Rule 'allowed' registered."
Evidence: Release server build log
    Blocking waiting for file lock on package cache
    Blocking waiting for file lock on package cache
    Blocking waiting for file lock on package cache
    Blocking waiting for file lock on package cache
   Compiling proc-macro2 v1.0.107
   Compiling quote v1.0.47
   Compiling unicode-ident v1.0.26
   Compiling cfg-if v1.0.5
   Compiling libc v0.2.190
   Compiling serde_core v1.0.229
   Compiling serde v1.0.229
   Compiling version_check v0.9.5
   Compiling bytes v1.12.1
   Compiling once_cell v1.21.4
   Compiling autocfg v1.5.1
   Compiling libm v0.2.16
   Compiling zerocopy v0.8.59
   Compiling itoa v1.0.18
   Compiling equivalent v1.0.2
   Compiling memchr v2.8.3
   Compiling log v0.4.34
   Compiling pin-project-lite v0.2.17
   Compiling parking_lot_core v0.9.12
   Compiling find-msvc-tools v0.1.14
   Compiling bitflags v2.13.2
   Compiling getrandom v0.3.4
   Compiling scopeguard v1.2.0
   Compiling shlex v2.0.1
   Compiling pkg-config v0.3.34
   Compiling futures-core v0.3.34
   Compiling stable_deref_trait v1.2.1
   Compiling allocator-api2 v0.2.21
   Compiling hashbrown v0.17.1
   Compiling foldhash v0.1.5
   Compiling futures-sink v0.3.34
   Compiling arrow-schema v53.4.1
   Compiling slab v0.4.12
   Compiling iana-time-zone v0.1.65
   Compiling lock_api v0.4.14
   Compiling tracing-core v0.1.36
   Compiling regex-syntax v0.8.11
   Compiling typenum v1.20.1
   Compiling byteorder v1.5.0
   Compiling ryu v1.0.23
   Compiling futures-task v0.3.34
   Compiling ahash v0.8.12
   Compiling generic-array v0.14.7
   Compiling lexical-util v1.0.7
   Compiling zmij v1.0.23
   Compiling vcpkg v0.2.15
   Compiling rustversion v1.0.23
   Compiling serde_json v1.0.151
   Compiling writeable v0.6.4
   Compiling crossbeam-utils v0.8.23
   Compiling num-traits v0.2.19
   Compiling percent-encoding v2.3.2
   Compiling httparse v1.10.1
   Compiling subtle v2.6.1
   Compiling litemap v0.8.3
   Compiling utf8_iter v1.0.4
   Compiling http v1.5.0
   Compiling aho-corasick v1.1.5
   Compiling tower-service v0.3.3
   Compiling thiserror v1.0.69
   Compiling base64 v0.22.1
   Compiling icu_properties_data v2.3.0
   Compiling icu_normalizer_data v2.3.0
   Compiling fnv v1.0.7
   Compiling hashbrown v0.15.5
   Compiling indexmap v2.14.2
   Compiling lexical-parse-integer v1.0.6
   Compiling lexical-write-integer v1.0.6
   Compiling rustix v1.1.5
   Compiling utf8parse v0.2.2
   Compiling lazy_static v1.5.1
   Compiling try-lock v0.2.5
   Compiling semver v1.0.28
   Compiling foreign-types-shared v0.1.1
   Compiling crossbeam-epoch v0.9.21
   Compiling cpufeatures v0.2.17
   Compiling lexical-parse-float v1.0.6
   Compiling lexical-write-float v1.0.6
   Compiling openssl v0.10.81
   Compiling httpdate v1.0.3
   Compiling tower-layer v0.3.3
   Compiling atomic-waker v1.1.2
   Compiling linux-raw-sys v0.12.1
   Compiling syn v3.0.6
   Compiling syn v2.0.119
   Compiling want v0.3.1
   Compiling foreign-types v0.3.2
   Compiling rustc_version v0.4.1
   Compiling anstyle-parse v1.0.0
   Compiling form_urlencoded v1.2.2
   Compiling futures-channel v0.3.34
   Compiling sync_wrapper v1.0.2
   Compiling anstyle-query v1.1.5
   Compiling either v1.18.0
   Compiling crossbeam-deque v0.8.8
   Compiling paste v1.0.15
   Compiling mime v0.3.17
   Compiling is_terminal_polyfill v1.70.2
   Compiling colorchoice v1.0.5
   Compiling native-tls v0.2.18
   Compiling heck v0.5.0
   Compiling anstyle v1.0.14
   Compiling flatbuffers v24.12.23
   Compiling proc-macro2-diagnostics v0.10.1
   Compiling thiserror v2.0.21
   Compiling rayon-core v1.13.0
   Compiling base64 v0.23.1
   Compiling cfg_aliases v0.2.2
   Compiling crc32fast v1.5.2
   Compiling anyhow v1.0.104
   Compiling ipnet v2.12.2
   Compiling openssl-probe v0.2.1
   Compiling anstream v1.0.0
   Compiling http-body v1.1.0
   Compiling nix v0.30.1
   Compiling block-buffer v0.10.4
   Compiling crypto-common v0.1.7
   Compiling http-body-util v0.1.5
   Compiling encoding_rs v0.8.42
   Compiling cfg_aliases v0.1.1
   Compiling ucd-trie v0.1.7
   Compiling digest v0.10.7
   Compiling same-file v1.0.6
   Compiling lexical-core v1.0.6
   Compiling utf-8 v0.7.6
   Compiling timely_container v0.27.0
   Compiling unicode-width v0.2.2
   Compiling adler2 v2.0.1
   Compiling alloc-no-stdlib v2.0.4
   Compiling bytemuck v1.25.2
   Compiling yansi v1.0.1
   Compiling winnow v0.7.15
   Compiling zstd-safe v7.2.1
   Compiling unicase v2.9.0
   Compiling sha1 v0.10.7
   Compiling jobserver v0.1.35
   Compiling toml_write v0.1.2
   Compiling simd-adler32 v0.3.10
   Compiling data-encoding v2.11.1
   Compiling bitflags v1.3.2
   Compiling jiff-core v0.1.1
   Compiling getopts v0.2.24
   Compiling alloc-stdlib v0.2.4
   Compiling mime_guess v2.0.5
   Compiling miniz_oxide v0.9.1
   Compiling timely_logging v0.27.0
   Compiling cc v1.6.0
   Compiling walkdir v2.5.0
   Compiling errno v0.3.14
   Compiling mio v1.2.4
   Compiling socket2 v0.6.5
   Compiling getrandom v0.2.17
   Compiling signal-hook-registry v1.4.8
   Compiling rand_core v0.9.5
   Compiling nix v0.28.0
   Compiling rand_core v0.6.4
   Compiling csv-core v0.1.13
   Compiling uncased v0.9.10
   Compiling simdutf8 v0.1.5
   Compiling timely_bytes v0.27.0
   Compiling zeroize v1.9.0
   Compiling getrandom v0.4.3
   Compiling regex-automata v0.4.18
   Compiling pest v2.9.2
   Compiling num-integer v0.1.47
   Compiling num-complex v0.4.6
   Compiling atoi v2.0.0
   Compiling multiversion_no_op v1.0.0
   Compiling snap v1.1.2
   Compiling core_detect v1.0.0
   Compiling rayon v1.12.0
   Compiling flate2 v1.1.10
   Compiling ordered-float v2.10.1
   Compiling rustls-pki-types v1.15.1
   Compiling num_cpus v1.17.0
   Compiling cpu-time v1.0.0
   Compiling jiff v0.2.37
   Compiling filetime v0.2.29
   Compiling brotli-decompressor v4.0.3
   Compiling itertools v0.14.0
   Compiling num-bigint v0.4.8
   Compiling num-iter v0.1.46
   Compiling sharded-slab v0.1.7
   Compiling combine v4.6.8
   Compiling tracing-log v0.2.0
   Compiling figment v0.10.19
   Compiling xattr v1.6.1
   Compiling thread_local v1.1.10
   Compiling csv v1.4.0
   Compiling endian-type v0.1.2
   Compiling deranged v0.5.8
   Compiling strsim v0.11.1
   Compiling inlinable_string v0.1.15
   Compiling twox-hash v2.1.4
   Compiling static_assertions v1.1.0
   Compiling powerfmt v0.2.0
   Compiling base64ct v1.8.3
   Compiling nu-ansi-term v0.50.3
   Compiling integer-encoding v3.0.4
   Compiling clap_lex v1.1.1
   Compiling pest_meta v2.9.2
   Compiling time-core v0.1.9
   Compiling num-conv v0.2.2
   Compiling thrift v0.17.0
   Compiling clap_builder v4.6.7
   Compiling twox-hash v1.6.3
   Compiling lz4_flex v0.11.6
   Compiling openssl-sys v0.9.117
   Compiling zstd-sys v2.0.13+zstd.1.5.6
   Compiling password-hash v0.5.0
   Compiling tar v0.4.46
   Compiling serde_path_to_error v0.1.20
   Compiling fd-lock v4.0.4
   Compiling blake2 v0.10.6
   Compiling sha2 v0.10.9
   Compiling crossbeam-channel v0.5.17
   Compiling matchit v0.7.3
   Compiling synstructure v0.14.0
   Compiling pest_generator v2.9.2
   Compiling home v0.5.12
   Compiling seq-macro v0.3.6
   Compiling time v0.3.55
   Compiling unicode-segmentation v1.13.3
   Compiling symlink v0.1.0
   Compiling num-rational v0.4.2
   Compiling brotli v7.0.0
   Compiling http-range-header v0.4.2
   Compiling hashbrown v0.14.5
   Compiling unicode-width v0.1.14
   Compiling argon2 v0.5.3
   Compiling uuid v1.26.1
   Compiling fs4 v1.1.0
   Compiling arc-swap v1.9.2
   Compiling num v0.4.3
   Compiling serde_derive v1.0.229
   Compiling zerofrom-derive v0.1.8
   Compiling zerocopy-derive v0.8.59
   Compiling yoke-derive v0.8.4
   Compiling tracing-attributes v0.1.31
   Compiling zerovec-derive v0.11.6
   Compiling tokio-macros v2.7.2
   Compiling displaydoc v0.2.7
   Compiling futures-macro v0.3.34
   Compiling thiserror-impl v1.0.69
   Compiling openssl-macros v0.1.1
   Compiling columnar_derive v0.11.1
   Compiling thiserror-impl v2.0.21
   Compiling enum-as-inner v0.6.1
   Compiling pear_codegen v0.2.9
   Compiling async-trait v0.1.92
   Compiling clap_derive v4.6.7
   Compiling pest_derive v2.9.2
   Compiling futures-util v0.3.34
   Compiling tracing v0.1.44
   Compiling sysctl v0.6.0
   Compiling zerofrom v0.1.8
   Compiling yoke v0.8.3
   Compiling pear v0.2.9
   Compiling zerovec v0.11.8
   Compiling zerotrie v0.2.5
   Compiling regex v1.13.1
   Compiling matchers v0.2.0
   Compiling mmap-rs v0.7.0
   Compiling tinystr v0.8.4
   Compiling potential_utf v0.1.6
   Compiling icu_collections v2.3.0
   Compiling icu_locale_core v2.3.0
   Compiling env_filter v2.0.0
   Compiling clap v4.6.7
   Compiling env_logger v0.11.11
   Compiling anndists v0.1.5
   Compiling smallvec v1.16.2
   Compiling chrono v0.4.39
   Compiling toml_datetime v0.6.11
   Compiling serde_spanned v0.6.9
   Compiling bincode v1.3.3
   Compiling serde_urlencoded v0.7.1
   Compiling tracing-serde v0.2.0
   Compiling inputlayer-ws-protocol v0.1.0 (~/.no-mistakes/worktrees/9eea230c3aae/01M43JW4WPWEQA59H7XZKEQ0QK/ws-protocol)
   Compiling axum-core v0.4.5
   Compiling columnar v0.11.1
   Compiling nibble_vec v0.1.0
   Compiling columnation v0.1.2
   Compiling tracing-subscriber v0.3.23
   Compiling toml_edit v0.22.27
   Compiling radix_trie v0.2.1
   Compiling parking_lot v0.12.5
   Compiling dashmap v5.5.3
   Compiling tokio v1.53.2
   Compiling icu_provider v2.3.1
   Compiling rustyline v14.0.0
   Compiling icu_normalizer v2.3.0
   Compiling icu_properties v2.3.0
   Compiling timely_communication v0.27.0
   Compiling tracing-appender v0.2.5
   Compiling idna_adapter v1.2.2
   Compiling idna v1.1.0
   Compiling toml v0.8.23
   Compiling url v2.5.8
   Compiling half v2.7.1
   Compiling ppv-lite86 v0.2.21
   Compiling arrow-buffer v53.4.1
   Compiling rand_chacha v0.3.1
   Compiling rand_chacha v0.9.0
   Compiling rand v0.9.5
   Compiling rand v0.8.8
   Compiling timely v0.27.0
   Compiling arrow-data v53.4.1
   Compiling tungstenite v0.24.0
   Compiling hnsw_rs v0.3.4
   Compiling arrow-array v53.4.1
   Compiling tokio-util v0.7.19
   Compiling tower v0.5.3
   Compiling tokio-native-tls v0.3.1
   Compiling tokio-tungstenite v0.24.0
   Compiling h2 v0.4.19
   Compiling tower-http v0.5.2
   Compiling tower-http v0.6.11
   Compiling arrow-select v53.4.1
   Compiling arrow-arith v53.4.1
   Compiling arrow-row v53.4.1
   Compiling differential-dataflow v0.20.0
   Compiling arrow-cast v53.4.1
   Compiling arrow-string v53.4.1
   Compiling arrow-ord v53.4.1
   Compiling zstd v0.13.3
   Compiling hyper v1.11.1
   Compiling arrow-ipc v53.4.1
   Compiling arrow-json v53.4.1
   Compiling arrow-csv v53.4.1
   Compiling hyper-util v0.1.21
   Compiling arrow v53.4.1
   Compiling parquet v53.4.1
   Compiling hyper-tls v0.6.0
   Compiling axum v0.7.9
   Compiling reqwest v0.12.28
   Compiling inputlayer-ontology-client v0.1.0 (~/.no-mistakes/worktrees/9eea230c3aae/01M43JW4WPWEQA59H7XZKEQ0QK/ontology-client)
   Compiling inputlayer v0.1.0 (~/.no-mistakes/worktrees/9eea230c3aae/01M43JW4WPWEQA59H7XZKEQ0QK)
    Finished `release` profile [optimized] target(s) in 2m 45s
EXIT=0
  • Evidence: Server log (local file: ~/.no-mistakes/evidence/01M43JW4WPWEQA59H7XZKEQ0QK/server.log)
  • Outcome: 🔧 2 issues found → no changes applied ✅ across 2 runs (10m15s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

🔧 **Rebase** - 5 issues found → auto-fixed ✅
  • ⚠️ Makefile - merge conflict rebasing onto origin/main
  • ⚠️ packages/inputlayer-py/src/inputlayer/__init__.py - merge conflict rebasing onto origin/main
  • ⚠️ packages/inputlayer-py/src/inputlayer/client_sync.py - merge conflict rebasing onto origin/main
  • ⚠️ packages/inputlayer-py/src/inputlayer/exceptions.py - merge conflict rebasing onto origin/main
  • ⚠️ packages/inputlayer-py/src/inputlayer/knowledge_graph.py - merge conflict rebasing onto origin/main

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

🔧 **Review** - 1 issue found → auto-fixed ✅
  • 🚨 packages/inputlayer-py/src/inputlayer/program.py:1279 - Program.compile counts one statement per _Plain entry, but compile_rule() (compiler.py:1542) returns &#34;\n&#34;.join([*compiled.constants, compiled.clause]). That is several statements whenever a persistent rule's negated ~R.any() binds a constant through +il_const_&lt;t&gt;(...) rows. Each _Plain text is appended as a single element of lines, so every write_indexes entry after it is too small by the number of constant rows. Example: kg.program().define_rules(V).insert(X(...)).commit(), where V's clause is ~KillSwitch.any(tool=&#34;t&#34;) and no positive atom carries "t". The engine applies the whole program, but _commit_program reads reply rows at the wrong indexes. parse_write_message then gets a rule-registration message, or "" past the end, and raises InternalError after the writes have already committed. Even when no error is raised, the inserted/deleted counts are wrong. The same off-by-N affects the guarded path at program.py:1317-1323 (strict deploys holding such rules plus fact writes), and define_rules (program.py:1218-1234) is the producer. Fix: in both the unguarded and guarded loops, add each _Plain as lines.extend(s.text.split(&#34;\n&#34;)), or store one _Plain per statement in define_rules, so line index equals statement index. Then add a unit test asserting write_indexes for a program that holds a rule with a constant-bound negation.

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

🔧 **Test** - 2 issues found → no changes applied ✅
  • ⚠️ No live evidence was gathered. The live guard and claim scenarios (make python-sdk-live / tests/guards_live.py) and the regression scenario for the constant-row index fix need an inputlayer-server release binary. Its build had not finished when this step had to return its result, so it was stopped and nothing was run against a real engine. Re-run this step with time for the cargo release build, or rely on CI's make python-sdk-live job.
  • ⚠️ live validation verdict: inconclusive (0 of 10 scenarios were driven live against the product); untested: Program with define_rules (negation bound via +il_const_s row) plus insert/retract commits with correct inserted/deleted counts and no InternalError, guard_cursor_advance_applies_whole: cursor-advancing guarded program applies whole once, replay applies nothing, 2-way race has one winner, guard_first_terminal_outcome: negation-only guard via staged il_txn_const row lets only the first outcome land, guard_note_replacement: program deleting the note its guard reads applies whole, strict guard raises PreconditionFailed from the error index and nothing is applied, stale deploy is refused, including rule and schema changes; deploy applies whole when guard holds, claim_twenty_way_race: exactly one of 20 concurrent claims wins and all see the same holder, Property: random guarded programs are all-or-nothing in both strict modes, Count-reply grammar conformance and no guard leftovers (il_txn, il_txn_pending, il_assert, il_txn_const_s are empty), Adversarial: strict=False on a program holding rules is refused with CompileError
  • Live validation: ⚠️ inconclusive - 0 of 10 scenarios driven live against the product
Scenario Result Live Evidence
Program with define_rules (negation bound via +il_const_s row) plus insert/retract commits with correct inserted/deleted counts and no InternalError ⏸️ untested no This needs a running inputlayer-server built from this worktree. The release build was started but had not finished when this step had to return its result, so it was stopped. To check it: run `cargo…
guard_cursor_advance_applies_whole: cursor-advancing guarded program applies whole once, replay applies nothing, 2-way race has one winner ⏸️ untested no No server binary: the release build had not finished when this step had to return. Run make python-sdk-live.
guard_first_terminal_outcome: negation-only guard via staged il_txn_const row lets only the first outcome land ⏸️ untested no No server binary: the release build had not finished when this step had to return. Run make python-sdk-live.
guard_note_replacement: program deleting the note its guard reads applies whole ⏸️ untested no No server binary: the release build had not finished when this step had to return. Run make python-sdk-live.
strict guard raises PreconditionFailed from the error index and nothing is applied ⏸️ untested no No server binary: the release build had not finished when this step had to return. Run make python-sdk-live.
stale deploy is refused, including rule and schema changes; deploy applies whole when guard holds ⏸️ untested no No server binary: the release build had not finished when this step had to return. Run make python-sdk-live.
claim_twenty_way_race: exactly one of 20 concurrent claims wins and all see the same holder ⏸️ untested no No server binary: the release build had not finished when this step had to return. Run make python-sdk-live.
Property: random guarded programs are all-or-nothing in both strict modes ⏸️ untested no No server binary: the release build had not finished when this step had to return. Run make python-sdk-live.
Count-reply grammar conformance and no guard leftovers (il_txn, il_txn_pending, il_assert, il_txn_const_s are empty) ⏸️ untested no No server binary: the release build had not finished when this step had to return. Run make python-sdk-live.
Adversarial: strict=False on a program holding rules is refused with CompileError ⏸️ untested no No server binary: the release build had not finished when this step had to return.
  • uv run --extra dev pytest tests/test_guards.py -q (42 passed; offline unit tests, not live)
  • cargo build --release --bin inputlayer-server (started for make python-sdk-live; stopped before it finished)
  • Wrote the live driver ~/.no-mistakes/evidence/01M43JW4WPWEQA59H7XZKEQ0QK/extra_live_scenarios.py (not run: no server binary)

🔧 No changes applied.
✅ Re-checked - no issues remain.

  • Live validation: ✅ go - 11 of 11 scenarios driven live against the product
Scenario Result Live Evidence
Program with define_rules (negation bound via +il_const_s row) plus insert/retract commits with correct inserted/deleted counts and no InternalError ✅ pass live extra_live_scenarios.txt S1/S1b: (applied, inserted, deleted) = (True, 1, 1); allowed rows were [a, b], then [] after kill_switch('t'). The pre-fix SDK raised InternalError against the same server (ex…
Guarded strict deploy with a constant-bound rule plus writes applies whole with correct counts ✅ pass live extra_live_scenarios.txt S2: (True, 1, 1); kill_switch rows ['y']
guard_cursor_advance_applies_whole: a cursor-advancing guarded program applies whole once, a replay applies nothing, and a 2-way race has one winner ✅ pass live python_sdk_live.txt tests/guards_live.py::test_guard_cursor_advance_applies_whole PASSED
guard_first_terminal_outcome: a negation-only guard through a staged il_txn_const row lets only the first outcome land ✅ pass live python_sdk_live.txt test_guard_first_terminal_outcome PASSED
guard_note_replacement: a program deleting the note its guard reads applies whole ✅ pass live python_sdk_live.txt test_guard_note_replacement PASSED
A strict guard raises PreconditionFailed from the error index and nothing is applied ✅ pass live python_sdk_live.txt test_strict_guard_raises_precondition_failed_from_error_index PASSED; extra_live_scenarios.txt S3 PreconditionFailed raised with kill_switch rows unchanged
A stale deploy is refused, including rule and schema changes; a deploy applies whole when its guard holds ✅ pass live python_sdk_live.txt test_stale_deploy_is_refused_including_rules PASSED, test_deploy_applies_whole_when_guard_holds PASSED
claim_twenty_way_race: exactly one of 20 concurrent claims wins and all see the same holder ✅ pass live python_sdk_live.txt test_claim_twenty_way_race PASSED (also test_hero_claim_once_per_need... PASSED)
Property: random guarded programs are all-or-nothing in both strict modes ✅ pass live python_sdk_live.txt test_guarded_program_is_all_or_nothing PASSED
Count-reply grammar conformance, and no guard leftovers (il_txn, il_txn_pending, il_assert, il_txn_const_s are empty) ✅ pass live python_sdk_live.txt test_count_reply_grammar PASSED; extra_live_scenarios.txt S5 shows all four relations empty
Adversarial: strict=False on a program holding rules is refused with CompileError ✅ pass live extra_live_scenarios.txt S4: CompileError 'strict=False cannot guard a program holding rules or schema...'
  • CARGO_INCREMENTAL=0 CARGO_PROFILE_RELEASE_DEBUG=0 cargo build --release --bin inputlayer-server (ran to completion, 2m45s)
  • Started target/release/inputlayer-server with INPUTLAYER_ADMIN_PASSWORD=admin on free port 36703 and a throwaway --data-dir
  • uv run --extra dev pytest tests/docs_queries_live.py tests/compile_rules_live.py tests/subscriptions_live.py tests/guards_live.py -v against that server (make python-sdk-live equivalent): 70 passed
  • python extra_live_scenarios.py (S1-S5: define_rules with constant-bound negation plus writes, guarded strict deploy, false guard, strict=False refusal, leftovers) against the live server with the branch SDK: OVERALL PASS
  • Same driver with PYTHONPATH pointed at the pre-fix SDK source (e3c62df9): InternalError 'Unexpected write reply ... Rule allowed registered.', which reproduces the bug
  • uv run --extra dev pytest tests/test_guards.py -q (write_indexes unit regression): 42 passed
  • Teardown: killed server, removed data dir and pre-fix copy, cargo clean
🔧 **Document** - 2 issues found → auto-fixed ✅

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

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

jsam added 4 commits October 4, 2026 13:51
…() existence checks

Guarded programs compile to the transaction-token form (strict=False) or
the abort form (strict=True, 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.
R.any() and ~R.any() work in rules, queries, conditional deletes and
guards on Relation and Derived classes; a negated atom bound only to
constants shares a variable with an equal positive column, else binds
through il_const_<t> (queries, views) or a staged il_txn_const_<t> row
(writes). kg.define() declares il_txn, il_txn_pending and il_assert in
one program; kg.retract() and the sync mirror join. 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 python-sdk-live.
@jsam
jsam marked this pull request as ready for review October 4, 2026 14:21
@jsam
jsam merged commit b4c8b46 into main Oct 4, 2026
20 checks passed
@jsam
jsam deleted the fm/sdk-p1-i branch October 4, 2026 14:22
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