Establish and pin how a seed writes history - #64

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-32-seed-writes-history
Sep 1, 2026
Merged

Establish and pin how a seed writes history#64
os-warren merged 1 commit into
mainfrom
claude/issue-32-seed-writes-history

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#32

The answer is yes, with a second pass nobody would have guessed. No application-level workaround was needed and none was written.

The answer

{ context: { isSystem: true } } is the sanctioned way to write history — the same leg src/jobs/dispatch.job.ts already uses, and the leg the platform's own seed loader uses (SeedLoaderService.SEED_OPTIONS = { isSystem: true, skipTriggers: true, seedReplay: true }). It exempts a write from the readonly strip.

ColumnHow you seed itMeasured
completed_atcarried on the insert✅ works
last_update_ata second pass in mode: 'update'✅ works — but an insert can never carry it

The stalled half is the one that would have bitten #7.beforeInsert stamps last_update_at unconditionally, and lifecycle hooks do run on the seed path — skipTriggers suppresses record-change automation, not hooks — so a system insert's value is overwritten with the boot clock. Measured on a real seeded boot: the row seeded with last_update_at: <45 days ago> came back holding boot time. It takes a second seed dataset on the same object in mode: 'update', matched on externalId, carrying only last_update_at; the beforeUpdate leg deliberately does not stamp on an administrative write, so that value lands.

Skip that pass and the "Not moving" view is empty on a freshly seeded demo, with no error anywhere — the seed reports success and the view is simply blank.

A third finding #7 needs

The seed loader resolves duly_task.owner as a natural key against sys_user.name, deferred to a second resolution pass. A bare id string matching no sys_user row does not resolve, and because owner is required: true the whole task row is refused (Owner is required, plus an unresolved-reference error). Measured: without a sys_user dataset seeded first, nothing seeds at allinserted: 0, errored: 4.

Both directions are pinned

test/seed-history.test.ts runs against a real booted kernel with the declarative seeder actually running (skipSeedData: false), so the seed path itself is under test rather than a hand-made stand-in.

  • system-context insert of a done task with a completion instant → succeeds, nothing stripped
  • an ordinary caller's identical payload → still refused by completed_at_required_when_done (asserted on code: VALIDATION_FAILED + name: ValidationError + the message, not on a bare toThrow), and nothing is written
  • the same refusal with isSystem: false spelled out, so the exemption is pinned to the flag's value and not to the key being absent
  • a non-system last_update_at backdate → dropped, with droppedFields asserted as well as the unchanged stored value

Ablations

Both mutations were confirmed on disk before running (grep for the injected and the deleted text), and both were restored by an EXIT INT TERM trap.

AblationPredictedObserved
drop readonly: true from completed_at (src/objects/task.object.ts)the two refusal tests go redexactly those 2 failed, 7 passed — the permissive assertions correctly stayed green
drop the mode: 'update' seed pass from the fixturethe stalled assertion goes red1 failed, 8 passed — expected '2026-09-01T08:09:31.664Z' to be '2026-07-18T08:09:31.478Z'

The second one is the important one: it proves the two-pass shape this PR documents is load-bearing rather than decoration.

Gates

All four green at 4f42406, which is the final commit:

pnpm validate exit 0 (one expected warning: hierarchy-security provider absent — the documented state of this repo)
pnpm typecheck exit 0
pnpm test exit 0 Test Files 15 passed (15) · Tests 428 passed (428)
pnpm build exit 0 Artifact: dist/objectstack.json (98.8 KB)

pnpm test was re-run afterpnpm build with dist/objectstack.json on disk — still 428/428 — confirming the suite's artifactPath guard holds and the tests report on src/, not on the last build.

Files changed

  • test/seed-history.test.ts (new)
  • AGENTS.md — the answer, next to the product invariants

src/data/ was deliberately not touched. The worked example lives in the test fixture and in AGENTS.md rather than in dulySeeds, because rows added there would boot on every pnpm dev and collide head-on with #7, which owns the real seed. This keeps the file surface to test/ + AGENTS.md. No breach of the declared surface.

Filed out of scope

Generated by Claude Code


Generated by Claude Code

A duly_task cannot be created in `done` by an ordinary caller: completed_at is
readonly, beforeInsert stamps only last_update_at, so
completed_at_required_when_done refuses the row. Correct for the dispatch path,
fatal for the seed path that #7 needs.
Measured on @objectstack/runtime 17.2.0: `{ context: { isSystem: true } }`
exempts the readonly strip and IS the sanctioned way to write history — but it
takes two passes. completed_at rides along on the insert. last_update_at cannot:
beforeInsert stamps it unconditionally and lifecycle hooks still run on the seed
path (skipTriggers suppresses record-change automation, not hooks), so it takes a
second seed dataset in `mode: 'update'`. Without that second pass there are no
stalled rows and the "Not moving" view is empty on a seeded demo.
test/seed-history.test.ts pins both directions against a real booted kernel with
the declarative seeder actually running: the system write succeeds, and an
ordinary caller's identical write is still refused by
completed_at_required_when_done (VALIDATION_FAILED). It also pins that the
insert-path readonly strip is a protocol-BOUNDARY guard — engine.insert applies
none of it — filed upstream as objectstack-ai/objectstack#14147.
AGENTS.md carries the answer next to the product invariants, including that the
seed loader resolves duly_task.owner as a natural key against sys_user.name, so a
seed must seed its users first or every task row is refused.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:15
@os-warren
os-warren merged commit edbbd6c into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-warren added a commit that referenced this pull request Sep 1, 2026
* Seed the demo: the product working on first boot
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Spread in-flight touch ages by how long a task has been open
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A task cannot be created directly in done — decide whether that is the intent

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Establish and pin how a seed writes history - #64

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-32-seed-writes-history
Sep 1, 2026
Merged

