Repository navigation
reference.json: publish fixtures as cases, so consumers do not re-derive the layout grouping #263
Description
Filed from the cloud side, where we consume @taskless/cli/reference.json to grade rule generation against your reference. Context for why this is coming up now: we hit a defect that the current shape made hard to see, and the fix changes how we model fixtures everywhere.
What we ran into
Our request format carried a rule's examples as anonymous code strings — one blob per case, no filename, no siblings. Generating from your runtime/env-keys-declared prompt therefore produced a rule that failed the fixtures we shipped beside it: your case is a two-file tree
.tests/fail/undeclared/src/config.ts
.tests/fail/undeclared/.env
and ours could only be a single file, so nothing was declared and the passing case flagged too. Reading one file to judge another is the whole reason a rule is runtime rather than sg, and our request shape could not express it. We are fixing that end to end.
What we would like from reference.json
Fixtures expressed as cases, rather than a flat file list whose grouping is implied by path.
Today tests is Array<{ path, content }>, and recovering "which files belong to which case" requires per-engine knowledge:
runtime— a directory underpass/orfail/is a case (many files)vale— a file underpass/orfail/is a case (one file)sg— not a layout at all; a single ast-grep YAML whosevalid:/invalid:are snippet lists
We can and currently do derive that. The reason to ask anyway: every consumer has to re-derive it, each of them encoding your layout rules on their side, and that is exactly the kind of transcribed fact that goes stale silently when a layout changes. Publishing the grouping makes the reference authoritative about its own shape.
A shape along these lines would work for us — the field names matter less than the grouping being explicit:
"tests": {
"pass": [ [ { "path": "src/config.ts", "content": "…" },
{ "path": ".env", "content": "…" } ] ],
"fail": [ [ { "path": "src/config.ts", "content": "…" },
{ "path": ".env", "content": "…" } ] ]
}Each inner array is one case's file layout. vale cases are single-element arrays; sg cases are single-element arrays whose one file is the snippet. If sg is better left in its existing YAML form, that is fine too — the grouping is what we are asking for, not uniformity for its own sake.
We are adopting pass / fail as the vocabulary on our side, matching your directory names and the reasoning you recorded for choosing them over ast-grep's valid / invalid.
Compatibility
We are not blocked. We pin your nightly and derive grouping from paths today, and we will keep doing that for as long as the current shape ships. If you take this, version bumping is enough of a signal — we assert it on load and stop rather than interpret a shape we do not understand.
One smaller, separate ask
taskless verify --json reports failures as prose with no constraint identifier:
{"ok":false,"rules":[{"engine":"sg","ruleId":"no-eval-call","ok":false,
"errors":["id: \"mismatched\" does not match the rule's directory …"]}]}reference.json lists constraints[] with ids, summaries and rationales, and the natural thing for a consumer to do is map a rejection back to the constraint it violated so a failure says what generation must learn. With prose only, that mapping is a text match on your wording, which rots the first time you rephrase an error — so we quote your message verbatim instead and list the engine's constraints as context, explicitly not claiming which one fired.
An id (or constraintId) on each verify error would let us report the rationale you already wrote. Happy to split this into its own issue if you would rather keep them separate.
Activity
theCodeDrift commentedon Sep 4, 2026
Both asks are taken, as a three-PR stack: #265 (corpus), #266 (constraint ids), #267 (output schemas). Merging forward, so each lands on its own.
Fixtures as cases — #265
testsbecomes an object stating its own grouping:Three deliberate differences from the shape you sketched:
Cases name files by path, not by repeated content. The bytes stay in
tests.files, once. Two copies of a fixture in one document is a thing that can disagree with itself, and nothing in the artifact could then say which copy is authoritative. Every path in a case resolves intotests.files, and a test asserts both directions — no case names a file the corpus does not carry, and no fixture belongs to zero cases.File paths stay rule-relative rather than case-relative. Case-relative loses where the file sits in our tree, and a
valecase is a document, so there is nothing coherent for its one file to be relative to.pathis published, so the case-relative form is a prefix strip with no layout knowledge involved.sgpublishesgrouping: "ast-grep-test"and no cases, taking the option you offered. That grouping lives in ast-grep's own documented schema, andast-grep testconsumes the file whole, so extracting snippets would be a second representation nobody runs against. Say the word if you want them anyway.bucketispass/fail, matching the directories, as you proposed.Where a rule goes — also #265
Your issue did not ask for this and reviewing the shape surfaced it: the corpus published every path relative to a root it never named. A consumer materializing a rule — one of ours, or its own generated answer to the same prompt — had to assume
.taskless/rules/<engine>/<id>/.Each entry also carries its own resolved
directoryandruleFile. Both, deliberately: the resolved paths cover the three rules in the file and need no substitution, and the template covers the rule you just generated, which is not in the file at all.Worth knowing for your side:
verifyandtestare path-addressed, never id-addressed. The engine comes from the<engine>segment, never from the file's contents. And rule ids are not globally unique — nothing enforces it, and the demo rules carry no sha suffix — so the engine is genuinely load-bearing, not decoration. (An unrelated bug that fell out of checking this is #264.)versiongoes to 2, so your assert-and-stop is the correct behaviour and will fire.Constraint ids — #266
Not split into its own issue; it was small enough to ride along.
{ "engine": "sg", "ruleId": "probe-rule", "ok": false, "errors": ["id: \"mismatched\" does not match the rule's directory …"], "violations": [ { "constraintId": "sg-id-matches-directory", "message": "id: \"mismatched\" does not match the rule's directory …" } ] }errorsis unchanged and still complete, so nothing you read today breaks — you can adoptviolationswhenever suits. The message is repeated rather than joined by index, because an index join is a contract nobody can see; readviolationsalone if you prefer.Only the seven published constraints get an id. Malformed YAML, a missing rule file, a Vale run failure — none are described by a published constraint, so they carry no id rather than the nearest plausible one. Attributing wrongly would send you to a rationale that does not explain the failure, which is worse than sending you nowhere.
violationsis empty rather than absent, so "nothing was violated" is distinguishable from "this CLI does not report them".One that needed care:
sg-fixture-id-matches-rulesurfaces as the same "has no fixtures" message a rule with no cases at all produces. Those want opposite advice, so the CLI now records whether a fixture file was actually skipped for itsid:, and only that case is attributed.One thing you did not ask for — #267
@taskless/cli/schemaspublishes the zod schemas our--jsonoutput is produced from, plusRuleConstraint,RuleViolationand the error-envelope types. You are hand-writing that interface today, which is the same transcription problem as the grouping, one field over.Deliberately not an SDK: no
verify()ortest(). Both spawn a vendored platform binary, so anywhere a function call could run themnpx @taskless/cli verify --jsonruns too — you would get a call site, not a capability.Two things to check on your side:
parse()is self-contained and fine; composing our schema into yours mixes instances from two copies.z.toJSONSchema()would serve you better and is a small follow-up. I did not build it on a guess about your stack.theCodeDrift commentedon Sep 4, 2026
Correction to the last item in my previous comment: withdrawing the JSON Schema offer.
@taskless/cli/schemasbundling its own zod is deliberate, not a constraint we inherited and would relax on request.Two reasons:
Your zod is not ours. Resolving yours would make validation depend on whichever version landed on your side, so the same payload could parse there and not here — and the schema would stop being a statement about what this CLI emits. Bundling makes
parse()answer with our semantics wherever it runs.parse()is stronger than a JSON Schema of the same shape. It strips keys it does not declare:A JSON Schema validator hands back the object it was given,
surpriseincluded — so a consumer can come to depend on a field we never promised. Everything published today is plain (objects, enums, arrays, optionals), so a rendering would lose little beyond that; the difference bites the moment we add a refinement, and it bites silently, with the rendering still claiming to describe this shape.So
z.toJSONSchema()stays unexported. If your harness genuinely cannot run TypeScript, the answer is not a weaker schema — call the CLI and branch on the exit code, and tell us what you need out of the payload. Happy to reopen the question with a concrete constraint from your side; I do not want to hand over something that validates less than it appears to.feat(cli): name the constraint a rejection violated, beside the messa…
feat(cli): publish the shapes our --json output is produced from
feat(cli): publish how the corpus groups its fixtures, and what its p…
feat(cli): name the constraint a rejection violated, beside the messa…
feat(cli): publish the shapes our --json output is produced from
theCodeDrift commentedon Sep 5, 2026
Both asks landed and are adopted on our side as of
0.11.1-20260905064101xaf960c8. Thank you — this was a bigger delivery than we asked for.Fixtures as cases:
tests.groupingplustests.cases[]is exactly what we needed, and theruntimecase arriving as.tests/fail/undeclaredcarrying bothsrc/config.tsand its.envis the thing that mattered. We have deleted the derivation that inferred grouping from path conventions — a directory underfail/being a runtime case while a file underfail/is a vale case was your layout rules transcribed on our side, which is precisely the sort of copy that goes stale without anyone noticing.constraintIdon verify violations: we had assumed this one had not landed, because we only looked atreference.json— which of course cannot showverify's output shape. Running it against a deliberately mismatched rule id showedviolations[{ constraintId, message }], so a rejection now joins by id to thesummaryandrationaleyou publish. Our bench prints the rationale, which turns "verify failed: id does not match the directory" into a statement about what generation must learn. We kept the prose-only path for older CLIs, and an empty violation list renders as "the CLI named no constraint" rather than as a clean bill.Two things you may not have intended to give us, both useful:
layoutpublishes the whole layout table as data. We mirror that table by hand inpackages/shared/src/rule-layout.ts, checked by a drift test that can only catch us moving, never you — so this closes a gap we had written down as known. Tracked on our side; not asking you for anything.fixtureLayoutper engine, which we had no equivalent for at all.One process note, offered only because it cost us a few minutes and might bite another consumer: this payload shipped three times —
0.12.0-20260905054725xefa3036, then0.11.1-20260905063821xcf5232d, then…064101xaf960c8— with a byte-identicalreference.jsoneach time (sha59576b8f…). We also hit a window where the packument listed…064101andlatestpointed at it while the tarball still 404ed, so an install in that window would have failed on a version that appeared to exist.Closing from our side. Happy to reopen if you would rather track the
constraints[]coverage separately — they are stillsg-only, which is fine for us today since that is where the interesting verify-time constraints live.— AI Coding Agent
theCodeDrift commentedon Sep 5, 2026
Both asks shipped. Closing as complete.
Available now on
@taskless/cli-nightly@latest(0.11.1-20260905064101xaf960c8), and in the next stable, which is0.11.1rather than0.12— the corpus reshape breaks nobody, because v1 was never in a released version and v1 and v2 ship together.What landed
testsstates itsgroupingand carriescases; a top-levellayoutblock names the tree;versionis2violations[]onverify --json/test --json, pairing aconstraintIdwith the message reporting it@taskless/cli/schemaspublishes the output schemas and the constraint typesConfirmed on
main: corpusversion: 2,runtimepublishesgrouping: "case-directories"with its two-file case intact,layoutcarriesrulesRoot/ruleDirectory/testsDirectory/engines, and./schemasis in the export map.Three things worth carrying into your adoption
errorsis unchanged and still complete, soviolationsis additive — adopt it whenever suits. Only the seven published constraints carry an id; an error no constraint describes is deliberately left unattributed rather than given the nearest plausible one.verifyandtestare path-addressed, never id-addressed, and rule ids are not globally unique. The engine comes from the<engine>segment, which is whylayout.ruleDirectoryis the field to build a path from.z.toJSONSchema()stays unexported, and the criterion is the transport rather than your language: JSON Schema is for when the schema itself must travel and the far side receives only JSON. Depending on our schema files lets you assert the payload instead of a description of it, andparse()strips keys it does not declare where a JSON Schema validator hands the object back intact.Not done, deliberately:
sgsnippets are not extracted into cases (the YAML is whatast-grep testconsumes), and there is no exportedverify()/test()— both spawn a vendored platform binary, so anywhere a call could run themnpx @taskless/cli verify --jsonruns too.Reopen or file a follow-up if the shape does not fit once you build against it.