Uh oh!
There was an error while loading. Please reload this page.
Flip duly_duty.source and duly_task.source defaults from catalog to self - #54
Merged
Conversation
A hand-created duty was born into the governed, scoreable set because the source select's default option was 'catalog'. Every path that legitimately produces a governed duty already stamps source explicitly (duly_catalog_apply writes 'catalog' per #34; the assignment fan-out writes 'assigned' on duly_task per #33), so the field default was only ever reached by a hand-created duty, which is by definition self-declared. Moves default: true from the catalog option to the self option on duly_duty.source. Adds a test pinning the direction next to the existing invariant tests: the default is self, and both governed values (catalog, assigned) are reachable only by explicit assignment, never as a fallback.
Same defaulting bug as #50, on the sibling caliber column. Verified before changing rather than copying: both manufactured producers already stamp source explicitly and do not rely on the field default -- the dispatcher copies duty.source onto every dispatched task (dispatch.plan.ts), and the assignment fan-out writes 'assigned' directly on both create_record nodes (assignment.flow.ts). The path that actually reaches the default is duly_member's allowCreate: true on duly_task with no create form stamping source -- a member hand-creating their own task, which is self-declared by definition. Generalizes the #50 pinning test in test/invariants.test.ts to assert the caliber-defaults-to-self property on both duly_duty and duly_task instead of duplicating the block, per PM extension of #50's file surface to include this sibling field.
os-warren
marked this pull request as ready for review
September 1, 2026 06:06
Uh oh!
There was an error while loading. Please reload this page.
os-warren pushed a commit
that referenced
this pull request
Sep 1, 2026
Picks up #54 (source defaults to self) and #56 (hierarchy-security). Both touch objects this guard resolves against; no field was renamed or removed, only `default:` flags on select options. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
This was referenced Sep 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#50
Fixes#55
What
duly_duty.sourceandduly_task.source— the two caliber columns — bothdeclared
catalogas the default option, so a hand-created record (nosourcesupplied) was born into the governed, scoreable set. That is theexact inverse of the invariant
sourceexists to serve: self-declared workis surfaced, never scored. On the duty side it also exposed a hand-authored
duty to
duly_catalog_sync, which rewrites cadence fields on anything whosesourceis'catalog'.Moves
default: truefrom thecatalogoption to theselfoption on bothduly_duty.source(src/objects/duty.object.ts, #50) andduly_task.source(
src/objects/task.object.ts, #55 — extended into this PR by PM call: thetask-side field is unowned this round, and splitting a one-line sibling-field
fix across two rounds was judged worse than the file-surface discipline was
worth).
duly_catalog_itemis untouched — a catalog item is theorganisation's declaration by construction.
Why this is safe — duty side (#50)
Every path that legitimately produces a governed
duly_dutyalready stampssourceexplicitly:duly_catalog_applywritessource: 'catalog'— confirmed unchanged atsrc/actions/catalog.handlers.ts:324, and pinned bytest/catalog-instantiate.test.ts("copies the catalog item's content andcadence onto the duty").
src/flows/assignment.flow.ts) writessource: 'assigned'— but onduly_task, notduly_duty(it nevercreates a
duly_dutyrow at all;objectName: 'duly_task'at bothcreate_recordnodes). It is therefore orthogonal to this field's default.Confirmed unchanged by
test/assignment-fanout.test.ts. Noting thisbecause the issue and adjudication comment both describe the fan-out as a
duty producer — it isn't; it produces tasks directly. Doesn't change the
fix, but the premise as stated is imprecise (acknowledged on A hand-created duty defaults to
source: 'catalog', so a self-declared duty is born scoreable #50).So the duty-side default was only ever reachable by a hand-created
duly_duty, which is by definition self-declared.Why this is safe — task side (#55), verified rather than copied
The producer set is different on
duly_task, so each was re-checked ratherthan assumed:
src/jobs/dispatch.plan.ts,dispatch.job.ts) copiesduty.sourceonto every dispatched task explicitly —source: duty.source ?? ''is always present in theTaskDraft, neveromitted from the insert. Confirmed unchanged; still relies on nothing from
the field default.
source: 'assigned'directly on bothcreate_recordnodes inassignment.flow.ts. Re-confirmed unchanged afterthe task-side edit.
duly_member's permission granton
duly_taskisallowCreate: true(src/security/permission-sets.ts),and
src/views/task.view.tsdefines no create form that stampssource.A member hand-creating their own task falls through to the field default —
which is exactly the self-declared case this fix protects. No producer was
found riding on the old default; the finding is the create-path gap itself,
now closed by the same mechanism as A hand-created duty defaults to
source: 'catalog', so a self-declared duty is born scoreable #50.Checked and found clean
docs/product/data-model.md's caliber paragraph — describescatalog/assignedas governed andselfas surfaced-never-scored, but neverstates which is the default. No edit needed.
seedfiles exist outsidenode_modules). Nothing to fix.src/views/duty.view.ts— putssourceon the form as a plain field withno override either way; out of file surface, unaffected by the fix.
Tests
test/invariants.test.tscarries one generalized test —a hand-created record is self-declared, not born governed (#50, #55)— looping over bothduly_dutyandduly_taskrather than duplicating the block per object, soa third caliber-bearing object landing with the wrong default fails the same
assertion. Each iteration asserts exactly one
sourceoption carriesdefault: true, that its value is'self', and that neithercatalognorassigneddoes.Ablation (both halves, same script shape)
For each object: committed the fix first, moved
default: trueback ontocatalogon disk, confirmed the mutation landed viagrep/git diff --statbefore measuring, ran
test/invariants.test.tsand got the expected rednaming that object (
AssertionError: <object>.source: expected 'catalog' to be 'self'), then restored via atrap ... EXIT INT TERMfrom the committedstate. Verified
git status --short/git diff --statempty after eachrestore, and the full suite green again both times.
Gates (all green on
1123013, final head)File surface
src/objects/duty.object.ts,src/objects/task.object.ts(PM-extended,unowned this round — no collision with #46/#51/#27),
test/invariants.test.ts.Nothing in
objectstack.config.tsorAGENTS.mdtouched.Generated by Claude Code