Skip to content

Flip duly_duty.source and duly_task.source defaults from catalog to self - #54

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-50-source-default
Sep 1, 2026
Merged

os-warren merged 2 commits into
mainfrom
claude/issue-50-source-default

Conversation

@os-warren

@os-warren os-warren commented Sep 1, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #50
Fixes #55

What

duly_duty.source and duly_task.source — the two caliber columns — both
declared catalog as the default option, so a hand-created record (no
source supplied) was born into the governed, scoreable set. That is the
exact inverse of the invariant source exists to serve: self-declared work
is surfaced, never scored. On the duty side it also exposed a hand-authored
duty to duly_catalog_sync, which rewrites cadence fields on anything whose
source is 'catalog'.

Moves default: true from the catalog option to the self option on both
duly_duty.source (src/objects/duty.object.ts, #50) and duly_task.source
(src/objects/task.object.ts, #55 — extended into this PR by PM call: the
task-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_item is untouched — a catalog item is the
organisation's declaration by construction.

Why this is safe — duty side (#50)

Every path that legitimately produces a governed duly_duty already stamps
source explicitly:

  • duly_catalog_apply writes source: 'catalog' — confirmed unchanged at
    src/actions/catalog.handlers.ts:324, and pinned by
    test/catalog-instantiate.test.ts ("copies the catalog item's content and
    cadence onto the duty").
  • The assignment fan-out (src/flows/assignment.flow.ts) writes
    source: 'assigned' — but on duly_task, not duly_duty (it never
    creates a duly_duty row at all; objectName: 'duly_task' at both
    create_record nodes). It is therefore orthogonal to this field's default.
    Confirmed unchanged by test/assignment-fanout.test.ts. Noting this
    because 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 rather
than assumed:

  • The dispatcher (src/jobs/dispatch.plan.ts, dispatch.job.ts) copies
    duty.source onto every dispatched task explicitly —
    source: duty.source ?? '' is always present in the TaskDraft, never
    omitted from the insert. Confirmed unchanged; still relies on nothing from
    the field default.
  • The assignment fan-out writes source: 'assigned' directly on both
    create_record nodes in assignment.flow.ts. Re-confirmed unchanged after
    the task-side edit.
  • The actual gap the default governs: duly_member's permission grant
    on duly_task is allowCreate: true (src/security/permission-sets.ts),
    and src/views/task.view.ts defines no create form that stamps source.
    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 — describes catalog/
    assigned as governed and self as surfaced-never-scored, but never
    states which is the default. No edit needed.
  • The seed (Demo seed data — the product working on first boot #7) — not yet landed (no seed files exist outside
    node_modules). Nothing to fix.
  • src/views/duty.view.ts — puts source on the form as a plain field with
    no override either way; out of file surface, unaffected by the fix.

Tests

test/invariants.test.ts carries one generalized test — a hand-created record is self-declared, not born governed (#50, #55) — looping over both
duly_duty and duly_task rather than duplicating the block per object, so
a third caliber-bearing object landing with the wrong default fails the same
assertion. Each iteration asserts exactly one source option carries
default: true, that its value is 'self', and that neither catalog nor
assigned does.

Ablation (both halves, same script shape)

For each object: committed the fix first, moved default: true back onto
catalog on disk, confirmed the mutation landed via grep/git diff --stat
before measuring, ran test/invariants.test.ts and got the expected red
naming that object (AssertionError: <object>.source: expected 'catalog' to be 'self'), then restored via a trap ... EXIT INT TERM from the committed
state. Verified git status --short / git diff --stat empty after each
restore, and the full suite green again both times.

Gates (all green on 1123013, final head)

pnpm validate    # ✓ Validation passed
pnpm typecheck   # ✓ (tsc --noEmit, no output)
pnpm test        # ✓ 12 files, 371 tests passed
pnpm build       # ✓ Build complete, dist/objectstack.json written

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.ts or AGENTS.md touched.


Generated by Claude Code

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 os-warren changed the title Flip duly_duty.source default from catalog to self Flip duly_duty.source and duly_task.source defaults from catalog to self Sep 1, 2026
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 06:06
@os-warren
os-warren merged commit 7c30a39 into main Sep 1, 2026
1 check passed
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant