[pitboss] phase 21: Track M.3 — ScheduledJob + GraphQLResolver + WebSocket + Middleware + Migration

This commit is contained in:
pitboss 2026-05-20 18:05:31 -05:00
parent 00b0fbaea9
commit f9bd51c024
84 changed files with 5898 additions and 40 deletions

View file

@ -0,0 +1,112 @@
//! Phase 21 (Track M.3) — Apollo GraphQL resolver adapter (JS).
//!
//! Fires when the surrounding source imports `@apollo/server` / the
//! legacy `apollo-server` / `apollo-server-express` package, or the
//! function body sits inside a `Query` / `Mutation` resolver map.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct GraphqlApolloAdapter;
const ADAPTER_NAME: &str = "graphql-apollo";
fn callee_is_apollo(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"ApolloServer" | "startStandaloneServer" | "gql" | "applyMiddleware" | "expressMiddleware"
)
}
fn source_imports_apollo(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"@apollo/server",
b"apollo-server",
b"require('apollo-server')",
b"require(\"apollo-server\")",
b"from 'apollo-server",
b"from \"apollo-server",
b"new ApolloServer",
b"const resolvers",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
fn extract_resolver(summary: &FuncSummary) -> (String, String) {
// Best-effort: split a fully-qualified name like `Query.user` into
// `("Query", "user")`. Falls back to ("Query", name) so the
// binding always carries some type_name + field.
if let Some((parent, field)) = summary.name.rsplit_once('.') {
return (parent.to_owned(), field.to_owned());
}
("Query".to_owned(), summary.name.clone())
}
impl FrameworkAdapter for GraphqlApolloAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::JavaScript
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_apollo);
let matches_source = source_imports_apollo(file_bytes);
if matches_call || matches_source {
let (type_name, field) = extract_resolver(summary);
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::GraphQLResolver { type_name, field },
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_js(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_apollo_resolver() {
let src: &[u8] = b"const { ApolloServer } = require('@apollo/server');\n\
const resolvers = { Query: { user: (_, { id }) => id } };\n";
let tree = parse_js(src);
let summary = FuncSummary {
name: "user".into(),
..Default::default()
};
let binding = GraphqlApolloAdapter
.detect(&summary, tree.root_node(), src)
.expect("apollo binds");
assert_eq!(binding.adapter, "graphql-apollo");
if let EntryKind::GraphQLResolver { type_name, field } = binding.kind {
assert_eq!(type_name, "Query");
assert_eq!(field, "user");
}
}
}

View file

@ -0,0 +1,103 @@
//! Phase 21 (Track M.3) — gqlgen (Go) GraphQL resolver adapter.
//!
//! Fires when the surrounding source imports the gqlgen runtime or
//! declares a resolver method on a `*queryResolver` / `*mutationResolver`
//! receiver — the canonical shape gqlgen generates.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct GraphqlGqlgenAdapter;
const ADAPTER_NAME: &str = "graphql-gqlgen";
fn callee_is_gqlgen(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"NewExecutableSchema" | "handler" | "Playground" | "GraphQL" | "Recover"
)
}
fn source_imports_gqlgen(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"github.com/99designs/gqlgen",
b"gqlgen/graphql",
b"queryResolver",
b"mutationResolver",
b"Resolver) Query(",
b"Resolver) Mutation(",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
fn extract_resolver(summary: &FuncSummary) -> (String, String) {
("Query".to_owned(), summary.name.clone())
}
impl FrameworkAdapter for GraphqlGqlgenAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Go
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_gqlgen);
let matches_source = source_imports_gqlgen(file_bytes);
if matches_call || matches_source {
let (type_name, field) = extract_resolver(summary);
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::GraphQLResolver { type_name, field },
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_go(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_go::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_gqlgen_query_resolver() {
let src: &[u8] = b"package graph\n\
import \"github.com/99designs/gqlgen/graphql\"\n\
type queryResolver struct{}\n\
func (r *queryResolver) User(ctx context.Context, id string) (string, error) { return id, nil }\n";
let tree = parse_go(src);
let summary = FuncSummary {
name: "User".into(),
..Default::default()
};
let binding = GraphqlGqlgenAdapter
.detect(&summary, tree.root_node(), src)
.expect("gqlgen binds");
assert_eq!(binding.adapter, "graphql-gqlgen");
assert!(matches!(binding.kind, EntryKind::GraphQLResolver { .. }));
}
}

View file

@ -0,0 +1,107 @@
//! Phase 21 (Track M.3) — Graphene (Python) GraphQL resolver adapter.
//!
//! Fires when the surrounding source imports `graphene` and the
//! function body sits inside a `graphene.ObjectType` with a
//! `resolve_<field>` definition.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct GraphqlGrapheneAdapter;
const ADAPTER_NAME: &str = "graphql-graphene";
fn callee_is_graphene(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"Schema" | "ObjectType" | "Field" | "String" | "Int" | "List"
)
}
fn source_imports_graphene(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"import graphene",
b"from graphene",
b"graphene.ObjectType",
b"graphene.Schema",
b"graphene.Field",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
fn extract_resolver(summary: &FuncSummary) -> (String, String) {
// `resolve_user` → ("Query", "user"). Best-effort.
if let Some(field) = summary.name.strip_prefix("resolve_") {
return ("Query".to_owned(), field.to_owned());
}
("Query".to_owned(), summary.name.clone())
}
impl FrameworkAdapter for GraphqlGrapheneAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Python
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_graphene);
let matches_source = source_imports_graphene(file_bytes);
if matches_call || matches_source {
let (type_name, field) = extract_resolver(summary);
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::GraphQLResolver { type_name, field },
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
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 fires_on_graphene_resolver() {
let src: &[u8] = b"import graphene\n\
class Query(graphene.ObjectType):\n user = graphene.String()\n def resolve_user(self, info, id):\n return id\n";
let tree = parse_python(src);
let summary = FuncSummary {
name: "resolve_user".into(),
..Default::default()
};
let binding = GraphqlGrapheneAdapter
.detect(&summary, tree.root_node(), src)
.expect("graphene binds");
assert_eq!(binding.adapter, "graphql-graphene");
if let EntryKind::GraphQLResolver { type_name, field } = binding.kind {
assert_eq!(type_name, "Query");
assert_eq!(field, "user");
}
}
}

View file

@ -0,0 +1,101 @@
//! Phase 21 (Track M.3) — Juniper (Rust) GraphQL resolver adapter.
//!
//! Fires when the surrounding source imports the `juniper` crate and
//! the function body sits inside a `#[graphql_object]` impl block.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct GraphqlJuniperAdapter;
const ADAPTER_NAME: &str = "graphql-juniper";
fn callee_is_juniper(name: &str) -> bool {
let last = name.rsplit_once("::").map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"RootNode" | "EmptyMutation" | "EmptySubscription" | "execute" | "execute_sync"
)
}
fn source_imports_juniper(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"use juniper",
b"juniper::",
b"#[graphql_object",
b"#[derive(GraphQLObject)]",
b"juniper::EmptyMutation",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
fn extract_resolver(summary: &FuncSummary) -> (String, String) {
("Query".to_owned(), summary.name.clone())
}
impl FrameworkAdapter for GraphqlJuniperAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Rust
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_juniper);
let matches_source = source_imports_juniper(file_bytes);
if matches_call || matches_source {
let (type_name, field) = extract_resolver(summary);
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::GraphQLResolver { type_name, field },
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_rust(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_rust::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_juniper_graphql_object() {
let src: &[u8] = b"use juniper::graphql_object;\n\
pub struct Query;\n\
#[graphql_object]\n\
impl Query {\n fn user(&self, id: String) -> String { id }\n}\n";
let tree = parse_rust(src);
let summary = FuncSummary {
name: "user".into(),
..Default::default()
};
let binding = GraphqlJuniperAdapter
.detect(&summary, tree.root_node(), src)
.expect("juniper binds");
assert_eq!(binding.adapter, "graphql-juniper");
assert!(matches!(binding.kind, EntryKind::GraphQLResolver { .. }));
}
}

View file

@ -0,0 +1,111 @@
//! Phase 21 (Track M.3) — Relay GraphQL resolver adapter (JS).
//!
//! Relay is the Facebook GraphQL client + spec; on the server side
//! `graphql-relay` provides node-id / connection helpers wrapped around
//! the standard `graphql-js` resolver shape. Fires when the source
//! imports `graphql-relay` / declares a node-id resolver or a
//! `mutationWithClientMutationId` helper.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct GraphqlRelayAdapter;
const ADAPTER_NAME: &str = "graphql-relay";
fn callee_is_relay(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"nodeDefinitions"
| "mutationWithClientMutationId"
| "connectionDefinitions"
| "globalIdField"
| "fromGlobalId"
)
}
fn source_imports_relay(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"graphql-relay",
b"require('graphql-relay')",
b"require(\"graphql-relay\")",
b"from 'graphql-relay'",
b"from \"graphql-relay\"",
b"nodeDefinitions",
b"mutationWithClientMutationId",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
fn extract_resolver(summary: &FuncSummary) -> (String, String) {
if let Some((parent, field)) = summary.name.rsplit_once('.') {
return (parent.to_owned(), field.to_owned());
}
("Node".to_owned(), summary.name.clone())
}
impl FrameworkAdapter for GraphqlRelayAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::JavaScript
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_relay);
let matches_source = source_imports_relay(file_bytes);
if matches_call || matches_source {
let (type_name, field) = extract_resolver(summary);
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::GraphQLResolver { type_name, field },
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_js(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_relay_node_definitions() {
let src: &[u8] = b"const { nodeDefinitions, fromGlobalId } = require('graphql-relay');\n\
function resolveUser(globalId) { return fromGlobalId(globalId); }\n";
let tree = parse_js(src);
let summary = FuncSummary {
name: "resolveUser".into(),
..Default::default()
};
let binding = GraphqlRelayAdapter
.detect(&summary, tree.root_node(), src)
.expect("relay binds");
assert_eq!(binding.adapter, "graphql-relay");
assert!(matches!(binding.kind, EntryKind::GraphQLResolver { .. }));
}
}

View file

@ -0,0 +1,102 @@
//! Phase 21 (Track M.3) — Django middleware adapter (Python).
//!
//! Fires when the surrounding source imports Django middleware base
//! classes (`MiddlewareMixin`) or declares a callable middleware whose
//! body defines `__call__(self, request)` / `process_request`.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct MiddlewareDjangoAdapter;
const ADAPTER_NAME: &str = "middleware-django";
fn callee_is_django_middleware(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"process_request"
| "process_response"
| "process_view"
| "process_exception"
| "__call__"
)
}
fn source_imports_django_middleware(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"django.utils.deprecation",
b"MiddlewareMixin",
b"def __call__(self, request",
b"def process_request",
b"django.middleware",
b"MIDDLEWARE = [",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
impl FrameworkAdapter for MiddlewareDjangoAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Python
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_django_middleware);
let matches_source = source_imports_django_middleware(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Middleware {
name: summary.name.clone(),
},
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
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 fires_on_django_middleware() {
let src: &[u8] = b"from django.utils.deprecation import MiddlewareMixin\n\
class AuditMiddleware(MiddlewareMixin):\n def process_request(self, request):\n pass\n";
let tree = parse_python(src);
let summary = FuncSummary {
name: "process_request".into(),
..Default::default()
};
let binding = MiddlewareDjangoAdapter
.detect(&summary, tree.root_node(), src)
.expect("django middleware binds");
assert_eq!(binding.adapter, "middleware-django");
assert!(matches!(binding.kind, EntryKind::Middleware { .. }));
}
}

View file

@ -0,0 +1,104 @@
//! Phase 21 (Track M.3) — Express middleware adapter (JS).
//!
//! Fires when the surrounding source imports Express and declares a
//! middleware function — a `(req, res, next) => …` callable mounted
//! via `app.use(...)` / `router.use(...)`.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct MiddlewareExpressAdapter;
const ADAPTER_NAME: &str = "middleware-express";
fn callee_is_express(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"use" | "next" | "json" | "urlencoded" | "static"
)
}
fn source_imports_express(file_bytes: &[u8]) -> bool {
// Phase 21 v1: require an explicit middleware-registration shape
// (`app.use(` / `router.use(`), not the bare `require('express')`
// import. Many non-middleware Express fixtures import the framework
// but never declare middleware; gating on the registration shape
// keeps the adapter focused on the function the brief targets.
const NEEDLES: &[&[u8]] = &[
b"app.use(",
b"router.use(",
b"express.Router()",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
impl FrameworkAdapter for MiddlewareExpressAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::JavaScript
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_express);
let matches_source = source_imports_express(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Middleware {
name: summary.name.clone(),
},
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_js(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_express_middleware() {
let src: &[u8] = b"const express = require('express');\n\
const app = express();\n\
function audit(req, res, next) { next(); }\n\
app.use(audit);\n";
let tree = parse_js(src);
let summary = FuncSummary {
name: "audit".into(),
..Default::default()
};
let binding = MiddlewareExpressAdapter
.detect(&summary, tree.root_node(), src)
.expect("express middleware binds");
assert_eq!(binding.adapter, "middleware-express");
if let EntryKind::Middleware { name } = binding.kind {
assert_eq!(name, "audit");
}
}
}

View file

@ -0,0 +1,94 @@
//! Phase 21 (Track M.3) — Laravel middleware adapter (PHP).
//!
//! Fires when the surrounding source declares a class with a `handle`
//! method whose signature matches Laravel's middleware contract
//! (`$request, Closure $next`).
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct MiddlewareLaravelAdapter;
const ADAPTER_NAME: &str = "middleware-laravel";
fn callee_is_laravel_middleware(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(last, "handle" | "terminate" | "next" | "withMiddleware")
}
fn source_imports_laravel_middleware(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"Illuminate\\Http\\Request",
b"Illuminate\\Foundation\\Http\\Middleware",
b"function handle($request, Closure $next",
b"function handle(Request $request, Closure $next",
b"app/Http/Middleware",
b"$middleware",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
impl FrameworkAdapter for MiddlewareLaravelAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Php
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_laravel_middleware);
let matches_source = source_imports_laravel_middleware(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Middleware {
name: summary.name.clone(),
},
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_php(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_php::LANGUAGE_PHP);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_laravel_handle() {
let src: &[u8] = b"<?php\nuse Illuminate\\Http\\Request;\nclass Audit {\n public function handle($request, Closure $next) { return $next($request); }\n}\n";
let tree = parse_php(src);
let summary = FuncSummary {
name: "handle".into(),
..Default::default()
};
let binding = MiddlewareLaravelAdapter
.detect(&summary, tree.root_node(), src)
.expect("laravel middleware binds");
assert_eq!(binding.adapter, "middleware-laravel");
assert!(matches!(binding.kind, EntryKind::Middleware { .. }));
}
}

View file

@ -0,0 +1,98 @@
//! Phase 21 (Track M.3) — Rack / Rails middleware adapter (Ruby).
//!
//! Fires when the surrounding source defines a Rack-shaped middleware
//! (`def call(env)`) or registers a Rails before-action callback.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct MiddlewareRailsAdapter;
const ADAPTER_NAME: &str = "middleware-rails";
fn callee_is_rails_middleware(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"call" | "before_action" | "around_action" | "after_action" | "use"
)
}
fn source_imports_rails_middleware(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"def call(env)",
b"def call (env",
b"before_action ",
b"after_action ",
b"around_action ",
b"Rails.application.config.middleware",
b"Rack::Builder",
b"@app = app",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
impl FrameworkAdapter for MiddlewareRailsAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Ruby
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_rails_middleware);
let matches_source = source_imports_rails_middleware(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Middleware {
name: summary.name.clone(),
},
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_ruby(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_ruby::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_rack_middleware_call() {
let src: &[u8] = b"class AuditMiddleware\n def initialize(app); @app = app; end\n def call(env)\n @app.call(env)\n end\nend\n";
let tree = parse_ruby(src);
let summary = FuncSummary {
name: "call".into(),
..Default::default()
};
let binding = MiddlewareRailsAdapter
.detect(&summary, tree.root_node(), src)
.expect("rack middleware binds");
assert_eq!(binding.adapter, "middleware-rails");
assert!(matches!(binding.kind, EntryKind::Middleware { .. }));
}
}

View file

@ -0,0 +1,98 @@
//! Phase 21 (Track M.3) — Spring `HandlerInterceptor` middleware
//! adapter (Java).
//!
//! Fires when the surrounding source imports
//! `org.springframework.web.servlet.HandlerInterceptor` or `Filter` and
//! the function body is `preHandle` / `postHandle` / `doFilter`.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct MiddlewareSpringAdapter;
const ADAPTER_NAME: &str = "middleware-spring";
fn callee_is_spring_middleware(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"preHandle" | "postHandle" | "afterCompletion" | "doFilter" | "addInterceptors"
)
}
fn source_imports_spring_middleware(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"HandlerInterceptor",
b"OncePerRequestFilter",
b"javax.servlet.Filter",
b"jakarta.servlet.Filter",
b"WebMvcConfigurer",
b"InterceptorRegistry",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
impl FrameworkAdapter for MiddlewareSpringAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Java
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_spring_middleware);
let matches_source = source_imports_spring_middleware(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Middleware {
name: summary.name.clone(),
},
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_java(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_java::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_spring_interceptor() {
let src: &[u8] = b"public class AuditInterceptor implements HandlerInterceptor {\n public boolean preHandle(Object req, Object res, Object handler) { return true; }\n}\n";
let tree = parse_java(src);
let summary = FuncSummary {
name: "preHandle".into(),
..Default::default()
};
let binding = MiddlewareSpringAdapter
.detect(&summary, tree.root_node(), src)
.expect("spring middleware binds");
assert_eq!(binding.adapter, "middleware-spring");
assert!(matches!(binding.kind, EntryKind::Middleware { .. }));
}
}

View file

@ -0,0 +1,119 @@
//! Phase 21 (Track M.3) — Django migration adapter (Python).
//!
//! Fires when the surrounding source imports `django.db.migrations` and
//! declares a `Migration` class with `operations = [...]`.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct MigrationDjangoAdapter;
const ADAPTER_NAME: &str = "migration-django";
fn callee_is_django_migration(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"CreateModel"
| "AddField"
| "AlterField"
| "DeleteModel"
| "RunPython"
| "RunSQL"
| "migrate"
)
}
fn source_imports_django_migration(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"django.db.migrations",
b"migrations.Migration",
b"migrations.RunPython",
b"operations = [",
b"dependencies = [",
b"from django.db import migrations",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
fn extract_version(file_bytes: &[u8]) -> Option<String> {
// Django migrations carry a numeric prefix on the filename
// (`0001_initial.py`); the version is more reliably the prefix of
// the file path, but we can also pull a top-level `# Version: NNNN`
// comment. Best-effort.
let text = std::str::from_utf8(file_bytes).unwrap_or("");
let needle = "# Generated by Django ";
if let Some(idx) = text.find(needle) {
let after = &text[idx + needle.len()..];
if let Some(end) = after.find(|c: char| c == ' ' || c == '\n') {
return Some(after[..end].trim().to_owned());
}
}
None
}
impl FrameworkAdapter for MigrationDjangoAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Python
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_django_migration);
let matches_source = source_imports_django_migration(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Migration {
version: extract_version(file_bytes),
},
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
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 fires_on_django_migration() {
let src: &[u8] = b"from django.db import migrations\n\
class Migration(migrations.Migration):\n operations = [migrations.CreateModel(name='User', fields=[])]\n";
let tree = parse_python(src);
let summary = FuncSummary {
name: "Migration".into(),
..Default::default()
};
let binding = MigrationDjangoAdapter
.detect(&summary, tree.root_node(), src)
.expect("django migration binds");
assert_eq!(binding.adapter, "migration-django");
assert!(matches!(binding.kind, EntryKind::Migration { .. }));
}
}

View file

@ -0,0 +1,122 @@
//! Phase 21 (Track M.3) — Flask-Migrate / Alembic migration adapter
//! (Python).
//!
//! Fires when the surrounding source imports `alembic` / `flask_migrate`
//! and declares an `upgrade()` / `downgrade()` revision function.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct MigrationFlaskAdapter;
const ADAPTER_NAME: &str = "migration-flask";
fn callee_is_flask_migration(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"upgrade"
| "downgrade"
| "execute"
| "create_table"
| "add_column"
| "drop_table"
| "alter_column"
)
}
fn source_imports_flask_migration(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"from alembic",
b"import alembic",
b"flask_migrate",
b"op.create_table",
b"op.add_column",
b"op.execute",
b"revision = '",
b"revision = \"",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
fn extract_version(file_bytes: &[u8]) -> Option<String> {
let text = std::str::from_utf8(file_bytes).unwrap_or("");
for needle in ["revision = '", "revision = \""] {
if let Some(idx) = text.find(needle) {
let after = &text[idx + needle.len()..];
let close = if needle.ends_with('"') { '"' } else { '\'' };
if let Some(end) = after.find(close) {
return Some(after[..end].to_owned());
}
}
}
None
}
impl FrameworkAdapter for MigrationFlaskAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Python
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_flask_migration);
let matches_source = source_imports_flask_migration(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Migration {
version: extract_version(file_bytes),
},
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
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 fires_on_alembic_revision() {
let src: &[u8] = b"from alembic import op\nrevision = 'abc123'\n\
def upgrade():\n op.create_table('users')\n";
let tree = parse_python(src);
let summary = FuncSummary {
name: "upgrade".into(),
..Default::default()
};
let binding = MigrationFlaskAdapter
.detect(&summary, tree.root_node(), src)
.expect("alembic binds");
assert_eq!(binding.adapter, "migration-flask");
if let EntryKind::Migration { version } = binding.kind {
assert_eq!(version.as_deref(), Some("abc123"));
}
}
}

