Skip to content

chore(deps): upgrade the ObjectStack platform to 18.0.0-next.0 - #2091

Merged
os-zhuang merged 11 commits into
mainfrom
claude/issue-2031-objectstack-18-next-0
Oct 13, 2026
Merged

os-zhuang merged 11 commits into
mainfrom
claude/issue-2031-objectstack-18-next-0

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #2031

Description

This PR upgrades HotCRM from @objectstack/* 17.7.0 to 18.0.0-next.0, on the maintainer's order 「首先处理 objectstack 升级到 18.0.0-next.0」.

18.0.0-next.0 is a protocol-major prerelease. It made pnpm verify fail in the places listed under Per-breakage list, and each one is fixed where it reached this app. The PR also carries the two maintainer rulings made on #2031 during the work.

Ruling 1, token ceilings (comment 6143114246, verbatim): 「hotcrm token 上限提升 50%」 and 「暂时只优先处理升级相关的任务」.

  • All 12 committed ceilings are raised ×1.5 exactly.
  • They are recorded as CEILING_KIND.RULED, the kind the gate's header prescribes for a grant. None of the products is anchor() of a reading, so they cannot be ANCHORED.
  • No other change was made to the gate's logic.

Ruling 2, the memory driver (comment 6144864473, verbatim): 「就应该在 sql 上测试,作废内存驱动的测试」.

  • Every suite leg that ran on driver-memory and is now refused runs on SQL.
  • Legs whose only subject was the absent-key shape that only driver-memory produces are deleted. One line per retired leg is below.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change: the artifact needs a v18 runtime, and a 17.x host refuses it at the handshake
  • Documentation update
  • Code refactoring
  • Performance improvement
  • CI/CD update

Related Issues

Fixes #2031

The waiting cards keep their own fixes. Each of these is out of scope for this PR:

The one exception is the retirement of test/helpers/identity-objects.ts, which was in scope via objectstack#22074 and is done here.

Changes Made

Pins.

  • All 24 @objectstack/* packages move 17.7.0 → 18.0.0-next.0, exact: 20 in dependencies, 4 in devDependencies.
  • specVersion and engines.protocol move to ^18.0.0-next.0 in objectstack.manifest.json, src/sales/index.ts and src/service/index.ts.

Lockfile delta. pnpm install regenerated pnpm-lock.yaml:

  • 790 → 791 packages: keys;
  • the 54 @objectstack/* and create-objectstack keys moved one for one;
  • @objectstack/service-mailbox@18.0.0-next.0 was added;
  • no third-party package changed version.

Per-breakage list

  1. Protocol handshake. engines.protocol / specVersion ^17.7.0 is refused by a protocol-18 runtime, so both move to ^18.0.0-next.0 (be03a57).

  2. Email-template category is retired and refused at parse (spec ec7f7f8). The bundle no longer writes it (be03a57).

  3. Flow value-slot templates are refused (flow-value-slot-template-dialect-refused). os validate reported 340 errors. All 32 flows and _billing-endpoint.ts now use:

    • CEL value envelopes (cel / expression()) in value slots;
    • {{ hole }} in text slots.

    os validate now passes (1602a55).

  4. More flow copy is translatable. 32 new keys per locale are translated in zh-CN, es-ES and ja-JP, and lint:i18n-gate reports 0 (248b865).

  5. manage_org_presentation is retired (ADR-0131 D6) and removed from tenant_admin (dac942b).

  6. The verify harness changed in 18 (0560e35, bf2f548):

    • the Default Organization exists at boot (verify 34dba5a);
    • permission sets are granted by name (c8a9783, aa71c4d);
    • one configuration object holds one live kernel (objectstack#22301), so variants are built again rather than copied;
    • createIdentityObjectsPlugin() replaces test/helpers/identity-objects.ts (objectstack#22074);
    • a detail write must name a readable master-detail header (objectstack#22519);
    • hooks.run is generic over the operation (a6f3a13);
    • bootStack mounts the provider of every capability requires names (97610a5).
  7. The trigger door refuses a self-triggered runAs: 'system' flow to every caller but the system principal, an admin included (ca135dc / objectstack#22310, c572fe2 / objectstack#22407). Sweeps now start through runScheduledFlow, a thin helper that hands the automation service the context a schedule tick builds. It retires when the kit ships a system-principal flow start. An agent's direct start of case_escalation_stamp is pinned as the 403 it now is (29dd01c).

  8. driver-memory refuses every tenant-scoped call. Handled under Ruling 2 (29dd01c).

  9. Behaviour moves now pinned in tests (29dd01c):

    • deleting an account takes its settled contracts with it: draft, in-approval, expired and terminated;
    • the seed data lands in the Default Organization;
    • a position assigned through the data door carries the assigning session's organization.
  10. Retired spellings are refused at the authoring door. The normalised-name premise now measures that refusal, and the {TODAY()} filter census became a date-envelope census (bf2f548).

Per-site exceptions (flows)

Every site keeps its 17.7.0 observable behaviour, except these two:

  • close_case · close (update_record) · resolved_by_article.
    • Before: an omitted answer left the stored link alone.
    • After: has(vars.resolved_by_article) ? resolved_by_article : null writes NULL, so an agent who leaves the field unanswered clears a link set earlier on the record form.
    • An empty answer already landed as NULL through case_resolution_article_normalize. CEL refuses an unbound variable, so no spelling keeps the old behaviour.
  • Billing hand-off payloads · the account block.
    • On a sparse row, a missing optional column was dropped from the JSON on 17.7.0 and is now sent as null.
    • On SQL the column is always present, and the two versions send the same bytes.

Ratchet self-test lines changed, and why

All of these changes were needed because every committed ceiling is now RULED:

  • test/source-token-ratchet.test.ts gains anchoredSandbox(). It re-declares the sandbox gate's rows ANCHORED at the largest multiple of 1,000 at or under each grant, the technique the RULED branch used while no committed row was ruled.
  • Advisory cases run on that sandbox, and nothing else about them changes: nags to re-anchor only when headroom is over twice the ruled buffer, offers a re-anchor only when anchor() would commit a LOWER number, and the ANCHORED control of exempts a RULED ceiling from that nag. The control's row.kind check now reads the sandbox's own --json kind.
  • New case. exempts every committed RULED ceiling, against the real table checks every committed ruled row: it prints the ruled line, never the re-anchor instruction, and is non-vacuous.
  • Non-vacuity. declared('anchored').length > 0 becomes "whichever half is measured, both parsers proven live on a sample row". rows().length > 0 becomes "every committed ANCHORED ceiling has its worked row, plus a sample row's anchor() arithmetic".
  • Ruled rows name their ruling. states a ruled ceiling as ruled now requires each ruled row to name its ruling (#N ruling) and the header to carry that ruling's record.
  • quotes the ruling asserts both chore(deps): upgrade @objectstack/* past 17.7.0 once the next release publishes — it carries the platform fixes #2016, #2000, #1583 and #1585 wait on #2031 quotes.
  • README band table. In test/docs-readme-token-figures.test.ts, the docstring rows state the new ceiling column (88,500 / 46,500). The README banners are unchanged: they follow the measured readings (~57k / ~29k).

Ruling 2: retired legs, one line each

  • test/unassigned-case-triage-reach.test.ts · the driver-memory (sparse rows — the key is ABSENT) matrix, and the memory half of the two drivers really do store different shapes. They asserted the triage rule's reach, its share rows and the claim seam on cases whose owner_id key is absent. The SQL matrix keeps every assertion, and the shape case keeps its SQL half.
  • test/sla-at-risk-live-work.test.ts · a resolved case is not at risk — over memory. It asserted the SLA-at-risk view predicate on sparse rows. The SQL block keeps the same assertions.
  • test/flow-escalation-ownerless-case.test.ts · owner column never written (sparse datasource), for case_escalation and case_escalation_on_create, four cases each. They asserted that a run on an absent owner_id completes, still escalates, addresses nobody and records the named gate.
  • test/flow-sla-ownerless-case.test.ts · sparse datasource (absent owner). It asserted that the SLA sweep survives an absent owner_id, flags every case and alerts nobody for it.
  • test/freeze-guard-reference-cleanup.test.ts · does not yield when the link was already empty, for opportunity_lifecycle, quote_workflow and lead_automation. It asserted that the freeze guard refuses a null over a link the row was never written with, which is the absent-key shape. The SQL twin stays, renamed a null over a link that was already empty is no change at all, and lands as one.

Legs moved to SQL with their assertions kept:

  • do-not-call-enforcement (the whole suite; the "key is ABSENT" case is now "never written");
  • the refusal matrices of forecast-period-boundary and forecast-period-end-boundary;
  • the campaign_lead_conversion_refresh desk in hooks-runtime-service;
  • is a no-op when there is no rep pool in hooks-runtime.

hotcrmMemoryStack was removed from test/helpers/verify-stack.ts. The static has() guard in test/object-validation-predicates.test.ts (#630) is driver-independent and untouched.

Other

  • Pin claims re-scoped (AGENTS.md step 5):
    • What's new (3 locales) and docs/STATUS.md name 18.0.0-next.0;
    • two code comments that called 17.7.0 / 17.4.0 "the pin" now date themselves;
    • "measured on 17.x" claims stay as written.
  • Changeset: .changeset/2031-objectstack-18-next-0.md.

Upstream rows (report 6142372109)

Card What it fixed Merged PRs
objectstack#22310 runtime: any signed-in member can run a runAs: 'system' flow through POST /automation/:name/trigger — the door checks only anonymity, … objectstack#22404, objectstack#22424
objectstack#22407 [finding] runtime(security): two other doors that start a flow by name still let any signed-in member start a self-triggered system flow, … objectstack#22404, objectstack#22424
objectstack#22178 analytics: a picklist-bound select dimension serves English category labels in every locale — translateSelectOptions builds its synthetic … objectstack#22213
objectstack#22212 lint: five os lint --strict blind spots measured against firing controls — aliased ctx.api in hook handlers, action-body free … objectstack#22281, objectstack#23146
objectstack#22238 cli: os lint translation coverage reads only a multi-package artifacts top level — every i18n/missing-* goes silent once an app ships … objectstack#22285
objectstack#22074 test kits: since 17.7.0 refuses unregistered object names, every reduced kernel without plugin-auth fails on sys_user, and … objectstack#22173
objectstack#22149 spec: defineSeed accepts a misspelled record key at compile time and os validate passes it, although its JSDoc promises "typos in … objectstack#22294
objectstack#22189 cli: os validate / os build capability preflight reads only the artifact top-level requires — in a multi-package artifact requires … hotcrm#2011, objectstack#22285
objectstack#22067 plugin-security: the first sign-up on a freshly seeded database blocks ~45 s while claimSeedOwnership re-owns every seed row through the … objectstack#22069, objectstack#22106
objectstack#22071 cli: app branding assets are served only from cwd/assets (or OS_RUNTIME_ASSETS_DIR) and silently skipped when that directory is absent … objectstack#22089, objectstack#22097
objectstack#22072 migrate meta: the stack-derived relevance proof that objectstack#20620's ruling names as the only exit for a notice is unbuilt — 17.7.0 … objectstack#22108, objectstack#22115
objectstack#22073 boot output: the "schedule trigger is NOT bound" sentence (~600 chars) prints twice for every scheduled flow — 16 of an 8-flow app's boot … objectstack#22089, objectstack#22097
objectstack#22079 public forms: sharing.publicLink reads as the "Generated public share URL" but is only a slug — the authored path (/forms/contact-us) … objectstack#22098, objectstack#22159
objectstack#22824 approvals: an empty slate under onEmptyApprovers: 'admin_rescue' opens on the tenant's administrators as named approvers, not as a … objectstack#22850
objectstack#22455 security(attachments): the attach / delete gate asks plugin-sharing's canEdit, which reads every controlled_by_parent object as public — a … hotcrm#2036, hotcrm#2043, objectstack#22492, objectstack#22513, objectstack#22523, objectstack#22535
objectstack#22437 rest(public forms): POST /forms/:slug/submit answers the anonymous submitter with the whole stored record, so any field a hook derives … hotcrm#2035, objectstack#22447, objectstack#22462
objectstack#22607 plugin-approvals (17.7.0): opening an approval step notifies none of its approvers — no inbox message, no email; the docs also still say no … objectstack#22625

Testing

Every link of pnpm verify ran under os-verify-lock.sh at HEAD 1407ca7, on a clean tree:

Link Exit Result
pnpm validate 0 ✓ Validation passed, 18 Objects, 357 Fields
pnpm typecheck 0 tsc --noEmit, no output
pnpm lint 0 18 suggestion(s)
pnpm lint:i18n-gate 0 ✓ i18n lint gate: 0 i18n/missing-* issues
pnpm hygiene 0 ✓ source hygiene clean
pnpm hygiene:tokens 0 ✓ source token ratchet clean, every row ruled
pnpm build 0 ✓ Build complete, 8 author-time warnings (see the notes below)
pnpm test 1 Test Files 1 failed / 178 passed (179), Tests 1 failed / 3658 passed / 1 skipped
  • The one pnpm test failure: script-bodies send_email writes the email and its timeline pointer saw a handed-over status of failed, under a 12-minute shared-box run. Re-run alone at the same HEAD it passed: Tests 57 passed (57), exit 0. It is reported as a flake and was not changed.
  • For the seat's fix(service): the SLA Violations tile counts open breaches at read time #2090 note: test/case-sla-breach-on-close.test.ts re-run alone at 1407ca7 gives Tests 21 passed (21), exit 0. The sweep's query_breached reads the query placeholder { $lt: '{now}' }, which 18.0.0-next.0 admits. The §4 pin compares the tile's second branch to the sweep's whole selection on a fresh boot.

Browser pass (Chromium at /opt/pw-browsers/chromium, after pnpm demo:reset and pnpm dev on port 4001, signed in as the demo admin):

  • the console boots into HotCRM;
  • the Executive Overview dashboard draws: a DOM probe counts 4 chart SVGs and 19 chart marks;
  • the Acme Corporation account record page renders;
  • Convert Lead on a fresh qualified lead opens Conversion Details, and Submit converts it: REST reads is_converted: true, status: converted, with an account and a contact linked;
  • the page logged no errors.

Ratchet readings, origin/main → this branch (tokens):

Package Business semantics Interaction layer Authored total
src/sales 57,449 → 57,878 (ceiling 59,000 → 88,500) 28,377 → 28,377 (31,000 → 46,500) 102,183 → 102,599 (107,000 → 160,500)
src/service 12,989 → 13,037 (14,000 → 21,000) 5,602 → 5,602 (7,000 → 10,500) 21,897 → 21,948 (22,000 → 33,000)
src/revenue 15,799 → 15,745 (16,000 → 24,000) 2,165 → 2,165 (3,000 → 4,500) 18,939 → 18,885 (19,000 → 28,500)
src/marketing 7,994 → 8,027 (9,000 → 13,500) 886 → 886 (1,000 → 1,500) 9,100 → 9,132 (10,000 → 15,000)

Screenshots

The browser steps above were captured in the session that ran this upgrade. They are not committed.

Risk

The next v* tag needs a v18 runtime, and that runtime is a prerelease.

  • The protocol handshake compares majors, so a 17.x host refuses this artifact at load, before it reads a single object. Merging this PR moves HotCRM's releasable line onto 18.0.0-next.0.
  • Scheduled flows still run only when the deployment sets OS_AUTOMATION_SCHEDULED_WORK_ENABLED=true.
  • A 17.x install's position rows carry no organization. Under 18 they do not staff the case-escalation pool or an approval step, because every case and request now carries the Default Organization. Such holders need assigning again.

Rollback: revert this PR. The 17.7.0 pins, lockfile and flows come back together, and no data migration ran.

Checklist

  • I have added a changeset (.changeset/2031-objectstack-18-next-0.md)
  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings: pnpm build prints 8 author-time warnings (see the notes below)
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes: 3658 / 3659 pass in the full run, and the one failure passed when re-run alone
  • Any dependent changes have been merged and published (@objectstack/* 18.0.0-next.0 is on npm)

Acceptance notes

  • Build warnings. pnpm build prints 8 author-time warnings. Seven name approval nodes whose every approver is a group: if none is staffed, the request opens on the organization's administrators (objectstack#22824). The eighth notes that hierarchy-security is an enterprise capability. None of them is a refusal.

  • What the pin-claim sweep covers. The sweep found claims that carry a version token. A stale claim stated in other words (for example "on the pinned engine" in campaign.object.ts) is found only by reading, and was not re-measured here.

  • test/fixtures/composed-artifact.json still states "protocol": "^17.7.0". No assertion and no script reads that key, so it was left as it is.

  • Memory-driver suites outside the ruling. test/undeclared-key-probe.test.ts (its memory leg) and test/decorative-field-sweep.test.ts still boot bare kernels on driver-memory. They pass, because no organization exists in those kernels, so they are outside "now refused". Ruling 2's wording could reach them, so whether they go is for the seat to decide.

  • The kit gap behind runScheduledFlow. @objectstack/verify's flows.run takes a person only, while hooks.run takes { system: true }. The gap is raised upstream from chore(deps): upgrade @objectstack/* past 17.7.0 once the next release publishes — it carries the platform fixes #2016, #2000, #1583 and #1585 wait on #2031. The helper names it and retires with it.

  • Organization-less position holders. Two platform implementations disagree on them:

    • plugin-security's holdingTakesEffectIn applies them in every organization;
    • plugin-approvals' expandPositionUsers filters organization_id by strict equality.

    This is raised upstream from chore(deps): upgrade @objectstack/* past 17.7.0 once the next release publishes — it carries the platform fixes #2016, #2000, #1583 and #1585 wait on #2031. Here, the test fixtures stamp the organization, as the data door does.

  • Dev-server sweep. On the dev boot the scheduled flows bound (22 bound to triggers), but no hourly tick fell inside the observed window, so the sweep there is NOT MEASURED. The fresh-boot sweep is measured by the suites above.

Additional Notes

  • src/service/objects/_case-assignment.ts is unchanged. Widening the escalation pool to organization-less holders was tried and measured as unnecessary, once the fixtures carry the organization the way the platform's data door stamps it. The app's rule stays: a case escalates inside its own organization.
  • The source-token readings move by ≤ +429 per layer. The flow rewrite is close to token-neutral.

Generated by Claude Code

claude added 11 commits October 13, 2026 08:54
All 24 @objectstack/* pins (dependencies and devDependencies) move
17.7.0 -> 18.0.0-next.0 together, exact. The lockfile was regenerated
by pnpm install, not edited by hand.

Claude-Session: https://claude.ai/code/session_01BCDqr1DqnQjEh5jA9gGkUG

Co-authored-by: Claude <noreply@anthropic.com>
- engines.protocol and specVersion move to ^18.0.0-next.0 (the app
  package, the service module and objectstack.manifest.json). The
  runtime handshake compares majors only (metadata-core
  checkProtocolCompat / rangeAdmitsMajor), so ^17.7.0 is refused by a
  protocol 18.0.0 runtime and ^18.0.0-next.0 is admitted.
- The email-template bundle no longer writes `category`, which
  @objectstack/spec 18.0.0-next.0 retires and refuses at parse
  (ec7f7f8, email-template-category-is-system-removed).
- test/hooks-runtime.test.ts: repWrites is generic over the operation,
  as @objectstack/verify's hooks.run now is (a6f3a13).

The flow value-slot and text-slot migration this release also requires
is not in this commit.

Claude-Session: https://claude.ai/code/session_01BCDqr1DqnQjEh5jA9gGkUG

Co-authored-by: Claude <noreply@anthropic.com>
…t.0 dialects

18.0.0-next.0 refuses the single-brace `{…}` template dialect in flow value
slots and text slots (the spec's semantic migration entries
`flow-value-slot-template-dialect-refused`, `flow-text-slot-single-brace-refused`
and `flow-text-slot-unbound-dollar-root-refused`; no D2 conversion exists).
`os validate` refused 340 sites in 32 flows; it now reports none.

- value slots (create/update fields, assignments, subflow inputs, notify
  recipients and templateData, screen defaults, loop collections, CRUD filter
  operands) are CEL value envelopes (`cel` from `@objectstack/spec`);
- the date macros are their CEL string forms (`isoDate(today())`,
  `isoDate(daysFromNow(n))`, `isoDate(daysAgo(n))`, `isoDatetime(now())`,
  `isoDate(addDays(today(), days))`), and `$User.Id` is `current_user.id`;
- text with holes in a value slot is one CEL concatenation;
- text slots (notify `actionUrl`, a screen `description`) take `{{ }}` holes;
- both billing hand-off bodies are ONE envelope building the whole payload
  (`billingBody`, `_billing-endpoint.ts`), since a nested token is refused
  and a nested envelope would be sent as the object it spells.

Guards follow the entries' rule per site: a key that may be absent (an
optional column of a fetched row, an optional screen answer, `owner_id`) is
guarded to null; `.id`, required and autonumber fields, variables every path
binds, and the record-change trigger's `record` (materialised over every
declared field) are read bare. An insert reads an explicit null as it read
the omitted key (the engine applies the field default to both).

Claude-Session: https://claude.ai/code/session_01BCDqr1DqnQjEh5jA9gGkUG

Co-authored-by: Claude <noreply@anthropic.com>
18.0.0-next.0 keys three more flow surfaces in the `flows` translation group,
read by the engine in the run's locale, and `os lint` reported 96
`i18n/missing-flow` keys (32 keys x zh-CN / es-ES / ja-JP):

- select options on the campaign_enrollment, claim_case and
  schedule_followup screens, worded exactly as each pack already words the
  object field's own option (crm_lead.status, crm_contact.department,
  crm_case.status, crm_task.type, crm_task.priority);
- the three lead_conversion refusal descriptions (plus the third refusal's
  title, worded as the other two), and screen_1's description, which can only
  carry the `{{duplicateWarning}}` hole: the warning's text is a variable;
- quote_generation's `refuse_held` end-node message.

17.7.0 refused these as keys (`FLOW_SCREEN_FIELD_NO_OPTIONS`); the comments
that said so, and the flows' "English-only" notes, are re-worded. The locale
packs are outside the token ratchet by ruling.

Claude-Session: https://claude.ai/code/session_01BCDqr1DqnQjEh5jA9gGkUG

Co-authored-by: Claude <noreply@anthropic.com>
- `signUpPerson` grants a permission set by NAME
  (`sys_user_permission_set.permission_set`): upstream c8a9783 (#15204 stage
  6b-2) stopped seeding a `sys_permission_set` row for a declared set, so the
  old row-id lookup found nothing ("no permission set named sales_rep").
  The name is still checked against the sets the booted registry serves.
- `test/helpers/identity-objects.ts` is retired: the four reduced kernels
  mount plugin-auth's own `createIdentityObjectsPlugin()` (objectstack#22074).
- Those kernels write the install's one organization: a system write on a
  tenant-scoped object now derives its organization from it and is refused
  without one (ADR-0131 D3/D9, `SystemWriteOrganizationRequiredError`), and
  plugin-auth, which creates it at boot, is not mounted there.
- `line-item-line-number` files its lines under real header rows: a
  non-system write of a master-detail child must name a header the caller can
  read (objectstack#22519).
- Probe flows and metadata pins take the CEL value envelope instead of the
  refused `{…}` template; the scheduled-sweep organization analyser reads an
  envelope's source the way it read a hole, and its red fixture is re-spelled.
- The SLA sweep reads the query engine's own `{now}` placeholder, the
  spelling the SLA Violations tile reads.

Claude-Session: https://claude.ai/code/session_01BCDqr1DqnQjEh5jA9gGkUG

Co-authored-by: Claude <noreply@anthropic.com>
- A second kernel over one configuration is refused (verify #22301, one live
  kernel per config object), so a leg that boots a variant builds a fresh
  artifact (`freshArtifact()` / `buildArtifactAgain()`) instead of copying
  the first one.
- driver-memory refuses every tenant-scoped call
  (`MemoryMultiTenantUnsupportedError`, verify 34dba5a, until ADR-0131 C8):
  the legs that only needed SOME driver move to the SQL in-memory datasource,
  and the three reduced kernels move to sqlite-wasm `:memory:`.
- `signUpPerson` stamps the session organization on a person's position row.
- Pins re-read under the 18 spellings: the lead-copy envelope, CEL
  recipients, the `{now}` SLA sweep compared to the tile's whole selection,
  the date-envelope census replacing the `{TODAY()}` token census, the
  retired `manage_org_presentation` capability, the account-delete cascade
  now that the hook defect is fixed upstream, and the normalised-name match
  under `lowerAscii().trim()` (no `replace()` in the CEL environment).

Claude-Session: https://claude.ai/code/session_01BCDqr1DqnQjEh5jA9gGkUG

Co-authored-by: Claude <noreply@anthropic.com>
- `tenant_admin` no longer lists `manage_org_presentation`: 18.0.0-next.0
  retires the per-organization overlay axis and the capability with it
  (ADR-0131 D6, the spec's `manage-org-presentation-retired` entry, whose
  remedy is to delete it from every set).
- The platform-version claims that named 17.7.0 as the current pin now date
  themselves (AGENTS.md step 5); measured-on statements stay as they were.

Claude-Session: https://claude.ai/code/session_01BCDqr1DqnQjEh5jA9gGkUG

Co-authored-by: Claude <noreply@anthropic.com>
…ruling

The maintainer's ruling, verbatim: 「hotcrm token 上限提升 50%」 /
「暂时只优先处理升级相关的任务」 (#2031, comment
6143114246). Each of the twelve committed ceilings is multiplied by exactly
1.5. None of the products is anchor() of a reading, so all twelve declare
CEILING_KIND.RULED, the kind the gate's header prescribes for a grant; the
header carries the ruling as their record and twelve RULED rows. The
anchored rows and the declined re-anchorings they replace stay as history,
marked `superseded (#2031)`. No other change to the gate's logic.

Self-test adaptations, only where a case assumed every committed ceiling is
ANCHORED:
- the three advisory cases run against a sandbox gate whose rows are
  re-declared ANCHORED (`anchoredSandbox()`), the technique the RULED branch
  used while no committed row was ruled;
- a new case asserts every committed RULED ceiling prints the ruled line and
  never the re-anchor instruction (#1607);
- the kinds and anchor(reading) cases prove their parsers live on a sample
  row now that the anchored half is empty, as the ruled half was before;
- each ruled row must name its ruling, and the header must carry its record;
- the README band docstring states the new ceiling column.

Claude-Session: https://claude.ai/code/session_01BCDqr1DqnQjEh5jA9gGkUG

Co-authored-by: Claude <noreply@anthropic.com>
…ver legs to SQL

- Sweeps: since 18.0.0-next.0 the trigger door refuses a self-triggered
  `runAs: 'system'` flow to every caller but the system principal, an
  admin's session included (objectstack ca135dc, #22310), and
  @objectstack/verify's flows.run takes a person only. `runScheduledFlow`
  hands the booted automation service the context a schedule tick builds and
  rejects on an unsuccessful run, as the door did. It retires when the kit
  ships a system-principal flow start.
- case_escalation_stamp: an agent's direct start is now pinned as the 403
  refusal it is; the stamp is measured through escalate_case's subflow.
- The normalised-name probe flows run as the user (they measure the value
  slot, not the elevation).
- Draft contracts go with their account on 18 (the cascade
  hooks-runtime-sales already pins), so the cascade-guard draft case pins it.
- The tenant-scope contact fixture leaves the seed's own address: the seed
  data now lands in the Default Organization (ADR-0131 D3).
- The without-approvals composition drops `approvals` from `requires`:
  bootStack now mounts every provider `requires` names (verify 97610a5).
- The ownerless-assignment pool carries the install's organization, as an
  assignment through the data door does on 18 (measured).
- driver-memory refuses every tenant-scoped call on 18. By the maintainer's
  ruling on #2031 (「就应该在 sql 上测试,作废内存驱动的测试」, comment
  6144864473) every refused leg runs on SQL, and the legs whose only subject
  was the absent-key shape are deleted; hotcrmMemoryStack goes with them.

Claude-Session: https://claude.ai/code/session_01BCDqr1DqnQjEh5jA9gGkUG

Co-authored-by: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 13, 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 Oct 13, 2026 12:22pm 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 backend Server-side behaviour — hooks, flows, actions labels Oct 13, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)

repo:hotcrm 席位,2026-10-13T12:27Z。复核记录在 #2031(本条之后的 ACCEPT 评论)。本 PR 改动 3,759 行(含锁文件),超过 3,000 行,按规矩由你合并或批准。

改了什么

  • HotCRM 依赖的 24 个 @objectstack/* 包从 17.7.0 升到 18.0.0-next.0(v18 预发布版)。
  • 32 个流程里 340 处旧的 {…} 写法改成 v18 要求的新写法。
  • 补了 32 个流程翻译键 × 3 种语言。
  • 按你的裁决,12 个 token 上限全部 ×1.5,记为「维护者授予」。
  • 按你的裁决,测试改在 SQL 上跑,删除了只能在内存驱动上测「缺键」的用例。
  • tenant_admin 去掉一个 v18 已退役的权限。

为什么改

风险与代价(含回滚)

  1. 只能跑在 v18 上: 合并后,下次打 hotcrm 版本,发出去的应用需要 v18(预发布版)运行时;还在 17.x 的环境会在启动握手时直接拒绝它。
  2. 老安装的岗位要重新分配: 17.x 老安装里的岗位记录没有组织。升到 18 后,这些人不再被分到工单升级池和审批步骤,需要重新分配岗位。
  3. 排程开关: 排程类流程(如 SLA 检查)仍需要环境开启 OS_AUTOMATION_SCHEDULED_WORK_ENABLED=true。
  4. 预发布会再变: 18 是预发布版,之后的 next.1、next.2 可能再引入破坏性变更。
  5. 一处行为差异: 只有在 API 调用不传「知识文章」参数时,关闭工单会把已关联的文章清空;从界面关闭的行为不变。

回滚: revert 本 PR 即可。17.7.0 的依赖、锁文件和流程一起恢复,没有跑任何数据迁移。

席位意见

  • 复核通过(ACCEPT)。
  • 本地完整校验全绿,只有 1 个测试在机器高负载时偶发失败,单独重跑通过。
  • 浏览器实测:应用启动、客户记录页、线索转换、管理层仪表盘都正常。
  • CI 除 Build and Test 还在跑,其余全绿。
  • 一处需要知道的取舍:上限标成「维护者授予」后,不会再随用量减少自动收紧,往下调也需要你的裁决。

你要做的(一个动作)
CI 全绿后,用 os-zhuang 或 hotlong 在 PR #2091 上批准,或者直接合并。你批准后,席位走合并队列落地。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review October 13, 2026 12:44
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 13, 2026
Merged via the queue into main with commit 2f3d898 Oct 13, 2026
12 checks passed
@objectstack-fleet objectstack-fleet Bot removed the needs-user-decision Needs the maintainer's call before work proceeds label Oct 13, 2026
This was referenced Oct 13, 2026
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 dependencies Dependency bumps and lockfile changes documentation Improvements or additions to documentation metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore(deps): upgrade @objectstack/* past 17.7.0 once the next release publishes — it carries the platform fixes #2016, #2000, #1583 and #1585 wait on

3 participants