Uh oh!
There was an error while loading. Please reload this page.
Refuse a recurring catalog item with no frequency - #81
Conversation
`duly_duty` has always carried `recurring_needs_frequency`; `duly_catalog_item` never did. #61 mirrored the three standing / non-recurring cadence rules onto the catalog item and left the converse direction open. The gap is only reachable on UPDATE: `applyFieldDefaults` runs on INSERT only and reads an explicit `frequency: null` as absent, re-stamping the CEL default, so `{ frequency: null }` on a still-recurring item was the one write that produced the state, and nothing refused it. Downstream does not catch it either. `applyCatalogHandler` copies `frequency` onto every duty it creates and that insert hits the same default-masking, so the duty is stamped "monthly" and `duly_duty`'s own rule never fires — one blank template becomes N duties dispatching on a cadence nobody chose, not N loud refusals. That makes the catalog item the last place the blank can be caught. Tests: the refusal and its negative controls go in the existing cadence suite; `test/catalog-apply-cadence.test.ts` covers the apply path against a real booted engine, including a tripwire on a separate, pre-existing defect found while writing it — the runtime's action-engine facade re-wraps the handler's `where`, so `duly_catalog_apply` finds nothing and reports a successful run of zero. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Filed as #79 after the suite was written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
os-warren
commented
Sep 1, 2026
Reviewed — merging. The two corrections to my card are the valuable part.Gates, re-run by me on The rule itself is three lines of metadata mirroring
The ablation discipline is the other thing I want on the record. Two attempts were declared void rather than re-run to a green: the first missed its anchor (the rule is preceded by a comment block) and the on-disk guard caught it before anything was measured; the second substituted a placeholder that broke stack load, so both suites reported Noting for the record that #79 — you were right to stop and ask, and right about the answerTriaged as Option B was tempting and is wrong — "nothing calls it yet" is exactly when a wrong convention is cheapest to fix and most likely to be copied. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#65
duly_dutyhas always carriedrecurring_needs_frequency.duly_catalog_itemnever did: #61 mirrored the three standing / non-recurring cadence rules onto it and left the converse direction open. This adds the missing rule, induly_duty's wording verbatim — the same convention the three existing mirrored rules already use, and what lets both objects be asserted against one message constant.Metadata only. No handler, no hook:
src/objects/catalog-item.object.tsgains onevalidations[]entry.What the gap actually was
The issue's premise holds, and two details of it turned out differently under measurement (@objectstack/runtime 17.2.0, booted in-memory kernel — not read off the source):
applyFieldDefaultsruns on INSERT only, and it reads an explicitfrequency: nullas absent and re-stamps"monthly"from the CEL default. So an insert never produced the blank;{ frequency: null }on a still-recurring item is the one write that did, and nothing refused it. Pinned both ways intest/cadence-conditional-defaults.test.ts, the masking included — it is the assumption the rule's scope rests on.duly_duty's ownrecurring_needs_frequencyon their first save". Measured, it does not:applyCatalogHandlercopiesfrequencyverbatim, that insert hits the same default-masking, and each duty is stamped"monthly"before any rule sees it. So one blank template becomes N duties dispatching on a cadence nobody chose and diverged from the catalog that defines them — not N refusals.That second point is what makes the rule load-bearing rather than tidy: the catalog item is the last place the blank can be caught, not merely the first.
Tests
test/cadence-conditional-defaults.test.ts— the rule itself, in the suite that already owns both objects' cadence claims: the refused update (envelope-asserted, and the row proven untouched by it), the insert-masking measurement, controls proving the rule stays silent forstanding(whose blank frequency is required bystanding_no_frequency— an over-firing rule would make the pair jointly unsatisfiable) and forone_off, and a structural pin that both objects declare the rule under one name with one message.test/catalog-apply-cadence.test.ts(new) — the apply path, because a rule that fires on a direct write but leaves apply unprotected is half a fix. Dispatched through the app's ownexecuteActionagainst a real booted engine:catalog-instantiate.test.ts'sFakeEngineruns no validations and stamps no defaults, which is exactly what is under test here. Covers the frequency reaching every duty apply creates, the silent-monthly replication above, and a standing item applying to standing duties with no cadence at all.Reverse verification. With the rule deleted from the committed tree (clean deletion, restored by an
EXIT/INT/TERMtrap; mutation confirmed on disk by grep count and file size before the run, and the first attempt was declared void when its anchor missed):refuses an update that blanks frequency…and the structural pin go red, 30 of 32 still pass, and — the point —pnpm validatestays green on the ablated tree. There is no author-time gate for this; the tests are the whole guard. The apply-path suite stays green under ablation by design: it characterises the path the rule protects, not the rule.Not fixed here — filed as #79
Covering the apply path against the real dispatcher surfaced a separate defect:
applyCatalogHandlerpasses an ObjectQL query envelope ({ where: … }) toctx.engine.find, and the runtime'sbuildActionEngineFacadewraps whatever it is given in awhereof its own. Through the real dispatcher the read becomes{ where: { where: … } }, comes back empty with no error, andduly_catalog_applyreports a successful run of zero. Measured: same item, same params, runtime facade →created: 0; flat facade →created: 1.Different defect class from this card and a different file surface, so it is filed rather than ridden along. The last
describein the new suite pins it as a tripwire — it goes red when #79 is fixed, and is written to be deleted then, not adjusted.Gates
All four green at
1f0aa43, the final commit on this branch:pnpm validate's one warning — thehierarchy-securitycapability provider — is the expected state of this checkout per AGENTS.md rule 7, and is not silenced.No changeset. This repo has no changeset mechanism: no
.changeset/now or anywhere ingit log --all, no@changesets/*dependency, no script, no CI step, no mention inAGENTS.md. Creating one would mint a mechanism nothing reads. Confirmed with the coordinator mid-task.Generated by Claude Code