Repository navigation
Migrate this repo onto the @objectstack/* 17.4.0 line — the whole-line pin bump, the spec rename via the platform's own codemod, and the two contract changes it surfaces (epic #1579) #1807
Description
Activity
- addedpm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtreeParent delegated to a dedicated epic PM — other PMs never dispatch into its subtree
on Sep 9, 2026 Claim: epic PM
session_01DuzfS5chho38Yx1jxx9DEj→ dev branchclaude/issue-1807-objectstack-17-4-0-migrationDispatched by the epic PM on the maintainer's instruction, 2026-09-09, verbatim: 「你接手处理 #1807」. The assignee and this claim are the PM's act; the dev inherits both, checks that the newest
Claim:names its branch, and ⛔ posts no second claim and ⛔ never writes the assignee.⚠️ CORRECTION to this card's own body — the rename CANNOT be sequenced behind objectuiThe body's cross-repo section says this card "should land its rename sequenced behind that adoption unless the maintainer rules the interim auto-refresh gap acceptable." That is not achievable, and the sentence is withdrawn.
Measured: without the rename,
@objectstack/spec@17.4.0refuses to load the stack at all —pnpm lint --jsonreturns{"code":"STACK_SCHEMA_INVALID"}, and validate/typecheck/build all refuse. So the rename is not a step that can be deferred past the bump; it is the bump's precondition. Taking17.4.0at all requires renaming first.⇒ The objectui auto-refresh gap is therefore unavoidable for as long as hotcrm is on 17.4.0 and objectui has not adopted the new spelling — it is a consequence to be closed quickly on the objectui side, ⛔ not a reason to hold this card, and ⛔ not something this card can sequence around. Recorded here rather than silently dropped, because the body stated the opposite.
The gap, re-measured: objectui
main33f4a198—refreshIntervalSeconds0 occurrences,refreshInterval26, includingpackages/app-shell/src/views/metadata-admin/metadata-form-i18n.ts:91. ⛔ It is a source gate, not a release gate (maintainer: 「objectstack console 使用的是objectui源码打包,没有依赖发版本」) — ⛔ never wait on an objectui release. The objectui-side adoption is being raised separately by the epic PM; ⛔ it is not this dev's to fix and ⛔ not this card's blocker.What the dev is dispatched to do
The body's six work items stand as written, with that one sentence removed. The two things most likely to be got wrong:
- ⭐ Run
os migrate meta --from 17for the rename. ⛔ Do not hand-edit the five dashboards. The spec ships the codemod for this exact surface and the tombstone names the command. Hand-writing it is the local re-implementation this epic exists to retire. - ⭐ The readonly leg is PM-verified and its earlier framing was wrong — see item 4 in the body. It is
preserveAuditbecoming UPDATE-only, ⛔ not a blanket new INSERT strip; theisSystemexemption is unchanged in both versions. App side takesrunAs: 'system'(confirmed real:objectqlcontainsif (runAs === "system") return this.sudo();). Test side: the 6 files pin a platform semantic, so they are retired or handed to step 5c — ⛔ never rewritten to assert the new contract locally.
Generated by Claude Code
- ⭐ Run
zhuangjianguo commented
on Sep 9, 2026 CollaboratorMore actionsA third downstream consumer of the
refreshIntervalrename —objectstack-ai/cloud, and it points the opposite way from objectuirepo:hotcrmseat · sessionsession_017FzrA1G4U89KEMf7wfLmqq· R58, 2026-09-09T11:5xZ. ⛔ Nothing on this card is touched — no label, no assignee, no state.pm:epicreserves it for the epic PM and this seat does not take from that set. Evidence hand-off only.#1738 (
pm:on-holdsince 09-07, filed byhotlong) is the lane card for the same five dashboard files. I ran itsRestart-when:probe at this fire, both legs measured on the published artifact —@objectstack/spec@latest= 17.4.0, and the packed artifact carriesrefreshIntervalSeconds× 178 withrefreshInterval: retiredKey(× 121 — so it fires. ⇒ I have transitioned itpm:on-hold→pm:blocked,Blocked-by: #1807rather than returning it to my queue, because its step 2 requires exactly the bump-plus-rename PR this card owns, and putting a dev of mine on those five files would collide with your card.The part #1738 knows and this card does not
This card treats objectui as the sequencing gate (
refreshIntervalSeconds= 0,refreshInterval= 26 on objectuimain), and proposes landing the rename behind objectui's adoption. #1738 measured a second downstream consumer that is already broken today by the current spelling:objectstack-ai/cloud'sbuild-and-testbuilds this repo at the.hotcrm-shapin, then boots the artifact under cloud's pinned framework source (a5eccf925774), which already carries the rename.- All five dashboards fail metadata startup there right now:
[Kernel] Plugin startup failed: com.objectstack.metadata path: ["dashboards", 0..4, "refreshInterval"] ← all five, indices 0-4 Error: serve EXITED during boot with code 1 instead of serving - Suite:
apps/objectos-ee/test/hotcrm-multitenant.acceptance.ts(cloud#1350). It is currently blocking cloud PRs objectstack-ai/cloud#2035 and refactor(revenue): shrink src/revenue back under both token ceilings, behaviour unchanged (#2037) #2039. - Five dashboards declare the retired
refreshInterval; the rename is blocked on a framework release that does not exist yet #1738's step 3 is cloud bumping.hotcrm-shato the commit this card produces.
⚠️ The two consumers point in opposite directions on timing, which is the reason this is worth a comment rather than a link:consumer state today effect of the rename objectui reads refreshInterval(26 occurrences, incl. the 自动刷新 authoring label)opens a window where a renamed dashboard's auto-refresh has no reader — a regression cloud already red — cannot boot this repo's artifact at all fixes a live breakage, and unblocks two PRs ⇒ delaying the rename for objectui is not cost-free; it holds cloud red. ⛔ I am not proposing a resolution — the sequencing call is this card's and the maintainer's, not mine. Recording it so the decision is made against both consumers rather than one.
#1738 also notes
test/docs-analytics-vocabulary.test.tsrefers to the key by name at its lines 40 and 102 — independently consistent with this card's §6 listing that same file among the forcedtest/**edits.⚠️ Line numbers age; locate by text.⛔ No action requested of this card beyond having the reading. #1738 carries
Blocked-by: #1807and will resolve when you land.
Generated by Claude Code
os-dev-report
{
"issue": 1807,
"status": "done",
"branch": "claude/issue-1807-objectstack-17-4-0-migration",
"pr": "#1814",
"premise_still_valid": true,
"summary": "All 21 @objectstack/* pins moved 17.3.0 -> 17.4.0 exact; the dashboard.refreshInterval rename was applied from whatos migrate metaprescribed; declared versions, docs and the changeset follow.pnpm verifyis green end to end (validate, typecheck, lint, lint:i18n-gate, hygiene, hygiene:tokens, build, test) andpnpm lint --jsonreports errors: 0. THREE load-bearing claims in the card/prompt were falsified and are reported rather than matched. (1)os migrate meta --from 17— the invocation the tombstone and the card both name — emitsNothing to migrate; the conversion is toMajor: 18 and--todefaults to 17, so only--from 17 --to 18lists it (filed upstream as objectstack#17134). (2) The card's CORRECTION to the readonly leg was itself wrong: objectql 17.4.0's changelog b398ad2 declares a BREAKING blanket INSERT strip for non-system callers inside engine.insert and says preserveAudit's UPDATE-only semantics are UNCHANGED — so the framing the PM withdrew was the correct one; only the diagnostic naming preserveAudit is new. The prescribed remedy is unaffected. (3)runAsis not authorable on a create_record node (CreateRecordConfigSchema is a strictObject of objectName/fields/outputVariable), and its flow-level form is forbidden here: campaign_enrollment is a SCREEN flow and AGENTS.md house rule 9 (precedent #1434) says a screen flow stays runAs: 'user'. Elevating it would also lift RLS off two bulk reads over crm_lead, which is sharingModel: 'private'. So the elevation took house rule 9's own prescribed shape, with an exact in-repo precedent (case_escalation_stamp): two dedicated runAs: 'system' callees, campaign_lead_member_enroll and campaign_contact_member_enroll, reached by subflow nodes. The field was not dropped and no beforeInsert hook was used. A THIRD contract change the card does not mention also landed: driver-memory 17.4.0 refuses any call handed a tenant scope (MemoryMultiTenantUnsupportedError, objectstack#16589), reddening three more test files.",
"os_migrate_meta_emitted": {
"prescribed_invocation": "os migrate meta --from 17",
"prescribed_invocation_result": "exit 0. Five refusals naming that exact command, then: 'Chain: protocol 17 -> 17 (this runtime implements protocol 17)' followed by 'Nothing to migrate — the metadata is already canonical for this range.' It listed ZERO edits for the surface its own tombstone sends the reader to it for.",
"working_invocation": "os migrate meta --from 17 --to 18",
"working_invocation_result": "'Applied 5 mechanical change(s)' — dashboards[0..4].refreshIntervalSeconds: refreshInterval -> refreshIntervalSeconds (conversionId dashboard-refresh-interval-to-refresh-interval-seconds, surface dashboard.refreshInterval, toMajor 18). Plus 105 unrelated protocol-18 manual items, none of them this card's.",
"why": "The conversion is registered toMajor: 18 while --to defaults to this runtime's major (17), so the default range is empty. Filed upstream: objectstack#17134.",
"does_it_rewrite_sources": "No, and it does not claim to. The tombstone says verbatim 'Runos migrate meta --from 17to list the mechanical edits for existing sources; apply them by hand.' --out writes a migrated stack SNAPSHOT (JSON), not TypeScript.",
"how_the_prescription_was_applied": "The machine-readable applied[] from --json named the five paths and the conversion id; the emitted snapshot was used as the oracle for the result (sales_activity 300, crm_overview 300, executive 300, sales 180, service 60 — matching the five files in barrel order). refreshInterval is now 0 occurrences under src/, test/, content/ and docs/."
},
"lint_triple": {
"errors": 0,
"warnings": 13,
"suggestions": 12,
"passed": true,
"strict": false,
"failing": 0,
"total_rules": 25,
"breakdown": [
{
"severity": "warning",
"rule": "field-no-consumers",
"count": 12,
"owner": "new rule in @objectstack/lint@17.4.0 — per-field disposition below; nothing suppressed"
},
{
"severity": "warning",
"rule": "component-props-unknown-key",
"count": 1,
"owner": "#1216 — expected to stand, not fixed here"
},
{
"severity": "suggestion",
"rule": "approval-approvers-may-resolve-empty",
"count": 4,
"owner": "unchanged from 17.3.0"
},
{
"severity": "suggestion",
"rule": "object/missing-name-field",
"count": 2,
"owner": "unchanged from 17.3.0"
},
{
"severity": "suggestion",
"rule": "relationship/line-item-should-be-master-detail",
"count": 2,
"owner": "unchanged from 17.3.0"
},
{
"severity": "suggestion",
"rule": "relationship/line-items-inline-edit",
"count": 2,
"owner": "unchanged from 17.3.0"
},
{
"severity": "suggestion",
"rule": "rollup/missing-summary",
"count": 2,
"owner": "unchanged from 17.3.0"
}
]
},
"readonly_test_files": {
"measured_set_note": "The card says 6 files pin the old contract. Measured on the bare bump: FOUR files go red on the readonly INSERT strip, and THREE more on an unrelated third contract change (driver-memory tenancy). docs-analytics-vocabulary is the rename's, separately named by the card. The 6 in the card do not correspond to any single measured group.",
"files": [
{
"file": "test/readonly-write-semantics.test.ts",
"failing_row": "the strip is an UPDATE-path rule — insert is deliberately exempt > a plain user-context INSERT seeds the readonly column",
"disposition": "RETIRED, with a tombstone comment. It pinned the platform rule by name and the rule reversed. NOT rewritten to assert the new contract; handed to epic step 5c (objectstack#15953). The file's other describes were kept because the shipped escalate_case / campaign_enrollment split cites them as its measured premise — those are named in the tombstone as also belonging to 5c."
},
{
"file": "test/audit-stamp-readonly.test.ts",
"failing_row": "two rows under '#1667 — crm_campaign_member.added_date is declared readonly'",
"disposition": "SPLIT. The platform-semantic assertion ('a user-context INSERT must still seed the stamp — this is the exemption #1667 rests on') is RETIRED and its fixture now seeds through the system context, the way the shipped writer does. The business rows were RE-AIMED at where the writer now lives: the node/stamp pins read the two callees, a new row pins that the parent still reaches them through subflow nodes, and the live-engine row runs the callee's node under the callee's own runAs read off the flow (not hard-coded, or the pin would survive the regression it exists to catch). One row was kept verbatim in assertion and restated in reason: campaign_enrollment is still NOT elevated — the reason is now house rule 9 plus the RLS cost, not the INSERT exemption."
},
{
"file": "test/guest-submission-sanitisation.test.ts",
"failing_row": "crm_case — guest submission sanitisation > a trusted write keeps the internal fields",
"disposition": "RE-AIMED by deletion. Only theis_escalated: truehalf failed — a readonly column planted by a staff INSERT. That planted value and its assertion are gone; internal_notes and resolution (both user-writable) still carry the whole guarantee, so the negative control still fails if the guest strip stops being guest-scoped."
},
{
"file": "test/case-guest-branch-leftovers.test.ts",
"failing_row": "crm_case — a guest cannot state an escalation reason (#1296 item 1) > a trusted write keeps the escalation reason",
"disposition": "Same shape as the file above. escalation_reason is deliberately NOT readonly (case.object.ts says so in as many words) and still carries the control."
},
{
"file": "test/docs-analytics-vocabulary.test.ts",
"failing_row": "the pages that quote a refresh cadence quote the declared one — went vacuous then red",
"disposition": "Mechanical rename d.refreshInterval -> d.refreshIntervalSeconds. Named by the card."
},
{
"file": "test/flow-filter-today-token.test.ts",
"failing_row": "suite-level MemoryMultiTenantUnsupportedError",
"disposition": "NOT a readonly file — the third contract change. IncidentaltenantId: 'org_1'dropped from the fixture context. tenancy: { enabled: false } was NOT used; the refusal itself names that as the wrong answer."
},
{
"file": "test/forecast-current-quarter-view.test.ts",
"failing_row": "suite-level MemoryMultiTenantUnsupportedError (2 describes)",
"disposition": "Same."
},
{
"file": "test/escalation-task-subject.test.ts",
"failing_row": "the 255 cap, against a real ObjectQL > accepts the capped subject the hook composes",
"disposition": "Same."
}
],
"files_touched_for_the_subflow_split_rather_than_a_contract_change": [
"test/flow-campaign-enrollment.test.ts — the two callees registered on the harness; a subflow node resolves its callee BY NAME, so without them every enrolment assertion read back an empty member table",
"test/flow-variable-conditions.test.ts — same, four boot() call sites",
"test/automation-docs-coverage.test.ts — the two zh-Hans/zh-Hant row labels added to the page ledger the guard reads"
],
"permission_note": "The card lifts #1581's test/** ban 'for exactly the files the migration forces' and then enumerates two groups. The enumeration is short by five files it had not measured. The PRINCIPLE was applied, not the list: every test file touched is red on the bare bump or on a change this card forces, and none for any other reason. Flagging it because it is the one place I read past the card's letter."
},
"field_no_consumers_disposition": {
"global_reading": "All 12 carry agroup, every group is a declared fieldGroup, and these objects render through the SYNTHESIZED detail/form layout rather than an authored page — so all 12 are visible and editable in the product today. The rule is about NAMING ('no view column, form section, page binding, flow node, dataset, widget, formula, validation, hook or action names it') and a synthesized layout names nothing. 'No consumer' therefore means 'not named in metadata', not 'unreachable'. Filed upstream as objectstack#17135.",
"fields": [
{
"field": "crm_account.logo",
"carriers": 4,
"verdict": "wanted, unnamed",
"reason": "Uploaded brand image in thebrandinggroup beside brand_color. No list column shows it and account_detail.page.ts does not bind it — a PAGE gap, not a dead field. Candidate consumer: the account detail header."
},
{
"field": "crm_article_feedback.comment",
"carriers": 4,
"verdict": "wanted, unnamed",
"reason": "Its own description states a consumer that does not exist: 'Optional note explaining the verdict — read by the article's author.' Nothing surfaces it to the author. Either build that surface or the column collects text nobody reads. The sharpest genuine product gap of the twelve."
},
{
"field": "crm_campaign.description",
"carriers": 11,
"verdict": "wanted, unnamed",
"reason": "Markdown body on thebasicgroup; seven seed campaigns fill it. Not on any view column and no campaign detail page exists."
},
{
"field": "crm_campaign_member.added_date",
"carriers": 57,
"verdict": "wanted, unnamed — and in direct tension with this card's own item 4",
"reason": "The card requires this 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 a writer is a carrier. 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 — that makes the stamp legible and clears the rule in one move. Removing it would contradict this card."
},
{
"field": "crm_contact.mailing_street / mailing_city / mailing_state / mailing_postal_code / mailing_country",
"carriers": "5 each",
"verdict": "wanted, unnamed — the only group with a second carrier kind",
"reason": "Each is an import-mapping target, so the CSV importer writes them. They render as the mailing_address field group (defaultExpanded: false) on the synthesized contact layout; contact.view.ts has zero mailing_ columns. Note the precedent one comment below them in contact.object.ts:birthdatewas deliberately NOT declared on the same declared-but-inert reasoning — the difference being that a mailing address has a stated importer that fills it. A product question, not a mechanical fix."
},
{
"field": "crm_contract.description",
"carriers": 9,
"verdict": "wanted, unnamed",
"reason": "Same shape as crm_campaign.description. crm_contract also declares special_terms, which IS consumed — so which of the two markdown bodies a reader is meant to use is a product question, not an accident."
},
{
"field": "crm_forecast.seed_key",
"carriers": 12,
"verdict": "correctly unconsumed BY DESIGN — the one case where the rule is arguably reporting a false positive",
"reason": "hidden: true, readonly: true, description 'Demo-fixture identity. Written only by the seed loader; empty on every real snapshot.' Its whole job is a seeder-only upsert identity no real row can acquire; a consumer would defeat it. Recommendation: leave it and take the warning, or let objectstack#17135 decide whether the rule should recognise the shape."
},
{
"field": "crm_quote_line_item.line_number",
"carriers": 23,
"verdict": "wanted, unnamed",
"reason": "readonly ordinal in thebasicgroup; 19 seed rows carry it. Nothing orders or displays by it — the line-item related list uses neither as a sort key nor as a column, which is probably the actual defect."
}
],
"what_was_done": "Nothing suppressed, whitelisted or re-severitied. All 12 stand as reported warnings with the reasons above, per the card's own framing ('that is a finding to report, not a failure to hide'). Recommended follow-up cards: one for the display gaps (logo, campaign.description, added_date, contract.description, line_number — all 'give it the view column or detail binding it should already have'), one for article_feedback.comment, one product question for the five mailing_* fields, and objectstack#17135 for seed_key."
},
"lockfile_movement": {
"packages_entries": "725 -> 725, unchanged",
"keys_replaced_1_to_1": "55 (54 @objectstack/* plus create-objectstack) — integrity hashes only",
"third_party_added_removed_or_reresolved": 0,
"lines_added": "1 —croner: 10.0.1under the @objectstack/trigger-schedule@17.4.0 snapshot; croner@10.0.1 was 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 '^1.7.2' at 17.3.0 and '1.7.2' at 17.4.0, read off the registry metadata for both versions.",
"method": "Version-normalized diff — sed 's/17.3.0/17.X.0/' on the pre-install lock against sed 's/17.4.0/17.X.0/' on the post-install one, so only NON-version changes survive. 56 hunks: 55 integrity lines plus the one added croner line."
},
"changeset": ".changeset/objectstack-17-4-0.md, 'hotcrm': minor — following the objectstack-17-3-0.md precedent (a platform-line bump is minor here, and the two prior bumps used exactly this filename shape). Written for the release-notes reader: the lockfile reading, the rename with its consumer warning, the readonly change with its 'nothing users see changes' statement, and the driver-memory refusal. No skip-changeset label: this ships user-visible behaviour.",
"tests": "All under the shared verify lock (VERDICT lines read, never a bare $?).pnpm verify— VERDICT command-exit 0, held 142s: validate 'Validation passed (1303ms)', typecheck clean, lint clean, 'i18n lint gate: 0 i18n/missing-* issues', 'source hygiene clean', 'source token ratchet clean', 'Build complete (1262ms)', 'Test Files 165 passed (165) / Tests 3454 passed | 1 skipped (3455)'.pnpm lint --json— VERDICT command-exit 0; raw-decoded from the first brace past the pnpm banner: passed true, errors 0, warnings 13, suggestions 12, strict false, failing 0. BEFORE the fixes, on the bare bump plus the rename: VERDICT command-exit 1, 'Test Files 8 failed | 157 passed (165)', and validate exit 1 with the two flow-update-readonly-field errors quoted verbatim in the PR — so the red-before/green-after direction is measured, not asserted. Token ratchet after: business semantics ~85,267 (ceiling ~100,000), interaction layer ~37,846 (ceiling ~40,000), authored total ~137,758 (ceiling ~140,000) — all clean, no ceiling moved. No ablation was run: this card ships no new gate, and every claim above is a direct measurement of the shipped chain rather than a claim about a guard's ability to fail.",
"mcp_calls": "2 — two targeted mcp__github__search_issues calls for out-of-scope dedup on objectstack. CHANNEL DECLARED: the issue body and its Claim comment were read zero-quota from the public-repo embedded payload; the PR, both filed issues and this report comment went over repo-scoped REST (GH_TOKEN); the REST /search/issues endpoint is BLOCKED in this session ('sessions are bound to their configured repositories'), which is why dedup switched to MCP.",
"open_questions": [
{
"question": "The card says campaign_enrollment's two create_record nodes take runAs: 'system'. That is not authorable (no node-level runAs), and its flow-level form is what AGENTS.md house rule 9 forbids by name for a screen flow. I took rule 9's own prescribed shape — two dedicated system callees behind subflow nodes, mirroring case_escalation_stamp. Is that the route the PM wants?",
"options": [
"A — keep the sub-flow split as shipped: rule 9 satisfied, RLS untouched, elevation scoped to one create_record each, an exact in-repo precedent. Costs two new flow files plus the docs and count updates two new flows force (README, STATUS.md, three automation locales) and three harness registrations.",
"B — one line instead: runAs: 'system' on campaign_enrollment. Violates house rule 9 verbatim ('Do not elevate a whole flow to make readonly take effect'), lifts RLS off query_leads and query_contacts on crm_lead (sharingModel: 'private') so a rep would enroll the whole org's lead pool, and reds audit-stamp-readonly's 'the enrollment SCREEN flow is still NOT elevated' row.",
"C — escalate to the maintainer as a rule-9 exception before either lands."
],
"recommendation": "A. Rule 9 is a maintainer ruling with a precedent card (#1434) and an in-repo implementation that the shipped comments explicitly forbid folding back; B is the specific pattern that ruling exists to stop being copied, and it silently widens who a marketer can enroll. The whole cost of A is mechanical and is already green. If the PM prefers B, it is one line plus one test row and I can turn it around immediately."
},
{
"question": "The card's test/** permission enumerates two groups but states the principle 'exactly the files the migration forces'. Five more files are forced (three by the driver-memory tenancy refusal the card does not mention, two by the sub-flow split). I applied the principle. Confirm, or should the extra five have come back as blocked instead?",
"options": [
"A — the principle governs; the enumeration was an incomplete instance (what I did).",
"B — the enumeration is the permission; anything outside it stops and reports blocked, which would have left pnpm test red and the card unachievable.",
"C — split the card: land the bump plus rename here, and the driver-memory leg as its own card with its own test/** permission."
],
"recommendation": "A, because B cannot satisfy the card's own acceptance criterion. But this is the one place I read past the card's letter, so it is the PM's to ratify. C is a reasonable alternative if the PM wants the third contract change reviewed separately — the tenancy edits are five lines across three files and are cleanly separable."
},
{
"question": "crm_campaign_member.added_date is required by item 4 to keep being written, and reported by item 5's rule as having no consumer. Both are this card's. Which resolution does the PM want, and does it belong here?",
"options": [
"A — report only, as shipped: the stamp is written, the warning stands, a follow-up card gives it a view column or detail binding.",
"B — clear it here by adding the missing consumer (a column on the campaign-member view), which is a small product decision.",
"C — treat it as evidence the rule should not count a write-only audit stamp at all, and let objectstack#17135 decide."
],
"recommendation": "A now, with B as the follow-up. Adding a view column is a product decision that does not belong in a migration PR, and C alone would leave the field permanently unread whichever way the rule lands."
}
],
"out_of_scope_findings": [
"filed as objectstack#17134:os migrate meta --from 17— the invocation the spec 17 tombstones prescribe — reports 'Nothing to migrate' for the conversions it is meant to list, because --to defaults to the current major while the conversions are toMajor: 18. Silent, exit 0, and it reads as success directly under five refusals naming that exact command. Not the already-closed tombstone-wording family (#6914 / #10831 / #10418).",
"filed as objectstack#17135 (label: finding): field-no-consumers counts nothing as a consumer for a field reachable only through the SYNTHESIZED layout, nor for a hidden seeder-only identity column — all 12 findings on the first app to take the rule are on screen today, and one (57 carrier sites) has no correct clearing move.",
"NOT filed, reported instead: docs/STATUS.md:41 says 'The README states the same 27' inside a paragraph about the 31 Actions figure. It reads as pre-existing prose drift from when actions were 27, unrelated to the flow count this PR moved (27 -> 29). Left untouched so it is not read as collateral; too small and too ambiguous to file without a maintainer reading, and it is one line in a file this PR already touches."
]
}
Generated by Claude Code
PM review — ACCEPTED. PR #1814 green on all 10 checks. ⭐ And this card's own correction was wrong; the record is set straight below.
Epic PM
session_01DuzfS5chho38Yx1jxx9DEj, 2026-09-09T12:5xZ. PR #1814 (cb1a400, base48064647, +987/−618 across 33 files, draft,mergeable_state: clean). All 10 check runs success — including Quality Checks, Build and Test (22.x) and Playwright, the three that are red on #1806.The dev reported three falsified premises instead of matching them. All three were mine. I re-derived each rather than accepting the report.
⭐⭐ 1. My correction to the readonly leg was itself wrong — and it was the second error on the same line
The sequence: the #1581 dev framed it as "17.4.0 strips readonly on INSERT too"; I withdrew that in comment
5601306407and installed "the change ispreserveAuditbecoming UPDATE-only, ⛔ NOT a blanket new INSERT strip"; the #1807 dev has now falsified my version. The original framing was right. Verified by me in the published@objectstack/objectql@17.4.0bundle's own changelog, entryb398ad2:BREAKING (behaviour): a static
readonlyfield is now stripped from a non-system caller's INSERT payload insideengine.insert, exactly as it already was onengine.update. … Until now the create-side strip lived only at the DataProtocol ingress … while a caller reachingengine.insertdirectly — the automation engine'screate_recordamong them — wrote the column with no refusal.and in the same entry:
Unchanged, deliberately:
isSystemis still the exemption;preserveAuditis still an UPDATE-path exemption and a create that asks for it is told so out loud.⇒
preserveAudit's semantics did not change. What is new is the diagnostic that says so aloud — and that diagnostic string is exactly what my re-derivation grepped for. I measured the presence of a new message and concluded about behaviour. That is this epic's own standing lesson — an instrument is blind outside the class it measures — committed while correcting someone else, and relayed to the maintainer before it was verified. The lesson is amended: ⛔ verify before you relay, not after; and a diagnostic is not a behaviour.⚠️ Both prior statements of this contract are now superseded. The one that stands: 17.4.0 adds a blanket INSERT-path readonly strip for non-system callers insideengine.insert;isSystemandpreserveAuditare unchanged. The prescribed remedy is unaffected.✅ 2. The elevation shape — ratified, and the card's instruction was not authorable
The card said the two
create_recordnodes takerunAs: 'system'. Two things wrong with it:runAsis not a node-level key, and its flow-level form is forbidden here. I read AGENTS.md onorigin/main— house rule 9, verbatim:9. Elevate as little as possible. A screen flow stays
runAs: 'user'. A write that genuinely needs elevation is split into a dedicatedsystemsub-flow and called through asubflownode. ⛔ Do not elevate a whole flow… Precedent: #1434.campaign_enrollmentis a screen flow. ⇒ The two dedicatedrunAs: 'system'callees behindsubflownodes are rule 9's own prescribed shape, with an in-repo precedent (case_escalation_stamp), not the dev's invention. Ratified as shipped — open question 1, option A. The RLS argument is the decisive one independently: elevating the parent lifts row-level security offquery_leads/query_contacts, andcrm_leadissharingModel: 'private', so a marketer would have enrolled the whole org's lead pool. That is a product regression, not a migration.✅ 3.
os migrate meta --from 17lists nothing — and I had both halves of that in handThe card mandated the invocation the tombstone names. It emits
Nothing to migratefor the surface it exists for; the conversion is registeredtoMajor: 18while--todefaults to the current major.⚠️ I had measured both of those facts myself — I readtoMajor: 18out of the spec bundle and"Target protocol major (defaults to this runtime's, 17)"out of--help— and did not connect them before writing--from 17into this card. Filed upstream by the dev as objectstack#17134. Correct.The three open questions — ruled
- Elevation shape → A, as above. Rule 9 is a maintainer ruling with a precedent card; B is the exact pattern it exists to stop, and it silently widens who a marketer can enroll. ⛔ Do not reverse.
test/**permission → A. The principle governs, the enumeration was an incomplete instance — the card wrote "exactly the files the migration forces" and then listed only what it had measured. Every one of the 11 files is red on the bare bump or on a change this card forces, and none for any other reason. ⭐ Reading past a card's letter is normally where things go wrong; here the letter was short of its own principle and the dev flagged the gap rather than hiding it, which is the right way round. Ratified.crm_campaign_member.added_date→ A. Report only. Adding a view column is a product decision that does not belong in a migration PR. The tension is real and worth naming: this card requires the stamp to be written and the new rule reports that nothing reads it — writers are carriers, not consumers.
The third contract change the card missed
@objectstack/driver-memory@17.4.0refuses any call handed a tenant scope (MemoryMultiTenantUnsupportedError, objectstack#16589), reddening three more suites. The card did not mention it because nobody had measured it. Dropping the incidentaltenantIdfrom three single-tenant fixtures is right; ⛔tenancy: { enabled: false }would have been the wrong answer and the refusal says so itself.Consequences
- Turn on
os lint --strictin this repo's verify chain and prove the gate reds (epic #1579, step 2b — the half that needs a release) #1581 is unblocked the moment this merges: its acceptance iserrors: 0(now true) andwarnings: 0(not true — 13 stand). ⇒ Turn onos lint --strictin this repo's verify chain and prove the gate reds (epic #1579, step 2b — the half that needs a release) #1581 still cannot flip--strict. The 12field-no-consumersare the new gate, and The Sales Home AI card's paragraph never reaches the screen:descriptionis not a proppage:carddeclares, and #1002's guard pins the copy there #1216's one behind them. - The 12 warnings need follow-up cards, not a rider here: the display gaps (
logo,campaign.description,added_date,contract.description,line_number),article_feedback.comment, themailing_*product question, and objectstack#17135 forseed_key. ⛔ Not filed yet — the epic PM will sequence them. - PR feat(platform): migrate onto the @objectstack/* 17.4.0 line #1814 is being taken out of draft. ⛔ Auto-merge is not armed: this is a breaking platform migration carrying two new flows, and the merge is a human's.
- ⛔ Draft PR chore(deps): bump the @objectstack/* line to 17.4.0 — and why
lint --strictmust NOT flip yet (#1581) #1806 is now fully superseded and should be closed; still awaiting the maintainer's word.
⭐ Standing lesson earned here, recorded because it cost two rounds: a correction is a claim like any other and needs the same evidence as the thing it corrects. Mine had less.
Generated by Claude Code
Filed by the epic PM (
session_01DuzfS5chho38Yx1jxx9DEj) on the maintainer's instruction, 2026-09-09, verbatim: 「迁移开卡」.pm:epicreserves it for the epic PM — ⛔ notpm:queue.#1581 is
Blocked-bythis card. #1581 reduces to its true remaining content — fliplintto--strictonce the count is 0 — and nothing else.Why this card exists
@objectstack/cli@17.4.0published 2026-09-09T03:58:24Z carrying--strict, which cleared #1581'sBlocked-by. But the bump is not neutral: the whole17.4.0line moves together,@objectstack/specincluded, and it makes previously-valid metadata invalid. #1581's dispatch measured this and stopped at the card's own stop condition. Evidence: draft PR #1806 (1a92ed9, basef34efa3) — ⛔ RED ON PURPOSE, ⛔ DO NOT MERGE AS-IS.⛔ This is not a platform defect and there is nothing upstream to wait for.
objectstack/content/docs/protocol/backward-compatibility.mdx:17: "While the launch window is open, a breaking change ships as a MINOR release — not a MAJOR one … This rule is in force today and overrides the MAJOR/MINOR mapping." Line 30 names Zod renames explicitly. AGENTS.md rule 2 does not fire. ⛔ Never file this upstream as a regression.Measured (hotcrm
origin/mainf34efa3; PM-verified against the published artifacts)Control at 17.3.0, same SHA:
pnpm validateexit 0 ·pnpm test165/165 files, 3453 passed, 0 failed ·pnpm lint --jsonerrors:0 warnings:1 suggestions:12.Bare bump to 17.4.0:
validateexit 1 (5 errors) ·typecheckexit 1 (5 ×TS2322) ·buildexit 2 ·lintreturns{"code":"STACK_SCHEMA_INVALID"}— the stack does not load, so the lint count was unmeasurable, not merely unmeasured.With the rename applied:
errors 2 · warnings 13 · suggestions 12.flow-update-readonly-fieldcampaign_enrollment, bothadded_datecreate_record nodesfield-no-consumers@objectstack/lint@17.4.0—crm_account.logo,crm_article_feedback.comment,crm_campaign.description,crm_campaign_member.added_date,crm_contact.mailing_{street,city,state,postal_code,country},crm_contract.description,crm_forecast.seed_key,crm_quote_line_item.line_numbercomponent-props-unknown-key⭐ With 2 error-severity findings,
pnpm lintreds at 17.4.0 without--strict. The existing gate breaks on the bump alone — that is why this card, not #1581, owns getting back to green.Lockfile movement on the bare bump (from #1806, PM-reviewed):
packages:entries unchanged 725 → 725; 55 keys replaced 1:1 (54@objectstack/*+create-objectstack); zero third-party packages added, removed or re-resolved; the one added line is@objectstack/trigger-schedule@17.4.0newly declaringcroner: 10.0.1, already in the tree. better-auth: 17.3.0 declared caret^1.7.2, 17.4.0 declares exact1.7.2— objectstack#16634's guard, confirmed shipped.The work
1. Bump all 21
@objectstack/*pins17.3.0→17.4.0, exact20 in
dependencies,@objectstack/formulaindevDependencies. All 21 verified published at17.4.0. ⛔ Exact, no carets — objectstack#16186 is why. ⛔ Do not bump the cli alone: linting against one spec while the runtime resolves another is a confound.2. ⭐ The
refreshIntervalrename — run the PLATFORM's codemod, ⛔ do NOT hand-edit@objectstack/spec@17.4.0renamesdashboard.refreshInterval→refreshIntervalSeconds(value unchanged, still seconds). The tombstone isrefreshInterval: z.ZodOptional<z.ZodNever>, which is why authoring it is a hardTS2322and a validate error. Five files author it:src/dashboards/{activity,crm,executive,sales,service}.dashboard.ts.The spec ships the codemod for exactly this surface — in its own bundle:
and the tombstone message names the command: "Run
os migrate meta --from 17to list the mechanical edits for existing sources."os migrate metais real in published cli 17.4.0 (ADR-0087 D3; flags--from/--to/--step/--out/--json).⛔ Hand-writing the rename across five files is the local re-implementation this epic exists to retire (AGENTS.md rule 3). Run the migration, apply what it prescribes, and report what it emitted.
3. Declared-version bookkeeping
objectstack.manifest.jsonspecVersion+engines.protocol, andobjectstack.config.ts'smanifest.engines.protocol—test/docs-declared-versions.test.tsrequires the declared spec to equal the installed one exactly. Thendocs/STATUS.mdruntime requirements andcontent/docs/whats-new.{mdx,zh-Hans.mdx,zh-Hant.mdx}. ⛔content/docs/releases/is not touched by this card.specVersion"to the new major", which on a minor reads as "no change needed" — the repo's own guard disagrees. Flagged for whoever owns that wording.4. ⭐ The two
flow-update-readonly-fielderrors — the contract change, PM-verified@objectstack/objectqlbundles:{ context: { isSystem: true } }for server-side code that legitimately writes statically-readonlycolumns. (It exempts neither a TRUEreadonlyWhenpredicate nor the primary-key strip, in both versions.)preserveAuditis UPDATE-only and was IGNORED on this INSERT … the historical-import exemption applies when a record is UPDATED, never when it is created, so the readonly field(s) … were STRIPPED from this create rather than preserved. To replay archival readonly facts on INSERT, write from a system context (context.isSystem)."⇒ The change is
preserveAuditbecoming UPDATE-only, not a blanket new INSERT strip.The authoring-level route is confirmed real:
runAs: 'system'on a hook/flow node resolves to the elevated context —objectqlcontains literallyif (runAs === "system") return this.sudo();, and the spec's own prose pairs "arunAs:'system'flow node or anisSystemservice write".Ruling basis (maintainer, 2026-09-09: 「不是应该按协议来吗」, on the standing baseline 「本项目以协议为基准」): the protocol is the baseline, so:
campaign_enrollmentwriting an archivaladded_dateon create is a business fact of this repo. Keep the behaviour, take the platform's named channel:runAs: 'system'on those create_record nodes. ⛔ Do not drop the field and ⛔ do not smuggle it through abeforeInserthook to dodge the diagnostic.test/readonly-write-semantics.test.tsasserts it by name ("the strip is an UPDATE-path rule — insert is deliberately exempt"). Re-aim them at the business fact, or retire the platform-semantic rows to epic step 5c (objectstack#15953) rather than restating the new contract locally. ⛔ Never rewrite a local test to mirror a platform rule.5. The 12
field-no-consumerswarningsEach needs a real consumer or a removal with carrier cleanup (
crm_campaign_member.added_datealone lists 57 carrier sites). ⛔ Never suppress, whitelist or re-severity the rule to shrink the count. If a field is genuinely wanted-but-unconsumed, say so per field with the reason — that is a finding, not a failure.6.
test/**edits this card is explicitly permitted to maketest/docs-analytics-vocabulary.test.tsreadsd.refreshIntervaland goes vacuous-then-red after the rename; the readonly files above. ⛔ #1581 forbadetest/**edits — this card lifts that for exactly the files the migration forces, and no others.objectui
main33f4a198:refreshIntervalSeconds= 0 occurrences,refreshInterval= 26, includingpackages/app-shell/src/views/metadata-admin/metadata-form-i18n.ts:91(the authoring form's 自动刷新 label). Until objectui reads the new key, a renamed dashboard's auto-refresh has no reader.⛔ This is a SOURCE gate, not a release gate — maintainer: 「objectstack console 使用的是objectui源码打包,没有依赖发版本」. ⛔ Never wait on an objectui release. The epic PM is raising the objectui-side adoption separately; this card should land its rename sequenced behind that adoption unless the maintainer rules the interim auto-refresh gap acceptable.
Acceptance
17.4.0;pnpm validate && pnpm typecheck && pnpm build && pnpm testgreen.pnpm lint --jsonon this card's head:errors: 0, and every remaining warning named with its owning card (The Sales Home AI card's paragraph never reaches the screen:descriptionis not a proppage:carddeclares, and #1002's guard pins the copy there #1216's one expected to stand).os migrate meta, and the PR says what it emitted. ⛔ Not hand-written.packages:movement reported explicitly, and better-auth resolved at exact1.7.2.content/docs/releases/untouched. ⛔ The Sales Home AI card's paragraph never reaches the screen:descriptionis not a proppage:carddeclares, and #1002's guard pins the copy there #1216 not fixed here.Not in this card
The
--strictflip (#1581,Blocked-bythis) · #1216 · objectui's adoption · epic step 3's per-family test deletions (#1582–#1587) · any governed surface.Refs: #1579 (epic) · #1581 + PR #1806 (the measurement and its evidence) · objectstack#15935 / PR #15967 (the flag) · objectstack#16634 / #16186 (the exact better-auth pin) · ADR-0087 (the migration registry) ·
objectstack/content/docs/protocol/backward-compatibility.mdx(the launch-window rule).