feat(cli, std, research): self-describing surfaces — two-layer help, per-entry meanings, op reference

The binary is the only always-present teacher (C29); the 2026-07-22 external
field test showed the help teaching nothing but the verb inventory. This
cycle makes every closed vocabulary the binary ships listable with a one-line
meaning, and opens the help with the concepts the field agent had to discover
forensically.

#315 — help + introspection:
- `aura --help` opens with the two-layer concepts paragraph (research verbs
  as sugar over registered process/campaign documents; the directly
  authorable data plane; bias-as-target-position + protective-stop execution
  model in R; how traces come to exist).
- Each document-bridged sugar verb's long help names the process shape it
  desugars to (sweep -> std::sweep; walkforward -> [std::grid,
  std::walk_forward]; mc -> + std::monte_carlo; generalize -> [std::sweep
  (selection), std::generalize]). `run` is not verb_sugar-bridged, so its
  help points at the canonical document-first form instead of claiming a
  desugaring it does not perform.
- `graph introspect --vocabulary` lists each node type with its schema doc
  (bare names taught nothing); `--node <T>`'s head line carries the meaning.
- `graph introspect --folds` (new): the tap-fold vocabulary with bind rule,
  output kind, and meaning — it existed only as source comments. The rows
  live in aura-std (`fold_vocabulary`), unit-pinned against
  `fold_binds_at`/`fold_output_kind` so the prose cannot drift from the
  executable rules, and doc_gate-checked (C29 entry seam). The discovery
  surface is graph introspect because folds bind at graph-declared taps
  (C27); the document layer still refuses a folds slot pending #310.
- `process introspect --metrics` lines carry each metric's meaning (the doc
  column existed since #316 but was never printed).
- `campaign introspect --block std::presentation` lists the four persisted
  taps' meanings under the persist_taps slot.
- The scaffolded project CLAUDE.md teaches the execution semantics (bias as
  held target, protective stop, R) and the document data plane.

#323 — C29 authoring surfaces:
- `introspect --unwired` enumerates the optional `description` envelope slot
  for both document kinds (absent-only, like risk/cost); the campaign lib
  fixture gains a description, and the risk/cost-bound e2e binds description
  too so "no open slots" keeps meaning complete.
- `graph build --help` carries the op-list reference: all nine op kinds with
  fields and a worked element each (OP_REFERENCE lives beside OpDoc so a new
  variant is one screen from its help line).
