Member detail page — open one person's duties without asking them - #112

Merged
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page
Sep 2, 2026
Merged

Member detail page — open one person's duties without asking them#112
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#13

A manager opens a person and sees everything, having entered nothing. duly_member is a read-only type: 'record' page over sys_user with the card's six sections in reading order, reachable from the Team nav group.

Gates green on e399e78 (the pushed head): validate 0 · typecheck 0 · test 0 (775 passed / 31 files) · build 0. The one validate warning is the expected open-edition state naming @objectstack/security-enterprise — AGENTS.md rule 7 says not to silence it.


The page, in the browser

Driven against pnpm demo on a private port with the 465-row demo seed, signed in as the demo admin, viewport 1400×1000 @2x. Screenshots live on the throwaway branch shots-issue-13 (not for merge).

Header, and "Right now" — name, unit, manager off the user record; position from the sys_user_position junction; then open work, the part past its grace, and the part nobody has touched in a fortnight.

Header and Right now

"Not moving" emphasised, and "This period" — the third number is the one that fires before a deadline exists, so it carries the rule. Governed duties lead with frequency and sort on it.

Not moving and This period

Standing duties (a different person, who has one) — listed, with no status column and nothing to tick.

Standing duties

"Assigned to them", and the bottom of the page — note the Discussion panel has no comment box. That took work; see §7.

Assigned to them

The Team nav group, with the People entry that opens a person.

Team nav

