Files
Aura/docs/plans/0037-bound-param-in-graph-model.md
T
Brummel 4227073fce plan: 0037 bound-param-in-graph-model
Six bite-sized tasks for spec 0037: aura-core records a BoundParam (original slot
position) in bind + a bound_params() accessor; aura-engine emits a conditional
"bound" field via scalar_str; graph-viewer.js merges free+bound params into slot
order and renders the bound slot dimmed as name=value; re-capture the sample-model
fixture; a new headless render guard (trailing + middle bind); full-workspace gate.

refs #63
2026-06-13 23:14:49 +02:00

24 KiB
Raw Blame History

Bound params in the graph model + viewer signature — Implementation Plan

Parent spec: docs/specs/0037-bound-param-in-graph-model.md

For agentic workers: REQUIRED SUB-SKILL: use the implement skill to run this plan. Steps use - [ ] checkboxes for tracking.

Goal: Surface a bind-bound param in the graph model and the aura graph viewer signature — all slots shown, the bound one as name=value, dimmed — while leaving the tunable surface (param_space / sweep axes) untouched.

Architecture: A conditional field threaded engine → model → viewer, the twin of cycle 0035's instance-name thread. PrimitiveBuilder::bind records a BoundParam (original slot position + name + kind + value); prim_record emits a conditional "bound" field via a new scalar_str helper; graph-viewer.js merges free + bound params into slot order by position and renders the bound slot dimmed. Render/debug surface only — dropped at lowering (C23), model stays deterministic (C14). Purely additive: a new struct, a Vec::new()-default field, a new accessor, a conditional model field defaulting to empty, a viewer field defaulting to []. No call site of bind/prim_record/adaptNodes/genDot/ cellLabel changes arity.

Tech Stack: crates/aura-core/src/node.rs (Rust), crates/aura-engine/src/ graph_model.rs (Rust), crates/aura-cli/assets/graph-viewer.js (JS), crates/aura-cli/tests/ (Rust + headless node guards), the crates/aura-cli/tests/fixtures/sample-model.json golden.


Files this plan creates or modifies:

  • Modify: crates/aura-core/src/node.rsBoundParam struct (after ParamSpec, ~:67), PrimitiveBuilder.bound field (struct :77-85), init in new (:96), record in bind (between the kind-assert :172 and remove(pos) :173), bound_params() accessor (after instance_name() :120-122), unit test (tests mod :229+, beside bind_removes_slot_and_reconstructs_positionally :386).
  • Modify: crates/aura-engine/src/graph_model.rsscalar_str helper (beside kind_str :14-21), conditional "bound" field in prim_record (:72-89; field block after the name conditional :80-83, {bound} in the format string :85), unit test (tests mod, beside prim_record_emits_name_for_explicit_only :383).
  • Modify: crates/aura-cli/assets/graph-viewer.jsadaptNodes (:32), genDot leaf-emit cellLabel call (:111), cellLabel sp/sig (:72-73).
  • Modify: crates/aura-cli/tests/fixtures/sample-model.json — re-capture; the signals composite's node "2" (blend) gains "bound":[[2,"weights[2]","f64","0.5"]].
  • Create: crates/aura-cli/tests/viewer_bound_param.mjs — headless render guard (trailing + middle bind).
  • Create: crates/aura-cli/tests/viewer_bound_param.rs — Rust shell that runs the .mjs via node. Mirror of viewer_name_prefix.rs.

Note on verification commands (project memory): use ONE positional cargo test filter per invocation (a multi-filter cargo test X Y silently prints nothing here); for "everything still green" use an unfiltered per-crate run and read the summary count. The viewer guards REQUIRE node on PATH (/usr/bin/node, v26 confirmed) — a skipped guard is not a guard.

Parse-the-bytes gate: the project declares no spec_validation parser (see CLAUDE.md project facts), so the inline-body parse gate is a documented no-op; the Rust bodies are caught by the implement compile gate and the JS by the node guard run.


Task 1: aura-core — BoundParam, bind records original position, bound_params()

Files:

  • Modify: crates/aura-core/src/node.rs

  • Test: crates/aura-core/src/node.rs (tests mod)

  • Step 1: Write the failing test