View file

@ -0,0 +1,95 @@
//! Phase 21 (Track M.3) — Laravel migration adapter (PHP).
//!
//! Fires when the surrounding source extends `Illuminate\\Database\\Migrations\\Migration`
//! and declares an `up()` / `down()` method whose body invokes
//! `Schema::create` / `Schema::table` / `DB::statement`.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct MigrationLaravelAdapter;
const ADAPTER_NAME: &str = "migration-laravel";
fn callee_is_laravel_migration(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"up" | "down" | "create" | "table" | "drop" | "statement" | "unprepared"
)
}
fn source_imports_laravel_migration(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"Illuminate\\Database\\Migrations\\Migration",
b"Illuminate\\Database\\Schema",
b"Schema::create",
b"Schema::table",
b"DB::statement",
b"use Illuminate\\Database\\Schema",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
impl FrameworkAdapter for MigrationLaravelAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Php
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_laravel_migration);
let matches_source = source_imports_laravel_migration(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Migration { version: None },
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_php(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_php::LANGUAGE_PHP);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_laravel_migration() {
let src: &[u8] = b"<?php\nuse Illuminate\\Database\\Migrations\\Migration;\nclass AddUsers extends Migration { public function up() { Schema::create('users', function($t){}); } }\n";
let tree = parse_php(src);
let summary = FuncSummary {
name: "up".into(),
..Default::default()
};
let binding = MigrationLaravelAdapter
.detect(&summary, tree.root_node(), src)
.expect("laravel migration binds");
assert_eq!(binding.adapter, "migration-laravel");
assert!(matches!(binding.kind, EntryKind::Migration { .. }));
}
}

View file

@ -0,0 +1,105 @@
//! Phase 21 (Track M.3) — Prisma migration adapter (JS / TS).
//!
//! Prisma migrations are SQL files generated by `prisma migrate dev`
//! plus a TS / JS seed script that calls `prisma.$executeRaw`. Fires
//! when the surrounding source imports `@prisma/client` and the
//! function body invokes one of the raw-execution callees.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct MigrationPrismaAdapter;
const ADAPTER_NAME: &str = "migration-prisma";
fn callee_is_prisma_migration(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"$executeRaw"
| "$executeRawUnsafe"
| "$queryRaw"
| "$queryRawUnsafe"
| "migrate"
| "deploy"
| "up"
)
}
fn source_imports_prisma_migration(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"@prisma/client",
b"require('@prisma/client')",
b"require(\"@prisma/client\")",
b"from '@prisma/client'",
b"from \"@prisma/client\"",
b"prisma.$executeRaw",
b"prisma.$queryRaw",
b"PrismaClient",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
impl FrameworkAdapter for MigrationPrismaAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::JavaScript
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_prisma_migration);
let matches_source = source_imports_prisma_migration(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Migration { version: None },
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_js(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_prisma_raw_migration() {
let src: &[u8] = b"const { PrismaClient } = require('@prisma/client');\nconst prisma = new PrismaClient();\n\
async function up(name) { await prisma.$executeRawUnsafe('CREATE TABLE ' + name); }\n";
let tree = parse_js(src);
let summary = FuncSummary {
name: "up".into(),
..Default::default()
};
let binding = MigrationPrismaAdapter
.detect(&summary, tree.root_node(), src)
.expect("prisma migration binds");
assert_eq!(binding.adapter, "migration-prisma");
assert!(matches!(binding.kind, EntryKind::Migration { .. }));
}
}

View file

@ -0,0 +1,118 @@
//! Phase 21 (Track M.3) — Rails ActiveRecord migration adapter (Ruby).
//!
//! Fires when the surrounding source declares a class inheriting from
//! `ActiveRecord::Migration[...]` or invokes the canonical migration
//! DSL (`create_table`, `add_column`, `execute`).
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct MigrationRailsAdapter;
const ADAPTER_NAME: &str = "migration-rails";
fn callee_is_rails_migration(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"up"
| "down"
| "change"
| "create_table"
| "add_column"
| "remove_column"
| "drop_table"
| "rename_column"
| "execute"
)
}
fn source_imports_rails_migration(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"ActiveRecord::Migration",
b"< ActiveRecord::Migration",
b"create_table ",
b"add_column ",
b"drop_table ",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
fn extract_version(file_bytes: &[u8]) -> Option<String> {
let text = std::str::from_utf8(file_bytes).unwrap_or("");
let needle = "ActiveRecord::Migration[";
if let Some(idx) = text.find(needle) {
let after = &text[idx + needle.len()..];
if let Some(end) = after.find(']') {
return Some(after[..end].trim().to_owned());
}
}
None
}
impl FrameworkAdapter for MigrationRailsAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Ruby
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_rails_migration);
let matches_source = source_imports_rails_migration(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Migration {
version: extract_version(file_bytes),
},
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_ruby(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_ruby::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_rails_migration() {
let src: &[u8] = b"class AddIndex < ActiveRecord::Migration[7.0]\n def up\n add_column :users, :name, :string\n end\nend\n";
let tree = parse_ruby(src);
let summary = FuncSummary {
name: "up".into(),
..Default::default()
};
let binding = MigrationRailsAdapter
.detect(&summary, tree.root_node(), src)
.expect("rails migration binds");
assert_eq!(binding.adapter, "migration-rails");
if let EntryKind::Migration { version } = binding.kind {
assert_eq!(version.as_deref(), Some("7.0"));
}
}
}

View file

@ -0,0 +1,103 @@
//! Phase 21 (Track M.3) — Sequelize migration adapter (JS).
//!
//! Fires when the surrounding source declares `module.exports = { up, down }`
//! whose `up` formal is `(queryInterface, Sequelize)` — Sequelize's
//! canonical migration shape — or imports the `sequelize` package.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct MigrationSequelizeAdapter;
const ADAPTER_NAME: &str = "migration-sequelize";
fn callee_is_sequelize_migration(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"up"
| "down"
| "createTable"
| "addColumn"
| "dropTable"
| "removeColumn"
| "addIndex"
)
}
fn source_imports_sequelize_migration(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"require('sequelize')",
b"require(\"sequelize\")",
b"from 'sequelize'",
b"from \"sequelize\"",
b"queryInterface.createTable",
b"queryInterface.addColumn",
b"queryInterface.bulkInsert",
b"sequelize-cli",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
impl FrameworkAdapter for MigrationSequelizeAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::JavaScript
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_sequelize_migration);
let matches_source = source_imports_sequelize_migration(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Migration { version: None },
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_js(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_sequelize_migration() {
let src: &[u8] = b"module.exports = {\n async up(queryInterface, Sequelize) { await queryInterface.createTable('users', {}); },\n async down(queryInterface, Sequelize) { await queryInterface.dropTable('users'); }\n};\n";
let tree = parse_js(src);
let summary = FuncSummary {
name: "up".into(),
..Default::default()
};
let binding = MigrationSequelizeAdapter
.detect(&summary, tree.root_node(), src)
.expect("sequelize migration binds");
assert_eq!(binding.adapter, "migration-sequelize");
assert!(matches!(binding.kind, EntryKind::Migration { .. }));
}
}

View file

@ -36,11 +36,27 @@ pub mod js_handlebars;
pub mod js_koa;
pub mod js_nest;
pub mod js_routes;
pub mod graphql_apollo;
pub mod graphql_gqlgen;
pub mod graphql_graphene;
pub mod graphql_juniper;
pub mod graphql_relay;
pub mod kafka_java;
pub mod kafka_python;
pub mod ldap_php;
pub mod ldap_python;
pub mod ldap_spring;
pub mod middleware_django;
pub mod middleware_express;
pub mod middleware_laravel;
pub mod middleware_rails;
pub mod middleware_spring;
pub mod migration_django;
pub mod migration_flask;
pub mod migration_laravel;
pub mod migration_prisma;
pub mod migration_rails;
pub mod migration_sequelize;
pub mod nats_go;
pub mod php_codeigniter;
pub mod php_laravel;
@ -80,9 +96,17 @@ pub mod rust_axum;
pub mod rust_rocket;
pub mod rust_routes;
pub mod rust_warp;
pub mod scheduled_celery;
pub mod scheduled_cron;
pub mod scheduled_quartz;
pub mod scheduled_sidekiq;
pub mod sqs_java;
pub mod sqs_node;
pub mod sqs_python;
pub mod websocket_actioncable;
pub mod websocket_channels;
pub mod websocket_socketio;
pub mod websocket_ws;
pub mod xpath_java;
pub mod xpath_js;
pub mod xpath_php;
@ -115,11 +139,27 @@ pub use js_fastify::JsFastifyAdapter;
pub use js_handlebars::JsHandlebarsAdapter;
pub use js_koa::JsKoaAdapter;
pub use js_nest::{JsNestAdapter, TsNestAdapter};
pub use graphql_apollo::GraphqlApolloAdapter;
pub use graphql_gqlgen::GraphqlGqlgenAdapter;
pub use graphql_graphene::GraphqlGrapheneAdapter;
pub use graphql_juniper::GraphqlJuniperAdapter;
pub use graphql_relay::GraphqlRelayAdapter;
pub use kafka_java::KafkaJavaAdapter;
pub use kafka_python::KafkaPythonAdapter;
pub use ldap_php::LdapPhpAdapter;
pub use ldap_python::LdapPythonAdapter;
pub use ldap_spring::LdapSpringAdapter;
pub use middleware_django::MiddlewareDjangoAdapter;
pub use middleware_express::MiddlewareExpressAdapter;
pub use middleware_laravel::MiddlewareLaravelAdapter;
pub use middleware_rails::MiddlewareRailsAdapter;
pub use middleware_spring::MiddlewareSpringAdapter;
pub use migration_django::MigrationDjangoAdapter;
pub use migration_flask::MigrationFlaskAdapter;
pub use migration_laravel::MigrationLaravelAdapter;
pub use migration_prisma::MigrationPrismaAdapter;
pub use migration_rails::MigrationRailsAdapter;
pub use migration_sequelize::MigrationSequelizeAdapter;
pub use nats_go::NatsGoAdapter;
pub use php_codeigniter::PhpCodeIgniterAdapter;
pub use php_laravel::PhpLaravelAdapter;
@ -155,9 +195,17 @@ pub use rust_actix::RustActixAdapter;
pub use rust_axum::RustAxumAdapter;
pub use rust_rocket::RustRocketAdapter;
pub use rust_warp::RustWarpAdapter;
pub use scheduled_celery::ScheduledCeleryAdapter;
pub use scheduled_cron::ScheduledCronAdapter;
pub use scheduled_quartz::ScheduledQuartzAdapter;
pub use scheduled_sidekiq::ScheduledSidekiqAdapter;
pub use sqs_java::SqsJavaAdapter;
pub use sqs_node::SqsNodeAdapter;
pub use sqs_python::SqsPythonAdapter;
pub use websocket_actioncable::WebsocketActionCableAdapter;
pub use websocket_channels::WebsocketChannelsAdapter;
pub use websocket_socketio::WebsocketSocketIoAdapter;
pub use websocket_ws::WebsocketWsAdapter;
pub use xpath_java::XpathJavaAdapter;
pub use xpath_js::XpathJsAdapter;
pub use xpath_php::XpathPhpAdapter;

View file

@ -0,0 +1,117 @@
//! Phase 21 (Track M.3) — Python Celery scheduled-task adapter.
//!
//! Fires when the surrounding source imports Celery (`from celery`,
//! `import celery`) and the function body carries a `@app.task` /
//! `@shared_task` / `@celery.task` decorator or invokes a Celery
//! scheduling callee.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct ScheduledCeleryAdapter;
const ADAPTER_NAME: &str = "scheduled-celery";
fn callee_is_celery(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"task" | "shared_task" | "apply_async" | "delay" | "add_periodic_task"
)
}
fn source_imports_celery(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"from celery",
b"import celery",
b"@app.task",
b"@celery.task",
b"@shared_task",
b"celery.schedules",
b"crontab(",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
fn extract_schedule(file_bytes: &[u8]) -> Option<String> {
let text = std::str::from_utf8(file_bytes).unwrap_or("");
for needle in ["crontab(", "schedule=crontab(", "'schedule': crontab("] {
if let Some(idx) = text.find(needle) {
let after = &text[idx + needle.len()..];
if let Some(end) = after.find(')') {
let inner = after[..end].trim();
if !inner.is_empty() {
return Some(inner.to_owned());
}
}
}
}
None
}
impl FrameworkAdapter for ScheduledCeleryAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Python
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_celery);
let matches_source = source_imports_celery(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::ScheduledJob {
schedule: extract_schedule(file_bytes),
},
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
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 fires_on_celery_shared_task() {
let src: &[u8] = b"from celery import shared_task\n\
@shared_task\n\
def tick(payload):\n print(payload)\n";
let tree = parse_python(src);
let summary = FuncSummary {
name: "tick".into(),
..Default::default()
};
let binding = ScheduledCeleryAdapter
.detect(&summary, tree.root_node(), src)
.expect("celery binds");
assert_eq!(binding.adapter, "scheduled-celery");
assert!(matches!(binding.kind, EntryKind::ScheduledJob { .. }));
}
}

View file

