Skip to content

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

Description

@os-warren

Found in the first browser pass, on the Role catalog screen.

A catalog item created with form: 'standing' and no frequency renders as:

Duty Form Frequency
Keep the chemical register current Standing Monthly

frequency defaults to monthly on both duly_catalog_item and duly_duty, and the default applies regardless of form. A standing duty never dispatches — that is the whole point of the form — so a frequency on one is meaningless, and worse, it reads to a configurer as though the thing runs monthly.

The dispatcher is not affected: it skips form: 'standing' outright and #43's tests pin that. This is a data-integrity and comprehension defect, not a dispatch defect. But the person setting up a customer's catalog reads this screen, and it tells them something false.

Scope

Decide and implement one of:

  • A — make frequency conditional: no default when form is standing, and a validation rule refusing a frequency on a standing duty. Mirrors the existing recurring_needs_frequency rule, which already asserts the converse.
  • B — leave the field but hide it in the UI for standing rows and exclude it from the catalog list view's columns.

A is the stronger answer — B leaves the wrong value in the data where an export, an import round-trip (#19) or an API consumer still sees it, and it is the data that is wrong, not the rendering. The object already carries recurring_needs_frequency; this is the missing other half of the same rule.

Apply to both duly_duty and duly_catalog_item — #5's instantiation copies cadence fields across, so a wrong value on the catalog side is replicated onto every duty made from it.

Same treatment worth checking for due_anchor, due_offset_days, lead_days and grace_days, which are equally meaningless on a standing duty and equally defaulted.

Activity

  1. self-assigned this
    on Sep 1, 2026
  2. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    Claiming this issue.

    • session: session_01SqkTcrxUFci7nqXdbBSe2p
    • branch: claude/issue-61-standing-no-frequency

    Generated by Claude Code

  3. added a commit that references this issue on Sep 1, 2026
    78ee086
  4. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor
    {
      "issue": 61,
      "status": "done",
      "branch": "claude/issue-61-standing-no-frequency",
      "pr": "https://github.com/objectstack-ai/duly/pull/66",
      "premise_still_valid": true,
      "summary": "frequency, due_anchor, due_offset_days, lead_days and grace_days now carry CEL-conditional defaults (the blessed null-guard idiom, cond ? value : null) on both duly_duty and duly_catalog_item, scoped per field to exactly which forms dispatch.plan.ts#planForDuty actually reads them for -- standing loses all five, one_off additionally loses due_anchor/due_offset_days/lead_days (period-anchored, meaningless without a period) but keeps frequency (out of this issue's adjudicated scope) and grace_days (its task has a real due date). Four new validation rules (mirroring recurring_needs_frequency's converse) refuse the meaningless combinations outright on both objects, so a standing/one-off row can never be saved holding them. Applied to both objects per #5's instantiation copying cadence fields verbatim.",
      "tests": "pnpm validate: pass (one pre-existing hierarchy-security warning, unrelated). pnpm typecheck: pass. pnpm test: 439/439 passed, 15 files (incl. new test/cadence-conditional-defaults.test.ts, 23 tests, real booted engine -- insert+read-back for defaults since pnpm validate never evaluates a CEL defaultValue; direct data.insert/update for VALIDATION_FAILED code+message assertions per ADR-0112). pnpm build: pass. Ablation: removed standing_no_frequency from duty.object.ts, confirmed on disk (grep -c 2->1, the surviving hit is the field description's own mention of the rule name), ran test/cadence-conditional-defaults.test.ts -- exactly 'refuses a frequency on a standing duty' went red, 22 others stayed green; restored via git checkout from the prior commit inside a trap EXIT INT TERM script, confirmed grep -c back to 2 and git diff clean, re-ran: 23/23 green. Negative control in the same file: an UPDATE blanking frequency on a still-recurring duty is refused by recurring_needs_frequency with its own exact message (row left untouched), and a paired test confirms neither new rule fires on the other form.",
      "open_questions": [],
      "out_of_scope_findings": ["filed as #65: duly_catalog_item has no recurring_needs_frequency-equivalent validation -- a recurring catalog item's frequency can be blanked via UPDATE with nothing to refuse it, then propagated onto every duty instantiated from it"]
    }

    Generated by Claude Code

  5. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    os-dev-report
    (the HTML-comment marker on my previous comment was stripped by GitHub's sanitizer — this comment is the report, in full, using a literal marker instead)

    {
      "issue": 61,
      "status": "done",
      "branch": "claude/issue-61-standing-no-frequency",
      "pr": "https://github.com/objectstack-ai/duly/pull/66",
      "premise_still_valid": true,
      "summary": "frequency, due_anchor, due_offset_days, lead_days and grace_days now carry CEL-conditional defaults (the blessed null-guard idiom, cond ? value : null) on both duly_duty and duly_catalog_item, scoped per field to exactly which forms dispatch.plan.ts#planForDuty actually reads them for -- standing loses all five, one_off additionally loses due_anchor/due_offset_days/lead_days (period-anchored, meaningless without a period) but keeps frequency (out of this issue's adjudicated scope) and grace_days (its task has a real due date). Four new validation rules (mirroring recurring_needs_frequency's converse) refuse the meaningless combinations outright on both objects, so a standing/one-off row can never be saved holding them. Applied to both objects per #5's instantiation copying cadence fields verbatim.",
      "tests": "pnpm validate: pass (one pre-existing hierarchy-security warning, unrelated). pnpm typecheck: pass. pnpm test: 439/439 passed, 15 files (incl. new test/cadence-conditional-defaults.test.ts, 23 tests, real booted engine -- insert+read-back for defaults since pnpm validate never evaluates a CEL defaultValue; direct data.insert/update for VALIDATION_FAILED code+message assertions per ADR-0112). pnpm build: pass. Ablation: removed standing_no_frequency from duty.object.ts, confirmed on disk (grep -c 2->1, the surviving hit is the field description's own mention of the rule name), ran test/cadence-conditional-defaults.test.ts -- exactly 'refuses a frequency on a standing duty' went red, 22 others stayed green; restored via git checkout from the prior commit inside a trap EXIT INT TERM script, confirmed grep -c back to 2 and git diff clean, re-ran: 23/23 green. Negative control in the same file: an UPDATE blanking frequency on a still-recurring duty is refused by recurring_needs_frequency with its own exact message (row left untouched), and a paired test confirms neither new rule fires on the other form.",
      "open_questions": [],
      "out_of_scope_findings": ["filed as #65: duly_catalog_item has no recurring_needs_frequency-equivalent validation -- a recurring catalog item's frequency can be blanked via UPDATE with nothing to refuse it, then propagated onto every duty instantiated from it"]
    }

    Generated by Claude Code

  6. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    ACCEPT — PM review of #66, round 5.

    Gates re-run with origin/main merged: all four EXIT=0, 448 tests. CI verify success. Ablation reproduced: deleting standing_no_frequency reds exactly × refuses a frequency on a standing duty, nothing else.

    Deriving the per-form rules from planForDuty was the right method

    I asked you to examine the neighbouring fields and either extend the treatment or say why not. You did neither by intuition — you scoped each field to which forms dispatch.plan.ts#planForDuty actually reads it for, which is the only defensible basis. Standing loses all five; one-off additionally loses due_anchor / due_offset_days / lead_days because they are period-anchored and there is no period, but keeps grace_days because its task has a real due date.

    That last distinction is exactly the thing I warned might get flattened — "standing and one-off are the same case" is the easy wrong answer and you didn't take it.

    The out-of-surface change is an upgrade, not a loss — and I checked

    test/dispatch.test.ts is outside your declared surface, and you removed three assertions from a block whose own comment says it exists to stop the planner and the schema "drifting into two answers". That reads like deleted coverage, so I looked.

    It is the opposite. The old pins read .defaultValue off the schema and compared it to the planner's constant. Now that those defaults are CEL expressions, that comparison would be reading an expression object — and pnpm validate accepts a CEL defaultValue structurally and never evaluates it (field.zod.ts returns unconditionally on shape === 'expression'). A structural pin on a CEL default proves nothing.

    The new file imports the same three planner constants and asserts them against a real row inserted through a booted engine, which does evaluate the CEL. That is strictly stronger: from "the literal matches the literal" to "a real insert comes back with the planner's value". Leaving the explanation in the vacated block, rather than just deleting it, is what let me verify that in one read.

    Insert-and-read-back rather than a schema assertion was the necessary call here, and knowing why validate cannot help is the part that makes it a decision rather than a workaround.

    #65 queued

    duly_catalog_item having no recurring_needs_frequency equivalent — so a recurring catalog item's frequency can be blanked by UPDATE with nothing to refuse it, and then propagated onto every duty instantiated from it — is a real asymmetry and a good catch while you were in both files.

    Merging. #7's dev is being told: these rules now refuse standing and one-off rows carrying cadence fields, so a seed that sets frequency on a standing item will be rejected rather than silently accepted.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions