# node_core — the portable AI node engine (universal core only)

Vendor this ONE folder into any project. **No third-party dependencies** (Python stdlib only) and
**no domain logic** — every domain specific (operations, fields, formulas, the checker) lives in a
SKILL the consuming project writes. The complete engine = this core + the skills loaded into it.

## What's inside (nothing else)
- `engine.py` — the S036 compositional engine: `Skill`, `Registry`, `learn` (discover → verify →
  adopt-iff-uniquely-determined-else-refuse → compose), `save`/`load`. **Byte-identical to this
  workspace's sealed canonical core** (sha first-16 `7efaf4ac36129bbe`).
- `loop.py` — the S045 simple self-learning loop: `learn_unique(all_comps, metric_fn, examples,
  holdout)` + `consistent_set`. A vendored clean copy (no domain code).
- `selftest.py` — proves portability + faithfulness. `python3 -m node_core.selftest`.

## Use it
```python
import node_core                       # no sys.path hacks needed once node_core is on the path

# Option A — S036 engine: your skill supplies a primitive family
class MyDomain:
    def primitives(self):
        return [({"op": "double"}, lambda d: d * 2), ({"op": "inc"}, lambda d: d + 1)]
reg = node_core.Registry()
node_core.learn("f", MyDomain(), [(2, 4), (3, 6)], [5, 10], reg)   # adopts 'double' or refuses
reg.value("f", 7)                                                  # -> 14

# Option B — S045 loop: an explicit candidate list + a metric function
node_core.learn_unique(["double", "inc"], lambda d, c: d*2 if c == "double" else d+1,
                       [(2, 4)], [5, 10])                          # adopt-iff-unique-else-refuse
```

## Distribute without messing up the engine workspace
- **Vendor:** copy `node_core/` into the consuming project (or `git subtree` / a pinned package). The
  consumer writes its OWN skills against the interface above — domain code never enters the core.
- **Decoupled:** the consumer imports `node_core`, NOT files under `experiments/`. Edits to experiments
  here cannot break a downstream project, and downstream skills never clutter this workspace.
- **Faithful + verifiable:** `engine.py`'s sha is asserted equal to the sealed core by `selftest.py`,
  so a vendored copy is provably the same engine, and drift is caught immediately.
- **What is NOT here:** skills/domain packs (accounting, etc.), experiment harnesses, datasets. Those
  belong to the consuming project. This folder is the engine, period.

## Portability guarantees (proven by `selftest.py`)
stdlib-only · zero domain literals in the core · engine byte-identical to the sealed core ·
imports and runs from any directory · learns and refuses correctly via both entry points.

## Note (drift)
`engine.py` is sha-locked to the sealed core (self-test enforces it). `loop.py` is the S045 loop
**decision-equivalent** to the sealed `tabular_learn.py` (the adopt/refuse decision — `adopted`,
`comp`, `n_consistent`, control flow — is byte-identical; verified decision-identical on 2000
randomized cases, 2026-06-29). It is NOT character-identical: its human-readable `reason`/`provenance`
strings were generalized ("candidates"/"family" rather than "compositions"/"atoms") because this loop
is universal over any candidate list, not just program compositions. The decision is the contract; the
prose is documentation. If the canonical loop's *logic* changes, re-vendor and re-run the self-test.

`skill_abi.py` freezes the S041 skill contract (the single source of truth the Zig port mirrors):
`SkillManifest` (cheap-metadata routing), `harden` (the one place a tool's edge becomes right-or-refuse),
`check_conformance` (P1–P3), `Skill` (fails loudly if non-conformant, P4), `SkillRegistry` (routes on
manifest metadata only, P5). No domain logic; stdlib only.

The existing `experiments/` here still import the loop from their original locations (sealed history,
left untouched); `node_core` is the clean go-forward distributable.