Establish and pin how a seed writes history#64
os-warren merged 1 commit into
mainfrom
claude/issue-32-seed-writes-history

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#32

The answer is yes, with a second pass nobody would have guessed. No application-level workaround was needed and none was written.

The answer

{ context: { isSystem: true } } is the sanctioned way to write history — the same leg src/jobs/dispatch.job.ts already uses, and the leg the platform's own seed loader uses (SeedLoaderService.SEED_OPTIONS = { isSystem: true, skipTriggers: true, seedReplay: true }). It exempts a write from the readonly strip.

ColumnHow you seed itMeasured
completed_atcarried on the insert✅ works
last_update_ata second pass in mode: 'update'✅ works — but an insert can never carry it

The stalled half is the one that would have bitten #7.beforeInsert stamps last_update_at unconditionally, and lifecycle hooks do run on the seed path — skipTriggers suppresses record-change automation, not hooks — so a system insert's value is overwritten with the boot clock. Measured on a real seeded boot: the row seeded with last_update_at: <45 days ago> came back holding boot time. It takes a second seed dataset on the same object in mode: 'update', matched on externalId, carrying only last_update_at; the beforeUpdate leg deliberately does not stamp on an administrative write, so that value lands.

Skip that pass and the "Not moving" view is empty on a freshly seeded demo, with no error anywhere — the seed reports success and the view is simply blank.

A third finding #7 needs

The seed loader resolves duly_task.owner as a natural key against sys_user.name, deferred to a second resolution pass. A bare id string matching no sys_user row does not resolve, and because owner is required: true the whole task row is refused (Owner is required, plus an unresolved-reference error). Measured: without a sys_user dataset seeded first, nothing seeds at allinserted: 0, errored: 4.

Both directions are pinned

test/seed-history.test.ts runs against a real booted kernel with the declarative seeder actually running (skipSeedData: false), so the seed path itself is under test rather than a hand-made stand-in.

  • system-context insert of a done task with a completion instant → succeeds, nothing stripped
  • an ordinary caller's identical payload → still refused by completed_at_required_when_done (asserted on code: VALIDATION_FAILED + name: ValidationError + the message, not on a bare toThrow), and nothing is written
  • the same refusal with isSystem: false spelled out, so the exemption is pinned to the flag's value and not to the key being absent
  • a non-system last_update_at backdate → dropped, with droppedFields asserted as well as the unchanged stored value

Ablations

Both mutations were confirmed on disk before running (grep for the injected and the deleted text), and both were restored by an EXIT INT TERM trap.

AblationPredictedObserved
drop readonly: true from completed_at (src/objects/task.object.ts)the two refusal tests go redexactly those 2 failed, 7 passed — the permissive assertions correctly stayed green
drop the mode: 'update' seed pass from the fixturethe stalled assertion goes red1 failed, 8 passed — expected '2026-09-01T08:09:31.664Z' to be '2026-07-18T08:09:31.478Z'

The second one is the important one: it proves the two-pass shape this PR documents is load-bearing rather than decoration.

Gates

All four green at 4f42406, which is the final commit:

pnpm validate exit 0 (one expected warning: hierarchy-security provider absent — the documented state of this repo)
pnpm typecheck exit 0
pnpm test exit 0 Test Files 15 passed (15) · Tests 428 passed (428)
pnpm build exit 0 Artifact: dist/objectstack.json (98.8 KB)

pnpm test was re-run afterpnpm build with dist/objectstack.json on disk — still 428/428 — confirming the suite's artifactPath guard holds and the tests report on src/, not on the last build.

Files changed

  • test/seed-history.test.ts (new)
  • AGENTS.md — the answer, next to the product invariants

src/data/ was deliberately not touched. The worked example lives in the test fixture and in AGENTS.md rather than in dulySeeds, because rows added there would boot on every pnpm dev and collide head-on with #7, which owns the real seed. This keeps the file surface to test/ + AGENTS.md. No breach of the declared surface.

Filed out of scope

Generated by Claude Code


Generated by Claude Code

A duly_task cannot be created in `done` by an ordinary caller: completed_at is
readonly, beforeInsert stamps only last_update_at, so
completed_at_required_when_done refuses the row. Correct for the dispatch path,
fatal for the seed path that #7 needs.
Measured on @objectstack/runtime 17.2.0: `{ context: { isSystem: true } }`
exempts the readonly strip and IS the sanctioned way to write history — but it
takes two passes. completed_at rides along on the insert. last_update_at cannot:
beforeInsert stamps it unconditionally and lifecycle hooks still run on the seed
path (skipTriggers suppresses record-change automation, not hooks), so it takes a
second seed dataset in `mode: 'update'`. Without that second pass there are no
stalled rows and the "Not moving" view is empty on a seeded demo.
test/seed-history.test.ts pins both directions against a real booted kernel with
the declarative seeder actually running: the system write succeeds, and an
ordinary caller's identical write is still refused by
completed_at_required_when_done (VALIDATION_FAILED). It also pins that the
insert-path readonly strip is a protocol-BOUNDARY guard — engine.insert applies
none of it — filed upstream as objectstack-ai/objectstack#14147.
AGENTS.md carries the answer next to the product invariants, including that the
seed loader resolves duly_task.owner as a natural key against sys_user.name, so a
seed must seed its users first or every task row is refused.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:15
@os-warren
os-warren merged commit edbbd6c into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-warren added a commit that referenced this pull request Sep 1, 2026
* Seed the demo: the product working on first boot
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Spread in-flight touch ages by how long a task has been open
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A task cannot be created directly in done — decide whether that is the intent

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Establish and pin how a seed writes history - #64

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-32-seed-writes-history
Sep 1, 2026
Merged

Establish and pin how a seed writes history#64
os-warren merged 1 commit into
mainfrom
claude/issue-32-seed-writes-history

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#32

