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
86 changes: 86 additions & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,6 +209,92 @@ green in the PR before you hand it back:
pnpm validate && pnpm typecheck && pnpm test && pnpm build
```

## How to write history — seeds, imports and fixtures

A `duly_task` cannot be created in `done` by an ordinary caller. `completed_at` is
`readonly`, the `beforeInsert` hook stamps only `last_update_at`, so
`completed_at_required_when_done` refuses the row:

```
ValidationError: A completed task must carry a completion timestamp. (code: VALIDATION_FAILED)
```

**That is correct and it stays.** Tasks are *dispatched* `open`; completion is a
later transition. The refusal is the product working.

Historical rows — a seed, an import, a fixture — are written from a **system
context** instead. `{ context: { isSystem: true } }` is the whole mechanism: it
exempts a write from the readonly strip. It is the same leg `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 takes two passes, and the second one is the half that gets forgotten.**

| Column | How you seed it |
|:-------|:----------------|
| `completed_at` | carried on the **insert**, from a system context |
| `last_update_at` | a **second pass in `mode: 'update'`** — an insert can never carry it |

Why `last_update_at` is different: `beforeInsert` stamps it 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. The `beforeUpdate` leg deliberately does not stamp on an
administrative write, so a second write carrying *only* `last_update_at` lands
untouched. Ergonomics card: #63.

Skip that second pass and there are no **stalled** rows — open tasks last touched
14+ days ago — so the "Not moving" view is empty on a freshly seeded demo. It is
the one signal the product claims is its most valuable, and nothing errors: the
seed reports success and the view is simply blank.

### The worked example

Three datasets, in this order, in `defineStack({ data })`:

```ts
// 1. sys_user FIRST. duly_task.owner is a user lookup and the seed loader
// resolves it as a NATURAL KEY against sys_user.name. A bare id that matches
// no sys_user row does not resolve, and because owner is required the whole
// task row is refused — "Owner is required" — with the loader also logging
// the unresolved reference. Measured: without this, nothing seeds.
{ object: 'sys_user', externalId: 'name', mode: 'insert',
records: [{ name: 'seed_user_alice', username: 'seed_user_alice', /* … */ }] },

// 2. The history itself. completed_at rides along on the insert.
{ object: 'duly_task', externalId: 'subject', mode: 'insert',
records: [{ subject: '…', owner: 'seed_user_alice', source: 'catalog',
status: 'done', completed_at: '2026-05-04T09:00:00.000Z' }] },

// 3. The SAME rows again, matched on externalId, to backdate the clock.
{ object: 'duly_task', externalId: 'subject', mode: 'update',
records: [{ subject: '…', last_update_at: '2026-07-18T08:00:00.000Z' }] },
```

Prefer **relative** instants (`Date.now() - 45 * DAY`) over literals for anything
whose *age* is the point: a hard-coded date stops being "stalled" as the repo ages.

Both directions are pinned by `test/seed-history.test.ts` — the system write
succeeds **and** an ordinary caller's identical write is still refused. Keep both
halves. A test that only proved "the seed may" would quietly turn `readonly: true`
into decoration.

### One caveat about which layer refuses

The insert-path readonly strip is a **boundary** guard: it lives in
`MetadataProtocolService.createData`, which is what `@objectstack/rest` — and so
REST, OpenAPI and MCP — writes through. `engine.insert` applies **no** readonly
strip at all (the update path is different: that strip is inside ObjectQL and is
`isSystem`-gated). So a direct `data.insert` from in-process code can set a
readonly column at insert with no context and no warning.

Filed upstream as **objectstack-ai/objectstack#14147**, and pinned as a tripwire in
`test/seed-history.test.ts` so it goes red when the platform closes it. Practical
consequence for authoring here: do not read an engine-level test as proof that a
column is protected — assert against `protocol.createData` when the refusal is the
thing you care about. Flows are unaffected today because `assignment.flow.ts`
declares `runAs: 'system'`, which is elevated regardless.

## Product invariants — do not "improve" these away

