[pitboss] phase 18: Track M.0 — New EntryKind variants: ClassMethod, MessageHandler, ScheduledJob, GraphQLResolver, WebSocket, Middleware, Migration

This commit is contained in:
pitboss 2026-05-20 13:31:11 -05:00
parent 2b96c6005b
commit 1b2f9cb7ca
16 changed files with 750 additions and 178 deletions

View file

@ -28,7 +28,7 @@
//! - `PayloadSlot::Argv(n)` — `main(argc, argv)` shape: appended to argv.
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKindTag, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -40,10 +40,10 @@ pub struct CEmitter;
/// `Function` covers free functions (libfuzzer-style + plain (const
/// char*, size_t)). `CliSubcommand` covers `main(argc, argv)`.
/// `LibraryApi` covers libFuzzer `LLVMFuzzerTestOneInput`.
const SUPPORTED: &[EntryKind] = &[
EntryKind::Function,
EntryKind::CliSubcommand,
EntryKind::LibraryApi,
const SUPPORTED: &[EntryKindTag] = &[
EntryKindTag::Function,
EntryKindTag::CliSubcommand,
EntryKindTag::LibraryApi,
];
// ── Phase 16: shape detector ─────────────────────────────────────────────────
@ -66,7 +66,7 @@ impl CShape {
/// Detect the shape from `(spec, source)`.
pub fn detect(spec: &HarnessSpec, source: &str) -> Self {
let entry = spec.entry_name.as_str();
let kind = spec.entry_kind;
let kind = spec.entry_kind.tag();
let has_main_argv = (source.contains("int main(") || source.contains("int main ("))
&& (source.contains("argc") || source.contains("char *argv")
@ -80,8 +80,8 @@ impl CShape {
return Self::MainArgv;
}
match kind {
EntryKind::CliSubcommand => Self::MainArgv,
EntryKind::LibraryApi => Self::LibfuzzerEntry,
EntryKindTag::CliSubcommand => Self::MainArgv,
EntryKindTag::LibraryApi => Self::LibfuzzerEntry,
_ => Self::FreeFn,
}
}
@ -362,13 +362,13 @@ impl LangEmitter for CEmitter {
emit(spec)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
fn entry_kinds_supported(&self) -> &'static [EntryKindTag] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
fn entry_kind_hint(&self, attempted: EntryKindTag) -> String {
format!(
"c emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 16 shape dispatch (main / libFuzzer / free function)"
"c emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 16 / 19 / 20 / 21 shape dispatch (main / libFuzzer / free function + future class / msg / job adapters)"
)
}
@ -646,7 +646,7 @@ clean:
#[cfg(test)]
mod tests {
use super::*;
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot};
use crate::labels::Cap;
use crate::symbol::Lang;
@ -674,14 +674,14 @@ mod tests {
#[test]
fn entry_kinds_supported_is_non_empty() {
assert!(!CEmitter.entry_kinds_supported().is_empty());
assert!(CEmitter.entry_kinds_supported().contains(&EntryKind::Function));
assert!(CEmitter.entry_kinds_supported().contains(&EntryKind::CliSubcommand));
assert!(CEmitter.entry_kinds_supported().contains(&EntryKind::LibraryApi));
assert!(CEmitter.entry_kinds_supported().contains(&EntryKindTag::Function));
assert!(CEmitter.entry_kinds_supported().contains(&EntryKindTag::CliSubcommand));
assert!(CEmitter.entry_kinds_supported().contains(&EntryKindTag::LibraryApi));
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = CEmitter.entry_kind_hint(EntryKind::LibraryApi);
let hint = CEmitter.entry_kind_hint(EntryKindTag::LibraryApi);
assert!(hint.contains("LibraryApi"));
assert!(hint.contains("Phase 16"));
}

View file

@ -16,7 +16,7 @@
//! `g++ -O0 -std=c++17 -o nyx_harness main.cpp` in the workdir.
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKindTag, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -24,10 +24,10 @@ use std::path::PathBuf;
pub struct CppEmitter;
/// Entry kinds the C++ emitter understands after Phase 16.
const SUPPORTED: &[EntryKind] = &[
EntryKind::Function,
EntryKind::CliSubcommand,
EntryKind::LibraryApi,
const SUPPORTED: &[EntryKindTag] = &[
EntryKindTag::Function,
EntryKindTag::CliSubcommand,
EntryKindTag::LibraryApi,
];
// ── Phase 16: shape detector ─────────────────────────────────────────────────
@ -47,7 +47,7 @@ pub enum CppShape {
impl CppShape {
pub fn detect(spec: &HarnessSpec, source: &str) -> Self {
let entry = spec.entry_name.as_str();
let kind = spec.entry_kind;
let kind = spec.entry_kind.tag();
let has_main_argv = (source.contains("int main(") || source.contains("int main ("))
&& (source.contains("argc") || source.contains("char *argv")
@ -62,8 +62,8 @@ impl CppShape {
return Self::MainArgv;
}
match kind {
EntryKind::CliSubcommand => Self::MainArgv,
EntryKind::LibraryApi => Self::LibfuzzerEntry,
EntryKindTag::CliSubcommand => Self::MainArgv,
EntryKindTag::LibraryApi => Self::LibfuzzerEntry,
_ => Self::FreeFn,
}
}
@ -315,13 +315,13 @@ impl LangEmitter for CppEmitter {
emit(spec)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
fn entry_kinds_supported(&self) -> &'static [EntryKindTag] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
fn entry_kind_hint(&self, attempted: EntryKindTag) -> String {
format!(
"cpp emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 16 shape dispatch (main / libFuzzer / free function)"
"cpp emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 16 / 19 / 20 / 21 shape dispatch (main / libFuzzer / free function + future class / msg / job adapters)"
)
}
@ -563,7 +563,7 @@ add_executable(nyx_harness main.cpp)
#[cfg(test)]
mod tests {
use super::*;
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot};
use crate::labels::Cap;
use crate::symbol::Lang;
@ -591,14 +591,14 @@ mod tests {
#[test]
fn entry_kinds_supported_is_non_empty() {
assert!(!CppEmitter.entry_kinds_supported().is_empty());
assert!(CppEmitter.entry_kinds_supported().contains(&EntryKind::Function));
assert!(CppEmitter.entry_kinds_supported().contains(&EntryKind::CliSubcommand));
assert!(CppEmitter.entry_kinds_supported().contains(&EntryKind::LibraryApi));
assert!(CppEmitter.entry_kinds_supported().contains(&EntryKindTag::Function));
assert!(CppEmitter.entry_kinds_supported().contains(&EntryKindTag::CliSubcommand));
assert!(CppEmitter.entry_kinds_supported().contains(&EntryKindTag::LibraryApi));
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = CppEmitter.entry_kind_hint(EntryKind::CliSubcommand);
let hint = CppEmitter.entry_kind_hint(EntryKindTag::CliSubcommand);
assert!(hint.contains("CliSubcommand"));
assert!(hint.contains("Phase 16"));
}

View file

@ -38,7 +38,7 @@
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKindTag, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -51,10 +51,10 @@ pub struct GoEmitter;
/// `HttpRoute` covers `net/http` and gin handlers. `CliSubcommand`
/// covers `flag.Parse` CLIs. `Function` covers plain functions and
/// fuzz harnesses.
const SUPPORTED: &[EntryKind] = &[
EntryKind::Function,
EntryKind::HttpRoute,
EntryKind::CliSubcommand,
const SUPPORTED: &[EntryKindTag] = &[
EntryKindTag::Function,
EntryKindTag::HttpRoute,
EntryKindTag::CliSubcommand,
];
impl LangEmitter for GoEmitter {
@ -62,13 +62,13 @@ impl LangEmitter for GoEmitter {
emit(spec)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
fn entry_kinds_supported(&self) -> &'static [EntryKindTag] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
fn entry_kind_hint(&self, attempted: EntryKindTag) -> String {
format!(
"go emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 15 shape dispatch"
"go emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 15 / 19 / 20 / 21 shape dispatch"
)
}
@ -215,7 +215,7 @@ impl GoShape {
/// to [`Self::Generic`]).
pub fn detect(spec: &HarnessSpec, source: &str) -> Self {
let entry = spec.entry_name.as_str();
let kind = spec.entry_kind;
let kind = spec.entry_kind.tag();
let has_http_handler = source.contains("http.ResponseWriter")
&& source.contains("*http.Request");
@ -265,10 +265,10 @@ impl GoShape {
if has_fuzz_signature {
return Self::FuzzVariadic;
}
if kind == EntryKind::HttpRoute {
if kind == EntryKindTag::HttpRoute {
return Self::HttpHandlerFunc;
}
if kind == EntryKind::CliSubcommand {
if kind == EntryKindTag::CliSubcommand {
return Self::FlagParseCli;
}
Self::Generic
@ -1098,7 +1098,7 @@ pub fn capitalize_first(s: &str) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot};
use crate::labels::Cap;
use crate::symbol::Lang;
@ -1168,14 +1168,14 @@ mod tests {
#[test]
fn entry_kinds_supported_is_non_empty() {
assert!(!GoEmitter.entry_kinds_supported().is_empty());
assert!(GoEmitter.entry_kinds_supported().contains(&EntryKind::Function));
assert!(GoEmitter.entry_kinds_supported().contains(&EntryKind::HttpRoute));
assert!(GoEmitter.entry_kinds_supported().contains(&EntryKind::CliSubcommand));
assert!(GoEmitter.entry_kinds_supported().contains(&EntryKindTag::Function));
assert!(GoEmitter.entry_kinds_supported().contains(&EntryKindTag::HttpRoute));
assert!(GoEmitter.entry_kinds_supported().contains(&EntryKindTag::CliSubcommand));
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = GoEmitter.entry_kind_hint(EntryKind::LibraryApi);
let hint = GoEmitter.entry_kind_hint(EntryKindTag::LibraryApi);
assert!(hint.contains("LibraryApi"));
assert!(hint.contains("Phase 15"));
}

View file

@ -37,7 +37,7 @@
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKindTag, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -50,10 +50,10 @@ pub struct JavaEmitter;
/// `HttpRoute` covers servlet / Spring / Quarkus shapes. `CliSubcommand`
/// covers `public static void main(String[])`. `Function` covers JUnit
/// tests and plain static methods.
const SUPPORTED: &[EntryKind] = &[
EntryKind::Function,
EntryKind::HttpRoute,
EntryKind::CliSubcommand,
const SUPPORTED: &[EntryKindTag] = &[
EntryKindTag::Function,
EntryKindTag::HttpRoute,
EntryKindTag::CliSubcommand,
];
impl LangEmitter for JavaEmitter {
@ -61,13 +61,13 @@ impl LangEmitter for JavaEmitter {
emit(spec)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
fn entry_kinds_supported(&self) -> &'static [EntryKindTag] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
fn entry_kind_hint(&self, attempted: EntryKindTag) -> String {
format!(
"java emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 14 shape dispatch"
"java emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 14 / 19 / 20 / 21 shape dispatch"
)
}
@ -204,7 +204,7 @@ impl JavaShape {
/// pipeline tagged the entry kind as [`EntryKind::Function`].
pub fn detect(spec: &HarnessSpec, source: &str) -> Self {
let entry = spec.entry_name.as_str();
let kind = spec.entry_kind;
let kind = spec.entry_kind.tag();
let has_servlet = source.contains("HttpServlet")
|| source.contains("javax.servlet")
@ -256,10 +256,10 @@ impl JavaShape {
return Self::JunitTest;
}
if kind == EntryKind::CliSubcommand {
if kind == EntryKindTag::CliSubcommand {
return Self::StaticMain;
}
if kind == EntryKind::HttpRoute {
if kind == EntryKindTag::HttpRoute {
return Self::SpringController;
}
Self::StaticMethod
@ -1810,7 +1810,7 @@ const JUNIT_HELPER: &str = r#"
#[cfg(test)]
mod tests {
use super::*;
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot};
use crate::labels::Cap;
use crate::symbol::Lang;
@ -1883,18 +1883,18 @@ mod tests {
assert!(!JavaEmitter.entry_kinds_supported().is_empty());
assert!(JavaEmitter
.entry_kinds_supported()
.contains(&EntryKind::Function));
.contains(&EntryKindTag::Function));
assert!(JavaEmitter
.entry_kinds_supported()
.contains(&EntryKind::HttpRoute));
.contains(&EntryKindTag::HttpRoute));
assert!(JavaEmitter
.entry_kinds_supported()
.contains(&EntryKind::CliSubcommand));
.contains(&EntryKindTag::CliSubcommand));
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = JavaEmitter.entry_kind_hint(EntryKind::LibraryApi);
let hint = JavaEmitter.entry_kind_hint(EntryKindTag::LibraryApi);
assert!(hint.contains("LibraryApi"));
assert!(hint.contains("Phase 14"));
}
@ -2380,7 +2380,7 @@ mod tests {
for (name, body, entry_name, kind, expected) in cases {
let path = dir.join(name);
std::fs::write(&path, body).expect("write fixture");
let spec = make_spec_with(*kind, entry_name, path.to_str().unwrap());
let spec = make_spec_with(kind.clone(), entry_name, path.to_str().unwrap());
assert_eq!(detect_shape(&spec), *expected, "case {name}");
}
let _ = std::fs::remove_dir_all(&dir);

View file

@ -16,7 +16,7 @@
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{js_shared, ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec};
use crate::dynamic::spec::{EntryKindTag, HarnessSpec};
use crate::evidence::UnsupportedReason;
pub use js_shared::{detect_shape, materialize_node, probe_shim, JsShape};
@ -29,13 +29,13 @@ impl LangEmitter for JavaScriptEmitter {
emit(spec)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
fn entry_kinds_supported(&self) -> &'static [EntryKindTag] {
js_shared::SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
fn entry_kind_hint(&self, attempted: EntryKindTag) -> String {
format!(
"javascript emitter supports {supported:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 13 shape dispatch in `js_shared`",
"javascript emitter supports {supported:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 13 / 19 / 20 / 21 shape dispatch in `js_shared`",
supported = js_shared::SUPPORTED,
)
}
@ -61,7 +61,7 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
#[cfg(test)]
mod tests {
use super::*;
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot};
use crate::labels::Cap;
use crate::symbol::Lang;
@ -144,14 +144,14 @@ mod tests {
#[test]
fn entry_kinds_supported_includes_http_and_cli_after_phase_13() {
let kinds = JavaScriptEmitter.entry_kinds_supported();
assert!(kinds.contains(&EntryKind::Function));
assert!(kinds.contains(&EntryKind::HttpRoute));
assert!(kinds.contains(&EntryKind::CliSubcommand));
assert!(kinds.contains(&EntryKindTag::Function));
assert!(kinds.contains(&EntryKindTag::HttpRoute));
assert!(kinds.contains(&EntryKindTag::CliSubcommand));
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = JavaScriptEmitter.entry_kind_hint(EntryKind::HttpRoute);
let hint = JavaScriptEmitter.entry_kind_hint(EntryKindTag::HttpRoute);
assert!(hint.contains("HttpRoute"));
assert!(hint.contains("Phase 13"));
}

View file

@ -25,7 +25,7 @@
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKindTag, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use crate::utils::project::DetectedFramework;
use std::path::PathBuf;
@ -73,7 +73,7 @@ impl JsShape {
/// Detect the shape from `(spec, source)`. Framework / runtime
/// markers in the source win over `spec.entry_kind`.
pub fn detect(spec: &HarnessSpec, source: &str) -> Self {
let kind = spec.entry_kind;
let kind = spec.entry_kind.tag();
let entry = spec.entry_name.as_str();
// ── Framework / runtime markers ─────────────────────────────
@ -155,7 +155,7 @@ impl JsShape {
return Self::BrowserEvent;
}
if kind == EntryKind::HttpRoute {
if kind == EntryKindTag::HttpRoute {
return Self::Express;
}
@ -1629,11 +1629,11 @@ fn resolve_http_payload(slot: &PayloadSlot) -> (&'static str, String, &'static s
}
/// Supported entry kinds for both JS + TS after Phase 13.
pub const SUPPORTED: &[EntryKind] = &[
EntryKind::Function,
EntryKind::HttpRoute,
EntryKind::CliSubcommand,
EntryKind::LibraryApi,
pub const SUPPORTED: &[EntryKindTag] = &[
EntryKindTag::Function,
EntryKindTag::HttpRoute,
EntryKindTag::CliSubcommand,
EntryKindTag::LibraryApi,
];
#[cfg(test)]

View file

@ -27,7 +27,7 @@ pub mod rust;
pub mod typescript;
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::spec::{EntryKind, HarnessSpec};
use crate::dynamic::spec::{EntryKindTag, HarnessSpec};
use crate::evidence::UnsupportedReason;
use crate::symbol::Lang;
@ -132,14 +132,17 @@ pub trait LangEmitter {
/// Build a harness source bundle for `spec`.
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason>;
/// The set of [`EntryKind`] variants this emitter understands.
/// The set of [`EntryKind`] variants this emitter understands,
/// projected to the [`EntryKindTag`] discriminant so the slice can
/// live in `'static` storage even after Phase 18 extended
/// `EntryKind` with data-bearing variants.
///
/// Must be non-empty: every emitter advertises at least one shape it can
/// (or will) drive — even stub modules whose `emit` returns
/// `LangUnsupported`. Empty would be indistinguishable from "language
/// not in the dispatch table" and would defeat the structured
/// advertisement that callers consume.
fn entry_kinds_supported(&self) -> &'static [EntryKind];
fn entry_kinds_supported(&self) -> &'static [EntryKindTag];
/// Human-actionable hint produced when `attempted` is not in
/// [`entry_kinds_supported`](LangEmitter::entry_kinds_supported).
@ -149,7 +152,7 @@ pub trait LangEmitter {
/// surfaces directly to operators triaging dynamic verification gaps;
/// keep it specific (name the supported kinds, name the phase that will
/// extend support).
fn entry_kind_hint(&self, attempted: EntryKind) -> String;
fn entry_kind_hint(&self, attempted: EntryKindTag) -> String;
/// Synthesise the language-specific manifest / lockfile contents that
/// pin the [`Environment`]'s direct deps + toolchain into a file the
@ -251,7 +254,7 @@ pub fn materialize_runtime(env: &Environment) -> RuntimeArtifacts {
/// in (instead of producing a never-runnable harness).
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
let supported = entry_kinds_supported(spec.lang);
if !supported.is_empty() && !supported.contains(&spec.entry_kind) {
if !supported.is_empty() && !supported.contains(&spec.entry_kind.tag()) {
return Err(UnsupportedReason::EntryKindUnsupported);
}
dispatch(spec.lang, |e| e.emit(spec))
@ -263,7 +266,7 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
/// Returns an empty slice when `lang` has no registered emitter — callers
/// distinguish that from "emitter exists but advertises none" by treating
/// empty as "language unsupported".
pub fn entry_kinds_supported(lang: Lang) -> &'static [EntryKind] {
pub fn entry_kinds_supported(lang: Lang) -> &'static [EntryKindTag] {
dispatch(lang, |e| e.entry_kinds_supported()).unwrap_or(&[])
}
@ -271,7 +274,7 @@ pub fn entry_kinds_supported(lang: Lang) -> &'static [EntryKind] {
///
/// Falls back to a generic message when `lang` has no registered emitter so
/// callers do not need to special-case that path.
pub fn entry_kind_hint(lang: Lang, attempted: EntryKind) -> String {
pub fn entry_kind_hint(lang: Lang, attempted: EntryKindTag) -> String {
dispatch(lang, |e| e.entry_kind_hint(attempted)).unwrap_or_else(|| {
format!(
"no harness emitter is registered for {lang:?}; attempted {attempted}"
@ -300,6 +303,7 @@ fn dispatch<R>(lang: Lang, f: impl FnOnce(&dyn LangEmitter) -> R) -> Option<R> {
#[cfg(test)]
mod tests {
use super::*;
use crate::dynamic::spec::EntryKind;
/// Every registered emitter must advertise at least one entry kind so the
/// verifier never produces an empty `supported` list in
@ -328,10 +332,110 @@ mod tests {
#[test]
fn entry_kind_hint_mentions_attempted() {
let hint = entry_kind_hint(Lang::Python, EntryKind::HttpRoute);
let hint = entry_kind_hint(Lang::Python, EntryKindTag::HttpRoute);
assert!(
hint.contains("HttpRoute"),
"hint must mention the attempted entry kind, got: {hint:?}"
);
}
/// Phase 18 (Track M.0) — every Phase 18 variant resolves to a
/// distinct [`EntryKindTag`] via [`EntryKind::tag`], and the
/// per-language emitters short-circuit those tags with a typed
/// `Inconclusive(EntryKindUnsupported)` hint that mentions the
/// follow-up phase that will close the gap.
#[test]
fn entry_kind_tag_round_trips_for_phase_18_variants() {
use crate::evidence::EntryKindTag as T;
assert_eq!(EntryKind::Function.tag(), T::Function);
assert_eq!(EntryKind::HttpRoute.tag(), T::HttpRoute);
assert_eq!(EntryKind::CliSubcommand.tag(), T::CliSubcommand);
assert_eq!(EntryKind::LibraryApi.tag(), T::LibraryApi);
assert_eq!(
EntryKind::ClassMethod {
class: "Cls".into(),
method: "do".into(),
}
.tag(),
T::ClassMethod
);
assert_eq!(
EntryKind::MessageHandler {
queue: "q".into(),
message_schema: None,
}
.tag(),
T::MessageHandler
);
assert_eq!(
EntryKind::ScheduledJob { schedule: None }.tag(),
T::ScheduledJob
);
assert_eq!(
EntryKind::GraphQLResolver {
type_name: "User".into(),
field: "name".into(),
}
.tag(),
T::GraphQLResolver
);
assert_eq!(
EntryKind::WebSocket { path: "/ws".into() }.tag(),
T::WebSocket
);
assert_eq!(
EntryKind::Middleware { name: "auth".into() }.tag(),
T::Middleware
);
assert_eq!(
EntryKind::Migration { version: None }.tag(),
T::Migration
);
assert_eq!(EntryKind::Unknown.tag(), T::Unknown);
}
/// Phase 18 (Track M.0) — none of the Phase 18 variants are wired
/// into any per-language emitter yet (those land in Phase 19 /
/// 20 / 21). Confirm every lang routes them through the
/// supported-set gate so the verifier produces a structured
/// `Inconclusive(EntryKindUnsupported)` rather than degrading
/// silently.
#[test]
fn entry_kind_phase_18_variants_are_unsupported_everywhere() {
use crate::evidence::EntryKindTag as T;
let new = [
T::ClassMethod,
T::MessageHandler,
T::ScheduledJob,
T::GraphQLResolver,
T::WebSocket,
T::Middleware,
T::Migration,
];
for lang in [
Lang::Python,
Lang::Rust,
Lang::JavaScript,
Lang::TypeScript,
Lang::Go,
Lang::Java,
Lang::Php,
Lang::Ruby,
Lang::C,
Lang::Cpp,
] {
let supported = entry_kinds_supported(lang);
for tag in new {
assert!(
!supported.contains(&tag),
"{lang:?} prematurely advertised {tag:?} — Phase 18 keeps the new variants unsupported until Phase 19 / 20 / 21 lands the per-lang adapters"
);
let hint = entry_kind_hint(lang, tag);
assert!(
hint.contains(tag.as_str()),
"{lang:?} hint must mention {tag:?}, got: {hint:?}"
);
}
}
}
}

View file

@ -30,7 +30,7 @@
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKindTag, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -43,10 +43,10 @@ pub struct PhpEmitter;
/// `HttpRoute` covers Slim / Laravel / Symfony route closures.
/// `CliSubcommand` covers `$argv`-driven CLI scripts. `Function`
/// covers plain functions and top-level scripts.
const SUPPORTED: &[EntryKind] = &[
EntryKind::Function,
EntryKind::HttpRoute,
EntryKind::CliSubcommand,
const SUPPORTED: &[EntryKindTag] = &[
EntryKindTag::Function,
EntryKindTag::HttpRoute,
EntryKindTag::CliSubcommand,
];
impl LangEmitter for PhpEmitter {
@ -54,13 +54,13 @@ impl LangEmitter for PhpEmitter {
emit(spec)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
fn entry_kinds_supported(&self) -> &'static [EntryKindTag] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
fn entry_kind_hint(&self, attempted: EntryKindTag) -> String {
format!(
"php emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 15 shape dispatch"
"php emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 15 / 19 / 20 / 21 shape dispatch"
)
}
@ -174,7 +174,7 @@ impl PhpShape {
/// the source win over `spec.entry_kind`.
pub fn detect(spec: &HarnessSpec, source: &str) -> Self {
let entry = spec.entry_name.as_str();
let kind = spec.entry_kind;
let kind = spec.entry_kind.tag();
let has_symfony_marker = source.contains("#[Route(")
|| source.contains("Symfony\\Component\\Routing")
@ -231,10 +231,10 @@ impl PhpShape {
if has_argv && !entry_named_function {
return Self::CliArgvScript;
}
if kind == EntryKind::HttpRoute {
if kind == EntryKindTag::HttpRoute {
return Self::RouteClosure;
}
if kind == EntryKind::CliSubcommand {
if kind == EntryKindTag::CliSubcommand {
return Self::CliArgvScript;
}
// TopLevelScript only fires when we actually saw the source
@ -1215,7 +1215,7 @@ fn function_exists_call(_func: &str) -> bool {
#[cfg(test)]
mod tests {
use super::*;
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot};
use crate::labels::Cap;
use crate::symbol::Lang;
@ -1294,18 +1294,18 @@ mod tests {
assert!(!PhpEmitter.entry_kinds_supported().is_empty());
assert!(PhpEmitter
.entry_kinds_supported()
.contains(&EntryKind::Function));
.contains(&EntryKindTag::Function));
assert!(PhpEmitter
.entry_kinds_supported()
.contains(&EntryKind::HttpRoute));
.contains(&EntryKindTag::HttpRoute));
assert!(PhpEmitter
.entry_kinds_supported()
.contains(&EntryKind::CliSubcommand));
.contains(&EntryKindTag::CliSubcommand));
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = PhpEmitter.entry_kind_hint(EntryKind::LibraryApi);
let hint = PhpEmitter.entry_kind_hint(EntryKindTag::LibraryApi);
assert!(hint.contains("LibraryApi"));
assert!(hint.contains("Phase 15"));
}

View file

@ -24,7 +24,7 @@
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKindTag, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use crate::utils::project::DetectedFramework;
use std::path::PathBuf;
@ -41,10 +41,10 @@ pub struct PythonEmitter;
/// argparse `main()` functions. `Function` covers pytest, async
/// coroutines, Celery tasks, and generic module-level functions
/// (positional + kwargs).
const SUPPORTED: &[EntryKind] = &[
EntryKind::Function,
EntryKind::HttpRoute,
EntryKind::CliSubcommand,
const SUPPORTED: &[EntryKindTag] = &[
EntryKindTag::Function,
EntryKindTag::HttpRoute,
EntryKindTag::CliSubcommand,
];
impl LangEmitter for PythonEmitter {
@ -52,13 +52,13 @@ impl LangEmitter for PythonEmitter {
emit(spec)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
fn entry_kinds_supported(&self) -> &'static [EntryKindTag] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
fn entry_kind_hint(&self, attempted: EntryKindTag) -> String {
format!(
"python emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 12 shape dispatch"
"python emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 12 / 19 / 20 / 21 shape dispatch"
)
}
@ -177,7 +177,7 @@ impl PythonShape {
/// the legacy substring-only entry-kind heuristic.
pub fn detect(spec: &HarnessSpec, source: &str) -> Self {
let entry = spec.entry_name.as_str();
let kind = spec.entry_kind;
let kind = spec.entry_kind.tag();
// ── Framework-first detection ────────────────────────────────
let has_flask =
@ -224,14 +224,14 @@ impl PythonShape {
return Self::FlaskRoute;
}
if kind == EntryKind::HttpRoute {
if kind == EntryKindTag::HttpRoute {
// The flow-step said HTTP but no framework import was
// detected — fall back to Flask which has the most forgiving
// test client wiring.
return Self::FlaskRoute;
}
if kind == EntryKind::CliSubcommand
if kind == EntryKindTag::CliSubcommand
|| entry == "main"
|| entry == "__main__"
|| source.contains("if __name__ == \"__main__\"")
@ -1925,7 +1925,7 @@ fn module_name(entry_file: &str) -> &str {
#[cfg(test)]
mod tests {
use super::*;
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot};
use crate::labels::Cap;
use crate::symbol::Lang;
@ -1992,14 +1992,14 @@ mod tests {
#[test]
fn entry_kinds_supported_includes_http_and_cli() {
let kinds = PythonEmitter.entry_kinds_supported();
assert!(kinds.contains(&EntryKind::Function));
assert!(kinds.contains(&EntryKind::HttpRoute));
assert!(kinds.contains(&EntryKind::CliSubcommand));
assert!(kinds.contains(&EntryKindTag::Function));
assert!(kinds.contains(&EntryKindTag::HttpRoute));
assert!(kinds.contains(&EntryKindTag::CliSubcommand));
}
#[test]
fn entry_kind_hint_names_attempted() {
let hint = PythonEmitter.entry_kind_hint(EntryKind::LibraryApi);
let hint = PythonEmitter.entry_kind_hint(EntryKindTag::LibraryApi);
assert!(hint.contains("LibraryApi"));
}

View file

@ -28,7 +28,7 @@
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKindTag, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -40,10 +40,10 @@ pub struct RubyEmitter;
/// `HttpRoute` covers Sinatra / Rails / Rack. `CliSubcommand` covers
/// `ARGV`-driven scripts. `Function` covers plain methods and
/// controller method shapes.
const SUPPORTED: &[EntryKind] = &[
EntryKind::Function,
EntryKind::HttpRoute,
EntryKind::CliSubcommand,
const SUPPORTED: &[EntryKindTag] = &[
EntryKindTag::Function,
EntryKindTag::HttpRoute,
EntryKindTag::CliSubcommand,
];
impl LangEmitter for RubyEmitter {
@ -51,13 +51,13 @@ impl LangEmitter for RubyEmitter {
emit(spec)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
fn entry_kinds_supported(&self) -> &'static [EntryKindTag] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
fn entry_kind_hint(&self, attempted: EntryKindTag) -> String {
format!(
"ruby emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 15 shape dispatch"
"ruby emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 15 / 19 / 20 / 21 shape dispatch"
)
}
@ -154,7 +154,7 @@ impl RubyShape {
/// the source win over `spec.entry_kind`.
pub fn detect(spec: &HarnessSpec, source: &str) -> Self {
let entry = spec.entry_name.as_str();
let kind = spec.entry_kind;
let kind = spec.entry_kind.tag();
let has_sinatra = source.contains("require 'sinatra'")
|| source.contains("require \"sinatra\"")
@ -188,7 +188,7 @@ impl RubyShape {
if has_rack {
return Self::RackMiddleware;
}
if kind == EntryKind::HttpRoute && has_class {
if kind == EntryKindTag::HttpRoute && has_class {
return Self::ControllerMethod;
}
if has_class && has_def && !entry.is_empty() && !entry_named_class {
@ -959,7 +959,7 @@ fn parse_first_class_name(source: &str) -> Option<String> {
#[cfg(test)]
mod tests {
use super::*;
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot};
use crate::labels::Cap;
use crate::symbol::Lang;
@ -989,18 +989,18 @@ mod tests {
assert!(!RubyEmitter.entry_kinds_supported().is_empty());
assert!(RubyEmitter
.entry_kinds_supported()
.contains(&EntryKind::Function));
.contains(&EntryKindTag::Function));
assert!(RubyEmitter
.entry_kinds_supported()
.contains(&EntryKind::HttpRoute));
.contains(&EntryKindTag::HttpRoute));
assert!(RubyEmitter
.entry_kinds_supported()
.contains(&EntryKind::CliSubcommand));
.contains(&EntryKindTag::CliSubcommand));
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = RubyEmitter.entry_kind_hint(EntryKind::LibraryApi);
let hint = RubyEmitter.entry_kind_hint(EntryKindTag::LibraryApi);
assert!(hint.contains("LibraryApi"));
assert!(hint.contains("Phase 15"));
}

View file

@ -23,7 +23,7 @@
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKindTag, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use crate::labels::Cap;
use std::path::PathBuf;
@ -38,11 +38,11 @@ pub struct RustEmitter;
/// covers clap-driven CLIs. `LibraryApi` covers libfuzzer
/// `fuzz_target!` entry points. `Function` covers plain free functions
/// and is the fallback when shape detection is inconclusive.
const SUPPORTED: &[EntryKind] = &[
EntryKind::Function,
EntryKind::HttpRoute,
EntryKind::CliSubcommand,
EntryKind::LibraryApi,
const SUPPORTED: &[EntryKindTag] = &[
EntryKindTag::Function,
EntryKindTag::HttpRoute,
EntryKindTag::CliSubcommand,
EntryKindTag::LibraryApi,
];
impl LangEmitter for RustEmitter {
@ -50,13 +50,13 @@ impl LangEmitter for RustEmitter {
emit(spec)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
fn entry_kinds_supported(&self) -> &'static [EntryKindTag] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
fn entry_kind_hint(&self, attempted: EntryKindTag) -> String {
format!(
"rust emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 16 shape dispatch (actix / axum / clap / libfuzzer)"
"rust emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 16 / 19 / 20 / 21 shape dispatch (actix / axum / clap / libfuzzer + future class / msg / job adapters)"
)
}
@ -527,7 +527,7 @@ impl RustShape {
/// bytes of the entry file (best-effort — empty string falls back
/// to [`Self::Generic`]).
pub fn detect(spec: &HarnessSpec, source: &str) -> Self {
let kind = spec.entry_kind;
let kind = spec.entry_kind.tag();
let entry = spec.entry_name.as_str();
let has_warp = source.contains("use warp::")
@ -598,9 +598,9 @@ impl RustShape {
return Self::LibfuzzerTarget;
}
match kind {
EntryKind::HttpRoute => Self::ActixWebRoute,
EntryKind::CliSubcommand => Self::ClapCli,
EntryKind::LibraryApi => Self::LibfuzzerTarget,
EntryKindTag::HttpRoute => Self::ActixWebRoute,
EntryKindTag::CliSubcommand => Self::ClapCli,
EntryKindTag::LibraryApi => Self::LibfuzzerTarget,
_ => Self::Generic,
}
}
@ -1050,7 +1050,7 @@ fn clap_invocation(spec: &HarnessSpec, func: &str) -> (String, String) {
#[cfg(test)]
mod tests {
use super::*;
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot};
use crate::labels::Cap;
use crate::symbol::Lang;
@ -1140,12 +1140,12 @@ mod tests {
assert!(!RustEmitter.entry_kinds_supported().is_empty());
assert!(RustEmitter
.entry_kinds_supported()
.contains(&EntryKind::Function));
.contains(&EntryKindTag::Function));
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = RustEmitter.entry_kind_hint(EntryKind::LibraryApi);
let hint = RustEmitter.entry_kind_hint(EntryKindTag::LibraryApi);
assert!(hint.contains("LibraryApi"));
assert!(hint.contains("Phase 16"));
}

View file

@ -16,7 +16,7 @@
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{js_shared, ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec};
use crate::dynamic::spec::{EntryKindTag, HarnessSpec};
use crate::evidence::UnsupportedReason;
/// Zero-sized [`LangEmitter`] handle for TypeScript.
@ -32,13 +32,13 @@ impl LangEmitter for TypeScriptEmitter {
js_shared::emit(spec, true)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
fn entry_kinds_supported(&self) -> &'static [EntryKindTag] {
js_shared::SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
fn entry_kind_hint(&self, attempted: EntryKindTag) -> String {
format!(
"typescript emitter supports {supported:?} (shared dispatch with javascript via `js_shared`); this finding's enclosing context is `EntryKind::{attempted}` — see Phase 13 shape dispatch",
"typescript emitter supports {supported:?} (shared dispatch with javascript via `js_shared`); this finding's enclosing context is `EntryKind::{attempted}` — see Phase 13 / 19 / 20 / 21 shape dispatch",
supported = js_shared::SUPPORTED,
)
}
@ -59,7 +59,7 @@ impl LangEmitter for TypeScriptEmitter {
#[cfg(test)]
mod tests {
use super::*;
use crate::dynamic::spec::{HarnessSpec, PayloadSlot, SpecDerivationStrategy};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy};
use crate::labels::Cap;
use crate::symbol::Lang;
@ -89,12 +89,12 @@ mod tests {
assert!(!TypeScriptEmitter.entry_kinds_supported().is_empty());
assert!(TypeScriptEmitter
.entry_kinds_supported()
.contains(&EntryKind::HttpRoute));
.contains(&EntryKindTag::HttpRoute));
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = TypeScriptEmitter.entry_kind_hint(EntryKind::HttpRoute);
let hint = TypeScriptEmitter.entry_kind_hint(EntryKindTag::HttpRoute);
assert!(hint.contains("HttpRoute"));
assert!(hint.contains("Phase 13"));
}

View file

@ -58,6 +58,14 @@ pub struct EntryRef {
/// attempted / supported variants without depending on the `dynamic` feature.
pub use crate::evidence::EntryKind;
/// Re-export of [`crate::evidence::EntryKindTag`].
///
/// The discriminant tag used by every site that needs a `Copy + Hash`
/// handle to an `EntryKind`: supported-set lookups, the
/// [`crate::evidence::InconclusiveReason::EntryKindUnsupported`] fields,
/// the lang-emitter trait surface.
pub use crate::evidence::EntryKindTag;
/// Where the payload goes when the harness fires.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PayloadSlot {
@ -363,7 +371,7 @@ impl HarnessSpec {
/// `Unsupported`.
pub fn entry_kind_is_supported(&self) -> bool {
let supported = crate::dynamic::lang::entry_kinds_supported(self.lang);
supported.contains(&self.entry_kind)
supported.contains(&self.entry_kind.tag())
}
/// Returns the ordered list of derivation strategies that
@ -1222,6 +1230,29 @@ fn attach_framework_binding(spec: &mut HarnessSpec, summaries: Option<&GlobalSum
if spec.lang == Lang::Java && binding.adapter == "java-spring" {
spec.java_toolchain.with_spring_test = true;
}
// Phase 18 (Track M.0): the binding carries the adapter's view
// of the entry shape — when the adapter stamps one of the new
// data-bearing variants (`ClassMethod`, `MessageHandler`,
// `ScheduledJob`, …), propagate that onto the spec so the
// verifier's `entry_kind_is_supported` gate sees the structural
// shape and short-circuits to a typed
// `Inconclusive(EntryKindUnsupported)`. We deliberately do not
// overwrite the legacy unit variants here: every adapter
// shipped through Phase 17 stamps `Function` / `HttpRoute` and
// the derivation pipeline already routes those correctly.
if matches!(
binding.kind.tag(),
crate::evidence::EntryKindTag::ClassMethod
| crate::evidence::EntryKindTag::MessageHandler
| crate::evidence::EntryKindTag::ScheduledJob
| crate::evidence::EntryKindTag::GraphQLResolver
| crate::evidence::EntryKindTag::WebSocket
| crate::evidence::EntryKindTag::Middleware
| crate::evidence::EntryKindTag::Migration
) {
spec.entry_kind = binding.kind.clone();
spec.spec_hash = compute_spec_hash(spec);
}
spec.framework = Some(binding);
}
}

View file

@ -338,7 +338,7 @@ fn entry_kind_unsupported_verdict(
diag: Option<&Diag>,
spec_entry_path: &str,
lang: crate::symbol::Lang,
attempted: crate::dynamic::spec::EntryKind,
attempted: crate::dynamic::spec::EntryKindTag,
policy: &SamplingPolicy,
) -> VerifyResult {
let supported = crate::dynamic::lang::entry_kinds_supported(lang).to_vec();
@ -618,7 +618,7 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
Some(diag),
&spec.entry_file,
spec.lang,
spec.entry_kind,
spec.entry_kind.tag(),
&opts.telemetry_policy,
);
}
@ -1210,13 +1210,13 @@ fn build_verdict(
) = &e
{
let supported = crate::dynamic::lang::entry_kinds_supported(spec.lang);
if !supported.contains(&spec.entry_kind) {
if !supported.contains(&spec.entry_kind.tag()) {
return entry_kind_unsupported_verdict(
finding_id.to_owned(),
None,
&spec.entry_file,
spec.lang,
spec.entry_kind,
spec.entry_kind.tag(),
&opts.telemetry_policy,
);
}

View file

@ -216,13 +216,88 @@ pub enum UnsupportedReason {
},
}
/// Discriminant tag for [`EntryKind`].
///
/// Phase 18 (Track M.0) extends [`EntryKind`] with data-bearing variants
/// (`ClassMethod`, `MessageHandler`, `ScheduledJob`, …) so the enum can no
/// longer be `Copy` and cannot appear in `&'static [EntryKind]` slices.
/// `EntryKindTag` is the unit-only sibling used for: the per-emitter
/// supported-set declaration (`LangEmitter::entry_kinds_supported` returns
/// `&'static [EntryKindTag]`), the supported / attempted fields on
/// [`InconclusiveReason::EntryKindUnsupported`], and any other site that
/// needs a `Copy + Hash` discriminant.
///
/// `Unknown` is the back-compat fallback: a future variant that an older
/// binary doesn't recognise round-trips as `Unknown` rather than failing
/// deserialisation. Mirrors the `#[serde(other)]` shape on the
/// data-bearing enum.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum EntryKindTag {
Function,
HttpRoute,
CliSubcommand,
LibraryApi,
ClassMethod,
MessageHandler,
ScheduledJob,
GraphQLResolver,
WebSocket,
Middleware,
Migration,
/// Back-compat fallback for unrecognised variants from future bundles.
#[serde(other)]
Unknown,
}
impl fmt::Display for EntryKindTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl EntryKindTag {
/// Stable string form (matches the Serde PascalCase representation).
pub fn as_str(&self) -> &'static str {
match self {
Self::Function => "Function",
Self::HttpRoute => "HttpRoute",
Self::CliSubcommand => "CliSubcommand",
Self::LibraryApi => "LibraryApi",
Self::ClassMethod => "ClassMethod",
Self::MessageHandler => "MessageHandler",
Self::ScheduledJob => "ScheduledJob",
Self::GraphQLResolver => "GraphQLResolver",
Self::WebSocket => "WebSocket",
Self::Middleware => "Middleware",
Self::Migration => "Migration",
Self::Unknown => "Unknown",
}
}
}
/// What kind of entry point a harness should call.
///
/// Lives in `evidence.rs` (not `dynamic::spec`) so that
/// [`InconclusiveReason::EntryKindUnsupported`] can name the attempted /
/// supported variants without depending on the `dynamic` feature. The
/// canonical accessor is `crate::dynamic::spec::EntryKind` (re-export).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
///
/// Phase 18 (Track M.0) extends the enum with seven data-bearing variants
/// (`ClassMethod`, `MessageHandler`, `ScheduledJob`, `GraphQLResolver`,
/// `WebSocket`, `Middleware`, `Migration`) plus an `Unknown` back-compat
/// fallback. Each new variant carries the language-agnostic minimum
/// context the per-language adapter needs to stand the entry up; lang
/// emitters opt in per follow-up phase (19 / 20 / 21) and unsupported
/// kinds short-circuit to `Inconclusive(EntryKindUnsupported)` with a
/// hint pointing at the phase that will close the gap.
///
/// Because the new variants own `String` / `serde_json::Value` payloads
/// the enum is no longer `Copy` (or `Hash`). The sibling
/// [`EntryKindTag`] discriminant is the right type for any site that
/// needs a `Copy + Hash` handle (supported-set lookups, hashmap keys,
/// `InconclusiveReason::EntryKindUnsupported` fields).
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum EntryKind {
/// Free function. Build a `main` that calls it directly.
Function,
@ -232,17 +307,212 @@ pub enum EntryKind {
CliSubcommand,
/// Library API surface. Build an in-process consumer.
LibraryApi,
/// Method on a class / struct / module type. Carries the qualified
/// class name and the method to drive so the lang emitter can build
/// a `Cls(<ctor-args>).method(<payload>)` invocation. Land in
/// Phase 19.
ClassMethod {
class: String,
method: String,
},
/// Message-queue subscriber / consumer. `queue` is the topic /
/// stream / channel name; `message_schema`, when present, is a
/// free-form JSON description of the expected message body that the
/// harness can use to mint a fresh envelope around the payload.
/// Land in Phase 20.
MessageHandler {
queue: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
message_schema: Option<serde_json::Value>,
},
/// Scheduled job / cron handler. `schedule`, when present, is the
/// raw schedule expression as it appears in source (cron syntax,
/// rate string, etc.) — kept opaque because each scheduler library
/// uses a slightly different grammar. Land in Phase 21.
ScheduledJob {
#[serde(default, skip_serializing_if = "Option::is_none")]
schedule: Option<String>,
},
/// GraphQL resolver — `type_name.field` pair the harness drives via
/// an in-process GraphQL execution layer. Land in Phase 21.
GraphQLResolver {
type_name: String,
field: String,
},
/// WebSocket handler — `path` is the canonical mount point; the
/// harness opens a loopback ws connection and sends the payload as
/// the first message frame. Land in Phase 21.
WebSocket {
path: String,
},
/// HTTP / framework middleware — `name` is the middleware identifier
/// (class name, function name, registration key) the harness mounts
/// on a synthetic pipeline before invoking it with a crafted
/// request. Land in Phase 21.
Middleware {
name: String,
},
/// Database migration / schema-change script — `version`, when
/// present, is the migration revision identifier (Alembic / Flyway /
/// Rails string) so the harness can pin the apply step. Land in
/// Phase 21.
Migration {
#[serde(default, skip_serializing_if = "Option::is_none")]
version: Option<String>,
},
/// Back-compat fallback. An older binary that does not yet
/// recognise a future variant deserialises it into `Unknown` rather
/// than failing the bundle load. Mirrors the
/// `#[serde(other)]` shape on [`EntryKindTag`].
Unknown,
}
impl EntryKind {
/// Discriminant tag — used for supported-set lookups and any other
/// site that needs a `Copy + Hash` handle.
pub fn tag(&self) -> EntryKindTag {
match self {
Self::Function => EntryKindTag::Function,
Self::HttpRoute => EntryKindTag::HttpRoute,
Self::CliSubcommand => EntryKindTag::CliSubcommand,
Self::LibraryApi => EntryKindTag::LibraryApi,
Self::ClassMethod { .. } => EntryKindTag::ClassMethod,
Self::MessageHandler { .. } => EntryKindTag::MessageHandler,
Self::ScheduledJob { .. } => EntryKindTag::ScheduledJob,
Self::GraphQLResolver { .. } => EntryKindTag::GraphQLResolver,
Self::WebSocket { .. } => EntryKindTag::WebSocket,
Self::Middleware { .. } => EntryKindTag::Middleware,
Self::Migration { .. } => EntryKindTag::Migration,
Self::Unknown => EntryKindTag::Unknown,
}
}
}
impl fmt::Display for EntryKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Function => "Function",
Self::HttpRoute => "HttpRoute",
Self::CliSubcommand => "CliSubcommand",
Self::LibraryApi => "LibraryApi",
};
f.write_str(s)
f.write_str(self.tag().as_str())
}
}
impl<'de> Deserialize<'de> for EntryKind {
/// Back-compat deserialiser. Externally-tagged enums do not
/// support `#[serde(other)]` on Serde 1.0.228, so we route through
/// `serde_json::Value` and fall through to [`EntryKind::Unknown`]
/// for any tag the current binary does not recognise. Older
/// bundles whose `entry_kind` is a bare PascalCase string (the
/// pre-Phase-18 wire format for the four unit variants) continue
/// to decode unchanged.
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error as _;
let value = serde_json::Value::deserialize(deserializer)
.map_err(D::Error::custom)?;
// Bare-string form (legacy unit variants).
if let Some(tag) = value.as_str() {
return Ok(match tag {
"Function" => Self::Function,
"HttpRoute" => Self::HttpRoute,
"CliSubcommand" => Self::CliSubcommand,
"LibraryApi" => Self::LibraryApi,
"Unknown" => Self::Unknown,
_ => Self::Unknown,
});
}
// Externally-tagged struct form: { "ClassMethod": { ... } }.
if let Some(map) = value.as_object() {
if map.len() == 1 {
let (tag, body) = map.iter().next().expect("len == 1");
let body = body.clone();
let parsed = match tag.as_str() {
"Function" => Some(Self::Function),
"HttpRoute" => Some(Self::HttpRoute),
"CliSubcommand" => Some(Self::CliSubcommand),
"LibraryApi" => Some(Self::LibraryApi),
"Unknown" => Some(Self::Unknown),
"ClassMethod" => {
#[derive(Deserialize)]
struct F {
class: String,
method: String,
}
serde_json::from_value::<F>(body).ok().map(|f| Self::ClassMethod {
class: f.class,
method: f.method,
})
}
"MessageHandler" => {
#[derive(Deserialize)]
struct F {
queue: String,
#[serde(default)]
message_schema: Option<serde_json::Value>,
}
serde_json::from_value::<F>(body).ok().map(|f| Self::MessageHandler {
queue: f.queue,
message_schema: f.message_schema,
})
}
"ScheduledJob" => {
#[derive(Deserialize)]
struct F {
#[serde(default)]
schedule: Option<String>,
}
serde_json::from_value::<F>(body)
.ok()
.map(|f| Self::ScheduledJob { schedule: f.schedule })
}
"GraphQLResolver" => {
#[derive(Deserialize)]
struct F {
type_name: String,
field: String,
}
serde_json::from_value::<F>(body).ok().map(|f| Self::GraphQLResolver {
type_name: f.type_name,
field: f.field,
})
}
"WebSocket" => {
#[derive(Deserialize)]
struct F {
path: String,
}
serde_json::from_value::<F>(body)
.ok()
.map(|f| Self::WebSocket { path: f.path })
}
"Middleware" => {
#[derive(Deserialize)]
struct F {
name: String,
}
serde_json::from_value::<F>(body)
.ok()
.map(|f| Self::Middleware { name: f.name })
}
"Migration" => {
#[derive(Deserialize)]
struct F {
#[serde(default)]
version: Option<String>,
}
serde_json::from_value::<F>(body)
.ok()
.map(|f| Self::Migration { version: f.version })
}
_ => None,
};
return Ok(parsed.unwrap_or(Self::Unknown));
}
}
Ok(Self::Unknown)
}
}
@ -314,10 +584,15 @@ pub enum InconclusiveReason {
/// [`EntryKind`]. Carries the language, the attempted entry kind, the
/// list of entry kinds the emitter currently understands, and a
/// human-actionable hint pointing at the phase that will add support.
///
/// Phase 18: `attempted` / `supported` use the [`EntryKindTag`]
/// discriminant rather than the (now data-bearing) [`EntryKind`] so
/// the verdict stays cheap to copy and the serialised form remains
/// a list of PascalCase strings.
EntryKindUnsupported {
lang: Lang,
attempted: EntryKind,
supported: Vec<EntryKind>,
attempted: EntryKindTag,
supported: Vec<EntryKindTag>,
hint: String,
},
/// The capability's corpus lacks a paired benign control payload, so
@ -1917,4 +2192,166 @@ mod tests {
let json = serde_json::to_string(&crate::labels::SourceKind::UserInput).unwrap();
assert_eq!(json, "\"user_input\"");
}
// ── Phase 18 (Track M.0) — EntryKind data-bearing variants ──────────────
/// Legacy unit variants round-trip as bare PascalCase strings — the
/// pre-Phase-18 wire format an older binary expects.
#[test]
fn entry_kind_legacy_unit_variants_round_trip() {
for (kind, json) in [
(EntryKind::Function, "\"Function\""),
(EntryKind::HttpRoute, "\"HttpRoute\""),
(EntryKind::CliSubcommand, "\"CliSubcommand\""),
(EntryKind::LibraryApi, "\"LibraryApi\""),
] {
let serialised = serde_json::to_string(&kind).unwrap();
assert_eq!(serialised, json, "serialise {kind:?}");
let parsed: EntryKind = serde_json::from_str(json).unwrap();
assert_eq!(parsed, kind, "deserialise {json}");
}
}
/// New Phase 18 variants serialise as externally-tagged objects and
/// round-trip with their data payloads intact.
#[test]
fn entry_kind_phase_18_variants_round_trip() {
let cases: Vec<EntryKind> = vec![
EntryKind::ClassMethod {
class: "UserController".into(),
method: "show".into(),
},
EntryKind::MessageHandler {
queue: "orders.new".into(),
message_schema: Some(serde_json::json!({"type":"object"})),
},
EntryKind::MessageHandler {
queue: "orders.new".into(),
message_schema: None,
},
EntryKind::ScheduledJob {
schedule: Some("0 */6 * * *".into()),
},
EntryKind::ScheduledJob { schedule: None },
EntryKind::GraphQLResolver {
type_name: "Query".into(),
field: "user".into(),
},
EntryKind::WebSocket { path: "/ws/feed".into() },
EntryKind::Middleware { name: "auth_filter".into() },
EntryKind::Migration {
version: Some("0042_user_table".into()),
},
EntryKind::Migration { version: None },
EntryKind::Unknown,
];
for kind in cases {
let json = serde_json::to_string(&kind).unwrap();
let parsed: EntryKind = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, kind, "round-trip {json}");
}
}
/// Back-compat: a bundle that mentions a future variant the current
/// binary does not recognise deserialises to [`EntryKind::Unknown`]
/// instead of failing the parse. Mirrors the
/// `#[serde(other)]` shape promised in the Phase 18 brief.
#[test]
fn entry_kind_unknown_future_variant_falls_back_to_unknown() {
// Externally-tagged object form.
let unknown_obj = r#"{"FutureKind":{"foo":42}}"#;
let parsed: EntryKind = serde_json::from_str(unknown_obj).unwrap();
assert_eq!(parsed, EntryKind::Unknown);
// Bare-string form (e.g. older binary writes a future name as a
// unit tag rather than a struct).
let unknown_str = "\"FutureKind\"";
let parsed: EntryKind = serde_json::from_str(unknown_str).unwrap();
assert_eq!(parsed, EntryKind::Unknown);
}
/// Tag discriminant projection — used by every supported-set lookup
/// path so the slice can stay `'static` after Phase 18.
#[test]
fn entry_kind_tag_matches_variant_for_each_phase_18_variant() {
assert_eq!(EntryKind::Function.tag(), EntryKindTag::Function);
assert_eq!(EntryKind::HttpRoute.tag(), EntryKindTag::HttpRoute);
assert_eq!(EntryKind::CliSubcommand.tag(), EntryKindTag::CliSubcommand);
assert_eq!(EntryKind::LibraryApi.tag(), EntryKindTag::LibraryApi);
assert_eq!(
EntryKind::ClassMethod {
class: String::new(),
method: String::new()
}
.tag(),
EntryKindTag::ClassMethod
);
assert_eq!(
EntryKind::MessageHandler {
queue: String::new(),
message_schema: None
}
.tag(),
EntryKindTag::MessageHandler
);
assert_eq!(
EntryKind::ScheduledJob { schedule: None }.tag(),
EntryKindTag::ScheduledJob
);
assert_eq!(
EntryKind::GraphQLResolver {
type_name: String::new(),
field: String::new()
}
.tag(),
EntryKindTag::GraphQLResolver
);
assert_eq!(
EntryKind::WebSocket {
path: String::new()
}
.tag(),
EntryKindTag::WebSocket
);
assert_eq!(
EntryKind::Middleware {
name: String::new()
}
.tag(),
EntryKindTag::Middleware
);
assert_eq!(
EntryKind::Migration { version: None }.tag(),
EntryKindTag::Migration
);
assert_eq!(EntryKind::Unknown.tag(), EntryKindTag::Unknown);
}
/// [`EntryKindTag`] round-trips through the externally-tagged wire
/// format used by [`InconclusiveReason::EntryKindUnsupported`] and
/// honours `#[serde(other)]` for unknown tags.
#[test]
fn entry_kind_tag_serde_round_trip_and_unknown_fallback() {
for tag in [
EntryKindTag::Function,
EntryKindTag::HttpRoute,
EntryKindTag::CliSubcommand,
EntryKindTag::LibraryApi,
EntryKindTag::ClassMethod,
EntryKindTag::MessageHandler,
EntryKindTag::ScheduledJob,
EntryKindTag::GraphQLResolver,
EntryKindTag::WebSocket,
EntryKindTag::Middleware,
EntryKindTag::Migration,
EntryKindTag::Unknown,
] {
let json = serde_json::to_string(&tag).unwrap();
let rt: EntryKindTag = serde_json::from_str(&json).unwrap();
assert_eq!(rt, tag);
}
// Future tag → Unknown via `#[serde(other)]`.
let parsed: EntryKindTag = serde_json::from_str("\"FutureKind\"").unwrap();
assert_eq!(parsed, EntryKindTag::Unknown);
}
}

View file

@ -20,7 +20,7 @@ mod spec_strategies {
use nyx_scanner::commands::scan::Diag;
use nyx_scanner::dynamic::spec::{
derive_from_callgraph_entry, derive_from_func_summary, derive_from_rule_namespace,
EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy,
EntryKind, EntryKindTag, HarnessSpec, PayloadSlot, SpecDerivationStrategy,
};
use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions};
use nyx_scanner::evidence::{
@ -360,13 +360,13 @@ mod spec_strategies {
hint,
}) => {
assert_eq!(lang, nyx_scanner::symbol::Lang::C);
assert!(matches!(attempted, EntryKind::HttpRoute));
assert!(matches!(attempted, EntryKindTag::HttpRoute));
assert!(
!supported.is_empty(),
"supported list must be non-empty so operators can triage"
);
assert!(
supported.contains(&EntryKind::Function),
supported.contains(&EntryKindTag::Function),
"C emitter must advertise Function support; got {supported:?}"
);
assert!(