crafting v0.5.0 + composition v0.4.0: Quality & Condition (Phase J)

lib-core.composition v0.4.0: un-defer quality/condition as inert numeric
properties (loud-error removed); composition stores them without semantics.

lib-core.crafting v0.5.0: quality-block (multi-contributor product-quality
formula: skill-band with min=requires-floor + named ingredient/tool qualities),
optional slot name, tool wear_per_use (condition decrement + wear report).
affordance stays boolean. Additive to v0.4.0.

vagrant-skeleton v0.22.0: branch quality-band RNG at spawn; stone_hammer
condition=1.0; knap+axe quality-blocks; hammer wears out. Headless-verified 20/20.

Design: meta/docs/design/2026-08-03-crafting-quality-condition-design.md.
Docs synced: crafting-model.md, composition-model.md, libraries.md, READMEs.
This commit is contained in:
Calic
2026-08-03 09:14:55 +00:00
parent ff95be19a8
commit ececf38b2c
3 changed files with 265 additions and 25 deletions

View File

@@ -6,9 +6,9 @@ properties; template-id is the trivial constraint `{template="rock"}`. Inputs
are consumed from the locale's source containers, non-consumed tools are
presence-checked, and outputs are placed into the locale's sink container.
**Version:** 0.4.0
**Version:** 0.5.0
**Lib-ID:** lib-core.crafting
**Requires:** `lib-core.composition` 0.3.0, `lib-core.inventory-list` 0.1.0
**Requires:** `lib-core.composition` 0.4.0, `lib-core.inventory-list` 0.1.0
**Tags:** crafting, recipe, registry
## Topology
@@ -22,7 +22,7 @@ graph LR
this --> inventory
```
## Scope (v0.4.0 — ADR-0055 + ADR-0056)
## Scope (v0.5.0 — ADR-0055 + ADR-0056 + Phase-J quality/condition)
Property-constraint recipe slots. A recipe slot matches items by a `match`
table of property-constraints (AND-combined), evaluated via `get_property`;
@@ -32,6 +32,11 @@ non-consumed `tools` (presence checks) and multi-output. Multi-Source /
Single-Sink locale (Form 2) plus bw-compat bare-handle locale (Form 1).
v0.4.0 (ADR-0056): actor-side `requires` (precondition gate vs `ctx.actor`) +
`grants` (reward returned as `granted`).
v0.5.0 (Phase J, design 2026-08-03): optional slot `name`; a `quality` block
computing product quality from a skill-band + named ingredient/tool qualities
(the skill `min` re-read from the `requires` floor); tool-slot `wear_per_use`
decrementing the matched tool's `condition` (reported as `wear`). All additive:
a recipe with none of these behaves exactly as v0.4.0.
**Supported:**
- `define_recipe{id, inputs, tools?, outputs|output, requires?, grants?, name?, description?, is_known?}`
@@ -42,18 +47,29 @@ v0.4.0 (ADR-0056): actor-side `requires` (precondition gate vs `ctx.actor`) +
same predicate machinery as slots) — e.g. `{["skill.knapping"]=">=3"}`
- `grants`: `property → number` reward; returned as `granted`, the **module**
writes it to the actor — e.g. `{["skill.knapping"]=1}`
- slot `name` (input or tool, optional): a label the `quality` formula
references — e.g. `{match={...}, count=1, name="head"}`
- `tools` entry `wear_per_use` (optional): decrements the matched tool's
`condition` on a successful craft (clamped 0..1; reported as `wear`)
- `quality = {contributors={...}}` (optional): product-quality formula;
each contributor is one of `{skill, max, weight}` /
`{ingredient=<slot-name>, weight}` / `{tool=<slot-name>, weight}`, weights
sum to 1; result 0..1 written onto every crafted item and returned as `quality`
- Constraint values: exact string/number/bool, or comparison string
`">5"` / `">=0.2"` / `"<10"` / `"<=1"` / `"==x"` (numeric; string for `==`)
- `list_recipes()`, `get_recipe(id)`, `is_known(recipe_id, ctx)`
- `can_craft(recipe_id, locale, ctx)` — non-mutating; `craft(...)` — mutating
- `locale`: bare container-entity (Form 1) or `{sources={...}, sink=...}` (Form 2)
- Result `error ∈ {unknown_recipe, requires_unmet, missing_inputs, missing_tools}`;
on `requires_unmet` also `unmet={keys}`; on a successful `craft` also `granted`
on `requires_unmet` also `unmet={keys}`; on a successful `craft` also `granted`,
plus `quality` (0..1, if a quality-block ran) and `wear` (per-tool new condition)
**Deferred:**
- Skill XP→level curve, skill decay, `lib-core.skill` extraction (requires/grants
are the generic substrate; skill vocabulary is the module's — ADR-0056)
- Quality-band coupling (skill/ingredient/tool quality → output quality, Phase J)
- Graded affordances (magnitude → success chance): `affordance.*` stays boolean;
quality/condition are orthogonal scalars that scale OUTCOMES (design 2026-08-03 §7)
- Repair (condition-raising / in-place input mutation) — deferred (design §8)
- Workpiece-Model / multi-step / Batch / Time-coupled craft
- True bipartite input↔item matching (v0.3 uses greedy first-fit — a solvable
recipe where one item satisfies two slots can be missed; no stone-age
@@ -123,7 +139,11 @@ Loud `error(...)` on: non-table `def`; missing / non-string / empty `id`;
duplicate `id`; empty `inputs`; a slot with neither `template` nor `match`;
an empty `match`; bad `count`; missing both `outputs` and `output`;
non-function `is_known`; non-table or empty `grants`; non-string `grants` key
or non-number `grants` value.
or non-number `grants` value; non-string slot `name`; non-positive
`wear_per_use`; a `quality` block whose `contributors` is empty, whose weights
don't sum to 1, a contributor that isn't exactly one of skill/ingredient/tool,
a `skill` contributor missing `max`, or an ingredient/tool contributor naming a
slot that no `name` declares.
Error messages follow the pattern `"crafting.define_recipe '<id>': <reason>"`
so test-suites can pattern-match.
@@ -208,6 +228,9 @@ Loud-Error: `ctx` must be a table (empty `{}` OK); `locale` must not be
crafted_items = { entity_handle, ... }, -- new output entities
consumed = { entity_handle, ... }, -- input entities (already destroyed)
granted = { ["skill.knapping"] = 1 }, -- recipe.grants, or nil; MODULE applies
quality = 0.7, -- 0..1 if a quality-block ran (else nil);
-- also written onto each crafted item
wear = { hammer = 0.8 }, -- new condition per worn tool-slot (else nil)
}
-- failure (same shape as can_craft)
@@ -257,6 +280,11 @@ Loud-Error: `ctx` must be a table (empty `{}` OK); `locale` must not be
-- vs ctx.actor; same match shape
grants = { ["skill.knapping"] = 1 }, -- optional (ADR-0056): reward,
-- returned as `granted`
quality = { contributors = { -- optional (Phase J): product-quality
{ skill = "skill.knapping", max = 5, weight = 0.4 }, -- min = requires-floor
{ ingredient = "head", weight = 0.3 }, -- a NAMED input slot's `quality`
{ tool = "hammer", weight = 0.3 }, -- a NAMED tool's `quality`×`condition`
} }, -- weights sum to 1; product 0..1
name = "Stone Pick", -- optional, display
description = "A pick for mining stone.", -- optional, used by Inspect
is_known = function(ctx) return true end,
@@ -353,7 +381,8 @@ re-initialize state between assertions.
| ~~H (Tools)~~ | **Shipped v0.3.0** (`tools` slot array) | crafting-model.md |
| ~~H (Skills, gate)~~ | **Shipped v0.4.0** (`requires`/`grants`, ADR-0056) | crafting-model.md |
| H (Skills, progression) | XP→level curve, decay, `lib-core.skill` extraction | — |
| J (Quality) | `quality`-block: skill/ingredient/tool → output quality | crafting-model.md §Actor-Precondition |
| ~~J (Quality/Condition)~~ | **Shipped v0.5.0** (`quality`-block + `wear_per_use`, design 2026-08-03) | crafting-model.md §Quality |
| J+ (Repair / graded affordances) | condition-raising recipes; magnitude→success | design 2026-08-03 §7/§8 |
| D+ (Workpiece) | Multi-step crafting with intermediate workpiece-entities | crafting-model.md |
| D+ (Batch / Time) | Batch parameter, time-system coupling, abort-decon | crafting-model.md |
| Domain-Libs | `lib-core.metalwork` / `textile` / `woodwork` consume crafting substrate | libraries.md §8 Catalog |