The answer is yes, with a second pass nobody would have guessed. No application-level workaround was needed and none was written.

The answer

{ context: { isSystem: true } } is the sanctioned way to write history — the same leg src/jobs/dispatch.job.ts already uses, and the leg the platform's own seed loader uses (SeedLoaderService.SEED_OPTIONS = { isSystem: true, skipTriggers: true, seedReplay: true }). It exempts a write from the readonly strip.

ColumnHow you seed itMeasured
completed_atcarried on the insert✅ works
last_update_ata second pass in mode: 'update'✅ works — but an insert can never carry it

The stalled half is the one that would have bitten #7.beforeInsert stamps last_update_at unconditionally, and lifecycle hooks do run on the seed path — skipTriggers suppresses record-change automation, not hooks — so a system insert's value is overwritten with the boot clock. Measured on a real seeded boot: the row seeded with last_update_at: <45 days ago> came back holding boot time. It takes a second seed dataset on the same object in mode: 'update', matched on externalId, carrying only last_update_at; the beforeUpdate leg deliberately does not stamp on an administrative write, so that value lands.

Skip that pass and the "Not moving" view is empty on a freshly seeded demo, with no error anywhere — the seed reports success and the view is simply blank.

A third finding #7 needs

The seed loader resolves duly_task.owner as a natural key against sys_user.name, deferred to a second resolution pass. A bare id string matching no sys_user row does not resolve, and because owner is required: true the whole task row is refused (Owner is required, plus an unresolved-reference error). Measured: without a sys_user dataset seeded first, nothing seeds at allinserted: 0, errored: 4.

Both directions are pinned

test/seed-history.test.ts runs against a real booted kernel with the declarative seeder actually running (skipSeedData: false), so the seed path itself is under test rather than a hand-made stand-in.

  • system-context insert of a done task with a completion instant → succeeds, nothing stripped
  • an ordinary caller's identical payload → still refused by completed_at_required_when_done (asserted on code: VALIDATION_FAILED + name: ValidationError + the message, not on a bare toThrow), and nothing is written
  • the same refusal with isSystem: false spelled out, so the exemption is pinned to the flag's value and not to the key being absent
  • a non-system last_update_at backdate → dropped, with droppedFields asserted as well as the unchanged stored value

Ablations

Both mutations were confirmed on disk before running (grep for the injected and the deleted text), and both were restored by an EXIT INT TERM trap.

AblationPredictedObserved
drop readonly: true from completed_at (src/objects/task.object.ts)the two refusal tests go redexactly those 2 failed, 7 passed — the permissive assertions correctly stayed green
drop the mode: 'update' seed pass from the fixturethe stalled assertion goes red1 failed, 8 passed — expected '2026-09-01T08:09:31.664Z' to be '2026-07-18T08:09:31.478Z'

The second one is the important one: it proves the two-pass shape this PR documents is load-bearing rather than decoration.

Gates

All four green at 4f42406, which is the final commit:

pnpm validate exit 0 (one expected warning: hierarchy-security provider absent — the documented state of this repo)
pnpm typecheck exit 0
pnpm test exit 0 Test Files 15 passed (15) · Tests 428 passed (428)
pnpm build exit 0 Artifact: dist/objectstack.json (98.8 KB)

pnpm test was re-run afterpnpm build with dist/objectstack.json on disk — still 428/428 — confirming the suite's artifactPath guard holds and the tests report on src/, not on the last build.

Files changed

  • test/seed-history.test.ts (new)
  • AGENTS.md — the answer, next to the product invariants

src/data/ was deliberately not touched. The worked example lives in the test fixture and in AGENTS.md rather than in dulySeeds, because rows added there would boot on every pnpm dev and collide head-on with #7, which owns the real seed. This keeps the file surface to test/ + AGENTS.md. No breach of the declared surface.

Filed out of scope

Generated by Claude Code


Generated by Claude Code

A duly_task cannot be created in `done` by an ordinary caller: completed_at is
readonly, beforeInsert stamps only last_update_at, so
completed_at_required_when_done refuses the row. Correct for the dispatch path,
fatal for the seed path that #7 needs.
Measured on @objectstack/runtime 17.2.0: `{ context: { isSystem: true } }`
exempts the readonly strip and IS the sanctioned way to write history — but it
takes two passes. completed_at rides along on the insert. last_update_at cannot:
beforeInsert stamps it unconditionally and lifecycle hooks still run on the seed
path (skipTriggers suppresses record-change automation, not hooks), so it takes a
second seed dataset in `mode: 'update'`. Without that second pass there are no
stalled rows and the "Not moving" view is empty on a seeded demo.
test/seed-history.test.ts pins both directions against a real booted kernel with
the declarative seeder actually running: the system write succeeds, and an
ordinary caller's identical write is still refused by
completed_at_required_when_done (VALIDATION_FAILED). It also pins that the
insert-path readonly strip is a protocol-BOUNDARY guard — engine.insert applies
none of it — filed upstream as objectstack-ai/objectstack#14147.
AGENTS.md carries the answer next to the product invariants, including that the
seed loader resolves duly_task.owner as a natural key against sys_user.name, so a
seed must seed its users first or every task row is refused.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:15
@os-warren
os-warren merged commit edbbd6c into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-warren added a commit that referenced this pull request Sep 1, 2026
* Seed the demo: the product working on first boot
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Spread in-flight touch ages by how long a task has been open
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A task cannot be created directly in done — decide whether that is the intent

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Establish and pin how a seed writes history - #64

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-32-seed-writes-history
Sep 1, 2026
Merged

Establish and pin how a seed writes history#64
os-warren merged 1 commit into
mainfrom
claude/issue-32-seed-writes-history

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#32

The answer is yes, with a second pass nobody would have guessed. No application-level workaround was needed and none was written.

The answer