These are the product, not preferences. If a task seems to require breaking one,
Expand Down
Loading
Loading
, '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
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
86 changes: 86 additions & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,6 +209,92 @@ green in the PR before you hand it back:
pnpm validate && pnpm typecheck && pnpm test && pnpm build
```

## How to write history — seeds, imports and fixtures

A `duly_task` cannot be created in `done` by an ordinary caller. `completed_at` is
`readonly`, the `beforeInsert` hook stamps only `last_update_at`, so
`completed_at_required_when_done` refuses the row:

```
ValidationError: A completed task must carry a completion timestamp. (code: VALIDATION_FAILED)
```

**That is correct and it stays.** Tasks are *dispatched* `open`; completion is a
later transition. The refusal is the product working.

Historical rows — a seed, an import, a fixture — are written from a **system
context** instead. `{ context: { isSystem: true } }` is the whole mechanism: it
exempts a write from the readonly strip. It is the same leg `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 takes two passes, and the second one is the half that gets forgotten.**

| Column | How you seed it |
|:-------|:----------------|
| `completed_at` | carried on the **insert**, from a system context |
| `last_update_at` | a **second pass in `mode: 'update'`** — an insert can never carry it |

Why `last_update_at` is different: `beforeInsert` stamps it 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. The `beforeUpdate` leg deliberately does not stamp on an
administrative write, so a second write carrying *only* `last_update_at` lands
untouched. Ergonomics card: #63.

Skip that second pass and there are no **stalled** rows — open tasks last touched
14+ days ago — so the "Not moving" view is empty on a freshly seeded demo. It is
the one signal the product claims is its most valuable, and nothing errors: the
seed reports success and the view is simply blank.

### The worked example

Three datasets, in this order, in `defineStack({ data })`:

```ts
// 1. sys_user FIRST. duly_task.owner is a user lookup and the seed loader
// resolves it as a NATURAL KEY against sys_user.name. A bare id that matches
// no sys_user row does not resolve, and because owner is required the whole
// task row is refused — "Owner is required" — with the loader also logging
// the unresolved reference. Measured: without this, nothing seeds.
{ object: 'sys_user', externalId: 'name', mode: 'insert',
records: [{ name: 'seed_user_alice', username: 'seed_user_alice', /* … */ }] },

// 2. The history itself. completed_at rides along on the insert.
{ object: 'duly_task', externalId: 'subject', mode: 'insert',
records: [{ subject: '…', owner: 'seed_user_alice', source: 'catalog',
status: 'done', completed_at: '2026-05-04T09:00:00.000Z' }] },

// 3. The SAME rows again, matched on externalId, to backdate the clock.
{ object: 'duly_task', externalId: 'subject', mode: 'update',
records: [{ subject: '…', last_update_at: '2026-07-18T08:00:00.000Z' }] },
```

Prefer **relative** instants (`Date.now() - 45 * DAY`) over literals for anything
whose *age* is the point: a hard-coded date stops being "stalled" as the repo ages.

Both directions are pinned by `test/seed-history.test.ts` — the system write
succeeds **and** an ordinary caller's identical write is still refused. Keep both
halves. A test that only proved "the seed may" would quietly turn `readonly: true`
into decoration.

### One caveat about which layer refuses

The insert-path readonly strip is a **boundary** guard: it lives in
`MetadataProtocolService.createData`, which is what `@objectstack/rest` — and so
REST, OpenAPI and MCP — writes through. `engine.insert` applies **no** readonly
strip at all (the update path is different: that strip is inside ObjectQL and is
`isSystem`-gated). So a direct `data.insert` from in-process code can set a
readonly column at insert with no context and no warning.

Filed upstream as **objectstack-ai/objectstack#14147**, and pinned as a tripwire in
`test/seed-history.test.ts` so it goes red when the platform closes it. Practical
consequence for authoring here: do not read an engine-level test as proof that a
column is protected — assert against `protocol.createData` when the refusal is the
thing you care about. Flows are unaffected today because `assignment.flow.ts`
declares `runAs: 'system'`, which is elevated regardless.

## Product invariants — do not "improve" these away