In crates/aura-core/src/node.rs, inside #[cfg(test)] mod tests (after bind_removes_slot_and_reconstructs_positionally, ~:408), add:

    #[test]
    fn bind_records_bound_param_at_original_position() {
        // middle-of-three: binding `b` records its ORIGINAL slot position (1).
        let mid = probe3().bind("b", Scalar::I64(8));
        assert_eq!(
            mid.bound_params(),
            [BoundParam { pos: 1, name: "b".into(), kind: ScalarKind::I64, value: Scalar::I64(8) }]
                .as_slice(),
        );

        // chained reverse-order bind (b then a): each entry carries its ORIGINAL
        // position regardless of the shrinking array — positions {1, 0} in call order.
        let pair = probe3().bind("b", Scalar::I64(8)).bind("a", Scalar::I64(7));
        assert_eq!(
            pair.bound_params(),
            [
                BoundParam { pos: 1, name: "b".into(), kind: ScalarKind::I64, value: Scalar::I64(8) },
                BoundParam { pos: 0, name: "a".into(), kind: ScalarKind::I64, value: Scalar::I64(7) },
            ]
            .as_slice(),
        );

        // an unbound builder records nothing.
        assert!(probe3().bound_params().is_empty());
    }
  • Step 2: Run test to verify it fails

Run: cargo test -p aura-core bind_records_bound_param_at_original_position Expected: FAIL — does not compile: cannot find type BoundParam in this scope / no method named bound_params found for struct PrimitiveBuilder.

  • Step 3: Write the implementation

(3a) Add the BoundParam struct immediately after ParamSpec (after its closing } at ~:67, before the PrimitiveBuilder doc-comment at :69). Scalar / ScalarKind are already imported at node.rs:13 — no new import:

/// One param bound to a structural constant by [`PrimitiveBuilder::bind`] — a
/// render/debug surface only (C23), dropped at lowering. `pos` is the slot's
/// position in the node's ORIGINAL (pre-bind) param list, so the model serializer
/// and viewer can place it back into slot order regardless of bind sequence.
#[derive(Clone, Debug, PartialEq)]
pub struct BoundParam {
    pub pos: usize,
    pub name: String,
    pub kind: ScalarKind,
    pub value: Scalar,
}

(3b) Add the bound field to PrimitiveBuilder (struct at :77-85). Replace:

pub struct PrimitiveBuilder {
    name: &'static str,
    instance_name: Option<String>,
    schema: NodeSchema,
    // The build closure's type is exactly the recipe contract (a param slice in, a
    // boxed node out); a type alias would not clarify it.
    #[allow(clippy::type_complexity)]
    build: Box<dyn Fn(&[Scalar]) -> Box<dyn Node>>,
}

with:

pub struct PrimitiveBuilder {
    name: &'static str,
    instance_name: Option<String>,
    schema: NodeSchema,
    bound: Vec<BoundParam>,
    // The build closure's type is exactly the recipe contract (a param slice in, a
    // boxed node out); a type alias would not clarify it.
    #[allow(clippy::type_complexity)]
    build: Box<dyn Fn(&[Scalar]) -> Box<dyn Node>>,
}

(3c) Initialize bound in new (:96). Replace:

        Self { name, instance_name: None, schema, build: Box::new(build) }

with:

        Self { name, instance_name: None, schema, bound: Vec::new(), build: Box::new(build) }

(3d) Add the bound_params() accessor immediately after instance_name() (after its closing } at :122):

    /// The params bound to structural constants by [`bind`](Self::bind), in
    /// bind-call order, each carrying its ORIGINAL slot position. The render-
    /// surface twin of [`instance_name`](Self::instance_name): the model
    /// serializer reads it to show a bound slot's fixed value (the value captured
    /// in the build closure is otherwise unreachable to it). Empty when no binds.
    pub fn bound_params(&self) -> &[BoundParam] {
        &self.bound
    }

