Compare commits

...

14 Commits

Author SHA1 Message Date
claude cfc2d9f5b1 feat(core): doc_gate refuses the namespaced short-name alibi (GREEN)
The fieldtest's spec_gap closed per the #316 decision (tighten, not
ratify): doc_gate normalizes the doc against both the full entry name
and its bare display segment after the last :: -- an extension node
ns::Scale with doc "Scale" now refuses (RestatesName), matching the
register seam's effective bare-name behaviour. Unnamespaced entries
are unchanged (the checks coincide); genuine meaning lines containing
the name as a substring stay accepted (equality, not containment).
The C29 contract sentence carries the precision.

Gates: workspace green (98 suites, 0 failed -- incl. the C29 coverage
walk over every shipped doc under the tightened gate), clippy clean.

refs #316
2026-07-23 21:12:39 +02:00
claude b04b56148d test(core): RED -- doc_gate refuses the namespaced short-name alibi
Executable spec for the fieldtest spec_gap (decision on #316: tighten,
not ratify): doc_gate("lab316_nodes::Scale", "Scale") must be
Err(RestatesName) -- a doc normalizing to the final ::-segment of a
namespaced entry name restates the name. Unnamespaced entries and
genuine meaning lines containing the name stay Ok (guard assertions
green). Currently red: the shipped check compares the full name only.

refs #316
2026-07-23 21:00:28 +02:00
claude 96e79184e0 fix(docs, registry): fieldtest follow-ups -- guide sync, doc-op documentation, surface-neutral refusal
Three of the four immediate fieldtest findings for the self-description
cycle:

- authoring-guide S0: the worked Scale literal gains the required
  NodeSchema.doc line (+ the C29 comment) -- the shipped example was a
  hard E0063 for anyone pasting it (the exact #314 teaching-failure
  class, caught from the consumer seat).
- The op table becomes "The nine ops": the C29 doc op is documented
  with its JSON shape, the register requirement, the at-most-once rule
  and the gate; the README op-kinds list names it too. The fieldtester
  had to guess the shape from commit bodies -- now it is on the public
  surface.
- The store-gate refusal's fix hint is surface-neutral: builder
  .doc(...), blueprint "doc" member, or op-script {"op":"doc"} --
  previously it prescribed the Rust builder to a JSON author.

Gates: registry, graph_construct (63) and project_load (14) suites
green; refusal-prose pins unchanged (pinned substrings untouched).

refs #316
2026-07-23 20:58:19 +02:00
claude 2d988fdf3d fieldtest: 316-self-description — 4 examples, 1 bug / 3 friction / 1 spec-gap
Consumer-seat run over all four C29 seams (load, register, op-script doc,
document description) from the public interface only. The three working
findings verify the #314 failure class removed end to end; the findings
route: guide sync + refusal-prose neutrality + doc_gate short-name parity
this run, discoverability surfaces to the tracker.

refs #316
2026-07-23 20:55:21 +02:00
claude bd7585b609 audit(cycle 316): drift disposition, bench seed docs + ratified re-pin, C29 scope paragraph
Cycle-close audit for the self-description cycle. Architect: drift_found
(mild), core holds -- the gate is pure string shape in core imported by
every seam (C28), the engine never evaluates description text (C17/inv
10), determinism pins untouched (C1); the store boundary is gated by
construction with the raw write private; Op::Doc is a closed typed
construct (C25). Regression: aura-bench green (exit 0) after one
cycle-caused infra fix and one ratified re-pin.

Infra fix: the bench campaign seeds (BP_SMA_A/B) predate C29 and were
refused by the store gate (exit 2, infrastructure-first rule) -- both
thread the authored sma_signal meaning line.

Ratify: cli_fixed_cost's run_line_fnv moved because bench_sma_a.json now
carries its doc -- the blueprint's content id shifts exactly as the spec
anticipates for fixtures gaining docs; campaign result fingerprints are
unchanged (descriptions stay out of execution, C29). Baselines re-pinned
via aura-bench pin (throughput baselines re-pinned in the same sweep,
all deltas within tolerance at pin time).

Drift disposition:
- [high] C29 guarantee overclaim vs emit/binding-column vocabularies ->
  contract now carries an explicit Scope paragraph naming the residue,
  tracked as #321 (also covers the [medium] non-roster schema walk gap).
- [low] Delay's meaning line cited the stale C5 for the delay register;
  corrected to C9.