These are the product, not preferences. If a task seems to require breaking one,
Expand Down
Loading
Loading
, '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
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
86 changes: 86 additions & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,6 +209,92 @@ green in the PR before you hand it back:
pnpm validate && pnpm typecheck && pnpm test && pnpm build
```

## How to write history — seeds, imports and fixtures

A `duly_task` cannot be created in `done` by an ordinary caller. `completed_at` is
`readonly`, the `beforeInsert` hook stamps only `last_update_at`, so
`completed_at_required_when_done` refuses the row:

```
ValidationError: A completed task must carry a completion timestamp. (code: VALIDATION_FAILED)
```

**That is correct and it stays.** Tasks are *dispatched* `open`; completion is a
later transition. The refusal is the product working.

Historical rows — a seed, an import, a fixture — are written from a **system
context** instead. `{ context: { isSystem: true } }` is the whole mechanism: it
exempts a write from the readonly strip. It is the same leg `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 takes two passes, and the second one is the half that gets forgotten.**

| Column | How you seed it |
|:-------|:----------------|
| `completed_at` | carried on the **insert**, from a system context |
| `last_update_at` | a **second pass in `mode: 'update'`** — an insert can never carry it |

Why `last_update_at` is different: `beforeInsert` stamps it 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. The `beforeUpdate` leg deliberately does not stamp on an
administrative write, so a second write carrying *only* `last_update_at` lands
untouched. Ergonomics card: #63.

Skip that second pass and there are no **stalled** rows — open tasks last touched
14+ days ago — so the "Not moving" view is empty on a freshly seeded demo. It is
the one signal the product claims is its most valuable, and nothing errors: the
seed reports success and the view is simply blank.

### The worked example

Three datasets, in this order, in `defineStack({ data })`:

```ts
// 1. sys_user FIRST. duly_task.owner is a user lookup and the seed loader
// resolves it as a NATURAL KEY against sys_user.name. A bare id that matches
// no sys_user row does not resolve, and because owner is required the whole
// task row is refused — "Owner is required" — with the loader also logging
// the unresolved reference. Measured: without this, nothing seeds.
{ object: 'sys_user', externalId: 'name', mode: 'insert',
records: [{ name: 'seed_user_alice', username: 'seed_user_alice', /* … */ }] },

// 2. The history itself. completed_at rides along on the insert.
{ object: 'duly_task', externalId: 'subject', mode: 'insert',
records: [{ subject: '…', owner: 'seed_user_alice', source: 'catalog',
status: 'done', completed_at: '2026-05-04T09:00:00.000Z' }] },

// 3. The SAME rows again, matched on externalId, to backdate the clock.
{ object: 'duly_task', externalId: 'subject', mode: 'update',
records: [{ subject: '…', last_update_at: '2026-07-18T08:00:00.000Z' }] },
```

Prefer **relative** instants (`Date.now() - 45 * DAY`) over literals for anything
whose *age* is the point: a hard-coded date stops being "stalled" as the repo ages.

Both directions are pinned by `test/seed-history.test.ts` — the system write
succeeds **and** an ordinary caller's identical write is still refused. Keep both
halves. A test that only proved "the seed may" would quietly turn `readonly: true`
into decoration.

### One caveat about which layer refuses

The insert-path readonly strip is a **boundary** guard: it lives in
`MetadataProtocolService.createData`, which is what `@objectstack/rest` — and so
REST, OpenAPI and MCP — writes through. `engine.insert` applies **no** readonly
strip at all (the update path is different: that strip is inside ObjectQL and is
`isSystem`-gated). So a direct `data.insert` from in-process code can set a
readonly column at insert with no context and no warning.

Filed upstream as **objectstack-ai/objectstack#14147**, and pinned as a tripwire in
`test/seed-history.test.ts` so it goes red when the platform closes it. Practical
consequence for authoring here: do not read an engine-level test as proof that a
column is protected — assert against `protocol.createData` when the refusal is the
thing you care about. Flows are unaffected today because `assignment.flow.ts`
declares `runAs: 'system'`, which is elevated regardless.

## Product invariants — do not "improve" these away

