Skip to content

feat: P-022 first deliverable — CFG JSON seam, Rust workspace + own-ir, exact oracle#170

Merged
PhysShell merged 2 commits into
mainfrom
claude/ownnet-execution-surfaces-adr-8kyc0n
Jul 4, 2026
Merged

feat: P-022 first deliverable — CFG JSON seam, Rust workspace + own-ir, exact oracle#170
PhysShell merged 2 commits into
mainfrom
claude/ownnet-execution-surfaces-adr-8kyc0n

Conversation

@PhysShell

Copy link
Copy Markdown
Owner

Что и зачем

Первый код-деливерабл Rust-миграции — шаги 0–1 плана P-022, ровно как заморожено в доке («workspace skeleton + own-ir round-trip + oracle harness, gated by the differential ratchet from commit one»):

  • cfg --format json (ownlang/cfg_json.py) — канонический CFG-seam для оракула: versioned envelope, per-function таблица символов в порядке первого появления (сохраняет identity-структуру — то, что порт ломает первым), фиксированный op-словарь, детерминированный sorted-keys дамп. Human-дамп остаётся debug-видом по умолчанию. Пин: tests/test_cfg_json.py (19 проверок) в аггрегаторе.
  • rust/ workspace + crates/own-ir[workspace.lints] и [profile.release] в точности по P-022 (unsafe_code=forbid, restriction-denies; panic per-binary → не в профиле). own-ir ре-типизирует OwnIR-контракт под правилом «Rust принимает ровно то, что принимает Python load()»: типизированы только валидируемые поля, остальное — flattened extra (additive-tolerance + точный round-trip); version-gate первым; span/location leaf. tests/roundtrip.rs: все 19 Python-фикстур round-trip value-for-value + гейты (версия, словари lifetime/effect, bool≠int, пустые identity). cargo test 8/8, clippy 0 замечаний под полными строгими линтами, fmt clean.
  • scripts/oracle_exact.py — exact-parity harness (НЕ fuzzy oracle_compare.py, см. P-022): exit/crash-gate до любого диффа, канонизация JSON (sorted keys) + path-normalization, stderr сравнивается; режимы compare/snapshot/verify с манифестом по ключу (corpus hash, reference commit) и staleness-gate на смену любого ключа. --selftest покрывает self-parity, divergence-детект, crash-gate, канонизацию, snapshot round-trip; e2e: 20 examples × 2 поверхности = 40 записей parity.

Семантика Python-ядра не изменена (только новая поверхность cfg --format json); Python остаётся авторитетным.

Тип изменения

  • feat — новая возможность
  • fix / docs / refactor / chore / test / ci

Как проверено

  • python tests/run_tests.py — зелёный (добавлен test_cfg_json.py, 19/19)
  • ruff check . и mypy — чисто (19 файлов)
  • селфтесты затронутых скриптов — python scripts/oracle_exact.py --selftest PASS
  • Rust: cargo test 8/8 (round-trip всех OwnIR-фикстур), cargo clippy --all-targets 0 issues под строгими линтами, cargo fmt --check clean
  • e2e: oracle snapshot+verify по examples/ — 40/40 parity

Связанные issue

