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

Forbid a frequency (and the neighbouring cadence fields) on a standing duty#66
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:

FieldBlank + forbidden onStill applies toWhy
frequencystandingrecurring, one_offAdjudicated 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_daysstanding, one_offrecurring onlyThese 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_daysstandingrecurring, one_offMeasures 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 bothduly_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 mainSep 1, 2026
1 check passed
Sign up for freeto 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

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

Forbid a frequency (and the neighbouring cadence fields) on a standing duty#66
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:

FieldBlank + forbidden onStill applies toWhy
frequencystandingrecurring, one_offAdjudicated 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_daysstanding, one_offrecurring onlyThese 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_daysstandingrecurring, one_offMeasures 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 bothduly_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 mainSep 1, 2026
1 check passed
Sign up for freeto 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

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

Forbid a frequency (and the neighbouring cadence fields) on a standing duty#66
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:

FieldBlank + forbidden onStill applies toWhy
frequencystandingrecurring, one_offAdjudicated 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_daysstanding, one_offrecurring onlyThese 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_daysstandingrecurring, one_offMeasures 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 bothduly_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 mainSep 1, 2026
1 check passed
Sign up for freeto 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

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

Forbid a frequency (and the neighbouring cadence fields) on a standing duty#66
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:

FieldBlank + forbidden onStill applies toWhy
frequencystandingrecurring, one_offAdjudicated 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_daysstanding, one_offrecurring onlyThese 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_daysstandingrecurring, one_offMeasures 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 bothduly_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 mainSep 1, 2026
1 check passed
Sign up for freeto 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

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

Forbid a frequency (and the neighbouring cadence fields) on a standing duty#66
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:

FieldBlank + forbidden onStill applies toWhy
frequencystandingrecurring, one_offAdjudicated 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_daysstanding, one_offrecurring onlyThese 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_daysstandingrecurring, one_offMeasures 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 bothduly_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 mainSep 1, 2026
1 check passed
Sign up for freeto 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

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

Forbid a frequency (and the neighbouring cadence fields) on a standing duty#66
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:

FieldBlank + forbidden onStill applies toWhy
frequencystandingrecurring, one_offAdjudicated 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_daysstanding, one_offrecurring onlyThese 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_daysstandingrecurring, one_offMeasures 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 bothduly_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 mainSep 1, 2026
1 check passed
Sign up for freeto 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

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

Forbid a frequency (and the neighbouring cadence fields) on a standing duty#66
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:

FieldBlank + forbidden onStill applies toWhy
frequencystandingrecurring, one_offAdjudicated 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_daysstanding, one_offrecurring onlyThese 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_daysstandingrecurring, one_offMeasures 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 bothduly_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 mainSep 1, 2026
1 check passed
Sign up for freeto 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

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

Forbid a frequency (and the neighbouring cadence fields) on a standing duty#66
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:

FieldBlank + forbidden onStill applies toWhy
frequencystandingrecurring, one_offAdjudicated 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_daysstanding, one_offrecurring onlyThese 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_daysstandingrecurring, one_offMeasures 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 bothduly_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 mainSep 1, 2026
1 check passed
Sign up for freeto 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

@os-warren