Skip to content

docs(readme): state the real milestone, and that positions bind by name - #56

Merged
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-46-readme-status-and-binding
Sep 10, 2026
Merged

zhuangjianguo merged 2 commits into
mainfrom
claude/issue-46-readme-status-and-binding

Conversation

@zhuangjianguo

@zhuangjianguo zhuangjianguo commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #46

Two README.md corrections, both about prose that was true once and stopped being true. No code, no metadata, no behaviour change — README.md is the only file in the diff, over two commits (6a86e41 then the rework in 0a9c340).

Branched off d34b225 (post-#55), so the operator table this card was written against is already the newer one; it is untouched here.

Edit 1 — the status line

Status: **M0 — scaffold and configuration domain.** became one line naming the current state plus pointers to the two authorities (DESIGN.md §11, docs/backlog). Deliberately not a milestone checklist — that is the shape that drifted in the first place.

I verified the completion claim against this tree rather than taking the dispatch's list on trust:

Claim in the new line Reading
the model ls src/objects/*.object.ts = 11
the permission layer src/profiles/ = 5 permission sets (clm_admin · clm_finance · clm_legal · clm_records · clm_requester), src/sharing/positions.ts = 7 positions, contract.sharing.ts + FLS
intake, approval ladder, signing and execution formalities src/flows/ F1 · F5 · F7 present; src/flows/index.ts registers 12 flows in allFlows, wired at objectstack.config.ts:191
post-signature reminder layer legal-review-sla · turn-stalled · obligation-due · payment-overdue · renewal-notice · expiration-sweep (+ _daily-sweep.ts)
analytics src/datasets/ = 4, src/dashboards/ = 3
both locale bundles src/translations/en + zh-CN; the i18n gate reports 0 missing keys across 2 locale(s)
M4 is what remains src/flows/index.ts header: "F8 (e-signature) and F15 (CRM hand-off) arrive with cards 12 and 13"; src/skills/ does not exist (S1–S6, card 13); content/ does not exist (docs site, card 14); src/mappings/ does not exist
cards 12 · 13 · 14 docs/backlog/README.md milestone column — 12/13/14 are the only M4 rows

Edit 2 — the position assignment binds by name

New ### Assigning a position from a script subsection, placed after the dev-admin paragraph so the operator narrative and its tables are untouched. Every sentence in it has a reading behind it, on this tree:

Claim Reading
sys_user_position.position is a plain text column carrying a sys_position.name @objectstack/plugin-security@17.4.0 dist/index.mjs: position: Field.text({ maxLength: 100, description: "Position machine name (references sys_position.name)." }), and the object's own description — "Assigns a position (sys_position.name) to a user"
its neighbour user_id is a real lookup same object: user_id: Field.lookup("sys_user", …)
the field name tells you nothing here — the object mixes both kinds, and only three of its fourteen fields carry an _id suffix all fourteen fields of SysUserPosition enumerated out of the same dist: id text · user_id lookup · position text · business_unit_id lookup · organization_id lookup · granted_by lookup · valid_from datetime · valid_until datetime · reason text · delegated_from lookup · last_certified_at datetime · certified_by lookup · created_at datetime · updated_at datetime
sys_user_permission_set.permission_set_id is genuinely id-typed same dist: permission_set_id: Field.lookup("sys_permission_set", …)
POST /api/v1/data/sys_user_position is the wire shape @objectstack/client@17.4.0 builds a create as POST to the data route joined with the object name (create: async (object, data) → fetch(baseUrl + getRoute("data") + "/" + object, { method: "POST", body: JSON.stringify(data) })), and the route defaults carry data: "/api/v1/data". The object declares managedBy: "system-data" — "the bucket default is full CRUD"
a name-spelled row grants the position's set and clm_requester src/security/bind-position-sets.ts — BINDINGS pairs each position with its own set and then spreads Object.values(CLM_POSITION).map(p => [p, RequesterSet.name]); LegalSet.name = clm_legal, RequesterSet.name = clm_requester
explain requires object and operation; userId alone answers 400 VALIDATION_FAILED @objectstack/spec@17.4.0 ExplainRequestSchema: object: z.ZodString, operation: z.ZodEnum, both non-optional, userId: z.ZodOptional
an id in position is stored verbatim, matches nothing, grants no set static half mine (a text column with no reference resolution cannot refuse an id; the bindings look positions up by name); the runtime half — 201, no diagnostic, no clm_legal — is #50's measurement, handed down with this dispatch. Flagged in the report as the one claim not measured first-hand here.
the platform's silent acceptance is still open described as open and tracked, not as fixed — objectstack-ai/objectstack#16712

Gates

Exit codes captured before any pipe (each gate redirected to a file, $? read immediately after):

EXITS validate=0 lint=0 typecheck=0 i18n=0
pnpm validate   → 0
pnpm lint       → 0   21 warning(s), 5 suggestion(s) (979ms)
pnpm typecheck  → 0   (tsc --noEmit, silent)
pnpm lint:i18n-gate → 0   ✓ i18n gate · 0 missing keys across 2 locale(s) · 1541 keys expected

Re-run on 0a9c340 after the rework, same method. Byte-identical verdicts to the run on 6a86e41 — same 21 warnings, same 5 suggestions, same 1541 keys and 0 missing — which is the measurement behind "a prose diff cannot move them".

None of them moved, and none of them could: tsconfig.json includes only objectstack.config.ts and src/**/*, validate/lint walk the stack metadata graph, and scripts/check-lint-i18n-gate.mjs never reads README.md. The lint warnings above are the tree's pre-existing carrier-only-field and empty-approver-slate notes, untouched by a prose diff.

Browser

No browser run, and none is owed. The diff changes no runnable surface — no object, view, page, app, flow or action — so there is nothing to drive. AGENTS.md's browser rule is scoped to cards that change a surface a human touches; this one changes only prose about surfaces that already exist.

Rework — the _id-suffix tell was false, and is gone

The first push claimed position was "the one field on that row without an _id suffix". Enumerating every field of SysUserPosition in @objectstack/plugin-security@17.4.0 shows that is false twice over: fourteen fields, only three of which carry the suffix, and granted_by, delegated_from and certified_by are each Field.lookup("sys_user") with no suffix at all. The sentence taught a tell that its own object falsifies three times — an operator generalising it onto granted_by would spell a name into a genuine lookup, which is the mirror image of the bug this subsection exists to prevent. It was the card's own failure mode, reintroduced in a new sentence, and it was mine: I had verified position, user_id and permission_set_id individually and carried the "one field" framing over without enumerating the row.

0a9c340 rebuilds that one paragraph on the evidence that does hold — the field's own declaration (text, description "Position machine name (references sys_position.name)") — and says outright that the field name tells you nothing here because the object mixes both kinds. The sys_user_permission_set.permission_set_id contrast stays as it was: that one really is id-typed, which is exactly what makes the wrong guess tempting. Nothing else in the diff moved — no restructuring, no new sections, Edit 1 and #55's tables untouched.

Acceptance notes

Two observations found while verifying, neither filed and neither touched here:

  • The dispatch and the card both say "11 flows"; allFlows in src/flows/index.ts registers 12 (SignatureRecordOnCreateFlow and RenewalStartFlow are the pair that makes the count read as ten files). Nothing in the README states a flow count, so the new status line names the reminder layer without a number.
  • The setup section says "assign the seven positions" over a table of seven rows, but one of those rows is (business requesters) — a default permission set, not a position — while the seventh real position, clm_admin (src/sharing/positions.ts), is absent from the table. DESIGN.md §11 disagrees with itself on the same count (the layout block says "7 position", the M1 row says "8 position / 6 set"). Left alone: it is The demo makes M3's reminder layer look broken: 54 of 120 contracts have no legal_owner, so four of six scheduled jobs notify nobody #55's table, Edit 2 does not require touching it, and DESIGN.md is off this card's file surface. The PM has confirmed it and is filing it as its own card.

Generated by Claude Code

The status line still read `M0 — scaffold and configuration domain` on a tree
that carries M1-M3: the eleven objects, the permission layer, intake, the
approval ladder, signing and execution formalities, the post-signature reminder
layer, four datasets, three dashboards and both locale bundles. A first-time
reader was told this is a scaffold and then found 820 seeded rows. Replaced with
one line naming the current state plus a pointer to DESIGN.md 11 and the three
remaining cards, rather than a checklist that drifts the same way.

The setup section named the seven positions but never said how the assignment
binds. `sys_user_position.position` is a plain text column holding a
`sys_position.name` - the one field on the row without an `_id` suffix, while
`user_id` is a real lookup and `sys_user_permission_set.permission_set_id` is
genuinely id-typed. Anyone scripting the setup reaches for an id by analogy,
gets `201` with no diagnostic, and ends up with an account holding no permission
set and empty navigation. Documented the wire shape, the set a name-spelled row
actually grants, the `explain` call that reads it back, and the silent id
acceptance (upstream objectstack-ai/objectstack#16712, open).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR
The subsection claimed `position` was "the one field on that row without an
`_id` suffix". Enumerating `SysUserPosition` in
@objectstack/plugin-security@17.4.0 shows fourteen fields, of which only three
carry the suffix - and `granted_by`, `delegated_from` and `certified_by` are all
`Field.lookup("sys_user")` with no suffix at all. The sentence taught a tell that
is falsified three times over on its own object: an operator generalising it onto
`granted_by` would spell a name into a genuine lookup, the mirror image of the
bug the subsection exists to prevent.

Rebuilt the paragraph on the evidence that does hold - the field's own
declaration, `text` with the description "Position machine name (references
sys_position.name)" - and says outright that the field name tells you nothing
here because the object mixes both kinds. The
`sys_user_permission_set.permission_set_id` contrast stays: that one really is
id-typed, which is what makes the wrong guess tempting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants