mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss] phase 01: Track L.0 — FrameworkAdapter trait + per-lang dispatch table
This commit is contained in:
parent
4a2acf1bf9
commit
16834a6e7c
30 changed files with 456 additions and 0 deletions
|
|
@ -1642,6 +1642,7 @@ mod tests {
|
|||
spec_hash: "0000000000000000".to_owned(),
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1176,6 +1176,7 @@ mod tests {
|
|||
spec_hash: "test0000abcd1234".into(),
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
280
src/dynamic/framework/mod.rs
Normal file
280
src/dynamic/framework/mod.rs
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
//! Framework adapter abstraction (Track L.0).
|
||||
//!
|
||||
//! Replaces the ad-hoc per-language route / `main` detection that was
|
||||
//! scattered across [`crate::dynamic::lang`] sub-modules with a single
|
||||
//! dispatching trait. Every later phase in Track L plugs a concrete
|
||||
//! adapter (Flask, Spring, Express, axum, …) into this trait.
|
||||
//!
|
||||
//! # Determinism
|
||||
//!
|
||||
//! [`detect_binding`] iterates the per-language adapter slice returned
|
||||
//! by [`registry::adapters_for`] in registration order and returns the
|
||||
//! first non-`None` match. The registration order is fixed at
|
||||
//! compile time and kept sorted by [`FrameworkAdapter::name`] so a
|
||||
//! phase that adds a new adapter cannot silently re-order an existing
|
||||
//! match.
|
||||
|
||||
pub mod registry;
|
||||
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// HTTP method recognised by route bindings. Mirrors
|
||||
/// [`crate::entry_points::HttpMethod`] but is re-declared here so the
|
||||
/// framework module does not pull in the static-analysis entry-point
|
||||
/// types in callers that only need the dynamic-side shape.
|
||||
pub use crate::entry_points::HttpMethod;
|
||||
|
||||
/// HTTP route shape extracted from a framework binding (path +
|
||||
/// method). Only populated when [`FrameworkBinding::kind`] is
|
||||
/// [`EntryKind::HttpRoute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RouteShape {
|
||||
/// HTTP verb (`GET`, `POST`, …).
|
||||
pub method: HttpMethod,
|
||||
/// Route path template as registered with the framework (e.g.
|
||||
/// `"/users/{id}"`). Adapter-specific placeholder syntax is
|
||||
/// preserved verbatim.
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Where on the external surface a function formal originates from.
|
||||
///
|
||||
/// Adapters classify each declared parameter into one of these
|
||||
/// buckets so downstream harness emitters know which request field
|
||||
/// carries the payload.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ParamSource {
|
||||
/// URL path placeholder (e.g. `/users/{id}` → `id`).
|
||||
PathSegment(String),
|
||||
/// URL query string parameter.
|
||||
QueryParam(String),
|
||||
/// HTTP request header.
|
||||
Header(String),
|
||||
/// JSON request body (deserialised whole).
|
||||
JsonBody,
|
||||
/// HTML form field.
|
||||
FormField(String),
|
||||
/// HTTP cookie.
|
||||
Cookie(String),
|
||||
/// Implicit context object (e.g. `*gin.Context`, `HttpRequest`).
|
||||
/// Not adversary-controlled directly; included so the binding
|
||||
/// captures every formal position.
|
||||
Implicit,
|
||||
}
|
||||
|
||||
/// Binding between a function formal and its external request slot.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ParamBinding {
|
||||
/// 0-based position in [`FuncSummary::param_names`].
|
||||
pub index: usize,
|
||||
/// Declared parameter name (mirrors
|
||||
/// `summary.param_names[index]`).
|
||||
pub name: String,
|
||||
/// External slot this parameter is wired to.
|
||||
pub source: ParamSource,
|
||||
}
|
||||
|
||||
/// Shape of how the handler writes a response. Track L plans to use
|
||||
/// this to pick the right oracle (HTML render → XSS, JSON → no-op,
|
||||
/// redirect → open-redirect).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ResponseShape {
|
||||
/// Response media kind.
|
||||
pub kind: ResponseKind,
|
||||
}
|
||||
|
||||
/// Coarse classification of a response writer's output.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ResponseKind {
|
||||
Json,
|
||||
Html,
|
||||
Text,
|
||||
Redirect,
|
||||
Stream,
|
||||
}
|
||||
|
||||
/// Middleware attached to a route (auth filter, CSRF guard,
|
||||
/// before-action, decorator chain, …). Adapters record the name so
|
||||
/// later phases can classify it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MiddlewareShape {
|
||||
/// Adapter-local middleware identifier (e.g. `"login_required"`,
|
||||
/// `"@PreAuthorize"`, `"csrf"`).
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Full framework binding for a function: every detail about how an
|
||||
/// external surface reaches the function body.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct FrameworkBinding {
|
||||
/// Stable id of the adapter that produced this binding. Equal to
|
||||
/// the originating [`FrameworkAdapter::name`]. Persisted into
|
||||
/// trace details verbatim.
|
||||
pub adapter: String,
|
||||
/// Entry-surface taxonomy bucket this function falls into.
|
||||
pub kind: EntryKind,
|
||||
/// HTTP route shape when [`Self::kind`] is
|
||||
/// [`EntryKind::HttpRoute`].
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub route: Option<RouteShape>,
|
||||
/// Per-formal external-slot classification. May be empty if the
|
||||
/// adapter does not yet model parameter shapes (e.g. a Phase-01
|
||||
/// stub).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub request_params: Vec<ParamBinding>,
|
||||
/// Response writer shape, when the adapter can determine it.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub response_writer: Option<ResponseShape>,
|
||||
/// Middleware chain attached to the route, in declaration order.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub middleware: Vec<MiddlewareShape>,
|
||||
}
|
||||
|
||||
/// Per-framework adapter trait. Each implementation inspects a
|
||||
/// function (via its [`FuncSummary`] and the file's AST root) and
|
||||
/// decides whether the function is bound to an external entry
|
||||
/// surface.
|
||||
///
|
||||
/// Implementations live next to the per-language harness emitters in
|
||||
/// [`crate::dynamic::lang`] and register into [`registry::adapters_for`]
|
||||
/// in subsequent Track-L phases. Phase 01 ships the trait and an
|
||||
/// empty registry per language.
|
||||
pub trait FrameworkAdapter: Sync {
|
||||
/// Stable adapter id (e.g. `"flask"`, `"spring-mvc"`, `"axum"`).
|
||||
/// Used for deterministic ordering inside the registry and for
|
||||
/// the trace-event detail string emitted by the verifier.
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// Language this adapter targets.
|
||||
fn lang(&self) -> Lang;
|
||||
|
||||
/// Inspect a function and return its [`FrameworkBinding`] when
|
||||
/// the function is driven by this adapter, otherwise `None`.
|
||||
///
|
||||
/// `ast` is the file's tree-sitter root node and `file_bytes` is
|
||||
/// the raw source so adapters can re-walk for decorators,
|
||||
/// routing macros, or registration sites that the
|
||||
/// [`FuncSummary`] alone does not preserve.
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding>;
|
||||
}
|
||||
|
||||
/// Walk every adapter registered for `lang` in registration order
|
||||
/// and return the first non-`None` binding. Returns `None` when no
|
||||
/// adapter matches or when no adapters are registered for `lang`.
|
||||
pub fn detect_binding(
|
||||
summary: &FuncSummary,
|
||||
ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
lang: Lang,
|
||||
) -> Option<FrameworkBinding> {
|
||||
for adapter in registry::adapters_for(lang) {
|
||||
debug_assert_eq!(
|
||||
adapter.lang(),
|
||||
lang,
|
||||
"adapter '{}' registered under wrong lang",
|
||||
adapter.name()
|
||||
);
|
||||
if let Some(binding) = adapter.detect(summary, ast, file_bytes) {
|
||||
return Some(binding);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::summary::FuncSummary;
|
||||
|
||||
fn synth_summary(name: &str, lang: &str) -> FuncSummary {
|
||||
FuncSummary {
|
||||
name: name.into(),
|
||||
file_path: "tests/synthetic.rs".into(),
|
||||
lang: lang.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_python(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_python::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_is_empty_for_every_lang_phase_01() {
|
||||
// Regression guard: Phase 01 ships the trait + dispatch
|
||||
// machinery but registers zero adapters. Subsequent Track-L
|
||||
// phases register concrete adapters per language; this test
|
||||
// documents the starting baseline so accidental re-ordering
|
||||
// is caught by `tests/determinism_audit.rs`.
|
||||
for lang in [
|
||||
Lang::Rust,
|
||||
Lang::C,
|
||||
Lang::Cpp,
|
||||
Lang::Java,
|
||||
Lang::Go,
|
||||
Lang::Php,
|
||||
Lang::Python,
|
||||
Lang::Ruby,
|
||||
Lang::TypeScript,
|
||||
Lang::JavaScript,
|
||||
] {
|
||||
assert!(
|
||||
registry::adapters_for(lang).is_empty(),
|
||||
"{:?} starts with zero registered adapters",
|
||||
lang
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_binding_returns_none_with_empty_registry() {
|
||||
// Empty registry means `detect_binding` short-circuits to
|
||||
// `None` for every input regardless of summary content.
|
||||
let summary = synth_summary("handler", "python");
|
||||
let src: &[u8] = b"def handler():\n pass\n";
|
||||
let tree = parse_python(src);
|
||||
let binding = detect_binding(&summary, tree.root_node(), src, Lang::Python);
|
||||
assert!(binding.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn framework_binding_round_trips_through_serde() {
|
||||
// The binding is persisted into repro bundles; ensure every
|
||||
// field round-trips.
|
||||
let original = FrameworkBinding {
|
||||
adapter: "flask".into(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape {
|
||||
method: HttpMethod::POST,
|
||||
path: "/users/{id}".into(),
|
||||
}),
|
||||
request_params: vec![ParamBinding {
|
||||
index: 0,
|
||||
name: "id".into(),
|
||||
source: ParamSource::PathSegment("id".into()),
|
||||
}],
|
||||
response_writer: Some(ResponseShape {
|
||||
kind: ResponseKind::Json,
|
||||
}),
|
||||
middleware: vec![MiddlewareShape {
|
||||
name: "login_required".into(),
|
||||
}],
|
||||
};
|
||||
let json = serde_json::to_string(&original).unwrap();
|
||||
let parsed: FrameworkBinding = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed, original);
|
||||
}
|
||||
}
|
||||
53
src/dynamic/framework/registry.rs
Normal file
53
src/dynamic/framework/registry.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
//! Per-language [`super::FrameworkAdapter`] dispatch table.
|
||||
//!
|
||||
//! Phase 01 (Track L.0) ships an empty table for every language; the
|
||||
//! [`super::FrameworkAdapter`] trait, [`super::FrameworkBinding`] data
|
||||
//! shape, and the [`super::detect_binding`] dispatcher are wired
|
||||
//! through so subsequent Track-L phases only need to register a
|
||||
//! concrete adapter here.
|
||||
//!
|
||||
//! # Ordering contract
|
||||
//!
|
||||
//! Within each `static` slice, adapters must be listed in alphabetical
|
||||
//! order of [`super::FrameworkAdapter::name`]. The lexical ordering
|
||||
//! gives a deterministic first-match result that survives merges /
|
||||
//! rebases without subtle re-ordering bugs. A `framework` unit test
|
||||
//! ([`super::tests::registry_is_empty_for_every_lang_phase_01`])
|
||||
//! captures the Phase-01 starting baseline so a phase that registers
|
||||
//! its first adapter is forced to update both the slice *and* the
|
||||
//! regression guard in the same change.
|
||||
|
||||
use super::FrameworkAdapter;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
/// Adapters registered for `lang`, returned in deterministic
|
||||
/// first-match order. Returns an empty slice for languages that have
|
||||
/// no adapters registered yet.
|
||||
pub fn adapters_for(lang: Lang) -> &'static [&'static dyn FrameworkAdapter] {
|
||||
match lang {
|
||||
Lang::Rust => RUST,
|
||||
Lang::C => C,
|
||||
Lang::Cpp => CPP,
|
||||
Lang::Java => JAVA,
|
||||
Lang::Go => GO,
|
||||
Lang::Php => PHP,
|
||||
Lang::Python => PYTHON,
|
||||
Lang::Ruby => RUBY,
|
||||
Lang::TypeScript => TYPESCRIPT,
|
||||
Lang::JavaScript => JAVASCRIPT,
|
||||
}
|
||||
}
|
||||
|
||||
// All slices intentionally empty in Phase 01. Later Track-L phases
|
||||
// register concrete adapters (Flask, Spring, axum, Express, …) into
|
||||
// the appropriate language slice.
|
||||
static RUST: &[&dyn FrameworkAdapter] = &[];
|
||||
static C: &[&dyn FrameworkAdapter] = &[];
|
||||
static CPP: &[&dyn FrameworkAdapter] = &[];
|
||||
static JAVA: &[&dyn FrameworkAdapter] = &[];
|
||||
static GO: &[&dyn FrameworkAdapter] = &[];
|
||||
static PHP: &[&dyn FrameworkAdapter] = &[];
|
||||
static PYTHON: &[&dyn FrameworkAdapter] = &[];
|
||||
static RUBY: &[&dyn FrameworkAdapter] = &[];
|
||||
static TYPESCRIPT: &[&dyn FrameworkAdapter] = &[];
|
||||
static JAVASCRIPT: &[&dyn FrameworkAdapter] = &[];
|
||||
|
|
@ -200,6 +200,7 @@ mod tests {
|
|||
spec_hash: "0000000000000000".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
};
|
||||
let err = build(&spec).unwrap_err();
|
||||
assert!(matches!(err, HarnessError::Unsupported(_)));
|
||||
|
|
@ -222,6 +223,7 @@ mod tests {
|
|||
spec_hash: "test0000abcd1234".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
};
|
||||
let harness = build(&spec).unwrap();
|
||||
assert!(harness.workdir.join("harness.py").exists());
|
||||
|
|
|
|||
|
|
@ -666,6 +666,7 @@ mod tests {
|
|||
spec_hash: "ctest0000000001".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -583,6 +583,7 @@ mod tests {
|
|||
spec_hash: "cpptest00000001".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -764,6 +764,7 @@ mod tests {
|
|||
spec_hash: "go0000000000001".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -998,6 +998,7 @@ mod tests {
|
|||
spec_hash: "java00000000001".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ mod tests {
|
|||
spec_hash: "js000000000001".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -990,6 +990,7 @@ mod tests {
|
|||
spec_hash: "jsshared00000001".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -606,6 +606,7 @@ mod tests {
|
|||
spec_hash: "php0000000000001".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1213,6 +1213,7 @@ mod tests {
|
|||
spec_hash: "00000000deadbeef".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -685,6 +685,7 @@ mod tests {
|
|||
spec_hash: "rb000000000001".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -797,6 +797,7 @@ mod tests {
|
|||
spec_hash: "rusttest00000001".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ mod tests {
|
|||
spec_hash: "ts000000000001ab".into(),
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ pub mod build_sandbox;
|
|||
pub mod corpus;
|
||||
pub mod differential;
|
||||
pub mod environment;
|
||||
pub mod framework;
|
||||
pub mod harness;
|
||||
pub mod lang;
|
||||
pub mod mount_filter;
|
||||
|
|
|
|||
|
|
@ -680,6 +680,7 @@ mod tests {
|
|||
spec_hash: "cafecafecafe0001".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
use crate::callgraph::{CallGraph, CallGraphAnalysis};
|
||||
use crate::commands::scan::Diag;
|
||||
use crate::dynamic::corpus::CORPUS_VERSION;
|
||||
use crate::dynamic::framework::FrameworkBinding;
|
||||
use crate::dynamic::stubs::StubKind;
|
||||
use crate::evidence::{Confidence, FlowStepKind, UnsupportedReason};
|
||||
use crate::labels::Cap;
|
||||
|
|
@ -124,6 +125,25 @@ pub struct HarnessSpec {
|
|||
/// the cache deserialise as an empty list.
|
||||
#[serde(default)]
|
||||
pub stubs_required: Vec<StubKind>,
|
||||
/// Track L.0 — framework binding recovered for the entry function
|
||||
/// (route shape, request slots, response writer, middleware chain).
|
||||
///
|
||||
/// Populated by [`crate::dynamic::framework::detect_binding`] when
|
||||
/// a registered [`crate::dynamic::framework::FrameworkAdapter`]
|
||||
/// matches the resolved entry; `None` when no adapter matches or
|
||||
/// when the spec-derivation path lacks the AST context required
|
||||
/// to dispatch. Phase 01 ships with an empty adapter registry so
|
||||
/// this field is `None` for every spec; subsequent Track-L phases
|
||||
/// register adapters and back-fill the binding.
|
||||
///
|
||||
/// Excluded from [`compute_spec_hash`]: the binding is descriptive
|
||||
/// metadata derived from the entry function and does not change
|
||||
/// the harness boundary topology that the spec hash protects.
|
||||
/// `#[serde(default, skip_serializing_if = "Option::is_none")]` so
|
||||
/// pre-Phase-01 serialised specs deserialise unchanged and an
|
||||
/// absent binding does not bloat repro-bundle JSON.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub framework: Option<FrameworkBinding>,
|
||||
}
|
||||
|
||||
fn default_derivation_strategy() -> SpecDerivationStrategy {
|
||||
|
|
@ -1054,11 +1074,51 @@ fn finalize_spec(
|
|||
spec_hash: String::new(),
|
||||
derivation,
|
||||
stubs_required,
|
||||
// Phase 01 (Track L.0): the framework adapter registry is
|
||||
// empty, so leave the binding unpopulated. Subsequent phases
|
||||
// back-fill via `attach_framework_binding` once the spec's
|
||||
// entry has been resolved and an AST is available.
|
||||
framework: None,
|
||||
};
|
||||
attach_framework_binding(&mut spec);
|
||||
spec.spec_hash = compute_spec_hash(&spec);
|
||||
spec
|
||||
}
|
||||
|
||||
/// Dispatch the resolved entry function through
|
||||
/// [`crate::dynamic::framework::detect_binding`] and stash the result
|
||||
/// on [`HarnessSpec::framework`].
|
||||
///
|
||||
/// Invoked unconditionally at the tail of [`finalize_spec`] so every
|
||||
/// strategy ([`SpecDerivationStrategy::FromFlowSteps`] …
|
||||
/// [`SpecDerivationStrategy::FromCallgraphEntry`]) benefits without
|
||||
/// per-strategy plumbing.
|
||||
///
|
||||
/// # Phase 01 contract
|
||||
///
|
||||
/// The framework adapter registry is empty in Phase 01, so this
|
||||
/// function fast-paths to a no-op when
|
||||
/// [`crate::dynamic::framework::registry::adapters_for`] returns an
|
||||
/// empty slice. That avoids parsing the entry file from disk in the
|
||||
/// common (empty) case and keeps the spec-derivation path side-effect
|
||||
/// free. Subsequent Track-L phases that register concrete adapters
|
||||
/// also extend this function to parse `spec.entry_file` and call
|
||||
/// [`crate::dynamic::framework::detect_binding`] with the resulting
|
||||
/// tree-sitter root.
|
||||
fn attach_framework_binding(spec: &mut HarnessSpec) {
|
||||
if crate::dynamic::framework::registry::adapters_for(spec.lang).is_empty() {
|
||||
return;
|
||||
}
|
||||
// Phase-01 stub. When Track L.1+ registers its first adapter,
|
||||
// this branch will (a) read `spec.entry_file` via
|
||||
// `std::fs::read`, (b) parse with the language's tree-sitter
|
||||
// grammar, (c) construct a `FuncSummary` from `spec` + the
|
||||
// matching summary index, and (d) call
|
||||
// `crate::dynamic::framework::detect_binding`. Left empty here
|
||||
// because Phase 01 ships zero adapters and the verifier's
|
||||
// acceptance test demands byte-identical verdicts.
|
||||
}
|
||||
|
||||
/// Walk `flow_steps` and return the entry point: the enclosing function of
|
||||
/// the first `Source` step that has a function annotation. This is the
|
||||
/// outermost callable that receives the tainted input.
|
||||
|
|
@ -1331,6 +1391,7 @@ mod tests {
|
|||
spec_hash: String::new(),
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
};
|
||||
spec.spec_hash = compute_spec_hash(&spec);
|
||||
spec
|
||||
|
|
|
|||
|
|
@ -640,6 +640,7 @@ mod tests {
|
|||
spec_hash: "abcd1234abcd1234".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,15 @@ use std::sync::Mutex;
|
|||
pub enum TraceStage {
|
||||
SpecStarted,
|
||||
SpecDone,
|
||||
/// Track L.0 — a [`crate::dynamic::framework::FrameworkAdapter`]
|
||||
/// claimed the spec's entry function. `detail` carries the
|
||||
/// adapter name verbatim (e.g. `"flask"`, `"spring-mvc"`).
|
||||
FrameworkAdapterDetected,
|
||||
/// Track L.0 — no registered adapter matched the spec's entry
|
||||
/// function. Emitted alongside [`Self::SpecDone`] for every spec
|
||||
/// so a trace consumer can audit framework-detection coverage by
|
||||
/// counting `framework_adapter_*` events.
|
||||
FrameworkAdapterNone,
|
||||
BuildStarted,
|
||||
BuildDone,
|
||||
SandboxStarted,
|
||||
|
|
@ -56,6 +65,8 @@ impl TraceStage {
|
|||
match self {
|
||||
Self::SpecStarted => "spec_started",
|
||||
Self::SpecDone => "spec_done",
|
||||
Self::FrameworkAdapterDetected => "framework_adapter_detected",
|
||||
Self::FrameworkAdapterNone => "framework_adapter_none",
|
||||
Self::BuildStarted => "build_started",
|
||||
Self::BuildDone => "build_done",
|
||||
Self::SandboxStarted => "sandbox_started",
|
||||
|
|
|
|||
|
|
@ -572,6 +572,25 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
|
|||
spec.spec_hash, spec.lang, spec.entry_kind
|
||||
)),
|
||||
);
|
||||
// Track L.0: surface framework-adapter dispatch outcome to the
|
||||
// trace so operators (and the Phase 30 determinism audit) can see
|
||||
// whether an adapter claimed the entry function. Phase 01 always
|
||||
// emits the `None` variant because the adapter registry is empty;
|
||||
// subsequent Track-L phases register adapters and switch the
|
||||
// event to `Detected` with the adapter name in `detail`.
|
||||
match &spec.framework {
|
||||
Some(binding) => trace.record(
|
||||
crate::dynamic::trace::TraceStage::FrameworkAdapterDetected,
|
||||
Some(format!(
|
||||
"adapter={} kind={:?}",
|
||||
binding.adapter, binding.kind
|
||||
)),
|
||||
),
|
||||
None => trace.record(
|
||||
crate::dynamic::trace::TraceStage::FrameworkAdapterNone,
|
||||
Some(format!("lang={:?} entry={}", spec.lang, spec.entry_name)),
|
||||
),
|
||||
}
|
||||
|
||||
// Pre-flight gate: surface a structured `Inconclusive(EntryKindUnsupported)`
|
||||
// up-front when the spec's [`EntryKind`] is not in the lang emitter's
|
||||
|
|
|
|||
|
|
@ -491,6 +491,7 @@ pub fn run_shape_fixture_lang(
|
|||
spec_hash: spec_hash.clone(),
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
};
|
||||
|
||||
// Phase 14: Java shape fixtures bundle annotation / type stubs as
|
||||
|
|
@ -785,6 +786,7 @@ pub fn run_harness_snapshot_lang(
|
|||
spec_hash: "snapshotsnapshot".into(),
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
};
|
||||
|
||||
let harness = lang_emit::emit(&spec).expect("emitter must produce a harness");
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ fn flask_spec(entry_rel: &str) -> HarnessSpec {
|
|||
spec_hash: "phase09testabcd1".into(),
|
||||
derivation: SpecDerivationStrategy::FromCallgraphEntry,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -744,6 +744,7 @@ public class App {
|
|||
spec_hash: "phase14staging00".into(),
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
};
|
||||
|
||||
let captured = capture_project_dependencies(project_root.path(), &spec);
|
||||
|
|
|
|||
|
|
@ -364,6 +364,7 @@ mod e2e_phase_08 {
|
|||
spec_hash: spec_hash.clone(),
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
};
|
||||
|
||||
(spec, tmp)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ mod repro_determinism_tests {
|
|||
spec_hash: spec_hash.to_owned(),
|
||||
derivation: nyx_scanner::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +173,7 @@ mod repro_determinism_tests {
|
|||
spec_hash: spec_hash.to_owned(),
|
||||
derivation: nyx_scanner::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -304,6 +306,7 @@ fn main() {
|
|||
spec_hash: spec_hash.to_owned(),
|
||||
derivation: nyx_scanner::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -359,6 +362,7 @@ fn main() {
|
|||
spec_hash: spec_hash.to_owned(),
|
||||
derivation: nyx_scanner::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -414,6 +418,7 @@ fn main() {
|
|||
spec_hash: spec_hash.to_owned(),
|
||||
derivation: nyx_scanner::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -469,6 +474,7 @@ fn main() {
|
|||
spec_hash: spec_hash.to_owned(),
|
||||
derivation: nyx_scanner::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ fn flask_eval_spec() -> HarnessSpec {
|
|||
spec_hash: FLASK_EVAL_SPEC_HASH.into(),
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ mod repro_hermetic_tests {
|
|||
spec_hash: "hermetic00000001".into(),
|
||||
derivation: nyx_scanner::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ fn make_spec(hash: &str) -> HarnessSpec {
|
|||
spec_hash: hash.into(),
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue