Skip to content

[codex] Advance reflected substrate and runtime emission - #466

Merged
briansrls merged 23 commits into
mainfrom
swift-ram-158
Apr 16, 2026
Merged

briansrls merged 23 commits into
mainfrom
swift-ram-158

Conversation

@briansrls

Copy link
Copy Markdown
Contributor

What changed

This PR advances the reflection stack from substrate/spec plumbing into executable runtime paths.

It lands:

  • src/v3/std/substrate.dag as the reflected structural surface for Dag, Behavior, BindNode, TransformNode, BranchNode, LoopNode, ids, declarations, and related payload types
  • src/v3/spec/rust.dag updates for reflected substrate realizations, FieldBinding, callable realizations, the Rust language syntax bundle, and positional variant-pattern support for externally realized tuple variants
  • compiler changes in parse.rs, lower.rs, infer.rs, dag.rs, and emit_rust.rs to support:
    • reflection surface loading and realization indexing
    • field-access dispatch from realization data
    • implicit generic-call instantiation for list combinators
    • record and list literals in expression position
    • spec-driven callable/type/pattern/value emission
    • module-style Rust emission for function-only outputs
    • crate-linked execution of emitted reflected functions against live Dag values
    • deferred branch/payload binding on retryable generic match scrutinees
  • test coverage for:
    • reflected substrate inhabitance parity with dag.rs
    • FieldBinding coverage for reflected record fields
    • generic list call inference without *_int wrappers
    • emitted-Rust roundtrips for reflected Dag reads and Behavior payload matches

Why it changed

The reflection work needed to move past static substrate declarations and into executable proof points.

Before this change, the branch had most of the reflected data model and list/generic-call progress, but the runtime question for Unit 4 was still open in practice: could compiled .dag code read live reflected substrate values, and could the emitter handle reflected enum payload matches end to end?

This PR answers that in the affirmative for the currently supported subset.

Root cause addressed

Two concrete blockers were closed here:

  1. Generic higher-order calls still needed implicit template binding synthesis from argument types, which prevented list combinators from being used compositionally without monomorphic wrappers.
  2. Emitted reflected code could not yet be exercised as linked Rust functions against live compiler substrate values, so the runtime half of the reflection claim remained unproven.

This PR fixes both by extending inference and by adding module-mode emission plus a crate-linked roundtrip test harness.

Impact

  • Reflection Units 1-3 are materially in place on this branch.
  • The runtime execution path for compiled reflected functions now exists and is tested.
  • Review can start on the reflected substrate/spec shape and the execution plumbing independently of the final unused_parameters.dag migration.

Validation

Ran:

cargo test -p v3-compiler -- --nocapture

The full v3-compiler suite passes, including:

  • reflected substrate tests
  • field-binding coverage tests
  • generic list roundtrip tests
  • emitted-module roundtrips over live Dag values

Remaining blocker after this PR

This does not fully complete the unused_parameters.dag migration yet.

The next remaining edge is emitted Rust type naming for instantiated/container result shapes needed by the final reflected lens output, especially things like List<UnusedParameter> and records carrying SourceSpan. The runtime path is now there; the remaining work is to close that type-realization/emission gap and then wire the full lens migration on top.

briansrls and others added 10 commits April 15, 2026 20:54
…tus + research framing

Three updates to v3's self-hosting design note:

- §2 L1 status: prereqs shipping (Prereqs 0-3 merged via #458/#460,
  Prereqs 4-5 in flight), reflection PR is next. Replaces stale
  "in design" label.
- §2.1 (new): the pipeline is a .dag composition, not a file
  convention. Stages are typed functions composed via let bindings
  with explicit dependencies. Stage contracts are first-class (not
  decorative), per-stage fixed-point is structural, independent
  stages parallelize, and the compiler is analyzable by its own
  lenses. Pipeline declaration ships FIRST (model before implement);
  each stage starts as ExternalRealization during migration.
- §5 (infer): explicitly framed as research, not engineering.
  I0 result cross-referenced (no decidability blocker). The open
  question narrows from "is this possible?" to "which write-surface
  option?"

Also updates src/v3/ROADMAP.md M3 section to cross-reference
SELF_HOSTING.md with key points.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New section in SELF_HOSTING.md capturing the design principle:
every pipeline stage models its inputs and outputs to spec BEFORE
the stage body is implemented. Stage implementation is filling in
function bodies over already-declared types — the same model-
before-implement discipline the thesis demands of user programs,
applied to the compiler itself.

Per-stage domain modeling covers: parse (files, encodings, tokens,
grammar as data), lower (surface AST as proper .dag Disj
declarations with structural decompression), infer (rules as data
operations, substitution stacks and scope as structural types),
emit (full target language spec including rendering conventions).

Between-stage contracts are the pipeline's function signatures —
first-class, not decorative. A lens verifies each contract holds.

The v2 lesson this prevents: types accreted reactively, downstream
stages reconstructed facts instead of receiving them as typed
edges. The annotate_descent_evidence pattern (33 heuristics in
v2's complexity pass) is the canonical failure mode.

Practical sequencing: model review → pipeline slot → implementation
→ parity test. Step 1 is where the hard thinking happens.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Updates SELF_HOSTING.md §2 dependency diagram and gating rules:

- L2.5 (per-stage domain modeling) runs in parallel with L2 (lens
  migrations). Both start after L1 (reflection). L2 writes lenses
  in dsl/lenses/; L2.5 declares pipeline-domain types in std/ and
  extdeps/. Independent work streams, no file overlap.
- L2.5 stages proceed in L3's implementation order: emit domain
  first (partially exists), lower domain second, parse domain
  third, infer domain last (blocked on I3 write-surface experiment).
  Each domain model runs ahead of its stage's implementation.
- New gating rule: L3 stage N cannot start until L2.5's model for
  stage N is reviewed. Implementation fills in function bodies
  over already-declared types.
- Four-step sequencing table: model review → pipeline slot →
  implementation → parity test.
- v3 ROADMAP.md M3 section updated with L2.5 in the dependency
  chain.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three fixes from codex review on #464:

1. §2.1 v2 contrast reframed. v2's compile.dag IS a .dag
   composition — the gap is decorative metadata with no consumers
   + no structural self-analysis + stage boundaries as convention
   not enforcement. Removed the claim that v2 "lacks a declared
   pipeline graph."

2. §2.1 typed boundary restated as lens-checked property. The
   infer output guarantee (all ports Resolved) is lens-enforced,
   not type-level — there is no InferredDag wrapper type. The doc
   now says "lens-checked property" explicitly and notes a future
   wrapper type as a stronger possible guarantee, not a current
   promise.

3. §2.2 substrate-vs-implementation split added. Walker-local
   state (SubstStack, Scope, UnificationState, fold locals) stays
   in stage .dag bodies, NOT in std/. Only cross-stage and
   downstream-consumed types go in std/. Rule: "if it crosses a
   boundary or a lens reads it, std/. If it's internal to one
   stage's walk, implementation." Table enumerates both categories.
   Infer domain bullet updated to match. Principle paragraph
   updated to say "at boundaries" not "universally."

Also fixes stale Prereq 5 status (merged, not in flight).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New section in SELF_HOSTING.md capturing three architectural
commitments for omni-emission/ingestion:

§2.3 — The compiler knows only substrate primitives (six
connectives + five behaviors). It does NOT know what language it
parses or emits. .dag is a GrammarSpec. Rust is a LanguageSpec.
Both are data. The compiler is parametric on both input and output
language.

§2.3.1 — Emission/ingestion cost model. Any medium that can
represent the substrate's primitives can be a target. Cost is
structural: O(|substrate| × k^depth) where k is the expansion
factor per nesting level. Well-matched targets (k ≈ 1) are cheap;
poorly-matched targets (k > 1) grow exponentially with depth.
Ingestion adds an information-deficit dimension — how much
structural information the source language doesn't carry.

§2.3.2 — Emission layering: spec vs preferences. Three layers
with compositional opacity: Layer 1 (language specification —
facts from the target's own spec), Layer 2 (representation
mapping — substrate to target constructs), Layer 3 (style/lint/
idiom preferences — composable ON TOP of the spec). Quality of
emitted code is a typed, composable property. The same layering
applies to ingestion (grammar facts vs parsing preferences).

§2.3.3 — Four design challenges: GrammarSpec formalism,
LanguageSpec completeness, bootstrap seed size, per-language
spec authoring cost.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…p.dag

Two fixes addressing the remaining review blockers on #464:

1. ROADMAP Track 17b + Phase 4: bootstrap.dag's stage vocabulary
   (CompilerStage, TransformContract, ChangeClassification,
   BootstrapStrategy) is explicitly superseded by the pipeline
   composition from SELF_HOSTING.md §2.1. regen.dag reads the
   pipeline's typed stage signatures, not a separate bootstrap.dag
   type set. Phase 4 Level 1-2 updated to reference pipeline
   signatures and lens-checked contracts instead of bootstrap.dag's
   TransformContract vocabulary. Resolves the "two authorities for
   stage metadata" finding both reviewers flagged.

2. SELF_HOSTING.md §2 gating rule 2: explicit distinction between
   L2.5 and the decorative-modeling pattern Track 17 warns about.
   Each L2.5 type has a named consumer: the L3 stage implementation
   that reads it. The test is: "which function body in L3 reads
   this type?" If you can't name one, don't declare it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…leSpec

Three fixes addressing ChatGPT review findings on #464:

1. Shape A / Shape B boundary respected. §2.3 now explicitly
   scopes to Shape A targets only. English, YAML, SPICE, and
   Terraform are never EmissionTarget values — they are Shape B
   outputs of .dag user programs. The thesis's category-error
   warning (§"Omni-emission") is referenced directly. The cost
   model in §2.3.1 keeps the expansion-factor analysis but
   separates Shape A cost (in LanguageSpec, compiler-owned) from
   Shape B cost (in user program type specs, user-owned).

2. GrammarSpec + ElaborationSpec split. The pipeline signature
   grows an elaboration parameter: grammar owns syntax (tokens →
   surface tree), elaboration owns semantics (surface → substrate).
   This matches MODELING.md's four-layer stack (surface sugar /
   composition / semantic kernel / foundation). Collapsing both
   into GrammarSpec would leave elaboration as implicit compiler
   behavior. New design challenge §2.3.3 #2 names the
   ElaborationSpec design question.

