[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

@ -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:
///