{ context: { isSystem: true } } is the sanctioned way to write history — the same leg src/jobs/dispatch.job.ts already uses, and the leg the platform's own seed loader uses (SeedLoaderService.SEED_OPTIONS = { isSystem: true, skipTriggers: true, seedReplay: true }). It exempts a write from the readonly strip.

ColumnHow you seed itMeasured
completed_atcarried on the insert✅ works
last_update_ata second pass in mode: 'update'✅ works — but an insert can never carry it

The stalled half is the one that would have bitten #7.beforeInsert stamps last_update_at unconditionally, and lifecycle hooks do run on the seed path — skipTriggers suppresses record-change automation, not hooks — so a system insert's value is overwritten with the boot clock. Measured on a real seeded boot: the row seeded with last_update_at: <45 days ago> came back holding boot time. It takes a second seed dataset on the same object in mode: 'update', matched on externalId, carrying only last_update_at; the beforeUpdate leg deliberately does not stamp on an administrative write, so that value lands.

Skip that pass and the "Not moving" view is empty on a freshly seeded demo, with no error anywhere — the seed reports success and the view is simply blank.

A third finding #7 needs

The seed loader resolves duly_task.owner as a natural key against sys_user.name, deferred to a second resolution pass. A bare id string matching no sys_user row does not resolve, and because owner is required: true the whole task row is refused (Owner is required, plus an unresolved-reference error). Measured: without a sys_user dataset seeded first, nothing seeds at allinserted: 0, errored: 4.

Both directions are pinned

test/seed-history.test.ts runs against a real booted kernel with the declarative seeder actually running (skipSeedData: false), so the seed path itself is under test rather than a hand-made stand-in.

  • system-context insert of a done task with a completion instant → succeeds, nothing stripped
  • an ordinary caller's identical payload → still refused by completed_at_required_when_done (asserted on code: VALIDATION_FAILED + name: ValidationError + the message, not on a bare toThrow), and nothing is written
  • the same refusal with isSystem: false spelled out, so the exemption is pinned to the flag's value and not to the key being absent
  • a non-system last_update_at backdate → dropped, with droppedFields asserted as well as the unchanged stored value

Ablations

Both mutations were confirmed on disk before running (grep for the injected and the deleted text), and both were restored by an EXIT INT TERM trap.

AblationPredictedObserved
drop readonly: true from completed_at (src/objects/task.object.ts)the two refusal tests go redexactly those 2 failed, 7 passed — the permissive assertions correctly stayed green
drop the mode: 'update' seed pass from the fixturethe stalled assertion goes red1 failed, 8 passed — expected '2026-09-01T08:09:31.664Z' to be '2026-07-18T08:09:31.478Z'

The second one is the important one: it proves the two-pass shape this PR documents is load-bearing rather than decoration.

Gates

All four green at 4f42406, which is the final commit:

pnpm validate exit 0 (one expected warning: hierarchy-security provider absent — the documented state of this repo)
pnpm typecheck exit 0
pnpm test exit 0 Test Files 15 passed (15) · Tests 428 passed (428)
pnpm build exit 0 Artifact: dist/objectstack.json (98.8 KB)

pnpm test was re-run afterpnpm build with dist/objectstack.json on disk — still 428/428 — confirming the suite's artifactPath guard holds and the tests report on src/, not on the last build.

Files changed

  • test/seed-history.test.ts (new)
  • AGENTS.md — the answer, next to the product invariants

src/data/ was deliberately not touched. The worked example lives in the test fixture and in AGENTS.md rather than in dulySeeds, because rows added there would boot on every pnpm dev and collide head-on with #7, which owns the real seed. This keeps the file surface to test/ + AGENTS.md. No breach of the declared surface.

Filed out of scope

Generated by Claude Code


Generated by Claude Code

A duly_task cannot be created in `done` by an ordinary caller: completed_at is
readonly, beforeInsert stamps only last_update_at, so
completed_at_required_when_done refuses the row. Correct for the dispatch path,
fatal for the seed path that #7 needs.
Measured on @objectstack/runtime 17.2.0: `{ context: { isSystem: true } }`
exempts the readonly strip and IS the sanctioned way to write history — but it
takes two passes. completed_at rides along on the insert. last_update_at cannot:
beforeInsert stamps it unconditionally and lifecycle hooks still run on the seed
path (skipTriggers suppresses record-change automation, not hooks), so it takes a
second seed dataset in `mode: 'update'`. Without that second pass there are no
stalled rows and the "Not moving" view is empty on a seeded demo.
test/seed-history.test.ts pins both directions against a real booted kernel with
the declarative seeder actually running: the system write succeeds, and an
ordinary caller's identical write is still refused by
completed_at_required_when_done (VALIDATION_FAILED). It also pins that the
insert-path readonly strip is a protocol-BOUNDARY guard — engine.insert applies
none of it — filed upstream as objectstack-ai/objectstack#14147.
AGENTS.md carries the answer next to the product invariants, including that the
seed loader resolves duly_task.owner as a natural key against sys_user.name, so a
seed must seed its users first or every task row is refused.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:15
@os-warren
os-warren merged commit edbbd6c into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-warren added a commit that referenced this pull request Sep 1, 2026
* Seed the demo: the product working on first boot
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Spread in-flight touch ages by how long a task has been open
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A task cannot be created directly in done — decide whether that is the intent

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Establish and pin how a seed writes history - #64

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-32-seed-writes-history
Sep 1, 2026
Merged

Establish and pin how a seed writes history#64
os-warren merged 1 commit into
mainfrom
claude/issue-32-seed-writes-history

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#32

The answer is yes, with a second pass nobody would have guessed. No application-level workaround was needed and none was written.

The answer