These are the product, not preferences. If a task seems to require breaking one,
Expand Down
Loading
Loading
, '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
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
86 changes: 86 additions & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,6 +209,92 @@ green in the PR before you hand it back:
pnpm validate && pnpm typecheck && pnpm test && pnpm build
```

## How to write history — seeds, imports and fixtures

A `duly_task` cannot be created in `done` by an ordinary caller. `completed_at` is
`readonly`, the `beforeInsert` hook stamps only `last_update_at`, so
`completed_at_required_when_done` refuses the row:

```
ValidationError: A completed task must carry a completion timestamp. (code: VALIDATION_FAILED)
```

**That is correct and it stays.** Tasks are *dispatched* `open`; completion is a
later transition. The refusal is the product working.

Historical rows — a seed, an import, a fixture — are written from a **system
context** instead. `{ context: { isSystem: true } }` is the whole mechanism: it
exempts a write from the readonly strip. It is the same leg `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 takes two passes, and the second one is the half that gets forgotten.**

| Column | How you seed it |
|:-------|:----------------|
| `completed_at` | carried on the **insert**, from a system context |
| `last_update_at` | a **second pass in `mode: 'update'`** — an insert can never carry it |

Why `last_update_at` is different: `beforeInsert` stamps it 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. The `beforeUpdate` leg deliberately does not stamp on an
administrative write, so a second write carrying *only* `last_update_at` lands
untouched. Ergonomics card: #63.

Skip that second pass and there are no **stalled** rows — open tasks last touched
14+ days ago — so the "Not moving" view is empty on a freshly seeded demo. It is
the one signal the product claims is its most valuable, and nothing errors: the
seed reports success and the view is simply blank.

### The worked example

Three datasets, in this order, in `defineStack({ data })`:

```ts
// 1. sys_user FIRST. duly_task.owner is a user lookup and the seed loader
// resolves it as a NATURAL KEY against sys_user.name. A bare id that matches
// no sys_user row does not resolve, and because owner is required the whole
// task row is refused — "Owner is required" — with the loader also logging
// the unresolved reference. Measured: without this, nothing seeds.
{ object: 'sys_user', externalId: 'name', mode: 'insert',
records: [{ name: 'seed_user_alice', username: 'seed_user_alice', /* … */ }] },

// 2. The history itself. completed_at rides along on the insert.
{ object: 'duly_task', externalId: 'subject', mode: 'insert',
records: [{ subject: '…', owner: 'seed_user_alice', source: 'catalog',
status: 'done', completed_at: '2026-05-04T09:00:00.000Z' }] },

// 3. The SAME rows again, matched on externalId, to backdate the clock.
{ object: 'duly_task', externalId: 'subject', mode: 'update',
records: [{ subject: '…', last_update_at: '2026-07-18T08:00:00.000Z' }] },
```

Prefer **relative** instants (`Date.now() - 45 * DAY`) over literals for anything
whose *age* is the point: a hard-coded date stops being "stalled" as the repo ages.

Both directions are pinned by `test/seed-history.test.ts` — the system write
succeeds **and** an ordinary caller's identical write is still refused. Keep both
halves. A test that only proved "the seed may" would quietly turn `readonly: true`
into decoration.

### One caveat about which layer refuses

The insert-path readonly strip is a **boundary** guard: it lives in
`MetadataProtocolService.createData`, which is what `@objectstack/rest` — and so
REST, OpenAPI and MCP — writes through. `engine.insert` applies **no** readonly
strip at all (the update path is different: that strip is inside ObjectQL and is
`isSystem`-gated). So a direct `data.insert` from in-process code can set a
readonly column at insert with no context and no warning.

Filed upstream as **objectstack-ai/objectstack#14147**, and pinned as a tripwire in
`test/seed-history.test.ts` so it goes red when the platform closes it. Practical
consequence for authoring here: do not read an engine-level test as proof that a
column is protected — assert against `protocol.createData` when the refusal is the
thing you care about. Flows are unaffected today because `assignment.flow.ts`
declares `runAs: 'system'`, which is elevated regardless.

## Product invariants — do not "improve" these away

