Changelog
Copy MarkdownAll notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.1.0] - 2026-07-31
Added
Cooper.load_file/2andCooper.load_string/2, loading a CASC config file (or source string) into native Elixir terms end to end: lex+parse, loop expansion, import resolution, merge, reference resolution, and secret-wrapping.- A complete CASC grammar (
priv/grammar/casc.aether, built on Ichor, a sibling grammar-compiler library): version headers, comments, disabled statements, key paths and nested blocks, dotted and quoted key segments, secret keys (*key), and every literal form CASC.md §6 defines — nil, booleans, integers (decimal/hex/octal/binary), floats, atoms, dates and times, durations, byte sizes, strings (double/single/triple-quoted), lists, and tuples (kept as real Elixir tuples, never coerced to lists). Cooper.IPv4/Cooper.IPv6(CASC.md §6.7): dedicated, validated types for IP address literals (127.0.0.1,::1/128) — an out-of-range octet, malformed address, or CIDR prefix outside0..32/0..128fails at load time with a named error, never a crash. Both support CIDR containment (contains?/2) and network math (network/1,netmask/1,first_host/1,last_host/1, plusbroadcast/1onCooper.IPv4).- Variables (
@name/@*name, public/private) and all five interpolation/reference forms (CASC.md §7):@{}variables,${}environment reads (with:default,:+alt,:?"required", list, and indexed forms),%{}lazy config references (resolved against the final merged tree, with cycle detection),!{resolver:payload}extensible dispatch, and!Name(arg)tagged values — five built-in tags (int/float/bool/duration/bytes) plus consumer-registered ones via the:tagsoption. - The merge model (CASC.md §8): deep-merge by default,
~key { ... }whole-subtree replace,+key/-keylist append/remove,-key.pathdelete, and a hard error (never a silent coercion) merging into a tuple. forloops (CASC.md §5.5): index and element bindings, parallel (zipped) multi-binding iteration, interpolated destination paths, andfrom <template>(a lazy base resolved against the final tree, with the loop body layered on top as overrides).importstatements (CASC.md §5.1): bare paths (with{a,b}brace and**glob expansion),scheme://dispatch via the:import_schemesoption, import-cycle detection, and public-variable propagation across import boundaries.Cooper.Secret, wrapping every*key-prefixed value —inspect/1andto_string/1redact unconditionally;Cooper.Secret.reveal/1is the only way to the real value. Wrapping happens at merge time and travels with the value through any later%{...}reference orfor ... fromtemplate copy, rather than staying pinned to the value's original declared path. A secret embedded inside a larger interpolated string redacts only its own portion, not the whole string, and independently for each of several secrets interpolated into the same string.- Two interchangeable backends per grammar (
Grammar.Nativeby default,Grammar.VMkept for parity/benchmarking) — seebench/native_vs_vm.exsandtest/cooper/backend_parity_test.exs. test/SPEC_COVERAGE.md, a traceability table mapping every section of CASC.md to the test(s) covering it, plus one documented gap: backslash-continued strings (CASC.md §6.5's fifth string form) are not implemented, since the spec gives no worked example to validate an implementation against.guides/EXAMPLES.md,guides/casc/TUTORIAL.md,guides/casc/CASC_EXAMPLES.md, andguides/casc/CASC_CHEATSHEET.md— every example in all four is verified against a realCooper.load_string/2call, not just written by hand.mix precommit(an alias inmix.exs):format,compile --warnings-as-errors,credo --strict,sobelow,test,dialyzer, in that fast-to-slow order, all underMIX_ENV=testso Credo/Dialyzer seetest/support/too. Backing dev/test dependencies:credo,dialyxir(PLT built with:mix/:ichoradded explicitly, sinceichor'sruntime: falsehides it from dialyxir's automatic OTP-app discovery even thoughtest/support/vm_parity.exgenuinely calls into it),sobelow,excoveralls, plusmox/faker/stream_dataavailable for tests that need them (none do yet)..credo.exsand.dialyzer_ignore.exs(the latter narrowly scoped to two known-benign Dialyzer findings aroundMapSet's opaque type in generated code, not a blanket suppression) are new at the repo root. SeeCONTRIBUTION.md's "Making a change" §4.
Changed
- Ichor split into
ichor(the Aether front-end,Grammar.Analysis, both codegen backends -- dev-time-only) andichor_runtime(the small support library generated code actually calls at runtime):priv/grammar/casc.aether/casc_interp.aetherare now compiled ahead of time bymix ichor.geninto checked-in modules (lib/cooper/native_grammar/native.ex,lib/cooper/native_interp_grammar/native.ex) instead of at Cooper's own compile time viause Ichor, grammar:, actions:, andlib/cooper/native_grammar/capture_shapes.ex(scripts/gen_capture_shapes.exs) does the same for the one other pieceCooper.Grammar'srun_with_context/3needed fromichorproper at runtime.mix.exsnow depends onichor_runtimeas an ordinary runtime dependency andichoritselfonly: [:dev, :test], runtime: false-- confirmed viaMIX_ENV=prod mix deps/mix compilethat a release build now touches onlyichor_runtime. TheGrammar.VMparity backend (bench/native_vs_vm.exs,test/cooper/backend_parity_test.exs) moved out oflib/intotest/support/vm_parity.exaccordingly, since it still needsichorproper andlib/compiles in every environment including:prod. ichor/ichor_runtimenow depend on their Hex-published releases (ichor ~> 0.2.1,ichor_runtime ~> 0.1.0) instead of Ichor's pre-mergefeature/runtimegit branch --ichor_runtimeis its own independently-published, independently-versioned package now (not a subdirectory ofichor's own repo), so nooverride:/sparse:wiring is needed the way there was while both lived behind onegit:spec.lib/cooper/native_grammar/native.ex,lib/cooper/native_interp_grammar/native.ex, andlib/cooper/native_grammar/capture_shapes.exregenerated against 0.2.1'smix ichor.gen; behavior unchanged (0.2.x's own changes were theichor/ichor_runtimesplit and docs -- see Ichor's own CHANGELOG.md).RESOLVER_REF_RAW(!{resolver:payload}'s token, in bothcasc.aetherandcasc_interp.aether) now uses Ichor 0.1.1's@native(...)token-position escape hatch (new module:Cooper.Native.ResolverRef) instead of a fixed one-level-of-nested-braces combinator form — a payload may now brace-nest to any depth, matching CASC.md §7.4's own "brace-balanced" wording exactly, rather than the previous implementation's slightly narrower approximation of it.- The
Grammar.VMparity backend (nowCooper.Test.VMParity.run_with_context_vm/3, see above) updated for Ichor 0.1.1'sGrammar.VM.Lexer→Grammar.VM.Tokenizerrename and its newcustom_lexemes/@keywords/@refine-reclassification pipeline stages.
Fixed
Cooper.Loader's per-importsub_ctxnow carries:envforward — previously, a${?NAME}conditional statement (CASC.md §7.2) inside an imported file crashed withKeyError: key :env not foundinstead of reading the importer's own environment, since the imported file's own parse-time context silently dropped that key.