Skip to content

feat(platform): migrate onto the @objectstack/* 17.4.0 line - #1814

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-1807-objectstack-17-4-0-migration
Sep 9, 2026
Merged

os-steve merged 1 commit into
mainfrom
claude/issue-1807-objectstack-17-4-0-migration

Conversation

@claude

@claude claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #1807

Description

Migrate this repo onto the @objectstack/* 17.4.0 line: the whole-line pin bump, the dashboard.refreshInterval rename, and the contract changes the bump surfaces. pnpm verify is green end to end and pnpm lint --json reports errors: 0.

⭐ Three load-bearing claims in the card turned out to be wrong. They are reported here rather than quietly matched — the readonly framing in particular, which the card had already corrected once in the wrong direction.

Type of Change

  • Breaking change (platform line bump with authored-metadata migrations)
  • Documentation update

Related Issues

Card #1807. The --strict flip on #1581 is unblocked by this, and is not part of this PR. #1216's component-props-unknown-key warning is left standing — it is not this card's. Draft PR #1806 is untouched.

Changes Made

1. The pin bump — 21 packages, exact

20 in dependencies plus @objectstack/formula in devDependencies, all 17.3.0 → 17.4.0, no carets.

Lockfile movement, measured with a version-normalized diff (sed 's/17.3.0/17.X.0/' against sed 's/17.4.0/17.X.0/', so only non-version changes survive):

reading value
packages: entries 725 → 725
integrity keys replaced 1:1 55 (54 @objectstack/* + create-objectstack)
third-party packages added / removed / re-resolved 0
lines added 1 — croner: 10.0.1 under @objectstack/trigger-schedule@17.4.0, a version already in the tree

better-auth resolves at exact 1.7.2. Confirmed at the source as well as in the lock: @objectstack/plugin-auth declares "better-auth": "^1.7.2" at 17.3.0 and "better-auth": "1.7.2" at 17.4.0 (read off the registry metadata for both versions).

2. The rename — what os migrate meta actually emitted

⚠️ os migrate meta --from 17 — the command the tombstone names and the card prescribes — emits nothing for this surface. Verbatim:

  ℹ Chain:  protocol 17 → 17 (this runtime implements protocol 17)
  ✓ Nothing to migrate — the metadata is already canonical for this range.

The conversion is registered with toMajor: 18, and --to defaults to this runtime's major (17), so the chain 17→17 is empty. The command that lists it is os migrate meta --from 17 --to 18:

  Applied 5 mechanical change(s):
    • dashboards[0].refreshIntervalSeconds: refreshInterval → refreshIntervalSeconds (dashboard-refresh-interval-to-refresh-interval-seconds)
    • dashboards[1].refreshIntervalSeconds: … (same conversion)
    • dashboards[2].refreshIntervalSeconds: … (same conversion)
    • dashboards[3].refreshIntervalSeconds: … (same conversion)
    • dashboards[4].refreshIntervalSeconds: … (same conversion)

⚠️ And the platform does not rewrite the sources. Its own tombstone message says so in as many words: "Run os migrate meta --from 17 to list the mechanical edits for existing sources; apply them by hand." --out writes a migrated stack snapshot (JSON), not TypeScript. So the edits here are the codemod's prescription applied, not a hand-derived rename: the machine-readable applied[] (--json) named the five paths and the conversion id, and the emitted snapshot was used as the oracle for the result —

index dashboard refreshIntervalSeconds in the platform's snapshot file
0 sales_activity_dashboard 300 activity.dashboard.ts
1 crm_overview_dashboard 300 crm.dashboard.ts
2 executive_dashboard 300 executive.dashboard.ts
3 sales_dashboard 180 sales.dashboard.ts
4 service_dashboard 60 service.dashboard.ts

refreshInterval = 0 occurrences under src/, test/, content/, docs/ after the change.

3. Declared versions

objectstack.manifest.json specVersion + engines.protocol, objectstack.config.ts manifest.engines.protocol, docs/STATUS.md, and all three content/docs/whats-new.* locales. content/docs/releases/ untouched.

4. ⭐ The readonly leg — the card's correction was itself wrong, in the other direction

The card withdrew its earlier framing ("17.4.0 strips readonly on INSERT too") and replaced it with "the change is preserveAudit becoming UPDATE-only, ⛔ NOT a blanket new INSERT strip". The withdrawn framing was the correct one. @objectstack/objectql@17.4.0's own changelog entry b398ad2:

BREAKING (behaviour): a static readonly field is now stripped from a non-system caller's INSERT payload inside engine.insert, exactly as it already was on engine.update. … Until now the create-side strip lived only at the DataProtocol ingress (stripReadonlyForInsert in @objectstack/metadata-protocol), so readonly meant one thing on insert and another on update: every external REST/GraphQL/MCP create was stripped, while a caller reaching engine.insert directly — the automation engine's create_record among them — wrote the column with no refusal, no WARN and no dropped-field event.

and, in the same entry:

Unchanged, deliberately: isSystem is still the exemption; preserveAudit is still an UPDATE-path exemption and a create that asks for it is told so out loud.

So preserveAudit's semantics did not change — what is new is the diagnostic that says so aloud, which is the string the card's re-derivation read. The shipped author-time rule agrees; pnpm validate at 17.4.0 printed, before the fix:

writes field 'added_date', which object 'crm_campaign_member' declares readonly:true. Under runAs:'user' the engine silently strips readonly fields from the INSERT payload too (the same strip the UPDATE path runs) … rule: flow-update-readonly-field

The remedy the card names is unaffected by the delta, and is what shipped. Two further deltas in how it had to be applied:

⚠️ runAs is not a node-level key. CreateRecordConfigSchema is a strictObject declaring exactly objectName, fields, outputVariable; runAs lives on the flow. The card's "the two added_date create_record nodes take runAs: 'system'" is not authorable as written, and the platform diagnostic says "declare the flow runAs:'system'".

⛔ But this flow may not be elevated. campaign_enrollment is a screen flow, and AGENTS.md house rule 9 (maintainer ruling, precedent #1434) is explicit:

9. Elevate as little as possible. A screen flow stays runAs: 'user'. A write that genuinely needs elevation is split into a dedicated system sub-flow and called through a subflow node. ⛔ Do not elevate a whole flow to make readonly take effect.

That rule is not a style preference here. Elevating the parent would lift row-level security off query_leads and query_contacts — and crm_lead is sharingModel: 'private', so a rep clicking Enroll Members would have enrolled the whole organisation's lead pool instead of the leads they can see. A product change, not a migration.

So the shape is the one case_escalation_stamp already uses for escalate_case (src/flows/case-escalation-stamp.flow.ts, same ruling): the screen flow keeps the marketer's identity for the campaign read, the eligibility queries, the opt-out honour and the per-member dedupe, and hands only the insert to a dedicated callee. New file src/flows/campaign-member-enroll.flow.ts ships campaign_lead_member_enroll and campaign_contact_member_enroll — autolaunched, runAs: 'system', one create_record each, no branching. Attribution is unaffected: elevation is not anonymity (resolveRunDataContext returns { isSystem: true, actor, ...userId }), so each membership row still belongs to the person who enrolled it.

⛔ The field was not dropped, and no beforeInsert hook was used to dodge the diagnostic.

5. ⭐ A THIRD contract change the card does not mention

@objectstack/driver-memory@17.4.0 refuses any call the engine hands a tenant scope (MemoryMultiTenantUnsupportedError, objectstack#16589) — it has no row-level tenant isolation, so it refuses rather than answering unisolated. Three test harnesses went red on the bare bump for this reason and for no other:

  • test/flow-filter-today-token.test.ts
  • test/forecast-current-quarter-view.test.ts
  • test/escalation-task-subject.test.ts

Each passed an incidental tenantId: 'org_1' into a single-tenant fixture measuring something else entirely (a {TODAY()} token, quarter boundaries, a 255-character cap). The tenantId is gone from those five createContext calls. ⛔ tenancy: { enabled: false } was not used — the refusal names that as the wrong answer for data that really is per-organization.

6. The test/** files touched, and why

The card lifts #1581's test/** ban "for exactly the files the migration forces". The enumeration in the card is short by the ones it had not measured; the principle is what was applied, and every file below is red on the bare bump or on a change this card forces:

file forced by disposition
docs-analytics-vocabulary.test.ts the rename d.refreshInterval → d.refreshIntervalSeconds (named by the card)
readonly-write-semantics.test.ts readonly INSERT strip describe('the strip is an UPDATE-path rule — insert is deliberately exempt') retired, with a tombstone. ⛔ Not rewritten to assert the new contract
audit-stamp-readonly.test.ts readonly INSERT strip + the sub-flow split the platform-semantic row retired; the business rows re-aimed at where the writer now lives
guest-submission-sanitisation.test.ts readonly INSERT strip the is_escalated half of a negative control retired; internal_notes / resolution still carry the guarantee
case-guest-branch-leftovers.test.ts readonly INSERT strip same; escalation_reason (deliberately not readonly) still carries the guarantee
flow-filter-today-token.test.ts driver-memory tenancy refusal incidental tenantId dropped
forecast-current-quarter-view.test.ts driver-memory tenancy refusal incidental tenantId dropped
escalation-task-subject.test.ts driver-memory tenancy refusal incidental tenantId dropped
flow-campaign-enrollment.test.ts the sub-flow split callees registered on the harness — a subflow node resolves by name
flow-variable-conditions.test.ts the sub-flow split same
automation-docs-coverage.test.ts two new flows the two zh-Hans / zh-Hant row labels added to the page ledger

⛔ No test was rewritten to mirror the new platform rule. Under AGENTS.md scope rule 3 those rows belong upstream; they are retired here with a note handing them to epic step 5c (objectstack#15953).

7. Docs the two new flows force

Two new flows move the registered count 27 → 29: README.md (three sites), docs/STATUS.md's validate transcript, and a row plus the header count on content/docs/administration/automation.{mdx,zh-Hans.mdx,zh-Hant.mdx}. The Chinese labels follow the caller's own 营销活动/行銷活動 + 登记/登記 vocabulary and take 子流程 in the type column, as case_escalation_stamp does.

Testing

  • pnpm verify green end to end — validate · typecheck · lint · lint:i18n-gate · hygiene · hygiene:tokens · build · test
  • pnpm test — 165 test files passed, 3454 passed, 1 skipped, 0 failed
  • pnpm lint --json — errors: 0 · warnings: 13 · suggestions: 12, passed: true
  • Changeset added (.changeset/objectstack-17-4-0.md)

Full lint breakdown by rule id:

severity rule id count owner
error — 0 —
warning field-no-consumers 12 new rule in @objectstack/lint@17.4.0; per-field disposition below
warning component-props-unknown-key 1 #1216 — expected to stand, ⛔ not fixed here
suggestion approval-approvers-may-resolve-empty 4 unchanged from 17.3.0
suggestion object/missing-name-field 2 unchanged from 17.3.0
suggestion relationship/line-item-should-be-master-detail 2 unchanged from 17.3.0
suggestion relationship/line-items-inline-edit 2 unchanged from 17.3.0
suggestion rollup/missing-summary 2 unchanged from 17.3.0

Token ratchet after the change: business semantics ~85,267 (ceiling ~100,000), interaction layer ~37,846 (ceiling ~40,000), authored total ~137,758 (ceiling ~140,000). All three clean; no ceiling moved.

Additional Notes

The 12 field-no-consumers warnings — per-field disposition

⛔ Nothing suppressed, whitelisted or re-severitied. These are reported findings, per the card: "if a field is genuinely wanted-but-unconsumed, say so per field with the reason."

One reading applies to all twelve and should be settled before any of them is acted on. Every one carries a group, every group is a declared fieldGroup, and each of these objects renders through the synthesized detail/form layout rather than an authored *.page.ts — so all twelve are visible and editable in the product today. The rule's own wording is about naming: "no view column, form section, page binding, flow node, dataset, widget, formula, validation, hook or action names it." A synthesized layout names nothing. So "no consumer" here means "not named in metadata", not "unreachable by a user" — which is why none of these is a straightforward removal.

# field carriers disposition and reason
1 crm_account.logo 4 (locale labels only) Wanted, unnamed. An uploaded brand image in the branding group beside brand_color. No list column shows it and account_detail.page.ts does not bind it — the gap is a page gap, not a dead field. Candidate consumer: the account detail header.
2 crm_article_feedback.comment 4 (locale labels only) Wanted, unnamed. Its own description states the consumer that does not exist yet: "Optional note explaining the verdict — read by the article's author." Nothing surfaces it to the author. Either build that surface or the column is collecting text nobody reads.
3 crm_campaign.description 11 (4 labels + 7 seed values) Wanted, unnamed. Markdown body on the basic group; seven seed campaigns fill it. Not on any view column and no campaign detail page exists.
4 crm_campaign_member.added_date 57 (2 flow nodes + 4 labels + 51 seed values) ⭐ The sharpest one, and it is in tension with item 4 of this card. The card requires the enrollment stamp to keep being written — and this PR built two elevated sub-flows so it can be — while the new rule reports that nothing reads it: the two flow nodes are writers, and writers are carriers, not consumers. Ruled readonly in #1667 precisely because it is an audit stamp. Recommendation: give it a view column or a detail binding on crm_campaign_member, which makes the stamp legible and settles the rule in the same move. ⛔ Removing it would contradict this card.
5–9 crm_contact.mailing_street · mailing_city · mailing_state · mailing_postal_code · mailing_country 5 each (4 labels + 1 import-mapping target) Wanted, unnamed — and the only group of the twelve with a second carrier kind: each is an import-mapping target, so the CSV importer writes them. They render as the mailing_address field group on the synthesized contact layout (defaultExpanded: false); contact.view.ts has zero mailing_ columns. Note the precedent one comment below them in contact.object.ts: birthdate was deliberately not declared on the same "declared but inert" reasoning — with the difference that a mailing address has a stated importer that fills it.
10 crm_contract.description 9 (4 labels + externalId + 4 seed values) Wanted, unnamed. Same shape as #3. Note crm_contract also declares special_terms, which IS consumed — so the two markdown bodies are not redundant by accident, and which one a reader is meant to use is a product question.
11 crm_forecast.seed_key 12 (4 labels + externalId + 7 seed values) ⚠️ Correctly unconsumed by design — the one where the rule is arguably reporting a false positive. It is hidden: true, readonly: true, and its description says "Demo-fixture identity. Written only by the seed loader; empty on every real snapshot." Its whole job is to be a seeder-only upsert identity. A consumer would defeat it. Recommendation: leave it and take the warning, or raise the hidden: true + seeder-identity shape upstream as a case the rule should recognise.
12 crm_quote_line_item.line_number 23 (4 labels + 19 seed values) Wanted, unnamed. readonly ordinal in the basic group; 19 seed rows carry it. Nothing orders or displays by it — the line-item related list does not use it as a sort or a column, which is the obvious consumer and is probably the actual defect here.

Recommended follow-ups, none of which belongs in a migration PR: one card for the display gaps (#1, #3, #4, #10, #12 — all "give it the view column or detail binding it should already have"), one for #2 (build the author-facing surface its description promises, or retire the column), one product question for #5–9 (does the contact layout want the mailing group on a view, given the importer fills it), and one upstream question for #11.

Assumptions and judgement calls, stated

  1. The test/** permission was read as its principle, not its enumeration — "exactly the files the migration forces". Five files beyond the card's list are red on the bump and are edited; none is edited for any other reason. Table in section 6.
  2. AGENTS.md house rule 9 was treated as binding over the card's literal instruction, because the card's instruction is not authorable as written (no node-level runAs) and its flow-level form is what rule 9 forbids by name. The sub-flow split is rule 9's own prescribed shape and has an in-repo precedent. If the PM prefers the one-line flow-level elevation, this is the decision to reverse — it costs the RLS widening described in section 4 and reds audit-stamp-readonly.test.ts's "the enrollment SCREEN flow is still NOT elevated" row.
  3. os migrate meta's prescription was applied, not re-derived. The command cannot rewrite .ts sources and its own tombstone says to apply the listed edits by hand; the snapshot it emits was used as the oracle for the result.
  4. The 12 field-no-consumers warnings are reported, not fixed. Every one needs a product decision (which view column, which detail binding) or a 57-site carrier removal, and ci(deps): bump softprops/action-gh-release from 1 to 2 #4 is in direct tension with this card's own item 4.
  5. Comments in src/ recording "measured on 17.3.0" were left alone. They are dated records of when a measurement was taken, not claims about the current pin; rewriting them would falsify the record.
  6. docs/STATUS.md:41 carries a sentence — "The README states the same 27" — inside a paragraph about the 31 Actions figure. It reads as pre-existing drift from when actions were 27, unrelated to the flow count this PR moved. Left untouched; noted so it is not read as collateral.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DuzfS5chho38Yx1jxx9DEj


Generated by Claude Code

Bump all 21 @objectstack/* pins 17.3.0 -> 17.4.0, exact, and take the three
contract changes the line carries.

1. dashboard.refreshInterval -> refreshIntervalSeconds across all five
   dashboards, applied from what `os migrate meta` prescribed. The old key is a
   ZodNever tombstone, so the rename is the bump's precondition rather than a
   step that can follow it.

2. A static readonly field is now stripped from a non-system caller's INSERT
   payload, not only from an update. crm_campaign_member.added_date is that
   shape, so the enrollment INSERT moves into two dedicated runAs:'system'
   sub-flows reached through subflow nodes. The campaign_enrollment SCREEN flow
   stays runAs:'user' per AGENTS.md house rule 9 -- elevating it would also lift
   row-level security off its two bulk reads over a private-sharing object.

3. driver-memory refuses any call handed a tenant scope. Three test harnesses
   passed an incidental tenantId into single-tenant fixtures that measure
   nothing about tenancy; they no longer ask an unisolating driver to isolate.

Declared versions follow: objectstack.manifest.json specVersion +
engines.protocol, objectstack.config.ts manifest.engines.protocol, docs/STATUS.md
and the three whats-new locales.

Test rows that pinned the retired platform semantic are retired rather than
re-aimed at the new one -- restating a platform write rule locally is what
AGENTS.md scope rule 3 forbids.

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

vercel Bot commented Sep 9, 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 9, 2026 12:19pm UTC

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci/cd CI plumbing and the verification pipeline dependencies Dependency bumps and lockfile changes metadata Declarative metadata — schema, security posture, UI surfaces configuration Build and app configuration files backend Server-side behaviour — hooks, flows, actions labels Sep 9, 2026
@os-steve
os-steve marked this pull request as ready for review September 9, 2026 12:28
@os-steve
os-steve added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 965933b Sep 9, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Server-side behaviour — hooks, flows, actions ci/cd CI plumbing and the verification pipeline configuration Build and app configuration files dependencies Dependency bumps and lockfile changes documentation Improvements or additions to documentation metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

2 participants