(3e) In bind, record the BoundParam at its original position. Insert between the kind-assert (ends :172) and self.schema.params.remove(pos); (:173):

        // [render side] record the bound slot for the model/viewer at its ORIGINAL
        // pre-bind position. `pos` indexes the already-shrunk array (earlier binds
        // removed), so map it back to the full-arity index by adding one for each
        // earlier-bound slot at an original position <= it. Render/debug only (C23);
        // the closure capture below is what bootstrap actually reads.
        let name = self.schema.params[pos].name.clone();
        let kind = self.schema.params[pos].kind;
        let mut orig = pos;
        let mut prior: Vec<usize> = self.bound.iter().map(|b| b.pos).collect();
        prior.sort_unstable();
        for p in prior {
            if p <= orig {
                orig += 1;
            }
        }
        self.bound.push(BoundParam { pos: orig, name, kind, value });
  • Step 4: Run test to verify it passes

Run: cargo test -p aura-core bind_records_bound_param_at_original_position Expected: PASS (test ... ok, 1 passed).

  • Step 5: Verify aura-core suite stays green

Run: cargo test -p aura-core Expected: PASS — all tests ok, 0 failed (existing bind_* tests included).


Task 2: aura-engine — scalar_str + conditional "bound" in prim_record

Files:

  • Modify: crates/aura-engine/src/graph_model.rs

  • Test: crates/aura-engine/src/graph_model.rs (tests mod)

  • Step 1: Write the failing test