@ -0,0 +1,146 @@
//! Phase 21 (Track M.3) — Node cron scheduled-job adapter.
//!
//! Fires when the surrounding source imports a JavaScript cron library
//! (`node-cron`, `cron`, `node-schedule`) and the function body invokes
//! a job-scheduling callee. The binding's [`EntryKind::ScheduledJob`]
//! is stamped with a best-effort `schedule` extracted from the source
//! (a `cron.schedule('* * * * *', fn)` literal); a missing literal
//! falls back to `None`.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct ScheduledCronAdapter;
const ADAPTER_NAME: &str = "scheduled-cron";
fn callee_is_cron(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"schedule" | "CronJob" | "scheduleJob" | "RecurrenceRule" | "job"
)
}
fn source_imports_cron(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"require('node-cron')",
b"require(\"node-cron\")",
b"from 'node-cron'",
b"from \"node-cron\"",
b"require('cron')",
b"require(\"cron\")",
b"from 'cron'",
b"from \"cron\"",
b"require('node-schedule')",
b"require(\"node-schedule\")",
b"from 'node-schedule'",
b"from \"node-schedule\"",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
fn extract_schedule(file_bytes: &[u8]) -> Option<String> {
let text = std::str::from_utf8(file_bytes).unwrap_or("");
for needle in [
"cron.schedule('",
"cron.schedule(\"",
"schedule.scheduleJob('",
"schedule.scheduleJob(\"",
"new CronJob('",
"new CronJob(\"",
] {
if let Some(idx) = text.find(needle) {
let after = &text[idx + needle.len()..];
let close = if needle.ends_with('"') { '"' } else { '\'' };
if let Some(end) = after.find(close) {
return Some(after[..end].to_owned());
}
}
}
None
}
impl FrameworkAdapter for ScheduledCronAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::JavaScript
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_cron);
let matches_source = source_imports_cron(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::ScheduledJob {
schedule: extract_schedule(file_bytes),
},
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_js(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_node_cron_schedule() {
let src: &[u8] = b"const cron = require('node-cron');\n\
function tick(payload) { console.log(payload); }\n\
cron.schedule('*/5 * * * *', tick);\n";
let tree = parse_js(src);
let summary = FuncSummary {
name: "tick".into(),
..Default::default()
};
let binding = ScheduledCronAdapter
.detect(&summary, tree.root_node(), src)
.expect("node-cron binds");
assert_eq!(binding.adapter, "scheduled-cron");
if let EntryKind::ScheduledJob { schedule } = binding.kind {
assert_eq!(schedule.as_deref(), Some("*/5 * * * *"));
} else {
panic!("expected ScheduledJob");
}
}
#[test]
fn skips_plain_function() {
let src: &[u8] = b"function add(a, b) { return a + b; }\n";
let tree = parse_js(src);
let summary = FuncSummary {
name: "add".into(),
..Default::default()
};
assert!(ScheduledCronAdapter
.detect(&summary, tree.root_node(), src)
.is_none());
}
}

View file

@ -0,0 +1,135 @@
//! Phase 21 (Track M.3) — Java Quartz scheduled-job adapter.
//!
//! Fires when the surrounding source imports the Quartz scheduling API
//! (`org.quartz.*`, `@Scheduled` from Spring's task-scheduling package)
//! and the function body invokes / annotates a job-execution callee.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct ScheduledQuartzAdapter;
const ADAPTER_NAME: &str = "scheduled-quartz";
fn callee_is_quartz(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"execute" | "scheduleJob" | "newJob" | "newTrigger" | "JobBuilder" | "TriggerBuilder"
)
}
fn source_imports_quartz(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"org.quartz",
b"@Scheduled",
b"org.springframework.scheduling",
b"import org.quartz",
b"implements Job",
b"@DisallowConcurrentExecution",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
fn extract_schedule(file_bytes: &[u8]) -> Option<String> {
let text = std::str::from_utf8(file_bytes).unwrap_or("");
for needle in [
"@Scheduled(cron = \"",
"@Scheduled(cron=\"",
"withSchedule(CronScheduleBuilder.cronSchedule(\"",
"cronSchedule(\"",
] {
if let Some(idx) = text.find(needle) {
let after = &text[idx + needle.len()..];
if let Some(end) = after.find('"') {
return Some(after[..end].to_owned());
}
}
}
None
}
impl FrameworkAdapter for ScheduledQuartzAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Java
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_quartz);
let matches_source = source_imports_quartz(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::ScheduledJob {
schedule: extract_schedule(file_bytes),
},
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_java(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_java::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_quartz_job() {
let src: &[u8] = b"import org.quartz.Job;\n\
public class TickJob implements Job {\n\
public void execute(JobExecutionContext ctx) { }\n\
}\n";
let tree = parse_java(src);
let summary = FuncSummary {
name: "execute".into(),
..Default::default()
};
let binding = ScheduledQuartzAdapter
.detect(&summary, tree.root_node(), src)
.expect("quartz binds");
assert_eq!(binding.adapter, "scheduled-quartz");
assert!(matches!(binding.kind, EntryKind::ScheduledJob { .. }));
}
#[test]
fn extracts_spring_cron_schedule() {
let src: &[u8] = b"@Scheduled(cron = \"0 0 12 * * ?\")\n\
public void tick() { }\n";
let tree = parse_java(src);
let summary = FuncSummary {
name: "tick".into(),
..Default::default()
};
let binding = ScheduledQuartzAdapter
.detect(&summary, tree.root_node(), src)
.expect("scheduled binds");
if let EntryKind::ScheduledJob { schedule } = binding.kind {
assert_eq!(schedule.as_deref(), Some("0 0 12 * * ?"));
}
}
}

View file

@ -0,0 +1,125 @@
//! Phase 21 (Track M.3) — Ruby Sidekiq worker / scheduled-job adapter.
//!
//! Fires when the surrounding source includes the Sidekiq worker
//! mixin (`include Sidekiq::Worker` / `Sidekiq::Job`) or invokes a
//! Sidekiq scheduling callee (`perform_async`, `perform_in`).
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct ScheduledSidekiqAdapter;
const ADAPTER_NAME: &str = "scheduled-sidekiq";
fn callee_is_sidekiq(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"perform_async" | "perform_in" | "perform" | "set"
)
}
fn source_imports_sidekiq(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"include Sidekiq::Worker",
b"include Sidekiq::Job",
b"Sidekiq::Worker",
b"Sidekiq::Job",
b"require 'sidekiq'",
b"require \"sidekiq\"",
b"sidekiq_options",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
fn extract_schedule(file_bytes: &[u8]) -> Option<String> {
let text = std::str::from_utf8(file_bytes).unwrap_or("");
for needle in [
"sidekiq_options queue: :",
"sidekiq_options queue: \"",
"sidekiq_options queue: '",
] {
if let Some(idx) = text.find(needle) {
let after = &text[idx + needle.len()..];
let close: &[char] = if needle.ends_with(':') {
&[',', '\n']
} else if needle.ends_with('"') {
&['"']
} else {
&['\'']
};
if let Some(end) = after.find(|c: char| close.contains(&c)) {
let v = after[..end].trim();
if !v.is_empty() {
return Some(v.to_owned());
}
}
}
}
None
}
impl FrameworkAdapter for ScheduledSidekiqAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Ruby
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_sidekiq);
let matches_source = source_imports_sidekiq(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::ScheduledJob {
schedule: extract_schedule(file_bytes),
},
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_ruby(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_ruby::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_sidekiq_worker() {
let src: &[u8] = b"class TickWorker\n include Sidekiq::Worker\n def perform(payload)\n puts payload\n end\nend\n";
let tree = parse_ruby(src);
let summary = FuncSummary {
name: "perform".into(),
..Default::default()
};
let binding = ScheduledSidekiqAdapter
.detect(&summary, tree.root_node(), src)
.expect("sidekiq binds");
assert_eq!(binding.adapter, "scheduled-sidekiq");
assert!(matches!(binding.kind, EntryKind::ScheduledJob { .. }));
}
}

View file

@ -0,0 +1,113 @@
//! Phase 21 (Track M.3) — Rails ActionCable WebSocket adapter (Ruby).
//!
//! Fires when the surrounding source declares an `ApplicationCable` /
//! `ActionCable::Channel::Base` subclass and the function body sits on
//! a `receive` / `subscribed` / `unsubscribed` callback.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct WebsocketActionCableAdapter;
const ADAPTER_NAME: &str = "websocket-actioncable";
fn callee_is_actioncable(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"receive" | "subscribed" | "unsubscribed" | "transmit" | "broadcast"
)
}
fn source_imports_actioncable(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"ApplicationCable::Channel",
b"ActionCable::Channel::Base",
b"< ApplicationCable",
b"< ActionCable::Channel",
b"require 'action_cable'",
b"require \"action_cable\"",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
fn extract_path(file_bytes: &[u8]) -> String {
let text = std::str::from_utf8(file_bytes).unwrap_or("");
for needle in ["stream_from '", "stream_from \"", "stream_for '", "stream_for \""] {
if let Some(idx) = text.find(needle) {
let after = &text[idx + needle.len()..];
let close = if needle.ends_with('"') { '"' } else { '\'' };
if let Some(end) = after.find(close) {
return after[..end].to_owned();
}
}
}
"/cable".to_owned()
}
impl FrameworkAdapter for WebsocketActionCableAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Ruby
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_actioncable);
let matches_source = source_imports_actioncable(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::WebSocket {
path: extract_path(file_bytes),
},
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_ruby(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_ruby::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_actioncable_channel() {
let src: &[u8] = b"class ChatChannel < ApplicationCable::Channel\n def subscribed\n stream_from 'chat_room'\n end\n def receive(data)\n end\nend\n";
let tree = parse_ruby(src);
let summary = FuncSummary {
name: "receive".into(),
..Default::default()
};
let binding = WebsocketActionCableAdapter
.detect(&summary, tree.root_node(), src)
.expect("action_cable binds");
assert_eq!(binding.adapter, "websocket-actioncable");
if let EntryKind::WebSocket { path } = binding.kind {
assert_eq!(path, "chat_room");
}
}
}

View file

@ -0,0 +1,112 @@
//! Phase 21 (Track M.3) — Django Channels WebSocket adapter (Python).
//!
//! Fires when the surrounding source imports Django Channels
//! (`channels.generic.websocket`, `AsyncWebsocketConsumer`) and the
//! function body sits inside a `WebsocketConsumer` subclass.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct WebsocketChannelsAdapter;
const ADAPTER_NAME: &str = "websocket-channels";
fn callee_is_channels(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"receive" | "receive_json" | "connect" | "disconnect" | "send" | "send_json"
)
}
fn source_imports_channels(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"channels.generic.websocket",
b"WebsocketConsumer",
b"AsyncWebsocketConsumer",
b"JsonWebsocketConsumer",
b"AsyncJsonWebsocketConsumer",
b"from channels",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
fn extract_path(file_bytes: &[u8]) -> String {
let text = std::str::from_utf8(file_bytes).unwrap_or("");
for needle in ["re_path(r'", "re_path('", "path('", "path(\""] {
if let Some(idx) = text.find(needle) {
let after = &text[idx + needle.len()..];
let close: &[char] = &['\'', '"'];
if let Some(end) = after.find(|c: char| close.contains(&c)) {
return after[..end].to_owned();
}
}
}
"/ws/".to_owned()
}
impl FrameworkAdapter for WebsocketChannelsAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Python
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_channels);
let matches_source = source_imports_channels(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::WebSocket {
path: extract_path(file_bytes),
},
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
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 fires_on_channels_consumer() {
let src: &[u8] = b"from channels.generic.websocket import WebsocketConsumer\n\
class ChatConsumer(WebsocketConsumer):\n def receive(self, text_data=None, bytes_data=None):\n pass\n";
let tree = parse_python(src);
let summary = FuncSummary {
name: "receive".into(),
..Default::default()
};
let binding = WebsocketChannelsAdapter
.detect(&summary, tree.root_node(), src)
.expect("channels binds");
assert_eq!(binding.adapter, "websocket-channels");
assert!(matches!(binding.kind, EntryKind::WebSocket { .. }));
}
}

View file

@ -0,0 +1,116 @@
//! Phase 21 (Track M.3) — Socket.IO WebSocket adapter (Python).
//!
//! Fires when the surrounding source imports `python-socketio` /
//! `socketio` and the function body is registered against an `on(...)`
//! event name.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct WebsocketSocketIoAdapter;
const ADAPTER_NAME: &str = "websocket-socketio";
fn callee_is_socketio(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"on" | "emit" | "send" | "AsyncServer" | "Server" | "event"
)
}
fn source_imports_socketio(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"import socketio",
b"from socketio",
b"socketio.Server",
b"socketio.AsyncServer",
b"@sio.event",
b"@sio.on(",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
fn extract_path(file_bytes: &[u8]) -> String {
let text = std::str::from_utf8(file_bytes).unwrap_or("");
for needle in ["sio.on('", "sio.on(\"", "@sio.on('", "@sio.on(\""] {
if let Some(idx) = text.find(needle) {
let after = &text[idx + needle.len()..];
let close = if needle.ends_with('"') { '"' } else { '\'' };
if let Some(end) = after.find(close) {
return after[..end].to_owned();
}
}
}
"/".to_owned()
}
impl FrameworkAdapter for WebsocketSocketIoAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Python
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_socketio);
let matches_source = source_imports_socketio(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::WebSocket {
path: extract_path(file_bytes),
},
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
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 fires_on_socketio_event() {
let src: &[u8] = b"import socketio\n\
sio = socketio.Server()\n\
@sio.on('message')\n\
def message(sid, data):\n pass\n";
let tree = parse_python(src);
let summary = FuncSummary {
name: "message".into(),
..Default::default()
};
let binding = WebsocketSocketIoAdapter
.detect(&summary, tree.root_node(), src)
.expect("socketio binds");
assert_eq!(binding.adapter, "websocket-socketio");
if let EntryKind::WebSocket { path } = binding.kind {
assert_eq!(path, "message");
}
}
}

View file

@ -0,0 +1,116 @@
//! Phase 21 (Track M.3) — `ws` (Node WebSocket) adapter.
//!
//! Fires when the surrounding source requires/imports the `ws` package
//! and the function body is the `on('message', ...)` listener on a
//! `WebSocket.Server` / `WebSocketServer` instance.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct WebsocketWsAdapter;
const ADAPTER_NAME: &str = "websocket-ws";
fn callee_is_ws(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"WebSocket" | "WebSocketServer" | "Server" | "on" | "send"
)
}
fn source_imports_ws(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"require('ws')",
b"require(\"ws\")",
b"from 'ws'",
b"from \"ws\"",
b"new WebSocketServer",
b"new WebSocket.Server",
b"WebSocket.Server",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
fn extract_path(file_bytes: &[u8]) -> String {
let text = std::str::from_utf8(file_bytes).unwrap_or("");
for needle in ["path: '", "path: \"", "path:'", "path:\""] {
if let Some(idx) = text.find(needle) {
let after = &text[idx + needle.len()..];
let close = if needle.ends_with('"') { '"' } else { '\'' };
if let Some(end) = after.find(close) {
return after[..end].to_owned();
}
}
}
"/".to_owned()
}
impl FrameworkAdapter for WebsocketWsAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::JavaScript
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_ws);
let matches_source = source_imports_ws(file_bytes);
if matches_call || matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::WebSocket {
path: extract_path(file_bytes),
},
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_js(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_ws_server() {
let src: &[u8] = b"const { WebSocketServer } = require('ws');\n\
const wss = new WebSocketServer({ port: 0, path: '/feed' });\n\
function onMessage(data) { }\n";
let tree = parse_js(src);
let summary = FuncSummary {
name: "onMessage".into(),
..Default::default()
};
let binding = WebsocketWsAdapter
.detect(&summary, tree.root_node(), src)
.expect("ws binds");
assert_eq!(binding.adapter, "websocket-ws");
if let EntryKind::WebSocket { path } = binding.kind {
assert_eq!(path, "/feed");
}
}
}

View file

