Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion 10-core/app-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,19 @@ For tiny operations (filters, maps, reshapes) that don't deserve a dedicated age
```yaml
- id: filter-welded
inline:
kind: predicate # predicate | map | shape
kind: predicate # predicate (runnable today); map | shape reserved
description: Keep only welded assemblies
code: |
e => e.AssemblyType == AssemblyType.Welded
```

Inline glue lives in the app file. It is visible in the topology and inspectable in the canvas — **no hidden logic.**

> **Runtime support:** only `kind: predicate` executes today. `map` and `shape` are
> reserved for a future release; `aware app validate` / `compile` reject them up
> front (rather than failing at `run`) so an app never locks against an unrunnable
> inline kind (#160).

### Atom references (v0.20)

The persona audit unanimously flagged inline JavaScript lambdas (`code: | e => e.type == "Welded"`) as "not no-code." v0.20 ships a **named atom library** — typed, versioned, reusable predicates / maps / reduces that an inline-glue block can reference instead of inlining code.
Expand Down
14 changes: 14 additions & 0 deletions cli/src/app_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,20 @@ pub fn find_app_source(path: &Path) -> Option<std::path::PathBuf> {
/// End-to-end: load + compile + write. Called by `aware app compile`.
pub fn compile_to_disk(source: &Path, paths: &Paths) -> Result<std::path::PathBuf, AwareError> {
let app = crate::manifest::loader::load_app(source)?;
// Refuse to produce a lock for an app the runtime can't execute (e.g. an
// inline kind the orchestrator rejects). Gating here covers every
// lock-producing path — `app compile`, `app inspect`, … — so an unrunnable
// construct fails before locking, not at run (#160).
let issues = crate::validate::validate_app(&app);
if let Some(err) = issues
.iter()
.find(|i| i.severity == crate::validate::Severity::Error)
{
return Err(AwareError::Validation(format!(
"app failed validation: [{}] {}",
err.code, err.message
)));
}
let agents = discover_agents(paths)?;
let lock = compile(&app, &agents, source)?;
write_lockfile(&lock, source)
Expand Down
2 changes: 2 additions & 0 deletions cli/src/commands/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,8 @@ fn compile_cmd(ctx: &Context, path: &std::path::Path) -> Result<(), AwareError>
path.display()
))
})?;
// compile_to_disk validates before locking, so an unrunnable construct (e.g.
// an inline kind the runtime rejects) fails here rather than at run (#160).
let lock_path = crate::app_lock::compile_to_disk(&source, &ctx.paths)?;
println!(
"\u{2713} compiled {} \u{2192} {}",
Expand Down
114 changes: 105 additions & 9 deletions cli/src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,17 +186,39 @@ pub fn validate_app(app: &App) -> Vec<ValidationIssue> {
}
}

