diff --git a/Cargo.lock b/Cargo.lock index 8116af6..39a1dee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,244 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "kaptan-runtime" version = "0.1.0" + +[[package]] +name = "kaptan-spec" +version = "0.1.0" +dependencies = [ + "trybuild", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-triple" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "toml" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "trybuild" +version = "1.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06649c6f63d86604ba0c8950d5a1829fc9a17afd70fc6629f481d75b6a624c78" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 0db5e63..8a5c639 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "kaptan-spec", "kaptan-runtime", ] resolver = "2" diff --git a/README.md b/README.md index c9cfb2c..925a65f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,42 @@ # kaptan -next-gen flight controller operating system + +Kaptan is a modular flight controller platform focused on deterministic bare-metal execution for flight-critical paths and strong verification discipline around the rest of the stack. + +The repository is currently in infrastructure setup. Formal project specifications should be linked directly from the code they govern through compile-time attributes. + +## Current Direction + +- Flight-critical runtime stays bare-metal with no RTOS in the control path. +- The first vehicle scope is multirotor. +- The first board support lane is Kakute F7 Mini V3. +- Non-critical extensibility, companion integration, and user-facing tooling will be added only after core runtime and verification contracts are established. + +## Immediate Priorities + +- Create specification infrastructure without inventing the first formal spec. +- Establish crate and board boundaries that fit the first supported hardware lane. +- Add deterministic host-side validation before simulation and hardware automation expand. + +## Repository Structure + +- `kaptan-runtime/`: bare-metal runtime crate scaffold. +- `tests/sitl/`: host-side simulation and deterministic verification scaffolding. +- `tests/hitl/`: hardware-in-the-loop scaffolding. +- `scripts/`: repository validation and developer helper scripts. + +## Spec Infrastructure + +Formal specs should be colocated with the code or board package they govern. + +- Implementation-linked specs should use versioned sidecar filenames such as `control_loop.v0.1.0.spec.md` next to the related module or package. +- Rust items should link to those sidecar specs through `#[spec("...")]`. +- Missing linked spec files must fail compilation. + +Example: + +```rust +use kaptan_spec::spec; + +#[spec("control_loop.v0.1.0.spec.md")] +pub struct ControlLoop; +``` diff --git a/SPECIFICATIONS.md b/SPECIFICATIONS.md new file mode 100644 index 0000000..09f1e56 --- /dev/null +++ b/SPECIFICATIONS.md @@ -0,0 +1,65 @@ +# Specifications + +Formal Kaptan specifications should normally live next to the code item they govern. + +Examples: + +- `kaptan-runtime/src/control_loop.rs` +- `kaptan-runtime/src/control_loop.v0.1.0.spec.md` +- `kaptan-runtime/src/scheduler.rs` +- `kaptan-runtime/src/scheduler.v0.1.0.spec.md` + +## Compile-Time Linking + +Code implementations should link to their governing spec through `#[spec("...")]`. + +Example: + +```rust +use kaptan_spec::spec; + +#[spec("control_loop.v0.1.0.spec.md")] +pub struct ControlLoop; +``` + +The attribute expands to a compile-time file inclusion guard. If the linked file does not exist, compilation fails. + +## Spec Format + +Formal specs are expected to declare these keys in YAML frontmatter: + +- `title` +- `status` +- `owners` + +Allowed statuses are: + +- `in-progress` +- `implemented` +- `deprecated` + +Owner entries must use full name and email address in this format: + +```text +- Full Name +``` + +The spec path is the identifier for linkage and traceability. + +- The code path plus the `#[spec("...")]` reference identifies the governing spec. +- The version is part of the spec filename and therefore part of the spec identity. + +## Template + +Use this starter when creating a new colocated spec: + +```markdown +--- +title: Replace with title +status: in-progress +owners: + - Full Name +--- + +# TODO +``` \ No newline at end of file diff --git a/kaptan-spec/Cargo.toml b/kaptan-spec/Cargo.toml new file mode 100644 index 0000000..166942b --- /dev/null +++ b/kaptan-spec/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "kaptan-spec" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lib] +proc-macro = true + +[dev-dependencies] +trybuild = "1.0" \ No newline at end of file diff --git a/kaptan-spec/src/lib.rs b/kaptan-spec/src/lib.rs new file mode 100644 index 0000000..176d869 --- /dev/null +++ b/kaptan-spec/src/lib.rs @@ -0,0 +1,70 @@ +use proc_macro::{Literal, Punct, Spacing, TokenStream, TokenTree}; + +#[proc_macro_attribute] +pub fn spec(attr: TokenStream, item: TokenStream) -> TokenStream { + match parse_spec_path(attr) { + Ok(path) => expand_spec_guard(&path, item), + Err(message) => compile_error(&message), + } +} + +fn parse_spec_path(attr: TokenStream) -> Result { + let tokens: Vec = attr.into_iter().collect(); + + match tokens.as_slice() { + [TokenTree::Literal(path)] if is_string_literal(path) => Ok(path.to_string()), + [ + TokenTree::Ident(name), + TokenTree::Punct(eq), + TokenTree::Literal(path), + ] if name.to_string() == "path" && is_equals(eq) && is_string_literal(path) => { + Ok(path.to_string()) + } + [] => Err("missing spec path; use #[spec(\"file.v0.1.0.spec.md\")]".into()), + _ => Err("invalid spec attribute; use #[spec(\"file.v0.1.0.spec.md\")]".into()), + } +} + +fn is_string_literal(literal: &Literal) -> bool { + let value = literal.to_string(); + + value.starts_with('"') || value.starts_with("r\"") || value.starts_with("r#") +} + +fn is_equals(punct: &Punct) -> bool { + punct.as_char() == '=' && punct.spacing() == Spacing::Alone +} + +fn expand_spec_guard(path: &str, item: TokenStream) -> TokenStream { + let guard = format!("const _: &str = include_str!({path});"); + let mut output = TokenStream::new(); + + output.extend( + guard + .parse::() + .expect("spec macro must generate valid Rust tokens"), + ); + output.extend(item); + output +} + +fn compile_error(message: &str) -> TokenStream { + let escaped = escape_string(message); + format!("compile_error!(\"{escaped}\");") + .parse::() + .expect("spec macro must generate valid compile_error tokens") +} + +fn escape_string(value: &str) -> String { + let mut escaped = String::with_capacity(value.len()); + + for ch in value.chars() { + match ch { + '\\' => escaped.push_str("\\\\"), + '"' => escaped.push_str("\\\""), + _ => escaped.push(ch), + } + } + + escaped +} diff --git a/kaptan-spec/tests/spec_macro.rs b/kaptan-spec/tests/spec_macro.rs new file mode 100644 index 0000000..14a3505 --- /dev/null +++ b/kaptan-spec/tests/spec_macro.rs @@ -0,0 +1,11 @@ +#[test] +fn accepts_existing_sidecar_spec() { + let tests = trybuild::TestCases::new(); + tests.pass("tests/ui/flight_core_contract.rs"); +} + +#[test] +fn rejects_missing_sidecar_spec() { + let tests = trybuild::TestCases::new(); + tests.compile_fail("tests/ui/fail_missing_sidecar.rs"); +} diff --git a/kaptan-spec/tests/ui/fail_missing_sidecar.rs b/kaptan-spec/tests/ui/fail_missing_sidecar.rs new file mode 100644 index 0000000..df44e06 --- /dev/null +++ b/kaptan-spec/tests/ui/fail_missing_sidecar.rs @@ -0,0 +1,8 @@ +use kaptan_spec::spec; + +#[spec("fail_missing_sidecar.v0.1.0.spec.md")] +struct MissingSpec; + +fn main() { + let _ = core::mem::size_of::(); +} \ No newline at end of file diff --git a/kaptan-spec/tests/ui/fail_missing_sidecar.stderr b/kaptan-spec/tests/ui/fail_missing_sidecar.stderr new file mode 100644 index 0000000..2e244a0 --- /dev/null +++ b/kaptan-spec/tests/ui/fail_missing_sidecar.stderr @@ -0,0 +1,5 @@ +error: couldn't read `$DIR/tests/ui/fail_missing_sidecar.v0.1.0.spec.md`: No such file or directory (os error 2) + --> tests/ui/fail_missing_sidecar.rs:3:1 + | +3 | #[spec("fail_missing_sidecar.v0.1.0.spec.md")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/kaptan-spec/tests/ui/flight_core_contract.rs b/kaptan-spec/tests/ui/flight_core_contract.rs new file mode 100644 index 0000000..0d95b34 --- /dev/null +++ b/kaptan-spec/tests/ui/flight_core_contract.rs @@ -0,0 +1,9 @@ +use kaptan_spec::spec; + +#[spec("flight_core_contract.v0.1.0.spec.md")] +struct FlightCore; + +fn main() { + let size = core::mem::size_of::(); + assert_eq!(size, 0); +} \ No newline at end of file diff --git a/kaptan-spec/tests/ui/flight_core_contract.v0.1.0.spec.md b/kaptan-spec/tests/ui/flight_core_contract.v0.1.0.spec.md new file mode 100644 index 0000000..e0d3901 --- /dev/null +++ b/kaptan-spec/tests/ui/flight_core_contract.v0.1.0.spec.md @@ -0,0 +1,10 @@ +--- +title: Flight core contract +status: in-progress +owners: + - Test Name +--- + +# Summary + +Minimal spec for compile-time linkage testing. \ No newline at end of file