Skip to content

test(fixtures): derive PLATFORM_OBJECTS from the installed platform rosters - #1574

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-1481-derive-platform-objects
Sep 3, 2026
Merged

test(fixtures): derive PLATFORM_OBJECTS from the installed platform rosters#1574
os-musk merged 2 commits into
mainfrom
claude/issue-1481-derive-platform-objects

Conversation

@os-musk

@os-musk os-musk commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1481

test/helpers/metadata-fixtures.ts exported sixteen platform object names typed out by hand, citing "verified against the 16.1.0 bundles in node_modules" against a 17.2.0 pin. This derives the set from the installed packages' own rosters instead. Same export name, still a Set of strings, so no reader needed a line changed — the two files fenced to #1434 (test/metadata-references.test.ts, test/readonly-write-semantics.test.ts) and the unfenced test/action-references.test.ts are all untouched.

All measurements on origin/main @ 18444c8, verified at 5180990, ObjectStack 17.2.0 installed.

The unconfirmed hypothesis, measured first: nothing registers sys_approval

Confirmed. @objectstack/plugin-approvals 17.2.0 exports SysApprovalRequest, SysApprovalAction, SysApprovalApprover and SysApprovalDelegation — and no SysApproval. The only occurrence of the bare token in any installed bundle is a prefix guard:

node_modules/@objectstack/plugin-approvals/dist/index.mjs:6301
    if (!object || String(object).startsWith("sys_approval")) return;

A control confirms the search was live rather than vacuously empty: the same name: '...' pattern hits 6 files for sys_approval_request and 75 for sys_user, and 0 for sys_approval.

So the allowlist admitted a name that matches nothing at runtime — precisely the class the set exists to reject. That is the card's strongest sentence and it is now proven rather than reported. src/apps/crm.app.ts carried the same stale claim in a comment; it now names the four objects that exist. Comment only, no code.

The predicate, its basis, and its control

A roster entry is an ObjectSchema.create(...) descriptor, and what makes it an object rather than some other metadata is that it carries a fields map. So the filter is "value carrying both name and fields".

Basis measured: 62 exported values across the six packages carry both, and all 62 names are sys_* — no non-platform name is admitted.

The control is the near miss. 7 exported values carry a name but no fields, and they are exactly the things that are not objects — so the predicate excludes precisely them:

excluded value its name what it is
ACCOUNT_APP, SETUP_APP, STUDIO_APP account, setup, studio apps
SysOrganizationDetailPage, SysPositionDetailPage, SysUserDetailPage sys_*_detail pages
SystemOverviewDashboard system_overview dashboard

Nothing exports fields without a name (0 cases). The derivation is therefore anchored to registration, not to "everything the package exports".

Which packages, and why these

Each roster is loaded by something this stack declares, so a name it registers really does resolve at runtime. The capability-to-package map lives in the CLI's CAPABILITY_PROVIDERS and is not exported (checked: no such export, and the identifier is absent from the CLI dist), so the packages — not the object names — are the part still written out.

roster source loaded because names of which new
@objectstack/platform-objects core roster, always loaded 44 37
@objectstack/plugin-approvals requires: 'approvals' 4 0
@objectstack/plugin-sharing requires: 'sharing' 5 5
@objectstack/service-automation requires: 'automation' 2 2
@objectstack/service-messaging always-on slate (email) 7 6
@objectstack/service-storage always-on slate (storage) 2 1
hand-listed residue (plugin-audit) provisioned at init 3 1

The set difference, counted

16 names become 65: 50 enter, exactly 1 leaves.

Leaves (1): sys_approval — registered by nothing, as measured above.

Enters (50): sys_account sys_api_key sys_audit_log sys_automation_run sys_business_unit sys_business_unit_member sys_device_code sys_flow_dispatch sys_http_delivery sys_import_job sys_invitation sys_job sys_job_queue sys_job_run sys_jwks sys_member sys_metadata sys_metadata_audit sys_metadata_history sys_migration sys_migration_journal sys_notification_delivery sys_notification_preference sys_notification_receipt sys_notification_subscription sys_notification_template sys_oauth_access_token sys_oauth_application sys_oauth_client_assertion sys_oauth_client_resource sys_oauth_consent sys_oauth_refresh_token sys_oauth_resource sys_record_share sys_report_schedule sys_saved_report sys_scim_provider sys_secret sys_session sys_setting sys_setting_audit sys_share_link sys_sharing_rule sys_sso_provider sys_team_member sys_two_factor sys_upload_session sys_user_preference sys_verification sys_view_definition

All 15 surviving hand-listed names are present in the derived set (checked by reading the real module, not the probe: size 65, every one of the 15 present, sys_approval absent).

Nothing currently red turns green spuriously

  • There was no red to convert. Both guard suites were green at the base commit — 2 files, 46 tests passed — and are green after, with the identical 46. No sys_* reference in today's app metadata changes verdict in either direction.
  • Every arrival is backed by a real registration, read from the installed package rather than assumed. The three non-derived entries were verified by reading plugin-audit's own descriptors.
  • The one name capable of hiding a dangling reference is the one removed. The widening cannot admit a dangling reference, because admission now requires a roster entry to exist.
  • Of the 21 distinct sys_* tokens src/ mentions, 7 are newly admitted (sys_audit_log, sys_business_unit_member, sys_http_delivery, sys_member, sys_record_share, sys_saved_report, sys_sharing_rule), each backed by a roster entry above. sys_approval_process, sys_queue, sys_user_position, sys_webhook, sys_fetch_previous_update and sys_approval remain outside the set — nothing gained admission without registering.