refs #316, refs #321
2026-07-23 20:42:02 +02:00
claude 9c7f60b269 docs(ledger): C29 self-description contract -- INDEX entry + domain invariant 13
The cycle's design principle enters the ledger: every closed-vocabulary
entry the binary exposes carries a one-line meaning behind one shared
deterministic shape gate (doc_gate) at three seams -- compile/unit for
engine-shipped entries, load for native node crates, register for the
content-addressed store (documents: an additive-optional gated
description). Forbids the engine evaluating description text (C17 /
invariant 10), any influence on execution/identity/determinism (C1),
retroactive invalidation of registered artifacts, and machine-invented
meaning lines. Why: the audience is headless LLM agents (#319); field
evidence #314 showed schema knowledge being recovered by CAS forensics
from the release binary -- the removed failure class.

All spec acceptance criteria are now met across the four iterations of
this cycle (core carrier + std texts; domain threading; load seam;
register seam + op-script doc slot + document description).

closes #316
2026-07-23 20:34:33 +02:00
claude 7126886b81 feat(engine, cli): op-script doc slot -- the vocabulary grows the C29 meaning line
Iteration 3 completion (tasks 5-6): the construction vocabulary gains
the slot its own #125 scope-cut comment anticipated. Op::Doc { text }
is the op-script twin of the builder's .doc(...) -- a closed, typed
metadata construct (C25, no logic content); at most one per script, a
second refuses via OpError::DuplicateDoc (the existing duplicate-fault
family), rendered "a doc op may appear at most once". GraphSession
threads the text through Composite::with_doc at finish, so op-built
composites pass the C29 store gate like builder-built ones.

The op-script register test runs on the described variant
(SIGNAL_DOC_DESCRIBED) for both shapes; the doc-less script now pins
the C29 refusal, and the E2E phase added the RestatesName op-script
case. Full gates: cargo test --workspace green (98 suites, 0 failed);
clippy -D warnings clean.

refs #316
2026-07-23 20:33:00 +02:00
claude 593e233e66 test(cli): C29 register-refusal e2e -- doc-less root, doc-less nested, described twin
Iteration 3 task 4: register_refuses_undescribed_composites_end_to_end
pins the store gate's verb surface -- a doc-less root refuses naming
`sma_signal` and the rule, a doc-less named nested composite refuses
naming `inner`, the described shipped example registers (exit codes and
stderr through the C14 contract). The pre-existing twin pin
graph_doc_field_moves_content_id_but_not_identity_id stays green.

Checkpoint note: the loop escalated this task spec-ambiguous because
its plan gate demanded a fully green graph_construct suite while the
op-script register test is known-open until the doc-op task lands (a
plan-sequencing defect, orchestrator's authoring error); the task's own
work was verified green and is committed here, the doc-op task follows.

refs #316
2026-07-23 19:53:49 +02:00
claude 750ee93180 feat(research, registry, cli, runner): C29 register seam -- description gate, store-boundary doc gate, authored root docs
Iteration 3 of the self-description plan, tasks 1-3 (checkpoint commit
mid-iteration; the register e2e, the op-script doc slot, and the full
gates follow):

- aura-research: DocFault::BadDescription { subject, fault }; a present
  document description passes doc_gate in validate_process /
  validate_campaign (absent is never a fault and stays byte-identical);
  prose arm in doc_fault_prose; tests incl. content-id participation.
- aura-registry: the store boundary gates admission -- the public
  put_blueprint parses the canonical bytes and walks root + named
  nested composite docs (None refuses like ""), refusing via
  UndescribedComposite { name, fault } / MalformedBlueprint; the raw
  write survives as private put_blueprint_unchecked (store-mechanics
  tests + the never-retroactive witness: doc-less pre-C29 entries stay
  readable). Registry-internal seeders triaged junk-vs-real.
- authored root docs: the 4 example blueprints + their 4 open-fixture
  twins, the scaffold starter template, the scaled_signal and hl_signal
  inline fixtures, and the three programmatic member.rs builders
  (breakout, meanrev, hl_channel) via GraphBuilder::doc -- mechanical
  propagation of the authored lines.

Recovery note: the implement run's task 3 discarded tasks 1+2's edits
in three files via a file-level checkout (the issue-#23 guard shape);
the loop's boundary snapshot caught it and the content was restored
from recovery snapshot 11a6ae2, then re-verified (research 81 green,
registry 51 green, cli 58/59).

Known-open at this checkpoint: the op-script register test is red --
op-built composites have no doc surface (#125 scope cut); the op
vocabulary grows the anticipated doc slot in the next task of this
iteration (decision logged on the issue).

refs #316
2026-07-23 19:47:46 +02:00
claude d6694d0641 feat(runner, cli): C29 load seam -- undescribed extension vocabulary refuses at load
Iteration 2 of the self-description plan: field presence is compile-
enforced since iteration 1, but an extension crate can still ship the
empty-string alibi (or a name-restating doc) the compiler cannot see.
load_crate now walks every charter-checked type id, resolves its
builder, and runs aura_core::doc_gate over schema.doc; a fault refuses
the load via ProjectError::UndescribedVocabularyEntry { type_id, fault }
(exit 1 through the CLI's Display-driven surface, C14).

The variant carries the DocGateFault beyond the spec's minimal sketch
because the spec's error-handling section requires the refusal to name
entry + field + rule -- with only the type id the prose could not
distinguish the two faults (decision logged on #316).

Two gate-failing fixture crates drive the e2e, modelled on
badcharter-project: undescribed-project (und::Opaque, doc: "") for the
Empty arm, restated-project (restated::Echo, doc: "Restated Echo") for
the RestatesName arm -- the second contributed by the E2E phase to
close the only-unit-pinned gap on that arm. A runner unit test pins
both rendered Display arms directly. The described twins (demo-project,
nested-nodes-project) stay green and are now load-bearing for the gate.

Gates: cargo test -p aura-cli --test project_load (14 green, incl. both
refusal e2es); cargo test --workspace green; clippy -D warnings clean.

refs #316
2026-07-23 18:19:10 +02:00
claude ab3f16879b feat(runner, cli, research): metric/tap schema carriers + the C29 coverage walk
C29 compile/unit seam, tasks 6-10 -- iteration 1 of the self-description
plan complete: every engine-shipped vocabulary entry now carries a
gate-clean one-line meaning.

- aura-runner: the internal tap recording sink threads its doc (the
  last unthreaded NodeSchema site) -- first full workspace build since
  the field became required.
- aura-cli scaffold: the sample node's template carries a doc line plus
  a maintenance comment, so every scaffolded extension crate starts
  gate-clean; both extension-vocabulary fixture exemplars thread real
  meaning lines (the load seam walks these in iteration 2). New E2E:
  the scaffold's own doc must pass its own gate -- an alibi doc in the
  template would teach the wrong pattern to every new project, and the
  compiler checks only the field's presence, never its shape.
- aura-research: metric_vocabulary()/tap_vocabulary() promote from bare
  name arrays to MetricSchema/TapSchema carriers (id + doc, 17 + 4
  authored lines); callers adapt to .id with byte-identical output
  (introspection goldens unchanged). The #190 guard keeps its triple --
  the doc column adds no fourth roster site.
- Coverage: tests/self_description.rs walks all five vocabularies
  (blocks, metrics, taps, folds, std nodes) through doc_gate -- the
  guard that keeps any future edit from blanking a doc.

Adjudicated review residue: the 17 metric doc strings are surfaced by
no CLI path yet -- deliberate; task 8 pins callers byte-identical, the
surfacing belongs to the CLI self-description work (#315). Plan-cited
coordinates lib.rs:1003/:1283 resolved to the real caller sites
(:1019/:1299); the plan's literal test code was reconciled to the real
accessors (FoldRegistry::core().roster(), Env::std()).

Gates: cargo test --workspace green (0 failed, incl. the new coverage
walk + scaffold E2E); cargo clippy --workspace --all-targets --
-D warnings clean.

refs #316
2026-07-23 17:19:01 +02:00
claude d8c6938027 feat(engine, market, strategy, backtest): NodeSchema.doc threading across the domain crates
C29 compile/unit seam, tasks 4+5 of the self-description plan.

aura-engine (task 4): derive_signature stamps doc: "" with the stance
recorded in-code -- a derived composite signature is graph wiring, not
a vocabulary entry; no seam walks its doc, the described surface is the
composite's own doc at the register seam. All in-crate test literals
thread doc: "test-only schema".

Domain crates (task 5): the 12 production builder sites in
aura-market / aura-strategy / aura-backtest carry authored meaning
lines; test sites in aura-backtest / aura-composites / aura-ingest
thread the test-only doc.

Three texts were corrected against the actual node semantics after
quality review rather than kept from the first authoring pass:
SimBroker is the frictionless integrator of held exposure times price
return into cumulative pip equity (no fills/stops/lifecycle -- that is
PositionManagement's domain), the shared cost-node line names the
PM-geometry inputs and both charge modes (AtClose / PerHeldCycle)
instead of a "per-trade gross R" mapping, and LongOnly's line
conditions on its enabled param and speaks port-term "exposure".

Gates: cargo test green for all six touched crates (engine, market,
strategy, backtest, composites, ingest); the workspace-wide gate
follows task 6 (aura-runner is the one remaining unthreaded site,
E0063 by design until then).

refs #316
2026-07-23 16:08:26 +02:00
claude a32dc38d18 feat(std): meaning lines for all 27 shipped node schemas
C29 compile/unit seam, task 3 of the self-description plan: every
aura-std NodeSchema literal threads its one-line doc. Four texts were
corrected against the actual eval/finalize semantics rather than taken
from the plan table verbatim: Delay is a lag-N register (not one-step),
GatedRecorder flushes an ungated final row at finalize, Latch is a
level-sensitive set/reset register (captures no input), SeriesReducer
emits its single summary row at finalize (not per cycle).

Gate: cargo build -p aura-std --lib clean; full std test run follows at
the all-crates gate once strategy/backtest/engine thread their sites
(the earlier per-crate test gate was unsatisfiable in isolation --
std's dev-deps pull crates whose sites belong to later tasks).

refs #316
2026-07-23 15:25:16 +02:00
claude df3c2f06bd feat(core): doc_gate shape check + required NodeSchema.doc field
C29 compile/unit seam, core carrier (iteration 1 of the self-description
spec): DocGateFault { Empty, RestatesName } + doc_gate() — deterministic
string-shape check, never content judgement — and NodeSchema gains the
required pub doc: &'static str, so every construction site must declare
a meaning line (compiler-enumerated). Core's own 10 test literals thread
doc: "test-only schema"; doc_gate/DocGateFault re-exported at crate root.

Downstream crates are E0063-broken by design until their sites thread
the field (tasks 3-7 of the plan); gates here are crate-scoped:
cargo test -p aura-core (59 green) + clippy -p aura-core (clean).

Interim subset commit of the implement run after adjudicating a
cross-task discard: task 2's file checkout had destroyed task 1's
node.rs/lib.rs work; restored from the loop's recovery snapshot
53c55c3 and merged with task 2's field threading.

refs #316
2026-07-23 15:10:42 +02:00
116 changed files with 1447 additions and 108 deletions
+10
View File
@@ -159,6 +159,16 @@ design decision, not a refactor, and belongs in the ledger.
harness and is an execution viewer / trace explorer (structure before, live
streams during, recorded traces after) — never a scene editor; topology is
grown in Rust + hot-reload, runtime params are UI-tunable.
13. **Self-description: every surface explains itself.** Every closed-
vocabulary entry the binary exposes (nodes, metrics, tap slots, folds,
blocks) carries a one-line meaning, enforced at its entry seam —
compile/unit for engine-shipped entries, load for native node crates,
register for the content-addressed store (documents: an optional gated
`description`). The gate is deterministic string shape (`doc_gate`),
never content judgement: the engine never evaluates description text
(no freetext logic hole, invariant 10), descriptions never influence
execution, determinism (C1), or identity ids, and registered artifacts
are never retroactively invalidated. (C29)
## HTML surfaces
+3 -2
View File
@@ -119,8 +119,9 @@ aura sweep crossover.bp.json --list-axes
```
The op kinds are `source`, `input`, `add`, `feed`, `connect`, `expose`, `tap`,
and `gang` (`tap` declares a recorded measurement point on an interior wire —
see the authoring guide). See
`gang`, and `doc` (`tap` declares a recorded measurement point on an interior
wire; `doc` declares the composite's one-line meaning, required at register —
C29; see the authoring guide). See
`aura graph introspect --node <T>` for a type's exact ports and the op-script
grammar in the design ledger for the full semantics.
+1
View File
@@ -1214,6 +1214,7 @@ mod tests {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }],
output: vec![],
params: vec![],
doc: "test-only schema",
}
}
@@ -87,7 +87,12 @@ impl PositionManagement {
.collect();
PrimitiveBuilder::new(
"PositionManagement",
NodeSchema { inputs, output, params: vec![] },
NodeSchema {
inputs,
output,
params: vec![],
doc: "turns bias plus protective stop into a managed position in R",
},
|_| Box::new(PositionManagement::new()),
)
}
+1
View File
@@ -68,6 +68,7 @@ impl SimBroker {
],
output: vec![FieldSpec { name: "equity".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "frictionless sim-optimal broker: integrates held exposure times price return into cumulative pip equity",
},
move |_| Box::new(SimBroker::new(pip_size)),
)
@@ -1,9 +1,9 @@
{
"surface": "campaign_heavy",
"metrics": {
"cpu_percent": 2116.0,
"peak_rss_mb": 98.25,
"wall_s": 5.604210818
"cpu_percent": 2152.0,
"peak_rss_mb": 101.3828125,
"wall_s": 5.572580056
},
"fingerprint": "cells=8 c0s0w17 c0s1v[0.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24] c0s3b2883 c1s0w17 c1s1v[0.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24] c1s3b2883 c2s0w17 c2s1v[0.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24] c2s3b2883 c3s0w17 c3s1v[0.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24] c3s3b2883 c4s0w17 c4s1v[0.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24] c4s3b2883 c5s0w17 c5s1v[0.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24] c5s3b2883 c6s0w17 c6s1v[0.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24] c6s3b2883 c7s0w17 c7s1v[0.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24] c7s3b2883",
"reps": 3,
@@ -12,6 +12,6 @@
"nproc": 24
},
"profile": "release",
"commit": "d62dede",
"date": "2026-07-17"
"commit": "9c7f60b",
"date": "2026-07-23"
}
@@ -1,9 +1,9 @@
{
"surface": "campaign_sweep",
"metrics": {
"cpu_percent": 2170.0,
"peak_rss_mb": 98.21875,
"wall_s": 1.418092216
"cpu_percent": 2190.0,
"peak_rss_mb": 92.203125,
"wall_s": 1.394470036
},
"fingerprint": "cells=8 c0s0w17 c1s0w17 c2s0w17 c3s0w17 c4s0w17 c5s0w17 c6s0w17 c7s0w17",
"reps": 3,
@@ -12,6 +12,6 @@
"nproc": 24
},
"profile": "release",
"commit": "d62dede",
"date": "2026-07-17"
"commit": "9c7f60b",
"date": "2026-07-23"
}
@@ -1,16 +1,16 @@
{
"surface": "cli_fixed_cost",
"metrics": {
"help_ms": 1.5094509999999999,
"run_ms": 3.679743
"help_ms": 1.5046899999999999,
"run_ms": 3.63305
},
"fingerprint": "run_line_fnv=792dc4434f8e3dee",
"fingerprint": "run_line_fnv=6bb0d796f760d140",
"reps": 3,
"host": {
"hostname": "Raki",
"nproc": 24
},
"profile": "release",
"commit": "13d8500",
"date": "2026-07-17"
"commit": "9c7f60b",
"date": "2026-07-23"
}
@@ -1,8 +1,8 @@
{
"surface": "engine_throughput",
"metrics": {
"bars_per_s": 13591474.080269944,
"wall_s": 0.735755367
"bars_per_s": 14005588.039151624,
"wall_s": 0.714000724
},
"fingerprint": "last=0.417351 max_dd=8.274577 flips=265308 n=10000000",
"reps": 3,
@@ -11,6 +11,6 @@
"nproc": 24
},
"profile": "release",
"commit": "d62dede",
"date": "2026-07-17"
"commit": "9c7f60b",
"date": "2026-07-23"
}
@@ -1,8 +1,8 @@
{
"surface": "ingest_throughput",
"metrics": {
"bars_per_s": 12912289.748572402,
"wall_s": 0.388838858
"bars_per_s": 13000457.745164338,
"wall_s": 0.386201786
},
"fingerprint": "n=251040 xor=0019885debd3bd5e",
"reps": 3,
@@ -11,6 +11,6 @@
"nproc": 24
},
"profile": "release",
"commit": "d62dede",
"date": "2026-07-17"
"commit": "9c7f60b",
"date": "2026-07-23"
}
+2 -2
View File
@@ -67,8 +67,8 @@ pub const HEAVY_PROCESS_DOC_QUICK: &str = r#"{
/// `crates/aura-cli/examples/r_sma.json` except the second variant's bound
/// lengths (3/6 instead of 2/4), which gives it a distinct content id — two
/// strategies, same axes surface.
pub const BP_SMA_A: &str = r#"{"format_version":1,"blueprint":{"name":"sma_signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
pub const BP_SMA_B: &str = r#"{"format_version":1,"blueprint":{"name":"sma_signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":6}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
pub const BP_SMA_A: &str = r#"{"format_version":1,"blueprint":{"name":"sma_signal","doc":"fast/slow SMA difference clamped into a directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
pub const BP_SMA_B: &str = r#"{"format_version":1,"blueprint":{"name":"sma_signal","doc":"fast/slow SMA difference clamped into a directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":6}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
fn run_in(bin: &Path, dir: &Path, args: &[&str]) -> Result<(String, Option<i32>), String> {
let out = std::process::Command::new(bin)
@@ -85,7 +85,7 @@ fn metric_vocabulary_covers_all_shipped_scalar_metrics() {
scalar_fields.extend(selection_scalars);
let vocabulary: BTreeSet<String> =
aura_research::metric_vocabulary().iter().map(|s| s.to_string()).collect();
aura_research::metric_vocabulary().iter().map(|m| m.id.to_string()).collect();
let missing_from_vocabulary: Vec<_> = scalar_fields.difference(&vocabulary).collect();
let stale_in_vocabulary: Vec<_> = vocabulary.difference(&scalar_fields).collect();
@@ -128,7 +128,7 @@ fn rankable_roster_is_single_sourced_and_nested() {
}
for name in aura_campaign::PER_MEMBER_METRICS {
assert!(
aura_research::metric_vocabulary().contains(name),
aura_research::metric_vocabulary().iter().any(|m| m.id == *name),
"per-member metric '{name}' missing from the research vocabulary"
);
}
+1 -1
View File
@@ -1 +1 @@
{"format_version":1,"blueprint":{"name":"r_breakout_signal","nodes":[{"primitive":{"type":"Delay","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"RollingMin","name":"channel_lo","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":0,"from_field":0},{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":1,"from_field":0},{"from":2,"to":4,"slot":0,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":5,"slot":1,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":3,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":6,"to":7,"slot":1,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":3,"slot":0},{"node":4,"slot":1}],"source":"F64"}],"output":[{"node":7,"field":0,"name":"bias"}]}}
{"format_version":1,"blueprint":{"name":"r_breakout_signal","doc":"rolling-extreme breakout latched into a long/short signal","nodes":[{"primitive":{"type":"Delay","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"RollingMin","name":"channel_lo","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":0,"from_field":0},{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":1,"from_field":0},{"from":2,"to":4,"slot":0,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":5,"slot":1,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":3,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":6,"to":7,"slot":1,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":3,"slot":0},{"node":4,"slot":1}],"source":"F64"}],"output":[{"node":7,"field":0,"name":"bias"}]}}
+1 -1
View File
@@ -1 +1 @@
{"format_version":1,"blueprint":{"name":"hl_channel","nodes":[{"primitive":{"type":"Delay","name":"prev_high","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"Delay","name":"prev_low","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"RollingMin","name":"channel_lo","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":0,"from_field":0},{"from":2,"to":4,"slot":1,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":4,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":0,"from_field":0},{"from":7,"to":8,"slot":1,"from_field":0}],"input_roles":[{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"low","targets":[{"node":1,"slot":0}],"source":"F64"},{"name":"close","targets":[{"node":4,"slot":0},{"node":5,"slot":1}],"source":"F64"}],"output":[{"node":8,"field":0,"name":"bias"}]}}
{"format_version":1,"blueprint":{"name":"hl_channel","doc":"prior high/low channel breaks latched into a directional signal","nodes":[{"primitive":{"type":"Delay","name":"prev_high","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"Delay","name":"prev_low","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"RollingMin","name":"channel_lo","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":0,"from_field":0},{"from":2,"to":4,"slot":1,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":4,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":0,"from_field":0},{"from":7,"to":8,"slot":1,"from_field":0}],"input_roles":[{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"low","targets":[{"node":1,"slot":0}],"source":"F64"},{"name":"close","targets":[{"node":4,"slot":0},{"node":5,"slot":1}],"source":"F64"}],"output":[{"node":8,"field":0,"name":"bias"}]}}
+1 -1
View File
@@ -1 +1 @@
{"format_version":1,"blueprint":{"name":"r_meanrev_signal","nodes":[{"primitive":{"type":"EMA","name":"mean_window","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Mul"}},{"primitive":{"type":"EMA","name":"var_window","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"Sqrt"}},{"primitive":{"type":"Scale","name":"band","bound":[{"pos":0,"name":"factor","kind":"F64","value":{"F64":2.0}}]}},{"primitive":{"type":"Add"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":1,"from_field":0},{"from":1,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0},{"from":4,"to":5,"slot":0,"from_field":0},{"from":0,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":0,"to":7,"slot":0,"from_field":0},{"from":5,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":1,"from_field":0},{"from":7,"to":9,"slot":0,"from_field":0},{"from":8,"to":10,"slot":0,"from_field":0},{"from":9,"to":10,"slot":1,"from_field":0},{"from":9,"to":11,"slot":0,"from_field":0},{"from":8,"to":11,"slot":1,"from_field":0},{"from":11,"to":12,"slot":0,"from_field":0},{"from":10,"to":12,"slot":1,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0},{"node":8,"slot":0},{"node":9,"slot":1}],"source":"F64"}],"output":[{"node":12,"field":0,"name":"bias"}]}}
{"format_version":1,"blueprint":{"name":"r_meanrev_signal","doc":"EMA deviation against a volatility band, latched into a mean-reversion signal","nodes":[{"primitive":{"type":"EMA","name":"mean_window","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Mul"}},{"primitive":{"type":"EMA","name":"var_window","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"Sqrt"}},{"primitive":{"type":"Scale","name":"band","bound":[{"pos":0,"name":"factor","kind":"F64","value":{"F64":2.0}}]}},{"primitive":{"type":"Add"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":1,"from_field":0},{"from":1,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0},{"from":4,"to":5,"slot":0,"from_field":0},{"from":0,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":0,"to":7,"slot":0,"from_field":0},{"from":5,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":1,"from_field":0},{"from":7,"to":9,"slot":0,"from_field":0},{"from":8,"to":10,"slot":0,"from_field":0},{"from":9,"to":10,"slot":1,"from_field":0},{"from":9,"to":11,"slot":0,"from_field":0},{"from":8,"to":11,"slot":1,"from_field":0},{"from":11,"to":12,"slot":0,"from_field":0},{"from":10,"to":12,"slot":1,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0},{"node":8,"slot":0},{"node":9,"slot":1}],"source":"F64"}],"output":[{"node":12,"field":0,"name":"bias"}]}}
+1 -1
View File
@@ -1 +1 @@
{"format_version":1,"blueprint":{"name":"sma_signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
{"format_version":1,"blueprint":{"name":"sma_signal","doc":"fast/slow SMA difference clamped into a directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
+6
View File
@@ -54,6 +54,9 @@ enum OpDoc {
as_name: String,
into: Vec<String>,
},
/// Declare the composite's one-line meaning (C29, #316) — the op-script
/// twin of the builder's `.doc(...)`.
Doc { text: String },
}
impl OpDoc {
@@ -68,6 +71,7 @@ impl OpDoc {
OpDoc::Expose { .. } => "expose",
OpDoc::Tap { .. } => "tap",
OpDoc::Gang { .. } => "gang",
OpDoc::Doc { .. } => "doc",
}
}
}
@@ -85,6 +89,7 @@ impl From<OpDoc> for Op {
OpDoc::Expose { from, as_name } => Op::Expose { from, as_name },
OpDoc::Tap { from, as_name } => Op::Tap { from, as_name },
OpDoc::Gang { as_name, into } => Op::Gang { as_name, into },
OpDoc::Doc { text } => Op::Doc { text },
}
}
}
@@ -129,6 +134,7 @@ fn format_op_error(e: &OpError) -> String {
}
OpError::GangArity { gang } => format!("gang `{gang}`: needs at least two members"),
OpError::Incomplete(ce) => format!("{ce:?}"),
OpError::DuplicateDoc => "a doc op may appear at most once".to_string(),
}
}
+44 -4
View File
@@ -92,9 +92,10 @@ fn guard_one_mode(cmd: &DocIntrospectCmd, family: &str) {
/// vocabulary, but the mode rides the shared introspect struct and answers
/// for both families.
fn print_metric_roster() {
for name in aura_research::metric_vocabulary() {
let rankable = aura_campaign::RANKABLE_METRICS.contains(name);
let gate = aura_campaign::PER_MEMBER_METRICS.contains(name);
for m in aura_research::metric_vocabulary() {
let name = m.id;
let rankable = aura_campaign::RANKABLE_METRICS.contains(&name);
let gate = aura_campaign::PER_MEMBER_METRICS.contains(&name);
// The generalize applicability is the registry's own R-expectancy
// predicate — no fourth roster site (#190/#207).
let generalize = aura_registry::check_r_metric(name).is_ok();
@@ -181,7 +182,7 @@ pub(crate) fn doc_fault_prose(f: &DocFault) -> String {
DocFault::UnknownEmitKind(kind) => format!("presentation.emit: unknown kind \"{kind}\""),
DocFault::UnknownTap { index, tap } => format!(
"presentation.persist_taps[{index}]: unknown tap \"{tap}\" (taps: {})",
tap_vocabulary().join(" | ")
tap_vocabulary().iter().map(|t| t.id).collect::<Vec<_>>().join(" | ")
),
DocFault::UnknownBindingColumn { role, column } => format!(
"data.bindings.{role}: \"{column}\" names no archive column (columns: {})",
@@ -190,6 +191,16 @@ pub(crate) fn doc_fault_prose(f: &DocFault) -> String {
DocFault::ProcessRefMustBeContentId => {
"process.ref: a process is referenced by content id in this version".into()
}
DocFault::BadDescription { subject, fault } => match fault {
aura_core::DocGateFault::Empty => format!(
"description: `{subject}`'s description is empty — omit the \
field or write a one-line meaning (C29)"
),
aura_core::DocGateFault::RestatesName => format!(
"description: `{subject}`'s description merely restates the \
document name (C29)"
),
},
}
}
@@ -816,6 +827,35 @@ mod tests {
assert!(!prose.contains("UnknownTap"), "Debug leak: {prose}");
}
/// C29 (#316): the description-quality fault names the subject and the
/// rule (C29) for both sub-faults — an empty description and one that
/// merely restates the document name — never leaking the Debug variant
/// names of either `DocFault` or the nested `DocGateFault`.
#[test]
fn bad_description_prose_names_subject_and_rule_for_both_faults() {
let empty = doc_fault_prose(&DocFault::BadDescription {
subject: "ger40-momentum".into(),
fault: aura_core::DocGateFault::Empty,
});
assert!(
empty.contains("`ger40-momentum`'s description is empty"),
"subject and emptiness named: {empty}"
);
assert!(empty.contains("C29"), "rule cited: {empty}");
assert!(!empty.contains("BadDescription") && !empty.contains("DocGateFault"), "Debug leak: {empty}");
let restated = doc_fault_prose(&DocFault::BadDescription {
subject: "ger40-momentum".into(),
fault: aura_core::DocGateFault::RestatesName,
});
assert!(
restated.contains("`ger40-momentum`'s description merely restates the document name"),
"subject and restatement named: {restated}"
);
assert!(restated.contains("C29"), "rule cited: {restated}");
assert!(!restated.contains("BadDescription") && !restated.contains("DocGateFault"), "Debug leak: {restated}");
}
/// #260: the instrument-map key-mismatch fault is path-addressed and names
/// both directions — the campaign instruments the map misses AND the map
/// keys naming no campaign instrument — in one line, no Debug leak. The
+4 -1
View File
@@ -152,6 +152,8 @@ impl Scale {
pub fn new(factor: f64) -> Self {
Self { factor, out: [Cell::from_f64(0.0)] }
}
// Replace `doc` below when renaming this sample node — it must keep
// describing the node's own behaviour, not this scaffolding step.
pub fn builder() -> PrimitiveBuilder {
PrimitiveBuilder::new(
"__NS__::Scale",
@@ -163,6 +165,7 @@ impl Scale {
}],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![ParamSpec { name: "factor".into(), kind: ScalarKind::F64 }],
doc: "scalar gain: emits the input times the factor param",
},
|p| Box::new(Scale::new(p[0].f64())),
)
@@ -219,7 +222,7 @@ crate is attached to its project via `[nodes]` in the project's `Aura.toml`.
const GITIGNORE_PROJECT: &str = "/runs\n";
const SIGNAL_JSON_STD: &str = r#"{"format_version":1,"blueprint":{"name":"__NAME_SNAKE___signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
const SIGNAL_JSON_STD: &str = r#"{"format_version":1,"blueprint":{"name":"__NAME_SNAKE___signal","doc":"fast/slow SMA difference clamped into a directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
const CLAUDE_MD_PROJECT: &str = r#"# __NAME__ — an aura research project (data-only)
+2 -2
View File
@@ -60,9 +60,9 @@ fn persist_taps_from(trace: bool) -> Vec<String> {
tap_vocabulary()
.iter()
.filter(|t| {
aura_runner::runner::tap_channel(t) != Some(aura_runner::runner::TapChannel::Net)
aura_runner::runner::tap_channel(t.id) != Some(aura_runner::runner::TapChannel::Net)
})
.map(|t| t.to_string())
.map(|t| t.id.to_string())
.collect()
} else {
vec![]
+1 -1
View File
@@ -2182,7 +2182,7 @@ const HL_RANGE_CLOSED_BLUEPRINT: &str = r#"{
"output": [{"node":0,"field":0,"name":"bias"}]
}
}"#;
const HL_SIGNAL_OPEN_BLUEPRINT: &str = r#"{"format_version":1,"blueprint":{"name":"hl_signal","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"low","targets":[{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
const HL_SIGNAL_OPEN_BLUEPRINT: &str = r#"{"format_version":1,"blueprint":{"name":"hl_signal","doc":"high/low SMA difference clamped into a directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"low","targets":[{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
/// Property (#231 task 4 quality follow-up): `aura run` over a multi-column
/// blueprint with NO `--real` (the synthetic default) refuses through the
@@ -26,6 +26,7 @@ impl Identity {
}],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "one-input f64 pass-through",
},
|_| Box::new(Identity::new()),
)
@@ -29,6 +29,7 @@ impl Identity {
}],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "one-input f64 pass-through",
},
|_| Box::new(Identity::new()),
)
+1 -1
View File
@@ -1 +1 @@
{"format_version":1,"blueprint":{"name":"r_breakout_signal","nodes":[{"primitive":{"type":"Delay","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi"}},{"primitive":{"type":"RollingMin","name":"channel_lo"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":0,"from_field":0},{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":1,"from_field":0},{"from":2,"to":4,"slot":0,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":5,"slot":1,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":3,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":6,"to":7,"slot":1,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":3,"slot":0},{"node":4,"slot":1}],"source":"F64"}],"output":[{"node":7,"field":0,"name":"bias"}],"gangs":[{"name":"channel_length","kind":"I64","members":[{"node":1,"pos":0,"name":"length"},{"node":2,"pos":0,"name":"length"}]}]}}
{"format_version":1,"blueprint":{"name":"r_breakout_signal","doc":"rolling-extreme breakout latched into a long/short signal","nodes":[{"primitive":{"type":"Delay","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi"}},{"primitive":{"type":"RollingMin","name":"channel_lo"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":0,"from_field":0},{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":1,"from_field":0},{"from":2,"to":4,"slot":0,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":5,"slot":1,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":3,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":6,"to":7,"slot":1,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":3,"slot":0},{"node":4,"slot":1}],"source":"F64"}],"output":[{"node":7,"field":0,"name":"bias"}],"gangs":[{"name":"channel_length","kind":"I64","members":[{"node":1,"pos":0,"name":"length"},{"node":2,"pos":0,"name":"length"}]}]}}
+1 -1
View File
@@ -1 +1 @@
{"format_version":1,"blueprint":{"name":"hl_channel","nodes":[{"primitive":{"type":"Delay","name":"prev_high","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"Delay","name":"prev_low","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi"}},{"primitive":{"type":"RollingMin","name":"channel_lo"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":0,"from_field":0},{"from":2,"to":4,"slot":1,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":4,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":0,"from_field":0},{"from":7,"to":8,"slot":1,"from_field":0}],"input_roles":[{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"low","targets":[{"node":1,"slot":0}],"source":"F64"},{"name":"close","targets":[{"node":4,"slot":0},{"node":5,"slot":1}],"source":"F64"}],"output":[{"node":8,"field":0,"name":"bias"}],"gangs":[{"name":"channel_length","kind":"I64","members":[{"node":2,"pos":0,"name":"length"},{"node":3,"pos":0,"name":"length"}]}]}}
{"format_version":1,"blueprint":{"name":"hl_channel","doc":"prior high/low channel breaks latched into a directional signal","nodes":[{"primitive":{"type":"Delay","name":"prev_high","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"Delay","name":"prev_low","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi"}},{"primitive":{"type":"RollingMin","name":"channel_lo"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":0,"from_field":0},{"from":2,"to":4,"slot":1,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":4,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":0,"from_field":0},{"from":7,"to":8,"slot":1,"from_field":0}],"input_roles":[{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"low","targets":[{"node":1,"slot":0}],"source":"F64"},{"name":"close","targets":[{"node":4,"slot":0},{"node":5,"slot":1}],"source":"F64"}],"output":[{"node":8,"field":0,"name":"bias"}],"gangs":[{"name":"channel_length","kind":"I64","members":[{"node":2,"pos":0,"name":"length"},{"node":3,"pos":0,"name":"length"}]}]}}
+1 -1
View File
@@ -1 +1 @@
{"format_version":1,"blueprint":{"name":"r_meanrev_signal","nodes":[{"primitive":{"type":"EMA","name":"mean_window"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Mul"}},{"primitive":{"type":"EMA","name":"var_window"}},{"primitive":{"type":"Sqrt"}},{"primitive":{"type":"Scale","name":"band"}},{"primitive":{"type":"Add"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":1,"from_field":0},{"from":1,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0},{"from":4,"to":5,"slot":0,"from_field":0},{"from":0,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":0,"to":7,"slot":0,"from_field":0},{"from":5,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":1,"from_field":0},{"from":7,"to":9,"slot":0,"from_field":0},{"from":8,"to":10,"slot":0,"from_field":0},{"from":9,"to":10,"slot":1,"from_field":0},{"from":9,"to":11,"slot":0,"from_field":0},{"from":8,"to":11,"slot":1,"from_field":0},{"from":11,"to":12,"slot":0,"from_field":0},{"from":10,"to":12,"slot":1,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0},{"node":8,"slot":0},{"node":9,"slot":1}],"source":"F64"}],"output":[{"node":12,"field":0,"name":"bias"}],"gangs":[{"name":"window","kind":"I64","members":[{"node":0,"pos":0,"name":"length"},{"node":3,"pos":0,"name":"length"}]}]}}
{"format_version":1,"blueprint":{"name":"r_meanrev_signal","doc":"EMA deviation against a volatility band, latched into a mean-reversion signal","nodes":[{"primitive":{"type":"EMA","name":"mean_window"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Mul"}},{"primitive":{"type":"EMA","name":"var_window"}},{"primitive":{"type":"Sqrt"}},{"primitive":{"type":"Scale","name":"band"}},{"primitive":{"type":"Add"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":1,"from_field":0},{"from":1,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0},{"from":4,"to":5,"slot":0,"from_field":0},{"from":0,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":0,"to":7,"slot":0,"from_field":0},{"from":5,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":1,"from_field":0},{"from":7,"to":9,"slot":0,"from_field":0},{"from":8,"to":10,"slot":0,"from_field":0},{"from":9,"to":10,"slot":1,"from_field":0},{"from":9,"to":11,"slot":0,"from_field":0},{"from":8,"to":11,"slot":1,"from_field":0},{"from":11,"to":12,"slot":0,"from_field":0},{"from":10,"to":12,"slot":1,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0},{"node":8,"slot":0},{"node":9,"slot":1}],"source":"F64"}],"output":[{"node":12,"field":0,"name":"bias"}],"gangs":[{"name":"window","kind":"I64","members":[{"node":0,"pos":0,"name":"length"},{"node":3,"pos":0,"name":"length"}]}]}}
+1 -1
View File
@@ -1 +1 @@
{"format_version":1,"blueprint":{"name":"sma_signal","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
{"format_version":1,"blueprint":{"name":"sma_signal","doc":"fast/slow SMA difference clamped into a directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
@@ -0,0 +1,3 @@
/target
Cargo.lock
/runs
@@ -0,0 +1 @@
# restated fixture — static project context only (C17); paths only (cycle 0102).
@@ -0,0 +1,15 @@
[package]
name = "restated-project"
version = "0.1.0"
edition = "2024"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
aura-core = { path = "../../../../aura-core" }
# Standalone workspace root: never a member of the engine workspace
# (docs/project-layout.md, the empty-[workspace] note).
[workspace]
@@ -0,0 +1,83 @@
//! Fixture project for the C29 load-seam e2e (#316): one fully resolvable
//! pass-through node under the `restated` namespace whose `NodeSchema` doc
//! merely restates its own type id (`DocGateFault::RestatesName`, the fault
//! arm `undescribed-project` does not exercise). The charter is satisfied
//! (prefixed id, list/resolver in sync) so the refusal under test is exactly
//! the doc gate's name-restatement branch.
use aura_core::{
Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder,
ScalarKind,
};
/// One-input f64 pass-through whose doc is a no-content restatement of its
/// own name.
pub struct Echo {
out: [Cell; 1],
}
impl Echo {
pub fn new() -> Self {
Self { out: [Cell::from_f64(0.0)] }
}
pub fn builder() -> PrimitiveBuilder {
PrimitiveBuilder::new(
"restated::Echo",
NodeSchema {
inputs: vec![PortSpec {
kind: ScalarKind::F64,
firing: Firing::Any,
name: "value".into(),
}],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
// The one deliberate violation this fixture exists for: this
// norm-equals the type id "restated::Echo" once punctuation
// and case are stripped, so it carries no meaning beyond the
// name itself.
doc: "Restated Echo",
},
|_| Box::new(Echo::new()),
)
}
}
impl Default for Echo {
fn default() -> Self {
Self::new()
}
}
impl Node for Echo {
fn lookbacks(&self) -> Vec<usize> {
vec![1]
}
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
let w = ctx.f64_in(0);
if w.is_empty() {
return None;
}
self.out[0] = Cell::from_f64(w[0]);
Some(&self.out)
}
fn label(&self) -> String {
"restated::Echo".to_string()
}
}
fn vocabulary(type_id: &str) -> Option<PrimitiveBuilder> {
match type_id {
"restated::Echo" => Some(Echo::builder()),
_ => None,
}
}
fn type_ids() -> &'static [&'static str] {
&["restated::Echo"]
}
aura_core::aura_project! {
namespace: "restated",
vocabulary: vocabulary,
type_ids: type_ids,
}
@@ -0,0 +1,3 @@
/target
Cargo.lock
/runs
@@ -0,0 +1 @@
# undescribed fixture — static project context only (C17); paths only (cycle 0102).
@@ -0,0 +1,15 @@
[package]
name = "undescribed-project"
version = "0.1.0"
edition = "2024"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
aura-core = { path = "../../../../aura-core" }
# Standalone workspace root: never a member of the engine workspace
# (docs/project-layout.md, the empty-[workspace] note).
[workspace]
@@ -0,0 +1,78 @@
//! Fixture project for the C29 load-seam e2e (#316): one fully resolvable
//! pass-through node under the `und` namespace whose `NodeSchema` carries the
//! empty-string alibi `doc: ""` — compile-legal (the field is present), but
//! the load gate must refuse it. The charter is satisfied (prefixed id,
//! list/resolver in sync) so the refusal under test is exactly the doc gate.
use aura_core::{
Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder,
ScalarKind,
};
/// One-input f64 pass-through with a deliberately empty doc.
pub struct Opaque {
out: [Cell; 1],
}
impl Opaque {
pub fn new() -> Self {
Self { out: [Cell::from_f64(0.0)] }
}
pub fn builder() -> PrimitiveBuilder {
PrimitiveBuilder::new(
"und::Opaque",
NodeSchema {
inputs: vec![PortSpec {
kind: ScalarKind::F64,
firing: Firing::Any,
name: "value".into(),
}],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
// The one deliberate violation this fixture exists for.
doc: "",
},
|_| Box::new(Opaque::new()),
)
}
}
impl Default for Opaque {
fn default() -> Self {
Self::new()
}
}
impl Node for Opaque {
fn lookbacks(&self) -> Vec<usize> {
vec![1]
}
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
let w = ctx.f64_in(0);
if w.is_empty() {
return None;
}
self.out[0] = Cell::from_f64(w[0]);
Some(&self.out)
}
fn label(&self) -> String {
"und::Opaque".to_string()
}
}
fn vocabulary(type_id: &str) -> Option<PrimitiveBuilder> {
match type_id {
"und::Opaque" => Some(Opaque::builder()),
_ => None,
}
}
fn type_ids() -> &'static [&'static str] {
&["und::Opaque"]
}
aura_core::aura_project! {
namespace: "und",
vocabulary: vocabulary,
type_ids: type_ids,
}
+141 -3
View File
@@ -54,6 +54,40 @@ const SIGNAL_DOC: &str = r#"[
{"op":"expose","from":"bias.bias","as":"bias"}
]"#;
/// SIGNAL_DOC with the C29 doc op — the register-reaching variant (a store
/// write requires a described composite; build-only tests keep SIGNAL_DOC).
const SIGNAL_DOC_DESCRIBED: &str = r#"[
{"op":"doc","text":"fast/slow SMA difference clamped into a directional bias"},
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":4}}},
{"op":"add","type":"Sub"},
{"op":"add","type":"Bias"},
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
{"op":"connect","from":"fast.value","to":"sub.lhs"},
{"op":"connect","from":"slow.value","to":"sub.rhs"},
{"op":"connect","from":"sub.value","to":"bias.signal"},
{"op":"expose","from":"bias.bias","as":"bias"}
]"#;
/// SIGNAL_DOC with a `doc` op whose text merely restates the op-script
/// composite's name ("graph", the literal `replay("graph", ..)` gives every
/// CLI-built op-script) -- the RestatesName arm of the C29 shape gate,
/// reached via the op-script route rather than a builder-authored blueprint.
const SIGNAL_DOC_NAME_RESTATED: &str = r#"[
{"op":"doc","text":"Graph"},
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":4}}},
{"op":"add","type":"Sub"},
{"op":"add","type":"Bias"},
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
{"op":"connect","from":"fast.value","to":"sub.lhs"},
{"op":"connect","from":"slow.value","to":"sub.rhs"},
{"op":"connect","from":"sub.value","to":"bias.signal"},
{"op":"expose","from":"bias.bias","as":"bias"}
]"#;
/// Every op below applies cleanly (all eager checks pass), but `sub.rhs` is
/// never wired — a 0-cover input slot only the holistic finalize gate can
/// reject. The smallest document that is op-valid yet whole-document-invalid.
@@ -74,6 +108,21 @@ fn graph_build_emits_blueprint_for_a_valid_document() {
assert!(stdout.contains("\"SMA\""), "carries the nodes: {stdout}");
}
/// C29 (#316): the op-script `doc` op's text lands verbatim in the built
/// blueprint's `doc` field -- `graph build` is a pure translation from ops to
/// the canonical envelope, so a doc op is not consumed or reworded on the way
/// through, only threaded (the shape gate is a separate, register-time check;
/// see `graph_register_refuses_an_op_script_doc_that_restates_the_composite_name`).
#[test]
fn graph_build_emits_the_op_script_doc_verbatim_into_the_blueprint() {
let (stdout, _stderr, ok) = run(&["graph", "build"], SIGNAL_DOC_DESCRIBED);
assert!(ok, "exit success");
assert!(
stdout.contains(r#""doc":"fast/slow SMA difference clamped into a directional bias""#),
"the doc op's exact text appears in the blueprint's doc field: {stdout}"
);
}
/// The canonical blueprint artifact carries no trailing newline: `aura graph build`
/// emits exactly the library `blueprint_to_json` bytes (the form #158 content-addresses),
/// not a display-framed line. Byte-canonical across the CLI and library surfaces (#164).
@@ -462,6 +511,47 @@ fn graph_register_is_idempotent() {
assert_eq!(registered_id(&out1), registered_id(&out2), "same id twice");
}
/// C29 register seam end-to-end (#316): a doc-less root composite refuses at
/// `aura graph register` naming the composite and the rule (exit 1, C14); a
/// doc-less named nested composite refuses naming the nested name; the
/// described shipped example registers. The gate lives at the store write
/// every register path shares — this pins its verb surface.
#[test]
fn register_refuses_undescribed_composites_end_to_end() {
let dir = temp_cwd("register-doc-gate");
// doc-less root: the shipped example minus its doc member
let text = std::fs::read_to_string(example("r_sma.json")).expect("read example");
let mut v: serde_json::Value = serde_json::from_str(&text).expect("example parses");
v["blueprint"].as_object_mut().expect("blueprint object").remove("doc");
let docless = dir.join("docless.json");
std::fs::write(&docless, serde_json::to_string(&v).unwrap()).expect("write fixture");
let (out, err, code) = run_in(&dir, &["graph", "register", docless.to_str().unwrap()]);
assert_eq!(code, Some(1), "doc-less root refuses: {out} {err}");
assert!(
err.contains("composite `sma_signal` carries no doc"),
"stderr names the composite and the rule: {err}"
);
assert!(err.contains("C29"), "stderr cites the contract: {err}");
// doc-less named nested composite inside a described root
v["blueprint"]["doc"] = serde_json::Value::String("a described root".into());
v["blueprint"]["nodes"]
.as_array_mut()
.expect("nodes array")
.push(serde_json::json!({"composite": {"name": "inner", "nodes": []}}));
let nested = dir.join("nested.json");
std::fs::write(&nested, serde_json::to_string(&v).unwrap()).expect("write fixture");
let (out, err, code) = run_in(&dir, &["graph", "register", nested.to_str().unwrap()]);
assert_eq!(code, Some(1), "doc-less nested refuses: {out} {err}");
assert!(
err.contains("composite `inner` carries no doc"),
"stderr names the nested composite: {err}"
);
// the described shipped example registers
let (out, err, code) = run_in(&dir, &["graph", "register", &example("r_sma.json")]);
assert_eq!(code, Some(0), "described example registers: {out} {err}");
assert!(out.starts_with("registered blueprint "), "prints the register line: {out}");
}
/// Property (#196, the campaign-axis namespace): `--params <FILE>` prints the
/// RAW composite param space — one `{name}:{kind:?}` line per open param, in
/// lowering order, WITHOUT the harness-wrap prefix `aura sweep --list-axes`
@@ -647,11 +737,12 @@ fn graph_register_rejects_an_unknown_node_type_with_prose() {
#[test]
fn graph_register_accepts_an_op_script_and_stores_the_envelope_content_id() {
let dir = temp_cwd("register-op-script");
// The op-script form (a JSON array of ops), written to a file.
// The op-script form (a JSON array of ops), written to a file. Register
// requires a described composite (C29), so the described twin is used here.
let op_script = dir.join("op-script.json");
std::fs::write(&op_script, SIGNAL_DOC).expect("write op-script fixture");
std::fs::write(&op_script, SIGNAL_DOC_DESCRIBED).expect("write op-script fixture");
// Its `graph build` envelope (a JSON object), the shape `register` already accepts.
let (envelope_bytes, _e, built) = run(&["graph", "build"], SIGNAL_DOC);
let (envelope_bytes, _e, built) = run(&["graph", "build"], SIGNAL_DOC_DESCRIBED);
assert!(built, "graph build produces the envelope");
let envelope = dir.join("envelope.json");
std::fs::write(&envelope, &envelope_bytes).expect("write envelope fixture");
@@ -669,6 +760,33 @@ fn graph_register_accepts_an_op_script_and_stores_the_envelope_content_id() {
registered_id(&env_out),
"op-script and its built envelope register to the same content id"
);
// C29: the doc-less op-script form refuses at register like any other
// doc-less composite entering the store.
let docless_script = dir.join("docless-script.json");
std::fs::write(&docless_script, SIGNAL_DOC).expect("write op-script fixture");
let (dl_out, dl_err, dl_code) =
run_in(&dir, &["graph", "register", docless_script.to_str().unwrap()]);
assert_eq!(dl_code, Some(1), "doc-less op-script refuses: {dl_out} {dl_err}");
assert!(dl_err.contains("carries no doc"), "stderr names the rule: {dl_err}");
assert!(dl_err.contains("C29"), "stderr cites the contract: {dl_err}");
}
/// C29: an op-script's `doc` op is not a bare presence checkbox -- its text
/// goes through the same shape gate a builder-authored doc does. A doc that
/// merely restates the composite's name (here "graph", every CLI op-script's
/// literal name) refuses at register exactly like the builder-authored
/// RestatesName case already pinned at the registry-unit layer, but reached
/// end-to-end through the op-script CLI seam this time.
#[test]
fn graph_register_refuses_an_op_script_doc_that_restates_the_composite_name() {
let dir = temp_cwd("register-op-script-restated-doc");
let restated_script = dir.join("restated-doc-script.json");
std::fs::write(&restated_script, SIGNAL_DOC_NAME_RESTATED).expect("write op-script fixture");
let (out, err, code) = run_in(&dir, &["graph", "register", restated_script.to_str().unwrap()]);
assert_eq!(code, Some(1), "name-restating doc refuses: {out} {err}");
assert!(err.contains("merely restates"), "stderr names the rule: {err}");
assert!(err.contains("C29"), "stderr cites the contract: {err}");
}
/// Property (#202, the same on-ramp seam at the sibling verb): `aura graph
@@ -1157,6 +1275,26 @@ fn graph_build_reports_gang_kind_mismatch_as_prose() {
assert!(!stderr.contains("GangKindMismatch"), "does not leak the Debug variant name: {stderr}");
}
/// The `doc` op's duplicate refusal (a second meaning line in one op-script)
/// reads as prose at the binary seam, attributed to the second `doc` op's
/// index -- never the raw `DuplicateDoc` Debug variant name.
#[test]
fn graph_build_reports_duplicate_doc_fault_as_prose() {
let doc = r#"[
{"op":"source","role":"price","kind":"F64"},
{"op":"doc","text":"first"},
{"op":"doc","text":"second"}
]"#;
let (stdout, stderr, ok) = run(&["graph", "build"], doc);
assert!(!ok, "non-zero exit on a duplicate doc fault");
assert!(stdout.is_empty(), "no blueprint emitted on a fault: {stdout}");
assert!(
stderr.contains("op 2 (doc): a doc op may appear at most once"),
"names the op + cause as prose: {stderr}"
);
assert!(!stderr.contains("DuplicateDoc"), "does not leak the Debug variant name: {stderr}");
}
/// Property (corrupt-gang-blueprint, the `gang_fault_prose` seam): `aura graph
/// register` on a hand-corrupted blueprint whose gangs section fails structural
/// validation (a single-member gang) refuses with the `blueprint_load_prose` ->
+80
View File
@@ -19,6 +19,14 @@ fn badcharter_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/badcharter-project")
}
fn undescribed_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/undescribed-project")
}
fn restated_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/restated-project")
}
fn aura(args: &[&str], cwd: &Path) -> Output {
Command::new(env!("CARGO_BIN_EXE_aura"))
.args(args)
@@ -164,6 +172,78 @@ fn vocabulary_charter_violation_refuses_end_to_end() {
assert!(err.contains("lacks the project prefix"), "stderr must name the charter cause: {err}");
}
/// C29 load seam end-to-end (#316): an extension node whose doc fails the
/// shape gate (the empty-string alibi the compiler cannot catch) refuses at
/// load with the entry and the rule named — through the real
/// `aura-cli::project::load` path (libloading + descriptor read), not merely
/// the pure `doc_gate` unit function. The described twin's load is pinned by
/// `project_run_resolves_demo_node_and_is_bit_identical`.
#[test]
fn undescribed_vocabulary_entry_refuses_end_to_end() {
let dir = undescribed_dir();
let build = Command::new("cargo")
.arg("build")
.current_dir(&dir)
.output()
.expect("spawn cargo build for the undescribed fixture");
assert!(
build.status.success(),
"undescribed fixture build failed:\n{}",
String::from_utf8_lossy(&build.stderr)
);
let run = aura(&["run", "x.json"], &dir);
assert_eq!(
run.status.code(),
Some(1),
"an undescribed vocabulary entry is a runtime refusal (exit 1), \
not a usage error"
);
let err = String::from_utf8_lossy(&run.stderr);
assert!(
err.contains("vocabulary entry `und::Opaque` has an empty doc"),
"stderr must name the entry and the rule: {err}"
);
assert!(err.contains("C29"), "stderr must cite the contract: {err}");
}
/// C29 load seam end-to-end, the sibling fault arm (#316): a doc that merely
/// restates its own type id (`DocGateFault::RestatesName`) refuses at load
/// exactly like the empty-doc case above, through the same real
/// `aura-cli::project::load` path. `RestatesName`'s rendered prose was
/// previously pinned only against the pure `Display` impl in
/// `aura-runner::project`'s unit tests (never reachable at all through the
/// CLI) — a regression that reordered `doc_gate`'s two fault arms, or that
/// skipped the gate call for this arm specifically, would pass every
/// existing test and still ship a self-description hole for exactly this
/// case: an alibi doc that types-checks but says nothing.
#[test]
fn restated_name_vocabulary_entry_refuses_end_to_end() {
let dir = restated_dir();
let build = Command::new("cargo")
.arg("build")
.current_dir(&dir)
.output()
.expect("spawn cargo build for the restated fixture");
assert!(
build.status.success(),
"restated fixture build failed:\n{}",
String::from_utf8_lossy(&build.stderr)
);
let run = aura(&["run", "x.json"], &dir);
assert_eq!(
run.status.code(),
Some(1),
"a name-restating vocabulary entry is a runtime refusal (exit 1), \
not a usage error"
);
let err = String::from_utf8_lossy(&run.stderr);
assert!(
err.contains("vocabulary entry `restated::Echo` has a doc that merely restates its name"),
"stderr must name the entry and the rule: {err}"
);
assert!(err.contains("C29"), "stderr must cite the contract: {err}");
}
/// Force a file's modification time to a fixed instant (explicit mtime, no
/// sleeping — mtime granularity would make a "touch then compare" race).
fn set_mtime(path: &Path, secs_since_epoch: u64) {
+30
View File
@@ -211,3 +211,33 @@ fn attached_crate_namespace_resolves_after_build() {
"{}", String::from_utf8_lossy(&out.stdout)
);
}
/// Property (#316 self-description): the scaffold's own seed node ships a
/// `doc:` line that itself passes the C29 shape gate (`aura_core::doc_gate`)
/// — non-empty and not a bare restatement of the type name. This exact text
/// is what a new extension author copy-pastes as the very first node of
/// their crate; if the scaffold's own doc failed the gate it would teach the
/// wrong pattern (an alibi doc) to every project built from it, and nothing
/// else would catch that — `NodeSchema.doc` is required at compile time
/// (E0063 without it) but its *shape* is never checked by the compiler.
#[test]
fn nodes_new_scaffold_doc_passes_the_doc_gate() {
let cwd = temp_cwd("doc-gate");
assert!(aura(&["new", "lab"], &cwd).status.success());
let proj = cwd.join("lab");
let engine = env!("CARGO_MANIFEST_DIR").to_string() + "/../..";
assert!(aura(&["nodes", "new", "lab-nodes", "--engine-path", &engine], &proj).status.success());
let lib = std::fs::read_to_string(cwd.join("lab-nodes/src/lib.rs")).unwrap();
let doc_line = lib
.lines()
.find(|l| l.trim_start().starts_with("doc:"))
.unwrap_or_else(|| panic!("scaffold must carry a doc: field: {lib}"));
let doc_text = doc_line
.split_once('"')
.and_then(|(_, rest)| rest.rsplit_once('"'))
.map(|(text, _)| text)
.unwrap_or_else(|| panic!("doc: field must be a string literal: {doc_line}"));
aura_core::doc_gate("lab_nodes::Scale", doc_text).unwrap_or_else(|f| {
panic!("scaffold's own doc line fails its own gate: {f:?}{doc_text:?}")
});
}
@@ -37,7 +37,7 @@ const GER40_TO_MS: &str = "1727740799999";
/// via `aura nodes new ... --namespace knob_lab`) supplies the `knob_lab`
/// vocabulary namespace — `Scale` is the starter node every node-crate
/// scaffold emits (#183).
const OPEN_BLUEPRINT: &str = r#"{"format_version":1,"blueprint":{"name":"scaled_signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}},{"primitive":{"type":"knob_lab::Scale","name":"gain"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":4,"field":0,"name":"bias"}]}}"#;
const OPEN_BLUEPRINT: &str = r#"{"format_version":1,"blueprint":{"name":"scaled_signal","doc":"fast/slow SMA bias scaled by an extension gain node","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}},{"primitive":{"type":"knob_lab::Scale","name":"gain"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":4,"field":0,"name":"bias"}]}}"#;
/// The minimal executable process pipeline (one sweep stage) — the campaign
/// cell's methodology reference.
+43
View File
@@ -0,0 +1,43 @@
//! C29 coverage: every engine-shipped vocabulary entry carries a
//! gate-passing one-line meaning. Blocks, metrics, tap slots, folds, and
//! the std node vocabulary — the compile/unit seam of the self-description
//! contract.
use aura_core::doc_gate;
#[test]
fn every_shipped_vocabulary_entry_passes_the_doc_gate() {
// process + campaign blocks (BlockSchema.doc — the pre-existing model)
for b in aura_research::process_vocabulary()
.iter()
.chain(aura_research::campaign_vocabulary().iter())
{
doc_gate(b.id, b.doc)
.unwrap_or_else(|f| panic!("block {}: {:?}", b.id, f));
}
// metrics
for m in aura_research::metric_vocabulary() {
doc_gate(m.id, m.doc)
.unwrap_or_else(|f| panic!("metric {}: {:?}", m.id, f));
}
// tap slots
for t in aura_research::tap_vocabulary() {
doc_gate(t.id, t.doc)
.unwrap_or_else(|f| panic!("tap {}: {:?}", t.id, f));
}
// tap folds (the #283 registry roster already carries docs)
for (name, doc) in aura_runner::tap_plan::FoldRegistry::core().roster() {
doc_gate(name, doc)
.unwrap_or_else(|f| panic!("fold {}: {:?}", name, f));
}
// std node vocabulary: resolve each shipped type id to its schema
let env = aura_runner::project::Env::std();
for &type_id in aura_vocabulary::std_vocabulary_types() {
let builder = env
.resolve(type_id)
.unwrap_or_else(|| panic!("std type {type_id} must resolve"));
let schema = builder.schema();
doc_gate(type_id, schema.doc)
.unwrap_or_else(|f| panic!("node {type_id}: {:?}", f));
}
}
@@ -35,6 +35,7 @@ impl ConstLongBias {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "price".into() }],
output: vec![FieldSpec { name: "bias".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "test-only schema",
},
|_| Box::new(ConstLongBias { out: [Cell::from_f64(0.0)] }),
)
+2 -2
View File
@@ -44,8 +44,8 @@ pub use column::{Column, Window};
pub use ctx::Ctx;
pub use error::KindMismatch;
pub use node::{
zip_params, BindOpError, BoundParam, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec,
PrimitiveBuilder,
doc_gate, zip_params, BindOpError, BoundParam, DocGateFault, FieldSpec, Firing, Node,
NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder,
};
pub use scalar::{Scalar, ScalarKind, Timestamp};
pub use series_fold::SeriesFold;
+92 -3
View File
@@ -345,6 +345,36 @@ pub struct NodeSchema {
pub inputs: Vec<PortSpec>,
pub output: Vec<FieldSpec>,
pub params: Vec<ParamSpec>,
/// One-line meaning of the node — non-load-bearing metadata, shown by
/// introspection surfaces.
pub doc: &'static str,
}
/// Shape fault of a vocabulary entry's meaning line (C29 gate).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DocGateFault {
Empty,
RestatesName,
}
/// Deterministic shape check for a vocabulary entry's meaning line —
/// string shape only, never content judgement (the engine must not
/// evaluate prose).
pub fn doc_gate(name: &str, doc: &str) -> Result<(), DocGateFault> {
if doc.trim().is_empty() {
return Err(DocGateFault::Empty);
}
fn norm(s: &str) -> String {
s.chars()
.filter(|c| c.is_ascii_alphanumeric())
.map(|c| c.to_ascii_lowercase())
.collect()
}
let short_name = name.rsplit("::").next().unwrap_or(name);
if norm(doc) == norm(name) || norm(doc) == norm(short_name) {
return Err(DocGateFault::RestatesName);
}
Ok(())
}
/// The universal composable dataflow unit (C8): a **producer, consumer, or both**.
@@ -446,13 +476,14 @@ mod tests {
inputs: vec![],
output: vec![],
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
doc: "test-only schema",
},
|_| Box::new(Bare),
);
assert_eq!(with.label(), "SMA");
let none = PrimitiveBuilder::new(
"Sub",
NodeSchema { inputs: vec![], output: vec![], params: vec![] },
NodeSchema { inputs: vec![], output: vec![], params: vec![], doc: "test-only schema" },
|_| Box::new(Bare),
);
assert_eq!(none.label(), "Sub");
@@ -482,7 +513,7 @@ mod tests {
fn node_name_strips_namespace_prefix() {
let b = PrimitiveBuilder::new(
"demo::Identity",
NodeSchema { inputs: vec![], output: vec![], params: vec![] },
NodeSchema { inputs: vec![], output: vec![], params: vec![], doc: "test-only schema" },
|_| unreachable!("never built in this test"),
);
assert_eq!(b.node_name(), "identity");
@@ -514,7 +545,7 @@ mod tests {
fn primitive_builder_build_runs_the_closure() {
let f = PrimitiveBuilder::new(
"Bare",
NodeSchema { inputs: vec![], output: vec![], params: vec![] },
NodeSchema { inputs: vec![], output: vec![], params: vec![], doc: "test-only schema" },
|_| Box::new(Bare),
);
assert_eq!(f.params(), Vec::<ParamSpec>::new());
@@ -529,6 +560,7 @@ mod tests {
inputs: vec![],
output: vec![],
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
doc: "test-only schema",
},
|_| Box::new(Bare),
);
@@ -538,6 +570,21 @@ mod tests {
);
}
#[test]
fn schema_carries_declared_doc() {
let b = PrimitiveBuilder::new(
"SMA",
NodeSchema {
inputs: vec![],
output: vec![],
params: vec![],
doc: "simple moving average",
},
|_| Box::new(Bare),
);
assert_eq!(b.schema().doc, "simple moving average");
}
/// A node whose label echoes the param vector its constructor received — lets a
/// test read back the positional vector `.build()` reconstructed after binds.
struct Probe(Vec<Cell>);
@@ -565,6 +612,7 @@ mod tests {
ParamSpec { name: "b".into(), kind: ScalarKind::I64 },
ParamSpec { name: "c".into(), kind: ScalarKind::F64 },
],
doc: "test-only schema",
},
|p| Box::new(Probe(p.to_vec())),
)
@@ -641,6 +689,7 @@ mod tests {
inputs: vec![],
output: vec![],
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
doc: "test-only schema",
},
|_| Box::new(Bare),
);
@@ -659,6 +708,7 @@ mod tests {
ParamSpec { name: "dup".into(), kind: ScalarKind::I64 },
ParamSpec { name: "dup".into(), kind: ScalarKind::I64 },
],
doc: "test-only schema",
},
|_| Box::new(Bare),
);
@@ -674,6 +724,7 @@ mod tests {
inputs: vec![],
output: vec![],
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
doc: "test-only schema",
},
|_| Box::new(Bare),
);
@@ -725,6 +776,7 @@ mod tests {
inputs: vec![],
output: vec![],
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
doc: "test-only schema",
},
|_| Box::new(Bare),
)
@@ -746,6 +798,43 @@ mod tests {
})
);
}
#[test]
fn doc_gate_refuses_empty_and_whitespace() {
assert_eq!(doc_gate("EMA", ""), Err(DocGateFault::Empty));
assert_eq!(doc_gate("EMA", " \t"), Err(DocGateFault::Empty));
}
#[test]
fn doc_gate_refuses_name_restatement() {
assert_eq!(doc_gate("EMA", "EMA"), Err(DocGateFault::RestatesName));
assert_eq!(doc_gate("EMA", "e_m_a"), Err(DocGateFault::RestatesName));
assert_eq!(doc_gate("rolling_max", "Rolling Max"), Err(DocGateFault::RestatesName));
}
/// C29 tightening (#316 fieldtest): for a namespaced entry, the bare
/// display segment is a restatement too — a doc that adds nothing beyond
/// the name must not pass just because the name carries a `::` prefix.
/// Genuine meaning lines that merely *contain* the name stay legal.
#[test]
fn doc_gate_refuses_namespaced_short_name_restatement() {
// The most natural alibi at the extension-author seam: the node's
// own display name as its doc.
assert_eq!(doc_gate("lab316_nodes::Scale", "Scale"), Err(DocGateFault::RestatesName));
// Guard: a real meaning line stays Ok even when it contains the
// short name as a substring...
assert_eq!(
doc_gate("lab316_nodes::Scale", "scalar gain: emits the input times the factor param"),
Ok(())
);
// ...and a namespaced entry with a genuine doc is untouched.
assert_eq!(doc_gate("und::Opaque", "one-input f64 pass-through"), Ok(()));
}
#[test]
fn doc_gate_accepts_a_meaning_line() {
assert_eq!(doc_gate("EMA", "exponential moving average over the input series"), Ok(()));
}
}
#[cfg(test)]
+23 -5
View File
@@ -129,7 +129,15 @@ fn derive_signature(c: &Composite) -> NodeSchema {
.collect();
let mut params = Vec::new();
collect_params(c.nodes(), c.gangs(), "", &mut params);
NodeSchema { inputs, output, params }
NodeSchema {
inputs,
output,
params,
// A derived composite signature is graph wiring, not a vocabulary entry:
// no seam walks this doc. The described surface is the composite's own
// `doc`, gated at register (C29, register seam).
doc: "",
}
}
/// The scalar kind of the interior input slot a composite target addresses,
@@ -1838,6 +1846,7 @@ mod tests {
inputs: vec![],
output: vec![FieldSpec { name: "v".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "test-only schema",
};
let to = NodeSchema {
inputs: vec![
@@ -1846,6 +1855,7 @@ mod tests {
],
output: vec![],
params: vec![],
doc: "test-only schema",
};
assert!(edge_kind_check(&from, 0, &to, 0).is_ok());
assert_eq!(
@@ -2269,7 +2279,7 @@ mod tests {
fn pass1() -> BlueprintNode {
PrimitiveBuilder::new(
"Pass1",
NodeSchema { inputs: vec![f64_any()], output: out_v(), params: vec![] },
NodeSchema { inputs: vec![f64_any()], output: out_v(), params: vec![], doc: "test-only schema" },
|_| Box::new(Pass1 { out: [Cell::from_f64(0.0)] }),
)
.into()
@@ -2277,7 +2287,12 @@ mod tests {
fn join2() -> BlueprintNode {
PrimitiveBuilder::new(
"Join2",
NodeSchema { inputs: vec![f64_any(), f64_any()], output: out_v(), params: vec![] },
NodeSchema {
inputs: vec![f64_any(), f64_any()],
output: out_v(),
params: vec![],
doc: "test-only schema",
},
|_| Box::new(Join2 { out: [Cell::from_f64(0.0)] }),
)
.into()
@@ -2285,7 +2300,7 @@ mod tests {
fn sink_f64() -> BlueprintNode {
PrimitiveBuilder::new(
"SinkF64",
NodeSchema { inputs: vec![f64_any()], output: vec![], params: vec![] },
NodeSchema { inputs: vec![f64_any()], output: vec![], params: vec![], doc: "test-only schema" },
|_| Box::new(SinkF64),
)
.into()
@@ -2297,6 +2312,7 @@ mod tests {
inputs: vec![PortSpec { kind: ScalarKind::I64, firing: Firing::Any, name: "in".into() }],
output: vec![],
params: vec![],
doc: "test-only schema",
},
|_| Box::new(SinkI64),
)
@@ -2849,6 +2865,7 @@ mod tests {
inputs: vec![f64_any()],
output: vec![],
params: vec![],
doc: "test-only schema",
};
let h = Harness::bootstrap(FlatGraph {
nodes: vec![
@@ -3454,6 +3471,7 @@ mod tests {
inputs: vec![PortSpec { kind: ScalarKind::I64, firing: Firing::Any, name: "in".into() }],
output: vec![FieldSpec { name: "v".into(), kind: ScalarKind::I64 }],
params: vec![],
doc: "test-only schema",
},
|_| panic!("build must not run when validation fails pre-build"),
);
@@ -3556,7 +3574,7 @@ mod tests {
// cannot take .named).
let paramless_sma = PrimitiveBuilder::new(
"Pass1",
NodeSchema { inputs: vec![f64_any()], output: out_v(), params: vec![] },
NodeSchema { inputs: vec![f64_any()], output: out_v(), params: vec![], doc: "test-only schema" },
|_| Box::new(Pass1 { out: [Cell::from_f64(0.0)] }),
)
.named("sma");
+46 -4
View File
@@ -42,6 +42,10 @@ pub enum Op {
Tap { from: String, as_name: String },
/// Fuse two or more sibling params into one public knob (#61).
Gang { as_name: String, into: Vec<String> },
/// Declare the composite's one-line meaning (C29, #316) — the op-script
/// twin of the builder's `.doc(...)`. At most one per script; a second
/// is a fault (a declarative script carries no last-wins ambiguity).
Doc { text: String },
}
/// A per-op construction fault, by-identifier so the cause names the op.
@@ -92,6 +96,8 @@ pub enum OpError {
/// A holistic finalize fault (totality / injectivity / unbound root role),
/// wrapping the unchanged engine gate's `CompileError`.
Incomplete(CompileError),
/// A second `doc` op — the meaning line is declared at most once.
DuplicateDoc,
}
/// A per-op-fallible blueprint accumulator. Holds the same interior data a
@@ -113,6 +119,7 @@ pub struct GraphSession<'v> {
tap_names: HashSet<String>,
coverage: HashMap<(usize, usize), usize>,
gangs: Vec<Gang>,
doc: Option<String>,
}
impl<'v> GraphSession<'v> {
@@ -135,6 +142,7 @@ impl<'v> GraphSession<'v> {
tap_names: HashSet::new(),
coverage: HashMap::new(),
gangs: Vec::new(),
doc: None,
}
}
@@ -150,6 +158,13 @@ impl<'v> GraphSession<'v> {
Op::Expose { from, as_name } => self.expose(from, as_name),
Op::Tap { from, as_name } => self.tap(from, as_name),
Op::Gang { as_name, into } => self.gang(as_name, into),
Op::Doc { text } => {
if self.doc.is_some() {
return Err(OpError::DuplicateDoc);
}
self.doc = Some(text);
Ok(())
}
}
}
@@ -424,13 +439,14 @@ impl<'v> GraphSession<'v> {
.into_iter()
.map(|(name, source, targets)| Role { name, targets, source })
.collect();
// No doc is attached here: the op-script vocabulary deliberately has no
// doc-carrying surface yet (#125 scope cut) — an op-built composite
// carries no authored rationale until that vocabulary grows a slot.
let c = Composite::new(self.name, self.nodes, self.edges, roles, self.out)
// The doc op (C29, #316) threads through `Composite::with_doc`.
let mut c = Composite::new(self.name, self.nodes, self.edges, roles, self.out)
.with_taps(self.taps)
.with_gangs(self.gangs)
.map_err(OpError::Incomplete)?;
if let Some(t) = self.doc {
c = c.with_doc(t);
}
check_param_namespace_injective(&c.param_space()).map_err(OpError::Incomplete)?;
validate_wiring(c.nodes(), c.edges(), c.input_roles(), c.output(), c.taps()).map_err(|e| match e {
CompileError::UnconnectedPort { node, slot } => OpError::UnconnectedPort {
@@ -866,6 +882,32 @@ mod tests {
);
}
/// C29 (#316): the doc op sets the composite's meaning line — the
/// op-script twin of the builder's .doc(...).
#[test]
fn doc_op_sets_the_composite_doc() {
let mut s = scaffold();
s.apply(Op::Doc { text: "a described op-built composite".into() }).unwrap();
s.apply(Op::Feed { role: "price".into(), into: vec!["fast.series".into(), "slow.series".into()] })
.unwrap();
s.apply(Op::Connect { from: "fast.value".into(), to: "sub.lhs".into() }).unwrap();
s.apply(Op::Connect { from: "slow.value".into(), to: "sub.rhs".into() }).unwrap();
s.apply(Op::Expose { from: "sub.value".into(), as_name: "out".into() }).unwrap();
let c = s.finish().expect("finishes");
assert_eq!(c.doc(), Some("a described op-built composite"));
}
/// A second doc op is a fault, not a silent overwrite.
#[test]
fn duplicate_doc_op_is_refused() {
let mut s = scaffold();
s.apply(Op::Doc { text: "first".into() }).unwrap();
assert_eq!(
s.apply(Op::Doc { text: "second".into() }),
Err(OpError::DuplicateDoc)
);
}
#[test]
fn finish_reports_incomplete_on_unwired_slot() {
// a fully-named scaffold missing the sub.rhs connection -> holistic 0-arm
+2
View File
@@ -348,6 +348,7 @@ mod tests {
],
output: vec![FieldSpec { name: "diff".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "test-only schema",
},
|_| Box::new(Bare),
)
@@ -462,6 +463,7 @@ mod tests {
FieldSpec { name: "lo".into(), kind: ScalarKind::I64 },
],
params: vec![],
doc: "test-only schema",
},
|_| Box::new(Bare),
)
+9
View File
@@ -878,6 +878,7 @@ mod tests {
inputs: kinds.iter().enumerate().map(|(i, &kind)| PortSpec { kind, firing, name: format!("col[{i}]") }).collect(),
output: vec![],
params: vec![],
doc: "test-only schema",
}
}
@@ -895,6 +896,7 @@ mod tests {
inputs: vec![f64_port(Firing::Any), f64_port(Firing::Any)],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "test-only schema",
}
}
}
@@ -924,6 +926,7 @@ mod tests {
inputs: vec![f64_port(Firing::Barrier(0)), f64_port(Firing::Barrier(0))],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "test-only schema",
}
}
}
@@ -951,6 +954,7 @@ mod tests {
inputs: vec![f64_port(Firing::Any)],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::Bool }],
params: vec![],
doc: "test-only schema",
}
}
}
@@ -984,6 +988,7 @@ mod tests {
],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "test-only schema",
}
}
}
@@ -1022,6 +1027,7 @@ mod tests {
FieldSpec { name: "volume".into(), kind: ScalarKind::F64 },
],
params: vec![],
doc: "test-only schema",
}
}
}
@@ -1057,6 +1063,7 @@ mod tests {
FieldSpec { name: "i".into(), kind: ScalarKind::I64 },
],
params: vec![],
doc: "test-only schema",
}
}
}
@@ -1084,6 +1091,7 @@ mod tests {
inputs: vec![f64_port(Firing::Any)],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "test-only schema",
}
}
}
@@ -2442,6 +2450,7 @@ mod tests {
FieldSpec { name: "i".into(), kind: ScalarKind::I64 },
],
params: vec![],
doc: "test-only schema",
}
}
}
+1
View File
@@ -43,6 +43,7 @@ fn recorder_sig(kinds: &[ScalarKind]) -> NodeSchema {
.collect(),
output: vec![],
params: vec![],
doc: "test-only schema",
}
}
+1
View File
@@ -29,6 +29,7 @@ fn run_sample_over(prices: Vec<(Timestamp, Scalar)>) -> RunReport {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }],
output: vec![],
params: vec![],
doc: "test-only schema",
};
let mut h = Harness::bootstrap(FlatGraph {
nodes: vec![
@@ -35,6 +35,7 @@ fn run_sample(window: (Timestamp, Timestamp), source: Box<dyn Source>) -> RunRep
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }],
output: vec![],
params: vec![],
doc: "test-only schema",
};
let mut h = Harness::bootstrap(FlatGraph {
nodes: vec![
+1
View File
@@ -88,6 +88,7 @@ impl Resample {
FieldSpec { name: "close".into(), kind: ScalarKind::F64 },
],
params: vec![ParamSpec { name: "period_minutes".into(), kind: ScalarKind::I64 }],
doc: "emits a completed bar once per resample boundary, never mid-bar",
},
|p| Box::new(Resample::new(p[0].i64())),
)
+2
View File
@@ -72,6 +72,7 @@ impl Session {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "trigger".into() }],
output: vec![FieldSpec { name: "bars_since_open".into(), kind: ScalarKind::I64 }],
params: vec![],
doc: "bars elapsed since the session open, from the configured open time and timezone",
},
move |_| Box::new(Session::new(open_hour, open_minute, tz, period_minutes)),
)
@@ -99,6 +100,7 @@ impl SessionFrankfurt {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "trigger".into() }],
output: vec![FieldSpec { name: "bars_since_open".into(), kind: ScalarKind::I64 }],
params: vec![ParamSpec { name: "period_minutes".into(), kind: ScalarKind::I64 }],
doc: "bars elapsed since the Frankfurt session open (09:00 Europe/Berlin)",
},
|p| Box::new(Session::new(9, 0, chrono_tz::Europe::Berlin, p[0].i64())),
)
+111 -9
View File
@@ -21,8 +21,8 @@ use aura_core::PrimitiveBuilder;
use aura_analysis::{one_sided_p_laplace, MetricStats, MetricVocabulary, SplitMix64};
use aura_backtest::{RunMetrics, RunReport};
use aura_engine::{
blueprint_from_json, blueprint_identity_json, expected_max_of_normals, FamilySelection,
SelectionMode,
blueprint_from_json, blueprint_identity_json, expected_max_of_normals, BlueprintDoc,
CompositeData, FamilySelection, NodeData, SelectionMode,
};
use aura_research::{CampaignDoc, DocRef};
@@ -143,12 +143,26 @@ impl Registry {
/// so a repeated write re-writes identical content. The registry does NOT
/// verify `sha256(bytes) == hash` (no `sha2` dep here): the caller owns the
/// hash, and reproduction's bit-identical metric compare is the integrity check.
pub fn put_blueprint(&self, hash: &str, canonical_json: &str) -> Result<(), RegistryError> {
/// Raw write — C29-gated callers go through [`Registry::put_blueprint`]; this
/// stays for store-mechanics tests and pre-C29 store content.
fn put_blueprint_unchecked(&self, hash: &str, canonical_json: &str) -> Result<(), RegistryError> {
fs::create_dir_all(self.blueprints_dir())?;
fs::write(self.blueprint_path(hash), canonical_json)?;
Ok(())
}
/// Write-once content-addressed put, C29-gated (#316): only a canonical
/// blueprint whose root composite and every named nested composite carry
/// a gate-passing doc enters the store. The gate guards the store
/// boundary itself — verb, sugar, and any future caller alike — and the
/// write path only: already-registered doc-less entries stay readable.
pub fn put_blueprint(&self, hash: &str, canonical_json: &str) -> Result<(), RegistryError> {
let doc: BlueprintDoc = serde_json::from_str(canonical_json)
.map_err(RegistryError::MalformedBlueprint)?;
gate_composite_docs(&doc.blueprint)?;
self.put_blueprint_unchecked(hash, canonical_json)
}
/// Read a stored blueprint by content id; `Ok(None)` if absent — the same
/// treat-as-empty discipline `load` applies to a missing runs store.
pub fn get_blueprint(&self, hash: &str) -> Result<Option<String>, RegistryError> {
@@ -259,6 +273,25 @@ impl Registry {
}
}
/// C29 walk (#316): the root and every named nested composite must carry a
/// gate-passing doc. `doc: None` refuses exactly like `Some("")` — both are
/// the Empty fault ("carries no doc").
fn gate_composite_docs(c: &CompositeData) -> Result<(), RegistryError> {
let fault = match c.doc.as_deref() {
None => Some(aura_core::DocGateFault::Empty),
Some(d) => aura_core::doc_gate(&c.name, d).err(),
};
if let Some(fault) = fault {
return Err(RegistryError::UndescribedComposite { name: c.name.clone(), fault });
}
for n in &c.nodes {
if let NodeData::Composite(nested) = n {
gate_composite_docs(nested)?;
}
}
Ok(())
}
/// Referential-validation findings for a campaign document. By-identifier
/// and Display-free (the CLI phrases them).
#[derive(Clone, Debug, PartialEq)]
@@ -897,6 +930,14 @@ pub enum RegistryError {
NonRMetric(String),
/// A generalization was asked for with fewer than two instruments.
TooFewInstruments(usize),
/// C29 register seam (#316): the canonical bytes offered to the blueprint
/// store do not parse as a canonical blueprint — the gate cannot certify
/// what it cannot read.
MalformedBlueprint(serde_json::Error),
/// C29 register seam (#316): a root or named nested composite entering
/// the store without a gate-passing doc. Registered artifacts are never
/// retroactively invalidated — the gate guards the write path only.
UndescribedComposite { name: String, fault: aura_core::DocGateFault },
}
impl fmt::Display for RegistryError {
@@ -920,6 +961,23 @@ impl fmt::Display for RegistryError {
f,
"a generalization needs >= 2 instruments, got {n}"
),
RegistryError::MalformedBlueprint(e) => {
write!(f, "blueprint: not a canonical blueprint: {e}")
}
RegistryError::UndescribedComposite { name, fault } => match fault {
aura_core::DocGateFault::Empty => write!(
f,
"blueprint: composite `{name}` carries no doc — a registered \
composite describes itself (C29); add a doc line (builder \
.doc(\"...\"), blueprint \"doc\" member, or op-script \
{{\"op\":\"doc\"}}) before register"
),
aura_core::DocGateFault::RestatesName => write!(
f,
"blueprint: composite `{name}` has a doc that merely restates \
its name a registered composite describes itself (C29)"
),
},
}
}
}
@@ -1070,13 +1128,50 @@ mod tests {
fn blueprint_store_round_trips_by_content_id() {
let reg = Registry::open(temp_family_dir("blueprint_store_round_trip"));
let canonical = r#"{"format_version":1,"input_roles":[],"nodes":[],"edges":[]}"#;
reg.put_blueprint("deadbeef", canonical).expect("put");
reg.put_blueprint_unchecked("deadbeef", canonical).expect("put");
// exact bytes come back, keyed by content id
assert_eq!(reg.get_blueprint("deadbeef").expect("get"), Some(canonical.to_string()));
// an absent id is Ok(None), not an error (treat-as-empty discipline)
assert_eq!(reg.get_blueprint("never-written").expect("get"), None);
}
/// C29 (#316): the store refuses a doc-less root (None and "" alike),
/// a name-restating doc, and admits a described composite. The raw
/// unchecked path still round-trips pre-C29 doc-less bytes — registered
/// artifacts are never retroactively invalidated.
#[test]
fn blueprint_store_gates_docs_at_the_write_boundary() {
let reg = Registry::open(temp_family_dir("blueprint_store_doc_gate"));
let docless = r#"{"format_version":1,"blueprint":{"name":"s","nodes":[]}}"#;
let e = reg.put_blueprint("h1", docless).expect_err("doc-less root must refuse");
assert!(
e.to_string().contains("composite `s` carries no doc"),
"prose names the composite and the rule: {e}"
);
let restated = r#"{"format_version":1,"blueprint":{"name":"echo","doc":"Echo","nodes":[]}}"#;
let e = reg.put_blueprint("h2", restated).expect_err("restated doc must refuse");
assert!(e.to_string().contains("merely restates"), "prose names the rule: {e}");
let described = r#"{"format_version":1,"blueprint":{"name":"s","doc":"a described fixture blueprint","nodes":[]}}"#;
reg.put_blueprint("h3", described).expect("described root registers");
assert_eq!(reg.get_blueprint("h3").expect("get"), Some(described.to_string()));
// never-retroactive witness: a pre-C29 doc-less entry stays readable
reg.put_blueprint_unchecked("h4", docless).expect("raw write");
assert_eq!(reg.get_blueprint("h4").expect("get"), Some(docless.to_string()));
}
/// A doc-less named NESTED composite refuses even when the root is
/// described — the walk covers every named nested composite (C29).
#[test]
fn blueprint_store_gates_nested_composite_docs() {
let reg = Registry::open(temp_family_dir("blueprint_store_nested_doc_gate"));
let nested_docless = r#"{"format_version":1,"blueprint":{"name":"root","doc":"a described root","nodes":[{"composite":{"name":"inner","nodes":[]}}]}}"#;
let e = reg.put_blueprint("h1", nested_docless).expect_err("doc-less nested must refuse");
assert!(
e.to_string().contains("composite `inner` carries no doc"),
"prose names the nested composite: {e}"
);
}
#[test]
fn document_stores_round_trip_by_content_id() {
let reg = Registry::open(temp_family_dir("document_store_round_trip"));
@@ -1810,7 +1905,8 @@ mod tests {
vec![],
vec![],
vec![],
);
)
.with_doc("fixture composite for the registry seeder test");
let space = composite.param_space();
let real = space.first().expect("fixture composite has an open param");
let real_name = real.name.clone();
@@ -1941,6 +2037,7 @@ mod tests {
vec![],
vec![],
)
.with_doc("fixture composite for the registry seeder test")
}
/// A second, identity-distinct fixture: same shape over the zero-arg
@@ -1955,6 +2052,7 @@ mod tests {
vec![],
vec![],
)
.with_doc("fixture composite for the registry seeder test")
}
#[test]
@@ -2095,7 +2193,8 @@ mod tests {
vec![],
vec![],
vec![],
);
)
.with_doc("fixture composite for the registry seeder test");
let (_json_b, content_b, identity_twin) = seed_blueprint(&reg, &twin);
assert_eq!(identity, identity_twin, "twins must share one identity id");
assert_ne!(content_a, content_b, "twins must have distinct content ids");
@@ -2150,7 +2249,8 @@ mod tests {
vec![],
vec![],
vec![],
);
)
.with_doc("fixture composite for the registry seeder test");
assert!(composite.param_space().is_empty(), "fixture is fully bound");
let bound = composite.bound_param_space();
let bound_param = bound.first().expect("fixture has one bound param");
@@ -2243,7 +2343,8 @@ mod tests {
vec![],
vec![],
vec![],
);
)
.with_doc("fixture composite for the registry seeder test");
let space = composite.param_space();
assert_eq!(space.len(), 2, "fixture exposes two open params");
let covered = space[0].name.clone(); // e.g. "b1.scale"
@@ -2316,7 +2417,8 @@ mod tests {
source: Some(aura_core::ScalarKind::F64),
}],
vec![],
);
)
.with_doc("fixture composite for the registry seeder test");
let space = composite.param_space();
let axis = space.first().expect("fixture has an open param").name.clone();
let blueprint_json = blueprint_to_json(&composite).expect("serializes");
+114 -28
View File
@@ -13,7 +13,7 @@
use std::collections::BTreeMap;
use aura_core::{Scalar, ScalarKind};
use aura_core::{doc_gate, DocGateFault, Scalar, ScalarKind};
use serde::{Deserialize, Serialize};
/// The one accepted document format version.
@@ -727,6 +727,14 @@ pub fn content_id_of(canonical: &str) -> String {
// Intrinsic validation (store-free).
// ---------------------------------------------------------------------------
/// A shipped metric with its one-line meaning (C29). One carrier — the
/// #190 guard keeps this list, the RMetrics serde fields, and the CLI
/// count in lockstep; the doc column adds no fourth roster site.
pub struct MetricSchema {
pub id: &'static str,
pub doc: &'static str,
}
/// The closed declared-metric vocabulary: the scalar field names of the
/// shipped metrics/selection types (RMetrics + RunMetrics scalars, plus the
/// FamilySelection annotation scores). Single source for gate/stage checks
@@ -746,25 +754,25 @@ pub fn content_id_of(canonical: &str) -> String {
/// hand when the upstream field sets change (this list stays a hand-copy by
/// the crate's ratified core-only isolation; #147 single-sourced the rankable
/// roster elsewhere and the extended #190 guard pins this list's nesting).
pub fn metric_vocabulary() -> &'static [&'static str] {
pub fn metric_vocabulary() -> &'static [MetricSchema] {
&[
"expectancy_r",
"win_rate",
"avg_win_r",
"avg_loss_r",
"profit_factor",
"max_r_drawdown",
"sqn",
"sqn_normalized",
"net_expectancy_r",
"n_trades",
"n_open_at_end",
"total_pips",
"max_drawdown",
"bias_sign_flips",
"deflated_score",
"overfit_probability",
"neighbourhood_score",
MetricSchema { id: "expectancy_r", doc: "mean realized R per closed trade — the headline E[R]" },
MetricSchema { id: "win_rate", doc: "fraction of closed trades with positive realized R" },
MetricSchema { id: "avg_win_r", doc: "mean realized R over winning trades" },
MetricSchema { id: "avg_loss_r", doc: "mean realized R over losing trades" },
MetricSchema { id: "profit_factor", doc: "gross winning R divided by absolute gross losing R" },
MetricSchema { id: "max_r_drawdown", doc: "deepest peak-to-trough decline of the cumulative R curve" },
MetricSchema { id: "sqn", doc: "System Quality Number: mean R over its std deviation, scaled by trade count" },
MetricSchema { id: "sqn_normalized", doc: "SQN normalized to a 100-trade basis for n-independent comparison" },
MetricSchema { id: "net_expectancy_r", doc: "expectancy net of the cost model's per-trade cost in R" },
MetricSchema { id: "n_trades", doc: "count of closed trades in the run" },
MetricSchema { id: "n_open_at_end", doc: "positions still open when the run ends" },
MetricSchema { id: "total_pips", doc: "summed pip result over all closed trades" },
MetricSchema { id: "max_drawdown", doc: "deepest peak-to-trough decline of the pip equity curve" },
MetricSchema { id: "bias_sign_flips", doc: "count of sign changes in the strategy's bias stream" },
MetricSchema { id: "deflated_score", doc: "selection score deflated for multiple testing against the bootstrap null" },
MetricSchema { id: "overfit_probability", doc: "probability the selected edge is overfit, from the bootstrap null" },
MetricSchema { id: "neighbourhood_score", doc: "plateau robustness: score aggregated over the param-space neighbourhood" },
]
}
@@ -773,12 +781,23 @@ pub fn emit_vocabulary() -> &'static [&'static str] {
&["family_table", "selection_report"]
}
/// A closed-vocabulary persisted-tap slot with its one-line meaning (C29).
pub struct TapSchema {
pub id: &'static str,
pub doc: &'static str,
}
/// The wrap convention's persisted sink names — the closed tap vocabulary
/// (#201 decision 1). A genuinely new observable becomes a new entry or an
/// authored sink in the strategy blueprint (C22: the choice of sinks is part
/// of the experiment) — never an open node-path namespace in the document.
pub fn tap_vocabulary() -> &'static [&'static str] {
&["equity", "exposure", "r_equity", "net_r_equity"]
pub fn tap_vocabulary() -> &'static [TapSchema] {
&[
TapSchema { id: "equity", doc: "cumulative pip equity per cycle" },
TapSchema { id: "exposure", doc: "signed position exposure per cycle" },
TapSchema { id: "r_equity", doc: "cumulative gross R per cycle" },
TapSchema { id: "net_r_equity", doc: "cumulative net R per cycle, after the cost model" },
]
}
/// The closed archive-column vocabulary a campaign `data.bindings` VALUE may
@@ -821,16 +840,24 @@ pub enum DocFault {
UnknownTap { index: usize, tap: String },
UnknownBindingColumn { role: String, column: String },
ProcessRefMustBeContentId,
/// C29 (#316): a document's optional top-level `description` is present
/// but fails the shape gate. Absent is never a fault.
BadDescription { subject: String, fault: DocGateFault },
}
fn is_known_metric(name: &str) -> bool {
metric_vocabulary().contains(&name)
metric_vocabulary().iter().any(|m| m.id == name)
}
/// Intrinsic validation of a process document (P1 constraints included).
/// Returns ALL findings, not the first.
pub fn validate_process(doc: &ProcessDoc) -> Vec<DocFault> {
let mut faults = Vec::new();
if let Some(desc) = &doc.description
&& let Err(fault) = doc_gate(&doc.name, desc)
{
faults.push(DocFault::BadDescription { subject: doc.name.clone(), fault });
}
if doc.pipeline.is_empty() {
faults.push(DocFault::EmptyPipeline);
}
@@ -894,6 +921,11 @@ pub fn validate_process(doc: &ProcessDoc) -> Vec<DocFault> {
/// Returns ALL findings, not the first.
pub fn validate_campaign(doc: &CampaignDoc) -> Vec<DocFault> {
let mut faults = Vec::new();
if let Some(desc) = &doc.description
&& let Err(fault) = doc_gate(&doc.name, desc)
{
faults.push(DocFault::BadDescription { subject: doc.name.clone(), fault });
}
if doc.data.instruments.is_empty() {
faults.push(DocFault::EmptyInstruments);
}
@@ -1000,7 +1032,7 @@ pub fn validate_campaign(doc: &CampaignDoc) -> Vec<DocFault> {
}
}
for (i, t) in doc.presentation.persist_taps.iter().enumerate() {
if !tap_vocabulary().contains(&t.as_str()) {
if !tap_vocabulary().iter().any(|k| k.id == t.as_str()) {
faults.push(DocFault::UnknownTap { index: i, tap: t.clone() });
}
}
@@ -1280,7 +1312,7 @@ pub fn open_slots_campaign(text: &str) -> Result<Vec<OpenSlot>, DocError> {
"presentation",
format!(
"required section: persist_taps ({}) + emit",
tap_vocabulary().join(" | ")
tap_vocabulary().iter().map(|t| t.id).collect::<Vec<_>>().join(" | ")
),
));
}
@@ -1881,6 +1913,56 @@ mod tests {
assert_eq!(validate_process(&doc), Vec::new());
}
/// C29 (#316): a present document description passes the shape gate;
/// absent is never a fault. Both fault arms fire and carry the doc name.
#[test]
fn document_description_gate_flags_empty_and_restatement() {
let base = r#"{"format_version":1,"kind":"process","name":"p","pipeline":[{"block":"std::generalize","metric":"sqn"}]}"#;
let mut doc: ProcessDoc = serde_json::from_str(base).expect("fixture parses");
assert!(validate_process(&doc).is_empty(), "no description → no fault");
doc.description = Some(" ".into());
assert!(matches!(
validate_process(&doc).as_slice(),
[DocFault::BadDescription { subject, fault: DocGateFault::Empty }] if subject == "p"
));
doc.description = Some("P".into());
assert!(matches!(
validate_process(&doc).as_slice(),
[DocFault::BadDescription { fault: DocGateFault::RestatesName, .. }]
));
doc.description = Some("walk-forward validation methodology".into());
assert!(validate_process(&doc).is_empty(), "described → clean");
}
/// The campaign side takes the identical gate, exercised over the
/// crate's existing valid campaign fixture (`CAMPAIGN_FIXTURE`, which
/// validates clean — pinned by `validate_campaign_accepts_a_valid_risk_section`).
#[test]
fn campaign_description_gate_mirrors_process() {
let mut doc: CampaignDoc = serde_json::from_str(CAMPAIGN_FIXTURE).unwrap();
assert!(validate_campaign(&doc).is_empty(), "fixture validates clean");
doc.description = Some("".into());
assert!(matches!(
validate_campaign(&doc).as_slice(),
[DocFault::BadDescription { fault: DocGateFault::Empty, .. }]
));
doc.description = Some("GER40 walk-forward over the momentum stack".into());
assert!(validate_campaign(&doc).is_empty(), "described → clean");
}
/// C29 (#316): a present description participates in the content id
/// (absent ⇒ byte-identical is pinned by the existing round-trips).
#[test]
fn document_description_participates_in_the_content_id() {
let base = r#"{"format_version":1,"kind":"process","name":"p","pipeline":[{"block":"std::generalize","metric":"sqn"}]}"#;
let bare: ProcessDoc = serde_json::from_str(base).unwrap();
let mut described: ProcessDoc = serde_json::from_str(base).unwrap();
described.description = Some("walk-forward validation methodology".into());
let bare_id = content_id_of(&process_to_json(&bare));
let described_id = content_id_of(&process_to_json(&described));
assert_ne!(bare_id, described_id, "description must move the content id");
}
#[test]
fn validate_process_accepts_the_fixture_and_reports_each_fault() {
let ok = parse_process(PROCESS_FIXTURE).unwrap();
@@ -2072,7 +2154,8 @@ mod tests {
/// never an open node-path namespace in the document.
#[test]
fn tap_vocabulary_is_the_wrap_conventions_four_sink_names() {
assert_eq!(tap_vocabulary(), ["equity", "exposure", "r_equity", "net_r_equity"]);
let ids: Vec<&str> = tap_vocabulary().iter().map(|t| t.id).collect();
assert_eq!(ids, ["equity", "exposure", "r_equity", "net_r_equity"]);
}
/// Each `presentation.persist_taps` entry outside `tap_vocabulary()` is an
@@ -2092,7 +2175,7 @@ mod tests {
);
// every in-vocabulary name passes
let mut all = ok.clone();
all.presentation.persist_taps = tap_vocabulary().iter().map(|t| t.to_string()).collect();
all.presentation.persist_taps = tap_vocabulary().iter().map(|t| t.id.to_string()).collect();
assert_eq!(validate_campaign(&all), Vec::new());
}
@@ -2157,7 +2240,10 @@ mod tests {
assert_eq!(slot.kind, SlotKind::TapKinds);
assert_eq!(
slot_kind_label(slot.kind),
format!("list of: {}", tap_vocabulary().join(" | "))
format!(
"list of: {}",
tap_vocabulary().iter().map(|t| t.id).collect::<Vec<_>>().join(" | ")
)
);
assert_eq!(
slot_kind_label(slot.kind),
@@ -2188,7 +2274,7 @@ mod tests {
pres.hint,
format!(
"required section: persist_taps ({}) + emit",
tap_vocabulary().join(" | ")
tap_vocabulary().iter().map(|t| t.id).collect::<Vec<_>>().join(" | ")
)
);
assert_eq!(
+3
View File
@@ -782,6 +782,7 @@ pub fn reopen_all(signal: Composite, overrides: &[String]) -> Composite {
fn r_breakout_signal(channel: Option<i64>) -> Composite {
use aura_std::{Delay, Gt, Latch};
let mut g = GraphBuilder::new("r_breakout_signal");
g.doc("rolling-extreme breakout latched into a long/short signal");
let delay = g.add(Delay::builder().bind("lag", Scalar::i64(1)));
let mut mx_b = RollingMax::builder().named("channel_hi");
let mut mn_b = RollingMin::builder().named("channel_lo");
@@ -827,6 +828,7 @@ fn r_breakout_signal(channel: Option<i64>) -> Composite {
fn r_meanrev_signal(window: Option<i64>, band_k: Option<f64>) -> Composite {
use aura_std::{Add, Ema, Gt, Latch, Mul, Scale, Sqrt};
let mut g = GraphBuilder::new("r_meanrev_signal");
g.doc("EMA deviation against a volatility band, latched into a mean-reversion signal");
let (mut mean_b, mut var_b) =
(Ema::builder().named("mean_window"), Ema::builder().named("var_window"));
if let Some(n) = window {
@@ -891,6 +893,7 @@ fn r_meanrev_signal(window: Option<i64>, band_k: Option<f64>) -> Composite {
fn r_channel_signal(channel: Option<i64>) -> Composite {
use aura_std::{Delay, Gt, Latch};
let mut g = GraphBuilder::new("hl_channel");
g.doc("prior high/low channel breaks latched into a directional signal");
let delay_hi = g.add(Delay::builder().named("prev_high").bind("lag", Scalar::i64(1)));
let delay_lo = g.add(Delay::builder().named("prev_low").bind("lag", Scalar::i64(1)));
let mut mx_b = RollingMax::builder().named("channel_hi");
+59 -1
View File
@@ -7,7 +7,7 @@
//! vocabulary charter. `Env` is the per-invocation context every verb reads:
//! merged resolver, runs root, data path, provenance.
use aura_core::PrimitiveBuilder;
use aura_core::{DocGateFault, PrimitiveBuilder, doc_gate};
use aura_core::project::{
AURA_DESCRIPTOR_MAGIC, AURA_DESCRIPTOR_VERSION, AURA_PROJECT_SYMBOL,
CORE_VERSION, ProjectDescriptor, RUSTC_VERSION, StrSlice,
@@ -63,6 +63,11 @@ pub enum ProjectError {
/// handing off to `load_crate`, so this never reaches the cargo-metadata
/// probe (whose raw os-error text would otherwise leak through).
PointerDirMissing(PathBuf),
/// C29 load seam (#316): a resolved extension-vocabulary entry whose doc
/// fails the shape gate — the load refuses rather than admit an
/// undescribed node. Field presence is compile-enforced; shape is not,
/// so the gate carries the failed rule for the refusal prose.
UndescribedVocabularyEntry { type_id: String, fault: DocGateFault },
}
impl fmt::Display for ProjectError {
@@ -100,6 +105,19 @@ impl fmt::Display for ProjectError {
"node crate at `{pointer}` (from [nodes] in Aura.toml): {inner}"
),
Self::PointerDirMissing(p) => write!(f, "{} does not exist", p.display()),
Self::UndescribedVocabularyEntry { type_id, fault } => {
let rule = match fault {
DocGateFault::Empty => "has an empty doc",
DocGateFault::RestatesName => {
"has a doc that merely restates its name"
}
};
write!(
f,
"vocabulary entry `{type_id}` {rule} — every vocabulary \
entry ships a one-line meaning (C29)"
)
}
}
}
}
@@ -579,6 +597,18 @@ fn load_crate(crate_root: &Path, release: bool) -> Result<NativeEnv, ProjectErro
let type_id_list = (desc.type_ids)();
check_charter(&namespace, type_id_list, &|t| resolver(t))?;
// C29 load seam (#316): every resolved entry must describe itself. The
// charter cross-check above guarantees each listed id resolves, so a
// `None` here is unreachable — skipped rather than panicked on (a
// hostile dylib must never panic the host).
for &t in type_id_list {
if let Some(builder) = resolver(t) {
doc_gate(t, builder.schema().doc).map_err(|fault| {
ProjectError::UndescribedVocabularyEntry { type_id: t.to_string(), fault }
})?;
}
}
Ok(NativeEnv { namespace, dylib_sha256, resolver, type_id_list })
}
@@ -903,4 +933,32 @@ mod tests {
"older source: not stale"
);
}
/// The C29 load-seam refusal prose (`UndescribedVocabularyEntry`) names
/// the entry and the failed rule for BOTH `DocGateFault` arms. Only
/// `Empty` is reachable end-to-end today (the fixture in
/// `aura-cli/tests/project_load.rs` uses `doc: ""`); `RestatesName` has
/// no such fixture, so this pins its rendered message directly against
/// `Display` — a regression there would otherwise pass every test.
#[test]
fn undescribed_vocabulary_entry_display_names_the_entry_and_rule() {
let empty = ProjectError::UndescribedVocabularyEntry {
type_id: "ns::Foo".to_string(),
fault: DocGateFault::Empty,
};
assert_eq!(
empty.to_string(),
"vocabulary entry `ns::Foo` has an empty doc — every vocabulary \
entry ships a one-line meaning (C29)"
);
let restates = ProjectError::UndescribedVocabularyEntry {
type_id: "ns::Foo".to_string(),
fault: DocGateFault::RestatesName,
};
assert_eq!(
restates.to_string(),
"vocabulary entry `ns::Foo` has a doc that merely restates its \
name every vocabulary entry ships a one-line meaning (C29)"
);
}
}
+1 -1
View File
@@ -247,7 +247,7 @@ pub fn tap_channel(tap: &str) -> Option<TapChannel> {
debug_assert!(
aura_research::tap_vocabulary()
.iter()
.all(|t| matches!(*t, "equity" | "exposure" | "r_equity" | "net_r_equity")),
.all(|t| matches!(t.id, "equity" | "exposure" | "r_equity" | "net_r_equity")),
"tap_vocabulary drifted from the channels persist_campaign_traces routes"
);
match tap {
+1
View File
@@ -324,6 +324,7 @@ fn tap_sink_schema(kind: ScalarKind) -> NodeSchema {
inputs: vec![PortSpec { kind, firing: Firing::Any, name: "in".to_string() }],
output: vec![],
params: vec![],
doc: "internal recording sink bound to a declared tap",
}
}
+1
View File
@@ -23,6 +23,7 @@ impl Abs {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "value".into() }],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "absolute value of the input series",
},
|_| Box::new(Abs::new()),
)
+1
View File
@@ -38,6 +38,7 @@ impl Add {
],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "elementwise sum of two input series",
},
|_| Box::new(Add::new()),
)
+1
View File
@@ -37,6 +37,7 @@ impl And {
],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::Bool }],
params: vec![],
doc: "logical AND of two boolean series",
},
|_| Box::new(And::new()),
)
+1
View File
@@ -33,6 +33,7 @@ impl Const {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "clock".into() }],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![ParamSpec { name: "value".into(), kind: ScalarKind::F64 }],
doc: "constant-valued stream from a single param",
},
|p| Box::new(Const::new(p[0].f64())),
)
+1
View File
@@ -53,6 +53,7 @@ impl CumSum {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "running cumulative sum of the input series",
},
|_| Box::new(CumSum::new()),
)
+1
View File
@@ -65,6 +65,7 @@ impl Delay {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![ParamSpec { name: "lag".into(), kind: ScalarKind::I64 }],
doc: "emits the input from `lag` cycles ago — the explicit delay register (C9)",
},
|p| Box::new(Delay::new(p[0].i64() as usize)),
)
+1
View File
@@ -30,6 +30,7 @@ impl Div {
],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "elementwise quotient of two input series",
},
|_| Box::new(Div::new()),
)
+1
View File
@@ -63,6 +63,7 @@ impl Ema {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
doc: "exponential moving average over the input series",
},
|p| Box::new(Ema::new(p[0].i64() as usize)),
)
+1
View File
@@ -40,6 +40,7 @@ impl EqConst {
inputs: vec![PortSpec { kind: ScalarKind::I64, firing: Firing::Any, name: "value".into() }],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::Bool }],
params: vec![ParamSpec { name: "target".into(), kind: ScalarKind::I64 }],
doc: "true where the input equals a constant param",
},
|p| Box::new(EqConst::new(p[0].i64())),
)
+6 -1
View File
@@ -41,7 +41,12 @@ impl GatedRecorder {
let build_kinds = kinds.clone();
PrimitiveBuilder::new(
"GatedRecorder",
NodeSchema { inputs, output: vec![], params: vec![] }, // sink: empty output (C8)
NodeSchema {
inputs,
output: vec![],
params: vec![],
doc: "recording sink that persists its input while the gate is true, plus a final row at finalize",
}, // sink: empty output (C8)
move |_| Box::new(GatedRecorder::new(&build_kinds, gate_col, tx.clone())),
)
}
+1
View File
@@ -39,6 +39,7 @@ impl Gt {
],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::Bool }],
params: vec![],
doc: "true where the first input exceeds the second",
},
|_| Box::new(Gt::new()),
)
+1
View File
@@ -46,6 +46,7 @@ impl Latch {
],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "level-sensitive set/reset register holding 1.0 when latched, 0.0 when reset",
},
|_| Box::new(Latch::new()),
)
+6 -1
View File
@@ -52,7 +52,12 @@ impl LinComb {
.collect();
PrimitiveBuilder::new(
"LinComb",
NodeSchema { inputs, output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params },
NodeSchema {
inputs,
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params,
doc: "linear combination of its inputs with constant weights",
},
|p| Box::new(LinComb::new(
p.iter().map(|c| c.f64()).collect(),
)),
+1
View File
@@ -26,6 +26,7 @@ impl Max {
],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "elementwise maximum of two input series",
},
|_| Box::new(Max::new()),
)
+1
View File
@@ -27,6 +27,7 @@ impl Min {
],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "elementwise minimum of two input series",
},
|_| Box::new(Min::new()),
)
+1
View File
@@ -24,6 +24,7 @@ impl Mul {
],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "elementwise product of two input series",
},
|_| Box::new(Mul::new()),
)
+6 -1
View File
@@ -53,7 +53,12 @@ impl Recorder {
let build_kinds = kinds.clone();
PrimitiveBuilder::new(
"Recorder",
NodeSchema { inputs, output: vec![], params: vec![] }, // sink: empty output (C8)
NodeSchema {
inputs,
output: vec![],
params: vec![],
doc: "recording sink persisting its input series into the trace store",
}, // sink: empty output (C8)
move |_| Box::new(Recorder::new(&build_kinds, firing, tx.clone())),
)
}
+1
View File
@@ -54,6 +54,7 @@ impl RollingMax {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
doc: "rolling maximum over a fixed lookback window",
},
|p| Box::new(RollingMax::new(p[0].i64() as usize)),
)
+1
View File
@@ -43,6 +43,7 @@ impl RollingMin {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
doc: "rolling minimum over a fixed lookback window",
},
|p| Box::new(RollingMin::new(p[0].i64() as usize)),
)
+1
View File
@@ -28,6 +28,7 @@ impl Scale {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "signal".into() }],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![ParamSpec { name: "factor".into(), kind: ScalarKind::F64 }],
doc: "input multiplied by a constant factor param",
},
|p| Box::new(Scale::new(p[0].f64())),
)
+1
View File
@@ -38,6 +38,7 @@ impl Select {
],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "chooses between two inputs by a boolean selector",
},
|_| Box::new(Select::new()),
)
+1
View File
@@ -26,6 +26,7 @@ impl SeriesReducer {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing, name: "col[0]".to_string() }],
output: vec![], // sink: empty output (C8)
params: vec![],
doc: "folds an input series into a single summary row emitted at finalize",
},
move |_| Box::new(SeriesReducer::new(tx.clone())),
)
+1
View File
@@ -36,6 +36,7 @@ impl Sign {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "value".into() }],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "sign of the input: -1, 0 or +1",
},
|_| Box::new(Sign::new()),
)
+1
View File
@@ -65,6 +65,7 @@ impl Sma {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
doc: "simple moving average over the input series",
},
|p| Box::new(Sma::new(p[0].i64() as usize)),
)
+1
View File
@@ -23,6 +23,7 @@ impl Sqrt {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "value".into() }],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "square root of the input series",
},
|_| Box::new(Sqrt::new()),
)
+1
View File
@@ -29,6 +29,7 @@ impl Sub {
],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "elementwise difference of two input series",
},
|_| Box::new(Sub::new()),
)
+1
View File
@@ -46,6 +46,7 @@ impl When {
],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "emits the input only on cycles where the condition input is true",
},
|_| Box::new(When::new()),
)
+1
View File
@@ -32,6 +32,7 @@ impl Bias {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "signal".into() }],
output: vec![FieldSpec { name: "bias".into(), kind: ScalarKind::F64 }],
params: vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }],
doc: "clamps the strategy signal into the bias contract [-1,+1]",
},
|p| Box::new(Bias::new(p[0].f64())),
)
+10 -1
View File
@@ -224,7 +224,16 @@ pub fn cost_node_builder(
) -> PrimitiveBuilder {
let mut inputs = geometry_input_ports();
inputs.extend(extra_inputs);
PrimitiveBuilder::new(name, NodeSchema { inputs, output: cost_output_fields(), params }, build)
PrimitiveBuilder::new(
name,
NodeSchema {
inputs,
output: cost_output_fields(),
params,
doc: "cost-model node: charges its cost in R from position geometry, at close or accrued per held cycle",
},
build,
)
}
#[cfg(test)]
+1
View File
@@ -58,6 +58,7 @@ impl CostSum {
.map(|n| FieldSpec { name: (*n).into(), kind: ScalarKind::F64 })
.collect(),
params: vec![],
doc: "sums cost-model contributions into one cost-in-R stream",
},
move |_| Box::new(CostSum::new(n_costs)),
)
+1
View File
@@ -31,6 +31,7 @@ impl LongOnly {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "exposure".into() }],
output: vec![FieldSpec { name: "exposure".into(), kind: ScalarKind::F64 }],
params: vec![ParamSpec { name: "enabled".into(), kind: ScalarKind::Bool }],
doc: "when enabled, clamps negative exposure to flat (long-only); otherwise passes exposure through",
},
|p| Box::new(LongOnly::new(p[0].bool())),
)
+1
View File
@@ -36,6 +36,7 @@ impl Sizer {
],
output: vec![FieldSpec { name: "size".into(), kind: ScalarKind::F64 }],
params: vec![ParamSpec { name: "risk_budget".into(), kind: ScalarKind::F64 }],
doc: "size = risk_budget / stop_distance at the deploy edge; bias only gates firing, research runs flat-1R",
},
|p| Box::new(Sizer::new(p[0].f64())),
)
+1
View File
@@ -34,6 +34,7 @@ impl FixedStop {
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "price".into() }],
output: vec![FieldSpec { name: "stop_distance".into(), kind: ScalarKind::F64 }],
params: vec![ParamSpec { name: "distance".into(), kind: ScalarKind::F64 }],
doc: "derives the protective stop that defines the risk unit R",
},
|p| Box::new(FixedStop::new(p[0].f64())),
)
+1
View File
@@ -72,6 +72,7 @@ impl VolTfStop {
ParamSpec { name: "length".into(), kind: ScalarKind::I64 },
ParamSpec { name: "k".into(), kind: ScalarKind::F64 },
],
doc: "volatility- and timeframe-scaled protective stop rule",
},
|p| Box::new(VolTfStop::new(p[0].i64(), p[1].i64(), p[2].f64())),
)
+5 -1
View File
@@ -134,6 +134,9 @@ impl Scale {
}],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![ParamSpec { name: "factor".into(), kind: ScalarKind::F64 }],
// C29: every vocabulary entry ships a one-line meaning — the
// doc field is required (E0063 without it) and gated at load.
doc: "scalar gain: emits the input times the factor param",
},
|p| Box::new(Scale::new(p[0].f64())),
)
@@ -217,10 +220,11 @@ $ aura graph build < smacross.json > blueprint.json
Nodes are referenced by an **identifier** (given by `add`, see below); ports
are dotted `<identifier>.<port>` on both sides of a wire.
### The eight ops
### The nine ops
| op | JSON shape | does |
|---|---|---|
| `doc` | `{"op":"doc","text":<str>}` | declare the composite's one-line meaning (C29) — the op-script twin of the Rust builder's `.doc(...)`. Required before `aura graph register` accepts the product (the store refuses a doc-less composite); at most one per script (a second refuses: `a doc op may appear at most once`). The text is gated: empty or merely restating the composite's name refuses at register. |
| `source` | `{"op":"source","role":<str>,"kind":<ScalarKind>}` | reserve a bound root **source** role of `kind` — a real input the harness feeds (e.g. `"price"`). |
| `input` | `{"op":"input","role":<str>}` | reserve an open root **input** role (kind inferred from the slots it feeds) — for a fragment meant to be wired by an *enclosing* graph. A standalone document built with `aura graph build` finalizes as a closed root, so an `input` role that is never bound refuses at the end: `finalize: root input role <name> is unbound`. |
| `add` | `{"op":"add","type":<TypeId>,"name":<str>?,"bind":{<param>:<Scalar>}?}` | instantiate a node of a type in the closed vocabulary (`aura graph introspect --vocabulary`) — see §0 below for how a type gets into that vocabulary in the first place. `name` becomes the node's identifier for later ops (default: the type's own lowercase label — two unnamed nodes of the same type then collide). `bind` sets zero or more of its params. |
+7
View File
@@ -194,6 +194,13 @@ the repo, not session memory, is the source of truth.
measurement/strategy → backtest → execution) beside a process column, an
assembly position, and an import-only shell — the import direction enforced
structurally by the full-workspace `c28_layering` guard.
- **[C29 — Self-description: every surface explains itself](contracts/c29-self-description.md)** —
Every closed-vocabulary entry carries a one-line meaning behind one shared
shape gate (`doc_gate`) at three seams — compile/unit (engine-shipped),
load (native node crates), register (the content-addressed store, incl. an
optional gated document `description`); descriptions never influence
execution or identity ids, and registered artifacts are never
retroactively invalidated.
---
@@ -0,0 +1,72 @@
# C29 — Self-description: every surface explains itself
**Guarantee.** Every closed-vocabulary entry the binary exposes carries a
one-line meaning, enforced at the seam where the entry enters the system;
everything entering the content-addressed store describes itself; a refusal
for a missing description names the entry, the field, and the rule. One
shared deterministic gate — `doc_gate` in aura-core, importable by every
seam per [C28](c28-stratification.md); it fails on an empty/whitespace doc
and on a doc that normalizes to the entry's own name — the full id or its
bare display segment after the last `::`, so a namespaced extension entry
cannot pass its short-name alibi
(`DocGateFault::{Empty, RestatesName}`) — guards three seams:
1. **Compile/unit seam (engine-shipped).** `NodeSchema.doc` is a required
field, so the compiler enumerates every construction site; metrics and
tap slots are schema carriers (`MetricSchema` / `TapSchema`, id + doc);
blocks (`BlockSchema.doc`, the pre-existing model) and tap folds (the
registry roster, [C27](c27-declared-taps.md)) already carried theirs.
The coverage walk (`aura-cli/tests/self_description.rs`) asserts the
gate over all five vocabularies — the engine satisfies its own law.
2. **Load seam (extension).** `load_crate` walks every charter-checked
type id and gates the resolved schema's doc; a fault refuses the load
naming type and rule (`ProjectError::UndescribedVocabularyEntry`).
Field presence is compile-enforced for any crate built against the new
core; the load gate catches the shape violations the compiler cannot
see across the cdylib boundary — the empty-string alibi and the name
restatement.
3. **Register seam (data plane).** The registry's public `put_blueprint`
parses the canonical bytes and requires a gate-passing doc on the root
composite and on every named nested composite — the gate guards the
store boundary itself, so the register verb, every register-then-run
sugar path, and any future caller are gated by construction. The
op-script vocabulary carries the `doc` op as the builder `.doc(...)`'s
twin ([C25](c25-role-model.md): a closed, typed metadata construct).
Process/campaign documents take an additive-optional top-level
`description`, gated only when present (`DocFault::BadDescription`).
**Scope at this record's writing (#321).** The gate walks five
vocabularies — process/campaign blocks, metrics, tap slots, tap folds, and
the closed node roster — plus the document `description` and the three
seams above. Recorded residue, tracked as #321 and not silently claimed:
`emit_vocabulary()` and `binding_column_vocabulary()` remain bare name
arrays, and builder schemas outside the closed node roster (e.g. the
internal tap recording sink) carry compile-required but shape-unwalked
docs. Closing #321 widens the walk and removes this paragraph.
**Id treatment.** Description fields that serialize participate in the
content id and are blanked for the identity id, exactly as `Composite.doc`
(`strip_debug_symbols`); `NodeSchema` is not serialized
([C24](c24-blueprint-data.md)), so node docs move no id. Documents have no
identity projection — their pair is: absent `description` is byte-identical
to the field-less form (existing content ids stable), present participates.
**Forbids.** The engine evaluating description text — the gate is
deterministic string shape, never content judgement (no freetext logic
hole; C17 / domain invariant 10 untouched). A description influencing
execution, identity ids, or determinism (C1). Retroactive invalidation of
already-registered artifacts — the gate guards the write path only (the
raw in-crate write survives as the store-mechanics and back-compat
witness: pre-C29 doc-less entries stay readable). Machine-invented meaning
lines — an authored text is corrected against the source, never weakened
to an empty string or a name restatement.
**Why.** The audience is headless LLM agents and scripts (#319 ruling),
for whom the binary is the only always-present teacher. Field evidence
2026-07-22 (#314): an agent holding only the release binary had to recover
execution semantics and document schemas by CAS forensics — the removed
failure class is exactly that forensic recovery.
Rendering of the carried texts on the help/introspection surfaces is #315;
the generated agent bootstrap card is #267; a fold introspection surface
is blocked on #310.
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"graph","nodes":[{"primitive":{"type":"lab316_nodes::Scale","name":"scaled","bound":[{"pos":0,"name":"factor","kind":"F64","value":{"F64":2.0}}]}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":1.0}}]}}],"edges":[{"from":0,"to":1,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0}],"source":"F64"}],"output":[{"node":1,"field":0,"name":"bias"}]}}
@@ -0,0 +1,8 @@
[
{"op": "source", "role": "price", "kind": "F64"},
{"op": "add", "type": "lab316_nodes::Scale", "name": "scaled", "bind": {"factor": {"F64": 2.0}}},
{"op": "feed", "role": "price", "into": ["scaled.value"]},
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 1.0}}},
{"op": "connect", "from": "scaled.value", "to": "bias.signal"},
{"op": "expose", "from": "bias.bias", "as": "bias"}
]
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"graph","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
@@ -0,0 +1,12 @@
[
{"op": "source", "role": "price", "kind": "F64"},
{"op": "add", "type": "SMA", "name": "fast"},
{"op": "add", "type": "SMA", "name": "slow"},
{"op": "feed", "role": "price", "into": ["fast.series", "slow.series"]},
{"op": "add", "type": "Sub", "name": "sub"},
{"op": "connect", "from": "fast.value", "to": "sub.lhs"},
{"op": "connect", "from": "slow.value", "to": "sub.rhs"},
{"op": "add", "type": "Bias", "name": "bias"},
{"op": "connect", "from": "sub.value", "to": "bias.signal"},
{"op": "expose", "from": "bias.bias", "as": "bias"}
]
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"graph","doc":"fast/slow SMA crossover spread turned into a bounded directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}

Some files were not shown because too many files have changed in this diff Show More