Skip to content

Forbid a frequency (and the neighbouring cadence fields) on a standing duty - #66

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-61-standing-no-frequency
Sep 1, 2026
Merged

os-warren merged 1 commit into
mainfrom
claude/issue-61-standing-no-frequency

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes #61

What changed

frequency — and the four fields the issue asked me to check alongside it (due_anchor, due_offset_days, lead_days, grace_days) — now carry conditional defaults on both duly_duty and duly_catalog_item, plus validation rules that refuse the meaningless combinations outright (Option A from the issue; B — hiding it in the UI — was rejected there because it leaves the wrong value in the data).

The scoping is not uniform across all five fields, because it follows what dispatch.plan.ts#planForDuty actually reads, not a blanket "non-recurring" rule:

Field Blank + forbidden on Still applies to Why
frequency standing recurring, one_off Adjudicated scope (Option A mirrors recurring_needs_frequency's converse, standing only). One-off's equally-meaningless frequency is a separate, narrower case this issue didn't ask me to touch.
due_anchor, due_offset_days, lead_days standing, one_off recurring only These compute a period due date; only a recurring duty has a period (planForDuty returns before reading any of the three for the other two forms).
grace_days standing recurring, one_off Measures lateness against a task's due date. A one-off's task has a real one (set directly, not computed); only standing, which never has a task at all, loses it.

Mechanism: each default is now a CEL defaultValue using the blessed null-guard idiom (cond ? value : null, objectstack#3306 — the same evaluator Field.formula shares), e.g.

defaultValue: F`record.form == "standing" ? null : "monthly"`,

form is declared above every cadence field, so by the time each of these runs, record.form is already resolved. Four new validation rules pair with the defaults (two on duly_duty mirror recurring_needs_frequency's converse, two apply the same treatment to duly_catalog_item; see the object files for the exact set — standing_no_frequency, non_recurring_no_due_timing, standing_no_grace_days on each object).

Applied to both duly_duty and duly_catalog_item — #5's instantiation (applyCatalogHandler) copies these five fields verbatim onto every duty made from a catalog item, so a wrong value on the catalog side would otherwise be replicated onto every person who takes the role.

Why a real-engine test suite, not a schema-structural pin

pnpm validate does not check a CEL defaultValue's syntax or behaviour at all — @objectstack/spec's authoring gate discriminates the shape and, for an expression envelope, returns unconditionally ("a CEL result type is unknowable at parse time"). So the only thing that proves the conditional default actually works is inserting a row through a booted engine and reading it back, which is what test/cadence-conditional-defaults.test.ts does (23 new tests, duly_duty and duly_catalog_item both covered, plus a #5-shaped instantiation check and a negative control — see below).

test/dispatch.test.ts's old "the cadence fallbacks are the object schema" block used to pin due_anchor / due_offset_days / lead_days structurally (Duty.fields.x.defaultValue === literal); that shape no longer applies once the default is a CEL expression, so it's replaced with a comment pointing at the new file, which checks the same DEFAULT_DUE_ANCHOR / DEFAULT_DUE_OFFSET_DAYS / DEFAULT_LEAD_DAYS constants against a real inserted recurring row instead — a stronger pin, not a weaker one.

Ablation

Removed the standing_no_frequency validation block from duty.object.ts (confirmed on disk: grep -c dropped from 2 occurrences to 1, the remaining one being the field description's own mention of the rule name), ran test/cadence-conditional-defaults.test.ts: exactly the targeted test ("refuses a frequency on a standing duty") went red, all 22 others stayed green. Restored via git checkout -- src/objects/duty.object.ts from a prior commit (script used a trap ... EXIT INT TERM so this happens even on a kill), confirmed back to 2 occurrences and a clean git diff against HEAD, re-ran the file: 23/23 green again.

Negative control: test/cadence-conditional-defaults.test.ts also asserts recurring_needs_frequency still fires — an UPDATE that blanks frequency on a duty that stays form: 'recurring' is refused with that rule's own exact message, and the row is left untouched. A second test inserts a recurring duty with an explicit non-default frequency and a standing duty with none, to confirm neither new rule accidentally fires on the other form.

Gates (all at 78ee086)

pnpm validate    ✓ (the one pre-existing hierarchy-security warning, unrelated)
pnpm typecheck   ✓
pnpm test        ✓ 439/439, 15 files
pnpm build       ✓

Neighbouring fields not touched

effective_from / effective_to and their effective_window_ordered rule are untouched — a standing duty legitimately has an effective window (when it started/stopped being someone's responsibility), unrelated to dispatch cadence.

Filed separately (out of scope for this issue)

  • filed as duly_catalog_item has no recurring_needs_frequency-equivalent validation #65: duly_catalog_item has no recurring_needs_frequency-equivalent rule at all (it never had one, unrelated to standing) — a recurring catalog item's frequency can be blanked via an UPDATE with nothing to refuse it, and that blank would propagate onto every duty instantiated from it afterward. Not fixed here (different validation, not a mechanical extension of this fix).

Generated by Claude Code

…g duty

A standing duty never dispatches, so a stamped frequency/due_anchor/
due_offset_days/lead_days/grace_days reads as though it runs on a schedule
it never will. Makes each default conditional on `form` (CEL null-guard
idiom) and adds the validation rules that refuse the meaningless
combinations outright, on both duly_duty and duly_catalog_item so a wrong
value on the catalog side is never replicated onto an instantiated duty.

Fixes #61

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:19
@os-warren
os-warren merged commit 8758bb5 into main Sep 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A standing duty carries a frequency — "Standing / Monthly" reads as if it dispatches

1 participant