for n in &app.nodes {
if let Some(inline) = &n.inline
&& inline.description.trim().is_empty()
{
out.push(ValidationIssue::error(
"E_APP_INLINE_NO_DESC",
format!("inline node {:?} missing description", n.id),
));
check_inline_nodes(&app.nodes, &mut out);
out
}

/// Recursively validate inline-glue nodes, descending into `for-each` `do:` bodies
/// (which the compiler flattens and the runtime executes), so unsupported inline
/// kinds are caught wherever they appear, not just at the top level (#160).
fn check_inline_nodes(nodes: &[crate::manifest::app::Node], out: &mut Vec<ValidationIssue>) {
for n in nodes {
if let Some(inline) = &n.inline {
if inline.description.trim().is_empty() {
out.push(ValidationIssue::error(
"E_APP_INLINE_NO_DESC",
format!("inline node {:?} missing description", n.id),
));
}
// The runtime executes only `predicate` inline glue today (see
// orchestrator). Reject other kinds so an unrunnable app fails at
// validate/compile — not after the author validated + locked it.
if inline.kind != "predicate" {
out.push(ValidationIssue::error(
"E_APP_INLINE_KIND",
format!(
"inline node {:?}: kind {:?} is not runnable yet (only 'predicate' is supported)",
n.id, inline.kind
),
));
}
}
if let Some(body) = &n.do_ {
check_inline_nodes(body, out);
}
}
out
}

/// Validate the safety contract for write-mode nodes against the installed
Expand Down Expand Up @@ -297,6 +319,80 @@ mod tests {
assert!(!has_errors(&issues), "issues: {issues:?}");
}

#[test]
fn rejects_unrunnable_inline_kind_at_validate() {
// kind: shape passes parse but the runtime only runs `predicate`; validate
// must reject it so the failure surfaces before compile/lock (#160).
let yaml = r#"
app: inline-shape
version: 0.0.1
description: |
Inline shape node.
requires: []
nodes:
- id: passthrough
inline:
kind: shape
description: reshape a value
code: "() => ({ ok: true })"
"#;
let app: App = serde_yaml::from_str(yaml).unwrap();
let issues = validate_app(&app);
assert!(has_errors(&issues), "issues: {issues:?}");
assert!(issues.iter().any(|i| i.code == "E_APP_INLINE_KIND"));
}

#[test]
fn rejects_unrunnable_inline_kind_nested_in_for_each_body() {
// A shape node inside a for-each `do:` body is flattened + run, so it must
// be rejected at validate too — not just top-level nodes (#160 review).
let yaml = r#"
app: inline-shape-body
version: 0.0.1
description: |
for-each with an inline shape in its body.
requires: []
nodes:
- id: loop
for-each: '{{ items }}'
do:
- id: reshape
inline:
kind: shape
description: reshape each item
code: "() => ({ ok: true })"
"#;
let app: App = serde_yaml::from_str(yaml).unwrap();
let issues = validate_app(&app);
assert!(
issues.iter().any(|i| i.code == "E_APP_INLINE_KIND"),
"issues: {issues:?}"
);
}

#[test]
fn predicate_inline_kind_passes_validate() {
let yaml = r#"
app: inline-pred
version: 0.0.1
description: |
Inline predicate node.
requires: []
nodes:
- id: gate
inline:
kind: predicate
description: gate on type
code: 'e.type == "Welded"'
"#;
let app: App = serde_yaml::from_str(yaml).unwrap();
let issues = validate_app(&app);
assert!(
!issues.iter().any(|i| i.code == "E_APP_INLINE_KIND"),
"issues: {issues:?}"
);
}

#[test]
fn detects_cycle_in_connections() {
let yaml = r#"
Expand Down
53 changes: 53 additions & 0 deletions cli/tests/app_validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,56 @@ requires: []
.code(3)
.stdout(predicate::str::contains("E_APP_CYCLE"));
}

#[test]
fn inline_shape_kind_rejected_by_validate() {
// kind: shape parses + would compile, but the runtime only runs `predicate`;
// validate must reject it up front (#160).
let tmp = tempfile::tempdir().unwrap();
let app = r#"app: inline-shape
version: 0.1.0
description: inline shape repro
requires: []
nodes:
- id: passthrough
inline:
kind: shape
description: reshape a value
code: "() => ({ ok: true })"
"#;
std::fs::write(tmp.path().join("inline-shape.flo"), app).unwrap();

Command::cargo_bin("aware")
.unwrap()
.args(["app", "validate"])
.arg(tmp.path())
.assert()
.failure()
.stdout(predicate::str::contains("E_APP_INLINE_KIND"));
}

#[test]
fn inline_shape_kind_rejected_by_compile() {
// compile must not produce a lock for an app the runtime can't execute (#160).
let tmp = tempfile::tempdir().unwrap();
let app = r#"app: inline-shape
version: 0.1.0
description: inline shape repro
requires: []
nodes:
- id: passthrough
inline:
kind: shape
description: reshape a value
code: "() => ({ ok: true })"
"#;
std::fs::write(tmp.path().join("inline-shape.flo"), app).unwrap();

Command::cargo_bin("aware")
.unwrap()
.args(["app", "compile"])
.arg(tmp.path())
.assert()
.failure()
.stderr(predicate::str::contains("E_APP_INLINE_KIND"));
}