catalog
def catalog()->str:The blocks and fns, with their purpose and props, as text for the prompt.
author(concept, ask) asks a model for one demo on a concept. The model writes data only: a JSON spec. It never writes code. Then the loop checks the spec, and gives the errors back to the model to fix:
validate the spec: types, binds, gates and the think rule.check the spec in headless Chrome: it draws, it plays, the keys work, the contrast is good.None and log the errors. The caller keeps its text lesson.ask(prompt, sp) is any function that returns text, as in abhyasa. The tests use a fake one.
The prompt lists the blocks and fns from their manifests, so it never names a block that the runtime does not have.
The blocks and fns, with their purpose and props, as text for the prompt.
The catalog names every block and every fn.
The system prompt gives the rules of a good demo, the shape of a spec, and the catalog.
The user prompt describes the concept, the first error of the learner (if there is one), and two gallery demos as examples. near picks the gallery demos that share the most words with the concept. A long table in an example is cut to three rows.
The k gallery demos that share the most words with concept.
The prompt that asks for a demo on concept, a dict with name, statement, subject and misconceptions.
The repair prompt gives back the spec and only its errors.
The prompt that asks the model to fix only errors in spec.
parse_json finds the JSON object in a reply, also inside a fence or after some text.
The first JSON object in s. Raise ValueError when there is none.
author runs the loop. It stamps the spec with the schema, an id and the concept slug. log, when given, gets one entry for each failed try.
A checked demo spec on concept from the model ask, or None after repairs failed repairs.
fake replies from a list, one reply for each call, and keeps the prompts.
A good spec passes on the first call. The spec gets the concept slug.
A spec with one error gets one repair. The repair prompt names the error.
broken = json.loads(good); del broken['blocks'][1]['after']
a, log = fake(json.dumps(broken), good), []
s = author('Sums', a, check=False, log=log)
assert s is not None and len(a.calls) == 2
assert 'gives away the answer of gate "sum"' in a.calls[1] and 'Fix only these errors' in a.calls[1]
test_eq(log[0]['tries'], 1)Three bad replies give up quietly: the result is None, and the log has three entries.
A model that fails gives up too, with no exception.
With a browser, check runs too, and a good spec still passes in one call.
A live run uses a real model, so it is not part of the tests. With the rishi package and its model claude/sonnet, it looks like this:
The first live run (26 October 2026, claude/sonnet, three concepts: guṇa sandhi, cosine similarity and binary search):
| concept | result | model calls |
|---|---|---|
| guṇa sandhi | passed on the first try | 1 |
| cosine similarity | passed after one repair: a point showed the answer before the gate, and the think rule caught it | 2 |
| binary search | passed on the first try | 1 |
So 3 of 3 passed, 2 of them on the first try. One spec put {n} in its plain-text ask. The check could not see it, so validate now refuses a template in a plain-text prop.