In crates/aura-engine/src/graph_model.rs, inside #[cfg(test)] mod tests (after prim_record_emits_name_for_explicit_only, ~:394), add. The tests mod already imports Scalar, PrimitiveBuilder (:244-245) and aura_std::{Exposure, …, Sma} (:247):

    #[test]
    fn prim_record_emits_bound_only_when_bound() {
        // a bound i64 param → a "bound" field carrying [pos,"name","kind","value"]
        let bound = prim_record(&Sma::builder().bind("length", Scalar::I64(5)));
        assert!(bound.contains(r#""bound":[[0,"length","i64","5"]]"#), "{bound}");
        // the bound slot also leaves the tunable "params" surface (bind's tuning side)
        assert!(bound.contains(r#""params":[]"#), "{bound}");

        // a bound f64 value renders canonically (shortest round-trip), e.g. 0.5
        let f = prim_record(&Exposure::builder().bind("scale", Scalar::F64(0.5)));
        assert!(f.contains(r#""bound":[[0,"scale","f64","0.5"]]"#), "{f}");

        // an unbound builder → no "bound" field at all
        let unbound = prim_record(&Sma::builder());
        assert!(!unbound.contains(r#""bound""#), "{unbound}");
    }
  • Step 2: Run test to verify it fails

Run: cargo test -p aura-engine prim_record_emits_bound_only_when_bound Expected: FAIL — does not compile: no method named bound_params (until Task 1 lands) OR, with Task 1 present, assertion failure ("bound" substring absent).

  • Step 3: Write the implementation

(3a) Add the scalar_str helper immediately after kind_str (after its closing } at :21):

/// A scalar VALUE as a canonical, deterministic string for the model (C14). `f64`
/// uses the shortest round-trippable debug form, which keeps a decimal point on
/// whole values (`1.0`, not `1`) so a bound f64 is never mistaken for an i64 in
/// the render. The value side of `kind_str`.
fn scalar_str(s: aura_core::Scalar) -> String {
    use aura_core::Scalar;
    match s {
        Scalar::I64(n) => n.to_string(),
        Scalar::F64(f) => format!("{f:?}"),
        Scalar::Bool(b) => b.to_string(),
        Scalar::Ts(t) => t.0.to_string(),
    }
}

(3b) In prim_record (:72-89), add the bound field block after the name conditional (after its closing line :83, before the format! at :84):

    // bound params: present only when the node has binds (mirrors the conditional
    // "name" field). each entry: [pos,"name","kind","value"].
    let bound = if b.bound_params().is_empty() {
        String::new()
    } else {
        let items = join(b.bound_params().iter().map(|bp| {
            format!(
                "[{},{},{},{}]",
                bp.pos,
                json_str(&bp.name),
                json_str(kind_str(bp.kind)),
                json_str(&scalar_str(bp.value)),
            )
        }));
        format!(r#","bound":[{items}]"#)
    };

(3c) Insert {bound} into the format! template (:85), after "params":[{params}] and before ,"ins". Replace:

        r#"{{"prim":{{{name}"type":{},"role":{},"params":[{params}],"ins":[{ins}],"outs":[{outs}]}}}}"#,

with:

        r#"{{"prim":{{{name}"type":{},"role":{},"params":[{params}]{bound},"ins":[{ins}],"outs":[{outs}]}}}}"#,
  • Step 4: Run test to verify it passes

Run: cargo test -p aura-engine prim_record_emits_bound_only_when_bound Expected: PASS (1 passed).

  • Step 5: Verify the no-bind golden stays byte-stable

Run: cargo test -p aura-engine model_golden Expected: PASS — the inline model_golden (sample_root, no binds) is unchanged: the conditional field is inert for unbound nodes.

  • Step 6: Verify aura-engine suite stays green

Run: cargo test -p aura-engine Expected: PASS — all tests ok, 0 failed (model_is_deterministic included).


Task 3: graph-viewer.js — carry bound, render the merged signature

Files:

  • Modify: crates/aura-cli/assets/graph-viewer.js

  • Step 1: adaptNodes carries bound

In crates/aura-cli/assets/graph-viewer.js (:32), replace:

      out[key] = { prim: { name: p.name, type: p.type, role: p.role, params: p.params, ins: adaptIns(p.ins), outs: p.outs } };

with:

      out[key] = { prim: { name: p.name, type: p.type, role: p.role, params: p.params, bound: p.bound || [], ins: adaptIns(p.ins), outs: p.outs } };
  • Step 2: genDot leaf-emit forwards bound

In genDot (:111), replace:

        block += `${cid} [label=${cellLabel(cid, { name: s.name, type: s.type, params: s.params, ins: s.ins, outs: s.outs, bodyBg: bodyBgFor(s.role), composite: false }, showTypes)}];\n`;

with:

        block += `${cid} [label=${cellLabel(cid, { name: s.name, type: s.type, params: s.params, bound: s.bound, ins: s.ins, outs: s.outs, bodyBg: bodyBgFor(s.role), composite: false }, showTypes)}];\n`;
  • Step 3: cellLabel merges free + bound params into slot order

In cellLabel (:72-73), replace:

  const sp = o.params.map(([n, k]) => `<font color="${col(k)}">${showTypes && k ? `${n}:${k}` : n}</font>`);
  const sig = o.params.length ? `<font color="#9399b2">[</font>${sp.join('<font color="#9399b2">, </font>')}<font color="#9399b2">]</font>` : "";

with:

  // merge free params and bound params back into slot order by original position;
  // a bound slot renders as a dimmed name=value (reuses the 0035 separator grey).
  const bnd = o.bound || [];
  const total = o.params.length + bnd.length;
  const byPos = new Map(bnd.map(([pos, n, k, v]) => [pos, [n, k, v]]));
  const slots = [];
  let fi = 0;
  for (let pos = 0; pos < total; pos++) {
    if (byPos.has(pos)) {
      const [n, , v] = byPos.get(pos);
      slots.push(`<font color="#9399b2">${n}=${v}</font>`);
    } else {
      const [n, k] = o.params[fi++];
      slots.push(`<font color="${col(k)}">${showTypes && k ? `${n}:${k}` : n}</font>`);
    }
  }
  const sig = total ? `<font color="#9399b2">[</font>${slots.join('<font color="#9399b2">, </font>')}<font color="#9399b2">]</font>` : "";

(o.bound || [] keeps the composite / boundary cellLabel calls — which pass no bound — rendering exactly as before: total collapses to params.length.)

  • Step 4: Verify the existing viewer guards stay green

The existing guards exercise genDot/cellLabel over models with no bound field; the merge must render them identically. They read the OLD fixture (Task 4 re-captures it) — both states are backward-compatible.

Run: cargo test -p aura-cli --test viewer_name_prefix Expected: PASS.

Run: cargo test -p aura-cli --test viewer_dot Expected: PASS.

Run: cargo test -p aura-cli --test viewer_nested_depth Expected: PASS.


Task 4: Re-capture the sample-model.json fixture

Files:

  • Modify: crates/aura-cli/tests/fixtures/sample-model.json

  • Modify (temporary): crates/aura-cli/src/main.rs (a throwaway capture test, removed in Step 4)

  • Step 1: Add a temporary capture test

In crates/aura-cli/src/main.rs, inside #[cfg(test)] mod tests (:560, use super::* already present), add:

    #[test]
    fn capture_sample_model_tmp() {
        println!("AURA_MODEL_CAPTURE:{}", aura_engine::model_to_json(&sample_blueprint()));
    }
  • Step 2: Capture the model into the fixture

Run (one positional filter, per project note):

cargo test -p aura-cli capture_sample_model_tmp -- --nocapture 2>/dev/null \
  | grep '^AURA_MODEL_CAPTURE:' | sed 's/^AURA_MODEL_CAPTURE://' \
  > crates/aura-cli/tests/fixtures/sample-model.json

Expected: the fixture is rewritten as a single JSON line. Verify the blend node gained the bound field:

grep -c '"bound":\[\[2,"weights\[2\]","f64","0.5"\]\]' crates/aura-cli/tests/fixtures/sample-model.json

Expected: 1.

  • Step 3: Validate the fixture is well-formed JSON

Run:

node -e "JSON.parse(require('fs').readFileSync('crates/aura-cli/tests/fixtures/sample-model.json','utf8')); console.log('OK valid JSON')"

Expected: OK valid JSON.

  • Step 4: Remove the temporary capture test

Delete the capture_sample_model_tmp test added in Step 1 from crates/aura-cli/src/main.rs.

  • Step 5: Verify the fixture consumers stay green

The two consumers assert DOT-id grammar / nesting depth, not params, and read the re-captured fixture (now carrying bound, handled by Task 3's p.bound || []).

Run: cargo test -p aura-cli --test viewer_dot Expected: PASS.

Run: cargo test -p aura-cli --test viewer_nested_depth Expected: PASS.


Task 5: New headless render guard — bound slot in true slot order

Files:

  • Create: crates/aura-cli/tests/viewer_bound_param.mjs

  • Create: crates/aura-cli/tests/viewer_bound_param.rs

  • Step 1: Write the headless guard script

Create crates/aura-cli/tests/viewer_bound_param.mjs:

// Headless render guard for the `aura graph` viewer (graph-viewer.js).
//
// Property protected: a bind-bound param slot renders in the node signature as a
// DIMMED `name=value`, in TRUE slot order (by original position), with the free
// slots in their type colour — never dropped. Covers a trailing bind (the
// cycle-0036 showcase shape) AND a non-trailing (middle) bind, which an
// append-after-free design would render in the wrong order.
//
// It loads the real viewer module and drives the exported pure `genDot`. `node`
// is REQUIRED: a skipped guard is not a guard.

import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

const require = createRequire(import.meta.url);
const here = dirname(fileURLToPath(import.meta.url));

// Load the viewer once with an empty model; normalizeModel/genDot are pure and
// take the model/def explicitly, so each case is rendered by passing its own.
global.window = { AURA_MODEL: { root: { nodes: {}, edges: [] }, composites: {} } };
const { normalizeModel, genDot } = require(join(here, "..", "assets", "graph-viewer.js"));
const dotFor = (model) => genDot(normalizeModel(model).root, "root", new Set(), false).dot;

const fail = (msg, dot) => {
  console.error(msg + "\n--- dot ---\n" + dot);
  process.exit(1);
};

// --- Model A: trailing bind (the cycle-0036 showcase shape) ---
const trailing = {
  root: { nodes: { "0": { prim: {
    name: "blend", type: "LinComb", role: "node",
    params: [["weights[0]", "f64"], ["weights[1]", "f64"]],
    bound: [[2, "weights[2]", "f64", "0.5"]],
    ins: [["f64", "any", "term[0]"], ["f64", "any", "term[1]"], ["f64", "any", "term[2]"]],
    outs: [["value", "f64"]],
  } } }, edges: [] },
  composites: {},
};
const dotA = dotFor(trailing);

const BOUND = '<font color="#9399b2">weights[2]=0.5</font>';
const FREE0 = '<font color="#89b4fa">weights[0]</font>';
const FREE1 = '<font color="#89b4fa">weights[1]</font>';
if (!dotA.includes(BOUND)) fail("trailing: bound slot weights[2]=0.5 not rendered dimmed", dotA);
if (!dotA.includes(FREE0) || !dotA.includes(FREE1)) fail("trailing: a free weight slot is missing or not type-coloured", dotA);
const a0 = dotA.indexOf(FREE0), a1 = dotA.indexOf(FREE1), ab = dotA.indexOf(BOUND);
if (!(a0 < a1 && a1 < ab)) fail("trailing: slot order is not weights[0] < weights[1] < weights[2]=0.5", dotA);

// --- Model B: NON-TRAILING (middle) bind — pins slot-order faithfulness ---
const middle = {
  root: { nodes: { "0": { prim: {
    name: "mid", type: "Probe", role: "node",
    params: [["a", "f64"], ["c", "f64"]],
    bound: [[1, "b", "f64", "9.0"]],
    ins: [["f64", "any", "t0"], ["f64", "any", "t1"], ["f64", "any", "t2"]],
    outs: [["value", "f64"]],
  } } }, edges: [] },
  composites: {},
};
const dotB = dotFor(middle);

const MA = '<font color="#89b4fa">a</font>';
const MB = '<font color="#9399b2">b=9.0</font>';
const MC = '<font color="#89b4fa">c</font>';
if (!dotB.includes(MA) || !dotB.includes(MB) || !dotB.includes(MC)) fail("middle: a free or bound slot is missing", dotB);
const ba = dotB.indexOf(MA), bb = dotB.indexOf(MB), bc = dotB.indexOf(MC);
if (!(ba < bb && bb < bc)) fail("middle: bound slot b=9.0 not placed between free a and c (append-only bug)", dotB);

console.log("OK — bound slot renders dimmed name=value in true slot order (trailing + middle bind).");
process.exit(0);
  • Step 2: Write the Rust shell

Create crates/aura-cli/tests/viewer_bound_param.rs:

//! Integration guard: the `aura graph` viewer renders a bind-bound param slot as
//! a dimmed `name=value` in true slot order (trailing + non-trailing bind).
//!
//! The property lives in JavaScript (`cellLabel` in assets/graph-viewer.js), so
//! this shells out to `node` running the headless guard
//! `tests/viewer_bound_param.mjs`, which loads the real viewer module and drives
//! the exported `genDot` over two minimal models.
//!
//! `node` is REQUIRED: if it is not on PATH this test FAILS (a skipped guard is
//! not a guard).

use std::path::PathBuf;
use std::process::Command;

#[test]
fn viewer_renders_bound_param_in_slot_order() {
    let script = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("viewer_bound_param.mjs");
    assert!(script.exists(), "guard script missing at {}", script.display());

    let out = match Command::new("node").arg(&script).output() {
        Ok(out) => out,
        Err(e) => panic!(
            "node is required for the viewer bound-param guard but could not be run \
             ({e}); install Node.js or ensure `node` is on PATH"
        ),
    };

    let stdout = String::from_utf8_lossy(&out.stdout);
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(
        out.status.success(),
        "viewer bound-param guard failed (exit {:?}).\n--- node stdout ---\n{stdout}\n--- node stderr ---\n{stderr}",
        out.status.code()
    );
}
  • Step 3: Run the new guard to verify it passes

Run: cargo test -p aura-cli --test viewer_bound_param Expected: PASS — viewer_renders_bound_param_in_slot_order ... ok; the .mjs prints OK — bound slot renders dimmed name=value in true slot order ….


Task 6: Full-workspace gate

Files: none (verification only)

  • Step 1: Build

Run: cargo build --workspace Expected: finishes, 0 errors.

  • Step 2: Test

Run: cargo test --workspace Expected: all tests pass, 0 failed (the new aura-core + aura-engine unit tests, the new viewer_bound_param guard, and every existing test — the sweep param pins at main.rs:595-598 and cli_run.rs:214 stay byte-identical, since the tunable surface is unchanged).

  • Step 3: Lint

Run: cargo clippy --workspace --all-targets -- -D warnings Expected: finishes, 0 warnings.


Spec coverage map

Spec section Task
BoundParam + bind records original position + bound_params() Task 1
scalar_str + conditional "bound" in prim_record Task 2
adaptNodes/genDot/cellLabel merge + dimmed render Task 3
sample-model.json re-capture (blend gains "bound") Task 4
Headless render guard (trailing + middle bind, slot order) Task 5
Goldens / sweep pins stay green; build/test/clippy clean Tasks 2, 4, 6
Acceptance criteria #1#4 Tasks 16 (render: 3+5; model: 2+4; tunable surface untouched: 6; tests: 1,2,5)