3. StyleSpec reframed as RenderingSpec — declared rendering
   conventions with structural authority. NOT "preferences" or
   "annotations." When present, they are hard constraints on the
   emitter's output formatting. "Optional" means the emitter CAN
   run without them, not that they are soft suggestions. Aligns
   with MODELING.md ("models are shared facts, not preferences")
   and INVARIANTS.md ("no annotation mechanisms through M3").

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…vocation)

Two additions to substrate-reflection-design.md surfaced by the
swift-ram-158 reflection implementer:

Prereq 0.5 — Implicit generic-call instantiation. Prereq 0 lands
explicit template binding (apply(3, negate)). Prereq 0.5 extends
to implicit binding: when fold(singleton(1), 0, |acc, x| acc + x)
is called, inference synthesizes T := Int, U := Int from argument
types without explicit template arguments. This is the gap the
*_int monomorphized wrappers in list.dag exist to bridge. Standard
HM-style inference scoped to call sites, no new substrate variants.
Blocks: lens migration (Unit 3), list.dag wrapper dissolution,
compositional generic list operations.

§4.4 — Lens invocation mechanism. Two paths:
- Path A (compiled Rust integration) for parity tests — emitted
  Rust function linked into test binary, called with a Dag value,
  output compared byte-identical to Rust lens. Blocked on
  Prereq 0.5.
- Path C (interpreter) for development and self-analysis — dag run
  evaluates the lens on a Dag value. No Rust compilation needed.
  Available earlier.
- Path B (subprocess) rejected — awkward, slow, loses type safety.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@briansrls

This comment has been minimized.

@briansrls
briansrls marked this pull request as ready for review April 16, 2026 05:28

@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: 68ee74706e

ℹ️ 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 src/v3/compiler/src/infer.rs Outdated
.collect::<Option<Vec<_>>>()?;
Some(ResolvedArrow {
inputs,
output: TypeShape::new(target),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Return parent sum type for variant constructor signatures

resolve_direct_target_signature always emits output: TypeShape::new(target) for Conj targets, but sum constructors are lowered as calls to anonymous variant payload declarations (Conj) rather than the parent Disj. That means expressions like classify(Plus) infer the constructor result as the variant payload type instead of Sign, and then fail with a type mismatch at call sites expecting the sum type. The constructor signature path needs to map variant constructors back to their enclosing Disj output type.

Useful? React with 👍 / 👎.

Comment thread src/v3/compiler/src/infer.rs Outdated
&mut arguments,
) {
CallableBindingResolution::Resolved => {}
CallableBindingResolution::Retry => {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Propagate Retry when callable argument binding is incomplete

When bind_expected_callable_to_actual returns Retry, resolve_callable_target currently ignores it and continues as if binding succeeded. For higher-order calls this can resolve signatures with incomplete template bindings in early iterations; downstream nodes may then be marked Unresolved, and those ports are not recoverable later because infer skips setting types on unresolved ports. Returning Retry here is necessary to avoid locking valid programs into permanent inference failures while callable signatures are still being learned.

Useful? React with 👍 / 👎.

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

codex · gpt-5.4 · 68ee7470

⚠️ Review (blocking: 2, non-blocking: 1+/0-)

BLOCKING (2)

Root Cause

  • src/v3/compiler/tests/m2_substrate_inhabitance_test.rs The new parity gate checks that selected reflected record types exist and have bindings, but it never compares reflected coproduct labels/payloads against dag.rs → add a full carrier-parity test before treating substrate reflection as landed.

Non-blocking — Strengths

  • src/v3/compiler/tests/m1_3_emit_rust_test.rs The crate-linked emit_rust_module roundtrips are the right proof shape for this PR because they execute emitted reflected functions against a live Dag instead of stopping at static emission text.

⚠️ The runtime path and test harness look directionally right, but the reflected substrate still disagrees with the real carrier on load-bearing shapes, so this would freeze the wrong authority for later lens migrations.

Comment thread src/v3/std/substrate.dag Outdated
value: DeclarationId
}

type FieldValue

This comment was marked as resolved.

Comment thread src/v3/std/substrate.dag
}

// Computation substrate.
type TransformTarget

This comment was marked as resolved.

@briansrls

This comment has been minimized.

@briansrls

This comment has been minimized.

@briansrls

briansrls commented Apr 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Meta-Review (Loop Health)

⚖️ SHIP_WITH_DEBT

Loop summary.

From the attached history, I can verify N = 3 visible review events on this PR: K = 1 codex-cli review and L = 2 chatgpt-browser events, one of which is just an in-progress placeholder. The visible elapsed time is about 57 minutes — from 2026-04-16 05:27:39Z to 06:24:38Z (ALL_REVIEWS.txt:1-41). I cannot verify M commits from the provided artifacts/private PR view, so I’m not going to fake that number.

Forward progress evidence.

This loop did not just churn comments. It moved the root cause.

First, it enabled real consumers, which is the load-bearing test. The codex review explicitly praised the crate-linked emit_rust_module roundtrips because they execute emitted reflected functions against a live Dag instead of stopping at text (ALL_REVIEWS.txt:30-36). The current diff extends that proof shape with reflected-runtime tests such as rustc_roundtrip_emitted_module_invokes_reflected_dag_function and behavior-payload roundtrips (pr-466.diff:6374-6398). That is principle 1 in action: substrate changes now have live consumers.

Second, it paid off scaffolds instead of teaching the codebase to depend on them. The design docs said the *_int wrappers in src/v3/std/list.dag were temporary bridges until implicit generic-call instantiation existed (pr-466.diff:402-457). This diff removes singleton_int, cons_int, fold_int, map_int, and filter_int (pr-466.diff:8099-8125), and adds tests proving plain generic fold/map calls now synthesize template bindings directly (pr-466.diff:6593-6630). That is net scaffold reduction, not scaffold accumulation.

Third, the main prior blocker was actually addressed at the substrate, not papered over in a consumer. The earlier browser review stopped on the fact that runtime/spec knew richer FieldValue shapes than the reflected substrate, and that emit_rust.rs was still recovering variant identity from strings (ALL_REVIEWS.txt:6-20). The current diff closes most of that gap:

  • dag.rs expands FieldValue to Record, List, and Variant { constructor: DeclarationId, payload } instead of constructor_name: String (pr-466.diff:1157-1176);
  • src/v3/std/substrate.dag now reflects those shapes (pr-466.diff:8136-8230);
  • src/v3/spec/rust.dag now models FieldBinding, RustFieldAccess, CallableRealization, and RustCallableStrategy structurally (pr-466.diff:7384-7451);
  • m2_substrate_inhabitance_test.rs now includes substrate_coproducts_match_runtime_carriers and rust_dag_realizes_reflected_substrate_types, which is exactly the parity gate codex said was missing (ALL_REVIEWS.txt:30-36; pr-466.diff:7136-7309).

Fourth, findings are being promoted to structural rules, not rediscovered one file later. This diff adds new cross-layer invariants — Bounded Substrate Seed, Lenses Are Substrate Declarations, and Every Dependency Is A Substrate Fact (pr-466.diff:1-75). That matches the project’s own review doctrine: facts must flow forward, and substrate-level issues are blocking because they get copied (modeling-discipline.md:51-68, 167-179; INVARIANTS.md:102-129).

Debt accumulation evidence.

Debt did not disappear, but it shrunk faster than it grew.

The remaining debt is a small centralized bridge, not the old copyable bridge. emit_rust.rs no longer switches on freeform constructor_name payload strings. But it still resolves variants through named_variant_id(dag, "RustFieldAccess", "DirectField") and similar lookups for RustCallableStrategy (pr-466.diff:2159-2272). That is still name recovery. It is much narrower than before, but it is not the final form.

There is also still an execution-path gap for compiled .dag lenses. The docs now say clearly that the reflection surface exists and the remaining blocker is invocation/execution plumbing, not missing substrate reflection (pr-466.diff:222-275). docs/substrate-reflection-design.md now names Path A and Path C explicitly, which is good accounting, but it also means that part of the self-inspection story is still follow-up debt (pr-466.diff:331-391).

So yes, there is debt. But it is tracked debt after real payoff, not unbounded accumulation.

Cheating signal.

Low.

The implementer is documenting compromises and then deleting them, not hiding them. The *_int wrappers were explicitly described as temporary and then removed (pr-466.diff:402-457, 8099-8125). The lens docs now explicitly say the remaining blocker is invocation, not reflection (pr-466.diff:222-275). The biggest recent fixes are structural: richer reflected substrate, DeclarationId-carried variant identity, carrier-parity tests, and scaffold deletion. That is the opposite of “smallest blast radius, hope nobody notices.”

The only cheating-like residue I still see is the named_variant_id(...) lookup. That should be follow-up debt, not normalized architecture.

Why this is not PAUSE_AND_REGROUP.

Because the regroup already happened. The current INVARIANTS.md explicitly records an earlier meta-review that said the project had to pause and regroup by graduating recurring authority problems into the Semantic authority after lowering invariant (INVARIANTS.md:1720-1777). This PR is not missing that lesson; it is spending it.

Why this is not KEEP_ITERATING.

Because the next round is unlikely to unlock a new class of correctness consumer. The big loop gains are already in: live Dag roundtrips, substrate parity tests, scaffold payoff, and invariant graduation. Another round would mostly shave the last centralized name-lookup bridge and extend lens invocation plumbing. That is follow-up value, not a reason to keep this PR in review.

Meta-verdict.

This loop is making forward progress, not shifting debt. It enabled new consumers, dissolved explicit scaffolds, and moved the old blocker from “runtime/spec disagree with reflection” to “one centralized bootstrap lookup still exists.” That is the point where the loop should stop pretending it still needs another full review cycle.

Merge this PR. Then do a small follow-up for:

  1. eliminating named_variant_id(...) name recovery in emit_rust.rs, and
  2. wiring the compiled-lens invocation path now that the reflection surface is present.

View conversation

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

codex · gpt-5.4 · 45998180

⚠️ Review (blocking: 2, non-blocking: 1+/0-)

BLOCKING (2)

Root Cause

  • src/v3/spec/rust.dag Type-application emission has syntax strings but no typed constructor→syntax mapping, so emit_rust falls back to matching std type names → add a realization keyed by constructor DeclarationId (or equivalent declared field on LanguageSpec) and dispatch on that.
  • src/v3/compiler/src/infer.rs Alias-normalization is reusing the “find named anchor” walk as a type-equivalence relation → add a separate equivalence walk that peels aliases but preserves template + TemplateArgument structure.

Non-blocking — Strengths

  • src/v3/compiler/tests/m2_substrate_inhabitance_test.rs The new coproduct-parity assertions are the right proof shape because they compare reflected runtime carrier labels/payloads instead of stopping at record-field coverage.

ROADMAP — Verified

  • Prereq 0.5 implicit generic-call instantiation: fold/map now compile and roundtrip without *_int wrappers, and the wrappers are deleted from src/v3/std/list.dag.

ROADMAP — Incomplete

  • Typed type-application realization: Container/instantiated type emission still depends on compiler-side name dispatch, so the “all rendering decisions come from declared target data” step is not structurally complete yet.

⚠️ The reflected substrate/runtime path is materially stronger now, but these two new bridges would still freeze name- and instantiation-loss into the compiler boundary if they land as-is.

Ok(render_named_template(
&self.indexes.syntax.type_applications.optional,
&[("element", &inner)],
))

This comment was marked as resolved.

SourceSpan::new("<inferred>", 0, 0)
}