Why widening is the correct direction. The old docblock called the set a deliberate subset, but its stated contract is "a reference to a sys_* name outside this set matches nothing at runtime". A name the platform really registers does resolve, so withholding it makes the guard wrong in the false-red direction — the first failure mode the card names, and the one that costs an unrelated PR a red run. Accuracy here happens to be permissive. No guard was weakened and the derived set was not hand-curated back down.

The residue: expected, and not a failure

@objectstack/plugin-audit registers sys_audit_log, sys_activity and sys_comment by calling syncObjectSchema() on each from provisionSystemTables() at plugin init. Its descriptors are module-private — 'SysActivity' in require('@objectstack/plugin-audit') is false, its exports are 10 functions and 1 string — and its exports map offers only ".", so no deep subpath reaches them. They are not statically derivable from any public surface and stay hand-listed with that reason beside them.

This matters rather than being cosmetic: sys_activity is live app metadata (ctx.api.object('sys_activity').insert(...) in two action bodies), so a derivation that silently dropped it would have been a false red. sys_audit_log is included on the same basis — it registers, so a reference to it resolves.

Reverse verification

The derivation is load-bearing, not decorative. Making the registration predicate match nothing (return false) turns the guards red in the predicted direction:

Test Files  1 failed | 1 passed (2)
     Tests  2 failed | 44 passed (46)

view "all_accounts" bulk "transfer_owner": param object "sys_user" is not registered
sales_activity_dashboard filter "owner_id": optionsFrom object "sys_user" is not registered
crm_overview_dashboard   filter "owner_id": optionsFrom object "sys_user" is not registered

The mutation was confirmed on disk before the run (injected marker present 1, original predicate line gone 0) rather than trusted to the editor's exit code, and the mutated file is first-party TypeScript that vitest transpiles from source, so there is no dist leg to rebuild. Restore was git checkout HEAD -- on an absolute path, proved three ways: git diff HEAD empty, worktree blob 755b910b equal to the HEAD blob (both non-empty), and 0 ablation markers left.

Gates

Full pnpm verify chain, green at 5180990:

gate result
validate pass (pre-existing colSpan advisories only)
typecheck pass
lint pass (49 pre-existing warnings)
lint:i18n-gate pass, 0 i18n/missing-*
hygiene pass
hygiene:tokens pass, authored total ~135,444 of ~140,000 ceiling
build pass, artifact 1981.2 KB
test 160 files, 3391 passed, 1 skipped, 0 failed

typecheck was confirmed to actually cover the change rather than passing vacuously: tsc --listFiles includes both edited files and resolves all six roster .d.ts files.

Scope

Three files, exactly the declared surface: test/helpers/metadata-fixtures.ts, the src/apps/crm.app.ts comment, and one changeset (patch).

No new gate was added. Deriving an existing list is not a gate; a check that the list matches the registry would be, and that belongs to the platform.


Generated by Claude Code

The set was sixteen platform object names typed out by hand, citing "the
16.1.0 bundles in node_modules" against a 17.2.0 pin. Both dangling-reference
guards use it as half of their resolvability predicate, so a machine roster
kept by hand fails them in both directions once it drifts.

It had drifted: nothing registers `sys_approval`. plugin-approvals exports
SysApprovalRequest / Action / Approver / Delegation and no SysApproval, and the
only occurrence of the bare token in any installed bundle is a
`startsWith('sys_approval')` prefix guard. So the allowlist admitted a name
matching nothing at runtime — the class it exists to reject.

Now read from six packages, each loaded by something this stack declares,
filtered to values carrying both `name` and `fields`. 16 names become 65: 50
enter, one leaves. Same export name, still a Set<string>, so no reader
changes. sys_audit_log / sys_activity / sys_comment stay hand-listed with the
reason — plugin-audit provisions them at init and exports no descriptor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDbLQQiy7ew8sdwiCQEZwC
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Sep 3, 2026 4:53pm UTC

Request Review

@github-actions github-actions Bot added ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces labels Sep 3, 2026
The derivation commit left two consecutive `/**` openers above the
PLATFORM_OBJECTS docblock — an off-by-one in the splice that produced it, which
kept the original opener and added a new one.

It is legal JavaScript: the stray opener sits inside the block comment it
accidentally opens, so it is just text, which is why every gate passed and was
right to. But it mis-renders in any editor or doc tool that folds on `/**`, and
this file's whole job in this PR is to be more trustworthy than the
hand-maintained list it replaces.

Comment text only — the delimiter is now balanced at 7 openers and 7 closers.
No code, no wording, no changeset change.

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

Labels

ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] The hand-maintained PLATFORM_OBJECTS allowlist is derivable from the installed packages, and has already drifted

2 participants