These are the product, not preferences. If a task seems to require breaking one,
Expand Down
Loading
Loading
, '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
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
86 changes: 86 additions & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,6 +209,92 @@ green in the PR before you hand it back:
pnpm validate && pnpm typecheck && pnpm test && pnpm build
```

## How to write history — seeds, imports and fixtures

A `duly_task` cannot be created in `done` by an ordinary caller. `completed_at` is
`readonly`, the `beforeInsert` hook stamps only `last_update_at`, so
`completed_at_required_when_done` refuses the row:

```
ValidationError: A completed task must carry a completion timestamp. (code: VALIDATION_FAILED)
```

**That is correct and it stays.** Tasks are *dispatched* `open`; completion is a
later transition. The refusal is the product working.

Historical rows — a seed, an import, a fixture — are written from a **system
context** instead. `{ context: { isSystem: true } }` is the whole mechanism: it
exempts a write from the readonly strip. It is the same leg `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 takes two passes, and the second one is the half that gets forgotten.**

| Column | How you seed it |
|:-------|:----------------|
| `completed_at` | carried on the **insert**, from a system context |
| `last_update_at` | a **second pass in `mode: 'update'`** — an insert can never carry it |

Why `last_update_at` is different: `beforeInsert` stamps it 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. The `beforeUpdate` leg deliberately does not stamp on an
administrative write, so a second write carrying *only* `last_update_at` lands
untouched. Ergonomics card: #63.

Skip that second pass and there are no **stalled** rows — open tasks last touched
14+ days ago — so the "Not moving" view is empty on a freshly seeded demo. It is
the one signal the product claims is its most valuable, and nothing errors: the
seed reports success and the view is simply blank.

### The worked example

Three datasets, in this order, in `defineStack({ data })`:

```ts
// 1. sys_user FIRST. duly_task.owner is a user lookup and the seed loader
// resolves it as a NATURAL KEY against sys_user.name. A bare id that matches
// no sys_user row does not resolve, and because owner is required the whole
// task row is refused — "Owner is required" — with the loader also logging
// the unresolved reference. Measured: without this, nothing seeds.
{ object: 'sys_user', externalId: 'name', mode: 'insert',
records: [{ name: 'seed_user_alice', username: 'seed_user_alice', /* … */ }] },

// 2. The history itself. completed_at rides along on the insert.
{ object: 'duly_task', externalId: 'subject', mode: 'insert',
records: [{ subject: '…', owner: 'seed_user_alice', source: 'catalog',
status: 'done', completed_at: '2026-05-04T09:00:00.000Z' }] },

// 3. The SAME rows again, matched on externalId, to backdate the clock.
{ object: 'duly_task', externalId: 'subject', mode: 'update',
records: [{ subject: '…', last_update_at: '2026-07-18T08:00:00.000Z' }] },
```

Prefer **relative** instants (`Date.now() - 45 * DAY`) over literals for anything
whose *age* is the point: a hard-coded date stops being "stalled" as the repo ages.

Both directions are pinned by `test/seed-history.test.ts` — the system write
succeeds **and** an ordinary caller's identical write is still refused. Keep both
halves. A test that only proved "the seed may" would quietly turn `readonly: true`
into decoration.

### One caveat about which layer refuses

The insert-path readonly strip is a **boundary** guard: it lives in
`MetadataProtocolService.createData`, which is what `@objectstack/rest` — and so
REST, OpenAPI and MCP — writes through. `engine.insert` applies **no** readonly
strip at all (the update path is different: that strip is inside ObjectQL and is
`isSystem`-gated). So a direct `data.insert` from in-process code can set a
readonly column at insert with no context and no warning.

Filed upstream as **objectstack-ai/objectstack#14147**, and pinned as a tripwire in
`test/seed-history.test.ts` so it goes red when the platform closes it. Practical
consequence for authoring here: do not read an engine-level test as proof that a
column is protected — assert against `protocol.createData` when the refusal is the
thing you care about. Flows are unaffected today because `assignment.flow.ts`
declares `runAs: 'system'`, which is elevated regardless.

## Product invariants — do not "improve" these away