- The authoring guide's S0 worked literal is byte-pinned to the scaffold's
  LIB_RS template (rendered with the guide's my_lab namespace); the scaffold
  e2es compile that template for real, so the pin is a transitive compile
  guard. The template gains the guide's C29 comment, the guide gains the
  template's rename note — one worked example, two surfaces, zero drift.

Deliberately out of scope (ratified #319, 2026-07-24): no doc-strings for
the quintet's 48 flag fields — the sugar surface retires; only the pointer
lines above bridge toward the document layer.

Verification: full workspace suite green, clippy -D warnings clean,
aura-bench exit 0 with all fingerprints OK (help_ms +1.6% — the longer help
text, in tolerance), cargo doc warning count unchanged (7 pre-existing).
9 new e2e pins in tests/help_self_description.rs; guide + CLAUDE.md pins in
scaffold unit tests; fold-vocabulary rules + doc_gate unit-pinned in
aura-std.

closes #315
closes #323
This commit is contained in:
2026-07-24 10:02:38 +02:00
parent 6fb7caf929
commit 829a1984e6
10 changed files with 388 additions and 10 deletions
+1 -1
View File
@@ -74,6 +74,6 @@ pub use sma::Sma;
pub use sqrt::Sqrt;
pub use sub::Sub;
pub use tap_cell::newest_cell;
pub use tap_fold::{fold_binds_at, fold_output_kind, FoldKind, TapFold};
pub use tap_fold::{fold_binds_at, fold_output_kind, fold_vocabulary, FoldKind, FoldSchema, TapFold};
pub use tap_live::TapLive;
pub use when::When;
+67
View File
@@ -45,6 +45,32 @@ pub fn fold_output_kind(fold: FoldKind, kind: ScalarKind) -> ScalarKind {
}
}
/// One fold entry's introspection row (C29): id, bind/output rules in prose,
/// one-line meaning. `fold_vocabulary` is the binary's discovery surface for
/// [`FoldKind`] (`aura graph introspect --folds`, #315); the unit tests pin
/// each row's rule prose against [`fold_binds_at`] / [`fold_output_kind`] so
/// the table cannot drift from the executable rules.
pub struct FoldSchema {
pub id: &'static str,
pub kind: FoldKind,
pub binds: &'static str,
pub out: &'static str,
pub doc: &'static str,
}
/// The closed fold vocabulary as introspection rows, in [`FoldKind`] order.
pub fn fold_vocabulary() -> &'static [FoldSchema] {
&[
FoldSchema { id: "Count", kind: FoldKind::Count, binds: "any tap", out: "i64", doc: "number of rows the tap recorded" },
FoldSchema { id: "Sum", kind: FoldKind::Sum, binds: "f64 taps", out: "f64", doc: "sum of the tap's recorded values" },
FoldSchema { id: "Mean", kind: FoldKind::Mean, binds: "f64 taps", out: "f64", doc: "arithmetic mean of the tap's recorded values" },
FoldSchema { id: "Min", kind: FoldKind::Min, binds: "f64 taps", out: "f64", doc: "smallest recorded value" },
FoldSchema { id: "Max", kind: FoldKind::Max, binds: "f64 taps", out: "f64", doc: "largest recorded value" },
FoldSchema { id: "First", kind: FoldKind::First, binds: "any tap", out: "tap kind", doc: "earliest recorded value, kind-preserving" },
FoldSchema { id: "Last", kind: FoldKind::Last, binds: "any tap", out: "tap kind", doc: "latest recorded value, kind-preserving" },
]
}
/// Owned accumulator for every fold kind at once (a handful of words; keeping
/// it total avoids a per-kind state enum). Sequential `sum += v` matches a
/// slice-driven mean's operation order, so streamed and slice `Mean` agree
@@ -164,6 +190,47 @@ mod tests {
use aura_core::AnyColumn;
use std::sync::mpsc;
/// The vocabulary rows are prose renderings of the executable rules —
/// pin them against `fold_binds_at` / `fold_output_kind` so the
/// introspection surface cannot drift from what the engine enforces.
#[test]
fn fold_vocabulary_rows_match_the_executable_rules() {
let rows = fold_vocabulary();
assert_eq!(rows.len(), 7, "one row per FoldKind");
for row in rows {
let f64_only = fold_binds_at(row.kind, ScalarKind::F64)
&& !fold_binds_at(row.kind, ScalarKind::I64);
match row.binds {
"f64 taps" => assert!(f64_only, "{} claims f64-only but binds wider", row.id),
"any tap" => assert!(
fold_binds_at(row.kind, ScalarKind::I64)
&& fold_binds_at(row.kind, ScalarKind::Bool),
"{} claims any-tap but refuses a kind",
row.id
),
other => panic!("unknown binds prose {other:?} on {}", row.id),
}
match row.out {
"i64" => assert_eq!(fold_output_kind(row.kind, ScalarKind::F64), ScalarKind::I64),
"f64" => assert_eq!(fold_output_kind(row.kind, ScalarKind::F64), ScalarKind::F64),
"tap kind" => {
assert_eq!(fold_output_kind(row.kind, ScalarKind::F64), ScalarKind::F64);
assert_eq!(fold_output_kind(row.kind, ScalarKind::I64), ScalarKind::I64);
}
other => panic!("unknown out prose {other:?} on {}", row.id),
}
}
}
/// C29 entry seam: every fold row ships a gate-clean one-line meaning.
#[test]
fn fold_vocabulary_docs_pass_the_doc_gate() {
for row in fold_vocabulary() {
aura_core::doc_gate(row.id, row.doc)
.unwrap_or_else(|f| panic!("fold {} doc fails the gate: {f:?}", row.id));
}
}
/// Drive a fold over an f64 series and return the emitted rows.
fn run_f64_fold(fold: FoldKind, values: &[f64]) -> Vec<(Timestamp, Vec<Scalar>)> {
let (tx, rx) = mpsc::channel();