From 0749d3d4e12ba684bd37cb9d2945a53b66568d24 Mon Sep 17 00:00:00 2001 From: Warren Date: Tue, 1 Sep 2026 05:55:28 +0000 Subject: [PATCH 1/2] Flip duly_duty.source default from catalog to self (#50) 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. --- src/objects/duty.object.ts | 11 +++++++++-- test/invariants.test.ts | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/objects/duty.object.ts b/src/objects/duty.object.ts index 6dedcb6..8aae6e9 100644 --- a/src/objects/duty.object.ts +++ b/src/objects/duty.object.ts @@ -83,9 +83,16 @@ export const Duty = ObjectSchema.create({ label: 'Source', required: true, options: [ - { label: 'Role catalog', value: 'catalog', color: '#16515F', default: true }, + { label: 'Role catalog', value: 'catalog', color: '#16515F' }, { label: 'Assigned by manager', value: 'assigned', color: '#8C6512' }, - { label: 'Self-declared', value: 'self', color: '#576B73' }, + // The default. Every path that legitimately produces a governed duty + // stamps `source` explicitly — `duly_catalog_apply` writes 'catalog' + // (#34), the assignment fan-out writes 'assigned' (#33) — so the + // default is only ever reached by a hand-created duty, which is by + // definition self-declared. Fail-safe direction: a producer that + // forgets to stamp caliber produces an unscored duty, not a scored + // one (#50). + { label: 'Self-declared', value: 'self', color: '#576B73', default: true }, ], }), diff --git a/test/invariants.test.ts b/test/invariants.test.ts index fca20b1..6a8769d 100644 --- a/test/invariants.test.ts +++ b/test/invariants.test.ts @@ -59,6 +59,26 @@ describe('product invariants', () => { } }); + it('a hand-created duty is self-declared, not born governed (#50)', () => { + // The safe default for an ambiguous duty is the UNscoreable one. A duty + // created with no `source` supplied must land as 'self' — never + // 'catalog', which would additionally expose it to duly_catalog_sync's + // cadence rewrite for a catalog item it never came from. + const options = Duty.fields.source.options ?? []; + const defaults = options.filter((o) => o.default); + expect(defaults, 'exactly one option may claim the default').toHaveLength(1); + expect(defaults[0]?.value).toBe('self'); + + // The two governed calibers are reachable only when the producer that + // knows states them explicitly — duly_catalog_apply writes 'catalog' + // (#34), the assignment fan-out writes 'assigned' (#33). Neither may ride + // in on the field default. + for (const governed of ['catalog', 'assigned'] as const) { + const option = options.find((o) => o.value === governed); + expect(option?.default, `${governed} must not be the default`).not.toBe(true); + } + }); + it('every object states its sharing model explicitly', () => { for (const schema of [Duty, Task, LogEntry, CatalogItem]) { expect(schema.sharingModel, `${schema.name} must state an OWD`).toBeTruthy(); From 1123013ade8177ec32710825f31b8ae99a9f10bd Mon Sep 17 00:00:00 2001 From: Warren Date: Tue, 1 Sep 2026 06:01:27 +0000 Subject: [PATCH 2/2] Flip duly_task.source default from catalog to self (#55) 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. --- src/objects/task.object.ts | 14 ++++++++++++-- test/invariants.test.ts | 39 ++++++++++++++++++++++---------------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/objects/task.object.ts b/src/objects/task.object.ts index 85f7254..73a5cb6 100644 --- a/src/objects/task.object.ts +++ b/src/objects/task.object.ts @@ -66,9 +66,19 @@ export const Task = ObjectSchema.create({ label: 'Source', required: true, options: [ - { label: 'Role catalog', value: 'catalog', color: '#16515F', default: true }, + { label: 'Role catalog', value: 'catalog', color: '#16515F' }, { label: 'Assigned by manager', value: 'assigned', color: '#8C6512' }, - { label: 'Self-declared', value: 'self', color: '#576B73' }, + // The default. Both manufactured producers stamp this explicitly and + // do not rely on it: the dispatcher copies `duty.source` onto every + // dispatched task (`dispatch.plan.ts` — `source: duty.source ?? ''`, + // never omitted from the draft), and the assignment fan-out writes + // `source: 'assigned'` directly on both `create_record` nodes + // (`assignment.flow.ts`). The path that actually reaches this + // default is `duly_member`'s `allowCreate: true` on `duly_task` + // (`permission-sets.ts`) with no create form stamping `source` — a + // member hand-creating their own task, which is self-declared by + // definition (#55). + { label: 'Self-declared', value: 'self', color: '#576B73', default: true }, ], }), diff --git a/test/invariants.test.ts b/test/invariants.test.ts index 6a8769d..72dad55 100644 --- a/test/invariants.test.ts +++ b/test/invariants.test.ts @@ -59,23 +59,30 @@ describe('product invariants', () => { } }); - it('a hand-created duty is self-declared, not born governed (#50)', () => { - // The safe default for an ambiguous duty is the UNscoreable one. A duty - // created with no `source` supplied must land as 'self' — never - // 'catalog', which would additionally expose it to duly_catalog_sync's - // cadence rewrite for a catalog item it never came from. - const options = Duty.fields.source.options ?? []; - const defaults = options.filter((o) => o.default); - expect(defaults, 'exactly one option may claim the default').toHaveLength(1); - expect(defaults[0]?.value).toBe('self'); + it('a hand-created record is self-declared, not born governed (#50, #55)', () => { + // The safe default for an ambiguous caliber is the UNscoreable one, on + // every object that carries the column — not just the one that surfaced + // it first. A duty or task created with no `source` supplied must land + // as 'self' — never 'catalog' (which additionally exposes a duty to + // duly_catalog_sync's cadence rewrite for a catalog item it never came + // from) and never 'assigned'. + // + // Both governed calibers are reachable only when the producer that knows + // states them explicitly: on duly_duty, duly_catalog_apply writes + // 'catalog' (#34); on duly_task, the dispatcher copies `duty.source` + // (#43) and the assignment fan-out writes 'assigned' directly (#33). + // Neither may ride in on the field default — the default is reached only + // by a hand-created record, which is self-declared by definition. + for (const [name, schema] of [['duly_duty', Duty], ['duly_task', Task]] as const) { + const options = schema.fields.source.options ?? []; + const defaults = options.filter((o) => o.default); + expect(defaults, `${name}.source: exactly one option may claim the default`).toHaveLength(1); + expect(defaults[0]?.value, `${name}.source`).toBe('self'); - // The two governed calibers are reachable only when the producer that - // knows states them explicitly — duly_catalog_apply writes 'catalog' - // (#34), the assignment fan-out writes 'assigned' (#33). Neither may ride - // in on the field default. - for (const governed of ['catalog', 'assigned'] as const) { - const option = options.find((o) => o.value === governed); - expect(option?.default, `${governed} must not be the default`).not.toBe(true); + for (const governed of ['catalog', 'assigned'] as const) { + const option = options.find((o) => o.value === governed); + expect(option?.default, `${name}.source: ${governed} must not be the default`).not.toBe(true); + } } });