These are the product, not preferences. If a task seems to require breaking one,
Expand Down
Loading
Loading
, '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
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
86 changes: 86 additions & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,6 +209,92 @@ green in the PR before you hand it back:
pnpm validate && pnpm typecheck && pnpm test && pnpm build
```

## How to write history — seeds, imports and fixtures

A `duly_task` cannot be created in `done` by an ordinary caller. `completed_at` is
`readonly`, the `beforeInsert` hook stamps only `last_update_at`, so
`completed_at_required_when_done` refuses the row:

```
ValidationError: A completed task must carry a completion timestamp. (code: VALIDATION_FAILED)
```

**That is correct and it stays.** Tasks are *dispatched* `open`; completion is a
later transition. The refusal is the product working.

Historical rows — a seed, an import, a fixture — are written from a **system
context** instead. `{ context: { isSystem: true } }` is the whole mechanism: it
exempts a write from the readonly strip. It is the same leg `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 takes two passes, and the second one is the half that gets forgotten.**

| Column | How you seed it |
|:-------|:----------------|
| `completed_at` | carried on the **insert**, from a system context |
| `last_update_at` | a **second pass in `mode: 'update'`** — an insert can never carry it |

Why `last_update_at` is different: `beforeInsert` stamps it 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. The `beforeUpdate` leg deliberately does not stamp on an
administrative write, so a second write carrying *only* `last_update_at` lands
untouched. Ergonomics card: #63.

Skip that second pass and there are no **stalled** rows — open tasks last touched
14+ days ago — so the "Not moving" view is empty on a freshly seeded demo. It is
the one signal the product claims is its most valuable, and nothing errors: the
seed reports success and the view is simply blank.

### The worked example

Three datasets, in this order, in `defineStack({ data })`:

```ts
// 1. sys_user FIRST. duly_task.owner is a user lookup and the seed loader
// resolves it as a NATURAL KEY against sys_user.name. A bare id that matches
// no sys_user row does not resolve, and because owner is required the whole
// task row is refused — "Owner is required" — with the loader also logging
// the unresolved reference. Measured: without this, nothing seeds.
{ object: 'sys_user', externalId: 'name', mode: 'insert',
records: [{ name: 'seed_user_alice', username: 'seed_user_alice', /* … */ }] },

// 2. The history itself. completed_at rides along on the insert.
{ object: 'duly_task', externalId: 'subject', mode: 'insert',
records: [{ subject: '…', owner: 'seed_user_alice', source: 'catalog',
status: 'done', completed_at: '2026-05-04T09:00:00.000Z' }] },

// 3. The SAME rows again, matched on externalId, to backdate the clock.
{ object: 'duly_task', externalId: 'subject', mode: 'update',
records: [{ subject: '…', last_update_at: '2026-07-18T08:00:00.000Z' }] },
```

Prefer **relative** instants (`Date.now() - 45 * DAY`) over literals for anything
whose *age* is the point: a hard-coded date stops being "stalled" as the repo ages.

Both directions are pinned by `test/seed-history.test.ts` — the system write
succeeds **and** an ordinary caller's identical write is still refused. Keep both
halves. A test that only proved "the seed may" would quietly turn `readonly: true`
into decoration.

### One caveat about which layer refuses

The insert-path readonly strip is a **boundary** guard: it lives in
`MetadataProtocolService.createData`, which is what `@objectstack/rest` — and so
REST, OpenAPI and MCP — writes through. `engine.insert` applies **no** readonly
strip at all (the update path is different: that strip is inside ObjectQL and is
`isSystem`-gated). So a direct `data.insert` from in-process code can set a
readonly column at insert with no context and no warning.

Filed upstream as **objectstack-ai/objectstack#14147**, and pinned as a tripwire in
`test/seed-history.test.ts` so it goes red when the platform closes it. Practical
consequence for authoring here: do not read an engine-level test as proof that a
column is protected — assert against `protocol.createData` when the refusal is the
thing you care about. Flows are unaffected today because `assignment.flow.ts`
declares `runAs: 'system'`, which is elevated regardless.

## Product invariants — do not "improve" these away

