spec: 0104 topology-identity hash (boss-signed)
Boss-signed via grounding-check PASS (fresh-context agent): all five load-bearing assumptions about current behaviour are ratified by named currently-green tests — the byte-canonical golden (signal_serializes_to_canonical_golden), the --content-id CLI contract (graph_introspect_content_id_is_deterministic_and_distinguishes and the bad-document refusal), the three topology_hash roles (introspect content id, blueprint-store round-trip key, reproduce fetch anchor — each with its own green test), the I64:4-vs-5 distinguishing property, and the bound-array openness encoding (bound slot textually present, open slot absent). Design carried from the #171 reconciliation (adversarially reviewed): the identity hash is an additive SIBLING of topology_hash — introspection-only this cycle, no manifest field, no store key; the byte-canonical form and every consumer of it stay byte-for-byte untouched. Reviewer note carried to planning: allowing --content-id and --identity-id together relaxes the current exactly-one flag dispatch in graph_construct.rs — the usage tests must be adjusted deliberately, not incidentally. refs #180, refs #171
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
# The topology-identity hash — Design Spec
|
||||
|
||||
**Date:** 2026-07-02
|
||||
**Status:** Draft — awaiting sign-off (boss run: grounding-check gate)
|
||||
**Authors:** orchestrator + Claude
|
||||
**Cycle:** 0104 — milestone "Project environment — the project-as-crate
|
||||
authoring loop" (#180); work item #171 (fork resolutions: the cycle-0104
|
||||
reconciliation comment on #171)
|
||||
|
||||
## Goal
|
||||
|
||||
Two blueprints expressing the same topology get the same **identity hash**
|
||||
regardless of authoring path — an op-script-built graph and a Rust-builder
|
||||
graph, or two builders differing only in debug names, become comparable by
|
||||
id. The identity hash is **additive**: the byte-canonical `topology_hash`
|
||||
keeps all three of its current roles (introspect content id, reproduction
|
||||
store key, reproduce fetch anchor) byte-for-byte untouched.
|
||||
|
||||
## Architecture
|
||||
|
||||
One new engine-side projection and one additive CLI flag:
|
||||
|
||||
- **`blueprint_identity_json` (aura-engine, `blueprint_serde.rs`)** builds
|
||||
the same serialization document as `blueprint_to_json`, then normalizes
|
||||
every non-load-bearing debug symbol before serializing: the composite
|
||||
debug-name, primitive instance names, bound-param names, role names, and
|
||||
output re-export names are all replaced by `""`. Everything load-bearing
|
||||
survives untouched: node **type ids**, node **order**, **edges** (raw
|
||||
indices), role **targets and order**, output **(node, field) pairs and
|
||||
order**, and the **`bound` arrays' positions, kinds, and values** — so
|
||||
param **openness** stays identity-bearing (a bound slot carries
|
||||
`pos`+`value`, an open slot is textually absent) and an open blueprint
|
||||
never shares an id with any bound instance.
|
||||
- **`aura graph introspect --identity-id` (aura-cli)** — a sibling of the
|
||||
existing `--content-id`: hashes the identity JSON through the same
|
||||
research-side SHA-256 helper `content_id` already uses (sha2 stays out of
|
||||
the engine, invariant 8). `--content-id` output is byte-unchanged.
|
||||
|
||||
Decisions carried from the #171 reconciliation comment (adversarially
|
||||
reviewed): the initial "redefine `topology_hash`" shape was **refuted** —
|
||||
`topology_hash` keys the byte-exact reproduction store and `reproduce`
|
||||
recovers bootstrap points **by param-path name** against the fetched
|
||||
blueprint, so instance names are load-bearing on that path and a name-blind
|
||||
store key would let cross-path name-twins clobber it. Hence: sibling id,
|
||||
zero golden churn, reproduction provably unaffected. No manifest field and
|
||||
no store keyed by the identity hash this cycle (no dedup consumer exists —
|
||||
the deferred-until-a-consumer discipline).
|
||||
|
||||
## Concrete code shapes
|
||||
|
||||
### The user-facing comparison (the acceptance-criterion evidence)
|
||||
|
||||
```console
|
||||
# two authoring paths, one topology: the op-script (composite named "graph")
|
||||
# and the Rust builder (named "sma_signal") — whatever input form
|
||||
# `--content-id` consumes today, `--identity-id` consumes identically
|
||||
$ aura graph introspect --content-id <op-script path's input>
|
||||
9f3c…e1 # byte id, path A
|
||||
$ aura graph introspect --identity-id <op-script path's input>
|
||||
5b77…a2 # topology id, path A
|
||||
$ aura graph introspect --identity-id <rust-builder path's input>
|
||||
5b77…a2 # SAME topology id
|
||||
$ aura graph introspect --content-id <rust-builder path's input>
|
||||
41d0…07 # byte ids still differ
|
||||
```
|
||||
|
||||
(The hex values are illustrative; the input form mirrors `--content-id`'s
|
||||
existing contract exactly.)
|
||||
|
||||
### Engine: the identity projection (secondary, before → after shape)
|
||||
|
||||
`blueprint_to_json` today walks the composite into a `BlueprintDoc` DTO and
|
||||
serializes it. The identity projection reuses that walk and normalizes the
|
||||
DTO in place:
|
||||
|
||||
```rust
|
||||
/// Identity-canonical form: the byte-canonical document with every
|
||||
/// non-load-bearing debug symbol blanked (invariant 11 / C23). Feeds the
|
||||
/// research-side identity hash; NOT a load path and NOT the reproduction
|
||||
/// store's byte form.
|
||||
pub fn blueprint_identity_json(bp: &Composite) -> String {
|
||||
let mut doc = build_doc(bp); // the existing blueprint_to_json walk
|
||||
strip_debug_symbols(&mut doc.blueprint);
|
||||
serialize_doc(&doc) // the existing serializer
|
||||
}
|
||||
|
||||
fn strip_debug_symbols(b: &mut BlueprintData) {
|
||||
b.name = String::new();
|
||||
for node in &mut b.nodes {
|
||||
match node {
|
||||
NodeData::Primitive(p) => {
|
||||
p.name = None;
|
||||
for bound in &mut p.bound {
|
||||
bound.name = String::new(); // pos/kind/value survive
|
||||
}
|
||||
}
|
||||
NodeData::Composite(c) => strip_debug_symbols(c),
|
||||
}
|
||||
}
|
||||
for role in &mut b.input_roles {
|
||||
role.name = String::new(); // targets + order survive
|
||||
}
|
||||
for out in &mut b.output {
|
||||
out.name = String::new(); // (node, field) + order survive
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
(Field/type names above follow the existing `blueprint_serde.rs` DTOs; the
|
||||
planner pins exact identifiers. If `build_doc`/`serialize_doc` are not yet
|
||||
factored out of `blueprint_to_json`, factoring them is part of this cycle —
|
||||
behaviour-preserving for the byte path, guarded by the existing canonical
|
||||
golden.)
|
||||
|
||||
### CLI: the sibling flag (secondary)
|
||||
|
||||
`GraphIntrospectCmd` gains one flag beside `--content-id`:
|
||||
|
||||
```rust
|
||||
/// Print the topology-identity id (debug names stripped) and exit.
|
||||
#[arg(long)]
|
||||
identity_id: bool,
|
||||
```
|
||||
|
||||
Its branch mirrors the `--content-id` branch byte-for-byte, substituting
|
||||
`blueprint_identity_json` for the canonical bytes before the same
|
||||
`content_id` SHA-256 helper.
|
||||
|
||||
## Components
|
||||
|
||||
| Component | Crate | New/changed |
|
||||
|---|---|---|
|
||||
| `blueprint_identity_json` + `strip_debug_symbols` (+ doc-walk factoring if needed) | aura-engine | new fn(s) |
|
||||
| `--identity-id` flag + branch | aura-cli (`graph_construct.rs`) | additive |
|
||||
| Property tests (engine) + e2e (CLI) | both | new tests |
|
||||
|
||||
Untouched by design: `topology_hash` stamping, `put_blueprint`/
|
||||
`get_blueprint`, `aura reproduce`, `blueprint_to_json` bytes, every golden.
|
||||
|
||||
## Data flow
|
||||
|
||||
```
|
||||
Composite ──build_doc──► BlueprintDoc ──serialize──► byte-canonical JSON ──► content_id / topology_hash / store
|
||||
└─clone──► strip_debug_symbols ──serialize──► identity JSON ──► identity id (introspect only)
|
||||
```
|
||||
|
||||
## Error handling
|
||||
|
||||
No new failure modes: `--identity-id` shares `--content-id`'s input path
|
||||
(stdin/file op-list or blueprint doc) and its existing refusal contract;
|
||||
combining `--content-id --identity-id` prints both, one per line, in that
|
||||
order (flags are not mutually exclusive).
|
||||
|
||||
## Testing strategy
|
||||
|
||||
1. **Engine property tests (`blueprint_serde.rs`):**
|
||||
- renamed twins: two builders identical up to `.named(...)` and composite
|
||||
name → equal identity JSON, unequal canonical JSON;
|
||||
- openness: a blueprint with `scale` bound vs unbound → unequal identity
|
||||
JSON (boundness survives);
|
||||
- bound values: I64:4 vs I64:5 → unequal identity JSON (the existing
|
||||
distinguishing property, now on the identity form);
|
||||
- wiring: swapping two edges' targets → unequal identity JSON (order and
|
||||
indices load-bearing);
|
||||
- the existing byte-canonical golden stays byte-identical (the factoring
|
||||
is behaviour-preserving).
|
||||
2. **CLI e2e (`graph_construct.rs` tests):** an op-script-built doc and the
|
||||
Rust-built `sma_signal` fixture expressing the same topology → same
|
||||
`--identity-id` output, different `--content-id` output; `--content-id`
|
||||
alone byte-unchanged against its existing pin.
|
||||
3. **Regression:** full workspace suite green unchanged (reproduce,
|
||||
goldens, store untouched).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. The op-script and Rust-builder twins share an identity id while their
|
||||
content ids differ (the #171 headline, proven e2e).
|
||||
2. An open blueprint and any bound instance of it never share an identity
|
||||
id; bound-value twins differing in one value never share one.
|
||||
3. `--content-id`, `topology_hash`, the blueprint store, and `aura
|
||||
reproduce` are byte-for-byte unaffected (existing tests + goldens green
|
||||
unchanged).
|
||||
4. The identity projection is documented as introspection-only this cycle
|
||||
(no manifest field, no store key) — the deferral recorded on #171.
|
||||
Reference in New Issue
Block a user