The emphasis on "Not moving" is ADR-0065 scoped styles, not a className — the build-time Tailwind never scans runtime metadata. Confirmed by computed style rather than by eye: objectui emitted .os-s-right_now_stalled { border-left: 3px solid #8C6512 !important; padding-left: 12px !important; } and the node computes 3px rgb(140, 101, 18) / padding-left: 12px.


The three things that must not be on the page

Each fails silently if broken — a log count reads as a helpful number, a "+ New" reads as a feature, a peer comparison reads as a dashboard. None turns anything red on its own, which is what test/member-page.test.ts is for.

The work log. The object's machine name appears nowhere in src/pages/member.page.ts — including in its comments, because the card's acceptance is a grep and a grep cannot read intent. The name, the reasoning and the guard live together in the test, which greps the source and walks the parsed page (objectName resolves to exactly duly_duty, duly_task, sys_user_position).

Editable controls. No record:details (its inlineEdit defaults ON where the object is editable — declaring the component is enough), no record:quick_actions, no element:form / button / text_input / record_picker, no add picker, no actions. Highlight chips carry readonly: true, which HeaderHighlight enforces. One residue remains and it is not ours to close — §3.

Comparison to peers. No aggregate component of any kind, and every list is bound to this record through relationshipField with no filter naming a user — not even {current_user_id}, which is the viewer rather than the person being read.

Both guards were ablated to prove they can fail. Injecting a duly_log_entry related list turned 3 assertions red; dropping readonly from one chip turned 1 red. Each mutation's anchor was asserted present exactly once before the run, the script carried a restore trap, and the tree was confirmed byte-identical afterwards.


What the platform can and cannot express here

The card asked for this to be measured rather than assumed. Seven gaps, each measured against the installed 17.2.0 packages, each filed at objectstack-ai/objectui rather than worked around, each carrying its number in the page header. Two were found only in the browser.

GapFiledWhat the page does instead
§1No record-context token for filter values — CONTEXT_TOKENS is {current_user_id} + {current_org_id}, both the viewer#7297"Right now" is three related lists with their own count badges. An element:number here would have counted the whole org under one person's name
§2record:related_list has no groupBy#7301frequency leads the columns and the sort. Weaker than grouping; not pretended otherwise
§3A page cannot declare its related lists read-only — "+ New" is host-resolved from the child object#7300Nothing. 8 "+ New" buttons render. Closing it object-side would close creation everywhere, which is a different and wrong change
§4Related-list columns cannot cross a lookup#7301The task list carries assignment — one click from the assigner, not zero
§5priority is read by usePageAssignment but not declared on PageSchema#7298Nothing authorable. See below
§6record:related_list cannot bind a multi-value field#7299Nothing. The one line of the card not delivered
§7The auto-appended discussion panel cannot be declined, and carries a comment box#7298Closed — by declaring the component that opens it

Three of these deserve more than a table row.

§6 — "showing who assigned each" is not delivered

This is the one requirement from the card that is missing, and it is missing because it cannot be written.

The assigner lives on duly_assignment.assigner. Two routes, both closed. A column on the task list would have to cross a lookup (§4). A related list on duly_assignment bound to assignees — the field that actually names this person — was authored, run against the seeded demo, and refused:

GET /api/v1/data/duly_assignment?filter=["assignees","=","ql8NO1TcKsuFrrH-"]
400 INVALID_FILTER — The bare equality spelling { "assignees": value } WAS NOT
APPLIED: "assignees" is a multi-value … field, stored by this driver as a JSON
TEXT column … Use "$contains" for membership …

The driver is behaving perfectly — loud, named, with the working spelling. But RelatedList composes its parent filter as bare equality and $contains is not reachable from RecordRelatedListProps, so the list cannot be written correctly at all. It was deleted rather than left rendering a 400.

§7 — a comment box that had to be closed by opening it

Not in the schema; found by reading the rendered page. RecordDetailView appends a chatter panel to every record page whose object does not set enable.feeds: false, hard-coded showCommentInput: true, enableReactions: true, enableThreading: true. sys_user is a locked platform object, so that switch is not ours, and the renderer's own documented escape (assignedPage.disableDiscussion = true) is not a PageSchema key — and PageSchema is a strictObject, so writing it is a parse error.

A comment box on a person's record is worse than merely editable here: it is where a performance note would go, on a page whose whole premise is that the manager enters nothing.

The fix is authorable but backwards — declaring an explicit record:discussion suppresses the append and hands its config to the author, where feed.showCommentInput: falseis honoured (RecordActivityTimeline.tsx:215, with objectui's own test pinning "hides it even when the host CAN persist a comment"). So the page declares one, with all three writes off. Verified: 0 textareas / contenteditables on the rendered page. The test pins this as a positive assertion, because deleting the node does not remove the panel — it hands it back to the host with the writes on.

§5 — which page wins, and why that is luck

@objectstack/platform-objects ships sys_user_detail for the same object with isDefault: true. Measured on this checkout: GET /api/v1/meta/page returns both, duly_member first, and it wins — by list position and a stable sort, since usePageAssignment breaks ties on a priority key nobody can author. isDefault is not consulted at all, which is why this page leaves it false rather than making the flag a lie on whichever page loses.


Notes for review

kind: 'full', not slotted, is a product decision. A slotted page falls through to buildDefaultPageSchema, whose tabs synthesizer emits one related list per object with an FK to sys_user — including the work-log object. The one thing this page must never show is the thing the default layout adds for free.

The nav entry is a people list, not a page link. A type: 'page' nav item routes through PageView, which mounts no RecordContextProvider — every related list would have a null parent and render nothing. It also needs requiresObject: 'sys_user': without it defineStack refuses the whole stack, because the nav cross-reference check resolves objectName against config.objects only. That is also the correct runtime behaviour (hide, don't 404).

Lateness and stagnation are asked the same way the existing views ask themlate_after < {today} and last_update_at < {14_days_ago} — so one person cannot be late on this page and on time on the Late list (#48).

Shared files.zh-CN.ts and authored-text.ts are touched, as flagged for the #107/#108/#109 batch; main was merged at 56b6aae (#110) and all four gates re-run after it. That merge also brought test/node-builtins.d.ts, a narrow declare module 'node:fs' — this branch had solved the same problem with Vite's ?raw, and converged on the merged shim rather than ship two ways to read a file in a test.

element:text copy is localized inline, not through the bundle, and that is a spec gap rather than a shortcut: PageTranslation.components declares title | description | label | placeholder | emptyText | submitLabel and has no key for content — the one string element:text renders. I18nLabelSchema accepts an inline { en, 'zh-CN' } map and that is the route the platform's own sys-user.page.ts uses. The 16 strings are declared in the walk's untranslatable ledger with that reason and pinned by count, so the day the key lands they move into the bundles and the count drops to zero.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p


Generated by Claude Code

os-warrenand others added 4 commits September 2, 2026 04:43
`duly_member`: a read-only `type: 'record'` page over `sys_user` that shows a
manager one person's whole picture with nothing entered — header, what is open
right now, what this period asks of them, what they hold permanently, what has
moved lately, and what was put on them by someone else.
Reached by opening a person: the Team nav group gains a `People` entry, since a
`type: 'page'` nav item routes through objectui's `PageView`, which mounts no
record context. `kind: 'full'` rather than `slotted` is a product decision — the
default-page synthesizer emits a related list per object with an FK to
`sys_user`, which on this stack includes the work-log object this page must
never show.
Five platform gaps were measured against the installed 17.2.0 packages and are
recorded on the page rather than worked around; the page's header comment names
each one and what it does instead. The load-bearing one: no record-context token
exists for filter values (`CONTEXT_TOKENS` is `{current_user_id}` +
`{current_org_id}`, both the VIEWER), so `element:number` cannot be scoped to
the person and "Right now" is three record-bound related lists with their own
count badges instead of three org-wide figures.
`test/member-page.test.ts` pins the four "not on this page" invariants, all of
which fail silently: the work-log object is greped out of the source and walked
out of the parsed page, no writing component type is declared, every highlight
chip is `readonly`, and no aggregate or peer comparison exists anywhere.
Gates: validate 0, typecheck 0, test 0 (768 passed / 30 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…omment box
Two findings from driving the seeded demo as the admin, neither visible to any
gate:
`assigned_by_whom` — a `duly_assignment` related list bound on `assignees` —
was REFUSED by the driver, 400 INVALID_FILTER: `RelatedList` builds its parent
filter as bare equality and `assignees` is `multiple: true`, which this driver
stores as a JSON TEXT column. `$contains` is the working spelling and it is not
reachable from `RecordRelatedListProps`. The list is removed rather than left
rendering an error; "showing who assigned each" is the one line of the card
that is not fully delivered, and it is recorded as gap §6 and filed.
`RecordDetailView` auto-appends a chatter panel — comment box, reactions,
threaded replies, all hard-coded on — to every record page whose object does
not set `enable.feeds: false`. `sys_user` is a locked platform object, and the
renderer's own opt-out (`disableDiscussion`) is not a `PageSchema` key, so it
cannot be written. Placing an EXPLICIT `record:discussion` suppresses the
append and takes its config back: all three writes off, leaving the record's
read-only history. Verified in the browser — the composer is gone
(0 textareas / contenteditables on the page).
Also measured and now recorded: the page wins the `sys_user` assignment against
the platform's `sys_user_detail` on this checkout, but by list position only
(both `priority ?? 0`, stable sort) — gap §5 stands.
Gates: validate 0, typecheck 0, test 0 (769 passed / 30 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…numbers
#110 landed `test/node-builtins.d.ts` — a narrow `declare module 'node:fs'`
exposing `readFileSync`, with the reasoning that `@types/node` stays out of a
metadata package. That is the same problem `test/raw-import.d.ts` solved here
with Vite's `?raw`, and one way to read a file in a test beats two, so this
drops the `?raw` declaration and uses the merged shim. Its `URL` overload is
what makes `node:url` unnecessary.
Also stamps every gap in the page header with the issue it was filed as, so a
gap without a number reads as a workaround wearing a comment:
§1 no record-context filter token objectui#7297
§2 related list cannot group objectui#7301
§3 no page-level read-only for a related list objectui#7300
§4 related-list columns cannot cross a lookup objectui#7301
§5 `priority` read but not declared objectui#7298
§6 related list cannot bind a multi-value field objectui#7299
§7 `disableDiscussion` read but not declared objectui#7298
Gates re-run on the merge: validate 0, typecheck 0, test 0 (775 passed /
31 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 2, 2026 05:09
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging.

Gates, re-run by me on the head merged with current main:validate 0, typecheck 0, test 0 (Test Files 31 passed, Tests 775 passed), build 0.

The three hard rules, checked in the source rather than taken from the report:duly_log_entry — 0 occurrences in the page; the single hit for percentile|rank|average is the header comment stating the prohibition, not an element; all 5 highlight chips readonly: true; the record:discussion block turns off showCommentInput, enableReactions, enableThreading, the filter and the subscription toggles. Reachable from the Team group as People.

The one line not delivered is the right kind of not-delivered. "Showing who assigned each" was authored two ways, run, and refused by the driver (400 INVALID_FILTERRelatedList composes bare equality and assignees is multiple: true), so the list was deleted rather than left rendering an error, and the task list carries the assignment column one click from the answer. A page that shows an error box on a demo is worse than a page that shows one fewer section. Filed as objectui#7299 with the exact refusal.

The discussion panel was the important catch. An auto-appended comment box with reactions and threaded replies, on a page whose whole premise is that the manager enters nothing — and on a person's record, which is precisely where a performance note would go. disableDiscussion is read by the renderer but is not a PageSchema key (objectui#7298), so the explicit record:discussion with every write off is the only authorable way to shut it, and it was verified as 0 composers on the rendered page. That is the difference between "read-only" as an intention and as a measurement.

Seven platform gaps measured against 17.2.0 and filed at objectui (#7297–#7301 plus the two above) instead of being worked around; the one you left for a PM call — PageTranslation.components has no content key for element:text — belongs at objectstack and I have filed it as objectstack#14412. Your reasoning was right: the schema comment names its two deliberate exclusions and this is not one of them.

Recording one measurement as a watch item rather than a fix: GET /api/v1/meta/page returns both duly_member and the platform's sys_user_detail for sys_user, and ours wins by list position and a stable sort only. A platform reordering would make this page vanish with no error. Filed as a finding so it is not re-derived.


Generated by Claude Code

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.

Member detail page — open one person's duties without asking them

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

Member detail page — open one person's duties without asking them - #112

Merged
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page
Sep 2, 2026
Merged

Member detail page — open one person's duties without asking them#112
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#13

A manager opens a person and sees everything, having entered nothing. duly_member is a read-only type: 'record' page over sys_user with the card's six sections in reading order, reachable from the Team nav group.

Gates green on e399e78 (the pushed head): validate 0 · typecheck 0 · test 0 (775 passed / 31 files) · build 0. The one validate warning is the expected open-edition state naming @objectstack/security-enterprise — AGENTS.md rule 7 says not to silence it.


The page, in the browser

Driven against pnpm demo on a private port with the 465-row demo seed, signed in as the demo admin, viewport 1400×1000 @2x. Screenshots live on the throwaway branch shots-issue-13 (not for merge).

Header, and "Right now" — name, unit, manager off the user record; position from the sys_user_position junction; then open work, the part past its grace, and the part nobody has touched in a fortnight.

Header and Right now

"Not moving" emphasised, and "This period" — the third number is the one that fires before a deadline exists, so it carries the rule. Governed duties lead with frequency and sort on it.

Not moving and This period

Standing duties (a different person, who has one) — listed, with no status column and nothing to tick.

Standing duties

"Assigned to them", and the bottom of the page — note the Discussion panel has no comment box. That took work; see §7.

Assigned to them

The Team nav group, with the People entry that opens a person.

Team nav

The emphasis on "Not moving" is ADR-0065 scoped styles, not a className — the build-time Tailwind never scans runtime metadata. Confirmed by computed style rather than by eye: objectui emitted .os-s-right_now_stalled { border-left: 3px solid #8C6512 !important; padding-left: 12px !important; } and the node computes 3px rgb(140, 101, 18) / padding-left: 12px.


The three things that must not be on the page

Each fails silently if broken — a log count reads as a helpful number, a "+ New" reads as a feature, a peer comparison reads as a dashboard. None turns anything red on its own, which is what test/member-page.test.ts is for.

The work log. The object's machine name appears nowhere in src/pages/member.page.ts — including in its comments, because the card's acceptance is a grep and a grep cannot read intent. The name, the reasoning and the guard live together in the test, which greps the source and walks the parsed page (objectName resolves to exactly duly_duty, duly_task, sys_user_position).

Editable controls. No record:details (its inlineEdit defaults ON where the object is editable — declaring the component is enough), no record:quick_actions, no element:form / button / text_input / record_picker, no add picker, no actions. Highlight chips carry readonly: true, which HeaderHighlight enforces. One residue remains and it is not ours to close — §3.

Comparison to peers. No aggregate component of any kind, and every list is bound to this record through relationshipField with no filter naming a user — not even {current_user_id}, which is the viewer rather than the person being read.

Both guards were ablated to prove they can fail. Injecting a duly_log_entry related list turned 3 assertions red; dropping readonly from one chip turned 1 red. Each mutation's anchor was asserted present exactly once before the run, the script carried a restore trap, and the tree was confirmed byte-identical afterwards.


What the platform can and cannot express here

The card asked for this to be measured rather than assumed. Seven gaps, each measured against the installed 17.2.0 packages, each filed at objectstack-ai/objectui rather than worked around, each carrying its number in the page header. Two were found only in the browser.

GapFiledWhat the page does instead
§1No record-context token for filter values — CONTEXT_TOKENS is {current_user_id} + {current_org_id}, both the viewer#7297"Right now" is three related lists with their own count badges. An element:number here would have counted the whole org under one person's name
§2record:related_list has no groupBy#7301frequency leads the columns and the sort. Weaker than grouping; not pretended otherwise
§3A page cannot declare its related lists read-only — "+ New" is host-resolved from the child object#7300Nothing. 8 "+ New" buttons render. Closing it object-side would close creation everywhere, which is a different and wrong change
§4Related-list columns cannot cross a lookup#7301The task list carries assignment — one click from the assigner, not zero
§5priority is read by usePageAssignment but not declared on PageSchema#7298Nothing authorable. See below
§6record:related_list cannot bind a multi-value field#7299Nothing. The one line of the card not delivered
§7The auto-appended discussion panel cannot be declined, and carries a comment box#7298Closed — by declaring the component that opens it

Three of these deserve more than a table row.

§6 — "showing who assigned each" is not delivered

This is the one requirement from the card that is missing, and it is missing because it cannot be written.

The assigner lives on duly_assignment.assigner. Two routes, both closed. A column on the task list would have to cross a lookup (§4). A related list on duly_assignment bound to assignees — the field that actually names this person — was authored, run against the seeded demo, and refused:

GET /api/v1/data/duly_assignment?filter=["assignees","=","ql8NO1TcKsuFrrH-"]
400 INVALID_FILTER — The bare equality spelling { "assignees": value } WAS NOT
APPLIED: "assignees" is a multi-value … field, stored by this driver as a JSON
TEXT column … Use "$contains" for membership …

The driver is behaving perfectly — loud, named, with the working spelling. But RelatedList composes its parent filter as bare equality and $contains is not reachable from RecordRelatedListProps, so the list cannot be written correctly at all. It was deleted rather than left rendering a 400.

§7 — a comment box that had to be closed by opening it

Not in the schema; found by reading the rendered page. RecordDetailView appends a chatter panel to every record page whose object does not set enable.feeds: false, hard-coded showCommentInput: true, enableReactions: true, enableThreading: true. sys_user is a locked platform object, so that switch is not ours, and the renderer's own documented escape (assignedPage.disableDiscussion = true) is not a PageSchema key — and PageSchema is a strictObject, so writing it is a parse error.

A comment box on a person's record is worse than merely editable here: it is where a performance note would go, on a page whose whole premise is that the manager enters nothing.

The fix is authorable but backwards — declaring an explicit record:discussion suppresses the append and hands its config to the author, where feed.showCommentInput: falseis honoured (RecordActivityTimeline.tsx:215, with objectui's own test pinning "hides it even when the host CAN persist a comment"). So the page declares one, with all three writes off. Verified: 0 textareas / contenteditables on the rendered page. The test pins this as a positive assertion, because deleting the node does not remove the panel — it hands it back to the host with the writes on.

§5 — which page wins, and why that is luck

@objectstack/platform-objects ships sys_user_detail for the same object with isDefault: true. Measured on this checkout: GET /api/v1/meta/page returns both, duly_member first, and it wins — by list position and a stable sort, since usePageAssignment breaks ties on a priority key nobody can author. isDefault is not consulted at all, which is why this page leaves it false rather than making the flag a lie on whichever page loses.


Notes for review

kind: 'full', not slotted, is a product decision. A slotted page falls through to buildDefaultPageSchema, whose tabs synthesizer emits one related list per object with an FK to sys_user — including the work-log object. The one thing this page must never show is the thing the default layout adds for free.

The nav entry is a people list, not a page link. A type: 'page' nav item routes through PageView, which mounts no RecordContextProvider — every related list would have a null parent and render nothing. It also needs requiresObject: 'sys_user': without it defineStack refuses the whole stack, because the nav cross-reference check resolves objectName against config.objects only. That is also the correct runtime behaviour (hide, don't 404).

Lateness and stagnation are asked the same way the existing views ask themlate_after < {today} and last_update_at < {14_days_ago} — so one person cannot be late on this page and on time on the Late list (#48).

Shared files.zh-CN.ts and authored-text.ts are touched, as flagged for the #107/#108/#109 batch; main was merged at 56b6aae (#110) and all four gates re-run after it. That merge also brought test/node-builtins.d.ts, a narrow declare module 'node:fs' — this branch had solved the same problem with Vite's ?raw, and converged on the merged shim rather than ship two ways to read a file in a test.

element:text copy is localized inline, not through the bundle, and that is a spec gap rather than a shortcut: PageTranslation.components declares title | description | label | placeholder | emptyText | submitLabel and has no key for content — the one string element:text renders. I18nLabelSchema accepts an inline { en, 'zh-CN' } map and that is the route the platform's own sys-user.page.ts uses. The 16 strings are declared in the walk's untranslatable ledger with that reason and pinned by count, so the day the key lands they move into the bundles and the count drops to zero.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p


Generated by Claude Code

os-warrenand others added 4 commits September 2, 2026 04:43
`duly_member`: a read-only `type: 'record'` page over `sys_user` that shows a
manager one person's whole picture with nothing entered — header, what is open
right now, what this period asks of them, what they hold permanently, what has
moved lately, and what was put on them by someone else.
Reached by opening a person: the Team nav group gains a `People` entry, since a
`type: 'page'` nav item routes through objectui's `PageView`, which mounts no
record context. `kind: 'full'` rather than `slotted` is a product decision — the
default-page synthesizer emits a related list per object with an FK to
`sys_user`, which on this stack includes the work-log object this page must
never show.
Five platform gaps were measured against the installed 17.2.0 packages and are
recorded on the page rather than worked around; the page's header comment names
each one and what it does instead. The load-bearing one: no record-context token
exists for filter values (`CONTEXT_TOKENS` is `{current_user_id}` +
`{current_org_id}`, both the VIEWER), so `element:number` cannot be scoped to
the person and "Right now" is three record-bound related lists with their own
count badges instead of three org-wide figures.
`test/member-page.test.ts` pins the four "not on this page" invariants, all of
which fail silently: the work-log object is greped out of the source and walked
out of the parsed page, no writing component type is declared, every highlight
chip is `readonly`, and no aggregate or peer comparison exists anywhere.
Gates: validate 0, typecheck 0, test 0 (768 passed / 30 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…omment box
Two findings from driving the seeded demo as the admin, neither visible to any
gate:
`assigned_by_whom` — a `duly_assignment` related list bound on `assignees` —
was REFUSED by the driver, 400 INVALID_FILTER: `RelatedList` builds its parent
filter as bare equality and `assignees` is `multiple: true`, which this driver
stores as a JSON TEXT column. `$contains` is the working spelling and it is not
reachable from `RecordRelatedListProps`. The list is removed rather than left
rendering an error; "showing who assigned each" is the one line of the card
that is not fully delivered, and it is recorded as gap §6 and filed.
`RecordDetailView` auto-appends a chatter panel — comment box, reactions,
threaded replies, all hard-coded on — to every record page whose object does
not set `enable.feeds: false`. `sys_user` is a locked platform object, and the
renderer's own opt-out (`disableDiscussion`) is not a `PageSchema` key, so it
cannot be written. Placing an EXPLICIT `record:discussion` suppresses the
append and takes its config back: all three writes off, leaving the record's
read-only history. Verified in the browser — the composer is gone
(0 textareas / contenteditables on the page).
Also measured and now recorded: the page wins the `sys_user` assignment against
the platform's `sys_user_detail` on this checkout, but by list position only
(both `priority ?? 0`, stable sort) — gap §5 stands.
Gates: validate 0, typecheck 0, test 0 (769 passed / 30 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…numbers
#110 landed `test/node-builtins.d.ts` — a narrow `declare module 'node:fs'`
exposing `readFileSync`, with the reasoning that `@types/node` stays out of a
metadata package. That is the same problem `test/raw-import.d.ts` solved here
with Vite's `?raw`, and one way to read a file in a test beats two, so this
drops the `?raw` declaration and uses the merged shim. Its `URL` overload is
what makes `node:url` unnecessary.
Also stamps every gap in the page header with the issue it was filed as, so a
gap without a number reads as a workaround wearing a comment:
§1 no record-context filter token objectui#7297
§2 related list cannot group objectui#7301
§3 no page-level read-only for a related list objectui#7300
§4 related-list columns cannot cross a lookup objectui#7301
§5 `priority` read but not declared objectui#7298
§6 related list cannot bind a multi-value field objectui#7299
§7 `disableDiscussion` read but not declared objectui#7298
Gates re-run on the merge: validate 0, typecheck 0, test 0 (775 passed /
31 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 2, 2026 05:09
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging.

Gates, re-run by me on the head merged with current main:validate 0, typecheck 0, test 0 (Test Files 31 passed, Tests 775 passed), build 0.

The three hard rules, checked in the source rather than taken from the report:duly_log_entry — 0 occurrences in the page; the single hit for percentile|rank|average is the header comment stating the prohibition, not an element; all 5 highlight chips readonly: true; the record:discussion block turns off showCommentInput, enableReactions, enableThreading, the filter and the subscription toggles. Reachable from the Team group as People.

The one line not delivered is the right kind of not-delivered. "Showing who assigned each" was authored two ways, run, and refused by the driver (400 INVALID_FILTERRelatedList composes bare equality and assignees is multiple: true), so the list was deleted rather than left rendering an error, and the task list carries the assignment column one click from the answer. A page that shows an error box on a demo is worse than a page that shows one fewer section. Filed as objectui#7299 with the exact refusal.

The discussion panel was the important catch. An auto-appended comment box with reactions and threaded replies, on a page whose whole premise is that the manager enters nothing — and on a person's record, which is precisely where a performance note would go. disableDiscussion is read by the renderer but is not a PageSchema key (objectui#7298), so the explicit record:discussion with every write off is the only authorable way to shut it, and it was verified as 0 composers on the rendered page. That is the difference between "read-only" as an intention and as a measurement.

Seven platform gaps measured against 17.2.0 and filed at objectui (#7297–#7301 plus the two above) instead of being worked around; the one you left for a PM call — PageTranslation.components has no content key for element:text — belongs at objectstack and I have filed it as objectstack#14412. Your reasoning was right: the schema comment names its two deliberate exclusions and this is not one of them.

Recording one measurement as a watch item rather than a fix: GET /api/v1/meta/page returns both duly_member and the platform's sys_user_detail for sys_user, and ours wins by list position and a stable sort only. A platform reordering would make this page vanish with no error. Filed as a finding so it is not re-derived.


Generated by Claude Code

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.

Member detail page — open one person's duties without asking them

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

Member detail page — open one person's duties without asking them - #112

Merged
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page
Sep 2, 2026
Merged

Member detail page — open one person's duties without asking them#112
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#13

A manager opens a person and sees everything, having entered nothing. duly_member is a read-only type: 'record' page over sys_user with the card's six sections in reading order, reachable from the Team nav group.

Gates green on e399e78 (the pushed head): validate 0 · typecheck 0 · test 0 (775 passed / 31 files) · build 0. The one validate warning is the expected open-edition state naming @objectstack/security-enterprise — AGENTS.md rule 7 says not to silence it.


The page, in the browser

Driven against pnpm demo on a private port with the 465-row demo seed, signed in as the demo admin, viewport 1400×1000 @2x. Screenshots live on the throwaway branch shots-issue-13 (not for merge).

Header, and "Right now" — name, unit, manager off the user record; position from the sys_user_position junction; then open work, the part past its grace, and the part nobody has touched in a fortnight.

Header and Right now

"Not moving" emphasised, and "This period" — the third number is the one that fires before a deadline exists, so it carries the rule. Governed duties lead with frequency and sort on it.

Not moving and This period

Standing duties (a different person, who has one) — listed, with no status column and nothing to tick.

Standing duties

"Assigned to them", and the bottom of the page — note the Discussion panel has no comment box. That took work; see §7.

Assigned to them

The Team nav group, with the People entry that opens a person.

Team nav

The emphasis on "Not moving" is ADR-0065 scoped styles, not a className — the build-time Tailwind never scans runtime metadata. Confirmed by computed style rather than by eye: objectui emitted .os-s-right_now_stalled { border-left: 3px solid #8C6512 !important; padding-left: 12px !important; } and the node computes 3px rgb(140, 101, 18) / padding-left: 12px.


The three things that must not be on the page

Each fails silently if broken — a log count reads as a helpful number, a "+ New" reads as a feature, a peer comparison reads as a dashboard. None turns anything red on its own, which is what test/member-page.test.ts is for.

The work log. The object's machine name appears nowhere in src/pages/member.page.ts — including in its comments, because the card's acceptance is a grep and a grep cannot read intent. The name, the reasoning and the guard live together in the test, which greps the source and walks the parsed page (objectName resolves to exactly duly_duty, duly_task, sys_user_position).

Editable controls. No record:details (its inlineEdit defaults ON where the object is editable — declaring the component is enough), no record:quick_actions, no element:form / button / text_input / record_picker, no add picker, no actions. Highlight chips carry readonly: true, which HeaderHighlight enforces. One residue remains and it is not ours to close — §3.

Comparison to peers. No aggregate component of any kind, and every list is bound to this record through relationshipField with no filter naming a user — not even {current_user_id}, which is the viewer rather than the person being read.

Both guards were ablated to prove they can fail. Injecting a duly_log_entry related list turned 3 assertions red; dropping readonly from one chip turned 1 red. Each mutation's anchor was asserted present exactly once before the run, the script carried a restore trap, and the tree was confirmed byte-identical afterwards.


What the platform can and cannot express here

The card asked for this to be measured rather than assumed. Seven gaps, each measured against the installed 17.2.0 packages, each filed at objectstack-ai/objectui rather than worked around, each carrying its number in the page header. Two were found only in the browser.

GapFiledWhat the page does instead
§1No record-context token for filter values — CONTEXT_TOKENS is {current_user_id} + {current_org_id}, both the viewer#7297"Right now" is three related lists with their own count badges. An element:number here would have counted the whole org under one person's name
§2record:related_list has no groupBy#7301frequency leads the columns and the sort. Weaker than grouping; not pretended otherwise
§3A page cannot declare its related lists read-only — "+ New" is host-resolved from the child object#7300Nothing. 8 "+ New" buttons render. Closing it object-side would close creation everywhere, which is a different and wrong change
§4Related-list columns cannot cross a lookup#7301The task list carries assignment — one click from the assigner, not zero
§5priority is read by usePageAssignment but not declared on PageSchema#7298Nothing authorable. See below
§6record:related_list cannot bind a multi-value field#7299Nothing. The one line of the card not delivered
§7The auto-appended discussion panel cannot be declined, and carries a comment box#7298Closed — by declaring the component that opens it

Three of these deserve more than a table row.

§6 — "showing who assigned each" is not delivered

This is the one requirement from the card that is missing, and it is missing because it cannot be written.

The assigner lives on duly_assignment.assigner. Two routes, both closed. A column on the task list would have to cross a lookup (§4). A related list on duly_assignment bound to assignees — the field that actually names this person — was authored, run against the seeded demo, and refused:

GET /api/v1/data/duly_assignment?filter=["assignees","=","ql8NO1TcKsuFrrH-"]
400 INVALID_FILTER — The bare equality spelling { "assignees": value } WAS NOT
APPLIED: "assignees" is a multi-value … field, stored by this driver as a JSON
TEXT column … Use "$contains" for membership …

The driver is behaving perfectly — loud, named, with the working spelling. But RelatedList composes its parent filter as bare equality and $contains is not reachable from RecordRelatedListProps, so the list cannot be written correctly at all. It was deleted rather than left rendering a 400.

§7 — a comment box that had to be closed by opening it

Not in the schema; found by reading the rendered page. RecordDetailView appends a chatter panel to every record page whose object does not set enable.feeds: false, hard-coded showCommentInput: true, enableReactions: true, enableThreading: true. sys_user is a locked platform object, so that switch is not ours, and the renderer's own documented escape (assignedPage.disableDiscussion = true) is not a PageSchema key — and PageSchema is a strictObject, so writing it is a parse error.

A comment box on a person's record is worse than merely editable here: it is where a performance note would go, on a page whose whole premise is that the manager enters nothing.

The fix is authorable but backwards — declaring an explicit record:discussion suppresses the append and hands its config to the author, where feed.showCommentInput: falseis honoured (RecordActivityTimeline.tsx:215, with objectui's own test pinning "hides it even when the host CAN persist a comment"). So the page declares one, with all three writes off. Verified: 0 textareas / contenteditables on the rendered page. The test pins this as a positive assertion, because deleting the node does not remove the panel — it hands it back to the host with the writes on.

§5 — which page wins, and why that is luck

@objectstack/platform-objects ships sys_user_detail for the same object with isDefault: true. Measured on this checkout: GET /api/v1/meta/page returns both, duly_member first, and it wins — by list position and a stable sort, since usePageAssignment breaks ties on a priority key nobody can author. isDefault is not consulted at all, which is why this page leaves it false rather than making the flag a lie on whichever page loses.


Notes for review

kind: 'full', not slotted, is a product decision. A slotted page falls through to buildDefaultPageSchema, whose tabs synthesizer emits one related list per object with an FK to sys_user — including the work-log object. The one thing this page must never show is the thing the default layout adds for free.

The nav entry is a people list, not a page link. A type: 'page' nav item routes through PageView, which mounts no RecordContextProvider — every related list would have a null parent and render nothing. It also needs requiresObject: 'sys_user': without it defineStack refuses the whole stack, because the nav cross-reference check resolves objectName against config.objects only. That is also the correct runtime behaviour (hide, don't 404).

Lateness and stagnation are asked the same way the existing views ask themlate_after < {today} and last_update_at < {14_days_ago} — so one person cannot be late on this page and on time on the Late list (#48).

Shared files.zh-CN.ts and authored-text.ts are touched, as flagged for the #107/#108/#109 batch; main was merged at 56b6aae (#110) and all four gates re-run after it. That merge also brought test/node-builtins.d.ts, a narrow declare module 'node:fs' — this branch had solved the same problem with Vite's ?raw, and converged on the merged shim rather than ship two ways to read a file in a test.

element:text copy is localized inline, not through the bundle, and that is a spec gap rather than a shortcut: PageTranslation.components declares title | description | label | placeholder | emptyText | submitLabel and has no key for content — the one string element:text renders. I18nLabelSchema accepts an inline { en, 'zh-CN' } map and that is the route the platform's own sys-user.page.ts uses. The 16 strings are declared in the walk's untranslatable ledger with that reason and pinned by count, so the day the key lands they move into the bundles and the count drops to zero.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p


Generated by Claude Code

os-warrenand others added 4 commits September 2, 2026 04:43
`duly_member`: a read-only `type: 'record'` page over `sys_user` that shows a
manager one person's whole picture with nothing entered — header, what is open
right now, what this period asks of them, what they hold permanently, what has
moved lately, and what was put on them by someone else.
Reached by opening a person: the Team nav group gains a `People` entry, since a
`type: 'page'` nav item routes through objectui's `PageView`, which mounts no
record context. `kind: 'full'` rather than `slotted` is a product decision — the
default-page synthesizer emits a related list per object with an FK to
`sys_user`, which on this stack includes the work-log object this page must
never show.
Five platform gaps were measured against the installed 17.2.0 packages and are
recorded on the page rather than worked around; the page's header comment names
each one and what it does instead. The load-bearing one: no record-context token
exists for filter values (`CONTEXT_TOKENS` is `{current_user_id}` +
`{current_org_id}`, both the VIEWER), so `element:number` cannot be scoped to
the person and "Right now" is three record-bound related lists with their own
count badges instead of three org-wide figures.
`test/member-page.test.ts` pins the four "not on this page" invariants, all of
which fail silently: the work-log object is greped out of the source and walked
out of the parsed page, no writing component type is declared, every highlight
chip is `readonly`, and no aggregate or peer comparison exists anywhere.
Gates: validate 0, typecheck 0, test 0 (768 passed / 30 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…omment box
Two findings from driving the seeded demo as the admin, neither visible to any
gate:
`assigned_by_whom` — a `duly_assignment` related list bound on `assignees` —
was REFUSED by the driver, 400 INVALID_FILTER: `RelatedList` builds its parent
filter as bare equality and `assignees` is `multiple: true`, which this driver
stores as a JSON TEXT column. `$contains` is the working spelling and it is not
reachable from `RecordRelatedListProps`. The list is removed rather than left
rendering an error; "showing who assigned each" is the one line of the card
that is not fully delivered, and it is recorded as gap §6 and filed.
`RecordDetailView` auto-appends a chatter panel — comment box, reactions,
threaded replies, all hard-coded on — to every record page whose object does
not set `enable.feeds: false`. `sys_user` is a locked platform object, and the
renderer's own opt-out (`disableDiscussion`) is not a `PageSchema` key, so it
cannot be written. Placing an EXPLICIT `record:discussion` suppresses the
append and takes its config back: all three writes off, leaving the record's
read-only history. Verified in the browser — the composer is gone
(0 textareas / contenteditables on the page).
Also measured and now recorded: the page wins the `sys_user` assignment against
the platform's `sys_user_detail` on this checkout, but by list position only
(both `priority ?? 0`, stable sort) — gap §5 stands.
Gates: validate 0, typecheck 0, test 0 (769 passed / 30 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…numbers
#110 landed `test/node-builtins.d.ts` — a narrow `declare module 'node:fs'`
exposing `readFileSync`, with the reasoning that `@types/node` stays out of a
metadata package. That is the same problem `test/raw-import.d.ts` solved here
with Vite's `?raw`, and one way to read a file in a test beats two, so this
drops the `?raw` declaration and uses the merged shim. Its `URL` overload is
what makes `node:url` unnecessary.
Also stamps every gap in the page header with the issue it was filed as, so a
gap without a number reads as a workaround wearing a comment:
§1 no record-context filter token objectui#7297
§2 related list cannot group objectui#7301
§3 no page-level read-only for a related list objectui#7300
§4 related-list columns cannot cross a lookup objectui#7301
§5 `priority` read but not declared objectui#7298
§6 related list cannot bind a multi-value field objectui#7299
§7 `disableDiscussion` read but not declared objectui#7298
Gates re-run on the merge: validate 0, typecheck 0, test 0 (775 passed /
31 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 2, 2026 05:09
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging.

Gates, re-run by me on the head merged with current main:validate 0, typecheck 0, test 0 (Test Files 31 passed, Tests 775 passed), build 0.

The three hard rules, checked in the source rather than taken from the report:duly_log_entry — 0 occurrences in the page; the single hit for percentile|rank|average is the header comment stating the prohibition, not an element; all 5 highlight chips readonly: true; the record:discussion block turns off showCommentInput, enableReactions, enableThreading, the filter and the subscription toggles. Reachable from the Team group as People.

The one line not delivered is the right kind of not-delivered. "Showing who assigned each" was authored two ways, run, and refused by the driver (400 INVALID_FILTERRelatedList composes bare equality and assignees is multiple: true), so the list was deleted rather than left rendering an error, and the task list carries the assignment column one click from the answer. A page that shows an error box on a demo is worse than a page that shows one fewer section. Filed as objectui#7299 with the exact refusal.

The discussion panel was the important catch. An auto-appended comment box with reactions and threaded replies, on a page whose whole premise is that the manager enters nothing — and on a person's record, which is precisely where a performance note would go. disableDiscussion is read by the renderer but is not a PageSchema key (objectui#7298), so the explicit record:discussion with every write off is the only authorable way to shut it, and it was verified as 0 composers on the rendered page. That is the difference between "read-only" as an intention and as a measurement.

Seven platform gaps measured against 17.2.0 and filed at objectui (#7297–#7301 plus the two above) instead of being worked around; the one you left for a PM call — PageTranslation.components has no content key for element:text — belongs at objectstack and I have filed it as objectstack#14412. Your reasoning was right: the schema comment names its two deliberate exclusions and this is not one of them.

Recording one measurement as a watch item rather than a fix: GET /api/v1/meta/page returns both duly_member and the platform's sys_user_detail for sys_user, and ours wins by list position and a stable sort only. A platform reordering would make this page vanish with no error. Filed as a finding so it is not re-derived.


Generated by Claude Code

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.

Member detail page — open one person's duties without asking them

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

Member detail page — open one person's duties without asking them - #112

Merged
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page
Sep 2, 2026
Merged

Member detail page — open one person's duties without asking them#112
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#13

A manager opens a person and sees everything, having entered nothing. duly_member is a read-only type: 'record' page over sys_user with the card's six sections in reading order, reachable from the Team nav group.

Gates green on e399e78 (the pushed head): validate 0 · typecheck 0 · test 0 (775 passed / 31 files) · build 0. The one validate warning is the expected open-edition state naming @objectstack/security-enterprise — AGENTS.md rule 7 says not to silence it.


The page, in the browser

Driven against pnpm demo on a private port with the 465-row demo seed, signed in as the demo admin, viewport 1400×1000 @2x. Screenshots live on the throwaway branch shots-issue-13 (not for merge).

Header, and "Right now" — name, unit, manager off the user record; position from the sys_user_position junction; then open work, the part past its grace, and the part nobody has touched in a fortnight.

Header and Right now

"Not moving" emphasised, and "This period" — the third number is the one that fires before a deadline exists, so it carries the rule. Governed duties lead with frequency and sort on it.

Not moving and This period

Standing duties (a different person, who has one) — listed, with no status column and nothing to tick.

Standing duties

"Assigned to them", and the bottom of the page — note the Discussion panel has no comment box. That took work; see §7.

Assigned to them

The Team nav group, with the People entry that opens a person.

Team nav

The emphasis on "Not moving" is ADR-0065 scoped styles, not a className — the build-time Tailwind never scans runtime metadata. Confirmed by computed style rather than by eye: objectui emitted .os-s-right_now_stalled { border-left: 3px solid #8C6512 !important; padding-left: 12px !important; } and the node computes 3px rgb(140, 101, 18) / padding-left: 12px.


The three things that must not be on the page

Each fails silently if broken — a log count reads as a helpful number, a "+ New" reads as a feature, a peer comparison reads as a dashboard. None turns anything red on its own, which is what test/member-page.test.ts is for.

The work log. The object's machine name appears nowhere in src/pages/member.page.ts — including in its comments, because the card's acceptance is a grep and a grep cannot read intent. The name, the reasoning and the guard live together in the test, which greps the source and walks the parsed page (objectName resolves to exactly duly_duty, duly_task, sys_user_position).

Editable controls. No record:details (its inlineEdit defaults ON where the object is editable — declaring the component is enough), no record:quick_actions, no element:form / button / text_input / record_picker, no add picker, no actions. Highlight chips carry readonly: true, which HeaderHighlight enforces. One residue remains and it is not ours to close — §3.

Comparison to peers. No aggregate component of any kind, and every list is bound to this record through relationshipField with no filter naming a user — not even {current_user_id}, which is the viewer rather than the person being read.

Both guards were ablated to prove they can fail. Injecting a duly_log_entry related list turned 3 assertions red; dropping readonly from one chip turned 1 red. Each mutation's anchor was asserted present exactly once before the run, the script carried a restore trap, and the tree was confirmed byte-identical afterwards.


What the platform can and cannot express here

The card asked for this to be measured rather than assumed. Seven gaps, each measured against the installed 17.2.0 packages, each filed at objectstack-ai/objectui rather than worked around, each carrying its number in the page header. Two were found only in the browser.

GapFiledWhat the page does instead
§1No record-context token for filter values — CONTEXT_TOKENS is {current_user_id} + {current_org_id}, both the viewer#7297"Right now" is three related lists with their own count badges. An element:number here would have counted the whole org under one person's name
§2record:related_list has no groupBy#7301frequency leads the columns and the sort. Weaker than grouping; not pretended otherwise
§3A page cannot declare its related lists read-only — "+ New" is host-resolved from the child object#7300Nothing. 8 "+ New" buttons render. Closing it object-side would close creation everywhere, which is a different and wrong change
§4Related-list columns cannot cross a lookup#7301The task list carries assignment — one click from the assigner, not zero
§5priority is read by usePageAssignment but not declared on PageSchema#7298Nothing authorable. See below
§6record:related_list cannot bind a multi-value field#7299Nothing. The one line of the card not delivered
§7The auto-appended discussion panel cannot be declined, and carries a comment box#7298Closed — by declaring the component that opens it

Three of these deserve more than a table row.

§6 — "showing who assigned each" is not delivered

This is the one requirement from the card that is missing, and it is missing because it cannot be written.

The assigner lives on duly_assignment.assigner. Two routes, both closed. A column on the task list would have to cross a lookup (§4). A related list on duly_assignment bound to assignees — the field that actually names this person — was authored, run against the seeded demo, and refused:

GET /api/v1/data/duly_assignment?filter=["assignees","=","ql8NO1TcKsuFrrH-"]
400 INVALID_FILTER — The bare equality spelling { "assignees": value } WAS NOT
APPLIED: "assignees" is a multi-value … field, stored by this driver as a JSON
TEXT column … Use "$contains" for membership …

The driver is behaving perfectly — loud, named, with the working spelling. But RelatedList composes its parent filter as bare equality and $contains is not reachable from RecordRelatedListProps, so the list cannot be written correctly at all. It was deleted rather than left rendering a 400.

§7 — a comment box that had to be closed by opening it

Not in the schema; found by reading the rendered page. RecordDetailView appends a chatter panel to every record page whose object does not set enable.feeds: false, hard-coded showCommentInput: true, enableReactions: true, enableThreading: true. sys_user is a locked platform object, so that switch is not ours, and the renderer's own documented escape (assignedPage.disableDiscussion = true) is not a PageSchema key — and PageSchema is a strictObject, so writing it is a parse error.

A comment box on a person's record is worse than merely editable here: it is where a performance note would go, on a page whose whole premise is that the manager enters nothing.

The fix is authorable but backwards — declaring an explicit record:discussion suppresses the append and hands its config to the author, where feed.showCommentInput: falseis honoured (RecordActivityTimeline.tsx:215, with objectui's own test pinning "hides it even when the host CAN persist a comment"). So the page declares one, with all three writes off. Verified: 0 textareas / contenteditables on the rendered page. The test pins this as a positive assertion, because deleting the node does not remove the panel — it hands it back to the host with the writes on.

§5 — which page wins, and why that is luck

@objectstack/platform-objects ships sys_user_detail for the same object with isDefault: true. Measured on this checkout: GET /api/v1/meta/page returns both, duly_member first, and it wins — by list position and a stable sort, since usePageAssignment breaks ties on a priority key nobody can author. isDefault is not consulted at all, which is why this page leaves it false rather than making the flag a lie on whichever page loses.


Notes for review

kind: 'full', not slotted, is a product decision. A slotted page falls through to buildDefaultPageSchema, whose tabs synthesizer emits one related list per object with an FK to sys_user — including the work-log object. The one thing this page must never show is the thing the default layout adds for free.

The nav entry is a people list, not a page link. A type: 'page' nav item routes through PageView, which mounts no RecordContextProvider — every related list would have a null parent and render nothing. It also needs requiresObject: 'sys_user': without it defineStack refuses the whole stack, because the nav cross-reference check resolves objectName against config.objects only. That is also the correct runtime behaviour (hide, don't 404).

Lateness and stagnation are asked the same way the existing views ask themlate_after < {today} and last_update_at < {14_days_ago} — so one person cannot be late on this page and on time on the Late list (#48).

Shared files.zh-CN.ts and authored-text.ts are touched, as flagged for the #107/#108/#109 batch; main was merged at 56b6aae (#110) and all four gates re-run after it. That merge also brought test/node-builtins.d.ts, a narrow declare module 'node:fs' — this branch had solved the same problem with Vite's ?raw, and converged on the merged shim rather than ship two ways to read a file in a test.

element:text copy is localized inline, not through the bundle, and that is a spec gap rather than a shortcut: PageTranslation.components declares title | description | label | placeholder | emptyText | submitLabel and has no key for content — the one string element:text renders. I18nLabelSchema accepts an inline { en, 'zh-CN' } map and that is the route the platform's own sys-user.page.ts uses. The 16 strings are declared in the walk's untranslatable ledger with that reason and pinned by count, so the day the key lands they move into the bundles and the count drops to zero.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p


Generated by Claude Code

os-warrenand others added 4 commits September 2, 2026 04:43
`duly_member`: a read-only `type: 'record'` page over `sys_user` that shows a
manager one person's whole picture with nothing entered — header, what is open
right now, what this period asks of them, what they hold permanently, what has
moved lately, and what was put on them by someone else.
Reached by opening a person: the Team nav group gains a `People` entry, since a
`type: 'page'` nav item routes through objectui's `PageView`, which mounts no
record context. `kind: 'full'` rather than `slotted` is a product decision — the
default-page synthesizer emits a related list per object with an FK to
`sys_user`, which on this stack includes the work-log object this page must
never show.
Five platform gaps were measured against the installed 17.2.0 packages and are
recorded on the page rather than worked around; the page's header comment names
each one and what it does instead. The load-bearing one: no record-context token
exists for filter values (`CONTEXT_TOKENS` is `{current_user_id}` +
`{current_org_id}`, both the VIEWER), so `element:number` cannot be scoped to
the person and "Right now" is three record-bound related lists with their own
count badges instead of three org-wide figures.
`test/member-page.test.ts` pins the four "not on this page" invariants, all of
which fail silently: the work-log object is greped out of the source and walked
out of the parsed page, no writing component type is declared, every highlight
chip is `readonly`, and no aggregate or peer comparison exists anywhere.
Gates: validate 0, typecheck 0, test 0 (768 passed / 30 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…omment box
Two findings from driving the seeded demo as the admin, neither visible to any
gate:
`assigned_by_whom` — a `duly_assignment` related list bound on `assignees` —
was REFUSED by the driver, 400 INVALID_FILTER: `RelatedList` builds its parent
filter as bare equality and `assignees` is `multiple: true`, which this driver
stores as a JSON TEXT column. `$contains` is the working spelling and it is not
reachable from `RecordRelatedListProps`. The list is removed rather than left
rendering an error; "showing who assigned each" is the one line of the card
that is not fully delivered, and it is recorded as gap §6 and filed.
`RecordDetailView` auto-appends a chatter panel — comment box, reactions,
threaded replies, all hard-coded on — to every record page whose object does
not set `enable.feeds: false`. `sys_user` is a locked platform object, and the
renderer's own opt-out (`disableDiscussion`) is not a `PageSchema` key, so it
cannot be written. Placing an EXPLICIT `record:discussion` suppresses the
append and takes its config back: all three writes off, leaving the record's
read-only history. Verified in the browser — the composer is gone
(0 textareas / contenteditables on the page).
Also measured and now recorded: the page wins the `sys_user` assignment against
the platform's `sys_user_detail` on this checkout, but by list position only
(both `priority ?? 0`, stable sort) — gap §5 stands.
Gates: validate 0, typecheck 0, test 0 (769 passed / 30 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…numbers
#110 landed `test/node-builtins.d.ts` — a narrow `declare module 'node:fs'`
exposing `readFileSync`, with the reasoning that `@types/node` stays out of a
metadata package. That is the same problem `test/raw-import.d.ts` solved here
with Vite's `?raw`, and one way to read a file in a test beats two, so this
drops the `?raw` declaration and uses the merged shim. Its `URL` overload is
what makes `node:url` unnecessary.
Also stamps every gap in the page header with the issue it was filed as, so a
gap without a number reads as a workaround wearing a comment:
§1 no record-context filter token objectui#7297
§2 related list cannot group objectui#7301
§3 no page-level read-only for a related list objectui#7300
§4 related-list columns cannot cross a lookup objectui#7301
§5 `priority` read but not declared objectui#7298
§6 related list cannot bind a multi-value field objectui#7299
§7 `disableDiscussion` read but not declared objectui#7298
Gates re-run on the merge: validate 0, typecheck 0, test 0 (775 passed /
31 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 2, 2026 05:09
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging.

Gates, re-run by me on the head merged with current main:validate 0, typecheck 0, test 0 (Test Files 31 passed, Tests 775 passed), build 0.

The three hard rules, checked in the source rather than taken from the report:duly_log_entry — 0 occurrences in the page; the single hit for percentile|rank|average is the header comment stating the prohibition, not an element; all 5 highlight chips readonly: true; the record:discussion block turns off showCommentInput, enableReactions, enableThreading, the filter and the subscription toggles. Reachable from the Team group as People.

The one line not delivered is the right kind of not-delivered. "Showing who assigned each" was authored two ways, run, and refused by the driver (400 INVALID_FILTERRelatedList composes bare equality and assignees is multiple: true), so the list was deleted rather than left rendering an error, and the task list carries the assignment column one click from the answer. A page that shows an error box on a demo is worse than a page that shows one fewer section. Filed as objectui#7299 with the exact refusal.

The discussion panel was the important catch. An auto-appended comment box with reactions and threaded replies, on a page whose whole premise is that the manager enters nothing — and on a person's record, which is precisely where a performance note would go. disableDiscussion is read by the renderer but is not a PageSchema key (objectui#7298), so the explicit record:discussion with every write off is the only authorable way to shut it, and it was verified as 0 composers on the rendered page. That is the difference between "read-only" as an intention and as a measurement.

Seven platform gaps measured against 17.2.0 and filed at objectui (#7297–#7301 plus the two above) instead of being worked around; the one you left for a PM call — PageTranslation.components has no content key for element:text — belongs at objectstack and I have filed it as objectstack#14412. Your reasoning was right: the schema comment names its two deliberate exclusions and this is not one of them.

Recording one measurement as a watch item rather than a fix: GET /api/v1/meta/page returns both duly_member and the platform's sys_user_detail for sys_user, and ours wins by list position and a stable sort only. A platform reordering would make this page vanish with no error. Filed as a finding so it is not re-derived.


Generated by Claude Code

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.

Member detail page — open one person's duties without asking them

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

Member detail page — open one person's duties without asking them - #112

Merged
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page
Sep 2, 2026
Merged

Member detail page — open one person's duties without asking them#112
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#13

A manager opens a person and sees everything, having entered nothing. duly_member is a read-only type: 'record' page over sys_user with the card's six sections in reading order, reachable from the Team nav group.

Gates green on e399e78 (the pushed head): validate 0 · typecheck 0 · test 0 (775 passed / 31 files) · build 0. The one validate warning is the expected open-edition state naming @objectstack/security-enterprise — AGENTS.md rule 7 says not to silence it.


The page, in the browser

Driven against pnpm demo on a private port with the 465-row demo seed, signed in as the demo admin, viewport 1400×1000 @2x. Screenshots live on the throwaway branch shots-issue-13 (not for merge).

Header, and "Right now" — name, unit, manager off the user record; position from the sys_user_position junction; then open work, the part past its grace, and the part nobody has touched in a fortnight.

Header and Right now

"Not moving" emphasised, and "This period" — the third number is the one that fires before a deadline exists, so it carries the rule. Governed duties lead with frequency and sort on it.

Not moving and This period

Standing duties (a different person, who has one) — listed, with no status column and nothing to tick.

Standing duties

"Assigned to them", and the bottom of the page — note the Discussion panel has no comment box. That took work; see §7.

Assigned to them

The Team nav group, with the People entry that opens a person.

Team nav

The emphasis on "Not moving" is ADR-0065 scoped styles, not a className — the build-time Tailwind never scans runtime metadata. Confirmed by computed style rather than by eye: objectui emitted .os-s-right_now_stalled { border-left: 3px solid #8C6512 !important; padding-left: 12px !important; } and the node computes 3px rgb(140, 101, 18) / padding-left: 12px.


The three things that must not be on the page

Each fails silently if broken — a log count reads as a helpful number, a "+ New" reads as a feature, a peer comparison reads as a dashboard. None turns anything red on its own, which is what test/member-page.test.ts is for.

The work log. The object's machine name appears nowhere in src/pages/member.page.ts — including in its comments, because the card's acceptance is a grep and a grep cannot read intent. The name, the reasoning and the guard live together in the test, which greps the source and walks the parsed page (objectName resolves to exactly duly_duty, duly_task, sys_user_position).

Editable controls. No record:details (its inlineEdit defaults ON where the object is editable — declaring the component is enough), no record:quick_actions, no element:form / button / text_input / record_picker, no add picker, no actions. Highlight chips carry readonly: true, which HeaderHighlight enforces. One residue remains and it is not ours to close — §3.

Comparison to peers. No aggregate component of any kind, and every list is bound to this record through relationshipField with no filter naming a user — not even {current_user_id}, which is the viewer rather than the person being read.

Both guards were ablated to prove they can fail. Injecting a duly_log_entry related list turned 3 assertions red; dropping readonly from one chip turned 1 red. Each mutation's anchor was asserted present exactly once before the run, the script carried a restore trap, and the tree was confirmed byte-identical afterwards.


What the platform can and cannot express here

The card asked for this to be measured rather than assumed. Seven gaps, each measured against the installed 17.2.0 packages, each filed at objectstack-ai/objectui rather than worked around, each carrying its number in the page header. Two were found only in the browser.

GapFiledWhat the page does instead
§1No record-context token for filter values — CONTEXT_TOKENS is {current_user_id} + {current_org_id}, both the viewer#7297"Right now" is three related lists with their own count badges. An element:number here would have counted the whole org under one person's name
§2record:related_list has no groupBy#7301frequency leads the columns and the sort. Weaker than grouping; not pretended otherwise
§3A page cannot declare its related lists read-only — "+ New" is host-resolved from the child object#7300Nothing. 8 "+ New" buttons render. Closing it object-side would close creation everywhere, which is a different and wrong change
§4Related-list columns cannot cross a lookup#7301The task list carries assignment — one click from the assigner, not zero
§5priority is read by usePageAssignment but not declared on PageSchema#7298Nothing authorable. See below
§6record:related_list cannot bind a multi-value field#7299Nothing. The one line of the card not delivered
§7The auto-appended discussion panel cannot be declined, and carries a comment box#7298Closed — by declaring the component that opens it

Three of these deserve more than a table row.

§6 — "showing who assigned each" is not delivered

This is the one requirement from the card that is missing, and it is missing because it cannot be written.

The assigner lives on duly_assignment.assigner. Two routes, both closed. A column on the task list would have to cross a lookup (§4). A related list on duly_assignment bound to assignees — the field that actually names this person — was authored, run against the seeded demo, and refused:

GET /api/v1/data/duly_assignment?filter=["assignees","=","ql8NO1TcKsuFrrH-"]
400 INVALID_FILTER — The bare equality spelling { "assignees": value } WAS NOT
APPLIED: "assignees" is a multi-value … field, stored by this driver as a JSON
TEXT column … Use "$contains" for membership …

The driver is behaving perfectly — loud, named, with the working spelling. But RelatedList composes its parent filter as bare equality and $contains is not reachable from RecordRelatedListProps, so the list cannot be written correctly at all. It was deleted rather than left rendering a 400.

§7 — a comment box that had to be closed by opening it

Not in the schema; found by reading the rendered page. RecordDetailView appends a chatter panel to every record page whose object does not set enable.feeds: false, hard-coded showCommentInput: true, enableReactions: true, enableThreading: true. sys_user is a locked platform object, so that switch is not ours, and the renderer's own documented escape (assignedPage.disableDiscussion = true) is not a PageSchema key — and PageSchema is a strictObject, so writing it is a parse error.

A comment box on a person's record is worse than merely editable here: it is where a performance note would go, on a page whose whole premise is that the manager enters nothing.

The fix is authorable but backwards — declaring an explicit record:discussion suppresses the append and hands its config to the author, where feed.showCommentInput: falseis honoured (RecordActivityTimeline.tsx:215, with objectui's own test pinning "hides it even when the host CAN persist a comment"). So the page declares one, with all three writes off. Verified: 0 textareas / contenteditables on the rendered page. The test pins this as a positive assertion, because deleting the node does not remove the panel — it hands it back to the host with the writes on.

§5 — which page wins, and why that is luck

@objectstack/platform-objects ships sys_user_detail for the same object with isDefault: true. Measured on this checkout: GET /api/v1/meta/page returns both, duly_member first, and it wins — by list position and a stable sort, since usePageAssignment breaks ties on a priority key nobody can author. isDefault is not consulted at all, which is why this page leaves it false rather than making the flag a lie on whichever page loses.


Notes for review

kind: 'full', not slotted, is a product decision. A slotted page falls through to buildDefaultPageSchema, whose tabs synthesizer emits one related list per object with an FK to sys_user — including the work-log object. The one thing this page must never show is the thing the default layout adds for free.

The nav entry is a people list, not a page link. A type: 'page' nav item routes through PageView, which mounts no RecordContextProvider — every related list would have a null parent and render nothing. It also needs requiresObject: 'sys_user': without it defineStack refuses the whole stack, because the nav cross-reference check resolves objectName against config.objects only. That is also the correct runtime behaviour (hide, don't 404).

Lateness and stagnation are asked the same way the existing views ask themlate_after < {today} and last_update_at < {14_days_ago} — so one person cannot be late on this page and on time on the Late list (#48).

Shared files.zh-CN.ts and authored-text.ts are touched, as flagged for the #107/#108/#109 batch; main was merged at 56b6aae (#110) and all four gates re-run after it. That merge also brought test/node-builtins.d.ts, a narrow declare module 'node:fs' — this branch had solved the same problem with Vite's ?raw, and converged on the merged shim rather than ship two ways to read a file in a test.

element:text copy is localized inline, not through the bundle, and that is a spec gap rather than a shortcut: PageTranslation.components declares title | description | label | placeholder | emptyText | submitLabel and has no key for content — the one string element:text renders. I18nLabelSchema accepts an inline { en, 'zh-CN' } map and that is the route the platform's own sys-user.page.ts uses. The 16 strings are declared in the walk's untranslatable ledger with that reason and pinned by count, so the day the key lands they move into the bundles and the count drops to zero.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p


Generated by Claude Code

os-warrenand others added 4 commits September 2, 2026 04:43
`duly_member`: a read-only `type: 'record'` page over `sys_user` that shows a
manager one person's whole picture with nothing entered — header, what is open
right now, what this period asks of them, what they hold permanently, what has
moved lately, and what was put on them by someone else.
Reached by opening a person: the Team nav group gains a `People` entry, since a
`type: 'page'` nav item routes through objectui's `PageView`, which mounts no
record context. `kind: 'full'` rather than `slotted` is a product decision — the
default-page synthesizer emits a related list per object with an FK to
`sys_user`, which on this stack includes the work-log object this page must
never show.
Five platform gaps were measured against the installed 17.2.0 packages and are
recorded on the page rather than worked around; the page's header comment names
each one and what it does instead. The load-bearing one: no record-context token
exists for filter values (`CONTEXT_TOKENS` is `{current_user_id}` +
`{current_org_id}`, both the VIEWER), so `element:number` cannot be scoped to
the person and "Right now" is three record-bound related lists with their own
count badges instead of three org-wide figures.
`test/member-page.test.ts` pins the four "not on this page" invariants, all of
which fail silently: the work-log object is greped out of the source and walked
out of the parsed page, no writing component type is declared, every highlight
chip is `readonly`, and no aggregate or peer comparison exists anywhere.
Gates: validate 0, typecheck 0, test 0 (768 passed / 30 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…omment box
Two findings from driving the seeded demo as the admin, neither visible to any
gate:
`assigned_by_whom` — a `duly_assignment` related list bound on `assignees` —
was REFUSED by the driver, 400 INVALID_FILTER: `RelatedList` builds its parent
filter as bare equality and `assignees` is `multiple: true`, which this driver
stores as a JSON TEXT column. `$contains` is the working spelling and it is not
reachable from `RecordRelatedListProps`. The list is removed rather than left
rendering an error; "showing who assigned each" is the one line of the card
that is not fully delivered, and it is recorded as gap §6 and filed.
`RecordDetailView` auto-appends a chatter panel — comment box, reactions,
threaded replies, all hard-coded on — to every record page whose object does
not set `enable.feeds: false`. `sys_user` is a locked platform object, and the
renderer's own opt-out (`disableDiscussion`) is not a `PageSchema` key, so it
cannot be written. Placing an EXPLICIT `record:discussion` suppresses the
append and takes its config back: all three writes off, leaving the record's
read-only history. Verified in the browser — the composer is gone
(0 textareas / contenteditables on the page).
Also measured and now recorded: the page wins the `sys_user` assignment against
the platform's `sys_user_detail` on this checkout, but by list position only
(both `priority ?? 0`, stable sort) — gap §5 stands.
Gates: validate 0, typecheck 0, test 0 (769 passed / 30 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…numbers
#110 landed `test/node-builtins.d.ts` — a narrow `declare module 'node:fs'`
exposing `readFileSync`, with the reasoning that `@types/node` stays out of a
metadata package. That is the same problem `test/raw-import.d.ts` solved here
with Vite's `?raw`, and one way to read a file in a test beats two, so this
drops the `?raw` declaration and uses the merged shim. Its `URL` overload is
what makes `node:url` unnecessary.
Also stamps every gap in the page header with the issue it was filed as, so a
gap without a number reads as a workaround wearing a comment:
§1 no record-context filter token objectui#7297
§2 related list cannot group objectui#7301
§3 no page-level read-only for a related list objectui#7300
§4 related-list columns cannot cross a lookup objectui#7301
§5 `priority` read but not declared objectui#7298
§6 related list cannot bind a multi-value field objectui#7299
§7 `disableDiscussion` read but not declared objectui#7298
Gates re-run on the merge: validate 0, typecheck 0, test 0 (775 passed /
31 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 2, 2026 05:09
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging.

Gates, re-run by me on the head merged with current main:validate 0, typecheck 0, test 0 (Test Files 31 passed, Tests 775 passed), build 0.

The three hard rules, checked in the source rather than taken from the report:duly_log_entry — 0 occurrences in the page; the single hit for percentile|rank|average is the header comment stating the prohibition, not an element; all 5 highlight chips readonly: true; the record:discussion block turns off showCommentInput, enableReactions, enableThreading, the filter and the subscription toggles. Reachable from the Team group as People.

The one line not delivered is the right kind of not-delivered. "Showing who assigned each" was authored two ways, run, and refused by the driver (400 INVALID_FILTERRelatedList composes bare equality and assignees is multiple: true), so the list was deleted rather than left rendering an error, and the task list carries the assignment column one click from the answer. A page that shows an error box on a demo is worse than a page that shows one fewer section. Filed as objectui#7299 with the exact refusal.

The discussion panel was the important catch. An auto-appended comment box with reactions and threaded replies, on a page whose whole premise is that the manager enters nothing — and on a person's record, which is precisely where a performance note would go. disableDiscussion is read by the renderer but is not a PageSchema key (objectui#7298), so the explicit record:discussion with every write off is the only authorable way to shut it, and it was verified as 0 composers on the rendered page. That is the difference between "read-only" as an intention and as a measurement.

Seven platform gaps measured against 17.2.0 and filed at objectui (#7297–#7301 plus the two above) instead of being worked around; the one you left for a PM call — PageTranslation.components has no content key for element:text — belongs at objectstack and I have filed it as objectstack#14412. Your reasoning was right: the schema comment names its two deliberate exclusions and this is not one of them.

Recording one measurement as a watch item rather than a fix: GET /api/v1/meta/page returns both duly_member and the platform's sys_user_detail for sys_user, and ours wins by list position and a stable sort only. A platform reordering would make this page vanish with no error. Filed as a finding so it is not re-derived.


Generated by Claude Code

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.

Member detail page — open one person's duties without asking them

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

Member detail page — open one person's duties without asking them - #112

Merged
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page
Sep 2, 2026
Merged

Member detail page — open one person's duties without asking them#112
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#13

A manager opens a person and sees everything, having entered nothing. duly_member is a read-only type: 'record' page over sys_user with the card's six sections in reading order, reachable from the Team nav group.

Gates green on e399e78 (the pushed head): validate 0 · typecheck 0 · test 0 (775 passed / 31 files) · build 0. The one validate warning is the expected open-edition state naming @objectstack/security-enterprise — AGENTS.md rule 7 says not to silence it.


The page, in the browser

Driven against pnpm demo on a private port with the 465-row demo seed, signed in as the demo admin, viewport 1400×1000 @2x. Screenshots live on the throwaway branch shots-issue-13 (not for merge).

Header, and "Right now" — name, unit, manager off the user record; position from the sys_user_position junction; then open work, the part past its grace, and the part nobody has touched in a fortnight.

Header and Right now

"Not moving" emphasised, and "This period" — the third number is the one that fires before a deadline exists, so it carries the rule. Governed duties lead with frequency and sort on it.

Not moving and This period

Standing duties (a different person, who has one) — listed, with no status column and nothing to tick.

Standing duties

"Assigned to them", and the bottom of the page — note the Discussion panel has no comment box. That took work; see §7.

Assigned to them

The Team nav group, with the People entry that opens a person.

Team nav

The emphasis on "Not moving" is ADR-0065 scoped styles, not a className — the build-time Tailwind never scans runtime metadata. Confirmed by computed style rather than by eye: objectui emitted .os-s-right_now_stalled { border-left: 3px solid #8C6512 !important; padding-left: 12px !important; } and the node computes 3px rgb(140, 101, 18) / padding-left: 12px.


The three things that must not be on the page

Each fails silently if broken — a log count reads as a helpful number, a "+ New" reads as a feature, a peer comparison reads as a dashboard. None turns anything red on its own, which is what test/member-page.test.ts is for.

The work log. The object's machine name appears nowhere in src/pages/member.page.ts — including in its comments, because the card's acceptance is a grep and a grep cannot read intent. The name, the reasoning and the guard live together in the test, which greps the source and walks the parsed page (objectName resolves to exactly duly_duty, duly_task, sys_user_position).

Editable controls. No record:details (its inlineEdit defaults ON where the object is editable — declaring the component is enough), no record:quick_actions, no element:form / button / text_input / record_picker, no add picker, no actions. Highlight chips carry readonly: true, which HeaderHighlight enforces. One residue remains and it is not ours to close — §3.

Comparison to peers. No aggregate component of any kind, and every list is bound to this record through relationshipField with no filter naming a user — not even {current_user_id}, which is the viewer rather than the person being read.

Both guards were ablated to prove they can fail. Injecting a duly_log_entry related list turned 3 assertions red; dropping readonly from one chip turned 1 red. Each mutation's anchor was asserted present exactly once before the run, the script carried a restore trap, and the tree was confirmed byte-identical afterwards.


What the platform can and cannot express here

The card asked for this to be measured rather than assumed. Seven gaps, each measured against the installed 17.2.0 packages, each filed at objectstack-ai/objectui rather than worked around, each carrying its number in the page header. Two were found only in the browser.

GapFiledWhat the page does instead
§1No record-context token for filter values — CONTEXT_TOKENS is {current_user_id} + {current_org_id}, both the viewer#7297"Right now" is three related lists with their own count badges. An element:number here would have counted the whole org under one person's name
§2record:related_list has no groupBy#7301frequency leads the columns and the sort. Weaker than grouping; not pretended otherwise
§3A page cannot declare its related lists read-only — "+ New" is host-resolved from the child object#7300Nothing. 8 "+ New" buttons render. Closing it object-side would close creation everywhere, which is a different and wrong change
§4Related-list columns cannot cross a lookup#7301The task list carries assignment — one click from the assigner, not zero
§5priority is read by usePageAssignment but not declared on PageSchema#7298Nothing authorable. See below
§6record:related_list cannot bind a multi-value field#7299Nothing. The one line of the card not delivered
§7The auto-appended discussion panel cannot be declined, and carries a comment box#7298Closed — by declaring the component that opens it

Three of these deserve more than a table row.

§6 — "showing who assigned each" is not delivered

This is the one requirement from the card that is missing, and it is missing because it cannot be written.

The assigner lives on duly_assignment.assigner. Two routes, both closed. A column on the task list would have to cross a lookup (§4). A related list on duly_assignment bound to assignees — the field that actually names this person — was authored, run against the seeded demo, and refused:

GET /api/v1/data/duly_assignment?filter=["assignees","=","ql8NO1TcKsuFrrH-"]
400 INVALID_FILTER — The bare equality spelling { "assignees": value } WAS NOT
APPLIED: "assignees" is a multi-value … field, stored by this driver as a JSON
TEXT column … Use "$contains" for membership …

The driver is behaving perfectly — loud, named, with the working spelling. But RelatedList composes its parent filter as bare equality and $contains is not reachable from RecordRelatedListProps, so the list cannot be written correctly at all. It was deleted rather than left rendering a 400.

§7 — a comment box that had to be closed by opening it

Not in the schema; found by reading the rendered page. RecordDetailView appends a chatter panel to every record page whose object does not set enable.feeds: false, hard-coded showCommentInput: true, enableReactions: true, enableThreading: true. sys_user is a locked platform object, so that switch is not ours, and the renderer's own documented escape (assignedPage.disableDiscussion = true) is not a PageSchema key — and PageSchema is a strictObject, so writing it is a parse error.

A comment box on a person's record is worse than merely editable here: it is where a performance note would go, on a page whose whole premise is that the manager enters nothing.

The fix is authorable but backwards — declaring an explicit record:discussion suppresses the append and hands its config to the author, where feed.showCommentInput: falseis honoured (RecordActivityTimeline.tsx:215, with objectui's own test pinning "hides it even when the host CAN persist a comment"). So the page declares one, with all three writes off. Verified: 0 textareas / contenteditables on the rendered page. The test pins this as a positive assertion, because deleting the node does not remove the panel — it hands it back to the host with the writes on.

§5 — which page wins, and why that is luck

@objectstack/platform-objects ships sys_user_detail for the same object with isDefault: true. Measured on this checkout: GET /api/v1/meta/page returns both, duly_member first, and it wins — by list position and a stable sort, since usePageAssignment breaks ties on a priority key nobody can author. isDefault is not consulted at all, which is why this page leaves it false rather than making the flag a lie on whichever page loses.


Notes for review

kind: 'full', not slotted, is a product decision. A slotted page falls through to buildDefaultPageSchema, whose tabs synthesizer emits one related list per object with an FK to sys_user — including the work-log object. The one thing this page must never show is the thing the default layout adds for free.

The nav entry is a people list, not a page link. A type: 'page' nav item routes through PageView, which mounts no RecordContextProvider — every related list would have a null parent and render nothing. It also needs requiresObject: 'sys_user': without it defineStack refuses the whole stack, because the nav cross-reference check resolves objectName against config.objects only. That is also the correct runtime behaviour (hide, don't 404).

Lateness and stagnation are asked the same way the existing views ask themlate_after < {today} and last_update_at < {14_days_ago} — so one person cannot be late on this page and on time on the Late list (#48).

Shared files.zh-CN.ts and authored-text.ts are touched, as flagged for the #107/#108/#109 batch; main was merged at 56b6aae (#110) and all four gates re-run after it. That merge also brought test/node-builtins.d.ts, a narrow declare module 'node:fs' — this branch had solved the same problem with Vite's ?raw, and converged on the merged shim rather than ship two ways to read a file in a test.

element:text copy is localized inline, not through the bundle, and that is a spec gap rather than a shortcut: PageTranslation.components declares title | description | label | placeholder | emptyText | submitLabel and has no key for content — the one string element:text renders. I18nLabelSchema accepts an inline { en, 'zh-CN' } map and that is the route the platform's own sys-user.page.ts uses. The 16 strings are declared in the walk's untranslatable ledger with that reason and pinned by count, so the day the key lands they move into the bundles and the count drops to zero.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p


Generated by Claude Code

os-warrenand others added 4 commits September 2, 2026 04:43
`duly_member`: a read-only `type: 'record'` page over `sys_user` that shows a
manager one person's whole picture with nothing entered — header, what is open
right now, what this period asks of them, what they hold permanently, what has
moved lately, and what was put on them by someone else.
Reached by opening a person: the Team nav group gains a `People` entry, since a
`type: 'page'` nav item routes through objectui's `PageView`, which mounts no
record context. `kind: 'full'` rather than `slotted` is a product decision — the
default-page synthesizer emits a related list per object with an FK to
`sys_user`, which on this stack includes the work-log object this page must
never show.
Five platform gaps were measured against the installed 17.2.0 packages and are
recorded on the page rather than worked around; the page's header comment names
each one and what it does instead. The load-bearing one: no record-context token
exists for filter values (`CONTEXT_TOKENS` is `{current_user_id}` +
`{current_org_id}`, both the VIEWER), so `element:number` cannot be scoped to
the person and "Right now" is three record-bound related lists with their own
count badges instead of three org-wide figures.
`test/member-page.test.ts` pins the four "not on this page" invariants, all of
which fail silently: the work-log object is greped out of the source and walked
out of the parsed page, no writing component type is declared, every highlight
chip is `readonly`, and no aggregate or peer comparison exists anywhere.
Gates: validate 0, typecheck 0, test 0 (768 passed / 30 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…omment box
Two findings from driving the seeded demo as the admin, neither visible to any
gate:
`assigned_by_whom` — a `duly_assignment` related list bound on `assignees` —
was REFUSED by the driver, 400 INVALID_FILTER: `RelatedList` builds its parent
filter as bare equality and `assignees` is `multiple: true`, which this driver
stores as a JSON TEXT column. `$contains` is the working spelling and it is not
reachable from `RecordRelatedListProps`. The list is removed rather than left
rendering an error; "showing who assigned each" is the one line of the card
that is not fully delivered, and it is recorded as gap §6 and filed.
`RecordDetailView` auto-appends a chatter panel — comment box, reactions,
threaded replies, all hard-coded on — to every record page whose object does
not set `enable.feeds: false`. `sys_user` is a locked platform object, and the
renderer's own opt-out (`disableDiscussion`) is not a `PageSchema` key, so it
cannot be written. Placing an EXPLICIT `record:discussion` suppresses the
append and takes its config back: all three writes off, leaving the record's
read-only history. Verified in the browser — the composer is gone
(0 textareas / contenteditables on the page).
Also measured and now recorded: the page wins the `sys_user` assignment against
the platform's `sys_user_detail` on this checkout, but by list position only
(both `priority ?? 0`, stable sort) — gap §5 stands.
Gates: validate 0, typecheck 0, test 0 (769 passed / 30 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…numbers
#110 landed `test/node-builtins.d.ts` — a narrow `declare module 'node:fs'`
exposing `readFileSync`, with the reasoning that `@types/node` stays out of a
metadata package. That is the same problem `test/raw-import.d.ts` solved here
with Vite's `?raw`, and one way to read a file in a test beats two, so this
drops the `?raw` declaration and uses the merged shim. Its `URL` overload is
what makes `node:url` unnecessary.
Also stamps every gap in the page header with the issue it was filed as, so a
gap without a number reads as a workaround wearing a comment:
§1 no record-context filter token objectui#7297
§2 related list cannot group objectui#7301
§3 no page-level read-only for a related list objectui#7300
§4 related-list columns cannot cross a lookup objectui#7301
§5 `priority` read but not declared objectui#7298
§6 related list cannot bind a multi-value field objectui#7299
§7 `disableDiscussion` read but not declared objectui#7298
Gates re-run on the merge: validate 0, typecheck 0, test 0 (775 passed /
31 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 2, 2026 05:09
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging.

Gates, re-run by me on the head merged with current main:validate 0, typecheck 0, test 0 (Test Files 31 passed, Tests 775 passed), build 0.

The three hard rules, checked in the source rather than taken from the report:duly_log_entry — 0 occurrences in the page; the single hit for percentile|rank|average is the header comment stating the prohibition, not an element; all 5 highlight chips readonly: true; the record:discussion block turns off showCommentInput, enableReactions, enableThreading, the filter and the subscription toggles. Reachable from the Team group as People.

The one line not delivered is the right kind of not-delivered. "Showing who assigned each" was authored two ways, run, and refused by the driver (400 INVALID_FILTERRelatedList composes bare equality and assignees is multiple: true), so the list was deleted rather than left rendering an error, and the task list carries the assignment column one click from the answer. A page that shows an error box on a demo is worse than a page that shows one fewer section. Filed as objectui#7299 with the exact refusal.

The discussion panel was the important catch. An auto-appended comment box with reactions and threaded replies, on a page whose whole premise is that the manager enters nothing — and on a person's record, which is precisely where a performance note would go. disableDiscussion is read by the renderer but is not a PageSchema key (objectui#7298), so the explicit record:discussion with every write off is the only authorable way to shut it, and it was verified as 0 composers on the rendered page. That is the difference between "read-only" as an intention and as a measurement.

Seven platform gaps measured against 17.2.0 and filed at objectui (#7297–#7301 plus the two above) instead of being worked around; the one you left for a PM call — PageTranslation.components has no content key for element:text — belongs at objectstack and I have filed it as objectstack#14412. Your reasoning was right: the schema comment names its two deliberate exclusions and this is not one of them.

Recording one measurement as a watch item rather than a fix: GET /api/v1/meta/page returns both duly_member and the platform's sys_user_detail for sys_user, and ours wins by list position and a stable sort only. A platform reordering would make this page vanish with no error. Filed as a finding so it is not re-derived.


Generated by Claude Code

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.

Member detail page — open one person's duties without asking them

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

Member detail page — open one person's duties without asking them - #112

Merged
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page
Sep 2, 2026
Merged

Member detail page — open one person's duties without asking them#112
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#13

A manager opens a person and sees everything, having entered nothing. duly_member is a read-only type: 'record' page over sys_user with the card's six sections in reading order, reachable from the Team nav group.

Gates green on e399e78 (the pushed head): validate 0 · typecheck 0 · test 0 (775 passed / 31 files) · build 0. The one validate warning is the expected open-edition state naming @objectstack/security-enterprise — AGENTS.md rule 7 says not to silence it.


The page, in the browser

Driven against pnpm demo on a private port with the 465-row demo seed, signed in as the demo admin, viewport 1400×1000 @2x. Screenshots live on the throwaway branch shots-issue-13 (not for merge).

Header, and "Right now" — name, unit, manager off the user record; position from the sys_user_position junction; then open work, the part past its grace, and the part nobody has touched in a fortnight.

Header and Right now

"Not moving" emphasised, and "This period" — the third number is the one that fires before a deadline exists, so it carries the rule. Governed duties lead with frequency and sort on it.

Not moving and This period

Standing duties (a different person, who has one) — listed, with no status column and nothing to tick.

Standing duties

"Assigned to them", and the bottom of the page — note the Discussion panel has no comment box. That took work; see §7.

Assigned to them

The Team nav group, with the People entry that opens a person.

Team nav

The emphasis on "Not moving" is ADR-0065 scoped styles, not a className — the build-time Tailwind never scans runtime metadata. Confirmed by computed style rather than by eye: objectui emitted .os-s-right_now_stalled { border-left: 3px solid #8C6512 !important; padding-left: 12px !important; } and the node computes 3px rgb(140, 101, 18) / padding-left: 12px.


The three things that must not be on the page

Each fails silently if broken — a log count reads as a helpful number, a "+ New" reads as a feature, a peer comparison reads as a dashboard. None turns anything red on its own, which is what test/member-page.test.ts is for.

The work log. The object's machine name appears nowhere in src/pages/member.page.ts — including in its comments, because the card's acceptance is a grep and a grep cannot read intent. The name, the reasoning and the guard live together in the test, which greps the source and walks the parsed page (objectName resolves to exactly duly_duty, duly_task, sys_user_position).

Editable controls. No record:details (its inlineEdit defaults ON where the object is editable — declaring the component is enough), no record:quick_actions, no element:form / button / text_input / record_picker, no add picker, no actions. Highlight chips carry readonly: true, which HeaderHighlight enforces. One residue remains and it is not ours to close — §3.

Comparison to peers. No aggregate component of any kind, and every list is bound to this record through relationshipField with no filter naming a user — not even {current_user_id}, which is the viewer rather than the person being read.

Both guards were ablated to prove they can fail. Injecting a duly_log_entry related list turned 3 assertions red; dropping readonly from one chip turned 1 red. Each mutation's anchor was asserted present exactly once before the run, the script carried a restore trap, and the tree was confirmed byte-identical afterwards.


What the platform can and cannot express here

The card asked for this to be measured rather than assumed. Seven gaps, each measured against the installed 17.2.0 packages, each filed at objectstack-ai/objectui rather than worked around, each carrying its number in the page header. Two were found only in the browser.

GapFiledWhat the page does instead
§1No record-context token for filter values — CONTEXT_TOKENS is {current_user_id} + {current_org_id}, both the viewer#7297"Right now" is three related lists with their own count badges. An element:number here would have counted the whole org under one person's name
§2record:related_list has no groupBy#7301frequency leads the columns and the sort. Weaker than grouping; not pretended otherwise
§3A page cannot declare its related lists read-only — "+ New" is host-resolved from the child object#7300Nothing. 8 "+ New" buttons render. Closing it object-side would close creation everywhere, which is a different and wrong change
§4Related-list columns cannot cross a lookup#7301The task list carries assignment — one click from the assigner, not zero
§5priority is read by usePageAssignment but not declared on PageSchema#7298Nothing authorable. See below
§6record:related_list cannot bind a multi-value field#7299Nothing. The one line of the card not delivered
§7The auto-appended discussion panel cannot be declined, and carries a comment box#7298Closed — by declaring the component that opens it

Three of these deserve more than a table row.

§6 — "showing who assigned each" is not delivered

This is the one requirement from the card that is missing, and it is missing because it cannot be written.

The assigner lives on duly_assignment.assigner. Two routes, both closed. A column on the task list would have to cross a lookup (§4). A related list on duly_assignment bound to assignees — the field that actually names this person — was authored, run against the seeded demo, and refused:

GET /api/v1/data/duly_assignment?filter=["assignees","=","ql8NO1TcKsuFrrH-"]
400 INVALID_FILTER — The bare equality spelling { "assignees": value } WAS NOT
APPLIED: "assignees" is a multi-value … field, stored by this driver as a JSON
TEXT column … Use "$contains" for membership …

The driver is behaving perfectly — loud, named, with the working spelling. But RelatedList composes its parent filter as bare equality and $contains is not reachable from RecordRelatedListProps, so the list cannot be written correctly at all. It was deleted rather than left rendering a 400.

§7 — a comment box that had to be closed by opening it

Not in the schema; found by reading the rendered page. RecordDetailView appends a chatter panel to every record page whose object does not set enable.feeds: false, hard-coded showCommentInput: true, enableReactions: true, enableThreading: true. sys_user is a locked platform object, so that switch is not ours, and the renderer's own documented escape (assignedPage.disableDiscussion = true) is not a PageSchema key — and PageSchema is a strictObject, so writing it is a parse error.

A comment box on a person's record is worse than merely editable here: it is where a performance note would go, on a page whose whole premise is that the manager enters nothing.

The fix is authorable but backwards — declaring an explicit record:discussion suppresses the append and hands its config to the author, where feed.showCommentInput: falseis honoured (RecordActivityTimeline.tsx:215, with objectui's own test pinning "hides it even when the host CAN persist a comment"). So the page declares one, with all three writes off. Verified: 0 textareas / contenteditables on the rendered page. The test pins this as a positive assertion, because deleting the node does not remove the panel — it hands it back to the host with the writes on.

§5 — which page wins, and why that is luck

@objectstack/platform-objects ships sys_user_detail for the same object with isDefault: true. Measured on this checkout: GET /api/v1/meta/page returns both, duly_member first, and it wins — by list position and a stable sort, since usePageAssignment breaks ties on a priority key nobody can author. isDefault is not consulted at all, which is why this page leaves it false rather than making the flag a lie on whichever page loses.


Notes for review

kind: 'full', not slotted, is a product decision. A slotted page falls through to buildDefaultPageSchema, whose tabs synthesizer emits one related list per object with an FK to sys_user — including the work-log object. The one thing this page must never show is the thing the default layout adds for free.

The nav entry is a people list, not a page link. A type: 'page' nav item routes through PageView, which mounts no RecordContextProvider — every related list would have a null parent and render nothing. It also needs requiresObject: 'sys_user': without it defineStack refuses the whole stack, because the nav cross-reference check resolves objectName against config.objects only. That is also the correct runtime behaviour (hide, don't 404).

Lateness and stagnation are asked the same way the existing views ask themlate_after < {today} and last_update_at < {14_days_ago} — so one person cannot be late on this page and on time on the Late list (#48).

Shared files.zh-CN.ts and authored-text.ts are touched, as flagged for the #107/#108/#109 batch; main was merged at 56b6aae (#110) and all four gates re-run after it. That merge also brought test/node-builtins.d.ts, a narrow declare module 'node:fs' — this branch had solved the same problem with Vite's ?raw, and converged on the merged shim rather than ship two ways to read a file in a test.

element:text copy is localized inline, not through the bundle, and that is a spec gap rather than a shortcut: PageTranslation.components declares title | description | label | placeholder | emptyText | submitLabel and has no key for content — the one string element:text renders. I18nLabelSchema accepts an inline { en, 'zh-CN' } map and that is the route the platform's own sys-user.page.ts uses. The 16 strings are declared in the walk's untranslatable ledger with that reason and pinned by count, so the day the key lands they move into the bundles and the count drops to zero.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p


Generated by Claude Code

os-warrenand others added 4 commits September 2, 2026 04:43
`duly_member`: a read-only `type: 'record'` page over `sys_user` that shows a
manager one person's whole picture with nothing entered — header, what is open
right now, what this period asks of them, what they hold permanently, what has
moved lately, and what was put on them by someone else.
Reached by opening a person: the Team nav group gains a `People` entry, since a
`type: 'page'` nav item routes through objectui's `PageView`, which mounts no
record context. `kind: 'full'` rather than `slotted` is a product decision — the
default-page synthesizer emits a related list per object with an FK to
`sys_user`, which on this stack includes the work-log object this page must
never show.
Five platform gaps were measured against the installed 17.2.0 packages and are
recorded on the page rather than worked around; the page's header comment names
each one and what it does instead. The load-bearing one: no record-context token
exists for filter values (`CONTEXT_TOKENS` is `{current_user_id}` +
`{current_org_id}`, both the VIEWER), so `element:number` cannot be scoped to
the person and "Right now" is three record-bound related lists with their own
count badges instead of three org-wide figures.
`test/member-page.test.ts` pins the four "not on this page" invariants, all of
which fail silently: the work-log object is greped out of the source and walked
out of the parsed page, no writing component type is declared, every highlight
chip is `readonly`, and no aggregate or peer comparison exists anywhere.
Gates: validate 0, typecheck 0, test 0 (768 passed / 30 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…omment box
Two findings from driving the seeded demo as the admin, neither visible to any
gate:
`assigned_by_whom` — a `duly_assignment` related list bound on `assignees` —
was REFUSED by the driver, 400 INVALID_FILTER: `RelatedList` builds its parent
filter as bare equality and `assignees` is `multiple: true`, which this driver
stores as a JSON TEXT column. `$contains` is the working spelling and it is not
reachable from `RecordRelatedListProps`. The list is removed rather than left
rendering an error; "showing who assigned each" is the one line of the card
that is not fully delivered, and it is recorded as gap §6 and filed.
`RecordDetailView` auto-appends a chatter panel — comment box, reactions,
threaded replies, all hard-coded on — to every record page whose object does
not set `enable.feeds: false`. `sys_user` is a locked platform object, and the
renderer's own opt-out (`disableDiscussion`) is not a `PageSchema` key, so it
cannot be written. Placing an EXPLICIT `record:discussion` suppresses the
append and takes its config back: all three writes off, leaving the record's
read-only history. Verified in the browser — the composer is gone
(0 textareas / contenteditables on the page).
Also measured and now recorded: the page wins the `sys_user` assignment against
the platform's `sys_user_detail` on this checkout, but by list position only
(both `priority ?? 0`, stable sort) — gap §5 stands.
Gates: validate 0, typecheck 0, test 0 (769 passed / 30 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…numbers
#110 landed `test/node-builtins.d.ts` — a narrow `declare module 'node:fs'`
exposing `readFileSync`, with the reasoning that `@types/node` stays out of a
metadata package. That is the same problem `test/raw-import.d.ts` solved here
with Vite's `?raw`, and one way to read a file in a test beats two, so this
drops the `?raw` declaration and uses the merged shim. Its `URL` overload is
what makes `node:url` unnecessary.
Also stamps every gap in the page header with the issue it was filed as, so a
gap without a number reads as a workaround wearing a comment:
§1 no record-context filter token objectui#7297
§2 related list cannot group objectui#7301
§3 no page-level read-only for a related list objectui#7300
§4 related-list columns cannot cross a lookup objectui#7301
§5 `priority` read but not declared objectui#7298
§6 related list cannot bind a multi-value field objectui#7299
§7 `disableDiscussion` read but not declared objectui#7298
Gates re-run on the merge: validate 0, typecheck 0, test 0 (775 passed /
31 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 2, 2026 05:09
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging.

Gates, re-run by me on the head merged with current main:validate 0, typecheck 0, test 0 (Test Files 31 passed, Tests 775 passed), build 0.

The three hard rules, checked in the source rather than taken from the report:duly_log_entry — 0 occurrences in the page; the single hit for percentile|rank|average is the header comment stating the prohibition, not an element; all 5 highlight chips readonly: true; the record:discussion block turns off showCommentInput, enableReactions, enableThreading, the filter and the subscription toggles. Reachable from the Team group as People.

The one line not delivered is the right kind of not-delivered. "Showing who assigned each" was authored two ways, run, and refused by the driver (400 INVALID_FILTERRelatedList composes bare equality and assignees is multiple: true), so the list was deleted rather than left rendering an error, and the task list carries the assignment column one click from the answer. A page that shows an error box on a demo is worse than a page that shows one fewer section. Filed as objectui#7299 with the exact refusal.

The discussion panel was the important catch. An auto-appended comment box with reactions and threaded replies, on a page whose whole premise is that the manager enters nothing — and on a person's record, which is precisely where a performance note would go. disableDiscussion is read by the renderer but is not a PageSchema key (objectui#7298), so the explicit record:discussion with every write off is the only authorable way to shut it, and it was verified as 0 composers on the rendered page. That is the difference between "read-only" as an intention and as a measurement.

Seven platform gaps measured against 17.2.0 and filed at objectui (#7297–#7301 plus the two above) instead of being worked around; the one you left for a PM call — PageTranslation.components has no content key for element:text — belongs at objectstack and I have filed it as objectstack#14412. Your reasoning was right: the schema comment names its two deliberate exclusions and this is not one of them.

Recording one measurement as a watch item rather than a fix: GET /api/v1/meta/page returns both duly_member and the platform's sys_user_detail for sys_user, and ours wins by list position and a stable sort only. A platform reordering would make this page vanish with no error. Filed as a finding so it is not re-derived.


Generated by Claude Code

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.

Member detail page — open one person's duties without asking them

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

Member detail page — open one person's duties without asking them - #112

Merged
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page
Sep 2, 2026
Merged

Member detail page — open one person's duties without asking them#112
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#13

A manager opens a person and sees everything, having entered nothing. duly_member is a read-only type: 'record' page over sys_user with the card's six sections in reading order, reachable from the Team nav group.

Gates green on e399e78 (the pushed head): validate 0 · typecheck 0 · test 0 (775 passed / 31 files) · build 0. The one validate warning is the expected open-edition state naming @objectstack/security-enterprise — AGENTS.md rule 7 says not to silence it.


The page, in the browser

Driven against pnpm demo on a private port with the 465-row demo seed, signed in as the demo admin, viewport 1400×1000 @2x. Screenshots live on the throwaway branch shots-issue-13 (not for merge).

Header, and "Right now" — name, unit, manager off the user record; position from the sys_user_position junction; then open work, the part past its grace, and the part nobody has touched in a fortnight.

Header and Right now

"Not moving" emphasised, and "This period" — the third number is the one that fires before a deadline exists, so it carries the rule. Governed duties lead with frequency and sort on it.

Not moving and This period

Standing duties (a different person, who has one) — listed, with no status column and nothing to tick.

Standing duties

"Assigned to them", and the bottom of the page — note the Discussion panel has no comment box. That took work; see §7.

Assigned to them

The Team nav group, with the People entry that opens a person.

Team nav

The emphasis on "Not moving" is ADR-0065 scoped styles, not a className — the build-time Tailwind never scans runtime metadata. Confirmed by computed style rather than by eye: objectui emitted .os-s-right_now_stalled { border-left: 3px solid #8C6512 !important; padding-left: 12px !important; } and the node computes 3px rgb(140, 101, 18) / padding-left: 12px.


The three things that must not be on the page

Each fails silently if broken — a log count reads as a helpful number, a "+ New" reads as a feature, a peer comparison reads as a dashboard. None turns anything red on its own, which is what test/member-page.test.ts is for.

The work log. The object's machine name appears nowhere in src/pages/member.page.ts — including in its comments, because the card's acceptance is a grep and a grep cannot read intent. The name, the reasoning and the guard live together in the test, which greps the source and walks the parsed page (objectName resolves to exactly duly_duty, duly_task, sys_user_position).

Editable controls. No record:details (its inlineEdit defaults ON where the object is editable — declaring the component is enough), no record:quick_actions, no element:form / button / text_input / record_picker, no add picker, no actions. Highlight chips carry readonly: true, which HeaderHighlight enforces. One residue remains and it is not ours to close — §3.

Comparison to peers. No aggregate component of any kind, and every list is bound to this record through relationshipField with no filter naming a user — not even {current_user_id}, which is the viewer rather than the person being read.

Both guards were ablated to prove they can fail. Injecting a duly_log_entry related list turned 3 assertions red; dropping readonly from one chip turned 1 red. Each mutation's anchor was asserted present exactly once before the run, the script carried a restore trap, and the tree was confirmed byte-identical afterwards.


What the platform can and cannot express here

The card asked for this to be measured rather than assumed. Seven gaps, each measured against the installed 17.2.0 packages, each filed at objectstack-ai/objectui rather than worked around, each carrying its number in the page header. Two were found only in the browser.

GapFiledWhat the page does instead
§1No record-context token for filter values — CONTEXT_TOKENS is {current_user_id} + {current_org_id}, both the viewer#7297"Right now" is three related lists with their own count badges. An element:number here would have counted the whole org under one person's name
§2record:related_list has no groupBy#7301frequency leads the columns and the sort. Weaker than grouping; not pretended otherwise
§3A page cannot declare its related lists read-only — "+ New" is host-resolved from the child object#7300Nothing. 8 "+ New" buttons render. Closing it object-side would close creation everywhere, which is a different and wrong change
§4Related-list columns cannot cross a lookup#7301The task list carries assignment — one click from the assigner, not zero
§5priority is read by usePageAssignment but not declared on PageSchema#7298Nothing authorable. See below
§6record:related_list cannot bind a multi-value field#7299Nothing. The one line of the card not delivered
§7The auto-appended discussion panel cannot be declined, and carries a comment box#7298Closed — by declaring the component that opens it

Three of these deserve more than a table row.

§6 — "showing who assigned each" is not delivered

This is the one requirement from the card that is missing, and it is missing because it cannot be written.

The assigner lives on duly_assignment.assigner. Two routes, both closed. A column on the task list would have to cross a lookup (§4). A related list on duly_assignment bound to assignees — the field that actually names this person — was authored, run against the seeded demo, and refused:

GET /api/v1/data/duly_assignment?filter=["assignees","=","ql8NO1TcKsuFrrH-"]
400 INVALID_FILTER — The bare equality spelling { "assignees": value } WAS NOT
APPLIED: "assignees" is a multi-value … field, stored by this driver as a JSON
TEXT column … Use "$contains" for membership …

The driver is behaving perfectly — loud, named, with the working spelling. But RelatedList composes its parent filter as bare equality and $contains is not reachable from RecordRelatedListProps, so the list cannot be written correctly at all. It was deleted rather than left rendering a 400.

§7 — a comment box that had to be closed by opening it

Not in the schema; found by reading the rendered page. RecordDetailView appends a chatter panel to every record page whose object does not set enable.feeds: false, hard-coded showCommentInput: true, enableReactions: true, enableThreading: true. sys_user is a locked platform object, so that switch is not ours, and the renderer's own documented escape (assignedPage.disableDiscussion = true) is not a PageSchema key — and PageSchema is a strictObject, so writing it is a parse error.

A comment box on a person's record is worse than merely editable here: it is where a performance note would go, on a page whose whole premise is that the manager enters nothing.

The fix is authorable but backwards — declaring an explicit record:discussion suppresses the append and hands its config to the author, where feed.showCommentInput: falseis honoured (RecordActivityTimeline.tsx:215, with objectui's own test pinning "hides it even when the host CAN persist a comment"). So the page declares one, with all three writes off. Verified: 0 textareas / contenteditables on the rendered page. The test pins this as a positive assertion, because deleting the node does not remove the panel — it hands it back to the host with the writes on.

§5 — which page wins, and why that is luck

@objectstack/platform-objects ships sys_user_detail for the same object with isDefault: true. Measured on this checkout: GET /api/v1/meta/page returns both, duly_member first, and it wins — by list position and a stable sort, since usePageAssignment breaks ties on a priority key nobody can author. isDefault is not consulted at all, which is why this page leaves it false rather than making the flag a lie on whichever page loses.


Notes for review

kind: 'full', not slotted, is a product decision. A slotted page falls through to buildDefaultPageSchema, whose tabs synthesizer emits one related list per object with an FK to sys_user — including the work-log object. The one thing this page must never show is the thing the default layout adds for free.

The nav entry is a people list, not a page link. A type: 'page' nav item routes through PageView, which mounts no RecordContextProvider — every related list would have a null parent and render nothing. It also needs requiresObject: 'sys_user': without it defineStack refuses the whole stack, because the nav cross-reference check resolves objectName against config.objects only. That is also the correct runtime behaviour (hide, don't 404).

Lateness and stagnation are asked the same way the existing views ask themlate_after < {today} and last_update_at < {14_days_ago} — so one person cannot be late on this page and on time on the Late list (#48).

Shared files.zh-CN.ts and authored-text.ts are touched, as flagged for the #107/#108/#109 batch; main was merged at 56b6aae (#110) and all four gates re-run after it. That merge also brought test/node-builtins.d.ts, a narrow declare module 'node:fs' — this branch had solved the same problem with Vite's ?raw, and converged on the merged shim rather than ship two ways to read a file in a test.

element:text copy is localized inline, not through the bundle, and that is a spec gap rather than a shortcut: PageTranslation.components declares title | description | label | placeholder | emptyText | submitLabel and has no key for content — the one string element:text renders. I18nLabelSchema accepts an inline { en, 'zh-CN' } map and that is the route the platform's own sys-user.page.ts uses. The 16 strings are declared in the walk's untranslatable ledger with that reason and pinned by count, so the day the key lands they move into the bundles and the count drops to zero.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p


Generated by Claude Code

os-warrenand others added 4 commits September 2, 2026 04:43
`duly_member`: a read-only `type: 'record'` page over `sys_user` that shows a
manager one person's whole picture with nothing entered — header, what is open
right now, what this period asks of them, what they hold permanently, what has
moved lately, and what was put on them by someone else.
Reached by opening a person: the Team nav group gains a `People` entry, since a
`type: 'page'` nav item routes through objectui's `PageView`, which mounts no
record context. `kind: 'full'` rather than `slotted` is a product decision — the
default-page synthesizer emits a related list per object with an FK to
`sys_user`, which on this stack includes the work-log object this page must
never show.
Five platform gaps were measured against the installed 17.2.0 packages and are
recorded on the page rather than worked around; the page's header comment names
each one and what it does instead. The load-bearing one: no record-context token
exists for filter values (`CONTEXT_TOKENS` is `{current_user_id}` +
`{current_org_id}`, both the VIEWER), so `element:number` cannot be scoped to
the person and "Right now" is three record-bound related lists with their own
count badges instead of three org-wide figures.
`test/member-page.test.ts` pins the four "not on this page" invariants, all of
which fail silently: the work-log object is greped out of the source and walked
out of the parsed page, no writing component type is declared, every highlight
chip is `readonly`, and no aggregate or peer comparison exists anywhere.
Gates: validate 0, typecheck 0, test 0 (768 passed / 30 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…omment box
Two findings from driving the seeded demo as the admin, neither visible to any
gate:
`assigned_by_whom` — a `duly_assignment` related list bound on `assignees` —
was REFUSED by the driver, 400 INVALID_FILTER: `RelatedList` builds its parent
filter as bare equality and `assignees` is `multiple: true`, which this driver
stores as a JSON TEXT column. `$contains` is the working spelling and it is not
reachable from `RecordRelatedListProps`. The list is removed rather than left
rendering an error; "showing who assigned each" is the one line of the card
that is not fully delivered, and it is recorded as gap §6 and filed.
`RecordDetailView` auto-appends a chatter panel — comment box, reactions,
threaded replies, all hard-coded on — to every record page whose object does
not set `enable.feeds: false`. `sys_user` is a locked platform object, and the
renderer's own opt-out (`disableDiscussion`) is not a `PageSchema` key, so it
cannot be written. Placing an EXPLICIT `record:discussion` suppresses the
append and takes its config back: all three writes off, leaving the record's
read-only history. Verified in the browser — the composer is gone
(0 textareas / contenteditables on the page).
Also measured and now recorded: the page wins the `sys_user` assignment against
the platform's `sys_user_detail` on this checkout, but by list position only
(both `priority ?? 0`, stable sort) — gap §5 stands.
Gates: validate 0, typecheck 0, test 0 (769 passed / 30 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…numbers
#110 landed `test/node-builtins.d.ts` — a narrow `declare module 'node:fs'`
exposing `readFileSync`, with the reasoning that `@types/node` stays out of a
metadata package. That is the same problem `test/raw-import.d.ts` solved here
with Vite's `?raw`, and one way to read a file in a test beats two, so this
drops the `?raw` declaration and uses the merged shim. Its `URL` overload is
what makes `node:url` unnecessary.
Also stamps every gap in the page header with the issue it was filed as, so a
gap without a number reads as a workaround wearing a comment:
§1 no record-context filter token objectui#7297
§2 related list cannot group objectui#7301
§3 no page-level read-only for a related list objectui#7300
§4 related-list columns cannot cross a lookup objectui#7301
§5 `priority` read but not declared objectui#7298
§6 related list cannot bind a multi-value field objectui#7299
§7 `disableDiscussion` read but not declared objectui#7298
Gates re-run on the merge: validate 0, typecheck 0, test 0 (775 passed /
31 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 2, 2026 05:09
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging.

Gates, re-run by me on the head merged with current main:validate 0, typecheck 0, test 0 (Test Files 31 passed, Tests 775 passed), build 0.

The three hard rules, checked in the source rather than taken from the report:duly_log_entry — 0 occurrences in the page; the single hit for percentile|rank|average is the header comment stating the prohibition, not an element; all 5 highlight chips readonly: true; the record:discussion block turns off showCommentInput, enableReactions, enableThreading, the filter and the subscription toggles. Reachable from the Team group as People.

The one line not delivered is the right kind of not-delivered. "Showing who assigned each" was authored two ways, run, and refused by the driver (400 INVALID_FILTERRelatedList composes bare equality and assignees is multiple: true), so the list was deleted rather than left rendering an error, and the task list carries the assignment column one click from the answer. A page that shows an error box on a demo is worse than a page that shows one fewer section. Filed as objectui#7299 with the exact refusal.

The discussion panel was the important catch. An auto-appended comment box with reactions and threaded replies, on a page whose whole premise is that the manager enters nothing — and on a person's record, which is precisely where a performance note would go. disableDiscussion is read by the renderer but is not a PageSchema key (objectui#7298), so the explicit record:discussion with every write off is the only authorable way to shut it, and it was verified as 0 composers on the rendered page. That is the difference between "read-only" as an intention and as a measurement.

Seven platform gaps measured against 17.2.0 and filed at objectui (#7297–#7301 plus the two above) instead of being worked around; the one you left for a PM call — PageTranslation.components has no content key for element:text — belongs at objectstack and I have filed it as objectstack#14412. Your reasoning was right: the schema comment names its two deliberate exclusions and this is not one of them.

Recording one measurement as a watch item rather than a fix: GET /api/v1/meta/page returns both duly_member and the platform's sys_user_detail for sys_user, and ours wins by list position and a stable sort only. A platform reordering would make this page vanish with no error. Filed as a finding so it is not re-derived.


Generated by Claude Code

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.

Member detail page — open one person's duties without asking them

1 participant

@os-warren