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:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user