@ -214,18 +214,31 @@ mod tests {
}
#[test]
fn registry_baseline_after_phase_20() {
// Phase 20 (Track M.2) adds 10 MessageHandler-flavoured
// framework adapters distributed across Java (3 — Kafka,
// RabbitMQ, SQS), Python (4 — Kafka, Pub/Sub, RabbitMQ, SQS),
// Go (2 — Pub/Sub, NATS), and JavaScript (1 — SQS). The
// Phase 17 baseline for the other languages stays put: Php 10,
// Ruby 8, TypeScript 4, Rust 6, C/Cpp empty.
fn registry_baseline_after_phase_21() {
// Phase 21 (Track M.3) adds the remaining five `EntryKind`
// variants — `ScheduledJob` / `GraphQLResolver` / `WebSocket`
// / `Middleware` / `Migration` — distributed across the
// language slices. Per-lang deltas vs the Phase 20 baseline:
// Java: +2 (ScheduledQuartz, MiddlewareSpring) 14 → 16
// Php: +2 (MiddlewareLaravel, MigrationLaravel) 10 → 12
// Python: +7 (GraphqlGraphene, MiddlewareDjango,
// MigrationDjango, MigrationFlask,
// ScheduledCelery, WebsocketChannels,
// WebsocketSocketIo) 15 → 22
// Ruby: +4 (MiddlewareRails, MigrationRails,
// ScheduledSidekiq, WebsocketActionCable) 8 → 12
// JavaScript: +7 (GraphqlApollo, GraphqlRelay,
// MiddlewareExpress, MigrationPrisma,
// MigrationSequelize, ScheduledCron,
// WebsocketWs) 12 → 19
// Go: +1 (GraphqlGqlgen) 9 → 10
// Rust: +1 (GraphqlJuniper) 6 → 7
// TypeScript / C / Cpp stay unchanged.
let java_registered = registry::adapters_for(Lang::Java);
assert_eq!(
java_registered.len(),
14,
"Java must have Phase 17 baseline (11) + M.2 Kafka/Rabbit/SQS (3)",
16,
"Java must have Phase 20 baseline (14) + M.3 Quartz/Spring-middleware (2)",
);
for adapter in java_registered {
assert_eq!(adapter.lang(), Lang::Java);
@ -233,8 +246,8 @@ mod tests {
let php_registered = registry::adapters_for(Lang::Php);
assert_eq!(
php_registered.len(),
10,
"Php must have J.1..J.7 (7) + L.14 Laravel/Symfony/CodeIgniter (3) adapters",
12,
"Php must have Phase 20 baseline (10) + M.3 Laravel middleware+migration (2)",
);
for adapter in php_registered {
assert_eq!(adapter.lang(), Lang::Php);
@ -242,8 +255,8 @@ mod tests {
let python_registered = registry::adapters_for(Lang::Python);
assert_eq!(
python_registered.len(),
15,
"Python must have Phase 17 baseline (11) + M.2 Kafka/Pub-Sub/Rabbit/SQS (4)",
22,
"Python must have Phase 20 baseline (15) + M.3 Phase-21 (7)",
);
for adapter in python_registered {
assert_eq!(adapter.lang(), Lang::Python);
@ -251,8 +264,8 @@ mod tests {
let ruby_registered = registry::adapters_for(Lang::Ruby);
assert_eq!(
ruby_registered.len(),
8,
"Ruby must have the J.1 + J.2 + J.3 + J.6 + J.7 (5) + L.13 Rails/Sinatra/Hanami (3) adapters",
12,
"Ruby must have Phase 20 baseline (8) + M.3 Phase-21 (4)",
);
for adapter in ruby_registered {
assert_eq!(adapter.lang(), Lang::Ruby);
@ -260,8 +273,8 @@ mod tests {
let js_registered = registry::adapters_for(Lang::JavaScript);
assert_eq!(
js_registered.len(),
12,
"JavaScript must have Phase 17 baseline (11) + M.2 sqs-node (1)",
19,
"JavaScript must have Phase 20 baseline (12) + M.3 Phase-21 (7)",
);
for adapter in js_registered {
assert_eq!(adapter.lang(), Lang::JavaScript);
@ -270,7 +283,7 @@ mod tests {
assert_eq!(
ts_registered.len(),
4,
"TypeScript must have the J.8(×3) prototype-pollution adapters + L.11 ts-nest",
"TypeScript stays at Phase 20 baseline (4)",
);
for adapter in ts_registered {
assert_eq!(adapter.lang(), Lang::TypeScript);
@ -278,8 +291,8 @@ mod tests {
let go_registered = registry::adapters_for(Lang::Go);
assert_eq!(
go_registered.len(),
9,
"Go must have Phase 17 baseline (7) + M.2 pubsub-go/nats-go (2)",
10,
"Go must have Phase 20 baseline (9) + M.3 gqlgen (1)",
);
for adapter in go_registered {
assert_eq!(adapter.lang(), Lang::Go);
@ -287,8 +300,8 @@ mod tests {
let rust_registered = registry::adapters_for(Lang::Rust);
assert_eq!(
rust_registered.len(),
6,
"Rust must have the J.6 + J.7 (2) + L.15 actix/axum/rocket/warp (4) adapters",
7,
"Rust must have Phase 20 baseline (6) + M.3 juniper (1)",
);
for adapter in rust_registered {
assert_eq!(adapter.lang(), Lang::Rust);

View file

@ -45,6 +45,7 @@ pub fn adapters_for(lang: Lang) -> &'static [&'static dyn FrameworkAdapter] {
// later phase that appends a new adapter cannot silently re-order
// the existing first-match.
static RUST: &[&dyn FrameworkAdapter] = &[
&super::adapters::GraphqlJuniperAdapter,
&super::adapters::HeaderRustAdapter,
&super::adapters::RedirectRustAdapter,
&super::adapters::RustActixAdapter,
@ -64,8 +65,10 @@ static JAVA: &[&dyn FrameworkAdapter] = &[
&super::adapters::JavaThymeleafAdapter,
&super::adapters::KafkaJavaAdapter,
&super::adapters::LdapSpringAdapter,
&super::adapters::MiddlewareSpringAdapter,
&super::adapters::RabbitJavaAdapter,
&super::adapters::RedirectJavaAdapter,
&super::adapters::ScheduledQuartzAdapter,
&super::adapters::SqsJavaAdapter,
&super::adapters::XpathJavaAdapter,
&super::adapters::XxeJavaAdapter,
@ -75,6 +78,7 @@ static GO: &[&dyn FrameworkAdapter] = &[
&super::adapters::GoEchoAdapter,
&super::adapters::GoFiberAdapter,
&super::adapters::GoGinAdapter,
&super::adapters::GraphqlGqlgenAdapter,
&super::adapters::HeaderGoAdapter,
&super::adapters::NatsGoAdapter,
&super::adapters::PubsubGoAdapter,
@ -84,6 +88,8 @@ static GO: &[&dyn FrameworkAdapter] = &[
static PHP: &[&dyn FrameworkAdapter] = &[
&super::adapters::HeaderPhpAdapter,
&super::adapters::LdapPhpAdapter,
&super::adapters::MiddlewareLaravelAdapter,
&super::adapters::MigrationLaravelAdapter,
&super::adapters::PhpCodeIgniterAdapter,
&super::adapters::PhpLaravelAdapter,
&super::adapters::PhpSymfonyAdapter,
@ -94,9 +100,13 @@ static PHP: &[&dyn FrameworkAdapter] = &[
&super::adapters::XxePhpAdapter,
];
static PYTHON: &[&dyn FrameworkAdapter] = &[
&super::adapters::GraphqlGrapheneAdapter,
&super::adapters::HeaderPythonAdapter,
&super::adapters::KafkaPythonAdapter,
&super::adapters::LdapPythonAdapter,
&super::adapters::MiddlewareDjangoAdapter,
&super::adapters::MigrationDjangoAdapter,
&super::adapters::MigrationFlaskAdapter,
&super::adapters::PubsubPythonAdapter,
&super::adapters::PythonDjangoAdapter,
&super::adapters::PythonFastApiAdapter,
@ -106,18 +116,25 @@ static PYTHON: &[&dyn FrameworkAdapter] = &[
&super::adapters::PythonStarletteAdapter,
&super::adapters::RabbitPythonAdapter,
&super::adapters::RedirectPythonAdapter,
&super::adapters::ScheduledCeleryAdapter,
&super::adapters::SqsPythonAdapter,
&super::adapters::WebsocketChannelsAdapter,
&super::adapters::WebsocketSocketIoAdapter,
&super::adapters::XpathPythonAdapter,
&super::adapters::XxePythonAdapter,
];
static RUBY: &[&dyn FrameworkAdapter] = &[
&super::adapters::HeaderRubyAdapter,
&super::adapters::MiddlewareRailsAdapter,
&super::adapters::MigrationRailsAdapter,
&super::adapters::RedirectRubyAdapter,
&super::adapters::RubyErbAdapter,
&super::adapters::RubyHanamiAdapter,
&super::adapters::RubyMarshalAdapter,
&super::adapters::RubyRailsAdapter,
&super::adapters::RubySinatraAdapter,
&super::adapters::ScheduledSidekiqAdapter,
&super::adapters::WebsocketActionCableAdapter,
&super::adapters::XxeRubyAdapter,
];
static TYPESCRIPT: &[&dyn FrameworkAdapter] = &[
@ -127,16 +144,23 @@ static TYPESCRIPT: &[&dyn FrameworkAdapter] = &[
&super::adapters::TsNestAdapter,
];
static JAVASCRIPT: &[&dyn FrameworkAdapter] = &[
&super::adapters::GraphqlApolloAdapter,
&super::adapters::GraphqlRelayAdapter,
&super::adapters::HeaderJsAdapter,
&super::adapters::JsExpressAdapter,
&super::adapters::JsFastifyAdapter,
&super::adapters::JsHandlebarsAdapter,
&super::adapters::JsKoaAdapter,
&super::adapters::JsNestAdapter,
&super::adapters::MiddlewareExpressAdapter,
&super::adapters::MigrationPrismaAdapter,
&super::adapters::MigrationSequelizeAdapter,
&super::adapters::PpJsonDeepAssignJsAdapter,
&super::adapters::PpLodashMergeJsAdapter,
&super::adapters::PpObjectAssignJsAdapter,
&super::adapters::RedirectJsAdapter,
&super::adapters::ScheduledCronAdapter,
&super::adapters::SqsNodeAdapter,
&super::adapters::WebsocketWsAdapter,
&super::adapters::XpathJsAdapter,
];

View file

@ -57,6 +57,7 @@ const SUPPORTED: &[EntryKindTag] = &[
EntryKindTag::CliSubcommand,
EntryKindTag::ClassMethod,
EntryKindTag::MessageHandler,
EntryKindTag::GraphQLResolver,
];
impl LangEmitter for GoEmitter {
@ -592,6 +593,11 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
return Ok(emit_message_handler_harness(spec, queue));
}
// Phase 21 (Track M.3): GraphQLResolver short-circuit (gqlgen).
if let crate::evidence::EntryKind::GraphQLResolver { type_name, field } = &spec.entry_kind {
return Ok(emit_graphql_resolver_harness(&spec.entry_name, type_name, field));
}
let entry_source = read_entry_source(&spec.entry_file);
let shape = GoShape::detect(spec, &entry_source);
let main_go = generate_main_go(spec, shape);
@ -1269,6 +1275,85 @@ func main() {{
}
}
// ── Phase 21 (Track M.3) — synthetic entry-kind harnesses ─────────────────────
/// Phase 21 (Track M.3) — GraphQL resolver harness for Go (gqlgen).
///
/// Looks up the named resolver via the entry package's `NyxResolvers`
/// map (mirrors the `NyxReceivers` / `NyxHandlers` contracts from
/// Phase 19 / 20), constructs a synthetic `context.Background()`, and
/// invokes the resolver with the payload positionally.
fn emit_graphql_resolver_harness(handler: &str, type_name: &str, field: &str) -> HarnessSource {
let shim = probe_shim();
let go_mod = generate_go_mod();
let source = format!(
r##"// Nyx dynamic harness — GraphQL resolver (Phase 21 / Track M.3).
package main
import (
"context"
"fmt"
"os"
"reflect"
"nyx-harness/entry"
)
{shim}
func nyxPayload() string {{
if v := os.Getenv("NYX_PAYLOAD"); v != "" {{
return v
}}
return ""
}}
func main() {{
__nyx_install_crash_guard("{type_name}.{field}")
payload := nyxPayload()
fmt.Println("__NYX_GRAPHQL_RESOLVER__: " + "{type_name}" + "." + "{field}")
fmt.Println("__NYX_SINK_HIT__")
cb, ok := entry.NyxResolvers["{handler}"]
if !ok {{
fmt.Fprintln(os.Stderr, "NYX_RESOLVER_NOT_FOUND: " + "{handler}")
os.Exit(78)
}}
v := reflect.ValueOf(cb)
args := make([]reflect.Value, v.Type().NumIn())
for i := 0; i < v.Type().NumIn(); i++ {{
want := v.Type().In(i)
if want.Kind() == reflect.String {{
args[i] = reflect.ValueOf(payload)
}} else if want.String() == "context.Context" {{
args[i] = reflect.ValueOf(context.Background())
}} else {{
args[i] = reflect.Zero(want)
}}
}}
defer func() {{
if r := recover(); r != nil {{
fmt.Fprintf(os.Stderr, "NYX_EXCEPTION: panic: %v\n", r)
}}
}}()
out := v.Call(args)
if len(out) > 0 {{
fmt.Println(out[0].Interface())
}}
}}
"##,
handler = handler,
type_name = type_name,
field = field,
);
HarnessSource {
source,
filename: "main.go".to_owned(),
command: vec!["./nyx_harness".to_owned()],
extra_files: vec![("go.mod".to_owned(), go_mod)],
entry_subpath: Some("entry/entry.go".to_owned()),
}
}
#[derive(Debug, Clone, Copy)]
enum GoBroker {
Pubsub,

View file

@ -56,6 +56,8 @@ const SUPPORTED: &[EntryKindTag] = &[
EntryKindTag::CliSubcommand,
EntryKindTag::ClassMethod,
EntryKindTag::MessageHandler,
EntryKindTag::ScheduledJob,
EntryKindTag::Middleware,
];
impl LangEmitter for JavaEmitter {
@ -611,6 +613,20 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
return Ok(emit_message_handler_harness(spec, queue, &entry_class));
}
// Phase 21 (Track M.3): ScheduledJob short-circuit (Quartz).
if let crate::evidence::EntryKind::ScheduledJob { schedule } = &spec.entry_kind {
let entry_source = read_entry_source(&spec.entry_file);
let entry_class = derive_entry_class(&entry_source);
return Ok(emit_scheduled_job_harness(spec, schedule.as_deref(), &entry_class));
}
// Phase 21 (Track M.3): Middleware short-circuit (Spring HandlerInterceptor / Filter).
if let crate::evidence::EntryKind::Middleware { name } = &spec.entry_kind {
let entry_source = read_entry_source(&spec.entry_file);
let entry_class = derive_entry_class(&entry_source);
return Ok(emit_middleware_harness(spec, name, &entry_class));
}
let entry_source = read_entry_source(&spec.entry_file);
let shape = JavaShape::detect(spec, &entry_source);
let entry_class = derive_entry_class(&entry_source);
@ -2103,6 +2119,165 @@ public class NyxHarness {{
}
}
// ── Phase 21 (Track M.3) — synthetic entry-kind harnesses ─────────────────────
fn emit_scheduled_job_harness(
spec: &HarnessSpec,
schedule: Option<&str>,
entry_class: &str,
) -> HarnessSource {
let probe = probe_shim();
let pre_call = pre_call_setup(spec);
let method = &spec.entry_name;
let schedule_repr = schedule.unwrap_or("<unscheduled>");
let source = format!(
r#"// Nyx dynamic harness — scheduled job (Phase 21 / Track M.3).
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
public class NyxHarness {{
{probe}
public static void main(String[] args) {{
String payload = nyxPayload();
{pre_call} System.out.println("__NYX_SCHEDULED_JOB__: " + {schedule:?});
System.out.println("__NYX_SINK_HIT__");
try {{
Class<?> cls = Class.forName({entry_class:?});
Constructor<?> ctor = cls.getDeclaredConstructor();
ctor.setAccessible(true);
Object instance = ctor.newInstance();
Method m = null;
for (Method candidate : cls.getDeclaredMethods()) {{
if (candidate.getName().equals({method:?})) {{ m = candidate; break; }}
}}
if (m == null) {{
System.err.println("NYX_METHOD_NOT_FOUND: " + {method:?});
System.exit(78);
}}
m.setAccessible(true);
Class<?>[] params = m.getParameterTypes();
Object[] mArgs = new Object[params.length];
for (int i = 0; i < params.length; i++) {{
mArgs[i] = params[i].equals(String.class) ? payload : null;
}}
m.invoke(instance, mArgs);
}} catch (InvocationTargetException ite) {{
Throwable cause = ite.getCause() == null ? ite : ite.getCause();
System.err.println("NYX_EXCEPTION: " + cause.getClass().getName() + ": " + cause.getMessage());
}} catch (Throwable e) {{
System.err.println("NYX_EXCEPTION: " + e.getClass().getName() + ": " + e.getMessage());
}}
}}
static String nyxPayload() {{
String v = System.getenv("NYX_PAYLOAD");
if (v != null && !v.isEmpty()) return v;
String b64 = System.getenv("NYX_PAYLOAD_B64");
if (b64 != null && !b64.isEmpty()) {{
byte[] decoded = java.util.Base64.getDecoder().decode(b64);
return new String(decoded, java.nio.charset.StandardCharsets.UTF_8);
}}
return "";
}}
}}
"#,
entry_class = entry_class,
method = method,
schedule = schedule_repr,
pre_call = pre_call,
);
HarnessSource {
source,
filename: "NyxHarness.java".to_owned(),
command: vec![
"java".to_owned(),
"-cp".to_owned(),
".".to_owned(),
"NyxHarness".to_owned(),
],
extra_files: vec![],
entry_subpath: Some(format!("{entry_class}.java")),
}
}
fn emit_middleware_harness(spec: &HarnessSpec, name: &str, entry_class: &str) -> HarnessSource {
let probe = probe_shim();
let pre_call = pre_call_setup(spec);
let method = &spec.entry_name;
let source = format!(
r#"// Nyx dynamic harness — middleware (Phase 21 / Track M.3).
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
public class NyxHarness {{
{probe}
public static void main(String[] args) {{
String payload = nyxPayload();
{pre_call} System.out.println("__NYX_MIDDLEWARE__: " + {name:?});
System.out.println("__NYX_SINK_HIT__");
try {{
Class<?> cls = Class.forName({entry_class:?});
Constructor<?> ctor = cls.getDeclaredConstructor();
ctor.setAccessible(true);
Object instance = ctor.newInstance();
Method m = null;
for (Method candidate : cls.getDeclaredMethods()) {{
if (candidate.getName().equals({method:?})) {{ m = candidate; break; }}
}}
if (m == null) {{
System.err.println("NYX_METHOD_NOT_FOUND: " + {method:?});
System.exit(78);
}}
m.setAccessible(true);
Class<?>[] params = m.getParameterTypes();
Object[] mArgs = new Object[params.length];
for (int i = 0; i < params.length; i++) {{
mArgs[i] = params[i].equals(String.class) ? payload : null;
}}
m.invoke(instance, mArgs);
}} catch (InvocationTargetException ite) {{
Throwable cause = ite.getCause() == null ? ite : ite.getCause();
System.err.println("NYX_EXCEPTION: " + cause.getClass().getName() + ": " + cause.getMessage());
}} catch (Throwable e) {{
System.err.println("NYX_EXCEPTION: " + e.getClass().getName() + ": " + e.getMessage());
}}
}}
static String nyxPayload() {{
String v = System.getenv("NYX_PAYLOAD");
if (v != null && !v.isEmpty()) return v;
String b64 = System.getenv("NYX_PAYLOAD_B64");
if (b64 != null && !b64.isEmpty()) {{
byte[] decoded = java.util.Base64.getDecoder().decode(b64);
return new String(decoded, java.nio.charset.StandardCharsets.UTF_8);
}}
return "";
}}
}}
"#,
entry_class = entry_class,
method = method,
name = name,
pre_call = pre_call,
);
HarnessSource {
source,
filename: "NyxHarness.java".to_owned(),
command: vec![
"java".to_owned(),
"-cp".to_owned(),
".".to_owned(),
"NyxHarness".to_owned(),
],
extra_files: vec![],
entry_subpath: Some(format!("{entry_class}.java")),
}
}
#[derive(Debug, Clone, Copy)]
enum JavaBroker {
Kafka,

View file

@ -583,6 +583,31 @@ pub fn emit(spec: &HarnessSpec, is_typescript: bool) -> Result<HarnessSource, Un
return Ok(emit_message_handler(spec, queue, is_typescript));
}
// Phase 21 (Track M.3): ScheduledJob short-circuit.
if let crate::evidence::EntryKind::ScheduledJob { schedule } = &spec.entry_kind {
return Ok(emit_scheduled_job(spec, schedule.as_deref(), is_typescript));
}
// Phase 21 (Track M.3): GraphQLResolver short-circuit.
if let crate::evidence::EntryKind::GraphQLResolver { type_name, field } = &spec.entry_kind {
return Ok(emit_graphql_resolver(spec, type_name, field, is_typescript));
}
// Phase 21 (Track M.3): WebSocket short-circuit.
if let crate::evidence::EntryKind::WebSocket { path } = &spec.entry_kind {
return Ok(emit_websocket_handler(spec, path, is_typescript));
}
// Phase 21 (Track M.3): Middleware short-circuit.
if let crate::evidence::EntryKind::Middleware { name } = &spec.entry_kind {
return Ok(emit_middleware(spec, name, is_typescript));
}
// Phase 21 (Track M.3): Migration short-circuit.
if let crate::evidence::EntryKind::Migration { version } = &spec.entry_kind {
return Ok(emit_migration(spec, version.as_deref(), is_typescript));
}
let entry_source = read_entry_source(&spec.entry_file);
let shape = JsShape::detect(spec, &entry_source);
let entry_subpath = entry_subpath_for_shape(shape, is_typescript);
@ -780,6 +805,264 @@ _broker.subscribe({queue:?}, async (envelope) => {{
}
}
// ── Phase 21 (Track M.3) — synthetic entry-kind harnesses ─────────────────────
fn nyx_js_preamble(spec: &HarnessSpec, is_typescript: bool) -> (String, String) {
let probe = probe_shim();
let entry_subpath = if is_typescript { "entry.ts" } else { "entry.js" };
let require_path = entry_require_path(entry_subpath);
let preamble = format!(
r#"'use strict';
{probe}
const payload = (process.env.NYX_PAYLOAD && process.env.NYX_PAYLOAD.length > 0)
? process.env.NYX_PAYLOAD
: (process.env.NYX_PAYLOAD_B64
? Buffer.from(process.env.NYX_PAYLOAD_B64, 'base64').toString('utf8')
: '');
let _entry;
try {{
_entry = require('./{require_path}');
}} catch (e) {{
process.stderr.write('NYX_IMPORT_ERROR: ' + e.message + '\n');
process.exit(77);
}}
function _nyxResolve(name) {{
const _h = _entry[name]
|| (_entry.default && _entry.default[name])
|| (typeof _entry.default === 'function' && _entry.default.name === name ? _entry.default : null);
return (typeof _h === 'function') ? _h : null;
}}
process.stdout.write('__NYX_SINK_HIT__\n');
"#,
probe = probe,
require_path = require_path,
);
let _ = spec;
(preamble, entry_subpath.to_owned())
}
fn emit_scheduled_job(spec: &HarnessSpec, schedule: Option<&str>, is_typescript: bool) -> HarnessSource {
let (preamble, entry_subpath) = nyx_js_preamble(spec, is_typescript);
let handler = &spec.entry_name;
let schedule_repr = schedule.unwrap_or("<unscheduled>");
let body = format!(
r#"{preamble}
// Phase 21 (Track M.3) — scheduled job.
process.stdout.write('__NYX_SCHEDULED_JOB__: ' + {schedule:?} + '\n');
const _h = _nyxResolve({handler:?});
if (_h == null) {{
process.stderr.write('NYX_HANDLER_NOT_FOUND: ' + {handler:?} + '\n');
process.exit(78);
}}
(async () => {{
try {{
const _result = await Promise.resolve(_h(payload));
if (_result != null) process.stdout.write(String(_result) + '\n');
}} catch (e) {{
process.stderr.write('NYX_EXCEPTION: ' + (e.constructor ? e.constructor.name : 'Error') + ': ' + e.message + '\n');
}}
}})();
"#,
preamble = preamble,
handler = handler,
schedule = schedule_repr,
);
HarnessSource {
source: body,
filename: "harness.js".to_owned(),
command: vec!["node".to_owned(), "harness.js".to_owned()],
extra_files: Vec::new(),
entry_subpath: Some(entry_subpath),
}
}
fn emit_graphql_resolver(
spec: &HarnessSpec,
type_name: &str,
field: &str,
is_typescript: bool,
) -> HarnessSource {
let (preamble, entry_subpath) = nyx_js_preamble(spec, is_typescript);
let handler = &spec.entry_name;
let body = format!(
r#"{preamble}
// Phase 21 (Track M.3) — GraphQL resolver.
process.stdout.write('__NYX_GRAPHQL_RESOLVER__: ' + {type_name:?} + '.' + {field:?} + '\n');
const _h = _nyxResolve({handler:?});
if (_h == null) {{
process.stderr.write('NYX_RESOLVER_NOT_FOUND: ' + {handler:?} + '\n');
process.exit(78);
}}
(async () => {{
try {{
// Apollo resolver shape: (parent, args, context, info).
const _info = {{ fieldName: {field:?}, parentType: {type_name:?} }};
const _result = await Promise.resolve(_h(null, {{ id: payload, input: payload }}, {{}}, _info));
if (_result != null) process.stdout.write(String(_result) + '\n');
}} catch (e) {{
process.stderr.write('NYX_EXCEPTION: ' + (e.constructor ? e.constructor.name : 'Error') + ': ' + e.message + '\n');
}}
}})();
"#,
preamble = preamble,
handler = handler,
type_name = type_name,
field = field,
);
HarnessSource {
source: body,
filename: "harness.js".to_owned(),
command: vec!["node".to_owned(), "harness.js".to_owned()],
extra_files: Vec::new(),
entry_subpath: Some(entry_subpath),
}
}
fn emit_websocket_handler(spec: &HarnessSpec, path: &str, is_typescript: bool) -> HarnessSource {
let (preamble, entry_subpath) = nyx_js_preamble(spec, is_typescript);
let handler = &spec.entry_name;
let body = format!(
r#"{preamble}
// Phase 21 (Track M.3) — WebSocket handler.
process.stdout.write('__NYX_WEBSOCKET__: ' + {path:?} + '\n');
const _h = _nyxResolve({handler:?});
if (_h == null) {{
process.stderr.write('NYX_HANDLER_NOT_FOUND: ' + {handler:?} + '\n');
process.exit(78);
}}
(async () => {{
try {{
// ws library: handler(message); socket.io: handler(socket, data).
let _result;
try {{
_result = await Promise.resolve(_h(payload));
}} catch (e1) {{
if (e1 && e1.constructor && e1.constructor.name === 'TypeError') {{
_result = await Promise.resolve(_h({{ id: 'nyx-sock' }}, payload));
}} else {{
throw e1;
}}
}}
if (_result != null) process.stdout.write(String(_result) + '\n');
}} catch (e) {{
process.stderr.write('NYX_EXCEPTION: ' + (e.constructor ? e.constructor.name : 'Error') + ': ' + e.message + '\n');
}}
}})();
"#,
preamble = preamble,
handler = handler,
path = path,
);
HarnessSource {
source: body,
filename: "harness.js".to_owned(),
command: vec!["node".to_owned(), "harness.js".to_owned()],
extra_files: Vec::new(),
entry_subpath: Some(entry_subpath),
}
}
fn emit_middleware(spec: &HarnessSpec, name: &str, is_typescript: bool) -> HarnessSource {
let (preamble, entry_subpath) = nyx_js_preamble(spec, is_typescript);
let handler = &spec.entry_name;
let body = format!(
r#"{preamble}
// Phase 21 (Track M.3) — middleware.
process.stdout.write('__NYX_MIDDLEWARE__: ' + {name:?} + '\n');
const _h = _nyxResolve({handler:?});
if (_h == null) {{
process.stderr.write('NYX_HANDLER_NOT_FOUND: ' + {handler:?} + '\n');
process.exit(78);
}}
const _req = {{ body: payload, query: {{ q: payload }}, params: {{ id: payload }}, headers: {{}}, method: 'POST', url: '/nyx' }};
const _res = {{ statusCode: 200, headers: {{}}, end: function(d){{ if (d != null) process.stdout.write(String(d) + '\n'); }}, setHeader: function(k, v){{ this.headers[k] = v; }} }};
(async () => {{
try {{
const _result = await Promise.resolve(_h(_req, _res, function(_e){{ if (_e) process.stderr.write('NYX_NEXT_ERR: ' + _e + '\n'); }}));
if (_result != null) process.stdout.write(String(_result) + '\n');
}} catch (e) {{
process.stderr.write('NYX_EXCEPTION: ' + (e.constructor ? e.constructor.name : 'Error') + ': ' + e.message + '\n');
}}
}})();
"#,
preamble = preamble,
handler = handler,
name = name,
);
HarnessSource {
source: body,
filename: "harness.js".to_owned(),
command: vec!["node".to_owned(), "harness.js".to_owned()],
extra_files: Vec::new(),
entry_subpath: Some(entry_subpath),
}
}
fn emit_migration(spec: &HarnessSpec, version: Option<&str>, is_typescript: bool) -> HarnessSource {
let (preamble, entry_subpath) = nyx_js_preamble(spec, is_typescript);
let handler = &spec.entry_name;
let version_repr = version.unwrap_or("<no-version>");
let body = format!(
r#"{preamble}
// Phase 21 (Track M.3) — migration.
process.stdout.write('__NYX_MIGRATION__: ' + {version:?} + '\n');
const _h = _nyxResolve({handler:?});
if (_h == null) {{
process.stderr.write('NYX_HANDLER_NOT_FOUND: ' + {handler:?} + '\n');
process.exit(78);
}}
// Synthetic queryInterface for sequelize-style up/down(queryInterface, Sequelize).
const _qi = {{
createTable: async function(){{}},
addColumn: async function(){{}},
dropTable: async function(){{}},
removeColumn: async function(){{}},
bulkInsert: async function(){{}},
sequelize: {{ query: async function(){{}} }},
}};
const _prisma = {{
$executeRaw: async function(){{}},
$executeRawUnsafe: async function(s){{ if (s) process.stdout.write('NYX_PRISMA_SQL: ' + s + '\n'); }},
$queryRaw: async function(){{}},
$queryRawUnsafe: async function(){{}},
}};
(async () => {{
try {{
let _result;
// Try the sequelize shape first (queryInterface, Sequelize).
try {{
_result = await Promise.resolve(_h(_qi, {{}}));
}} catch (e1) {{
// Prisma / raw migration shape — pass payload.
try {{
_result = await Promise.resolve(_h(payload));
}} catch (e2) {{
_result = await Promise.resolve(_h());
}}
}}
if (_result != null) process.stdout.write(String(_result) + '\n');
}} catch (e) {{
process.stderr.write('NYX_EXCEPTION: ' + (e.constructor ? e.constructor.name : 'Error') + ': ' + e.message + '\n');
}}
}})();
"#,
preamble = preamble,
handler = handler,
version = version_repr,
);
HarnessSource {
source: body,
filename: "harness.js".to_owned(),
command: vec!["node".to_owned(), "harness.js".to_owned()],
extra_files: Vec::new(),
entry_subpath: Some(entry_subpath),
}
}
/// Phase 04 — Track J.2 SSTI harness for Node (Handlebars).
///
/// Reads `NYX_PAYLOAD`, simulates Handlebars's `{{helper a b}}`
@ -1827,7 +2110,7 @@ fn resolve_http_payload(slot: &PayloadSlot) -> (&'static str, String, &'static s
}
}
/// Supported entry kinds for both JS + TS after Phase 13.
/// Supported entry kinds for both JS + TS after Phase 21.
pub const SUPPORTED: &[EntryKindTag] = &[
EntryKindTag::Function,
EntryKindTag::HttpRoute,
@ -1835,6 +2118,11 @@ pub const SUPPORTED: &[EntryKindTag] = &[
EntryKindTag::LibraryApi,
EntryKindTag::ClassMethod,
EntryKindTag::MessageHandler,
EntryKindTag::ScheduledJob,
EntryKindTag::GraphQLResolver,
EntryKindTag::WebSocket,
EntryKindTag::Middleware,
EntryKindTag::Migration,
];
#[cfg(test)]

View file

@ -394,16 +394,65 @@ mod tests {
assert_eq!(EntryKind::Unknown.tag(), T::Unknown);
}
/// Phase 18 (Track M.0) baseline — the variants not yet wired by a
/// follow-up phase still route through the supported-set gate so the
/// verifier produces a structured `Inconclusive(EntryKindUnsupported)`
/// rather than degrading silently. Phase 19 lands `ClassMethod`;
/// Phase 20 lands `MessageHandler` on five langs (Python, Java,
/// JavaScript, TypeScript, Go); the rest stay unsupported.
/// Phase 21 (Track M.3) — the five remaining `EntryKind` variants
/// (`ScheduledJob` / `GraphQLResolver` / `WebSocket` / `Middleware`
/// / `Migration`) are now wired on the per-lang emitters the brief
/// targets. This regression guard pins the per-lang advertisement
/// matrix. Languages outside each variant's lang-set still route
/// through the supported-set gate so the verifier emits
/// `Inconclusive(EntryKindUnsupported)` rather than degrading
/// silently.
#[test]
fn entry_kind_phase_21_variants_are_unsupported_everywhere() {
fn entry_kind_phase_21_variants_advertised_per_brief() {
use crate::evidence::EntryKindTag as T;
let still_unsupported = [
let want = |lang: Lang, tag: T| -> bool {
match (lang, tag) {
// ScheduledJob: cron (JS), quartz (Java), celery (Python),
// sidekiq (Ruby). TypeScript shares the JS emitter so it
// inherits the variant through the shared SUPPORTED slice.
(
Lang::Python | Lang::JavaScript | Lang::TypeScript | Lang::Java | Lang::Ruby,
T::ScheduledJob,
) => true,
// GraphQLResolver: apollo + relay (JS), graphene (Python),
// juniper (Rust), gqlgen (Go). TypeScript shares the JS
// emitter so it inherits resolver dispatch.
(
Lang::Python
| Lang::JavaScript
| Lang::TypeScript
| Lang::Rust
| Lang::Go,
T::GraphQLResolver,
) => true,
// WebSocket: socketio + channels (Python), ws (JS),
// actioncable (Ruby).
(Lang::Python | Lang::JavaScript | Lang::TypeScript | Lang::Ruby, T::WebSocket) => true,
// Middleware: express (JS), django (Python), rails (Ruby),
// spring (Java), laravel (PHP).
(
Lang::Python
| Lang::JavaScript
| Lang::TypeScript
| Lang::Java
| Lang::Ruby
| Lang::Php,
T::Middleware,
) => true,
// Migration: rails (Ruby), django + flask (Python),
// laravel (PHP), sequelize + prisma (JS).
(
Lang::Python
| Lang::JavaScript
| Lang::TypeScript
| Lang::Ruby
| Lang::Php,
T::Migration,
) => true,
_ => false,
}
};
let phase_21_tags = [
T::ScheduledJob,
T::GraphQLResolver,
T::WebSocket,
@ -423,16 +472,20 @@ mod tests {
Lang::Cpp,
] {
let supported = entry_kinds_supported(lang);
for tag in still_unsupported {
assert!(
!supported.contains(&tag),
"{lang:?} prematurely advertised {tag:?} — Phase 21 has not landed the per-lang adapters for this variant"
);
let hint = entry_kind_hint(lang, tag);
assert!(
hint.contains(tag.as_str()),
"{lang:?} hint must mention {tag:?}, got: {hint:?}"
for tag in phase_21_tags {
let expected = want(lang, tag);
let actual = supported.contains(&tag);
assert_eq!(
actual, expected,
"{lang:?} expected supported={expected:?} for {tag:?}; got supported={actual:?}",
);
if !actual {
let hint = entry_kind_hint(lang, tag);
assert!(
hint.contains(tag.as_str()),
"{lang:?} hint for unsupported {tag:?} must mention the attempted tag, got: {hint:?}"
);
}
}
}
}

View file

@ -48,6 +48,8 @@ const SUPPORTED: &[EntryKindTag] = &[
EntryKindTag::HttpRoute,
EntryKindTag::CliSubcommand,
EntryKindTag::ClassMethod,
EntryKindTag::Middleware,
EntryKindTag::Migration,
];
impl LangEmitter for PhpEmitter {
@ -495,6 +497,16 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
return Ok(emit_class_method_harness(class, method));
}
// Phase 21 (Track M.3): Middleware short-circuit (Laravel handle()).
if let crate::evidence::EntryKind::Middleware { name } = &spec.entry_kind {
return Ok(emit_middleware_harness(&spec.entry_name, name));
}
// Phase 21 (Track M.3): Migration short-circuit (Laravel up()).
if let crate::evidence::EntryKind::Migration { version } = &spec.entry_kind {
return Ok(emit_migration_harness(&spec.entry_name, version.as_deref()));
}
let entry_source = read_entry_source(&spec.entry_file);
let shape = PhpShape::detect(spec, &entry_source);
let source = generate_source(spec, shape);
@ -1243,6 +1255,131 @@ try {{
}
}
// ── Phase 21 (Track M.3) — synthetic entry-kind harnesses ─────────────────────
fn nyx_php_preamble() -> String {
let shim = probe_shim();
format!(
r#"<?php
// Nyx dynamic harness — Phase 21 / Track M.3 (auto-generated).
{shim}
$payload = (string) (getenv('NYX_PAYLOAD') ?: '');
$_b64 = getenv('NYX_PAYLOAD_B64');
if ((!$payload || $payload === '') && is_string($_b64) && $_b64 !== '') {{
$decoded = base64_decode($_b64, true);
if ($decoded !== false) $payload = $decoded;
}}
try {{
require_once __DIR__ . '/entry.php';
}} catch (Throwable $e) {{
fwrite(STDERR, 'NYX_IMPORT_ERROR: ' . $e->getMessage() . "\n");
exit(77);
}}
echo "__NYX_SINK_HIT__\n";
"#,
shim = shim,
)
}
fn emit_middleware_harness(handler: &str, name: &str) -> HarnessSource {
let preamble = nyx_php_preamble();
let body = format!(
r#"{preamble}
echo "__NYX_MIDDLEWARE__: " . {name:?} . "\n";
$req = new stdClass();
$req->body = $payload;
$req->path = '/nyx';
$req->method = 'POST';
$req->query = [ 'q' => $payload ];
$next = function ($r) {{ return $r; }};
if (class_exists({handler:?})) {{
$inst = new {handler}();
if (method_exists($inst, 'handle')) {{
try {{
$result = $inst->handle($req, $next);
if ($result !== null) echo (string)$result . "\n";
}} catch (Throwable $e) {{
fwrite(STDERR, 'NYX_EXCEPTION: ' . get_class($e) . ': ' . $e->getMessage() . "\n");
}}
}} else {{
fwrite(STDERR, 'NYX_METHOD_NOT_FOUND: handle' . "\n");
exit(78);
}}
}} elseif (function_exists({handler:?})) {{
try {{
$result = call_user_func({handler:?}, $req, $next);
if ($result !== null) echo (string)$result . "\n";
}} catch (Throwable $e) {{
fwrite(STDERR, 'NYX_EXCEPTION: ' . get_class($e) . ': ' . $e->getMessage() . "\n");
}}
}} else {{
fwrite(STDERR, 'NYX_HANDLER_NOT_FOUND: ' . {handler:?} . "\n");
exit(78);
}}
"#,
preamble = preamble,
handler = handler,
name = name,
);
HarnessSource {
source: body,
filename: "harness.php".to_owned(),
command: vec!["php".to_owned(), "harness.php".to_owned()],
extra_files: vec![],
entry_subpath: Some("entry.php".to_owned()),
}
}
fn emit_migration_harness(handler: &str, version: Option<&str>) -> HarnessSource {
let preamble = nyx_php_preamble();
let version_repr = version.unwrap_or("<no-version>");
let body = format!(
r#"{preamble}
echo "__NYX_MIGRATION__: " . {version:?} . "\n";
if (class_exists({handler:?})) {{
$inst = new {handler}();
if (method_exists($inst, 'up')) {{
try {{
$result = $inst->up();
if ($result !== null) echo (string)$result . "\n";
}} catch (Throwable $e) {{
fwrite(STDERR, 'NYX_EXCEPTION: ' . get_class($e) . ': ' . $e->getMessage() . "\n");
}}
}} else {{
fwrite(STDERR, 'NYX_METHOD_NOT_FOUND: up' . "\n");
exit(78);
}}
}} elseif (function_exists({handler:?})) {{
try {{
$result = call_user_func({handler:?});
if ($result !== null) echo (string)$result . "\n";
}} catch (Throwable $e) {{
fwrite(STDERR, 'NYX_EXCEPTION: ' . get_class($e) . ': ' . $e->getMessage() . "\n");
}}
}} else {{
fwrite(STDERR, 'NYX_HANDLER_NOT_FOUND: ' . {handler:?} . "\n");
exit(78);
}}
"#,
preamble = preamble,
handler = handler,
version = version_repr,
);
HarnessSource {
source: body,
filename: "harness.php".to_owned(),
command: vec!["php".to_owned(), "harness.php".to_owned()],
extra_files: vec![],
entry_subpath: Some("entry.php".to_owned()),
}
}
fn build_call_expr(spec: &HarnessSpec, shape: PhpShape, func: &str) -> String {
match shape {
PhpShape::TopLevelScript => "null".to_owned(),

View file

@ -47,6 +47,11 @@ const SUPPORTED: &[EntryKindTag] = &[
EntryKindTag::CliSubcommand,
EntryKindTag::ClassMethod,
EntryKindTag::MessageHandler,
EntryKindTag::ScheduledJob,
EntryKindTag::GraphQLResolver,
EntryKindTag::WebSocket,
EntryKindTag::Middleware,
EntryKindTag::Migration,
];
impl LangEmitter for PythonEmitter {
@ -704,6 +709,41 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
return Ok(emit_message_handler(spec, queue));
}
// Phase 21 (Track M.3): ScheduledJob short-circuit. Synthetic
// harness — imports the entry module, invokes the named handler
// with the payload as the single positional argument (matching
// Celery's `task(arg)` shape), then prints the sink-hit sentinel.
if let crate::evidence::EntryKind::ScheduledJob { schedule } = &spec.entry_kind {
return Ok(emit_scheduled_job(spec, schedule.as_deref()));
}
// Phase 21 (Track M.3): GraphQLResolver short-circuit. Synthetic
// resolver dispatch — `resolve_<field>(self, info, payload)`.
if let crate::evidence::EntryKind::GraphQLResolver { type_name, field } = &spec.entry_kind {
return Ok(emit_graphql_resolver(spec, type_name, field));
}
// Phase 21 (Track M.3): WebSocket short-circuit. Invokes the
// handler with `(self, payload)` shape that python-socketio /
// Django Channels both accept.
if let crate::evidence::EntryKind::WebSocket { path } = &spec.entry_kind {
return Ok(emit_websocket_handler(spec, path));
}
// Phase 21 (Track M.3): Middleware short-circuit. Builds a
// synthetic `request` object whose body field carries the payload
// and invokes the middleware with `(request, lambda r: r)` next.
if let crate::evidence::EntryKind::Middleware { name } = &spec.entry_kind {
return Ok(emit_middleware(spec, name));
}
// Phase 21 (Track M.3): Migration short-circuit. Invokes the
// module-level `upgrade()` / `up()` function (no args) so the
// migration's SQL / DDL emitter runs.
if let crate::evidence::EntryKind::Migration { version } = &spec.entry_kind {
return Ok(emit_migration(spec, version.as_deref()));
}
let entry_source = read_entry_source(&spec.entry_file);
let shape = PythonShape::detect(spec, &entry_source);
let body = generate_for_shape(spec, shape);
@ -934,6 +974,257 @@ except Exception as _e:
}
}
// ── Phase 21 (Track M.3) — synthetic entry-kind harnesses ─────────────────────
/// Phase 21: ScheduledJob harness. Imports the entry module, locates
/// the named function, invokes it with the payload string as the
/// single positional argument, and prints the sink-hit sentinel.
fn emit_scheduled_job(spec: &HarnessSpec, schedule: Option<&str>) -> HarnessSource {
let preamble = harness_preamble(spec);
let postamble = harness_postamble();
let handler = &spec.entry_name;
let schedule_repr = schedule.unwrap_or("<unscheduled>");
let body = format!(
r#"# Shape: scheduled job — Phase 21 / Track M.3.
print("__NYX_SCHEDULED_JOB__: " + {schedule:?}, flush=True)
_h = getattr(_entry_mod, {handler:?}, None)
if _h is None:
print("NYX_HANDLER_NOT_FOUND: " + {handler:?}, file=sys.stderr, flush=True)
sys.exit(78)
try:
_result = _h(payload)
if _result is not None:
try:
print(str(_result), flush=True)
except Exception:
pass
except SystemExit as _e:
sys.exit(_e.code)
except Exception as _e:
print(f"NYX_EXCEPTION: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True)
"#,
handler = handler,
schedule = schedule_repr,
);
HarnessSource {
source: format!("{preamble}\n{body}\n{postamble}"),
filename: "harness.py".to_owned(),
command: vec!["python3".to_owned(), "harness.py".to_owned()],
extra_files: vec![],
entry_subpath: None,
}
}
/// Phase 21: GraphQLResolver harness. Imports the entry module,
/// locates the named resolver function, builds a synthetic `info`
/// context object, and invokes the resolver with `(info, payload)`.
fn emit_graphql_resolver(spec: &HarnessSpec, type_name: &str, field: &str) -> HarnessSource {
let preamble = harness_preamble(spec);
let postamble = harness_postamble();
let handler = &spec.entry_name;
let body = format!(
r#"# Shape: GraphQL resolver — Phase 21 / Track M.3.
print("__NYX_GRAPHQL_RESOLVER__: " + {type_name:?} + "." + {field:?}, flush=True)
class _NyxGraphQLInfo:
"""Synthetic resolver context — apollo-style {{ context, fieldName }}."""
def __init__(self, field_name):
self.field_name = field_name
self.context = {{}}
_resolver = getattr(_entry_mod, {handler:?}, None)
if _resolver is None:
print("NYX_RESOLVER_NOT_FOUND: " + {handler:?}, file=sys.stderr, flush=True)
sys.exit(78)
try:
# Graphene resolvers are `resolve_field(self, info, **args)`; we
# synthesise `self = None`, `info = _NyxGraphQLInfo`, and pass the
# payload positionally so a `def resolve_foo(self, info, id):` shape
# binds `id = payload`.
_result = _resolver(None, _NyxGraphQLInfo({field:?}), payload)
if _result is not None:
try:
print(str(_result), flush=True)
except Exception:
pass
except SystemExit as _e:
sys.exit(_e.code)
except TypeError:
# Fallback for free-function resolvers without the `self` formal.
try:
_result = _resolver(_NyxGraphQLInfo({field:?}), payload)
if _result is not None:
print(str(_result), flush=True)
except Exception as _e:
print(f"NYX_EXCEPTION: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True)
except Exception as _e:
print(f"NYX_EXCEPTION: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True)
"#,
type_name = type_name,
field = field,
handler = handler,
);
HarnessSource {
source: format!("{preamble}\n{body}\n{postamble}"),
filename: "harness.py".to_owned(),
command: vec!["python3".to_owned(), "harness.py".to_owned()],
extra_files: vec![],
entry_subpath: None,
}
}
/// Phase 21: WebSocket handler harness. Imports the entry module,
/// locates the handler (`receive` / `on_<event>` / free function),
/// and invokes it with the payload as the single message frame.
fn emit_websocket_handler(spec: &HarnessSpec, path: &str) -> HarnessSource {
let preamble = harness_preamble(spec);
let postamble = harness_postamble();
let handler = &spec.entry_name;
let body = format!(
r#"# Shape: WebSocket handler — Phase 21 / Track M.3.
print("__NYX_WEBSOCKET__: " + {path:?}, flush=True)
_h = getattr(_entry_mod, {handler:?}, None)
if _h is None:
print("NYX_HANDLER_NOT_FOUND: " + {handler:?}, file=sys.stderr, flush=True)
sys.exit(78)
try:
# python-socketio handlers are `def message(sid, data)`; Channels
# consumers are `def receive(self, text_data=None, bytes_data=None)`.
# Try (sid, payload) first, then fall back to (payload).
try:
_result = _h("nyx-sid", payload)
except TypeError:
try:
_result = _h(payload)
except TypeError:
_result = _h(None, payload)
if _result is not None:
try:
print(str(_result), flush=True)
except Exception:
pass
except SystemExit as _e:
sys.exit(_e.code)
except Exception as _e:
print(f"NYX_EXCEPTION: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True)
"#,
path = path,
handler = handler,
);
HarnessSource {
source: format!("{preamble}\n{body}\n{postamble}"),
filename: "harness.py".to_owned(),
command: vec!["python3".to_owned(), "harness.py".to_owned()],
extra_files: vec![],
entry_subpath: None,
}
}
/// Phase 21: Middleware harness. Builds a synthetic request object
/// whose body carries the payload, invokes the middleware with a
/// pass-through `next` callable.
fn emit_middleware(spec: &HarnessSpec, name: &str) -> HarnessSource {
let preamble = harness_preamble(spec);
let postamble = harness_postamble();
let handler = &spec.entry_name;
let body = format!(
r#"# Shape: middleware — Phase 21 / Track M.3.
print("__NYX_MIDDLEWARE__: " + {name:?}, flush=True)
class _NyxRequest:
"""Synthetic Django / Flask-ish request carrying the payload."""
def __init__(self, body):
self.body = body
self.path = "/nyx"
self.method = "POST"
self.META = {{}}
self.GET = {{"q": body}}
self.POST = {{"q": body}}
_h = getattr(_entry_mod, {handler:?}, None)
if _h is None:
print("NYX_HANDLER_NOT_FOUND: " + {handler:?}, file=sys.stderr, flush=True)
sys.exit(78)
try:
_req = _NyxRequest(payload)
# Try class-shaped middleware (instantiate with a get_response stub).
try:
_mw = _h(lambda r: r)
_result = _mw(_req)
except TypeError:
# Method on an existing class instance.
_result = _h(_req)
if _result is not None:
try:
print(str(_result), flush=True)
except Exception:
pass
except SystemExit as _e:
sys.exit(_e.code)
except Exception as _e:
print(f"NYX_EXCEPTION: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True)
"#,
name = name,
handler = handler,
);
HarnessSource {
source: format!("{preamble}\n{body}\n{postamble}"),
filename: "harness.py".to_owned(),
command: vec!["python3".to_owned(), "harness.py".to_owned()],
extra_files: vec![],
entry_subpath: None,
}
}
/// Phase 21: Migration harness. Invokes the module-level `upgrade()`
/// / `up()` function and prints the version sentinel.
fn emit_migration(spec: &HarnessSpec, version: Option<&str>) -> HarnessSource {
let preamble = harness_preamble(spec);
let postamble = harness_postamble();
let handler = &spec.entry_name;
let version_repr = version.unwrap_or("<no-version>");
let body = format!(
r#"# Shape: migration — Phase 21 / Track M.3.
print("__NYX_MIGRATION__: " + {version:?}, flush=True)
_h = getattr(_entry_mod, {handler:?}, None)
if _h is None:
print("NYX_HANDLER_NOT_FOUND: " + {handler:?}, file=sys.stderr, flush=True)
sys.exit(78)
try:
# Migrations conventionally take no arguments; pass payload if the
# function declares positional params (best-effort introspection).
import inspect
sig = None
try:
sig = inspect.signature(_h)
except (TypeError, ValueError):
sig = None
if sig is not None and len(sig.parameters) >= 1:
_result = _h(payload)
else:
_result = _h()
if _result is not None:
try:
print(str(_result), flush=True)
except Exception:
pass
except SystemExit as _e:
sys.exit(_e.code)
except Exception as _e:
print(f"NYX_EXCEPTION: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True)
"#,
version = version_repr,
handler = handler,
);
HarnessSource {
source: format!("{preamble}\n{body}\n{postamble}"),
filename: "harness.py".to_owned(),
command: vec!["python3".to_owned(), "harness.py".to_owned()],
extra_files: vec![],
entry_subpath: None,
}
}
#[derive(Debug, Clone, Copy)]
enum PythonBroker {
Kafka,

View file

@ -45,6 +45,10 @@ const SUPPORTED: &[EntryKindTag] = &[
EntryKindTag::HttpRoute,
EntryKindTag::CliSubcommand,
EntryKindTag::ClassMethod,
EntryKindTag::ScheduledJob,
EntryKindTag::WebSocket,
EntryKindTag::Middleware,
EntryKindTag::Migration,
];
impl LangEmitter for RubyEmitter {
@ -437,6 +441,26 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
return Ok(emit_class_method_harness(class, method));
}
// Phase 21 (Track M.3): ScheduledJob short-circuit (Sidekiq workers).
if let crate::evidence::EntryKind::ScheduledJob { schedule } = &spec.entry_kind {
return Ok(emit_scheduled_job_harness(&spec.entry_name, schedule.as_deref()));
}
// Phase 21 (Track M.3): WebSocket short-circuit (ActionCable channels).
if let crate::evidence::EntryKind::WebSocket { path } = &spec.entry_kind {
return Ok(emit_websocket_handler_harness(&spec.entry_name, path));
}
// Phase 21 (Track M.3): Middleware short-circuit (Rack-shape).
if let crate::evidence::EntryKind::Middleware { name } = &spec.entry_kind {
return Ok(emit_middleware_harness(&spec.entry_name, name));
}
// Phase 21 (Track M.3): Migration short-circuit (ActiveRecord up/down).
if let crate::evidence::EntryKind::Migration { version } = &spec.entry_kind {
return Ok(emit_migration_harness(&spec.entry_name, version.as_deref()));
}
let entry_source = read_entry_source(&spec.entry_file);
let shape = RubyShape::detect(spec, &entry_source);
let source = generate_source(spec, shape);
@ -554,6 +578,253 @@ end
}
}
// ── Phase 21 (Track M.3) — synthetic entry-kind harnesses ─────────────────────
fn nyx_ruby_preamble() -> String {
let shim = probe_shim();
format!(
r#"# Nyx dynamic harness — Phase 21 / Track M.3 (auto-generated).
{shim}
def nyx_payload
v = ENV['NYX_PAYLOAD']
return v if v && !v.empty?
b64 = ENV['NYX_PAYLOAD_B64']
if b64 && !b64.empty?
begin
require 'base64'
return Base64.decode64(b64)
rescue StandardError
return ''
end
end
''
end
$nyx_payload = nyx_payload
begin
require_relative './entry'
rescue LoadError, ScriptError => e
STDERR.puts("NYX_IMPORT_ERROR: #{{e.message}}")
exit 77
end
puts "__NYX_SINK_HIT__"
"#,
shim = shim,
)
}
fn emit_scheduled_job_harness(handler: &str, schedule: Option<&str>) -> HarnessSource {
let preamble = nyx_ruby_preamble();
let sched = schedule.unwrap_or("<unscheduled>");
let body = format!(
r#"{preamble}
puts "__NYX_SCHEDULED_JOB__: " + {sched:?}
# Sidekiq workers expose perform(*args) on a class. Try looking up the
# named class first; fall back to a top-level function.
target = nil
if Object.const_defined?({handler:?})
begin
target = Object.const_get({handler:?}).new
if target.respond_to?(:perform)
begin
result = target.perform($nyx_payload)
print(result.to_s) if result
rescue StandardError => e
STDERR.puts("NYX_EXCEPTION: #{{e.class.name}}: #{{e.message}}")
end
exit 0
end
rescue StandardError
end
end
if respond_to?({handler:?}.to_sym, true)
begin
result = send({handler:?}.to_sym, $nyx_payload)
print(result.to_s) if result
rescue StandardError => e
STDERR.puts("NYX_EXCEPTION: #{{e.class.name}}: #{{e.message}}")
end
else
STDERR.puts("NYX_HANDLER_NOT_FOUND: " + {handler:?})
exit 78
end
"#,
preamble = preamble,
handler = handler,
sched = sched,
);
HarnessSource {
source: body,
filename: "harness.rb".to_owned(),
command: vec!["ruby".to_owned(), "harness.rb".to_owned()],
extra_files: vec![],
entry_subpath: Some("entry.rb".to_owned()),
}
}
fn emit_websocket_handler_harness(handler: &str, path: &str) -> HarnessSource {
let preamble = nyx_ruby_preamble();
let body = format!(
r#"{preamble}
puts "__NYX_WEBSOCKET__: " + {path:?}
# ActionCable channels expose `receive(data)` on a subclass. Find the
# enclosing class via const lookup; fall back to top-level send.
if Object.const_defined?({handler:?})
cls = Object.const_get({handler:?})
begin
inst = cls.new rescue (cls.allocate rescue nil)
if inst && inst.respond_to?(:receive)
begin
result = inst.receive($nyx_payload)
print(result.to_s) if result
rescue StandardError => e
STDERR.puts("NYX_EXCEPTION: #{{e.class.name}}: #{{e.message}}")
end
exit 0
end
rescue StandardError
end
end
if respond_to?({handler:?}.to_sym, true)
begin
result = send({handler:?}.to_sym, $nyx_payload)
print(result.to_s) if result
rescue StandardError => e
STDERR.puts("NYX_EXCEPTION: #{{e.class.name}}: #{{e.message}}")
end
else
STDERR.puts("NYX_HANDLER_NOT_FOUND: " + {handler:?})
exit 78
end
"#,
preamble = preamble,
handler = handler,
path = path,
);
HarnessSource {
source: body,
filename: "harness.rb".to_owned(),
command: vec!["ruby".to_owned(), "harness.rb".to_owned()],
extra_files: vec![],
entry_subpath: Some("entry.rb".to_owned()),
}
}
fn emit_middleware_harness(handler: &str, name: &str) -> HarnessSource {
let preamble = nyx_ruby_preamble();
let body = format!(
r#"{preamble}
puts "__NYX_MIDDLEWARE__: " + {name:?}
# Rack-shape middleware: class with #call(env).
env = {{
'REQUEST_METHOD' => 'POST',
'PATH_INFO' => '/nyx',
'QUERY_STRING' => "q=#{{$nyx_payload}}",
'rack.input' => StringIO.new($nyx_payload),
'nyx.payload' => $nyx_payload,
}}
require 'stringio'
if Object.const_defined?({handler:?})
cls = Object.const_get({handler:?})
begin
inst = cls.new(lambda {{ |e| [200, {{}}, ['ok']] }})
if inst.respond_to?(:call)
result = inst.call(env)
print(result.to_s) if result
exit 0
end
rescue StandardError => e
STDERR.puts("NYX_EXCEPTION: #{{e.class.name}}: #{{e.message}}")
end
end
if respond_to?({handler:?}.to_sym, true)
begin
result = send({handler:?}.to_sym, env)
print(result.to_s) if result
rescue StandardError => e
STDERR.puts("NYX_EXCEPTION: #{{e.class.name}}: #{{e.message}}")
end
else
STDERR.puts("NYX_HANDLER_NOT_FOUND: " + {handler:?})
exit 78
end
"#,
preamble = preamble,
handler = handler,
name = name,
);
HarnessSource {
source: body,
filename: "harness.rb".to_owned(),
command: vec!["ruby".to_owned(), "harness.rb".to_owned()],
extra_files: vec![],
entry_subpath: Some("entry.rb".to_owned()),
}
}
fn emit_migration_harness(handler: &str, version: Option<&str>) -> HarnessSource {
let preamble = nyx_ruby_preamble();
let ver = version.unwrap_or("<no-version>");
let body = format!(
r#"{preamble}
puts "__NYX_MIGRATION__: " + {ver:?}
# ActiveRecord migrations expose `up` / `down` / `change` on a subclass.
if Object.const_defined?({handler:?})
cls = Object.const_get({handler:?})
begin
inst = cls.new
%i[up change down].each do |m|
if inst.respond_to?(m)
begin
result = inst.send(m)
print(result.to_s) if result
rescue StandardError => e
STDERR.puts("NYX_EXCEPTION: #{{e.class.name}}: #{{e.message}}")
end
exit 0
end
end
rescue StandardError => e
STDERR.puts("NYX_EXCEPTION: #{{e.class.name}}: #{{e.message}}")
end
end
if respond_to?({handler:?}.to_sym, true)
begin
result = send({handler:?}.to_sym)
print(result.to_s) if result
rescue StandardError => e
STDERR.puts("NYX_EXCEPTION: #{{e.class.name}}: #{{e.message}}")
end
else
STDERR.puts("NYX_HANDLER_NOT_FOUND: " + {handler:?})
exit 78
end
"#,
preamble = preamble,
handler = handler,
ver = ver,
);
HarnessSource {
source: body,
filename: "harness.rb".to_owned(),
command: vec!["ruby".to_owned(), "harness.rb".to_owned()],
extra_files: vec![],
entry_subpath: Some("entry.rb".to_owned()),
}
}
/// Phase 03 — Track J.1 deserialize harness for Ruby.
///
/// Wraps a call to `Marshal.load(input)` with a const-lookup

View file

@ -44,6 +44,7 @@ const SUPPORTED: &[EntryKindTag] = &[
EntryKindTag::CliSubcommand,
EntryKindTag::LibraryApi,
EntryKindTag::ClassMethod,
EntryKindTag::GraphQLResolver,
];
impl LangEmitter for RustEmitter {
@ -829,6 +830,13 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
return Ok(emit_class_method_harness(spec, class, method));
}
// Phase 21 (Track M.3): GraphQLResolver short-circuit (Juniper).
// Emits a `src/main.rs` that invokes `entry::<handler>(payload)`
// directly — Juniper resolvers are plain async fns in the source.
if let crate::evidence::EntryKind::GraphQLResolver { type_name, field } = &spec.entry_kind {
return Ok(emit_graphql_resolver_harness(spec, type_name, field));
}
let shape = detect_shape(spec);
// Generic + LibfuzzerTarget accept Param(0)/EnvVar; richer shapes
@ -948,6 +956,92 @@ fn b64_decode(input: &[u8]) -> Option<Vec<u8>> {{
}
}
// ── Phase 21 (Track M.3) — synthetic entry-kind harnesses ─────────────────────
/// Phase 21 (Track M.3) — GraphQL resolver harness for Rust (Juniper).
///
/// Emits a `src/main.rs` that invokes `entry::<handler>(&payload)` —
/// the harness assumes the entry module exposes a free function with
/// the resolver name; Juniper's `#[graphql_object]` impl methods are
/// not directly reachable through `mod entry`, so the v1 path goes
/// through a thin re-export the entry file is expected to publish.
fn emit_graphql_resolver_harness(
spec: &HarnessSpec,
type_name: &str,
field: &str,
) -> HarnessSource {
let shim = probe_shim();
let cargo_toml = generate_cargo_toml(spec.expected_cap);
let handler = &spec.entry_name;
let label = format!("{type_name}.{field}");
let body = format!(
r#"//! Nyx dynamic harness — GraphQL resolver (Phase 21 / Track M.3).
mod entry;
{shim}
fn main() {{
let payload = nyx_payload();
__nyx_install_crash_guard("{label}");
println!("__NYX_GRAPHQL_RESOLVER__: {type_name}.{field}");
println!("__NYX_SINK_HIT__");
let _ = entry::{handler}(&payload);
}}
fn nyx_payload() -> String {{
if let Ok(v) = std::env::var("NYX_PAYLOAD") {{
if !v.is_empty() {{
return v;
}}
}}
if let Ok(b64) = std::env::var("NYX_PAYLOAD_B64") {{
if let Some(bytes) = b64_decode(b64.as_bytes()) {{
return String::from_utf8_lossy(&bytes).into_owned();
}}
}}
String::new()
}}
fn b64_decode(input: &[u8]) -> Option<Vec<u8>> {{
const TABLE: [u8; 128] = {{
let mut t = [255u8; 128];
let alphabet: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut i = 0usize;
while i < alphabet.len() {{
t[alphabet[i] as usize] = i as u8;
i += 1;
}}
t
}};
let input: Vec<u8> = input.iter().copied().filter(|&c| c != b'\n' && c != b'\r').collect();
let mut out = Vec::with_capacity(input.len() * 3 / 4);
let mut i = 0;
while i + 3 < input.len() {{
let a = *TABLE.get(input[i] as usize)? as u32;
let b = *TABLE.get(input[i + 1] as usize)? as u32;
let c = if input[i + 2] == b'=' {{ 64 }} else {{ *TABLE.get(input[i + 2] as usize)? as u32 }};
let d = if input[i + 3] == b'=' {{ 64 }} else {{ *TABLE.get(input[i + 3] as usize)? as u32 }};
if a == 255 || b == 255 || c == 255 || d == 255 {{ return None; }}
out.push(((a << 2) | (b >> 4)) as u8);
if input[i + 2] != b'=' {{ out.push(((b << 4) | (c >> 2)) as u8); }}
if input[i + 3] != b'=' {{ out.push(((c << 6) | d) as u8); }}
i += 4;
}}
Some(out)
}}
"#,
handler = handler,
type_name = type_name,
field = field,
label = label,
);
HarnessSource {
source: body,
filename: "src/main.rs".into(),
command: vec!["target/release/nyx_harness".into()],
extra_files: vec![("Cargo.toml".into(), cargo_toml)],
entry_subpath: Some("src/entry.rs".into()),
}
}
/// True when the entry source declares `class` as a type that derives
/// or implements `Default`. Two byte-level patterns are recognised:
///

View file

@ -0,0 +1,9 @@
// Phase 21 — Apollo resolver benign control.
const _NYX_ADAPTER_MARKER = "require('@apollo/server')";
function resolveUser(parent, args, ctx) {
const id = String(args.id || '').replace(/[^A-Za-z0-9_-]/g, '');
return { id, name: 'user-' + id };
}
module.exports = { resolveUser };

View file

@ -0,0 +1,14 @@
// Phase 21 (Track M.3) — Apollo GraphQL resolver vuln fixture.
//
// `resolveUser(parent, args)` is a resolver from an Apollo schema that
// splices `args.id` into a SQL query via raw string concatenation —
// classic GraphQL → SQLi shape.
const _NYX_ADAPTER_MARKER = "require('@apollo/server')";
function resolveUser(parent, args, ctx) {
// SINK: tainted args.id concatenated into SQL.
const query = "SELECT * FROM users WHERE id = '" + args.id + "'";
return { id: args.id, name: 'user-' + args.id, _query: query };
}
module.exports = { resolveUser };

View file

@ -0,0 +1,15 @@
// Phase 21 — gqlgen benign control.
package benign
// import "github.com/99designs/gqlgen/graphql"
import "regexp"
var idAllow = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
func ResolveUser(id string) (string, error) {
if !idAllow.MatchString(id) {
return "", nil
}
return "user-" + id, nil
}

View file

@ -0,0 +1,23 @@
// Phase 21 (Track M.3) — gqlgen GraphQL resolver vuln fixture.
//
// `resolveUser(ctx, id)` is a gqlgen resolver (substring marker only —
// the real gqlgen runtime is not on the workdir's go.mod). The
// resolver splices the id into a shell command via os/exec.
package vuln
// import "github.com/99designs/gqlgen/graphql"
import (
"os/exec"
)
// type queryResolver struct{}
func ResolveUser(id string) (string, error) {
// SINK: tainted id concatenated into shell command.
out, err := exec.Command("/bin/sh", "-c", "echo lookup-"+id).Output()
if err != nil {
return "", err
}
return string(out), nil
}

View file

@ -0,0 +1,9 @@
"""Phase 21 — Graphene resolver benign control."""
import re
_NYX_ADAPTER_MARKER = "import graphene"
def resolve_user(self, info, id):
safe = re.sub(r"[^A-Za-z0-9_-]", "", str(id))
return "user-" + safe

View file

@ -0,0 +1,15 @@
"""Phase 21 (Track M.3) — Graphene resolver vuln fixture.
`resolve_user(self, info, id)` is a Graphene query resolver that
splices the tainted `id` into a shell command via `os.system`.
"""
import os
_NYX_ADAPTER_MARKER = "import graphene"
_NYX_OBJECT_TYPE_MARKER = "class Query(graphene.ObjectType):"
def resolve_user(self, info, id):
# SINK: tainted id concatenated into shell command.
os.system("echo lookup-" + str(id))
return "user-" + str(id)

View file

@ -0,0 +1,10 @@
//! Phase 21 — Juniper resolver benign control.
// use juniper::graphql_object;
pub fn resolve_user(id: &str) -> String {
let safe: String = id
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '-')
.collect();
format!("user-{}", safe)
}

View file

@ -0,0 +1,15 @@
//! Phase 21 (Track M.3) — Juniper GraphQL resolver vuln fixture.
//!
//! `resolve_user(id)` is a Juniper resolver (substring marker only —
//! the real `juniper` crate is not on the workdir's Cargo.toml). The
//! resolver builds a SQL query via raw string concat — classic
//! GraphQL → SQLi shape.
// use juniper::graphql_object;
pub fn resolve_user(id: &str) -> String {
// SINK: tainted id concatenated into SQL.
let query = format!("SELECT * FROM users WHERE id = '{}'", id);
let _ = query;
format!("user-{}", id)
}

View file

@ -0,0 +1,9 @@
// Phase 21 — graphql-relay benign control.
const _NYX_ADAPTER_MARKER = "require('graphql-relay')";
function resolveNode(parent, args) {
const id = String(args.id || '').replace(/[^A-Za-z0-9_-]/g, '');
return { id };
}
module.exports = { resolveNode };

View file

@ -0,0 +1,10 @@
// Phase 21 (Track M.3) — graphql-relay vuln fixture.
const _NYX_ADAPTER_MARKER = "require('graphql-relay')";
function resolveNode(parent, args, ctx, info) {
// SINK: tainted globalId interpolated into SQL.
const sql = "SELECT * FROM nodes WHERE id = '" + args.id + "'";
return { id: args.id, _sql: sql };
}
module.exports = { resolveNode };

View file

@ -0,0 +1,18 @@
"""Phase 21 — Django middleware benign control."""
import os
import shlex
_NYX_ADAPTER_MARKER = "from django.utils.deprecation import MiddlewareMixin"
class AuditMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
os.system("echo " + shlex.quote(str(request.body)))
return self.get_response(request)
def audit(get_response):
return AuditMiddleware(get_response)

View file

@ -0,0 +1,23 @@
"""Phase 21 (Track M.3) — Django middleware vuln fixture.
`AuditMiddleware.__call__(request)` splices `request.body` into a shell
command via `os.system`.
"""
import os
_NYX_ADAPTER_MARKER = "from django.utils.deprecation import MiddlewareMixin"
class AuditMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# SINK: tainted request body concatenated into shell command.
os.system("echo " + str(request.body))
return self.get_response(request)
# Module-level alias for the harness to resolve `audit` directly.
def audit(get_response):
return AuditMiddleware(get_response)

View file

@ -0,0 +1,11 @@
// Phase 21 — Express middleware benign control.
const _NYX_ADAPTER_MARKER = "require('express')";
function audit(req, res, next) {
const body = String(req.body || '');
if (body.length > 1024) return res.end('too large');
if (typeof next === 'function') next();
return 'ok';
}
module.exports = { audit };

View file

@ -0,0 +1,17 @@
// Phase 21 (Track M.3) — Express middleware vuln fixture.
//
// `audit(req, res, next)` is mounted via `app.use(audit)`. It splices
// the request body into a shell command via `execSync`.
const _NYX_ADAPTER_MARKER = "require('express')";
const _NYX_REGISTER_MARKER = "app.use(audit)";
const { execSync } = require('child_process');
function audit(req, res, next) {
// SINK: tainted req.body concatenated into shell command.
const out = execSync('echo ' + String(req.body || '')).toString();
if (typeof next === 'function') next();
return out;
}
module.exports = { audit };

View file

@ -0,0 +1,11 @@
<?php
// Phase 21 — Laravel middleware benign control.
// use Illuminate\\Http\\Request;
class Audit {
public function handle($request, $next) {
$body = is_object($request) && isset($request->body) ? (string)$request->body : (string)$request;
shell_exec("echo " . escapeshellarg($body));
return $next($request);
}
}

View file

@ -0,0 +1,17 @@
<?php
// Phase 21 (Track M.3) — Laravel middleware vuln fixture.
//
// `Audit::handle($request, $next)` splices `$request->body` into a
// shell command via `shell_exec` — classic Laravel middleware cmdi.
// use Illuminate\\Http\\Request;
// function handle($request, Closure $next)
class Audit {
public function handle($request, $next) {
$body = is_object($request) && isset($request->body) ? (string)$request->body : (string)$request;
// SINK: tainted body concatenated into shell command.
shell_exec("echo " . $body);
return $next($request);
}
}

View file

@ -0,0 +1,14 @@
# Phase 21 — Rack middleware benign control.
require 'shellwords'
class AuditMiddleware
def initialize(app)
@app = app
end
def call(env)
payload = (env['nyx.payload'] || env['QUERY_STRING']).to_s
system("echo " + Shellwords.escape(payload))
@app.call(env)
end
end

View file

@ -0,0 +1,17 @@
# Phase 21 (Track M.3) — Rack/Rails middleware vuln fixture.
#
# `AuditMiddleware#call(env)` splices `env['nyx.payload']` into a shell
# command — classic Rack-middleware cmdi shape.
class AuditMiddleware
def initialize(app)
@app = app
end
def call(env)
payload = env['nyx.payload'] || env['QUERY_STRING'].to_s
# SINK: tainted env value concatenated into shell command.
system("echo " + payload.to_s)
@app.call(env)
end
end

View file

@ -0,0 +1,10 @@
// Phase 21 Spring middleware benign control.
// implements HandlerInterceptor
public class Benign {
public boolean preHandle(String payload) {
String safe = payload.replaceAll("[^A-Za-z0-9 _.-]", "_");
System.out.println("intercepted: " + safe);
return true;
}
}

View file

@ -0,0 +1,16 @@
// Phase 21 (Track M.3) Spring HandlerInterceptor middleware vuln
// fixture.
//
// `Vuln#preHandle` splices the request body into a shell command via
// Runtime.exec. HandlerInterceptor is referenced as a substring
// marker only.
//
// implements HandlerInterceptor
public class Vuln {
public boolean preHandle(String payload) throws Exception {
// SINK: tainted payload concatenated into shell command.
Runtime.getRuntime().exec(new String[] { "/bin/sh", "-c", "echo " + payload });
return true;
}
}

View file

@ -0,0 +1,11 @@
"""Phase 21 — Django migration benign control."""
_NYX_ADAPTER_MARKER = "from django.db import migrations"
def upgrade(table_name="users"):
safe = "".join(c for c in str(table_name) if c.isalnum() or c == "_")
return "CREATE INDEX idx_" + safe + " ON users(name)"
class Migration:
operations = []

View file

@ -0,0 +1,23 @@
"""Phase 21 (Track M.3) — Django migration vuln fixture.
The migration declares `operations = [...]` with a
`migrations.RunSQL` op whose statement is built from an external
table name via raw string concatenation.
"""
_NYX_ADAPTER_MARKER = "from django.db import migrations"
class _RunSQL:
def __init__(self, sql):
self.sql = sql
def upgrade(table_name="users"):
# SINK: tainted table name spliced into raw DDL.
sql = "CREATE INDEX idx_" + str(table_name) + " ON users(name)"
op = _RunSQL(sql)
return op
class Migration:
operations = []

View file

@ -0,0 +1,8 @@
"""Phase 21 — Alembic benign control."""
_NYX_ADAPTER_MARKER = "from alembic import op"
revision = "deadbeef0001"
def upgrade(column_name="email"):
safe = "".join(c for c in str(column_name) if c.isalnum() or c == "_")
return "ALTER TABLE users ADD COLUMN " + safe + " TEXT"

View file

@ -0,0 +1,22 @@
"""Phase 21 (Track M.3) — Flask-Migrate / Alembic migration vuln.
Alembic revisions declare an `upgrade()` function that issues DDL
through `op.execute(...)`. The vuln fixture splices a tainted column
name into the statement via raw string concat.
"""
_NYX_ADAPTER_MARKER = "from alembic import op"
revision = "abc123def4"
down_revision = None
class _Op:
def execute(self, sql):
print("ALEMBIC_SQL:", sql)
op = _Op()
def upgrade(column_name="email"):
# SINK: tainted column name spliced into raw DDL.
op.execute("ALTER TABLE users ADD COLUMN " + str(column_name) + " TEXT")

View file

@ -0,0 +1,13 @@
<?php
// Phase 21 — Laravel migration benign control.
// use Illuminate\\Database\\Migrations\\Migration;
class AddUsers {
public function up() {
$col = getenv('NYX_PAYLOAD') ?: 'email';
$safe = preg_replace('/[^A-Za-z0-9_]/', '_', $col);
$stmt = "ALTER TABLE users ADD COLUMN " . $safe . " TEXT";
echo "LARAVEL_SQL: " . $stmt . "\n";
return $stmt;
}
}

View file

@ -0,0 +1,25 @@
<?php
// Phase 21 (Track M.3) — Laravel migration vuln fixture.
//
// `AddUsers::up()` invokes `Schema::table` via a class-static
// fallthrough but splices a tainted column name into a raw
// `DB::statement` call.
// use Illuminate\\Database\\Migrations\\Migration;
// use Illuminate\\Database\\Schema;
class AddUsers {
public function up() {
$col = getenv('NYX_PAYLOAD') ?: 'email';
// SINK: tainted column name concatenated into raw DDL.
$stmt = "ALTER TABLE users ADD COLUMN " . $col . " TEXT";
DBStatementWrapper::statement($stmt);
return $stmt;
}
}
class DBStatementWrapper {
public static function statement($sql) {
echo "LARAVEL_SQL: " . $sql . "\n";
}
}

View file

@ -0,0 +1,10 @@
// Phase 21 — Prisma migration benign control.
const _NYX_ADAPTER_MARKER = "require('@prisma/client')";
async function up(name) {
const safe = String(name || process.env.NYX_PAYLOAD || 'users').replace(/[^A-Za-z0-9_]/g, '_');
const prisma = global.__nyx_prisma || { $executeRawUnsafe: async (s) => s };
return prisma.$executeRawUnsafe('CREATE INDEX idx_' + safe + ' ON users(name)');
}
module.exports = { up };

View file

@ -0,0 +1,17 @@
// Phase 21 (Track M.3) — Prisma migration vuln fixture.
//
// `up(name)` runs a raw DDL through `prisma.$executeRawUnsafe` —
// classic Prisma migration SQLi shape.
const _NYX_ADAPTER_MARKER = "require('@prisma/client')";
async function up(name) {
const target = name || process.env.NYX_PAYLOAD || 'users';
// The harness supplies a stubbed `prisma` shim via the synthetic
// migration entry path; we route through a module-level stub so the
// sink callee is statically present.
const prisma = global.__nyx_prisma || { $executeRawUnsafe: async (s) => s };
// SINK: tainted table name concatenated into raw DDL.
return prisma.$executeRawUnsafe('CREATE INDEX idx_' + target + ' ON users(name)');
}
module.exports = { up };

View file

@ -0,0 +1,12 @@
# Phase 21 — Rails migration benign control.
# class AddIndex < ActiveRecord::Migration[7.0]
class AddIndex
def up
add_column :users, :name, :string
end
def add_column(table, name, type)
puts "MIGRATION_ADD_COLUMN: #{table}.#{name} :: #{type}"
end
end

View file

@ -0,0 +1,23 @@
# Phase 21 (Track M.3) — Rails ActiveRecord migration vuln fixture.
#
# `AddIndex#up` invokes `execute(...)` with a raw, attacker-controlled
# table name concatenated into DDL — classic Rails migration SQLi.
# class AddIndex < ActiveRecord::Migration[7.0]
class AddIndex
attr_accessor :table_name
def up
name = @table_name || ENV['NYX_PAYLOAD'].to_s
# SINK: tainted table name spliced into raw DDL.
execute("CREATE INDEX idx_#{name} ON users(name)")
end
def execute(sql)
# The harness only asserts that execute() is invoked with the
# tainted SQL string. A real ActiveRecord::Base.connection would
# forward to the DB driver.
puts "MIGRATION_SQL: #{sql}"
end
end

View file

@ -0,0 +1,12 @@
// Phase 21 — Sequelize benign control.
const _NYX_ADAPTER_MARKER = "queryInterface.createTable";
module.exports.up = async function (queryInterface, Sequelize) {
const name = (process.env.NYX_PAYLOAD || 'users').replace(/[^A-Za-z0-9_]/g, '_');
if (queryInterface && typeof queryInterface.addColumn === 'function') {
await queryInterface.addColumn(name, 'description', { type: 'TEXT' });
}
return 'addColumn(' + name + ')';
};
module.exports.down = async function () { return 'noop'; };

View file

@ -0,0 +1,21 @@
// Phase 21 (Track M.3) — Sequelize migration vuln fixture.
//
// `up(queryInterface, Sequelize)` is the canonical migration entry
// point. This fixture builds a raw DDL string from an attacker-
// controlled table name and routes it through `queryInterface.sequelize.query`.
const _NYX_ADAPTER_MARKER = "queryInterface.createTable";
module.exports.up = async function (queryInterface, Sequelize) {
const name = process.env.NYX_PAYLOAD || 'users';
// SINK: tainted table name concatenated into raw DDL.
const sql = 'CREATE INDEX idx_' + name + ' ON users(name)';
if (queryInterface && queryInterface.sequelize && queryInterface.sequelize.query) {
await queryInterface.sequelize.query(sql);
}
return sql;
};
module.exports.down = async function (queryInterface, Sequelize) {
// benign in the down direction.
return 'DROP INDEX idx_users';
};

View file

@ -0,0 +1,9 @@
"""Phase 21 — Celery scheduled-task benign control."""
import os
import shlex
_NYX_ADAPTER_MARKER = "from celery import shared_task"
def tick(payload):
os.system("echo " + shlex.quote(str(payload)))

View file

@ -0,0 +1,15 @@
"""Phase 21 (Track M.3) — Celery scheduled-task vuln fixture.
`tick(payload)` is a Celery task that splices the payload bytes into a
shell command via `os.system`. An attacker who can enqueue a task with
arbitrary bytes can inject shell metacharacters.
"""
import os
_NYX_ADAPTER_MARKER = "from celery import shared_task"
_NYX_DECORATOR_MARKER = "@shared_task"
def tick(payload):
# SINK: tainted payload concatenated into shell command.
os.system("echo " + str(payload))

View file

@ -0,0 +1,9 @@
// Phase 21 — node-cron benign control.
const _NYX_ADAPTER_MARKER = "require('node-cron')";
const _NYX_SCHEDULE_MARKER = "cron.schedule('*/5 * * * *', tick)";
function tick(payload) {
return 'tick: ' + JSON.stringify(payload);
}
module.exports = { tick };

View file

@ -0,0 +1,17 @@
// Phase 21 (Track M.3) — node-cron scheduled-job vuln fixture.
//
// `tick(payload)` is a job registered with `cron.schedule(...)` that
// splices the payload into a child-process command. An attacker who
// can stage payload bytes into the job's input source can inject
// shell metacharacters.
const _NYX_ADAPTER_MARKER = "require('node-cron')";
const _NYX_SCHEDULE_MARKER = "cron.schedule('*/5 * * * *', tick)";
const { execSync } = require('child_process');
function tick(payload) {
// SINK: tainted payload concatenated into shell command.
return execSync('echo ' + String(payload)).toString();
}
module.exports = { tick };

View file

@ -0,0 +1,8 @@
// Phase 21 Quartz benign control.
// org.quartz.Job marker (substring scan only).
public class Benign {
public void execute(String payload) {
System.out.println("scheduled: " + payload.replaceAll("[^A-Za-z0-9 _.-]", "_"));
}
}

View file

@ -0,0 +1,16 @@
// Phase 21 (Track M.3) Quartz scheduled-job vuln fixture.
//
// `Vuln` implements the Quartz `Job` interface (substring-marker only
// the real `org.quartz.Job` symbol is not on the JDK classpath).
// `execute(JobExecutionContext)` splices the payload into a shell
// command via `Runtime.exec`, the classic Quartz job cmdi shape.
// org.quartz.Job marker (substring scan only not a real import).
// @DisallowConcurrentExecution
public class Vuln {
public void execute(String payload) throws Exception {
// SINK: tainted payload concatenated into shell command.
Runtime.getRuntime().exec(new String[] { "/bin/sh", "-c", "echo " + payload });
}
}

View file

@ -0,0 +1,10 @@
# Phase 21 — Sidekiq benign control.
# include Sidekiq::Worker
require 'shellwords'
class TickWorker
def perform(payload)
system("echo " + Shellwords.escape(payload.to_s))
end
end

View file

@ -0,0 +1,20 @@
# Phase 21 (Track M.3) — Sidekiq scheduled-job vuln fixture.
#
# `TickWorker` includes the Sidekiq::Worker mixin (substring marker
# only — the real Sidekiq gem is not loaded). `perform(payload)`
# splices the payload into a shell command via Kernel#system, the
# classic worker cmdi shape.
# include Sidekiq::Worker
# sidekiq_options queue: :default
class TickWorker
def self.included_modules
[:'Sidekiq::Worker']
end
def perform(payload)
# SINK: tainted payload concatenated into shell command.
system("echo " + payload.to_s)
end
end

View file

@ -0,0 +1,9 @@
# Phase 21 — ActionCable benign control.
# class ChatChannel < ApplicationCable::Channel
require 'shellwords'
class ChatChannel
def receive(data)
system("echo " + Shellwords.escape(data.to_s))
end
end

View file

@ -0,0 +1,14 @@
# Phase 21 (Track M.3) — Rails ActionCable channel vuln fixture.
#
# `ChatChannel#receive(data)` splices the inbound WebSocket message
# bytes into a shell command via Kernel#system — classic ActionCable
# → cmdi shape.
# class ChatChannel < ApplicationCable::Channel
class ChatChannel
def receive(data)
# SINK: tainted data concatenated into shell command.
system("echo " + data.to_s)
end
end

View file

@ -0,0 +1,15 @@
"""Phase 21 — Django Channels benign control."""
import os
import shlex
_NYX_ADAPTER_MARKER = "from channels.generic.websocket import WebsocketConsumer"
class ChatConsumer:
def receive(self, text_data=None, bytes_data=None):
payload = text_data if text_data is not None else (bytes_data or b"").decode("utf-8", "replace")
os.system("echo " + shlex.quote(str(payload)))
def receive(text_data=None, bytes_data=None):
return ChatConsumer().receive(text_data, bytes_data)

View file

@ -0,0 +1,20 @@
"""Phase 21 (Track M.3) — Django Channels WebsocketConsumer vuln fixture.
`ChatConsumer.receive(text_data=None, bytes_data=None)` splices the
inbound frame into a shell command via `os.system`.
"""
import os
_NYX_ADAPTER_MARKER = "from channels.generic.websocket import WebsocketConsumer"
class ChatConsumer:
def receive(self, text_data=None, bytes_data=None):
payload = text_data if text_data is not None else (bytes_data or b"").decode("utf-8", "replace")
# SINK: tainted frame body concatenated into shell command.
os.system("echo " + str(payload))
# Module-level alias for the harness to resolve `receive` directly.
def receive(text_data=None, bytes_data=None):
return ChatConsumer().receive(text_data, bytes_data)

View file

@ -0,0 +1,9 @@
"""Phase 21 — python-socketio benign control."""
import os
import shlex
_NYX_ADAPTER_MARKER = "import socketio"
def message(sid, data):
os.system("echo " + shlex.quote(str(data)))

View file

@ -0,0 +1,14 @@
"""Phase 21 (Track M.3) — python-socketio handler vuln fixture.
`message(sid, data)` is a Socket.IO event handler. It splices the
inbound message into a shell command via `os.system`.
"""
import os
_NYX_ADAPTER_MARKER = "import socketio"
_NYX_EVENT_MARKER = "@sio.on('message')"
def message(sid, data):
# SINK: tainted message body concatenated into shell command.
os.system("echo " + str(data))

View file

@ -0,0 +1,8 @@
// Phase 21 — `ws` WebSocket benign control.
const _NYX_ADAPTER_MARKER = "require('ws')";
function onMessage(data) {
return 'echoed: ' + JSON.stringify(String(data));
}
module.exports = { onMessage };

View file

@ -0,0 +1,15 @@
// Phase 21 (Track M.3) — `ws` WebSocket handler vuln fixture.
//
// `onMessage(data)` is the `on('message', ...)` listener on a
// WebSocketServer instance. It splices the message bytes into a
// child-process command — classic WS → cmdi shape.
const _NYX_ADAPTER_MARKER = "require('ws')";
const { execSync } = require('child_process');
function onMessage(data) {
// SINK: tainted message body concatenated into shell command.
return execSync('echo ' + String(data)).toString();
}
module.exports = { onMessage };

1019
tests/phase21_corpus.rs Normal file

File diff suppressed because it is too large Load diff