fn type_shapes_equivalent(dag: &Dag, lhs: &TypeShape, rhs: &TypeShape) -> bool {

This comment was marked as resolved.

@briansrls

This comment has been minimized.

@briansrls

Copy link
Copy Markdown
Contributor Author

Director review — #466 (reflected substrate + runtime emission)

Overall: real progress, not merge-ready yet. The meta-review's SHIP_WITH_DEBT verdict is directionally right — this PR dissolved real scaffolds and enabled real consumers. But since there are merge conflicts and the implementer is still working, the cleaner path is to dissolve the remaining debt NOW rather than tracking it as follow-ups.

Three items to clean up before merge

1. Eliminate name-keyed dispatch in emit_rust.rs.

Two sites:

  • named_variant_id(dag, "RustFieldAccess", "DirectField") and similar lookups for RustCallableStrategy — these recover spec variant identity by string name instead of by DeclarationId. Fix: cache the variant DeclarationIds at startup (same pattern as PrimitiveCache / SubstrateMarkers) and dispatch on typed handles.
  • render_instantiated_type matching on "List" / "Map" — decides container rendering by declaration name. Fix: add a TypeApplicationSyntax realization in rust.dag keyed by template DeclarationId, not by name. The emitter reads the realization; never asks "is this a List?"

Both are the same pattern: fact computed upstream (DeclarationId), dropped at a boundary, reconstructed downstream by name. The fix is the same: thread the typed handle through.

2. Separate alias-normalization walk from name-anchor walk in infer.rs.

The codex review caught: alias normalization reuses the "find named anchor" walk as a type-equivalence relation. These are different operations — one peels aliases, the other finds a display name. Using one for both loses template + TemplateArgument structure during alias peeling. Fix: add a dedicated normalize_alias(decl_id) -> DeclarationId walk that follows Instantiation chains but preserves template argument bindings, separate from the name-anchor walk.

3. Resolve merge conflicts against current main.

#464 and #467 both merged since this PR was opened. Rebase onto current main and verify substrate.dag / SELF_HOSTING.md / ROADMAP.md are coherent with the merged content.

What's working well

  • FieldValue::Variant { constructor: DeclarationId } replacing constructor_name: String — real structural fix
  • Substrate coproduct parity tests (substrate_coproducts_match_runtime_carriers) — the right proof shape
  • *_int wrapper deletion + implicit generic-call instantiation — scaffold dissolved
  • Live Dag roundtrip tests — real consumer proof
  • Three new INVARIANTS.md entries graduated from review findings

The remaining debt is small, centralized in emit_rust.rs, and has clear fixes. Clean it up in this pass and the PR lands without tracked follow-ups.

@briansrls

This comment has been minimized.

@briansrls

briansrls commented Apr 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Meta-Review (Loop Health)

According to a document from 2026-04-16, the loop is not healthy enough to keep grinding one more review round. It made real progress, but it is now rediscovering the same structural bug class in new places instead of graduating that class into a hard invariant. chatgpt-review-a03fe004-b414-4a…

chatgpt-review-02f90137-32f4-4b…

Loop summary

Visible in the supplied history: 5 review events total, made up of 2 codex-cli reviews and 3 chatgpt-browser reviews, spanning roughly 96 minutes from the first visible browser review at 2026-04-16T05:27:39Z to the last visible browser event at 2026-04-16T07:04:05Z. The files do not state an exact commit count, but the visible history shows about 3 reviewed revision states / waves. chatgpt-review-a03fe004-b414-4a…

Forward progress evidence

There is real progress.

The loop did add real consumers, not just substrate growth. The codex review explicitly credits the new crate-linked emit_rust_module roundtrips because they execute emitted reflected functions against a live Dag, and the later codex review says the new coproduct-parity assertions are the right proof shape for reflected runtime carrier labels and payloads. It also verifies that implicit generic-call instantiation is now real enough that fold and map compile and roundtrip without the old *_int wrappers, and those wrappers were deleted from src/v3/std/list.dag. chatgpt-review-a03fe004-b414-4a…

chatgpt-review-a03fe004-b414-4a…

More importantly, one whole blocker class actually got paid down. The first browser review said the PR had a half-landed FieldValue bridge: runtime and lowering knew richer structural values than the reflected substrate admitted, and emitter consumers still recovered variant identity by string label. The later browser review explicitly says that blocker is gone: runtime FieldValue and reflected FieldValue now agree on Literal | Reference | Record | List | Variant, and the variant now carries constructor: DeclarationId instead of a free-form name string. That is genuine structural progress, not cosmetic churn. chatgpt-review-a03fe004-b414-4a…

chatgpt-review-a03fe004-b414-4a…

So this is not a dead loop. It did dissolve debt.

Debt accumulation evidence

The problem is that after dissolving one instance, the loop kept finding the same underlying disease one layer over.

Once FieldValue parity was fixed, the next browser review immediately identified the remaining structural leak: emit_rust still takes a structurally known DeclarationId and throws it away, then re-classifies by name in render_instantiated_type using template_name.as_str(). The same review also flags named_variant_id / require_callable_strategy, where the emitter re-finds RustFieldAccess and RustCallableStrategy constructors by hardcoded names. The follow-up codex review says the same thing in different words: typed type-application realization is still incomplete because emission still depends on compiler-side name dispatch. chatgpt-review-a03fe004-b414-4a…

chatgpt-review-a03fe004-b414-4a…

That is exactly the pattern the project already knows is dangerous: a fact is computed upstream, dropped at a boundary, then re-derived downstream by names. The project’s own invariants say the fix is to push structure earlier and make downstream exact, not to keep tuning the heuristic. They also state, very specifically, that after lowering DeclarationId must be the only semantic authority, with no name recovery and no parallel authority. That invariant was itself added because an earlier meta-review found repeated rounds rediscovering the same disease and concluded the right response was to stop iterating and graduate the pattern into a rule. chatgpt-review-02f90137-32f4-4b…

chatgpt-review-02f90137-32f4-4b…

So the debt pattern here is clear:

The loop fixed FieldValue string identity, but it did not fix name-keyed semantic recovery as a class. It just moved the same debt from one bridge to another.

Cheating signal

The implementer is not hiding everything. Some of the work is honest and structural. The FieldValue repair is exactly what good accounting looks like: parity landed, DeclarationId replaced free-form names, and tests were added to prove the carrier shape. That is forward motion. chatgpt-review-a03fe004-b414-4a…

But the most recent remaining fixes have the classic “smaller blast radius” smell. The unresolved behavior is still driven by hardcoded "List" / "Map" / enum-name lookups in Rust, and those are not presented as explicit scaffolds with dissolution triggers. They are just still there. In this codebase’s own discipline, that is exactly the kind of untracked bridge/transitional state that should not survive review: representation changes are supposed to land atomically, or be split smaller, not be left as “one more special case in the emitter.” chatgpt-review-02f90137-32f4-4b…

The modeling discipline also says substrate-level issues are usually blocking because they get copied. The remaining issues are substrate/boundary-authority issues, not local cleanup. chatgpt-review-560f5193-83af-4d…

So the cheating signal is:

Partly documented on the solved class, insufficiently documented on the remaining class.

Recent fixes are mixed: one structural payoff, then a series of “good enough for now” selector leaks still living in Rust.

Meta-verdict

🔁 PAUSE_AND_REGROUP

This loop is not pure wheel-spinning, but it has stopped being a good per-commit review loop. It accomplished one important structural repair, then immediately exposed that the deeper disease is still alive: spec data in .dag, selector logic in Rust. Another round will probably find one more surface of the same problem.

The bluff to call here is: stop patching instances and graduate the class.

Concretely, do not spend the next round fixing one more name match. First ratchet the recurring finding into the review loop as a structural requirement: target-selection and realization dispatch must be DeclarationId-keyed end-to-end, with no emitter-side recovery by declaration name. That is already where the project’s own invariants point. Until that happens, this loop is shifting debt, not just burning it down. chatgpt-review-a03fe004-b414-4a…

chatgpt-review-02f90137-32f4-4b…

In one sentence: real progress happened, but the review loop has crossed from “converging” into “recurring class not yet graduated,” so the next valuable action is regrouping around a new invariant, not another patch round.


View conversation

@briansrls

This comment has been minimized.

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

codex · gpt-5.4 · 9ef2e53e

⚠️ Review (blocking: 2, non-blocking: 2+/0-)

BLOCKING (2)

Root Cause

  • src/v3/compiler/src/bootstrap.rs v3 bootstrap still materializes overlapping dsl/ and src/v3/ declarations in the same table, so downstream lookup had to learn a source-rank tiebreaker → collapse staged overrides during bootstrap and let post-lowering consumers see one declaration per semantic name.
  • src/v3/std/substrate.dag opaque handle ids still have no declared comparison authority in the reflected substrate, so rust.dag/emit_rust borrow OrderedRing.eq/ne and recover them by name → declare the handle-comparison authority as a typed substrate fact and carry that DeclarationId through inference to emission.

Non-blocking — Strengths

  • src/v3/compiler/tests/m1_3_emit_rust_test.rs The module-mode roundtrips are the right proof shape for self-inspection because they run compiled reflected functions against live Dag/Behavior values instead of mocked structures.
  • src/v3/compiler/tests/m2_field_access_binding_test.rs The FieldBinding coverage test is a strong guardrail because it checks every reflected record field has an explicit Rust access mapping, not just a few happy-path reads.

ROADMAP — Verified

  • Reflected runtime execution path: emit_rust_module plus the new in-process roundtrips show compiled .dag code can now read live reflected substrate values through the Rust carrier.

⚠️ The runtime/reflection proof points are substantially stronger, but the new source-rank shadowing and name-recovered operator dispatch still leave two post-lowering authority leaks in the path.

@@ -1149,7 +1251,13 @@ impl Dag {
pub fn declaration_by_name(&self, name: &str) -> Option<&Declaration> {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

BLOCKING: Using file-path rank in declaration_by_name makes source location a post-lowering semantic authority, so staged src/v3/dsl overlaps are hidden by lookup heuristics instead of dissolved to one DeclarationId source (Single Authority / semantic authority after lowering).

@@ -1090,6 +2442,29 @@ fn walk_to_algebra_conj(dag: &Dag, start: DeclarationId) -> Option<DeclarationId
None

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

BLOCKING: The new canonical_operator_field fallback reconstructs operator identity from hardcoded OrderedRing/field names whenever an operand lacks an algebra Conj, which violates layer opacity and the new “Every Dependency Is A Substrate Fact” rule.

@briansrls

This comment has been minimized.

@briansrls

briansrls commented Apr 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Meta-Review (Loop Health)

According to a document from 2026-04-16, this loop is no longer healthy enough to keep spinning as-is. It has produced real gains, but the last rounds are mostly moving the same authority leak into new layers instead of retiring the class.

Loop summary

There are 7 review events in the attached history: 4 chatgpt-browser reviews and 3 codex-cli reviews, running from 05:27:39Z to 07:50:12Z on 2026-04-16, so about 2 hours 23 minutes elapsed. The files do not expose an exact commit list, but the review state changes show at least 4 distinct revision turns.

Forward progress evidence

This loop did accomplish real things. The earliest blocking issue was that the runtime carrier had widened FieldValue while the reflected substrate had not, and the emitter was still recovering variant identity from strings. By the next browser round, reviewers explicitly stopped re-raising that blocker: FieldValue parity was landed, constructor identity had moved to DeclarationId, and new parity tests plus roundtrip tests were accepted as the right proof shape. Codex also verified that implicit generic-call instantiation unblocked real consumers by letting fold and map compile without *_int wrappers, and the latest codex round says the runtime/reflection path can now read live reflected substrate values through the Rust carrier. Those are real consumers, not substrate growth for its own sake.

Debt accumulation evidence

The problem is that the same root class keeps coming back in new forms. First it was stringly FieldValue constructor names. Then it became name-keyed type-application rendering and selector recovery in emit_rust. Then it became source-path precedence as semantic authority. Then it became per-call-site anonymous instantiations patched over with type_shapes_equivalent. By 07:39, the browser review explicitly says the loop is stagnant because the same authority workaround is now copied across bootstrap, lowering, and emission, while callable specialization adds another repair layer instead of deleting one. The final codex review repeats the same root cause: collapse staged overrides in bootstrap and stop recovering operator/handle behavior by name. That is not convergence on one structural rule; it is debt migration.

This is exactly the kind of issue the active modeling discipline says is blocking: substrate-level mistakes get copied and become harder to unwind later. The project already has the right structural destination written down: the layer_opacity lens is supposed to gate compiler consumers, and after lowering DeclarationId is supposed to be the only semantic authority. The invariant text even says the reason for adding that rule was a prior meta-review where the same root question kept surviving round after round. That is the current PR’s pattern again.

Cheating signal

The implementer is not fully hiding compromises; some debt is openly discussed in roadmap/review language. But the most recent fixes are still “good enough for now” code-path repairs, not explicit scaffold receipts with a dissolution trigger. The tell is that reviewers themselves already proposed the needed graduation: one round says target selection must become DeclarationId-keyed, another says span.file may explain provenance but must never decide authority. The next round did not land that invariant as enforcement; it landed more local logic that depends on the same workaround. That is rational triage under budget, but it is still implementer cheating with insufficient accounting.

There is a second signal too: the roadmap already says the next high-leverage work is to make existing modeling load-bearing before adding more, because zero-consumer modeling is decorative. Yet this loop kept enlarging the substrate/emitter surface while the authority boundary was still unsettled.

Meta-verdict

🔁 PAUSE_AND_REGROUP

Do not run another normal review round on this PR shape. The loop has proven two things already: first, there is real value in the direction because consumers/tests are being enabled; second, the recurring failure mode is now obvious and structural. The next unit of work should be one regrouping step that graduates the repeated finding into enforcement: collapse bootstrap overrides before declarations enter the table, make post-lowering authority strictly DeclarationId-based, and stop allowing emit/infer to repair identity loss downstream. Until that happens, another round will mostly shift the same debt again.


View conversation

@briansrls

briansrls commented Apr 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Meta-Review (Loop Health)

According to a document from April 16, 2026, this loop is no longer a healthy “keep reviewing until it gets cleaner” loop. It produced real progress, then started relocating the same debt class instead of deleting it.

Loop summary

7 review events, across about 2 hours 22 minutes, with 3 codex-cli reviews and 4 chatgpt-browser reviews. The attached history shows roughly 4 visible revision snapshots, but not an exact commit count, so “M ≈ 4” is the honest number here.

Forward progress evidence

This loop did achieve real forward progress.

The first major blocker was real: runtime FieldValue had outgrown the reflected substrate. By the 06:24 review, that blocker was gone. The reviewer explicitly stopped re-raising it, said FieldValue parity had been fixed, and called the new parity plus roundtrip tests the right proof shape. That is genuine debt payoff, not churn.

It also enabled real consumers. Codex called out emit_rust_module roundtrips as proof that compiled reflected .dag code can read live Dag/Behavior values, and called the field-access binding test a strong guardrail because it checks that every reflected record field has an explicit Rust access mapping. That satisfies the “consumers define correctness” test much better than substrate growth with no reader.

There was also scaffold payoff. Codex verified that fold and map now compile and roundtrip without *_int wrappers, and that those wrappers were deleted from src/v3/std/list.dag. That is a tracked compromise actually being dissolved.

Debt accumulation evidence

The problem is that the loop’s main recurring disease did not get removed. It moved layers.

First the authority leak was stringly FieldValue variant identity. Then it became name-keyed target selection in emit_rust. Then it became source-path-ranked declaration authority (src/v3/ beats dsl/). Then it became callable specialization by allocating fresh anonymous instantiations and repairing later with type_shapes_equivalent. Different files, same class: facts stop flowing forward as canonical identity and get re-derived by name, path, or equivalence heuristics.

That means the loop is failing principle 3 and principle 4 together. The same root cause is being rediscovered in emit, then bootstrap/lower, then infer, instead of being promoted once into a structural rule that kills the whole class. The browser reviewer literally proposed that promotion twice: first, “target-selection decisions must be DeclarationId-keyed, never declaration-name keyed,” then, “after load, span.file may explain provenance, never decide authority.” The later review still found the same pattern spreading and explicitly downgraded loop health to stagnant.

Worse, the project already has the right invariant on paper: after lowering, DeclarationId is supposed to be the only semantic authority, with no global name recovery and no bootstrap-only special cases in downstream stages. So this loop is not waiting on a missing concept. It is accruing fresh exceptions against an already-written rule.

Cheating signal

The implementer is not hiding compromises. Some are documented, and earlier rounds clearly paid real debt down. This is not a stealth-debt branch.

But the most recent fixes are still the rational “small blast radius” moves an implementer makes when the loop is tiring out: source-rank tie-breakers instead of collapsing duplicate authorities before lowering; type_shapes_equivalent instead of canonicalizing specializations once; emitter lookups by names instead of typed selector facts. Those are not lazy fixes. They are triage fixes. But they are still debt-shifting fixes. The compromises are accounted for, yet they remain on the main semantic path instead of being quarantined behind a bounded scaffold with a near-term dissolution trigger.

Meta-verdict

🔁 PAUSE_AND_REGROUP

This loop is not bluffing about progress; it did unlock real consumers and dissolve real scaffolds. But the marginal value of the next ordinary review round is negative, because the unresolved class is now “authority split after lowering,” and the branch is teaching more layers to depend on that split. Another pass will likely find the same bug class in yet another layer.

The right next move is not “one more review.” It is:

Stop the patch churn, write the missing ratchet at the actual boundary, and rework this branch so post-lowering authority is singular: no source-path precedence, no name-keyed selector recovery, no anonymous-instantiation-plus-equivalence-repair. Until that happens, this loop is shifting debt faster than it is deleting it.


View conversation

@briansrls

This comment has been minimized.

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

codex · gpt-5.4 · 1f6d54cb

⚠️ Review (blocking: 2, non-blocking: 2+/1-)

BLOCKING (2)

Root Cause

  • src/v3/std/substrate.dag the file landed as a direct Rust-carrier mirror without a per-coproduct ledger → add 🟢/🟡/🔴 receipts beside each new sum or dissolve the temporary ones before downstream stages take them as authority
  • src/v3/compiler/src/lower.rs realization-category narrowing is still hard-coded to three categories plus primitive-only targets → widen realization_category_for_meta and validate_realization_field_target before landing the expanded rust.dag surface

Non-blocking — Strengths

  • src/v3/compiler/tests/m2_substrate_inhabitance_test.rs The new bootstrap reflection tests are a strong guardrail because they pin both the lowered substrate shape and the existence of rust.dag realization entries for each reflected carrier type, which catches drift earlier than consumer-specific roundtrips.
  • src/v3/compiler/tests/m1_substrate_test.rs The Prereq 0.5 test cluster checks inferred template bindings, lambda parameter resolution, and the conflict diagnostic, which is the right fail-closed proof shape for implicit generic-call instantiation.

Non-blocking — Improvements (fix in-PR if easy, else defer to roadmap)

  • docs/lens-library-design.md This doc now says execution is the only remaining blocker, but m2_lens_unused_parameters_migration_test.rs still codifies a separate recursive walk_steps template-binding conflict, so the migration status should be tightened to the tested state for the L1 reflection / M2 lens-migration lane.

ROADMAP — Verified

  • Prereq 0.5 implicit generic-call instantiation: The new inference tests plus emitted-Rust roundtrips show bare fold/map/filter calls now synthesize template bindings without the old _int wrappers.

ROADMAP — Incomplete

  • lens_unused_parameters .dag migration: The active migration test still expects unused_parameters.dag to fail on the recursive walk_steps binding conflict and keeps parity ignored, so the lens has not yet reached an execution-only state.

⚠️ The reflected substrate direction is materially stronger, but the new substrate sums still need explicit dissolution receipts and the expanded rust.dag surface currently outruns lower-time realization validation.

Comment thread src/v3/std/substrate.dag
declaration: DeclarationId
}

type PortState

This comment was marked as resolved.

Comment thread src/v3/spec/rust.dag
fields: []
cost: 1
}

This comment was marked as resolved.

@briansrls

This comment has been minimized.

@briansrls

briansrls commented Apr 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Meta-Review (Loop Health)

Loop summary. I can verify 9 logged review events in ALL_REVIEWS.txt, spanning ~8h47m on April 16, 2026 (05:27:39Z → 14:15:03Z): 4 codex-cli reviews and 5 chatgpt-browser reviews. The attached history does not expose git hashes, so the commit count is only inferable from review-bearing snapshots; the safest estimate is about 5 discernible revision rounds / pushes rather than an exact commit total.

Forward progress evidence. This loop did accomplish real things. The first blocking class — reflected substrate/runtime mismatch around FieldValue — was actually fixed: later review rounds explicitly stop re-raising it and credit the PR with getting FieldValue aligned across dag.rs and src/v3/std/substrate.dag, plus parity tests and roundtrip tests as real consumer proof. More importantly for principle 1, this is not substrate growth with zero consumers: the review history credits new crate-linked emit_rust_module roundtrips, FieldBinding coverage, a reflected runtime execution path that reads live Dag/Behavior values, bootstrap reflection tests, and implicit generic-call instantiation that removes old *_int wrappers for fold/map/filter. Those are genuine consumers/tests, not just theory.

Debt accumulation evidence. The loop is also clearly shifting debt. The recurring pattern is the same in new clothes: first it was string recovery from FieldValue, then name-keyed type-application rendering, then source-path precedence as semantic authority, then fresh anonymous instantiations plus type_shapes_equivalent repair, then lens-side producer reconstruction instead of reading produced_by, and finally a substrate mirror with no per-coproduct ledger plus still-hardcoded realization validation. The reviews themselves say this repeatedly: “ratchet a new invariant,” “span.file may explain provenance, never decide authority,” “same authority workaround is now copied across bootstrap/lower/emit,” and “consumer-side reconstruction … before more migrated lenses copy it.”

The current diff confirms that this is not just stale review language. It still contains exactly the workaround shapes the loop kept rediscovering: source-rank shadowing in bootstrap (declaration_name_preference_rank), name dispatch in emit_rust (template_name.as_str() matching "List"), and lens-side reconstruction via find_producer / behavior_port plus a recursive walk_steps blocker path in unused_parameters.dag rather than straight consumption of substrate facts (pr-466.diff L1097-L1119, L3406-L3412, L9678-L9693, L9711-L9784). That is classic “smaller blast radius now, more repair layers later,” not dissolution.

Cheating signal. Mixed, but trending bad. The implementer is not hiding everything: the reviews explicitly note an expected-failure test that keeps the lens gap honest, and the loop has real tests instead of vague promises. But the latest state is still dominated by cheap local repairs rather than structural closure: path-ranking instead of single authority, equivalence repair instead of canonical identity, lens scans instead of reading produced_by, and substrate expansion without the requested coproduct receipts. The giveaway is that the newest codex review is still asking for scaffold accounting (“per-coproduct ledger”) and still calling out hard-coded realization narrowing at lower time, which means the compromises are not being fully documented where they land.

This is exactly the class of loop the project already knows how to stop. INVARIANTS.md’s semantic authority after lowering section says a prior meta-review found the same disease — new surfaces of one underlying authority problem — and the explicit lesson was to pause and graduate the pattern into an invariant instead of patching its next instance. That matters here because the source-path precedence issue also directly violates the thesis note that src/v3/spec/ is a bootstrap-local artifact, not a second authority.

Meta-verdict

🔁 PAUSE_AND_REGROUP

The direction is right. The loop is not.

This PR has enough real consumer enablement that REVERT_AND_RETHINK would be wrong, and too much untracked / still-spreading structural debt for SHIP_WITH_DEBT. It is also past the point where another ordinary per-commit pass is likely to help: the reviews are no longer discovering independent bugs, they are rediscovering the same missing invariant across new layers. Stop iterating on instances. Write the structural rule and refactor around it.

The regroup target should be explicit and singular: post-lowering semantic authority must be one thing, and consumers must read carried substrate facts instead of reconstructing them. Concretely: eliminate source-rank authority, eliminate name-keyed target selection, and eliminate lens-side producer/result-port reconstruction before taking another review lap. Once that rule is landed, the next round will add value again.


View conversation

@briansrls

briansrls commented Apr 16, 2026 •

Copy link
Copy Markdown
Contributor Author

ChatGPT Review

Principle audit.

1. Fail-closed. Satisfied. The new reflected-runtime and emitted-module path still fails through diagnostics or EmitError rather than silently fabricating success, and the lens migration tests are honest hard assertions rather than permissive smoke tests.

2. Illegal states unrepresentable. Mostly satisfied. The reflected substrate surface is materially better than before: Dag, Behavior, payload records, and field bindings are now modeled explicitly instead of being smuggled through ad hoc Rust-only access. I do not have a new blocker here.

3. Facts flow forward. Blocking. The new pattern-realization path still does not carry “how this realized sum decomposes” as one typed downstream fact. PatternRealization declares only a generic VectorList strategy plus raw syntax snippets in src/v3/spec/rust.dag:138-151, 796-804. Then emit_rust has to recover the actual semantic structure by searching for variants labeled Empty and Cons, and it reconstructs head/tail access through emitter-local field_overrides instead of reading a declared decomposition authority (src/v3/compiler/src/emit_rust.rs:1183-1242, 1613-1709). That is exactly the thesis/discipline failure mode: part of the fact is declared in target data, the rest is recovered later by convention.

4. Coproduct dissolution. Satisfied from my side. I am not re-adding Codex’s already-made ledger point; beyond that, I do not see a new coproduct concern in this round.

5. Single authority. Blocking. The unused_parameters migration no longer has an independent oracle. src/v3/compiler/src/lens_unused_parameters.rs now delegates to an included generated snapshot (src/v3/compiler/src/lens_unused_parameters.rs:5-11, 34-45), and the migration test’s “Rust lens” side calls that wrapper (src/v3/compiler/tests/m2_lens_unused_parameters_migration_test.rs:138-154, 173-202). But the design doc’s proof shape is parity against the hand-written Rust lens, and only after that deletion (docs/substrate-reflection-design.md:1470-1489). As landed, the test is generated-vs-generated from the same .dag source, so it can prove freshness of two emission paths, not correctness of the migration. I also still agree with the earlier producer-edge note: unused_parameters.dag is still reconstructing producers by scanning d.nodes instead of reading DagPort.produced_by (src/v3/lenses/unused_parameters.dag:111-177).

6. API-level enforcement. Not yet structural. lower.rs now grows its own substitution/canonicalization stack for field-project typing (src/v3/compiler/src/lower.rs:1285-1454, 3036-3052), while infer.rs already has a separate equivalence/canonicalization path for callable specialization (src/v3/compiler/src/infer.rs:1776-1825, 2905-2924, 2992-3040). That means “canonical instantiated type identity” is still something each stage can re-implement, not something the API enforces once. I would treat this as the cross-layer pattern to ratchet next, rather than as a third separate blocker.

Design question. During a lens migration, what is the independent authority that remains long enough to prove the .dag lens is correct before the Rust lens disappears? What is at stake is the whole migration method: if the first proof point is allowed to compare a checked-in generated shim against a freshly emitted version of the same source, future lens migrations can self-certify while sharing the same bug.

Verdict. REQUEST_CHANGES. This is real progress: the reflected carrier surface is stronger, the emitted-module roundtrips are valuable, and the first lens shim is a meaningful consumer. But I would not bank the round while realized-pattern elimination still depends on emitter-side list semantics, and while the migration test no longer compares against an independent Rust authority.

LOOP HEALTH: stagnant — this round clearly enables a real consumer, but it also turns the first migration proof into generated-vs-generated self-comparison and adds another stage-local normalization walk, so the debt is still shifting sideways rather than disappearing.


View conversation

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

codex · gpt-5.4 · 94fe5576

⚠️ Review (blocking: 1, non-blocking: 2+/0-)

BLOCKING (1)

Root Cause

  • src/v3/spec/rust.dag the Rust target spec is still adding strategy coproducts without a per-type dissolution ledger → add 🟢/🟡/🔴 receipts beside each new sum or dissolve the temporary strategy layer into structural carrier facts now

Non-blocking — Strengths

  • src/v3/compiler/tests/m2_lens_unused_parameters_migration_test.rs The migration is now empirically grounded: the compiled .dag lens matches the Rust lens on fixtures and self-analyzes cleanly, which is the right proof shape for the reflection thesis.
  • src/v3/compiler/tests/m1_substrate_test.rs Asserting CallableRealization and the rust_language syntax bundle at bootstrap materially strengthens fail-closed coverage for the new reflected target surface.

ROADMAP — Verified

  • lens_unused_parameters .dag migration parity: The new migration test compiles the .dag lens, roundtrips it through emitted Rust, and matches the legacy Rust lens on core fixtures plus self-analysis.
  • Advance reflected substrate and runtime emission: m1_3_emit_rust_test.rs now roundtrips reflected field access, list carriers, and emitted callable lowering against live Dag values, so this lane is no longer design-only staging.

⚠️ This revision closes several real gaps, but the new rust.dag strategy coproducts still need explicit dissolution receipts and previously tracked blockers are not all closed.

Comment thread src/v3/spec/rust.dag
rust_access: RustFieldAccess
}