Нет issue. Реализует migration steps 0–1 из P-022 (#166, #168).

Чеклист

  • изменение покрыто тестом/селфтестом — test_cfg_json.py, roundtrip.rs, oracle_exact.py --selftest
  • README/docs обновлены — usage в __main__ docstring; архитектура уже описана в P-022
  • коммиты в conventional-commit стиле (feat:)

🤖 Generated with Claude Code


Generated by Claude Code

…r, exact oracle

Migration steps 0-1 of docs/proposals/P-022-rust-core-migration.md, gated
by the differential ratchet from commit one:

- ownlang/cfg_json.py + `cfg --format json`: the canonical, frozen
  CFG-layer oracle seam (versioned envelope, per-function symbol table in
  first-appearance order preserving identity structure, fixed instruction
  vocabulary, deterministic sorted-key dump). Human dump stays the debug
  default; --format gate extended (cfg: human|json; json rejected on
  ownir). Pinned by tests/test_cfg_json.py (19 checks) in run_tests.py.

- rust/ workspace: [workspace.lints] + [profile.release] exactly as
  specced (unsafe_code=forbid, pedantic/nursery warn, restriction denies;
  panic per-binary, so NOT in the profile). crates/own-ir re-types the
  OwnIR fact contract with serde under the acceptance rule 'Rust accepts
  exactly what Python load() accepts': only load()-validated fields are
  typed, everything else rides a flattened extra map (additive tolerance
  + exact round-trip); version gate first; span/location leaf module.
  tests/roundtrip.rs: all 19 Python OwnIR fixtures round-trip
  value-for-value, version/vocabulary/bool-int/empty-identity gates.
  cargo test 8/8, clippy 0 issues under the full strict lints, fmt clean.

- scripts/oracle_exact.py: the exact parity harness (NOT the cross-tool
  fuzzy oracle_compare.py — see P-022). Exit/crash gate before any
  output diff; canonicalized JSON (sorted keys) + path-normalized text;
  stderr compared; compare / snapshot / verify modes with the manifest
  keyed by (corpus hash, reference commit) and a staleness gate that
  fails verify when EITHER key changed. --selftest covers self-parity,
  divergence detection, the crash gate, canonicalization, and the
  snapshot round-trip; e2e: 20 examples x 2 surfaces = 40 records parity.

Gate green: run_tests, ruff, mypy ownlang, cargo test/clippy/fmt.
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@PhysShell, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 38caea70-5db9-47ad-8805-7cf92d003368

📥 Commits

Reviewing files that changed from the base of the PR and between 368c7ba and 43a45a6.

⛔ Files ignored due to path filters (1)
  • rust/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • ownlang/__main__.py
  • ownlang/cfg_json.py
  • rust/.gitignore
  • rust/Cargo.toml
  • rust/crates/own-ir/Cargo.toml
  • rust/crates/own-ir/src/lib.rs
  • rust/crates/own-ir/src/span.rs
  • rust/crates/own-ir/tests/roundtrip.rs
  • scripts/oracle_exact.py
  • tests/run_tests.py
  • tests/test_cfg_json.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/ownnet-execution-surfaces-adr-8kyc0n

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Owner Author

@coderabbitai review
@codex review

First code deliverable of the P-022 migration (steps 0–1). Focus areas:

  1. ownlang/cfg_json.py — is the canonical shape right as a frozen contract? Especially the symbol-table design (first-appearance indices standing in for Python's id(sym) identity) and the op vocabulary. Anything volatile or under-determined that would make the seam flaky for the Rust diff?
  2. rust/crates/own-ir — the acceptance rule is "Rust accepts exactly what Python load() accepts": typed fields are only the load()-validated ones, everything else in flattened extra. Any acceptance asymmetries vs ownlang/ownir.py::load we missed (fields validated in Python but untyped here, or vice versa)? Round-trip covers all 19 fixtures.
  3. scripts/oracle_exact.py — exit/crash gate, canonicalization (does it drop anything semantic?), the (corpus hash, reference commit) staleness rule, and whether the crash-signature list is sufficient.
  4. Workspace lints[workspace.lints] matches P-022; tests carry a justified file-scoped #[allow(clippy::panic, clippy::expect_used)]. Reasonable?

Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
gh pr view 170 --repo PhysShell/Own.NET --json title,body,files,additions,deletions | head -c 3000
echo "---FILES---"
gh pr diff 170 --repo PhysShell/Own.NET --name-only

Length of output: 2855


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== cfg_json.py ==="
cat ownlang/cfg_json.py
echo "=== ownir.py (load) ==="
fd -t f "ownir.py" ownlang | xargs cat

Length of output: 139327


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== rust/Cargo.toml ==="
cat rust/Cargo.toml
echo "=== own-ir Cargo.toml ==="
cat rust/crates/own-ir/Cargo.toml
echo "=== lib.rs ==="
cat rust/crates/own-ir/src/lib.rs
echo "=== span.rs ==="
cat rust/crates/own-ir/src/span.rs

Length of output: 14100


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== __main__.py diff-relevant (cfg command) ==="
rg -n "cfg" ownlang/__main__.py | head -50
echo "---"
rg -n -B3 -A40 "def cmd_cfg" ownlang/__main__.py
echo "=== oracle_exact.py ==="
cat scripts/oracle_exact.py

Length of output: 16587


🏁 Script executed:

#!/bin/bash
set -euo pipefail
wc -l rust/crates/own-ir/tests/roundtrip.rs
sed -n '1,40p' rust/crates/own-ir/tests/roundtrip.rs
echo "..."
rg -n "allow\(clippy" rust/crates/own-ir/tests/roundtrip.rs
echo "=== fixture files ==="
fd -e json . tests -x echo {} 2>/dev/null | rg -i "ownir|fixture" | head -30

Length of output: 3085


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '40,104p' rust/crates/own-ir/tests/roundtrip.rs

Length of output: 2677


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "succ" ownlang/cfg.py | head -30

Length of output: 800


Good — succ is already a list[int] built in append order, so list(b.succ) in cfg_json.py is fully deterministic (no set-ordering risk).

I now have enough evidence to answer all four points concretely.

@PhysShell, reviewed all the changed files (ownlang/cfg_json.py, rust/crates/own-ir/{src,tests}, rust/Cargo.toml, scripts/oracle_exact.py) plus the referenced ownlang/ownir.py::load and ownlang/cfg.py. Point-by-point:

1. cfg_json.py — shape as a frozen contract

  • The symbol-table design is sound: _SymTable keys on id(sym), assigns indices in first-appearance order, and every reference (params, then instruction operands) goes through syms.ref(...). That correctly encodes Python's identity structure (aliasing, distinct same-named symbols) portably, without leaking id() itself.
  • succ is already stored as list[int] built by direct assignment in cfg.py (Lines 564, 579, 581, 592, 595, 600, 657) — never a set — so list(b.succ) is fully deterministic. No hidden iteration-order volatility there.
  • Op vocabulary: the isinstance chain in _instr_json ends in raise AssertionError(...) for an unhandled instruction, which is exactly the right "fail loudly on vocabulary drift" behavior for a frozen contract.
  • Determinism is achieved correctly, but note it's split across two files: cfg_json.py builds plain dicts in fixed field order, and the actual sort_keys=True canonicalization happens at the call site (ownlang/__main__.py Line 139, json.dumps(module_cfg_json(cfgs), indent=2, sort_keys=True)). That's fine functionally, but worth a one-line comment in cfg_json.py itself noting the caller is responsible for sort_keys=True — otherwise a future direct caller of module_cfg_json() (e.g. from a test or another script) could emit non-canonical JSON without noticing.
  • Nothing else looks under-determined: blocks are explicitly sorted by id, symbols are append-only in first-appearance order, no dict/set is dumped without an explicit order guarantee.

2. own-ir acceptance parity vs ownlang/ownir.py::load
I diffed every validated field in load() against the typed fields in lib.rs, and they line up well:

  • Version gate first, same absent-means-v0 semantics, same "must be integer, not bool" rule (Rust gets this for free from the type system — Option<i64> rejects a JSON true).
  • services[].lifetime closed vocabulary (singleton/scoped/transient) ↔ Lifetime enum with rename_all = "lowercase" — matches, and is required in both.
  • services[].name non-empty string — matches (Rust: required String + explicit validate() non-empty check).
  • deps/weak_deps/root_resolves/scope_cached (arrays of strings), file/ctor_file/ctor_type (strings), line/ctor_line (int, not bool) — all match via Option<Vec<String>> / Option<String> / Option<i64> typing.
  • root_resolve_sites/scope_cache_sites {type, file, line} shape ↔ Site struct — matches.
  • functions[].params[].name (non-empty, required), .line (int not bool), .effect closed vocabulary including plainParam.name: String + validate() + ParamEffect enum — matches.
  • effects[] (deps, io: bool, line, bindings[].{name, init, refs, line}) ↔ Effect/Binding structs — matches.
  • Fields Python's load() does not validate (e.g. component name/file, subscription event/handler/released/line, function body/name) correctly fall into extra rather than being typed — consistent with the "only load()-validated fields are typed" rule.

I did not find an asymmetry in either direction. One thing to watch as the crate grows: load()'s error messages are richer (field-specific, e.g. "service 'lifetime' must be one of {...}") while OwnIrError messages currently only cover the version-gate and empty-identity cases explicitly — everything else surfaces as a raw serde_json deserialization error. That's already flagged honestly in the module doc ("Error message parity... not claimed yet"), so it's not a gap, just something the oracle harness's stderr comparison will eventually need to reconcile once oracle_exact.py is pointed at a real own-ir-backed binary instead of just Python-vs-Python.

3. scripts/oracle_exact.py

  • Exit/crash gate correctly runs before any output diff (_diff_records), and crash detection via _CRASH_MARKS ("Traceback (most recent call last)", "panicked at", "RUST_BACKTRACE") covers the two realistic failure modes (Python traceback, Rust panic with default hook).
  • Canonicalization (_canon_stream/_canon_text) parses JSON when the stripped stream starts with {/[ and re-dumps with sort_keys=True — this is safe: it only reorders keys, doesn't drop or coerce any values, so nothing semantic is lost. Path normalization (text.replace(input_path, "<input>")) is a plain string replace, so it's applied uniformly to both JSON and non-JSON streams via _canon_text at the end — good, no logic duplication drift.
  • The manifest's staleness gate correctly fails on either key changing — corpus_hash is checked explicitly in cmd_verify; reference_commit isn't independently re-checked at verify time (it's just carried for the printed summary), but since corpus_hash covers every corpus file's content+path, a regenerated-reference-with-changed-corpus case is caught; a regenerated reference commit with an unchanged corpus would not trip staleness. That's arguably intentional — you can't fully re-verify "did the reference's own behavior change" without re-running it — but if the intent per the PR description is "staleness-gate on any key change," this is a real gap: same-corpus + new reference commit currently passes verify silently against stale snapshots.

One minor edge case worth being aware of (not necessarily a blocker): a process killed by a signal (segfault, OOM-kill) produces no matching crash-mark text and a negative/non-standard returncode; it'll usually still be caught by the exit-status gate, but only if the reference doesn't fail identically — a symmetrical crash on both sides with the same exit code would slip through undetected as "parity." Low likelihood, but worth a --selftest case if you want full coverage of the crash-gate story.

4. Workspace lints + test-file allow
[workspace.lints] matches what's described (unsafe_code = "forbid", restriction lints denied surgically, pedantic/nursery at warn, panic/unwrap_used/expect_used/indexing_slicing/arithmetic_side_effects denied/warned appropriately), and [profile.release] correctly omits panic (per-binary choice, not workspace-wide) — consistent with the plan. The file-scoped #![allow(clippy::panic, clippy::expect_used)] in roundtrip.rs is reasonable and well-justified in its own comment: tests report failure by panicking, that's the mechanism, not a production-code violation of the "no panics" strictness doctrine. I'd endorse it as-is.

Overall this is a clean, well-scoped first deliverable — the acceptance-parity claim for own-ir holds up under direct comparison with load(), and the two harnesses (cfg_json.py's determinism, oracle_exact.py's exact-diff gates) are implemented consistently with their stated design. The one thing I'd actually act on is the reference_commit staleness gap in point 3 if "any key change" is meant literally.


✏️ Learnings added
Learnt from: PhysShell
URL: https://github.com/PhysShell/Own.NET/pull/170

Timestamp: 2026-07-03T21:02:42.979Z
Learning: In the PhysShell/Own.NET repository (P-022 Rust migration), `scripts/oracle_exact.py`'s `verify` mode only checks the `corpus_hash` manifest key for staleness at verify time (in `cmd_verify`); `reference_commit` is stored in the manifest and printed but not independently re-checked against the current reference binary/commit, so a reference-behavior change with an unchanged corpus would not trigger the staleness gate.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a73a1f75a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ownlang/__main__.py Outdated
pols = collect_policies(mod)
for fn in mod.functions:
cfg, _ = build_cfg(fn, rnames, sigs, pols)
cfgs = [build_cfg(fn, rnames, sigs, pols)[0] for fn in mod.functions]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass resource kinds into CFG JSON builds

When the new cfg --format json path builds CFGs, it omits collect_kinds(mod), so every exported symbol has resource_kind: null even when the resource declaration includes kind "...". The checker path does pass resource kinds, and cfg_json makes resource_kind part of the canonical symbol contract, so this drops semantic metadata from the frozen seam for kind-tagged resources.

Useful? React with 👍 / 👎.

#[serde(skip_serializing_if = "Option::is_none")]
pub ownir_version: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub components: Option<Vec<Component>>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject explicit nulls in OwnIR typed fields

Option<Vec<_>> collapses a present JSON null into the same state as an omitted field, so OwnIr::from_json(r#"{\"components\":null}"#) succeeds and to_value() then drops the field. Python load() treats only absence as the default and rejects a present non-list via the components shape check, so this violates the “Rust accepts exactly what Python load() accepts” rule; the same absent-vs-null distinction needs to be preserved for the other load-validated optional fields too.

Useful? React with 👍 / 👎.

Comment thread scripts/oracle_exact.py Outdated
Comment on lines +197 to +202
now_hash = _corpus_hash(files, root)
if now_hash != manifest["corpus_hash"]:
print("STALE SNAPSHOTS: the corpus changed since `snapshot` ran "
"(corpus_hash mismatch). Regenerate before verifying.",
file=sys.stderr)
return 2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate verification on the reference commit

verify only recomputes the corpus hash, but it never compares manifest["reference_commit"] with the current reference commit even though the manifest is documented as keyed by both. If the Python reference changes while the corpus is unchanged, this will compare candidates against stale goldens instead of returning the intended staleness error and forcing regeneration.

Useful? React with 👍 / 👎.

…ys, null parity

CodeRabbit + Codex round on the first deliverable; all four confirmed:

- cmd_cfg now passes collect_kinds(mod) into build_cfg like the checker
  path does (Codex P2): without it every kind-tagged resource exported
  resource_kind=null through the frozen seam. test_cfg_json pins a
  kind-tagged resource riding the projection (20/20).
- cfg_json owns its canonicalization (CodeRabbit): new canonical_json()
  is the contract's textual form; __main__ prints it instead of an
  ad-hoc json.dumps at the call site.
- oracle_exact verify now gates on BOTH manifest keys (Codex P2 +
  CodeRabbit): the reference key is a content hash of the reference
  implementation's source tree (--ref-src, default ownlang/) — not the
  git commit, which changes on every commit including ones that cannot
  affect reference behaviour. A signal death (negative returncode)
  counts as a crash even with no traceback text. Selftest adds the
  reference-edit staleness case (edit -> rc 2 -> restore -> parity).
- own-ir absent-vs-null parity (Codex P2): a present JSON null is now
  REJECTED exactly where Python load() rejects it (reject_null
  deserializer on every load()-validated optional), and ACCEPTED AND
  PRESERVED on round-trip for the three fields Python checks with
  'if x is not None' (subscription type/source_type, param effect) via a
  double-Option with a justified option_option allow. Two new tests pin
  both directions; 10/10.

Gate green: run_tests, ruff, mypy ownlang, cargo test/clippy/fmt,
oracle selftest.

Copy link
Copy Markdown
Owner Author

All four findings folded in 43a45a6 — thanks both, this was a high-signal round:

  • cfg kinds (Codex P2 @131) — confirmed real: cmd_cfg now passes collect_kinds(mod) like the checker path, so resource_kind rides the frozen seam instead of exporting null for every kind-tagged resource. test_cfg_json.py pins a kind-tagged fixture (20/20).
  • Canonical dump ownership (CodeRabbit) — cfg_json.canonical_json() is now the contract's own textual form; __main__ prints it instead of an ad-hoc json.dumps at the call site, so a future direct caller can't silently emit non-canonical text.
  • Reference staleness key (Codex P2 @202 + CodeRabbit) — verify now gates on both manifest keys. Design note: the reference key is a content hash of the reference source tree (--ref-src, default ownlang/), not the git commit — HEAD changes on every commit including Rust-only ones, which would force pointless regeneration and kill the "steady state runs zero Python" property. A signal death (negative returncode, no traceback text) now also counts as a crash (CodeRabbit's edge case). Selftest adds the reference-edit staleness round-trip.
  • Absent-vs-null parity (Codex P2 @190) — confirmed violation of the acceptance rule. A present JSON null is now rejected exactly where Python's load() rejects it (reject_null on every load()-validated optional) and accepted + preserved on round-trip for the three fields Python checks with if x is not None (subscription.type/source_type, param.effect) via a double-Option (justified option_option allow — the three states are the contract). Two new tests pin both directions (cargo 10/10).

Gate green across both stacks: run_tests / ruff / mypy, cargo test / clippy(0) / fmt, oracle selftest.


Generated by Claude Code

PhysShell pushed a commit that referenced this pull request Jul 3, 2026
… rationale

Track A (N4, no-dependency slice).

- test_ownir now asserts each frontend producer (Program.cs, ownts.py) stamps
  the same ownir_version as the core's OWNIR_VERSION — so 'bumped the core,
  forgot a frontend' (or the reverse) fails loudly instead of silently
  mis-reading facts. This is the no-dep half of version single-sourcing; the
  P-022 own-ir crate will be a fourth producer to fold in (or generate from the
  schema). (211/211 bridge checks.)

- Register: recover and record the strong pro-schema argument now that its
  trigger has fired. OwnIR is THE language every frontend + the core speak; a
  language without a formal grammar lies about itself. The schema looked
  redundant only while load() was the sole validator (zero-dep, no second type
  set). #170's crates/own-ir hand-writes Rust serde structs that must match
  load() — two hand-maintained validators in two languages, the exact drift
  formalization exists to kill. So the schema flips from a shadow of load() to
  the source both are generated from (serde via typify; C#/TS source-gen),
  collapsing the hand-synced shapes AND the four ownir_version literals into one
  authority. Recommendation: co-design spec/ownir.schema.json WITH the own-ir
  crate, not a jsonschema validator bolted onto the zero-dep core.

Refs docs/notes/tech-debt-register.md (N4), P-022 #170.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJSfUpRbRXegrwJNV1rx1X

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Final pass over the fold commit 43a45a6, please — it implements exactly the findings from your round + Codex's: cmd_cfg passes collect_kinds (resource_kind rides the seam), cfg_json.canonical_json() owns the canonical dump, oracle_exact gates verify on both manifest keys (reference key = content hash of the reference source tree via --ref-src, not the git commit) + negative-returncode counts as a crash, and own-ir now has absent-vs-null parity with load() (reject_null on validated optionals; double-Option preserving explicit null for subscription.type/source_type/param.effect). Gate green on both stacks (run_tests / ruff / mypy; cargo test 10/10 / clippy 0 / fmt).


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
gh pr view 170 --repo PhysShell/Own.NET --json commits --jq '.commits[-5:]'
echo "---"
git -C . log --oneline -5 2>/dev/null || true
echo "---diff---"
gh pr diff 170 --repo PhysShell/Own.NET | head -c 500

Length of output: 5696


🏁 Script executed:

#!/bin/bash
set -euo pipefail
git show a73a1f7..43a45a6 --stat 2>/dev/null || true
echo "---"
git diff a73a1f7 43a45a6 -- ownlang/cfg_json.py ownlang/__main__.py 2>/dev/null | head -300

Length of output: 4934


🏁 Script executed:

#!/bin/bash
set -euo pipefail
git diff a73a1f7 43a45a6 -- scripts/oracle_exact.py 2>/dev/null

Length of output: 7141


🏁 Script executed:

#!/bin/bash
set -euo pipefail
git diff a73a1f7 43a45a6 -- rust/crates/own-ir/src/lib.rs 2>/dev/null | head -250

Length of output: 8966


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "is not None" ownlang/ownir.py

Length of output: 1627


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "effect" rust/crates/own-ir/src/lib.rs | head -30

Length of output: 559


🏁 Script executed:

#!/bin/bash
set -euo pipefail
git diff a73a1f7 43a45a6 -- rust/crates/own-ir/tests/roundtrip.rs 2>/dev/null

Length of output: 2345


@PhysShell, verified the fold commit 43a45a6 line-by-line against the diff from a73a1f7, and it lands exactly as described:

  1. CFG kindscmd_cfg in ownlang/__main__.py now computes kinds = collect_kinds(mod) and threads it into build_cfg(fn, rnames, sigs, pols, kinds), matching the checker path. Confirmed this closes the resource_kind=null gap for kind-tagged resources.
  2. Canonical dump ownershipcfg_json.py gained canonical_json(cfgs) (owns sort_keys=True/indent=2), and __main__.py's cmd_cfg now calls it directly instead of the previous local json.dumps(module_cfg_json(cfgs), ...). module_cfg_json's docstring also now explicitly warns future callers to use canonical_json rather than dumping it themselves. Good defensive documentation.
  3. Staleness gateoracle_exact.py now derives reference_hash via a new _tree_hash() over --ref-src (default ownlang/), stores it in the manifest alongside corpus_hash, and cmd_verify checks both independently, failing rc 2 on either mismatch. reference_commit is kept but explicitly marked informational-only, exactly addressing the gap I flagged (a same-corpus reference-behavior change is now caught). The signal-death case (proc.returncode < 0) is folded into the crash flag, and --selftest exercises the edit → stale(rc 2) → restore → parity round-trip for the reference source.
  4. own-ir absent-vs-null parity — new reject_null/nullable deserializers are applied precisely where I'd expect: reject_null on every field Python validates via isinstance checks (rejecting a present null), and the double-Option nullable only on the three fields where ownlang/ownir.py::load uses if x is not None and not isinstance(...) (Subscription.type_name/source_type at lines 476/483, Param.effect at line 603) — so explicit null there is accepted and round-trips losslessly. The two new roundtrip.rs tests pin both directions correctly.

No discrepancies between the commit message and the actual diff. This closes out all four findings from the previous round. Nice, tight fold.

[resolve]

ᕦ(ò_óˇ)ᕤ

@PhysShell
PhysShell merged commit 2ecab43 into main Jul 4, 2026
29 checks passed
PhysShell pushed a commit that referenced this pull request Jul 4, 2026
… rationale

Track A (N4, no-dependency slice).

- test_ownir now asserts each frontend producer (Program.cs, ownts.py) stamps
  the same ownir_version as the core's OWNIR_VERSION — so 'bumped the core,
  forgot a frontend' (or the reverse) fails loudly instead of silently
  mis-reading facts. This is the no-dep half of version single-sourcing; the
  P-022 own-ir crate will be a fourth producer to fold in (or generate from the
  schema). (211/211 bridge checks.)

- Register: recover and record the strong pro-schema argument now that its
  trigger has fired. OwnIR is THE language every frontend + the core speak; a
  language without a formal grammar lies about itself. The schema looked
  redundant only while load() was the sole validator (zero-dep, no second type
  set). #170's crates/own-ir hand-writes Rust serde structs that must match
  load() — two hand-maintained validators in two languages, the exact drift
  formalization exists to kill. So the schema flips from a shadow of load() to
  the source both are generated from (serde via typify; C#/TS source-gen),
  collapsing the hand-synced shapes AND the four ownir_version literals into one
  authority. Recommendation: co-design spec/ownir.schema.json WITH the own-ir
  crate, not a jsonschema validator bolted onto the zero-dep core.

Refs docs/notes/tech-debt-register.md (N4), P-022 #170.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJSfUpRbRXegrwJNV1rx1X
PhysShell added a commit that referenced this pull request Jul 4, 2026
… new dep (#175)

* spec(ownir): add machine-readable JSON Schema, pinned to code with no new dep

OwnIR now has a machine grammar to sit alongside the prose spec
(spec/OwnIR.md): spec/ownir.schema.json (JSON Schema draft 2020-12,
ownir_version as const:0). It mirrors the contract the Python validator
(ownlang/ownir.py::load) enforces — envelope, owned-resource records,
flow bodies, DI graph, reactive effects — and is shaped for the P-022
`own-ir` Rust crate to generate serde types from: the flow-op vocabulary
is a `oneOf` discriminated on `op`, and the enums (resource kinds, DI
lifetimes, param effects) are closed.

The schema's trigger has fired: a second language-native type set (the
Rust crate in #170) is arriving, so a single source both are generated
from stops the two hand-maintained validators from drifting.

The core cannot import a jsonschema validator (zero-dependency
constraint), so instead of validating documents this binds the schema's
*vocabulary* to the code's authoritative sets in tests/test_ownir.py:

- resourceKind enum  == _KNOWN_RESOURCE_KINDS  (the load() routing set)
- diLifetime enum    == di.LIFETIMES
- ownir_version const == OWNIR_VERSION
- every flowOp const lowers through _lower_flow without the fail-loud
  "unknown op" raise; a bogus op the schema omits still raises

Schema and validator therefore cannot drift without a red build, no
dependency added. Registered in spec/README.md; tech-debt register item
3 updated to shipped.

228/228 bridge checks pass; full suite green.

* style(test): sort the new ownir imports (ruff I001)

The schema-consistency imports (_KNOWN_RESOURCE_KINDS, DI_LIFETIMES)
tripped ruff's import-order check on the PR; merge them into the sorted
block.

* fix(ownir): address Codex review — null parity + bidirectional enum binds

Three P2 findings from the Codex review on the schema PR:

1. Null resource types. The reference loaders (Python load(), the Rust
   own-ir parity tests) accept and preserve `"type": null` — load() only
   rejects a non-null non-string. The schema made `type`/`source_type`
   string-only, so it (and generated serde types) would reject documents
   the pipeline accepts. Widen them to ["string","null"] — maps to
   Option<String>. `source` widened too (load() never constrained it).

2. paramEffect never pinned. The schema advertised a closed paramEffect
   enum but the drift test jumped straight from diLifetime to flow ops,
   so it never bound. Extract the effect tuple in load() to a named
   authority `_PARAM_EFFECTS` and assert schema.paramEffect == it.

3. Flow-op bind was one-directional. The test only proved schema-ops
   lower; a handled op the schema forgot stayed green. Extract the
   lowerer's vocabulary to `_FLOW_OPS` (the _lower_flow `else` now
   distinguishes "declared-but-unhandled" from "unknown vocabulary") and
   assert schema.flowOp consts == _FLOW_OPS — both directions. The
   drive-test still lowers every declared op so a phantom set entry fails.

Both _FLOW_OPS and _PARAM_EFFECTS now mirror _KNOWN_RESOURCE_KINDS: a
named frozenset used by load()/_lower_flow AND bound to the schema by
equality, so schema and validator cannot drift either way.

230/230 bridge checks; ruff + mypy --strict + full suite green.

* fix(ownir): add event/handler/lambda to resourceRecord schema

CodeRabbit (Major): the resourceRecord schema omitted the event,
handler, and lambda fields that every subscription/timer/capture fact
carries — event/handler are core identifying fields (finding messages,
rendered output) read as strings via sub.get(), and lambda is the bool
flag (ownir.py:2171) marking an un-detachable inline-lambda handler.
Without them the schema-generated Rust own-ir serde types would drop the
fields and lose round-trip fidelity for real facts, undermining the
schema's single-source purpose. Added additive/optional, matching code
semantics (event/handler string, lambda bool).

230/230 bridge checks; full suite green.

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants