Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/objects/duty.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
],
}),

Expand Down
14 changes: 12 additions & 2 deletions src/objects/task.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
],
}),

Expand Down
27 changes: 27 additions & 0 deletions test/invariants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,33 @@ describe('product invariants', () => {
}
});

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');

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);
}
}
});

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();
Expand Down
Loading