{ context: { isSystem: true } } is the sanctioned way to write history — the same leg src/jobs/dispatch.job.ts already uses, and the leg the platform's own seed loader uses (SeedLoaderService.SEED_OPTIONS = { isSystem: true, skipTriggers: true, seedReplay: true }). It exempts a write from the readonly strip.

ColumnHow you seed itMeasured
completed_atcarried on the insert✅ works
last_update_ata second pass in mode: 'update'✅ works — but an insert can never carry it

The stalled half is the one that would have bitten #7.beforeInsert stamps last_update_at unconditionally, and lifecycle hooks do run on the seed path — skipTriggers suppresses record-change automation, not hooks — so a system insert's value is overwritten with the boot clock. Measured on a real seeded boot: the row seeded with last_update_at: <45 days ago> came back holding boot time. It takes a second seed dataset on the same object in mode: 'update', matched on externalId, carrying only last_update_at; the beforeUpdate leg deliberately does not stamp on an administrative write, so that value lands.

Skip that pass and the "Not moving" view is empty on a freshly seeded demo, with no error anywhere — the seed reports success and the view is simply blank.

A third finding #7 needs

The seed loader resolves duly_task.owner as a natural key against sys_user.name, deferred to a second resolution pass. A bare id string matching no sys_user row does not resolve, and because owner is required: true the whole task row is refused (Owner is required, plus an unresolved-reference error). Measured: without a sys_user dataset seeded first, nothing seeds at allinserted: 0, errored: 4.

Both directions are pinned

test/seed-history.test.ts runs against a real booted kernel with the declarative seeder actually running (skipSeedData: false), so the seed path itself is under test rather than a hand-made stand-in.

  • system-context insert of a done task with a completion instant → succeeds, nothing stripped
  • an ordinary caller's identical payload → still refused by completed_at_required_when_done (asserted on code: VALIDATION_FAILED + name: ValidationError + the message, not on a bare toThrow), and nothing is written
  • the same refusal with isSystem: false spelled out, so the exemption is pinned to the flag's value and not to the key being absent
  • a non-system last_update_at backdate → dropped, with droppedFields asserted as well as the unchanged stored value

Ablations

Both mutations were confirmed on disk before running (grep for the injected and the deleted text), and both were restored by an EXIT INT TERM trap.

AblationPredictedObserved
drop readonly: true from completed_at (src/objects/task.object.ts)the two refusal tests go redexactly those 2 failed, 7 passed — the permissive assertions correctly stayed green
drop the mode: 'update' seed pass from the fixturethe stalled assertion goes red1 failed, 8 passed — expected '2026-09-01T08:09:31.664Z' to be '2026-07-18T08:09:31.478Z'

The second one is the important one: it proves the two-pass shape this PR documents is load-bearing rather than decoration.

Gates

All four green at 4f42406, which is the final commit:

pnpm validate exit 0 (one expected warning: hierarchy-security provider absent — the documented state of this repo)
pnpm typecheck exit 0
pnpm test exit 0 Test Files 15 passed (15) · Tests 428 passed (428)
pnpm build exit 0 Artifact: dist/objectstack.json (98.8 KB)

pnpm test was re-run afterpnpm build with dist/objectstack.json on disk — still 428/428 — confirming the suite's artifactPath guard holds and the tests report on src/, not on the last build.

Files changed

  • test/seed-history.test.ts (new)
  • AGENTS.md — the answer, next to the product invariants

src/data/ was deliberately not touched. The worked example lives in the test fixture and in AGENTS.md rather than in dulySeeds, because rows added there would boot on every pnpm dev and collide head-on with #7, which owns the real seed. This keeps the file surface to test/ + AGENTS.md. No breach of the declared surface.

Filed out of scope

Generated by Claude Code


Generated by Claude Code

A duly_task cannot be created in `done` by an ordinary caller: completed_at is
readonly, beforeInsert stamps only last_update_at, so
completed_at_required_when_done refuses the row. Correct for the dispatch path,
fatal for the seed path that #7 needs.
Measured on @objectstack/runtime 17.2.0: `{ context: { isSystem: true } }`
exempts the readonly strip and IS the sanctioned way to write history — but it
takes two passes. completed_at rides along on the insert. last_update_at cannot:
beforeInsert stamps it unconditionally and lifecycle hooks still run on the seed
path (skipTriggers suppresses record-change automation, not hooks), so it takes a
second seed dataset in `mode: 'update'`. Without that second pass there are no
stalled rows and the "Not moving" view is empty on a seeded demo.
test/seed-history.test.ts pins both directions against a real booted kernel with
the declarative seeder actually running: the system write succeeds, and an
ordinary caller's identical write is still refused by
completed_at_required_when_done (VALIDATION_FAILED). It also pins that the
insert-path readonly strip is a protocol-BOUNDARY guard — engine.insert applies
none of it — filed upstream as objectstack-ai/objectstack#14147.
AGENTS.md carries the answer next to the product invariants, including that the
seed loader resolves duly_task.owner as a natural key against sys_user.name, so a
seed must seed its users first or every task row is refused.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:15
@os-warren
os-warren merged commit edbbd6c into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-warren added a commit that referenced this pull request Sep 1, 2026
* Seed the demo: the product working on first boot
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Spread in-flight touch ages by how long a task has been open
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A task cannot be created directly in done — decide whether that is the intent

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Establish and pin how a seed writes history - #64

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-32-seed-writes-history
Sep 1, 2026
Merged

Establish and pin how a seed writes history#64
os-warren merged 1 commit into
mainfrom
claude/issue-32-seed-writes-history

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#32

The answer is yes, with a second pass nobody would have guessed. No application-level workaround was needed and none was written.

The answer

{ context: { isSystem: true } } is the sanctioned way to write history — the same leg src/jobs/dispatch.job.ts already uses, and the leg the platform's own seed loader uses (SeedLoaderService.SEED_OPTIONS = { isSystem: true, skipTriggers: true, seedReplay: true }). It exempts a write from the readonly strip.