These are the product, not preferences. If a task seems to require breaking one,
Expand Down
Loading
Loading
, '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
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
86 changes: 86 additions & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,6 +209,92 @@ green in the PR before you hand it back:
pnpm validate && pnpm typecheck && pnpm test && pnpm build
```

## How to write history — seeds, imports and fixtures

A `duly_task` cannot be created in `done` by an ordinary caller. `completed_at` is
`readonly`, the `beforeInsert` hook stamps only `last_update_at`, so
`completed_at_required_when_done` refuses the row:

```
ValidationError: A completed task must carry a completion timestamp. (code: VALIDATION_FAILED)
```

**That is correct and it stays.** Tasks are *dispatched* `open`; completion is a
later transition. The refusal is the product working.

Historical rows — a seed, an import, a fixture — are written from a **system
context** instead. `{ context: { isSystem: true } }` is the whole mechanism: it
exempts a write from the readonly strip. It is the same leg `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 takes two passes, and the second one is the half that gets forgotten.**

| Column | How you seed it |
|:-------|:----------------|
| `completed_at` | carried on the **insert**, from a system context |
| `last_update_at` | a **second pass in `mode: 'update'`** — an insert can never carry it |

Why `last_update_at` is different: `beforeInsert` stamps it 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. The `beforeUpdate` leg deliberately does not stamp on an
administrative write, so a second write carrying *only* `last_update_at` lands
untouched. Ergonomics card: #63.

Skip that second pass and there are no **stalled** rows — open tasks last touched
14+ days ago — so the "Not moving" view is empty on a freshly seeded demo. It is
the one signal the product claims is its most valuable, and nothing errors: the
seed reports success and the view is simply blank.

### The worked example

Three datasets, in this order, in `defineStack({ data })`:

```ts
// 1. sys_user FIRST. duly_task.owner is a user lookup and the seed loader
// resolves it as a NATURAL KEY against sys_user.name. A bare id that matches
// no sys_user row does not resolve, and because owner is required the whole
// task row is refused — "Owner is required" — with the loader also logging
// the unresolved reference. Measured: without this, nothing seeds.
{ object: 'sys_user', externalId: 'name', mode: 'insert',
records: [{ name: 'seed_user_alice', username: 'seed_user_alice', /* … */ }] },

// 2. The history itself. completed_at rides along on the insert.
{ object: 'duly_task', externalId: 'subject', mode: 'insert',
records: [{ subject: '…', owner: 'seed_user_alice', source: 'catalog',
status: 'done', completed_at: '2026-05-04T09:00:00.000Z' }] },

// 3. The SAME rows again, matched on externalId, to backdate the clock.
{ object: 'duly_task', externalId: 'subject', mode: 'update',
records: [{ subject: '…', last_update_at: '2026-07-18T08:00:00.000Z' }] },
```

Prefer **relative** instants (`Date.now() - 45 * DAY`) over literals for anything
whose *age* is the point: a hard-coded date stops being "stalled" as the repo ages.

Both directions are pinned by `test/seed-history.test.ts` — the system write
succeeds **and** an ordinary caller's identical write is still refused. Keep both
halves. A test that only proved "the seed may" would quietly turn `readonly: true`
into decoration.

### One caveat about which layer refuses

The insert-path readonly strip is a **boundary** guard: it lives in
`MetadataProtocolService.createData`, which is what `@objectstack/rest` — and so
REST, OpenAPI and MCP — writes through. `engine.insert` applies **no** readonly
strip at all (the update path is different: that strip is inside ObjectQL and is
`isSystem`-gated). So a direct `data.insert` from in-process code can set a
readonly column at insert with no context and no warning.

Filed upstream as **objectstack-ai/objectstack#14147**, and pinned as a tripwire in
`test/seed-history.test.ts` so it goes red when the platform closes it. Practical
consequence for authoring here: do not read an engine-level test as proof that a
column is protected — assert against `protocol.createData` when the refusal is the
thing you care about. Flows are unaffected today because `assignment.flow.ts`
declares `runAs: 'system'`, which is elevated regardless.

## Product invariants — do not "improve" these away