type RustFieldAccess

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

BLOCKING: New target-spec coproducts RustFieldAccess, RustCallableStrategy, and RustPatternStrategy land without 🟢/🟡/🔴 receipts, so their scaffold-vs-terminal status is still implicit on a downstream-consumed .dag surface (Coproduct Dissolution).

@briansrls

Copy link
Copy Markdown
Contributor Author

codex · gpt-5.4 · 94fe5576

⚠️ Review (blocking: 1, non-blocking: 2+/0-)

BLOCKING (1)

Root Cause

  • src/v3/spec/rust.dag the Rust target spec is still adding strategy coproducts without a per-type dissolution ledger → add 🟢/🟡/🔴 receipts beside each new sum or dissolve the temporary strategy layer into structural carrier facts now

Non-blocking — Strengths

  • src/v3/compiler/tests/m2_lens_unused_parameters_migration_test.rs The migration is now empirically grounded: the compiled .dag lens matches the Rust lens on fixtures and self-analyzes cleanly, which is the right proof shape for the reflection thesis.
  • src/v3/compiler/tests/m1_substrate_test.rs Asserting CallableRealization and the rust_language syntax bundle at bootstrap materially strengthens fail-closed coverage for the new reflected target surface.

ROADMAP — Verified

  • lens_unused_parameters .dag migration parity: The new migration test compiles the .dag lens, roundtrips it through emitted Rust, and matches the legacy Rust lens on core fixtures plus self-analysis.
  • Advance reflected substrate and runtime emission: m1_3_emit_rust_test.rs now roundtrips reflected field access, list carriers, and emitted callable lowering against live Dag values, so this lane is no longer design-only staging.

