[pitboss/grind] deferred session-0002 (20260521T143544Z-f898)

This commit is contained in:
pitboss 2026-05-21 11:22:13 -05:00
parent be4021d8c0
commit b3766311fb
20 changed files with 388 additions and 664 deletions

View file

@ -283,10 +283,17 @@ pub fn method_formal_types(method: Node<'_>, bytes: &[u8]) -> Vec<(String, Strin
/// Extract placeholder names from a route path template.
///
/// Supports two placeholder syntaxes:
/// Supports three placeholder syntaxes:
/// - JAX-RS / Spring / Micronaut: `/users/{id}` → `id`,
/// `/users/{id:[0-9]+}` → `id`.
/// - Servlet-mapping `*` wildcards: ignored (no name to bind).
/// - Spring 5.3+ capture-all variables: `/files/{*path}` → `path`
/// (matches the remainder of the URI including slashes).
/// - Bare Ant-style `*` / `**` wildcards (`/users/*`, `/files/**`):
/// intentionally yield no placeholders. They are unnamed by Spring's
/// `AntPathMatcher` and cannot bind by formal name; handlers that
/// need the matched segment use `HttpServletRequest.getRequestURI()`
/// (already routed to [`ParamSource::Implicit`]) or the named
/// `{*name}` capture-all syntax above.
pub fn extract_path_placeholders(path: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let bytes = path.as_bytes();
@ -295,7 +302,8 @@ pub fn extract_path_placeholders(path: &str) -> Vec<String> {
if bytes[i] == b'{'
&& let Some(end) = bytes[i + 1..].iter().position(|&b| b == b'}') {
let inner = &path[i + 1..i + 1 + end];
let name = inner.split(':').next().unwrap_or(inner).trim();
let inner_name = inner.split(':').next().unwrap_or(inner).trim();
let name = inner_name.strip_prefix('*').unwrap_or(inner_name);
if !name.is_empty() && !out.iter().any(|n| n == name) {
out.push(name.to_owned());
}
@ -420,6 +428,26 @@ mod tests {
assert_eq!(extract_path_placeholders("/u/{id:[0-9]+}"), vec!["id"]);
}
#[test]
fn extracts_capture_all_variable() {
assert_eq!(extract_path_placeholders("/files/{*path}"), vec!["path"]);
assert_eq!(
extract_path_placeholders("/api/{tenant}/files/{*resource}"),
vec!["tenant", "resource"]
);
}
#[test]
fn unnamed_ant_globs_yield_no_placeholders() {
// Bare `*` and `**` are unnamed by Spring's AntPathMatcher and have
// no name to bind a formal to. Handlers that need the matched
// segment use the request object (routed to [`ParamSource::Implicit`])
// or the named `{*name}` capture-all syntax above.
assert!(extract_path_placeholders("/users/*").is_empty());
assert!(extract_path_placeholders("/files/**").is_empty());
assert!(extract_path_placeholders("/a/*/b/**/c").is_empty());
}
#[test]
fn join_drops_double_slash() {
assert_eq!(join_route_path("/api", "/x"), "/api/x");

View file

@ -18,7 +18,7 @@ use tree_sitter::Node;
use super::ruby_routes::{
bind_path_params, class_extends, class_name, find_class_with_method, first_string_arg,
kwarg_string, method_formal_names, source_imports_rails, verb_from_ident,
first_symbol_arg, kwarg_string, method_formal_names, source_imports_rails, verb_from_ident,
};
pub struct RubyRailsAdapter;
@ -40,9 +40,13 @@ fn class_is_rails_controller(class: Node<'_>, bytes: &[u8]) -> bool {
/// Walk the file's top-level `call` nodes looking for a
/// `Rails.application.routes.draw` block or bare `get / post / ...`
/// dispatch lines, and return the first `(method, path)` whose
/// `to: 'controller#action'` kwarg references the target. Returns
/// `None` when no route mapping is present (the caller then falls
/// back to the conventional `/{action}` shape).
/// `to: 'controller#action'` kwarg references the target. Respects
/// `namespace :api do ... end` and `scope :v1 do ... end` /
/// `scope path: '/v1' do ... end` nesting so a route declared inside
/// such a block resolves against the prefixed path + controller name
/// Rails actually mounts it under. Returns `None` when no mapping
/// is present (the caller then falls back to the conventional
/// `/{action}` shape).
fn find_route_mapping<'a>(
root: Node<'a>,
bytes: &'a [u8],
@ -50,7 +54,7 @@ fn find_route_mapping<'a>(
action: &str,
) -> Option<(HttpMethod, String)> {
let mut hit: Option<(HttpMethod, String)> = None;
visit_routes(root, bytes, controller, action, &mut hit);
visit_routes(root, bytes, controller, action, "", "", &mut hit);
hit
}
@ -59,19 +63,98 @@ fn visit_routes<'a>(
bytes: &'a [u8],
controller: &str,
action: &str,
path_prefix: &str,
ctrl_prefix: &str,
out: &mut Option<(HttpMethod, String)>,
) {
if out.is_some() {
return;
}
if node.kind() == "call"
&& let Some(found) = try_route_mapping(node, bytes, controller, action) {
if node.kind() == "call" {
if let Some((kind, ident)) = route_nesting_kind(node, bytes) {
let (path_pfx, ctrl_pfx) = match kind {
NestingKind::Namespace => (
format!("{path_prefix}/{ident}"),
format!("{ctrl_prefix}{ident}/"),
),
NestingKind::ScopeSymbol => (
format!("{path_prefix}/{ident}"),
format!("{ctrl_prefix}{ident}/"),
),
NestingKind::ScopePath => (format!("{path_prefix}/{ident}"), ctrl_prefix.to_owned()),
};
recurse_into_block(node, bytes, controller, action, &path_pfx, &ctrl_pfx, out);
return;
}
if let Some(found) = try_route_mapping(node, bytes, controller, action, path_prefix, ctrl_prefix) {
*out = Some(found);
return;
}
}
let mut cur = node.walk();
for child in node.children(&mut cur) {
visit_routes(child, bytes, controller, action, out);
visit_routes(child, bytes, controller, action, path_prefix, ctrl_prefix, out);
}
}
enum NestingKind {
Namespace,
ScopeSymbol,
ScopePath,
}
/// If `call` is a routes-DSL nesting block (`namespace :api do ... end`,
/// `scope :v1 do ... end`, or `scope path: '/v1' do ... end`) return
/// the kind + the extracted identifier (a bare token for namespace /
/// symbol-scope, a leading-slash-stripped path for path-scope).
fn route_nesting_kind<'a>(call: Node<'a>, bytes: &'a [u8]) -> Option<(NestingKind, String)> {
let mut cur = call.walk();
let mut ident: Option<&str> = None;
let mut args: Option<Node<'a>> = None;
for child in call.named_children(&mut cur) {
match child.kind() {
"identifier" => ident = child.utf8_text(bytes).ok(),
"argument_list" => args = Some(child),
_ => {}
}
}
let ident = ident?;
let args = args?;
match ident {
"namespace" => {
let sym = first_symbol_arg(args, bytes)?;
Some((NestingKind::Namespace, sym))
}
"scope" => {
if let Some(sym) = first_symbol_arg(args, bytes) {
Some((NestingKind::ScopeSymbol, sym))
} else {
let path = kwarg_string(args, bytes, "path")?;
let trimmed = path.trim_start_matches('/').to_owned();
if trimmed.is_empty() {
return None;
}
Some((NestingKind::ScopePath, trimmed))
}
}
_ => None,
}
}
fn recurse_into_block<'a>(
call: Node<'a>,
bytes: &'a [u8],
controller: &str,
action: &str,
path_prefix: &str,
ctrl_prefix: &str,
out: &mut Option<(HttpMethod, String)>,
) {
let mut cur = call.walk();
for child in call.named_children(&mut cur) {
if child.kind() == "do_block" || child.kind() == "block" {
visit_routes(child, bytes, controller, action, path_prefix, ctrl_prefix, out);
}
}
}
@ -80,6 +163,8 @@ fn try_route_mapping<'a>(
bytes: &'a [u8],
controller: &str,
action: &str,
path_prefix: &str,
ctrl_prefix: &str,
) -> Option<(HttpMethod, String)> {
let mut cur = call.walk();
let mut verb: Option<HttpMethod> = None;
@ -100,8 +185,14 @@ fn try_route_mapping<'a>(
let path = first_string_arg(args, bytes)?;
let to = kwarg_string(args, bytes, "to")?;
let (ctrl, act) = to.split_once('#')?;
if controller_matches(ctrl, controller) && act == action {
return Some((verb, path));
let full_ctrl = format!("{ctrl_prefix}{ctrl}");
if controller_matches(&full_ctrl, controller) && act == action {
let full_path = if path_prefix.is_empty() {
path
} else {
format!("{}/{}", path_prefix, path.trim_start_matches('/'))
};
return Some((verb, full_path));
}
None
}
@ -269,6 +360,51 @@ mod tests {
assert!(matches!(id.source, crate::dynamic::framework::ParamSource::PathSegment(_)));
}
#[test]
fn routes_draw_namespace_applies_prefix_to_path_and_controller() {
let src: &[u8] = b"Rails.application.routes.draw do\n namespace :api do\n get '/users', to: 'users#index'\n end\nend\n\nclass Api::UsersController < ApplicationController\n def index\n 'ok'\n end\nend\n";
let tree = parse(src);
let binding = RubyRailsAdapter
.detect(&summary("index"), tree.root_node(), src)
.expect("binding");
let route = binding.route.unwrap();
assert_eq!(route.path, "/api/users");
assert_eq!(route.method, HttpMethod::GET);
}
#[test]
fn routes_draw_scope_path_prefixes_path_only() {
let src: &[u8] = b"Rails.application.routes.draw do\n scope path: '/v1' do\n get '/users', to: 'users#index'\n end\nend\n\nclass UsersController < ApplicationController\n def index\n 'ok'\n end\nend\n";
let tree = parse(src);
let binding = RubyRailsAdapter
.detect(&summary("index"), tree.root_node(), src)
.expect("binding");
let route = binding.route.unwrap();
assert_eq!(route.path, "/v1/users");
}
#[test]
fn routes_draw_scope_symbol_prefixes_path_and_controller() {
let src: &[u8] = b"Rails.application.routes.draw do\n scope :admin do\n get '/users', to: 'users#index'\n end\nend\n\nclass Admin::UsersController < ApplicationController\n def index\n 'ok'\n end\nend\n";
let tree = parse(src);
let binding = RubyRailsAdapter
.detect(&summary("index"), tree.root_node(), src)
.expect("binding");
let route = binding.route.unwrap();
assert_eq!(route.path, "/admin/users");
}
#[test]
fn routes_draw_nested_namespaces_compose_prefixes() {
let src: &[u8] = b"Rails.application.routes.draw do\n namespace :api do\n namespace :v1 do\n get '/users', to: 'users#index'\n end\n end\nend\n\nclass Api::V1::UsersController < ApplicationController\n def index\n 'ok'\n end\nend\n";
let tree = parse(src);
let binding = RubyRailsAdapter
.detect(&summary("index"), tree.root_node(), src)
.expect("binding");
let route = binding.route.unwrap();
assert_eq!(route.path, "/api/v1/users");
}
#[test]
fn skips_when_class_is_not_a_controller() {
let src: &[u8] = b"class Foo\n def bar\n 'ok'\n end\nend\n";

View file

@ -145,7 +145,7 @@ fn named_child_of_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
pub fn class_name<'a>(class: Node<'a>, bytes: &'a [u8]) -> Option<&'a str> {
let mut cur = class.walk();
for c in class.named_children(&mut cur) {
if c.kind() == "constant" {
if c.kind() == "constant" || c.kind() == "scope_resolution" {
return c.utf8_text(bytes).ok();
}
}
@ -352,6 +352,22 @@ fn is_implicit_formal(name: &str) -> bool {
matches!(name, "env" | "request" | "req" | "params" | "response" | "res")
}
/// Read the first positional symbol argument (`:foo`) from an
/// `argument_list` child. Used by the Rails router DSL to pull the
/// namespace name out of `namespace :api do ... end` and the
/// positional form of `scope :v1 do ... end`. The returned string
/// is the symbol's identifier portion without the leading colon.
pub fn first_symbol_arg<'a>(args: Node<'a>, bytes: &'a [u8]) -> Option<String> {
let mut cur = args.walk();
for c in args.named_children(&mut cur) {
if c.kind() == "simple_symbol" {
let raw = c.utf8_text(bytes).ok()?;
return Some(raw.trim_start_matches(':').to_owned());
}
}
None
}
/// Read the first positional string-literal argument from an
/// `argument_list` child. Used by every Ruby route adapter to pull
/// a path template out of `get '/run' do ... end` and the Rails

View file

@ -565,10 +565,9 @@ pub enum ReplayResult {
/// Tri-state map of [`ReplayResult`] onto the eval-corpus
/// `VerifyResult::replay_stable` field shape.
///
/// * `Some(true)` — replay matched the recorded outcome.
/// * `Some(false)` — replay diverged or aborted in a way that the M7
/// Gate-5 inversion treats as instability.
/// * `None` — replay was not informative (toolchain mismatched, docker
/// * `Some(true)` - replay matched the recorded outcome.
/// * `Some(false)` - replay diverged or aborted.
/// * `None` - replay was not informative (toolchain mismatched, docker
/// unavailable, or the bundle had no `reproduce.sh`). The corpus
/// tabulator treats `None` as "no signal" and excludes the row from
/// the per-cell `stable_replays` numerator.
@ -582,15 +581,14 @@ pub fn replay_stability(result: &ReplayResult) -> Option<bool> {
}
}
/// Phase 28 — Track H.3. Run `reproduce.sh` in `bundle_root` and map the
/// shell exit code into a [`ReplayResult`].
/// Run `reproduce.sh` in `bundle_root` and map the shell exit code into a
/// [`ReplayResult`].
///
/// `extra_args` is appended to `reproduce.sh` (`--docker` when the caller
/// wants the docker backend; empty for the process backend).
///
/// This is the host-side companion to the M7 Gate 5 inversion: callers
/// who want "did this bundle replay green?" semantics see a typed result
/// and the M7 gate script gets a uniform contract to assert against.
/// Callers who want "did this bundle replay green?" semantics get a typed
/// result instead of parsing shell output.
pub fn replay_bundle(
bundle_root: &Path,
extra_args: &[&str],

View file

@ -1,9 +1,9 @@
//! Telemetry event log (§21.1).
//! Telemetry event log.
//!
//! Writes one JSON line per verdict to `~/.cache/nyx/dynamic/events.jsonl`.
//! `NYX_NO_TELEMETRY=1` silently disables all writes (§21.4).
//! `NYX_NO_TELEMETRY=1` silently disables all writes.
//!
//! # Schema (Phase 27)
//! # Schema
//!
//! Every record starts with three envelope fields so the on-disk format can
//! evolve across releases without silently mixing incompatible records:
@ -12,11 +12,10 @@
//! - `nyx_version`: the Cargo package version that wrote the record.
//! - `corpus_version`: the payload-corpus version active at write time.
//!
//! Followed by a `kind` discriminator (`"verdict"` or `"rank_delta"`). All
//! readers (`read_events`, the M7 ship gate) require `schema_version ==
//! [`SCHEMA_VERSION`]; mismatched records produce
//! [`TelemetryReadError::SchemaMismatch`] instead of being silently parsed
//! as if they matched.
//! Followed by a `kind` discriminator (`"verdict"` or `"rank_delta"`). All
//! readers require `schema_version == SCHEMA_VERSION`; mismatched records
//! produce [`TelemetryReadError::SchemaMismatch`] instead of being silently
//! parsed as if they matched.
//!
//! ```json
//! {
@ -258,12 +257,10 @@ fn lang_from_path(path: &str) -> String {
.unwrap_or_else(|| "unknown".to_owned())
}
/// Sampling decision for telemetry writes (Phase 27, Track H.2).
/// Sampling decision for telemetry writes.
///
/// Confirmed and Inconclusive verdicts are calibration-critical (false-Confirmed
/// rate gates M7 ship; Inconclusive reasons drive the spec-derivation roadmap)
/// and are always retained. Other verdict statuses can be downsampled to bound
/// log growth on high-volume scans.
/// Confirmed and Inconclusive verdicts are kept for calibration. Other verdict
/// statuses can be downsampled to bound log growth on high-volume scans.
///
/// The decision is seeded by `spec_hash` so the *same* finding makes the *same*
/// keep-or-drop call across reruns. Without this, two scans of the same project
@ -413,12 +410,11 @@ pub fn log_path() -> Option<std::path::PathBuf> {
events_log_path()
}
// ── Reading events back (Phase 27) ───────────────────────────────────────────
// Reading events back
/// Structured error returned by [`read_events`].
///
/// Surfaced to the M7 ship gate so Gate 2 can fail loudly on schema-mismatch
/// rather than silently treating mismatched records as "no data".
/// Returned when a log mixes records from incompatible schema versions.
#[derive(Debug, thiserror::Error)]
pub enum TelemetryReadError {
#[error("io error reading {path}: {source}")]
@ -451,14 +447,12 @@ pub enum TelemetryReadError {
///
/// Returns each line as a `serde_json::Value` so callers can dispatch on the
/// `kind` discriminator themselves. Rejects any record whose `schema_version`
/// does not match [`SCHEMA_VERSION`] (this is the explicit failure mode the
/// M7 ship gate Gate 2 consumes; a v0 record from an older release must not
/// silently parse as if the schema had never changed).
/// does not match [`SCHEMA_VERSION`]. A v0 record from an older release must
/// not silently parse as if the schema had never changed.
///
/// Blank lines are skipped. Any malformed JSON or missing `schema_version`
/// fails the whole read; partial recovery is not the contract here because
/// the ship gate already treats "log missing or unreadable" as "no data,
/// skip Gate 2 with a notice."
/// Blank lines are skipped. Any malformed JSON or missing `schema_version`
/// fails the whole read; partial recovery is not the contract for telemetry
/// logs.
pub fn read_events(path: &Path) -> Result<Vec<serde_json::Value>, TelemetryReadError> {
let file = std::fs::File::open(path).map_err(|e| TelemetryReadError::Io {
path: path.to_path_buf(),
@ -551,8 +545,8 @@ pub fn feedback_wrong_for_finding(path: &Path, finding_id: &str) -> Option<bool>
/// One telemetry event per ranked finding that carries a dynamic verdict delta.
///
/// Emitted by `rank::rank_diags` for every diag whose dynamic verdict shifts
/// its rank score (delta != 0). Used by the M7 calibration pipeline to tune
/// the N/M boost/penalty constants from real-world verdict distributions.
/// its rank score (delta != 0). Used to tune the N/M boost/penalty constants
/// from real-world verdict distributions.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct RankDeltaEvent {
pub schema_version: u32,

View file

@ -85,14 +85,11 @@ pub struct VerifyOptions {
/// Default `false`. [`Self::from_config`] honours the
/// `NYX_VERIFY_REPLAY_STABLE` environment variable (`1` / `true`).
pub replay_stable_check: bool,
/// Phase 31 follow-up: when `true` and `replay_stable_check` is also
/// `true`, the verifier passes `--docker` to `reproduce.sh` instead of
/// running it through the host's process backend. Lets the eval-corpus
/// driver mark `replay_stable` based on the bare-image replay path so
/// the M7 ship-gate's Gate 5 reflects the docker bundle's green/red
/// signal — required when the corpus walks a host that has stripped
/// the language toolchains (the bare-image CI matrix at
/// `.github/workflows/repro-bare.yml`).
/// When `true` and `replay_stable_check` is also `true`, the verifier
/// passes `--docker` to `reproduce.sh` instead of running it through the
/// host's process backend. This lets eval-corpus runs mark
/// `replay_stable` from the bare-image replay path when the host has
/// stripped language toolchains.
///
/// Default `false`. [`Self::from_config`] honours the
/// `NYX_VERIFY_REPLAY_DOCKER` environment variable (`1` / `true`).