These are the product, not preferences. If a task seems to require breaking one,
Expand Down
Loading
Loading
, '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
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
86 changes: 86 additions & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,6 +209,92 @@ green in the PR before you hand it back:
pnpm validate && pnpm typecheck && pnpm test && pnpm build
```

## How to write history — seeds, imports and fixtures

A `duly_task` cannot be created in `done` by an ordinary caller. `completed_at` is
`readonly`, the `beforeInsert` hook stamps only `last_update_at`, so
`completed_at_required_when_done` refuses the row:

```
ValidationError: A completed task must carry a completion timestamp. (code: VALIDATION_FAILED)
```

**That is correct and it stays.** Tasks are *dispatched* `open`; completion is a
later transition. The refusal is the product working.

Historical rows — a seed, an import, a fixture — are written from a **system
context** instead. `{ context: { isSystem: true } }` is the whole mechanism: it
exempts a write from the readonly strip. It is the same leg `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 takes two passes, and the second one is the half that gets forgotten.**

| Column | How you seed it |
|:-------|:----------------|
| `completed_at` | carried on the **insert**, from a system context |
| `last_update_at` | a **second pass in `mode: 'update'`** — an insert can never carry it |

Why `last_update_at` is different: `beforeInsert` stamps it 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. The `beforeUpdate` leg deliberately does not stamp on an
administrative write, so a second write carrying *only* `last_update_at` lands
untouched. Ergonomics card: #63.

Skip that second pass and there are no **stalled** rows — open tasks last touched
14+ days ago — so the "Not moving" view is empty on a freshly seeded demo. It is
the one signal the product claims is its most valuable, and nothing errors: the
seed reports success and the view is simply blank.

### The worked example

Three datasets, in this order, in `defineStack({ data })`:

```ts
// 1. sys_user FIRST. duly_task.owner is a user lookup and the seed loader
// resolves it as a NATURAL KEY against sys_user.name. A bare id that matches
// no sys_user row does not resolve, and because owner is required the whole
// task row is refused — "Owner is required" — with the loader also logging
// the unresolved reference. Measured: without this, nothing seeds.
{ object: 'sys_user', externalId: 'name', mode: 'insert',
records: [{ name: 'seed_user_alice', username: 'seed_user_alice', /* … */ }] },

// 2. The history itself. completed_at rides along on the insert.
{ object: 'duly_task', externalId: 'subject', mode: 'insert',
records: [{ subject: '…', owner: 'seed_user_alice', source: 'catalog',
status: 'done', completed_at: '2026-05-04T09:00:00.000Z' }] },

// 3. The SAME rows again, matched on externalId, to backdate the clock.
{ object: 'duly_task', externalId: 'subject', mode: 'update',
records: [{ subject: '…', last_update_at: '2026-07-18T08:00:00.000Z' }] },
```

Prefer **relative** instants (`Date.now() - 45 * DAY`) over literals for anything
whose *age* is the point: a hard-coded date stops being "stalled" as the repo ages.

Both directions are pinned by `test/seed-history.test.ts` — the system write
succeeds **and** an ordinary caller's identical write is still refused. Keep both
halves. A test that only proved "the seed may" would quietly turn `readonly: true`
into decoration.

### One caveat about which layer refuses

The insert-path readonly strip is a **boundary** guard: it lives in
`MetadataProtocolService.createData`, which is what `@objectstack/rest` — and so
REST, OpenAPI and MCP — writes through. `engine.insert` applies **no** readonly
strip at all (the update path is different: that strip is inside ObjectQL and is
`isSystem`-gated). So a direct `data.insert` from in-process code can set a
readonly column at insert with no context and no warning.

Filed upstream as **objectstack-ai/objectstack#14147**, and pinned as a tripwire in
`test/seed-history.test.ts` so it goes red when the platform closes it. Practical
consequence for authoring here: do not read an engine-level test as proof that a
column is protected — assert against `protocol.createData` when the refusal is the
thing you care about. Flows are unaffected today because `assignment.flow.ts`
declares `runAs: 'system'`, which is elevated regardless.

## Product invariants — do not "improve" these away

These are the product, not preferences. If a task seems to require breaking one,
Expand Down
Loading
Loading