⚠️ This revision closes several real gaps, but the new rust.dag strategy coproducts still need explicit dissolution receipts and previously tracked blockers are not all closed.

@briansrls

briansrls commented Apr 16, 2026 •

Copy link
Copy Markdown
Contributor Author

ChatGPT Review

Principle audit.

1. Fail-closed. Mostly satisfied. The new realization-index loading and the new substrate tests are doing the right thing here: malformed CallableRealization, PatternRealization, and TypeInstantiationRealization targets are rejected instead of being tolerated downstream (src/v3/compiler/tests/m1_substrate_test.rs:872-914). I do not have a new fail-closed blocker.

2. Illegal states unrepresentable. Mostly satisfied. Reflecting PortState, Behavior, TransformTarget, BranchPattern, FieldValue, and the payload records into src/v3/std/substrate.dag is a real improvement over Rust-only shadow structure, and the runtime/tests are now aligned to that surface. I do not see a new illegal-state regression in this round.

3. Facts flow forward. Blocking (still present from prior review). The reflected list-pattern lane still does not carry “how this realized sum decomposes” as one typed downstream fact. PatternRealization only declares a generic strategy plus raw carrier snippets (scrutinee, empty_pattern, cons_pattern, head_expr, tail_expr) in src/v3/spec/rust.dag:144-152,812-820. Then emit_rust has to recover the semantics by searching the Disj for variants literally labeled Empty and Cons, and by synthesizing head/tail through emitter-local field_overrides in src/v3/compiler/src/emit_rust.rs:1634-1688. That is still the thesis’s lossy-compression/reconstruction failure mode.

