Repository navigation
v0.7.0
Changelog
Added
- An agent skill for writing bloomery projects.
npx skills add morzecrew/bloomery
installsbloomery-skills: a routing index from a task to the references it needs,
covering specs, compiling, planning a change, quality rules, targets and the Python API.
Every spec example validates against the exported JSON Schema, every Python import is in
bloomery.__all__, and everybloomerycommand line names a command and flags the CLI
accepts. A census,skills/coverage.toml, gives every spec kind, emit target, CLI command
and quality-rule kind a reference that shows it or a reason it is out of scope, so a new
one cannot land unrecorded. Retrieval is out of scope until a project outside bloomery's
own examples declares aRetrievalSpec.
Changed
-
A catalog recipe's operands carry types. A recipe may declare
types:, a mapping
from names in itsrequiresto logical types written as a canonical field'stype:
is. Before a recipe'sexpr:is evaluated, each operand's extraction is cast to the
typetypes:names it, or else to the declared type of the canonical field of the same
name, so arithmetic runs on declared types rather than on whatever an engine infers
from a text-shaped extraction. A recipe whoseexpr:applies+,-(unary too),*
or/to an operand typed neither way, or to a namerequiresdoes not bind, is now
refused at compile time withUntypedRecipeOperand, naming the recipe, the operand and
the fix; atypes:key thatrequiresdoes not name is refused at load.Upgrade note. This is breaking for a catalog whose recipe arithmetic uses an
untyped non-canonical operand: its compile is refused until the recipe'stypes:gains
the entry the refusal names. Every recipe whoseexpr:reads a typed operand now casts
it, canonical ones included, so its silver model and the project's fingerprint move
even where the catalog needs no edit; recompile and re-plan. -
A catalog recipe's
/is exact. A recipe'sexpr:is parsed SQL rather than a built
transform, so its division carried no exactness marker: SQLGlot rendered it as a float
division — an explicitCAST(x AS DOUBLE PRECISION)on PostgreSQL andCAST(x AS DOUBLE)
on Trino — narrowed back to the declared decimal, so values needing more than ~15
significant digits could round. Every/in the recipe, nested ones included, is now
lowered through thedividetransform's marker. Its dividend is cast to a decimal at least
29 integer digits and 9 fractional wide, so integer operands divide fractionally, a
dividend wider than the quotient does not overflow, and a nested quotient is not rounded
before the next division; the result is narrowed to the declared type once. The division
is the transform's —
exact on PostgreSQL and Trino and rendered per dialect like any other division. DuckDB,
which has no exact decimal division, is unchanged in being inexact there.Upgrade note. A project whose catalog carries a dividing recipe compiles a different
silver model, so its fingerprint moves; recompile the project and re-plan against the new
fingerprint. A project with no dividing recipe is unaffected.
Removed
- The
inexact_divisionadvisory. A recipe's division is now lowered through the
dividetransform's marker, so no advisory stands where the reference's own rule says a
refusal belongs;AdvisoryCodecarriesundeclared_audienceonly.
Fixed
-
A SQL expression SQLGlot cannot convert is refused as an unparseable one. SQLGlot's
JSON-arrow path parser callsint()on a number token, so a text such asx->1e5or
x->>8emakesparse_oneraise a bareValueErrorrather than aSqlglotError. Every
handler around a parse caught the latter (andRecursionError), so a step body or a
sql_macrobody carrying one crossed the compile boundary as a rawValueError
(#281), and an authoredexpr:was refused at load with SQLGlot's own text instead of
bloomery's. The classes are now named once,(SqlglotError, ValueError), in
spec/common.py, and caught at each of the five parse doors, so such a text is a
StepErrornaming the step or aSpecParseErrornaming the field, as any other
unparseable text is.DepthCappedSqlstill passes it on for the later stage that owns
that field's refusals. -
A definition's timeline shows an exact division as a division. Its changes printed
the internalBLM_EXACT_DIV(...)marker wherever adividetransform or a catalog
recipe's/moved a body. They now print the division it renders as:
CAST(CAST(total AS DECIMAL(38, 9)) / qty AS DECIMAL(12, 4))forecom_basic's
unit_price. The comparison still reads the IR's own text, so a body that changed
still reports as changed. -
A declared
tax_basis: unknownis refused beside money. The tax-basis check
tripped only on an absenttax_basis, so a field that declaredtax_basis: unknown
explicitly passed it:amount + adjcompiled and added a figure whose canonical field
says nothing usable about tax.unknownnow poisons additive arithmetic by being
absent or declared, exactly as the guardrails page states. -
InvariantViolatedreaches the shell as an internal error. It subclasses
BloomeryError, so the command line's refusal arm claimed it and exited1— reading a
bloomery bug as though the spec were at fault. It is now handled ahead of that arm: exit
3, the traceback on stderr under the report line, as any other defect is.