ColumnHow you seed itMeasured
completed_atcarried on the insert✅ works
last_update_ata second pass in mode: 'update'✅ works — but an insert can never carry it

The stalled half is the one that would have bitten #7.beforeInsert stamps last_update_at unconditionally, and lifecycle hooks do run on the seed path — skipTriggers suppresses record-change automation, not hooks — so a system insert's value is overwritten with the boot clock. Measured on a real seeded boot: the row seeded with last_update_at: <45 days ago> came back holding boot time. It takes a second seed dataset on the same object in mode: 'update', matched on externalId, carrying only last_update_at; the beforeUpdate leg deliberately does not stamp on an administrative write, so that value lands.

Skip that pass and the "Not moving" view is empty on a freshly seeded demo, with no error anywhere — the seed reports success and the view is simply blank.

A third finding #7 needs

The seed loader resolves duly_task.owner as a natural key against sys_user.name, deferred to a second resolution pass. A bare id string matching no sys_user row does not resolve, and because owner is required: true the whole task row is refused (Owner is required, plus an unresolved-reference error). Measured: without a sys_user dataset seeded first, nothing seeds at allinserted: 0, errored: 4.

Both directions are pinned

test/seed-history.test.ts runs against a real booted kernel with the declarative seeder actually running (skipSeedData: false), so the seed path itself is under test rather than a hand-made stand-in.

  • system-context insert of a done task with a completion instant → succeeds, nothing stripped
  • an ordinary caller's identical payload → still refused by completed_at_required_when_done (asserted on code: VALIDATION_FAILED + name: ValidationError + the message, not on a bare toThrow), and nothing is written
  • the same refusal with isSystem: false spelled out, so the exemption is pinned to the flag's value and not to the key being absent
  • a non-system last_update_at backdate → dropped, with droppedFields asserted as well as the unchanged stored value

Ablations

Both mutations were confirmed on disk before running (grep for the injected and the deleted text), and both were restored by an EXIT INT TERM trap.

AblationPredictedObserved
drop readonly: true from completed_at (src/objects/task.object.ts)the two refusal tests go redexactly those 2 failed, 7 passed — the permissive assertions correctly stayed green
drop the mode: 'update' seed pass from the fixturethe stalled assertion goes red1 failed, 8 passed — expected '2026-09-01T08:09:31.664Z' to be '2026-07-18T08:09:31.478Z'

The second one is the important one: it proves the two-pass shape this PR documents is load-bearing rather than decoration.

Gates

All four green at 4f42406, which is the final commit:

pnpm validate exit 0 (one expected warning: hierarchy-security provider absent — the documented state of this repo)
pnpm typecheck exit 0
pnpm test exit 0 Test Files 15 passed (15) · Tests 428 passed (428)
pnpm build exit 0 Artifact: dist/objectstack.json (98.8 KB)

pnpm test was re-run afterpnpm build with dist/objectstack.json on disk — still 428/428 — confirming the suite's artifactPath guard holds and the tests report on src/, not on the last build.

Files changed

  • test/seed-history.test.ts (new)
  • AGENTS.md — the answer, next to the product invariants

src/data/ was deliberately not touched. The worked example lives in the test fixture and in AGENTS.md rather than in dulySeeds, because rows added there would boot on every pnpm dev and collide head-on with #7, which owns the real seed. This keeps the file surface to test/ + AGENTS.md. No breach of the declared surface.

Filed out of scope

Generated by Claude Code


Generated by Claude Code

A duly_task cannot be created in `done` by an ordinary caller: completed_at is
readonly, beforeInsert stamps only last_update_at, so
completed_at_required_when_done refuses the row. Correct for the dispatch path,
fatal for the seed path that #7 needs.
Measured on @objectstack/runtime 17.2.0: `{ context: { isSystem: true } }`
exempts the readonly strip and IS the sanctioned way to write history — but it
takes two passes. completed_at rides along on the insert. last_update_at cannot:
beforeInsert stamps it unconditionally and lifecycle hooks still run on the seed
path (skipTriggers suppresses record-change automation, not hooks), so it takes a
second seed dataset in `mode: 'update'`. Without that second pass there are no
stalled rows and the "Not moving" view is empty on a seeded demo.
test/seed-history.test.ts pins both directions against a real booted kernel with
the declarative seeder actually running: the system write succeeds, and an
ordinary caller's identical write is still refused by
completed_at_required_when_done (VALIDATION_FAILED). It also pins that the
insert-path readonly strip is a protocol-BOUNDARY guard — engine.insert applies
none of it — filed upstream as objectstack-ai/objectstack#14147.
AGENTS.md carries the answer next to the product invariants, including that the
seed loader resolves duly_task.owner as a natural key against sys_user.name, so a
seed must seed its users first or every task row is refused.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:15
@os-warren
os-warren merged commit edbbd6c into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-warren added a commit that referenced this pull request Sep 1, 2026
* Seed the demo: the product working on first boot
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Spread in-flight touch ages by how long a task has been open
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A task cannot be created directly in done — decide whether that is the intent

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Establish and pin how a seed writes history - #64

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-32-seed-writes-history
Sep 1, 2026
Merged

Establish and pin how a seed writes history#64
os-warren merged 1 commit into
mainfrom
claude/issue-32-seed-writes-history

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#32

The answer is yes, with a second pass nobody would have guessed. No application-level workaround was needed and none was written.

The answer

{ context: { isSystem: true } } is the sanctioned way to write history — the same leg src/jobs/dispatch.job.ts already uses, and the leg the platform's own seed loader uses (SeedLoaderService.SEED_OPTIONS = { isSystem: true, skipTriggers: true, seedReplay: true }). It exempts a write from the readonly strip.

ColumnHow you seed itMeasured
completed_atcarried on the insert✅ works
last_update_ata second pass in mode: 'update'✅ works — but an insert can never carry it

