audit: cycle 0106 tidy — ledger/glossary lift, unwired-hint fix, spec/plan retired
Architect drift review (holds: C14 headless authoring, C16/C17 no-DSL with aura-research a true leaf, content-id move behaviour-preserving per the untouched id goldens). Resolution per item: - fix (docs): C18 gains the cycle-0106 realization note (processes/ + campaigns/ stores, document canonical form, the content-id primitive's library home, the referential tier, no-executor status); the #109 analysis-meta-level open thread records the artifact half shipped v1 and what stays open (executor question, verb dissolution 'once it carries', the remaining #188 amendment package). Glossary: new campaign document + process document entries; content id generalized to all three artifact classes; experiment marked superseded by the campaign document (#188 re-cut of the builder-API thread). - fix (code): the --unwired guide advertised an identity_id process ref that validate_campaign refuses — the process.ref open-slot hint now says content-id-only (strategy refs keep both); lib + seam pins updated. - filed forward: #190 (metric-vocabulary single-sourcing, the 0105 roster drift class), #191 (identity-ref store scan is O(store), performance only). - carry-on: regression gates green by hand — cargo test --workspace 916/0, clippy -D warnings clean, cargo doc --no-deps 0 warnings; no baseline to ratify (project has no metric baseline scripts). Cycle 0106 spec and plan retired per convention (git rm); durable record is the ledger notes above plus #188/#189. refs #189
This commit is contained in:
@@ -341,7 +341,7 @@ fn campaign_introspect_unwired_reports_the_spec_example_slots() {
|
|||||||
write_doc(&dir, "draft.campaign.json", draft);
|
write_doc(&dir, "draft.campaign.json", draft);
|
||||||
let (out, code) = run_code_in(&dir, &["campaign", "introspect", "--unwired", "draft.campaign.json"]);
|
let (out, code) = run_code_in(&dir, &["campaign", "introspect", "--unwired", "draft.campaign.json"]);
|
||||||
assert_eq!(code, Some(0));
|
assert_eq!(code, Some(0));
|
||||||
assert!(out.contains("open slot: process.ref (required, one of: content_id, identity_id)"));
|
assert!(out.contains("open slot: process.ref (required, content id of a process document)"));
|
||||||
assert!(out.contains("open slot: strategies[0].axes.slow (axis declared empty — a P1 axis is a non-empty finite set)"));
|
assert!(out.contains("open slot: strategies[0].axes.slow (axis declared empty — a P1 axis is a non-empty finite set)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -844,7 +844,9 @@ pub fn open_slots_campaign(text: &str) -> Result<Vec<OpenSlot>, DocError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v.get("process").and_then(|p| p.get("ref")).is_none() {
|
if v.get("process").and_then(|p| p.get("ref")).is_none() {
|
||||||
slots.push(open("process.ref", "required, one of: content_id, identity_id"));
|
// deliberately narrower than the strategy-ref hint: validate_campaign
|
||||||
|
// refuses an identity_id process ref, so the guide must not offer it
|
||||||
|
slots.push(open("process.ref", "required, content id of a process document"));
|
||||||
}
|
}
|
||||||
if v.get("seed").and_then(|s| s.as_u64()).is_none() {
|
if v.get("seed").and_then(|s| s.as_u64()).is_none() {
|
||||||
slots.push(open("seed", "required, non-negative integer"));
|
slots.push(open("seed", "required, non-negative integer"));
|
||||||
@@ -1215,7 +1217,7 @@ mod tests {
|
|||||||
"presentation": { "persist_taps": [], "emit": [] } }"#;
|
"presentation": { "persist_taps": [], "emit": [] } }"#;
|
||||||
let slots = open_slots_campaign(draft).unwrap();
|
let slots = open_slots_campaign(draft).unwrap();
|
||||||
assert!(slots.iter().any(|s| s.path == "process.ref"
|
assert!(slots.iter().any(|s| s.path == "process.ref"
|
||||||
&& s.hint == "required, one of: content_id, identity_id"));
|
&& s.hint == "required, content id of a process document"));
|
||||||
assert!(slots.iter().any(|s| s.path == "strategies[0].axes.slow"
|
assert!(slots.iter().any(|s| s.path == "strategies[0].axes.slow"
|
||||||
&& s.hint == "axis declared empty — a P1 axis is a non-empty finite set"));
|
&& s.hint == "axis declared empty — a P1 axis is a non-empty finite set"));
|
||||||
|
|
||||||
|
|||||||
+30
-2
@@ -1367,8 +1367,24 @@ spans the third variant, `FamilyKind::WalkForward` — the same branch rebuilds
|
|||||||
windowed slice from `manifest.window` (winner params via the shared `manifest→cells` recovery),
|
windowed slice from `manifest.window` (winner params via the shared `manifest→cells` recovery),
|
||||||
so an `aura walkforward` family re-derives bit-identically too. All three family kinds now
|
so an `aura walkforward` family re-derives bit-identically too. All three family kinds now
|
||||||
persist *and* reproduce through the one shared `topology_hash`+`put_blueprint` hook.
|
persist *and* reproduce through the one shared `topology_hash`+`put_blueprint` hook.
|
||||||
|
**Realization (cycle 0106, #189 — research-artifact document stores).** The registry's
|
||||||
### C19 — Bootstrap: blueprint → instance (recursive)
|
content-addressed store family grew two siblings beside `blueprints/`: `processes/` and
|
||||||
|
`campaigns/` hold the two research-artifact document types shipped by the #188 role-model
|
||||||
|
pass — the **process document** (role 5: a named validation/eval methodology, a closed std
|
||||||
|
stage vocabulary wrapping shipped primitives) and the **campaign document** (role 6b:
|
||||||
|
persisted experiment intent — instruments × windows × strategy refs by content/identity id ×
|
||||||
|
param axes × process ref (content-id-only) × data-level presentation). Documents are
|
||||||
|
canonical JSON (`format_version` envelope, omit-defaults, no trailing newline) keyed by the
|
||||||
|
**shared content-id primitive, now library-hosted** (`aura_research::content_id_of`;
|
||||||
|
`aura-cli`'s `content_id`/`topology_hash` delegate byte-identically — the id goldens pin the
|
||||||
|
move). Unlike `put_blueprint` (caller owns the hash), the document puts self-key from
|
||||||
|
canonical bytes; gets are `Ok(None)` treat-as-empty. The **referential tier**
|
||||||
|
(`validate_campaign_refs`) resolves process/strategy refs against the stores (identity refs
|
||||||
|
by store scan) and checks each campaign axis — name AND declared `ScalarKind`, the axis
|
||||||
|
carries its kind once — against the referenced blueprint's `param_space`. Campaign P1
|
||||||
|
control constructs (axes/gates/ladders per #188) carry intent only: **no executor exists**;
|
||||||
|
executor need is re-tested against the intent-persistence diagnosis before one is scoped
|
||||||
|
(#189 triage, decided item 6).
|
||||||
**Guarantee.** Construction is a distinct phase, recursive at every level. Each
|
**Guarantee.** Construction is a distinct phase, recursive at every level. Each
|
||||||
node type has a **factory** `params → sized concrete node` (e.g. `SMA(length)`
|
node type has a **factory** `params → sized concrete node` (e.g. `SMA(length)`
|
||||||
sizes its ring buffer). A **blueprint** is the param-generic, input-role-generic
|
sizes its ring buffer). A **blueprint** is the param-generic, input-role-generic
|
||||||
@@ -2127,6 +2143,18 @@ artifact, and any run is deterministic once instantiated" (C1).
|
|||||||
brainstorm / milestone), and the **composable-orchestration** half — the axes
|
brainstorm / milestone), and the **composable-orchestration** half — the axes
|
||||||
(sweep / MC / walk-forward) becoming tools applied to a World-constructed harness
|
(sweep / MC / walk-forward) becoming tools applied to a World-constructed harness
|
||||||
instead of the hard-wired `aura-cli` verbs (`sweep_family` / `walkforward_family`).
|
instead of the hard-wired `aura-cli` verbs (`sweep_family` / `walkforward_family`).
|
||||||
|
**Status (cycle 0106, #189 — the artifact half shipped v1).** The #188 role-model pass
|
||||||
|
re-cut the "experiment-builder API" reading of this thread (role-6b work mis-typed in
|
||||||
|
role-2 technique): experiment intent is now **data**, not a Rust API. Cycle 0106 shipped
|
||||||
|
both document vocabularies (process document, campaign document — `aura-research`),
|
||||||
|
two-tier validation, the op-script-precedent introspection contract
|
||||||
|
(`aura process|campaign introspect --vocabulary/--block/--unwired/--content-id`), and
|
||||||
|
content-addressed stores (C18 realization note) — authorable and checkable headless, no
|
||||||
|
compile, no run. Still open here: the executor question (deliberately unscoped until the
|
||||||
|
intent-persistence diagnosis is re-tested, #189), the "once it carries" dissolution of the
|
||||||
|
hard-wired verbs into the campaign path (user decision 2026-07-03 on #188), and the
|
||||||
|
remaining #188 amendment package (role-model contract entry; C20 "plain Rust loops"
|
||||||
|
clause; C22 wording; invariant-10 clarification).
|
||||||
- **Inferential honesty of the World — family-selection without false-discovery
|
- **Inferential honesty of the World — family-selection without false-discovery
|
||||||
control (tracked: milestone "Inferential validation (defend against false
|
control (tracked: milestone "Inferential validation (defend against false
|
||||||
discovery at sweep scale)", #144 / #145 / #146; adjacent #139).** The World's
|
discovery at sweep scale)", #144 / #145 / #146; adjacent #139).** The World's
|
||||||
|
|||||||
+11
-3
@@ -43,6 +43,10 @@ A frozen artifact that is a deployed strategy + broker: the live trading program
|
|||||||
**Avoid:** —
|
**Avoid:** —
|
||||||
A downstream consumer node, never part of the strategy: the signal-quality side is the **R-evaluator** (per-trade R-outcomes from the in-R `RiskExecutor` → R-expectancy), read as **gross R** (signal) and **net R** (after the `cost model`). The historical in-graph **realistic broker** — the in-loop fill/commission/slippage account simulator that backtrader (`BackBroker`) and LEAN call a "broker" — is retired (real friction is not historically knowable) — cost is approximated by the `cost model` (a C9 cost-graph, in R), and the only real broker is the **live deploy-edge I/O adapter** against an actual venue (e.g. cTrader Open API), the sole reliable ground truth (measured never modelled, C11/C13); account money lives only at that deploy edge.
|
A downstream consumer node, never part of the strategy: the signal-quality side is the **R-evaluator** (per-trade R-outcomes from the in-R `RiskExecutor` → R-expectancy), read as **gross R** (signal) and **net R** (after the `cost model`). The historical in-graph **realistic broker** — the in-loop fill/commission/slippage account simulator that backtrader (`BackBroker`) and LEAN call a "broker" — is retired (real friction is not historically knowable) — cost is approximated by the `cost model` (a C9 cost-graph, in R), and the only real broker is the **live deploy-edge I/O adapter** against an actual venue (e.g. cTrader Open API), the sole reliable ground truth (measured never modelled, C11/C13); account money lives only at that deploy edge.
|
||||||
|
|
||||||
|
### campaign document
|
||||||
|
**Avoid:** experiment doc, campaign file
|
||||||
|
The role-6b research artifact (#188/#189): persisted experiment intent as closed-vocabulary canonical JSON — instruments × windows × strategy refs (by `content id` or `identity id`) × per-strategy param axes (each axis declares its `ScalarKind` once over bare values) × a process reference (content-id-only) × data-level presentation (taps to persist, tables to emit). Authored and validated headless (`aura campaign validate|introspect|register`), content-addressed beside blueprints in the registry store; carries P1 control constructs (bounded axes, gates, ladders) as intent — execution semantics are a separate, later concern.
|
||||||
|
|
||||||
### cdylib
|
### cdylib
|
||||||
**Avoid:** —
|
**Avoid:** —
|
||||||
The dynamically-loadable Rust library form of a project and its nodes, hot-reloaded during authoring; the hot-reload unit is always the project-side cdylib. Frozen to a static artifact for deploy.
|
The dynamically-loadable Rust library form of a project and its nodes, hot-reloaded during authoring; the hot-reload unit is always the project-side cdylib. Frozen to a static artifact for deploy.
|
||||||
@@ -57,7 +61,7 @@ A node that wires a sub-graph and exposes one output (a combined signal, or a st
|
|||||||
|
|
||||||
### content id
|
### content id
|
||||||
**Avoid:** —
|
**Avoid:** —
|
||||||
The SHA-256 (hex) of a blueprint's byte-canonical `blueprint_to_json` form — the byte-exact identity that keys the reproduction store and anchors `aura reproduce` (stamped into a run manifest as the `topology hash`). Surfaced as `aura graph introspect --content-id`; two documents share a content id only when their canonical bytes agree, debug names included — contrast `identity id`.
|
The SHA-256 (hex) of an artifact's byte-canonical form — for a blueprint, `blueprint_to_json` (the byte-exact identity that keys the reproduction store and anchors `aura reproduce`, stamped into a run manifest as the `topology hash`); for a `process document` / `campaign document`, its canonical JSON (keying the sibling registry stores). One primitive, library-hosted in `aura-research` (`content_id_of`; the CLI delegates). Surfaced as `aura graph|process|campaign introspect --content-id`; two artifacts share a content id only when their canonical bytes agree, debug names included — contrast `identity id`.
|
||||||
|
|
||||||
### conviction-based risk allocation
|
### conviction-based risk allocation
|
||||||
**Avoid:** risk budgeting, risk parity, risk contribution
|
**Avoid:** risk budgeting, risk parity, risk contribution
|
||||||
@@ -88,8 +92,8 @@ A directed wiring link in a harness that forwards **one field** (`from_field`) o
|
|||||||
A downstream node's output over time — the headline form is the **net-R equity curve** (gross R minus the `cost model`'s cost-in-R, via the `net_r_equity` tap); currency equity is not a research artifact (it lives only at the live deploy edge). An equity *curve* is explicitly not a strategy's output (the `bias` stream is).
|
A downstream node's output over time — the headline form is the **net-R equity curve** (gross R minus the `cost model`'s cost-in-R, via the `net_r_equity` tap); currency equity is not a research artifact (it lives only at the live deploy edge). An equity *curve* is explicitly not a strategy's output (the `bias` stream is).
|
||||||
|
|
||||||
### experiment
|
### experiment
|
||||||
**Avoid:** —
|
**Avoid:** experiment-builder API
|
||||||
A Rust (builder-API) definition of anything beyond a single backtest — sweep, Monte-Carlo, walk-forward, or a structural matrix; it lives in a project's `experiments/`. Authored in native Rust, never a config DSL — an active *generator* of runs, not MLflow's passive run-*container* (that grouping role is the `run registry` / family).
|
**Superseded by `campaign document` (2026-07-03, #188/#189).** The pre-role-model reading — a Rust builder-API definition of anything beyond a single backtest, living in a project's `experiments/` — was role-6b work mis-typed in role-2 technique: experiment intent is now closed-vocabulary *data* (the campaign document), not Rust source, so a tiny campaign change never costs a compile cycle. What stays Rust is node/block *logic* (role 2). The run registry / family remains the passive run-container counterpart.
|
||||||
|
|
||||||
### experiment matrix
|
### experiment matrix
|
||||||
**Avoid:** structural matrix
|
**Avoid:** structural matrix
|
||||||
@@ -175,6 +179,10 @@ The egui-native visual face (`aura play`) that plays any harness — program str
|
|||||||
**Avoid:** transactions, trade blotter
|
**Avoid:** transactions, trade blotter
|
||||||
A broker-independent, time-ordered table of position events (scalar columns: `event_ts, action, position_id, instrument_id, volume`) — the **derived** first difference of the executed **book** (book-tracking `deal = target − book − in_flight`; a close sizes the actual book). A **deploy / reconciliation** artifact (real volume), not a research artifact and not "fed to a broker" in research; computed, not emitted per `eval` (one decision instant may yield >1 event), and **not** the strategy's direct DAG output (that is the `bias` stream).
|
A broker-independent, time-ordered table of position events (scalar columns: `event_ts, action, position_id, instrument_id, volume`) — the **derived** first difference of the executed **book** (book-tracking `deal = target − book − in_flight`; a close sizes the actual book). A **deploy / reconciliation** artifact (real volume), not a research artifact and not "fed to a broker" in research; computed, not emitted per `eval` (one decision instant may yield >1 event), and **not** the strategy's direct DAG output (that is the `bias` stream).
|
||||||
|
|
||||||
|
### process document
|
||||||
|
**Avoid:** process doc, methodology file
|
||||||
|
The role-5 research artifact (#188/#189): a named, versionable validation/eval methodology as closed-vocabulary canonical JSON — an ordered pipeline of std stage blocks (`std::sweep`, `std::gate`, `std::walk_forward`, `std::monte_carlo`, `std::generalize`) wrapping shipped analysis/selection primitives, with typed gate predicates over the declared metric vocabulary. Authored and validated headless (`aura process validate|introspect|register`), content-addressed in the registry store; campaigns reference it by `content id` only — a one-off methodology is a process document with a disposable name, never an inline body.
|
||||||
|
|
||||||
### R
|
### R
|
||||||
**Avoid:** —
|
**Avoid:** —
|
||||||
The native unit of strategy performance: 1R = the loss if the protective stop is hit, so a trade's outcome is a multiple of R (−1R stopped out, +2R, …) and the primary question is *"how much R out per 1R risked?"*. Risk-normalized and account-/instrument-agnostic — the research yardstick replacing pips, defined by the stop (Van Tharp R-multiples), read as **gross R** (signal only) and **net R** (after the `cost model`).
|
The native unit of strategy performance: 1R = the loss if the protective stop is hit, so a trade's outcome is a multiple of R (−1R stopped out, +2R, …) and the primary question is *"how much R out per 1R risked?"*. Risk-normalized and account-/instrument-agnostic — the research yardstick replacing pips, defined by the stop (Van Tharp R-multiples), read as **gross R** (signal only) and **net R** (after the `cost model`).
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,376 +0,0 @@
|
|||||||
# Research-artifact vocabularies: process & campaign documents — Design Spec
|
|
||||||
|
|
||||||
**Date:** 2026-07-03
|
|
||||||
**Status:** Draft — awaiting sign-off (boss run: grounding-check gate)
|
|
||||||
**Authors:** orchestrator + Claude
|
|
||||||
**Cycle:** 0106 — milestone "Research artifacts — process & campaign documents";
|
|
||||||
work item #189 (design basis: #188 body + both addenda + fork-triage +
|
|
||||||
resolution comments; spec-bearing fork resolutions: the cycle-0106
|
|
||||||
reconciliation comment on #189)
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Give roles 5 and 6b (#188) their artifacts: two new **closed-vocabulary,
|
|
||||||
typed, serialized document types** — the **process document** (a named,
|
|
||||||
versionable validation/eval methodology) and the **campaign document**
|
|
||||||
(persisted experiment intent: data windows × strategy references × param
|
|
||||||
axes × process reference × data-level presentation) — with **parsing,
|
|
||||||
two-tier validation, the op-script-precedent introspection contract, and
|
|
||||||
content-addressing**. **No executor in this cycle**: the deliverable is that
|
|
||||||
methodology and experiment intent stop being smeared over CLI flags and
|
|
||||||
shell history and become nameable, diffable, content-addressed documents an
|
|
||||||
LLM or human can author and validate headless, without a Rust compile and
|
|
||||||
without executing a run.
|
|
||||||
|
|
||||||
Power ceiling is the ratified P1 tier (#188 addendum 2026-07-03 12:08):
|
|
||||||
bounded axes over declared finite sets and structured gates between stages.
|
|
||||||
Ladders (the third ratified P1 family) are **within the ceiling but not in
|
|
||||||
the v1 vocabulary** — they describe adaptive retry, which has no host
|
|
||||||
semantics until an executor exists; they land with the executor cycle. The
|
|
||||||
v1 process vocabulary is a thin named layer over **shipped** primitives
|
|
||||||
only (fork-triage decided item 5): the R-metrics reduction, the
|
|
||||||
argmax/plateau selection rules, deflation annotation, the R-bootstrap
|
|
||||||
Monte-Carlo, and cross-instrument generalization.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
One new leaf crate **`crates/aura-research`** (workspace member; deps:
|
|
||||||
`aura-core`, `serde`, `serde_json`, `sha2`) owns:
|
|
||||||
|
|
||||||
- the two document types and their block/construct types (serde derives
|
|
||||||
directly on them — these types ARE the wire format; the engine is not
|
|
||||||
involved),
|
|
||||||
- parsing (`format_version` envelope, deny-unknown-fields),
|
|
||||||
- **intrinsic validation** (store-free): vocabulary membership, typed
|
|
||||||
slots, P1 constraints,
|
|
||||||
- the **introspection contract**: enumerate vocabulary, describe one
|
|
||||||
block's typed slots, list a partial document's open slots,
|
|
||||||
- **canonical serialization + content id** — and the canonical-bytes
|
|
||||||
SHA-256 helper moves here as the library-hosted single primitive;
|
|
||||||
`aura-cli`'s existing `content_id` (crates/aura-cli/src/main.rs:2587,
|
|
||||||
shared today by `topology_hash` at main.rs:2595 and
|
|
||||||
`graph introspect --content-id`/`--identity-id`) delegates to it,
|
|
||||||
byte-identically.
|
|
||||||
|
|
||||||
**`crates/aura-registry`** gains the content-addressed stores for both
|
|
||||||
document types beside the existing blueprint store
|
|
||||||
(`put_blueprint`/`get_blueprint`, crates/aura-registry/src/lib.rs:119/127):
|
|
||||||
`processes/<content-id>.json`, `campaigns/<content-id>.json`, plus the
|
|
||||||
**referential validation** tier (registry depends on engine and can load
|
|
||||||
blueprints; aura-research stays a leaf and never sees a store).
|
|
||||||
|
|
||||||
**`crates/aura-cli`** gains two role-addressed verb families — `aura
|
|
||||||
process …` (role 5) and `aura campaign …` (role 6b) — each with `validate
|
|
||||||
<file>` and `introspect` mirroring the shipped `graph introspect` flag
|
|
||||||
idiom (`--vocabulary` / `--block <id>` / `--unwired <file>` /
|
|
||||||
`--content-id <file>`, exactly-one-of enforced as in
|
|
||||||
crates/aura-cli/src/graph_construct.rs:199), and `register <file>` to put a
|
|
||||||
valid document into the project's store. Faults are presented as
|
|
||||||
house-style prose at the binary seam (the #162 convention; error types
|
|
||||||
stay Display-free and by-identifier); usage errors exit 2, runtime
|
|
||||||
refusals exit 1 (the #175 split).
|
|
||||||
|
|
||||||
## Concrete code shapes
|
|
||||||
|
|
||||||
### The worked author example (role 5 + role 6b, no Rust, no run)
|
|
||||||
|
|
||||||
A methodology author writes `wf-deflated-screen.process.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"format_version": 1,
|
|
||||||
"kind": "process",
|
|
||||||
"name": "wf-deflated-screen",
|
|
||||||
"description": "Sweep, keep only deflated-positive candidates, then walk-forward.",
|
|
||||||
"pipeline": [
|
|
||||||
{ "block": "std::sweep",
|
|
||||||
"metric": "net_expectancy_r",
|
|
||||||
"select": "plateau:worst",
|
|
||||||
"deflate": true },
|
|
||||||
{ "block": "std::gate",
|
|
||||||
"all": [
|
|
||||||
{ "metric": "net_expectancy_r", "cmp": "gt", "value": 0.0 },
|
|
||||||
{ "metric": "overfit_probability", "cmp": "lt", "value": 0.1 }
|
|
||||||
] },
|
|
||||||
{ "block": "std::walk_forward",
|
|
||||||
"folds": 4,
|
|
||||||
"in_sample_bars": 4000,
|
|
||||||
"out_of_sample_bars": 1000,
|
|
||||||
"metric": "net_expectancy_r",
|
|
||||||
"select": "argmax" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ aura process validate wf-deflated-screen.process.json
|
|
||||||
process document valid (intrinsic): 3 pipeline blocks, 2 gate predicates
|
|
||||||
$ aura process introspect --content-id wf-deflated-screen.process.json
|
|
||||||
content:4e2d…
|
|
||||||
$ aura process register wf-deflated-screen.process.json # inside a project
|
|
||||||
registered process content:4e2d… (runs/processes/4e2d….json)
|
|
||||||
```
|
|
||||||
|
|
||||||
A campaign author writes `ger40-momentum-screen.campaign.json`, referencing
|
|
||||||
the process and a strategy **by id only** (reference-only slot, #188
|
|
||||||
resolution comment):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"format_version": 1,
|
|
||||||
"kind": "campaign",
|
|
||||||
"name": "ger40-momentum-screen",
|
|
||||||
"data": {
|
|
||||||
"instruments": ["GER40"],
|
|
||||||
"windows": [ { "from_ms": 1704067200000, "to_ms": 1719792000000 } ]
|
|
||||||
},
|
|
||||||
"strategies": [
|
|
||||||
{ "ref": { "content_id": "9f3a…" },
|
|
||||||
"axes": { "fast": { "kind": "I64", "values": [8, 12, 16] },
|
|
||||||
"slow": { "kind": "I64", "values": [21, 34] } } }
|
|
||||||
],
|
|
||||||
"process": { "ref": { "content_id": "4e2d…" } },
|
|
||||||
"seed": 1,
|
|
||||||
"presentation": {
|
|
||||||
"persist_taps": ["net_r_equity"],
|
|
||||||
"emit": ["family_table", "selection_report"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ aura campaign validate ger40-momentum-screen.campaign.json
|
|
||||||
campaign document valid (intrinsic): 1 strategy, 2 axes (6 points), process ref, 1 window
|
|
||||||
campaign document valid (referential): strategy 9f3a… found, process 4e2d… found, axes ⊆ param space
|
|
||||||
$ aura campaign introspect --vocabulary
|
|
||||||
std::data campaign section: instruments + windows
|
|
||||||
std::strategy campaign section: strategy ref (content or identity id) + param axes
|
|
||||||
…
|
|
||||||
$ aura campaign introspect --unwired draft.campaign.json
|
|
||||||
open slot: process.ref (required, one of: content_id, identity_id)
|
|
||||||
open slot: strategies[0].axes.slow (axis declared empty — a P1 axis is a non-empty finite set)
|
|
||||||
```
|
|
||||||
|
|
||||||
Authoring feedback loops (`--unwired`, precise refusals) replace the
|
|
||||||
compile cycle; the Blockly litmus test is checkable because
|
|
||||||
`--vocabulary`/`--block` enumerate every block and its typed slots.
|
|
||||||
|
|
||||||
### Document types (aura-research, primary shapes)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// crates/aura-research/src/lib.rs (new crate)
|
|
||||||
#[derive(Serialize, Deserialize, ...)]
|
|
||||||
pub struct ProcessDoc {
|
|
||||||
pub format_version: u32, // envelope, must be 1
|
|
||||||
pub kind: DocKind, // tag: Process
|
|
||||||
pub name: String,
|
|
||||||
#[serde(default, skip_serializing_if = "...")]
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub pipeline: Vec<StageBlock>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, ...)]
|
|
||||||
#[serde(tag = "block")]
|
|
||||||
pub enum StageBlock {
|
|
||||||
#[serde(rename = "std::sweep")]
|
|
||||||
Sweep { metric: MetricId, select: SelectRule, #[serde(default)] deflate: bool },
|
|
||||||
#[serde(rename = "std::gate")]
|
|
||||||
Gate { all: Vec<Predicate> }, // non-empty conjunction
|
|
||||||
#[serde(rename = "std::walk_forward")]
|
|
||||||
WalkForward { folds: u32, in_sample_bars: u64, out_of_sample_bars: u64,
|
|
||||||
metric: MetricId, select: SelectRule },
|
|
||||||
#[serde(rename = "std::monte_carlo")]
|
|
||||||
MonteCarlo { resamples: u32, block_len: u32 }, // terminal annotator in v1
|
|
||||||
#[serde(rename = "std::generalize")]
|
|
||||||
Generalize { metric: MetricId }, // terminal annotator in v1
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, ...)]
|
|
||||||
pub struct Predicate { pub metric: MetricId, pub cmp: Cmp, pub value: f64 }
|
|
||||||
pub enum Cmp { Gt, Ge, Lt, Le } // closed comparator set; no eq on floats
|
|
||||||
|
|
||||||
// MetricId: newtype over the closed declared-metric vocabulary — the scalar
|
|
||||||
// field names of shipped RMetrics/RunMetrics (crates/aura-analysis/src/lib.rs:58/:86)
|
|
||||||
// plus the selection annotations deflated_score / overfit_probability /
|
|
||||||
// neighbourhood_score (FamilySelection, crates/aura-analysis/src/lib.rs:32).
|
|
||||||
// SelectRule: Argmax | PlateauMean | PlateauWorst — mirrors SelectionMode
|
|
||||||
// (crates/aura-analysis/src/lib.rs:17) and the CLI --select strings.
|
|
||||||
```
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[derive(Serialize, Deserialize, ...)]
|
|
||||||
pub struct CampaignDoc {
|
|
||||||
pub format_version: u32,
|
|
||||||
pub kind: DocKind, // tag: Campaign
|
|
||||||
pub name: String,
|
|
||||||
#[serde(default, ...)] pub description: Option<String>,
|
|
||||||
pub data: DataSection, // instruments: Vec<String>, windows: Vec<Window{from_ms,to_ms}>
|
|
||||||
pub strategies: Vec<StrategyEntry>, // ref + axes
|
|
||||||
pub process: ProcessRef, // { ref: DocRef } — reference-only, never inline
|
|
||||||
pub seed: u64,
|
|
||||||
pub presentation: Presentation, // persist_taps: Vec<String>, emit: Vec<EmitKind>
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, ...)]
|
|
||||||
pub enum DocRef { ContentId(String), IdentityId(String) } // one-of, typed slots
|
|
||||||
pub struct StrategyEntry { pub r#ref: DocRef, pub axes: BTreeMap<String, Axis> }
|
|
||||||
pub struct Axis { pub kind: ScalarKind, pub values: Vec<Scalar> }
|
|
||||||
// custom serde: `{"kind": "I64", "values": [8, 12, 16]}` — values are bare
|
|
||||||
// JSON, parsed AS the declared kind
|
|
||||||
pub enum EmitKind { FamilyTable, SelectionReport } // closed v1 set
|
|
||||||
```
|
|
||||||
|
|
||||||
An axis declares its `ScalarKind` **once** — the kind is a property of the
|
|
||||||
parameter, not of each value (user veto, 2026-07-03, of a per-cell tagged
|
|
||||||
draft), so a mixed-kind axis is unrepresentable rather than merely refused.
|
|
||||||
The op-script binds keep their per-cell tagged `Scalar` form
|
|
||||||
(`OpDoc::Add { bind: BTreeMap<String, Scalar> }`,
|
|
||||||
crates/aura-cli/src/graph_construct.rs:37): a bind slot holds exactly one
|
|
||||||
value, so there the tag is the only kind carrier; an axis is a homogeneous
|
|
||||||
list, so the kind belongs to the axis.
|
|
||||||
|
|
||||||
### Validation, canonical form, introspection (secondary shapes)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// intrinsic tier (aura-research; store-free)
|
|
||||||
pub fn validate_process(doc: &ProcessDoc) -> Vec<DocFault>;
|
|
||||||
pub fn validate_campaign(doc: &CampaignDoc) -> Vec<DocFault>;
|
|
||||||
// DocFault: by-identifier, Display-free (CLI phrases it) — e.g.
|
|
||||||
// UnknownMetric{stage,metric}, EmptyAxis{strategy,axis}, EmptyPipeline,
|
|
||||||
// GateAfterTerminalStage{stage}, BadFormatVersion(u32), EmptyConjunction{stage},
|
|
||||||
// NoWindow, NoStrategy, UnknownEmitKind(String) …
|
|
||||||
|
|
||||||
// canonical form + ids (aura-research)
|
|
||||||
pub fn process_to_json(doc: &ProcessDoc) -> String; // canonical: omit-defaults,
|
|
||||||
pub fn campaign_to_json(doc: &CampaignDoc) -> String; // struct field order, no trailing \n
|
|
||||||
pub fn content_id_of(canonical_bytes: &str) -> String; // SHA-256 hex — THE primitive;
|
|
||||||
// aura-cli::content_id delegates here
|
|
||||||
|
|
||||||
// introspection contract (aura-research)
|
|
||||||
pub fn process_vocabulary() -> Vec<BlockInfo>; // id + one-line doc
|
|
||||||
pub fn campaign_vocabulary() -> Vec<BlockInfo>;
|
|
||||||
pub fn describe_block(id: &str) -> Option<BlockSchema>; // typed slots: name/kind/required/enum values
|
|
||||||
pub fn open_slots_process(text: &str) -> Result<Vec<OpenSlot>, DocError>; // partial-doc probe
|
|
||||||
pub fn open_slots_campaign(text: &str) -> Result<Vec<OpenSlot>, DocError>;
|
|
||||||
```
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// referential tier (aura-registry; needs stores + engine)
|
|
||||||
pub fn put_process(dir: &Path, canonical_json: &str) -> Result<String, RegistryError>; // returns content id
|
|
||||||
pub fn get_process(dir: &Path, content_id: &str) -> Result<String, RegistryError>;
|
|
||||||
pub fn put_campaign(…) / get_campaign(…); // same pattern as put_blueprint/get_blueprint
|
|
||||||
pub fn validate_campaign_refs(doc: &CampaignDoc, roots: &StoreRoots)
|
|
||||||
-> Vec<RefFault>;
|
|
||||||
// RefFault: ProcessNotFound(id), StrategyNotFound(id), IdentityAmbiguityEmpty(id),
|
|
||||||
// AxisNotInParamSpace{strategy_ref, axis}, AxisKindMismatch{strategy_ref, axis}
|
|
||||||
// — axes ⊆ the referenced blueprint's param_space, and each axis's declared
|
|
||||||
// kind must equal the param's ScalarKind (one check per axis; blueprint
|
|
||||||
// loaded from the store via blueprint_from_json).
|
|
||||||
```
|
|
||||||
|
|
||||||
Intrinsic P1 constraints enforced (the ratified ceiling made structural):
|
|
||||||
every axis is a non-empty finite value list; every gate is a non-empty
|
|
||||||
conjunction of typed `{metric, cmp, value}` triples whose metrics are in
|
|
||||||
the declared vocabulary; gates may not follow `std::monte_carlo` /
|
|
||||||
`std::generalize` (terminal annotators in v1 — no shipped aggregation
|
|
||||||
metric exists for their outputs to gate on; widening this is executor-cycle
|
|
||||||
work); no construct carries variables, recursion, or unbounded iteration —
|
|
||||||
such shapes are unrepresentable, not merely refused.
|
|
||||||
|
|
||||||
### CLI wiring (secondary)
|
|
||||||
|
|
||||||
`Command::Process(ProcessCmd)` / `Command::Campaign(CampaignCmd)` in
|
|
||||||
crates/aura-cli/src/main.rs beside `Command::Graph`; a new
|
|
||||||
`crates/aura-cli/src/research_docs.rs` module (graph_construct.rs
|
|
||||||
analogue) holds dispatch + fault presentation. `validate` runs intrinsic
|
|
||||||
always, referential when a project env is discovered (Aura.toml discovery,
|
|
||||||
crates/aura-cli/src/project.rs:169) and reports each tier on its own line;
|
|
||||||
outside a project the referential line is
|
|
||||||
`referential checks skipped (no Aura.toml found up from <cwd>)`.
|
|
||||||
`register` refuses invalid documents; `--content-id` refuses
|
|
||||||
intrinsically-invalid documents (an invalid document has no canonical
|
|
||||||
form).
|
|
||||||
|
|
||||||
## Components
|
|
||||||
|
|
||||||
- `crates/aura-research` (new): document types, intrinsic validation,
|
|
||||||
canonical form + content id (the moved primitive), introspection
|
|
||||||
contract, `DocFault`/`DocError`.
|
|
||||||
- `crates/aura-registry`: `processes/`/`campaigns/` stores (blueprint-store
|
|
||||||
pattern), referential tier (`validate_campaign_refs`), `RefFault`.
|
|
||||||
- `crates/aura-cli`: `process`/`campaign` verb families, presentation of
|
|
||||||
faults, `content_id` delegation to `aura_research::content_id_of`.
|
|
||||||
- `Cargo.toml` (workspace): new member.
|
|
||||||
|
|
||||||
## Data flow
|
|
||||||
|
|
||||||
Author writes JSON text → `parse` (envelope + deny-unknown-fields) →
|
|
||||||
intrinsic `validate_*` (vocabulary membership, typed slots, P1
|
|
||||||
constraints) → canonical `*_to_json` → `content_id_of` → `register` puts
|
|
||||||
canonical bytes into the project store keyed by content id → a campaign's
|
|
||||||
`DocRef`s resolve against those stores (referential tier), and its axes
|
|
||||||
resolve against the referenced blueprint's `param_space` loaded from the
|
|
||||||
blueprint store. Nothing executes; the registry stays the World's memory
|
|
||||||
(C18), now holding intent documents beside blueprints and run records.
|
|
||||||
|
|
||||||
## Error handling
|
|
||||||
|
|
||||||
Three by-identifier fault layers, all Display-free with CLI-side prose
|
|
||||||
(the #162 seam convention, pinned like
|
|
||||||
`graph_build_renders_a_bind_kind_mismatch_as_prose`):
|
|
||||||
|
|
||||||
- `DocError` — parse-level: not JSON, bad envelope (`format_version` ≠ 1),
|
|
||||||
unknown `kind`, unknown fields, malformed ref.
|
|
||||||
- `DocFault` — intrinsic-validation findings (list, not first-fault):
|
|
||||||
unknown metric/block/emit kind, empty axis/pipeline/conjunction, gate
|
|
||||||
after a terminal stage, missing sections.
|
|
||||||
- `RefFault` — referential findings: unresolved process/strategy id,
|
|
||||||
identity id resolving to no stored blueprint, axis not in the referenced
|
|
||||||
strategy's param space.
|
|
||||||
|
|
||||||
Exit codes: clap usage errors 2; validation refusals and runtime faults 1
|
|
||||||
(the #175 split, already pinned at the binary seam).
|
|
||||||
|
|
||||||
## Testing strategy
|
|
||||||
|
|
||||||
- **aura-research unit tests:** parse/serialize round-trips for both
|
|
||||||
types; canonical-form golden pins (byte-exact, the blueprint-golden
|
|
||||||
pattern); content-id stability goldens; one refusal test per `DocFault`
|
|
||||||
variant (incl. every P1 constraint); vocabulary/describe/open-slots
|
|
||||||
shape tests (every block's slots enumerable — the Blockly litmus pin);
|
|
||||||
a test that `serde` rejects an inline process body in the campaign's
|
|
||||||
`process` slot (reference-only is structural).
|
|
||||||
- **aura-registry tests:** store round-trip for both types (hash-keyed
|
|
||||||
like the blueprint-store tests); `validate_campaign_refs` positive +
|
|
||||||
one per `RefFault` (fixtures: a stored blueprint with a known
|
|
||||||
`param_space`).
|
|
||||||
- **aura-cli seam tests:** binary-level validate (valid, intrinsic
|
|
||||||
refusal, referential refusal inside a scaffolded project, outside-project
|
|
||||||
skip line); introspect `--vocabulary`/`--block`/`--unwired`/
|
|
||||||
`--content-id` outputs; prose (no Debug leak) pinned; exit codes pinned;
|
|
||||||
`content_id` delegation equivalence (`topology_hash`/`--content-id`
|
|
||||||
bytes unchanged — guarded by the existing golden
|
|
||||||
`signal_serializes_to_canonical_golden` and id tests staying green
|
|
||||||
untouched).
|
|
||||||
- Full gates: `cargo test --workspace`, `cargo clippy --workspace
|
|
||||||
--all-targets -- -D warnings`, `cargo doc --workspace --no-deps`.
|
|
||||||
|
|
||||||
## Acceptance criteria
|
|
||||||
|
|
||||||
1. The worked example above runs verbatim (modulo hash hex): both
|
|
||||||
documents author as text, validate headless with per-tier report lines,
|
|
||||||
yield stable content ids, and register into a project's store — no
|
|
||||||
Rust compile, no run executed.
|
|
||||||
2. `aura process introspect` / `aura campaign introspect` expose the full
|
|
||||||
introspection contract (vocabulary, block schema, open slots of a
|
|
||||||
partial document, content id) with the exactly-one-of flag guard.
|
|
||||||
3. Every invalid shape in the fault tables refuses with house-style prose
|
|
||||||
naming the offending element; no Rust `Debug` form reaches the user;
|
|
||||||
exit codes follow the usage-2/runtime-1 split.
|
|
||||||
4. The declared-metric vocabulary, select rules, and stage blocks are
|
|
||||||
exactly the shipped-primitive wrappers named in this spec — nothing
|
|
||||||
invented; ladders and any executor semantics are absent.
|
|
||||||
5. The pre-existing suite stays green unchanged; blueprint content/identity
|
|
||||||
ids and `topology_hash` are byte-identical to before (the content-id
|
|
||||||
primitive moved, not changed).
|
|
||||||
6. Workspace gates green: test, clippy `-D warnings`, doc build.
|
|
||||||
Reference in New Issue
Block a user