Skip to content
Draft
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
44 changes: 40 additions & 4 deletions .agents/skills/ptf-extract/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,34 @@ input error.
2. Extract only facts explicitly supported by the paper. Write its standalone
YAML directly to `specs/functions/<apa_article_key>.yaml`, following
`references/spec-template.yaml`.
3. Record missing or ambiguous required scientific information as explicit
blockers and set affected functions to `blocked`; otherwise set reviewed,
complete functions to `ready-for-implementation`.
4. Run `cargo run --manifest-path codegen/Cargo.toml -- validate` and fix
3. For particle-size or texture-related inputs, examine whether sand, silt, and
clay are mass or volume fractions; whether percentages use the fine-earth
fraction; the particle-size boundaries and their units; any named
particle-size or texture-classification system; whether the fractions should
sum to 100; and whether the source directly uses a categorical texture-class
predictor.
4. For functions with numeric particle-size inputs that could potentially be
supplied through the common USDA texture adapter, record
`input_adapters.usda_texture` as:
- `supported` only when the source provides explicit compatibility evidence;
- `unsupported` only when the source provides explicit incompatibility
evidence;
- `unknown` when the relevant definitions are absent or ambiguous.

Require non-empty evidence for every recorded status. Do not add this adapter
metadata merely because input parameters are named `sand`, `silt`, or
`clay`. If the source directly uses a categorical texture-class predictor,
preserve that predictor as part of the published model rather than replacing
it with representative numeric fractions.

An `unknown` adapter status blocks only a USDA compatibility claim, not an
otherwise complete PTF specification.
5. Record missing or ambiguous scientific information required to interpret or
implement the published PTF as explicit blockers and set affected functions
to `blocked`; otherwise set reviewed, complete functions to
`ready-for-implementation`. Do not treat information needed only to establish
USDA adapter compatibility as a PTF implementation blocker.
6. Run `cargo run --manifest-path codegen/Cargo.toml -- validate` and fix
validation errors before finishing. Validation never justifies inferred
science.

Expand All @@ -45,3 +69,15 @@ exact YAML path and explicit blockers.
- Use `generation.public_python: manual` only when the public wrapper cannot
follow the standard generated API; it never opts the native NumPy ufunc out
of generation.
- Never infer USDA compatibility from parameter names alone.
- Never replace measured or source-defined particle-size fractions with
representative USDA fractions during extraction.
- Never create a golden test by converting a texture-class label to
representative sand, silt, and clay values.
- If the publication directly defines a categorical texture-class predictor,
preserve it rather than rewriting the published model as a numeric
sand-silt-clay model.
- Never copy the common USDA representative-value table into an individual PTF
specification.
- `ptf-extract` records compatibility evidence; it does not perform user-data
conversion.
30 changes: 29 additions & 1 deletion .agents/skills/ptf-extract/references/extraction-quality-gate.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,41 @@ APA-style slug and identifies the generated public module, for example
`generation.public_python: manual` only for an intentional manual public
wrapper; native ufunc generation remains required.

## Texture-input adapters

For each function with particle-size or texture-related inputs, review the
source evidence for fraction basis, fine-earth basis, particle-size boundaries,
named classification system, sum-to-100 expectations, and direct categorical
texture predictors. Record the result in `input_adapters.usda_texture` when
relevant.

Verify that:

- every mapped `sand`, `silt`, or `clay` adapter input exists in the function's
declared `inputs`;
- roles are supported by source definitions and are not assigned from variable
names alone;
- `supported` includes explicit evidence of USDA compatibility;
- `unsupported` explains the explicit incompatibility;
- `unknown` identifies the missing or ambiguous definitions;
- adapter metadata does not alter the published PTF formula;
- representative USDA values are not embedded in the PTF source specification;
and
- missing compatibility evidence does not set an otherwise complete PTF to
`blocked`.

Compatibility evidence belongs in the structured adapter metadata. It may also
be explained in `scientific_notes` when additional context helps scientific
review. The extractor records evidence only; it does not convert user data.

## Blockers

Set affected functions to `blocked` and name the missing evidence when a
formula, constant, unit, output mapping, semantic expression, golden value,
numeric policy, or applicability fact is missing or ambiguous. Do not use
`TODO` as a substitute for a structured required value; write it only in an
explicit blocker note. Schema-valid YAML may still be blocked.
explicit blocker note. Schema-valid YAML may still be blocked. An `unknown`
USDA adapter status blocks only a compatibility claim.

## Statuses