4. Coproduct dissolution. Aside from Codex’s already-made receipt point on the new rust.dag strategy coproducts, I do not see a second dissolution blocker to add here.

5. Single-authority metadata. Blocking. The unused-parameters migration still lacks an independent oracle. src/v3/compiler/src/lens_unused_parameters.rs delegates directly to the checked-in generated snapshot via include!("lens_unused_parameters_generated.rs") (src/v3/compiler/src/lens_unused_parameters.rs:5-12,34-45), and the migration test’s “Rust lens” side calls that wrapper (src/v3/compiler/tests/m2_lens_unused_parameters_migration_test.rs:160-186,233-240). So the parity proof is still checked-in-generated vs fresh-generated, not .dag vs an independent Rust implementation. Separately, the migrated .dag lens still reconstructs producer edges by scanning d.nodes (src/v3/lenses/unused_parameters.dag:111-115,170-181) even though DagPort.produced_by is now part of the reflected substrate authority (src/v3/std/substrate.dag:21-25). That means the new upstream fact exists, but this new consumer still doesn’t read it.

6. API-level enforcement. Mostly satisfied on the substrate surface itself: the new reflected types do move real invariants into the data model, and the tests make that concrete. My remaining concern is the same root issue as #3/#5: downstream consumers can still bypass the new typed facts and recover them by convention, so the API does not yet force use of the new authorities.