The stalled half is the one that would have bitten #7.beforeInsert stamps last_update_at unconditionally, and lifecycle hooks do run on the seed path — skipTriggers suppresses record-change automation, not hooks — so a system insert's value is overwritten with the boot clock. Measured on a real seeded boot: the row seeded with last_update_at: <45 days ago> came back holding boot time. It takes a second seed dataset on the same object in mode: 'update', matched on externalId, carrying only last_update_at; the beforeUpdate leg deliberately does not stamp on an administrative write, so that value lands.

Skip that pass and the "Not moving" view is empty on a freshly seeded demo, with no error anywhere — the seed reports success and the view is simply blank.

A third finding #7 needs

The seed loader resolves duly_task.owner as a natural key against sys_user.name, deferred to a second resolution pass. A bare id string matching no sys_user row does not resolve, and because owner is required: true the whole task row is refused (Owner is required, plus an unresolved-reference error). Measured: without a sys_user dataset seeded first, nothing seeds at allinserted: 0, errored: 4.

Both directions are pinned

test/seed-history.test.ts runs against a real booted kernel with the declarative seeder actually running (skipSeedData: false), so the seed path itself is under test rather than a hand-made stand-in.

  • system-context insert of a done task with a completion instant → succeeds, nothing stripped
  • an ordinary caller's identical payload → still refused by completed_at_required_when_done (asserted on code: VALIDATION_FAILED + name: ValidationError + the message, not on a bare toThrow), and nothing is written
  • the same refusal with isSystem: false spelled out, so the exemption is pinned to the flag's value and not to the key being absent
  • a non-system last_update_at backdate → dropped, with droppedFields asserted as well as the unchanged stored value

Ablations

Both mutations were confirmed on disk before running (grep for the injected and the deleted text), and both were restored by an EXIT INT TERM trap.

AblationPredictedObserved
drop readonly: true from completed_at (src/objects/task.object.ts)the two refusal tests go redexactly those 2 failed, 7 passed — the permissive assertions correctly stayed green
drop the mode: 'update' seed pass from the fixturethe stalled assertion goes red1 failed, 8 passed — expected '2026-09-01T08:09:31.664Z' to be '2026-07-18T08:09:31.478Z'

The second one is the important one: it proves the two-pass shape this PR documents is load-bearing rather than decoration.

Gates

All four green at 4f42406, which is the final commit:

pnpm validate exit 0 (one expected warning: hierarchy-security provider absent — the documented state of this repo)
pnpm typecheck exit 0
pnpm test exit 0 Test Files 15 passed (15) · Tests 428 passed (428)
pnpm build exit 0 Artifact: dist/objectstack.json (98.8 KB)

pnpm test was re-run afterpnpm build with dist/objectstack.json on disk — still 428/428 — confirming the suite's artifactPath guard holds and the tests report on src/, not on the last build.

Files changed

  • test/seed-history.test.ts (new)
  • AGENTS.md — the answer, next to the product invariants

src/data/ was deliberately not touched. The worked example lives in the test fixture and in AGENTS.md rather than in dulySeeds, because rows added there would boot on every pnpm dev and collide head-on with #7, which owns the real seed. This keeps the file surface to test/ + AGENTS.md. No breach of the declared surface.

Filed out of scope

Generated by Claude Code


Generated by Claude Code

A duly_task cannot be created in `done` by an ordinary caller: completed_at is
readonly, beforeInsert stamps only last_update_at, so
completed_at_required_when_done refuses the row. Correct for the dispatch path,
fatal for the seed path that #7 needs.
Measured on @objectstack/runtime 17.2.0: `{ context: { isSystem: true } }`
exempts the readonly strip and IS the sanctioned way to write history — but it
takes two passes. completed_at rides along on the insert. last_update_at cannot:
beforeInsert stamps it unconditionally and lifecycle hooks still run on the seed
path (skipTriggers suppresses record-change automation, not hooks), so it takes a
second seed dataset in `mode: 'update'`. Without that second pass there are no
stalled rows and the "Not moving" view is empty on a seeded demo.
test/seed-history.test.ts pins both directions against a real booted kernel with
the declarative seeder actually running: the system write succeeds, and an
ordinary caller's identical write is still refused by
completed_at_required_when_done (VALIDATION_FAILED). It also pins that the
insert-path readonly strip is a protocol-BOUNDARY guard — engine.insert applies
none of it — filed upstream as objectstack-ai/objectstack#14147.
AGENTS.md carries the answer next to the product invariants, including that the
seed loader resolves duly_task.owner as a natural key against sys_user.name, so a
seed must seed its users first or every task row is refused.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:15
@os-warren
os-warren merged commit edbbd6c into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-warren added a commit that referenced this pull request Sep 1, 2026
* Seed the demo: the product working on first boot
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Spread in-flight touch ages by how long a task has been open
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A task cannot be created directly in done — decide whether that is the intent

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Establish and pin how a seed writes history - #64

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-32-seed-writes-history
Sep 1, 2026
Merged

Establish and pin how a seed writes history#64
os-warren merged 1 commit into
mainfrom
claude/issue-32-seed-writes-history

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#32

The answer is yes, with a second pass nobody would have guessed. No application-level workaround was needed and none was written.

The answer

{ context: { isSystem: true } } is the sanctioned way to write history — the same leg src/jobs/dispatch.job.ts already uses, and the leg the platform's own seed loader uses (SeedLoaderService.SEED_OPTIONS = { isSystem: true, skipTriggers: true, seedReplay: true }). It exempts a write from the readonly strip.

ColumnHow you seed itMeasured
completed_atcarried on the insert✅ works
last_update_ata second pass in mode: 'update'✅ works — but an insert can never carry it

