AGPL-3.0-or-later · forever.

the path · 13

Extract facts, then the law — the model never writes the verdict.

run itnika try 13-extract-then-law

13-extract-then-law.nika.yamlsource
# SPDX-License-Identifier: Apache-2.0# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json## 13 · Extract facts, then the law — the model never writes the verdict.## 04 taught a typed extract. This lesson is the next wall: once the# facts are integers, a `nika:jq` (or the decide builtin, spec 11) is the judge.# Paying a second infer to "pick the level" is the expensive mistake# this file exists to unlearn (measured 2026-08-19 · a 40+ task# extract→law run burned tokens on schema digits, anyOf fences, and# infer-before-check).## Demonstrates ·#   - `infer.schema:` with `type: integer` + a numeric `enum`#     (`-1|0|1|3`) — never `enum: ["0","1","3"]`. Models emit JSON `3`.#     Hint `digit-string-enum`.#   - `for_each:` over OBJECT items · the prompt reads `${{ item.stem }}`#     / `${{ item.text }}` (resume-eligible as a WHOLE fan since 0.110).#   - `on_error: recover: null` · a bad extract is a hole, not a crash#   - `nika:jq` is the law · the model does not name a level#   - prove the law on const fixtures + `nika:assert` (hint `unproven-law`)#   - probe with `mock/echo` BEFORE a paid model (this file is that probe)## Next door · [`14-decide-publish`](14-decide-publish.nika.yaml) is the# same law with a named Decision Bundle (`nika:decide`).## Run · nika try 13-extract-then-law#   (offline · zero keys)nika: extract-then-lawmodel: mock/echopermits:  tools: ["nika:jq", "nika:log", "nika:assert"]const:  # Known answers for the jq law — computed by hand, not by a model.  # `unproven-law` stays silent when this prove + assert is present.  cases:    - null    - { parallele: -1 }    - { parallele: 0 }    - { parallele: 1 }    - { parallele: 3 }  # Two notes as objects so the body must navigate `item.field`.  notes:    - stem: ada      text: "one ticket at a time · CI hook · no human commit after merge"    - stem: rumor      text: "someone said they use ChatGPT"tasks:  extract:    for_each:      items: ${{ const.notes }}      max_parallel: 2      fail_fast: false    on_error:      recover: null    infer:      prompt: |        Extract facts only. Never write a level or a belt.        stem=${{ item.stem }}        ${{ item.text }}      max_tokens: 256      schema:        type: object        additionalProperties: false        required: [parallele]        properties:          parallele:            type: integer            enum: [-1, 0, 1, 3]            description: "-1 unknown · 0 none · 1 one stream · 3 many"  score:    with:      rows: ${{ tasks.extract.output }}    invoke:      tool: nika:jq      args:        input: ${{ with.rows }}        expression: >-          map(            if . == null or .parallele == -1 then              {level: "unknown", why: "missing facts"}            elif .parallele == 3 then              {level: "many", why: "parallel streams"}            else              {level: "one", why: "single stream or none"}            end          )  prove:    invoke:      tool: nika:jq      args:        input: ${{ const.cases }}        expression: >-          (map(            if . == null or .parallele == -1 then              {level: "unknown", why: "missing facts"}            elif .parallele == 3 then              {level: "many", why: "parallel streams"}            else              {level: "one", why: "single stream or none"}            end          ) | map(.level)) == ["unknown", "unknown", "one", "one", "many"]  compiled:    with:      ok: ${{ tasks.prove.output }}    invoke:      tool: nika:assert      args:        condition: ${{ with.ok }}        message: "jq law drifted from the hand-computed fixture map"  show:    with:      cards: ${{ tasks.score.output }}    invoke:      tool: nika:log      args:        level: info        message: "law applied · ${{ with.cards }}"outputs:  cards: ${{ tasks.score.output }}

nika-spec@332f5232d · sha256 91452e2f3f649cd5