Design question. Is the new reflection surface meant to be the canonical semantic authority now, or merely a richer staging area beside the old reconstruction habits? That matters because, if it is canonical, consumers like emit_rust and the migrated lenses need to stop rediscovering list decomposition and producer edges by convention; otherwise the PR is adding a second nicer model without actually collapsing the old one.

Verdict. REQUEST_CHANGES. This round makes real progress — the reflected substrate surface is materially better, the field-binding coverage tests are good, and the emitted-lens freshness ratchet is worthwhile — but I still see two structural blockers: list-pattern decomposition is still reconstructed downstream, and the unused-parameters migration still lacks an independent oracle while ignoring the new produced_by authority.

LOOP HEALTH: stagnant — there is genuine consumer enablement here, but the same root class keeps surviving across layers: new upstream facts land, and downstream code still reconstructs them by convention instead of consuming them directly.


View conversation

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Director Review — PR #466 (Reflection + First Lens Migration)

Reviewed substrate.dag, rust.dag, lenses/unused_parameters.dag, all compiler source changes (dag.rs, infer.rs, lower.rs, emit_rust.rs, lens_unused_parameters.rs), generated code, and all test files.

What this PR delivers

This is real. The reflection stack works end-to-end:

  • substrate.dag faithfully reflects the Rust substrate (5 behaviors, 6 connectives, all payload types)
  • rust.dag covers 21+ type realizations with typed DeclarationId references
  • unused_parameters.dag compiles, matches the Rust lens on fixtures, self-analyzes to zero
  • Module-mode emission + crate-linked roundtrip execution is proven
  • Implicit generic instantiation (Prereq 0.5) via SubstStack is structurally correct
  • Zero layer-opacity violations in emit_rust.rs — all name-bridge elimination complete

Architectural findings

dag.rs: Clean. Substrate stable, no new behavior variants. Caches use typed DeclarationId/TypeShape handles. One-time name→id bootstrap bridge, everything downstream is typed.

infer.rs (+1570 lines): SubstStack for generic instantiation is balanced and scoped correctly. No heuristic name-matching — all dispatch is on structural TypeConnective variants. Fixpoint loop integrates pattern resolution and payload binding as steps, not post-passes.

lower.rs (+1370 lines): Two-phase model preserved. Record lowering, realization validation, DeclarationRef support all structurally grounded. No layer-opacity violations.

emit_rust.rs (+1700 lines): Excellent. Three typed realization indexes (Type, Operator, Behavior). Module-mode reads from rust.dag spec, not hardcoded rendering. Zero hardcoded type-name strings in non-comment positions.

unused_parameters.dag: Clean thesis-aligned implementation. Uses fold/concat/cons, all matches are exhaustive, termination via walk_steps with length(d.ports) fuel. No heuristics.

Issues to track

1. substrate.dag naming inconsistencies vs dag.rs:

  • ValueNode uses payload where dag.rs has data (rust.dag bridges this via DirectField accessor — intentional but confusing)
  • LoopBound in substrate.dag vs Bound in dag.rs
  • Path uses result_port where dag.rs has output (accessor wraps it)

These aren't blocking — the rust.dag FieldBinding entries bridge them — but they create naming confusion for anyone reading both files. Worth a cleanup pass.