Expand Down
30 changes: 26 additions & 4 deletions .agents/skills/ptf-extract/references/spec-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,37 @@ functions:
models:
h_theta: null
k_h: null
inputs: []
outputs: []
input_adapters:
usda_texture:
status: unknown
inputs:
sand: sand
silt: null
clay: clay
evidence: Particle-size boundaries are not yet established from the source.
inputs:
- name: sand
symbol: null
unit: '%'
domain: '[0, 100]'
description: Sand content defined by the source.
- name: clay
symbol: null
unit: '%'
domain: '[0, 100]'
description: Clay content defined by the source.
outputs:
type: scalar
name: result
symbol: null
unit: '1'
domain: null
description: Target quantity predicted by the source.
golden_tests: []
edge_cases: []
documentation:
notes: []
warnings: []
implementation:
variables: []
scientific_notes: |
## Supported models

Expand Down
6 changes: 5 additions & 1 deletion codegen/src/documentation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! Targets choose their own section ordering and markup. This module only
//! describes the information they have available to render.

use crate::model::{Function, Outputs, Parameter, Scope, Source};
use crate::model::{Function, InputAdapters, Outputs, Parameter, Scope, Source};

#[derive(Clone, Copy, Debug)]
pub(crate) struct SourceDocument<'a> {
Expand Down Expand Up @@ -35,6 +35,7 @@ pub(crate) struct FunctionDocument<'a> {
pub(crate) remarks: Remarks<'a>,
pub(crate) notes: &'a [String],
pub(crate) warnings: &'a [String],
pub(crate) input_adapters: Option<&'a InputAdapters>,
}

#[derive(Clone, Copy, Debug)]
Expand Down Expand Up @@ -90,6 +91,7 @@ pub(crate) fn for_function(function: &Function) -> FunctionDocument<'_> {
},
notes: &function.documentation.notes,
warnings: &function.documentation.warnings,
input_adapters: function.input_adapters.as_ref(),
}
}

