Found while implementing #50 (flip duly_duty.source's default to self), whose
file surface was src/objects/duty.object.ts only. Filed rather than fixed —
src/objects/task.object.ts is a different object with its own claim risk,
and the PM adjudication comment on #50 scoped only the duty object.
What
duly_task.source (src/objects/task.object.ts) declares the identical
three-option select, with the identical comment calling it the caliber field
metrics are allowed to read — and the identical default: true sitting on
catalog:
source: Field.select({label: 'Source',required: true,options: [{label: 'Role catalog',value: 'catalog',color: '#16515F',default: true},{label: 'Assigned by manager',value: 'assigned',color: '#8C6512'},{label: 'Self-declared',value: 'self',color: '#576B73'},],}),Unlike the assignment fan-out (which writes duly_task.source explicitly —
see below), the dispatcher (src/jobs/dispatch.plan.ts:382,
source: duty.source ?? '') also always writes it explicitly, copied from the
parent duty. So the dispatched-task path is not exposed.
But src/security/permission-sets.ts grants the duly_member position
duly_task: { allowCreate: true, allowRead: true, allowEdit: true, readScope: 'own', writeScope: 'own' } — a member can create their own duly_task
directly, not just via dispatch. src/views/task.view.ts puts source on
several columns/tooltips but defines no create form that stamps it, so a
hand-created task falls back to the field default: catalog. That is the
same inversion #50 just fixed on the duty side — a self-declared task would be
born into the governed, scoreable set.
Why filed separately rather than fixed alongside #50
Correction to #50's stated premise, surfaced along the way
#50 and its adjudication comment describe "the assignment fan-out" as a
producer of a governed duty with source: 'assigned'. It isn't: the
fan-out (src/flows/assignment.flow.ts) never creates a duly_duty row at
all — both its create_record nodes target objectName: 'duly_task'
directly. It writes source: 'assigned' on the task, unconditionally,
so it is unaffected by either this finding or #50. Doesn't change either
fix; noted because the premise as stated is imprecise and this is the
object where it actually applies.
Suggested acceptance sketch
Found while implementing #50 (flip
duly_duty.source's default toself), whosefile surface was
src/objects/duty.object.tsonly. Filed rather than fixed —src/objects/task.object.tsis a different object with its own claim risk,and the PM adjudication comment on #50 scoped only the duty object.
What
duly_task.source(src/objects/task.object.ts) declares the identicalthree-option select, with the identical comment calling it the caliber field
metrics are allowed to read — and the identical
default: truesitting oncatalog:Unlike the assignment fan-out (which writes
duly_task.sourceexplicitly —see below), the dispatcher (
src/jobs/dispatch.plan.ts:382,source: duty.source ?? '') also always writes it explicitly, copied from theparent duty. So the dispatched-task path is not exposed.
But
src/security/permission-sets.tsgrants theduly_memberpositionduly_task: { allowCreate: true, allowRead: true, allowEdit: true, readScope: 'own', writeScope: 'own' }— a member can create their ownduly_taskdirectly, not just via dispatch.
src/views/task.view.tsputssourceonseveral columns/tooltips but defines no create form that stamps it, so a
hand-created task falls back to the field default:
catalog. That is thesame inversion #50 just fixed on the duty side — a self-declared task would be
born into the governed, scoreable set.
Why filed separately rather than fixed alongside #50
task.object.tsvsduty.object.ts) — outside A hand-created duty defaults tosource: 'catalog', so a self-declared duty is born scoreable #50'sclaimed file surface, and the PM adjudication comment on A hand-created duty defaults to
source: 'catalog', so a self-declared duty is born scoreable #50 scoped onlythe duty object, not stating a position on the task-side twin.
duly_taskhas no analogue toduly_catalog_sync's cadence rewrite, so the blast radius is scoring-only,not scoring-plus-cadence-corruption. Worth its own look rather than a
drive-by copy of A hand-created duty defaults to
source: 'catalog', so a self-declared duty is born scoreable #50's diff.Correction to #50's stated premise, surfaced along the way
#50 and its adjudication comment describe "the assignment fan-out" as a
producer of a governed duty with
source: 'assigned'. It isn't: thefan-out (
src/flows/assignment.flow.ts) never creates aduly_dutyrow atall — both its
create_recordnodes targetobjectName: 'duly_task'directly. It writes
source: 'assigned'on the task, unconditionally,so it is unaffected by either this finding or #50. Doesn't change either
fix; noted because the premise as stated is imprecise and this is the
object where it actually applies.
Suggested acceptance sketch
duly_taskwith nosourcesupplied isselfcatalog/assigned(copied from the duty) —already covered by
test/dispatch.test.ts/test/dispatch-wiring.test.tsassigned— already covered bytest/assignment-fanout.test.tssource: 'catalog', so a self-declared duty is born scoreable #50 added intest/invariants.test.ts