2. Generated code clone density: 90 .clone() calls in 287 lines. Known debt, tracked in ownership-rendering-design.md (PR #471). Phase 1 of the ownership track will address this at L1.5.

3. Freshness gate exists: The generated_module_matches_checked_in_snapshot test compiles the .dag source and asserts byte-identical output. This is the right gate.

Test coverage assessment

Area Coverage Status
Reflected type declarations m2_substrate_inhabitance 13 record types, 11 sum types verified
Field bindings m2_field_access_binding 16 of 21 realized types checked
Lens compilation + parity m2_lens_unused_parameters 6 fixtures + self-analysis
Reflected API usage m1_substrate Dag walk tests Real recursive scenarios
Emit roundtrip m1_3_emit_rust User record list from reflected binds
Freshness gate m2_lens_unused_parameters Byte-identical snapshot check

Gap: SourceSpan missing from m2_substrate_inhabitance (tested in m2_field_access_binding instead — coverage exists but is split).

Verdict

Real progress. The reflection infrastructure works, the first lens migration is empirically grounded, and the test coverage is substantive. The remaining concerns (naming inconsistencies, clone density, pattern realization completeness) are tracked and don't block the architectural milestone.

@briansrls

Copy link
Copy Markdown
Contributor Author

Direction for merge-readiness — synthesized from all review rounds

Three reviewers (Codex, ChatGPT, Director) converged on the same diagnosis: the PR delivers real consumer enablement but has two structural debt patterns that will compound if they ship as-is. The meta-review called PAUSE_AND_REGROUP. I agree with the diagnosis but the regroup scope is narrow — two concrete fixes, not a rethink.

Fix 1 (BLOCKING): Expose produced_by in substrate.dag — eliminate producer reconstruction

The problem. unused_parameters.dag lines 111-177 implement find_producer and find_behavior — scanning d.nodes to find which behavior produced a given port. But Port in dag.rs already carries produced_by: Option<NodeId>. The lens is reconstructing a fact the substrate already has.

Why this blocks. Every future lens that needs producer information will copy this scan pattern. The meta-review identified this as the recurring theme across all 9 review rounds: "consumers reconstruct facts instead of reading carried substrate facts." If the first migrated lens ships with this pattern, it becomes the template.

The fix:

  1. Add produced_by: NodeId to DagPort in src/v3/std/substrate.dag (it's already on the Rust Port struct)
  2. Add a FieldBinding entry in rust.dag mapping produced_by to the Rust field
  3. Rewrite unused_parameters.dag: delete find_producer (lines 170-178), replace with direct port.produced_by reads
  4. The find_behavior helper (lines 160-168) may still be needed for node-id → behavior lookup, but the port → producer path should be a single field read, not a scan

Expected impact: ~20 lines deleted from the lens, one field added to substrate.dag, one FieldBinding in rust.dag. Small diff, large principle enforcement.

Fix 2 (BLOCKING): Restore independent oracle for lens parity test

The problem. The old handwritten Rust lens was the independent oracle. Now lens_unused_parameters.rs delegates to the generated shim, so render_rust_lens in the migration test calls the generated code. The parity test is generated-vs-generated from the same .dag source — it proves emission freshness, not migration correctness.

Why this blocks. The design doc's proof shape (substrate-reflection-design.md §12.6) requires parity against the hand-written Rust lens BEFORE deletion. If the first lens migration self-certifies, every future migration can share the same bug.

The fix:

  1. Preserve the original handwritten lens logic as a test-only function (e.g., fn handwritten_check(dag: &Dag) -> Vec<UnusedParameter> inside the test file, not in the library)
  2. The parity test compares: handwritten Rust lens vs compiled .dag lens on the same fixtures
  3. Once parity is confirmed and the test is green, the handwritten code is dead test infrastructure — it can be deleted in a follow-up PR with a note saying "parity confirmed at commit X"
  4. The freshness gate (generated snapshot matches fresh emission) is a SEPARATE test and stays as-is

Track (not blocking, but document): Pattern realization gap

The problem. emit_rust.rs reconstructs list pattern matching by searching for variants named Empty/Cons and building field_overrides for head/tail access (emit_rust.rs ~1183-1242, 1613-1709). This is a layer-opacity violation — the emitter reads below-boundary names.

Why it's not blocking this PR. The pattern realization design belongs in L2.5 (emit domain completeness). It requires the SharingModel / ownership design work tracked in docs/ownership-rendering-design.md and PR #471. Fixing it here would be scope creep.

What to do now: Add a comment in emit_rust.rs at the list-pattern reconstruction site:

// LAYER-OPACITY DEBT: This reconstructs list decomposition by searching for
// variants named "Empty"/"Cons" instead of reading a declared pattern
// realization from rust.dag. Tracked in ownership-rendering-design.md and
// SELF_HOSTING.md §2.3. Fix: PatternRealization in rust.dag declares the
// decomposition; emitter reads it. Blocked on L2.5 emit domain work.

Track (not blocking): Naming inconsistencies in substrate.dag

  • ValueNode.payload vs dag.rs ValueNode.data — rust.dag bridges this, but confusing
  • LoopBound vs dag.rs Bound
  • Path.result_port vs dag.rs Path.output

These are cosmetic but worth a cleanup pass. Not blocking because the FieldBinding entries handle the mapping correctly.

Track (not blocking): Parallel canonicalization stacks

lower.rs and infer.rs both implement their own substitution/canonicalization walks. ChatGPT flagged this as "canonical instantiated type identity is something each stage can re-implement." This is the right observation but the fix belongs in L2.5 infer domain modeling (single canonical identity authority), not in this PR.

Track (not blocking): rust.dag coproduct dissolution ledgers

Codex's recurring ask: new sum types in rust.dag (RustCallableStrategy, PatternRealization, etc.) need dissolution receipts. Add them as comments next to each sum type declaration.

Summary — what to do before next review round

# Action Lines affected Blocks merge?
1 Expose produced_by on DagPort in substrate.dag + rust.dag FieldBinding ~5 lines added YES
2 Rewrite lens producer lookups to use port.produced_by directly ~20 lines deleted YES
3 Preserve handwritten lens as test-only oracle, test parity against it ~50 lines moved to test file YES
4 Add layer-opacity debt comment on list pattern reconstruction 5-line comment No
5 Add dissolution receipts on new rust.dag coproducts Comments only No
6 Naming cleanup (payload→data, LoopBound→Bound) Optional, not blocking No

Items 1-3 are the regroup. Items 4-6 are documentation. Once 1-3 land, the PR addresses the meta-review's core concern ("consumers must read carried substrate facts instead of reconstructing them") and the migration proof is independent. The remaining tracked debt is bounded and named.

@briansrls

Copy link
Copy Markdown
Contributor Author

ChatGPT review in progress... (view conversation)

Check back in ~30 minutes for the full review.

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

codex · gpt-5.4 · 711b7bd4

⚠️ Review (blocking: 2, non-blocking: 1+/0-)

BLOCKING (2)

Root Cause

  • src/v3/std/list.dag The old alias-to-structural List promotion landed without carrying over the per-coproduct ledger -> add the adjacent terminal/scaffold/dissolve receipt before more recursive surfaces depend on this authority.
  • src/v3/spec/rust.dag The placeholder-language rename updated templates to {..} but left literal syntax on the old %Q token while string emission still bypasses template substitution -> store a real quote delimiter or route string literals through the same named-template renderer.

Non-blocking — Strengths

  • src/v3/compiler/src/lower.rs Seeding function signatures before body lowering gives recursive and higher-order call sites a single Arrow authority from the start instead of depending on later repair passes.

ROADMAP — Verified

  • Implicit generic instantiation: The pipeline now preserves callable template arguments through lowering, inference, and emission, so the generic std.list helpers can replace the old monomorphic wrappers end to end.
  • Structural std.list recursion: List now bootstraps as a structural sum and the new tests exercise match and recursive descent over it in ordinary user code instead of only through helper wrappers.

⚠️ This revision advances the reflected substrate substantially, but the new structural List authority still needs its receipt and rust.dag currently wires an invalid string delimiter into emitted Rust.

Comment thread src/v3/std/list.dag
// user code can match and recurse over `Empty | Cons`. The bootstrap
// loader prefers `src/v3/` declarations over `dsl/` declarations, so
// this staged definition is now the authority v3 walks to.
type List<element>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

BLOCKING: The new substrate-level List coproduct becomes the v3 authority here but lands without the required dissolution receipt, so its terminal-vs-scaffold status is still implicit (Coproduct Dissolution).

Comment thread src/v3/spec/rust.dag
for_each_syntax: { prefix: "for ", separator: " in " }
}

data rust_literals: LiteralSyntax = {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

BLOCKING: rust_literals still sets string_delimiter to %Q, but emit_rust::render_value prints LiteralSyntax.string_delimiter verbatim, so any emitted Rust string literal becomes invalid target syntax instead of a faithful Rust carrier fact (model fidelity / FAIL-CLOSED).

@briansrls

Copy link
Copy Markdown
Contributor Author

codex · gpt-5.4 · 711b7bd4

⚠️ Review (blocking: 2, non-blocking: 1+/0-)

BLOCKING (2)

Root Cause

  • src/v3/std/list.dag The old alias-to-structural List promotion landed without carrying over the per-coproduct ledger -> add the adjacent terminal/scaffold/dissolve receipt before more recursive surfaces depend on this authority.
  • src/v3/spec/rust.dag The placeholder-language rename updated templates to {..} but left literal syntax on the old %Q token while string emission still bypasses template substitution -> store a real quote delimiter or route string literals through the same named-template renderer.

Non-blocking — Strengths

  • src/v3/compiler/src/lower.rs Seeding function signatures before body lowering gives recursive and higher-order call sites a single Arrow authority from the start instead of depending on later repair passes.

ROADMAP — Verified

  • Implicit generic instantiation: The pipeline now preserves callable template arguments through lowering, inference, and emission, so the generic std.list helpers can replace the old monomorphic wrappers end to end.
  • Structural std.list recursion: List now bootstraps as a structural sum and the new tests exercise match and recursive descent over it in ordinary user code instead of only through helper wrappers.

⚠️ This revision advances the reflected substrate substantially, but the new structural List authority still needs its receipt and rust.dag currently wires an invalid string delimiter into emitted Rust.

@briansrls
briansrls merged commit be75d9e into main Apr 16, 2026
2 checks passed
briansrls added a commit that referenced this pull request Apr 16, 2026
L1 shipped with PR #466 (2026-04-16). All prereqs merged.
First lens migration (unused_parameters.dag) proven with
independent oracle + self-analysis. M2 partially complete
(generics, match, lists, numeric recursion all working).

L1.5 (clean bootstrap) is the next critical-path deliverable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
briansrls added a commit that referenced this pull request Apr 16, 2026
L1 shipped with PR #466 (2026-04-16). All prereqs merged.
First lens migration (unused_parameters.dag) proven with
independent oracle + self-analysis. M2 partially complete
(generics, match, lists, numeric recursion all working).

L1.5 (clean bootstrap) is the next critical-path deliverable.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
briansrls added a commit that referenced this pull request Apr 16, 2026
* docs: update roadmap — L1 reflection complete, L1.5 is NEXT

L1 shipped with PR #466 (2026-04-16). All prereqs merged.
First lens migration (unused_parameters.dag) proven with
independent oracle + self-analysis. M2 partially complete
(generics, match, lists, numeric recursion all working).

L1.5 (clean bootstrap) is the next critical-path deliverable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: L1.5 clean bootstrap design — all questions resolved

Dedicated design doc for the four L1.5 deliverables:
1. Pipeline composition declaration (pipeline.dag)
2. Per-stage fixed-point (serialized Dag snapshots)
3. Test authority types (std/verification.dag)
4. Ownership Phase 1 (consumer-count index + SharingModel)

Key design decisions:
- Dag is the universal intermediate; stages declare postconditions
- Lenses are orthogonal to stages; compiler composes on top
- Default lenses (complexity, ownership) are opt-out
- No explicit error gates — fail-closed ports handle partial failure
- Stage snapshots are deterministic serialized Dag bytes
- Regen is part of the pipeline, not a meta-pipeline
- TestPredicate is a 7-variant Disj of verifiable properties

All four deliverables are independent and can be worked in parallel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@briansrls
briansrls deleted the swift-ram-158 branch June 1, 2026 18:43
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