Expand Down Expand Up @@ -133,6 +135,7 @@ mod tests {
prediction_target: "Test property.".into(),
models: Models::default(),
},
input_adapters: None,
inputs: Vec::new(),
outputs: Outputs::Scalar {
field: parameter("result"),
Expand Down Expand Up @@ -169,6 +172,7 @@ mod tests {
k_h: Some("Conductivity model.".into()),
},
},
input_adapters: None,
inputs: vec![parameter("sand")],
outputs: Outputs::Record {
name: "TestResult".into(),
Expand Down
8 changes: 6 additions & 2 deletions codegen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod render;
mod semantic;
mod specs;
mod targets;
mod usda_texture;
mod validate;
mod version;

Expand Down Expand Up @@ -41,6 +42,7 @@ impl Cli {
match self.command {
Command::Validate => {
let entries = load_validated_specifications(root)?;
usda_texture::load(root)?;
println!(
"validated {} PTF specification files containing {} functions",
entries.len(),
Expand All @@ -53,11 +55,13 @@ impl Cli {
}
Command::Generate => {
let entries = load_validated_specifications(root)?;
targets::run(root, entries)
let usda_texture = usda_texture::load(root)?;
targets::run(root, entries, &usda_texture)
}
Command::CheckGenerated => {
let entries = load_validated_specifications(root)?;
targets::check_generated(root, entries)
let usda_texture = usda_texture::load(root)?;
targets::check_generated(root, entries, &usda_texture)
}
Command::Version { version } => version::run(root, &version),
}
Expand Down
53 changes: 53 additions & 0 deletions codegen/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ struct FunctionReference {
status: String,
public_api: PublicApi,
scope: FunctionScope,
#[serde(default)]
input_adapters: Option<InputAdapters>,
inputs: Vec<InputReference>,
outputs: OutputReference,
implementation: Option<Implementation>,
Expand Down Expand Up @@ -119,6 +121,7 @@ impl<'de> Deserialize<'de> for Spec {
status: function.status,
public_api: function.public_api,
scope: function.scope,
input_adapters: function.input_adapters,
inputs,
outputs,
implementation: function.implementation,
Expand Down Expand Up @@ -176,6 +179,8 @@ pub(crate) struct Function {
pub(crate) status: String,
pub(crate) public_api: PublicApi,
pub(crate) scope: FunctionScope,
#[serde(default)]
pub(crate) input_adapters: Option<InputAdapters>,
pub(crate) inputs: Vec<Parameter>,
pub(crate) outputs: Outputs,
pub(crate) implementation: Option<Implementation>,
Expand All @@ -185,6 +190,54 @@ pub(crate) struct Function {
pub(crate) documentation: Documentation,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub(crate) struct InputAdapters {
pub(crate) usda_texture: Option<UsdaTextureAdapter>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub(crate) struct UsdaTextureAdapter {
pub(crate) status: AdapterStatus,
#[serde(default)]
pub(crate) inputs: Option<TextureInputMapping>,
pub(crate) evidence: String,
}

#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum AdapterStatus {
Supported,
Unsupported,
Unknown,
}

impl AdapterStatus {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Supported => "supported",
Self::Unsupported => "unsupported",
Self::Unknown => "unknown",
}
}
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub(crate) struct TextureInputMapping {
pub(crate) sand: Option<String>,
pub(crate) silt: Option<String>,
pub(crate) clay: Option<String>,
}

impl TextureInputMapping {
pub(crate) fn roles(&self) -> [(&'static str, Option<&str>); 3] {
[
("sand", self.sand.as_deref()),
("silt", self.silt.as_deref()),
("clay", self.clay.as_deref()),
]
}
}

impl Function {
pub(crate) fn result_class(&self) -> Option<&str> {
match &self.outputs {
Expand Down
92 changes: 92 additions & 0 deletions codegen/src/specs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,15 @@ mod tests {
.unwrap();
}

fn specification_with_adapter(adapter: &str) -> String {
specification(
"adapter",
" implementation:\n variables: [{name: value, expr: x}]\n",
"",
)
.replace(" inputs:\n", &format!("{adapter} inputs:\n"))
}

#[test]
fn rejects_a_code_generating_function_without_implementation() {
let root = fixture_root("mixed");
Expand Down Expand Up @@ -455,4 +464,87 @@ mod tests {

assert!(error.contains("filename stem must be an APA-style slug"));
}

#[test]
fn accepts_supported_unsupported_unknown_and_absent_usda_adapters() {
for (label, adapter) in [
(
"supported",
" input_adapters:\n usda_texture:\n status: supported\n inputs: {sand: x}\n evidence: The source defines x as USDA total sand by mass of fine earth.\n",
),
(
"unsupported",
" input_adapters:\n usda_texture:\n status: unsupported\n evidence: The source uses incompatible particle-size boundaries.\n",
),
(
"unknown",
" input_adapters:\n usda_texture:\n status: unknown\n inputs: {sand: x}\n evidence: The source does not report the sand particle-size boundary.\n",
),
("absent", ""),
] {
let root = fixture_root(label);
fs::write(
root.join("specs/functions/adapter.yaml"),
specification_with_adapter(adapter),
)
.unwrap();
let entries = load(&root).expect("adapter fixture must satisfy the schema");
assert!(crate::validate::specifications(&entries).is_empty());
fs::remove_dir_all(root).unwrap();
}
}

#[test]
fn schema_rejects_supported_adapter_without_evidence_or_mapped_input() {
for (label, adapter, expected) in [
(
"no-evidence",
" input_adapters:\n usda_texture:\n status: supported\n inputs: {sand: x}\n",
"evidence",
),
(
"no-mapping",
" input_adapters:\n usda_texture:\n status: supported\n inputs: {}\n evidence: Explicit USDA definitions are reported.\n",
"not valid under any",
),
] {
let root = fixture_root(label);
fs::write(
root.join("specs/functions/adapter.yaml"),
specification_with_adapter(adapter),
)
.unwrap();
let error = load(&root)
.expect_err("invalid adapter must fail")
.to_string();
assert!(error.contains(expected), "{error}");
fs::remove_dir_all(root).unwrap();
}
}

#[test]
fn semantic_validation_rejects_unknown_and_duplicate_mapped_inputs() {
for (label, mapping, expected) in [
("missing", "{sand: missing}", "is not declared"),
(
"duplicate",
"{sand: x, silt: x}",
"mapped to multiple texture roles",
),
] {
let root = fixture_root(label);
let adapter = format!(
" input_adapters:\n usda_texture:\n status: unknown\n inputs: {mapping}\n evidence: Particle-size compatibility is not reported.\n"
);
fs::write(
root.join("specs/functions/adapter.yaml"),
specification_with_adapter(&adapter),
)
.unwrap();
let entries = load(&root).expect("fixture must satisfy structural schema");
let errors = crate::validate::specifications(&entries).join("\n");
assert!(errors.contains(expected), "{errors}");
fs::remove_dir_all(root).unwrap();
}
}
}
Loading