The stalled half is the one that would have bitten #7.beforeInsert stamps last_update_at unconditionally, and lifecycle hooks do run on the seed path — skipTriggers suppresses record-change automation, not hooks — so a system insert's value is overwritten with the boot clock. Measured on a real seeded boot: the row seeded with last_update_at: <45 days ago> came back holding boot time. It takes a second seed dataset on the same object in mode: 'update', matched on externalId, carrying only last_update_at; the beforeUpdate leg deliberately does not stamp on an administrative write, so that value lands.

Skip that pass and the "Not moving" view is empty on a freshly seeded demo, with no error anywhere — the seed reports success and the view is simply blank.

A third finding #7 needs

The seed loader resolves duly_task.owner as a natural key against sys_user.name, deferred to a second resolution pass. A bare id string matching no sys_user row does not resolve, and because owner is required: true the whole task row is refused (Owner is required, plus an unresolved-reference error). Measured: without a sys_user dataset seeded first, nothing seeds at allinserted: 0, errored: 4.

Both directions are pinned

test/seed-history.test.ts runs against a real booted kernel with the declarative seeder actually running (skipSeedData: false), so the seed path itself is under test rather than a hand-made stand-in.

  • system-context insert of a done task with a completion instant → succeeds, nothing stripped
  • an ordinary caller's identical payload → still refused by completed_at_required_when_done (asserted on code: VALIDATION_FAILED + name: ValidationError + the message, not on a bare toThrow), and nothing is written
  • the same refusal with isSystem: false spelled out, so the exemption is pinned to the flag's value and not to the key being absent
  • a non-system last_update_at backdate → dropped, with droppedFields asserted as well as the unchanged stored value

Ablations

Both mutations were confirmed on disk before running (grep for the injected and the deleted text), and both were restored by an EXIT INT TERM trap.

AblationPredictedObserved
drop readonly: true from completed_at (src/objects/task.object.ts)the two refusal tests go redexactly those 2 failed, 7 passed — the permissive assertions correctly stayed green
drop the mode: 'update' seed pass from the fixturethe stalled assertion goes red1 failed, 8 passed — expected '2026-09-01T08:09:31.664Z' to be '2026-07-18T08:09:31.478Z'

The second one is the important one: it proves the two-pass shape this PR documents is load-bearing rather than decoration.

Gates

All four green at 4f42406, which is the final commit:

pnpm validate exit 0 (one expected warning: hierarchy-security provider absent — the documented state of this repo)
pnpm typecheck exit 0
pnpm test exit 0 Test Files 15 passed (15) · Tests 428 passed (428)
pnpm build exit 0 Artifact: dist/objectstack.json (98.8 KB)

pnpm test was re-run afterpnpm build with dist/objectstack.json on disk — still 428/428 — confirming the suite's artifactPath guard holds and the tests report on src/, not on the last build.

Files changed

  • test/seed-history.test.ts (new)
  • AGENTS.md — the answer, next to the product invariants

src/data/ was deliberately not touched. The worked example lives in the test fixture and in AGENTS.md rather than in dulySeeds, because rows added there would boot on every pnpm dev and collide head-on with #7, which owns the real seed. This keeps the file surface to test/ + AGENTS.md. No breach of the declared surface.

Filed out of scope

Generated by Claude Code


Generated by Claude Code

A duly_task cannot be created in `done` by an ordinary caller: completed_at is
readonly, beforeInsert stamps only last_update_at, so
completed_at_required_when_done refuses the row. Correct for the dispatch path,
fatal for the seed path that #7 needs.
Measured on @objectstack/runtime 17.2.0: `{ context: { isSystem: true } }`
exempts the readonly strip and IS the sanctioned way to write history — but it
takes two passes. completed_at rides along on the insert. last_update_at cannot:
beforeInsert stamps it unconditionally and lifecycle hooks still run on the seed
path (skipTriggers suppresses record-change automation, not hooks), so it takes a
second seed dataset in `mode: 'update'`. Without that second pass there are no
stalled rows and the "Not moving" view is empty on a seeded demo.
test/seed-history.test.ts pins both directions against a real booted kernel with
the declarative seeder actually running: the system write succeeds, and an
ordinary caller's identical write is still refused by
completed_at_required_when_done (VALIDATION_FAILED). It also pins that the
insert-path readonly strip is a protocol-BOUNDARY guard — engine.insert applies
none of it — filed upstream as objectstack-ai/objectstack#14147.
AGENTS.md carries the answer next to the product invariants, including that the
seed loader resolves duly_task.owner as a natural key against sys_user.name, so a
seed must seed its users first or every task row is refused.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:15
@os-warren
os-warren merged commit edbbd6c into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-warren added a commit that referenced this pull request Sep 1, 2026
* Seed the demo: the product working on first boot
`pnpm dev` on an empty database now opens on a running system rather than
five empty grids — a three-level business-unit tree, thirteen people, a
twenty-item role catalog across three position codes, thirty-one duties and
six months of dispatched history.
History is produced by the dispatcher's own planner (`planDispatch`) rather
than by a second period walk, so every period key is the engine's spelling by
construction and "standing duties hold zero tasks" is structurally impossible
to violate rather than merely absent from the fixture.
`last_update_at` is written by a second `mode: 'update'` seed pass, per #32 /
PR #64 — an insert can never carry it, and without that pass the "Not moving"
view is empty while the seed reports success.
Fixes#7
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Spread in-flight touch ages by how long a task has been open
Keeps every non-designated row inside the fortnight while putting real
values in the 7-to-14-day band, so the dashboard's nested >7d / >14d / >30d
tiles read 6 / 3 / 2 rather than 3 / 3 / 2.
Also corrects the fan-out comment after #72: the reason a seeded assignment
does not fan out is the loader's own skipTriggers, not an unbound trigger.
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A task cannot be created directly in done — decide whether that is the intent

1 participant

@os-warren