From 8683ee92157c56f6e8dfcd883767c482ef4d181e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 00:18:44 +0000 Subject: [PATCH] chore: version packages --- .../25921-auth-zero-consumer-exports.md | 21 - .../25921-core-zero-consumer-exports.md | 41 - .../25921-fields-zero-consumer-exports.md | 20 - ...21-plugin-chatbot-zero-consumer-exports.md | 19 - ...-plugin-dashboard-zero-consumer-exports.md | 18 - ...921-plugin-detail-zero-consumer-exports.md | 25 - ...25921-plugin-list-zero-consumer-exports.md | 18 - .changeset/25921-providers-upload-fold.md | 19 - .../25921-react-zero-consumer-exports.md | 32 - .changeset/25921-upload-adapter-contract.md | 10 - .changeset/27193-recompute-home-amounts.md | 13 - ...5-record-currency-door-names-home-field.md | 12 - .changeset/29041-push-notification-channel.md | 14 - .changeset/29042-native-push-registration.md | 14 - .../29255-installer-client-source-skill.md | 10 - .../29395-skills-search-auto-default.md | 12 - ...11-platform-skill-package-contributions.md | 10 - .../29418-record-node-operand-value-docs.md | 11 - .changeset/29425-declared-record-input.md | 23 - .../29461-d11-added-locale-keeps-write.md | 11 - .../29506-title-format-refused-teaching.md | 11 - .../29512-approval-decision-outputs-skill.md | 11 - ...approver-directory-refusal-queue-paging.md | 20 - ...29604-queue-service-refuses-failed-read.md | 19 - .changeset/29625-stdio-credential-env.md | 10 - .../29653-migrate-blank-multi-values.md | 14 - .../29801-related-list-placement-skill.md | 12 - .../29818-push-device-locale-descriptions.md | 12 - .changeset/29820-push-capability-serve.md | 32 - .changeset/29851-contributed-nav-labels.md | 13 - ...port-carries-manifest-object-extensions.md | 20 - .../29873-install-prints-withheld-maps.md | 12 - ...asource-update-kept-pool-stores-nothing.md | 18 - .../29880-approvals-list-unknown-status.md | 18 - .../29891-email-send-display-name-400.md | 12 - .changeset/29894-batch-row-hook-aborted.md | 12 - .changeset/29895-user-locale-case-fold.md | 10 - .../29896-field-sortable-false-honoured.md | 22 - .../29897-properties-panel-readonly-reason.md | 10 - .../29898-picklist-binding-unknown-refused.md | 23 - .../29899-publish-sheet-picklist-options.md | 12 - ...900-report-runtime-filter-field-unknown.md | 21 - .changeset/29901-save-status-held.md | 13 - .changeset/29902-publish-sheet-hook-count.md | 12 - ...9903-base-export-without-pair-extension.md | 19 - .changeset/29904-type-mismatch-fail-closed.md | 24 - ...voke-sessions-on-password-reset-retired.md | 19 - .../29910-no-submitter-returned-docs.md | 10 - .../29916-connection-error-no-invented-503.md | 12 - ...29918-lifecycle-first-sweep-fresh-store.md | 13 - .../29919-dispatch-loop-next-due-wake.md | 17 - .../29926-type-mismatch-outcome-per-role.md | 26 - .../29934-dev-start-one-data-key-file.md | 12 - .../29935-update-action-refreshes-record.md | 12 - .../29936-dev-door-single-metadata-plugin.md | 10 - .changeset/29937-empty-stub-protocol-range.md | 10 - .../29942-related-list-title-translation.md | 17 - ...9943-body-table-totals-additive-columns.md | 14 - .../29944-body-table-empty-keeps-header.md | 12 - .changeset/29945-body-table-overflow-cues.md | 20 - ...946-record-page-user-field-related-list.md | 11 - ...29951-page-header-placed-actions-reader.md | 18 - .../29951-skills-placed-actions-drawn.md | 11 - .../29954-disabled-reason-tooltip-width.md | 12 - ...flow-condition-type-mismatch-every-site.md | 26 - .changeset/29960-push-vendor-docblocks.md | 10 - .../29961-stored-expression-backfill.md | 15 - ...9962-activity-record-name-reader-locale.md | 18 - .changeset/29969-platform-push-eval.md | 10 - .changeset/29971-sign-in-mobile-handoff.md | 15 - .../29972-serve-trusted-app-schemes-env.md | 14 - .changeset/29978-import-row-hook-aborted.md | 12 - .changeset/29983-api-trigger-signature.md | 21 - ...29988-studio-interfaces-nav-rail-module.md | 10 - .../29988-studio-nav-item-inspector-module.md | 10 - .../29988-studio-package-switcher-module.md | 10 - .../29988-studio-pillar-helpers-modules.md | 10 - .changeset/29990-i18n-related-list-title.md | 12 - .changeset/29997-handoff-signin-entry.md | 19 - ...packageless-overlay-move-heals-registry.md | 11 - .changeset/30001-platform-locale-table.md | 12 - ...30005-cross-closure-dataset-filter-keys.md | 22 - ...06-dataset-filter-key-dimension-measure.md | 16 - .../30009-api-trigger-declared-signature.md | 14 - ...30011-environment-scoped-rest-providers.md | 14 - .../30016-flow-date-arithmetic-run-fails.md | 12 - ...n-members-confirm-beside-params-refused.md | 22 - .../30018-page-header-placed-actions.md | 16 - .../30023-stored-sort-sortable-false-lint.md | 22 - .../30024-list-refused-sort-not-sent.md | 14 - ...30025-flow-bare-name-screen-open-binder.md | 13 - .../30026-time-relative-sweep-typing.md | 22 - .../30031-page-builder-header-breadcrumb.md | 10 - .changeset/30033-related-table-name-column.md | 10 - .../30034-personal-view-url-default-state.md | 11 - .../30035-record-feed-reread-after-save.md | 12 - .../30036-activity-feed-default-view.md | 11 - .changeset/30037-sort-picker-hidden-fields.md | 14 - .changeset/30038-flow-closure-refusal-copy.md | 11 - .changeset/30038-trigger-object-enter.md | 13 - .changeset/30039-studio-zh-terms.md | 15 - .../30039-view-panel-zh-filter-prompt.md | 11 - ...cks-the-key-file-that-opens-its-secrets.md | 20 - .../30043-dock-sends-selected-app-and-page.md | 18 - .changeset/30046-studio-off-nav-list-views.md | 14 - .changeset/30046-studio-view-dropdown.md | 15 - .changeset/30050-console-locale-match.md | 12 - .changeset/30052-facade-scoped-classes.md | 12 - ...3-hand-series-private-docblocks-slice-2.md | 13 - .../30053-hand-series-private-docblocks.md | 12 - .../30053-liveness-search-fields-repoint.md | 11 - .changeset/30054-objectql-return-types.md | 10 - .../30054-protocol-door-return-types.md | 10 - .changeset/30055-erasure-pinned-bodies.md | 12 - .../30057-studio-form-designer-form-view.md | 16 - .../30059-studio-customize-app-navigation.md | 17 - .../30062-declared-override-label-order.md | 14 - ...lared-override-mapping-removal-remedies.md | 16 - .changeset/30063-studio-customize-page.md | 17 - .changeset/30064-studio-dataset-customize.md | 16 - .changeset/30065-declared-action-override.md | 18 - .changeset/30069-related-export-permission.md | 15 - .changeset/30070-customization-mark-words.md | 11 - ...ntime-package-nav-contributions-refused.md | 19 - .changeset/30073-personal-view-conditions.md | 15 - .changeset/30074-crm-track-history.md | 13 - .../30075-default-page-stage-advance.md | 17 - .../30076-forward-date-range-presets.md | 22 - .changeset/30077-list-add-columns.md | 19 - .changeset/30080-dashboard-tile-drill.md | 10 - .../30081-seed-mode-default-create-only.md | 25 - ...-customizations-disabled-package-fields.md | 10 - .../30088-studio-pair-object-actions.md | 17 - ...0089-studio-list-view-selection-actions.md | 16 - .changeset/30091-data-key-trial-page.md | 16 - .changeset/30093-page-sort-unknown-field.md | 20 - .../30094-page-record-liveness-regrade.md | 11 - .../30095-object-list-view-sort-door.md | 19 - ...0103-home-amount-rate-field-scale-floor.md | 21 - ...-install-custom-pair-into-writable-pair.md | 15 - .changeset/30107-reset-without-email.md | 13 - .../30108-member-default-feed-grants.md | 17 - ...30119-studio-customization-build-notice.md | 12 - ...9-studio-customization-environment-line.md | 11 - ...-notify-link-defaults-to-trigger-record.md | 13 - .../30122-timeline-one-row-per-action.md | 12 - .../30123-scaffold-security-datasets.md | 13 - .../30124-analytics-filter-array-refusal.md | 12 - .changeset/30125-blank-sample-note.md | 8 - .changeset/30125-invitation-sign-up-joins.md | 12 - .changeset/30125-sign-out-no-return-link.md | 12 - .../30126-console-copy-titles-toast-money.md | 16 - ...126-console-nav-icons-labels-axis-retry.md | 17 - .../30126-inbox-topic-mcp-refusal-history.md | 12 - .changeset/30134-client-next-week-year-end.md | 10 - .../30136-first-organization-default-name.md | 10 - .../30140-compare-to-forward-presets.md | 16 - .changeset/30157-handoff-verification-mail.md | 15 - .changeset/30164-zh-cn-dashboard-word.md | 10 - ...unshipped-locale-write-open-before-pair.md | 11 - ...declared-bulk-action-name-draws-nothing.md | 19 - ...177-draft-only-object-default-list-view.md | 10 - ...78-numeric-screen-fields-refuse-strings.md | 18 - ...creen-field-numeric-kinds-contract-text.md | 12 - .../30184-requiredwhen-transition-gate.md | 14 - ...92-environment-scoped-security-provider.md | 14 - .../30193-override-label-pair-precedence.md | 14 - .changeset/30197-compare-to-past-presets.md | 13 - ...-org-admin-update-refused-before-lookup.md | 12 - .../30199-suggested-binding-reconsider.md | 15 - .changeset/30201-org-mfa-needs-enrollment.md | 21 - .changeset/30202-keep-a-sign-in-method.md | 22 - .changeset/30203-allowed-ip-ranges-live.md | 20 - .../30204-feature-flags-namespace-removed.md | 22 - .changeset/30205-banned-user-work-handover.md | 26 - .changeset/30208-setup-admin-wording.md | 12 - .changeset/30209-audit-log-default-view.md | 15 - .../30209-setup-record-names-not-ids.md | 14 - ...10-business-unit-enterprise-requirement.md | 13 - ...organization-parent-sort-multi-org-help.md | 10 - .../30212-impersonation-landing-agent-sets.md | 12 - .../30212-sender-name-follows-product-name.md | 14 - ...30215-uninstall-forgets-engine-manifest.md | 12 - .../30217-install-ledger-follows-os-home.md | 15 - ...18-environment-scoped-objectql-provider.md | 13 - .../30220-requiredwhen-formpage-kanban.md | 16 - ...30223-hitl-continuation-not-user-bubble.md | 11 - .../30228-queue-reclaim-expired-lease.md | 15 - .changeset/30229-adr0146-override-types.md | 10 - .changeset/30235-recycle-bin-spec-surface.md | 14 - .../30238-studio-restore-rereads-marks.md | 11 - .../30244-served-labels-console-header.md | 11 - ...osed-date-and-sweep-run-fails-sentences.md | 17 - .../30251-ownership-queue-on-failed-read.md | 12 - .../30255-clone-inherits-held-grants.md | 13 - .changeset/30259-action-param-widget.md | 13 - .../30260-action-param-widget-reader.md | 19 - ...261-packageless-overlays-view-container.md | 14 - .../30267-view-panel-zh-composite-rows.md | 15 - .../30279-time-relative-entry-surface.md | 10 - .../30281-seed-locale-per-organization.md | 28 - .../30282-feature-flags-namespace-text.md | 14 - .../30283-time-relative-object-mismatch.md | 19 - .changeset/30285-i18n-walk-in-lint.md | 16 - .changeset/30285-studio-translation-table.md | 16 - ...286-default-language-follows-deployment.md | 18 - .changeset/30289-scenario-card-schema.md | 8 - .changeset/30291-scenario-tag-checks.md | 19 - ...30295-org-mfa-notice-and-enroll-refusal.md | 15 - ...299-sign-in-config-fresh-after-sign-out.md | 10 - .../30300-keep-the-last-identity-provider.md | 19 - .../30302-mfa-enforcement-follow-ups.md | 19 - .changeset/30303-fls-key-extension-fields.md | 12 - ...abled-package-extension-fields-withheld.md | 19 - .changeset/30311-served-app-labels.md | 11 - .changeset/30311-served-object-view-labels.md | 11 - ...312-auth-settings-clear-applies-default.md | 17 - .../30313-allowed-ip-ranges-client-address.md | 11 - .../30317-draft-only-object-view-seed.md | 10 - .../30319-copy-doors-inherited-grants.md | 12 - .changeset/30320-ipv6-cidr-allowed-ranges.md | 15 - .changeset/30323-install-note-sample-data.md | 15 - .changeset/30323-sample-data-count.md | 14 - ...6-install-answer-protocol-check-skipped.md | 12 - ...6-install-result-protocol-check-skipped.md | 13 - ...27-new-datasource-mode-and-placeholders.md | 15 - .../30328-datasource-sync-next-steps.md | 14 - .changeset/30329-deployment-setting-reset.md | 15 - .../30330-marketplace-readme-fits-page.md | 18 - ...sqlite-filename-neither-memory-nor-path.md | 18 - .../30334-external-write-read-only-line.md | 19 - ...5-datasource-delete-names-bound-objects.md | 13 - .../30335-storage-test-message-locale.md | 10 - ...0337-team-page-identity-note-and-counts.md | 13 - .../30338-keep-sign-in-language-pick.md | 10 - .../30338-save-register-language-pick.md | 11 - .../30343-timeline-one-action-one-entry.md | 15 - .changeset/30344-changes-chip-audit-read.md | 13 - ...ec-seed-locale-and-queue-refuse-wording.md | 13 - .changeset/30348-seed-receipt-landed-ids.md | 16 - .../30352-register-saml-idp-entity-id.md | 11 - .../30358-allowed-visualization-chip-words.md | 10 - .changeset/30366-forgot-password-no-mail.md | 17 - ...xternal-read-only-effective-permissions.md | 15 - ...77-extension-rollup-recompute-on-enable.md | 12 - ...30383-field-path-rules-extension-fields.md | 11 - ...390-served-object-label-record-surfaces.md | 17 - ...391-extension-rename-drops-stale-plural.md | 20 - ...0394-non-grid-bulk-bar-declared-actions.md | 16 - .../30396-time-relative-condition-object.md | 12 - .changeset/30398-caller-address-read.md | 17 - .../30399-save-sign-in-language-pick.md | 11 - .../30400-sqlite-filename-names-a-database.md | 22 - ...407-test-connection-reads-health-answer.md | 10 - .../30410-kanban-external-read-only-line.md | 8 - .../30411-org-require-mfa-follow-ups.md | 13 - .../30418-settings-toggle-confirm-off.md | 15 - .../30420-dev-admin-chip-password-off.md | 10 - .../30421-team-member-related-list-columns.md | 12 - .../30429-seed-receipt-landed-on-throw.md | 17 - ...lueprint-widget-aggregate-calendar-date.md | 12 - .../30436-studio-saved-to-environment.md | 10 - .changeset/30440-managed-clamp-modify-all.md | 15 - .../30444-datasource-delete-bound-objects.md | 12 - .../30454-identity-join-titles-name-people.md | 18 - ...55-manifest-list-serves-sourced-options.md | 14 - ...efault-language-boot-window-one-matcher.md | 14 - .changeset/30464-install-response-bare-row.md | 19 - .../30467-ledger-adoption-staging-dir.md | 8 - ...0469-import-lands-in-datasource-package.md | 17 - .../30469-synced-object-field-labels.md | 10 - ...471-field-name-indexes-extension-fields.md | 29 - ...il-packaged-actions-served-object-label.md | 14 - .changeset/30494-fail-open-date-sentences.md | 14 - .../30500-settings-toggle-confirm-off.md | 10 - ...30504-contributed-nav-packaged-app-base.md | 10 - .changeset/30509-approval-notices-email.md | 14 - ...email-channel-log-transport-unavailable.md | 13 - .changeset/30512-plugin-audit-locales.md | 10 - .changeset/30512-plugin-webhooks-locales.md | 10 - .changeset/30512-service-mailbox-locales.md | 10 - .../30512-service-messaging-locales-a.md | 10 - .../30512-service-messaging-locales-b.md | 10 - .changeset/30512-service-realtime-locales.md | 10 - .changeset/30512-service-storage-locales.md | 10 - ...525-approvals-status-non-string-message.md | 10 - .../30526-dataset-measure-drill-scopes.md | 14 - .changeset/30535-non-grid-bulk-bar-gate.md | 13 - .changeset/30538-locale-table-read-aliases.md | 17 - ...42-action-override-doors-and-boot-order.md | 22 - .changeset/30554-in-app-doc-links-locale.md | 11 - .../30562-sql-never-drop-table-with-rows.md | 18 - ...30563-driver-catalog-requester-language.md | 10 - .../30565-organization-mfa-notice-once.md | 12 - .../30581-nav-contributions-reenable.md | 11 - .../30584-blueprint-field-default-value.md | 18 - .changeset/30607-seed-locale-liveness-note.md | 12 - ...emaining-field-indexes-extension-fields.md | 27 - ...xtension-redeclared-owner-field-refused.md | 21 - ...report-chart-table-drill-measure-scopes.md | 16 - .../30616-sample-data-count-after-reload.md | 15 - .../30618-script-subtag-read-aliases.md | 14 - .changeset/30619-bundle-locale-own-key.md | 10 - .../30623-moved-action-override-refusal.md | 12 - ...0626-migrate-withheld-extension-columns.md | 10 - ...bject-save-reports-refused-storage-sync.md | 12 - .../30646-assigned-work-injected-owner.md | 10 - ...nment-write-unaddressable-row-not-found.md | 17 - .changeset/banned-user-credentials-refused.md | 21 - .../banned-user-oauth-credentials-refused.md | 21 - .changeset/by-id-write-row-reread-rules.md | 15 - .../change-email-notice-every-sign-in.md | 11 - ...cli-connection-label-shared-url-grammar.md | 11 - .../cloud-3485-turso-remote-balanced-group.md | 15 - .changeset/cloud-3565-chatbot-tool-labels.md | 10 - .changeset/cloud-3565-record-write-card.md | 16 - ...cloud-3565-spec-record-write-tool-names.md | 12 - .changeset/connect-agent-work-assistants.md | 20 - .changeset/delegated-admin-unreadable-row.md | 15 - .../driver-options-declared-keys-only.md | 16 - .../list-queue-filter-provisioned-storage.md | 10 - .changeset/org-admin-removal-caller-read.md | 14 - .../org-admin-removal-stored-row-verdict.md | 14 - .changeset/password-reset-revocation-trail.md | 14 - .../per-record-action-missing-record-id.md | 10 - .changeset/phone-reset-text-after-response.md | 12 - .../record-timeline-folded-types-from-spec.md | 10 - .changeset/rollback-addresses-served-row.md | 8 - ...ning-g-docs-external-object-scope-skill.md | 11 - ...ng-g-external-object-organization-scope.md | 23 - ...ec-hardening-p3-write-refused-read-door.md | 16 - .../sec-hardening-r6-agent-ceiling-grant.md | 18 - ...hardening-s-external-scope-every-caller.md | 16 - .../sec-hardening-u-write-implies-read.md | 24 - .changeset/settings-action-confirm-text.md | 10 - .changeset/setup-admin-wording-zh-cn.md | 18 - .changeset/sign-up-mail-after-response.md | 12 - .../sys-file-uploader-is-its-creator.md | 18 - ...pecial-scheme-read-however-many-slashes.md | 32 - .changeset/visitor-verification-resend.md | 12 - content/docs/deployment/self-hosting.mdx | 8 +- content/docs/releases/index.mdx | 2 +- content/docs/upgrading.mdx | 2 +- docker/README.md | 8 +- examples/app-crm/CHANGELOG.md | 76 ++ examples/app-crm/package.json | 2 +- examples/app-multi-package/CHANGELOG.md | 65 ++ examples/app-multi-package/package.json | 2 +- examples/app-showcase/CHANGELOG.md | 103 +++ examples/app-showcase/package.json | 2 +- examples/app-todo/CHANGELOG.md | 103 +++ examples/app-todo/package.json | 2 +- examples/embed-objectql/CHANGELOG.md | 85 ++ examples/embed-objectql/package.json | 2 +- packages/adapters/hono/CHANGELOG.md | 22 + packages/adapters/hono/package.json | 2 +- packages/apps/account/CHANGELOG.md | 78 ++ packages/apps/account/package.json | 2 +- packages/apps/connector-docusign/CHANGELOG.md | 66 ++ packages/apps/connector-docusign/package.json | 2 +- packages/apps/connector-github/CHANGELOG.md | 66 ++ packages/apps/connector-github/package.json | 2 +- packages/apps/connector-stripe/CHANGELOG.md | 66 ++ packages/apps/connector-stripe/package.json | 2 +- packages/apps/setup/CHANGELOG.md | 78 ++ packages/apps/setup/package.json | 2 +- packages/apps/studio/CHANGELOG.md | 78 ++ packages/apps/studio/package.json | 2 +- packages/cli/CHANGELOG.md | 525 +++++++++++ packages/cli/package.json | 2 +- packages/client-react/CHANGELOG.md | 75 ++ packages/client-react/package.json | 2 +- packages/client/CHANGELOG.md | 92 ++ packages/client/package.json | 2 +- packages/cloud-connection/CHANGELOG.md | 256 ++++++ packages/cloud-connection/package.json | 2 +- .../connectors/connector-imap/CHANGELOG.md | 4 + .../connectors/connector-imap/package.json | 2 +- .../connectors/connector-mcp/CHANGELOG.md | 75 ++ .../connectors/connector-mcp/package.json | 2 +- .../connectors/connector-openapi/CHANGELOG.md | 75 ++ .../connectors/connector-openapi/package.json | 2 +- .../connectors/connector-rest/CHANGELOG.md | 75 ++ .../connectors/connector-rest/package.json | 2 +- .../connectors/connector-slack/CHANGELOG.md | 74 ++ .../connectors/connector-slack/package.json | 2 +- packages/console/CHANGELOG.md | 4 + packages/console/package.json | 2 +- packages/core/CHANGELOG.md | 232 +++++ packages/core/package.json | 2 +- packages/create-objectstack/CHANGELOG.md | 24 + packages/create-objectstack/package.json | 2 +- .../src/templates/blank/package.json | 18 +- packages/drivers/driver-memory/CHANGELOG.md | 75 ++ packages/drivers/driver-memory/package.json | 2 +- packages/drivers/driver-mongodb/CHANGELOG.md | 75 ++ packages/drivers/driver-mongodb/package.json | 2 +- packages/drivers/driver-odata/CHANGELOG.md | 75 ++ packages/drivers/driver-odata/package.json | 2 +- packages/drivers/driver-sql/CHANGELOG.md | 100 +++ packages/drivers/driver-sql/package.json | 2 +- .../drivers/driver-sqlite-wasm/CHANGELOG.md | 77 ++ .../drivers/driver-sqlite-wasm/package.json | 2 +- packages/drivers/driver-turso/CHANGELOG.md | 87 ++ packages/drivers/driver-turso/package.json | 2 +- packages/formula/CHANGELOG.md | 161 ++++ packages/formula/package.json | 2 +- packages/lint/CHANGELOG.md | 385 +++++++++ packages/lint/package.json | 2 +- packages/mcp/CHANGELOG.md | 212 +++++ packages/mcp/package.json | 2 +- packages/metadata-core/CHANGELOG.md | 72 ++ packages/metadata-core/package.json | 2 +- packages/metadata-fs/CHANGELOG.md | 7 + packages/metadata-fs/package.json | 2 +- packages/metadata-protocol/CHANGELOG.md | 367 ++++++++ packages/metadata-protocol/package.json | 2 +- packages/metadata/CHANGELOG.md | 78 ++ packages/metadata/package.json | 2 +- packages/objectql/CHANGELOG.md | 316 +++++++ packages/objectql/package.json | 2 +- packages/observability/CHANGELOG.md | 65 ++ packages/observability/package.json | 2 +- packages/platform-objects/CHANGELOG.md | 222 +++++ packages/platform-objects/package.json | 2 +- packages/plugins/embedder-openai/CHANGELOG.md | 65 ++ packages/plugins/embedder-openai/package.json | 2 +- .../plugins/knowledge-memory/CHANGELOG.md | 75 ++ .../plugins/knowledge-memory/package.json | 2 +- .../plugins/knowledge-ragflow/CHANGELOG.md | 75 ++ .../plugins/knowledge-ragflow/package.json | 2 +- packages/plugins/organizations/CHANGELOG.md | 121 +++ packages/plugins/organizations/package.json | 2 +- .../plugins/plugin-approvals/CHANGELOG.md | 149 ++++ .../plugins/plugin-approvals/package.json | 2 +- packages/plugins/plugin-audit/CHANGELOG.md | 129 +++ packages/plugins/plugin-audit/package.json | 2 +- packages/plugins/plugin-auth/CHANGELOG.md | 369 ++++++++ packages/plugins/plugin-auth/package.json | 2 +- packages/plugins/plugin-dev/CHANGELOG.md | 148 ++++ packages/plugins/plugin-dev/package.json | 2 +- packages/plugins/plugin-email/CHANGELOG.md | 116 +++ packages/plugins/plugin-email/package.json | 2 +- .../plugins/plugin-hono-server/CHANGELOG.md | 84 ++ .../plugins/plugin-hono-server/package.json | 2 +- .../plugins/plugin-pinyin-search/CHANGELOG.md | 93 ++ .../plugins/plugin-pinyin-search/package.json | 2 +- packages/plugins/plugin-security/CHANGELOG.md | 263 ++++++ packages/plugins/plugin-security/package.json | 2 +- packages/plugins/plugin-sharing/CHANGELOG.md | 149 ++++ packages/plugins/plugin-sharing/package.json | 2 +- packages/plugins/plugin-webhooks/CHANGELOG.md | 98 +++ packages/plugins/plugin-webhooks/package.json | 2 +- packages/qa/dogfood/CHANGELOG.md | 186 ++++ packages/qa/dogfood/package.json | 2 +- packages/qa/downstream-contract/CHANGELOG.md | 65 ++ packages/qa/downstream-contract/package.json | 2 +- packages/qa/http-conformance/CHANGELOG.md | 19 + packages/qa/http-conformance/package.json | 2 +- packages/qa/scenarios/CHANGELOG.md | 115 +++ packages/qa/scenarios/package.json | 2 +- packages/rest/CHANGELOG.md | 202 +++++ packages/rest/package.json | 2 +- packages/runtime/CHANGELOG.md | 350 ++++++++ packages/runtime/package.json | 2 +- packages/sdui-parser/CHANGELOG.md | 4 + packages/sdui-parser/package.json | 2 +- .../services/service-analytics/CHANGELOG.md | 91 ++ .../services/service-analytics/package.json | 2 +- .../services/service-automation/CHANGELOG.md | 161 ++++ .../services/service-automation/package.json | 2 +- packages/services/service-cache/CHANGELOG.md | 75 ++ packages/services/service-cache/package.json | 2 +- .../service-cluster-redis/CHANGELOG.md | 66 ++ .../service-cluster-redis/package.json | 2 +- .../services/service-cluster/CHANGELOG.md | 74 ++ .../services/service-cluster/package.json | 2 +- .../services/service-datasource/CHANGELOG.md | 171 ++++ .../services/service-datasource/package.json | 2 +- packages/services/service-i18n/CHANGELOG.md | 75 ++ packages/services/service-i18n/package.json | 2 +- packages/services/service-job/CHANGELOG.md | 85 ++ packages/services/service-job/package.json | 2 +- .../services/service-knowledge/CHANGELOG.md | 74 ++ .../services/service-knowledge/package.json | 2 +- .../services/service-mailbox/CHANGELOG.md | 93 ++ .../services/service-mailbox/package.json | 2 +- .../services/service-messaging/CHANGELOG.md | 149 ++++ .../services/service-messaging/package.json | 2 +- .../services/service-package/CHANGELOG.md | 76 ++ .../services/service-package/package.json | 2 +- packages/services/service-print/CHANGELOG.md | 79 ++ packages/services/service-print/package.json | 2 +- packages/services/service-push/CHANGELOG.md | 110 +++ packages/services/service-push/package.json | 2 +- packages/services/service-queue/CHANGELOG.md | 107 +++ packages/services/service-queue/package.json | 2 +- .../services/service-realtime/CHANGELOG.md | 91 ++ .../services/service-realtime/package.json | 2 +- .../services/service-settings/CHANGELOG.md | 227 +++++ .../services/service-settings/package.json | 2 +- packages/services/service-sms/CHANGELOG.md | 93 ++ packages/services/service-sms/package.json | 2 +- .../services/service-storage/CHANGELOG.md | 113 +++ .../services/service-storage/package.json | 2 +- packages/skills/CHANGELOG.md | 129 +++ packages/skills/package.json | 2 +- packages/spec/CHANGELOG.md | 712 +++++++++++++++ packages/spec/package.json | 2 +- packages/triggers/trigger-api/CHANGELOG.md | 82 ++ packages/triggers/trigger-api/package.json | 2 +- .../trigger-record-change/CHANGELOG.md | 74 ++ .../trigger-record-change/package.json | 2 +- .../triggers/trigger-schedule/CHANGELOG.md | 77 ++ .../triggers/trigger-schedule/package.json | 2 +- packages/types/CHANGELOG.md | 65 ++ packages/types/package.json | 2 +- packages/verify/CHANGELOG.md | 246 ++++++ packages/verify/package.json | 2 +- skills/objectstack-ai/SKILL.md | 2 +- skills/objectstack-api/SKILL.md | 2 +- skills/objectstack-automation/SKILL.md | 2 +- skills/objectstack-data/SKILL.md | 2 +- skills/objectstack-formula/SKILL.md | 2 +- skills/objectstack-i18n/SKILL.md | 2 +- skills/objectstack-platform/SKILL.md | 2 +- skills/objectstack-query/SKILL.md | 2 +- skills/objectstack-ui/SKILL.md | 2 +- skills/objectstack-upgrade/SKILL.md | 2 +- skills/objectui/SKILL.md | 2 +- ui/apps/console/CHANGELOG.md | 158 ++++ ui/apps/console/package.json | 2 +- ui/packages/app-shell/CHANGELOG.md | 818 ++++++++++++++++++ ui/packages/app-shell/package.json | 2 +- ui/packages/auth/CHANGELOG.md | 122 +++ ui/packages/auth/package.json | 2 +- ui/packages/cli/CHANGELOG.md | 75 ++ ui/packages/cli/package.json | 2 +- ui/packages/collaboration/CHANGELOG.md | 48 + ui/packages/collaboration/package.json | 2 +- ui/packages/components/CHANGELOG.md | 184 ++++ ui/packages/components/package.json | 2 +- ui/packages/core/CHANGELOG.md | 202 +++++ ui/packages/core/package.json | 2 +- ui/packages/create-plugin/CHANGELOG.md | 4 + ui/packages/create-plugin/package.json | 2 +- ui/packages/data-objectstack/CHANGELOG.md | 118 +++ ui/packages/data-objectstack/package.json | 2 +- ui/packages/fields/CHANGELOG.md | 187 ++++ ui/packages/fields/package.json | 2 +- ui/packages/i18n/CHANGELOG.md | 372 ++++++++ ui/packages/i18n/package.json | 2 +- ui/packages/layout/CHANGELOG.md | 92 ++ ui/packages/layout/package.json | 2 +- ui/packages/mobile/CHANGELOG.md | 16 + ui/packages/mobile/package.json | 2 +- ui/packages/permissions/CHANGELOG.md | 67 ++ ui/packages/permissions/package.json | 2 +- ui/packages/plugin-calendar/CHANGELOG.md | 85 ++ ui/packages/plugin-calendar/package.json | 2 +- ui/packages/plugin-charts/CHANGELOG.md | 79 ++ ui/packages/plugin-charts/package.json | 2 +- ui/packages/plugin-chatbot/CHANGELOG.md | 150 ++++ ui/packages/plugin-chatbot/package.json | 2 +- ui/packages/plugin-dashboard/CHANGELOG.md | 172 ++++ ui/packages/plugin-dashboard/package.json | 2 +- ui/packages/plugin-designer/CHANGELOG.md | 130 +++ ui/packages/plugin-designer/package.json | 2 +- ui/packages/plugin-detail/CHANGELOG.md | 287 ++++++ ui/packages/plugin-detail/package.json | 2 +- ui/packages/plugin-editor/CHANGELOG.md | 24 + ui/packages/plugin-editor/package.json | 2 +- ui/packages/plugin-form/CHANGELOG.md | 148 ++++ ui/packages/plugin-form/package.json | 2 +- ui/packages/plugin-gantt/CHANGELOG.md | 142 +++ ui/packages/plugin-gantt/package.json | 2 +- ui/packages/plugin-grid/CHANGELOG.md | 206 +++++ ui/packages/plugin-grid/package.json | 2 +- ui/packages/plugin-kanban/CHANGELOG.md | 93 ++ ui/packages/plugin-kanban/package.json | 2 +- ui/packages/plugin-list/CHANGELOG.md | 235 +++++ ui/packages/plugin-list/package.json | 2 +- ui/packages/plugin-map/CHANGELOG.md | 41 + ui/packages/plugin-map/package.json | 2 +- ui/packages/plugin-markdown/CHANGELOG.md | 24 + ui/packages/plugin-markdown/package.json | 2 +- ui/packages/plugin-report/CHANGELOG.md | 78 ++ ui/packages/plugin-report/package.json | 2 +- ui/packages/plugin-timeline/CHANGELOG.md | 133 +++ ui/packages/plugin-timeline/package.json | 2 +- ui/packages/plugin-tree/CHANGELOG.md | 135 +++ ui/packages/plugin-tree/package.json | 2 +- ui/packages/plugin-view/CHANGELOG.md | 126 +++ ui/packages/plugin-view/package.json | 2 +- ui/packages/react-runtime/CHANGELOG.md | 4 + ui/packages/react-runtime/package.json | 2 +- ui/packages/react/CHANGELOG.md | 148 ++++ ui/packages/react/package.json | 2 +- ui/packages/types/CHANGELOG.md | 80 ++ ui/packages/types/package.json | 2 +- 600 files changed, 16176 insertions(+), 5134 deletions(-) delete mode 100644 .changeset/25921-auth-zero-consumer-exports.md delete mode 100644 .changeset/25921-core-zero-consumer-exports.md delete mode 100644 .changeset/25921-fields-zero-consumer-exports.md delete mode 100644 .changeset/25921-plugin-chatbot-zero-consumer-exports.md delete mode 100644 .changeset/25921-plugin-dashboard-zero-consumer-exports.md delete mode 100644 .changeset/25921-plugin-detail-zero-consumer-exports.md delete mode 100644 .changeset/25921-plugin-list-zero-consumer-exports.md delete mode 100644 .changeset/25921-providers-upload-fold.md delete mode 100644 .changeset/25921-react-zero-consumer-exports.md delete mode 100644 .changeset/25921-upload-adapter-contract.md delete mode 100644 .changeset/27193-recompute-home-amounts.md delete mode 100644 .changeset/27195-record-currency-door-names-home-field.md delete mode 100644 .changeset/29041-push-notification-channel.md delete mode 100644 .changeset/29042-native-push-registration.md delete mode 100644 .changeset/29255-installer-client-source-skill.md delete mode 100644 .changeset/29395-skills-search-auto-default.md delete mode 100644 .changeset/29411-platform-skill-package-contributions.md delete mode 100644 .changeset/29418-record-node-operand-value-docs.md delete mode 100644 .changeset/29425-declared-record-input.md delete mode 100644 .changeset/29461-d11-added-locale-keeps-write.md delete mode 100644 .changeset/29506-title-format-refused-teaching.md delete mode 100644 .changeset/29512-approval-decision-outputs-skill.md delete mode 100644 .changeset/29604-approver-directory-refusal-queue-paging.md delete mode 100644 .changeset/29604-queue-service-refuses-failed-read.md delete mode 100644 .changeset/29625-stdio-credential-env.md delete mode 100644 .changeset/29653-migrate-blank-multi-values.md delete mode 100644 .changeset/29801-related-list-placement-skill.md delete mode 100644 .changeset/29818-push-device-locale-descriptions.md delete mode 100644 .changeset/29820-push-capability-serve.md delete mode 100644 .changeset/29851-contributed-nav-labels.md delete mode 100644 .changeset/29872-export-carries-manifest-object-extensions.md delete mode 100644 .changeset/29873-install-prints-withheld-maps.md delete mode 100644 .changeset/29879-datasource-update-kept-pool-stores-nothing.md delete mode 100644 .changeset/29880-approvals-list-unknown-status.md delete mode 100644 .changeset/29891-email-send-display-name-400.md delete mode 100644 .changeset/29894-batch-row-hook-aborted.md delete mode 100644 .changeset/29895-user-locale-case-fold.md delete mode 100644 .changeset/29896-field-sortable-false-honoured.md delete mode 100644 .changeset/29897-properties-panel-readonly-reason.md delete mode 100644 .changeset/29898-picklist-binding-unknown-refused.md delete mode 100644 .changeset/29899-publish-sheet-picklist-options.md delete mode 100644 .changeset/29900-report-runtime-filter-field-unknown.md delete mode 100644 .changeset/29901-save-status-held.md delete mode 100644 .changeset/29902-publish-sheet-hook-count.md delete mode 100644 .changeset/29903-base-export-without-pair-extension.md delete mode 100644 .changeset/29904-type-mismatch-fail-closed.md delete mode 100644 .changeset/29909-revoke-sessions-on-password-reset-retired.md delete mode 100644 .changeset/29910-no-submitter-returned-docs.md delete mode 100644 .changeset/29916-connection-error-no-invented-503.md delete mode 100644 .changeset/29918-lifecycle-first-sweep-fresh-store.md delete mode 100644 .changeset/29919-dispatch-loop-next-due-wake.md delete mode 100644 .changeset/29926-type-mismatch-outcome-per-role.md delete mode 100644 .changeset/29934-dev-start-one-data-key-file.md delete mode 100644 .changeset/29935-update-action-refreshes-record.md delete mode 100644 .changeset/29936-dev-door-single-metadata-plugin.md delete mode 100644 .changeset/29937-empty-stub-protocol-range.md delete mode 100644 .changeset/29942-related-list-title-translation.md delete mode 100644 .changeset/29943-body-table-totals-additive-columns.md delete mode 100644 .changeset/29944-body-table-empty-keeps-header.md delete mode 100644 .changeset/29945-body-table-overflow-cues.md delete mode 100644 .changeset/29946-record-page-user-field-related-list.md delete mode 100644 .changeset/29951-page-header-placed-actions-reader.md delete mode 100644 .changeset/29951-skills-placed-actions-drawn.md delete mode 100644 .changeset/29954-disabled-reason-tooltip-width.md delete mode 100644 .changeset/29959-flow-condition-type-mismatch-every-site.md delete mode 100644 .changeset/29960-push-vendor-docblocks.md delete mode 100644 .changeset/29961-stored-expression-backfill.md delete mode 100644 .changeset/29962-activity-record-name-reader-locale.md delete mode 100644 .changeset/29969-platform-push-eval.md delete mode 100644 .changeset/29971-sign-in-mobile-handoff.md delete mode 100644 .changeset/29972-serve-trusted-app-schemes-env.md delete mode 100644 .changeset/29978-import-row-hook-aborted.md delete mode 100644 .changeset/29983-api-trigger-signature.md delete mode 100644 .changeset/29988-studio-interfaces-nav-rail-module.md delete mode 100644 .changeset/29988-studio-nav-item-inspector-module.md delete mode 100644 .changeset/29988-studio-package-switcher-module.md delete mode 100644 .changeset/29988-studio-pillar-helpers-modules.md delete mode 100644 .changeset/29990-i18n-related-list-title.md delete mode 100644 .changeset/29997-handoff-signin-entry.md delete mode 100644 .changeset/30000-packageless-overlay-move-heals-registry.md delete mode 100644 .changeset/30001-platform-locale-table.md delete mode 100644 .changeset/30005-cross-closure-dataset-filter-keys.md delete mode 100644 .changeset/30006-dataset-filter-key-dimension-measure.md delete mode 100644 .changeset/30009-api-trigger-declared-signature.md delete mode 100644 .changeset/30011-environment-scoped-rest-providers.md delete mode 100644 .changeset/30016-flow-date-arithmetic-run-fails.md delete mode 100644 .changeset/30017-action-icon-members-confirm-beside-params-refused.md delete mode 100644 .changeset/30018-page-header-placed-actions.md delete mode 100644 .changeset/30023-stored-sort-sortable-false-lint.md delete mode 100644 .changeset/30024-list-refused-sort-not-sent.md delete mode 100644 .changeset/30025-flow-bare-name-screen-open-binder.md delete mode 100644 .changeset/30026-time-relative-sweep-typing.md delete mode 100644 .changeset/30031-page-builder-header-breadcrumb.md delete mode 100644 .changeset/30033-related-table-name-column.md delete mode 100644 .changeset/30034-personal-view-url-default-state.md delete mode 100644 .changeset/30035-record-feed-reread-after-save.md delete mode 100644 .changeset/30036-activity-feed-default-view.md delete mode 100644 .changeset/30037-sort-picker-hidden-fields.md delete mode 100644 .changeset/30038-flow-closure-refusal-copy.md delete mode 100644 .changeset/30038-trigger-object-enter.md delete mode 100644 .changeset/30039-studio-zh-terms.md delete mode 100644 .changeset/30039-view-panel-zh-filter-prompt.md delete mode 100644 .changeset/30042-boot-picks-the-key-file-that-opens-its-secrets.md delete mode 100644 .changeset/30043-dock-sends-selected-app-and-page.md delete mode 100644 .changeset/30046-studio-off-nav-list-views.md delete mode 100644 .changeset/30046-studio-view-dropdown.md delete mode 100644 .changeset/30050-console-locale-match.md delete mode 100644 .changeset/30052-facade-scoped-classes.md delete mode 100644 .changeset/30053-hand-series-private-docblocks-slice-2.md delete mode 100644 .changeset/30053-hand-series-private-docblocks.md delete mode 100644 .changeset/30053-liveness-search-fields-repoint.md delete mode 100644 .changeset/30054-objectql-return-types.md delete mode 100644 .changeset/30054-protocol-door-return-types.md delete mode 100644 .changeset/30055-erasure-pinned-bodies.md delete mode 100644 .changeset/30057-studio-form-designer-form-view.md delete mode 100644 .changeset/30059-studio-customize-app-navigation.md delete mode 100644 .changeset/30062-declared-override-label-order.md delete mode 100644 .changeset/30062-declared-override-mapping-removal-remedies.md delete mode 100644 .changeset/30063-studio-customize-page.md delete mode 100644 .changeset/30064-studio-dataset-customize.md delete mode 100644 .changeset/30065-declared-action-override.md delete mode 100644 .changeset/30069-related-export-permission.md delete mode 100644 .changeset/30070-customization-mark-words.md delete mode 100644 .changeset/30072-runtime-package-nav-contributions-refused.md delete mode 100644 .changeset/30073-personal-view-conditions.md delete mode 100644 .changeset/30074-crm-track-history.md delete mode 100644 .changeset/30075-default-page-stage-advance.md delete mode 100644 .changeset/30076-forward-date-range-presets.md delete mode 100644 .changeset/30077-list-add-columns.md delete mode 100644 .changeset/30080-dashboard-tile-drill.md delete mode 100644 .changeset/30081-seed-mode-default-create-only.md delete mode 100644 .changeset/30086-customizations-disabled-package-fields.md delete mode 100644 .changeset/30088-studio-pair-object-actions.md delete mode 100644 .changeset/30089-studio-list-view-selection-actions.md delete mode 100644 .changeset/30091-data-key-trial-page.md delete mode 100644 .changeset/30093-page-sort-unknown-field.md delete mode 100644 .changeset/30094-page-record-liveness-regrade.md delete mode 100644 .changeset/30095-object-list-view-sort-door.md delete mode 100644 .changeset/30103-home-amount-rate-field-scale-floor.md delete mode 100644 .changeset/30106-install-custom-pair-into-writable-pair.md delete mode 100644 .changeset/30107-reset-without-email.md delete mode 100644 .changeset/30108-member-default-feed-grants.md delete mode 100644 .changeset/30119-studio-customization-build-notice.md delete mode 100644 .changeset/30119-studio-customization-environment-line.md delete mode 100644 .changeset/30121-notify-link-defaults-to-trigger-record.md delete mode 100644 .changeset/30122-timeline-one-row-per-action.md delete mode 100644 .changeset/30123-scaffold-security-datasets.md delete mode 100644 .changeset/30124-analytics-filter-array-refusal.md delete mode 100644 .changeset/30125-blank-sample-note.md delete mode 100644 .changeset/30125-invitation-sign-up-joins.md delete mode 100644 .changeset/30125-sign-out-no-return-link.md delete mode 100644 .changeset/30126-console-copy-titles-toast-money.md delete mode 100644 .changeset/30126-console-nav-icons-labels-axis-retry.md delete mode 100644 .changeset/30126-inbox-topic-mcp-refusal-history.md delete mode 100644 .changeset/30134-client-next-week-year-end.md delete mode 100644 .changeset/30136-first-organization-default-name.md delete mode 100644 .changeset/30140-compare-to-forward-presets.md delete mode 100644 .changeset/30157-handoff-verification-mail.md delete mode 100644 .changeset/30164-zh-cn-dashboard-word.md delete mode 100644 .changeset/30168-unshipped-locale-write-open-before-pair.md delete mode 100644 .changeset/30169-undeclared-bulk-action-name-draws-nothing.md delete mode 100644 .changeset/30177-draft-only-object-default-list-view.md delete mode 100644 .changeset/30178-numeric-screen-fields-refuse-strings.md delete mode 100644 .changeset/30178-screen-field-numeric-kinds-contract-text.md delete mode 100644 .changeset/30184-requiredwhen-transition-gate.md delete mode 100644 .changeset/30192-environment-scoped-security-provider.md delete mode 100644 .changeset/30193-override-label-pair-precedence.md delete mode 100644 .changeset/30197-compare-to-past-presets.md delete mode 100644 .changeset/30198-org-admin-update-refused-before-lookup.md delete mode 100644 .changeset/30199-suggested-binding-reconsider.md delete mode 100644 .changeset/30201-org-mfa-needs-enrollment.md delete mode 100644 .changeset/30202-keep-a-sign-in-method.md delete mode 100644 .changeset/30203-allowed-ip-ranges-live.md delete mode 100644 .changeset/30204-feature-flags-namespace-removed.md delete mode 100644 .changeset/30205-banned-user-work-handover.md delete mode 100644 .changeset/30208-setup-admin-wording.md delete mode 100644 .changeset/30209-audit-log-default-view.md delete mode 100644 .changeset/30209-setup-record-names-not-ids.md delete mode 100644 .changeset/30210-business-unit-enterprise-requirement.md delete mode 100644 .changeset/30210-organization-parent-sort-multi-org-help.md delete mode 100644 .changeset/30212-impersonation-landing-agent-sets.md delete mode 100644 .changeset/30212-sender-name-follows-product-name.md delete mode 100644 .changeset/30215-uninstall-forgets-engine-manifest.md delete mode 100644 .changeset/30217-install-ledger-follows-os-home.md delete mode 100644 .changeset/30218-environment-scoped-objectql-provider.md delete mode 100644 .changeset/30220-requiredwhen-formpage-kanban.md delete mode 100644 .changeset/30223-hitl-continuation-not-user-bubble.md delete mode 100644 .changeset/30228-queue-reclaim-expired-lease.md delete mode 100644 .changeset/30229-adr0146-override-types.md delete mode 100644 .changeset/30235-recycle-bin-spec-surface.md delete mode 100644 .changeset/30238-studio-restore-rereads-marks.md delete mode 100644 .changeset/30244-served-labels-console-header.md delete mode 100644 .changeset/30250-fail-closed-date-and-sweep-run-fails-sentences.md delete mode 100644 .changeset/30251-ownership-queue-on-failed-read.md delete mode 100644 .changeset/30255-clone-inherits-held-grants.md delete mode 100644 .changeset/30259-action-param-widget.md delete mode 100644 .changeset/30260-action-param-widget-reader.md delete mode 100644 .changeset/30261-packageless-overlays-view-container.md delete mode 100644 .changeset/30267-view-panel-zh-composite-rows.md delete mode 100644 .changeset/30279-time-relative-entry-surface.md delete mode 100644 .changeset/30281-seed-locale-per-organization.md delete mode 100644 .changeset/30282-feature-flags-namespace-text.md delete mode 100644 .changeset/30283-time-relative-object-mismatch.md delete mode 100644 .changeset/30285-i18n-walk-in-lint.md delete mode 100644 .changeset/30285-studio-translation-table.md delete mode 100644 .changeset/30286-default-language-follows-deployment.md delete mode 100644 .changeset/30289-scenario-card-schema.md delete mode 100644 .changeset/30291-scenario-tag-checks.md delete mode 100644 .changeset/30295-org-mfa-notice-and-enroll-refusal.md delete mode 100644 .changeset/30299-sign-in-config-fresh-after-sign-out.md delete mode 100644 .changeset/30300-keep-the-last-identity-provider.md delete mode 100644 .changeset/30302-mfa-enforcement-follow-ups.md delete mode 100644 .changeset/30303-fls-key-extension-fields.md delete mode 100644 .changeset/30304-disabled-package-extension-fields-withheld.md delete mode 100644 .changeset/30311-served-app-labels.md delete mode 100644 .changeset/30311-served-object-view-labels.md delete mode 100644 .changeset/30312-auth-settings-clear-applies-default.md delete mode 100644 .changeset/30313-allowed-ip-ranges-client-address.md delete mode 100644 .changeset/30317-draft-only-object-view-seed.md delete mode 100644 .changeset/30319-copy-doors-inherited-grants.md delete mode 100644 .changeset/30320-ipv6-cidr-allowed-ranges.md delete mode 100644 .changeset/30323-install-note-sample-data.md delete mode 100644 .changeset/30323-sample-data-count.md delete mode 100644 .changeset/30326-install-answer-protocol-check-skipped.md delete mode 100644 .changeset/30326-install-result-protocol-check-skipped.md delete mode 100644 .changeset/30327-new-datasource-mode-and-placeholders.md delete mode 100644 .changeset/30328-datasource-sync-next-steps.md delete mode 100644 .changeset/30329-deployment-setting-reset.md delete mode 100644 .changeset/30330-marketplace-readme-fits-page.md delete mode 100644 .changeset/30331-sqlite-filename-neither-memory-nor-path.md delete mode 100644 .changeset/30334-external-write-read-only-line.md delete mode 100644 .changeset/30335-datasource-delete-names-bound-objects.md delete mode 100644 .changeset/30335-storage-test-message-locale.md delete mode 100644 .changeset/30337-team-page-identity-note-and-counts.md delete mode 100644 .changeset/30338-keep-sign-in-language-pick.md delete mode 100644 .changeset/30338-save-register-language-pick.md delete mode 100644 .changeset/30343-timeline-one-action-one-entry.md delete mode 100644 .changeset/30344-changes-chip-audit-read.md delete mode 100644 .changeset/30347-spec-seed-locale-and-queue-refuse-wording.md delete mode 100644 .changeset/30348-seed-receipt-landed-ids.md delete mode 100644 .changeset/30352-register-saml-idp-entity-id.md delete mode 100644 .changeset/30358-allowed-visualization-chip-words.md delete mode 100644 .changeset/30366-forgot-password-no-mail.md delete mode 100644 .changeset/30370-external-read-only-effective-permissions.md delete mode 100644 .changeset/30377-extension-rollup-recompute-on-enable.md delete mode 100644 .changeset/30383-field-path-rules-extension-fields.md delete mode 100644 .changeset/30390-served-object-label-record-surfaces.md delete mode 100644 .changeset/30391-extension-rename-drops-stale-plural.md delete mode 100644 .changeset/30394-non-grid-bulk-bar-declared-actions.md delete mode 100644 .changeset/30396-time-relative-condition-object.md delete mode 100644 .changeset/30398-caller-address-read.md delete mode 100644 .changeset/30399-save-sign-in-language-pick.md delete mode 100644 .changeset/30400-sqlite-filename-names-a-database.md delete mode 100644 .changeset/30407-test-connection-reads-health-answer.md delete mode 100644 .changeset/30410-kanban-external-read-only-line.md delete mode 100644 .changeset/30411-org-require-mfa-follow-ups.md delete mode 100644 .changeset/30418-settings-toggle-confirm-off.md delete mode 100644 .changeset/30420-dev-admin-chip-password-off.md delete mode 100644 .changeset/30421-team-member-related-list-columns.md delete mode 100644 .changeset/30429-seed-receipt-landed-on-throw.md delete mode 100644 .changeset/30430-blueprint-widget-aggregate-calendar-date.md delete mode 100644 .changeset/30436-studio-saved-to-environment.md delete mode 100644 .changeset/30440-managed-clamp-modify-all.md delete mode 100644 .changeset/30444-datasource-delete-bound-objects.md delete mode 100644 .changeset/30454-identity-join-titles-name-people.md delete mode 100644 .changeset/30455-manifest-list-serves-sourced-options.md delete mode 100644 .changeset/30456-default-language-boot-window-one-matcher.md delete mode 100644 .changeset/30464-install-response-bare-row.md delete mode 100644 .changeset/30467-ledger-adoption-staging-dir.md delete mode 100644 .changeset/30469-import-lands-in-datasource-package.md delete mode 100644 .changeset/30469-synced-object-field-labels.md delete mode 100644 .changeset/30471-field-name-indexes-extension-fields.md delete mode 100644 .changeset/30486-related-list-rail-packaged-actions-served-object-label.md delete mode 100644 .changeset/30494-fail-open-date-sentences.md delete mode 100644 .changeset/30500-settings-toggle-confirm-off.md delete mode 100644 .changeset/30504-contributed-nav-packaged-app-base.md delete mode 100644 .changeset/30509-approval-notices-email.md delete mode 100644 .changeset/30510-email-channel-log-transport-unavailable.md delete mode 100644 .changeset/30512-plugin-audit-locales.md delete mode 100644 .changeset/30512-plugin-webhooks-locales.md delete mode 100644 .changeset/30512-service-mailbox-locales.md delete mode 100644 .changeset/30512-service-messaging-locales-a.md delete mode 100644 .changeset/30512-service-messaging-locales-b.md delete mode 100644 .changeset/30512-service-realtime-locales.md delete mode 100644 .changeset/30512-service-storage-locales.md delete mode 100644 .changeset/30525-approvals-status-non-string-message.md delete mode 100644 .changeset/30526-dataset-measure-drill-scopes.md delete mode 100644 .changeset/30535-non-grid-bulk-bar-gate.md delete mode 100644 .changeset/30538-locale-table-read-aliases.md delete mode 100644 .changeset/30542-action-override-doors-and-boot-order.md delete mode 100644 .changeset/30554-in-app-doc-links-locale.md delete mode 100644 .changeset/30562-sql-never-drop-table-with-rows.md delete mode 100644 .changeset/30563-driver-catalog-requester-language.md delete mode 100644 .changeset/30565-organization-mfa-notice-once.md delete mode 100644 .changeset/30581-nav-contributions-reenable.md delete mode 100644 .changeset/30584-blueprint-field-default-value.md delete mode 100644 .changeset/30607-seed-locale-liveness-note.md delete mode 100644 .changeset/30608-remaining-field-indexes-extension-fields.md delete mode 100644 .changeset/30610-extension-redeclared-owner-field-refused.md delete mode 100644 .changeset/30615-report-chart-table-drill-measure-scopes.md delete mode 100644 .changeset/30616-sample-data-count-after-reload.md delete mode 100644 .changeset/30618-script-subtag-read-aliases.md delete mode 100644 .changeset/30619-bundle-locale-own-key.md delete mode 100644 .changeset/30623-moved-action-override-refusal.md delete mode 100644 .changeset/30626-migrate-withheld-extension-columns.md delete mode 100644 .changeset/30635-object-save-reports-refused-storage-sync.md delete mode 100644 .changeset/30646-assigned-work-injected-owner.md delete mode 100644 .changeset/assignment-write-unaddressable-row-not-found.md delete mode 100644 .changeset/banned-user-credentials-refused.md delete mode 100644 .changeset/banned-user-oauth-credentials-refused.md delete mode 100644 .changeset/by-id-write-row-reread-rules.md delete mode 100644 .changeset/change-email-notice-every-sign-in.md delete mode 100644 .changeset/cli-connection-label-shared-url-grammar.md delete mode 100644 .changeset/cloud-3485-turso-remote-balanced-group.md delete mode 100644 .changeset/cloud-3565-chatbot-tool-labels.md delete mode 100644 .changeset/cloud-3565-record-write-card.md delete mode 100644 .changeset/cloud-3565-spec-record-write-tool-names.md delete mode 100644 .changeset/connect-agent-work-assistants.md delete mode 100644 .changeset/delegated-admin-unreadable-row.md delete mode 100644 .changeset/driver-options-declared-keys-only.md delete mode 100644 .changeset/list-queue-filter-provisioned-storage.md delete mode 100644 .changeset/org-admin-removal-caller-read.md delete mode 100644 .changeset/org-admin-removal-stored-row-verdict.md delete mode 100644 .changeset/password-reset-revocation-trail.md delete mode 100644 .changeset/per-record-action-missing-record-id.md delete mode 100644 .changeset/phone-reset-text-after-response.md delete mode 100644 .changeset/record-timeline-folded-types-from-spec.md delete mode 100644 .changeset/rollback-addresses-served-row.md delete mode 100644 .changeset/sec-hardening-g-docs-external-object-scope-skill.md delete mode 100644 .changeset/sec-hardening-g-external-object-organization-scope.md delete mode 100644 .changeset/sec-hardening-p3-write-refused-read-door.md delete mode 100644 .changeset/sec-hardening-r6-agent-ceiling-grant.md delete mode 100644 .changeset/sec-hardening-s-external-scope-every-caller.md delete mode 100644 .changeset/sec-hardening-u-write-implies-read.md delete mode 100644 .changeset/settings-action-confirm-text.md delete mode 100644 .changeset/setup-admin-wording-zh-cn.md delete mode 100644 .changeset/sign-up-mail-after-response.md delete mode 100644 .changeset/sys-file-uploader-is-its-creator.md delete mode 100644 .changeset/url-credential-special-scheme-read-however-many-slashes.md delete mode 100644 .changeset/visitor-verification-resend.md create mode 100644 packages/qa/scenarios/CHANGELOG.md create mode 100644 packages/services/service-push/CHANGELOG.md diff --git a/.changeset/25921-auth-zero-consumer-exports.md b/.changeset/25921-auth-zero-consumer-exports.md deleted file mode 100644 index b94a45754e5..00000000000 --- a/.changeset/25921-auth-zero-consumer-exports.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@object-ui/auth': patch ---- - -refactor(auth)!: retire twelve zero-consumer exports (the Turnstile script URL, the two phone-identifier helpers, the authenticated-fetch options type, the org-role list, the invitation-status list and guard, and five form primitives) - -Clause-②: no (narrowing) -Metadata-protocol: none - - - -**BREAKING** for a host that imports any of these twelve names from `@object-ui/auth`. None had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected. Every module stays in the package: `TurnstileWidget` keeps loading its script, `LoginForm` keeps reading phone identifiers, `createAuthClient` keeps narrowing invitation statuses, `org-roles.ts` keeps building its pickers from the role list, and `LoginForm` / `RegisterForm` / `ForgotPasswordForm` keep drawing with the form primitives. Only the entry stops publishing them. `TurnstileWidget`, `createAuthenticatedFetch`, `ORG_ROLE_OWNER` / `ORG_ROLE_ADMIN` / `ORG_ROLE_DELEGATED_ADMIN` / `ORG_ROLE_MEMBER`, `ORG_ROLE_LABELS`, `orgRoleGrade`, `invitableOrgRoles`, `assignableOrgRoles`, the `OrgRole` and `AuthInvitationStatus` types, `AUTH_LINK_CLASS`, `AUTH_PRIMARY_BUTTON_CLASS`, `AuthAlertIcon`, `AuthErrorBanner`, `AuthFormHeader` and `AuthSpinner` stay exported unchanged. - -What leaves, and what to write instead: - -- FROM `TURNSTILE_SCRIPT_URL` TO `TurnstileWidget`, which loads the Cloudflare Turnstile script itself. -- FROM `normalizePhoneIdentifier`, `looksLikePhoneIdentifier` TO `LoginForm`, which accepts a phone number as the identifier and normalizes it; keep a local copy if your own sign-in form needs them. -- FROM `AuthenticatedAdapterOptions` TO a local interface: no function in the package takes it (`createAuthenticatedFetch` has its own options). -- FROM `ORG_ROLES` TO `invitableOrgRoles` / `assignableOrgRoles` for the roles a picker offers, or the four `ORG_ROLE_*` constants. -- FROM `AUTH_INVITATION_STATUSES`, `isAuthInvitationStatus` TO the `AuthInvitationStatus` type; `AuthInvitation.status` already arrives narrowed from `createAuthClient`. -- FROM `AUTH_FIELD_LABEL_CLASS`, `AUTH_INPUT_CLASS`, `AuthCheckIcon`, `AuthDivider`, `AuthMailIcon` TO no replacement: the built-in forms apply them themselves; copy the class strings or icons if a custom form needs the same look. diff --git a/.changeset/25921-core-zero-consumer-exports.md b/.changeset/25921-core-zero-consumer-exports.md deleted file mode 100644 index 74fef54be24..00000000000 --- a/.changeset/25921-core-zero-consumer-exports.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@object-ui/core': patch ---- - -refactor(core)!: retire forty-two zero-consumer entry exports, and delete the query-AST convenience pair nothing reads - -Clause-②: no (narrowing) -Metadata-protocol: none - - - -**BREAKING** for a host that imports any of these forty-two names from `@object-ui/core`. None had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected. - -Two of them are **gone from the package, not only from the entry**, because nothing inside the package read them either: `buildQueryAST` and the `defaultQueryASTBuilder` instance it wrapped are deleted from `query/query-ast.ts`. - -The other forty stay in their modules for the code and tests that read them and only leave the entry. Every other name the entry exported stays exported unchanged, among them `QueryASTBuilder`, `columnIdentity`, `columnHeader`, `normalizeColumnIdentity`, `toDomProps`, `pickDomProps`, `AFFORDANCE_GRANTS`, `attachObjectSortability`, `readObjectSortability`, `markSwitchedOffActions`, `isActionSwitchedOff`, `attachCascadeDeleteObjects`, `readCascadeDeleteObjects`, `getSortValue`, `compareSortValues`, `bindSubjectPermissions`, `subjectPermissionsOf`, `bindSubjectCapabilities`, `subjectCapabilitiesOf`, `detectNonCanonicalRowSpelling`, `ACTION_DEF_KEYS`, `SPEC_ACTION_KEYS`, `RETIRED_ACTION_KEYS`, `warnOnUnknownActionKeys`, `interpretActionResponse`, `readActionPayload`, `actionCopySource`, `fillTemplateHoles`, `generateThemeVars`, `generateColorVars`, `elementDataSourceBlock`, `ERROR_CODES`, `createError`, `ObjectUIError` with all its subclasses, `debugLog`, `parseDebugFlags`, `CONTEXT_TOKENS`, `resolveFilterPlaceholders`, `buildChartSeries`, `isIntegerAggregate`, `resolveDimensionFieldMeta` and `createFieldColorResolver`. - -What leaves, and what to write instead: - -- FROM `buildQueryAST(query)`, `defaultQueryASTBuilder` TO `new QueryASTBuilder().build(query)`. -- FROM `CANONICAL_COLUMN_IDENTITY_KEY`, `LEGACY_COLUMN_IDENTITY_KEYS`, `CANONICAL_COLUMN_LABEL_KEY` TO `columnIdentity(column)` / `columnHeader(column)`, which read the canonical key and its legacy spellings; FROM `normalizeColumnIdentities(columns)` TO `columns.map(normalizeColumnIdentity)`. -- FROM `isOpenDomAttributeFamily` TO `toDomProps` / `pickDomProps`, which already pass every `data-*` and `aria-*` key. -- FROM `AffordanceGrantRow` TO `AffordanceGrant`, or `(typeof AFFORDANCE_GRANTS)[ConsoleAffordance]` for one row. -- FROM `normalizeObjectSortability`, `OBJECT_SORTABILITY_KEY` TO `attachObjectSortability` / `readObjectSortability`. -- FROM `ACTION_SWITCHED_OFF_KEY` TO `markSwitchedOffActions` / `markSwitchedOffActionsByObject` / `isActionSwitchedOff`. -- FROM `CASCADE_DELETE_OBJECTS_KEY` TO `attachCascadeDeleteObjects` / `readCascadeDeleteObjects`. -- FROM `compareCellValues(a, b, options)` TO `compareSortValues(getSortValue(a, options), getSortValue(b, options))`. -- FROM `SUBJECT_PERMISSIONS`, `SUBJECT_CAPABILITIES` TO `bindSubjectPermissions` / `subjectPermissionsOf` and `bindSubjectCapabilities` / `subjectCapabilitiesOf`. -- FROM `ROW_PREDICATE_CANONICAL_ROOT` TO the literal `'record'`, or `detectNonCanonicalRowSpelling` to check a predicate. -- FROM `NAVIGATION_ALIAS_KEYS`, `HOST_DISPATCH_ACTION_KEYS`, `KNOWN_ACTION_KEYS`, `classifyActionKeys` TO `warnOnUnknownActionKeys`, which classifies an action against the same vocabulary, or `ACTION_DEF_KEYS` / `SPEC_ACTION_KEYS` for the authored key lists. -- FROM `hasDeprecatedObjectParams`, `warnOnDeprecatedObjectParams` TO nothing: `ActionRunner` warns about deprecated object params itself when it runs the action. -- FROM `isLegacyActionEnvelope` TO `interpretActionResponse` / `readActionPayload`, which unwrap the legacy envelope. -- FROM `actionCopyText`, `fillTransitionalCopy` TO `actionCopySource` with `hasTemplateHoles` / `fillTemplateHoles`. -- FROM `toCSSColor`, `generateTypographyVars`, `generateBorderRadiusVars`, `generateShadowVars` TO `generateThemeVars(theme)`, which emits all of those variables, or `generateColorVars` for the colour block alone. -- FROM `isElementDataSourceBlock` TO nothing a host needs: the component registry reads it when `elementDataSourceBlock` marks a renderer. -- FROM `isErrorCode(code)` TO `code in ERROR_CODES`; FROM `formatErrorMessage(error)` TO `error.message` with `ERROR_CODES[error.code]` for the suggestion and doc link. -- FROM `isDebugEnabled` TO `debugLog` / `debugTime`, which are no-ops when debug mode is off. -- FROM `ContextTokenName` TO `(typeof CONTEXT_TOKENS)[number]`. -- FROM `isBuiltinAggregate`, `resolveMeasureLabel` TO `buildChartSeries` with `builtinAggregateLabels`, which labels a built-in aggregate measure the same way; FROM `INTEGER_AGGREGATES` TO `isIntegerAggregate`. -- FROM `resolveDimensionFieldOptions(...)` TO `resolveDimensionFieldMeta(...)`, the same walk, reading each entry's `options`; FROM `resolveRelationshipTarget` TO `resolveDimensionFieldMeta`, which follows the relationship for a dotted path. -- FROM `isColorLiteral`, `buildFieldColorMap`, `FieldColorOption` TO `createFieldColorResolver(fieldDef)`, which builds the colour map and accepts colour literals as-is. diff --git a/.changeset/25921-fields-zero-consumer-exports.md b/.changeset/25921-fields-zero-consumer-exports.md deleted file mode 100644 index 65a2be9d399..00000000000 --- a/.changeset/25921-fields-zero-consumer-exports.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/fields': patch ---- - -refactor(fields)!: retire six zero-consumer exports (four file-value helpers, the filter-condition extra operators, the grid column sum) - -Clause-②: no (narrowing) -Metadata-protocol: none - - - -**BREAKING** for a host that imports any of these six names from `@object-ui/fields`. None had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected. Every module stays in the package: `FileField`, `ImageField` and `AvatarField` keep reading the file-value helpers, `FilterConditionField` keeps offering its extra operators, `fileUrlFromId` keeps building on the storage base path and `GridField` keeps summing its total column. Only the entry stops publishing them. `fileIdOf`, `fileUrlFromId`, `readFileValue`, `readFileValues`, `fileValueForSubmit`, `isFileIdToken`, `UploadIncompleteError`, the `FileValueView` and `UploadResultLike` types, `condToMongo`, `kvToCondition`, `isMatchAllCriteria`, `FilterConditionField`, `evalArith`, `lookupAutofillPatch`, `computeRow`, `GridField`, `LineItemsField` and the `GridColumn` type stay exported unchanged. - -What leaves, and what to write instead: - -- FROM `isImageValue` TO `readFileValue(value)` and a check on its `mimeType`. -- FROM `uploadResultView`, `withRecentUploads` TO `FileField` / `ImageField` / `AvatarField`, which show a just-uploaded file themselves. -- FROM `FILE_STORAGE_BASE_PATH` TO `fileUrlFromId`, which builds the download URL for a file id on that path. -- FROM `FILTER_CONDITION_EXTRA_OPERATORS` TO `FilterConditionField`, which offers those operators itself. -- FROM `sumColumn` TO `GridField`, which draws the total of its declared total column, or a local reduce over the rows. diff --git a/.changeset/25921-plugin-chatbot-zero-consumer-exports.md b/.changeset/25921-plugin-chatbot-zero-consumer-exports.md deleted file mode 100644 index e5fe3bea3df..00000000000 --- a/.changeset/25921-plugin-chatbot-zero-consumer-exports.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/plugin-chatbot': patch ---- - -refactor(plugin-chatbot)!: retire eight zero-consumer exports (the floating chatbot context hook and its three types, the message item props, one adapter helper and two display helpers) - -Clause-②: no (narrowing) -Metadata-protocol: none - - - -**BREAKING** for a host that imports any of these eight names from `@object-ui/plugin-chatbot`. None had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected. Every module stays in the package: `FloatingChatbotTrigger` and `FloatingChatbotPanel` keep reading the floating state through the hook, `toRuntimeMessages` keeps converting tool invocations, and `ChatbotEnhanced` keeps rendering tool results and error summaries with the two helpers. Only the entry stops publishing them. `FloatingChatbot`, `FloatingChatbotProvider`, `FloatingChatbotTrigger`, `FloatingChatbotPanel` and their `…Props` types, `toRuntimeMessages`, `authoredToRuntimeMessage`, `toRuntimeRole`, `toRuntimeTimestamp`, `toRuntimeToolState`, `humanizeToolName`, `TypingIndicator`, `emitAiUsageRefresh` and `AGENT_ALIAS_GROUPS` stay exported unchanged. - -What leaves, and what to write instead: - -- FROM `useFloatingChatbot`, `FloatingChatbotState`, `FloatingChatbotActions`, `FloatingChatbotContextValue` TO `FloatingChatbotTrigger` and `FloatingChatbotPanel` inside a `FloatingChatbotProvider`, which open, close and toggle the panel themselves; for a control of your own, hold the open state in your own component. -- FROM `ChatMessageProps` TO `ChatbotProps`: `Chatbot` renders the message rows itself. -- FROM `toRuntimeToolInvocation` TO `toRuntimeMessages` or `authoredToRuntimeMessage`, which convert every tool invocation of a message. -- FROM `unwrapToolResult`, `summarizeChatError` TO no replacement: `ChatbotEnhanced` applies both itself; keep a local copy if your own chat surface needs them. diff --git a/.changeset/25921-plugin-dashboard-zero-consumer-exports.md b/.changeset/25921-plugin-dashboard-zero-consumer-exports.md deleted file mode 100644 index 5076ad20d97..00000000000 --- a/.changeset/25921-plugin-dashboard-zero-consumer-exports.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -refactor(plugin-dashboard)!: retire five zero-consumer exports (the config panel's defaults table and hook, the retired-node tombstone and its report and reset helpers) - -Clause-②: no (narrowing) -Metadata-protocol: none - - - -**BREAKING** for a host that imports any of these five names from `@object-ui/plugin-dashboard`. None had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected. Every module stays in the package: the config panels keep using the hook and its table, and the package still registers the tombstone for every retired dashboard node type, so an authored `view:dashboard` is refused by name exactly as before. Only the entry stops publishing them. `RETIRED_DASHBOARD_NODE_TYPES`, `ConfigPanelTranslate`, `DashboardMetricNodeSchema`, the three `WidgetDataset*` types and every component stay exported unchanged. - -What leaves, and what to write instead (none has a replacement export): - -- FROM `CONFIG_PANEL_DEFAULT_TRANSLATIONS`, `useConfigPanelTranslation` TO no replacement: `DashboardConfigPanel` and `WidgetConfigPanel` translate their own labels; mount an `I18nProvider` to change them. -- FROM `RetiredDashboardNodeTombstone` TO no replacement: the package registers it under every key of `RETIRED_DASHBOARD_NODE_TYPES` itself, so rendering a retired node through `SchemaRenderer` still shows the refusal. -- FROM `reportRetiredDashboardNodeType`, `resetRetiredDashboardNodeTypeReports` TO no replacement: the tombstone reports on its own, once per retired type; read the migration text from `RETIRED_DASHBOARD_NODE_TYPES`. diff --git a/.changeset/25921-plugin-detail-zero-consumer-exports.md b/.changeset/25921-plugin-detail-zero-consumer-exports.md deleted file mode 100644 index c912292a1ee..00000000000 --- a/.changeset/25921-plugin-detail-zero-consumer-exports.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -refactor(plugin-detail)!: retire twenty-one zero-consumer exports (record-page synthesis, layout, related-card and timeline helpers, one hook and three types) - -Clause-②: no (narrowing) -Metadata-protocol: none - - - -**BREAKING** for a host that imports any of these twenty-one names from `@object-ui/plugin-detail`. None had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected. Every module stays in the package and keeps its in-package readers (the record page synthesizer, `DetailView`, `RecordPageColumns`, the related cards and the timeline stream); only the entry stops publishing them. Everything else on the entry, including `buildDefaultPageSchema`, `buildDefaultTabs`, `isWideFieldType`, `InlineFieldInput`, `recordSitsInQueue`, `DefaultRecordPageContext`, `prefetchRelatedCounts`, `relatedCountTargetsIn`, `mergeTimelineStream`, `resolveRelatedArrangement` and the `RelatedArrangement` / `RecordPageTier` / `DefaultPageColumns` types, stays exported unchanged. - -What leaves, and what to write instead: - -- FROM `buildDefaultDiscussion`, `buildDefaultTimeline`, `deriveStages` TO `buildDefaultPageSchema`, which builds the whole default record page including its discussion, timeline and stage path. -- FROM `RECORD_PAGE_WIDE_MIN_WIDTH`, `recordPageColumnOfTab`, `RecordPageColumn` TO `recordPageTierFor` and `buildDefaultPageColumns`, which place the page's tabs into columns for a width. -- FROM `inferDetailColumns`, `applyAutoSpan`, `applyDetailAutoLayout` TO `DetailView`, which lays out its sections itself; `isWideFieldType` stays. -- FROM `extractLookupId`, `TEXTUAL_REF_FALLBACK_TYPES` TO `InlineFieldInput`, which reads lookup values itself. -- FROM `queueClaimAdmitted` TO `useQueueClaimOffered`, which answers whether the Claim button shows. -- FROM `useDefaultRecordPage` TO `DefaultRecordPageContext` with `useContext`. -- FROM `relatedCountTargetOf` TO `relatedCountTargetsIn`. -- FROM `filterTimelineStream` TO `mergeTimelineStream`; the timeline panel applies its filter chips itself. -- FROM `RELATED_CARD_NEW_BADGE_MS`, `editRelatedArrangement`, `relatedCardId`, `relatedDefaultOrderHash`, `RelatedArrangementEdit` TO `resolveRelatedArrangement` with `relatedArrangementFromRecord` / `relatedArrangementToRecord`; the related cards edit and badge the arrangement themselves. -- FROM `DetailTabsProps` TO `DetailViewProps`: `DetailTabs` is not on the entry, `DetailView` renders it. diff --git a/.changeset/25921-plugin-list-zero-consumer-exports.md b/.changeset/25921-plugin-list-zero-consumer-exports.md deleted file mode 100644 index 632206e7649..00000000000 --- a/.changeset/25921-plugin-list-zero-consumer-exports.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/plugin-list': patch ---- - -refactor(plugin-list)!: retire three zero-consumer exports (the view switcher dropdown, the tab bar select and the conditional formatting wrapper) - -Clause-②: no (narrowing) -Metadata-protocol: none - - - -**BREAKING** for a host that imports any of these three names from `@object-ui/plugin-list`. None had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected. Every module stays in the package (`ListView` keeps rendering `ViewSwitcherDropdown`); only the entry stops publishing them. `ListView`, `ViewSwitcher`, `TabBar`, `UserFilters`, `normalizeFilterCondition`, `normalizeFilters`, `LIST_DEFAULT_TRANSLATIONS` and every exported type, `TabBarProps` included, stay exported unchanged. - -What leaves, and what to write instead: - -- FROM `ViewSwitcherDropdown` TO `ViewSwitcher`, or render `ListView`, which draws the dropdown itself. -- FROM `TabBarSelect` TO `TabBar`, which takes the same `TabBarProps`. -- FROM `evaluateConditionalFormatting` TO `resolveConditionalFormatting` from `@object-ui/core`, the function it wrapped: same arguments, result typed as `React.CSSProperties`. diff --git a/.changeset/25921-providers-upload-fold.md b/.changeset/25921-providers-upload-fold.md deleted file mode 100644 index 48667720096..00000000000 --- a/.changeset/25921-providers-upload-fold.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/fields': patch -'@object-ui/data-objectstack': patch ---- - -refactor(fields, data-objectstack)!: `UploadProvider` moves into `@object-ui/fields`, the ObjectStack upload adapter into `@object-ui/data-objectstack`, and `@object-ui/providers` is retired - -Clause-②: yes (narrowing) — `@object-ui/fields` gains `UploadProvider`, `useUpload`, `UploadProviderProps` and the three upload contract types, and `@object-ui/data-objectstack` gains `createObjectStackUploadAdapter` and `ObjectStackUploadAdapterOptions`; the whole `@object-ui/providers` package is retired, which takes `createS3Adapter`, `createAzureBlobAdapter`, their option types and the exported `createObjectUrlAdapter` with it. -Metadata-protocol: none - - - -**BREAKING.** `@object-ui/providers` is retired: `18.3.0` is its last published version, and it gets no further release. Its one remaining job was the upload context the file, image and avatar widgets of `@object-ui/fields` upload through, so that context now lives in `@object-ui/fields` itself, and the adapter that talks to an ObjectStack server's storage service lives in `@object-ui/data-objectstack`, beside the other fetch logic against that server. The upload contract the two share is `@object-ui/types`'. - -What leaves, and what to do: - -- FROM `@object-ui/providers` TO `@object-ui/fields` and `@object-ui/data-objectstack`: import `UploadProvider`, `useUpload` and the `UploadProviderProps`, `UploadAdapter`, `UploadOptions` and `UploadResult` types from `@object-ui/fields` (the three contract types also from `@object-ui/types`), and `createObjectStackUploadAdapter` with `ObjectStackUploadAdapterOptions` from `@object-ui/data-objectstack`, then drop the `@object-ui/providers` dependency. `createS3Adapter`, `createAzureBlobAdapter`, `S3AdapterOptions` and `AzureBlobAdapterOptions` had no consumer and leave with no replacement: a host that uploads to its own bucket writes an object with a `name` and an `upload(file, options)` method against the `UploadAdapter` type of `@object-ui/types` and hands it to `UploadProvider`. `createObjectUrlAdapter` is no longer exported: it stays the local `blob:` adapter `useUpload` falls back to when no `UploadProvider` is mounted. - -What does not change: `UploadProvider`, `useUpload` and `createObjectStackUploadAdapter` keep their signatures and behaviour, the presigned upload request is the same, and the widgets read the same context. diff --git a/.changeset/25921-react-zero-consumer-exports.md b/.changeset/25921-react-zero-consumer-exports.md deleted file mode 100644 index c0e17c889c8..00000000000 --- a/.changeset/25921-react-zero-consumer-exports.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@object-ui/react': patch ---- - -refactor(react)!: retire twenty zero-consumer exports, and delete the two motion hooks, two helpers and one type nothing reads - -Clause-②: no (narrowing) -Metadata-protocol: none - - - -**BREAKING** for a host that imports any of these twenty names from `@object-ui/react`. None had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected. - -Seven of them are **gone from the package, not only from the entry**, because nothing inside the package read them either: the `hooks/useReducedMotion.ts` module (`useReducedMotion`) and the `hooks/useViewTransition.ts` module (`useViewTransition`, `ViewTransitionConfig`, `ViewTransitionResult`) are deleted, and the functions `useHasPageVariables` and `getShortcutDescriptions` and the type `AnimationTriggerType` are deleted from their modules. - -The other thirteen stay in their modules for the code that reads them and only leave the entry: `formatUnresolvableVisibilityMessage` (the visibility reporter still formats with it), `evaluateConfigBagInScope` (`useConfigBagEvaluator` still calls it), `SUPPORTED_TRANSITION_PRESETS` / `SUPPORTED_EASING_FUNCTIONS` and the three `SUPPORTED_NOTIFICATION_*` sets (the spec-parity tests still hold them against the spec vocabularies, and `resolveNotificationPosition` still checks positions against its set), `useOptionalTheme`, `resolveNotificationPresentation` (`NotificationProvider` still calls it), the raw `HostMapStyleContext` / `RecordPagePreferencesContext` / `PrintPageContext` objects, and `NoDataSourcePanel` (`ElementDataSourceGate` still renders it). Every other name the entry exported stays exported unchanged, among them `reportUnresolvableVisibilityPredicate`, `UNRESOLVABLE_VISIBILITY_PREFIX`, `UNRESOLVABLE_ENABLEMENT_PREFIX`, `useAnimation`, `useTheme`, `useNotifications`, `NotificationProvider`, the `HostMapStyleProvider` / `RecordPagePreferencesProvider` / `PrintPageProvider` providers with their `useHostMapStyle` / `useRecordPagePreferences` / `usePrintPage` hooks, `usePageVariables`, `useKeyboardShortcuts` and `ElementDataSourceGate`. - -What leaves, and what to write instead: - -- FROM `useReducedMotion` TO `window.matchMedia('(prefers-reduced-motion: reduce)')` in your own hook; the package no longer carries one. -- FROM `useViewTransition`, `ViewTransitionConfig`, `ViewTransitionResult` TO `document.startViewTransition` called directly; the package no longer carries a wrapper. -- FROM `useHasPageVariables` TO `usePageVariables()`, which answers a frozen empty value outside a `PageVariablesProvider`. -- FROM `getShortcutDescriptions` TO a local map over your `KeyboardShortcutDefinition[]` (filter on `description` and `enabled !== false`). -- FROM `AnimationTriggerType` TO a local string union: no function in the package takes it. -- FROM `formatUnresolvableVisibilityMessage` TO `reportUnresolvableVisibilityPredicate`, which formats and reports in one call; filter its lines by `UNRESOLVABLE_VISIBILITY_PREFIX` / `UNRESOLVABLE_ENABLEMENT_PREFIX`. -- FROM `evaluateConfigBagInScope` TO `SchemaRenderer`, which evaluates a node's config bag itself. -- FROM `SUPPORTED_TRANSITION_PRESETS`, `SUPPORTED_EASING_FUNCTIONS` TO the `TransitionPresetType` / `EasingPreset` types, or the spec's `TransitionPresetSchema` / `EasingFunctionSchema` for a runtime list. -- FROM `useOptionalTheme` TO `useTheme` inside a `ThemeProvider`. -- FROM `resolveNotificationPresentation` TO `useNotificationsByPresentation(presentation)`, which returns the notifications a surface of that presentation renders. -- FROM `SUPPORTED_NOTIFICATION_DISPLAY_TYPES`, `SUPPORTED_NOTIFICATION_POSITIONS`, `SUPPORTED_NOTIFICATION_ACTION_VARIANTS` TO the `NotificationDisplayType` / `NotificationPositionValue` / `NotificationActionVariant` types, or `resolveNotificationPosition` to normalize a position. -- FROM `HostMapStyleContext`, `RecordPagePreferencesContext`, `PrintPageContext` TO their providers (`HostMapStyleProvider`, `RecordPagePreferencesProvider`, `PrintPageProvider`) and hooks (`useHostMapStyle`, `useRecordPagePreferences`, `usePrintPage`). -- FROM `NoDataSourcePanel` TO `ElementDataSourceGate`, which renders it when no data source resolves, or `noDataSourceMessage` for the text. diff --git a/.changeset/25921-upload-adapter-contract.md b/.changeset/25921-upload-adapter-contract.md deleted file mode 100644 index 1dbff6edfdc..00000000000 --- a/.changeset/25921-upload-adapter-contract.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/types': patch ---- - -feat(types): `@object-ui/types` publishes the upload adapter contract, `UploadAdapter`, `UploadOptions` and `UploadResult` - -Clause-②: yes (widening) -Metadata-protocol: none - -The three interfaces the file, image and avatar widgets upload through, unchanged in shape from the `@object-ui/providers` package they leave. They live here, as `DataSource` does, because `@object-ui/fields` (whose `UploadProvider` reads an adapter) and `@object-ui/data-objectstack` (whose `createObjectStackUploadAdapter` builds one) do not depend on each other. A host that writes its own adapter types it from here. Nothing existing changes. diff --git a/.changeset/27193-recompute-home-amounts.md b/.changeset/27193-recompute-home-amounts.md deleted file mode 100644 index c68142aa0a7..00000000000 --- a/.changeset/27193-recompute-home-amounts.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/objectql': patch -'@objectstack/cli': patch ---- - -feat(objectql,cli): `os migrate recompute-home-amounts` — recompute stored home-currency amounts at the rates in force now, report first, rows without a rate listed and skipped - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@objectstack/objectql`: `recomputeHomeAmounts(engine, logger, { object, field?, since?, apply?, maxRecords?, maxListed?, source? })`**, with `assertHomeAmountRecomputeSince` (the pure `since` check a door can run before it opens a database), `formatHomeAmountRecomputeReport`, `homeAmountRecomputeComplete` and `HOME_AMOUNT_SKIP_REASONS`. It walks every stored row of an object whose `record`-mode amounts declare `currencyConfig.homeAmount` and projects each through the write-time projection itself (the same rate row, division, single rounding, same-currency rule and organization-timezone day), in the row's own organization, so each organization converts into its own home currency at its own rates and is counted apart. A dry run (the default) reports what would change and writes nothing; `apply` writes only the rows that differ, through `engine.update` with the home field in the payload, so the value stored is the one the write itself computes, and only the home and rate fields move (`updated_at` is kept, so a consumer syncing by `updated_at` does not see the recompute). Being a write, it moves what reads the home field as a fresh write would: stored formulas, parent summaries, and the per-row audit rows where audit capture is installed. Rows that cannot be converted are listed by reason and never written: `rate_missing` (with the currency and date), `code_blank` (an amount with no currency code; the remedy is `os migrate record-currency-codes`), `document_date_blank`, `home_currency_unset` and `unconvertible`. `since` filters on the document date (`homeAmount.rateDate`). One run-level `sys_audit_log` entry records each applied run (counts per organization and reason, and the skipped rows), also when an unclassified error stops the run partway (marked `aborted`, then the error is thrown on). A walk cut short by `maxRecords` is `truncated` and not complete. Refused before any row is read: an unknown object (`OBJECT_NOT_FOUND`), an object or `field` declaring no home amount (`INVALID_FIELD`), a `since` that is not `YYYY-MM-DD` (`VALIDATION_FAILED`). -- **`@objectstack/objectql`: `ObjectQL.homeAmountReadsFor(context?)`**, the reads the write-time projection makes (its serving, and the organization a write converts a row under), for a caller that projects a stored row as a write would. -- **`@objectstack/cli`: `os migrate recompute-home-amounts --object [--field ] [--since YYYY-MM-DD] [--apply] [--yes] [--json] [--force] [--max-records] [--database-url]`**, the shell of `os migrate record-currency-codes`, booting the settings service (each organization's `localization.currency` and `.timezone`) and the audit ledger. It exits 1 while any row is listed and skipped, on a dry run as on an apply, because a total over the home field silently leaves such a row out, and when `--max-records` cut the walk short. diff --git a/.changeset/27195-record-currency-door-names-home-field.md b/.changeset/27195-record-currency-door-names-home-field.md deleted file mode 100644 index 9997dcd83ab..00000000000 --- a/.changeset/27195-record-currency-door-names-home-field.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/service-analytics': patch ---- - -fix(service-analytics): the refusal of an ungrouped aggregate over a record-currency amount names the home field as the remedy for a consolidated total (ADR-0144 D6) - -Clause-②: yes (widening) -Metadata-protocol: none - -- **Now.** A `sum`, `avg`, `min` or `max` over a `currencyMode: 'record'` amount that does not group by its code field is still refused `INVALID_FIELD` / 400 before anything is read, with the same code, status, member, field and object. Its message now names both remedies. For one total in the home currency, point the measure at the amount's home field: by name (`homeAmount.field`) when the host relays it, or else as "the field its `currencyConfig.homeAmount.field` names", with the declaration to add if it has none. For one row per currency, add the dimension over the code field. Before, the message named only the grouping. -- **Unchanged.** A measure over the home field is no record-mode amount, so it is served unchanged: ungrouped, grouped by the code field, and as a `totals` grand total, labelled with the home currency. -- **The widening.** `AnalyticsServiceConfig.sourceFieldMeta` may answer one more optional key, `homeAmountField`: the amount's `currencyConfig.homeAmount.field`, beside a `currencyField` only. `AnalyticsServicePlugin` relays it. A host that does not answer it gets the message without the field's name. diff --git a/.changeset/29041-push-notification-channel.md b/.changeset/29041-push-notification-channel.md deleted file mode 100644 index 67fc4e16a93..00000000000 --- a/.changeset/29041-push-notification-channel.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/service-push': patch -'@objectstack/service-messaging': patch -'@objectstack/spec': patch ---- - -feat(notifications): the `push` channel — a `notify` node naming `push` reaches the recipient's phones through APNs and FCM - -Clause-②: yes (widening) -Metadata-protocol: none - -- **New package `@objectstack/service-push`.** `PushServicePlugin` registers the kernel's `push` service: APNs over HTTP/2 with token-based auth (an ES256 provider token signed with the `.p8` key, reused for fifty minutes) and FCM over the HTTP v1 API (an OAuth access token from the service account). It reads `PushServiceConfigSchema` from its `config` option and refuses an inline secret at boot with the schema's guidance. It serves the direct transport (`transport: 'direct'`, the operator's own APNs key and Firebase project, for a white-label build); the hosted relay, the schema's default and so also an empty config, is refused at init until its sender ships (#29920). Each secret is named by `credentialRef` and resolved when the transport first signs; in the open tier the reference is the name of an environment variable (ADR-0143 D3). -- **`@objectstack/service-messaging`: the `push` channel.** Mounted while a `push` service is registered, as the sms channel is while an `sms` service is. One delivery reaches every enabled device the recipient registered, on each vendor the push service has a transport for. The push carries the notification id and, when it is a same-server path (`PushRouteSchema`), the notification's `actionUrl` as `route`; any other `actionUrl` is left out and the delivery still goes. Each device's push also carries the origin stored on its row (`PushDeliveryDataSchema.origin`) when the row holds one; the registry does not store it yet, so today none does. A token the vendor reports dead (APNs `BadDeviceToken` / `Unregistered`, FCM `UNREGISTERED`) is retired through `invalidateDeviceToken`: the device row is kept, disabled, and the user's other devices still get the push. A recipient with no enabled device, or whose every device was reported dead, is graded `invalid_recipient`; no push service is refused `transport_not_configured`, as the sms and email channels refuse it. -- **`@objectstack/spec`: description and comment text only.** The `[PLANNED]` markers come off `PushServiceConfigSchema`, `PushDirectTransportConfigSchema`, `ApnsTransportConfigSchema`, `FcmTransportConfigSchema` and `PushDeliveryDataSchema`, which now have their reader and producer; `PushRelayTransportConfigSchema` keeps `[PLANNED — not yet served]`, now saying the push service refuses it at boot (#29920). `NotificationChannelSchema`'s JSDoc lists `push` among the registered channels, the docblocks name `@objectstack/service-push` where they named `plugin-notification-push`, and the `NOT_FOUND` synonym waiver's reason names service-messaging's own provenance row. No key, schema shape or export changes. diff --git a/.changeset/29042-native-push-registration.md b/.changeset/29042-native-push-registration.md deleted file mode 100644 index 0fe03e7ebe8..00000000000 --- a/.changeset/29042-native-push-registration.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/console': patch -'@object-ui/mobile': patch -'@object-ui/auth': patch ---- - -feat(console,mobile,auth): the Console registers the mobile shell's push token under its own session and unregisters it on sign-out and when the shell removes the connection - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@object-ui/console`:** in the ObjectStack mobile shell, a signed-in person who has reached Home or an app is asked for push permission once per page load, after a short delay (never on a sign-in screen): the Console calls `getPushToken()` and registers the token with `POST /api/v1/notifications/devices`. A `null` answer (simulator, permission denied, no token) sends nothing. A token the shell rotates is registered again; an unchanged token for the same person is not sent twice. Another person signing in on the page registers the device for them, and the registry moves the token away from the previous person. An impersonation never registers, so the impersonated person's pushes never reach the administrator's device. The registration sends `deviceId` as the shell's `installId` when it has one, and `origin` only when the page was loaded over `http:` or `https:`. The device is unregistered (`DELETE /api/v1/notifications/devices/:deviceId`) before sign-out ends the session, and when the shell removes this connection; a shell that is neither iOS nor Android registers nothing. A browser is unchanged. -- **`@object-ui/mobile`:** `ObjectStackNative` gains the optional `installId` (`string | null`), and the package exports `NATIVE_PUSH_TOKEN_CHANGED_EVENT` (`objectstack-native:pushTokenChanged`, announced as `has('pushTokenChanged')`) with its event type `NativePushTokenChangedEvent`. Both are feature-detected: a shell that predates them carries neither. -- **`@object-ui/auth`:** `registerBeforeSignOut(step)` registers a step that `signOut()` awaits while the session still exists, and returns the function that removes it. The steps run together and can never hold sign-out back: a rejection is reported and dropped, and sign-out goes ahead after `BEFORE_SIGN_OUT_TIMEOUT_MS` (3 s) whether or not they have settled. diff --git a/.changeset/29255-installer-client-source-skill.md b/.changeset/29255-installer-client-source-skill.md deleted file mode 100644 index dd942483594..00000000000 --- a/.changeset/29255-installer-client-source-skill.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): the automation skill's OAuth recipe teaches the installer-supplied client - -Clause-②: no -Metadata-protocol: none - -Recipe 6 of `objectstack-automation/references/integration-recipes.md` now shows `clientSource: 'installer'` beside a declared `clientId` + `credentialRef`, with when to use which: a declared client for an app its publisher deploys, the installer form for one installed on deployments the publisher never sees. The administrator registers a client with the vendor against the `redirectUri` the connection's status read answers, connects with `{ clientId, clientSecret }` (no secret for a public client; a reconnect naming no client keeps the stored one), and the connection reads back `client: { source: 'installer', clientId, hasSecret }`, never the secret. diff --git a/.changeset/29395-skills-search-auto-default.md b/.changeset/29395-skills-search-auto-default.md deleted file mode 100644 index b669604a8e6..00000000000 --- a/.changeset/29395-skills-search-auto-default.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): with no `searchableFields` declared, a field marked `searchable: true` joins the default search set - -Clause-②: no -Metadata-protocol: none - -- **Before.** `objectstack-data`, `objectstack-query` and the `objectstack-ui` list-views rule taught the record-search auto-default as type-only: the name field plus the text-like and `select` columns. A field marked `searchable: true` was described as doing nothing, and a `$searchFields` override naming one as refused. -- **Now.** Each says what the platform resolves: with no `searchableFields` declared, the set `search` scans, and the set a `$searchFields` override may name, is the type-based default plus every field marked `searchable: true`, of any type except `secret`, `password`, `vector` or a virtual `formula`. Each also says what does not change: `searchable: false`, the default, never removes a field the type default scans, and declaring `searchableFields` chooses the set. The enum half of the type default is spelled `select` alone, as the resolver has it. -- No schema, key or behaviour changes. diff --git a/.changeset/29411-platform-skill-package-contributions.md b/.changeset/29411-platform-skill-package-contributions.md deleted file mode 100644 index c6bb512c0b2..00000000000 --- a/.changeset/29411-platform-skill-package-contributions.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): the platform skill teaches the `contributions` member of the package customizations read - -Clause-②: no -Metadata-protocol: none - -`skills/objectstack-platform/SKILL.md` now names `contributions` beside the overrides, extensions and switched-off items that `GET /packages//customizations` lists: one entry per item another package adds to the base, stamped with its `packageId`; a `field` or `flow` entry names the base's `object` it is on, a `nav` entry names the base's `app`, its `group` when it targets one, and the item id as `name`; `counts.contributions` counts items, not packages. The text is paid for by deleting three restatements in the same file, so the token ceiling is unchanged. diff --git a/.changeset/29418-record-node-operand-value-docs.md b/.changeset/29418-record-node-operand-value-docs.md deleted file mode 100644 index 613aec2d65b..00000000000 --- a/.changeset/29418-record-node-operand-value-docs.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): objectstack-automation says a flow's record nodes treat a computed filter operand as a value, never as a query operator - -Clause-②: no -Metadata-protocol: none - -- **Now.** § Filter operands states the rule from the service-automation change: a computed operand is a string, number, bigint, boolean or date, or at an `$in` / `$nin` / `$between` comparand a list of them with no empty entry. Anything else fails the node as a guard before the query runs. The fix: make the expression evaluate to the compared value (for a record, its `id`), and write conditions in the filter itself. The table of failures a `fault` edge does not route now names that refusal beside the `null` operand. -- `SKILL.md` stays under its token ceiling (no ceiling moves). No schema, key, command or behaviour changes. diff --git a/.changeset/29425-declared-record-input.md b/.changeset/29425-declared-record-input.md deleted file mode 100644 index 475bc4af1aa..00000000000 --- a/.changeset/29425-declared-record-input.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/runtime': patch -'@objectstack/spec': patch ---- - -fix(runtime): a flow start input declared a record id (`objectName`) is loaded in the caller's own scope from the declared object at every door that starts the flow by name, before the run exists - -Clause-②: yes (narrowing) — a start that ran before is now refused when it hands a declared record-id input an id the caller cannot read in the declared object, an id that names no record there, or a value that is not one id. No flow shipped in this repository declares the key, and an input without the declaration answers exactly as before. -Metadata-protocol: none - - - -**The rule.** A flow start input may declare that its value is the id of a record of a named object, with `objectName` on the variable (`{ name: 'accountId', type: 'text', isInput: true, objectName: 'crm_account' }`). Every door that starts the flow by name now loads such an input from the declared object in the caller's own scope before the run exists, whether or not the flow's definition reads the input in plain sight. A record the caller cannot read there and an id that names no record get the same answer, `404 RECORD_NOT_FOUND` (the action door's own refusal), and no run is created. A value that is not one id (a string or a number) is refused `400` naming the input: `VALIDATION_FAILED` from the start check, or, at an action whose param declares a type, `VALIDATION_ERROR` from that param's own contract, which answers first. The system principal is not judged. - -**Which doors.** `POST /api/v1/automation/:name/trigger` and the legacy `POST /api/v1/automation/trigger/:name`; a declared endpoint of `type: 'flow'`; an action of `type: 'flow'` over the REST action route and the MCP `run_action` tool. All of them ask the one check they already shared for inputs a definition reads in plain sight. - -**Beside the definition's own reads, never in place of them.** An input declared a record id of one object and also read by the definition as an id of another (a `get_record` node's `filter.id`, say) is loaded from both, so the caller must be able to read both rows. A parent flow that hands its input on, unchanged, to a `subflow` or `map` callee whose input carries the declaration is judged by that declaration too. A `loop` or `map` that later rebinds the name does not withdraw the declaration: it is the author's word about the value the caller sent. - -**What stays as it was.** An input without the declaration keeps the visible-read rule: it is judged only where the definition reads it as a record id. A start whose caller can read the declared record starts as before. - -**If a start now answers 404 or 400.** Hand the declared input the id of a record the caller can read in the object the input names, or start the flow as the system principal. - -`@objectstack/spec`: the `objectName` describe on a flow variable no longer says that no door applies it, and its liveness row is `live`, so `os validate` no longer warns `liveness-planned-property` on a flow that declares it. diff --git a/.changeset/29461-d11-added-locale-keeps-write.md b/.changeset/29461-d11-added-locale-keeps-write.md deleted file mode 100644 index 87069733985..00000000000 --- a/.changeset/29461-d11-added-locale-keeps-write.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): objectstack-platform says the D11 close refuses a package-less write only where the row moves as a declared override, and an added email-template locale keeps that write - -Clause-②: no -Metadata-protocol: none - -- **Before.** The ADR-0146 customization paragraph said a pre-D11 unbound Setup row's package-less save, reset and rollback are refused once it is movable. That is false for a row of an email-template locale the base does not ship: `os migrate packageless-overlays` moves it as the pair's own member (`addedMember`, no `overrides`), and the close judges only the declared-override move, so that row's package-less write stays open before and after its move. The clause also left out publish, which the close refuses too. -- **Now.** The clause says the close covers a row movable as a declared override, lists save, publish, reset and rollback, and says an email-template row at a locale the base does not ship moves as the pair's own member and keeps that write. The file stays at its token ceiling: a duplicate "CLI Commands" pointer section is cut, its home the Part 3 pointer to `references/operations.md`. No rule is removed; no schema or behaviour changes. diff --git a/.changeset/29506-title-format-refused-teaching.md b/.changeset/29506-title-format-refused-teaching.md deleted file mode 100644 index 864fa67058d..00000000000 --- a/.changeset/29506-title-format-refused-teaching.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): teach that a save and `os validate` refuse `titleFormat` - -Clause-②: no -Metadata-protocol: none - -- `objectstack-data` (object properties table) and `objectstack-formula` (template carriers) no longer call `titleFormat` deprecated: since #29499 the metadata save gate and `os validate` refuse an object that declares it (`title-format-retired`), and it moves to `nameField`. The formula skill drops the sentence that restated the template grammar cell beside it. -- ADR-0079 D3 gains a dated amendment note citing ruling 6574393684 and #29499; the decision is unchanged. diff --git a/.changeset/29512-approval-decision-outputs-skill.md b/.changeset/29512-approval-decision-outputs-skill.md deleted file mode 100644 index 351005244e4..00000000000 --- a/.changeset/29512-approval-decision-outputs-skill.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): the automation skill teaches that an approval's decision outputs are enforced at the decision - -Clause-②: no -Metadata-protocol: none - -- **Before.** `skills/objectstack-automation/references/state-machines-and-approvals.md` said only that a typed `decisionOutputs` entry renders the matching record picker in the decision dialog (`multiple` collects an id array). An agent writing an approval flow from it could read `type` and `multiple` as widget hints and not know that a value the next step cannot use is refused. -- **Now.** The *Recording a decision* section says what the approvals service enforces, on approve and reject alike and before any write: a picker value must resolve through the `expression` approver's resolvers (`user` by any account's record id, `department` by record id in the request's organization, `team` by record id not owned by another organization, `position` by the machine name of a position someone in that organization holds); `multiple: true` takes a list of distinct values for every kind, bare keys included, and without it one value; any other non-blank value is refused with `400 VALIDATION_FAILED` naming `outputs.KEY`. The Console sends a list for a `multiple: true` text output. The existing `lead_review` node is the example. The file stays within its token ceiling (5724 of 5730). diff --git a/.changeset/29604-approver-directory-refusal-queue-paging.md b/.changeset/29604-approver-directory-refusal-queue-paging.md deleted file mode 100644 index f9b0dac2830..00000000000 --- a/.changeset/29604-approver-directory-refusal-queue-paging.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/plugin-approvals': patch -'@objectstack/plugin-sharing': patch ---- - -fix(plugin-approvals)!: an approval node refuses to open when a team or position approver's directory read fails, or a queue approver's expansion rejects, and the ownership-queue reads page to completion - -Clause-②: yes (narrowing) — an approval node no longer opens when a `team` or `position` approver (static, or an `expression` approver with `resolveAs` `team` / `position`) cannot be expanded because a directory read or the cross-organization read filter failed, or when a `queue` approver's expansion rejects. Under every `onEmptyApprovers` policy, the `fallbackApprovers` slate included, it now fails the node with a retryable `APPROVER_DIRECTORY_UNREADABLE:` error naming the read, where it used to open on the rest of the slate, on the fallback slate or on the administrators, or auto-approve. The registered ownership-queue service does not reject yet: a failed member read inside it still answers nobody until #29604 part 2 (#30251), so a `queue` approver refuses here only when `expandMembers` itself rejects. Every other input answers as before. -Metadata-protocol: none - - - -**BREAKING** accept-set narrowing at the approval node (`@objectstack/plugin-approvals`): a failed directory read behind a `team` or `position` approver, or a `queue` approver whose expansion rejects, refuses the node instead of opening it on a slate that is missing people (ADR-0142 D6 as amended, ruling record 6599731698 on #29604). - -- **Before.** A `team` or `position` approver whose `sys_team_member`, `sys_user_position` or `sys_member` read failed kept its `type:value` literal slot, which nobody can act on, and logged the failure. A `queue` approver whose expansion threw answered "expanded to nobody" and added no slot. Either way the node went on as if those people did not exist: beside other approvers it opened on them alone, and on an otherwise empty slate the node's `onEmptyApprovers` policy acted, opening on the `fallbackApprovers` or the organization's administrators, or auto-approving. -- **Now.** The node does not open and writes no request, approver or action row. It fails with an `ApproverDirectoryUnreadableError` whose message starts `APPROVER_DIRECTORY_UNREADABLE:`, names the approver, the organization and the read that failed (a failure of the cross-organization read filter is named as that filter's read, on a static approver and on an `expression` one alike), and is retryable: the flow node's failure is a runtime one, so the run can be resumed once the directory can be read. A `fallbackApprovers` slate whose own `team` or `position` read fails refuses the same way. `department` / `business_unit`, `org_membership_level` and `manager` approvers are not named by the ruling and answer exactly as before. -- **Unchanged.** A decision on a request opened before the approver snapshot existed still re-reads its slate on the old path, which keeps the literal slot on a failed read, because the decision has already been recorded when that read happens. -- **Not yet.** The ownership-queue service shipped in `@objectstack/plugin-sharing` answers a failed `sys_queue` / `sys_queue_member`, team or position read with nobody (logged once) rather than rejecting, so a `queue` approver backed by it still adds no slot on such a failure. Its refusal arrives with #29604 part 2, once the contract can carry it (#30251). - -`@objectstack/plugin-sharing`: the ownership-queue service reads its `sys_queue` and `sys_queue_member` rows by id, page after page, to the end, where it read one page of 10,000 rows and answered on it. A queue's 10,001st member, an organization's 10,001st queue and the queues naming a recipient past that bound are now found. A read that fails partway answers as a failed read did before (that read contributes nobody, logged once, with the read's reason and without the walk's own "refuses instead" clause), never with the pages it had read. diff --git a/.changeset/29604-queue-service-refuses-failed-read.md b/.changeset/29604-queue-service-refuses-failed-read.md deleted file mode 100644 index 012338227ae..00000000000 --- a/.changeset/29604-queue-service-refuses-failed-read.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/plugin-sharing': patch -'@objectstack/plugin-approvals': patch ---- - -fix(plugin-sharing)!: the ownership-queue service refuses a failed member read when the caller asks, and the approver slate and the sharing-recipient grant recompute ask - -Clause-②: yes (narrowing) — `expandMembers` and `isMember` of the registered ownership-queue service, called with `onFailedRead: 'refuse'`, now reject when a read behind the queue fails, where they answered nobody. The approver slate asks for that answer, so an approval node whose `queue` approver cannot be read refuses to open under every `onEmptyApprovers` policy, where it opened on the rest of the slate, on the fallback slate or on the administrators, or auto-approved. The sharing-rule recompute asks for it too, so a whole-rule pass (`evaluateRule`, the REST evaluate endpoint, the boot backfill), the recipient-axis revoke and the per-user pass on a `queue`-recipient rule reject on such a failure, where they completed by revoking the grants of the members who could not be read. Every call without the option, and every read that succeeds, answers as before. -Metadata-protocol: none - - - -**BREAKING** accept-set narrowing at the ownership-queue service's `'refuse'` answer and at its two recompute consumers (`@objectstack/plugin-sharing`, `@objectstack/plugin-approvals`): an empty expansion is a fact, a failed read is a fault (ADR-0142 D2 as amended), and the approver slate (D6) and the sharing-recipient grant recompute (D7) now get the fault instead of "nobody". - -- **The service.** `OwnershipQueueService.expandMembers(name, org, { onFailedRead: 'refuse' })` and `isMember(user, name, org, { onFailedRead: 'refuse' })` reject when the queue's `sys_queue` row, its `sys_queue_member` rows, or a member table one of its specifications reads (`sys_team_member`, `sys_user_position`, `sys_member`, `sys_business_unit_member`, `sys_business_unit`) cannot be read, or a paged read cannot vouch for completion. The rejection's message starts `[queue] queue '' in organization '' does not answer its members, because a read behind it failed:` and names that read; the read's own error rides along as `cause`. It never answers `[]`, a partial set or `false` instead. `isMember` still answers `true` when a read that succeeded reaches the user, and answers `false` only when every read behind it succeeded. An empty queue is still `[]`, and a name that resolves nowhere is still `[]` / `false`. -- **Unchanged: the default.** Without the option (`onFailedRead: 'nobody'`), a failed read still contributes nobody, logged once. The sitting-record access path (`queuesOf`), the claim admission and the assignment pool take that answer, so they deny on a failed read exactly as before. A failed read of the activation ledger (`sys_metadata_activation`) is not a failed read in this sense under either answer: it still reads "switched on" for that call, logged, the posture the sharing-rule service takes for its own position recipients. -- **The approver slate (D6).** A `queue` approver asks with `onFailedRead: 'refuse'` beside the addressing purpose. When the registered service rejects, the node fails with the retryable `APPROVER_DIRECTORY_UNREADABLE:` error that a failed team or position read already raises, naming the queue's failed read, and writes no request, approver or action row. Before, the registered service answered nobody, so the queue added no slot and the node's `onEmptyApprovers` policy acted on a slate missing those people. -- **The sharing-recipient grant recompute (D7).** A `queue`-recipient rule's expansion and its per-user membership check ask with `onFailedRead: 'refuse'`. A failed member read now rejects before any grant is diffed, so the rule's existing grants stay as they were until a pass can read the queue, and each recompute's caller logs it: the record pass and the object pass per rule (`record reconcile failed for rule`, `object reconcile failed for rule`), the queue-membership and business-unit write recomputes (`queue-membership revocation failed`, `queue-membership re-grant failed`, `business-unit revocation failed`, `business-unit re-grant failed`), the membership-row recompute (`recipient membership re-evaluation failed`) and the boot backfill (`boot rule backfill failed for rule`). A direct call to `evaluateRule`, `revokeRuleGrantsForRetiredRecipients` or `reconcileRuleForUser`, the REST evaluate endpoint included, rejects with the service's failure. Before, the failed read answered nobody and the pass revoked the grant of every member behind it. This is the answer a `team` or `position` recipient has given since its own reads began to refuse. -- **Supersedes the approval node's "Not yet".** The approval node's own entry in this release ("an approval node refuses to open when a team or position approver's directory read fails…") says, in its `Clause-②:` line and its **Not yet** bullet, that the registered ownership-queue service answers a failed member read with nobody rather than rejecting, so a `queue` approver backed by it refuses only when `expandMembers` itself rejects. That was true when it was written and is not after this change: the service now rejects on that failure when asked, and the slate asks, so a `queue` approver backed by it refuses on a failed `sys_queue`, `sys_queue_member`, team, position or business-unit read, as a `team` or `position` approver does. Read this bullet in place of those two sentences. diff --git a/.changeset/29625-stdio-credential-env.md b/.changeset/29625-stdio-credential-env.md deleted file mode 100644 index 661db8eb5c6..00000000000 --- a/.changeset/29625-stdio-credential-env.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): the automation skill's OAuth recipe names a stdio MCP server's credential route as `transport.credentialEnv` - -Clause-②: no -Metadata-protocol: none - -Recipe 6 of `objectstack-automation/references/integration-recipes.md` said a `stdio` server's credentials ride `transport.env` — the literal-in-metadata route the `connector-credential-literal` advisory warns against. It now says what the `mcp` provider does: the grant-bearing kinds stay `http` only; a `stdio` server takes a static `auth: { type: 'bearer', credentialRef }`, handed to the child process in the variable `transport.credentialEnv` names, never a literal in `transport.env`. Paid inside the file's token ceiling by dropping the opening paragraph's restatement of two facts that keep their homes: the packaged spec (SKILL.md § Integrations are declared, not coded) and where an `api` hook's secret goes (SKILL.md's `secretRef` row and recipe 4). No eval asserted the old route; none changes. diff --git a/.changeset/29653-migrate-blank-multi-values.md b/.changeset/29653-migrate-blank-multi-values.md deleted file mode 100644 index c0278226f29..00000000000 --- a/.changeset/29653-migrate-blank-multi-values.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -feat(cli): `os migrate blank-multi-values` rewrites the `[]` and `''` still stored on optional multi-value columns to `null` - -Clause-②: no -Metadata-protocol: none - -Since #29343 the write door stores a cleared optional multi-value value (`[]` or `''` on a `multiselect`, `checkboxes` or `tags` column, or on a `multiple: true` `select`, `radio`, `lookup`, `user`, `file` or `image` column) as `null` (ruling A on #29343). Rows cleared before that still hold the JSON text `'[]'` or `'""'`, so `= null`, grouping and the read shape split them from rows cleared since. This new command is the one-time clean that ruling's execution step 3 binds, an `os migrate` data step of its own beside `blank-single-values`, not part of `os migrate value-shapes`, which rewrites nothing. - -- **Columns.** Read from `blankAsNullColumns` in `@objectstack/core`, the rule the write door and the seed replay read: its optional columns on the `$empty` table's `multi_value` row. A `required` column, a `readonly` column (a platform-stamped `[]`, such as `sys_activity.changed_fields`) and a single-value column are never touched, nor is a non-empty list. -- **Dry run by default.** It counts the stored `[]` and `''` per object and column and writes no row. `--apply` rewrites each affected row's blank columns to `null` through the engine. `--object` narrows the walk and refuses a name the deployment does not declare. `--apply` refuses a SQLite file another process holds unless `--force`. -- **No flag.** Nothing is gated on it. A re-run that reports zero is the verification. diff --git a/.changeset/29801-related-list-placement-skill.md b/.changeset/29801-related-list-placement-skill.md deleted file mode 100644 index b43fb73bcbe..00000000000 --- a/.changeset/29801-related-list-placement-skill.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): objectstack-data teaches `relatedList` placement by each value's role, not by record-page tabs or columns - -Clause-②: no -Metadata-protocol: none - -- **Before.** `rules/relationships.md` said `relatedList: 'primary'` draws "a full-width table in the details column after the first field section (in the Details tab, no tab of its own)" and that `true` / omitted is "a side card (the shared **Related** tab)". The spec's `relatedList` describe no longer names any tab or column, so the skill taught a page layout the record page does not draw. -- **Now.** The three bullets follow the spec describe: `'primary'` is part of the record's main content, a full-width table in the record's detail body (still the default for an inline-edited `master_detail` FK); `false` means the child object is not shown on the parent record page; `true` or omitted is an auxiliary panel on the record page. Layout stays ADR-0085's to define. -- The file gets smaller and stays under its token ceiling; no ceiling moves, no schema, key or behaviour changes. diff --git a/.changeset/29818-push-device-locale-descriptions.md b/.changeset/29818-push-device-locale-descriptions.md deleted file mode 100644 index 94efa2e59c6..00000000000 --- a/.changeset/29818-push-device-locale-descriptions.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch -'@objectstack/service-messaging': patch ---- - -fix(spec,messaging): a push device's `locale` is described as what it is — stored for the device, and no push renders by it - -Clause-②: no -Metadata-protocol: none - -- **`@objectstack/spec`**: `UserDeviceSchema.locale` and `RegisterDeviceRequestSchema.locale` no longer say pushes to the device are rendered in its locale. They now say the value is stored for the device and that a push arrives in the recipient's language: `sys_user.locale`, then the deployment default. Description text only; nothing a caller sends is accepted or refused differently. -- **`@objectstack/service-messaging`**: the `sys_user_device.locale` field description says the same, in all four shipped languages. A test now pins it end to end: with the real push channel over the real device registry, a phone registered with a locale other than its user's receives the push in the user's language, on the inline fan-out and on the durable outbox path alike. diff --git a/.changeset/29820-push-capability-serve.md b/.changeset/29820-push-capability-serve.md deleted file mode 100644 index b5b11c7950c..00000000000 --- a/.changeset/29820-push-capability-serve.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/core': minor -'@objectstack/service-push': minor -'@objectstack/cli': minor -'@objectstack/verify': minor ---- - -feat(push): `requires: ['push']` and the stack's `push` key turn mobile push on under `objectstack serve`, and a relay configuration is refused at boot - -Clause-②: yes (widening) -Metadata-protocol: added -Skills-docs: follow-up owed — #29969 (skills/objectstack-platform/evals/config-plugins-ops.json: no eval asks for `requires: ['push']` or the `push` key); this diff teaches both in `skills/objectstack-platform/SKILL.md` (the top-level key enumeration and the `requires` table) and in the new `content/docs/kernel/runtime-services/push-service.mdx` - -A deployment that runs `objectstack serve` from a stack file can now turn on mobile push without host code. Before this, the only way to register a push service was a host that composed `PushServicePlugin` in code, so a `notify` node naming `push` had nothing to deliver through on a served stack. - -- **`@objectstack/spec`: the `push` capability token and the `push` stack key.** `push` joins `PLATFORM_CAPABILITY_TOKENS`, provided by `@objectstack/service-push` in the open edition. It is requested, never on the always-on slate. The stack definition gains a top-level `push` key beside `email` and `sms`: it is `PushServiceConfigSchema`, composed as a single value (`COMPOSE_KEY_DISPOSITIONS`). Authoring accepts a direct block, a relay block and absence; it refuses an inline secret, by key name (`key`, `privateKey`, `serviceAccount`, …) with `credentialRef` as the remedy, or pasted into a `credentialRef` (ADR-0143 D3). `PushServiceConfigSchema`'s docblock now names the stack key and `objectstack serve` as its reader; `PushRelayTransportConfigSchema` keeps its `[PLANNED — not yet served]` marker. -- **`@objectstack/core`: the `push` provider row and its argument.** `CAPABILITY_PROVIDERS.push` names `PushServicePlugin`, and `resolveCapabilityArgument('push', …)` builds its argument from the stack's `push` key through `resolvePushCapabilityArg`, read off the provider module as the `email` and `sms` readers are. Both `objectstack serve` and `@objectstack/verify`'s `bootStack` read this table, so an app's tests get the push service its server gets. -- **`@objectstack/service-push`: `resolvePushCapabilityArg`.** It parses the `push` key with `PushServiceConfigSchema` and refuses, before the kernel boots, two configurations: the relay transport (an absent key and `push: {}` read as it), whose sender has not shipped, with a message that says so and prescribes `transport: 'direct'` with an `apns` and/or `fcm` block; and anything the schema refuses, an inline secret first among them, naming each path and the schema's guidance and never the value. On a stack that declares `requires: ['push']` either refusal fails the boot as `Capability "push" (@objectstack/service-push) failed to start: …`. -- **`@objectstack/cli` and `@objectstack/verify`** depend on `@objectstack/service-push`, so the provider the token names resolves out of the box, as the open edition promises. - -To turn push on, add `requires: ['push']` and a direct block naming each vendor's secret by reference: - -```ts -push: { - transport: 'direct', - apns: { teamId: 'ABCDE12345', keyId: 'KEY1234567', topic: 'com.example.app', environment: 'production', credentialRef: 'APNS_SIGNING_KEY' }, - fcm: { projectId: 'example-app', credentialRef: 'FCM_SERVICE_ACCOUNT' }, -}, -``` - -In the open tier each `credentialRef` is the name of an environment variable holding the secret. A `push` key on a stack whose `requires` does not name `push` is not read, and `objectstack serve` logs one boot warning saying so. The runtime-services chapter has a new `services.push` page. diff --git a/.changeset/29851-contributed-nav-labels.md b/.changeset/29851-contributed-nav-labels.md deleted file mode 100644 index ce9cec4f258..00000000000 --- a/.changeset/29851-contributed-nav-labels.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -feat(studio): a contributed nav entry shows the label the live app shows, and a package that only contributes navigation says where it goes - -Clause-②: yes (widening) -Metadata-protocol: none - -- **The label.** A navigation entry another package contributes to a base's app now shows the label the live app shows, with its id as a muted second line under it. Before, the row showed only the id. The label is looked up by `(app, id)` in the app row the Interfaces pillar already reads (`GET /meta/app?package=`), which serves the folded navigation: the served entries the app's published layer does not hold. No request is added and nothing is read from the contributor's manifest. An id the base also holds, or one two contributions share, keeps the bare id, so no label is guessed. A label-less entry reads what it inherits, through the rail's own resolver. A never-published app has no served row, so its contributed rows keep the id. -- **The contributor's empty state.** A package with no app of its own whose listed manifest declares `navigationContributions` now says what it adds: "This package has no app of its own. It adds 2 navigation entries to HotCRM's app.", with a button that opens that package. The declared app is an address only. Its owner comes from the served app read (`GET /meta/app/:app`), and the count comes from the owner's served customization read (`GET /packages/:owner/customizations`), counting the `nav` contributions stamped with this package for that app. That is at most two reads per declared target app, sent only on that empty state. When the package is disabled, the target app is missing, either read fails, or the count is 0, the text stays "This package has no app yet." -- **New strings.** `@object-ui/i18n`'s studio namespace gains five keys in `en` and `zh` (`engine.studio.if.noOwnApp`, `.noOwnAppTitle`, `.contributesOne`, `.contributesOther`, `.openOwner`). diff --git a/.changeset/29872-export-carries-manifest-object-extensions.md b/.changeset/29872-export-carries-manifest-object-extensions.md deleted file mode 100644 index f4d7d137b50..00000000000 --- a/.changeset/29872-export-carries-manifest-object-extensions.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/objectql': patch -'@objectstack/metadata-protocol': patch -'@objectstack/runtime': patch ---- - -fix(objectql, metadata-protocol, runtime): a package's export carries the `objectExtensions` it was installed with, and the extended object's owner's export does not - -Clause-②: yes (widening) -Metadata-protocol: none - -A package installed through `POST /api/v1/marketplace/install-local` (what `os package install` sends, and how every entry of a whole-deployment export is restored) that extends another package's object exported without its `objectExtensions`: `GET /api/v1/packages//export` answered the package's other items and no extension, while the extension's fields were folded into the owner's export instead, as if the owner had shipped them. So a deployment restored from an export lost those fields on its next export, with no error, and a `.custom` pair installed from its export no longer exported its extension. - -- **The extending package's export** (and its entry in `GET /api/v1/packages/export`) now carries every extension it contributes as `objectExtensions`, however it was installed: an extension published through `/api/v1/meta/object_extension` (as before) or a manifest's `objectExtensions` entry (install-local, or a code package). A published extension is exported as it is stored; a manifest entry as it was registered, with `priority` written only when it is not the default `200`. -- **The extended object's owner's export** now carries the object as the owner declares it, without the fields, field patches or labels of ANY other package's extension. Until now only its own `.custom` pair's extension was left out; an extension from any other installed package was folded in. -- **Every served read is unchanged**: `GET /api/v1/meta/object/`, the object list, the console and the data API keep serving the object with every extension folded in. - -`SchemaRegistry` gains `getObjectExtensionsOf(packageId)`, which reads a package's `extend` contributions back as the `objectExtensions` entries they were registered from (the inverse of `registerObjectExtension`). `listItems`, `getAllObjects` and `foldObjectExtendersOnto` take an `onlyExtensionsOf` option beside `withoutExtensionsOf`: each object is resolved with only that package's `extend` contributions folded in. The metadata protocol's per-package layer (the package export) now passes `onlyExtensionsOf`, naming the listed package, and no longer passes `withoutExtensionsOf`, which keeps its meaning for any other caller. - -Supersedes, in the same release: the `.custom` pair note for #29903 says an extension contributed by any other installed package is still folded into the base's object in the base's export. After this change it is not: the base's export leaves out every other package's extension, and that package's own export carries it. diff --git a/.changeset/29873-install-prints-withheld-maps.md b/.changeset/29873-install-prints-withheld-maps.md deleted file mode 100644 index 77bc00adb92..00000000000 --- a/.changeset/29873-install-prints-withheld-maps.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os package install` prints the connector maps the package export withheld (#29873) - -Clause-②: no -Metadata-protocol: none - -An exported package names each connector header or environment map the export withheld whole in its `withheldMaps`, and both install modes of the runtime's install-local endpoint name each map in their answer. The command dropped that list, so an operator who installed an export the documented way (`os package install DIR/NN-ID/objectstack.json`) was never told that the connector sends none of those headers or environment variables. - -The command now prints one warning, one line per map (`connector 'NAME' PATH`, for example `connector 'erp' providerConfig.headers`), and the remedy: re-enter each map where the connector is authored. It does this for a managed install (hot-loaded or not) and for an unmanaged one, from a catalog id or an artifact file. An answer without the list prints nothing new, and the exit code is unchanged. The command has no `--json` output. diff --git a/.changeset/29879-datasource-update-kept-pool-stores-nothing.md b/.changeset/29879-datasource-update-kept-pool-stores-nothing.md deleted file mode 100644 index 79bba6191d6..00000000000 --- a/.changeset/29879-datasource-update-kept-pool-stores-nothing.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/service-datasource': patch ---- - -fix(service-datasource)!: a datasource update whose new configuration fails to connect stores none of it, and a whitespace-only `secret` is refused like an empty one - -Clause-②: yes (narrowing) — `PATCH /api/v1/datasources/:name` and `POST /api/v1/datasources` refuse one more body that used to answer 200 / 201: a `secret` whose value is whitespace only (`" "`, a tab and a newline), now `400 DATASOURCE_ADMIN_ERROR` with the empty-secret answer. The widening half: the `reregisterPool` member of the exported `DatasourceAdminServiceConfig` may now answer `{ previousPoolKept: boolean }` (still optional; a seam that answers nothing reads as before). -Metadata-protocol: none - - - -**BREAKING** accept-set narrowing at the datasource admin doors (`@objectstack/service-datasource`): a whitespace-only `secret` is refused. - -- **Before.** A `PATCH` whose new configuration failed to connect (a wrong password, a blank `secret`, a changed `config` that does not reach the database) answered `200` with `status: "error"` and a reason saying the previous pool was kept and the new configuration was NOT in force. It had already stored the new record, repointed `external.credentialsRef` at a new `sys_secret` row and removed the working one, so the next restart connected the configuration that failed. A whitespace-only `secret` passed the empty-secret guard and reached this path. -- **Now.** The update rebuilds the pool before it stores anything. When the previous pool is kept, nothing of the patch is stored: the record, its `credentialsRef` and every other key of the patch stay as they were, and the `sys_secret` row sealed for the patch is removed again. The answer is unchanged (`200`, `status: "error"`, "the new configuration is NOT in force"), and now true: a restart connects the configuration that was serving. A configuration that connects is stored and in force, and the replaced secret is retired, as before. A whitespace-only `secret` is refused before anything is written, by the same guard and with the same message as the empty string; a value with whitespace around other characters is still sealed as sent. -- **Who moves.** The console sends `secret` only when the field holds a value. A script or SDK call that sends a whitespace-only `secret` gets the 400. A host that wires its own `reregisterPool` seam keeps today's behaviour until it answers `{ previousPoolKept: true }` for a rebuild that kept the previous pool. - -**The remedy.** To keep the stored credential, leave `secret` out of the request (or send `null`). To replace it, send the new credential. diff --git a/.changeset/29880-approvals-list-unknown-status.md b/.changeset/29880-approvals-list-unknown-status.md deleted file mode 100644 index c692c20fe3e..00000000000 --- a/.changeset/29880-approvals-list-unknown-status.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest)!: the approvals request list refuses a status value no approval request can have, and reads a comma list of statuses as a list - -Clause-②: yes (narrowing) — `GET /api/v1/approvals/requests` (and its environment-scoped twin) refuses a `status` value outside the set `sys_approval_request.status` declares (`pending`, `approved`, `rejected`, `recalled`, `returned`, `cancelled`) with `400 VALIDATION_ERROR`, where it answered `200 {"data":[]}`. An empty value or an empty comma entry is refused too. The widening half: a comma list of declared statuses now filters on all of them, where it matched no row. -Metadata-protocol: none - - - -**BREAKING** accept-set narrowing at the approvals request list (`@objectstack/rest`): a `status` value that is not a declared approval status is refused instead of answering an empty list. - -- **Before.** `?status=bogus`, `?status=Pending` or `?status=waiting` answered `200 {"data":[]}`, which reads the same as "no request is in that status", so an integration polling the inbox with a misspelt status skipped every pending approval in silence. `?status=pending,returned`, the spelling `@objectstack/client`'s `approvals.listRequests({ status: [...] })` sends for an array, was read as one status and answered the same empty list. `?status=` filtered on nothing and answered every status. -- **Now.** Each comma-separated entry must be a declared status, spelt exactly. Otherwise the request is refused with `400` and the nested `{ "error": { "code": "VALIDATION_ERROR", "message" } }` body the route already answers for an unknown or repeated parameter name; the message names the bad value and the allowed values, and the approval service is not asked. One declared status filters as before. Several filter on any of them (the service's `$in` push-down), and `total` counts on the same filter. A declared status with no rows still answers `200 {"data":[]}`. -- **Who moves.** The console and the client SDK send declared statuses only. A script that sends another spelling, or an empty `status=`, gets the 400. - -**The remedy.** Send one of `pending`, `approved`, `rejected`, `recalled`, `returned`, `cancelled` (lower case), or several separated by commas. To list every status, leave `status` out. diff --git a/.changeset/29891-email-send-display-name-400.md b/.changeset/29891-email-send-display-name-400.md deleted file mode 100644 index af6cd8bee88..00000000000 --- a/.changeset/29891-email-send-display-name-400.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/plugin-email': patch ---- - -fix(plugin-email): a non-string recipient display name answers the send door's 400, not 500 - -Clause-②: no -Metadata-protocol: none - -`POST /api/v1/email/send` with a recipient such as `{ "address": "ok@example.com", "name": 42 }` in `to`, `cc`, `bcc` or `replyTo` (or as one entry of a `to` / `cc` / `bcc` list) now answers `400 VALIDATION_FAILED` with "invalid recipient address in `to`: the display name of ok@example.com must be a string" (`reply-to address` for `replyTo`). It used to answer `500 EMAIL_SEND_FAILED` with `a.name?.trim is not a function`. - -The refusal itself was already right. The 500 came from the code that writes the `sys_email` rejection row after the refusal: it called `.trim()` on the caller's raw display name. That writer now reads the name as unknown input and leaves a non-string name out of the row (the row's `error` column names it), so the caller gets the 400 and the rejection is still recorded. A string name, an absent name and a `null` name behave as before. diff --git a/.changeset/29894-batch-row-hook-aborted.md b/.changeset/29894-batch-row-hook-aborted.md deleted file mode 100644 index 6e0011144c1..00000000000 --- a/.changeset/29894-batch-row-hook-aborted.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix(metadata-protocol): a hook that aborts with free text answers `HOOK_ABORTED` on the per-object batch row too, not `VALIDATION_ERROR` - -Clause-②: no -Metadata-protocol: none - -- **Before.** Since #27597 (and #27672) a sandboxed hook body that aborts with free text (`throw new Error('…')`, no `code`) answers `/api/v1/data/:object`, `/createMany` and `/api/v1/batch` with `400 HOOK_ABORTED` and the hook's sentence. `POST /api/v1/data/:object/batch` answered the same abort as a row `{ code: 'VALIDATION_ERROR', message: }`: the row mapper (`toRowApiError`) derived the code from the `400` the doors serve, so a bulk client branching on the code read a business refusal as malformed input. -- **Now.** The row answers `HOOK_ABORTED` (the `UNDECLARED_HOOK_REFUSAL_CODE` the other doors read). The value moves, from `VALIDATION_ERROR` to `HOOK_ABORTED`, on every row that shares the mapper: `POST /api/v1/data/:object/batch` with `operation` `create`, `update`, `upsert` or `delete`, in every mode (plain, `atomic`, `continueOnError`), and the per-row results of `updateManyData` / `deleteManyData`. -- **Unchanged.** A code the hook declared still wins (a registered code is its own row code; an unregistered one still gets the code its status derives). The row's `message` is still the hook's sentence and `httpStatus` is still absent when the body declared no status. A crashed body and a non-sandbox producer answer as before. No error code is added; `HOOK_ABORTED` is the one #27597 registered. diff --git a/.changeset/29895-user-locale-case-fold.md b/.changeset/29895-user-locale-case-fold.md deleted file mode 100644 index d078288bb06..00000000000 --- a/.changeset/29895-user-locale-case-fold.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix: a user's language preference is stored in its BCP-47 case, so `zh-cn` and `ZH-CN` are saved as `zh-CN` and the console renders that user's language (#29895) - -Clause-②: no -Metadata-protocol: none - -`sys_user.locale` used to store a tag as written. The column checks only the shape of a tag, so `zh-cn` was accepted, and the console, which picks its UI language by exact match, then rendered English for that user with no error. Every write of the column is now stored in BCP-47 case: language lower case, region upper case, script title case (`ZH-HANS-cn` → `zh-Hans-CN`). This applies to the data API (`PATCH /api/v1/data/sys_user/:id` and its bulk form), the admin user import and the identity-provider locale sync. The fold is the one `LocaleSchema` applies to translation locales. Nothing new is refused: `zh_CN` is still refused with 400, and an empty value still clears the preference. Rows stored mis-cased before this release are not rewritten. Such a row reads back as stored until the column is written again. diff --git a/.changeset/29896-field-sortable-false-honoured.md b/.changeset/29896-field-sortable-false-honoured.md deleted file mode 100644 index ee304c4221e..00000000000 --- a/.changeset/29896-field-sortable-false-honoured.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/metadata-protocol': minor -'@objectstack/mcp': minor ---- - -fix(spec,metadata-protocol,mcp)!: a field marked `sortable: false` is no longer offered or performed as a sort — the sortability projection reports it unsortable, and the Data API and the stdio MCP `query_records` refuse a sort on it with `400 INVALID_SORT` (#29896) - -Clause-②: yes (narrowing) — `GET /api/v1/data/:object` (and every request that reaches the same list door: `POST /api/v1/data/:object/query`, the export route, the RPC dispatcher, the record-page `related[].sort`) and the stdio MCP `query_records` refuse a sort on a field whose author declared `sortable: false`, which they used to answer sorted by the column; a stored list view `sort` or `record:related_list` `sort` naming such a field is now refused when it is read. The widening half: `@objectstack/spec/api` exports `FIELD_UNSORTABLE_AUTHOR_DECLARED`, and `FieldSortabilitySchema.reason` accepts `'author-declared'` beside `'virtual-type'`. -Metadata-protocol: narrowed -Skills-docs: none — `skills/**` names no `sortable` field key, and `content/docs/**` already teaches `sortable: false` as "do not allow sorting by this field" and `INVALID_SORT` as "the field must exist and have `sortable: true`", which is what the platform now does. The prescription ships in the refusal text and the upgrade-guide entry. - - - -**BREAKING** accept-set narrowing at the Data API list door (`@objectstack/metadata-protocol`) and the stdio MCP `query_records` (`@objectstack/mcp`): a sort on a field marked `sortable: false` is refused. - -- **Before.** An author who marked a field `sortable: false` (a long text, an unindexed column on a large table) got nothing for it. The sortability projection served on `GET /api/v1/meta/object/NAME` reported the field `{ sortable: true }`, so the list header and the Sort picker offered it, `GET /api/v1/data/NAME?sort=FIELD` answered 200 sorted by it, and so did the stdio MCP `query_records` with that `orderBy`. -- **Now.** The projection reports `{ sortable: false, reason: 'author-declared' }` for that field, so the grid header and the Sort picker, which read the projection, stop offering it. The Data API judges the sort by the same projection and refuses it with `400 INVALID_SORT` before any read, the answer a sort on a `formula` field already gets; the stdio MCP `query_records` refuses it the same way before the engine runs. Only the literal `false` counts: a field with no `sortable` key, or with `sortable: true`, sorts as before. A `formula` field also marked `sortable: false` keeps its `reason: 'virtual-type'` answer. -- **Stored sorts.** A list view whose `sort` names such a field, or a page `record:related_list` block whose `sort` does, used to load sorted and is now refused when it is read. Lint does not yet judge these at authoring. The upgrade-guide entry `field-sortable-false-sort-refused` lists them. -- **Not changed.** In-process callers that reach `ObjectQL.find` directly (hooks, flows) keep sorting by the stored column: the engine door judges only whether a column is stored, and `sortable: false` governs what the platform offers callers, not what its storage can do. - -**The remedy.** Drop the sort on that field or sort by another one, in requests and in stored view and related-list sorts. An author who wants the field sortable removes `sortable: false` from it. diff --git a/.changeset/29897-properties-panel-readonly-reason.md b/.changeset/29897-properties-panel-readonly-reason.md deleted file mode 100644 index c2425b3e4b1..00000000000 --- a/.changeset/29897-properties-panel-readonly-reason.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): on a read-only item, the Properties panel shown with nothing selected says why its controls are disabled - -Clause-②: no -Metadata-protocol: none - -On the metadata editor page (Studio's All metadata item page, Setup's metadata pages), a locked item, a read-only package, a caller without `manage_metadata`, a code-shipped artifact or a walled catalog type disables every control in the Properties panel. The scoped inspector (a selected flow node or edge) already said why; the panel shown with nothing selected (an object's or view's default inspector, or the generic form a flow shows) greyed its controls silently, and Tab never reached them. The page now wraps that panel, and the generic form on a page without a canvas, in the same reason wrapper the scoped inspectors use: hovering a disabled control or tabbing to the panel opens a tooltip with the reason, and the panel's accessible description is that reason. A writable item renders the panel exactly as before. No registered default inspector changes: the page draws the reason around it. diff --git a/.changeset/29898-picklist-binding-unknown-refused.md b/.changeset/29898-picklist-binding-unknown-refused.md deleted file mode 100644 index 07e2bb8fb19..00000000000 --- a/.changeset/29898-picklist-binding-unknown-refused.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor -'@objectstack/spec': minor -'@objectstack/cli': minor ---- - -fix(metadata-protocol)!: the metadata save and publish doors refuse a select field bound to a picklist no loaded package declares, instead of storing it and then refusing every write to the field (#29898) - -Clause-②: no (narrowing) — the metadata save door (draft and live), the per-item and package publish, and the restore doors refuse an `object` or `object_extension` whose field names a picklist no loaded package declares, a body they accepted before. Nothing is widened. -Metadata-protocol: narrowed -Skills-docs: no move — `skills/**` teaches no picklist reference verdict and `content/docs/**` carries no severity table for it; the prescription ships in the refusal text, the FROM → TO below and the upgrade-guide entry. - - - -**BREAKING** accept-set narrowing at the metadata save and publish doors: `PUT /api/v1/meta/object/NAME` and `PUT /api/v1/meta/object_extension/NAME` (draft and live), `POST /api/v1/packages/PKG/publish-drafts`, the per-item publish, and the rollback / revert restore doors. - -- **Before.** A select (or multiselect) field with `picklist: 'nope_list'`, a name no loaded package declares, saved `200` with its draft reading `_diagnostics.valid: true`, and `publish-drafts` answered `outcome: published`. The field was then served with no options, and every create or update that set it answered `400 VALIDATION_FAILED` ("takes its values from picklist \"nope_list\", which no loaded package declares"). The boot and a late install already refuse the same shape for a package's own objects, and `os validate` / `os build` refuse it for a stack's own fields. -- **Now.** The doors refuse it `422 INVALID_METADATA`, one finding per unknown binding at `fields.FIELD.picklist`, naming the field and the list. A list is declared when the engine's registry resolves it, the same read that serves the field's options: the package's own, a dependency's, the base package's of a `.custom` pair, or any other package installed here. -- **Unchanged.** A binding the object already holds is not judged again on a re-save, so an object published before this release keeps saving; the stored-metadata healing pass (`os migrate meta --stored`) is not judged. A draft stored before this release is judged when it is published. -- **`@objectstack/spec`.** The upgrade guide entry `picklist-binding-unknown-refused` (a semantic migration: no automatic rewrite, because whether the author meant another list, one still to be declared or inline options is a judgement about the field). -- **`@objectstack/cli`.** A source comment in the `os validate` picklist judge no longer says the runtime write path is unjudged. - -**FROM → TO.** `fields.t: { type: 'select', picklist: 'nope_list' }` → `picklist: 'NAME'` of a list a package installed here declares, or declare `nope_list` in a code package (a `*.picklist.ts` file, or `defineStack({ picklists })`), or `options: [...]` in place of `picklist`. diff --git a/.changeset/29899-publish-sheet-picklist-options.md b/.changeset/29899-publish-sheet-picklist-options.md deleted file mode 100644 index 15dc3f16e94..00000000000 --- a/.changeset/29899-publish-sheet-picklist-options.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): Studio's publish review sheet no longer lists an `Options` change on a picklist-bound field nobody edited (#29899) - -Clause-②: no -Metadata-protocol: none - -On an object with a field bound to a package picklist (`{ type: 'select', picklist: 'NAME' }`), the publish review sheet listed `~ FIELD · Options` on every publish, beside the change the author actually made. The draft is stored without the field's `options`, while the live read serves the options the runtime resolved from the picklist, and the sheet compared the two as read. - -The sheet now compares both versions of an object in the form the publish door stores: a field that names a picklist is read without `options` on both sides. A field with its own inline `options` is untouched, so a real options change there is still listed, and a field moved between a picklist and inline options still lists both keys. diff --git a/.changeset/29900-report-runtime-filter-field-unknown.md b/.changeset/29900-report-runtime-filter-field-unknown.md deleted file mode 100644 index c16ebb24d95..00000000000 --- a/.changeset/29900-report-runtime-filter-field-unknown.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/lint': minor ---- - -fix(lint)!: a report whose `runtimeFilter` names a field its dataset's object lacks is refused at publish (`report-filter-field-unknown`), as the dashboard twin already is (#29900) - -Clause-②: yes (narrowing) — the report publish door, `os validate` and `os build` refuse a report `runtimeFilter` key they accepted before; `@objectstack/lint` also exports `validateReportRuntimeFilters`, `REPORT_FILTER_FIELD_UNKNOWN` and the `ReportRuntimeFilterFinding` type. -Metadata-protocol: narrowed -Skills-docs: none — `skills/**` and `content/docs/**` teach `runtimeFilter` keys that are fields of the dataset's object, which this rule accepts; no page teaches a key the object lacks. The reasoning ships as the `os explain report-filter-field-unknown` entry. - - - -**BREAKING** accept-set narrowing at the report publish door (`PUT /api/v1/meta/report/NAME`, `POST /api/v1/meta/report/NAME/publish`, package draft publish; Studio, REST and MCP authors) and in `os validate` / `os build`. - -- **Before.** A report with `runtimeFilter: { no_such_field: 'x' }` over a dataset whose object has no `no_such_field` published `200`, and every reader of the report saw a "Dataset query failed: 400 Bad Request — Filter member no_such_field …" panel instead of numbers. The same key on a dashboard widget's `filter` was already refused `422` (`widget-filter-field-unknown`). -- **Now.** It is refused `422 INVALID_METADATA` with `report-filter-field-unknown` at `reports[0].runtimeFilter.no_such_field`, and the verdict names the object and the nearest field ("Did you mean …"). A `joined` report's block filter is judged against the block's dataset, and the report-level `runtimeFilter` (merged into every block's query) against each block's dataset. -- **Accepted, as on a widget.** A field of the dataset's object, a registry-injected system column (`created_at`, `owner_id` where injected), a dotted relationship path, and every `$` operator and combinator. One more key is accepted on a report: a bare key naming a dimension of the bound dataset, which the dataset query binds through that dimension. -- **Not judged.** A `dataset` that resolves to nothing (`chart-dataset-unknown` reports it once), an object the stack (or the publish snapshot) does not carry, an object with no readable field map, and a dotted key whose prefix the dataset's `include` does not declare. -- **`os explain report-filter-field-unknown`** prints the reasoning; `RULE_EXPLANATIONS` gains the entry. - -**What an author does.** Nothing for a report whose filter keys name real fields or dimensions. A refused key never served the report (every reader saw the error panel), so there is no rewrite that keeps what the author meant: name a field of the dataset's object, a dimension of the dataset, or a `relationship.field` path whose prefix the dataset's `include` declares. The refusal names the key and the nearest field. diff --git a/.changeset/29901-save-status-held.md b/.changeset/29901-save-status-held.md deleted file mode 100644 index 527a413be40..00000000000 --- a/.changeset/29901-save-status-held.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(app-shell): the metadata editor's save-status chip says "Not saved" while a save is held, instead of promising an auto-save that never comes (#29901) - -Clause-②: no -Metadata-protocol: none - -On the metadata editor page (Studio's All metadata item page, Setup's metadata pages) with auto-save on, a draft the server refused with `422 INVALID_METADATA` stays dirty, and the chip beside Save kept reading "Auto-saving…" although the auto-save is held until the refused part changes and no further save is sent. The same happened while the inspector holds a fault it reports (a CEL predicate that does not parse), which holds the auto-save the same way. - -The chip now has a held state: while either hold stands on a dirty draft it reads "Not saved — fix the errors to save" in the destructive tone, with the Save button's own explanation as its tooltip. It returns to the usual states as soon as the hold lifts: editing the part the server named, or fixing the inspector fault, lets the auto-save run and the chip reads "Auto-saving…", then "Saved". Nothing about when a save is sent or held changes. New string: `engine.edit.notSavedHeld` (en, zh). diff --git a/.changeset/29902-publish-sheet-hook-count.md b/.changeset/29902-publish-sheet-hook-count.md deleted file mode 100644 index 6074fdc83a9..00000000000 --- a/.changeset/29902-publish-sheet-hook-count.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(app-shell): the Studio publish sheet names a batch of hooks or translations in words ("3 hooks", zh 「钩子 3」) instead of the raw type code ("3 hook") (#29902) - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@object-ui/i18n`**: two new count families in every pack, `preview.changes.typeCount.hook` and `preview.changes.typeCount.translation`, with each CLDR slot the pack's language selects. -- **`@object-ui/app-shell`**: the Pending changes / publish sheet's summary line and group headings read those families for `hook` (authored in an object's Hooks tab) and `translation` (authored in Studio's "All metadata" form editor). A type with no family still shows its raw name. diff --git a/.changeset/29903-base-export-without-pair-extension.md b/.changeset/29903-base-export-without-pair-extension.md deleted file mode 100644 index 62cee36a241..00000000000 --- a/.changeset/29903-base-export-without-pair-extension.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/objectql': patch -'@objectstack/metadata-protocol': patch ---- - -fix(objectql, metadata-protocol): a base package's own export carries its objects without its `.custom` pair's extension - -Clause-②: yes (widening) -Metadata-protocol: none - -After an administrator added a field to a vendor package's object through Studio's Customize (an extension published in `.custom`, for example `region__c`, or a relabelled base field), `GET /api/v1/packages//export` carried that field and label inside the base's object, as if the vendor had shipped them. The pair's export carries the same extension as its `objectExtensions` entry, so installing the two exports elsewhere declared the customer's field twice, once as the vendor's. ADR-0146 D8 item 3 says the base's export carries the base unchanged. - -- **The base's export** (and its entry in `GET /api/v1/packages/export`) now carries each object as the base declares it, with no field, label or other patch from its pair's extension. -- **The pair's export** is unchanged: it carries the extension in `objectExtensions`. -- **Every served read is unchanged**: `GET /api/v1/meta/object/`, the object list, the console and the data API keep serving the object with the pair's field and labels folded in. - -An extension contributed by any other installed package is still folded into the base's object in the base's export, as before. - -`SchemaRegistry.listItems` takes a `withoutExtensionsOf` option beside `includeDisabledPackages`, and `getAllObjects` and `foldObjectExtendersOnto` take the same option: each object is resolved with that package's `extend` contributions left out of the fold. Only the metadata protocol's per-package layer (the package export) passes it. diff --git a/.changeset/29904-type-mismatch-fail-closed.md b/.changeset/29904-type-mismatch-fail-closed.md deleted file mode 100644 index 8d182206707..00000000000 --- a/.changeset/29904-type-mismatch-fail-closed.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/formula': minor -'@objectstack/lint': minor -'@objectstack/mcp': minor -'@objectstack/spec': minor ---- - -fix(lint)!: publish refuses a validation rule (or another fail-closed predicate) that orders a text or boolean field against a number, instead of warning that it "evaluates to null" and then refusing every write (#29904) - -Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the object save door and the package publish) refuses a `type-mismatch` it used to pass with a warning, at the predicate sites that fail closed. Nothing is widened. -Metadata-protocol: narrowed - - - -**BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate`, the object save door and `POST /api/v1/packages/:id/publish-drafts`. - -- **Before.** `record.name > 5` on a text field, written as a validation rule's `condition`, published with one `expression-invalid` advisory at `severity: warning` saying the expression "silently evaluates to null". A validation rule fails closed, so from then on every create and update whose `name` was not numeric text was refused: `400 VALIDATION_FAILED` "Validation rule 'name_gt' could not be evaluated (runtime: no such overload: dyn > int) — write rejected". -- **Now.** The same rule is refused at publish as `expression-invalid` at `severity: error`, at the rule's `condition` path, with a message that says the write would be refused and how to compare like with like. This is the severity the nullable-comparison verdict (`record.qty > 1000` on a nullable number) already had at the same slot. -- **Which sites.** Those whose runtime refuses the write (or aborts the operation) when the expression faults: a validation rule's `condition` and `when`, nested `then` / `otherwise` rules included; a field's `requiredWhen` and `readonlyWhen`; an option's `visibleWhen` (a write that picks the option); a hook's `condition`. The `validate_expression` MCP tool answers the same for `site: 'validation'`. -- **Unchanged.** A formula field's `expression` keeps the warning: there the fault reads null, which is what the warning says. So do flow conditions (the run fails, the write that fired it lands), a field's display `visibleWhen` / `settledWhen`, field group and action predicates, and `conditionalFormatting`. Sharing-rule conditions compile to a query filter and are not judged by this verdict. -- **`@objectstack/spec`.** The upgrade guide entry `validation-type-mismatch-refused` (a semantic migration: no automatic rewrite, because whether the author meant the length, a number stored as text or another field is a judgement about the rule). -- **`@objectstack/formula`.** `ExprSchemaHint` gains `faultOutcome?: 'fail-closed'`, which the caller sets for a fail-closed site; the `type-mismatch` refusal's `params` carry `outcome: 'fail-closed'` when it is graded an error there. - -**FROM → TO.** `condition: 'record.name > 5'` on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text (it faults, and refuses the write, on any other). A boolean: `record.active > 0` → `record.active == true`. diff --git a/.changeset/29909-revoke-sessions-on-password-reset-retired.md b/.changeset/29909-revoke-sessions-on-password-reset-retired.md deleted file mode 100644 index 54236248a92..00000000000 --- a/.changeset/29909-revoke-sessions-on-password-reset-retired.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/plugin-auth': minor ---- - -feat(spec)!: retire `emailAndPassword.revokeSessionsOnPasswordReset` — a completed password reset always ends every session the account holds, and there is no setting (#29909) - -Clause-②: no (narrowing) — `EmailAndPasswordConfig.revokeSessionsOnPasswordReset` becomes a `retiredKey()` tombstone, so the schema parse and `tsc` refuse it at any value, and plugin-auth's entry guard refuses it at any value too, `true` included, which it accepted before. Nothing is widened. -Metadata-protocol: narrowed - - - -**BREAKING** for a configuration that still writes `emailAndPassword.revokeSessionsOnPasswordReset`, at any value. - -- **`@objectstack/spec`.** `EmailAndPasswordConfig.revokeSessionsOnPasswordReset` is retired (ADR-0049 enforce-or-remove). A completed self-service password reset already ended every session the account holds, on every reset door, and an explicit `false` was already refused at boot, so the key could hold one value only. It is now a tombstone: a typed configuration that writes it fails `tsc`, and `AuthConfigSchema` / `EmailAndPasswordConfigSchema` refuse it with the prescription. The authorable-surface row reads `[RETIRED]`, `RETIRED_KEYS_BY_MAJOR[18]` lists `system/EmailAndPasswordConfig:revokeSessionsOnPasswordReset`, and the upgrade guide carries the semantic entry `email-and-password-revoke-sessions-on-reset-retired`. The prescription is exported as `REVOKE_SESSIONS_ON_PASSWORD_RESET_RETIRED` (`@objectstack/spec/system`). No ADR-0087 conversion: the key lives in the configuration a host hands the auth plugin, never in a stack or a stored row, so `os migrate meta` has nothing to rewrite. -- **`@objectstack/plugin-auth`.** The auth manager takes its configuration unparsed, so it refuses the retired key itself, where the configuration enters: at construction, which stops the boot, and on a configuration patch, where the standing configuration keeps ruling. It now refuses `true` as well as `false`, with the tombstone's own prescription. `true` asked for what the platform does anyway; the key is to be deleted either way. The auth instance still sets better-auth's own option of that name itself, so a completed reset ends the account's sessions exactly as before. -- **Unchanged.** What a password reset does: every self-service reset door ends every session the account holds, and each ended session is stamped `revoke_reason: password_reset`. An absent key is accepted, as before. - -**The remedy.** Delete `revokeSessionsOnPasswordReset` from the `emailAndPassword` block of the auth configuration, whatever it holds; nothing replaces it. diff --git a/.changeset/29910-no-submitter-returned-docs.md b/.changeset/29910-no-submitter-returned-docs.md deleted file mode 100644 index 80cef7b0adf..00000000000 --- a/.changeset/29910-no-submitter-returned-docs.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(approvals): say how a request with no submitter leaves the `returned` state - -Clause-②: no -Metadata-protocol: none - -- **`@objectstack/spec`: comment text only.** The `IApprovalService` docblocks on `recall`, `sendBack` and `resubmit` now state the path for a request with no submitter (its run's trigger resolved no user): nobody can recall it once it is `returned`, a system-context `resubmit` is its way out, and the next round opens `pending` with no submitter, where the system or the override actor may recall it. The `resubmit` docblock now names the system context it already admits. No key, type, schema or behaviour changes. diff --git a/.changeset/29916-connection-error-no-invented-503.md b/.changeset/29916-connection-error-no-invented-503.md deleted file mode 100644 index 4a1e90b7705..00000000000 --- a/.changeset/29916-connection-error-no-invented-503.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/data-objectstack': patch ---- - -Clause-②: no -Metadata-protocol: none - -A `ConnectionError` for a request nothing answered (an unreachable host, a rejected `fetch`) no longer reads as a server that answered with a 503 (#29916). - -- The `ConnectionError` constructor leaves `statusCode` undefined when the caller passes none. Before, it stamped `503`. `connect()` builds it that way when discovery's `fetch` rejects, every data method opens with `await this.connect()`, and the post-connect `error` connection state builds it the same way, so a read against a down backend was taken for the server's own answer. `answeredLoadFailure` now returns `null` for it and `classifyLoadError` reads `network`, so a failed-read state keeps its "check your connection" sentence. -- A discovery read the server answered with an error status (`!res.ok`) now carries that status (a 401 reads as `unauthorized`), instead of the invented 503. -- An answered 503 or 504 through `createErrorFromResponse` keeps its status, as before. diff --git a/.changeset/29918-lifecycle-first-sweep-fresh-store.md b/.changeset/29918-lifecycle-first-sweep-fresh-store.md deleted file mode 100644 index ca0b038457f..00000000000 --- a/.changeset/29918-lifecycle-first-sweep-fresh-store.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -fix(objectql): the lifecycle sweep no longer runs 60 s after a boot that created its datastore from empty; its first sweep runs one interval later - -Clause-②: yes (widening) — the exported `LifecycleEngineLike` gains the optional member `wasDatastoreCreatedFromEmpty?(): boolean` (an engine without it keeps the old schedule); no accept set changes. -Metadata-protocol: none - -- `LifecycleService.start()` asks the engine, at the initial delay, whether this process created the datastore from empty (`ObjectQL.wasDatastoreCreatedFromEmpty()`, answered from every driver's schema-sync stats without a statement). When it did, the scheduled first sweep is skipped and the sweep runs on the interval, one interval later. Such a store holds no row older than the boot, and no declared window is shorter than an hour, so the skipped sweep had nothing to reap. It still read every declared object, the `lifecycle` settings and every lookup the reference audit walks: 130 statements in the first minute of a hosted tenant kernel's first boot. -- A store that existed before the boot, or an engine that cannot say, sweeps at the initial delay as before. A direct `sweep()` call always sweeps. `OS_LIFECYCLE_DISABLED`, `initialDelayMs` and `sweepIntervalMs` keep their meaning. -- `LifecycleEngineLike` gains the optional member `wasDatastoreCreatedFromEmpty?(): boolean`. An engine double that omits it keeps the old schedule. -- A re-boot on an existing store still sweeps at the initial delay. Persisting a last-swept watermark for that case is separate work. diff --git a/.changeset/29919-dispatch-loop-next-due-wake.md b/.changeset/29919-dispatch-loop-next-due-wake.md deleted file mode 100644 index e93288f72b4..00000000000 --- a/.changeset/29919-dispatch-loop-next-due-wake.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/core': patch -'@objectstack/service-messaging': patch -'@objectstack/service-queue': patch ---- - -fix(core,service-messaging,service-queue): an idle dispatch loop wakes at the next due time, and a sole writer's loop no longer re-runs the idle ramp after every boot - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@objectstack/core`: `DispatchLoop` wakes at the next due time.** A new optional `DispatchLoopOptions.nextDueAt()` answers the earliest instant work becomes claimable (a deferred row, a claim's expiry). The loop asks it once per idle streak, after the first empty tick, and never sleeps past the answer, so a deferred row goes out within one tick of coming due whatever `maxIdleIntervalMs` is. The answer is remembered rather than re-read on every ceiling tick, so an idle loop's steady-state cost is unchanged; the new `DispatchLoop.wakeAt(dueAtMs)` lets a producer in the same process announce a deferred row at no read. An answer at or before the tick that found nothing is not chased, and a rejected tick or read still counts as idle, so neither can make the loop spin. -- **`@objectstack/core`: `DispatchLoopOptions.soleWriter`** (default `false`). On a host where no other process writes the store, an empty tick sleeps straight to the next due time or the ceiling instead of re-running the ramp from `intervalMs`, which only ever bought latency on rows another process wrote. A fresh sole-writer loop over an empty store now ticks once in its first minute; before, every boot re-ran the ramp (six ticks in the first minute at the dispatchers' 500 ms base, five at the queue's 1 s). -- **`@objectstack/service-messaging`:** `INotificationOutbox.nextDueAt` / `IHttpOutbox.nextDueAt` (optional; both built-in stores implement it: two reads, the earliest future `next_attempt_at` / `next_retry_at` and the oldest `in_flight` claim plus the claim TTL), wired into `NotificationDispatcher` and `HttpDispatcher`; `soleWriter` on both dispatchers' options, and `dispatchSoleWriter` on `MessagingServicePlugin`. Retries, quiet hours, digest windows and a crashed node's claims are taken at their due time instead of up to `dispatchMaxIdleIntervalMs` late. -- **`@objectstack/service-queue`:** `DbQueueAdapter` reads the earliest future `scheduled_for` of its subscribed queues (one read per idle streak), a deferred `publish()` announces its due time to the loop through `wakeAt` instead of waiting out the backoff, and `DbQueueAdapterOptions.soleWriter` (reachable as `QueueServicePlugin` `db.soleWriter`) skips the ramp. - -Nothing changes for a host that sets neither `soleWriter` flag except the due-time wake: the ramp keeps its shape, cut short only when a due row is earlier, and an idle stretch costs one extra read at its start. diff --git a/.changeset/29926-type-mismatch-outcome-per-role.md b/.changeset/29926-type-mismatch-outcome-per-role.md deleted file mode 100644 index fcdd474ae02..00000000000 --- a/.changeset/29926-type-mismatch-outcome-per-role.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/formula': minor -'@objectstack/lint': minor -'@objectstack/mcp': minor -'@objectstack/spec': minor ---- - -fix(lint)!: a text-vs-number comparison in a record-triggered flow condition is refused with what really happens (the run fails, the write lands), and in an advisory validation rule the refusal no longer says the write is refused (#29926) - -Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses a `type-mismatch` in a record-triggered flow's start or edge condition that it used to pass with a warning; and the fail-closed arm of the verdict is served under its own code `type-mismatch-fail-closed` instead of `type-mismatch` with `params.outcome`. Added beside them: the codes `type-mismatch-run-fails` and `type-mismatch-fail-open`, and the exported type `ExprFaultOutcome`. -Metadata-protocol: narrowed -Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names each outcome) and the MCP skill text in `packages/mcp/src/skill-md.ts` (`flow_condition` now errors too); `skills/**` teaches no text-vs-number comparison in a flow condition or an advisory rule, so no skill or eval moves. - - - -**BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. - -- **Record-triggered flow conditions — before.** `name > 5` on a text field, as the start node's `condition` or an edge `condition` of a flow whose start `triggerType` is `record-*`, published with an `expression-invalid` warning saying the expression "silently evaluates to null". It does not: the engine's `evaluateCondition` throws on the fault, so the run fails at that condition, and the record-change trigger isolates the write that fired it, so the write lands and the flow's work is not done. -- **Now.** The same condition is refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with a message saying the run fails at the condition and the triggering write still lands. This is the severity and outcome the nullable-comparison verdict already has on those conditions (#29026). A flow that is not record-triggered, and a node `condition` other than the start node's (inert), keep the warning. The `validate_expression` MCP tool answers the same for `site: 'flow_condition'`. -- **Advisory validation rules** (`severity: 'warning'` / `'info'`). Still refused at publish, as the nullable twin refuses them, but under `type-mismatch-fail-open` with a message that says what happens: the rule is skipped, the write proceeds, and the rule never gives its advisory. The nullable twin's message on the same rule now says the same instead of "the write is rejected fail-closed". A `severity: 'error'` rule keeps both refusals and their wording. -- **Refusal codes (`@objectstack/formula`).** A code names one message template, so each outcome has its own: `type-mismatch` (warning, reads null), `type-mismatch-fail-closed`, `type-mismatch-run-fails`, `type-mismatch-fail-open` (all errors). `ExprSchemaHint.faultOutcome` takes `'fail-closed' | 'run-fails' | 'fail-open'` (exported as `ExprFaultOutcome`); the `outcome` param on `type-mismatch` is gone. -- **`@objectstack/spec`.** Upgrade guide entry `flow-condition-type-mismatch-refused` (semantic: no mechanical rewrite). The `validation-type-mismatch-refused` entry's acceptance criteria now say a create answers 201, the data door's status. - -**FROM → TO.** Flow condition `name > 5` on a text field → `size(name) > 5` for its length, `score > 5` on a number field for a value, or `double(name) > 5` only when every value is numeric text. A boolean: `active > 0` → `active == true`. A consumer keyed to the code: `type-mismatch` with `params.outcome === 'fail-closed'` → `type-mismatch-fail-closed`. - -**The renamed code shipped in 18.3.0.** `@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` already serve #29904's fail-closed refusal as `type-mismatch` with `params.outcome: 'fail-closed'` (the `outcome` param is in the 18.3.0 `ExpressionRefusalParams` type), although 18.3.0's changelog has no line for it: its changeset (`29904-type-mismatch-fail-closed.md`) was not consumed by that release. This release renames that arm `type-mismatch-fail-closed` and removes the `outcome` param, so a consumer keyed to 18.3.0's `type-mismatch` + `outcome` moves to the new code (FROM → TO above). The same changeset also says flow conditions keep the warning; a record-triggered flow's start and edge conditions are now refused, as above. diff --git a/.changeset/29934-dev-start-one-data-key-file.md b/.changeset/29934-dev-start-one-data-key-file.md deleted file mode 100644 index 5821789659f..00000000000 --- a/.changeset/29934-dev-start-one-data-key-file.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/cli': patch -'@objectstack/service-settings': patch ---- - -fix(cli): `os dev` and `os start` share one data-key file, so a secret saved under `npm run dev` reads back under `os start` - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@objectstack/cli`.** Both commands open the same project database, but `os start` used to mint its own data key under its own home (`PROJECT/.objectstack/dev-crypto-key` in project mode) while `os dev` kept the key at `$HOME/.objectstack/dev-crypto-key` (or `$OS_HOME`'s). Every secret written during development then read back as unset under `os start`. Both commands now resolve the key file the way `os dev` always has (`os start --home` counts as `OS_HOME`) and hand it to their `serve` child on the internal `OS_INTERNAL_CRYPTO_KEY_FILE` channel. The project-local file an earlier `os start` minted is not orphaned: when it alone holds a key it is still read, and the boot says which file; when it and the shared file hold different keys, the shared one is used and a warning names both paths. An explicit `OS_SECRET_KEY` / `OS_DEV_CRYPTO_KEY` still wins, unchanged. -- **`@objectstack/service-settings`.** New exports `resolvePersistedKeyFile` (the one resolution both commands take, beside the provider's own key-file path) and `KEY_FILE_CHANNEL_ENV`; the local crypto provider's file tier reads the file that channel names, ahead of `OS_HOME`. diff --git a/.changeset/29935-update-action-refreshes-record.md b/.changeset/29935-update-action-refreshes-record.md deleted file mode 100644 index 7cb3daa6964..00000000000 --- a/.changeset/29935-update-action-refreshes-record.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/core': patch ---- - -fix(core): a successful `operation: 'update'` action now refreshes the record it wrote, with no `refreshAfter: true` (#29935) - -Clause-②: no -Metadata-protocol: none - -The spec fills in `refreshAfter: false` on an action authored without it, and that parsed shape is what the Console is served. The server-action dispatch (`createServerActionHandler`) refreshed only when `refreshAfter !== false`, so a declared update never refreshed: the route wrote the record and answered 200, the toast reported success, and the record page kept the old values and the action button whose `visible` predicate had turned false. The list the record came from kept it under a filter it no longer matched. - -A declared `operation: 'update'` writes the record it addresses by definition, so its success is now treated as a record change whatever `refreshAfter` says, the same way a client-side `dataSource.update` already reaches every reader. The record-scoped invalidation re-reads the record page (drawer and full page) and refetches every list of that object. `refreshAfter` keeps governing every other server action: a non-update action served with the default `refreshAfter: false` still sends no re-read. The spec default is unchanged. diff --git a/.changeset/29936-dev-door-single-metadata-plugin.md b/.changeset/29936-dev-door-single-metadata-plugin.md deleted file mode 100644 index bf84cda892f..00000000000 --- a/.changeset/29936-dev-door-single-metadata-plugin.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os dev` on an `os init -t app` scaffold no longer composes a second `MetadataPlugin` and warns "Plugin superseded: 'com.objectstack.metadata'" - -Clause-②: no — boot diagnostics only; nothing accepted or published changes -Metadata-protocol: none - -The dev artifact door in `os serve` / `os dev` added its own `MetadataPlugin` unless the stack already held one, but it recognised that plugin only by its class name. The published `@objectstack/metadata` bundle names the class `_MetadataPlugin`, so on an installed CLI the check missed the `MetadataPlugin` the standalone stack had already composed, added a second one, and every dev boot of an untouched scaffold printed a "Plugin superseded" diagnostic blaming a composition the user never wrote. The dev door now asks the same question the host-config door asks: a plugin named `com.objectstack.metadata` (or, as before, a class named `MetadataPlugin`) means the stack has one. A host config with no `MetadataPlugin` of its own still gets the dev door, as before. diff --git a/.changeset/29937-empty-stub-protocol-range.md b/.changeset/29937-empty-stub-protocol-range.md deleted file mode 100644 index b5ccd5018e1..00000000000 --- a/.changeset/29937-empty-stub-protocol-range.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/runtime': patch ---- - -fix(runtime): the empty kernel's own stub declares its protocol range, so `os start` in an empty directory boots with no compatibility warning - -Clause-②: no — boot diagnostics only; nothing accepted or published changes -Metadata-protocol: none - -The empty-boot stub `createDefaultHostConfig({ requireArtifact: false })` writes to `~/.objectstack/dist/objectstack.json` (manifest id `com.objectstack.empty`) now declares `engines: { protocol: '^' }`, the major of the runtime that writes it. Before, it declared no range, so the ADR-0087 handshake warned "package 'com.objectstack.empty' declares no engines.protocol range" on the first boot of the docs' one-line Community quickstart (`npm i -g @objectstack/cli && os start`), about a package the user never wrote. A stub already on disk from an older CLI is refreshed in place on the next `os start`, including when the start supervisor hands it down by path (it resolves `/dist/objectstack.json` as an artifact once that file exists): only `manifest.engines.protocol` is rewritten, and only while the file at the home location is still the stub. The same refresh re-stamps a stub written under another protocol major, which would otherwise be refused as `OS_PROTOCOL_INCOMPATIBLE` once the runtime moves to the next major. An artifact that is not the stub, or a file that is not JSON, is left alone. diff --git a/.changeset/29942-related-list-title-translation.md b/.changeset/29942-related-list-title-translation.md deleted file mode 100644 index 5ab69e6ea05..00000000000 --- a/.changeset/29942-related-list-title-translation.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/cli': minor -'@objectstack/metadata-protocol': minor ---- - -feat(spec): a lookup or master-detail field's related-list title is translatable - -Clause-②: yes -Metadata-protocol: added - -A related list the record page derives from a lookup or master-detail field is titled by the field's `relatedListTitle`, or by the child object's label when it has none. That title had no translation slot, so a zh-CN reader saw it in the authoring language, and `os i18n check` could not see the gap. A lookup from an object back to itself was the plainest case: with no title of its own, a contract's record page listed its renewals under 「合同」, the same word as the contract beside it. - -- `FieldTranslationSchema` declares `relatedListTitle`: `objects..fields..relatedListTitle`, on the child object whose field declares the title. `translateObject` overlays it on the served child object (`GET /meta/object`, and the app bundle's object members), which is what the console derives the related list's title from, on the side card and on the body table alike. -- It follows the packaged-base rule (ADR-0029 D9.2a): a title an org re-authored on top of the packaged declaration keeps its text. An authored `relatedListTitle` with no entry keeps its authored text; the child object's label is never substituted for it. -- `os i18n extract` scaffolds the key and `os i18n check` counts it under the field bucket. A field without a `relatedListTitle` is recorded without being demanded. -- The object save door puts a served (translated) `relatedListTitle` back to its source text, as it does for the field's other translated slots. diff --git a/.changeset/29943-body-table-totals-additive-columns.md b/.changeset/29943-body-table-totals-additive-columns.md deleted file mode 100644 index 661e3874efb..00000000000 --- a/.changeset/29943-body-table-totals-additive-columns.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -fix(plugin-detail): a record page's body tables total only the columns declared additive — a version number or an instalment number is no longer summed - -Clause-②: no -Metadata-protocol: none - -**Visible behaviour change on a released page.** The 「Totals (this page)」 line under a body table of the default record page (a related list drawn full width in the details column) used to add up every `currency`, `number`, `integer` and `decimal` column. Ordinals were summed with the quantities: a contract's versions v1–v4 read 「版本号 10」, five instalments 「期次 15」. The line now totals: - -- a `currency` column, when every counted row is in one known currency, formatted in it. Each row's currency is the one its cell displays (`resolveFieldCurrency`): the field's code under `currencyMode: 'fixed'`, the tenant default under `dynamic`, the row's own code field under `record`. Rows in different currencies, or a row whose currency is unknown (a `record`-mode row with a blank code, a `dynamic` amount with no tenant default), give that column no total rather than a bare or wrong-currency sum. A `currencyConfig` without `currencyMode: 'fixed'` no longer formats in its `defaultCurrency`, which the spec does not read outside `fixed`; it shows the tenant default, as the column's cells do; -- a `number`, `integer` or `decimal` column only when the record's own object declares a `summary` field whose `summaryOperations` sums that child field (`function: 'sum'`, `object` naming the child). The roll-up is the author's statement that the column adds up. Without one, as for an amount stored as `number` with its currency on the parent, the column gets no total; -- nothing else. When no column qualifies the line is not drawn. diff --git a/.changeset/29944-body-table-empty-keeps-header.md b/.changeset/29944-body-table-empty-keeps-header.md deleted file mode 100644 index 9c2b87087bf..00000000000 --- a/.changeset/29944-body-table-empty-keeps-header.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -fix(plugin-detail): an empty body table of the record page keeps its header (title, 0, New) with the empty sentence under it, instead of folding into one line that read the empty sentence before the table's title - -Clause-②: no -Metadata-protocol: none - -**Visible behaviour change on a released page.** The default record page draws a body table (a related list drawn full width in the details column, or the Details tab) beside its sibling sections. Because the tables share that container, the related-list empty fold took them for a Related tab stack: once one table had rows, an empty one left its place and reappeared as a single line after the last table, reading 「暂无相关记录 · 偏离 · + 新建」 (no related records · Deviations · New). A body table now never folds. An empty one keeps the header row its non-empty siblings draw (title, count 0, ⤢, New), and the empty sentence moves out of that row to the table's place under it. - -The fold of a record's Related tab, and the empty-panels line of the related column, are unchanged: only a list whose relationship is one of the page's body tables opts out. diff --git a/.changeset/29945-body-table-overflow-cues.md b/.changeset/29945-body-table-overflow-cues.md deleted file mode 100644 index 80e7b179de9..00000000000 --- a/.changeset/29945-body-table-overflow-cues.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/plugin-detail': patch -'@object-ui/components': patch -'@object-ui/i18n': patch ---- - -fix(plugin-detail): a record page's body table that is wider than the details column says so — its first column stays pinned and ⤢ is offered as 「All columns」 - -Clause-②: no -Metadata-protocol: none - -A body table of the default record page (a related list drawn in the details column) keeps its place in that column, as ADR-0085's role-based layout puts it. When its columns are wider than the column, it now makes the overflow obvious: - -- **The first column stays pinned** while the table scrolls sideways, so a scrolled row is still identifiable (the table's existing `frozenColumns: 1`, set only while the table overflows). -- **⤢ is offered in words.** The enlarge control in the card header, icon-only before, shows a visible 「All columns」 label (`detail.relatedCardAllColumns`, all ten locales) while the table overflows. It opens the same enlarged tab as before, where every column fits. -- **The right-edge shadow** while columns remain to the right is the table's own and unchanged; it clears when the table is scrolled to its end. - -A body table that fits shows none of these: no pinned column and the icon-only ⤢. - -`@object-ui/components`: a `data-table` header label carries its full text as a `title`, so a header the ellipsis cuts can be read on hover, and the table's scroll box carries a `data-table-scroll-box` marker a host can read the overflow from. diff --git a/.changeset/29946-record-page-user-field-related-list.md b/.changeset/29946-record-page-user-field-related-list.md deleted file mode 100644 index 9e16bf8fe8d..00000000000 --- a/.changeset/29946-record-page-user-field-related-list.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): the one-request record page accepts a related list keyed by a `user` field again - -Clause-②: no -Metadata-protocol: none - -- **Regression in 18.3.0 (#29946, duly#185).** `POST /data/:object/:id/page` (#27713) checked each related list's field against a private `lookup`/`master_detail` list and read `reference` raw, so a related list on `sys_user` keyed by a `user` field (an owner, an assignee) answered `400 VALIDATION_FAILED … is not a lookup or master-detail field that references 'sys_user'`, and the console's full-page record view showed "could not load this record". The related list's own row query (`{ [field]: id }`) follows that field, and 18.2.0 read each related list through that list read, with no type check. -- The check now asks the spec's one arbiter of a field's target, `referenceTargetOf` (`lookup`, `master_detail`, `user`, `tree`; a `user` field's target is fixed to `sys_user` by its type, whether or not its `reference` spells it), the same judgement the engine's `$expand` and delete walk read. A field that does not reference the page's object is still a located `400 invalid_relationship`: a `user` field on any other object's page, a `lookup` to another object, a `tree` on another object, an unreadable `reference` carrier. The message now reads `… is not a lookup, master-detail, user or tree field that references ''.` diff --git a/.changeset/29951-page-header-placed-actions-reader.md b/.changeset/29951-page-header-placed-actions-reader.md deleted file mode 100644 index 156bc3c88b3..00000000000 --- a/.changeset/29951-page-header-placed-actions-reader.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/types': patch -'@object-ui/app-shell': patch -'@objectstack/spec': patch -'@objectstack/platform-objects': patch -'@objectstack/plugin-approvals': patch ---- - -fix(console): an action another package places on `sys_user` at `record_header` / `record_more` draws again on the user's record page, in the drawer and on the full page (#29951) - -Clause-②: no -Metadata-protocol: none - -- **The header reads `placedActions: true`.** `page:header`'s `placedActions` (#30018) is now drawn by the console's header renderer (`PageHeaderActions` in `@object-ui/components`): `true` draws every action of the page's object whose `locations` include `record_header` or `record_more`, in declaration order, read from the object's metadata at render time, so an action an installed package adds to the object is drawn without editing the page. Omitted or `false`, an authored `actions` list is still the whole set (#11592). A `true` beside an authored list, which the spec refuses, draws none of the object's actions and is reported once in the console. -- **One placed-header reader.** `placedHeaderActions` (`@object-ui/types`) is the rule both headers read: the synthesized default record page's header in `RecordDetailView` and a `placedActions: true` header. The `page:header` registration publishes the `placedActions` input. -- **The platform record pages opt in.** `sys_user_detail` and `sys_organization_detail` (`@objectstack/platform-objects`) and `sys_approval_request_detail` (`@objectstack/plugin-approvals`) set `placedActions: true` in place of the id lists #27219 froze, which dropped every header action another package places on those objects. The tab strips are unchanged, and every action the lists named is placed at `record_header` / `record_more`, so each still draws. -- `@objectstack/spec`: the `placedActions` describe no longer opens with `[PLANNED — not yet rendered]`, and the `page` liveness record names the reader. diff --git a/.changeset/29951-skills-placed-actions-drawn.md b/.changeset/29951-skills-placed-actions-drawn.md deleted file mode 100644 index 49fd6156d74..00000000000 --- a/.changeset/29951-skills-placed-actions-drawn.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): `objectstack-ui` no longer says `page:header`'s `placedActions: true` is not drawn, and an eval exercises it (#29951) - -Clause-②: no -Metadata-protocol: none - -- `rules/pages.md`: the component catalogue's `page:header` row says `placedActions: true` draws every placed action, read live. The console's header renderer reads the key since #29951's ui half, so the "not drawn yet" qualifier #30018 wrote is dropped. -- `evals/record-page.json`: a third case asks for a custom full record page whose header shows every action packages place on the object; the expected answer keeps the page and its tab strip and sets `placedActions: true` on the `page:header` node, with no `actions` id list beside it. diff --git a/.changeset/29954-disabled-reason-tooltip-width.md b/.changeset/29954-disabled-reason-tooltip-width.md deleted file mode 100644 index 0547003023a..00000000000 --- a/.changeset/29954-disabled-reason-tooltip-width.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/components': patch -'@object-ui/plugin-detail': patch ---- - -fix(ui): a greyed action's `disabledReason` tooltip wraps inside a phone-width viewport instead of running off-screen (#29954) - -Clause-②: no -Metadata-protocol: none - -The record action bar, the `record:quick_actions` button and the schema-driven `action:*` controls (button, icon, group, menu, through the shared disabled-reason trigger) rendered the authored `disabledReason` in a tooltip with no width bound, so a long reason sat on one line and was cut at the right edge of a 390px viewport. All three tooltips now carry the same `max-w-xs` bound the home page's disabled-reason tooltip uses, so the reason wraps. diff --git a/.changeset/29959-flow-condition-type-mismatch-every-site.md b/.changeset/29959-flow-condition-type-mismatch-every-site.md deleted file mode 100644 index a228bec2162..00000000000 --- a/.changeset/29959-flow-condition-type-mismatch-every-site.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/formula': minor -'@objectstack/lint': minor -'@objectstack/mcp': minor -'@objectstack/spec': minor ---- - -fix(lint)!: every flow condition's text-vs-number comparison gets a verdict that says what the run does — a record-triggered flow's `record.` read and `decision` branch are refused like its bare name, a time-relative sweep of the flow's object is refused like a record-triggered flow, a bare name a flow variable holds is no longer typed as the field, and any other flow's warning says the run fails instead of "evaluates to null" (#29959) - -Clause-②: yes (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses, on a record-triggered flow, a `type-mismatch` in a `decision` branch and in a `record.` / `previous.` read of a start or edge condition, which it passed with no verdict before; refuses the same on a time-relative flow sweeping the object its conditions are typed against, where 18.3.0 served a warning; and a flow condition's `type-mismatch` warning is served under its own code `type-mismatch-run-may-fail` instead of `type-mismatch`. Added beside them (the widening half): the optional `severity` and `triggerKind` inputs of the `validate_expression` MCP tool, the code `type-mismatch-run-may-fail` in `@objectstack/formula`'s `ExpressionRefusalCode`, the optional `flowVariables` key of `@objectstack/formula`'s `ExprSchemaHint`, and a bare flow-condition name that a declared flow variable or node id holds is no longer typed as the field it shadows (no verdict where 18.3.0 served a `type-mismatch` warning). -Metadata-protocol: narrowed -Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names the decision branch, the `record.` read and the warning on other flows) and the MCP skill text in `packages/mcp/src/skill-md.ts` (the two new `validate_expression` inputs); `skills/**` teaches no text-vs-number comparison in a flow condition (grep for `type mismatch`, `evaluates to null`, `validate_expression` and the decision examples), so no skill or eval moves. - - - -**BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. - -- **Record-triggered flows — before.** The start and edge conditions refused `name > 5` on a text field (`type-mismatch-run-fails`), but `record.name > 5` in the same slot, and `name > 5` in a `decision` node's `config.conditions[].expression`, published with no verdict at all. Both fail the run the same way: the engine's `evaluateCondition` throws on the fault, the run binds the trigger record both as `record` and flattened to bare names, and a decision branch is evaluated over the same variables as an edge. -- **Now.** Both are refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with the message the bare name already gets: the run fails at the condition and the triggering write still lands. `previous.` is judged the same way. The date verdicts stay on the bare name only, as before. -- **Time-relative flows.** A sweep launches each run with the swept row as its record, so on a flow whose `timeRelative.object` is the start node's `objectName` (or is omitted) the start, edge and `decision` conditions get the record-triggered verdict above, bare name included: `type-mismatch-run-fails`, where 18.3.0 served a `type-mismatch` warning. A sweep of another object keeps the warning below. -- **Any other flow** (no `triggerType`, a `schedule` flow, an `api` flow) and an inert node `condition` keep a warning. Its text said the expression "silently evaluates to null", which is false on every flow: a condition that faults fails the run. It is now `type-mismatch-run-may-fail`, worded for the failed run, and a warning because such a flow is handed whatever its entrance carries (an API request body, an action's partial record, a parent flow's record, or nothing), so the field's type is not proven to be what the name holds. A `record.` read and a `decision` branch there get the same warning. The flow registration log line reads the same text. -- **A bare name a flow variable holds.** The run seeds a flow's declared variables (and each node's outputs, under its id) before it flattens the record, and the flattening skips a name already bound. So in a flow that declares `variables: [{ name: 'title', type: 'number' }]` over a text field `title`, a bare `title > 5` reads the number and runs; it was refused as `type-mismatch-run-fails` on the head that added that error, and warned on 18.3.0. It now gets no type verdict (the `flow-variable-shadows-field` warning still names the shadowing). `record.title > 5` reads the field and stays refused. -- **`validate_expression` (MCP).** Two optional inputs, so the tool answers what the gate will: `severity` (`error` | `warning` | `info`, site `validation`; default `error`) gives an advisory rule the gate's `type-mismatch-fail-open` instead of the fail-closed verdict, and `triggerKind` (`record_change` | `time_relative` | `schedule` | `api` | `none`, site `flow_condition`; default `record_change`) gives a `schedule`, `api` or `none` flow the gate's warning instead of the run-fails error (`time_relative` answers run-fails, its record being a row of `objectName`). Either input on a site it does not describe is refused as a tool error. A call that passes neither answers as before. -- **`@objectstack/spec`.** Upgrade guide entry `flow-record-read-and-decision-type-mismatch-refused` (semantic: no mechanical rewrite). - -**FROM → TO.** In a record-triggered flow, or a time-relative flow sweeping its own object: `record.name > 5` (or a decision branch `name > 5`) on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text. A boolean: `record.active > 0` → `record.active == true`. A consumer keyed to the code of a flow condition's warning: `type-mismatch` → `type-mismatch-run-may-fail` (`@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` serve `type-mismatch` there; the record-scoped `type-mismatch`, on a formula field or a display predicate, is unchanged). diff --git a/.changeset/29960-push-vendor-docblocks.md b/.changeset/29960-push-vendor-docblocks.md deleted file mode 100644 index 3d5038e5c02..00000000000 --- a/.changeset/29960-push-vendor-docblocks.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(spec): the push vendor docblocks read ADR-0012 §6.1 as amended - -Clause-②: no -Metadata-protocol: none - -The `PushVendorSchema` docblock and the push transport and stored device docblocks (`system/push-config.zod.ts`, `system/push-device.zod.ts`) no longer say the domestic Android vendors plug in as alternate channel plugins. They now say what ADR-0012 §6.1 decides: `vendor` is the token type the sender routes on (the direct transport today, the hosted relay, the default, once served), core ships `apns` and `fcm`, and phase 2 adds HMS / Honor / Xiaomi / OPPO / vivo behind the relay, with no per-vendor plugin and no per-vendor channel. A new `PushVendorSchema` value is still a spec change made together with the relay's routing for that vendor. Comment text only: no schema, `.describe()`, export or default changes, and `PushVendorSchema` stays `apns | fcm`. diff --git a/.changeset/29961-stored-expression-backfill.md b/.changeset/29961-stored-expression-backfill.md deleted file mode 100644 index 259f34c5fe0..00000000000 --- a/.changeset/29961-stored-expression-backfill.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/objectql': patch -'@objectstack/cli': patch ---- - -fix(objectql,cli): `os migrate stored-expressions` fills a stored computed field on rows stored before it was declared, and the boot names the command when a sample of rows needs it - -Clause-②: yes (widening) -Metadata-protocol: none - -A stored computed field (a non-`formula` field declaring `expression`) is computed by the engine on every insert and update and on nothing else, so declaring one on an object that already holds rows left the new column NULL on every existing row until each was next written; a filter, sort, roll-up or analytics anchor over it read NULL where the expression gives a value, with no error. A fresh database was right and an upgraded one was not. - -- **`@objectstack/cli`: `os migrate stored-expressions [--object ]… [--field ] [--changed] [--apply] [--yes] [--json] [--force] [--max-records] [--database-url]`.** A dry run (the default) reports, per field, the rows holding a NULL the expression gives a value; `--apply` writes them. Each value is the write's own computation over the stored row (option labels, the one reference hop, the `can()` map), and the apply writes through `engine.update` with `updated_at` kept, so hooks, roll-ups over the column and audit capture see an ordinary write. A NULL the expression itself answers (an empty reference, a null input) is left alone. A value that differs from the expression — the expression changed after the row was written — is counted on every run and rewritten only with `--changed`; an expression reading the clock always differs there. A row on which an expression does not evaluate while it holds a value is listed and not written, because the write would store NULL over it. The command exits 1 while a row is skipped or could not be written, or when `--max-records` cut the walk short. Idempotent: a re-run finds nothing to fill. -- **Boot advisory.** At `kernel:bootstrapped` the engine plugin reads the first 100 rows of each object declaring a stored computed field (nothing for an object declaring none) and, when that sample holds such a NULL, warns once naming each `object.field` and `os migrate stored-expressions`. It writes nothing and never fails a boot. -- **`@objectstack/objectql`:** `backfillStoredExpressions`, `formatStoredExpressionBackfillReport`, `storedExpressionBackfillComplete`, `announceStoredExpressionHoles`, `sampleStoredExpressionHoles`, and `ObjectQL.computeStoredExpressionsOver(object, rows, context?, onFault?)` — a stored computed field's values over rows already stored, computed as a write computes them. diff --git a/.changeset/29962-activity-record-name-reader-locale.md b/.changeset/29962-activity-record-name-reader-locale.md deleted file mode 100644 index a072fefa82d..00000000000 --- a/.changeset/29962-activity-record-name-reader-locale.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -fix(plugin-detail): the record page's activity feed names the record as the reader reads it, as the record's heading does, when the object is named by a formula - -Clause-②: no -Metadata-protocol: none - -An activity summary quotes the record's name as its writer read it: `sys_activity.record_label`, and the same text inside `summary`, are stored when the write happens. The server composes the rest of the summary in the reader's language on read, but not the name. For an object whose name field is a formula over `label(record.FIELD)`, that name is in the writer's language, so an en-US reader of a review a zh-CN user created read `Created Review "法务 · Legal Counsel 1"` while the heading above it read `Legal · Legal Counsel 1`. - -Each activity row of the page's own record now names the record by the value of the object's name field on the record the page read, which is the heading's name. The change is made where the feed is drawn (`RecordActivityTimeline`), so the activity panel, the 动态 stream and the 动态 panel all read it. Only the name the summary quotes (`"…"`, or `「…」` in Japanese) is replaced. - -What stays as stored: - -- an object whose name field is a stored field (text and the like): the name a row stored is the name the record had then, and a rename does not rewrite older rows; -- a row about another record, a row that carries no `record_label`, and a summary that does not quote the stored name; -- a page with no record around the feed, or whose record has no name to show. diff --git a/.changeset/29969-platform-push-eval.md b/.changeset/29969-platform-push-eval.md deleted file mode 100644 index 39183279c9f..00000000000 --- a/.changeset/29969-platform-push-eval.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -test(skills): the platform skill's evals ask for push notifications on an `os serve` deployment - -Clause-②: no -Metadata-protocol: none - -`skills/objectstack-platform/evals/config-plugins-ops.json` gains case 12: the prompt asks to turn on push notifications for an `os serve` deployment, and the expected answer declares `requires: ['push']` with a top-level `push:` key on the stack definition and writes no secret inline (the APNs signing key and the Firebase service-account JSON are named by reference). The assertions stay with what `objectstack-platform/SKILL.md` teaches today, the `push` token and the top-level key; the push block's transport and vendor shape are not asserted until the skill teaches them. diff --git a/.changeset/29971-sign-in-mobile-handoff.md b/.changeset/29971-sign-in-mobile-handoff.md deleted file mode 100644 index c4678af1a07..00000000000 --- a/.changeset/29971-sign-in-mobile-handoff.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/console': patch ---- - -fix(console): the sign-in page finishes the mobile sign-in hand-off instead of landing on a missing Console page - -Clause-②: no -Metadata-protocol: none - -The mobile app opens the Console's sign-in page in the system browser with `?redirect=` naming the hand-off route (`/api/v1/auth/one-time-token/handoff?...`), so that a user whose identity provider refuses embedded WebViews can still sign in. After sign-in the page sent every safe redirect through the Console mount, so the browser landed on `/_console/apps/.../api/v1/auth/one-time-token/handoff?...` and the page said it does not exist. Now: - -- A `redirect` whose path is exactly the hand-off route under the auth base the Console talks to is navigated to unprefixed, by script from the sign-in page. Every other redirect, `/api/v1/data/x` included, stays a Console route as before; a lookalike path, another base path or an absolute URL is never followed out of the mount. -- The hand-off comes before the organization steps: a user with several organizations is no longer sent to the picker first, and one with no active organization does not wait for it to be chosen. -- A session created more than five minutes ago, which the hand-off refuses, keeps the sign-in form on screen instead of forwarding to a JSON refusal. Signing in on that form makes a new session and forwards. -- The sign-up page does the same after a sign-up. diff --git a/.changeset/29972-serve-trusted-app-schemes-env.md b/.changeset/29972-serve-trusted-app-schemes-env.md deleted file mode 100644 index 3b6d9b63973..00000000000 --- a/.changeset/29972-serve-trusted-app-schemes-env.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os serve` reads `OS_TRUSTED_APP_SCHEMES` into AuthPlugin's `trustedAppSchemes`, so a deployment can register its mobile app for the sign-in hand-off - -Clause-②: no -Metadata-protocol: none - -- **Before.** `serve` constructed `AuthPlugin` with no `trustedAppSchemes`, and no setting a deployment could make reached the key. The `one-time-token` plugin never mounted and `GET /api/v1/auth/one-time-token/handoff` answered `400 … is not a registered app scheme` for every app, whatever the operator set. -- **Now.** `OS_TRUSTED_APP_SCHEMES` is read the way `OS_TRUSTED_ORIGINS` is: comma-separated, each entry trimmed, empty entries dropped, repeats folded. The list is passed as `trustedAppSchemes`. `os dev` and `os start` run `serve` as a child with the inherited environment, so they read it through the same line. -- **Unset stays unset.** No default scheme is registered: unset, empty or only separators, the plugin stays unmounted and the hand-off refuses every target, as before. -- **A bad entry still fails the boot.** Entries are not validated, lower-cased or dropped by the reader; plugin-auth's `assertTrustedAppSchemes` refuses a bad one at construction, naming it. -- Environment variable only: no stack key is added. The hosted runtime's own `AuthPlugin` is not changed. Listed in `content/docs/deployment/environment-variables.mdx` beside `OS_TRUSTED_ORIGINS`. diff --git a/.changeset/29978-import-row-hook-aborted.md b/.changeset/29978-import-row-hook-aborted.md deleted file mode 100644 index 18f1bfa988b..00000000000 --- a/.changeset/29978-import-row-hook-aborted.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/core': patch ---- - -fix(core): a hook that aborts with free text answers `HOOK_ABORTED` on the import row too, not `IMPORT_ROW_FAILED` - -Clause-②: no -Metadata-protocol: none - -- **Before.** A sandboxed hook body that aborts with free text (`throw new Error('…')`, no `code`) answers `/api/v1/data/:object`, `/createMany`, `/api/v1/batch` and every `POST /api/v1/data/:object/batch` row with `HOOK_ABORTED` and the hook's sentence. `POST /api/v1/data/:object/import` and the async import job answered the same abort as a row `{ ok: false, code: 'IMPORT_ROW_FAILED', error: }`, so an import client branching on the row's `code` could not tell a business rule's refusal from any other failed row. -- **Now.** The import row answers `HOOK_ABORTED` (the `UNDECLARED_HOOK_REFUSAL_CODE` the other doors read), judged by `resolveThrownHttpError`, the resolver that declares that code. The value moves, from `IMPORT_ROW_FAILED` to `HOOK_ABORTED`, on every row the core import runner builds from a write failure: the synchronous import, the async import job's stored results, the create and update halves of an upsert, and every write path (single-row, batched, partial-success). -- **Unchanged.** A code the hook declared still wins, registered or not. The row's `error` is still the hook's sentence. A body that crashed, a refusal that declared a 5xx status, a driver fault and every other row failure keep the code they had. The row's `code` is an open string in `ImportRowResultSchema`, so no schema moves, and no error code is added: `HOOK_ABORTED` is the one the ledger already registers. diff --git a/.changeset/29983-api-trigger-signature.md b/.changeset/29983-api-trigger-signature.md deleted file mode 100644 index 9c419cdf88c..00000000000 --- a/.changeset/29983-api-trigger-signature.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec): an `api` flow's start node declares a third-party sender's signature scheme (`config.signature`), keyed by `config.secretRef` (#29983) - -Clause-②: yes (widening) -Metadata-protocol: added - - - -An inbound `api` hook admitted a post only by ObjectStack's own `x-objectstack-signature`, so a vendor that signs its callbacks its own way (DocuSign Connect, a Stripe-style `t=…,v1=…` header, a GitHub-style `sha256=` header) could not be admitted at all. The start node's `config` now carries `signature`, which declares how such a sender signs: - -- `signs: 'body'`: the raw body alone, the whole header value (after an optional `prefix`) is the digest. DocuSign Connect: `{ signs: 'body', header: 'X-DocuSign-Signature-1', algorithm: 'hmac-sha256', encoding: 'base64' }`. -- `signs: 'timestamp.body'`: `T.RAW_BODY`, the header being `NAME=value` elements whose `timestampElement` carries `T` and whose `digestElement` carries the digest. Stripe-style: `{ signs: 'timestamp.body', header: 'Stripe-Signature', algorithm: 'hmac-sha256', encoding: 'hex', timestampElement: 't', digestElement: 'v1' }`. - -The key is the start node's `config.secretRef`, as for an ObjectStack-signed hook. Declared, the scheme REPLACES the `x-objectstack-signature` check for that hook (one verifier per hook, never optional). The flow parse refuses a `signature` with no `secretRef`, one beside a literal `secret`, an algorithm other than `hmac-sha256`, an encoding other than `hex` or `base64`, a `timestamp.body` scheme with no `timestampElement` (or no `digestElement`, or the same name for both), a body-only scheme that names a timestamp element, a `header` naming `x-objectstack-signature` or another reserved header (`authorization`, `proxy-authorization`, `cookie`, `host`, `content-length`, `transfer-encoding`, `x-idempotency-key`) in any case, and one on a flow that binds no `api` trigger. - -New exports from `@objectstack/spec/automation`: `ApiTriggerSignatureSchema`, `ApiTriggerSignature`, its two arms `ApiTriggerBodySignatureSchema` / `ApiTriggerBodySignature` and `ApiTriggerTimestampedSignatureSchema` / `ApiTriggerTimestampedSignature`, `API_TRIGGER_SIGNATURE_KEY`, `API_TRIGGER_SIGNATURE_RESERVED_HEADERS`, `apiTriggerVerifierOf` (the one reader of which verifier a hook has: `objectstack-signature`, `declared` or `unusable`), `ApiTriggerVerifier`, `apiTriggerSignatureRefusals` and `ApiTriggerSignatureRefusal`. The `ITriggerApiService.handleInboundHook` contract text states what the implementer owes: a constant-time compare of the decoded 32 bytes, the same 300-second window as `x-objectstack-signature` for a scheme that signs a timestamp, no replay window for a body-only scheme, a repeated header read as its one comma-joined value, and the one `401 INVALID_SIGNATURE` for every failure, an unusable declaration included. - -This release declares the key; the `api` trigger does not verify it yet. Until it does, a hook that declares `signature` is still verified by `x-objectstack-signature` alone, so every post its vendor sends is refused `401`, and none is admitted unverified. diff --git a/.changeset/29988-studio-interfaces-nav-rail-module.md b/.changeset/29988-studio-interfaces-nav-rail-module.md deleted file mode 100644 index 5100c5f70b4..00000000000 --- a/.changeset/29988-studio-interfaces-nav-rail-module.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -internal: move-only — the Studio Interfaces pillar's navigation rail moves out of `StudioDesignSurface.tsx` into its own module (#29988, slice 3) - -Clause-②: no -Metadata-protocol: none - -No behaviour change. The rail Studio's Interfaces pillar draws for an app's navigation (the nav tree and its groups, the rows a customization package contributes, the off-nav sections for pages, dashboards, reports and views, the two rail contexts, the kind names and kind icons) now lives in `views/studio-design/InterfacesNavRail.tsx`. The moved code is byte-identical apart from its imports and eleven `export`s; `StudioDesignSurface.tsx` imports it from there. The package's public exports are unchanged. diff --git a/.changeset/29988-studio-nav-item-inspector-module.md b/.changeset/29988-studio-nav-item-inspector-module.md deleted file mode 100644 index a63998c5985..00000000000 --- a/.changeset/29988-studio-nav-item-inspector-module.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -internal: move-only — Studio's nav item inspector moves out of `StudioDesignSurface.tsx` into its own module (#29988, slice 1) - -Clause-②: no -Metadata-protocol: none - -No behaviour change. The menu-entry inspector Studio shows while you edit an app's navigation, with the nav target pickers and the select it draws with, now lives in `views/studio-design/StudioNavItemInspector.tsx`. The moved code is byte-identical apart from its imports and one `export`; `StudioDesignSurface.tsx` imports it from there and re-exports the inspector, so every import of it keeps working. The package's public exports are unchanged. diff --git a/.changeset/29988-studio-package-switcher-module.md b/.changeset/29988-studio-package-switcher-module.md deleted file mode 100644 index dc6ecad0d7f..00000000000 --- a/.changeset/29988-studio-package-switcher-module.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -internal: move-only — Studio's top-bar package switcher moves out of `StudioDesignSurface.tsx` into its own module (#29988, slice 2) - -Clause-②: no -Metadata-protocol: none - -No behaviour change. The package switcher in Studio's top bar (the package list, New package, the package detail sheet and the deleted-package eviction), with the package-list toast id and the Studio landing path it shares with the surface, now lives in `views/studio-design/PackageSwitcher.tsx`. The moved code is byte-identical apart from its imports and three `export`s; `StudioDesignSurface.tsx` imports it from there. The package's public exports are unchanged. diff --git a/.changeset/29988-studio-pillar-helpers-modules.md b/.changeset/29988-studio-pillar-helpers-modules.md deleted file mode 100644 index 942613c0f3f..00000000000 --- a/.changeset/29988-studio-pillar-helpers-modules.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -internal: move-only — Studio's flow rail helpers, pillar kit and Data pillar helpers move out of `StudioDesignSurface.tsx` into three modules, taking it under 10,000 lines (#29988, slice 4) - -Clause-②: no -Metadata-protocol: none - -No behaviour change. Three groups of helpers now live in their own modules under `views/studio-design/`. `AutomationsFlowRail.tsx` holds the Automations rail's flow status dot, its glyph tables, the flow reads and the new-flow trigger helpers. `StudioPillarKit.tsx` holds what every pillar shares: the held-edit notice, the unsaved-work and taken-identifier checks, the "Show me" focus helpers, the draft-saved announcement and the read-only link. `DataPillarHelpers.tsx` holds the Data pillar's field-name, CEL-stamp and form-preview helpers. The moved code is byte-identical apart from its imports and `export`s. `StudioDesignSurface.tsx` imports it from there and re-exports the five names it exported before, so every import of them keeps working. The package's public exports are unchanged. diff --git a/.changeset/29990-i18n-related-list-title.md b/.changeset/29990-i18n-related-list-title.md deleted file mode 100644 index c1d665d2e48..00000000000 --- a/.changeset/29990-i18n-related-list-title.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): the i18n skill lists a lookup or master-detail field's translatable `relatedListTitle` - -Clause-②: no -Metadata-protocol: none - -- **Before.** The `objectstack-i18n` object-level table listed `label`, `help`, `placeholder`, `options` and `inlineTitle` under `fields.{field_name}`, so an author had no word that the related-list title on a parent's record page is translatable. -- **Now.** The row adds `relatedListTitle`: the title of the related list a `lookup` or `master_detail` field draws on its parent's record page. The key sits on the child object's field, and an untranslated title keeps its authored text, never the child's label. -- The paragraph under *Diff & Coverage Schemas* saying `getCoverage()` / `suggestTranslations()` have no shipped implementation is cut; *Service Contract* in the same file says the same, and the section's opening sentence now names the two schemas as those methods' return shapes. No schema, key or behaviour changes. diff --git a/.changeset/29997-handoff-signin-entry.md b/.changeset/29997-handoff-signin-entry.md deleted file mode 100644 index 44f2a33ba85..00000000000 --- a/.changeset/29997-handoff-signin-entry.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/plugin-auth': patch -'@objectstack/spec': patch -'@object-ui/console': patch ---- - -feat(plugin-auth): a page navigation to the mobile sign-in hand-off that has to sign in first goes to the sign-in page, so the app opens only the hand-off - -Clause-②: no -Metadata-protocol: none - -`GET /api/v1/auth/one-time-token/handoff` answered a missing or old session with `401` as raw JSON, so the mobile app had to open the Console's `/login` route itself and know how that page treats `redirect`. An installed app stays in use for months, so a Console upgrade could strand it. Now the app opens only the hand-off: - -- A page navigation with no session, or with one older than five minutes, answers `302` to this deployment's Console sign-in page, with `redirect` naming the same request. `Location` is root-relative, so the browser stays on the origin the request arrived on, and `uiBasePath` is honoured. -- Any other request still answers `401`, so the app's anonymous probe keeps reading "this server registered me". A navigation reporting `Sec-Fetch-Site: same-origin` answers `401` too: it is the sign-in page forwarding a session the route refuses. A deployment that does not serve the Console keeps the `401` too, and says so once at `warn`. -- A token is still minted only for a session under five minutes old, and every `400` (a cross-site navigation, an unknown parameter, an unregistered scheme, a bad challenge) is still decided first, with no redirect. -- A device whose clock is behind does not bounce between the route and the sign-in page. The redirect carries `handoff_refused_at`, the server's time of the refusal, and the page holds its form for any session not created after it, whatever the device's clock says. A session created after it forwards, so a provider's sign-in that returns to the same URL still ends at the app. The links between the sign-in and sign-up pages carry the mark beside a hand-off `redirect`, and every other link is unchanged. - -An app can change its entry only once its server runs this release: an older server answers the new entry with JSON. diff --git a/.changeset/30000-packageless-overlay-move-heals-registry.md b/.changeset/30000-packageless-overlay-move-heals-registry.md deleted file mode 100644 index 3529fb99412..00000000000 --- a/.changeset/30000-packageless-overlay-move-heals-registry.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix(metadata-protocol): `os migrate packageless-overlays --apply` heals the registry after it deletes the unbound row, as the delete door does - -Clause-②: no -Metadata-protocol: none - -- **Now.** After the ceremony saves a row into its base's `.custom` pair and reads it back, it deletes the unbound row through the protocol's one repository and then runs the delete door's registry heal for an active row. The unbound row's registration leaves with the row, and the pair's row is registered again, so the process serves what a restart serves. Before, the delete went through a second repository and no heal followed, so the unbound row stayed registered beside the base's entry until the next restart. The five reads of the item already served the pair's row, so no read answered differently. -- **Unchanged.** A draft row's removal heals nothing, as a draft discard does not. Outcomes, receipts and the rows written are the same. diff --git a/.changeset/30001-platform-locale-table.md b/.changeset/30001-platform-locale-table.md deleted file mode 100644 index d19331c2bcb..00000000000 --- a/.changeset/30001-platform-locale-table.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -feat(spec/system): export the platform locale table `PLATFORM_LOCALES` and its read aliases - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`PLATFORM_LOCALES`** (from `@objectstack/spec/system`): the one list of languages the platform supports, as frozen rows `{ tag, label, rtl }` in menu order: `en`, `zh-Hans`, `zh-Hant`, `es`, `ar` (right to left), `fr`, `pt-BR`, `ru`, `de`, `ja`, `ko`, each labelled with its own native name. `PLATFORM_LOCALE_TAGS` is the tag list and `PlatformLocaleTag` the literal union of the tags. -- **`PLATFORM_LOCALE_READ_ALIASES`** (typed on its ten literal keys, `PlatformLocaleReadAlias`) and **`platformLocaleTagOf(code)`**: the codes stored values already carry, mapped to the table tag they mean (`zh`, `zh-CN`, `zh-SG` to `zh-Hans`; `zh-TW`, `zh-HK`, `zh-MO` to `zh-Hant`; `ja-JP` to `ja`; `es-ES` to `es`; `pt` to `pt-BR`; `en-US` to `en`). A table tag answers itself and the match ignores case (`zh-cn` reads as `zh-Hans`). Any other code answers `undefined`; no region fallback is guessed. -- The table is the target every surface is backfilled to; today the Console ships ten packs without `zh-Hant`, and platform metadata and the built-in emails ship four locales. Nothing reads the table yet and no schema refuses a tag that is not on it: every locale value that parsed before still parses. The protocol page `i18n-standard` gains the table, the read aliases and the rule (same-PR follow; a language is added or removed by changing the table and backfilling once). diff --git a/.changeset/30005-cross-closure-dataset-filter-keys.md b/.changeset/30005-cross-closure-dataset-filter-keys.md deleted file mode 100644 index 7e41c11f0c3..00000000000 --- a/.changeset/30005-cross-closure-dataset-filter-keys.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/lint': minor ---- - -fix(lint)!: under a package closure, a dashboard widget `filter` or report `runtimeFilter` on another package's dataset is judged against that dataset's object, so a key the object lacks is refused at publish (#30005) - -Clause-②: no (narrowing) — the runtime publish door, for a write that names its package, refuses a dashboard widget `filter` key or a report `runtimeFilter` key on a dataset outside the package's closure when the dataset's object has no such field, a body it accepted before. Nothing is widened: `narrowObjectsToPackageClosure` keeps its signature and its four-limb answer, and no export is added to any package entry. -Metadata-protocol: narrowed -Skills-docs: updated here — `content/docs/deployment/validating-metadata.mdx` (the fourth-door paragraph) now names the fifth closure limb for dashboard and report writes; `skills/**` teaches neither the rules nor the closure. - - - -**BREAKING** accept-set narrowing at the runtime publish door for `dashboard` and `report` writes (the per-item publish and the package draft publish; Studio, REST and MCP authors), for a write whose package is known. `os validate`, `os build` and `os lint` are unchanged: they never narrow to a closure. - -- **Before.** The publish gate narrows the live `objects` to the written package's closure (the package, its declared dependencies, platform objects, rows with no package) but carries every dataset. A runtime package's dashboard on `showcase_task_metrics`, a dataset the showcase package ships over `showcase_task`, therefore found the dataset but not its object, and `widget-filter-field-unknown` and `report-filter-field-unknown` skipped the filter as "object not in this snapshot". `filter: { no_such_field: 'x' }` published `200`, and every reader saw an empty tile or an error panel. -- **Now.** A dashboard or report write also carries the base object of each dataset it binds (`widgets[].dataset`; a report's `dataset` and each joined block's `dataset`), which is the object the dataset query resolves the filter against at run time. The same key is refused `422 INVALID_METADATA` at `dashboards[0].widgets[0].filter.no_such_field` (or `reports[0].runtimeFilter.no_such_field`), the verdict the same key already got on a dataset inside the closure or with no closure in force. -- **Which rules now judge such a dataset.** Every rule that resolves a dashboard's or report's filter against the dataset's object, all six already judging datasets inside the closure: `widget-filter-field-unknown`, `widget-filter-field-not-included`, `dashboard-filter-field-unknown`, `dashboard-filter-field-not-included`, `dashboard-filter-field-unprovisioned` and `report-filter-field-unknown`. -- **A dimension name as a widget filter key.** On a dataset outside the closure, a widget `filter` key that names a dimension of the dataset but no field of its object is now refused with `widget-filter-field-unknown`, as it already was on a dataset inside the closure. The dataset query binds such a key; the correction of that verdict, for both cases, is tracked in #30006. -- **Accepted, as before.** Every key the object has, a registry-injected system column, a dotted relationship path, every `$` operator. A dotted key whose hop lands on an object the snapshot still does not carry is not judged. -- **Not changed.** Only dashboard and report writes carry the extra objects. A flow trigger, a list view or a dataset base object outside the closure is still refused as before (`flow-trigger-unknown-object`, `list-view-object-outside-closure`, `object-reference-unknown`), and a list view's `chart.dataset` brings no object in. - -**What an author does.** Nothing for a board or report whose filter keys name real fields. A refused key never filtered anything, so there is no rewrite that keeps what the author meant: name a field of the dataset's object. The refusal names the key and the nearest field. diff --git a/.changeset/30006-dataset-filter-key-dimension-measure.md b/.changeset/30006-dataset-filter-key-dimension-measure.md deleted file mode 100644 index 0ebd2c56f32..00000000000 --- a/.changeset/30006-dataset-filter-key-dimension-measure.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/lint': patch ---- - -fix(lint): a widget filter keyed on a dataset dimension publishes, and a filter key naming a dataset measure is refused with its own message - -Clause-②: yes (widening) -Metadata-protocol: none -Skills-docs: updated here — content/docs/deployment/validating-metadata.mdx - -`widget-filter-field-unknown` and `report-filter-field-unknown` now judge a bare filter key against one accept set, held in `judgeDatasetFilterKeys`, so the two rules cannot drift: a field of the dataset's object (authored or registry-injected), then a dimension of the dataset. - -- **Widened:** a dashboard widget's own `filter` keyed on a dataset dimension's name (`{ state: 'open' }` for a dimension `state` over the field `status`) was refused at publish as an unknown field, although the dataset query binds it through the dimension's field. It now publishes, as the same key on a report's `runtimeFilter` already did. -- **Refused with its own message:** a bare key naming a dataset measure was already refused by both rules as an unknown field. It is now refused as a measure: a filter compares each row before aggregation, so the dataset query would compare the measure's raw column (`{ amount_sum: { $gt: 100 } }` compiles to `amount > 100`, not a filter on the total), or no column at all for a count, derived or duration measure. The fix line names the measure's column when it reads exactly one. A measure that shares a field's name is still the field, and accepted. - -No key that published before is refused now. diff --git a/.changeset/30009-api-trigger-declared-signature.md b/.changeset/30009-api-trigger-declared-signature.md deleted file mode 100644 index 8caad3b0f1a..00000000000 --- a/.changeset/30009-api-trigger-declared-signature.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/trigger-api': patch -'@objectstack/lint': patch -'@objectstack/skills': patch ---- - -feat(trigger-api): an `api` hook verifies the vendor signature its start node declares (`config.signature`) - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@objectstack/trigger-api`.** `start()` reads the hook's one verifier with `apiTriggerVerifierOf(binding.config)` (`@objectstack/spec/automation`). A start node that declares its sender's scheme (`config.signature`: DocuSign Connect's base64 `X-DocuSign-Signature-1`, a Stripe-style `t=…,v1=…`, a GitHub-style `sha256=`) now admits a correctly signed post that was answered `401` before. That scheme REPLACES the `x-objectstack-signature` check for its hook: the declared header is read on both faces of the door (the self-hosted mount and `handleInboundHook`) with `Headers.get`, so a repeated field is its one joined value; the HMAC-SHA256 is computed under the key `config.secretRef` names (read at every post through the engine's reader, `503` while it has no value), over the raw body or `.`; each digest is decoded strictly to 32 bytes and compared with `timingSafeEqual`, any one of several `v1` elements matching (a key roll); and a timestamped scheme is held to `HTTP_SIGNATURE_TOLERANCE_SECONDS` from `@objectstack/core`. A body-only scheme has no replay window, as the contract states. A valid `x-objectstack-signature` admits nothing on such a hook, and a post without the declared header is refused. A declaration the spec answers `unusable` arms a hook that refuses every post before any secret is read and never falls back to `x-objectstack-signature`; a declared scheme bound without the engine's `config.secretRef` reader is refused at `start()`. Every failure is the same `401 INVALID_SIGNATURE` with its own cause in the server log (`declared-header-absent`, `declared-header-malformed`, `declared-prefix-missing`, `declared-no-digest`, `declared-mismatch`, `declared-outside-window`, `declared-unusable`), never the key, the header value or the body. A hook that declares no scheme is verified exactly as before. -- **`@objectstack/lint`.** `validateFlowApiTriggerSecret` grades an `api` hook by that same reader: a usable declaration is ready on its `config.secretRef` alone, and an unusable one is the new error `flow-api-trigger-signature-unusable` (with its `os explain` entry), located at the key the spec refuses. It fires only on a declaration the flow parse already refuses, so no stack that passed before is refused now. -- **`@objectstack/skills`.** The automation and api skills teach `config.signature` where they said a vendor-signed callback cannot be admitted. diff --git a/.changeset/30011-environment-scoped-rest-providers.md b/.changeset/30011-environment-scoped-rest-providers.md deleted file mode 100644 index b65c9d89f2f..00000000000 --- a/.changeset/30011-environment-scoped-rest-providers.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): the approvals, email, sharing, sharing-rule and analytics routes answer from the request's environment kernel when one REST plugin fronts many environments - -Clause-②: no -Metadata-protocol: none - -- **What was wrong.** On a host that mounts one `RestApiPlugin` in front of many environment kernels (the hosted runtime shape), the five provider seams the plugin hands `RestServer` for `approvals`, `email`, `sharing`, `sharingRules` and `analytics` read the HOST kernel's service whatever environment they were asked for, and the unscoped (hostname-routed) mount asked them for no environment at all. The environment's own services were never reached: `GET /api/v1/approvals/requests` answered `{ data: [] }`, `GET /api/v1/approvals/requests/:id` answered 501, and the sharing, sharing-rule and email doors answered 501, on every hosted environment. The record page's approvals part answered no requests. -- **What changes.** Given an environment id and a registered `kernel-manager`, each of the five providers now reads its slot on that environment's kernel (`kernelManager.getOrCreate(id)`), the same acquisition the data routes make. The routes pass the id the shared request→environment resolution answered: the `/environments/:environmentId` param on the scoped mount, else the host's `kernel-resolver` (or the hostname / `X-Environment-Id` chain) on the unscoped one. There is no fallback to the host's occupant: an environment whose kernel carries no such service gets the door's own "not configured" answer. -- **Analytics.** `POST /analytics/dataset/query` already read the environment's service first, and now reads it alone: when the environment's kernel carries no analytics service, or cannot be acquired, the answer is the route's 501 rather than the host's service. -- **Unchanged.** A single-kernel deployment (no `kernel-manager`) reads all five services on the plugin's own kernel exactly as before, for every route and every id. The reserved `platform` id and a request the resolver places in no environment also read the plugin's own kernel. -- **No migration.** No configuration, route, response shape or export changes. diff --git a/.changeset/30016-flow-date-arithmetic-run-fails.md b/.changeset/30016-flow-date-arithmetic-run-fails.md deleted file mode 100644 index b8c28da5150..00000000000 --- a/.changeset/30016-flow-date-arithmetic-run-fails.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/formula': patch -'@objectstack/lint': patch ---- - -fix(formula,lint): a date-arithmetic verdict in a flow condition says the run fails, not that the value reads null - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@objectstack/formula`: four codes join `ExpressionRefusalCode`**: `date-arithmetic-run-fails`, `date-arithmetic-run-may-fail`, `date-text-arithmetic-run-fails` and `date-text-arithmetic-run-may-fail`, with the same params as `date-arithmetic` / `date-text-arithmetic`. A flow condition (a `'flattened'` site) that does arithmetic on a date field used to be told the field "silently evaluates to null", but the engine's `evaluateCondition` throws on the fault on every flow, so the run fails at the condition. Where the caller declares the run proven to hold the field (`faultOutcome: 'run-fails'`, with `bareNamesUnproven` unset) the verdict is served under `-run-fails` and says the run fails; anywhere else in a flow it is served under `-run-may-fail` and says the run fails wherever the name holds the field's date when the condition runs (a flow started by an API call, an action or a parent flow binds what its entrance hands over, which can be a number). The grade is unchanged: an error at every site. A `record`-scoped site (a formula field, a validation rule, a display predicate) keeps `date-arithmetic` / `date-text-arithmetic` and their sentence. A consumer keyed to the old codes on a flow condition re-keys to the new ones. -- **`@objectstack/lint`**: the fault-outcome ledger's docblock names the date verdicts' flow codes; the routing is the existing `faultOutcome` / `bareNamesUnproven` hint, unchanged. diff --git a/.changeset/30017-action-icon-members-confirm-beside-params-refused.md b/.changeset/30017-action-icon-members-confirm-beside-params-refused.md deleted file mode 100644 index 564d2095e97..00000000000 --- a/.changeset/30017-action-icon-members-confirm-beside-params-refused.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -fix(spec)!: a page `action:icon` (`ActionIconPropsSchema`) and each `action:group` / `action:menu` member refuse `confirmText` beside a non-empty array `params`, as `action:button`, a registered action (`ActionSchema`) and an inline action (`InlineActionSchema`) already do, instead of accepting a pair that opens two dialogs for one decision - -The `action:icon` block forwards its row's `confirmText` and an array `params` (as `actionParams`) to the console's action runner, and both containers do the same for each member. The runner awaits the confirm dialog and only then opens the param dialog, so the user saw a confirm that already read as "the action ran" while nothing had been sent, then a second dialog. The other three action doors refused that pair; these two accepted it. They now refuse it with the same predicate and the same message, at the `confirmText` path: `confirmText` on the icon's properties, `actions.N.confirmText` on a container member. - -Clause-②: no (narrowing) — `ActionIconPropsSchema` and the `action:group` / `action:menu` member rows refuse `confirmText` beside a non-empty array `params`, which they accepted before. Nothing is widened. - -Metadata-protocol: narrowed (a page `action:icon`, and an `action:group` or `action:menu` member, may no longer declare `confirmText` beside a non-empty array `params`) - - - -**BREAKING** accept-set narrowing at `ActionIconPropsSchema` and the `action:group` / `action:menu` member rows (`@objectstack/spec`), served where a page's component `properties` are judged: the component-props gate (`component-props-invalid`, `@objectstack/lint`) at every CLI command (`os validate`, `os compile`, the publish gate) and at the runtime `page` save door. `defineStack` does not parse a component's `properties` bag, so it does not refuse the pair. - -- **Before.** `{ type: 'action:menu', properties: { actions: [{ name: 'reject', label: 'Reject', type: 'api', target: '/api/v1/approvals/reject', confirmText: 'Reject this request?', params: [{ name: 'reason', type: 'textarea' }] }] } }` parsed clean, and the console showed a confirm dialog and then the param dialog. The same held for an `action:icon` with both keys. -- **Now.** The same `properties` are refused at `actions.0.confirmText` (at `confirmText` on an `action:icon`) with the action doors' message: the pair shows TWO dialogs for one decision; carry the question in `description` and drop `confirmText`. -- **Unchanged.** `confirmText` with no `params`, beside `params: []`, or beside an object `params` (an icon's static parameter values; a `type: 'api'` member's request payload — neither opens a dialog) parses as before; so does an array `params` with `description`. -- **Who sees it.** An author whose page declares an `action:icon`, or an `action:group` / `action:menu` member, with both keys. A stored page keeps loading and serving; its next save is refused, with the remedy in the finding. - -**The one-line fix: move the confirm sentence to the action's `description`, which the param dialog draws under its title, and drop `confirmText`.** `os migrate meta --from 17` lists the D3 entry `ui-page-action-row-confirm-text-beside-params-refused`, which names all four page action rows (`action:button`, `action:icon`, and each `action:group` / `action:menu` member) — the `action:button` refusal shipped earlier in 18.x is covered by the same entry. The rewrite is by hand, because whether the sentence reads as the dialog's description line is the author's call. On an `action:icon` with no `label`, `description` is also the tooltip text; give the icon a `label` to keep them apart. diff --git a/.changeset/30018-page-header-placed-actions.md b/.changeset/30018-page-header-placed-actions.md deleted file mode 100644 index 2e8105d8465..00000000000 --- a/.changeset/30018-page-header-placed-actions.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/skills': minor ---- - -feat(spec): a `page:header` declares `placedActions: true`, the explicit opt-in to draw every action of the bound object placed at `record_header` / `record_more`, read live (#30018) - -Clause-②: yes (widening) -Metadata-protocol: added - -- **`placedActions`** on a `page:header` component's `properties` (`PageHeaderProps`, the `ComponentPropsMap['page:header']` row): `true` asks the header to draw every action of the page's object whose `locations` include `record_header` or `record_more`, read live from the object's metadata, in place of an authored `actions` id list. An action another package places on the object is then drawn without editing the page. Optional with no default: an unset key stays unset in the parsed output. -- **The `actions` list keeps its meaning.** An authored list is the whole set the header draws, and a header with neither `actions` nor `placedActions: true` draws none of the object's actions. `placedActions: false` beside a list is accepted and changes nothing. -- **Refused together.** `placedActions: true` beside an authored `actions` list, an empty one included, is refused at `properties.placedActions` by `os validate`, `os build`, `os lint` and the page save door (`component-props-invalid`, an error). The message names both keys and asks the author to keep one: `actions` for a curated list that is the whole set, or `placedActions: true` for every placed action. Nothing that parsed before is refused: until this release `placedActions` was an unknown key on the closed shape. -- **Declared ahead of its reader.** The console's header renderer does not read the key yet, so a header that sets it draws what the same header without it draws (none of the object's actions unless `actions` names them). The key's describe opens with `[PLANNED — not yet rendered]` until the renderer reads it. -- The published JSON Schema `ui/PageHeaderProps` still accepts the pair: the rule depends on the value `true`, which no projected refinement pattern states, so the parse refuses it and `dropped-refinements.baseline.json` records the site. -- `@objectstack/skills`: `objectstack-ui` `rules/pages.md` names the key in the component catalogue, marked not drawn yet, and drops a refused-shape clause from the print-page paragraph that the sentence before it already states. diff --git a/.changeset/30023-stored-sort-sortable-false-lint.md b/.changeset/30023-stored-sort-sortable-false-lint.md deleted file mode 100644 index 144db57cf45..00000000000 --- a/.changeset/30023-stored-sort-sortable-false-lint.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/lint': minor -'@objectstack/spec': minor ---- - -fix(lint)!: a stored sort naming a field that declares `sortable: false` is refused by `os validate` and at publish, judged by the same sortability projection the Data API list door refuses it by — on a list view and on every stored page sort (#30023) - -Clause-②: yes (narrowing) — `sort-field-unsortable` (error) now also fires on a sort naming a field whose author declared `sortable: false`, on the list-view rungs it already walked and on every stored page sort it did not walk before (a block's own `sort`, every element `dataSource.sort`, a related list's `parentDataSource.sort`, a list page's `interfaceConfig.sort`, an app page's `record.sort`, where a `formula` sort is now refused too), so `os validate` / `os build` / `os lint` and the `page` and `view` publish doors refuse metadata they used to accept. The widening half: `@objectstack/lint` exports `indexObjectSortability` and the `SortabilityFields` type, and `checkSortDeclaration` takes an optional trailing `sortability` index. -Metadata-protocol: narrowed -Skills-docs: none — `skills/**` names no `sortable` field key and no page or view `sort` rule; `content/docs/**` already teaches `sortable: false` as "do not allow sorting by this field". The prescription ships in the finding's fix line, `os explain sort-field-unsortable` and the upgrade-guide entry. - - - -**BREAKING** accept-set narrowing at authoring (`@objectstack/lint`): a stored sort the Data API list door refuses is refused before it is stored. - -- **Before.** A list view whose `sort` named a field marked `sortable: false` passed `os validate` and the `PUT /api/v1/meta/view` door, then answered `400 INVALID_SORT` on its first fetch and on every fetch after it. No page sort was judged for sortability at all: a `record:related_list`, `record:line_items`, `list-view` or `object-*` block sorting by such a field (or by a `formula` field), an element `dataSource.sort`, a related list's `parentDataSource.sort`, a list page's `interfaceConfig.sort` and an app page's `record.sort` all saved green and failed at read. -- **Now.** `validateSortableFields` judges each of those positions by `resolveObjectSortability`, the projection the list door refuses by, and reports `sort-field-unsortable` at `error` with the door's own clause ("a field on object X that declares `sortable: false`") at the slot's path. The reference-integrity suite now runs on a `page` write for this one member, so the Studio, REST `/meta` and MCP page doors give the same refusal (422). Only the literal `false` counts: a field with no `sortable` key, or `sortable: true`, sorts as before. A `formula` field also marked `sortable: false` keeps its virtual-field verdict. Existence on page sorts stays `page-field-unknown`. -- **Not changed.** A stored view or page already carrying such a sort keeps loading and serving; only its next save is judged, and its first fetch still answers 400 as it did since the list door started refusing it. - -**The remedy.** Sort by another field, or remove `sortable: false` from the field to make it sortable. - -**Supersedes, in the same release.** The `@objectstack/spec` / `@objectstack/metadata-protocol` / `@objectstack/mcp` note for #29896 says "Lint does not yet judge these at authoring." From this release it does: `os validate` and the publish doors refuse every stored sort that note lists, and the stored page sorts it did not list. diff --git a/.changeset/30024-list-refused-sort-not-sent.md b/.changeset/30024-list-refused-sort-not-sent.md deleted file mode 100644 index 2e7ce2b305e..00000000000 --- a/.changeset/30024-list-refused-sort-not-sent.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/plugin-list': patch -'@object-ui/plugin-grid': patch ---- - -fix(plugin-list): a stored view sort the platform refuses is no longer sent, so the list loads instead of failing; a column on a `sortable: false` field offers no sort when the grid sorts in the browser - -Clause-②: no -Metadata-protocol: none - -**Visible behaviour change on a released page.** A list view whose stored `sort` (or a restored user sort) names a field the platform will not order by used to send that sort with its data request. The Data API answers such a sort `400 INVALID_SORT`, so the list showed its load error instead of its rows. Fields the platform refuses are the ones its sortability projection, served with the object's metadata, marks unsortable: a `formula` field today, and a field marked `sortable: false` once the platform honours that flag. - -- **`@object-ui/plugin-list`:** the list's data request, its server export and the kanban, calendar, timeline and gantt views (which fetch their own rows) now send the sort without the refused entries. The entries left keep their original order, and a sort with nothing refused in it is sent unchanged. The refused entry is still listed in the sort picker as removable only, as before, and the next sort the list saves leaves it out. With no projection served (an older platform, inline rows), the sort is sent as before. -- **`@object-ui/plugin-grid`:** a column listed in a view's `columns` now reads its field's `sortable: false`, as columns derived from the object's fields already did. Where the grid sorts the rows it holds (inline rows), such a column no longer offers a sort click. A `formula` column keeps its sort click there, since the browser orders by the value the cell shows; where the sort is sent to the server, the platform's projection still decides, as before. diff --git a/.changeset/30025-flow-bare-name-screen-open-binder.md b/.changeset/30025-flow-bare-name-screen-open-binder.md deleted file mode 100644 index 8cff0cf1bbf..00000000000 --- a/.changeset/30025-flow-bare-name-screen-open-binder.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/formula': patch -'@objectstack/lint': patch ---- - -fix(lint): a flow condition's bare-name type verdict no longer refuses a name a `screen` field holds, and stands down to the warning after a node that can bind any name (#30025) - -Clause-②: yes (widening) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) no longer refuses, on a record-triggered flow (or a time-relative sweep of its own object), a `type-mismatch-run-fails` on a BARE condition name that a `screen` node's declared field holds (no verdict now), nor on a bare name in a flow with a node that can bind a name no reader sees (`script`, `connector_action`, `wait`, `subflow`, `map`, a screen with no input contract), which is now the `type-mismatch-run-may-fail` warning. A `record.` / `previous.` read keeps its refusal, and nothing new is refused. The null-guard verdict's bare names also leave a screen's field names out. Added beside them: the optional `bareNamesUnproven` key of `@objectstack/formula`'s `ExprSchemaHint`, and `collectScreenFieldNames` in `@objectstack/lint`'s flow-variable-scope module. -Metadata-protocol: none -Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names the screen field and the open binder); `skills/**` and the MCP skill text teach no bare flow-condition name a screen or an open binder holds, so no skill or eval moves. - -- **Before.** A record-triggered flow whose `screen` node collects `title` (a number) and whose edge after it reads `title > 5`, with `title` a text field on the trigger object, was refused `type-mismatch-run-fails`: the bare name was typed as the field. The run binds the collected number there (a resume lands a screen's values under their plain names), so the condition runs. The same refusal hit a bare name after a `script`, `connector_action`, `wait`, `subflow` or `map` node, or a screen with no input contract, any of which can bind the name to a value nothing can see at build. -- **Now.** A screen's declared field names join the names a bare read holds instead of the field, so they are not typed as it (the null-guard verdict leaves them out too). In a flow with a node that can bind any name, a bare-name mismatch is the `type-mismatch-run-may-fail` warning, as the null-guard verdict already stands its bare names down there; `record.` is still refused. The stand-down reads the flow's nodes only: an entrance (and the publish door's per-write snapshot, which sees none) changes which record both the bare name and `record.` read, not whether a node rebinds the bare name, so a bare-name refusal on a record-triggered flow with no such node is unchanged at every door. diff --git a/.changeset/30026-time-relative-sweep-typing.md b/.changeset/30026-time-relative-sweep-typing.md deleted file mode 100644 index 2f13a7cbbe5..00000000000 --- a/.changeset/30026-time-relative-sweep-typing.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/lint': minor -'@objectstack/mcp': minor ---- - -fix(lint)!: a time-relative flow whose start node names no `objectName` has its conditions typed against the object it sweeps (`timeRelative.object`), so a text-vs-number comparison, a read of a field the swept object does not declare and date arithmetic there are refused like on a sweep of the start node's object; a sweep of another object is told why it only warns; and `validate_expression` takes the swept object (#30026) - -Clause-②: yes (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses, in the start, edge and `decision` conditions of a time-relative flow whose start node names no `objectName`, a `type-mismatch` (`type-mismatch-run-fails`), a `record.FIELD` read of a field the swept object does not declare (`unknown field`), and date arithmetic (`date-arithmetic-run-fails` / `date-text-arithmetic-run-fails`), which it passed with no verdict at all before. Added beside it (the widening half): the optional `sweepObject` input of the `validate_expression` MCP tool. -Metadata-protocol: narrowed -Skills-docs: updated here — the MCP skill text in `packages/mcp/src/skill-md.ts` (the new `sweepObject` input); `skills/**` teaches no time-relative flow condition with a text-vs-number comparison, so no skill or eval moves. - - - -**BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. - -- **Before.** A time-relative flow was typed only against its start node's `objectName`. A start that named none, with the swept object in `timeRelative.object` (the key the descriptor requires), got no type verdict on its conditions at all: `title > 5` on a text field published clean, and every run the sweep launched failed at that condition. -- **Now.** The conditions of such a flow are typed against `timeRelative.object`: each run's record is a row of that object, so its start, edge and `decision` conditions get every verdict a sweep of the start node's object already gets. Three of them are new refusals there: a text-vs-number comparison (`type-mismatch-run-fails`), a `record.FIELD` read of a field the swept object does not declare (`unknown field FIELD on OBJECT`; the run fails on the missing key), and arithmetic on a date field (`date-arithmetic-run-fails`, or `date-text-arithmetic-run-fails` against text). The shadowing and near-miss warnings and the root check are judged against the same object. -- **A sweep of another object** (the start node's `objectName` is not `timeRelative.object`) keeps the `type-mismatch-run-may-fail` warning, and the flow now gets one more warning, at its start node's `config.objectName`, saying why: the conditions are checked against that object's fields while each run's record is a row of the swept one. It is served only on a flow that got a warning it explains. -- **`validate_expression` (MCP).** New optional input `sweepObject` (site `flow_condition` with `triggerKind: 'time_relative'` only; a tool error anywhere else): the flow's `timeRelative.object`. When it is another object than `objectName`, a type mismatch is the gate's warning instead of the run-fails error. A call that does not pass it answers as before. -- **Correction to the 18.x changeset of the flow-condition type check.** It described a time-relative flow whose `timeRelative.object` "is omitted" as sweeping the start node's object. That shape never binds: `TimeRelativeTriggerSchema` requires `object`, and the gate refuses its absence (`flow-time-relative-descriptor-invalid`). Such a flow now gets the warning, not the run-fails verdict, on its conditions, since no run of it binds a row. - -**FROM → TO.** In a time-relative flow whose start names no `objectName`, sweeping `crm_opportunity` with a text field `title` and a date field `close_date`: `title > 5` (or `record.title > 5`, or a decision branch `title * 2 > 10`) → `size(title) > 5` for its length, a number field for a value, or `double(title) > 5` only when every value is numeric text. `record.nope > 1`, where `crm_opportunity` declares no `nope` → the field's declared name, or the object that declares it as `timeRelative.object`. `close_date + 1 > …` → `addDays(close_date, 1) > …` to shift a date (`addMonths(d, n)` and `daysFromNow(n)` likewise), or `daysBetween(a, b)` for a span in whole days. A flow whose start names one object and sweeps another: set the start node's `objectName` to the swept object, or remove it. diff --git a/.changeset/30031-page-builder-header-breadcrumb.md b/.changeset/30031-page-builder-header-breadcrumb.md deleted file mode 100644 index c8a9c66bcfd..00000000000 --- a/.changeset/30031-page-builder-header-breadcrumb.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): the `page:header` title row drops the retired `breadcrumb` and names the live `placedActions: true` - -Clause-②: no -Metadata-protocol: none - -- `skills/objectui/guides/page-builder.md`: the `page:header` sentence named `breadcrumb` among the title row's keys. `PageHeaderProps.breadcrumb` is a `retiredKey` tombstone since @objectstack/spec 17 (ADR-0087 D2), and the props lint refuses an authored one. The row now lists `title`, `subtitle`, `actions` or `placedActions: true`; the last two are refused together. diff --git a/.changeset/30033-related-table-name-column.md b/.changeset/30033-related-table-name-column.md deleted file mode 100644 index 864bb3ec63f..00000000000 --- a/.changeset/30033-related-table-name-column.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -fix(plugin-detail): the related table leads with the record's name when the relation declares no columns - -Clause-②: no -Metadata-protocol: none - -A related list with no columns declared took its columns from the child object's `highlightFields`, which by design never list the record's title, so the related tab on a record page showed amounts and dates with no deal or contact name, and the only link in a row was its first value. The record's name (`nameField`, else the field the platform derives, ADR-0079) now leads that set, listed once even when a highlight names it, and the grid links that first column to the record. The field walk used when the child declares no `highlightFields` leads with the declared name the same way. The enlarged tab and the table in the record body read the same set. Columns an author declares on the relation are drawn exactly as declared. A `titleFormat`-only object leads with the field the platform derives, not the formatted title, which has no column behind it. diff --git a/.changeset/30034-personal-view-url-default-state.md b/.changeset/30034-personal-view-url-default-state.md deleted file mode 100644 index ea93d4814ac..00000000000 --- a/.changeset/30034-personal-view-url-default-state.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): a personal view opened by its address no longer carries the default view's filter and sort in the address bar - -Clause-②: no -Metadata-protocol: none - -- While the list waits for the reader's own views to resolve a requested or pinned view it does not hold yet, the list's URL state is neither seeded nor written. The default view the page falls back to meanwhile is not the view the address names, so its stored filter, sort and search no longer reach the address bar. Before, when the list preferences landed first, that default seed was written to the URL, and a personal views read landing before the write committed left it there: a reload or a shared link then opened the personal view with conditions nobody chose for it and marked it personalized. -- When the view resolves, the seed is taken for it alone: from its own stored state, or from the `uf_*` params a link carried, as before. diff --git a/.changeset/30035-record-feed-reread-after-save.md b/.changeset/30035-record-feed-reread-after-save.md deleted file mode 100644 index 1ce6367a31d..00000000000 --- a/.changeset/30035-record-feed-reread-after-save.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): a record's 动态 stream is read again after a save, so its count and rows hold the change without a reload - -Clause-②: no -Metadata-protocol: none - -- **The record feed follows the record's invalidation.** A save through the edit dialog (or an action, an undo, a ⟳) put the record on the data-invalidation bus, and the page read the record again, but the 动态 feed kept the rows and the count the page payload answered when it opened. The feed now reads again on the same change, on the record page and in the drawer. -- **The re-read asks what the open asked.** Once the record changed after the open, the feed asks the record page route for the streams the open asked, and only those (the discussion, the activity rows, the stream's count), so the new activity row and the server's count land in place. The activity row a save writes exists before the save answers, so one ask after the save sees it; nothing polls. -- **The field changes follow the same change**, so on an object with `enable.trackHistory` the stream's field-change entries are read again too. diff --git a/.changeset/30036-activity-feed-default-view.md b/.changeset/30036-activity-feed-default-view.md deleted file mode 100644 index 414811137e6..00000000000 --- a/.changeset/30036-activity-feed-default-view.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/plugin-audit': patch ---- - -fix(plugin-audit): the activity list opens newest first, without the platform's own rows, under a plain name and description - -Clause-②: no -Metadata-protocol: none - -- **`sys_activity` declares two list views.** `recent` comes first, so it is the list Home's "view all activity" and the bell's Activity tab open (both name no view): newest first, and only rows a person wrote (`actor_id` set) about something other than the platform's own objects: the metadata store, the data-migration ledger, and every identity object `@objectstack/platform-objects/identity` declares (users, members, organizations, sessions, keys, SCIM): every sign-in stamps the last-login columns on `sys_user` under the signing-in person's own name, the signing key in `sys_jwks` is minted under the first sign-in's session, and adding a person writes `sys_user` and `sys_member` rows that person can read. Sign-in stamps, migrations, seeds and metadata history no longer sit between the business rows; an administrator's edits to users are in `all`. `all` lists every row the reader may read, platform rows included, newest first. Nothing is removed from storage and nothing changes in what is written; every excluded row still has its `sys_audit_log` entry. -- **Label and description.** The object reads `Activity Feed` (zh-CN `动态`) instead of `Activity`, which collided with an app's own activity object (a CRM's calls and meetings), and its description says what the page is instead of `Recent activity stream entries (lightweight, denormalized)`. diff --git a/.changeset/30037-sort-picker-hidden-fields.md b/.changeset/30037-sort-picker-hidden-fields.md deleted file mode 100644 index 05e12eb50fa..00000000000 --- a/.changeset/30037-sort-picker-hidden-fields.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/plugin-list': patch ---- - -fix(plugin-list): the list's sort picker no longer offers a field the object marks hidden, such as the queue column or the search index - -Clause-②: no -Metadata-protocol: none - -**Visible behaviour change on a released page.** A list's 排序 / Sort picker offered every field of the object, including the injected system columns the platform marks `hidden: true` (the queue column, the `__search` companion shown as an untranslated "Search Index", `organization_id`). The Filter panel's field list already left those out; the sort picker now follows the same rule. - -- A field the object definition marks `hidden: true` is no longer listed in the sort picker, and "Add sort" never starts on one. -- A view or saved sort that already orders by such a field keeps working: the field stays listed in that row as removable only (disabled), as the picker already does for an unreadable or refused field, so the row is not blank and can be removed. -- A hidden lookup (such as `organization_id`) no longer raises the "relational fields cannot be sorted" hint under the rows, since the picker never offered it. diff --git a/.changeset/30038-flow-closure-refusal-copy.md b/.changeset/30038-flow-closure-refusal-copy.md deleted file mode 100644 index 23bec21b707..00000000000 --- a/.changeset/30038-flow-closure-refusal-copy.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(app-shell): a flow bound to an object its package may not use is explained on the Problems list in the Console's language - -Clause-②: no -Metadata-protocol: none - -When a package's flow triggers on, scans, or declares a start input of an object outside what the package may use (its own objects, the objects of the packages it declares as dependencies, and system objects), the publish refuses it, and the flow's Problems list showed the server's English sentence in packaging terms to a reader in any language. The Problems list now words that refusal from the served entry's rule id (`flow-trigger-unknown-object`, `flow-input-unknown-object`) and its path: it names the object the flow holds there, says which objects a package can use, and points at the two fixes, picking an object from the list or declaring a dependency on the package that defines it. The copy lives in the Studio string namespace in English and Chinese. Any other server entry, and any of these at a position the Console does not word, still shows the server's sentence as sent. The served shape does not change: the editor pages now keep each served error's `code` on the entries they hand the canvas, where it used to be dropped. diff --git a/.changeset/30038-trigger-object-enter.md b/.changeset/30038-trigger-object-enter.md deleted file mode 100644 index d6dff3426ab..00000000000 --- a/.changeset/30038-trigger-object-enter.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(app-shell): New automation no longer saves a typed prefix as the trigger object (#30038) - -Clause-②: no -Metadata-protocol: none - -In Studio's New automation dialog, typing part of an object name and pressing Enter committed the typed text itself, so the flow was created bound to an object that does not exist. The trigger-object picker there now takes objects only: Enter or leaving the field commits the object the text names exactly, else the only match left in the list, else no object, and the picker says no object is chosen. A record preset then keeps the dialog open until an object is picked. - -Every other object picker keeps its free text, because a flow reference may hold an expression. diff --git a/.changeset/30039-studio-zh-terms.md b/.changeset/30039-studio-zh-terms.md deleted file mode 100644 index aef9ae925e5..00000000000 --- a/.changeset/30039-studio-zh-terms.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(app-shell): Studio on a zh-CN console reads in Chinese in the view, page and report panels, the publish sheet, the flow Problems tab and the save-notice / notify nodes, and calls a dashboard 仪表盘 everywhere - -Clause-②: no -Metadata-protocol: none - -The view, page and report property panels render the spec's authoring forms from the console's own copy of the spec, and only the dashboard form had a zh-CN overlay, so section headings such as Basics, Columns & filters and Navigation & sharing, their descriptions and schema-titled rows such as Searchable Fields read in English. The overlay now carries every `METADATA_FORM_REGISTRY` type, worded as the platform's own zh-CN metadata-forms bundle words them: view, page and report in full, the other types by field label, which is what the Pending changes sheet names a changed property by (a flow's `~ Status` now reads `~ 状态`). The sheet's summary line counts datasets as 数据集 instead of the raw type name. - -A flow whose trigger is not armed on this deployment showed the engine's developer sentence under 未在此运行, above 没有问题 —— 该流程结构有效. The note now says, from the engine's `unboundCause` code, what this deployment does not do and who can change it, keeps the engine's sentence as the hover detail, and the line below it says the structure checks found nothing to fix instead of "no problems". A cause the console has no row for still shows the engine's sentence. - -The palette names the save-notice node 保存提示, and the notify node's Template and Template data fields and the save-notice Message field read in Chinese. English consoles read the same English as before. zh-CN Console and Studio text uses 仪表盘 for dashboard throughout. diff --git a/.changeset/30039-view-panel-zh-filter-prompt.md b/.changeset/30039-view-panel-zh-filter-prompt.md deleted file mode 100644 index 72f451078a3..00000000000 --- a/.changeset/30039-view-panel-zh-filter-prompt.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): on a zh-CN console the view properties panel's empty filter button reads 「+ 添加筛选…」 and the description row's hint reads in Chinese - -Clause-②: no -Metadata-protocol: none - -- The `filter-builder` widget's empty trigger was an English literal (`+ Add filter…`); it now reads the Studio string the dataset scope filter already uses, so it follows the designer locale. -- The zh-CN view form overlay gains the `description` row's help text, which showed the schema's English description. diff --git a/.changeset/30042-boot-picks-the-key-file-that-opens-its-secrets.md b/.changeset/30042-boot-picks-the-key-file-that-opens-its-secrets.md deleted file mode 100644 index 53a348939ad..00000000000 --- a/.changeset/30042-boot-picks-the-key-file-that-opens-its-secrets.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/cli': patch -'@objectstack/service-settings': patch ---- - -fix(cli): when two data-key files hold different keys, `os start` and `os dev` read with the one that opens the project's stored secrets, or a production boot refuses - -Clause-②: yes (narrowing) — a production `os start` / `os serve` boot whose two candidate key files (the shared `$HOME/.objectstack/dev-crypto-key` or `$OS_HOME`'s, and the project's `.objectstack/dev-crypto-key`) both fail to open the database's newest stored secret now refuses to start, naming both files; it used to start and serve every stored secret as unset. The widening half: `@objectstack/service-settings` gains new exports (`chooseKeyFileByTrialOpen`, `settleKeyFileChannel`, `storedSecretReaderOverEngine`, `deferredLocalCryptoProvider`, `DataKeyOpensNothingError`, `KEY_FILE_CANDIDATE_CHANNEL_ENV` and their types). -Metadata-protocol: none - - - -**BREAKING** boot narrowing (`@objectstack/cli`, `@objectstack/service-settings`): a production boot whose candidate data keys open none of the stored secrets refuses to start. - -- **`@objectstack/cli`.** When the shared key file and the project's own key file (the one an earlier `os start` minted) hold different keys, the CLI no longer picks one. It hands the `serve` child both, and the child decides once the database is open: it opens the newest stored `sys_secret` value under each key and reads with the key that opens it, and the boot says which file it took. A project that only ever ran `os start`, on a machine whose home holds a key from `os dev` in another project, reads its secrets back again. With no stored secret yet, the shared file is used, as before. When neither key opens the stored secrets, a production boot (`os start`, `os serve`) refuses to start and names both files; a development boot (`os dev`) starts and prints a boot error naming both files. The one-shot commands (`os migrate`, `os secret rewrap`, `os secret orphans`, `os storage orphans`) now read the same file by the same rules: the project's own file when it alone holds a key, and the key that opens a stored secret when both do. -- **`@objectstack/service-settings`.** The settings service makes that choice in its `start()`, before any reader decrypts, when the CLI names a second candidate on the internal `OS_INTERNAL_CRYPTO_KEY_FILE_CANDIDATE` channel. A leading `~` in `OS_HOME` (or `OBJECTSTACK_HOME`) is now the home directory when the provider finds its key file, as `os start` already read it; it used to name a directory called `~` under the working directory. - -**The remedy.** If a production boot refuses with "neither data key opens this database's stored secrets", find the key those secrets were sealed under (a backup of either file, or the `OS_SECRET_KEY` the deployment once ran with), keep a copy of both files, and put that key at the shared file the message names, or set `OS_SECRET_KEY` to it. - -Supersedes, in the same release: the note for #29934 says that when the shared file and the project's file hold different keys, the shared one is used and a warning names both paths. After this change the boot uses whichever of the two opens the stored secrets, and refuses in production when neither does. diff --git a/.changeset/30043-dock-sends-selected-app-and-page.md b/.changeset/30043-dock-sends-selected-app-and-page.md deleted file mode 100644 index 7beb4969257..00000000000 --- a/.changeset/30043-dock-sends-selected-app-and-page.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): the console AI dock tells the agent which app the user has selected and which page or record is in view, instead of the constant `activeApp: 'AI'` and no page at all (#30043) - -Clause-②: no -Metadata-protocol: none - -Every chat request the console sends to `POST /api/v1/ai/agents/:agent/chat` used to carry `context.activeApp: 'AI'`, whatever app the user was in, and the floating dock sent no `context.surface`. So the assistant asked "which app?" and "which record?" while the screen showed both. - -Now: - -- `context.activeApp` is the package of the app the user has selected: the console app the dock sits in (whether the dock runs `ask` or `build`), the app picked for editing on the full-page `/ai/build?package=…`, the package the Studio copilot designs. With no app selected the key is left out; nothing sends a placeholder. -- The dock, on the desktop rail and the phone sheet alike, sends `context.surface` in cloud#1610's existing shape for the page in view: an object list (`artifact: { type: 'object', name }`, with `selection: { kind: 'view', id }` on a saved list view), a record page (the object plus `selection: { kind: 'record', id }`), a dashboard, report or page (`artifact: { type, name }`, `mode: 'design'` in the dashboard designer). A page that shows no one artifact (the app landing, search, a component page) sends no `surface`. -- Both are read on every send, so navigating with the dock open moves the next turn's context. - -The dock's "discussing" chip now names the object by its label on object pages, as the Studio copilot's chip already did for its artifacts. diff --git a/.changeset/30046-studio-off-nav-list-views.md b/.changeset/30046-studio-off-nav-list-views.md deleted file mode 100644 index 94515f23fb4..00000000000 --- a/.changeset/30046-studio-off-nav-list-views.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -feat(app-shell): Studio Interfaces opens a list view from 不在导航中 on its object's page; the 视图 group lists only what no header dropdown reaches, and names a form view and its own type - -Clause-②: yes (widening) -Metadata-protocol: none - -- **A list view opens on its object's page from every door.** A list view picked under 不在导航中 → 视图, and a link to the view itself (`?surface=view:OBJECT.KEY`, the All metadata door), now land on the object's page with its header dropdown (object / view ▾), on that view: the object's nav entry when it has one, else the object itself, with no entry (`?surface=object:OBJECT`, which a reload or a shared link opens again, `?view=` included). The panel there now also carries what only the view's own page offered: its conditional formatting (条件格式化), whose CEL verdicts hold the view's autosave as they held the view page's Save, and the sections of the spec's authoring form for a view, without the keys the panel already edits. A form view, and a view a read-only base's pair adds, still open as themselves. -- **The 视图 group lists what the dropdowns do not reach.** A list view whose object has a nav entry is no longer listed there, a view only drafted in the package included; the list views of an object with no entry stay listed. -- **Form views read as forms.** A form view is listed by its label, else as the object's name and the word for a form ("Lead form", 「线索表单」), never its item name (`crm_lead.form`); the group's heading says what it holds (表单视图, 视图, or 表单视图与列表视图). In the view inspector, a form view's 视图类型 offers the form layouts of the spec's `FormViewSchema.type` (simple, tabbed, wizard) and reads its own, where it read 「simple(未找到)」; a list view's picker is unchanged. -- **`@object-ui/i18n`: three Studio keys,** `engine.studio.if.offNav.viewForms`, `engine.studio.if.offNav.viewMixed` and `engine.studio.if.offNav.formName`, in en and zh. diff --git a/.changeset/30046-studio-view-dropdown.md b/.changeset/30046-studio-view-dropdown.md deleted file mode 100644 index 27b2ce4db28..00000000000 --- a/.changeset/30046-studio-view-dropdown.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -feat(app-shell): Studio Interfaces picks an object's list view from the header title, object / view ▾, the way the running list does, and the open view stays in the address - -Clause-②: no -Metadata-protocol: none - -- **The view switcher is in the header's title.** An `object` entry's header reads object / view ▾. The switcher is the running list's own dropdown (`ObjectViewTitleSwitcher`): the object's list views by label, a view only drafted in this package included, a search box, and New view, which opens the Create View dialog. Studio does not offer the reader's own features there: no ☆, no 📌, no recent views, no Save as my view. It shows when the object has one list view too, so New view is always one click away. A read-only package switches views and offers no New view. -- **The Properties panel no longer holds a view picker, Add view or "Back to …".** It keeps the open view's settings, and its title is the view's name as the switcher gives it (the label the object's served `listViews` carry). A view with no label is named "Default view" or by its key; the header names the object before it. -- **The open view is in the address.** Switching writes `?view=.` beside `?surface=` and `?nav=`; the entry's own view writes none. A reload or a shared link opens that view again, a view only drafted in this package included. A link naming a view of another object is not taken. A pending edit is still sent before a switch, and an edit that cannot be sent still asks first. -- **`ObjectViewTitleSwitcher`: the pin is optional.** `onPinnedViewChange`, `pinnedViewId` and `recentViewIds` may be left out; without `onPinnedViewChange` no row draws 📌 and no Pinned section is listed. The running list passes all three, so it is unchanged. -- **`@object-ui/i18n` (studio namespace):** adds `engine.studio.inspector.listView.defaultView`; removes `defaultTitle`, `namedTitle`, `addView`, `backToEntry`, `switchView` and `switchViewDraft`, which only the removed panel controls read. diff --git a/.changeset/30050-console-locale-match.md b/.changeset/30050-console-locale-match.md deleted file mode 100644 index 2da0a51dc21..00000000000 --- a/.changeset/30050-console-locale-match.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): the console picks a signed-in user's UI language in the server's `resolveLocale` order - -Clause-②: no -Metadata-protocol: none - -- **Before.** `pickRenderableLocale` matched `sys_user.locale` against the offered locales exactly, then by base language only. A row stored mis-cased (`zh-cn`, written before the server door folded case and served as stored), or a tag the deployment offers only as a variant (`zh` or `zh-Hans-CN` against `zh-CN`), matched nothing, so the console rendered English. -- **Now.** It uses the order the server's i18n routes resolve with: exact, then case-insensitive, then base language, then the first offered variant of that base language. `zh-cn`, `zh` and `zh-Hans-CN` render `zh-CN`; `en-GB` renders `en`. A tag no offered locale shares a language with still matches nothing, and the device language keeps deciding, as before. -- No schema, key or server behaviour changes. diff --git a/.changeset/30052-facade-scoped-classes.md b/.changeset/30052-facade-scoped-classes.md deleted file mode 100644 index c1bdf362a0d..00000000000 --- a/.changeset/30052-facade-scoped-classes.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -refactor(objectql): `ObjectRepository` and `ScopedContext` in `engine.ts` are facades over their own facet modules, moved by the `engine.ts` codemod's second pass; no behaviour or published declaration changes - -Clause-②: no -Metadata-protocol: none - -- 20 of the 24 methods of the two classes keep their signatures and public docblocks on the class and forward, in one call, to an unedited copy of their body in `engine-object-repository.ts` or `engine-scoped-context.ts`. Each instance gets one host, built on first use (`engine-scoped-hosts.ts`), so a spy or a subclass override on an instance is still the method a body reaches. -- Four `ScopedContext` methods keep their bodies on the class, each named in the facade gate's registry with the card that will move it: `transaction` and the symbol-keyed `SEED_USER_GRANTS` method hand the instance itself on; `derive` and the symbol-keyed `APP_AUTHORED_HOOK_API` method set a private flag on another instance. -- Nothing to do on upgrade: the exported names, the published `.d.ts` surface and what every method does are the same. diff --git a/.changeset/30053-hand-series-private-docblocks-slice-2.md b/.changeset/30053-hand-series-private-docblocks-slice-2.md deleted file mode 100644 index e2dc7fed4c4..00000000000 --- a/.changeset/30053-hand-series-private-docblocks-slice-2.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/objectql': patch -'@objectstack/spec': patch ---- - -refactor(objectql): the private docblocks of the last 28 hand-series stubs in `engine.ts` move into their hand modules, onto the functions that hold their bodies; no behaviour or public declaration changes - -Clause-②: no -Metadata-protocol: none - -- The second and last slice of the `engine.ts` codemod's `--hand-docs` pass: 10 hand modules, 858 docblock lines. With the first slice, every private hand-series stub that had a docblock now keeps it beside its body; public and protected docblocks stay on the class. `engine.ts` goes from 12,618 to 11,752 lines. -- The liveness ledger's `query.groupBy` row now cites `engine-delete-and-aggregate.ts#aggregate`, where the body lives, because `engine.ts` no longer names the key. -- Nothing to do on upgrade: the exported names, the public `.d.ts` surface and what every method does are the same. diff --git a/.changeset/30053-hand-series-private-docblocks.md b/.changeset/30053-hand-series-private-docblocks.md deleted file mode 100644 index d0f7e39b43f..00000000000 --- a/.changeset/30053-hand-series-private-docblocks.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -refactor(objectql): the private docblocks of 36 hand-series stubs in `engine.ts` move into their hand modules, onto the functions that hold their bodies; no behaviour or public declaration changes - -Clause-②: no -Metadata-protocol: none - -- A new third pass of the `engine.ts` codemod, `--hand-docs`, moves a private hand-series stub's docblock onto the function its stub calls, as the first two passes already do for the bodies they move. A public or protected method keeps its docblock on the class. -- This is the first of two slices: 8 hand modules here, the other 9 next. `engine.ts` goes from 13,550 to 12,618 lines. -- Nothing to do on upgrade: the exported names, the public `.d.ts` surface and what every method does are the same. diff --git a/.changeset/30053-liveness-search-fields-repoint.md b/.changeset/30053-liveness-search-fields-repoint.md deleted file mode 100644 index 5b1898bf5b9..00000000000 --- a/.changeset/30053-liveness-search-fields-repoint.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): the liveness ledger's `query.searchFields` row cites `engine-find.ts#expandSearchOnAst`, where the hand-off lives, so `check:liveness` is green again - -Clause-②: no -Metadata-protocol: none - -- The row cited `engine.ts#expandSearchOnAst`. The body has lived in `engine-find.ts` since the `engine.ts` codemod split, and the last mention of `searchFields` in `engine.ts`, a private docblock, moved out with the first slice of the hand-series docblock move, so the check found no mention of the key in the cited file. -- Nothing to do on upgrade: only the shipped `liveness/query.json` evidence text changes. diff --git a/.changeset/30054-objectql-return-types.md b/.changeset/30054-objectql-return-types.md deleted file mode 100644 index ec3baff2024..00000000000 --- a/.changeset/30054-objectql-return-types.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -refactor(objectql): the eight `ObjectQL` methods that kept an inferred return type spell it, and five of them move into their facets - -Clause-②: no -Metadata-protocol: none - -- `registerApp`, `registerHook`, `registerDriver`, `init`, `triggerHooks`, `destroy`, `setDatasourceMapping` and `getStatus` now declare their return type, copied from the published declaration file it was inferred into. The bodies of `getStatus`, `triggerHooks`, `setDatasourceMapping`, `init` and `destroy` move out of `engine.ts` into their facet modules (the methods stay on the class as one-call stubs). The built declaration files are byte-equal before and after, and no behaviour changes; only the source maps move. Nothing to do on upgrade. diff --git a/.changeset/30054-protocol-door-return-types.md b/.changeset/30054-protocol-door-return-types.md deleted file mode 100644 index 2dc0b6298b2..00000000000 --- a/.changeset/30054-protocol-door-return-types.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -refactor(metadata-protocol): the eight `ObjectStackProtocolImplementation` doors that kept an inferred return type spell it on the signature - -Clause-②: no -Metadata-protocol: none - -- `getDiscovery`, `findData`, `getData`, `cloneData`, `saveMetaItem`, `publishMetaItem`, `rollbackMetaItem` and `deleteMetaItem` now declare their return type, copied from the published declaration file it was inferred into. The built `index.d.ts` and `index.d.cts` are byte-equal before and after, and no behaviour changes; only the source maps move with the new lines. Nothing to do on upgrade. diff --git a/.changeset/30055-erasure-pinned-bodies.md b/.changeset/30055-erasure-pinned-bodies.md deleted file mode 100644 index fa0801d8fc7..00000000000 --- a/.changeset/30055-erasure-pinned-bodies.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -refactor(objectql): the six `ObjectQL` methods the #4918 query-options erasure pin kept in `engine.ts` are typed and move into their facet modules; no behaviour or public declaration changes - -Clause-②: no -Metadata-protocol: none - -- The six internal reads in `seedAutonumber`, `referenceExists`, `resolveMasterDetailParent`, `resolveMasterDetailParents`, `inspectDanglingReferences` and `probeReferencingRows` no longer erase their query options to `any`. Four are plain literals `tsc` now checks against `EngineQueryOptions`; the keyset walk's `where` and the dangling-reference audit's port options are named as `EngineQueryOptions` slots instead. `engine.ts` leaves the erasure baseline with zero sites. -- A new fourth pass of the `engine.ts` codemod, `--members`, moves the six bodies the way the first pass moves a body, into the facet modules it already wrote, and proves the move by undoing it. `engine.ts` goes from 11,771 to 11,316 lines. -- Nothing to do on upgrade: the exported names, the public `.d.ts` declarations and what every method does are the same. diff --git a/.changeset/30057-studio-form-designer-form-view.md b/.changeset/30057-studio-form-designer-form-view.md deleted file mode 100644 index c88b70fb742..00000000000 --- a/.changeset/30057-studio-form-designer-form-view.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(app-shell): the Studio form designer lays out the form the app opens, so an object with a default form view no longer edits a layout users never see - -Clause-②: no -Metadata-protocol: none - -- **Which layout.** Studio → Data → an object → Form asks the app's own resolver which form view New and Edit open (`selectRecordFormView`, over the shell's merged objects). With none, the designer edits the object's `fieldGroups`, as before. With one (an `isDefault` form view, or an `OBJECT.create` view for New), the designer shows that view's sections and every layout edit (moving fields, adding, renaming, moving and deleting sections) is saved to the view's draft, which the package publish publishes. A chip in the toolbar names the layout being edited ("Form view: OBJECT.form" or "Field groups"). -- **New and Edit.** Where New and Edit open different form views, a New / Edit switch in the toolbar picks which one the designer and the Preview show. A pending edit of the shown view is sent before the other one loads. -- **Fields not on the form.** On a form view, a field the view lists in no section is not on the form; the designer keeps such fields in a "Not on this form" bucket, and dragging one into a section adds it. A field entry's own keys (`required`, `readonly`, …), a section's other keys, and entries the canvas does not show are written back as they were. A section that references a field group (`group`) keeps the reference until its members are changed. -- **Read-only base.** On a read-only base the view is customized, not edited: the edit is saved into the base's paired customization package as a declared override of the base's view (ADR-0146 D2), the same door list views use. -- **Preview.** The Form tab's Preview now lays the form out by the same form view (its sections, and the tabbed layout), as the dialog it previews does; before, it drew the object's field groups whatever form view the app opened. -- **`@object-ui/i18n`.** English and Chinese strings for the chip, the New / Edit switch and the "Not on this form" bucket. diff --git a/.changeset/30059-studio-customize-app-navigation.md b/.changeset/30059-studio-customize-app-navigation.md deleted file mode 100644 index 5ba52a7b157..00000000000 --- a/.changeset/30059-studio-customize-app-navigation.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -feat(app-shell): Studio customizes an installed app's navigation into the base's customization package (#30059) - -Clause-②: no -Metadata-protocol: none - -On a read-only (installed or code-shipped) package that has a paired customization package (`.custom`), the Interfaces rail now edits the app's navigation. Before, the rail's Edit button showed only on a writable package and its "+" offered only new pages, dashboards and reports, so an object made in the pair could be reached only by a typed URL. - -- Edit and the "+" open the same navigation editor a writable package uses (add, rename, reorder, group and remove entries, including the ones the base shipped). Nothing is written until an edit is saved. The first saved edit copies the whole app into the pair, declaring `overrides: { package: , basedOn: }`, the same route a view's Customize takes. Later edits update that copy. The base package is never written. -- The entry pickers list the pair's objects, pages, dashboards, reports and views after the base's, so a new object in the pair can be put on the menu. An entry bound to an object only the pair holds carries `requiresObject: `, so the runtime hides it if the object is later removed instead of linking to a missing object. -- The navigation change is a draft in the pair's pending changes and publishes in the same batch (one commit) as the object it opens. -- The rail marks a customized navigation with the same compact "customized" mark its rows carry, "only customizations" counts it and keeps the navigation listed, and the mark's popover offers "Restore package original", which deletes the pair's draft and copy of the app after a confirmation. - -A customized app is a whole copy: a navigation entry the vendor adds in a later version does not appear until the customization is restored and redone. A writable package's navigation editing is unchanged. No spec, server or metadata-type change. diff --git a/.changeset/30062-declared-override-label-order.md b/.changeset/30062-declared-override-label-order.md deleted file mode 100644 index f76e9f46e08..00000000000 --- a/.changeset/30062-declared-override-label-order.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor -'@objectstack/rest': minor ---- - -fix(metadata-protocol,rest): a `.custom` pair's declared override of a page, dataset, app, book, dashboard, view or report is labelled in the ruled order on every `/meta` read — the pair's own translation, then the override's own label, then the base's translation (#30062) - -Clause-②: yes (widening) — `ObjectStackProtocolImplementation` gains six public methods: `getPackagedPageBase`, `getPackagedDatasetBase`, `getPackagedAppBase`, `getPackagedBookBase`, `servedOverrideBaseOf` and `getPairTranslationBundle` (measured on `packages/metadata-protocol/dist/index.d.ts`); `@objectstack/rest`'s exported `MetaBookTreeSources` accepts one more optional key, `resolveProtocol` (measured on `packages/rest/dist/index.d.ts`). No accepted key, value or answer is taken away. -Metadata-protocol: semantic - -- **Now.** A changed label on a declared override reads the pair's own translation of that key when the pair ships one, else the override's own text, in every locale, on `GET /meta/:type/:name`, `GET /meta/:type` and `GET /meta/book/:name/tree`. An unchanged label still reads the base's translation. Before, a page, dataset, app or book override read the base's translation over its own changed label, and no type read the pair's translation first. -- **The pair's translations.** They are the pair's own active `translation` rows (`package_id` = the pair). A `.custom` package has no code bundle, so it has no code translation source. A package-less authored row is in the deployment catalog but is not the pair's, so it does not rank first. -- **Object.** An object is extended, never replaced (ADR-0146 D10), so it has no declared override and no pair bundle: seven types take one. -- **The book tree.** `GET /meta/book/:name/tree` now judges a book against its packaged base as the list does, so the docs sidebar and the book list serve the same label (#26690). diff --git a/.changeset/30062-declared-override-mapping-removal-remedies.md b/.changeset/30062-declared-override-mapping-removal-remedies.md deleted file mode 100644 index 0c13d5c7c9f..00000000000 --- a/.changeset/30062-declared-override-mapping-removal-remedies.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor -'@object-ui/app-shell': minor ---- - -fix(metadata-protocol): a mapping override is served, a pair's removal of its own override of any overridable type brings the base's item back, and an excluded type's `overrides` refusal names a remedy that works for it (#30062) - -Clause-②: no — ADR-0146 D2 says an item in the customization package replaces the base's item and removing it brings the base's back, and ADR-0146's revert ("Restore package original") is deleting the overriding item from `.custom` so the base's document is served again. The 403 on removing a pair's own doc or mapping override, and the unserved mapping override, refused what that text already admits. A removal that names the base still keeps today's answer. -Metadata-protocol: semantic - -- **A mapping override in `.custom` is served.** A mapping item that declares `overrides: { package: '', … }` in the base's customization package now replaces the base's mapping for the by-name read (no `?package`, `?package=`), both lists and the layered read, as page, app, dataset, doc and book already did. The import door (`@objectstack/rest`'s `import-mapping.ts`) resolves a named mapping through that package-less by-name read, so it now gets the pair's mapping. -- **Removing a pair's own override works for every overridable type.** `DELETE` of a declared override the customization package holds (named with `?package=.custom`) was admitted only for a flow; a doc or mapping override answered `403 NOT_OVERRIDABLE` on the environment kernel. The removal now admits the pair's row for every type `isOverridableMetadataType` names. The row is deleted and the base's item is served again under every address. A removal that names the base (`?package=`) is unchanged: the pair's row stays and its override stays served. -- **An excluded type's refusal names a remedy that works for it.** A save of `overrides` on a type that takes no override now cites that type's own record (ADR-0146 D6 for the security catalog and code-only types, D1 for a datasource, D9 for a behavioural switch, D10 for a field, ADR-0126 §3 for one-shot types, D2 for the rest) and prescribes a door that exists for that type: a new item under a new name where the type may be created at runtime, the documented switch route for a sharing rule, Setup › Queues for a queue, Setup › Datasources for a datasource, an object extension for a field, and a new version of the base for a type defined only in package source. The error code and status are unchanged. -- **`@object-ui/app-shell`: the All metadata tile agrees with the Customize door.** On a read-only package, a type's tile reads 可定制 (Customizable) exactly where Studio offers a Customize door for it (flow and app included, whose overlay flag is off) and 只读 (Read-only) elsewhere (doc and mapping, which have no door yet). It no longer reads the overlay flag. - -The label precedence of a translated override (card item 3) is not in this change. diff --git a/.changeset/30063-studio-customize-page.md b/.changeset/30063-studio-customize-page.md deleted file mode 100644 index a924ca6e519..00000000000 --- a/.changeset/30063-studio-customize-page.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -feat(app-shell): Studio customizes an installed package's page into the base's customization package (#30063) - -Clause-②: no -Metadata-protocol: none - -On a read-only (installed or code-shipped) package, a page opened in the Interfaces pillar now offers Customize, the same button a dashboard, report or view shows. Before, Studio showed the page as read-only with no way to change it, although the server already serves a page override from the paired customization package (`.custom`). - -- Customize unlocks the page designer. Nothing is written until an edit is saved. The first saved edit copies the whole page into the pair, declaring `overrides: { package: , basedOn: }`. Later edits update that copy. The base package is never written. -- The edit is a draft in the pair's pending changes; once published, the running app serves the customized page at its usual address. -- The page's header carries the override mark, whose popover offers "Restore package original": it deletes the pair's draft and copy of the page after a confirmation, and the shipped page reads back. -- The Interfaces rail marks a customized page, "only customizations" counts and lists it, and the All metadata directory's page tile counts it. - -A customized page is a whole copy: a change the vendor ships to that page in a later version does not appear until the customization is restored and redone. A writable package's page editing is unchanged. No spec, server or metadata-type change. diff --git a/.changeset/30064-studio-dataset-customize.md b/.changeset/30064-studio-dataset-customize.md deleted file mode 100644 index 0d1bc3b69bc..00000000000 --- a/.changeset/30064-studio-dataset-customize.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/data-objectstack': patch -'@object-ui/i18n': patch ---- - -feat(studio): customize a read-only package's dataset in the All metadata area, see what uses it before publishing, and read a refused publish at the dataset - -Clause-②: yes (widening) -Metadata-protocol: none - -- **Customize a dataset.** On a read-only base package, Studio's All metadata area opens a dataset in its form editor with the dataset designer and live preview, and offers Customize. The first saved edit copies the whole dataset into the base's paired `.custom` package, declaring `overrides: { package, basedOn }`; the header's Publish applies it, and "Restore package original" on the override mark deletes the copy. On a writable package a dataset still opens the generic item page. -- **What uses it.** While a dataset is customized, the editor lists the dashboards, reports and other items that name it, read from the References read, so the person sees what a published change reaches. A failed read says it failed and offers to read again; it never reads as "nothing uses it". -- **A refused publish at the item.** A publish the server refuses for a dataset is placed on its measure or dimension and the designer's input (`Not published. Check Aggregate of the measure "Total Amount"`), in the Console's language, and shown at the dataset's editor as well as on its Pending changes entry. The server's own text stays under Details. -- **References read fixed.** `MetadataClient.references()` now reads the route's declared answer (`{ references: [{ type, name, label, path, kind }] }`) and maps each element to `MetadataReference` (`fromType`, `fromName`, new optional `fromLabel` and `kind`). It used to read a key the route never serves, so Setup's References tab and Studio's item tools showed "Nothing in the metadata graph points at this item. Safe to delete." for every item. An answer the declaration refuses is now a failed read. -- **Published surface.** `MetadataReference`, exported from `@object-ui/data-objectstack`, gains two optional fields, `fromLabel` (the referring item's label, when the server serves one) and `kind` (the served reference kind); existing fields are unchanged. `MetadataClient.references()` now accepts less: a `200` answer that `FindReferencesToMetaResponseSchema` refuses throws instead of resolving `[]`, so a caller must handle the rejection as it already does for a non-2xx answer; in this repository every caller reads it through `useReferencesScan`, which catches it and shows the failed-check state. diff --git a/.changeset/30065-declared-action-override.md b/.changeset/30065-declared-action-override.md deleted file mode 100644 index 2552407b87a..00000000000 --- a/.changeset/30065-declared-action-override.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor -'@objectstack/objectql': minor -'@objectstack/cli': minor -'@objectstack/cloud-connection': minor ---- - -feat(metadata-protocol,objectql): a package's action can be customized by a declared override in its paired `.custom` package (ADR-0146 D9 `action`), and an override that widens who may run the action is refused - -Clause-②: yes (widening) -Metadata-protocol: added - -- **A declared action override is admitted.** An `action` saved, published or installed in a base's paired `.custom` package with `overrides: { package: '', basedOn: '' }` was refused `403 NOT_OVERRIDABLE` ("action customization is not yet available"). It is now admitted by the save, publish and install doors, `os validate` and `os build`, like a flow override. It replaces the base's action whole, and every read of the base's action serves it under the base's name: the by-name and execution reads, the layered read's `effective` (with `code` still the base's), both lists, the object read's `actions` (what the list toolbar, row menu and record header are drawn from) and the engine's schema, which the REST `/actions` door and the MCP `run_action` bridge read the action from. Deleting the override serves the base's action again everywhere. The action activation switch keyed by the action's name turns the override off while it is armed, and the switch stays where it was when the base is restored. Only the pair declares: a dependent's declaration and the pair's undeclared same-named save stay refused, and a dependent's row is never served. -- **A script action's body is rebound.** The override's `body` runs in place of the base's handler after one resync (`kernel:ready`, `metadata:reloaded`, or the publish itself). Removing the override binds the base's handler again on the next resync, without re-registering the base. `@objectstack/objectql` adds `ObjectQL#registerActionOverride(objectName, actionName, handler, packageName)`, which binds a handler over the one registered under the key and keeps it beneath. `removeActionsByPackage(packageName)` now removes the package's entries from each key's stack, so the handler beneath is bound again. `registerAction` of another package's handler under a key an override holds takes the place beneath the override instead of replacing it: a marketplace-installed base binds its handlers in the install plugin's `kernel:ready` rehydrate, after the boot's resync bound the override, and after a restart the override still runs (and a restore runs the base's body). -- **An override may add a gate and never remove one (ADR-0066 D4).** The write gate refuses, with the new rule `declared_action_widens_access` (`403 NOT_OVERRIDABLE`), an action override that drops a capability the base's `requiredPermissions` names, sets `ai.exposed: true` on an action the base keeps off the agent surface, or drops the base's `ai.requiresConfirmation: true`. The refusal names each widened gate. Adding a capability or turning a gate on is admitted. Everything else (`label`, `type`, `target`, `body`, `params`, `visible` and the rest) is replaced whole, so an override can still change what runs, including a body that runs system-elevated, but only for people who could already run the action. `@objectstack/metadata-protocol` exports `actionOverrideWidening(baseItem, overrideItem)`, which returns the widened gates as clauses, and `OverrideWriteClosure.baseItemOf`, through which a door hands the judge the base's own item. `@objectstack/cli` (`os validate`) and `@objectstack/cloud-connection` (the managed install) pass it. -- The action switch route in the packaged-base refusal sentences is now spelled `POST /api/v1/actions/_activation/:object_or_global/:action`, which keeps the declared-save sentence within the REST door's 500-character message bound for long action names. - -Skills-docs: follow-up #30505 diff --git a/.changeset/30069-related-export-permission.md b/.changeset/30069-related-export-permission.md deleted file mode 100644 index 6d3623de4ee..00000000000 --- a/.changeset/30069-related-export-permission.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/plugin-detail': patch -'@object-ui/plugin-list': patch -'@object-ui/plugin-grid': patch -'@object-ui/i18n': patch ---- - -fix(plugin-detail): a record page's related table offers Export only to a reader the list page offers it to, and an export refusal reads in the Console's language - -Clause-②: no -Metadata-protocol: none - -- **`@object-ui/plugin-detail`: the related table asks the list page's question.** The enlarged related list (the record page's active tab) drew Export whenever its data source could export. It now also reads the child object's effective API operation set (`/me/permissions` `apiOperations`) through `usePermissions()`, the source the list page's `exportPermitted` reads, and draws no Export when that set leaves `export` out. A missing set keeps the button, as on the list page. A reader who may export still exports this relation's rows alone. -- **Export refusals read in the Console's language.** When the export route still refuses (a 403, such as `EXPORT_NOT_PERMITTED`), the related table, the list page and the grid show the new `view.exportNotPermitted` sentence in the reader's language instead of the route's English text. Other export failures keep the server's sentence. -- **`@object-ui/i18n`: `view.exportNotPermitted`** in all ten locale packs. diff --git a/.changeset/30070-customization-mark-words.md b/.changeset/30070-customization-mark-words.md deleted file mode 100644 index 906234c5b17..00000000000 --- a/.changeset/30070-customization-mark-words.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(app-shell): Studio's rails mark a customized row with the word 定制 / Custom and an added row with 新增 / Added, no fill, instead of a filled square with a glyph that read as an add button (#30070) - -Clause-②: yes (widening) — new studio i18n key `engine.customizationMark.customizedShort` (en, zh) -Metadata-protocol: none - -The compact customization mark the pillar rails draw (and the Interfaces rail's navigation-override mark, which wraps it) now prints one short coloured word with no background. Its tooltip and accessible name are unchanged: the full text ("已定制(HotCRM)") followed by the sentence naming the customization package. It is not a control: a click on it is a click on its row. diff --git a/.changeset/30072-runtime-package-nav-contributions-refused.md b/.changeset/30072-runtime-package-nav-contributions-refused.md deleted file mode 100644 index a41aff822de..00000000000 --- a/.changeset/30072-runtime-package-nav-contributions-refused.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor -'@objectstack/runtime': minor ---- - -fix(metadata-protocol, runtime)!: a package created at runtime is refused when it carries `navigationContributions`, which its install never registered (#30072) - -Clause-②: yes (narrowing) — `POST /api/v1/packages` (both install limbs, its no-protocol fallback included) and the protocol install primitive `installPackage` (and so the package duplicate and every in-process caller) refuse a manifest whose `navigationContributions` is a non-empty list with `400 VALIDATION_ERROR`, a manifest they stored and answered `201` for before. Widened: `@objectstack/metadata-protocol` publishes one new export, `runtimeNavigationContributionsRefusal`, the predicate both doors ask. -Metadata-protocol: narrowed - - - -**BREAKING** accept-set narrowing at the runtime package install door (Studio, REST, MCP and AI authors creating a package at runtime). `os validate`, `os build` and code packages are unchanged. - -- **Before.** A package created through `POST /api/v1/packages` with `navigationContributions` targeting another app answered `201` and was stored, but no entry ever appeared in the target app, not after a restart either: only a code package's `registerApp` registers contributions, and neither the install nor the `sys_packages` replay at boot does. Nothing said why. -- **Now.** The same install is refused `400 VALIDATION_ERROR` before anything is stored, with a sentence that names the key and both routes that work. The refusal is asked ahead of the duplicate-id `409`, so the answer does not depend on whether the id is installed. A manifest with no `navigationContributions`, or an empty list, installs as before. -- **Not changed.** A code package's contributions (the Setup app's capability plugins, a multi-package artifact's modules, a marketplace package) are registered and served as before. A package already stored with the key keeps loading at boot, and its entries stay unregistered, as they always were. - -**What an author does.** To add an entry to an installed app's menu, save an override of that app in its customization package (the app copied with the entry added; Studio's navigation editor writes it). To contribute menu entries into another package's app, ship them in a code package's manifest, which registers them when it loads. Then install the runtime package without `navigationContributions`. diff --git a/.changeset/30073-personal-view-conditions.md b/.changeset/30073-personal-view-conditions.md deleted file mode 100644 index fb180bb163e..00000000000 --- a/.changeset/30073-personal-view-conditions.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/plugin-list': patch ---- - -feat(app-shell,plugin-list): a personal view's own conditions are Filter panel rows, and 保存 writes the panel back into that view - -Clause-②: no -Metadata-protocol: none - -- **Opening one of the reader's own views (My views) shows its conditions as ordinary rows in the Filter panel**, with the active count on the Filter control. They are the list's filter while they are there: not applied under the list a second time, and not a personalization. A stored filter the panel cannot hold (an any-of group beside other conditions), or one naming a field the object no longer declares or the reader cannot read, stays applied under the list, as for any other view. -- **Editing those rows marks the list 已个性化, and the marker offers 保存 to that view** (for a reader allowed to update their views). 保存 replaces the row's `config` through the data API with the body Save as my view builds, so the stored `ListView` shape is unchanged and no key is added; the reader's temporary list state for the view is then cleared and the list opens on the saved view. Edits not saved stay the reader's temporary list state and are never written to the view. A panel the fold cannot write losslessly keeps the refusal Save as my view gives, and nothing is written. -- **Save as my view now clears the view it came from**: that view's stored list state (sort, filters, column layout, density, hidden fields) and cached search term go, and the address being left loses its `uf_*` params, so the source no longer reads filtered or 已个性化 after the save. -- Both saves read the Filter panel as the list reports it, not the address's copy, so an emptied panel saves as no filter. -- `@object-ui/plugin-list`: `ListView`'s host prop `viewDefaults` accepts `filters`, the view's own conditions for a host that shows them as panel rows; the `filters` personalization part is then judged against them. diff --git a/.changeset/30074-crm-track-history.md b/.changeset/30074-crm-track-history.md deleted file mode 100644 index b9fd7e855b1..00000000000 --- a/.changeset/30074-crm-track-history.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -feat(record page): the timeline's Changes chip holds the audit's field changes only, and is offered only while the record shows its field history - -Clause-②: no -Metadata-protocol: none - -- **`@object-ui/plugin-detail`: what Changes holds.** The timeline stream's Changes chip, and the activity panel's `changes_only` filter, hold the field changes read from the audit log (each with its before and after values) and nothing else. The activity rows that share their look (created, updated, deleted, assigned, shared) are events, so they sit under Activity whether or not the record shows its field history; before, they sat under Changes and, on the panel, under `changes_only`. -- **`@object-ui/plugin-detail`: `RecordFieldHistoryContext`.** A host tells the record page's timeline stream whether it shows the record's field history; while it says `false`, the stream draws the All / Activity / Discussion chips and no Changes chip, and a stream left on Changes goes back to All. With no provider every chip draws, as before. `RecordActivityTimeline` takes the chips to offer as `streamFilters` (all four when absent). -- **The Console's record page** hands over its `enable.trackHistory` gate, so an object that does not opt in no longer offers a Changes chip on its timeline tab. -- **The CRM example** opens `enable.trackHistory` on `crm_lead`, `crm_account`, `crm_contact` and `crm_opportunity`, so their record pages show each field change with its before and after values. The flag stays opt-in for every other object. diff --git a/.changeset/30075-default-page-stage-advance.md b/.changeset/30075-default-page-stage-advance.md deleted file mode 100644 index 67c58daa312..00000000000 --- a/.changeset/30075-default-page-stage-advance.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -feat(plugin-detail): the default record page lets an editor move a record to another stage by pressing it on the stage path - -Clause-②: no -Metadata-protocol: none - -**Visible behaviour change on a released page.** The record page the console synthesizes for an object with a stage field (no authored page) used to draw its stage path as a read-only progress rail, so moving a deal to its next stage took 编辑 → change the stage → 更新. That page's `record:path` now carries `allowStageChange: true`, the existing opt-in (no new metadata key): - -- **Who can press.** Unchanged from the opt-in: a reader the record page lets edit this record, on a stage field that is not read-only or computed, whose field edit grant holds for the stored row. Everyone else keeps the read-only path, and so does an editor while the record's inline edit holds unsaved changes. -- **Every press asks for confirmation**, then saves the stage through the record's ordinary update with the record's version, so the object's validation rules and `state_machine` decide. A refused move is shown under the path, naming any other field the move needs (for example a field made required by the new stage); nothing is written. -- **Only reachable stages are offered.** On every page with the opt-in, a stage the stage field's blocking `state_machine` cannot reach from the stored value is drawn as a readout, not a button, the same moves the record form, the grid's inline editor and the board offer. A field with no such rule offers every stage, and the server still judges each move. -- **Authored pages keep their own setting.** A page, or an authored `highlights` slot, that declares its own `record:path` keeps whatever it declares: `true`, `false`, or the key left out (a read-only rail). - -To keep a default record page's path read-only, assign the object an authored page (or a `highlights` slot) whose `record:path` leaves `allowStageChange` out or sets it `false`. diff --git a/.changeset/30076-forward-date-range-presets.md b/.changeset/30076-forward-date-range-presets.md deleted file mode 100644 index d08bf000ead..00000000000 --- a/.changeset/30076-forward-date-range-presets.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/core': minor -'@objectstack/lint': patch -'@objectstack/verify': patch -'@object-ui/core': minor -'@object-ui/components': minor -'@object-ui/i18n': minor ---- - -feat(spec): three forward date-range presets: `next_month`, `next_quarter` and `next_30_days` - -Clause-②: yes (widening) -Metadata-protocol: added - -The relative dates a dashboard date filter and the filter builder offer looked only backward. A forecast asks forward: what closes next month, next quarter, in the next 30 days. Those questions had to use a custom range, whose fixed dates go stale. - -`DATE_RANGE_PRESETS` now also declares `next_month` (the whole of next month), `next_quarter` (the whole of next quarter) and `next_30_days` (today and the 30 days after it, the mirror of `last_30_days`). Every place that accepts a preset accepts them: `dashboard.dateRange.defaultRange`, a date global filter's `defaultValue`, the filter builder's `between` row and `AnalyticsQuery.timeDimensions[].dateRange`. They sit beside the family they extend in the filter bar: `next_month` after `last_month`, `next_quarter` after `last_quarter`, `next_30_days` after `last_90_days`. Their `DATE_RANGE_PRESET_MACRO_WINDOWS` rows follow the inclusive last-day convention: `['{next_month_start}', '{next_month_end}']`, `['{next_quarter_start}', '{next_quarter_end}']` and `['{today}', '{30_days_from_now}']`. A preset name written as a bare filter comparand is refused with that window as the prescription, as for the other presets. - -`@objectstack/core`'s `resolveAnalyticsDateRangePreset` resolves the three in its half-open form. `next_30_days` stops before `{31_days_from_now}`. `next_month` and `next_quarter` stop before the month and the quarter after next, which no macro token names: the resolver reads `next_month_start` (or `next_quarter_start`) again from the start of the day it named, so the end is still a day the one token resolver computed. No macro token is added. - -In the console, the three presets have labels in all ten locale packs, and the client date-macro resolver now answers `{next_month_end}` and `{next_quarter_end}`, two spec tokens it did not resolve before. `@objectstack/verify`'s dashboard-window proof also writes fixture rows on a forward preset's future edges, so it judges the three new windows; the days it writes for the other presets are unchanged. `@objectstack/lint` changes documentation only. diff --git a/.changeset/30077-list-add-columns.md b/.changeset/30077-list-add-columns.md deleted file mode 100644 index b454546debe..00000000000 --- a/.changeset/30077-list-add-columns.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/plugin-list': patch -'@object-ui/app-shell': patch ---- - -feat(plugin-list): a reader can add any field they may read to their own list from the Hide Fields popover (#30077) - -Clause-②: no -Metadata-protocol: none - -The Hide Fields popover on a Console object list (the toolbar's, and the Hide Fields section of the Display panel) listed only the columns the view already has, so a reader who wanted another field on their list had to ask an administrator to change the view for everyone. After the view's columns it now lists every other field of the object the reader may read, unticked: business fields first, then system fields such as Owner. A field the reader may not read is not offered, and neither is a field the object definition marks `hidden`. Ticking one adds it as a column of the reader's own list; unticking it takes it off again. - -The addition is the reader's personalization, stored beside their hidden fields in their `ui.list.` preference row (`perView..addedFields`), never written into the view: another user sees the view's columns, and "Reset to view" drops it with the rest of the reader's list. While it is stored the list's Personalized marker names "Column layout". An added column is fetched and drawn like a declared one, and an added lookup field is expanded in the list read as a declared lookup column is. - -A column the view declares with no label of its own (a bare `{ field }` or field name) is now listed in the Hide Fields popover (and in the Group and Row color pickers, which share its list) under the object definition's field label, the label the grid's header shows, instead of under its machine name (`expected_revenue` now reads Expected Revenue); a column's own label and a translation still come first. - -It is offered wherever a Hide Fields choice is stored: a view that declares `columns`, and a saved view. On a system view or fallback tab that declares no columns the popover lists the view's columns alone, as before. - -For a `ListView` host: two new host props, `addedFields` (the names among `schema.columns` the reader added) and `onAddedFieldsChange` (the door that stores them). Without `onAddedFieldsChange` the popover is exactly what it was. No metadata key and no spec change. diff --git a/.changeset/30080-dashboard-tile-drill.md b/.changeset/30080-dashboard-tile-drill.md deleted file mode 100644 index 69688d9046e..00000000000 --- a/.changeset/30080-dashboard-tile-drill.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/plugin-dashboard': patch ---- - -feat(plugin-dashboard): a dataset-bound KPI tile opens the records behind its number - -Clause-②: no -Metadata-protocol: none - -A dashboard metric tile bound to a dataset now opens the same drill drawer its charts and tables already open, by click or by Enter/Space. The list is the widget's filter (the dashboard's filters included) AND the answer's date window AND the measure's `drillScopes` entry, which carries the dataset's own filter and the measure's own filter, so the records listed are the records the number counts. A tile whose measure has no `drillScopes` entry (a derived measure, or a scope that names a relationship path) offers no click, as does a tile whose filter the list cannot express; its markup is unchanged. Nothing to author: the click follows from the dataset answer. diff --git a/.changeset/30081-seed-mode-default-create-only.md b/.changeset/30081-seed-mode-default-create-only.md deleted file mode 100644 index e496783079b..00000000000 --- a/.changeset/30081-seed-mode-default-create-only.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/runtime': minor -'@objectstack/metadata-protocol': minor -'@objectstack/cloud-connection': minor ---- - -feat(spec)!: a seed is create-only by default — an omitted `mode` is `ignore`, so a restart, an install or an upgrade never writes a seeded value back over a customer's edit (#30081) - -Clause-②: yes (narrowing) — the declared default of `Seed.mode` (and of `SeedLoaderConfig.defaultMode`, now the same field) moves from `upsert` to `ignore`. The accept set is unchanged and every authored `mode` parses exactly as before; what narrows is what a replay does for a dataset that declares no mode: it no longer updates an existing row. -Metadata-protocol: semantic - - - -**BREAKING** for a seed dataset that declares no `mode` and relies on a replay to write its values back onto existing rows. - -- **Before.** A seed with no `mode` replayed as `upsert`. A seed loads again on every single-tenant boot (production included, under `os start` and `os serve`), on every package install or upgrade and on every publish, so each replay wrote the shipped value back over a customer's edit to a seeded row, with no warning. Measured on a production boot before the change (card comment 6633189577): an API edit to a seeded row went back to the shipped value on the next restart, after a boot-artifact upgrade, and after an `os package install` upgrade. -- **Now.** `SeedSchema.mode` defaults to `ignore`: a replay inserts a record whose row is missing and leaves an existing row exactly as it is. A row nobody edited is not rewritten either, so its `updated_at` does not move, and a value changed in a later seed version reaches only a row the load creates. Maintainer ruling 6631016988 (option A). -- **One default, stated once.** `SeedLoaderConfig.defaultMode` is `SeedSchema`'s own `mode` field, default included. The six replay sites (`@objectstack/runtime`: the inline boot seed, the per-organization replayer, the dev hot reload and the published-seed apply; `@objectstack/metadata-protocol`: the publish batch; `@objectstack/cloud-connection`: install, reseed and the rehydrate healer) stop restating `defaultMode: 'upsert'`, and the save door reads the schema's default for a seed item that declares no mode. -- **Compiled artifacts.** `os build` stores the parsed seed, so a package built before this change carries a written `mode: "upsert"` on every dataset. The loader cannot tell that value from an authored one, and nothing converts it: such a package keeps today's replay until it is rebuilt. -- **Installed packages.** A package installed with `os package install` (`@objectstack/cloud-connection`'s install-local) keeps the body it was installed with in its install ledger, and the sample-data reseed (`POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data`) replays that body's datasets in their stored mode. So an entry installed from a pre-change artifact keeps writing the shipped values back over edited rows on every reseed until the **rebuilt artifact is re-installed**; rebuilding alone changes nothing for it. An entry installed from a rebuilt artifact reseeds create-only: it puts back the rows that are missing and leaves an edited row as it is. The ledger's other replays never overwrite: the boot rehydrate seeds nothing, the heal seeds only when every seeded object is empty, and the per-organization replay runs only for a new organization. -- **Stored `seed` items.** A `seed` metadata item stored with no `mode` is not rewritten either. Its next publish parses it to `ignore`, so that publish inserts only the rows it is missing where it used to write its values back over existing rows, with no signal. -- **Unchanged.** An explicit `mode: 'upsert'` still writes every declared column back on each replay, and `columns: { FIELD: { applyOn: 'create' } }` still exempts the columns a customer or a job owns. `insert`, `update` and `replace` behave as before. - -**The remedy.** Rebuild every package so its compiled artifact carries the mode its source declares, and re-install the rebuilt artifact wherever the package was installed with `os package install`, so its install ledger carries that mode too. To put an installed package's sample data back to the shipped values, purge it (`…/purge-sample-data`) and then reseed. For a dataset that is reference data the package must keep current, declare `mode: 'upsert'`, and mark the columns the customer or a job owns `applyOn: 'create'`. Leave `mode` unset for business and starter data the customer owns once it is loaded. diff --git a/.changeset/30086-customizations-disabled-package-fields.md b/.changeset/30086-customizations-disabled-package-fields.md deleted file mode 100644 index 6002ffe430b..00000000000 --- a/.changeset/30086-customizations-disabled-package-fields.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(spec): the `contributions` array of `GetPackageCustomizationsResponseSchema` says a disabled package's fields are not listed - -Clause-②: no -Metadata-protocol: none - -The `.describe()` of the `contributions` array that `GET /api/v1/packages/:id/customizations` answers already left out a disabled package's flows and navigation items, but its field clause still read as listing the fields of every object extension from any package but the base and its pair. The server has not listed a disabled package's extension fields since the served object's fold began withholding a disabled package's extension; the field clause now says so, in the same parenthetical form as the flow and navigation clauses. Text only: no key, shape, default or export changes, and the reference page `content/docs/references/api/package-lifecycle.mdx` is regenerated from it. diff --git a/.changeset/30088-studio-pair-object-actions.md b/.changeset/30088-studio-pair-object-actions.md deleted file mode 100644 index d0e5c2e31dc..00000000000 --- a/.changeset/30088-studio-pair-object-actions.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -feat(app-shell): Studio adds an action to an installed app's object, saved in the base's customization package (#30088) - -Clause-②: no -Metadata-protocol: none - -On a read-only (installed or code-shipped) package that has, or can get, its paired customization package (`.custom`), the Data pillar's Actions view of the base's object now offers New. Before, it listed the vendor's actions with no New, so an operations person could not add a button such as "Mark qualified" to an installed app's leads, although the platform accepts the result. - -- New offers the same presets and the same action editor as on a writable package. The action is saved as its own `action` item in the pair, bound to the object by `objectName`, as a draft (the first write asks the ensure door for the pair). The base's object is never written. -- The vendor's actions stay read-only in the list. The added action is listed after them with the "added" customization mark, its edits autosave into its draft in the pair, and Delete removes its draft and its published item from the pair. An added action keeps the name it was created with: a rename is held with a line saying how to save. -- The action publishes with the pair's other drafts from Pending changes, and the Actions list's "only customizations" counts it and narrows the list to the pair's actions. - -A writable package's action editing is unchanged: its actions are still written into the object draft. No spec, server or metadata-type change. diff --git a/.changeset/30089-studio-list-view-selection-actions.md b/.changeset/30089-studio-list-view-selection-actions.md deleted file mode 100644 index 08307548504..00000000000 --- a/.changeset/30089-studio-list-view-selection-actions.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -feat(app-shell): the Studio list-view panel configures the selection bar's actions (#30089) - -Clause-②: no -Metadata-protocol: none - -The list-view panel of an object's grid view gains a "Selection actions" section. It edits two keys the list view already declares, and no other: - -- `bulkActions`: pick the object's actions by label, plus the built-in Delete (`delete`). Each runs once per selected record, promoted with the action's own label and params. -- `bulkActionDefs`: add, edit or remove a field update (`operation: 'update'`): a button label, the fields the dialog asks for (one required param per field, carrying a select's options and a lookup's object) and an optional confirmation. A def is written only once it has a label and a field and `BulkActionDefSchema` accepts it. - -The section says what happens to the built-in Delete: shown while the view declares neither key, and kept once either is declared only when `bulkActions` names `delete`. A def of another kind (`delete`, an aggregate `custom` dispatch) is listed and can be removed; it is edited in metadata. An emptied key is removed, so a view that declares neither key saves as it was read. On an installed app's read-only view, the first edit goes through the same customization copy as every other setting of the panel. Strings are added in English and Chinese to the `studio` namespace of `@object-ui/i18n`. diff --git a/.changeset/30091-data-key-trial-page.md b/.changeset/30091-data-key-trial-page.md deleted file mode 100644 index 4db447a42f7..00000000000 --- a/.changeset/30091-data-key-trial-page.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/cli': patch -'@objectstack/service-settings': patch ---- - -fix(service-settings): when two data-key files hold different keys, the boot judges the newest stored secrets as a page, not only the newest one, and reports a database split between the two keys - -Clause-②: no — an internal boot heuristic and its notice text. The shapes it changes (`StoredSecretReader` now answers the page, `KeyFileChoice` gains the `mixed` reason and a `tally`, `DataKeyOpensNothingError` gains `judged`) were added by #30042, whose note is still pending, so they first ship together in this release; no released export changes. -Metadata-protocol: none - -- **`@objectstack/service-settings`.** When the shared key file and the project's own key file hold different keys, the boot now trial-opens each of the newest stored `sys_secret` rows (up to ten) under both keys and reads with the key that opens more of them, on a tie the one that opens the newest. A row that opens under neither key (a corrupt or foreign row) no longer decides the boot: if an older row opens under one key, that key is used, and only a page where no row opens under either key keeps the production refusal. When each key opens some of the rows, the database holds secrets under both keys: the boot uses the key that opens more of them and prints an error naming both files, how many rows each key opened, and the fix. The boot's notices now say what was measured ("opens the 3 newest stored secrets", "opens 2 of the 3 newest stored secrets") instead of claiming the chosen file opens all of the database's stored secrets. -- **`@objectstack/cli`.** The one-shot commands (`os migrate`, `os secret rewrap`, `os secret orphans`, `os storage orphans`) share the same trial, so they make the same choice. - -**The remedy for a split database.** Keep a copy of both files, then save again each secret that reads as unset: it is sealed under the key the boot uses. Once nothing reads as unset, put that key at the shared file (or set `OS_SECRET_KEY` to it). - -Supersedes, in the same release: the note for #30042 says the boot opens the newest stored secret under each key and quotes the refusal as "neither data key opens this database's stored secrets". After this change the boot judges the newest rows as a page, and the refusal reads "neither data key opens any of the N newest stored secrets". diff --git a/.changeset/30093-page-sort-unknown-field.md b/.changeset/30093-page-sort-unknown-field.md deleted file mode 100644 index 56ebf8637d1..00000000000 --- a/.changeset/30093-page-sort-unknown-field.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/lint': minor ---- - -fix(lint)!: a stored page sort naming a field its object lacks is refused by `os validate` and at the page publish door (`page-field-unknown`, error), on every page sort slot (#30093) - -Clause-②: no (narrowing) — an unknown sort field on a page slot moves from silent or warning to error: `os validate` / `os build` / `os lint` and the `page` publish door refuse a page sort they accepted before. -Metadata-protocol: narrowed -Skills-docs: updated here — content/docs/deployment/validating-metadata.mdx - - - -**BREAKING** accept-set narrowing at the page publish door (`PUT /api/v1/meta/page/NAME`; Studio, REST and MCP authors) and in `os validate` / `os build` / `os lint`. - -- **Before.** A page sort naming a field the object does not have saved green, and its first read, and every read after it, answered `400 INVALID_SORT` at the list door. A list page's `interfaceConfig.sort` and a `record:related_list` block's `sort` drew a `page-field-unknown` warning on the CLI alone; an element `dataSource.sort`, a related list's `parentDataSource.sort`, an app page's `record.sort` and the own `sort` of a `record:line_items`, `list-view` or `object-*` block were read by nothing. -- **Now.** Every one of those slots is judged by `validateSortableFields`, the reference-integrity member the page publish door already runs, and an unknown name is `page-field-unknown` at `error`, at the slot's path, bound to the object the slot's renderer reads (`dataSource.object`, else the block's object prop; `interfaceConfig.source`, else the page's `object`; `parentDataSource.object`; `record.object`). The page door refuses it `422 INVALID_METADATA`. A registry-injected column on an `external` object is `page-field-unprovisioned` at `warning`, as before. `validatePageFieldBindings` no longer reads a page sort, so each slot has one reader. -- **Not judged.** A dotted relationship path (the list door refuses it as its own verdict), a system column such as `created_at`, an object the stack (or the publish snapshot) does not carry, and an object with no field map. -- **Not changed.** A stored page already carrying such a sort keeps loading; only its next save is judged, and its read answers 400 as it did before. - -**What an author does.** Name a field the object declares, or add the field to the object. A refused name never sorted the page, so there is nothing to keep. diff --git a/.changeset/30094-page-record-liveness-regrade.md b/.changeset/30094-page-record-liveness-regrade.md deleted file mode 100644 index 3c3eff97498..00000000000 --- a/.changeset/30094-page-record-liveness-regrade.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): the liveness ledger grades an app page's `record` binding `live`, citing the console reader for each of its keys - -Clause-②: no -Metadata-protocol: none - -- `liveness/page.json` graded `record` and every key under it `planned`, with the note that nothing read a page-level `record` yet. The console now reads it: an app page's binding is read once per page open as the first row under `sort`, and every component's `visibleWhen` sees that row as `record`. `record` and every key under it (`object`, `view`, `filter` with `field` / `operator` / `value`, `sort` with `field` / `order`) are now `live`, each citing the code that reads it. -- Nothing to do on upgrade: only the shipped `liveness/page.json` grades and evidence text change. No key, schema shape or export changes. diff --git a/.changeset/30095-object-list-view-sort-door.md b/.changeset/30095-object-list-view-sort-door.md deleted file mode 100644 index 59fab61a915..00000000000 --- a/.changeset/30095-object-list-view-sort-door.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/lint': patch ---- - -fix(lint)!: the object publish door refuses an object whose own list view sorts by a field the list door refuses (`sortable: false`, a `formula`, or a name the object lacks) (#30095) - -Clause-②: no (narrowing) — an object write the door accepted before is refused: an object whose built-in `listViews.*.sort` names a field the list door answers `400 INVALID_SORT` now saves `422 INVALID_METADATA`. -Metadata-protocol: none -Skills-docs: none — `skills/**` names no `sortable` field key, and `content/docs/deployment/validating-metadata.mdx` lists the doors by rule family, not by reference-integrity member, so it names no object crossing to correct; the prescription ships in the refusal text. - - - -**BREAKING** accept-set narrowing at the object publish door (`PUT /api/v1/meta/object/NAME`; Studio, REST and MCP authors). - -- **Before.** `validateSortableFields` judged a list view's `sort` on the CLI, at the `view` door and at the `page` door, but not on an `object` write. An object whose built-in list view sorted by a field declaring `sortable: false`, by a virtual `formula`, or by a name the object does not have saved green, and that list view answered `400 INVALID_SORT` on its first load and every load after it. -- **Now.** The member declares `object`, so the object door refuses that save `422 INVALID_METADATA` with `sort-field-unsortable` or `sort-field-unknown` at `error`, at the sort key's path (`objects.NAME.listViews.KEY.sort[i]`). Nothing is stored. -- **Not changed.** A stored object already carrying such a sort keeps loading; only its next save is judged, and a stored sibling in violation is never charged to another object's write. A `?mode=draft` save is not judged, as before. - -**What an author does.** Sort the list view by a sortable field, or remove `sortable: false` from the field it names. A refused sort never ordered the list, so there is nothing to keep. diff --git a/.changeset/30103-home-amount-rate-field-scale-floor.md b/.changeset/30103-home-amount-rate-field-scale-floor.md deleted file mode 100644 index 772b5b74f3d..00000000000 --- a/.changeset/30103-home-amount-rate-field-scale-floor.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: the object parse refuses a `homeAmount.rateField` whose number field declares a `scale` below 12, the decimals an exchange rate carries; `CURRENCY_FRACTION_DIGITS` is documented as CLDR fraction digits, not ISO 4217 minor units - -Clause-②: no (narrowing) — the object parse (`ObjectSchema`, so `os validate`, `os compile` and the metadata doors that parse an object) refuses an object whose `homeAmount.rateField` names a `number` field declaring `scale` 0 to 11, a declaration it accepted before. Nothing is widened. -Metadata-protocol: narrowed -Skills-docs: none — `skills/objectstack-data/rules/field-types.md` teaches `rateField` as a readonly `number` with no `scale`, which this change still accepts; the floor ships in the refusal text, the FROM → TO below and the upgrade-guide entry. - - - -**BREAKING** accept-set narrowing in the object parse: `fields.AMOUNT.currencyConfig.homeAmount.rateField`. - -- **Before.** A rate field declared `Field.number({ readonly: true, scale: 4 })` parsed. The engine stored the rate it used with all twelve decimals anyway (the rate row's own value, never rounded), so the declared `scale` only rounded the rate a reader was shown: `0.1389` for a stored `0.138912345678`, a rate that no longer reproduced the stored home amount. -- **Now.** The parse refuses it at `fields.AMOUNT.currencyConfig.homeAmount.rateField`, naming the field, its `scale` and the floor 12, the rate's scale (ruling 6469382727: twelve digits after the decimal point, total precision 24). A rate field with no `scale` is accepted: on a `number` field an absent `scale` declares no fixed width, so the rate shows every digit it was stored with. `scale: 12` or more is accepted. -- **Unchanged.** What the engine stores and computes, and every stored data row (record values). -- **Metadata already on disk.** A built artifact whose object declares such a rate field is refused whole at load (its `ObjectStackDefinitionSchema` parse fails before anything registers): fix the source and rebuild. A stored `sys_metadata` object row is kept as authored and still loads and registers; boot logs `[metadata_spec_invalid]` at `fields.AMOUNT.currencyConfig.homeAmount.rateField`, and its next save is refused `422 INVALID_METADATA` until the rate field's `scale` is raised to 12 or more, or deleted. -- **Docs.** `CURRENCY_FRACTION_DIGITS` and the currency page now say the table is CLDR's fraction digits, and list the sixteen codes where they differ from ISO 4217's minor units: IQD (CLDR 0, ISO 3) and AFN, ALL, COP, HUF, IDR, IRR, KPW, LAK, LBP, MGA, MMK, PKR, SOS, SYP, YER (CLDR 0, ISO 2), measured against SIX's ISO 4217 List One of 2024-06-25. - -**FROM → TO.** `amount_rate: Field.number({ readonly: true, scale: 4 })` → `amount_rate: Field.number({ readonly: true, scale: 12 })`, or `Field.number({ readonly: true })` with no `scale`. diff --git a/.changeset/30106-install-custom-pair-into-writable-pair.md b/.changeset/30106-install-custom-pair-into-writable-pair.md deleted file mode 100644 index e31f367bc49..00000000000 --- a/.changeset/30106-install-custom-pair-into-writable-pair.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/cloud-connection': patch -'@objectstack/cli': patch ---- - -fix(cloud-connection,cli): an exported `.custom` installed on another environment lands in that environment's writable pair, and its overrides are served (ADR-0146 D2 / D3) - -Clause-②: no -Metadata-protocol: none - -- **Before.** `os package install /objectstack.json` of a base's paired customization package answered success and printed "Package installed into the running kernel", but the install registered the pair as code. None of its overrides was served, `GET /packages//customizations` listed none of them, and the pair turned read-only, so `ensure-customization` answered `writable: false` and every further save into it was refused `WRITABLE_PACKAGE_REQUIRED`. -- **Now.** `POST /api/v1/marketplace/install-local` installs a pair as what it is. The target's own pair is answered by its ensure door, which creates it when absent. Every item of the body is then written into it through the save door a Studio edit takes, with `mode: 'publish'` and the installer as actor, in load order. The overrides are served at once, the customization read lists them, and the pair stays editable. Nothing is registered as code or written to the cached-manifest directory. The answer carries `customizationPair` (`basePackageId`, `packageId`, `created`, `writable`), `writtenCount` and `written`, and the CLI prints them. -- **Refusals, nothing written.** A body key with no item form (code, a `translations` bundle, a key the install-key table does not classify), a nameless item, or two items claiming one name: `422`, every problem listed. A pair manifest the ensure door would not write: `400 PLUGIN_MANIFEST_INVALID`. A pair inside a multi-package artifact: `422`. A pair already read-only on the target: `409 RESOURCE_CONFLICT`, asked before the ensure door. If an earlier install registered it as code, the message names the uninstall and the restart that clear it. The ensure door's own refusal is relayed with its status and code. -- **A save refused part-way.** The answer is a failure with the save's code and status, naming the item and where it sits in the body. A pair this install created is deleted again. A pair that existed before is kept, and the answer lists the items written and the ones not written. -- **The CLI.** A runtime that predates this answers a pair as a managed install. The CLI now prints that as an error with the way back (uninstall, upgrade and restart, install again) and exits 1, instead of printing success. diff --git a/.changeset/30107-reset-without-email.md b/.changeset/30107-reset-without-email.md deleted file mode 100644 index 8f2350b90bc..00000000000 --- a/.changeset/30107-reset-without-email.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/plugin-auth': patch ---- - -`GET /api/v1/auth/config` now says whether email leaves the deployment, in a new optional `features.emailDelivery` flag. It is `true` only when the wired email service reports itself configured, and `false` with no email service or with the stock `log` transport, which writes mail to the server console instead of sending it. The forgot-password page can read it to stop saying a reset link was sent on a deployment that sends no mail. - -The flag describes the deployment, not an address: `POST /api/v1/auth/request-password-reset` still answers a known and an unknown address the same way, with or without mail. - -Like every public auth flag, it is classified in `PUBLIC_AUTH_FEATURES` (login surface, opt-in), so an action may now also name it: `requiresFeature: 'emailDelivery'` is a new accepted value, lowered to `features.emailDelivery == true`. - -Clause-②: yes (widening) -Metadata-protocol: added diff --git a/.changeset/30108-member-default-feed-grants.md b/.changeset/30108-member-default-feed-grants.md deleted file mode 100644 index 2a40eb8bd50..00000000000 --- a/.changeset/30108-member-default-feed-grants.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security): every member reads and posts comments, and reads the activity history, of the records they can read — the platform baseline `member_default` now grants `sys_comment` (read, create, edit) and `sys_activity` (read) - -Clause-②: no — runtime permission behaviour (human-floor security/permission class, ruled by the maintainer), not a published accept-set or public-surface widening -Metadata-protocol: none - -Comments (`enable.feeds`) and the activity timeline (`enable.activities`) are on by default on every object, but no shipped permission set named `sys_comment` or `sys_activity`. A member of an app whose permission sets name only the app's own objects — what `create-objectstack` generates — was refused both (`403` on `GET /api/v1/data/sys_comment` and `GET /api/v1/data/sys_activity`), on records they can read: the record page showed "no permission" for comments and activity, and offered no comment box. An app author no longer needs to grant the two objects by hand. - -- **Which rows is unchanged.** The grant opens the two objects; plugin-audit's parent-record gates still decide the rows, so a member reads and posts comments, and reads the history, of exactly the records they can read, and nothing of any other record — a record another user owns under the app's row scope, or a record of an object the member does not read. A member edits the comments they wrote. The grant carries no delete bit (deleting a comment stays the moderation path, through a delete bit an app grants), no export and no view-all or modify-all. -- **`enable.feeds: false` still closes comments** on that object: creating a comment there, or moving one into it, answers `403 FEEDS_DISABLED`. `enable.activities: false` still writes no activity row for the object. -- **External members are not reached.** A holder of the `external` organization role does not hold the `everyone` anchor and resolves `external_default` in place of `member_default`, so a portal user still gets comments and activity only through an explicit grant, under the same two gates. -- **No log noise.** A member's activity read skips the parent objects they cannot read without reading them, so the console header's feed logs no `[Security] Access denied` line per skipped object. -- An app that already grants the two objects to its members keeps working unchanged: permission sets merge most-permissively, so its own lines (a delete bit included) still apply beside the baseline's. -- **Lands with the permission-set clone door fix** (`.changeset/30255-clone-inherits-held-grants.md`). This grant first landed alone and was reverted: on a deployment that does not install `@objectstack/plugin-audit`, neither object is registered, and cloning `member_default` answered `422 INVALID_METADATA`. The clone door now copies such a set, so the grant and the fix ship together. diff --git a/.changeset/30119-studio-customization-build-notice.md b/.changeset/30119-studio-customization-build-notice.md deleted file mode 100644 index 4bbfd3ed11b..00000000000 --- a/.changeset/30119-studio-customization-build-notice.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/cli': patch -'create-objectstack': patch ---- - -feat(cli,create-objectstack): `os build` says when the local dev database holds Studio customizations the artifact leaves behind; the starter's `AGENTS.md` and README say a Studio change lives in the database and name `os package export` - -Clause-②: yes (widening) — the exported CompileCommand gains a protected reportsLocalCustomizations flag (default false); nothing removed or narrowed -Metadata-protocol: none - -- **`@objectstack/cli`: `os build`** ends, on its text face, with one notice when the SQLite database `os dev` opens for the project (the same resolution, over the project's development `.env*` files) holds items of a `.custom` package: how many, in which pair, the database file, and that `os package export` takes them out while `os dev` runs. A change saved in Studio is such an item (ADR-0146 D2), never part of `src/`, so neither the artifact nor the starter's `Dockerfile` carries it. The read is local and read-only; a database that is not a local SQLite file is not probed and prints nothing, and a file that cannot be read is said to be unchecked, never empty. `os compile`, which `os dev` runs on every boot and rebuild, and `os build --json` print nothing new. -- **`create-objectstack`: the generated `AGENTS.md` and the blank starter's README** say that Studio saves into the environment's database as an item of `.custom`, that `build` and the `Dockerfile` leave it behind, that app changes are made in `src/`, and how a customization moves as a package (`os login`, `os package export`, then `os package install` on the deployment, in the numbered order). diff --git a/.changeset/30119-studio-customization-environment-line.md b/.changeset/30119-studio-customization-environment-line.md deleted file mode 100644 index 02b055a3ba6..00000000000 --- a/.changeset/30119-studio-customization-environment-line.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(app-shell): Studio's publish review sheet says a customization lives in this environment's database and moves only by `os package export` (#30119) - -Clause-②: yes (widening) — new `@object-ui/i18n` key `preview.changes.customizationEnvironment` in every pack; `preview/savedToCustomization` gains `customizationEnvironmentText` and `PACKAGE_EXPORT_COMMAND` -Metadata-protocol: none - -Reviewing a read-only package's customizations, the sheet adds one line under "Saved to BASE's customizations": the changes are kept in this environment's database as items of the `.custom` package it names (the id the server answered for the base), not in the project's source, and they reach another environment only through `os package export`. diff --git a/.changeset/30121-notify-link-defaults-to-trigger-record.md b/.changeset/30121-notify-link-defaults-to-trigger-record.md deleted file mode 100644 index d26815cbdac..00000000000 --- a/.changeset/30121-notify-link-defaults-to-trigger-record.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/service-automation': minor ---- - -feat(service-automation): a record-triggered flow's notification opens the record that started the run, without the author writing `sourceObject` and `sourceId` - -Clause-②: no — no accepted key, value or exported type changes; the node's existing `sourceObject` / `sourceId` / `actionUrl` keys keep their meaning when written. -Metadata-protocol: semantic - -- **Before.** A `notify` node built its click-through link only from an explicit `sourceObject` + `sourceId` pair (or an `actionUrl`). The obvious record-triggered flow — `recipients`, `title`, `message` — passed `os validate` and sent a notification whose link was empty, so a click opened the notification list instead of the record. -- **Now.** A `notify` node that writes none of `sourceObject`, `sourceId` and `actionUrl` links to the run's trigger record when the run has one: a record change (create or update), a time-relative sweep's row, or an action started on a record. The notification stores that record as its `source_object` / `source_id`, and the inbox opens `/OBJECT/ID`. -- **Unchanged.** An explicit pair or an explicit `actionUrl` wins, as before. A half-written pair is still dropped rather than defaulted. A run with no trigger record (a schedule, an inbound API post, a manual start with no record) links nowhere, as before. A delete event is not linked: its record is gone once the write lands. -- **To link elsewhere** from a record-triggered flow, write the `actionUrl` or the `sourceObject` + `sourceId` pair the notification should open. diff --git a/.changeset/30122-timeline-one-row-per-action.md b/.changeset/30122-timeline-one-row-per-action.md deleted file mode 100644 index 07ace9d3a4d..00000000000 --- a/.changeset/30122-timeline-one-row-per-action.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -fix(record page): one action reads as one timeline row, and every row of the timeline panel names who acted - -Clause-②: no -Metadata-protocol: none - -- **One row per action.** When one action wrote both an activity row and an audit-log entry (same record, matching kind: create and created, update and updated, delete and deleted; same actor; stamped within `TIMELINE_PAIR_WINDOW_MS`, 5 seconds), the record page's timeline draws it once: the activity row keeps its actor and summary and shows the field changes, before and after, on the same row. A reader who cannot read the audit log sees the same single row without them. The Changes chip, and the panel's `changes_only`, still show each audit entry on its own. Rows that pair with nothing draw as before. -- **The 动态 panel's peek rows name the actor**, beside the time. -- The record page's 动态 count still comes from the server's `timeline` count, which the spec defines as an upper bound: for a record created with field history on it can still read one more than the rows drawn. diff --git a/.changeset/30123-scaffold-security-datasets.md b/.changeset/30123-scaffold-security-datasets.md deleted file mode 100644 index 4b19774ff6b..00000000000 --- a/.changeset/30123-scaffold-security-datasets.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/cli': patch -'create-objectstack': patch ---- - -feat(cli,create-objectstack): a new project has a wired place for permission sets and datasets, `os g permission` / `os g dataset` write them, and the generated AGENTS.md names the package manager the project was created with - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`create-objectstack`: the blank starter ships `src/security/index.ts` and `src/datasets/index.ts`**, empty barrels already handed to the stack's `permissions` and `datasets` keys in `objectstack.config.ts`, like the eight directories it wired before. A permission set or a dataset exported from them is loaded and counted by `validate` with no edit to the config. The generated `AGENTS.md` lists both directories. -- **`@objectstack/cli`: `os g permission NAME [--object OBJECT]`** writes `src/security/NAME.permission.ts`, a `definePermissionSet` granting read, create and edit (no delete) on the object `--object` names, or the stack's only object, with `readScope: 'own'` declared. **`os g dataset NAME [--object OBJECT]`** writes `src/datasets/NAME.dataset.ts`, a `defineDataset` over that object with a `record_count` measure, grouped by the record's title field when it has one. `defineDataset` types the literal, so it needs no `as const`. Both pass `os validate`, `os build` and `os lint` with no finding on a fresh project. `os init` wires both directories too. -- **`create-objectstack`: the generated `AGENTS.md` (and `.github/copilot-instructions.md`) says `pnpm run validate` in a project created with pnpm**, and `npm run …` in one created with npm, the same package manager the install line and "Next steps" name. It used to say `npm run` in every project. diff --git a/.changeset/30124-analytics-filter-array-refusal.md b/.changeset/30124-analytics-filter-array-refusal.md deleted file mode 100644 index cefcc128197..00000000000 --- a/.changeset/30124-analytics-filter-array-refusal.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): an array in an analytics filter slot is refused once, and the refusal shows the object form - -Clause-②: no -Metadata-protocol: none - -A dashboard widget `filter`, a dataset or measure `filter`, or a report's `runtimeFilter` written as a list view's rule array (`[{ field: 'status', operator: 'in', value: ['new', 'in_progress'] }]`) used to fail `os validate` with two lines that contradicted each other and named no fix: `Expected record but received object.` and `Expected object but received object.`. It now fails with one `invalid_type` issue at the same path, saying that these slots take the condition object a query's `where` takes and showing it: `{ status: { $in: ['new', 'in_progress'] } }`. - -Only the text changes, and the duplicate line goes. Every filter accepted before is accepted, every filter refused before is refused, and the published JSON Schema is unchanged. diff --git a/.changeset/30125-blank-sample-note.md b/.changeset/30125-blank-sample-note.md deleted file mode 100644 index cb600d8c9bb..00000000000 --- a/.changeset/30125-blank-sample-note.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'create-objectstack': patch ---- - -Clause-②: no -Metadata-protocol: none - -The blank template now says its `note` object is a sample: the generated README gains a "The sample `note` object" section with the steps to delete or replace it, and `src/objects/note.object.ts` opens with a comment saying the same. A developer no longer finds a `note` object in their app, over REST or in an MCP client's object list, without anything telling them it came from the scaffold. diff --git a/.changeset/30125-invitation-sign-up-joins.md b/.changeset/30125-invitation-sign-up-joins.md deleted file mode 100644 index 7472b399d94..00000000000 --- a/.changeset/30125-invitation-sign-up-joins.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/console': patch ---- - -feat(console,app-shell): registering from an invitation link joins that invitation's organization and enters the app in one step - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@object-ui/console`: `/register`** opened from an invitation link (`?redirect=/accept-invitation/ID`) now accepts THAT invitation as the account the sign-up just created, switches the session to its organization and enters the console root. A new member used to pass three screens: the sign-up, the accept page ("Accept invitation"), and "Invitation accepted … Continue". The server still decides: an address that is not the invited one is refused, nothing is accepted, and the page goes on to the accept page as before, which shows the refusal. A person who opens the page already signed in accepts nothing there, and a sign-up that waits for email verification is accepted from the accept page the verification mail returns to. -- **`@object-ui/app-shell`: `DefaultRegisterPage`** does the same; it used to go to `/` after the sign-up and leave the invitation behind. New export **`useInvitationSignUpJoin(redirect)`**, the one hook both register pages call, answering `none`, `joining`, `joined` or `refused` (type `InvitationSignUpJoin`). diff --git a/.changeset/30125-sign-out-no-return-link.md b/.changeset/30125-sign-out-no-return-link.md deleted file mode 100644 index e0464fd11d8..00000000000 --- a/.changeset/30125-sign-out-no-return-link.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/auth': patch -'@object-ui/console': patch ---- - -fix(console,auth): after an explicit sign-out, whoever signs in next lands on their own home, not on the page the previous person had open - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@object-ui/auth`: `AuthContextValue.sessionEndedBySignOut`**, `true` from a `signOut()` until a session is established again in the tab, whoever it belongs to; `false` on load, while signed in, and after a session that ended any other way (expired, revoked, signed out in another tab). It is a new required member of the exported `AuthContextValue` type: code that builds its own value of that type (a test double, a custom provider) must add `sessionEndedBySignOut: false` (or its own reading) to type-check. -- **`@object-ui/console`: `LoginRedirect`** sends the visitor to `/login` with no `?redirect=` after an explicit sign-out. A new member signing in after the administrator signed out of a Setup page used to land on that page and read "you do not have access to this app". A session that expires or is revoked under the person still keeps the path (list state stripped as before), so that person signs back in where they were. diff --git a/.changeset/30126-console-copy-titles-toast-money.md b/.changeset/30126-console-copy-titles-toast-money.md deleted file mode 100644 index 92338e8b979..00000000000 --- a/.changeset/30126-console-copy-titles-toast-money.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/i18n': patch -'@object-ui/core': patch -'@object-ui/app-shell': patch -'@object-ui/plugin-dashboard': patch ---- - -fix(console): four copy and display details from a real-user walkthrough: Latin object names spaced in zh form titles, a title on the lookup's create dialog, a success toast that names the action, and money metrics at the currency's decimals - -Clause-②: yes (widening) -Metadata-protocol: none - -- **zh form titles space a Latin object name.** `新建{{object}}`, `编辑{{object}}` and `查看{{object}}` (and the AI chat's record-write card titles) read `新建 Repair Ticket` when the object has no Chinese label, and still `新建联系人` when it has one. `createI18n` applies the spacing to those keys only, through an i18next post-processor; the packs keep the plain `{{object}}` spelling. -- **The lookup's "create the referenced record" dialog has a title.** A `{ objectName, mode: 'create' }` modal descriptor with no `title` now reads `form.createTitle` with the object's label, the title of the console's own New dialog. An authored title still wins. -- **The default success toast names the action.** An action with a `label` and no `successMessage` (and no matching `outcomeMessages` entry) toasts the new pack key `actions.completedNamed` (`Resolve completed`, zh `“解决”已完成`) instead of the generic `Action completed successfully`. An action with no label keeps the generic text. -- **Money metrics keep the currency's decimals.** A dataset metric over a currency measure with no `format`, and a static metric card with a `currency` and no `format`, show the currency's own minor-unit width (CNY `¥1,293.00`, JPY `¥1,293`), the width the list cell shows. Before, the dataset metric printed the shortest form (`¥1,293`, `¥1,280.5`) and the static card rounded to whole units. An authored `format` pattern still states its width. diff --git a/.changeset/30126-console-nav-icons-labels-axis-retry.md b/.changeset/30126-console-nav-icons-labels-axis-retry.md deleted file mode 100644 index 659ab04b30e..00000000000 --- a/.changeset/30126-console-nav-icons-labels-axis-retry.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/layout': patch -'@object-ui/app-shell': patch -'@object-ui/plugin-detail': patch -'@object-ui/plugin-charts': patch -'@object-ui/console': patch ---- - -fix(console): a nav entry with no icon draws its object's icon, record field labels read in one style, a count chart fits a small count, and a page whose entry failed to load retries (#30126) - -Clause-②: yes (widening) -Metadata-protocol: none - -- **Navigation icons.** An app navigation entry of type `object` that names no `icon` now draws the icon its object declares, in the sidebar and in the `nav:menu` page block. Before, every such row drew the same generic page icon, so an app whose navigation names no icons showed one icon down the whole sidebar. The icon is read by object name from the metadata the console already holds, so no request is added and the navigation is not written to. An authored `icon` wins, and an object with no icon, a dashboard, page or report entry, and a host with no resolver keep the generic icon. `@object-ui/layout` exports `resolveNavItemIcon`, the types `NavIconTarget` and `NavTargetIconResolver`, and an optional `resolveTargetIcon` prop on `NavigationRenderer`, alongside the existing `resolveTargetLabel`. -- **Record field labels.** A record page's section field labels no longer carry an uppercase transform. Only Latin text took it, so in a zh console an untranslated label (`REPAIR COST`) read in capitals beside a translated one (`负责人`). Section labels now render as authored, in the style of the highlights strip above them. -- **Small count charts.** A count axis whose largest value is a small whole number (1 to 3) now tops out at that number, so a chart of one record draws its bar to the top of the axis instead of a quarter of the way up a 0–4 axis. An axis with an authored `min`, `max` or `stepSize`, fractional values, or a count of 4 or more draws as before. -- **Boot retry.** When the page's entry script fails to load before anything mounts (the server went away between serving the page and serving its entry, as during an `os dev` restart), the page now reloads itself, backing off from 1 second and doubling, at most five times per tab. Before, the loading splash stayed forever. The count is cleared on the first mount, and where `sessionStorage` cannot be read nothing retries. diff --git a/.changeset/30126-inbox-topic-mcp-refusal-history.md b/.changeset/30126-inbox-topic-mcp-refusal-history.md deleted file mode 100644 index b36d9f0adb4..00000000000 --- a/.changeset/30126-inbox-topic-mcp-refusal-history.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/service-messaging': patch -'@objectstack/mcp': patch ---- - -fix: the inbox's own notification list no longer shows the raw topic, and an MCP argument refusal ends at the fix - -Clause-②: no -Metadata-protocol: none - -- **`@objectstack/service-messaging`.** The `mine` list view of `sys_inbox_message` (a person's own notifications) showed a Topic column reading the producer's machine id, such as `notify` for a flow's notify node or `collab.mention` for a mention. Nothing labels those ids, so the column is gone from that view: it now shows the title, the severity and the time. The `topic` field itself is unchanged and still searchable, so a filter or another view can use it. -- **`@objectstack/mcp`.** When a tool call carries an argument the tool does not declare, the refusal names the argument and the spelling to send, and now stops there. It no longer ends with the sentence "This argument used to be dropped silently and the call still succeeded, …", which explained the history of the check to internal developers and gave the caller nothing to act on. Every tool still refuses an undeclared argument exactly as before; only the closing sentence is gone. diff --git a/.changeset/30134-client-next-week-year-end.md b/.changeset/30134-client-next-week-year-end.md deleted file mode 100644 index 2fe0cc94cda..00000000000 --- a/.changeset/30134-client-next-week-year-end.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/core': patch ---- - -fix(core): the client date-macro resolver answers `{next_week_end}` and `{next_year_end}` - -Clause-②: no -Metadata-protocol: none - -`resolveDateMacros` now resolves `{next_week_end}` (Sunday of next week, Monday-start weeks) and `{next_year_end}` (Dec 31 of next year), the two fixed tokens `@objectstack/spec`'s `DATE_MACRO_TOKENS` declared that the Console passed through as raw text. A test now reads `DATE_MACRO_TOKENS` from the spec and fails on any token the client leaves unresolved, so the two lists cannot drift again. diff --git a/.changeset/30136-first-organization-default-name.md b/.changeset/30136-first-organization-default-name.md deleted file mode 100644 index ac92c1eda45..00000000000 --- a/.changeset/30136-first-organization-default-name.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -feat(app-shell): the first organization's create dialog opens with a default name, so one click continues - -Clause-②: no -Metadata-protocol: none - -For a user with no organization yet — the dialog that opens right after sign-up — the organization name is prefilled from the user's e-mail. A work domain names the company from the domain's registrable label, capitalised (`jane@acme.com` → `Acme`, `jane@acme.co.uk` → `Acme`). A public mailbox domain (a closed list in code: gmail.com, outlook.com, qq.com, 163.com, proton.me and similar, subdomains included) names no company, so it takes the existing default organization name, "My Organization" / 「我的组织」 (`workspace.default`, already in every built-in pack; no translation key is added). An unlisted domain is treated as a work domain. The field stays editable, nothing is created until the user clicks, and creating a further organization still opens with an empty name. The e-mail domain rules live in a product-neutral app-shell util (`utils/emailDomain`), outside the organization pages. diff --git a/.changeset/30140-compare-to-forward-presets.md b/.changeset/30140-compare-to-forward-presets.md deleted file mode 100644 index 899e188ee04..00000000000 --- a/.changeset/30140-compare-to-forward-presets.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/i18n': patch -'@object-ui/plugin-dashboard': patch -'@object-ui/plugin-charts': patch ---- - -fix(core): a compareTo widget on a forward date range (next month, next quarter, next 30 days) compares with the period just before it, not with itself - -Clause-②: no -Metadata-protocol: none - -- **`@object-ui/core`: `shiftFilterByCompareTo`** with `kind: 'previousPeriod'` moved only `current_*` / `today` tokens, so a dashboard filtered to `next_month` or `next_quarter` compared the window with itself and the delta always read 0, and `next_30_days` (`{today}` … `{30_days_from_now}`) moved only its start and compared with a window that overlapped it on all but one day. A forward window now steps back one period: `{next_month_*}` becomes this month, `{next_quarter_*}` this quarter, and a `{today}` … `{N_days_from_now}` pair on one field becomes the N + 1 days that end yesterday (`{(N+1)_days_ago}` … `{yesterday}`), the same window the dataset executor computes for that range. Every other token, and the 13 backward presets, compare as before. -- **`@object-ui/core`: `compareToTrendLabelKey`** names that window: `vsThisMonth` for a forward month, `vsThisQuarter` for a forward quarter, and `vsPreviousPeriod` for a forward rolling window (it read `vsLastMonth`, `vsLastQuarter` and `vsYesterday`). -- **`@object-ui/i18n`:** `dashboard.trend.vsThisMonth` and `dashboard.trend.vsThisQuarter` in all ten locale packs (zh: 较本月, 较本季度). -- **`@object-ui/plugin-dashboard`, `@object-ui/plugin-charts`:** English fallbacks for the two keys in the metric tile and in the chart's comparison series label. diff --git a/.changeset/30157-handoff-verification-mail.md b/.changeset/30157-handoff-verification-mail.md deleted file mode 100644 index d27503708fe..00000000000 --- a/.changeset/30157-handoff-verification-mail.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/console': patch -'@object-ui/i18n': patch ---- - -fix(console): a person who verifies their email during the mobile sign-in hand-off gets a mail link that works, and the hand-off's refusal time survives the verification pages (#30157) - -Clause-②: no -Metadata-protocol: none - -On a server that requires email verification, a person signing up from the mobile app's sign-in (the hand-off `redirect` on `/register`) got a verification mail whose link landed on a Console page that does not exist (`/_console/api/v1/auth/one-time-token/handoff?…`). The mail is usually opened in a browser that is not the app's sign-in window, and often on another device, so the link now returns to `/verify-email?handoff=verified`: the page says the address is confirmed and to go back to the app and sign in there. It offers no Console sign-in and hands out no sign-in token in that browser. The same holds for the "Resend verification email" mail. Every other `redirect` (an invitation, a Console page) keeps the link it had. - -The hand-off's refusal time (`handoff_refused_at`) is now carried through `/verify-email-prompt` on all three hops: `/login`'s "email not verified" branch, `/register`'s "verification required" branch, and "Back to login". Beside any other `redirect`, those links are unchanged. - -New string: `auth.verifyEmail.handoffSuccessDescription` (all ten locales). diff --git a/.changeset/30164-zh-cn-dashboard-word.md b/.changeset/30164-zh-cn-dashboard-word.md deleted file mode 100644 index f06325194be..00000000000 --- a/.changeset/30164-zh-cn-dashboard-word.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/platform-objects': patch ---- - -fix(platform-objects): zh-CN metadata forms say 仪表盘 for dashboard (#30164) - -Clause-②: no -Metadata-protocol: none - -The zh-CN dashboard authoring form the platform serves (`MetadataFormsTranslations`, which `/api/v1/meta/types` localizes forms with) still said 仪表板 on six strings: the form title, the basics section description, and the help text of the header, show-title, show-description and widgets fields. The Console and Studio say 仪表盘, so one object carried two names in one flow of work. All six now say 仪表盘. The generated bundle is the extract's merge baseline, so its leaves were corrected in place and the bundle regenerated (`pnpm i18n:extract` reproduces it byte for byte; `pnpm check:i18n` is green). en, ja-JP and es-ES are unchanged. diff --git a/.changeset/30168-unshipped-locale-write-open-before-pair.md b/.changeset/30168-unshipped-locale-write-open-before-pair.md deleted file mode 100644 index da78dc7c046..00000000000 --- a/.changeset/30168-unshipped-locale-write-open-before-pair.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor ---- - -fix(metadata-protocol): an email template at a locale the base does not ship keeps its package-less write on a fresh install; the named-base refusal names `os migrate packageless-overlays` - -Clause-②: no -Metadata-protocol: semantic - -- **An added locale is writable before the pair exists.** Both overlay-write closes (the package-less write, and the write naming the read-only base with `?package=`) stay open where the fix they would name cannot be taken: an `email_template` member at a locale the base does not ship is refused as a declared override, so its write stays open (ADR-0146 D11). The close asked the override write gate about the pair; on a fresh install the pair is not ensured yet, the gate's closure read answers nothing for a package that does not exist, and the close read that "not concerned" as "admitted". So `PUT /api/v1/meta/email_template/` with a zh-CN body beside a base that ships en-US only answered `403 NOT_OVERRIDABLE`, naming a pair write that was refused too (`declared_package_ships_no_such_item`). The close now judges a pair that is not installed with the closure the ensure door gives it (the base and what the base reaches), so the write is accepted, as it is once the pair exists. A member the base ships is closed exactly as before. -- **The named-base refusal names the ceremony.** `os migrate packageless-overlays` moves a row stored bound to the read-only base into the pair with its declaration. The sentence for a save, publish or rollback naming the base now says such a row "is served until moved by `os migrate packageless-overlays`, overridden by the pair, or deleted naming the base". One spelling, every door; the code and status (`403 NOT_OVERRIDABLE`) are unchanged. diff --git a/.changeset/30169-undeclared-bulk-action-name-draws-nothing.md b/.changeset/30169-undeclared-bulk-action-name-draws-nothing.md deleted file mode 100644 index 0c834bb3c33..00000000000 --- a/.changeset/30169-undeclared-bulk-action-name-draws-nothing.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/plugin-grid': minor ---- - -fix(plugin-grid)!: a list's selection bar and row menu draw only actions the object declares; an undeclared bare `bulkActions` / `rowActions` name draws nothing and warns once in development (#30169) - -Clause-②: no (narrowing) — the grid stops drawing a bare `bulkActions` or legacy `rowActions` name that matches no action the object declares, which it drew before by humanized name. `packages/spec/src/ui/bulk-action.zod.ts` already says the member "names an action the object declares", and lint `action-name-undefined` (ADR-0049) refuses any other at authoring. Nothing is widened. -Metadata-protocol: narrowed -Skills-docs: no move — `skills/**` teaches no handler-only bulk or row action name (searched for `bulkActions` / `rowActions` examples and runner-handler text); `content/docs/ui/**` already names only declared actions in its `bulkActions` / `rowActions` examples. - - - -**BREAKING** for a host that registered a runner handler under a bare name the object does not declare (`ActionProvider handlers` / `runner.registerHandler('NAME', …)`) and listed that name in a view's `bulkActions` or `rowActions`. - -- **Before.** `resolveBulkActions` and `resolveLegacyRowActions` kept a name that matched no declared action, and `ObjectGrid` drew it by humanized name and dispatched it to the runner by name. A name nothing handled toasted "not executable" on click. This is how a list whose app had a package disabled kept a ghost button: the server withholds that package's actions from the served object, so their names matched nothing. -- **Now.** Such a name gets no selection-bar button and no row-menu entry, and in development the console logs one warning per list naming the key, the block and each name. The resolvers return those names as `undeclared` (renamed from `unresolved`), used only for that warning. The canonical `'delete'` (and the row menu's `'edit'` / `'delete'`) keep their own paths, unchanged. -- **Unchanged.** A name that matches a declared action is promoted to that action's def exactly as before, under the action's own label, icon, params, `visible` and `requiredPermissions`. - -**Remedy.** No rewrite is derived, because which fix an undeclared name needs is a judgement about the view: declare the named action on the object, or remove the name from `bulkActions` / `rowActions`. The development warning names each one. An action withheld by a disabled package needs nothing: it draws again when the package is enabled. diff --git a/.changeset/30177-draft-only-object-default-list-view.md b/.changeset/30177-draft-only-object-default-list-view.md deleted file mode 100644 index 9e84d49f6a1..00000000000 --- a/.changeset/30177-draft-only-object-default-list-view.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): in Studio Interfaces, a draft-only object's nav entry opens the object's drafted default list view, not a non-existent "Default view" - -Clause-②: no -Metadata-protocol: none - -An object that is still only a draft in the package is absent from the shell's published metadata, so a nav entry for it with no `viewName` used to open `.default`, a view that does not exist. The view switcher listed that "Default view" beside the view the author had drafted as the default, and the first edit in the panel created a second default list view in the package. The entry's default list view is now resolved against the published views and the package's drafted list views together, so it opens the drafted default view, the switcher lists only the views that exist, and an edit writes to that view. Until the package's drafts have been read, a default view that no published view flags is not loaded or listed, so no edit can be made against the wrong view. A published object with no default list view still opens `.default`, created by the first edit, as before. diff --git a/.changeset/30178-numeric-screen-fields-refuse-strings.md b/.changeset/30178-numeric-screen-fields-refuse-strings.md deleted file mode 100644 index 523e5bc7618..00000000000 --- a/.changeset/30178-numeric-screen-fields-refuse-strings.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/service-automation': patch -'@object-ui/app-shell': patch ---- - -fix(automation)!: a `currency` or `percent` screen field refuses a non-number on resume, as a `number` field does, and the console renders `percent` as a numeric input (#30178) - -Clause-②: yes (narrowing) — `POST …/runs/RUN/resume` refuses a string under a `type: 'currency'` or `type: 'percent'` screen field, a bag it accepted before. Nothing is widened. -Metadata-protocol: none - - - -**BREAKING** accept-set narrowing at the screen-flow resume door. - -- **Before.** A `type: 'percent'` or `type: 'currency'` screen field took any string on resume: `"75"` under a `max` of 60, `"abc"` too. The bound compares numbers, so it never judged a string, and the run consumed its suspension on the value. The console rendered `percent` as a text input and posted a string, so a `percent` field's `min` / `max` held for no console user. -- **Now.** `number`, `currency` and `percent` are one value domain: a present value that is not a finite JSON number is refused with `invalid_type` inside `400 INVALID_SCREEN_INPUT`, the suspension is kept, and a corrected resume still lands. A numeric string is not read as the number it spells. The console renders `percent` as a numeric input carrying `min` / `max`, so it posts a number and the browser stops an over-limit value before any request. -- **Unchanged.** An absent value and a hidden field are not judged; `rating`, `slider`, `progress`, `text` and untyped fields still take a string; a JSON number on `currency` / `percent` is judged against the bound as before. -- **For a caller that skips the console.** The resume bag carries the JSON number (`60`), not a string; the refusal names the field and says it must be a number. No stored metadata changes, so there is nothing for `objectstack migrate meta` to rewrite. diff --git a/.changeset/30178-screen-field-numeric-kinds-contract-text.md b/.changeset/30178-screen-field-numeric-kinds-contract-text.md deleted file mode 100644 index 3889a8161dd..00000000000 --- a/.changeset/30178-screen-field-numeric-kinds-contract-text.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(spec)!: the screen-field contract names `number`, `currency` and `percent` as the numeric value domain the resume enforces (#30178) - -Clause-②: yes (narrowing) — the resume door refuses a string under a `type: 'currency'` or `type: 'percent'` screen field (`@objectstack/service-automation`); this package's `ScreenFieldSpec` and `ScreenFieldConfigSchema` text now states that rule. No schema shape changes. -Metadata-protocol: none - - - -**BREAKING** (described here, enforced in `@objectstack/service-automation`): a `currency` or `percent` screen field takes a finite JSON number on resume, as a `number` field does; a numeric string is refused with `invalid_type`, never coerced. `ScreenFieldSpec.type` / `.min` / `.max` and `ScreenFieldConfigSchema.type` / `.min` / `.max` say so, and the `type` describe text names the three kinds. diff --git a/.changeset/30184-requiredwhen-transition-gate.md b/.changeset/30184-requiredwhen-transition-gate.md deleted file mode 100644 index 7b94ee8d994..00000000000 --- a/.changeset/30184-requiredwhen-transition-gate.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/components': patch ---- - -fix(ui): the record Edit form judges a field's `requiredWhen` as the engine does, a transition gate against the stored record, so a record already inside the gate without the value saves an unrelated edit - -Clause-②: yes (widening) -Metadata-protocol: none - -This widens the public surface of `@object-ui/core`: the new export `requiredWhenRestsOnStoredRecord` and the new `requiredWhenAtRest` field in the return type of `resolveFieldRuleState`. - -- **`@object-ui/components` form renderer:** on an Edit form (`previousValues` set), a field's `requiredWhen` is required only when the stored record complied with it. A stored record that already violates it (the predicate holds over the stored record and its value is missing, as with a contract activated before its signed-copy rule existed) is not blocked any more: it saves an edit that leaves the field empty, as the engine and REST `PATCH` already accept. This follows `Field.requiredWhen`: the write is refused only when the merged record violates the requirement AND the pre-write record complied. Still refused: the edit that moves a record into the gate without the value, and clearing the value of a record that held it. A Create form enforces as before. The "Required to run" note under an empty field (the update-action heads-up) is no longer shown for such a stored record, because the engine judges the action's write against the same stored record. -- **`@object-ui/core`:** `resolveFieldRuleState` takes `statics.name` (the field's name) and returns `requiredWhenAtRest`. With `previous` and `statics.name` given, a stored record already violating `requiredWhen` resolves `required: false` and `requiredWhenAtRest: true`. A caller that passes no `name` keeps the plain verdict. New export `requiredWhenRestsOnStoredRecord(pred, name, stored, scope?)`, the stored-record check alone. A predicate that cannot be evaluated over the stored record counts it as compliant, as on the server. diff --git a/.changeset/30192-environment-scoped-security-provider.md b/.changeset/30192-environment-scoped-security-provider.md deleted file mode 100644 index c84a014503a..00000000000 --- a/.changeset/30192-environment-scoped-security-provider.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): the `/security` routes, and the auth, settings, i18n and metadata seams, answer from the request's environment kernel when one REST plugin fronts many environments - -Clause-②: no -Metadata-protocol: none - -- **What was wrong.** On a host that mounts one `RestApiPlugin` in front of many environment kernels (the hosted runtime shape), the `security` provider read the HOST kernel's service whatever environment it was asked for, and the unscoped (hostname-routed) suggested-binding mount asked it for no environment at all. The host has no `security` service, so `GET /security/suggested-bindings` and `POST /security/suggested-bindings/:id/confirm` answered 501 on every hosted environment: no owner could accept an installed app's default permission set, and every invited member read 403 on that app's data. `security/explain` and the record, export and meta doors that read the same service fell back to the host's occupant whenever the environment's kernel did not answer. -- **What changes.** Given an environment id and a registered `kernel-manager`, the `security` provider now reads `security` on that environment's kernel, the same environment-scoped seam the approvals, email, sharing, sharing-rule and analytics providers use. Every route that reads it passes the id the shared request→environment resolution answered: the `/environments/:environmentId` param on the scoped mount, else the host's `kernel-resolver` (or the hostname / `X-Environment-Id` chain) on the unscoped one. The host's occupant is no longer a fallback. -- **The audited seams.** The `auth`, `settings`, `i18n` and `metadata` providers are environment-scoped the same way. On a hosted runtime the REST face's execution context now localizes (time zone, locale, currency) from the environment's own settings, as the runtime dispatcher's already did, instead of the host's. An environment whose kernel carries no `auth` service is no longer authenticated by the host's identity store. The `tenancy` provider keeps reading the plugin's own kernel: every consumer reads the bound environment kernel's `tenancy` first. -- **Unchanged.** A single-kernel deployment (no `kernel-manager`) reads every one of these services on the plugin's own kernel exactly as before, for every route and every id. The reserved `platform` id and a request the resolver places in no environment also read the plugin's own kernel. -- **No migration.** No configuration, route, response shape or export changes. diff --git a/.changeset/30193-override-label-pair-precedence.md b/.changeset/30193-override-label-pair-precedence.md deleted file mode 100644 index 87f30db905b..00000000000 --- a/.changeset/30193-override-label-pair-precedence.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -feat(i18n): one label order on an overridden item, for all eight document translators — the pair's translation, then the override's own label, then the base's translation - -Clause-②: yes (widening) -Metadata-protocol: none - -- **New option `TranslateDocumentOptions.pairBundle`.** The translations the served item's paired `.custom` package ships itself (ADR-0146 D2/D3), locale-keyed as `bundle` is. The serving layer's `bundle` is one merged catalog per locale with the package ids dropped, so only this option can tell a translation the pair ships from the base's. -- **The order, per translatable string.** A string the override changed (`valueOverridesPackagedBase` against `packagedBase`, the comparison every translator already makes) reads the pair's translations only, along the locale chain, and otherwise keeps its own text: the base's translation is never read for it. A string the override left alone reads, locale by locale, the pair's translation and then the deployment catalog, so a pair that ships only `en` never displaces the base's `zh-CN` on an untouched label. Nested labels follow the same rule: widget titles, filter and option labels, view bulk-action defs and form sections, report blocks, field labels, help and options, page component copy, tab items and the header, dataset members, app navigation nodes and book groups. -- **`translatePage`, `translateDataset`, `translateApp` and `translateBook` now read `packagedBase`** (their third parameter widens from `ResolveOptions` to `TranslateDocumentOptions`), each string judged against the base's counterpart: the page's components by id and its header, the dataset's members by `name`, the app's navigation nodes by `id`, the book's groups by `key`. A page header title the override left as the base wrote it translates as the base's label. -- **Nothing moves for an existing caller.** With no `pairBundle` (or one that answers none of the chain's locales) every translator gives exactly the answer it gave before, and with no `packagedBase` nothing counts as changed. Passing both for a `.custom` override is the serving layer's half (#30062). -- An object's `validations[].message` and its inline `actions` keep reading the merged catalog: the rule evaluator answers the same message from `i18n.t()` with no pair input, and the refusal text must equal the served text. diff --git a/.changeset/30197-compare-to-past-presets.md b/.changeset/30197-compare-to-past-presets.md deleted file mode 100644 index 6d83b5186b2..00000000000 --- a/.changeset/30197-compare-to-past-presets.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/core': patch ---- - -fix(core): a compareTo widget on an already-past date range (yesterday, last week / month / quarter / year, last N days) compares with the period before it, and previousYear from 29 February stays in February - -Clause-②: no -Metadata-protocol: none - -- **`shiftFilterByCompareTo`, `kind: 'previousPeriod'`.** `{yesterday}` and the `last_*` tokens were never moved, so a dashboard filtered to `yesterday`, `last_week`, `last_month`, `last_quarter` or `last_year` compared the window with itself and the delta always read 0. They now compare with the period before them: `{yesterday}` with `{2_days_ago}`, and `last_week_*` … `last_year_*` with the week, month, quarter or year before last. The macro vocabulary has no token for "two periods back", so those windows are computed: the `last_*` start is resolved in the display zone, moved back one period, and the end is the day before it. -- **Rolling windows.** `{N_days_ago}` … `{today}` (`last_7_days`, `last_30_days`, `last_90_days`; N + 1 days) moved only its end and compared with the N days ending yesterday, which overlap it. It now compares with the N + 1 days before it, `{(2N+1)_days_ago}` … `{(N+1)_days_ago}`, the same window the dataset executor computes. -- **`kind: 'previousYear'` from 29 February** re-resolved the filter at 1 March of the year before (`new Date(2027, 1, 29)`), so `this_month` compared with March. The day is now clamped to the month's last day, 28 February. -- **`compareToTrendLabelKey`** reads all of these as `vsPreviousPeriod` (they read `vsLastWeek`, `vsLastMonth`, `vsLastQuarter`, `vsLastYear` and `vsYesterday`); no `dashboard.trend.*` key names "the month before last". `today`, the `this_*` and the forward presets compare and label as before. diff --git a/.changeset/30198-org-admin-update-refused-before-lookup.md b/.changeset/30198-org-admin-update-refused-before-lookup.md deleted file mode 100644 index c41e9e9778f..00000000000 --- a/.changeset/30198-org-admin-update-refused-before-lookup.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security): an organization administrator's update by id is refused before the row is looked up, whatever the id names - -Clause-②: no -Metadata-protocol: none - -An organization administrator (`organization_admin` or `organization_admin_no_bypass`, holding no `adminScope`) updates an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. An edit in place is never part of that administrator's right, so the write is refused before the row is looked up: `403 PERMISSION_DENIED`, with the same sentence and details for a row, for a row the administrator cannot read, and for an id that names no row. - -Unchanged: a removal by id of a row the administrator cannot read, or of an id that names no row, answers the read door's `404 RECORD_NOT_FOUND`. An administrator who also holds an `adminScope` is judged by that scope. diff --git a/.changeset/30199-suggested-binding-reconsider.md b/.changeset/30199-suggested-binding-reconsider.md deleted file mode 100644 index ab124ecd833..00000000000 --- a/.changeset/30199-suggested-binding-reconsider.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(app-shell): a dismissed or revoked default permission set in the suggested-bindings panel can be reconsidered (#30199) - -Clause-②: no -Metadata-protocol: none - -The suggested-bindings panel (the install dialog and Marketplace package page, and Studio's Access pillar) drew a dismissed suggestion, or a revoked acceptance, as a state line with no action. An administrator who chose Dismiss could only take it back by calling `POST /api/v1/security/suggested-bindings/:id/reopen` by hand, although the server has offered that door all along. - -Such a row now offers "Reconsider". It calls the re-open door, and the row comes back as a pending suggestion with Accept and Dismiss (a Guest row with Dismiss only, as before). Re-opening grants nothing: the set reaches the Everyone position only when it is accepted again, under the same checks. The button is shown to the same callers who see Accept and Dismiss: a caller without `manage_metadata` is refused the list and sees no panel. A refusal (the row is gone, the set is no longer suggested, or the deployment's security service has no re-open door) is shown with the server's own message, and the panel re-reads the list. - -New client call: `reopenSuggestedBinding(id)`, beside `dismissSuggestedBinding`. New strings: `marketplace.suggestedBindings.reopen`, `.reopening` and `.reopenedToast` in all ten locale packs, and `engine.studio.access.suggestReopen`, `.suggestReopening` and `.suggestReopenedToast` (en, zh). diff --git a/.changeset/30201-org-mfa-needs-enrollment.md b/.changeset/30201-org-mfa-needs-enrollment.md deleted file mode 100644 index 32d9db0c0e7..00000000000 --- a/.changeset/30201-org-mfa-needs-enrollment.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix(plugin-auth)!: an organization cannot require multi-factor authentication while two-factor enrollment is off, and the MFA gate never holds a user who has no way to enroll (#30201) - -Clause-②: no (narrowing) — while the two-factor feature is off, a write that turns `sys_organization.require_mfa` on (an insert with it on, or an update of a row that stores it off) is refused `400 VALIDATION_FAILED`, a value an administrator could save before; re-saving a row that already stores it on passes. No published export, error code or settings key is added; nothing is widened. -Metadata-protocol: none -Skills-docs: follow-up owed — content/docs/permissions/authentication.mdx (the `mfa_required` row says `require_mfa` is settable per organization with no condition; it needs two-factor on) - - - -**BREAKING** accept-set narrowing on `sys_organization.require_mfa` writes, served by every data door (the organization's edit form, REST, MCP), on a deployment where two-factor is off. - -- **Before.** An administrator could tick Require Multi-Factor Auth on an organization while two-factor was off (the default, `features.twoFactor: false`), and it saved. Once the grace period ended, every member, the owner included, was held `403 MFA_REQUIRED` and sent to enroll, and the enrollment endpoint (`POST /api/v1/auth/two-factor/enable`) answered 404: the twoFactor plugin that serves it was never turned on. Nobody could undo it from inside the product, because Setup sits behind the same gate. The global switch never had this hole: the `auth.mfa_required` setting turns the requirement and the plugin on as one piece. -- **Now.** While two-factor is off, a write that turns `require_mfa` on is refused: an insert with it on, or an update of a row whose stored value is off. The refusal carries one `fields[]` entry on `require_mfa` (`invalid_value`) saying why, who would have been locked out, and the ways forward. It applies to every write context, the system's included, on the by-id door, an insert, and a multi-row update, which is judged row by row (one row turning it on refuses the whole write, with nothing written). Re-sending `require_mfa: true` for a row that already stores it (a whole-row update, an import re-run, a save with no change) passes, so other edits of that row are not blocked. Clearing the column and writing other columns pass as before, and every write passes once two-factor is on (read per write, so no restart is needed). -- **The gate.** ADR-0069 D3's MFA gate holds a user only while they can enroll. With two-factor off, an MFA requirement (an organization's `require_mfa`, or `auth.mfa_required` under an `OS_AUTH_TWO_FACTOR=false` pin, or a host that sets `mfaRequired` without `plugins.twoFactor`) holds nobody and starts no grace clock, and the server says so once in a warning with the remedy. An organization row stored with `require_mfa = true` before this release therefore locks no one out; it starts holding unenrolled members once two-factor is turned on. -- **One answer.** Whether two-factor is on is decided in one place, the same `OS_AUTH_TWO_FACTOR` env, then `plugins.twoFactor`, then off, for the plugin list, `features.twoFactor` in `/api/v1/auth/config`, the gate and the write refusal. -- **Unchanged.** An enrolled user is never held. With two-factor on, the requirement and its grace period behave exactly as before. The `auth.mfa_required` setting still turns two-factor on with the requirement. - -**The remedy.** To require multi-factor authentication for one organization, turn two-factor enrollment on for the deployment first, with `OS_AUTH_TWO_FACTOR=true`, and then tick the organization's box. Unless `OS_AUTH_TWO_FACTOR` is set to `false` (the env var outranks the settings), turning on the `auth.mfa_required` setting instead also works: it requires multi-factor authentication for every organization and turns enrollment on with it. diff --git a/.changeset/30202-keep-a-sign-in-method.md b/.changeset/30202-keep-a-sign-in-method.md deleted file mode 100644 index 193b4a4597b..00000000000 --- a/.changeset/30202-keep-a-sign-in-method.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/plugin-auth': patch -'@objectstack/service-settings': patch ---- - -fix(plugin-auth)!: saving the auth settings refuses to turn off the last way to sign in (#30202) - -Clause-②: yes (narrowing) — a save an administrator could make before is now refused at the settings save door with the existing `SETTINGS_VALIDATION` / `invalid_value` field error: a save after which email and password is off and nothing else signs anyone in, judged on the whole save, including the Google change that removes the last provider while email and password is off. No new error code or settings key. The widening half: `SettingsWritePredicateInput`, exported from `@objectstack/service-settings`, gains an optional `batch` field, the whole write a predicate is asked about. -Metadata-protocol: none -Skills-docs: none — no file under `skills/**` teaches `auth.email_password_enabled` or the settings write predicate; `content/docs/deployment/environment-variables.mdx` already names the `OS_AUTH_EMAIL_PASSWORD_ENABLED` override used for recovery below. - - - -**BREAKING** accept-set narrowing at the auth settings save door (`PUT /api/settings/auth`, `SettingsService.setMany('auth', …)`): a save that would leave no way to sign in is refused, where it was saved before. - -- **Before.** Setup → Settings → Authentication, "Enable email/password login" off, save: "Settings saved", with no social provider and SSO off. After the next sign-out nobody could sign in, the administrator included. -- **Now.** A save after which email and password is off and no other way in is in force answers `400 SETTINGS_VALIDATION`. The field error sits at `email_password_enabled` (`code: invalid_value`, `constraint.lastSignInMethod: email_password`), and its message, in the caller's language (en, zh-CN, ja-JP, es-ES), says to keep or set up another sign-in method before or in that save. Nothing in that batch is written, and email and password stays on. -- **The whole save is judged.** `{ email_password_enabled: false, google_enabled: false }` (or with `google_client_secret` cleared) on a deployment signing in with email and password plus Google is refused, with a field error at each of the two keys. Google filled in (switch, client ID and secret) in the same save that turns email and password off is accepted, and so is the switch alone over a client ID and secret already stored. -- **What counts as another way in.** A social or OIDC provider in force whose client ID and client secret are both set; enterprise SSO switched on with at least one identity provider registered (the switch alone does not count); phone-number sign-in; a host sign-in handoff the host declared. `ssoOnlyMode` alone, magic link, passkeys, and a provider listed without its credentials do not count. -- **The other direction.** While email and password is off, `google_enabled: false`, or clearing `google_client_id` / `google_client_secret` when no environment variable or stack config supplies it, is refused (`constraint.lastSignInMethod: google`) if Google is the last way in. -- **Already locked out.** Set `OS_AUTH_EMAIL_PASSWORD_ENABLED=true` and restart: the environment override wins over the stored `false`, and email and password sign-in comes back. Then set up another method before turning it off again. -- **For a write predicate's author (`@objectstack/service-settings`).** `SettingsWritePredicateInput.batch` holds every key the write sets in the namespace, the asked key among them, with the value it would store (`null` clears). `setMany` always passes it, frozen. A rule over several keys judges the state after the whole write from it instead of one key against the rest as they stand. Existing predicates need no change. diff --git a/.changeset/30203-allowed-ip-ranges-live.md b/.changeset/30203-allowed-ip-ranges-live.md deleted file mode 100644 index 3773b73aaa0..00000000000 --- a/.changeset/30203-allowed-ip-ranges-live.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/plugin-auth': patch -'@objectstack/service-settings': patch ---- - -fix(plugin-auth)!: clearing the allowed IP ranges applies at once, and a console save that leaves out the saving address is refused - -Clause-②: yes (narrowing) — a settings console save (`PUT /api/settings/auth`) of an `allowed_ip_ranges` list none of whose entries matches the saving request's own address is refused, where it was stored before; and `@objectstack/service-settings` widens its published surface: three new exported types (`SettingsWriteOptions`, `SettingsRequestTransport`, `SettingsWritePredicateOptions`), `setMany`'s fourth parameter accepts `SettingsWriteOptions`, `registerWritePredicate` takes an optional fourth `options`, and `SettingsWritePredicateInput` gains an optional `transport`. -Metadata-protocol: none -Skills-docs: none — `skills/**` teaches no `allowed_ip_ranges`; `content/docs/permissions/authentication.mdx` describes the setting and does not yet state the save refusal or the recovery, which ship here in the refusal text and this entry (the page is outside this change's surface and is named for a follow-up). - - - -**BREAKING** for an administrator who saves, through the settings console, an allowed-IP list (ADR-0069 D5) that does not include the address they are connected from. - -- **Clearing applies at once.** The console sends an emptied field as `null`, which resolves with source `default`, and the settings binding used to skip a default-sourced `allowed_ip_ranges`: the last stored list kept refusing sign-in (`403 IP_NOT_ALLOWED`) until a restart while the field showed empty. A cleared or restored-default value now hands the gate back to the deployment's own declaration in the same pass: no list when it declared none, the `allowedIpRanges` the plugin options or an `auth:configure` handler declared otherwise. -- **The save is refused.** A list no entry of which matches the saving request's address is refused with `400 SETTINGS_VALIDATION`, `invalid_value` at `allowed_ip_ranges`, the address in `constraint.clientAddress`, and a message in the caller's language that names it. The address is the one the gate judges at sign-in: the transport's peer address, or the forwarded one when the stack declares `server.trustProxy`. When the server cannot establish the address, every non-empty list is refused, because the gate would refuse that request too. Clearing the list is never refused, and an in-process write (boot, seed, migration) carries no request and is not judged. -- **Recovery when already locked out.** Start the server with `OS_AUTH_ALLOWED_IP_RANGES` set to an empty value. The settings env override wins over the stored list and opens the gate; remove it once the list is fixed. -- **`@objectstack/service-settings`.** `setMany`'s options (`SettingsWriteOptions`) and a write predicate's input gain an optional `transport` (`SettingsRequestTransport`: the transport's peer address and request headers), which the settings routes set on a save so a write predicate can judge the change against the caller's own connection. A predicate sees only the headers it names at registration (`registerWritePredicate(…, { requestHeaders })`, `SettingsWritePredicateOptions`), so a credential header never reaches one that did not ask for it. The transport identifies and authorizes nothing and is never stored. -- **Who moves.** An administrator adds their own address or range to the list, or saves it from an address inside it. diff --git a/.changeset/30204-feature-flags-namespace-removed.md b/.changeset/30204-feature-flags-namespace-removed.md deleted file mode 100644 index b2e94e5be60..00000000000 --- a/.changeset/30204-feature-flags-namespace-removed.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/service-settings': patch -'@objectstack/platform-objects': patch ---- - -fix(service-settings)!: Setup no longer offers a Feature Flags page whose four toggles changed nothing (#30204) - -Clause-②: no (narrowing) — removes the four settable keys of the `feature_flags` settings namespace (`ai_enabled`, `kanban_swimlanes`, `realtime_cursors`, `inline_comments`), the namespace itself (left empty by their removal), its Setup entry and the `featureFlagsSettingsManifest` export. Nothing is widened. -Metadata-protocol: none - - - -**BREAKING** for an administrator who saved one of the four Feature Flags toggles, and for a host that imports `featureFlagsSettingsManifest`. - -- **Why.** No code read any of the four keys, so turning one on changed nothing (ADR-0049 enforce-or-remove). A flag comes back with the feature that reads it. -- **Removed keys.** `feature_flags.ai_enabled`, `feature_flags.kanban_swimlanes`, `feature_flags.realtime_cursors` and `feature_flags.inline_comments`: removed; each had no effect. The in-app AI panel is decided elsewhere and is unchanged. -- **The namespace.** `feature_flags` is no longer registered, so `GET` and `PUT /api/settings/feature_flags` answer `404 UNKNOWN_NAMESPACE`, and `SettingsService.getManifest('feature_flags')` throws `UnknownNamespaceError` (`SETTINGS_UNKNOWN_NAMESPACE`). Setup's Configuration group no longer lists Feature Flags (`@objectstack/platform-objects`, entry `nav_settings_feature_flags` and its four locale labels). -- **Stored values.** A `sys_setting` row saved under `feature_flags` and an `OS_FEATURE_FLAGS_*` environment variable are left in place and become inert: nothing registers the namespace, so nothing resolves them. They are not cleaned up; delete them at will. - -The one-line fix for a host: - -- FROM `featureFlagsSettingsManifest` TO nothing: drop it from the host's `SettingsServicePlugin({ manifests })` list; `builtinSettingsManifests` no longer carries it. diff --git a/.changeset/30205-banned-user-work-handover.md b/.changeset/30205-banned-user-work-handover.md deleted file mode 100644 index 5a59061899d..00000000000 --- a/.changeset/30205-banned-user-work-handover.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/spec': patch -'@objectstack/core': patch -'@objectstack/objectql': patch -'@objectstack/rest': patch -'@objectstack/client': patch ---- - -feat(objectql,rest)!: a new assignment to a banned user is refused, and a banned user's records can be counted and handed over in bulk (#30205) - -Clause-②: yes (narrowing) — the published `FieldErrorCode` enum gains the member `reference_user_banned`. A non-system create or update that sets a user field (the owner field, a `user` field, or a `lookup` to `sys_user`) to a user whose ban is in force is refused `400 VALIDATION_FAILED` with that field code, a write that answered `201` / `200` before. A value the row already stores is not judged, so a record that already holds the banned user stays editable. The two new routes and SDK methods are additions. -Metadata-protocol: none -Skills-docs: no move in `skills/**`, which teaches no user-assignment verdict; `content/docs/permissions/administrator-guide.mdx` gains "Handing over a banned user's work" and its offboarding quick path points at it. - - - -**BREAKING** accept-set narrowing at every data write door (`POST` / `PATCH /api/v1/data/OBJECT`, batch, import, and a flow's elevated record write, which keeps the reference check): banning a user signs them out, but the records they own and are assigned stay where they were, and new work could still be handed to them. - -- **Before.** The Console's person picker hid a banned user, but `POST /api/v1/data/OBJECT` with `assignee` set to that user answered `201`, so new work landed on someone who cannot sign in. -- **Now.** The write-time reference check also asks whether a referenced user's ban is in force (`banned` is true and `ban_expires` is empty or still ahead) and refuses such a value with `reference_user_banned`, beside `reference_not_found`. It is read the way the existence probe reads, so a caller who cannot read the user object is refused the same way. A ban whose expiry has passed is not in force. -- **Unchanged.** A value every row of the write already stores (an edit form carries the whole record back), a system write the reference check already exempts (seed, install, restore), and a missing user (still `reference_not_found`). -- **New: the count.** `GET /api/v1/security/users/ID/assigned-work` (`client.security.assignedWork.count`) answers, per object and user field, how many records hold the user, counted under the caller's own context. Platform tables (`sys_*`), `managedBy` objects and `readonly` / `system` fields such as `created_by` are not counted; objects the caller cannot read are listed in `unreadable`; at most 200 fields are counted (`truncated`). -- **New: the hand-over.** `POST /api/v1/security/users/ID/assigned-work/transfer` (`client.security.assignedWork.transfer`) with `{ toUserId, objects? }` moves up to 500 records per call to an active user, each as an ordinary update under the caller's context, and answers per field what moved and what failed, plus how many remain. A banned, unknown or identical `toUserId` is refused before any write. Multi-valued user fields are counted but not transferred. Neither route touches the ban. -- **`@objectstack/spec`.** `FieldErrorCode` gains `reference_user_banned`, with its message in `en`, `zh-CN`, `ja-JP` and `es-ES`. - -**For a caller.** A write that names a banned user in a user field names an active one instead; the refusal names the field and the user. No stored metadata or row changes, so there is nothing for `objectstack migrate meta` to rewrite. An integration that reassigns work after a ban can call the transfer route and repeat it while `remaining` is above 0. diff --git a/.changeset/30208-setup-admin-wording.md b/.changeset/30208-setup-admin-wording.md deleted file mode 100644 index 9f72fc60ed1..00000000000 --- a/.changeset/30208-setup-admin-wording.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(ui): Setup's everyone note and header tags speak to an administrator - -Clause-②: no -Metadata-protocol: none - -- **Assigned users of a permission set:** a set the everyone position distributes now says 「所有登录成员都持有此权限集。」 / "Every signed-in member holds this set." It no longer names the everyone anchor. -- **Setup list header tags:** three tags now say what they mean for the administrator reading the page, in all ten language packs. Admin config becomes "Configured here" (zh 「在此配置」), Platform schema becomes "Built in · editable" (zh 「平台预置 · 可编辑」), and Identity becomes "Via identity service" (zh 「经身份服务写入」). The tooltip text and the rule for when a tag is drawn do not change. diff --git a/.changeset/30209-audit-log-default-view.md b/.changeset/30209-audit-log-default-view.md deleted file mode 100644 index f8c1b8a99b5..00000000000 --- a/.changeset/30209-audit-log-default-view.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/plugin-audit': patch -'@objectstack/platform-objects': patch ---- - -fix(plugin-audit,platform-objects): the audit log's default list shows what people did, not the rows the platform writes beside it; the audit objects read in Chinese - -Clause-②: no -Metadata-protocol: none - -Setup's 审计日志 opens the first `sys_audit_log` list view, `recent`. One sign-in used to put two rows there (the `login` row and an `update` of the person's `sys_user` stamping `last_login_at` / `last_login_ip`), the first sign-in through the console a third (the token-signing key in `sys_jwks`), and one settings change three or four (the `config_change` row, the stored setting, a secret's ciphertext, and the settings trail's own entry). - -- The ledger writer now marks the sign-in stamp row with `metadata: {"event":"auth.last_login_stamp"}`, the way `config_change` rows carry `settings.set`. Only an update of `sys_user` whose changes are the stamp's fields alone is marked; an administrator's edit of a user is not. The row is still written. -- `recent` leaves out the marked row, the rows on `sys_secret`, `sys_setting_audit`, `sys_platform_setting_audit`, `sys_jwks`, `sys_migration` and `sys_migration_journal`, and the rows on `sys_setting` / `sys_platform_setting` other than `config_change`. Nothing is removed from storage, and `all_events` (全部) still lists every row. -- `sys_platform_setting` and `sys_platform_setting_audit` were in no translation bundle, so they read English in every locale; they now ship in `@objectstack/platform-objects`' bundles with zh-CN translations (ja-JP and es-ES carry the English fill). `sys_comment_reaction`'s zh-CN bundle is translated. diff --git a/.changeset/30209-setup-record-names-not-ids.md b/.changeset/30209-setup-record-names-not-ids.md deleted file mode 100644 index 770a6086e02..00000000000 --- a/.changeset/30209-setup-record-names-not-ids.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/fields': patch -'@object-ui/plugin-detail': patch -'@object-ui/plugin-grid': patch ---- - -fix(ui): Setup record pages name a sharing rule's recipient and read its condition in words, and the audit list names a sign-in row's session by its user - -Clause-②: no -Metadata-protocol: none - -- **`@object-ui/plugin-detail`.** The record page's highlights strip and details body hand each read face the record as `dependentValues`, as a list grid hands its row. A `recipient-picker` value is an id that means something only beside its `recipient_type`, so a sharing rule's page read the stored user id under 接收方 where its list read the user's name; it now reads the name. -- **`@object-ui/fields`.** `widget: 'filter-condition'` has a read face of its own: a list or detail cell draws the criteria through the widget's read-only display, which says it in the object's field and option labels ("Priority equals Urgent"; an OR group joined by its word, a boolean as a word). A criteria the builder cannot represent still reads as the stored JSON, and an empty one is the empty cell. A read-only widget no longer waits for its `object_name` to say anything: without it, the fields keep their api names. -- **`@object-ui/plugin-grid`.** A pointer cell (`referenceVia`) reads the target record with its name field expanded when that field is a reference the viewer may read, so a record named by a lookup reads as the referenced record's name: the audit list's sign-in rows name the session by its user instead of printing the user id. Any other name field is read as before, with no `$expand`. diff --git a/.changeset/30210-business-unit-enterprise-requirement.md b/.changeset/30210-business-unit-enterprise-requirement.md deleted file mode 100644 index 111495f516b..00000000000 --- a/.changeset/30210-business-unit-enterprise-requirement.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/platform-objects': patch -'@objectstack/lint': patch ---- - -fix: the Business Unit page and the `security-private-no-readscope` hint say what needs the enterprise edition - -Clause-②: no -Metadata-protocol: none - -- **Business Unit page (`sys_business_unit` description, en and zh-CN).** It used to say units are "used to scope data and reports". It now says what a unit does in the open edition: sharing rules can share records with a unit, or a unit and the units under it, and members get that access and lose it when they leave. It also says that letting a permission set read every record of a unit (read scope `unit`) needs the enterprise edition. That matches the compile refusal, which is unchanged. -- **Business unit Timezone help (en and zh-CN).** It used to say date boundaries are computed in this zone. Nothing reads the column, so the help now says it changes no date calculation and that Settings → Localization → Default timezone is the one in effect, as the organization's Timezone help already does. -- **`security-private-no-readscope` hint (`os validate`, `os lint`).** The hint now offers only the scopes the stack can compile. When `requires` lists `hierarchy-security`, nothing changes: it offers `own_and_reports`, `unit`, `unit_and_below` and `org`. Without it, the hint offers `org` (or widening the object's `sharingModel`), and says that the three hierarchy scopes need `requires: ['hierarchy-security']`, which `@objectstack/security-enterprise` provides. The rule still fires on the same grants at the same severity. diff --git a/.changeset/30210-organization-parent-sort-multi-org-help.md b/.changeset/30210-organization-parent-sort-multi-org-help.md deleted file mode 100644 index a08c248a3eb..00000000000 --- a/.changeset/30210-organization-parent-sort-multi-org-help.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/platform-objects': patch ---- - -fix(platform-objects): Edit organization's Parent Organization and Sort Order help says they matter only where the deployment runs several organizations - -Clause-②: no -Metadata-protocol: none - -On a deployment with one organization, Edit organization still shows 上级组织 / Parent Organization and 排序 / Sort Order, and nothing told the administrator they do nothing there. Their help text, in the object declaration and in the `en` / `zh-CN` packs, now ends with "Meaningful only where the deployment runs several organizations." (「仅在部署运行多个组织时有意义。」). No field, permission or behaviour changes; the fields stay visible. diff --git a/.changeset/30212-impersonation-landing-agent-sets.md b/.changeset/30212-impersonation-landing-agent-sets.md deleted file mode 100644 index 4fdded9b42a..00000000000 --- a/.changeset/30212-impersonation-landing-agent-sets.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/fields': patch ---- - -fix(console): starting an impersonation lands on home, and a person's permission-set picker no longer offers the MCP agent sets - -Clause-②: no -Metadata-protocol: none - -- **Impersonation landing.** An administrator who starts an impersonation from a Setup page used to stay on that page, which the impersonated user usually cannot open, so the console showed "You don't have access to this app". The impersonation banner now navigates to `/` the moment a session the console held as an ordinary one becomes impersonated, and the console resolves the landing for the user it now holds. A console that boots into an impersonated session (a reload, a new tab, a deep link) stays on the URL it was opened at, and ending an impersonation does not move the page. The navigation goes through the host navigate the console supplies, so a deployment's basename applies. -- **Agent sets in the person picker.** The `permission-ref` field (the permission set on a person's grant, `sys_user_permission_set.permission_set`) no longer offers the three MCP agent ceiling sets (`mcp_agent_data_read`, `mcp_agent_data_write`, `mcp_agent_restricted`, the spec's `MCP_AGENT_PERMISSION_SETS`). Those are an AI assistant's own grants, which the runtime puts on the agent principal directly. A grant that already names one still shows it as the row's value, by its label beside its name. `position-ref` is unchanged. diff --git a/.changeset/30212-sender-name-follows-product-name.md b/.changeset/30212-sender-name-follows-product-name.md deleted file mode 100644 index 4101a04a9dc..00000000000 --- a/.changeset/30212-sender-name-follows-product-name.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/plugin-email': patch -'@objectstack/service-settings': patch ---- - -fix(plugin-email,service-settings): mail whose sender has no From name goes out under the product name, not "ObjectStack" - -Clause-②: no -Metadata-protocol: none - -- **Now.** A default sender with no display name of its own goes out under the product name the auth mails' subject and body already use: the Setup → Branding product name when an administrator set one, else the deployment's name (`OS_APP_NAME`, then `config.email.appName`, `config.email.defaultTemplateContext.appName` and the top-level `appName`), else `ObjectStack`. A Branding save renames the sender on the next mail, with no restart. The "Send test email" button sends under the same name. -- **Which senders.** The sender saved in Setup → Mail with the From name left empty, and a sender declared at boot without a name (`OS_EMAIL_FROM=no-reply@acme.com`, or `config.email.defaultFrom` with an `address` alone). Before, the first went out as `ObjectStack <…>` and the second with no display name. -- **Unchanged.** A From name somebody set always wins: one saved in Setup → Mail, or one declared at boot (`OS_EMAIL_FROM="Acme "`, `config.email.defaultFrom.name`). A sender a send names itself, and a template's own sender, are never renamed. Under a walled tenancy posture, where Setup → Branding is not offered, the deployment's name applies. -- **`@objectstack/service-settings`.** Setup → Mail → From name no longer has the default `ObjectStack`: an unsaved field reads empty, and `GET /api/settings/mail` answers `from_name` as `value: null`, `source: 'default'`. The field's help says that an empty value uses the product name. A From name saved earlier is kept and still wins; clear the field to follow the product name. diff --git a/.changeset/30215-uninstall-forgets-engine-manifest.md b/.changeset/30215-uninstall-forgets-engine-manifest.md deleted file mode 100644 index 52d422d7308..00000000000 --- a/.changeset/30215-uninstall-forgets-engine-manifest.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -fix(objectql): an uninstalled package no longer reads as read-only until a restart - -Clause-②: yes (widening) — `@objectstack/objectql`'s exported `SchemaRegistry` gains the public method `onPackageUninstalled(listener)`, which returns its unsubscribe. Nothing is refused that was accepted before. -Metadata-protocol: none - -An administrator who uninstalled a package through `DELETE /api/v1/marketplace/install-local/:id` got a 200 "withdrawn from the running kernel", but on the same instance the package id still read as a booted code package until the server restarted. Uninstalling a `.custom` pair that an older runtime had installed as code, as the pair install now prescribes, left `POST /api/v1/packages//ensure-customization` answering `created: true` with `writable: false`, so nothing could be customized into the new pair. The engine's manifest map, which the ADR-0070 writability check reads first, kept the id after the registry had withdrawn the package. - -`SchemaRegistry.uninstallPackage` now announces each completed uninstall to `onPackageUninstalled` listeners, after the package record is gone (never for a refused uninstall), and the engine drops its manifest entry there, so the id is released on whichever door withdrew the package, not per door. A reinstall of the same id registers it again as before. A listener that throws does not turn the completed uninstall into a refusal; it is logged at `error`. diff --git a/.changeset/30217-install-ledger-follows-os-home.md b/.changeset/30217-install-ledger-follows-os-home.md deleted file mode 100644 index abb3136d5a6..00000000000 --- a/.changeset/30217-install-ledger-follows-os-home.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/cloud-connection': patch -'@objectstack/cli': patch ---- - -fix(cloud-connection,cli): the install-local ledger follows `OS_HOME`, so `os dev --fresh` and a second instance under another `OS_HOME` start without an earlier run's installs - -Clause-②: yes (widening) -Metadata-protocol: none - -- **Now.** With no `storageDir`, the install-local ledger (`LocalManifestSource`, and so `MarketplaceInstallLocalPlugin`) lives at `$OS_HOME/installed-packages` when `OS_HOME` is set, and at `CWD/.objectstack/installed-packages` when it is not. Before, it was always the working directory, so `os dev --fresh` (a fresh `OS_HOME` and database) rehydrated a package an earlier run installed, and so did a second instance under another `OS_HOME`. -- **Unchanged.** `os dev` without `OS_HOME` keeps the ledger in the project, beside its database. `os start` in project mode sets `OS_HOME` to `CWD/.objectstack`, which is the same directory as before. An explicit `storageDir` is used as given. -- **Existing installs.** When the ledger moved (an `OS_HOME` other than `CWD/.objectstack`) and its new directory does not exist yet, the boot copies the `.json` files of the working-directory ledger into it once, and logs a warning naming both directories. The old files are left in place and are not read again; remove them. A copy that fails is logged as an error, and the next boot tries again. Under `os dev --fresh` nothing is copied. -- **`os doctor`** reads the same directory the runtime does, `OS_HOME` included. -- **The widening.** `@objectstack/cloud-connection` exports `resolveInstalledPackagesDir({ env?, cwd? })` (returns `{ dir, legacyDir }`), `INSTALLED_PACKAGES_HOME_SUBDIR`, and `LocalManifestSource.legacyDir` / `adoptLegacyEntries()`. `os doctor` takes the ledger directory from `resolveInstalledPackagesDir`; a `@objectstack/cloud-connection` without it is reported as a reader that does not declare the ledger directory. Align the two packages' versions. diff --git a/.changeset/30218-environment-scoped-objectql-provider.md b/.changeset/30218-environment-scoped-objectql-provider.md deleted file mode 100644 index 6270cba1f65..00000000000 --- a/.changeset/30218-environment-scoped-objectql-provider.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): the transactional `POST /batch`, the `/meta` object-state and published reads, and the meta read gate's disabled-package check use the request's environment's data engine when one REST plugin fronts many environments - -Clause-②: no -Metadata-protocol: none - -- **What was wrong.** On a host that mounts one `RestApiPlugin` in front of many environment kernels (the hosted runtime shape), the `objectql` provider read the HOST kernel's engine whatever environment it was asked for, and the unscoped (hostname-routed) mount asked it for no environment at all. `POST /batch` therefore opened its transaction on the host's engine and sent its update and delete ops through it, while its create ops went to the environment's own protocol, joined to the host's transaction. `GET /meta/object/:name/state/:field` judged the object by the host engine's registry, and `GET /meta/:type/:name/published` and the meta read gate asked the host engine's registry whether an environment's package is disabled. The request's execution context already read the environment's engine, so one request used two kernels. -- **What changes.** Given an environment id and a registered `kernel-manager`, the `objectql` provider now reads `objectql` on that environment's kernel, the same environment-scoped seam the approvals, email, sharing, security and other providers use. Every route that reads it passes the id the shared request→environment resolution answered: the `/environments/:environmentId` param on the scoped mount, else the host's `kernel-resolver` (or the hostname / `X-Environment-Id` chain) on the unscoped one. The host's engine is no longer a fallback: an environment whose kernel carries no engine answers `POST /batch` with `501 NOT_IMPLEMENTED`. -- **Unchanged.** A single-kernel deployment (no `kernel-manager`) reads the engine on the plugin's own kernel exactly as before, through the same factory-aware lookup: a factory that fails to build is still a loud `503`, and only an engine that was never registered reads as absent. The reserved `platform` id and a request the resolver places in no environment also read the plugin's own kernel. -- **No migration.** No configuration, route, response shape or export changes. diff --git a/.changeset/30220-requiredwhen-formpage-kanban.md b/.changeset/30220-requiredwhen-formpage-kanban.md deleted file mode 100644 index 027dd204bd9..00000000000 --- a/.changeset/30220-requiredwhen-formpage-kanban.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/console': patch -'@object-ui/plugin-kanban': patch -'@object-ui/plugin-form': patch ---- - -fix(ui): the console form page Edit, the kanban drop prompt and the Edit wizard judge a field's `requiredWhen` as the engine does, a transition gate against the stored record - -Clause-②: no -Metadata-protocol: none - -- **`@object-ui/console` form page (`/forms/:name?recordId=`):** an Edit no longer refuses a record that already sits inside a `requiredWhen` gate without the value (for example a contract activated before its signed-copy rule existed). The field shows no required marker there, and an unrelated edit saves, as REST `PATCH` already accepts. Still refused: the edit that moves a record into the gate without the value, and clearing the value of a record that held it. A create form enforces as before. -- **`@object-ui/plugin-kanban` drop prompt:** dropping a card no longer asks for a field the engine lets rest. A record already inside the gate without the value moves between columns with no dialog; a drop that moves a compliant record into the gate still asks for the value first. -- **`@object-ui/plugin-form` Edit wizard:** the final submit's cross-step check no longer refuses a record that already sits inside a `requiredWhen` gate without the value; it asks `requiredWhenRestsOnStoredRecord` about the record as first read. The record reaches only that stored-record check, so a `visibleWhen` reading `previous` is still unevaluable at that check, as before. Moving a compliant record into the gate is still refused, and a Create wizard enforces as before. - -All three read the stored-record verdict `resolveFieldRuleState` already returns (`requiredWhenAtRest`, or its exported check `requiredWhenRestsOnStoredRecord`); no second rule. diff --git a/.changeset/30223-hitl-continuation-not-user-bubble.md b/.changeset/30223-hitl-continuation-not-user-bubble.md deleted file mode 100644 index eb9d3fa0ec6..00000000000 --- a/.changeset/30223-hitl-continuation-not-user-bubble.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/plugin-chatbot': patch -'@object-ui/app-shell': patch ---- - -fix(plugin-chatbot,app-shell): after 确认 or 取消 on the chat's confirm card, the follow-up sent to the model no longer shows as the person's own message - -Clause-②: no -Metadata-protocol: none - -After the operator confirmed or cancelled an action on the chat's confirm card, the chat sent the model a short follow-up («[HITL pa_…] The operator approved the `update_record` call … Acknowledge the outcome …») through the ordinary user-message channel, so the person saw it as their own bubble, after a reload too, and the conversation list showed it as the conversation's preview. The model still receives that follow-up unchanged. What changed is who reads it as the person's words: the chat transcript draws no bubble for it (live, after a reload, and on a shared transcript), the conversation list previews the person's last ask instead, and the conversation-language probe no longer reads it as the person's latest turn, which turned a Chinese conversation's outbound text English when the action's result carried no Chinese. The follow-up is recognised by its leading `[HITL ]` tag, the one part of a stored message that survives a reload; `@object-ui/plugin-chatbot` exports the reader as `isHitlContinuation` (also from `./core`). An ordinary message is sent, drawn and previewed as before. diff --git a/.changeset/30228-queue-reclaim-expired-lease.md b/.changeset/30228-queue-reclaim-expired-lease.md deleted file mode 100644 index e9de72d922a..00000000000 --- a/.changeset/30228-queue-reclaim-expired-lease.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/service-queue': patch ---- - -fix(service-queue): a job whose worker died mid-handler is reclaimed once its lease runs out, instead of staying `running` forever - -Clause-②: no — restores the documented `leaseMs` reclaim; no accept set or export widens -Metadata-protocol: none - -`DbQueueAdapterOptions.leaseMs` documented a lease "before another worker may reclaim", but the claim read only `pending` rows, so a worker that crashed, was OOM-killed or was redeployed mid-handler left its `sys_job_queue` row `running` for good: never retried, never dead-lettered. - -- **Reclaim.** A poll tick reads the table's `running` rows (one read for every queue, at most once per lease window) and returns each whose `locked_until` has passed to the retry path. The lost run counts as an attempt: the row goes back to `pending` on its backoff, or to `dlq` once `maxAttempts` is spent, with `last_error` saying the lease expired. The write is a compare-and-set on the expired lease, so a holder that renewed in the meantime keeps its run. -- **Renewal.** While a handler runs, its worker moves its lease a full window ahead every third of `leaseMs`, so a slow but live handler is never reclaimed and run twice. A handler must not block the event loop for most of a lease window. -- **Sole writer.** With `soleWriter` no other process claims rows, so the reclaim reads once per boot (finding what a crashed previous process left behind) and again only when one of this process's own claims was left unsettled by a failed write and its lease runs out. `nextDueAt` folds in those leases and the earliest live lease the last read saw, at no read of its own, so the loop wakes for them instead of at the idle ceiling. -- **Cost.** Without `soleWriter`, an idle queue pays one more indexed read per lease window (default 30 s): an idle hour at the defaults goes from 125 to 246 reads. With `soleWriter`, one more read per boot: a fresh kernel's first minute goes from 2 to 3 reads, and an idle hour carries no other. diff --git a/.changeset/30229-adr0146-override-types.md b/.changeset/30229-adr0146-override-types.md deleted file mode 100644 index 5233153c8f2..00000000000 --- a/.changeset/30229-adr0146-override-types.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): the platform skill names every type a pair's declared override is served for, and that removing the pair's item serves the base's again - -Clause-②: no -Metadata-protocol: none - -`skills/objectstack-platform/SKILL.md`, the "Customizing an installed package (ADR-0146)" paragraph: a `.custom` pair's declared override is served for a view, dashboard, report, email_template, translation, flow, page, app, dataset, doc, book or mapping (the paragraph named six), and removing the pair's item (`DELETE …?package=hotcrm.custom`) serves the base's document again (ADR-0146's revert, which the paragraph did not say). The package's evals gain a dataset-and-page override case and a removal case. diff --git a/.changeset/30235-recycle-bin-spec-surface.md b/.changeset/30235-recycle-bin-spec-surface.md deleted file mode 100644 index e7b89aba5dc..00000000000 --- a/.changeset/30235-recycle-bin-spec-surface.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec): declare the recycle bin's spec surface (ADR-0148): the `sys_recycle_bin` entry contract, the host retention resolver hook, the snapshot shape, the restore refusal codes, the `restored` event name and the two grant keys - -Clause-②: yes (widening) -Metadata-protocol: added - -A protocol addition: new spellings with their readers named, and no tombstoned key returns. Declared, not yet read. Every delete is still a physical delete, and nothing writes or reads the recycle bin until the engine slice lands it (snapshot on delete, restore, purge, empty, the TTL reap, the permission mapping and the REST doors); the Console's admin page and its undo toast follow it. This release gives both something to build on. - -- **`@objectstack/spec` (`@objectstack/spec/data`):** `RECYCLE_BIN_OBJECT` (`'sys_recycle_bin'`) and `RECYCLE_BIN_ENTRY_FIELDS`, the entry's field list the object is built from: `object_name`, `record_id`, `record_count`, `snapshot`, `deleted_by`, `deleted_at`, `retention_days` and `purge_after` (the deadline stamped at delete time), all read-only, with `RecycleBinEntrySchema` as the entry's value shape. The object itself is registered with the engine slice, so no table and no platform-object name exist until a writer does. `RECYCLE_BIN_RETENTION_DAYS_ENV` (`'OS_RECYCLE_BIN_RETENTION_DAYS'`) and `RECYCLE_BIN_DEFAULT_RETENTION_DAYS` (30). `RecycleBinRetentionResolver`, the host hook's contract: `(token: string | undefined) => number | undefined`, called with the same opaque token `resolveFeatures` receives, answering a positive whole number of days or `undefined` to keep the default. `RecycleBinRetentionDaysSchema` and `RecycleBinRetentionDaysEnvSchema` hold the answer and the variable to that rule, and `recycleBinRetentionRefusal(source, value)` is the refusal text: zero or anything that is not a positive whole number is a configuration defect, never "purge now" or "keep forever". The framework knows days, never plans. `RecycleBinSnapshotSchema` (with `RecycleBinSnapshotRowSchema` and `RecycleBinClearedReferenceSchema`): the cascade set an entry holds, the master first and its details in cascade order, secret-channel refs left out, plus the `set_null` references the delete cleared. `RECYCLE_BIN_RESTORE_REFUSAL_CODES` and `RECYCLE_BIN_RESTORE_REFUSAL_STATUS` (409). `RECORD_RESTORED_EVENT` (`'data.record.restored'`): a restore emits it, never a create; `DataEventType` and `RealtimeEventType` list only emitted events, so the name joins them in the change that emits it. `RECYCLE_BIN_GRANT_KEYS`. -- **`@objectstack/spec`, error-code ledger:** `RESTORE_UNIQUE_VALUE_TAKEN`, `RESTORE_PARENT_PURGED` and `RESTORE_PACKAGE_UNINSTALLED`, registered under `@objectstack/objectql`, which answers them. -- **`@objectstack/spec`, object permissions:** two new optional keys, `allowRestoreOwnDeleted` (restore the entries of this object the caller deleted) and `manageRecycleBin` (list every entry of this object, restore any, purge; emptying purges every entry the holder manages). No default, so no parsed entry grows a key. Not read yet: a dispatched `restore` or `purge` stays denied by the permission evaluator's fail-closed backstop whatever they say, and their schema descriptions say they are not read yet. The retired `allowRestore` / `allowPurge` stay tombstoned. diff --git a/.changeset/30238-studio-restore-rereads-marks.md b/.changeset/30238-studio-restore-rereads-marks.md deleted file mode 100644 index 948ca10f191..00000000000 --- a/.changeset/30238-studio-restore-rereads-marks.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(studio): after Restore package original, the rail's customized mark and the "only customizations" count read the restored item without a reload (#30238) - -Clause-②: no -Metadata-protocol: none - -- **Before.** On an installed (read-only) app, restoring a customized dashboard, report, view or page in Studio brought the shipped item back, but the Interfaces rail kept marking it customized and "only customizations" kept counting it until the page was reloaded. The surface read the base's customizations (`GET /api/v1/packages/:id/customizations`) again only after a publish, and a restore deletes the pair's row without publishing. -- **Now.** The surface reads them again on every metadata-refresh pulse, which a restore already sends (as do a publish and a copilot publish), so every pillar rail's marks and "only customizations" count follow the restore. A publish still reads once. The All metadata area's pages make their own read each time one opens, so they already showed the restored state. diff --git a/.changeset/30244-served-labels-console-header.md b/.changeset/30244-served-labels-console-header.md deleted file mode 100644 index 2be0e4fbe0c..00000000000 --- a/.changeset/30244-served-labels-console-header.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): the Console header, the app report index and the recent list draw a served report, page or dashboard label as served - -Clause-②: no -Metadata-protocol: none - -- **Now.** A report's crumb and a page's phone title in the header, a report's row in the app's report index, and a report, page or dashboard row in the recent-items list (and an unlabelled sidebar entry that opens a dashboard) draw the label the `/meta` read or the app bundle served as given. The server has already translated it, by its label order for an overridden item: a `.custom` override's or a published edit's own text is kept over the base package's translation. Before, the served label reached the client's catalog lookup only as its fallback, so the base package's translation of the old name won. -- **Unchanged.** An item the read did not serve, or served with no label, is still named by the catalog key under its name (`{ns}.reports..label`, `pages`, `dashboards`), then by its humanized name or machine name as before. An unedited item shows the same text as before, because the server serves the catalog's translation for it. A language switch reads the metadata again, so a served label is never left in the old language. diff --git a/.changeset/30250-fail-closed-date-and-sweep-run-fails-sentences.md b/.changeset/30250-fail-closed-date-and-sweep-run-fails-sentences.md deleted file mode 100644 index b20bff02cd9..00000000000 --- a/.changeset/30250-fail-closed-date-and-sweep-run-fails-sentences.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/formula': patch -'@objectstack/lint': patch -'@objectstack/mcp': patch ---- - -fix(formula,lint,mcp): a fail-closed date verdict says the write is refused, and a time-relative sweep's run-fails verdict names no firing write - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@objectstack/formula`: three codes join `ExpressionRefusalCode`** (`EXPRESSION_REFUSAL_CODES`), with the params of the code they split from: - - `date-arithmetic-fail-closed` and `date-text-arithmetic-fail-closed`. On a site that fails closed (`faultOutcome: 'fail-closed'`: an `error` validation rule, a field `requiredWhen` / `readonlyWhen`, an option `visibleWhen`, a hook `condition`), date arithmetic used to be told "this faults at runtime, so the field silently evaluates to null". There the fault refuses the write instead, and the verdict now says so. A formula field and an advisory (`warning` / `info`) validation rule keep `date-arithmetic` / `date-text-arithmetic` and their sentence. A consumer keyed to the old codes on a fail-closed site re-keys to the new ones. - - `type-mismatch-sweep-run-fails`. A time-relative sweep of the typed object is graded `run-fails` like a record-triggered flow, but no write fires its run: the sweep starts one run per row it finds. Its condition's text-vs-number verdict used to say "the write that fired it still lands". Served where the new optional `ExprSchemaHint.timeRelativeSweep` is set beside `faultOutcome: 'run-fails'`, it now says the run for that row fails and nothing else happens. A record-triggered flow keeps `type-mismatch-run-fails` and its sentence. -- The grade is unchanged at every site. What is refused, warned or passed is unchanged. -- **`@objectstack/lint`**: the flow walk passes `timeRelativeSweep` on a `time_relative` flow. The nullable-comparison verdict, which also says "still lands", runs on record-triggered flows only, so it is unchanged. -- **`@objectstack/mcp`**: `validate_expression` with `site: 'flow_condition'` and `triggerKind: 'time_relative'` passes the same key, so its answer stays the gate's. diff --git a/.changeset/30251-ownership-queue-on-failed-read.md b/.changeset/30251-ownership-queue-on-failed-read.md deleted file mode 100644 index f1fab8c91ef..00000000000 --- a/.changeset/30251-ownership-queue-on-failed-read.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -feat(spec): `IOwnershipQueueService` lets a caller ask for a refusal on a failed member read (`onFailedRead`), the call-time signal ADR-0142 D2 asks each consumer to send - -Clause-②: yes (widening: an optional `onFailedRead` on `expandMembers`' options and an optional options argument on `isMember`; precedent PR #30015) -Metadata-protocol: none - -- **New type `OwnershipQueueFailedReadAnswer`** (`'nobody' | 'refuse'`, `@objectstack/spec/contracts`): what a membership read answers when a read behind the queue fails (`sys_queue`, `sys_queue_member`, `sys_team_member`, `sys_user_position`, `sys_member` through the position transition limb, the business-unit membership and tree). -- **`expandMembers(name, organizationId, { purpose?, onFailedRead? })`** and **`isMember(userId, name, organizationId, { onFailedRead? })`**. `'nobody'` is the default and the answer every caller had before: a failed read is logged at `warn` and contributes nobody. Under `'refuse'` a failed read rejects with a failure that names the read, never `[]`, a partial set or `false`; an empty queue is still `[]`. The docblock names who takes which: the access path (D3), the claim admission (D4) and the assignment pool (D5) take `'nobody'`; the approver slate (D6) and the sharing-recipient grant recompute (D7) take `'refuse'`. The key is independent of `purpose`. -- **Contract only.** Every existing call and implementation compiles unchanged and behaves as today; the producer in `@objectstack/plugin-sharing` and the D6/D7 callers that pass `'refuse'` land with #29604. diff --git a/.changeset/30255-clone-inherits-held-grants.md b/.changeset/30255-clone-inherits-held-grants.md deleted file mode 100644 index 3e209ce5f2c..00000000000 --- a/.changeset/30255-clone-inherits-held-grants.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch -'@objectstack/runtime': patch ---- - -fix(runtime, metadata-protocol): the permission-set clone door copies a platform set whose grants name an object this deployment does not install — `member_default` copies into its package's customization pair again on a boot without `@objectstack/plugin-audit` - -Clause-②: yes (widening) — `ObjectStackProtocolImplementation.saveMetaItem`'s request gains one optional key, `inheritedGrantObjects`, reachable from `@objectstack/metadata-protocol`'s `.` entry types -Metadata-protocol: none - -- **What failed:** cloning a set through `POST /api/v1/security/permission-sets/:name/clone` answered `422 INVALID_METADATA` when the source grants on an object no package registers here. The commonest source is the platform baseline `member_default`, which grants `sys_comment` and `sys_activity`. `@objectstack/plugin-audit` registers both objects, and it is an optional package: a deployment that does not install it could not clone `member_default` at all. -- **The rule it now follows:** the save door's unknown-grant refusal (#28990) already leaves alone a grant the set itself already holds, because that grant is not the current write's. A clone's grants are the source's, copied verbatim, so the same holds for a copy. The clone door now tells the save which grant keys the copy inherits (`inheritedGrantObjects`), and the refusal does not judge those. -- **Still refused:** an unknown object that the save does not say it inherits is this write's grant, and is refused exactly as before. That covers a `/meta` save, a re-save that adds a grant, and a draft's publish. No HTTP door forwards the new key. diff --git a/.changeset/30259-action-param-widget.md b/.changeset/30259-action-param-widget.md deleted file mode 100644 index ab5fe63cc03..00000000000 --- a/.changeset/30259-action-param-widget.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/platform-objects': minor ---- - -feat(spec): an action param can name the widget that draws it (`widget`, same key as on a field), so a param can ask for the permission-set picker (ADR-0131 D4) - -Clause-②: yes (widening) -Metadata-protocol: added - -- **`@objectstack/spec`: `ActionParamSchema.widget`**, an optional string with the same key, vocabulary and meaning as `FieldSchema.widget`. It names a registered field component, looked up as `field:` plus the name, to draw the param in the action dialog instead of the `type` default. `type` stays the value's type. The case it exists for is `widget: 'permission-ref'` on a `text` param: it lists the permission sets of the security catalog and submits the chosen machine name, the picker the grant column `sys_user_permission_set.permission_set` already uses. Like the field key it is an open string: an unregistered name is kept, and the dialog draws the `type` renderer instead. A non-string is refused. A param that does not declare the key parses exactly as before. Until now a param naming `widget` was refused as an unknown key. -- **Not read yet.** The console's param resolver does not carry the key to the dialog's widget until objectstack#30260 lands. Until then a param that declares it is drawn by its `type` renderer, and its value is submitted as before. The key's description says so, and the liveness ledger grades it `planned` with an author warning: `os validate` and `os lint` warn an author who writes it. -- **`@objectstack/platform-objects`: the action form's Params repeater offers the key** as a `Widget` column, labelled in en, zh-CN, ja-JP and es-ES. diff --git a/.changeset/30260-action-param-widget-reader.md b/.changeset/30260-action-param-widget-reader.md deleted file mode 100644 index fa7cdc5c48a..00000000000 --- a/.changeset/30260-action-param-widget-reader.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/core': minor -'@object-ui/fields': minor -'@object-ui/app-shell': minor -'@objectstack/spec': minor ---- - -feat(ui): an action param's `widget` draws the named widget in the action dialog — `widget: 'permission-ref'` on a `text` param is the permission-set picker - -Clause-②: yes (widening) — `@object-ui/core`'s exported `ActionParamDef` gains one optional member, `widget?: string`; `@object-ui/fields`' `RawActionParam` gains the same optional member. No accepted key, value or answer is taken away. -Metadata-protocol: semantic - -`ActionParamSchema.widget` (declared by `@objectstack/spec` with the same key, vocabulary and meaning as `FieldSchema.widget`) was accepted and stored but read by nothing: the action dialog drew every param with its `type` widget. The console now reads it: - -- `resolveActionParams` copies the param's `widget` onto the resolved param. A field-backed param (`{ field }`) that inherits the field's `type` also inherits the field's `widget`; a param that overrides `type` does not, because the field's widget was chosen for the field's value. The param's own `widget` always wins. -- `paramToField` draws the named widget when it is registered, in place of the `type` widget. A name nothing registers keeps the `type` widget (a `textarea` param stays a textarea), and a registered widget on a lookup param without a target is drawn instead of the plain record-id text box. -- So `{ name: 'permissionSet', type: 'text', widget: 'permission-ref' }` opens the permission-set picker: the sets are listed by label beside their machine name, and the machine name is submitted. The server still validates the value by `type`, so the request body is unchanged. - -The spec's liveness ledger marks `params.widget` live, and its `.describe()` no longer says the key is not read yet. diff --git a/.changeset/30261-packageless-overlays-view-container.md b/.changeset/30261-packageless-overlays-view-container.md deleted file mode 100644 index bc87e5b3ddb..00000000000 --- a/.changeset/30261-packageless-overlays-view-container.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor -'@objectstack/cli': patch ---- - -fix(metadata-protocol,cli): `os migrate packageless-overlays` lists a package-less view container `cannot_declare` instead of `movable`, and `--apply` no longer attempts (and fails) it on every run - -Clause-②: yes (widening) -Metadata-protocol: semantic - -- **`@objectstack/metadata-protocol`: the census gains the disposition `cannot_declare`** (`PackagelessOverlayDisposition`, reachable through the exported `PackagelessOverlayCensus`). A row lands there when the schema the save door judges its body by refuses an `overrides` key on it, so the declared save the move makes could never land. Of the five overlay types the ceremony moves, that is a view container (`list` / `listViews` / `form` / `formViews`): only a single view declares an override. Before, such a row was listed `movable` and every `--apply` reported it `failed` with `Unrecognized key(s) on this view container: overrides`, leaving the row where it was. Now `migratePackagelessOverlays` does not attempt it: no outcome, no failure, no pair ensured for it. -- **A view container's write onto a packaged name is accepted again.** `PUT /api/v1/meta/view/` with a container body (`list` / `listViews` / `form` / `formViews`) over a name one installed read-only package ships, naming no package or naming that base with `?package=`, and the matching `DELETE`, answered `403 NOT_OVERRIDABLE` with a fix (a declared save into the pair) that the container schema refuses. They now succeed, as they did before the ADR-0146 D11 closes; every other shape of the five overlay types is closed exactly as before. -- **The two closes read the same answer.** The package-less write close (`packagelessCloseSubject`) closes only what the ceremony moves, so a container's package-less save and delete stay open, as they were before the close: they are the one door such a row keeps. The close of a write naming the read-only base (`overlayPairFixAdmits`, the "the fix can be taken" predicate both closes ask) no longer names the pair as the fix for a container body, because that fix (a declared save into the pair) cannot be taken; that write keeps the answer it had before the close. -- **`@objectstack/cli`: `os migrate packageless-overlays`** prints the way out for a `cannot_declare` row under "left as they are": save each of the container's views into the pair as a single view named `.` (one the base ships declaring `overrides: { package, basedOn }`, a new one declaring nothing), then delete the container row with a delete naming no package (naming the base, for a row bound to it). In `--json`, the row carries `disposition: "cannot_declare"` and `outcome: "left"`, and `--apply` exits 0 for it. diff --git a/.changeset/30267-view-panel-zh-composite-rows.md b/.changeset/30267-view-panel-zh-composite-rows.md deleted file mode 100644 index 652f3c97234..00000000000 --- a/.changeset/30267-view-panel-zh-composite-rows.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/spec': patch -'@objectstack/platform-objects': patch -'@object-ui/app-shell': patch ---- - -fix: on a zh-CN console every row of the view properties panel's collapsed sections (表格选项 / 分页 / 终端用户控制 / 导航与共享) reads in Chinese - -Clause-②: no -Metadata-protocol: none - -- `viewForm` (`@objectstack/spec/ui`) now spells out the children of its `selection`, `pagination`, `userFilters`, `appearance`, `userActions`, `navigation` and `sharing` composites, and gives the rows that had none a hint. Each declared hint is the schema description the panel already showed, so an English console reads the same text; `os i18n extract` now emits a `metadataForms.view.fields['.']` key per row. -- `userFilters.fields` is enumerated as an inline-table repeater, and `UserFilterFieldSchema`'s six properties carry a `.meta({ title })` (Field / Label / Type / Options / Show Count / Default Values) — the English column heads, unchanged. -- The panel no longer offers `navigation.view` and `sharing.type`: both are `retiredKey()` tombstones that the schema-derived child list used to render as editable rows. A stored value is untouched by a panel save (the composite editor spreads the block it was given). -- The platform metadata-forms catalogs carry zh-CN, ja-JP and es-ES text for every new row, and the console's zh-CN overlay copies the zh-CN rows. diff --git a/.changeset/30279-time-relative-entry-surface.md b/.changeset/30279-time-relative-entry-surface.md deleted file mode 100644 index f0284eabbc6..00000000000 --- a/.changeset/30279-time-relative-entry-surface.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): the `flow-record-read-and-decision-type-mismatch-refused` upgrade-guide entry names the object a time-relative flow's conditions are typed against: the swept `config.timeRelative.object`, when the start node names no `objectName`, and the start node's `objectName` when that is the swept object (#30279) - -Clause-②: no -Metadata-protocol: none - -Text only: the entry's `surface` and the matching sentence of its `reason`, which `os migrate meta` and the upgrade guide show, said the refusal covers a time-relative flow "sweeping the start node's objectName". The author-time gate also types such a flow against `config.timeRelative.object` when its start names no `objectName`, and refuses the same conditions there; the entry now says so. Its id, kind, replacement and acceptance criteria are unchanged, and no verdict moves. diff --git a/.changeset/30281-seed-locale-per-organization.md b/.changeset/30281-seed-locale-per-organization.md deleted file mode 100644 index 49d92f21f2c..00000000000 --- a/.changeset/30281-seed-locale-per-organization.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@objectstack/runtime': minor -'@objectstack/cloud-connection': minor -'@objectstack/organizations': patch ---- - -feat(runtime,cloud-connection): the marketplace install, the sample-data reseed, the restart heal and the per-organization seed replay pick `Seed.locale` datasets by the organization's language - -Clause-②: yes (widening) -Metadata-protocol: semantic - -An app that ships one demo dataset per language market (`locale: ['zh-CN']` beside `locale: ['en-US']`) now gets the right one on every path that seeds an organization, not only on the boot path. Before, the install paths passed no locale, so a Chinese organization received the English rows and a page of refused-insert warnings for the Chinese twins (or both sets, where no unique key stopped the second), and one server's per-organization replay gave every new organization the boot app's `i18n.defaultLocale`. - -The locale is resolved for the organization the rows land in. The first answer wins: - -1. the organization's `localization.locale` setting, through the settings service's own cascade (an env override, the organization's row, the deployment-wide `global` row). Only a value somebody set counts: the manifest's built-in `en-US` default (`source: 'default'`) is not the organization's choice; -2. `sys_user.locale` of the person installing, reseeding or founding the organization; -3. the app's declared `i18n.defaultLocale`. - -Nothing answers ⇒ no locale is sent, and every dataset loads as before (the loader still warns, naming each locale-scoped dataset it let through). A dataset without `locale` loads whatever the answer is. When no dataset in a load declares `locale`, neither the setting nor the user is read. - -- **The per-organization replay** resolves the locale per organization, at replay time, instead of once at boot. At founding the organization has no setting of its own yet, so its founder's language decides; an organization inserted with no person behind it takes the app's default. The replayer (`seed-replayer` service) now takes an optional second argument, `(organizationId, { userId })`; a caller passing only the organization id works as before. -- **`@objectstack/organizations`**: the org-scoping middleware hands the replayer the insert's attributed user (better-auth's create runs as system with the signed-in creator attributed) as the founder. -- **`@objectstack/cloud-connection`**: the install and the reseed pass the installing user and the package's own `i18n.defaultLocale`; the restart heal passes the package's default only (no person starts it); the replayer this plugin registers when the host has none passes the founder and no app default, because it replays every installed package at once. -- **`@objectstack/runtime`** exports the shared producer: `resolveSeedLocaleForLoad(host, { seeds, organizationId, userId, appDefaultLocale })`, `appDefaultSeedLocale(body)`, `seedsDeclareLocale(seeds)`, `describeSeedLocale(resolved)` and the `ResolvedSeedLocale` / `SeedLocaleQuestion` / `SeedLocaleSource` types. -- A settings or user read that throws is not skipped over: the seed reports the failure (the install's seed summary, the reseed's `RESEED_SKIPPED`, the replay's warning) and writes nothing, rather than seeding another language over a choice that could not be read. A host with no `settings` service skips step 1. - -Unchanged: the boot seed and the dev hot-reload seeder still filter on the app's `i18n.defaultLocale`; package apply and draft publish still pass no locale. diff --git a/.changeset/30282-feature-flags-namespace-text.md b/.changeset/30282-feature-flags-namespace-text.md deleted file mode 100644 index 7986e95b30e..00000000000 --- a/.changeset/30282-feature-flags-namespace-text.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': patch -'@object-ui/i18n': patch ---- - -docs(spec,i18n): stop naming the removed feature-flags settings page - -Clause-②: no -Metadata-protocol: none - -The `feature_flags` settings namespace and its Setup page were removed from `@objectstack/service-settings` and `@objectstack/platform-objects`. Two shipped texts still pointed at it; both now say what is live. - -- **Settings hub subtitle (`@object-ui/i18n`).** `console.settingsHub.subtitle` no longer promises feature flags, in all ten language packs. English FROM "Configure your system, integrations, and feature flags." TO "Configure your system and integrations."; each other pack drops its own term the same way. No key is added or removed. -- **Spec doc comments (`@objectstack/spec`).** The kernel barrel and `PUBLIC_AUTH_FEATURES` docblocks no longer send readers to a `feature_flags` settings manifest or to `OS_FEATURE_FLAGS_*`. A setting a plugin reads is declared on that plugin's own settings manifest (ADR-0007, env-overridable as `OS_{NAMESPACE}_{KEY}`); capabilities a deployment advertises to clients are the `PUBLIC_AUTH_FEATURES` registry. The `SettingsManifestSchema` docblock (and its generated reference page) gives `mail`, `branding`, `localization` as example namespaces. No schema, type or export changes. diff --git a/.changeset/30283-time-relative-object-mismatch.md b/.changeset/30283-time-relative-object-mismatch.md deleted file mode 100644 index f585bbd9e77..00000000000 --- a/.changeset/30283-time-relative-object-mismatch.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/lint': minor ---- - -fix(lint)!: a time-relative flow whose start node names, in `config.objectName`, another object than the one it sweeps (`config.timeRelative.object`) is refused with `flow-time-relative-object-mismatch`: each run binds a row of the swept object, while the start's `objectName` is what its conditions are checked against (#30283) - -Clause-②: yes (narrowing) — a new published readiness rule id, `flow-time-relative-object-mismatch`, graded `error`: the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses a time-relative start whose `config.objectName` is set and is not `config.timeRelative.object`, which it passed with no readiness verdict before. -Metadata-protocol: narrowed -Skills-docs: none — `skills/**` and `content/docs/**` teach the time-relative sweep with `timeRelative.object` alone and no `objectName` on its start node, which this rule accepts; the MCP skill text keeps its `sweepObject` advice for `validate_expression`, whose expression verdict this change does not move. - - - -**BREAKING** accept-set narrowing at the author-time flow readiness gate, served by `os build`, `os validate`, `POST /api/v1/packages/:id/publish-drafts` and the flow publish door. - -- **Before.** A time-relative start could name `config.objectName: 'A'` and sweep `config.timeRelative.object: 'B'`. The run binds only `B`: the engine's binding resolver reads `timeRelative.object` first, and the trigger launches each run with a `B` row as `record` and `B` as the run's object. The expression checks typed the flow's conditions against `A` all the same. Measured both ways: `record.amount > 10`, where only `A` declares `amount`, passed every check and failed every run (`No such key: amount`); `record.priority > 1`, a correct read of a `B` field, was refused as `unknown field priority on A`. -- **Now.** That start is refused at `config.objectName`, naming both objects and the fix. A start whose `objectName` equals `timeRelative.object`, or that names none, is not reported; neither is a start whose `triggerType` routes the flow to the record-change trigger (a `record-` token, or an array holding one), which binds `objectName` and never sweeps. The runtime publish gate judges only the flow being written, so a stored flow that names two objects keeps being served; its own next publish is refused. -- **The `config.objectName` warning on a sweep of another object** (served where a condition's type verdict was softened to a warning) now names `flow-time-relative-object-mismatch` as the verdict that asks for the same fix. - -**What an author does.** Nothing for a time-relative start that names no `objectName`, or names the object it sweeps. A start that names two objects says which one the author meant, and nothing mechanical can: for the swept one, remove `config.objectName` (the sweep never reads it, so the run is unchanged) or set it to the value of `config.timeRelative.object`, then correct any condition the expression checks now refuse against that object; for the named one, point `config.timeRelative.object` (and its `dateField`) at it instead. The refusal names both objects. diff --git a/.changeset/30285-i18n-walk-in-lint.md b/.changeset/30285-i18n-walk-in-lint.md deleted file mode 100644 index c75945cef15..00000000000 --- a/.changeset/30285-i18n-walk-in-lint.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/lint': patch -'@objectstack/cli': patch -'@objectstack/spec': patch ---- - -refactor(lint): the i18n translatable-key walk moves from the CLI into `@objectstack/lint` - -Clause-②: no -Metadata-protocol: none - -`collectExpectedEntries` — the one definition of which translation keys a stack can carry, each with the source text it translates — now lives in `@objectstack/lint` (`src/i18n-walk.ts`) and is exported from the package root with `authorWarnedTranslationGroups`, `humanizeFieldPath` and the `ExpectedEntry` / `ExpectedEntryOptions` types. It moved so a browser can run the same walk the CLI runs: Studio's translation editor builds its source-to-translation table from it. `os i18n extract` and `os lint`'s translation coverage keep reading it unchanged, through the CLI's extractor module, which keeps bundle assembly and serialization and re-exports the walk. - -- No key, no skeleton and no coverage verdict changes: the walk's code moved as is. -- In a browser the walk cannot read the liveness ledger from disk, so its default leaves no translation group out. That is the same set the CLI leaves out today, because the ledger warns on no translation group; a browser caller that must honour a warned group passes `warnedGroups`. -- `@objectstack/spec`: the `action` and `view` liveness ledgers cite the walk at its new path. diff --git a/.changeset/30285-studio-translation-table.md b/.changeset/30285-studio-translation-table.md deleted file mode 100644 index a2edc2ff88c..00000000000 --- a/.changeset/30285-studio-translation-table.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -feat(app-shell): Studio edits a translation item as a source-to-translation table with its gaps counted - -Clause-②: no -Metadata-protocol: none - -A `translation` item in Studio now opens on a table for its language: a row per string the package declares (objects, fields, options, apps, navigation, views, actions, dashboards and the rest), the source text beside the translation, grouped by item, with a count such as "37 of 120 untranslated" and an "Untranslated only" filter. The rows come from `collectExpectedEntries` in `@objectstack/lint`, the walk `os i18n extract` and `os lint` use, over the package's declared documents. The platform objects the package points at (for example the user object behind an owner lookup) are included, because their labels ship in a few languages only. A row also counts as translated when another bundle already serves the language at its key. - -- Saving writes the same `translation` item to the same place as before (the `.custom` pair on a read-only package): each cell writes the item's JSON at its key. The item's label, language and description stay in the form above the table, and its JSON stays available as an advanced view for what the table does not list (messages, settings UI, metadata forms). -- The "no custom renderer, editing as JSON" notice no longer appears on this page. -- The metadata item list labels an item in a code package's `.custom` pair as customer-written, not as shipped in a code package. -- Labels in English and Chinese. diff --git a/.changeset/30286-default-language-follows-deployment.md b/.changeset/30286-default-language-follows-deployment.md deleted file mode 100644 index 002767e5d90..00000000000 --- a/.changeset/30286-default-language-follows-deployment.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/service-settings': patch ---- - -fix(service-settings)!: Setup's default language (`localization.locale`) offers and accepts the languages the deployment supports, not four hard-coded ones (#30286) - -Clause-②: yes (narrowing) — `SettingsService` gains `registerOptionSource(namespace, key, source)` and the package exports the `SettingsOptionSource`, `SettingsOptionSourceAnswer` and `SettingsServedOption` types; `getNamespace` takes an optional third `display` argument. The save of `localization.locale` (`PUT /api/settings/localization`, `SettingsService.setMany`) and an `OS_LOCALIZATION_LOCALE` override now accept the i18n service's locale list instead of the declared `en-US`, `zh-CN`, `ja-JP`, `es-ES`: a language the deployment supports is accepted where it was refused (`fr-FR`, `vi-VN`), and one of the four the deployment does not support is refused where it was accepted (`ja-JP` on an app that declares `i18n.supportedLocales: ['en', 'zh-CN']`). -Metadata-protocol: none - - - -**BREAKING** accept-set narrowing on the `localization.locale` settings save (Setup → Localization → Default language, `PUT /api/settings/localization`, `SettingsService.setMany`) and on an `OS_LOCALIZATION_LOCALE` override, on a deployment whose language list leaves out one of `en-US`, `zh-CN`, `ja-JP`, `es-ES`. - -- **Before.** Default language offered English (US), 简体中文, 日本語 and Español (España), and the save accepted exactly those four, whatever the deployment shipped. A French pack, or a Vietnamese translation item the user's language menu already offered, could not be chosen as the organization's default. -- **Now.** The options and the accepted values are the i18n service's locale list, the one `GET /api/v1/i18n/locales` and the user's language menu read (the app's declared `i18n.supportedLocales`, else every locale a bundle or translation item has loaded), asked at each save and page read, so a translation item added at runtime is offered at once. Each option is named in its own language (the platform locale table's name, else `Intl.DisplayNames` in that language, else the code). A stored spelling of an offered language is still accepted and shown chosen (`en-US` where `en` is offered, `zh-CN` where `zh-Hans` is). A value outside the list is refused `400 SETTINGS_VALIDATION` with `invalid_option` at `locale`, listing the offered values. With no i18n service in the composition the declared four stand. -- **Unchanged.** A value stored before this release that the deployment no longer offers stays in effect and readable: the page offers it, named as no longer offered in the reader's language, and a save of other keys passes it by; only a save that writes it again is judged. Every reader of the setting (formatting, auth email and SMS, seeds, activity titles) reads it as before. - -**The remedy.** To keep offering a language the deployment refuses, ship it: add the language to the app's `i18n.supportedLocales` (and its translations), then pick it in Setup. diff --git a/.changeset/30289-scenario-card-schema.md b/.changeset/30289-scenario-card-schema.md deleted file mode 100644 index 0ba9f0001b0..00000000000 --- a/.changeset/30289-scenario-card-schema.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@objectstack/scenarios': patch ---- - -Clause-②: no -Metadata-protocol: none - -New private package `@objectstack/scenarios` (`packages/qa/scenarios`, not published): the zod schema and loader for scenario cards, plus the open library's three control cards under `docs/qa/scenarios/`. A card states an authoring prompt, the structural `expect` the deterministic scorer reads, capability tags (area half closed to the platform-checklist areas) and a closed category, seed rows with data questions whose truth the loader computes from a declarative spec (`count | sum | avg | min | max | topN | unanswerable`, with filters, relative windows against a card-level `asOf`, relation paths through declared lookups, groupBy and order), personas with row scope whose per-persona truths are computed too, write operations that declare their outcome (post-state, refusal code or pending confirmation, with the audit actor), five guardrail checks, optional agent actions, and one iteration request. A malformed card is refused with its file and field path. No `packages/spec` change and no public surface change. diff --git a/.changeset/30291-scenario-tag-checks.md b/.changeset/30291-scenario-tag-checks.md deleted file mode 100644 index b8587c1bf6d..00000000000 --- a/.changeset/30291-scenario-tag-checks.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/scenarios': patch -'@objectstack/cli': patch ---- - -feat(scenarios): the deterministic scenario scorer lives in `@objectstack/scenarios` and judges capability tags, agent-action exposure, write outcomes and per-persona answers against a loaded card - -Clause-②: no -Metadata-protocol: none - -- **`@objectstack/scenarios/score`** (private, unpublished): the entry the scenario runner calls. `scoreCardArtifact(card, artifact)` scores a built `dist/objectstack.json`; `gradeCardOperation(card, operationId, observation)` judges an observed write against its declared outcome; `gradeCardAnswer(card, { question, persona?, turn })` grades an agent's answer against the loader's computed truth for that persona. Each takes a card the loader returned, and the graders read the card schema's own types. -- **The scorer moved here from `@objectstack/cli`.** That covers the structural shape score, the artifact-to-graph reader and the Run grader, which were never released from the CLI. It now also carries: - - **capability-tag checks**: a closed table of nine tags, listed in the package README. A tag outside it, or a row with nothing in the card to judge, is reported `unscored` and counted nowhere; - - **agent-action exposure**: each declared action exists on its object, carries `ai.exposed: true` and, where the card says it asks first, `ai.requiresConfirmation: true`. A missing action fails; - - **a write-operation grader**: `applied`, `refused` or `pending_confirmation`. An unobserved fact fails; - - **per-persona Run grading**: a `withheld` truth is must-not-reveal, and an all-rows figure outside a scoped persona's rows is a leak. -- **`@objectstack/cli`**: `MetadataEvalCase` is now declared in `corpus.ts`, beside the golden corpus it types, and `metadata-eval.ts` re-exports it under the same name. No behaviour change, no new flag and no new package-entry export. The CLI carries no scenario code. - -No model call anywhere. diff --git a/.changeset/30295-org-mfa-notice-and-enroll-refusal.md b/.changeset/30295-org-mfa-notice-and-enroll-refusal.md deleted file mode 100644 index d7fa481f1ae..00000000000 --- a/.changeset/30295-org-mfa-notice-and-enroll-refusal.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/auth': patch -'@object-ui/i18n': patch ---- - -fix(app-shell,auth,i18n): the organization form says requiring MFA needs two-factor on, an accepted save says who must enroll and when, and the enrollment screen names a two-factor-off deployment in the user's language - -Clause-②: yes (widening) -Metadata-protocol: none - -- **Organization form, before saving.** While two-factor is off on the deployment (`features.twoFactor: false` in `/api/v1/auth/config`), the `sys_organization` edit form says, under its title, that Require Multi-Factor Auth needs two-factor, that a save with it ticked is refused, and how to turn two-factor on (Require multi-factor authentication in Setup → Authentication, or `OS_AUTH_TWO_FACTOR=true`). When the save is refused, the server's refusal shows on the field, as before. -- **Organization form, after an accepted save.** A save that turns `require_mfa` on for a row that stored it off adds a line to the saved toast: every member without an authenticator app must set one up, and is blocked from data once the MFA grace period ends (7 days unless changed in Setup → Authentication), counted from their first use after the change. A save of a row that already required it says nothing new. -- **Enrollment screen.** `enrollTotp` and `verifyTotp` of `@object-ui/auth` now reject with the HTTP `status` and the server's `code` on the error (the message is unchanged). The authenticator step reads a `404` (the `/two-factor/*` endpoints are not mounted while two-factor is off) as a translated sentence saying enrollment is not available and who can turn it on, instead of `Two-factor enable failed (404)`, and `INVALID_PASSWORD` as the password screen's sentence. Any other refusal keeps the server's message. -- **The widening.** `@object-ui/i18n` gains `auth.remediation.mfa.unavailable`, `console.organizationMfa.needsTwoFactor` and `console.organizationMfa.nowRequired` in all ten language packs. The two enrollment rejections gain the optional `status` and `code` properties. diff --git a/.changeset/30299-sign-in-config-fresh-after-sign-out.md b/.changeset/30299-sign-in-config-fresh-after-sign-out.md deleted file mode 100644 index 6a05f867d2e..00000000000 --- a/.changeset/30299-sign-in-config-fresh-after-sign-out.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/auth': patch ---- - -fix(auth): the sign-in page reads the sign-in methods the server has now, not the ones it had when the tab was opened (#30299) - -Clause-②: no -Metadata-protocol: none - -The auth client kept the first `/auth/config` answer for the life of the page. An administrator who turned email and password off on the Authentication settings page (with another way in configured) and then signed out was shown the email and password form on the sign-in page, from the copy read while signed in, although the server refuses that form. The client now forgets the cached config whenever the tab is left without a session: on sign-out, and when a session read finds none (an expired session reaches the sign-in page without a sign-out). Readers on one page still share one request, and nothing changes while a session lasts. diff --git a/.changeset/30300-keep-the-last-identity-provider.md b/.changeset/30300-keep-the-last-identity-provider.md deleted file mode 100644 index aa349cdd4aa..00000000000 --- a/.changeset/30300-keep-the-last-identity-provider.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix(plugin-auth)!: deleting the last identity provider is refused, and a boot that serves no sign-in method says so (#30300) - -Clause-②: yes (narrowing) — a request an administrator could make before is now refused: `POST /api/v1/auth/sso/delete-provider` (Setup's "Delete SSO Provider") on the last identity provider, while email and password is off and nothing else signs anyone in, answers `409 DELETE_RESTRICTED` (an existing standard code, the one an organization delete refused for the same reason answers), or `503 SERVICE_UNAVAILABLE` when the provider list cannot be read to tell. No new error code, setting or field. The widening half: `AuthManager` gains `setSsoProviderDeleteFloor()`, marked `@internal`, through which `AuthPlugin` hands it the judge. The boot report is a new `error` log line only; nothing about the boot's outcome changes. -Metadata-protocol: none -Skills-docs: none — no file under `skills/**` teaches deleting an SSO provider or the sign-in boot reports; `content/docs/deployment/self-hosting.mdx` gains "No sign-in method left" with the three doors and the recovery. - - - -**BREAKING** accept-set narrowing at `POST /api/v1/auth/sso/delete-provider`: deleting the identity provider that is the last way to sign in is refused, where it was deleted before. - -- **Before.** Email and password off, one SSO identity provider registered and nothing else in force. Setup → the provider → "Delete SSO Provider": `200 {"success":true}`, the row gone. From then on nobody could sign in, the administrator included (measured on a real boot). -- **Now.** That delete answers `409` with `code: DELETE_RESTRICTED`, `lastSignInMethod: enterprise_sso`, and a message in the caller's language (en, zh-CN, ja-JP, es-ES) saying to turn email and password back on, or register another identity provider or set up another sign-in method, first. Nothing is deleted. When the provider list cannot be read to tell, it answers `503 SERVICE_UNAVAILABLE` instead of letting the delete through. -- **Still allowed.** Deleting a provider while another one remains, while email and password is on, while enterprise SSO is switched off, or while another way in counts: a social or OIDC provider with a client ID and secret, phone-number sign-in, or a host sign-in handoff. An unknown provider is still the vendor's `404`, and a caller with no session the vendor's `401`; the vendor's own provider-access check still decides who may delete which provider. -- **The redeploy door.** A boot whose configuration no longer declares the last way in (for example a redeploy without the `GOOGLE_CLIENT_*` pair while email and password is off) still boots, as before, and now reports at `error` under the grep token `no_sign_in_method_at_boot`, naming the consequence and the recovery. It is a positive claim only: a provider table that cannot be read says nothing. Refusing such a boot is a later option, not done: a deployment that signs people in through a door the auth plugin cannot see must keep starting. -- **Already locked out.** Set `OS_AUTH_EMAIL_PASSWORD_ENABLED=true` and restart: the environment override wins over a stored `auth.email_password_enabled = false` and over the stack config, and email and password sign-in comes back. diff --git a/.changeset/30302-mfa-enforcement-follow-ups.md b/.changeset/30302-mfa-enforcement-follow-ups.md deleted file mode 100644 index e711bc9e0eb..00000000000 --- a/.changeset/30302-mfa-enforcement-follow-ups.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix(plugin-auth)!: the global `auth.mfa_required` switch cannot be saved on while `OS_AUTH_TWO_FACTOR=false` keeps two-factor enrollment off (#30302) - -Clause-②: no (narrowing) — while `OS_AUTH_TWO_FACTOR` is set to off, a save that turns the `auth.mfa_required` setting on is refused `400 SETTINGS_VALIDATION` (`invalid_value` at `mfa_required`), a value an administrator could save before; saving it off, or re-saving a value already in force, passes. No published export, error code, settings key or `/auth/config` member is added; nothing is widened. -Metadata-protocol: none -Skills-docs: updated here — `content/docs/permissions/authentication.mdx` (the `mfa_required` row) and `docs/HARDENING.md` (the MFA row); `skills/**` teaches neither `mfa_required` nor `require_mfa`, so no skill or eval moves. - - - -**BREAKING** accept-set narrowing on the `auth.mfa_required` settings save (Setup → Authentication, `PUT /api/settings/auth`, `SettingsService.setMany`), on a deployment that sets `OS_AUTH_TWO_FACTOR` to off. - -- **Before.** Turning on "Require multi-factor authentication" saved and read as on. The settings pass turns the requirement and the two-factor plugin on as one piece, but the `OS_AUTH_TWO_FACTOR` env var outranks the plugin flag, so under `OS_AUTH_TWO_FACTOR=false` nobody could enroll an authenticator app, and the MFA gate, which holds only a user who can enroll, enforced nothing. An organization's own `require_mfa` already refused this state; the global switch did not. -- **Now.** That save is refused before anything is written: the whole save, other keys in it included, with one field error at `mfa_required` naming `OS_AUTH_TWO_FACTOR`, in the caller's language (English, Simplified Chinese, Japanese, Spanish), and the constraint `{ twoFactor: false, overriddenBy: 'OS_AUTH_TWO_FACTOR' }`. -- **Unchanged.** With `OS_AUTH_TWO_FACTOR` unset or on, the switch saves and turns enrollment on with the requirement, as before. Turning it off is never refused. A switch saved on before this release is left as stored and still not enforced; `GET /api/v1/auth/config` answers `features.twoFactor: false` there, the same decision the gate makes, which is the reading a console uses to say the requirement is not enforced. - -**The remedy.** Set `OS_AUTH_TWO_FACTOR=true` on the deployment (or remove it) and restart, then turn on `auth.mfa_required`. diff --git a/.changeset/30303-fls-key-extension-fields.md b/.changeset/30303-fls-key-extension-fields.md deleted file mode 100644 index 8b15154863d..00000000000 --- a/.changeset/30303-fls-key-extension-fields.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/lint': patch ---- - -fix(lint): a permission set's field key on a field that an `objectExtensions` entry adds, a sibling package's in the same artifact included, is no longer refused as `security-fls-unknown-field` (#30303) - -Clause-②: yes (widening: the FLS-key check accepts extension-added fields) -Metadata-protocol: none - -- **Before.** The FLS-key check resolved `object.field` against the object's declared `fields` only. On a multi-package artifact, an app package's permission set naming `crm_contract.o2c_probe_note`, a field its module package adds to `crm_contract` through `objectExtensions`, failed `os build`, `os validate` and `os lint` with "crm_contract declares no field o2c_probe_note", although the runtime serves the field and the key masks it. A single-package stack's own extension fields were refused the same way. -- **Now.** The key resolves against the object as the registry serves it: its declared fields plus every field an `objectExtensions` entry of any body of the artifact adds to it, folded as `@objectstack/objectql` folds them (fields by name; one entry per package per object, a later entry replacing an earlier one; `extend` naming the object verbatim). A field that no body declares or adds keeps the same refusal, with the same rule id, severity and message, and so does a field an entry only patches through `fieldPatches`, which adds no field. The hint's nearest name reads the served fields too. -- **One fold.** The controlled-by-parent merge (#23210), the membership-key column check and the FLS-key check now read extensions through one shared fold in `object-graph.ts`. diff --git a/.changeset/30304-disabled-package-extension-fields-withheld.md b/.changeset/30304-disabled-package-extension-fields-withheld.md deleted file mode 100644 index 30b802a5abe..00000000000 --- a/.changeset/30304-disabled-package-extension-fields-withheld.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/objectql': minor -'@objectstack/runtime': minor -'@objectstack/cli': minor ---- - -fix(objectql)!: a disabled package's `objectExtensions` fields are withheld from the object they extend, and come back with their values on enable - -Clause-②: yes (narrowing) — while a package is disabled, the served object it extends (`GET /api/v1/meta/object/:name`, the record page, the `/data` field map) no longer carries the fields, validation rules and indexes the package's `objectExtensions` add, so a `/data` write, filter, sort or `fields` projection naming one of those fields is refused as a field the object does not have (`400 INVALID_FIELD` / `INVALID_SORT`), and `GET /api/v1/packages/:id/customizations` no longer lists them among the base's `contributions`. -Metadata-protocol: semantic - - - -**BREAKING** served-object narrowing under package disable (`@objectstack/objectql`, `@objectstack/runtime`, `@objectstack/cli`). - -- **Before.** Disabling a package withheld its objects (`404 OBJECT_PACKAGE_DISABLED`), flows, navigation items, permissions and actions, but the fields its `objectExtensions` add to another package's object stayed on that object: in `/meta/object`, in every `/data` read and on the record page. -- **Now.** The served fold of an object leaves out every `extend` contribution of a disabled package: its fields, its validation rules, its indexes and its field patches. A base layer that already carries them (a tenant overlay saved from a served read, a metadata-service copy of a resolved object) has those fields and rules taken off too. A field the owner itself declares, or another enabled extension also adds, stays. A write naming a withheld field answers exactly what a write naming an unknown field answers; a write through the object's other fields lands and leaves the withheld values alone. -- **Kept.** Schema sync, the federated binding and the lifecycle sweep read the object with every extension folded in (`SchemaRegistry.getStorageObject` / `getAllStorageObjects`), so a disable never drops or skips a column, and a boot that starts with the package disabled still creates them. Enabling the package serves the fields again with the stored values. With `OS_SKIP_SCHEMA_SYNC=1`, `os migrate`'s drift plan no longer reports a disabled module's columns as orphan columns to drop. -- **Who moves.** A client that reads a disabled module's extension fields gets the object without them. A formula on the object that reads a withheld field answers `null`. A validation rule of another, enabled package that reads a withheld field cannot be evaluated, so writes to the object are refused (`400 VALIDATION_FAILED`, naming the rule) until the field's package is enabled again or the rule's package declares and keeps that package. diff --git a/.changeset/30311-served-app-labels.md b/.changeset/30311-served-app-labels.md deleted file mode 100644 index 728d2647c7f..00000000000 --- a/.changeset/30311-served-app-labels.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): every Console surface that names an app draws the served app label as served - -Clause-②: no -Metadata-protocol: none - -- **Now.** The app switcher, the sidebar's app title, the top bar's hidden-app menu, Home's app cards, apps strip, recent rows and setup link, the no-access empty state, the AI chat's app names, the marketplace's "Open app" buttons and the report index's other-app links draw the app `label` (and the app card its `description`) the `/meta/app` read served, as given. The server has already translated it, by its label order for an overridden app: a `.custom` pair's own text is kept over the base package's translation. Before, the served label reached the client's `apps..label` catalog lookup only as its fallback, so the base package's translation of the old name won. -- **Unchanged.** A pending draft under `?preview=draft`, a keyed label and an app with no label or description are still named by the catalog key, then by the machine name as before. An unedited app shows the same text as before, because the server serves the catalog's translation for it. diff --git a/.changeset/30311-served-object-view-labels.md b/.changeset/30311-served-object-view-labels.md deleted file mode 100644 index a578ae5db24..00000000000 --- a/.changeset/30311-served-object-view-labels.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): the Console sidebar, recent list and object list header draw a served object or list view label as served - -Clause-②: no -Metadata-protocol: none - -- **Now.** An unlabelled sidebar entry that opens an object or a list view, a recent object or list view row (in the sidebar and on Home), the object list page's title, the app header's title on an object route and an object's `/data` page title draw the label the `/meta` read or the app bundle served as given. The server has already translated it, by its label order for an overridden item: a `.custom` object extension's or view override's own text is kept over the base package's translation. Before, the served label reached the client's catalog lookup only as its fallback, so the base package's translation of the old name won. A list is titled by the served `pluralLabel`, else the served `label`. -- **Unchanged.** A list view the client derived (no served read names it), an object the cache does not hold yet, and a pending draft's label are still named by the catalog key, then by the machine name as before. An unedited item shows the same text as before, because the server serves the catalog's translation for it. Record pages, record forms and drawers still look the object's name up in the catalog; they are a follow-up. diff --git a/.changeset/30312-auth-settings-clear-applies-default.md b/.changeset/30312-auth-settings-clear-applies-default.md deleted file mode 100644 index 9659915b7b1..00000000000 --- a/.changeset/30312-auth-settings-clear-applies-default.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix(plugin-auth)!: clearing an authentication setting, or restoring the namespace's defaults, takes effect at once (#30312) - -Clause-②: yes (narrowing) — one save an administrator could make before is now refused at the settings save door with the existing `SETTINGS_VALIDATION` / `invalid_value` field error: clearing `auth.email_password_enabled` on a deployment that itself declared email and password off, when nothing else signs anyone in after the save. Before, that clear was saved and changed nothing at runtime; now a clear applies, and that one would lock everyone out. No new error code, settings key or export. -Metadata-protocol: none -Skills-docs: none — no file under `skills/**` teaches the auth settings namespace, its clear or the sign-in floor. - - - -**BREAKING** accept-set narrowing at the auth settings save door (`PUT /api/settings/auth`, `SettingsService.setMany('auth', …)`): one clear that was saved before is refused. - -- **The fix.** A cleared `auth.*` key (the console sends `null`) or a namespace reset resolves `source: 'default'`. Every key except `allowed_ip_ranges` (#30203) then kept its last stored value in force until a restart: lockout threshold, max sessions per user, password policy, session lifetime, MFA, API-key lifetime, rate limits, Google, audience. Now the running value returns in the same pass to what the deployment declared (plugin options, `auth:configure`), or to the built-in default where it declared nothing. A value in a block the auth manager judges (`emailAndPassword`, `plugins`, `audience`) is restored through that same block, so it is judged as a save of it would be. Environment overrides keep ruling. -- **The refused clear.** With email and password declared off by the deployment and turned on in Setup, and no other way in (Google switched off in Setup, no other provider, no SSO, no phone sign-in, no host handoff), clearing "Enable email/password login" now answers `400 SETTINGS_VALIDATION` at `email_password_enabled` (`constraint.lastSignInMethod: email_password`), and nothing is written. Set up another sign-in method first, or save the toggle on explicitly instead of clearing it. -- **Session lifetime.** A stored `session_expiry_days` / `session_refresh_days` now overrides its field of the deployment's declared `session` block instead of replacing the whole block. diff --git a/.changeset/30313-allowed-ip-ranges-client-address.md b/.changeset/30313-allowed-ip-ranges-client-address.md deleted file mode 100644 index fc61e7497d2..00000000000 --- a/.changeset/30313-allowed-ip-ranges-client-address.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/console': patch -'@object-ui/i18n': patch ---- - -feat(console): the Allowed IP ranges setting shows the address the server judges before you save - -Clause-②: no -Metadata-protocol: none - -A save of `auth.allowed_ip_ranges` whose list leaves out the saving request's own address is refused, and until now the Authentication settings page only told the admin that address in the refusal. The page now reads `GET /api/v1/auth/admin/client-address` (the save check's own reading of the address, `server.trustProxy` included) and draws one line under the field: the address, and whether it came from a trusted proxy's forwarded headers or, with `trustProxy` off, from the connection itself (behind a proxy, the proxy's address). When the server cannot establish an address the line says a non-empty list cannot be saved from here. The line stays beside a refusal. A caller the route refuses (not a platform admin), or a server without the route, sees no line and the page is otherwise unchanged. `@object-ui/i18n` adds `console.settingsView.clientAddressDirect`, `clientAddressViaProxy` and `clientAddressUnknown` in all ten shipped locales. diff --git a/.changeset/30317-draft-only-object-view-seed.md b/.changeset/30317-draft-only-object-view-seed.md deleted file mode 100644 index ec72e0ae733..00000000000 --- a/.changeset/30317-draft-only-object-view-seed.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(studio): on a draft-only object, a new or first-edited list view starts from the object's first five fields - -Clause-②: no -Metadata-protocol: none - -In Studio Interfaces, a list view created on an object that so far exists only as a draft in the package started with no columns. The first edit of a default view that did not exist yet wrote only the column the author touched, and New view with no columns picked wrote none. The list-view panel seeded those columns from the shell's merged objects, which hold published objects only. When the merged definition is missing, the panel now reads the object's drafted definition from this package (`GET /meta/object/:name?preview=draft&package=`, the read the field picker already makes) and seeds from it, so both start from the same first five fields a published object gets. A published object reads nothing new. If the drafted read fails or finds nothing, the seed stays empty as before. diff --git a/.changeset/30319-copy-doors-inherited-grants.md b/.changeset/30319-copy-doors-inherited-grants.md deleted file mode 100644 index a38cdfcc2af..00000000000 --- a/.changeset/30319-copy-doors-inherited-grants.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix(metadata-protocol): duplicating a package copies a permission set that grants on an optional plugin's object not installed here, as the permission-set clone door does - -Clause-②: no -Metadata-protocol: none - -- **Now.** `duplicatePackage` saves a copied permission set with the grant keys of its SOURCE row as stored passed as `inheritedGrantObjects`, so the save door's unknown-object refusal does not judge them (#30255's rule for the clone door). A platform set granting on `sys_comment` / `sys_activity` on a deployment without `@objectstack/plugin-audit` is copied; before, it was left in `failed[]` with the 422 finding and the duplicate answered `success: false`. -- **Still refused.** A grant the copy's rewrite renames onto the copy's own object (`SOURCE_NS_x` → `TARGET_NS_x`) is not the source's key and is judged as before: when that object did not copy, the set lands in `failed[]` naming that grant alone. The keys come from the stored row, never from the request. -- **Unchanged, by ruling.** `importUnmanagedPackage` still refuses such a set `422 INVALID_METADATA` and rolls the import back: the package is the request's body, not a stored row, so its permission sets are new writes and every grant is judged. diff --git a/.changeset/30320-ipv6-cidr-allowed-ranges.md b/.changeset/30320-ipv6-cidr-allowed-ranges.md deleted file mode 100644 index 3df60dd6617..00000000000 --- a/.changeset/30320-ipv6-cidr-allowed-ranges.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix(plugin-auth): an IPv6 CIDR in the allowed IP ranges admits the IPv6 clients inside it, at the sign-in gate and at the settings save check - -Clause-②: no -Metadata-protocol: none - -`auth.allowed_ip_ranges` (ADR-0069 D5) promises CIDR ranges, but its matcher read only IPv4 CIDR and exact addresses: an IPv6 entry such as `2001:db8::/32` was compared as text, so it never matched a real client. The sign-in gate then refused every IPv6 client the list named, and the settings save check refused the save for an administrator connected over IPv6 from inside it. `ipMatchesRange` now reads an IPv6 CIDR as the first `n` of 128 bits, and both doors use it, so they keep judging the same address the same way. - -- An exact IPv6 entry matches the address it denotes however it is spelled (`2001:DB8:0:0::1` matches `2001:db8::1`). -- An IPv4-mapped address, dotted (`::ffff:203.0.113.5`) or hex (`::ffff:cb00:7105`), is still judged as the IPv4 address it maps. A zone identifier on an address (`fe80::1%eth0`) is dropped before matching. -- Families never cross: an IPv4 range never admits a pure IPv6 address, and an IPv6 range never admits an IPv4 one. -- An IPv6 prefix must be a whole number from 0 to 128. An entry with any other prefix matches only its base address, as it did before. IPv4 entries are read exactly as before. diff --git a/.changeset/30323-install-note-sample-data.md b/.changeset/30323-install-note-sample-data.md deleted file mode 100644 index b9192f75bf7..00000000000 --- a/.changeset/30323-install-note-sample-data.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -feat(console): the install note says how many sample records an install loaded, and clears them in place - -Clause-②: no -Metadata-protocol: none - -- **The install note.** After an install into this runtime, the package page's result now says how many sample records the install loaded, read from the install answer's own `data.seeded` report (`inserted + updated + skipped` of an `inline` run), with a "Purge sample data" button right there. An install over an existing one adds that installing again loads the sample data again, including any that was cleared. An install that loaded none (no seed datasets, a copy, a walled session with no active organization) shows nothing new. The success sentence is definite where the runtime answered that it loaded the app: "… is now in the app switcher." -- **The console's own confirm.** Purging sample data (from the install note, or the ⋯ menu, local or cloud) asks in the console's dialog instead of the browser's. When this session installed the package, the dialog says how many sample records will go at most: the number the install loaded. The clearing rule is unchanged: only rows that carry a seed record's key are deleted. -- **A reach from the app's own pages.** When the installer leaves the package page, a console banner (the `banner` notification `ConsoleLayout` draws above every page) says the app holds sample data, with a "Purge sample data" action that brings them back to the package page with the confirm open. Clearing withdraws it. It lasts for the console session; the runtime serves no count of sample rows after the install answer. -- **Types.** `LocalManagedInstallResult` and `LocalUnmanagedInstallResult` gain the optional `seeded` (`LocalInstallSeeded`). -- **Strings.** `@object-ui/i18n` gains `marketplace.detail.sampleDataLoaded`, `sampleDataReloaded`, `purgeConfirmAtMost` (count families), `sampleDataBannerTitle` and `sampleDataBannerMessage` in all 10 locales, and rewords `marketplace.install.localSuccess` in all 10. diff --git a/.changeset/30323-sample-data-count.md b/.changeset/30323-sample-data-count.md deleted file mode 100644 index cf485dd0823..00000000000 --- a/.changeset/30323-sample-data-count.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/cloud-connection': patch ---- - -feat(cloud-connection): `GET /api/v1/marketplace/install-local/:manifestId/sample-data` answers how many of an installed app's sample records a purge would delete now - -Clause-②: yes (widening) -Metadata-protocol: none - -- **New read-only route.** `GET /api/v1/marketplace/install-local/:manifestId/sample-data` answers `{ success: true, data: { manifestId, count, unidentified } }`. `count` is the number of rows `POST …/purge-sample-data` would delete if it ran now, found by the purge's own matcher with nothing deleted. A record a user added is not counted, and neither is a sample record whose key field (its dataset's `externalId`) was edited, because the purge deletes neither. A sample record edited only in other fields is still a sample row: the purge deletes it and the count includes it. `unidentified` counts the sample records the purge would report in `errors` and leave in place (no usable key value, or a key two rows carry). Before this, the count was known only from the install answer's `data.seeded`, so after a reload no API could say how much sample data was left; the installed-apps listing's `withSampleData` says only whether any is left. -- **Same rules as the purge.** It requires `manage_metadata`, answers `404` for a package that is not installed and `422 OS_PROTOCOL_INCOMPATIBLE` for an entry this runtime refused to load, and counts in the purge's scope (the caller's active organization under an organization wall, every row without one). A walled session with no active organization gets `count: 0`, the same answer as the listing's `withSampleData: false` for that session. A package with no seed datasets gets `count: 0`. With no data engine or metadata service it answers `500 DRIVER_UNAVAILABLE`, never `0`. -- **It cannot delete.** It is a `GET` and accepts no query parameter: any parameter (`dryRun`, `confirm`, …) is refused with `400 VALIDATION_ERROR` before any row is read. -- **Unchanged.** What the purge deletes, its permission and its answer; the listing's `withSampleData`. -- `@objectstack/cloud-connection` now depends on `@objectstack/rest`, for the shared closed-query-set refusal message. diff --git a/.changeset/30326-install-answer-protocol-check-skipped.md b/.changeset/30326-install-answer-protocol-check-skipped.md deleted file mode 100644 index 3b209cf295a..00000000000 --- a/.changeset/30326-install-answer-protocol-check-skipped.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/cloud-connection': patch -'@objectstack/metadata-core': patch ---- - -feat(cloud-connection,metadata-core): an install the protocol handshake admitted without checking says so in its answer, not only in the server log - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@objectstack/cloud-connection`: `POST /api/v1/marketplace/install-local` answers `data.protocolCheckSkipped`** when the ADR-0087 handshake admitted the package without checking it: `{ reason, packageId, protocolVersion, message }`, plus `requiredRange` and `rangeSource` for `reason: 'unparsed-range'`. `reason` is the handshake's own verdict: `no-range` (the package declares no `engines.protocol`) or `unparsed-range` (it declares a range the handshake cannot read). `protocolVersion` is the protocol the package was installed under unchecked, and `message` says so in plain words for the person who pressed Install. It is on all three success answers (managed, `unmanaged`, and a paired customization package), and omitted, not nulled, when the range was checked, so that answer's key set is unchanged. The `warn` log line stays as it was. -- **`@objectstack/metadata-core`: `assertProtocolCompat` returns the verdict it admitted the package under** (new exported type `ProtocolCompatAdmitted`; it returned `void`), and the `no-range` and `unparsed-range` verdicts of `checkProtocolCompat` carry `protocolVersion`, the version they were judged against. A door that answers a caller reads the skipped check from the handshake itself instead of judging the range a second time. diff --git a/.changeset/30326-install-result-protocol-check-skipped.md b/.changeset/30326-install-result-protocol-check-skipped.md deleted file mode 100644 index 76bed3f0ee4..00000000000 --- a/.changeset/30326-install-result-protocol-check-skipped.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -feat(console): the install result says when a package was installed without a protocol compatibility check - -Clause-②: yes (widening) -Metadata-protocol: none - -- **What the administrator now reads.** `POST /api/v1/marketplace/install-local` answers `data.protocolCheckSkipped` when its protocol handshake admitted the package without checking it: the package declares no `engines.protocol` (`no-range`), or a range the runtime cannot read (`unparsed-range`). The three Console surfaces that show an install result now draw it as a caution line under the success sentence: the package page's Install result, the catalog's organization install message, and Setup → Packages → Import. The line names the package, the platform protocol it was installed under and, for `unparsed-range`, the range it declared, in the reader's locale. A `reason` this Console does not know shows the server's own `message`. An install whose range was checked shows nothing new. -- **Types.** `LocalManagedInstallResult` and `LocalUnmanagedInstallResult` gain the optional `protocolCheckSkipped`. -- **New strings.** `@object-ui/i18n` gains `marketplace.install.protocolCheckSkipped.noRange` and `.unparsedRange` in all 10 locales. diff --git a/.changeset/30327-new-datasource-mode-and-placeholders.md b/.changeset/30327-new-datasource-mode-and-placeholders.md deleted file mode 100644 index 2b4c704a973..00000000000 --- a/.changeset/30327-new-datasource-mode-and-placeholders.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(app-shell): a new datasource starts on the External schema mode and says what each mode does; a driver box's default is a placeholder, not a value - -Clause-②: yes (widening) -Metadata-protocol: none - -- **Starts on External.** Setup → Datasources → New datasource now starts on `schemaMode: 'external'` (sent with the `external: {}` block the schema requires) instead of `managed`. The page exists to connect a database that already holds tables and sync them in as objects, and only a federation source offers Sync. A managed datasource is ObjectStack's own storage: once an object is stored in it, the schema sync adds the object's columns to an existing table of the same name. The spec's own default (`managed` when a record declares none) is unchanged; only the dialog's starting choice moved. -- **The modes in business words.** The Schema mode select is ordered External, Managed, Validate only, its two main options are relabelled ("External: read an existing database", "Managed: ObjectStack creates and changes the tables"), and a sentence under it says what the chosen mode does to the database and which one to pick to read an existing database. -- **Why a managed row has no Sync.** A managed datasource's row (not the primary `default`) says it has no Sync objects because ObjectStack creates its tables, and how to read an existing database instead. -- **Box defaults are placeholders.** A driver's text or number box with a default (SQLite `Filename` `:memory:`, PostgreSQL `Host` `localhost` and `Port` `5432`, and so on) starts empty with the default as its placeholder, so a typed path is no longer appended to `:memory:`. A new datasource's Test and Create still send the default for a box left empty, so the request is what it was; an edit shows and sends the stored values. Switches and selects keep their defaults as values. -- **The widening.** Four new `studio` namespace rows in `@object-ui/i18n` (en and zh): `engine.datasource.schemaModeExternalHint`, `engine.datasource.schemaModeManagedHint`, `engine.datasource.schemaModeValidateOnlyHint` and `engine.datasource.noSyncManaged`; the en and zh text of `engine.datasource.schemaModeExternal` and `engine.datasource.schemaModeManaged` changes. diff --git a/.changeset/30328-datasource-sync-next-steps.md b/.changeset/30328-datasource-sync-next-steps.md deleted file mode 100644 index 0022b6f1d60..00000000000 --- a/.changeset/30328-datasource-sync-next-steps.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch -'@object-ui/plugin-form': patch ---- - -fix(app-shell): after Setup → Datasources syncs objects, the dialog links each new object and names the two steps left: add it to an app, and grant access - -Clause-②: yes (widening) -Metadata-protocol: none - -- **The result.** When a Sync run creates at least one object, the dialog stays open on the result instead of closing behind a toast: the count line, one link per created object (label, with the name beside it when they differ) that opens the object under the app the page is in, and the two steps left before people can use it, adding it to an app and granting access in a permission set. A run that creates nothing closes the dialog as before. Refused tables are still reported in the error toast. -- **The widening.** Four new `studio` namespace rows in `@object-ui/i18n` (en and zh): `engine.datasource.syncNextStepsOne`, `engine.datasource.syncNextSteps`, `engine.datasource.syncNextStepApp` and `engine.datasource.syncNextStepAccess`. -- **Form labels.** A declared field with no `label` is drawn in the form under its name with the first letter capitalised and underscores as spaces (`part_no` → `Part no`), the rule the list already draws it under, so one field reads the same in both. An authored label is unchanged. diff --git a/.changeset/30329-deployment-setting-reset.md b/.changeset/30329-deployment-setting-reset.md deleted file mode 100644 index ddea9973c38..00000000000 --- a/.changeset/30329-deployment-setting-reset.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/console': patch -'@object-ui/i18n': patch ---- - -fix(console): a value set on a deployment-level settings page reads `Modified`, names the default, and offers Reset to default (#30329) - -Clause-②: yes (widening) — two published language-pack keys in every built-in pack: `console.settingsView.sourceBadgeModified` and `console.settingsView.defaultValueHint` (with `{{value}}`). -Metadata-protocol: none - -A settings page whose manifest declares `scope: 'global'` (Setup's Authentication page is one) writes the deployment's row. A value it had saved was badged `Inherited from Global`, as if it came from somewhere else, and had no Reset to default, so going back meant knowing the default or emptying the box. - -- The page now passes each row the scope its Save writes, resolved the way the server resolves it: the specifier's `scope`, else the manifest's, else the spec default `tenant`. A value whose effective layer is that scope reads `Modified`, names the default layer's value (`Default: 8`; an option's label, Yes or No for a toggle; never a secret's), and offers Reset to default. Reset puts `null` in the draft and Save sends it, like any other change. -- An env-locked value never gets Reset. A value from a layer above the page's scope still reads `Inherited from …`, and the organization and user pages keep their `Overrides …` badges unchanged. -- An unset number box now reads empty after Reset; it kept showing the last number. diff --git a/.changeset/30330-marketplace-readme-fits-page.md b/.changeset/30330-marketplace-readme-fits-page.md deleted file mode 100644 index 5a49e4a6db5..00000000000 --- a/.changeset/30330-marketplace-readme-fits-page.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): a marketplace package's README fits its detail page; badges render as images and pipe tables as tables - -Clause-②: no -Metadata-protocol: none - -On a package's detail page below the `lg` breakpoint, one long line in a README code block widened the whole README past the page's right edge, where it was cut off with no way to scroll: the page's grid cells took their content's width as their minimum. Each cell now may shrink to the page, so long words wrap and only a code block or a table scrolls, inside itself. - -The README renderer also reads three things it printed as literal markup: - -- Images, `![alt](src)` and linked `[![alt](src)](href)` badges, render from an absolute `https:` URL, without sending the console's address as referrer. Any other source (a repository-relative path such as `LICENSE`, `http:`, `data:`, `javascript:`) is left out, alt text included. A linked image keeps its link only for an absolute `http:` or `https:` destination. -- GFM pipe tables render as tables, with the delimiter row's alignment, each in its own horizontal scroller. -- Thematic breaks (`---`, `***`, `___`) between blocks render as a rule. - -Everything else renders as before; release notes in the Versions panel go through the same renderer. diff --git a/.changeset/30331-sqlite-filename-neither-memory-nor-path.md b/.changeset/30331-sqlite-filename-neither-memory-nor-path.md deleted file mode 100644 index d4517be4d4d..00000000000 --- a/.changeset/30331-sqlite-filename-neither-memory-nor-path.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/service-datasource': patch ---- - -fix(service-datasource)!: Test connection and Save refuse a SQLite filename that is neither `:memory:` nor a usable file path, where Test connection used to pass it - -Clause-②: yes (narrowing) — `POST /api/v1/datasources/test`, `POST /api/v1/datasources` and a `PATCH /api/v1/datasources/:name` that touches `config` or `driver` refuse a `sqlite` / `sqlite-wasm` `config.filename` that is empty, starts with `:` without being exactly `:memory:`, or names a path that can be neither opened nor created. The test answered `ok: true` for the first two and the save stored all three; both now answer with the value and the reason (`result.ok: false` from the test, `400 DATASOURCE_ADMIN_ERROR` from the save), the envelope every invalid `config` already gets. -Metadata-protocol: none - - - -**BREAKING** accept-set narrowing at the datasource admin doors (`@objectstack/service-datasource`): a SQLite filename that names no usable database is refused by Test connection and by Save (#30331). - -- **Before.** The New datasource dialog's `:memory:` with a typed path after it (`:memory:/srv/data/parts.db`) tested green. Every layer reads a name that starts with `:` as fileless, so the probe's missing-file check skipped it. Saved, a `sqlite` datasource then opened it as a relative path under the server's working directory whose directory does not exist, so every query failed while the health check answered `false` (which the probe did not read as a failure); a `sqlite-wasm` one opened an ephemeral in-memory database, so writes succeeded and were gone at the next restart. An empty filename tested green too, and a path under a regular file or in a directory the server may not write could be saved. -- **Now.** Test connection answers `ok: false` and Save answers `400 DATASOURCE_ADMIN_ERROR`, under `Invalid configuration for driver '…'`, with a `config.filename:` line that names the value and why: not `:memory:` and not a file path, empty, or the operating system's reason the file cannot be opened or created. -- **Unchanged.** Exactly `:memory:` tests green and saves. An existing database file tests green and saves. A creatable path that does not exist yet is still refused by the test (which never creates it, #27512) and accepted by Save, which creates it on connect. `turso` names its target as a url and is not judged here. - -**The remedy.** Use exactly `:memory:` for an ephemeral database, or the path of a database file with nothing in front of it. diff --git a/.changeset/30334-external-write-read-only-line.md b/.changeset/30334-external-write-read-only-line.md deleted file mode 100644 index 54eb0fed094..00000000000 --- a/.changeset/30334-external-write-read-only-line.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/react': patch -'@object-ui/app-shell': patch -'@object-ui/i18n': patch -'@object-ui/components': patch -'@object-ui/plugin-calendar': patch -'@object-ui/plugin-detail': patch -'@object-ui/console': patch ---- - -fix(core,react,i18n,components,app-shell,console): a write refused because the object's external datasource does not accept writes says the table is read-only, not "you don't have permission" - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@object-ui/core`: `isExternalWriteRefusal(err)`**, re-exported from `@object-ui/react` beside the other write-refusal readers. True when the server refused a write with `EXTERNAL_WRITE_FORBIDDEN`, the ADR-0015 write gate's code for an object bound to an external datasource that does not admit writes (`datasource.external.allowWrites` and `object.external.writable` must both be true). Read from `code`, then `details.code`, like `isPermissionError`. The refusal is a 403, so `isPermissionError` answers true for it too: a caller asks this first. -- **`@object-ui/i18n`: `form.readOnlyExternalSource`**, in all ten packs: "This table is read-only. Its records come from an external data source that does not accept changes." -- **Every surface that met this refusal now says the table is read-only:** the console record form page and the record dialog (`@object-ui/components` form), which said "you don't have permission to save this record"; the record page delete, the list row delete and the record page inline edit, which showed the gate's raw diagnostic; and the calendar's drag-to-reschedule and the record path's stage change. A permission line sent an administrator through permission sets for a grant that cannot exist. diff --git a/.changeset/30335-datasource-delete-names-bound-objects.md b/.changeset/30335-datasource-delete-names-bound-objects.md deleted file mode 100644 index 3b58601fca8..00000000000 --- a/.changeset/30335-datasource-delete-names-bound-objects.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(app-shell): Setup → Datasources names the objects that still use a datasource, in the console's language, before and after its delete - -Clause-②: yes (widening) -Metadata-protocol: none - -- **The confirm.** Opening a datasource's delete reads object metadata and lists every object whose `datasource` names it (label, with the name beside it when they differ). While any is listed, the confirm says the datasource cannot be deleted yet and that each object must be deleted or moved to another datasource first. With none, or when the read fails, the confirm is unchanged. -- **The refusal.** A `400` from `DELETE /api/v1/datasources/:name` re-reads that list. When objects still name the datasource, the toast says, in the console's language, that it was not deleted, which objects still use it, and what to do next. Any other failure, and a `400` with no bound object, keep the server's message in the existing frame. No server text is matched. -- **The widening.** Three new `studio` namespace rows in `@object-ui/i18n` (en and zh): `engine.datasource.deleteBoundBody`, `engine.datasource.deleteBoundRefused` and `engine.datasource.boundObjectSeparator`. diff --git a/.changeset/30335-storage-test-message-locale.md b/.changeset/30335-storage-test-message-locale.md deleted file mode 100644 index 0953a201223..00000000000 --- a/.changeset/30335-storage-test-message-locale.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/service-storage': patch ---- - -fix(service-storage): Setup → Storage → Test connection answers in the caller's language - -Clause-②: no -Metadata-protocol: none - -The `storage/test` settings action writes its `message` in the language the settings action route hands its handler as `locale` (`Accept-Language`, else `?locale`), the same source the mail test notice reads. It covers the sentences the product writes: the success sentence (which still names the adapter), the refusal when the probe read back is not the one written, and the refusal for S3 settings missing a bucket or a region. An adapter's own error text stays verbatim: alone in English, as before; in another language behind a lead-in saying the connection test failed. Every language on the platform locale table is served (`en`, `zh-CN`, `zh-Hant`, `es-ES`, `ar`, `fr`, `pt-BR`, `ru`, `de`, `ja-JP`, `ko`), the caller's code read through `platformLocaleTagOf` and then shortened one subtag at a time; an absent locale, or one the table does not name, is answered in English exactly as before. `ok`, `severity`, the probe and its cleanup, and the operator log lines are unchanged. diff --git a/.changeset/30337-team-page-identity-note-and-counts.md b/.changeset/30337-team-page-identity-note-and-counts.md deleted file mode 100644 index b2f06e5221d..00000000000 --- a/.changeset/30337-team-page-identity-note-and-counts.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(app-shell): a team made in Setup no longer reads as managed by an outside identity provider, and a header action's write shows on the page's related lists and their counts at once - -Clause-②: no -Metadata-protocol: none - -- The `better-auth` note (the Identity tag's tooltip, and the Setup record header's object-label note) read "Managed by the identity provider" / 「由身份提供方管理」, which told the maker of a hand-made team that the row came from elsewhere and was not theirs to change. It now reads "Written through the built-in identity service" / 「经内置身份服务写入」, and its body says these records are created and changed with the actions on the page or through sign-in, invitation and security flows, all of which go through that service. The values change in all ten language packs; no key is added or removed. -- A record-page action that declares `refreshAfter: true` (an `api` action such as a team's Add Member, a server action, a flow, a declared operation) now refreshes the record AND every related list the page shows: the lists refetch and their tab and card counts re-count. It used to refresh the record alone, so a team's members tab read 0 beside a list of 2 until a later row action on that list. -- Nothing to do on upgrade. diff --git a/.changeset/30338-keep-sign-in-language-pick.md b/.changeset/30338-keep-sign-in-language-pick.md deleted file mode 100644 index 021e77a4398..00000000000 --- a/.changeset/30338-keep-sign-in-language-pick.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix: a language picked on the sign-in page is kept when the person who signs in is not the browser's last user (#30338) - -Clause-②: no -Metadata-protocol: none - -On a shared browser, signing in as someone other than the last user clears the previous user's saved state, and that sweep also removed the language just picked in the sign-in page's menu, so the person landed in the browser's language. The pick is now remembered for that page-load and put back after the sweep, so the next page opens in the chosen language. What the previous user left is still cleared when nothing was picked. A pick followed by a reload of the sign-in page, or by an identity provider's redirect, is still lost. Saving the pick as the person's own language is a separate change. diff --git a/.changeset/30338-save-register-language-pick.md b/.changeset/30338-save-register-language-pick.md deleted file mode 100644 index 1f48dc5e51b..00000000000 --- a/.changeset/30338-save-register-language-pick.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/console': patch ---- - -fix(console): a language picked on the register page is saved as the new person's own language when registering signs them in (#30338) - -Clause-②: no -Metadata-protocol: none - -A language picked in the register page's menu used to apply on that browser only: once the sign-up signed the person in, their saved language stayed empty, so another device, or the language their mail is written in, did not follow the pick. The register page now saves the pick to the person's own language setting before it moves on, the way the sign-in page does, on every way it leaves after a sign-up, joining an organization from an invitation link included. `DefaultRegisterPage` does the same for a pick made earlier on another signed-out page. Nothing is saved when no language was picked, a language the person already has is kept, and a save that fails does not hold up the sign-up (the pick still applies on that browser). A pick followed by a reload of the register page, or by an identity provider's redirect, is still not saved. diff --git a/.changeset/30343-timeline-one-action-one-entry.md b/.changeset/30343-timeline-one-action-one-entry.md deleted file mode 100644 index fcc121f7631..00000000000 --- a/.changeset/30343-timeline-one-action-one-entry.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/spec': patch -'@objectstack/rest': patch ---- - -fix(spec,rest): the record page's `timeline` count counts one create or delete written to both the activity and the ledger once, as the 动态 stream draws it (#30343) - -Clause-②: yes (narrowing) — `RecordPageTimelineCountSchema.total` falls for the same rows: with a field change counted, `created` (`deleted`) activity rows now step aside as often as the ledger holds `create` (`delete`) writes, beside the `updated` rows that already did; widening in the same line: `@objectstack/spec/api` exports one more constant, `RECORD_TIMELINE_FOLDED_ACTIVITY_TYPES`. No exported member is removed or renamed. -Metadata-protocol: none — a read answer's count, not anything an author writes. - - - -- **Now.** An audit reader on a record with field history sees the 动态 badge count one action once: one create and one edit read 「动态 2」 above the two rows the stream draws. Before, the create counted twice (3). -- **The rule.** Every `updated` row steps aside, as before. A create (a delete) steps aside as often as both sources hold one: the fewer of the record's `created` (`deleted`) activity rows and its `create` (`delete`) ledger writes. A record created before its history was kept has no `create` write, so its `created` row stays in the count, as the stream draws it. -- **Unchanged.** A member without audit read counts the same activity rows as before. A comment adds one. The total stays an upper bound for the `showCompleted` / `types` and no-diff cases; it can fall one short where the stream does not pair a create's (delete's) two rows (different actors, or stamped more than the stream's pairing window apart). diff --git a/.changeset/30344-changes-chip-audit-read.md b/.changeset/30344-changes-chip-audit-read.md deleted file mode 100644 index 5306dd76864..00000000000 --- a/.changeset/30344-changes-chip-audit-read.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/plugin-detail': patch ---- - -fix(app-shell,plugin-detail): the record page offers its 变更 (Field changes) chip, and the activity panel its Field Changes option, only to a reader whose audit-log read answers - -Clause-②: no -Metadata-protocol: none - -On an object with `enable.trackHistory`, the record page's 动态 (Activity) tab offered the chips 全部 / 活动 / 讨论 / 变更 whenever `sys_audit_log` was registered, whatever this reader was allowed to read. A member the server refuses the audit read to (403) got a 变更 chip that always showed nothing, which reads as "nobody changed this record". The page now offers 变更 only once this reader's `sys_audit_log` read has answered. A refused or failed read offers no 变更 chip, exactly as history off does, and the chip joins the others when the read answers. Nothing else waits on the read: the page payload still asks the field changes as before, and the stream's count already counts a refused audit read as no field change. - -The activity panel's filter dropdown follows the same rule: it offers Field Changes (`changes_only`) only when the stream would offer its 变更 chip. A host that says nothing keeps every option, as before. A `changes_only` filter that is no longer offered reads as All Activity. diff --git a/.changeset/30347-spec-seed-locale-and-queue-refuse-wording.md b/.changeset/30347-spec-seed-locale-and-queue-refuse-wording.md deleted file mode 100644 index 052929cbe9d..00000000000 --- a/.changeset/30347-spec-seed-locale-and-queue-refuse-wording.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(spec): `Seed.locale` says which loads filter on it, and the ownership-queue contract says the registered service honours `onFailedRead: 'refuse'` - -Clause-②: no -Metadata-protocol: none - -Wording only: no schema shape, no type and no behaviour changed. - -- **`Seed.locale`** (docblock and `.describe()`, which the references pages render). It no longer says that installing is locale-neutral. The boot seed and the dev hot-reload seeder filter on the app's `i18n.defaultLocale`. The marketplace install, the sample-data reseed, the restart heal and the per-organization replay filter on the locale `resolveSeedLocaleForLoad` (`@objectstack/runtime`) resolves for the load, first answer wins: the `localization.locale` setting, counted only when somebody set it (not `source: 'default'`); then the `sys_user.locale` of the user who installs, reseeds or founds the organization; then the app's `i18n.defaultLocale` where the load names one. Package apply and draft publish still pass no locale: they load every dataset and warn. -- **`OwnershipQueueFailedReadAnswer` and `IOwnershipQueueService.isMember`** (`@objectstack/spec/contracts`). The registered service, `@objectstack/plugin-sharing`'s, honours `'refuse'`; the "until then a caller passing `'refuse'` still gets `'nobody'`" clause is gone. The activation-ledger read keeps reading "switched on" when it fails, under both answers, and the contract now states that as decided. `isMember` under `'refuse'` reads the queue's own rows (`sys_queue`, `sys_queue_member`) first and rejects at once when one fails; past them it answers `true` as soon as a successful read reaches the user, even beside a failed read, and rejects only when no successful read reaches the user and one failed. diff --git a/.changeset/30348-seed-receipt-landed-ids.md b/.changeset/30348-seed-receipt-landed-ids.md deleted file mode 100644 index 03071ea59d2..00000000000 --- a/.changeset/30348-seed-receipt-landed-ids.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/metadata-protocol': minor -'@objectstack/runtime': patch ---- - -feat(spec,metadata-protocol): the publish receipt's `seedApplied.landed` names the rows a seed created and the rows it only matched, per object, capped at 200 ids per list - -Clause-②: yes (widening) -Metadata-protocol: added - -- **`@objectstack/spec`: `seedApplied.landed`** on both publish responses (`PublishMetaItemResponseSchema`, `PublishPackageDraftsResponseSchema`), one shared schema `SeedAppliedLandedSchema`: per object, `inserted` (the rows this publish created) and `matched` (pre-existing rows a seed record's key matched: updated, left as they were, or refused an update), each a `SeedAppliedIdListSchema` `{ ids, total, truncated }`. `ids` carries at most `SEED_APPLIED_LANDED_IDS_CAP` (200) ids, in the loader's record order (datasets in load order, records as authored); past it `truncated` is `true` and `total` is the uncut count. Absent `landed` means "not reported" (a load that threw, or a producer that reports no ids), never "nothing landed". Only `inserted` names rows the seed created: a `matched` row may be a customer's own record that shares the seed's natural key, and under `ignore` (the default) such a row is matched without being written. -- **`@objectstack/spec`: `SeedLoadResult.insertedIds`**, the ids among `landedIds` whose row the load created. Per load: a row an earlier load inserted is matched on a replay, so a reader tracking a seed's rows across publishes accumulates `insertedIds` over every load. `landedIds`' TSDoc now says what each seed mode puts in it. -- **`@objectstack/metadata-protocol`**: `applySeedBodies` returns `landed`, so `publishPackageDrafts` and `publishMetaItem` both carry it; the loader records which landed rows it inserted. `seedAppliedLanded(results)` is exported, the one projection both receipt producers fold the loader's ids through. -- **`@objectstack/runtime`**: the package-publish door's fallback seed apply (for protocols that do not self-apply) reports the same `landed`. -- Accepted cost (objectstack-ai/cloud#3598 ruling A): row ids appear on the public publish responses, bounded by the cap. diff --git a/.changeset/30352-register-saml-idp-entity-id.md b/.changeset/30352-register-saml-idp-entity-id.md deleted file mode 100644 index 9633597d58e..00000000000 --- a/.changeset/30352-register-saml-idp-entity-id.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix(plugin-auth): Setup's Register SAML Provider registers a complete form; it answered 400 for every one - -Clause-②: no — a form the bridge always refused registers; nothing a caller sends today is refused -Metadata-protocol: none - -- **Before.** `POST /api/v1/auth/admin/sso/register-saml` (Setup → SSO Providers → Register SAML Provider) answered `400 SAML_REGISTER_FAILED` "[body.samlConfig.idpMetadata] idpMetadata.entityID is required when IdP metadata XML is not provided" for every complete form. The bridge sent `samlConfig` with no IdP descriptor, which `@better-auth/sso` requires when no IdP metadata XML is given, so no SAML provider could be registered from Setup. -- **Now.** The bridge sends `samlConfig.idpMetadata.entityID` from the form's IdP Entity ID field (`issuer`), and a complete form registers the provider (one `sys_sso_provider` row) and answers the SP ACS and metadata URLs as before. The form's fields, the required set and the refusal of an incomplete form (`400 INVALID_REQUEST` naming the missing field) are unchanged. The IdP SSO URL and signing certificate stay where they were (`samlConfig.entryPoint`, `samlConfig.cert`). diff --git a/.changeset/30358-allowed-visualization-chip-words.md b/.changeset/30358-allowed-visualization-chip-words.md deleted file mode 100644 index 41fbdbe2aaf..00000000000 --- a/.changeset/30358-allowed-visualization-chip-words.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix: on a zh-CN console, the allowed-visualizations chips in a view's settings read the same Chinese words as the view-type select (#30358) - -Clause-②: no -Metadata-protocol: none - -The 允许的可视化 chips (`appearance.allowedVisualizations`) were the only English row left in a list view's end-user section on a zh-CN console: they read Grid, Kanban, Gallery, Calendar, Timeline, Gantt, Map, Chart, Tree. They now read 表格 / 列表, 看板, 画廊, 日历, 时间线, 甘特图, 地图, 图表, 树形, the words the view-type select a few rows up already uses. Both controls take the name from one shared helper, so they cannot drift apart. An en-US console keeps the chip names it had. A multi-select over any other list of values keeps its existing names in every language. diff --git a/.changeset/30366-forgot-password-no-mail.md b/.changeset/30366-forgot-password-no-mail.md deleted file mode 100644 index b21858adf29..00000000000 --- a/.changeset/30366-forgot-password-no-mail.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/auth': patch -'@object-ui/i18n': patch -'@object-ui/app-shell': patch -'@object-ui/console': patch ---- - -fix(auth): the forgot-password page no longer says a reset link was sent on a deployment that sends no mail (#30366) - -Clause-②: yes (widening) -Metadata-protocol: none - -`GET /api/v1/auth/config` reports `features.emailDelivery`: `false` when no email service is wired or the stock `log` transport only writes mail to the server console. On such a deployment the forgot-password form used to answer every request with "Check your email — we've sent a password reset link", although no mail leaves. When the server reports `emailDelivery: false`, the success screen now says a reset link can't be emailed here and to ask an administrator, who can set a new password from Setup → Users. - -The text describes the deployment, never the address: it carries no address, and a known and an unknown address read the same, so the page adds no account-existence oracle. Only an explicit `false` changes it; a server that predates the flag (no `emailDelivery` key) or a failed config fetch keeps the "check your email" screen, since those deployments may well send mail. - -`ForgotPasswordFormLabels` gains `noDeliveryTitle` and `noDeliveryDescription`, and `AuthPublicConfig.features` gains `emailDelivery`. New strings `auth.forgotPassword.noDeliveryTitle` and `auth.forgotPassword.noDeliveryDescription` in all ten locales, passed by the Console's forgot-password page and `DefaultForgotPasswordPage`. diff --git a/.changeset/30370-external-read-only-effective-permissions.md b/.changeset/30370-external-read-only-effective-permissions.md deleted file mode 100644 index bb6a3c4ff7e..00000000000 --- a/.changeset/30370-external-read-only-effective-permissions.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/core': patch -'@objectstack/objectql': patch -'@objectstack/plugin-security': patch -'@objectstack/plugin-hono-server': patch ---- - -fix(core,objectql): effective object permissions read the external write gate, so a read-only external object is no longer offered New, Import, Edit or Delete - -Clause-②: yes (widening) -Metadata-protocol: none - -- **Behaviour.** On an object whose external write gate is closed (ADR-0015 §5.3 Gate 3: a federated datasource, and not both `datasource.external.allowWrites` and `object.external.writable`), every insert, update and delete is refused `403 EXTERNAL_WRITE_FORBIDDEN` for every caller. The effective object-permission map now says so: the entry grants no `allowCreate`, `allowEdit`, `allowDelete` or `allowTransfer`, a `modifyAllRecords` on it is lowered to the `viewAllRecords` read it implies, and `apiOperations` carries no write operation (`create`, `update`, `delete`, `upsert`, `bulk`, `import`, `restore`, `purge`). Read and export are unchanged. Both consumers move together: `/auth/me/permissions` (the console hides those doors with no client change) and `ISecurityService.getEffectiveObjectPermissions`, so `current_user.can(object, 'create' | 'edit' | 'delete' | 'transfer')` now answers `false` on such an object, as the write itself always did. -- **`@objectstack/objectql`: `ObjectQL.isExternalWriteForbidden(objectName)`**, the gate's verdict as a read-only question. The write gate (`assertWriteAllowed`) reads the same verdict, so the two cannot disagree. -- **`@objectstack/core`: `engineExternalWriteForbidden(engine)`** (the accessor over an engine, asked when the member is present) and the optional `isExternalWriteForbidden` accessor on `EffectiveObjectPermissionsSchemaSource`; `annotateEffectiveApiOperations` takes the same predicate as an optional third argument. A caller that hands over neither gets the map it got before. diff --git a/.changeset/30377-extension-rollup-recompute-on-enable.md b/.changeset/30377-extension-rollup-recompute-on-enable.md deleted file mode 100644 index cb93fe2206a..00000000000 --- a/.changeset/30377-extension-rollup-recompute-on-enable.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/objectql': patch -'@objectstack/runtime': patch ---- - -fix(objectql): a roll-up summary a disabled package adds by extension is recomputed when the package is enabled again - -Clause-②: no — a stored summary column stays current across a disable; nothing a caller sends is refused -Metadata-protocol: none - -- **Before.** While a package was disabled, the `summary` fields its `objectExtensions` add to another package's object were withheld from the roll-up index with the rest of the served fold. When the summary's child belongs to an enabled package, the child writes made during the disable landed and recomputed nothing, so after `PATCH /packages/:id/enable` the parent served the value the summary held at the disable, until the next write of one of its children. -- **Now.** Enabling a disabled package recomputes, on every parent row, the roll-ups it adds by extension, and writes a row only where its stored value differs. It runs on every enable door (`SchemaRegistry.onPackageEnabled`, new), and `PATCH /packages/:id/enable` (and an install's `enableOnInstall: true`) answers after it has settled. A row that cannot be recomputed is logged at `error` and the rest of the walk goes on; `ObjectQL.refreshSummariesEnabledBy(packageId)` (new) re-runs it. diff --git a/.changeset/30383-field-path-rules-extension-fields.md b/.changeset/30383-field-path-rules-extension-fields.md deleted file mode 100644 index 0a2e6e5ed76..00000000000 --- a/.changeset/30383-field-path-rules-extension-fields.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/lint': patch ---- - -fix(lint): the field-path rules judge an object as the registry serves it, so a view, report, dataset, page, widget, mapping, related list or row policy naming a field that an `objectExtensions` entry adds is no longer refused (#30383) - -Clause-②: yes (widening: field-path rules accept fields a sibling package adds through `objectExtensions`, which the runtime already serves; any rule that deliberately stays on declared fields says why in its header) -Metadata-protocol: none (lint judgements, no authorable key) - -- **Before.** Twelve rules resolved a field path against the objects' declared `fields` only: dataset fields and filters, report runtime filters, dashboard widget filters, list view columns and keys, an object's own field lists, page bindings, filter preset comparands, RLS predicates, sharing-rule criteria, thank-you panels, measure aggregates and the field-consumer credit for dataset paths. On a multi-package artifact, where a module package adds fields to an app package's object through `objectExtensions`, a path naming one of those fields read as unknown (for example `dataset-field-unknown`, `list-view-field-unknown`, `rls-predicate-unknown-field`) although the runtime serves the field. Two rules folded extensions through readers of their own: `related-list-ref-unknown` read only the stack's top-level `objectExtensions`, so an inbound lookup a sibling package adds was not seen, and `mapping-target-field-unknown` kept every entry, where the registry keeps one per package per object. -- **Now.** Every one of them reads the one shared fold in `object-graph.ts`: the object's declared fields plus every field an `objectExtensions` entry of any body of the artifact adds, one entry per package per object, a later entry replacing an earlier one. A field no body declares or adds keeps the same refusal, with the same rule id, severity and message. The rules that judge a field's type (measure aggregates, preset comparands, sharing-rule criteria, thank-you panels) judge an extension-added field on its served definition, as they judge a declared one. A field two packages write at the same deciding priority is still a served name, but which definition wins is registration order, so the rules that judge its type leave it unjudged, and a thank-you panel admits it only when every such definition is engine-generated. diff --git a/.changeset/30390-served-object-label-record-surfaces.md b/.changeset/30390-served-object-label-record-surfaces.md deleted file mode 100644 index 84d2d98215b..00000000000 --- a/.changeset/30390-served-object-label-record-surfaces.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/fields': patch -'@object-ui/plugin-form': patch -'@object-ui/plugin-gantt': patch -'@object-ui/plugin-grid': patch -'@object-ui/components': patch -'@object-ui/console': patch ---- - -fix(app-shell,fields,plugin-form,plugin-gantt,plugin-grid,components,console): record pages, forms, drawers and preview cards draw a served object label as served - -Clause-②: no -Metadata-protocol: none - -- **Now.** The record page (its title, breadcrumbs, related-list titles, delete and access-denied copy), the record form page and the console's create/edit dialog (title, save toast, way back to the list), the record drawer, the record preview card, the hover card over a lookup, a record-pointer cell, an object-ref cell, the closed-form notice, the gantt name column, the page header's record chrome, a form view's heading, the AI dock's surface chip, the Activity row and the report index name an object by the label the server served, as given. The server has already translated it, by its label order for an overridden item, so a `.custom` object extension's rename is kept over the base package's translation. Before, the served label reached the client's catalog lookup only as its fallback, so the base package's translation of the old name won on these surfaces after the list and header were fixed. -- **Unchanged.** When no label is served, the catalog key is asked first and the machine name (or its humanized form) stays the last resort, as before. An unedited object shows the same text as before, because the server serves the catalog's translation for it. Related lists and reference rails that only know an object's name, and the packaged-actions table (its object list is read without the session's language), still look the name up in the catalog. English is unchanged. diff --git a/.changeset/30391-extension-rename-drops-stale-plural.md b/.changeset/30391-extension-rename-drops-stale-plural.md deleted file mode 100644 index 0535257e983..00000000000 --- a/.changeset/30391-extension-rename-drops-stale-plural.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/objectql': minor -'@objectstack/rest': minor ---- - -fix(objectql,rest)!: an object extension that renames `label` and sets no `pluralLabel` is served without the base's plural, so list titles stop showing the old name (#30391) - -Clause-②: no (narrowing) — `/meta/object` (item and list reads, both transports) and every registry read of an object (`SchemaRegistry.getObject`, the fold) stop serving `pluralLabel` for an object an extension renames without a plural of its own, where they served the base's plural (and, in a translated locale, the base's translation of it). The shipped showcase's `showcase_account` is one: its extension renames it `Account (Success Overlay)` and declares no plural. Nothing an author writes is refused. -Metadata-protocol: semantic -Skills-docs: follow-up — `skills/objectstack-data/SKILL.md` "Object Extension Model" says an extension "can add but never remove"; a renaming `label` now takes the base's `pluralLabel` off. The sentence wants that exception (outside this change's surface). - - - -**BREAKING** served-value narrowing for an object an extension renames. - -- **Before.** An `objectExtensions` entry or a `.custom` `object_extension` item that set `label` and no `pluralLabel` was folded with the new label and the base's `pluralLabel`. A translated read served the base package's translation of that plural (`objects.NAME.pluralLabel`), so a list title, read plural first, kept the old name in every locale. -- **Now.** `pluralLabel` is the plural of `label`: such an extension is served as an object that declares no `pluralLabel`, in every locale, so a list titles it by its label. Where the owning package's `.custom` pair saves its own translation of `objects.NAME.pluralLabel`, that translation is served (ruling record 6631250894: a translation in the pair, then the override's own text, then the base's translation). -- **Unchanged.** An extension that sets `pluralLabel` is served with it. One that sets no `label`, or repeats the label it extends, keeps the base's plural and its translation. A plural the tenant set stays, and a tenant's own rename yields as before (ADR-0029 D9.2a). An object no extension renames is served as before. The packaged base the translators compare against keeps its plural. - -Nothing has to be rewritten: an extension that wants a plural for its new name declares `pluralLabel` beside `label`, as it always could, and a `.custom` pair can save `objects.NAME.pluralLabel` in its own `translation` item for a translated locale. diff --git a/.changeset/30394-non-grid-bulk-bar-declared-actions.md b/.changeset/30394-non-grid-bulk-bar-declared-actions.md deleted file mode 100644 index d4cc5c07dc9..00000000000 --- a/.changeset/30394-non-grid-bulk-bar-declared-actions.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/plugin-list': minor -'@object-ui/plugin-grid': minor ---- - -fix(plugin-list)!: a list's selection bar over a non-grid view (kanban, calendar, gallery, …) draws only actions the object declares, by the grid's own rule; an undeclared bare `bulkActions` name draws nothing and warns once in development (#30394) - -Clause-②: yes (narrowing) — `@object-ui/plugin-grid` exports `resolveBulkActions` and `useUndeclaredActionNamesWarning` (with the `ActionLabelResolver`, `NamedActionDef`, `UndeclaredActionNameKey`, `UndeclaredActionNamesAddress` and `UndeclaredActionNamesBasis` types) so `@object-ui/plugin-list` reuses the grid's fold and warning instead of a second copy; the warning's address gains an optional `component`, defaulting to `ObjectGrid`, so the grid's message is byte-identical. -Metadata-protocol: narrowed -Skills-docs: no move — `skills/**` teaches no handler-only bulk action name (searched for `bulkActions` examples); `content/docs/ui/**` already names only declared actions. - - - -- **Before.** The bar ListView draws itself for every view but the grid mapped each `bulkActions` member straight to a button, labelled by the humanized name, whether or not the object declared it. A name the object does not declare (a misspelling, or an action a disabled package withholds from the served object) showed a button that toasted "not executable" on click. The grid stopped drawing such names in #30169. -- **Now.** The bar folds the names through the grid's `resolveBulkActions`: a name the object declares draws under that action's label (through the app's i18n bundle), the built-in `delete` keeps its own path and gate, and any other name draws nothing. In development the console logs one warning per list naming the key, the view and each name (`[ObjectUI] ListView bulkActions: kanban (objectName: …) — …`), the grid's own message. A bar with nothing left to draw is not shown. -- **Remedy.** Declare the named action on the object, or remove the name from `bulkActions`. An action withheld by a disabled package needs nothing: it draws again when the package is enabled. diff --git a/.changeset/30396-time-relative-condition-object.md b/.changeset/30396-time-relative-condition-object.md deleted file mode 100644 index 5041eaae8c0..00000000000 --- a/.changeset/30396-time-relative-condition-object.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/service-automation': patch -'@objectstack/mcp': patch ---- - -fix(service-automation,mcp): a time-relative flow's conditions are typed against the object its sweep scans, at registration and in the `validate_expression` advice - -Clause-②: no -Metadata-protocol: none - -- **`@objectstack/service-automation`**: the registration pass's advisory schema check (the logged `unknown field` / typo / type-mismatch warnings) typed a time-relative flow's conditions against the start node's `config.objectName`, while the run binds a row of `config.timeRelative.object`. A flow stored with a start naming another object kept a warning on a field that is right for the scanned object and none on a field that fails at run time; a start naming no `objectName` got no schema check at all. The pass now reads the object off the flow's trigger binding, the same answer the run uses: `timeRelative.object` when it is a string, `config.objectName` only otherwise. Every other flow is typed as before. The warnings stay logged, never thrown. -- **`@objectstack/mcp`**: `validate_expression`'s description, its `sweepObject` input text and the SKILL.md line no longer advise passing a `sweepObject` other than `objectName` as a supported shape. They say to pass the scanned object as `objectName`, and that a start whose `objectName` names another object is refused at publish (`flow-time-relative-object-mismatch`). The tool's inputs and verdicts are unchanged. diff --git a/.changeset/30398-caller-address-read.md b/.changeset/30398-caller-address-read.md deleted file mode 100644 index 7d493d03326..00000000000 --- a/.changeset/30398-caller-address-read.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -feat(plugin-auth): a platform admin can read the address the allowed IP ranges save check will judge, before saving, at `GET /api/v1/auth/admin/client-address` (#30398) - -Clause-②: yes (widening) — a new authenticated HTTP read on the auth mount, ledgered `server-only` in the plugin's auth route ledger; no spec schema, SDK method or package export moves. -Metadata-protocol: none - -A save of `auth.allowed_ip_ranges` (ADR-0069 D5) is refused when the list leaves out the address the saving request comes from, and the refusal names that address. Until now that refusal was the only place the address appeared, so an administrator learned which address counts by being refused. The new read answers it beforehand. - -- **The answer.** `200 { success: true, data: { clientAddress, trustProxy } }` with `Cache-Control: no-store`. `clientAddress` is the caller's address exactly as the save check and the sign-in gate judge it, through the same call the save check makes: the forwarded `X-Forwarded-For` / `X-Real-IP` address only when the stack declares `server.trustProxy`, the connection's peer address otherwise, in the same text (an IPv4-mapped peer reads `::ffff:127.0.0.1`, as the refusal names it). It is `null` when the server cannot establish an address; while any list is set, such a connection is refused at sign-in and any non-empty list at the save. `trustProxy` says whether forwarded headers counted. -- **Who may read it.** A platform admin, through the gate every ObjectStack `/admin/*` auth route uses: no session answers `401 UNAUTHENTICATED`, a session held by the authentication policy (an expired password, required two-factor not set up) answers `403` with that posture's code, and any other caller `403 PERMISSION_DENIED`. It is not served to anonymous callers, and `GET /api/v1/auth/config` is unchanged. -- **Closed query set.** The route takes no query parameters; any one is refused with `400 VALIDATION_ERROR`. -- **Behind the allow-list.** Like every auth route except the sign-in page's two render reads, the read is judged by the allow-list in force: a caller already outside it gets `403 IP_NOT_ALLOWED`. - -The save check, the sign-in gate and the address rule are unchanged: the save check now reaches the rule through the helper the read shares, with the same inputs. diff --git a/.changeset/30399-save-sign-in-language-pick.md b/.changeset/30399-save-sign-in-language-pick.md deleted file mode 100644 index 55c6c0725d0..00000000000 --- a/.changeset/30399-save-sign-in-language-pick.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/console': patch ---- - -fix(console): a language picked on the sign-in page is saved as the person's own language when they have none (#30399) - -Clause-②: yes (widening) -Metadata-protocol: none - -A language picked in the sign-in page's menu used to apply on that browser only: after signing in, the person's saved language stayed empty, so another device, or the language their mail is written in, did not follow the pick. The sign-in page now saves the pick to the person's own language setting before it moves on, when they have not chosen one yet. A language they already chose is kept, nothing is saved when no language was picked on the page, and a save that fails does not hold up the sign-in (the pick still applies on that browser). `@object-ui/app-shell` gains `hasSignInLanguagePick`, `saveSignInLanguagePick` and the `SignInLanguagePickWriter` type for the sign-in page. A pick followed by a reload of the sign-in page, or by an identity provider's redirect, is still not saved. diff --git a/.changeset/30400-sqlite-filename-names-a-database.md b/.changeset/30400-sqlite-filename-names-a-database.md deleted file mode 100644 index 626fc530529..00000000000 --- a/.changeset/30400-sqlite-filename-names-a-database.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/service-datasource': minor ---- - -fix(spec)!: a SQLite datasource's `config.filename` must name a database: `''` and a name that starts with `:` without being exactly `:memory:` are refused by the stack parse and the metadata save door, not only by the datasource admin doors - -Clause-②: no (narrowing) — `SqliteConfigSchema.filename` and `SqliteWasmConfigSchema.filename` refuse `''` and any `:`-prefixed value other than exactly `:memory:`, so `defineStack` / `os validate` / `os build` and `PUT /api/v1/meta/datasource/NAME` refuse a `sqlite` / `sqlite-wasm` datasource they accepted before. Nothing is widened. -Metadata-protocol: narrowed -Skills-docs: none — `skills/**` teaches no SQLite `filename` value, and `content/docs/**` teaches only `:memory:` and file paths, which stay accepted. The prescription ships in the refusal text, the FROM → TO below and the upgrade-guide entry. - - - -**BREAKING** accept-set narrowing on `SqliteConfigSchema.filename` and `SqliteWasmConfigSchema.filename` (`@objectstack/spec`), and so at every door that judges a datasource against the spec: `defineStack`, `os validate`, `os build` and `PUT /api/v1/meta/datasource/NAME`. - -- **Before.** Both schemas typed `filename` as any placeholder-free string. `:memory:/srv/data/parts.db` (the New datasource dialog's prefilled `:memory:` with a path typed after it) and `''` parsed. Every layer reads a name that starts with `:` as fileless, so no file was ever opened: a `sqlite` datasource opened it as a relative path whose directory does not exist and every statement failed, and a `sqlite-wasm` one opened a throwaway in-memory database whose writes were gone at the next connect. The datasource admin doors (Test connection, create, update) already refused both; a datasource declared in a stack or saved through the metadata door was stored anyway, and the admin door's update would not take it back. -- **Now.** The parse answers one issue at `filename` (`config.filename` on a datasource, `datasources.N.config.filename` in a stack) that names the value, says why and prescribes the fix: `'…' is neither ':memory:' nor a file path. A name that starts with ':' is not read as a file, so this datasource would store nothing: use exactly ':memory:' …`, or `'' is empty and names no database file. …`. `defineStack` raises it as `STACK_SCHEMA_INVALID` (422); the metadata door answers `422 INVALID_METADATA`. -- **Unchanged.** Exactly `:memory:`, an absent `filename` (still defaulting to `:memory:`), and relative, absolute, Windows-drive and `file:` paths parse as before. Whether a path can be opened or created is still judged only at runtime, by the datasource admin doors; the schema cannot touch the file system. -- **`@objectstack/service-datasource`.** The admin doors give the same answers as before, word for word. The two arms the string alone decides now come from the driver-contract parse they already run, so `sqlite-database-filename.ts` keeps only its filesystem arm: one definition, in the spec. -- **`@objectstack/spec`.** The upgrade-guide entry `sqlite-filename-names-no-database-refused` (a semantic migration: no automatic rewrite, because whether the author meant an ephemeral database or the path typed after the prefix is a judgement about the datasource). - -**FROM → TO.** `config: { filename: ':memory:/srv/data/parts.db' }` → `filename: '/srv/data/parts.db'` to keep the data in that file, or `filename: ':memory:'` for an ephemeral database; `filename: ''` → `':memory:'` or a file path. diff --git a/.changeset/30407-test-connection-reads-health-answer.md b/.changeset/30407-test-connection-reads-health-answer.md deleted file mode 100644 index 4fa27867f4a..00000000000 --- a/.changeset/30407-test-connection-reads-health-answer.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/service-datasource': patch ---- - -fix(service-datasource): Test connection answers `ok: false` when the driver connects but its own health check answers `false` (#30407) - -Clause-②: no — a served answer comes to match the driver's own health answer; no accept set changes. -Metadata-protocol: none - -The datasource probe awaited the handle's `ping` (the driver's `checkHealth()`) and dropped its boolean, so a connect that only warned and then reported itself unhealthy answered Test connection `ok: true`. The probe now reads that answer: a literal `false` answers `ok: false` with a sentence saying the driver connected but its health check reported the database as unhealthy. A connect or ping that throws keeps answering the thrown message, and a handle whose `ping` answers nothing keeps answering `ok: true`. diff --git a/.changeset/30410-kanban-external-read-only-line.md b/.changeset/30410-kanban-external-read-only-line.md deleted file mode 100644 index afe49ad55ad..00000000000 --- a/.changeset/30410-kanban-external-read-only-line.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@object-ui/plugin-kanban': patch ---- - -Clause-②: no -Metadata-protocol: none - -A kanban card move refused because the object's external datasource does not admit writes (`403 EXTERNAL_WRITE_FORBIDDEN`, ADR-0015) now says the table is read-only (`form.readOnlyExternalSource`) instead of "You are not authorized to perform this action.", which sent administrators looking through permission sets that can never unlock the table. The read-only check runs after an author-marked `userMessage` and before the permission check, the order the other write doors use; every other 403 keeps today's text. diff --git a/.changeset/30411-org-require-mfa-follow-ups.md b/.changeset/30411-org-require-mfa-follow-ups.md deleted file mode 100644 index ff513d46c61..00000000000 --- a/.changeset/30411-org-require-mfa-follow-ups.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/plugin-auth': patch -'@objectstack/platform-objects': patch ---- - -fix(plugin-auth,platform-objects): an organization's require-MFA refusal reads in the caller's language, an accepted save that turns it on names the grace period in force, and the field help says two-factor must be on (#30411) - -Clause-②: no -Metadata-protocol: none - -- **Before.** Saving `sys_organization.require_mfa` on while two-factor enrollment was off was refused with an English sentence, whatever language the administrator's console used. An accepted save that turned it on answered nothing about when members would be blocked, and the configured grace period (`auth.mfa_grace_period_days`) was readable only through the `auth` settings, which need `manage_platform_settings`, a capability an organization administrator does not hold. The field help did not say the requirement needs two-factor on. -- **Now.** The refusal is in the request's language (English, Simplified Chinese, Japanese, Spanish; matched from `Accept-Language` exactly, then by language, else English), with the same shape as before: `400 VALIDATION_FAILED`, one `require_mfa` / `invalid_value` field entry carrying the same sentence as the top-level message. A save that is admitted and turns the requirement on (two-factor on, the stored value off, or an insert) answers a save `notice` on the create or update answer, in the same language: every member without an authenticator app must enroll, within the grace period in force, in days, read per write from the same setting the MFA gate's clock holds to. A save that turns nothing on (clearing it, re-saving a stored `true`, writing other columns) answers no notice. The field help, in all four languages, says the requirement needs two-factor authentication on for the deployment (`OS_AUTH_TWO_FACTOR=true`) and that saving it on is refused while it is off. -- **Unchanged.** Which writes are refused and which are admitted, the refusal's code, status and field entry, the MFA gate and its grace clock, and every `auth` setting and its read permission. No member is added to any answer: `notice` is the create and update answers' existing optional member. diff --git a/.changeset/30418-settings-toggle-confirm-off.md b/.changeset/30418-settings-toggle-confirm-off.md deleted file mode 100644 index 84350dc5f69..00000000000 --- a/.changeset/30418-settings-toggle-confirm-off.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/service-settings': patch ---- - -feat: a settings toggle can declare the question asked before it is turned off (`confirmOffText`), and Setup's email and password sign-in toggle declares one - -Clause-②: yes (widening) -Metadata-protocol: added - -- `SpecifierSchema` (`@objectstack/spec/system`) gains an optional `confirmOffText: I18nLabel`: the confirm dialog text shown before a toggle's value changes to off. Omitted, nothing is asked, so every existing manifest reads as before. The name mirrors the `confirmText` an `action_button` handler already declares. -- Only a `toggle` may declare it. On any other specifier type the parse refuses it at `confirmOffText`, naming the type, because nothing would ever ask the question there. -- The platform translation bundle carries it at the toggle's own address, beside its label: `settings..keys..confirmOffText`. -- `@objectstack/service-settings`: the `auth` manifest's `email_password_enabled` toggle declares "People who sign in with a password will need another way in.", with zh-CN, ja-JP and es-ES text in the shipped bundles. The settings translation coverage test now holds a declared `confirmOffText` to the same per-locale coverage as the toggle's label. -- The console asking the question before save is a separate change; until it lands, the key is declared and parsed but not yet shown. diff --git a/.changeset/30420-dev-admin-chip-password-off.md b/.changeset/30420-dev-admin-chip-password-off.md deleted file mode 100644 index a3fb69649c1..00000000000 --- a/.changeset/30420-dev-admin-chip-password-off.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/console': patch ---- - -fix(console): the sign-in page no longer offers "Sign in as dev admin" when email and password sign-in is off - -Clause-②: no -Metadata-protocol: none - -Under `os dev`, the sign-in page drew the dev-seeded admin row whenever `/api/v1/auth/config` reported `devSeedAdmin`. Its chip signs in with email and password, so with that sign-in turned off the server refused it (`EMAIL_PASSWORD_DISABLED`) while the rest of the page correctly offered only the configured providers. The row is now drawn only when the same config does not report `emailPassword.enabled: false`, the answer the sign-in form already reads. diff --git a/.changeset/30421-team-member-related-list-columns.md b/.changeset/30421-team-member-related-list-columns.md deleted file mode 100644 index b468bf9d947..00000000000 --- a/.changeset/30421-team-member-related-list-columns.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/platform-objects': patch ---- - -fix(platform-objects): a team membership is named by the person and the team, and a team's Members list shows the user and when they joined, not two raw ids - -Clause-②: no -Metadata-protocol: none - -- **The record title.** `sys_team_member`'s title (`display_title`) composed its two stored lookup ids, so a team's Members panel, a membership's own page and every reference to the row read `USER_ID in TEAM_ID`. It now reads the user's `name` (their `email` where the name is blank) and the team's `name`, one hop through each lookup (ADR-0079 addendum, 2026-10-18), as `sys_business_unit_member`'s title does: `Ada Lovelace in Core Platform`. Both hops are read as the reader: a user or team the reader may not read, or one that no longer exists, leaves the title empty rather than showing an id. No column is stored, so a renamed user or team is named anew on the next read. -- **The team page's Members list.** A related list with no authored columns leads with the record title (ADR-0079), so the Members list opened on a Title column beside the User column. The `team_id` lookup now declares `relatedListColumns: ['user_id', 'created_at']`: the list shows the user and when the membership began, drawn as declared, with no title column. -- **A user's Teams list.** The `user_id` lookup declares `relatedListColumns: ['team_id', 'created_at']`, for a user page that derives its related lists. Setup's own `sys_user_detail` page is authored and lists no teams, so it is unchanged. diff --git a/.changeset/30429-seed-receipt-landed-on-throw.md b/.changeset/30429-seed-receipt-landed-on-throw.md deleted file mode 100644 index 76c1d065f69..00000000000 --- a/.changeset/30429-seed-receipt-landed-on-throw.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/metadata-protocol': minor -'@objectstack/runtime': patch ---- - -fix(metadata-protocol,runtime): a seed load that throws part-way still reports `seedApplied.landed` for the datasets that finished before the throw - -Clause-②: yes (widening: a thrown load's receipt carries `landed` for the datasets that finished, where it carried none) -Metadata-protocol: semantic - -- **`@objectstack/spec`: `seedApplied.landed`** (both publish responses) now says what it covers on a load that threw part-way (`success: false` with `error`): only the datasets that FINISHED before the throw. The dataset that threw is not listed (its rows are in `inserted` / `updated`, its object in `error`). Absent `landed` now means the apply failed before any load ran, or the producer reports no ids; a throw before any dataset finished reports `landed: []`. -- **`@objectstack/metadata-protocol`**: `SeedLoadPartialWrites` gains `finished`, the finished datasets' `object` / `landedIds` / `insertedIds` in load order, and `applySeedBodies`' catch folds it through the same `seedAppliedLanded` projection (same two lists, same 200 cap) the returned path uses. `seedLoadPartialWrites` and `SeedLoadPartialWrites` are exported from the package root. -- **`@objectstack/runtime`**: the package-publish door's fallback seed apply reads the same `seedLoadPartialWrites` off a thrown load and reports the same `landed`, plus the `inserted` / `updated` counts the response schema requires (it reported neither before) and the object whose seed stopped the load in `error`, as `metadata-protocol`'s apply does. -- Why the dataset that threw is left out: its ids are only partly known when it throws. A batched insert whose response was lost may have committed rows whose ids never came back, so a list of the ids the loader did see would read as complete while missing rows. - -Supersedes, in the same release: `.changeset/30348-seed-receipt-landed-ids.md` says absent `landed` means "not reported (a load that threw, or a producer that reports no ids)". After this change a load that threw part-way reports `landed` for its finished datasets; absent now means no load ran, or a producer that reports no ids. diff --git a/.changeset/30430-blueprint-widget-aggregate-calendar-date.md b/.changeset/30430-blueprint-widget-aggregate-calendar-date.md deleted file mode 100644 index e6fdd6774ee..00000000000 --- a/.changeset/30430-blueprint-widget-aggregate-calendar-date.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec): a blueprint dashboard widget says how it aggregates, and a blueprint calendar view says which date field it schedules on - -Clause-②: yes (widening: an optional `aggregate` on the blueprint dashboard widget and an optional `startDateField` on the blueprint view) -Metadata-protocol: added - -- **Widget `aggregate`.** `BlueprintDashboardSchema` widgets and the strict mirror `SolutionBlueprintStrictSchema.dashboards[].widgets[]` gain `aggregate: 'sum' | 'avg' | 'min' | 'max' | 'count'` (optional on the lenient side, required-but-nullable on the mirror). Before this, a widget could only name its `measure` and the reader picked the aggregation from the field type, so a widget meant to average a number field summed it. `sum`, `avg`, `min` and `max` aggregate the `measure` field; `count` counts records and needs no `measure` field (a field named beside it is not read). `measure`'s describe no longer says the aggregation is automatic, and neither side tells the model to infer anything from the widget title. -- **View `startDateField`.** `BlueprintViewSchema` and the strict mirror's `views[]` gain `startDateField`, a snake_case field name, carried into the built view as `calendar.startDateField`. It is read on a `calendar` view and ignored on the other view types, not refused: the strict mirror the design model writes through cannot state a cross-key rule, so a refusal on the lenient side alone would refuse at apply a proposal the model was allowed to generate. -- Both keys are optional on the lenient schema, so every existing blueprint parses unchanged. The strict mirror is only the design model's structured-output contract; a model generating against it now states both keys, `null` when it has nothing to say. diff --git a/.changeset/30436-studio-saved-to-environment.md b/.changeset/30436-studio-saved-to-environment.md deleted file mode 100644 index 3fcd88e987f..00000000000 --- a/.changeset/30436-studio-saved-to-environment.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): the Studio item header's "Saved to X's customizations" note says where the customization lives, in the publish sheet's own sentence (#30436) - -Clause-②: no -Metadata-protocol: none - -The note on an item header (Interfaces item, navigation editor, Data object, Automations flow) now goes on with the sentence the pending-changes sheet draws under it (`preview.changes.customizationEnvironment`, from #30119): the changes are kept in this environment's database as items of the real `.custom` pair, and reach another environment only through `os package export`. No new i18n key: the note reuses `customizationEnvironmentText`. Before the pair's first save the note names the `.custom` id that save creates. diff --git a/.changeset/30440-managed-clamp-modify-all.md b/.changeset/30440-managed-clamp-modify-all.md deleted file mode 100644 index a78d2b5400f..00000000000 --- a/.changeset/30440-managed-clamp-modify-all.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/core': patch ---- - -fix(core): effective object permissions on a guarded managed object grant no edit, delete or transfer the write guard refuses, `modifyAllRecords` included - -Clause-②: yes (widening) -Metadata-protocol: none - -The managed write clamp behind `/auth/me/permissions` and `ISecurityService.getEffectiveObjectPermissions` cleared `allowEdit`, `allowCreate` and `allowDelete` on an object in a guarded `managedBy` bucket (`better-auth`, `engine-owned`, `append-only`) that does not opt the verb in through `userActions`, but left two grants the write guard refuses: - -- **`modifyAllRecords`**, which grants edit, delete and transfer on its own. A permission set naming such an object with `{ modifyAllRecords: true }` made `current_user.can(object, 'edit' | 'delete' | 'transfer')` answer `true` while every one of those writes was refused. Where the guard refuses `edit` or `delete`, it is now lowered to `viewAllRecords` (the read it implies, so read is unchanged), and each verb the object still opts in is kept as its plain bit: an object opting `edit` in (such as `sys_user`) keeps edit and transfer and loses delete; an object opting nothing in keeps read only. An object opting both `edit` and `delete` in keeps `modifyAllRecords`. The external write clamp lowers it through the same helper. -- **`allowTransfer`**, which now follows `edit`: an owner change is an update, refused unless `edit` is opted in. This also corrects a super-user wildcard admin, who was answered `can(object, 'transfer')` `true` on every guarded object that does not opt `edit` in. - -No write changes: the guard refused each of these before and still does. Only the served capability answer, and the console buttons that read it, now match it. diff --git a/.changeset/30444-datasource-delete-bound-objects.md b/.changeset/30444-datasource-delete-bound-objects.md deleted file mode 100644 index eb554ba9c8e..00000000000 --- a/.changeset/30444-datasource-delete-bound-objects.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/service-datasource': patch ---- - -fix(service-datasource): the refusal to delete a datasource that objects still use names its reason and the objects in `error.details` - -Clause-②: yes (widening) -Metadata-protocol: none - -- `DELETE /api/v1/datasources/:name` on a runtime datasource that objects are still bound to keeps answering `400` `DATASOURCE_ADMIN_ERROR` with the same English message, and now also carries `error.details`: `{ reason: 'objects_bound', count, objects }`. `reason` tells this refusal apart from every other 400 of the datasource admin routes, `count` is the number in the message, and `objects` lists the bound objects' names, sorted, so a client can name them in its own language without reading object metadata. The names come from the same read as the count, so the two cannot disagree. -- `DatasourceAdminServiceConfig.countBoundObjectsDiagnosed` may now return `objects` (the names it counted); the shipped plugin wiring does. A host whose read reports only a number keeps working, and its refusal carries `count` without `objects`. -- Every other 400 of these routes answers exactly as before, with no `details`. The degraded-read refusal (`503` `SERVICE_UNAVAILABLE`) is unchanged. diff --git a/.changeset/30454-identity-join-titles-name-people.md b/.changeset/30454-identity-join-titles-name-people.md deleted file mode 100644 index 71507d9e365..00000000000 --- a/.changeset/30454-identity-join-titles-name-people.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/platform-objects': patch -'@objectstack/core': patch ---- - -fix(platform-objects): an organization membership, a two-factor enrollment and the two SCIM join rows are named by the people and groups they point at, not raw ids - -Clause-②: no -Metadata-protocol: none - -- **The record titles.** Four identity join objects built their title (`display_title`) by splicing stored lookup ids, so a record page's heading, a list's Title column and every reference to the row read ids. Each now reads names one hop through its lookups (ADR-0079 addendum, 2026-10-18), as `sys_business_unit_member` and `sys_team_member` do. A person is their `name`, or their `email` where the name is blank: - - `sys_member`: `Ada Lovelace (admin)`, or `Ada Lovelace` with no role; - - `sys_two_factor`: `Two-factor for Ada Lovelace`; - - `sys_scim_projection_grant`: `billing_admin → Ada Lovelace`; - - `sys_scim_group_member`: the provisioned user's `user_name` and the group's `display_name`, each its object's own title: `ada.lovelace@idp.example in Operations`. - - Every hop is read as the reader: a person, provisioned user or group the reader may not read, or one that no longer exists, leaves the title empty rather than showing an id. No column is stored, so a rename shows on the next read. -- **The authorization prelude reads no title.** `@objectstack/core`'s grant resolver reads `sys_member` three times (the caller's memberships, the active organization's first page of members, and the position-holder count). Each read now names the columns it decides on, as the `sys_user_permission_set` grant read already did, so the membership's title, which now reads a `sys_user` row, is never computed there. An authenticated request still pays the same reads and round trips as before. diff --git a/.changeset/30455-manifest-list-serves-sourced-options.md b/.changeset/30455-manifest-list-serves-sourced-options.md deleted file mode 100644 index 761edb66c96..00000000000 --- a/.changeset/30455-manifest-list-serves-sourced-options.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/service-settings': patch -'@objectstack/spec': patch ---- - -fix(service-settings): the settings manifest list serves a sourced select's options, the table the save judges against, as the namespace read does - -Clause-②: no — the manifest list now states the table the save already enforces; nothing a caller sends is refused differently. -Metadata-protocol: none - -A select key whose options come from the running system (`localization.locale`: the languages the deployment's i18n service reports) was served its live options only by the namespace read (`GET /api/settings/:namespace`). `SettingsService.getManifest()`, `SettingsService.listManifests()` and `GET /api/settings` still carried the declared table (the four codes `en-US`, `zh-CN`, `ja-JP`, `es-ES`), so a reader of the manifest list (the Setup hub, an SDK caller) was told a table that is not the one a save is judged against. - -- **Now.** All three are served through the same resolution as the namespace read: a sourced key carries its source's options; every other key, and a sourced key whose source has no answer (no i18n service in the composition), carries the declared table as before. `getManifest(namespace, display?)` and `listManifests(ctx?, display?)` take an optional display language; `GET /api/settings` passes the request's. The registered manifest is never changed. -- **Spec.** The `valueDomain` description in `settings-manifest.zod.ts` names the runtime option source beside `valueDomain` as the other mover of a select's enforcement boundary: where a source answers, the declared `options` are the fallback. diff --git a/.changeset/30456-default-language-boot-window-one-matcher.md b/.changeset/30456-default-language-boot-window-one-matcher.md deleted file mode 100644 index d83200010e7..00000000000 --- a/.changeset/30456-default-language-boot-window-one-matcher.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/core': patch -'@objectstack/service-settings': patch -'@objectstack/plugin-auth': patch ---- - -fix(service-settings): an `OS_LOCALIZATION_LOCALE` naming a language the boot is still loading is no longer dropped, and Setup's default language accepts by the same locale matcher as the IdP locale claim (#30456) - -Clause-②: yes (widening) — `@objectstack/core` exports `resolveOfferedLocale(requested, offered)`; `SettingsService` gains `holdOptionSourceVerdicts()` and `sealOptionSources()`. The save of `localization.locale` (`PUT /api/settings/localization`, `SettingsService.setMany`) and an `OS_LOCALIZATION_LOCALE` override now also accept a region or base of an offered language (`fr-FR` where `fr` is offered, `fr` where `fr-FR` is) and another case of an offered code (`zh-cn` for `zh-CN`); nothing accepted before is refused. The IdP locale claim (`resolveIdpLocale`) lands a script or region spelling on the offered code the platform locale table says it means (`zh-TW` on `zh-Hant`, not on a `zh-Hans` listed first). -Metadata-protocol: none - -- **The boot window.** The deployment's locale list fills during the boot: the app's authored translations arrive at `kernel:ready`. A `kernel:ready` reader of `localization.locale` (auth's mail and SMS default language) could ask before the language an `OS_LOCALIZATION_LOCALE` override names was in the list; the override was then reported `IGNORED` and the reader kept the next layer's value, though the boot went on to offer that language. `SettingsServicePlugin` now holds the option verdict on a sourced key until `kernel:bootstrapped`: while held, an override the list does not admit yet is in force and not reported; at `kernel:bootstrapped` it is judged again, and one the list still does not admit is reported once, as before, and a settings change is emitted for `localization.locale` so a reader that applied it reads the key again. The other value checks (pattern, domain, bounds, format) are judged at once, as ever. A `SettingsService` constructed without the plugin judges at once, as it always has. -- **One matcher.** Setup's default language and the IdP locale claim now both ask `resolveOfferedLocale` (`@objectstack/core`): `resolveLocale`'s exact and case-insensitive steps, then the platform locale table's language (`zh-TW` is `zh-Hant`, `en-US` is `en`), then `resolveLocale`'s base-language and variant steps. A language one door takes, the other takes too. -- **Comment.** `AUTH_EMAIL_TEMPLATE_LOCALES` (plugin-auth) no longer claims to be the setting's options: since #30286 the setting offers the deployment's locale list, and the two ends agree through `normalizeAuthEmailLocale`'s pass-through and the email service's `en-US` fallback. diff --git a/.changeset/30464-install-response-bare-row.md b/.changeset/30464-install-response-bare-row.md deleted file mode 100644 index 4dfd9b805d5..00000000000 --- a/.changeset/30464-install-response-bare-row.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): `PackageInstallResponseSchema` declares the installed row that `POST /api/v1/packages` really answers - -Clause-②: yes (narrowing) -Metadata-protocol: none - -**BREAKING** accept-set narrowing at the install answer contract (`@objectstack/spec`): `PackageInstallResponseSchema` refuses the `{ package }` wrapper it accepted, and `PackageInstallResponse['data']` no longer declares `package`, `dependencyResolution`, `namespaceConflicts` or `message`. - -`POST /api/v1/packages` answers `{ success: true, data: }` with status `201`, where the row is the `InstalledPackage` the registry recorded. `PackageInstallResponseSchema` (the output `PackageApiContracts.installPackage` binds) declared `data: { package, dependencyResolution?, namespaceConflicts?, message? }` instead. No surface sent that wrapper, so code written against the contract read `data.package` and got `undefined`. The first-party client already followed the wire: `client.packages.install` resolves to `InstalledPackage`. - -- **FROM** `data: { package: InstalledPackage, dependencyResolution?, namespaceConflicts?, message? }` **TO** `data: InstalledPackage`. The name, the type names (`PackageInstallResponse`, `PackageInstallResponseParsed`) and the binding are unchanged. The server's answer is unchanged. -- **Both directions.** The schema now accepts the bare row the door serves, which it refused, and refuses the `{ package }` wrapper no server sends. The type `PackageInstallResponse['data']` loses `package`, `dependencyResolution`, `namespaceConflicts` and `message`, so TypeScript code that reads them stops compiling. That code read `undefined` at runtime already. -- **Fix for a consumer:** read the row from `data` itself. Replace `response.data.package` with `response.data`. Drop any read of `data.dependencyResolution`, `data.namespaceConflicts` or `data.message` on this route: the door never sent them. -- **Level.** `patch`: this is the shape of an API answer, not anything a metadata author writes. - - diff --git a/.changeset/30467-ledger-adoption-staging-dir.md b/.changeset/30467-ledger-adoption-staging-dir.md deleted file mode 100644 index c0ab0f30047..00000000000 --- a/.changeset/30467-ledger-adoption-staging-dir.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@objectstack/cloud-connection': patch ---- - -Clause-②: no -Metadata-protocol: none - -The one-time copy of the working-directory install ledger into its `OS_HOME` location (#30217) now creates its staging directory with an exclusive `mkdirSync` of a randomly named sibling instead of `mkdtempSync`. Behaviour is unchanged: the copy still lands in a unique sibling of the ledger directory, is renamed into place, and is removed if the copy fails. This repairs the red `main` build (#30467), where the repository's in-tree scratch scan could not read the `mkdtempSync` base. diff --git a/.changeset/30469-import-lands-in-datasource-package.md b/.changeset/30469-import-lands-in-datasource-package.md deleted file mode 100644 index dfa526c560f..00000000000 --- a/.changeset/30469-import-lands-in-datasource-package.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/service-datasource': patch ---- - -fix(service-datasource)!: an object imported from a package's external datasource is saved into that package, or into its customization package - -Clause-②: yes (narrowing) — `POST /api/v1/datasources/:name/external/tables/:remote/import` (and the MCP `import_object` tool) now saves the object into the package that declares the datasource, or into that package's customization package `.custom` when the package is read-only, and names it in that package's namespace. An explicit `name` in the read-only base's own namespace (`showcase_orders` over the showcase's `showcase_external`) is now refused with ADR-0028's message naming the pair-prefixed name (`showcase_custom_orders`); under a walled tenancy posture (`group`, `isolated`) an import that needs a customization package that does not exist yet is refused, naming the platform operator and the ensure door. `ExternalDatasourceServiceConfig` gains the optional `resolveImportLanding`, `persistObject` an optional third `packageId` argument, and the package exports the `ImportLanding` type. -Metadata-protocol: none - - - -**BREAKING** accept-set narrowing at the external-datasource import door (`@objectstack/service-datasource`), ruling B on where an imported object lives (ruling record 6660988968). - -- **Where an import lands.** The datasource's owning package decides. A datasource no package declares (added in Setup) keeps today's answer: a package-less row, environment metadata that no package export carries (ADR-0146 D11). A datasource declared by a writable package saves the object into that package. A datasource declared by a read-only installed package (a booted code package or a marketplace install) saves it into that package's customization package `.custom`, which the import obtains from the ensure door first (ADR-0146 D2/D3), so `os package export` and `GET /api/v1/packages/.custom/export` carry it. -- **The name it gets.** The save door judges the name against the package written into, so an import over a read-only package's datasource is named in the pair's namespace: the showcase's `customers` table imports as `showcase_custom_customers`, where it imported as `showcase_customers`. The draft door (`…/tables/:remote/draft`, the `*.object.ts` an author commits into the package's own source) still names it in the datasource package's namespace. -- **Walled postures.** Under `group` and `isolated` the platform operator alone creates a customization package. The import never creates it: it saves into one that already exists, and while none does it is refused (`400 EXTERNAL_IMPORT_ERROR`) with a message naming the platform operator and `POST /api/v1/packages//ensure-customization`. It is never saved package-less instead. -- **Objects imported before this release** stay where they are, package-less and under their old names. Importing the same table again makes a second object under the new name in the customization package; delete the old one once nothing points at it. diff --git a/.changeset/30469-synced-object-field-labels.md b/.changeset/30469-synced-object-field-labels.md deleted file mode 100644 index 899eaf5b0c1..00000000000 --- a/.changeset/30469-synced-object-field-labels.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/service-datasource': patch ---- - -fix(service-datasource): an object drafted or imported from an external datasource table gives each field a readable label - -Clause-②: no -Metadata-protocol: none - -Every field of a generated object draft (`generateObjectDraft`, `importObject`, `os datasource introspect`) now carries a `label`, derived from the field's name by the same snake_case to Title Case step that already labels the object: a table with columns `part_no` and `part_name` yields fields labelled `Part No` and `Part Name`, so the record form of a synced object no longer shows raw column names. The label follows the name the field is saved under (after `rename` and the numbering of a taken name), a lookup keeps the label of the column it replaces, and the rendered `*.object.ts` source carries the same label. The object's own label is unchanged. Objects imported before this release keep their stored fields; re-import or edit the labels to change them. diff --git a/.changeset/30471-field-name-indexes-extension-fields.md b/.changeset/30471-field-name-indexes-extension-fields.md deleted file mode 100644 index bc9c9c5a7cf..00000000000 --- a/.changeset/30471-field-name-indexes-extension-fields.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@objectstack/lint': minor ---- - -fix(lint)!: the field-name indexes judge an object as the registry serves it, so a read, write or sort naming a field an `objectExtensions` entry adds is no longer refused as unknown, and the readonly, sortability and translation rules judge that field by its served definition (#30471) - -Clause-②: yes (narrowing) — `os validate` / `os build` / `os lint` refuse three shapes they accepted before: a hook body's literal `ctx.api.object(NAME).update(…)` / `.updateById(…)` / `.insert(…)` writing a field an `objectExtensions` entry adds and declares `readonly: true` moves from a `hook-body-write-unknown-field` warning to a `hook-api-update-readonly-field` error; and a translation key naming a field or validation rule that only a replaced earlier entry of the same package adds, or only a top-level `objectExtensions` beside `packages[]` adds, is now `translation-target-unknown` at `error`, where it resolved, and with that top-level-beside shape no longer read, a translation subtree `objects.X` keyed to an object the stack does not define and only such an entry extends loses the extended-object skip (rung 2b) and is `translation-target-unknown` at `error` too; and a field an `objectExtensions` entry RE-DECLARES on its owner's object is judged on the extension's definition, which replaces the owner's whole in the fold as in the registry's `mergeObjectDefinitions` (`{ ...base.fields, ...extension.fields }`), so an owner `amount: number` re-declared as `formula`, `readonly: true` or `text` now gets that definition's `sort-field-unsortable`, readonly-write or expression type-mismatch verdict, each at `error` (ADR-0146 D10 already refuses that shape on the extension; no CLI door does). The widening half: a formula, validation, flow condition, hook condition, sharing-rule condition, flow-node, action-body or hook-body write, react page field prop, translation key, page sort or list-view sort naming a field an `objectExtensions` entry of the same artifact adds is no longer refused as an unknown field. -Metadata-protocol: narrowed -Skills-docs: updated here — content/docs/automation/hook-bodies.mdx - - - -**BREAKING** accept-set narrowing in `os validate` / `os build` / `os lint` (`@objectstack/lint`), for multi-package artifacts whose bodies add fields to another package's object through `objectExtensions`. - -- **Before.** The field-name indexes of the expression passes (validation rules, formulas, flow and hook conditions, sharing-rule conditions), the three write rules, a react page's field props, the three readonly write rules and the sort rule read an object's declared `fields` only, so a name an `objectExtensions` entry adds was refused as unknown although the runtime serves the field: `flow-node-write-unknown-field` and `sort-field-unknown` / `page-field-unknown` (page sorts) at `error`, `action-body-write-unknown-field`, `hook-body-write-unknown-field` and `page-field-unknown` (react props) at `warning`, the expression passes' unknown-field error. The translation rule folded extensions through a reader of its own that kept every entry and also read a top-level `objectExtensions` beside `packages[]`. -- **Now.** Every one of them reads the one shared fold in `object-graph.ts` (since #30383 the field-path rules' fold): the declared fields plus each field an `objectExtensions` entry adds, one entry per package per object, `packages[]` alone when the artifact carries it. A name no body declares or adds keeps the same refusal, rule id, severity and message; a misspelling within edit distance 3 of an extension-added name now gets a did-you-mean naming it. A name the fold adds is judged by its served definition, so these findings move (union artifact and per-package leg alike): - - a hook body's `ctx.api` update / updateById / insert of a `readonly: true` field: `hook-body-write-unknown-field` `warning` → `hook-api-update-readonly-field` `error`, the severity a declared readonly field already gets (a non-system hook's `ctx.api` write is stripped at run time and the call returns success); - - a translation key naming a field or rule only a replaced earlier entry of the same package, or only a top-level `objectExtensions` beside `packages[]`, adds: resolved → `translation-target-unknown` `error`, as the registry serves the object; - - a translation subtree `objects.X` keyed to an object the stack does not define and only a top-level `objectExtensions` beside `packages[]` extends: skipped as an extended object (rung 2b) → `translation-target-unknown` `error`; - - an owner's field an `objectExtensions` entry re-declares is judged on the extension's definition, which replaces the owner's whole as the registry's `mergeObjectDefinitions` does (`{ ...base.fields, ...extension.fields }`): an owner `amount: number` re-declared as `formula` and sorted (page or list view) → `sort-field-unsortable` `error`; re-declared `readonly: true` and written by a flow `update_record` or a hook's `ctx.api` update → `flow-update-readonly-field` / `hook-api-update-readonly-field` `error`; re-declared `text` and multiplied in a validation rule → the expression type mismatch at `error`. Each passed before, the owner's definition being the one judged. ADR-0146 D10 refuses this shape on the extension at registration; no CLI door does; - - a flow `update_record` / `create_record` of a `readonly: true` field: `flow-node-write-unknown-field` `error` → `flow-update-readonly-field` `error`; - - a page sort (every slot) or list-view sort by a `formula` field or one declaring `sortable: false`: `page-field-unknown` / `sort-field-unknown` `error` → `sort-field-unsortable` `error`; - - a `readonlyWhen` field written by a flow `update_record` (`error` → `flow-update-readonly-when-field` `warning`), a hook's `ctx.api` update (`warning` → `hook-api-update-readonly-when-field` `warning`) or an action's `ctx.api` update (`warning` → `action-api-update-readonly-when-field` `warning`); - - an expression reading the field: the unknown-field error becomes the served type's verdict (a type mismatch or a missing null guard, at `error` in validation rules and hook conditions, at `warning` in formulas and sharing-rule conditions) or nothing. - Every other read, write or sort naming such a field passes: a hook's `ctx.input` stamp, an action's `ctx.api` write of a `readonly: true` field (an action runs elevated) and a `create_record` of a `readonlyWhen` field land at run time. -- **Not judged.** A field two packages write at the same deciding priority is a served name, but no rule judges its type, nullability, readonly or sortability: which definition wins is registration order. An object that declares no field of its own stays unjudged by the write, readonly and sort rules, extended or not (an introspected schema). A react page's `searchableFields` still reads declared fields only. -- **Not changed.** Which extension entries the fold keeps, the runtime, and `defineStack` (its related-list check stays additive). A stack that names no extension field reads as before. - -**What an author does.** For a hook writing a readonly extension field, declare the hook `runAs: 'system'`, stamp the value in the record's own `beforeInsert` / `beforeUpdate` hook, or drop the field from the payload. For a translation key the fold drops, key it to a field or rule of the entry the registry keeps (the package's last entry for that object, in a `packages[]` body), or delete it. diff --git a/.changeset/30486-related-list-rail-packaged-actions-served-object-label.md b/.changeset/30486-related-list-rail-packaged-actions-served-object-label.md deleted file mode 100644 index 0d6a591d626..00000000000 --- a/.changeset/30486-related-list-rail-packaged-actions-served-object-label.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/plugin-detail': patch -'@object-ui/app-shell': patch ---- - -fix(plugin-detail,app-shell): the related list, the reference rail and the packaged-actions table name a renamed object by the name the server serves - -Clause-②: yes (widening) -Metadata-protocol: none - -- **The surfaces.** A `record:related_list` block and a `record:reference_rail` card with no authored `title` are headed with the related object's served plural label, else its served label, as the server translated it for the session (a `.custom` extension's rename included). They used to ask only the client translation bundle, which still held the base package's name. The bundle is asked only when the server serves neither. -- **Packaged actions.** Setup → Packaged Actions reads the object list in the session's language (`Accept-Language`), not the browser's, and its Object column draws that served label as given. -- **The widening.** The served-object-label rule now lives in `@object-ui/core`, which `@object-ui/plugin-detail` can import: new exports `servedObjectLabel`, `servedObjectPluralLabel`, `metadataText` and the types `ObjectLabelLookup` and `ObjectPluralLabelLookup`. app-shell's internal copy is now a re-export of these, so both packages read one rule. diff --git a/.changeset/30494-fail-open-date-sentences.md b/.changeset/30494-fail-open-date-sentences.md deleted file mode 100644 index 58a1509df5e..00000000000 --- a/.changeset/30494-fail-open-date-sentences.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/formula': patch ---- - -fix(formula): an advisory validation rule's date verdict says the rule is skipped and the write proceeds - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@objectstack/formula`: two codes join `ExpressionRefusalCode`** (`EXPRESSION_REFUSAL_CODES`), with the params of the code each splits from: `date-arithmetic-fail-open` and `date-text-arithmetic-fail-open`. -- On an advisory (`severity: 'warning'` / `'info'`) validation rule (`faultOutcome: 'fail-open'`), date arithmetic used to be told "this faults at runtime, so the field silently evaluates to null". There is no field there, and nothing reads null: the rule is skipped and logged, and the write proceeds. The verdict now closes on the sentence `type-mismatch-fail-open` already uses for that outcome. This replaces the earlier note that an advisory rule keeps `date-arithmetic` / `date-text-arithmetic`. -- A formula field keeps `date-arithmetic` / `date-text-arithmetic` and the null sentence. A consumer keyed to the old codes on an advisory rule re-keys to the new ones. -- The grade is unchanged: the verdict is an error, as before. What is refused, warned or passed is unchanged. -- `@objectstack/lint` and `@objectstack/mcp` `validate_expression` already pass `faultOutcome: 'fail-open'` for an advisory rule, so both serve the new sentence with no change of their own. diff --git a/.changeset/30500-settings-toggle-confirm-off.md b/.changeset/30500-settings-toggle-confirm-off.md deleted file mode 100644 index 5ea9993911f..00000000000 --- a/.changeset/30500-settings-toggle-confirm-off.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/console': patch ---- - -feat(console): a settings toggle that declares `confirmOffText` asks before it is turned off - -Clause-②: no -Metadata-protocol: none - -A settings manifest's `toggle` specifier may declare `confirmOffText` (`SpecifierSchema`: "Confirm dialog text shown before a toggle's value changes to off (omit = no confirm)"), and the authentication manifest declares it on `email_password_enabled`. The Console's settings page did not read the key, so Setup turned email and password sign-in off without asking. Now turning such a toggle off opens a confirmation carrying that text, translated through `settings.NAMESPACE.keys.KEY.confirmOffText` when the locale has it (an inline locale map literal is picked for the page's language otherwise). Cancel leaves the toggle on and puts nothing in the page's unsaved changes, so nothing can be saved; Continue turns it off, and Save sends it like any other edit. Turning a toggle on asks nothing, and a toggle without the key changes at once, exactly as before. diff --git a/.changeset/30504-contributed-nav-packaged-app-base.md b/.changeset/30504-contributed-nav-packaged-app-base.md deleted file mode 100644 index ba3d54735ad..00000000000 --- a/.changeset/30504-contributed-nav-packaged-app-base.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix(metadata-protocol): contributed navigation entries are translated again, the whole Setup sidebar among them - -Clause-②: no -Metadata-protocol: none - -`getPackagedAppBase(name)` now answers the app's packaged declaration with the code layer's navigation contributions folded in, by the one fold every served app read applies (`SchemaRegistry.applyNavContributions`, ADR-0029 D7). Before, it answered the declaration alone, so `translateApp` read every contributed entry's id as one a `.custom` override authored (ADR-0146 D2), skipped the catalog and served the source text: on a `zh-CN` deployment every Setup leaf entry (Users, Permission Set Assignment, Sharing Rules, API Keys and the rest) read English beside Chinese group headings, although the catalog carries their keys. Contributed entries now read the catalog's translation; the served app's shape is unchanged, only label values move. A navigation node a `.custom` override authors is in no contribution, so it still reads only the pair's translations, else its own text. diff --git a/.changeset/30509-approval-notices-email.md b/.changeset/30509-approval-notices-email.md deleted file mode 100644 index 9205df09f61..00000000000 --- a/.changeset/30509-approval-notices-email.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/plugin-approvals': patch -'@objectstack/service-messaging': patch ---- - -feat(approvals,messaging): approval requests, reminders and outcomes also go by e-mail, linking to the request page; messaging gains a code-declared notification template tier - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@objectstack/plugin-approvals`: `approval.requested`, `approval.reminder`, `approval.approved` and `approval.rejected` request the `inbox` and `email` channels** in one emit. Before, every approval notice named no channel, so messaging delivered it to the in-app inbox alone and nobody who does not watch the Console bell heard of it. Every other `approval.*` topic stays inbox-only, and delivery preferences filter the e-mail as they filter any channel. The inbox is unchanged. On a deployment with no mail transport the e-mail is whatever the `email` channel answers there. -- **The approval e-mail** is the notice's own title as its subject and its body (in the recipient's language, as the inbox renders it) followed by one line that opens the request, in `en`, `zh-CN`, `ja-JP` and `es-ES`. The link is a new payload member, `approvalUrl`: the request page (`/system/approvals?request=ID`) under the deployment's public origin. `actionUrl` stays relative, as the Console routes on it. It opens the request for a signed-in reader and decides nothing; no ADR-0043 action link or token is in any payload. -- **`publicBaseUrl` now resolves when the option is unset**: `OS_AUTH_URL` (legacy `BETTER_AUTH_URL`), then `OS_BASE_URL`, never a guessed `localhost`. With none of them set, or a value that is not an absolute `http(s)` URL, links stay relative and the plugin warns once at start, naming the variable to set. The same origin also prefixes ADR-0043's `actionLinkUrl`, as its option always documented. To give approval e-mails a working link, set `OS_BASE_URL` (or `OS_AUTH_URL`) to the deployment's address. -- **`@objectstack/service-messaging`: `MessagingService.registerBuiltinTemplates(templates)`** (and the `BuiltinNotificationTemplate` type) declares `(topic, channel, locale)` notification templates in code. The channels' template store reads them at every rung of its locale ladder after the recipient organization's own `sys_notification_template` row: the rung's row, else the rung's built-in, else the next rung, else the generic title and body. Nothing is written to the database (ADR-0131 §1.4), so an organization that authors a row overrides the built-in with nothing to migrate. A malformed template, or a `(topic, channel, locale)` already declared, is refused with a `TypeError`, and nothing from that call is kept. diff --git a/.changeset/30510-email-channel-log-transport-unavailable.md b/.changeset/30510-email-channel-log-transport-unavailable.md deleted file mode 100644 index 24cd8d62909..00000000000 --- a/.changeset/30510-email-channel-log-transport-unavailable.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/service-messaging': patch ---- - -fix(notifications): the `email` channel answers unavailable while the email service only has the log transport, so a notification is no longer recorded as delivered when no mail left the server - -Clause-②: no -Metadata-protocol: none - -- **What was wrong.** plugin-email registers the `email` service on every boot; with no `mail` settings saved its live transport is the log transport, which writes the message to the server log and to `sys_email` and sends nothing. The `email` channel treated "an email service is registered" as "available", so fan-out wrote a `sys_notification_delivery` row per recipient and the dispatcher marked it `success` while nobody was mailed. -- **What changes.** The channel now asks the service's own deliverability answer, `IEmailService.isConfigured()`, on every call. While it answers `false`, `isAvailable()` answers `{ available: false, reason: 'transport_not_configured' }`: fan-out writes no delivery row and records the suppression on `sys_notification.suppressed_channels`, exactly as it does with no email service at all. `send()` refuses a row that was already written with the same `transport_not_configured` reason, graded permanent, and never hands it to the service. No new reason and no new error code. -- **No restart needed.** The answer is read from the live transport, so saving a working provider under Setup → Email makes the channel available on the next notification, and switching back to the log transport makes it unavailable again. -- **Unchanged.** An email service that does not implement `isConfigured` (it is optional on the contract) keeps today's behaviour: the channel stays available and sends through it. `EmailSenderSurface` gains the optional `isConfigured?(): boolean` member, mirroring the contract. diff --git a/.changeset/30512-plugin-audit-locales.md b/.changeset/30512-plugin-audit-locales.md deleted file mode 100644 index 57e32b2b44e..00000000000 --- a/.changeset/30512-plugin-audit-locales.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/plugin-audit': patch ---- - -i18n(plugin-audit): the audit, activity and comment objects and the activity-feed sentences ship in all eleven platform languages - -Clause-②: no -Metadata-protocol: none - -`sys_audit_log`, `sys_activity`, `sys_comment` and `sys_comment_reaction` now carry Traditional Chinese (`zh-Hant`), Arabic (`ar`), French (`fr`), Brazilian Portuguese (`pt-BR`), Russian (`ru`), German (`de`) and Korean (`ko`) beside the existing `en`, `zh-CN`, `ja-JP` and `es-ES`, and so do the activity-feed summaries ("Created … ", "Updated …: fields", "… mentioned you"). A reader in one of those languages sees the audit trail, the activity feed and the mention notice in their own language instead of English. Each new locale is registered under its platform-table tag with its source-hash companion and its messages, and the package's extract command lists the seven tags, so the existing `check:i18n` bundle check covers them. Part of #30512. diff --git a/.changeset/30512-plugin-webhooks-locales.md b/.changeset/30512-plugin-webhooks-locales.md deleted file mode 100644 index c6f58498f01..00000000000 --- a/.changeset/30512-plugin-webhooks-locales.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/plugin-webhooks': patch ---- - -i18n(plugin-webhooks): the webhook object's labels, help, options and views ship in all eleven platform languages - -Clause-②: no -Metadata-protocol: none - -`sys_webhook` now carries Traditional Chinese (`zh-Hant`), Arabic (`ar`), French (`fr`), Brazilian Portuguese (`pt-BR`), Russian (`ru`), German (`de`) and Korean (`ko`) beside the existing `en`, `zh-CN`, `ja-JP` and `es-ES`, so an administrator reading Setup in one of those languages sees the webhook screens in their own language instead of English. Each new locale is registered under its platform-table tag with its source-hash companion, and the package's extract command lists the seven tags, so the existing `check:i18n` bundle check covers them. Part of #30512. diff --git a/.changeset/30512-service-mailbox-locales.md b/.changeset/30512-service-mailbox-locales.md deleted file mode 100644 index 25a89ac8b6f..00000000000 --- a/.changeset/30512-service-mailbox-locales.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/service-mailbox': patch ---- - -i18n(service-mailbox): the mailbox objects' labels, help, options and views ship in all eleven platform languages - -Clause-②: no -Metadata-protocol: none - -The mailbox objects (`sys_mailbox`, `sys_mail_message` and `sys_mail_association`) now carry Traditional Chinese (`zh-Hant`), Arabic (`ar`), French (`fr`), Brazilian Portuguese (`pt-BR`), Russian (`ru`), German (`de`) and Korean (`ko`) beside the existing `en`, `zh-CN`, `ja-JP` and `es-ES`, so a user reading the app in one of those languages sees the mailbox screens in their own language instead of English. RFC 5322 header names (`Message-ID`, `In-Reply-To`, `Reply-To`, …) and the IMAP `UIDVALIDITY` term stay as written, as they already do in `zh-CN`. Each new locale is registered under its platform-table tag with its source-hash companion, and the package's extract command lists the seven tags, so the existing `check:i18n` bundle check covers them. Part of #30512. diff --git a/.changeset/30512-service-messaging-locales-a.md b/.changeset/30512-service-messaging-locales-a.md deleted file mode 100644 index b10d80cac8a..00000000000 --- a/.changeset/30512-service-messaging-locales-a.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/service-messaging': patch ---- - -i18n(service-messaging): the notification objects and the digest heading ship in Traditional Chinese, Arabic, French and Brazilian Portuguese - -Clause-②: no -Metadata-protocol: none - -The messaging objects (`sys_inbox_message`, `sys_notification_receipt`, `sys_notification_delivery`, `sys_notification_preference`, `sys_notification_template`, `sys_http_delivery`, `sys_user_device`) and the digest heading "You have {{count}} notifications" now carry `zh-Hant`, `ar`, `fr` and `pt-BR` beside the existing `en`, `zh-CN`, `ja-JP` and `es-ES`, so a recipient in one of those languages reads the notification screens and the digest heading in their own language instead of English. `t()` selects no plural form, so the Arabic heading is worded as a tally (`عدد الإشعارات لديك: {{count}}`), correct for every count. A recipient whose language is `fr-FR` now reads the `fr` pack. Each new locale is registered under its platform-table tag with its source-hash companion and its messages, and the package's extract command lists the tags, so the existing `check:i18n` bundle check covers them. First half of package 5 of #30512; `ru`, `de` and `ko` follow in the second half. diff --git a/.changeset/30512-service-messaging-locales-b.md b/.changeset/30512-service-messaging-locales-b.md deleted file mode 100644 index 239e9d5427a..00000000000 --- a/.changeset/30512-service-messaging-locales-b.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/service-messaging': patch ---- - -i18n(service-messaging): the notification objects and the digest heading ship in Russian, German and Korean - -Clause-②: no -Metadata-protocol: none - -The messaging objects and the digest heading "You have {{count}} notifications" now carry `ru`, `de` and `ko` as well, so the package ships all eleven platform languages. `t()` selects no plural form, so the Russian heading is worded as a tally (`Уведомлений: {{count}}`), correct for every count. Each new locale is registered under its platform-table tag with its source-hash companion and its messages, and the package's extract command lists the tags, so the existing `check:i18n` bundle check covers them. Second half of package 5 of #30512. diff --git a/.changeset/30512-service-realtime-locales.md b/.changeset/30512-service-realtime-locales.md deleted file mode 100644 index 38c87135504..00000000000 --- a/.changeset/30512-service-realtime-locales.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/service-realtime': patch ---- - -i18n(service-realtime): the presence object's labels, help and option names ship in all eleven platform languages - -Clause-②: no -Metadata-protocol: none - -`sys_presence` now carries Traditional Chinese (`zh-Hant`), Arabic (`ar`), French (`fr`), Brazilian Portuguese (`pt-BR`), Russian (`ru`), German (`de`) and Korean (`ko`) beside the existing `en`, `zh-CN`, `ja-JP` and `es-ES`, so a reader in one of those languages sees the object's labels in their own language instead of English. Each new locale is registered under its platform-table tag and carries its source-hash companion, and the package's extract command lists the seven tags, so the existing `check:i18n` bundle check covers them. Part of #30512. diff --git a/.changeset/30512-service-storage-locales.md b/.changeset/30512-service-storage-locales.md deleted file mode 100644 index 1ec6f03f9a7..00000000000 --- a/.changeset/30512-service-storage-locales.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/service-storage': patch ---- - -i18n(service-storage): the file and upload-session objects ship in all eleven platform languages - -Clause-②: no -Metadata-protocol: none - -`sys_file` and `sys_upload_session` now carry Traditional Chinese (`zh-Hant`), Arabic (`ar`), French (`fr`), Brazilian Portuguese (`pt-BR`), Russian (`ru`), German (`de`) and Korean (`ko`) beside the existing `en`, `zh-CN`, `ja-JP` and `es-ES`, so a reader in one of those languages sees the file and upload screens in their own language instead of English. Each new locale is registered under its platform-table tag with its source-hash companion, and the package's extract command lists the seven tags, so the existing `check:i18n` bundle check covers them. Package 10 of #30512. diff --git a/.changeset/30525-approvals-status-non-string-message.md b/.changeset/30525-approvals-status-non-string-message.md deleted file mode 100644 index aab924ba484..00000000000 --- a/.changeset/30525-approvals-status-non-string-message.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): the approvals request list's refusal of a non-string `status` says what the parameter takes, instead of quoting `[object Object]` - -Clause-②: no — the same requests are refused with the same status and envelope; only the message for a non-string `status` changes -Metadata-protocol: none - -`GET /api/v1/approvals/requests?status[$ne]=pending` (a `status` the query parser turns into an object) was refused with `"[object Object]"` quoted as if it were the value sent. It is now refused with `The "status" query parameter must be one status, or several separated by commas, given as plain text. Allowed values: pending, approved, rejected, recalled, returned, cancelled.` Still `400 VALIDATION_ERROR` in the nested envelope, and the approval service is still not asked. The approvals docs page now lists all six request statuses (`cancelled` was missing). diff --git a/.changeset/30526-dataset-measure-drill-scopes.md b/.changeset/30526-dataset-measure-drill-scopes.md deleted file mode 100644 index 36c557e8275..00000000000 --- a/.changeset/30526-dataset-measure-drill-scopes.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': patch -'@objectstack/service-analytics': patch -'@object-ui/data-objectstack': patch ---- - -feat(spec,service-analytics,data-objectstack): a dataset answer says, per measure, which records that measure counted (`drillScopes`) - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@objectstack/spec`: `AnalyticsResult.drillScopes?: Record`**, also declared on the `AnalyticsResultResponse` wire schema. Each selected measure name maps to the list filter, on the answer's `object`, that selects the records the measure aggregates: the dataset's own `filter`, AND the measure's own `filter`, AND, for a measure over a field, that field holding a value (both instants for a duration measure), with each `{token}` resolved as the query resolved it. It is `{}` when none of these applies. A measure that no list filter can select is left out: a derived measure, or a scope that names a relationship path (`account.region`, as a key or a `$field` reference). A measure left out offers no drill. A `__compare` column has no entry. -- **`@objectstack/service-analytics`: `queryDataset` sets `drillScopes` on every dataset answer that has rows**, on both query faces, from the same token-resolved filters the numbers were computed under. A drill that lists the clicked bucket (`dimensionFields` / `drillRawRows` / `drillRawTotals`, `drillRanges`) AND the widget's own filter AND this scope lists exactly the records the number counted. Before this, a drill built from the bucket and the widget filter alone listed every record in the bucket, so a filtered measure such as "paid invoices" showed 4 records behind the number 2. -- **`@object-ui/data-objectstack`: `queryDataset` passes `drillScopes` through**, typed as the spec slot. The adapter lists the keys it returns, so until now the sidecar was dropped before it reached a widget. diff --git a/.changeset/30535-non-grid-bulk-bar-gate.md b/.changeset/30535-non-grid-bulk-bar-gate.md deleted file mode 100644 index 95705baae13..00000000000 --- a/.changeset/30535-non-grid-bulk-bar-gate.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/plugin-list': patch -'@object-ui/plugin-grid': patch ---- - -fix(plugin-list): a list's selection bar over a non-grid view (kanban, calendar, gallery, …) draws a declared bulk action only for a user who holds its `requiredPermissions` and only when its `visible` admits a selected record, through the grid bar's own gate (#30535) - -Clause-②: yes (widening) — `@object-ui/plugin-grid` exports `useBulkActionItems` (with the `BulkActionItems` and `LegacyBulkActionItem` types), the grid selection bar's gate, so `@object-ui/plugin-list` reuses it instead of a second copy. -Metadata-protocol: none - -- **Before.** The bar ListView draws itself for every view but the grid folded `bulkActions` through the grid's `resolveBulkActions` and kept only each action's label. An action declaring `requiredPermissions` drew for every user, and one declaring `visible` drew for every selection, while the grid's bar hid both. A name repeated in `bulkActions` drew two buttons under one React key. -- **Now.** The bar passes the resolved actions through `useBulkActionItems`, the hook the grid's bar and its right-click menu read: an action whose `requiredPermissions` the user does not hold is not drawn (unknown capabilities fail open, as on the grid), an action switched off is not drawn, and `visible` is evaluated once per selected record, drawing the action when at least one selected record passes. A repeated name draws one button. The development warning for an object whose schema could not be read now names the component it came from (`ListView` or `ObjectGrid`) instead of "the grid". -- **Remedy.** Nothing to change in metadata. A user who should see a gated action needs the capability it declares. diff --git a/.changeset/30538-locale-table-read-aliases.md b/.changeset/30538-locale-table-read-aliases.md deleted file mode 100644 index 847a63501a7..00000000000 --- a/.changeset/30538-locale-table-read-aliases.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/core': minor ---- - -fix(spec,core): the server locale resolvers read the platform locale table, so a `zh-TW` / `zh-HK` / `zh-MO` reader gets a registered `zh-Hant` bundle instead of Simplified Chinese - -Clause-②: yes (widening) -Metadata-protocol: semantic - -`resolveBundleLocale` (`@objectstack/spec/system`) and `resolveLocale` (`@objectstack/core`) now run one ladder, the new `resolveLocaleAmong(requested, available)` export of `@objectstack/spec/system`: exact, case-insensitive, then **the same platform locale table language**, then base language, then the first registered variant. The new step maps the requested code and each available key through `platformLocaleTagOf` (the table's read aliases) and picks the key spelled as the table tag, else the first key the table maps to that tag. - -What changes for a reader: a request for `zh-TW`, `zh-HK` or `zh-MO` reaches a registered `zh-Hant` key, and `zh`, `zh-CN`, `zh-SG` or `zh-Hans` reaches `zh-CN` (or `zh-Hans`), whatever order the bundle registered them in. Before, the first registered `zh-*` key answered both, so a Traditional reader got the Simplified bundle (and with `zh-Hant` registered first, a Simplified reader got the Traditional one). A bare `zh` key now counts as Simplified for a `zh-TW` request, which the base-language step used to hand it. The step also applies to `ja` → `ja-JP`, `es` → `es-ES`, `en` → `en-US` and `pt` → `pt-BR`, which answer as before against today's alias-spelled bundles. - -`resolveOfferedLocale` (`@objectstack/core`, the matcher Setup's default-language options and the IdP locale claim use) now answers through the same ladder, so a Traditional request reaches an offered `zh-Hant`. Where a list offers two codes of one table language, the code spelled as the table tag wins over the first one offered: `zh` over `['zh-CN', 'zh-Hans']` answers `zh-Hans`, and `zh-HK` over `['zh-TW', 'zh-Hant']` answers `zh-Hant`. - -Unchanged: a code the table does not know (`es-MX`, `en-GB`, `pt-PT`) falls back through base language and variant expansion exactly as before, and a `zh-TW` request still reaches `zh-CN` when no Traditional key is registered. No call that matched before stops matching. Every caller of these functions moves with them, among them the i18n REST door, built-in validation and operation messages, email template locale folding, hook, flow and approval copy, and plugin-auth's refusal copy. diff --git a/.changeset/30542-action-override-doors-and-boot-order.md b/.changeset/30542-action-override-doors-and-boot-order.md deleted file mode 100644 index 43ee9b53649..00000000000 --- a/.changeset/30542-action-override-doors-and-boot-order.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch -'@objectstack/objectql': patch -'@objectstack/cloud-connection': patch ---- - -fix(objectql, metadata-protocol, cloud-connection)!: a declared `.custom` action override is keyed and judged the same way at every door, and only its declared base's handler slots beneath it - -Clause-②: yes (narrowing) — the protocol door (save and publish) now refuses a declared action override whose `objectName` is not the base action's, with `403 NOT_OVERRIDABLE` and rule `declared_package_ships_no_such_item`, the refusal `os validate` and the managed install already gave it; the save door used to admit it. The managed install now admits a re-install of the pair whose override relaxes an earlier tightening back to the base's own gates, which it used to refuse with `declared_action_widens_access` while the save door admitted it. Every other input answers as before. -Metadata-protocol: none -Skills-docs: follow-up #30505 — the declared action override's docs card; it owes one sentence that an override keeps the base action's `objectName`. - - - -**BREAKING** accept-set narrowing at the protocol door (`@objectstack/metadata-protocol`, ADR-0146 D2/D9): a declared action override names its base's action by `:`, the identity `os validate` and the managed install key it on. - -- **Before.** The save and publish doors matched a declared action override's base by name alone, so an override in the base's `.custom` pair could move the base's action to another object. Its handler was not rebound there, and `os validate` and the install door refused the same body. -- **Now.** The save door refuses it as the other two doors do: the base ships no action of that name on that object. Keep the base action's `objectName` in the override, or remove `overrides` and add the action under the pair's own name. - -`@objectstack/cloud-connection`: the managed install judges a declared action override against the installed base's code artifact (`SchemaRegistry#getArtifactItem`), as the save door does. The protocol's per-package listing it read before serves an armed override under the base's envelope, so a re-install of the pair was judged against its own previous version. - -`@objectstack/objectql`: `ObjectQL#registerActionOverride` takes the package the override declares (`overrides.package`) as an optional fifth argument, and the authored-action resync passes it. `registerAction` under a key such an override holds slots the handler beneath it only when the registry's code artifact of the action belongs to that package, on that object; any other registration, one with no package included, replaces the override and is warned like every other handler collision. Before, any other package's registration slotted beneath it, so a row of another base's pair bound before the real base registered kept running after the real base's handler arrived, while every read served the real base's action; and a registration with no package slotted beneath it at `debug`, where the method's docblock promises a warning. diff --git a/.changeset/30554-in-app-doc-links-locale.md b/.changeset/30554-in-app-doc-links-locale.md deleted file mode 100644 index adfa55f2721..00000000000 --- a/.changeset/30554-in-app-doc-links-locale.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/console': patch -'@object-ui/i18n': patch ---- - -fix(console): a link inside an app's doc keeps the app shell, and the docs chrome reads the session language - -Clause-②: no -Metadata-protocol: none - -A doc read inside an app (`/apps/APP/docs/BOOK/DOC`) rewrote its body cross-references to the portal's `/docs/NAME`, so following one left the app: no top bar, no app navigation. They now resolve under the app's own docs route, as the book sidebar already did, and the header's home link goes to the app's docs index. The header's "Documentation" label, the portal heading and the book sidebar's synthetic "Uncategorized" group title now read the new `help.documentation` and `help.uncategorized` keys, present in all ten locale packs; the group is titled by its key, and authored group labels keep the server's translation. diff --git a/.changeset/30562-sql-never-drop-table-with-rows.md b/.changeset/30562-sql-never-drop-table-with-rows.md deleted file mode 100644 index 93dc236ef74..00000000000 --- a/.changeset/30562-sql-never-drop-table-with-rows.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/driver-sql': patch ---- - -fix(driver-sql): schema sync never drops an existing table that holds rows; a `_id`-keyed table with rows is refused, naming the table and the column - -Clause-②: yes (narrowing) — `SqlDriver.initObjects` / `syncSchema` refuse one more case that used to succeed: an existing table that has a `_id` column, no `id` column and at least one row. It used to be dropped and recreated empty; it is now refused with an `ExistingTableAdoptionRefusal` naming the table, the column and the row count, and nothing is changed. -Metadata-protocol: none - - - -**BREAKING** accept-set narrowing in schema sync (`@objectstack/driver-sql`): a table the sync would have rebuilt is refused while it holds rows. - -- **Before.** Schema sync adopts an existing table by adding the columns the object declares and the table lacks, rows kept. A table keyed by `_id` with no `id` column (the storage key of the ObjectQL driver this one was ported from) was instead dropped and recreated as the object declares, whatever it held. Storing an object on a managed datasource over an existing database reached it: an object named after such a table deleted its rows, and the save answered success. -- **Now.** That rebuild runs only on an empty table, as before. A table that holds rows is refused before anything is written: no column is added, no index is built, no row is touched. The refusal is thrown from `initObjects` and reaches the caller of the sync. The metadata save, the boot sync and a datasource connect report a refused sync in the server log at `error`, followed by this message, as they do for any refused sync. -- **Who moves.** Only a deployment storing an object on a managed SQL datasource whose table of the same name is keyed by `_id` and holds rows. No platform table has that shape. - -**The remedy.** To keep the rows and use the table, rename its `_id` column to `id`, then save the object again (or restart). To read the table as it is, bind the object through a datasource whose `schemaMode` is `external`. If the rows are not needed, empty the table and save again; the sync then rebuilds it. diff --git a/.changeset/30563-driver-catalog-requester-language.md b/.changeset/30563-driver-catalog-requester-language.md deleted file mode 100644 index 5ef31289a11..00000000000 --- a/.changeset/30563-driver-catalog-requester-language.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/service-datasource': patch ---- - -fix(service-datasource): the datasource driver catalog serves its words in the requester's language, so the New datasource dialog reads in one language - -Clause-②: no -Metadata-protocol: none - -`GET /api/v1/datasources/drivers` now serves each driver's label and description, and each connection field's `title` and `description` (Filename, Auto-migrate schema, Host, Port, …), in the language the request names: the top `Accept-Language` tag, else `?locale=`. The code is read through the platform locale table (`platformLocaleTagOf`, shortened one subtag at a time), so `zh-TW` and `zh-HK` get Traditional Chinese, `zh`, `zh-CN` and `zh-SG` Simplified, `pt` Brazilian Portuguese and `fr-CA` French. Every language on the table is translated for every offered driver: Simplified and Traditional Chinese, Spanish, Arabic, French, Brazilian Portuguese, Russian, German, Japanese and Korean. English requests, codes the table does not name, and product names (SQLite, PostgreSQL, MySQL / MariaDB, MongoDB) are served the English text, never a key. `DRIVER_CATALOG` stays the English catalog, and the new `driverCatalogFor(locale)` in the package source builds the localized one; ids, icons, types and defaults are unchanged in every language. diff --git a/.changeset/30565-organization-mfa-notice-once.md b/.changeset/30565-organization-mfa-notice-once.md deleted file mode 100644 index 306fc759436..00000000000 --- a/.changeset/30565-organization-mfa-notice-once.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): turning on Require MFA on an organization shows the grace period once, with the number Setup set - -Clause-②: no -Metadata-protocol: none - -An accepted save that turns `require_mfa` on for an organization now gets a save notice from the server that names who must enroll and the MFA grace period in force. The console still added its own line under it, which says the same thing with the default "7 days". So the toast told the administrator the grace period twice, and when Setup → Authentication set another number, the two lines disagreed. - -The record form now leaves its own line out when the save answer's notice is the server's MFA notice. It recognizes that notice by its opening sentence, in each of the four languages the server writes it in, and not just by a notice being there: the answer carries one notice, the last one a hook or flow on the write set, so another notice on the same save keeps the console line. A save whose answer has no notice (a server that does not send one) shows the console line as before. diff --git a/.changeset/30581-nav-contributions-reenable.md b/.changeset/30581-nav-contributions-reenable.md deleted file mode 100644 index 293fed1f4d1..00000000000 --- a/.changeset/30581-nav-contributions-reenable.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -fix(objectql): a package registered again lists its navigation contributions once, not once more per registration - -Clause-②: no — the served app navigation returns to what a fresh boot serves; nothing a caller sends is refused -Metadata-protocol: none - -- **Before.** Registering a package body appended its `navigationContributions` to the target app's list every time. An artifact reload (`metadata:reloaded`, fired when `os dev` recompiles the artifact or on the HMR reload POST) re-registers every package of the artifact, so after one reload the sidebar listed every module's contributed entries twice, three times after the next, until the server restarted. An uninstalled package's contributions also stayed in the app. -- **Now.** A package body's contributions replace that package's previous set (`SchemaRegistry.setPackageNavContributions`, new), in the place its first registration put them, so the navigation after any number of reloads, re-registrations and disable/enable cycles equals a fresh boot's, in the same order. Uninstalling a package drops its contributions. `registerAppNavContribution` keeps its additive meaning. diff --git a/.changeset/30584-blueprint-field-default-value.md b/.changeset/30584-blueprint-field-default-value.md deleted file mode 100644 index 8e7f474ff3f..00000000000 --- a/.changeset/30584-blueprint-field-default-value.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec): a blueprint field carries a typed `defaultValue`, so an AI-built boolean flag defaults to false - -Clause-②: yes (widening) -Metadata-protocol: added -Skills-docs: none — no file under `skills/**` restates a blueprint field key (`skills/objectstack-ai/references/_index.md` only points at `solution-blueprint.zod.ts`); the reference page `content/docs/references/ai/solution-blueprint.mdx` is regenerated from the new describe. - -The AI solution blueprint had no slot for a field's default. A blueprint that wrote `{ type: 'boolean', defaultValue: false }` for a 是否完成 / 已付款 / `is_active` flag, as the build guidance teaches, parsed with `success: true` and lost the key, so the built field had no default and every record created without the flag read empty instead of false. A filter on false missed those records. - -- **`defaultValue` on `BlueprintFieldSchema` and on the strict structured-output mirror's fields (`SolutionBlueprintStrictSchema.objects[].fields[]`).** Both sides declare it as one scalar union, `boolean | number | string`. Lenient: optional. Strict: required and nullable (null = no default), as with every other key in that mirror. The describe says the value becomes the built field's `defaultValue`, and states the boolean-flag rule: a yes/no flag is a `boolean` field that is never `required` and carries `defaultValue: false`. -- **The value is checked against the field's `type`, on the lenient side.** The check is the field schema's own literal contract (`checkLiteralDefaultValue`, the one `FieldSchema` runs on a built field's `defaultValue`), so a default the blueprint admits is one the built field admits. These types take a default: `boolean` / `toggle` take true or false; `number` / `currency` / `percent` / `rating` / `slider` / `progress` take a number; `text` / `textarea` / `email` / `url` / `phone` / `markdown` / `html` / `richtext` / `code` / `color` take a string; `time` takes `"HH:MM"`; `select` / `radio` take the `value` of one of the field's own `options`, never a label. -- **Every other type is refused by name, never stripped.** The refusal is a `custom` issue at the field's `defaultValue` that names the field and its type, says why, and lists the types that take one: a `date` / `datetime` literal is frozen at design time; a reference or file type names a record or file that does not exist yet; a `multiselect` / `checkboxes` / `tags` field stores a list; a `formula` / `summary` / `autonumber` field is computed; `password` / `secret`, `signature` / `qrcode` and the structured JSON types have no one value right for every record. A runtime token (`NOW()`, `current_user`) is refused too: a blueprint default is a literal. -- The strict mirror cannot read `type` while it judges this key, so it can generate a value the lenient schema then refuses. `apply_blueprint` parses the lenient schema, and that refusal names the type at the key. - -Additive only: every blueprint that parsed before still parses with the lenient schema. A producer of the STRICT shape must now send `defaultValue` on each field (null when it has none). The cloud build applier is what carries the value into the built field; it moves on its own pin. diff --git a/.changeset/30607-seed-locale-liveness-note.md b/.changeset/30607-seed-locale-liveness-note.md deleted file mode 100644 index 45e6d098fd3..00000000000 --- a/.changeset/30607-seed-locale-liveness-note.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): the liveness ledger's `Seed.locale` row names the six seed-load request builders and where each one's locale comes from, so it no longer says the marketplace install passes none - -Clause-②: no -Metadata-protocol: none - -- `liveness/seed.json`'s `locale` row said three publish/install-time paths pass no locale, the marketplace install among them. The install paths now pick seed datasets by locale: the marketplace install, the sample-data reseed, the restart heal and the per-organization replay each resolve one (the organization's Localization setting, then the user's language, then the app's `i18n.defaultLocale`). The boot seed and the dev hot-reload seeder still use the app's `i18n.defaultLocale`. Package apply and draft publish still pass none. -- The earlier ruling that publish and install are locale-neutral is kept as recorded, followed by a dated line saying its bound now covers package apply and draft publish only. The note also says why the loader's no-locale warning stays. -- Nothing to do on upgrade: only the shipped `liveness/seed.json` note text and its `verifiedAt` date change. No key, schema shape or export changes. diff --git a/.changeset/30608-remaining-field-indexes-extension-fields.md b/.changeset/30608-remaining-field-indexes-extension-fields.md deleted file mode 100644 index 10a206782da..00000000000 --- a/.changeset/30608-remaining-field-indexes-extension-fields.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@objectstack/lint': minor ---- - -fix(lint)!: the remaining field indexes (form layout, notify email-field recipient, public form file field, flow templates, page visualization bindings, searchable fields, autonumber formats) judge an object as the registry serves it, and an object that declares no field keeps the list-view rule's skip once an extension adds to it (#30608) - -Clause-②: yes (narrowing) — `os validate` / `os build` / `os lint` refuse shapes they accepted before, each a field an `objectExtensions` entry of the same artifact adds or re-declares, now judged on its served definition: a notify node's `recipients.emailField` naming an owner `email` field an extension re-declares as `text` is `notify-email-field-not-email` at `error`; a form open to anonymous intake whose section names an `image` / `file` field an extension adds, or an owner field an extension re-declares as one, is `public-form-file-field` at `error`; a `list` page whose `allowedVisualizations` leads with `gallery` over an owner `image` field an extension re-declares as `text` is `page/visualization-without-binding` at `error`; a `searchableFields` entry naming an owner text field an extension re-declares as `formula` is `searchable-field-unsearchable` at `error`; an autonumber field an extension adds whose format names a field the object lacks is `autonumber-references-unknown-field` at `error`, and an autonumber format naming an owner field declared `required: true` that an extension re-declares without it is `autonumber-references-optional-field` at `warning`; a flow template `{{ record.F.sub }}` through an owner text field an extension re-declares as a lookup is `flow-template-lookup-traversal` at `warning`. The widening half: a form section, a notify email-field recipient, a flow template hole, a gallery or calendar binding, a `searchableFields` entry (an object's own set, a list view's narrowing, a react page's ``) or an autonumber format naming a field an `objectExtensions` entry adds is no longer refused as unknown, and a list view over an object that declares no field is no longer refused once an extension adds to it. -Metadata-protocol: narrowed -Skills-docs: none — measured: no sentence in `skills/` or `content/docs/` states a declared-only reading of the moved rule ids - - - -**BREAKING** accept-set narrowing in `os validate` / `os build` / `os lint` (`@objectstack/lint`), for multi-package artifacts whose bodies add fields to another package's object through `objectExtensions`. - -- **Before.** Seven field indexes read an object's declared `fields` only: the form-layout rule (`form-field-unknown`), the notify email-field recipient rule, the public form file-field rule, the flow template rule, the page visualization binding rule, the searchable-fields index (`indexObjectSearchTargets`, behind both an object's or list view's `searchableFields` and a react page's ``) and the autonumber format lint. A field an `objectExtensions` entry adds was unknown to them although the runtime serves it, and a field an extension re-declares was judged on the owner's definition. The list-view rule, since it reads the served graph, judged an object that declares no field as having only the extension's fields. -- **Now.** Each reads the shared fold in `object-graph.ts` (`objectsAsServed`): the declared fields plus each field an `objectExtensions` entry adds, an extension's re-declared field replacing the owner's definition whole, as the registry's `mergeObjectDefinitions` does. A name no body declares or adds keeps the same refusal, rule id, severity and message. These findings move (union artifact and per-package leg alike): - - an extension-added field named by a form section, a notify email-field recipient (typed `email`), a flow template hole, a `searchableFields` entry or an autonumber format (declared `required: true`): its unknown-field verdict (`form-field-unknown` `warning`, `notify-email-field-unknown` `error`, `flow-template-unknown-field` `warning`, `searchable-field-unknown` `error`, `autonumber-references-unknown-field` `error`) → nothing; a `gallery` / `calendar` binding derives from it, so `page/visualization-without-binding` → nothing; - - the same field typed otherwise: a notify recipient typed `text` → `notify-email-field-not-email` `error` (was `notify-email-field-unknown` `error`); an autonumber format naming it not `required` → `autonumber-references-optional-field` `warning` (was `autonumber-references-unknown-field` `error`); on a public form, an `image` / `file` / `avatar` / `video` / `audio` field → `public-form-file-field` `error` (was unjudged); - - an owner field an extension re-declares: judged on the extension's type, `required`, or autonumber format (the rows the Clause-② line names), and the inverse re-declarations pass (an owner `file` field re-declared `text` on a public form, an owner lookup re-declared `text` in a template, an owner autonumber re-declared `text`); - - an autonumber field an extension adds is checked as an owner's is; - - a list view over an object that declares no field: `list-view-field-unknown` `error` → nothing, extended or not. - - a flow variable bound to a record whose name is a field an `objectExtensions` entry adds to the trigger object: the record-triggered run flattens that field to top level too, so the variable is an ambiguous root and its holes are no longer judged (`flow-template-unknown-field` `warning` → nothing), as for a variable named after a declared trigger field; - - a form section on an extended object whose owner declares `fields` in the legacy array shape: the owner's array fields are now known names, so `form-field-unknown` `warning` on them → nothing. -- **Not judged.** A field two packages write at the same deciding priority is a served name, but no rule judges its type, `required` or autonumber format: which definition wins is registration order (a gallery page over an object carrying one is not reported). An object that declares no field of its own stays unjudged by the notify, public form, page visualization, searchable and autonumber rules and by the list-view rule, extended or not (an introspected schema). The form-layout and flow template rules judged such an object against the empty set before and still do, an extension's own names now known. -- **Not changed.** Which extension entries the fold keeps, the runtime, the object-level reads (`external`, the injected anchors, an object's own list views and `searchableFields`), and the finding paths. A stack that names no extension field reads as before. A flow variable whose name no body declares or adds on the trigger object is judged as before. A form section on an object nothing extends reads only map-shaped `fields`, as before: its legacy array fields are still reported `form-field-unknown`. - -**What an author does.** Fix the extension's field definition (type an email recipient `email`, keep a file field off a public form, give a gallery an image field, keep a search column stored, mark an autonumber scope field `required: true`) or the reference that names it. diff --git a/.changeset/30610-extension-redeclared-owner-field-refused.md b/.changeset/30610-extension-redeclared-owner-field-refused.md deleted file mode 100644 index 60e136ddcb8..00000000000 --- a/.changeset/30610-extension-redeclared-owner-field-refused.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor -'@objectstack/cli': minor -'@objectstack/cloud-connection': minor ---- - -fix(metadata-protocol,cli,cloud-connection)!: an object extension that re-declares an owner's field in `fields` beyond its display properties is refused at the save and publish doors, at `os validate` and at the install door, with a prescription naming `fieldPatches` (ADR-0146 D10) (#30610) - -Clause-②: no (narrowing) — the `object_extension` save, publish and restore doors, `os validate` and `POST /api/v1/marketplace/install-local` refuse an `objectExtensions[].fields` entry naming a field the extended object's owner declares when it changes anything outside the display-property whitelist, a body they accepted before. Nothing is widened. -Metadata-protocol: narrowed -Skills-docs: no move — `skills/**` and `content/docs/**` already teach `fieldPatches` as the way to change a base field's display, and no page teaches taking an owner field over through `fields`; the prescription ships in the refusal text. - - - -**BREAKING** accept-set narrowing at the metadata save and publish doors (`PUT /api/v1/meta/object_extension/NAME`, draft and live, the per-item and package publish, and the restore doors), at `os validate`, and at the marketplace install door. - -- **Before.** An extension written into `fields` with the name of a field the extended object's owner declares replaced the owner's field whole when the registry folded the object: a module package could turn the owner's `amount: number` into `text`, drop its `required`, or make it `readonly`, and every reader judged the field on the extension's definition. Only the registry's warning named it. -- **Now.** Such an entry is refused when it changes anything outside the display-property whitelist (`label`, `description`, `inlineHelpText`, `placeholder`, an option's `label`): `400 VALIDATION_ERROR` at the save and publish doors (located in `issues[]` at `fields.FIELD`), `422 VALIDATION_ERROR` at the install door (every entry in `details.refusals`), and exit 1 with the entry on `errors` at `os validate`. Each refusal names the keys the entry changes and prescribes `fieldPatches`. Both bodies are read through the field schema first, so a default the parse stamps is no change; options are compared by stored value, so a relabel or reorder is no change. -- **Unchanged.** A re-declaration that changes only display properties is admitted. An extension of an object its own package owns is not judged, nor is an added field. `os validate` judges an owner declared by another package of the same artifact; a target outside the artifact is judged at install, against the installed owner. The registry's warning on a re-declared field stays. `os build` does not yet run the check. - -**The remedy.** Move a display change of an owner's field into the extension's `fieldPatches` (the field name as its key, the whitelisted properties as its body), and add a behaviour of your own as a new field under a new name. diff --git a/.changeset/30615-report-chart-table-drill-measure-scopes.md b/.changeset/30615-report-chart-table-drill-measure-scopes.md deleted file mode 100644 index 16aa130d83b..00000000000 --- a/.changeset/30615-report-chart-table-drill-measure-scopes.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/plugin-report': patch -'@object-ui/plugin-dashboard': patch ---- - -fix(plugin-report,plugin-dashboard): a report, chart, table or cross-tab drill on a filtered measure lists the records the number counted - -Clause-②: no -Metadata-protocol: none - -A drill used to list the clicked bucket AND the report or widget filter only. The number also carries the dataset's own `filter` and the measure's own `filter`, so a drill on a filtered measure listed a superset: on the app-showcase Revenue Pulse dashboard, the Paid Invoices cell of the Sent row reads 0 and its drill listed all 4 sent invoices. The drill now also ANDs in the answer's `drillScopes` entry for the measure it stands for (the server's per-measure list filter; `{}` adds nothing, so a drill on an unfiltered measure is unchanged). - -- **What a click stands for.** A chart mark stands for its series' measure (a pivoted chart for its one measure), a cross-tab or matrix cell for its column's measure, and a table row for every measure it holds. -- **A table row whose measures select different records drills cell by cell.** When the row's measures all share one scope (one measure, or several plain ones), the row drills as before. Otherwise each measure cell with a scope is its own drill target (test id `dataset-drill-cell`) and the row is not. -- **No list that does not match the number.** A measure with no `drillScopes` entry (a derived measure, or a scope naming a relationship path) offers no drill, and neither does any measure of an answer that carries no `drillScopes`. A comparison (`__compare`) series opens nothing. A chart none of whose drawn measures has an entry gets no click handler. -- **Unchanged:** a report answer with no `object` (a server older than ADR-0021 D2) still hands the host its group-key fallback drill. diff --git a/.changeset/30616-sample-data-count-after-reload.md b/.changeset/30616-sample-data-count-after-reload.md deleted file mode 100644 index fe8435e6650..00000000000 --- a/.changeset/30616-sample-data-count-after-reload.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -feat(console): after a reload, the package page still says how many sample records a purge would delete - -Clause-②: no -Metadata-protocol: none - -- **The count on any visit.** The package page of a local install now reads the runtime's count of the sample rows a purge would delete right now (`GET /api/v1/marketplace/install-local/:manifestId/sample-data`), on every visit and again after an install, a reseed or a purge. Before, the purge confirm and the banner that follows the installer to the app knew a count only for an install made in the same browser session; a reload lost it. -- **The confirm.** The local purge confirm draws that count. When it cannot be read (refused, failed), it draws the count this session's install answered, as before. A read that began before an install, reseed or purge of this session answered is older than that answer and is not used. -- **The banner.** A count above zero raises the sample-data banner on leaving the page, as an install of this session does. A count of `0` drops what the session knew: no banner, and no count line in the confirm. -- **Types.** `marketplaceApi` gains `countLocalSampleData` and `LocalSampleDataCount`. -- **Strings.** `@object-ui/i18n` rewords `marketplace.detail.purgeConfirmAtMost` (all plural forms) in all 10 locales: it no longer says the count is "the number this install loaded", which is not what the runtime's count is. It stays an upper bound ("Up to N sample records will be deleted."), true of both counts. No key is added. diff --git a/.changeset/30618-script-subtag-read-aliases.md b/.changeset/30618-script-subtag-read-aliases.md deleted file mode 100644 index 0a59760d39d..00000000000 --- a/.changeset/30618-script-subtag-read-aliases.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -fix(spec/system): the platform locale table reads the script-subtag Chinese spellings, so a `zh-Hant-TW` / `zh-Hant-HK` reader gets a registered `zh-Hant` bundle instead of Simplified Chinese - -Clause-②: yes (widening) -Metadata-protocol: semantic - -`PLATFORM_LOCALE_READ_ALIASES` (`@objectstack/spec/system`) gains five rows, one for the script-subtag spelling of each regional Chinese alias: `zh-Hans-CN` and `zh-Hans-SG` read as `zh-Hans`; `zh-Hant-TW`, `zh-Hant-HK` and `zh-Hant-MO` read as `zh-Hant`. `platformLocaleTagOf` answers them (case-insensitively, as for every row), and `PlatformLocaleReadAlias` gains the five literal keys. - -What changes for a reader: browsers and operating systems send these spellings. `resolveBundleLocale` and `resolveLocaleAmong` (and so `resolveLocale` and `resolveOfferedLocale` in `@objectstack/core`) now map them through the table before the base-language step, so over a bundle that holds both scripts a `zh-Hant-TW`, `zh-Hant-HK` or `zh-Hant-MO` request reaches the `zh-Hant` key and a `zh-Hans-CN` or `zh-Hans-SG` request reaches the Simplified key (`zh-CN`, `zh-Hans` or a bare `zh`), whatever order the keys were registered in. Before, the first registered `zh-*` key answered them, and over `{ zh, zh-Hant }` a `zh-Hant-TW` request got the bare `zh` (Simplified) key. The table step maps each registered key through the table too, so a bundle key or offered code spelled `zh-Hant-TW` / `zh-Hans-CN` now counts as its script for every request the table reads as that script: `zh-TW` or `zh-Hant` over `[en, zh-CN, zh-Hant-TW]` gets `zh-Hant-TW` (before: `zh-CN`), `zh-CN` over `[en, zh-Hant, zh-Hans-CN]` gets `zh-Hans-CN` (before: `zh-Hant`), and `zh-Hant-TW` over `[en, zh, zh-Hant-HK]` gets `zh-Hant-HK` (before: `zh`). Setup's default-language options (`localization.locale`) name the five codes from the table (`zh-Hant-TW` is 繁體中文, `zh-Hans-CN` is 简体中文) where they used the runtime's display name (中文(繁體,台灣)), and a stored `zh-Hant-TW` default now shows selected on the 繁體中文 option: where the deployment listed `zh-CN` before `zh-Hant`, it used to take the place of the 简体中文 option. The values Setup accepts are unchanged. - -Unchanged: the list stays closed. Only these five spellings are added; a script-subtag spelling without a row (`zh-Hant-US`) and every other code the table does not name (`pt-PT`, `en-GB`, `es-419`) answer `undefined` and fall back through base language and variant expansion exactly as before. With no registered key of the requested script, a script-subtag request still reaches the other script's key, as before. No call that matched before stops matching. diff --git a/.changeset/30619-bundle-locale-own-key.md b/.changeset/30619-bundle-locale-own-key.md deleted file mode 100644 index 1b5f233e3ba..00000000000 --- a/.changeset/30619-bundle-locale-own-key.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): `resolveBundleLocale` and the bundle reads behind every translator answer a locale only from the bundle's own keys - -Clause-②: no -Metadata-protocol: none - -A requested locale code that names an `Object.prototype` member (`constructor`, `toString`, `__proto__`) is found on every plain-object bundle. Until now `resolveBundleLocale` answered such a code as a registered locale, and the translators' bundle read handed back the inherited function as that locale's data. One answer was visible to readers: an object read (`/meta`) under `?locale=constructor` or `Accept-Language: constructor` served the `Object` constructor as the label of every platform-injected system field still at its English default (so the label went missing from the JSON), and under `__proto__` served `Object.prototype` there. Both reads now check for an own property: the code reads as an unknown locale and takes the same fallback `'xx'` takes, so those fields keep their English label. A bundle that really carries such a key as its own (built by `JSON.parse` or on `Object.create(null)`) still answers it, and an own key whose value is `undefined` still does not answer the exact step. No signature, export or schema changes. diff --git a/.changeset/30623-moved-action-override-refusal.md b/.changeset/30623-moved-action-override-refusal.md deleted file mode 100644 index 7e0ddbb591d..00000000000 --- a/.changeset/30623-moved-action-override-refusal.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch -'@objectstack/cli': patch -'@objectstack/cloud-connection': patch ---- - -fix(metadata-protocol, cli, cloud-connection): a declared action override moved to another object is told which object the base ships the action on - -Clause-②: no -Metadata-protocol: none - -The `declared_package_ships_no_such_item` refusal (`403 NOT_OVERRIDABLE`) for a `.custom` action override whose `objectName` differs from the base action's used to say the base "ships no action named 'X'" and to "correct the name", though the name was right. It now names both objects and tells the author to keep the base action's `objectName`, at all three doors (the save and publish door, `os validate`, the managed install), from one sentence builder in `judgeOverrideWrite`. A name the base does not ship at all reads as before. The rule, code and status are unchanged. diff --git a/.changeset/30626-migrate-withheld-extension-columns.md b/.changeset/30626-migrate-withheld-extension-columns.md deleted file mode 100644 index b9154b4e84a..00000000000 --- a/.changeset/30626-migrate-withheld-extension-columns.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os migrate` value walks name the columns they skip because the package that adds them is disabled - -Clause-②: no -Metadata-protocol: none - -`os migrate blank-single-values`, `blank-multi-values`, `option-codes`, `text-scalars` and `value-shapes` read each object as it is served. While a module package is disabled, the fields its `objectExtensions` add are withheld from that object (their columns and values are kept), so these runs could not read those columns and their reports read as if they had. Each report now lists every such column as `object.field (package ID)`, and the `--json` document carries the same list as `withheldColumns`. Enable the package and re-run to cover them. Nothing else changes: the runs still read and rewrite only what the served object declares. diff --git a/.changeset/30635-object-save-reports-refused-storage-sync.md b/.changeset/30635-object-save-reports-refused-storage-sync.md deleted file mode 100644 index 6a14899cc64..00000000000 --- a/.changeset/30635-object-save-reports-refused-storage-sync.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix(metadata-protocol): an object save or publish whose storage sync the datasource refuses now says so on its answer - -Clause-②: no -Metadata-protocol: none - -`PUT /api/v1/meta/object/NAME` (a publish-mode save) and `POST /api/v1/meta/object/NAME/publish` sync the object's table after the item is written. When the datasource refused that sync, both answered `200` success with nothing about storage, and only the server log said the table was not created or altered. Both answers now carry one `warning` on their existing `advisories` key, with the rule id `object-storage-sync-refused`, naming the object and saying its storage did not follow. The datasource's own text is quoted when it declared a client refusal (a 4xx status); otherwise the advisory says the reason is in the server log, which keeps the `error` line it already printed. The status code and every other key are unchanged, the publish is still not failed, and an answer whose sync ran has no new key. - -A client that groups or suppresses advisories by rule id sees one new id. The package-draft batch publish (Studio's "publish whole app") still reports a refused sync in the server log only. diff --git a/.changeset/30646-assigned-work-injected-owner.md b/.changeset/30646-assigned-work-injected-owner.md deleted file mode 100644 index 69d65881936..00000000000 --- a/.changeset/30646-assigned-work-injected-owner.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/core': patch ---- - -fix(core): a banned user's assigned-work count and hand-over now include the platform's injected `owner_id` - -Clause-②: no -Metadata-protocol: none - -The field picker behind the assigned-work count and the bulk hand-over skipped every `system: true` user reference. The `owner_id` the platform injects on every business object is `system: true` (it is a platform column) but not `readonly` (ownership is transferable), so it was skipped: on any object whose author had not declared `owner_id` themselves, the records a banned user owned were counted as zero and never handed over. The picker now leaves out `readonly` user references only, so `created_by` / `updated_by` stay out and the injected `owner_id` is counted and handed over. Platform tables (`sys_*`), `managedBy` objects and injected columns with no storage stay excluded as before. diff --git a/.changeset/assignment-write-unaddressable-row-not-found.md b/.changeset/assignment-write-unaddressable-row-not-found.md deleted file mode 100644 index a088e923502..00000000000 --- a/.changeset/assignment-write-unaddressable-row-not-found.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security): an assignment write the administrator cannot address answers not-found, as a read does; a refusal names only values the caller can read - -Clause-②: no -Metadata-protocol: none - -An organization administrator or a delegated administrator (a caller holding an `adminScope`, an organization administrator who holds one included) updates or removes an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. - -- **A row the caller cannot read, and an id that names no row,** now answer `404 RECORD_NOT_FOUND`: the read door's answer for that object and id, which is what a by-id write of any other object already answers. Both used to answer `403 PERMISSION_DENIED` ("the assignment row … could not be read"). An organization administrator's edit in place is asked the same question first, so an edit of a row it reads is still refused as an edit. -- **A caller the read door refuses outright** (no read grant on the table) still gets that read refusal. -- **A refusal names a value only when the caller can read it.** A delegated administrator's `DELEGATION_SCOPE_VIOLATION` names the row's position, business unit, holder or permission set only when the caller's own read returns that column at its stored value, or when the caller wrote the value itself. Otherwise the sentence is the same without the value (for example "the assignment's position distributes a permission set which is not in the scope's allowlist"), and `details` leaves the value out. The verdict does not change: it is still taken on the stored row. -- **Unchanged.** A row the caller reads keeps its verdict and its guidance. Inserts and tenant-level administrators are judged as before, and every write that was admitted is still admitted. - -For clients: a by-id update or delete of an assignment row that is gone now answers `404`, as it does on every other object. diff --git a/.changeset/banned-user-credentials-refused.md b/.changeset/banned-user-credentials-refused.md deleted file mode 100644 index b9c24b53046..00000000000 --- a/.changeset/banned-user-credentials-refused.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/core': patch -'@objectstack/platform-objects': patch -'@objectstack/plugin-auth': patch ---- - -fix(core)!: a banned user's credentials are refused while the ban holds, at every door that accepts an API key - -Clause-②: yes (narrowing) — while a user's ban holds, a request authenticated by an API key that user owns is answered as a key that does not exist (the generic `401 UNAUTHENTICATED`), at every door that accepts a key: the REST data door, the MCP door, and every other door that resolves a key through the shared key-admission seam. The widening half: `@objectstack/core` exports the one ban predicate, `isUserBanInForce` (with `USER_BAN_FIELDS` and the `UserBanFields` type), which the key-admission seam and plugin-auth's session-shaped refusals now share. -Metadata-protocol: none - - - -**BREAKING** accept-set narrowing at every key-admitting door (`@objectstack/core`): a banned user's API keys are refused while the ban holds. - -- **The rule.** A ban holds every credential of the user. Banning a user already ends their sessions and refuses their sign-in; their API keys now answer as a key that does not exist for as long as the ban holds. A ban with an expiry stops binding once the expiry has passed, exactly as sign-in does — no sign-in is needed to lapse it. -- **Not a revocation.** No key row is revoked or rewritten. Lifting the ban (or letting it expire) admits the same keys again, with nothing reissued, each still judged by its own lifecycle: a key left unused past the inactivity window during the ban answers as inactive (`API_KEY_INACTIVE`), and its remedy is a new key. -- **One anonymous entry.** A key refused for its owner (a ban) or for the tenancy posture (no organization under `isolated`) resolves to the same entry as a key that does not exist, on every composition: the guest's grants on a face that serves the guest, nothing elsewhere. It never falls through to a session. -- **Who moves.** An integration running on a key whose owner is banned gets `401` until the ban is lifted or expires. - -**The remedy.** Lift the ban, or mint a replacement key under a user who is not banned. diff --git a/.changeset/banned-user-oauth-credentials-refused.md b/.changeset/banned-user-oauth-credentials-refused.md deleted file mode 100644 index bed16a02f52..00000000000 --- a/.changeset/banned-user-oauth-credentials-refused.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/runtime': patch -'@objectstack/plugin-auth': patch -'@objectstack/platform-objects': patch ---- - -fix(runtime,plugin-auth)!: a banned user's OAuth access tokens and refresh grants are refused while the ban holds - -Clause-②: yes (narrowing) — while a user's ban holds, an OAuth access token that user consented to is answered at the MCP door as a token that does not verify (the generic `401`, with the same body and `WWW-Authenticate` header), and so is an agent forward credential acting for that user; `/oauth2/userinfo` answers that token as an access token that does not verify (`401 invalid_token`), and `/oauth2/introspect` answers that user's access and refresh tokens as inactive (`{ "active": false }`); the token endpoint mints that user no token: a `refresh_token` grant answers as an invalid refresh token (`400 invalid_grant`), and an `authorization_code` exchange is refused, with the code spent: by the provider's own session answer (`400 invalid_request`) when the ban ended the user's sessions, as the ban routes do, and otherwise as an invalid code (`400 invalid_grant`). Those are the doors this change narrows. -Metadata-protocol: none - - - -**BREAKING** accept-set narrowing at the MCP door and at the OAuth authorization server's token, userinfo and introspection endpoints: a banned user's OAuth access tokens and refresh grants are refused while the ban holds. - -- **The rule.** Banning a user already ends their sessions and refuses their sign-in and their API keys. While the ban holds, their OAuth access tokens are now also refused at the MCP door and read as not valid at userinfo and introspection, and the token endpoint mints them no new token (no refresh, no code exchange). A ban with an expiry stops binding once the expiry has passed, exactly as sign-in does. -- **Not a revocation.** No token, refresh token or consent row is revoked or rewritten. Lifting the ban (or letting it expire) admits the same access token at the MCP door and lets the same refresh token renew, with nothing reissued. An authorization code presented while the ban held is spent, so the client signs the user in again. -- **Cost.** One primary-key read of the user's ban columns per OAuth credential the MCP door resolves, and one read of the user row per active introspection answer that names a user; the token and userinfo endpoints read nothing extra. A store fault on the MCP door's read answers `503 SERVICE_UNAVAILABLE`, as every other authorization-input read on that door does; a store fault on the introspection read fails the request rather than answering active or inactive. -- **Who moves.** An MCP client or OIDC relying party connected over OAuth on behalf of a banned user gets `401` at the MCP door and at userinfo, `{ "active": false }` from introspection, and `400` from the token endpoint, until the ban is lifted or expires. The Ban User confirmation (`@objectstack/platform-objects`) and the administrator and authentication guides now say so. - -**The remedy.** Lift the ban, or connect the client as a user who is not banned. diff --git a/.changeset/by-id-write-row-reread-rules.md b/.changeset/by-id-write-row-reread-rules.md deleted file mode 100644 index 3217934a710..00000000000 --- a/.changeset/by-id-write-row-reread-rules.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security): a by-id write's row re-read propagates a store fault, and judges the platform's own predicates without the caller-predicate guard - -Clause-②: yes (widening) -Metadata-protocol: none - -Before an update or delete by id, the security middleware re-reads the addressed row under the caller's context with the platform's write predicates for that row (the tenancy wall and the row-level write predicates). Two rules hold for that re-read, and the second for the controlled-by-parent master read a detail write asks as well: - -- **A store fault propagates as raised.** A re-read the store cannot answer (an unavailable datasource, a driver failure) is the write's answer, unchanged — for `ERR_DATASOURCE_UNAVAILABLE` the transports answer `503` — and nothing is written. Only a declared refusal (a 4xx) keeps the record-level `403 PERMISSION_DENIED`, one answer whatever the id names. The controlled-by-parent master read keeps its `403` for every failure. -- **The platform's own predicates are not the caller's predicate.** The field-level predicate guard refuses a caller's filter on a field its field rules do not let it read, and judges only the caller's predicate: a predicate subtree the platform composed and marked as its own is not judged. That holds for every such subtree that reaches the guard inside a caller-context read — the by-id write re-read and the controlled-by-parent master read, the analytics read-scope merge handed to an aggregate under the caller's context, and a record-sharing read scope composed before the security middleware. Only platform-composed predicates are exempt; nothing the caller writes is. Under the `group` and `isolated` tenancy postures, a caller whose field rules hide the organization column updates and deletes the records of its organization by id, writes a controlled-by-parent detail under a master of its organization, and aggregates over the records of its organization. A record of another organization and an id that names no record answer that caller exactly as they answer any other caller. - -Unchanged: a caller's own filter on a field it cannot read is refused, on a read, an analytics query and a predicate write; a record the caller reads but may not write keeps its `403`; a system write is not the gate's. diff --git a/.changeset/change-email-notice-every-sign-in.md b/.changeset/change-email-notice-every-sign-in.md deleted file mode 100644 index 97a0c1719a7..00000000000 --- a/.changeset/change-email-notice-every-sign-in.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix(plugin-auth): a change-email request notifies the current address whatever way the caller is signed in - -Clause-②: no -Metadata-protocol: none - -- **The rule.** Every `POST /api/v1/auth/change-email` request that is accepted mails the account's current address the `auth.email_change_notice`, whatever way the caller is signed in. The notice goes to the account the request changes: its address is read from the session the endpoint itself acted on. -- **Unchanged.** The notice's template and text; the confirmation mail to the requested address; a refused request still mails nothing; the notice still never blocks the change. diff --git a/.changeset/cli-connection-label-shared-url-grammar.md b/.changeset/cli-connection-label-shared-url-grammar.md deleted file mode 100644 index cf4484ecbd4..00000000000 --- a/.changeset/cli-connection-label-shared-url-grammar.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): a connection label printed by the CLI removes a password or a credential query pair wherever the URL rules read one - -Clause-②: no -Metadata-protocol: none - -- **One credential grammar.** The database label that `os dev`, `os start` and `os serve` print, and the migrate and resync confirms, now remove URL credentials through the same shared function the datasource read door applies (`redactUrlCredentials` from `@objectstack/spec/data`): the userinfo password, and every credential query pair judged by the shared query-pair judge and its one list of names. Wherever those rules read a credential, on every path the label takes, the label drops it. When the shared rules read more, the label follows with no edit of its own. -- **Unchanged.** Every value the shared rules read no credential in prints exactly as before: `postgres://`, `libsql://` and `file:` values, values with no userinfo, a username-only userinfo, and everything the URL parser already cleaned. diff --git a/.changeset/cloud-3485-turso-remote-balanced-group.md b/.changeset/cloud-3485-turso-remote-balanced-group.md deleted file mode 100644 index a6945f0d61d..00000000000 --- a/.changeset/cloud-3485-turso-remote-balanced-group.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/driver-turso': patch -'@objectstack/driver-sql': patch ---- - -fix(driver-turso): the remote face joins a large `$and` / `$or` as balanced halves, so comment and attachment reads fit a hosted libSQL server's depth limit - -Clause-②: no -Metadata-protocol: none - -On a remote Turso / libSQL database (`TursoDriver` with a `libsql://`, `https://` or `wss://` url and no `syncUrl`, as every hosted environment runs), every read of `sys_comment` and `sys_attachment` answered `500 DATABASE_ERROR` once the deployment registered roughly a hundred objects, as an installed app does. The driver's log named the cause: `Expression tree is too large (maximum depth 100)`. The comment and attachment read gates OR one `ReadableIds` branch per registered parent object, and the remote face's compiler joined a group's members into one flat chain, one expression level per member, while the hosted server refuses a tree deeper than 100. - -The local face (`SqlDriver`) has emitted such groups as balanced halves since #29888. The remote face compiles its SQL text itself, so that change never reached it. It now regroups by the same rule, shared from `@objectstack/driver-sql` as `joinBalancedGroup` (with `FLAT_GROUP_MAX_MEMBERS`): a group of at most 8 members compiles byte-identical to before, and a larger one as two parenthesised halves, recursively, so the depth it adds grows with the logarithm of its size. Measured on the gate's own shape with 134 branches and a three-level master chain: 162 levels before, 38 after. - -The regrouped expression selects exactly the rows the flat one did, on every dialect: `AND` and `OR` are associative over SQL's three-valued logic, the members keep their order, and so do their bind values. No gate, scope or tenant wall changes. diff --git a/.changeset/cloud-3565-chatbot-tool-labels.md b/.changeset/cloud-3565-chatbot-tool-labels.md deleted file mode 100644 index c795c1967ed..00000000000 --- a/.changeset/cloud-3565-chatbot-tool-labels.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -feat(i18n): `chatbot.tool.create_record` / `update_record` / `remove_sample_data` labels and the record-write card's `console.ai.recordWrite*` labels in all ten packs; `chatbot.tool.draft_record` retired - -Clause-②: no -Metadata-protocol: none - -The spec registry now lists the cloud chat agents' confirmed record write and the sample-data cleanup (cloud#3565) and drops `draft_record`, so the chatbot's tool timeline gets a sentence for each new step in every pack ("Create the record" / "新建记录", "Change the record" / "修改记录", "Remove the sample data" / "清理示例数据") and loses the retired one, keeping `toolLabels-locale-parity-7481` and `toolLabels-sentences-12724` green. The console's record-write card gets its eleven labels under `console.ai.recordWrite*`: the create and update titles (with the object's label), the three buttons 确认 / 打开完整表单 / 取消, the two stored reasons, the "not included" lead-in and the three states (writing, written, cancelled) — plus the 「清理示例数据」 card's four: `console.ai.sampleDataCleanupTitle` (its title over the seeded tables and counts), `console.ai.sampleDataCleanupIrreversible` (「删除后无法恢复」 — the removal cannot be undone, said on every card), and the two count families `console.ai.sampleDataCleanupAbsent` (the recorded rows the card does not list) and `console.ai.sampleDataCleanupEdited` (the rows kept because someone edited them), each with every plural slot its language selects (objectui#11445). diff --git a/.changeset/cloud-3565-record-write-card.md b/.changeset/cloud-3565-record-write-card.md deleted file mode 100644 index 33829500d65..00000000000 --- a/.changeset/cloud-3565-record-write-card.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/plugin-chatbot': patch ---- - -feat(plugin-chatbot): the record-write card — ONE record the chat proposes and the user confirms, with 确认 / 打开完整表单 / 取消 (cloud#3565) - -Clause-②: no -Metadata-protocol: none - -The cloud chat's `create_record` / `update_record` tools queue a HITL pending action in the user's name and answer the approval envelope plus the record: `{ status:'pending_approval', pendingActionId, recordWrite:{ operation, objectName, objectLabel, recordId?, label?, reason?, fields:[{ name, label, type, value, current? }], dropped } }`. The chat now draws that as a card instead of the generic Approve / Deny strip and the raw payload: - -- **New export** `detectRecordWrite(result)` (with the `RecordWrite` and `RecordWriteField` types), lifted onto `ChatbotEnhancedToolInvocation.recordWrite` by the live mapper beside `pendingActionId`. It reads the bare object, the persisted `{ type:'text', value }` wrapper and a JSON string; an envelope without the pending id or without a field to confirm is not a card. -- **The card** (`data-testid="record-write"`): the create or update title with the object's label, the record's label and reason, every field as a labelled row — an update shows the stored value struck through, then →, then the proposed one — the fields the server left out and why, and three buttons. 确认 calls `onToolApprove(toolCallId, true)` (the pending action's dispatcher writes as the user); 取消 calls it with `false` and `recordWriteCancelReason`; 打开完整表单 calls it with `false` and `recordWriteOpenFormReason` FIRST, then the new `onOpenRecordForm(write)` prop, so a record saved in the form is never written a second time from the card (disabled where the host passes no handler). After a decision the card says written (with the record's link through `onOpenRecord`, read from the dispatcher's `{ status:'written', objectName, recordId, label }` result), cancelled, or the server's refusal. -- **New props**: `onOpenRecordForm`, `recordWriteCreateTitle(objectLabel)`, `recordWriteUpdateTitle(objectLabel)`, `recordWriteConfirmLabel`, `recordWriteOpenFormLabel`, `recordWriteCancelLabel`, `recordWriteCancelReason`, `recordWriteOpenFormReason`, `recordWriteDroppedLabel`, `recordWriteSubmittingLabel`, `recordWriteWrittenLabel`, `recordWriteCancelledLabel`, all with English defaults. -- **「清理示例数据」** rides the same seam: `detectSampleDataCleanup(result)` (with the `SampleDataCleanup` type) lifts `{ status:'pending_approval', pendingActionId, sampleDataCleanup:{ tables:[{ objectName, label, count }], total, absent, edited } }` onto `sampleDataCleanup`, and the chat draws the tables and counts (`data-testid="sample-data-cleanup"`, title from the new `sampleDataCleanupTitleLabel` prop) above the generic Approve / Deny strip, with the raw payload hidden. The card always says the removal cannot be undone (`sampleDataCleanupIrreversibleLabel`, default "Removed rows cannot be recovered." — there is no recycle bin until objectstack#3146), says how many recorded rows it does not list when `absent` is above 0 (`sampleDataCleanupAbsentLabel(count)`) and how many stay because someone edited them when `edited` is above 0 (`sampleDataCleanupEditedLabel(count)`; an envelope without `edited` reads as 0). -- **`ToolDecisionState`** (widened, additive): every state may carry `decision: 'approved' | 'rejected'`, and an approved `success` may carry `result`, the dispatcher's answer. `useHitlInChat` sets both; the generic strip keeps reading `message` alone. diff --git a/.changeset/cloud-3565-spec-record-write-tool-names.md b/.changeset/cloud-3565-spec-record-write-tool-names.md deleted file mode 100644 index 05bde56e6cd..00000000000 --- a/.changeset/cloud-3565-spec-record-write-tool-names.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -feat(spec): `PLATFORM_TOOLS_BY_PACKAGE['service-ai-studio']` lists `create_record`, `update_record` and `remove_sample_data`; `draft_record` is retired - -Clause-②: no -Metadata-protocol: none - -The cloud chat agents now create or change ONE record of the user's data after the user confirms it on a card, and remove the sample rows a seed replay recorded after a card listing them (cloud#3565, the maintainer's ruling of 2026-11-04). The three tools replace `draft_record` (cloud#3383), whose "draft it and the user saves it in a form" ending was never built. The registry lists the three names in their alphabetical order and drops the retired one, so `PLATFORM_PROVIDED_TOOL_NAMES` carries them and a skill whose `tools[]` names them resolves under ADR-0109 reference integrity, while a skill still naming `draft_record` is reported as a fictional tool (`ai-skill-tool-unresolved`). That is the whole change: three accepted `skill.tools[]` values in, one out. No schema shape changes, no export is added or removed. - -**Reach of the retirement.** `draft_record` was a published constant, so removing it breaks any skill whose `tools[]` still names it. Measured before this landed: no skill in this repo names it (`validate-ai-tool-references.test.ts` names it only as the control proving the report fires; the two spec page fixtures called `draft_record` are page names, not tools); the one skill anywhere that names it is the cloud chat's `record_drafting` skill, which the cloud half of cloud#3565 (cloud#3574) moves to `create_record` / `update_record` in the same landing. A third-party skill that still names it reports `ai-skill-tool-unresolved` at validation and is not silently dropped. That is why this is graded `patch` and not left as a silent one. diff --git a/.changeset/connect-agent-work-assistants.md b/.changeset/connect-agent-work-assistants.md deleted file mode 100644 index 56676c7ca6c..00000000000 --- a/.changeset/connect-agent-work-assistants.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch -'@objectstack/mcp': patch ---- - -feat(app-shell): Connect an Agent adds connect cards for WorkBuddy, Doubao Work and QwenWork, and drops the VS Code card - -Clause-②: no -Metadata-protocol: none - -The Connect an Agent page (Account → Developer, and Setup) now carries three more client cards, each filled in with the deployment's MCP URL, listed after the Claude, Codex and Cursor cards: - -- **WorkBuddy** — the `mcpServers` entry for Connectors → Custom connector → Configure MCP, with the `streamableHttp` transport spelling WorkBuddy reads, and the reminder that the connector stays inactive until it is trusted. -- **Doubao Work** — the name, transport and server URL to type into Skills · Connectors → New → New custom connector. -- **QwenWork** — the `mcpServers` JSON for Extensions → Connectors → Paste JSON config, with the `streamable-http` spelling QwenWork reads. - -Each of the three names the fallback for a client version without the browser sign-in: the request header `Authorization: Bearer `. A minted key is now revealed in that form as well as the existing `x-api-key` line. - -The claude.ai / Claude Desktop and Claude Code cards merge into one full-width Claude card with a half for each. The Codex CLI card becomes a Codex card for the Codex app: the name, transport and URL to enter under Settings → MCP servers → Add server, and where to sign in (new string `connectAgent.codex.hint`); it no longer shows a `config.toml` snippet. The VS Code / Copilot card is gone, with its string `connectAgent.vscode.body`; the generic `mcpServers` snippet on the Cursor card and the MCP URL at the top still serve any MCP client. The client cards and the API-key card form one list, the API-key card last. Long one-line snippets wrap instead of being clipped. The Doubao Work and QwenWork card titles are strings too (`connectAgent.doubao.title`, `connectAgent.qwenwork.title`), so the zh pack shows the products' own names, 豆包工作 and 千问办公. New strings live under `connectAgent.fields`, `connectAgent.workbuddy`, `connectAgent.doubao` and `connectAgent.qwenwork`, translated in all ten packs. Nothing on the server changes; `@objectstack/mcp` updates only its description of the page. diff --git a/.changeset/delegated-admin-unreadable-row.md b/.changeset/delegated-admin-unreadable-row.md deleted file mode 100644 index 8d4df1af836..00000000000 --- a/.changeset/delegated-admin-unreadable-row.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security): a delegated administrator's refused assignment write answers the same for a row it cannot read as for a missing one - -Clause-②: no -Metadata-protocol: none - -A delegated administrator — a caller holding an `adminScope`, an organization administrator who holds one included — updates or removes an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. The delegated-administration gate (ADR-0090 D12) now asks first whether the caller itself reads that row, in its own context. - -- **A row the caller cannot read** is answered exactly as an id that names no row is: the read door's `404 RECORD_NOT_FOUND` for that object and id. Both are answered at the gate, before any later step judges the write. -- **A row the caller reads** keeps the verdict and the guidance it had: the gate judges the stored row, as before. -- **A caller the read door refuses outright** (no read grant on the table) is answered as before: the read refusal, for every id the gate refuses, and the writes it was admitted are still admitted. -- **Unchanged.** Inserts, tenant-level administrators, and organization administrators who hold no scope are judged as before. diff --git a/.changeset/driver-options-declared-keys-only.md b/.changeset/driver-options-declared-keys-only.md deleted file mode 100644 index fb1a343ff99..00000000000 --- a/.changeset/driver-options-declared-keys-only.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -fix(objectql): a data driver receives the declared driver options and nothing else - -Clause-②: no -Metadata-protocol: none - -The engine → driver hop is a closed contract, as the engine's own option bags are: every driver call the engine makes is handed the keys `DriverOptionsSchema` declares and no other key. - -- **Which calls.** `find`, `findOne`, `count`, `aggregate`, `insert`, `update` and `delete` (by id and multi-row), the reads and writes those verbs make along the way, and the raw `execute` door. -- **What arrives, unchanged.** `transaction`, `tenantId`, `tenantIds`, `timezone`, `preserveAudit` and `bypassTenantAudit` reach the driver exactly as before, and a caller's explicit value still wins. The schema's other declared keys pass when a caller sets them at a door that admits them, which is `insert` and the raw `execute` door; `find`, `findOne`, `update` and `delete` admit only the six keys above at their own door. The retired `timeout` tombstone is not handed on. -- **What stays with the engine.** Undeclared keys in a caller's engine option bag stay with the engine and are no longer copied onto a driver's options. A driver reads the query from its `query` argument, as the driver contract declares. -- **Hooks are unchanged.** An `after*` hook still reads `input.options` as the caller's bag with the driver options merged on. -- **Who moves.** A driver that read a key `DriverOptionsSchema` does not declare off its options argument: read the query from the `query` argument, or have the key declared on `DriverOptionsSchema` first. No driver in this repository reads one. diff --git a/.changeset/list-queue-filter-provisioned-storage.md b/.changeset/list-queue-filter-provisioned-storage.md deleted file mode 100644 index 0a6ae7c9042..00000000000 --- a/.changeset/list-queue-filter-provisioned-storage.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/plugin-list': patch ---- - -fix(plugin-list): the list's 在队列中 ("In a queue") filter is no longer offered on an external (federated) object - -Clause-②: no -Metadata-protocol: none - -An ADR-0015 `external` object is served with the platform's injected `queue` column like any `ownership: 'user'` object, but the remote database owns its schema, so the column has no storage behind it and no row of that table can ever sit in a queue. `objectHasOwnershipQueue` now also requires `platformProvisionsStorage(objectDef)` from `@objectstack/spec/data`, so the toggle appears only on objects whose storage the platform provisions. Ordinary `ownership: 'user'` objects are unchanged. diff --git a/.changeset/org-admin-removal-caller-read.md b/.changeset/org-admin-removal-caller-read.md deleted file mode 100644 index c57f14e8cd7..00000000000 --- a/.changeset/org-admin-removal-caller-read.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security): an organization administrator's refused assignment write answers the same for a row it cannot read as for a missing one - -Clause-②: no -Metadata-protocol: none - -An organization administrator (`organization_admin` or `organization_admin_no_bypass`) removes an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. The delegated-administration gate (ADR-0090 D12) now asks the administrator's own read, in its own context, whether that row is there to remove. - -- **The removal of a row the administrator cannot read** is answered exactly as the removal of an id that names no row is: the read door's `404 RECORD_NOT_FOUND` for that object and id. -- **A row the administrator can read** is judged on the stored row. A refusal names the organization boundary, and names the row's organization, position or permission set only when the administrator's own read returns that value as stored. -- **Unchanged.** An edit in place of a row it reads is refused as before, every removal that was admitted is still admitted, and a tenant-level administrator is judged as before. diff --git a/.changeset/org-admin-removal-stored-row-verdict.md b/.changeset/org-admin-removal-stored-row-verdict.md deleted file mode 100644 index 032dd8a53c1..00000000000 --- a/.changeset/org-admin-removal-stored-row-verdict.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security): an organization administrator's assignment removal is judged on the stored row, and its refusal names only values the caller can read - -Clause-②: no -Metadata-protocol: none - -An organization administrator (`organization_admin` or `organization_admin_no_bypass`) removes an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. Once its own read returns the row, the delegated-administration gate (ADR-0090 D12) judges the removal on the stored row, as it judges a delegated administrator's write. - -- **The verdict is the stored row's.** A field rule or a masking rule on the row's `position` or `permission_set` does not change it: the removal of a row inside the organization boundary is admitted, and the removal of a row outside it is refused. -- **A refusal names a value only when the caller can read it.** The row's organization, its position and the sets that position carries, or its permission set are named only when the caller's own read returned that column at its stored value. Otherwise the sentence leaves the value out, for example "the assignment's position is not inside the organization boundary", and `details` leaves it out too. -- **Unchanged.** A row the caller cannot read, and an id that names no row, still answer the read door's `404 RECORD_NOT_FOUND`. An edit in place is refused as before. Inserts, tenant-level administrators and delegated administrators are judged as before. diff --git a/.changeset/password-reset-revocation-trail.md b/.changeset/password-reset-revocation-trail.md deleted file mode 100644 index 40cf9734fe4..00000000000 --- a/.changeset/password-reset-revocation-trail.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/plugin-auth': patch -'@objectstack/platform-objects': patch ---- - -fix(plugin-auth): a session a password reset ends leaves a revocation trail - -Clause-②: no -Metadata-protocol: none - -- **A session a password reset ends leaves a revocation trail.** When a completed self-service password reset ends the account's sessions — the emailed link (`POST /api/v1/auth/reset-password`), and the phone OTP reset (`POST /api/v1/auth/phone-number/reset-password`); better-auth's email-OTP reset door, which the platform does not mount, is covered on its own path too — each `sys_session` row is now kept and stamped, `revoked_at` with `revoke_reason: password_reset`, the same kind of trail an interactive revoke leaves, instead of being deleted. The session is still refused (`401`). -- **The value says who ended the session.** `password_reset` is distinct from `user_revoked` (the owner revoking their own sessions) and from `admin_password_reset`, which an administrator's forced reset keeps writing. -- **`sys_session.revoke_reason`'s published vocabulary names `password_reset`** (`@objectstack/platform-objects`, the field's description and its translated help). -- **Unchanged.** Which sessions a reset ends; sign-out, which still deletes its row; the account's API keys, which a self-service reset does not revoke; and the audit log, which records no `logout` row for a reset. diff --git a/.changeset/per-record-action-missing-record-id.md b/.changeset/per-record-action-missing-record-id.md deleted file mode 100644 index ebe4f23af5e..00000000000 --- a/.changeset/per-record-action-missing-record-id.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/runtime': patch ---- - -fix(runtime): a per-record action called with no recordId is refused 400 naming it, and an unknown id 404s at the door, before its flow runs - -Clause-②: no — every call refused now already failed on `main`: the flow behind the action ran and answered `400 FLOW_FAILED` naming its internal node, with no id or with an id the caller cannot resolve. -Metadata-protocol: none - -An action declared `execution: 'perRecord'` (an api action or a flow action) called through the REST `/actions` door or MCP `run_action` with no `recordId`, or an empty one, is refused `400 VALIDATION_ERROR` naming `recordId`, and nothing is forwarded or started. The same api action called with an id the caller's scope does not resolve answers the door's own `404 RECORD_NOT_FOUND` instead of being forwarded, as a flow or script action already did. Actions declared `aggregate`, or declaring no `execution`, run record-less as before (#29173). Fixes #29881. diff --git a/.changeset/phone-reset-text-after-response.md b/.changeset/phone-reset-text-after-response.md deleted file mode 100644 index 04e5443a265..00000000000 --- a/.changeset/phone-reset-text-after-response.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix(plugin-auth): the phone-number password-reset request answers without waiting for the SMS send, as the emailed reset does - -Clause-②: no -Metadata-protocol: none - -- **The rule.** `POST /api/v1/auth/phone-number/request-password-reset` answers at once, the same way for any number: the reset text is built and sent after the response, as the emailed reset (`POST /api/v1/auth/request-password-reset`) already sends its mail. -- **A send that fails is logged on the server**, at error (`phone password-reset text was not delivered`), and never reaches the caller, who is answered `{ status: true }` either way, as before. The one-time code is in the text only, never in that line. -- **Unchanged.** The answer's status and body; the per-number cooldown and hourly cap and the per-IP limit on the route; the text itself; and a server-side `auth.api.requestPasswordResetPhoneNumber` call, which carries no request and still awaits its send. diff --git a/.changeset/record-timeline-folded-types-from-spec.md b/.changeset/record-timeline-folded-types-from-spec.md deleted file mode 100644 index 340708f92c9..00000000000 --- a/.changeset/record-timeline-folded-types-from-spec.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -refactor(plugin-detail): the record timeline stream pairs audit entries with activity rows by the spec's `RECORD_TIMELINE_FOLDED_ACTIVITY_TYPES` - -Clause-②: no -Metadata-protocol: none - -The 动态 stream used to keep its own copy of the audit action to activity type pairs (`create`/`created`, `update`/`updated`, `delete`/`deleted`). It now reads `RECORD_TIMELINE_FOLDED_ACTIVITY_TYPES` from `@objectstack/spec/api`, the same set the server's `timeline` count steps aside, so the badge and the drawn rows cannot fold different types. No behaviour change: the same three pairs fold, and any other audit action (`restore`, an unknown action) pairs with no activity row. diff --git a/.changeset/rollback-addresses-served-row.md b/.changeset/rollback-addresses-served-row.md deleted file mode 100644 index 19de3041cf6..00000000000 --- a/.changeset/rollback-addresses-served-row.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -Clause-②: no -Metadata-protocol: none - -fix(metadata-protocol): the per-item rollback (`POST /api/v1/meta/:type/:name/rollback`) restores into the row the by-name read naming no package serves. A `.custom` pair's declared override stored beside a row bound to its read-only base (a row saved before the named-base close and not yet moved) is now rolled back in place, and the base row is left untouched; before, the door read a package-agnostic binding, addressed the base row, and was refused `403 NOT_OVERRIDABLE` naming a row the caller never meant. The door takes no new parameter. A rollback whose served row is the base's own is still refused by the named-base close. diff --git a/.changeset/sec-hardening-g-docs-external-object-scope-skill.md b/.changeset/sec-hardening-g-docs-external-object-scope-skill.md deleted file mode 100644 index 5c5910c0a79..00000000000 --- a/.changeset/sec-hardening-g-docs-external-object-scope-skill.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): objectstack-data's datasources rule states the organization scope an external object declares under a walled tenancy posture - -Clause-②: no -Metadata-protocol: none - -- **Before.** The datasources rule taught `external` bindings, `columnMap`, auto-connect and credentials, and said nothing about what an external object's rows are to the organizations of a walled deployment. -- **Now.** One bullet under *Federated (external) objects*: under `OS_TENANCY_POSTURE` `isolated`/`group` every external object states its organization scope — its own `organization_id` field (or `columnMap` onto it), `tenancy: { enabled: false }` (shared), or `systemFields: { tenant: false }`; none refuses the boot by name, and `tenancy.tenantField` is refused there too. Paid within the file's token ceiling (910 → 908 of 911) by trimming the auto-connect footnote, the managed-datasource sentence and the credentials paragraph; no ceiling moves. diff --git a/.changeset/sec-hardening-g-external-object-organization-scope.md b/.changeset/sec-hardening-g-external-object-organization-scope.md deleted file mode 100644 index cb3149414c5..00000000000 --- a/.changeset/sec-hardening-g-external-object-organization-scope.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/plugin-security': minor -'@objectstack/core': minor -'@objectstack/spec': minor ---- - -feat(plugin-security)!: under a walled tenancy posture every external object states its organization scope - -Clause-②: no (narrowing) — under the `isolated` and `group` postures an external object (an `external` block) that states no organization scope is refused by name when the boot completes, and one registered after the boot answers no row and accepts no write; nothing is widened, and the `single` posture is unchanged. -Metadata-protocol: narrowed - - - -**BREAKING** for a deployment running the `isolated` or `group` tenancy posture (`OS_TENANCY_POSTURE`) with an external object that states nothing about its organization scope. - -- **The rule (ADR-0015 §10 as amended, ADR-0095 D1).** The platform provisions no storage for an external object, so the `organization_id` it injects into the registered schema is a column the remote may not carry. Whose rows an external object serves — one organization's or every organization's — is a fact only its author knows, and under a walled posture the object states it, in one of three ways: its own `organization_id` field (the remote rows carry the organization column and the driver returns it; a remote column of another name is mapped onto it with `external.columnMap: { : 'organization_id' }`); `tenancy: { enabled: false }` (shared by every organization); or `systemFields: { tenant: false }` (the same opt-out on the system fields). These are the wall's own two opt-out clauses and the one column it reads; the whole-block `systemFields: false` states nothing. -- **What the rule does.** `SecurityPlugin` refuses the boot from its `kernel:ready` handler, beside the ADR-0131 D5 refusal, naming every external object that states nothing and the three ways out (`ExternalObjectOrganizationScopeUndeclaredError`). An external object registered after the boot — a Studio import, a runtime datasource — that states nothing answers no row and accepts no write: the Layer 0 verdict is `deny` (`computeTenantLayer0Verdict`), the write wall refuses every insert, update and delete with a sentence naming the undeclared scope and the three ways out, and the plugin says so once in a warning. An object declaring its own `organization_id` is walled: a member of organization A reads only A's rows, through a driver that honours `where` and through one that ignores it (the engine's floor re-applies the predicate), and a row missing the column reads to no one. An object declaring `tenancy: { enabled: false }` or `systemFields: { tenant: false }` is shared. -- **`tenancy.tenantField` on an external object is closed the same way.** The organization wall keys on `organization_id` alone, and a declared tenant field is read only by storage the platform provisions, which an external object has none of; so on an external object a `tenantField` naming another column is refused at boot and denied after it, with the remedy spelled out (`external.columnMap` onto `organization_id`, or `tenancy: { enabled: false }`). Local objects are unchanged. -- **Who moves.** Every walled deployment with an external object stating nothing: declare one of the three and restart. The showcase's two federated analytics objects now declare `tenancy: { enabled: false }`. A `single`-posture deployment owes no declaration and behaves exactly as before. -- **`@objectstack/core`.** `TenantLayer0Input` gains the optional `externalOrganizationScopeUndeclared`; set, the verdict is `deny` under a walled posture, ahead of the platform-admin crossing. Omitted, every existing caller reads as before. -- **`@objectstack/spec`.** The ADR-0087 D3 entry `external-object-organization-scope-declared` tells an upgrading operator what to declare. - -Skills-docs: follow-up #29866 — `content/docs/data-modeling/virtual-objects.mdx` (a multi-organization section), `skills/objectstack-data/rules/datasources.md` (the three declarations), `docs/adr/0015-external-datasource-federation.md` (the §10 and D7 amendment); the sibling docs PR on `claude/sec-hardening-g-docs`, governed (Tier H), landed by the maintainer's hand. diff --git a/.changeset/sec-hardening-p3-write-refused-read-door.md b/.changeset/sec-hardening-p3-write-refused-read-door.md deleted file mode 100644 index 9c8362328cd..00000000000 --- a/.changeset/sec-hardening-p3-write-refused-read-door.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security)!: a write from a caller the read door refuses is refused, whatever rows it addresses - -Clause-②: no (narrowing) — a predicate update or delete (`multi: true`, addressed by a `where`) from a caller whose read of the object is refused is refused with that read refusal before the write runs, where it was admitted before; nothing is widened. -Metadata-protocol: none - - - -**BREAKING** for a caller whose read of an object is refused while its write is admitted, and that updates or deletes that object's records by predicate. With the permission model's own grants that caller exists in one shape only: an object whose `requiredPermissions` map gates `read` and not `update` or `delete` (ADR-0066 ⑤), and a caller holding the write grant without the capability the read requires. - -- **The rule.** A caller the read door refuses writes none of the object's records, by id or by predicate. The predicate update and delete such a caller addresses — a flow's `update_record` or `delete_record` node with `multi: true` run as that user, or an engine `update` / `delete` with `multi: true` under that caller's context — now answer the read door's own refusal (the `PERMISSION_DENIED` envelope that caller's read gets), and nothing is written. The by-id update and delete are refused too. -- **Unchanged.** A caller who reads the object keeps its predicate write, scoped to the records it reads. A caller the object's CRUD grants refuse is refused as before. A write the platform issues under the caller's context (a hook's or a cascade's own write, the referential clear of a deleted record's lookups) and a system write keep their answers. A store fault on the read propagates as before. -- **Who moves.** A deployment that relies on such a caller writing by predicate grants it the capability the object's read requires, or gates the write verbs on a capability that caller holds. diff --git a/.changeset/sec-hardening-r6-agent-ceiling-grant.md b/.changeset/sec-hardening-r6-agent-ceiling-grant.md deleted file mode 100644 index efd29904c88..00000000000 --- a/.changeset/sec-hardening-r6-agent-ceiling-grant.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/plugin-security': patch -'@objectstack/plugin-auth': patch ---- - -fix(plugin-security, plugin-auth)!: a person-to-permission-set assignment refuses an agent ceiling set at every write door, and so does the self-registration permission set - -Clause-②: no (narrowing) — a non-system insert or update of `sys_user_permission_set` that would store an assignment naming one of the three MCP agent ceiling sets (`mcp_agent_data_read`, `mcp_agent_data_write`, `mcp_agent_restricted`) is refused with `400 VALIDATION_FAILED` (`invalid_value` at `permission_set`), for every caller, a platform administrator included; and an audience configuration whose `selfRegistrationPermissionSet` names one is refused (`[audience] invalid audience configuration`), at construction and through the auth settings; nothing is widened. -Metadata-protocol: none - - - -**BREAKING** for a deployment whose administrators assign an MCP agent ceiling set to users through the permission-set assignment table, or whose audience names one as `selfRegistrationPermissionSet`. - -- **The rule (ADR-0090 D10).** The agent ceiling sets are an AI agent's own grants. The door that admits an agent connected over MCP derives its ceiling from the scopes its user consented to and puts it on the agent's request; no assignment row is read for it. A `sys_user_permission_set` row is a person's grant, so it never names one. Every door that writes the table refuses such a row: the data API's create and update, `createMany`, `updateMany`, the per-object and cross-object batch, and both import doors (the row fails with `code: 'invalid_value'` at `permission_set`). An update that keeps an assignment naming one in force (a new holder, a validity window, an echo of the set) is refused too. -- **The self-registration set (`@objectstack/plugin-auth`).** A self-registrant is a person, so the audience configuration that names the set every self-registrant receives refuses an agent ceiling set, beside its existing refusal of `admin_full_access`: `auth.audience.selfRegistrationPermissionSet` in the stack config stops the boot, and `audience_self_registration_permission_set` in the auth settings is refused while the standing audience keeps ruling. Both answer `[audience] invalid audience configuration: selfRegistrationPermissionSet must not be '...'`. -- **Unchanged.** Every other permission set stays assignable, and removing an assignment stays open. A system write is not judged: the platform's fixed-name grant writers name no ceiling, the self-registration grant writes only the validated set, and seed replay and the development-only demo-staff seed write the sets their stack declares. An agent connected over MCP holds its ceiling exactly as before. An organization administrator is still refused the same sets on the organization boundary first (`403 PERMISSION_DENIED`). -- **Who moves, and the fix.** An assignment stored before this release that names an agent ceiling set is left as it is, and can no longer be edited: delete it (`DELETE /api/v1/data/sys_user_permission_set/ID`), or rename it to a permission set meant for people. List them with `GET /api/v1/data/sys_user_permission_set?permission_set=mcp_agent_data_read` and the same query for the other two names. An audience that names an agent ceiling set as `selfRegistrationPermissionSet` declares a permission set meant for people instead (`member_default`, or the deployment's own). diff --git a/.changeset/sec-hardening-s-external-scope-every-caller.md b/.changeset/sec-hardening-s-external-scope-every-caller.md deleted file mode 100644 index bde71022d0c..00000000000 --- a/.changeset/sec-hardening-s-external-scope-every-caller.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security)!: an external object that states no organization scope reads as empty for every caller, the platform owner included - -Clause-②: no (narrowing) — under the `isolated` and `group` postures, an external object registered after the boot that states no organization scope reads as empty for every caller, the declared platform owner included; nothing is widened, and the `single` posture is unchanged. -Metadata-protocol: none - - - -**BREAKING** for a deployment running the `isolated` or `group` tenancy posture (`OS_TENANCY_POSTURE`) that registers an external object after the boot without stating its organization scope. - -- **The rule (ADR-0015 §10 as amended).** Whose rows an external object serves is a fact only its author can state. Under a walled posture an external object that states none is refused by name when the boot completes, and one registered after the boot — a Studio import, a runtime datasource — reads as empty for every caller, the platform owner included, and accepts no write, until it states its scope. That deny judges the object's configuration, not the reader's organization. -- **What is unchanged.** The declared platform owner's reads still cross the organization wall everywhere else: on local objects, on an external object that declares its own `organization_id`, and in a session with no active organization. Writes stay walled for everyone. A member's reads are unchanged. -- **Who moves, and the fix.** Declare one of the three on the external object, as the ADR-0087 entry `external-object-organization-scope-declared` prescribes: an `organization_id` field of its own (the rows are then walled by organization), `tenancy: { enabled: false }` (shared by every organization), or `systemFields: { tenant: false }` (the same opt-out on the system fields). diff --git a/.changeset/sec-hardening-u-write-implies-read.md b/.changeset/sec-hardening-u-write-implies-read.md deleted file mode 100644 index 16559a166f9..00000000000 --- a/.changeset/sec-hardening-u-write-implies-read.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/plugin-security': minor -'@object-ui/app-shell': minor ---- - -fix(spec)!: an object permission that edits or deletes a record also reads it - -Clause-②: no (narrowing) — the authoring object-permission schema refuses an entry granting `allowEdit: true` or `allowDelete: true` without read (`allowRead`, `viewAllRecords` or `modifyAllRecords`), a permission set it accepted before; the permission evaluator grants edit and delete only together with read; and a by-id update or delete from a caller the read door refuses answers that refusal. Nothing is widened. -Metadata-protocol: narrowed -Skills-docs: docs moved — `content/docs/permissions/permission-sets.mdx` states the rule under the object permission bits; `skills/**` teaches no entry granting edit or delete without read (its examples grant read beside edit), so no skill moves. - - - -**BREAKING** accept-set narrowing on `ObjectPermissionSchema` (and so `PermissionSetSchema`, `definePermissionSet`, `defineStack`, `os validate` / `os build`, and the metadata save and publish doors for `permission`). - -- **Before.** `objects: { crm_lead: { allowEdit: true } }` parsed, and the evaluator granted edit on `crm_lead` to a holder who could not read it. -- **Now.** The schema refuses it: one `custom` issue at `objects.crm_lead.allowEdit` (or `.allowDelete`), naming both fixes. `objectPermissionGrants` answers edit and delete as `read ∧ grant` (the shape `allowExport` already had), and `PermissionEvaluator.checkObjectPermission` asks it of the caller's merged entry, so the read half may come from another set — the same answer `/me/permissions` gives the client. -- **Stored rows.** A permission set stored before this release keeps loading; its unread edit or delete grant grants nothing. The response-side `EffectiveObjectPermissionSchema` stays tolerant. A built artifact carrying the shape is refused when it is parsed and is rebuilt from a corrected source. -- **By-id writes.** A by-id update or delete from a caller whose read of the object is refused — with these grants that is a capability the object requires for a read and not for a write — answers that read refusal for every id it addresses: a caller who cannot read an object writes none of its records by id. -- **`@object-ui/app-shell`.** The permission set editor ticks Read when Edit or Delete is ticked, and unticking any bit that reads (Read, View All or Modify All) unticks Edit and Delete once no bit that reads remains on the entry. -- Unchanged: `allowCreate` and `allowTransfer` are not part of this rule. - -**FROM → TO.** `objects: { crm_lead: { allowEdit: true } }` → `objects: { crm_lead: { allowRead: true, allowEdit: true } }` if the set should edit the object, or `objects: { crm_lead: {} }` if it should not. The upgrade-guide entry is `permission-write-without-read-refused` (a semantic migration: no automatic rewrite, because only the author knows which of the two the set means). diff --git a/.changeset/settings-action-confirm-text.md b/.changeset/settings-action-confirm-text.md deleted file mode 100644 index a0c6634e365..00000000000 --- a/.changeset/settings-action-confirm-text.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/console': patch ---- - -fix(console): a settings action whose handler declares `confirmText` asks before it runs - -Clause-②: no -Metadata-protocol: none - -The settings manifest's action handler (`SpecifierHandlerSchema`, its `http` and `action` arms) declares `confirmText`: "Confirm dialog text before invoking (omit = no confirm)". The Console's `action_button` row never read it, so an action its manifest marked as needing a confirmation ran on the first click. Now a declared `confirmText` opens a confirmation dialog carrying that text (translated through `settings.NAMESPACE.actions.ID.confirmText` when the locale has it); Cancel runs nothing and Continue runs the action once. A handler without `confirmText`, and a `navigate` handler, runs on click exactly as before. diff --git a/.changeset/setup-admin-wording-zh-cn.md b/.changeset/setup-admin-wording-zh-cn.md deleted file mode 100644 index 864080bf125..00000000000 --- a/.changeset/setup-admin-wording-zh-cn.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/platform-objects': patch -'@objectstack/plugin-security': patch -'@objectstack/plugin-approvals': patch -'@objectstack/plugin-sharing': patch -'@objectstack/plugin-email': patch -'@objectstack/service-settings': patch ---- - -fix: Setup's form help speaks to an administrator, and the queue pages, the mail status banner and the test-email toast read in Chinese on a zh-CN console - -Clause-②: yes (widening) -Metadata-protocol: none - -- **Field help (English source and zh-CN):** the permission set and position assignment forms, the out-of-office (approval delegation) rule, and the business unit, organization and user forms no longer describe the implementation. Gone: "fail-closed", "half-open [from, until)", "agent grants", "graph semantics", "expanded by graph queries", "Null = root of tenant", "HRIS", "When true / When false" (zh-CN 「为 true 时」 now reads 「开启后」) and an HTTP status code. The help now says when an assignment starts and ends, what turning a switch on does, and what to fill in. Text only: no field name, key, option value or behaviour changes. -- **`@objectstack/plugin-sharing`:** `sys_queue` and `sys_queue_member` join the plugin's translation bundle, with zh-CN text, so the 队列 and 队列成员 pages, their columns, help and the 新建 button read in Chinese. ja-JP and es-ES carry English fills for now. -- **`@objectstack/plugin-email`:** the live-status statement at the top of Setup → Email and the "Send test email" answer (sent, failed, logged only) are written in the caller's display language, en and zh-CN today; another language gets English. -- **`@objectstack/service-settings`:** a settings action handler (`SettingsActionHandler`) receives an optional `locale`, the caller's display language read from the request the same way the live-status statement's is (`Accept-Language`, else `?locale`); `runAction` takes it as a fifth `display` argument. Existing handlers compile and behave unchanged. diff --git a/.changeset/sign-up-mail-after-response.md b/.changeset/sign-up-mail-after-response.md deleted file mode 100644 index 1d4c47d6d49..00000000000 --- a/.changeset/sign-up-mail-after-response.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix(plugin-auth): a sign-up answers without waiting for any mail send, whether or not the address already has an account - -Clause-②: no -Metadata-protocol: none - -- **The rule.** `POST /api/v1/auth/sign-up/email` answers without waiting for any mail send, whether or not the address already has an account: the verification mail, and the notice to an address that already has an account, are built and sent after the response, as the password-reset mail already is. -- **A send that fails is logged on the server**, at error (`verification email was not delivered`; the notice keeps its own error line), and never reaches the caller, whose answer is the one a sent mail gets, as before. -- **Unchanged.** The answer's status and body; the sign-up rate limits; the mails themselves; a signed-in caller's `/send-verification-email` resend, which still awaits its send and still reports one that failed; and a server-side `auth.api.signUpEmail` call, which carries no request and still awaits its sends. diff --git a/.changeset/sys-file-uploader-is-its-creator.md b/.changeset/sys-file-uploader-is-its-creator.md deleted file mode 100644 index 8d90f85817f..00000000000 --- a/.changeset/sys-file-uploader-is-its-creator.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/service-storage': patch ---- - -fix(service-storage)!: a stored file's uploader is the user who created it, as an attachment's and a comment's author are - -Clause-②: no (narrowing) — a non-system create of `sys_file` records the session's user as `owner_id`, whatever the request carries, and one whose session names no signed-in user is refused. Nothing is widened. -Metadata-protocol: none - - - -**BREAKING** accept-set narrowing on `sys_file` creates (`@objectstack/service-storage`), served by every data door (REST, MCP, the console). - -- **Now.** A stored file's uploader is the user who created it, as an attachment's (`sys_attachment.uploaded_by`) and a comment's (`sys_comment.author_id`) author are. A non-system create of `sys_file` records the session's user as `owner_id`, over whatever the request carries. The column was already read-only on update, so the stamp is the row's uploader for life. -- **Refused.** A non-system create of `sys_file` whose session names no signed-in user is refused with 403 `RECORD_NOT_ACCESSIBLE`, the answer an attachment or comment create with no signed-in user already gets: there is no user to record as the uploader. -- **Unchanged.** The upload doors (presigned, chunked, complete) already record the signed-in user under the system context, and keep doing so. System-context writes (seeds, imports, migrations) keep the `owner_id` they supply, as does a programmatic create with no session at all on a bare kernel. An update of `owner_id` is dropped and reported in `droppedFields`, as before. Stored rows keep what they were written with. The console needs no change: it never creates `sys_file` rows through the data API. - -**The remedy.** None for the console or the upload doors. A client that creates a `sys_file` row through the data API gets the row recorded under its own user, whatever `owner_id` it sends; to record a different uploader, write from a system context (a migration or an import). A server-side caller that creates `sys_file` rows with no signed-in user runs as the system principal or names the user it acts for. diff --git a/.changeset/url-credential-special-scheme-read-however-many-slashes.md b/.changeset/url-credential-special-scheme-read-however-many-slashes.md deleted file mode 100644 index fb4a0dd1f53..00000000000 --- a/.changeset/url-credential-special-scheme-read-however-many-slashes.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: a URL with a special scheme is read for credentials the way a URL parser reads it, however many slashes follow the scheme - -Clause-②: no (narrowing) -Metadata-protocol: narrowed - - - -**BREAKING** accept-set narrowing at the two metadata doors that judge a URL credential: an authored connector instance (`DeclarativeConnectorEntrySchema` — `defineStack({ connectors })`, `os validate` / `os compile`, the artifact load, `PUT /api/v1/meta/connector/:name`) and a datasource driver config's URL keys (`config.url` on postgres, mysql, mongo and turso, turso `config.syncUrl`) at every door that parses the datasource. - -- **A special scheme's authority is read as a URL parser reads it.** For `http`, `https`, `ws`, `wss` and `ftp`, in any letter case, the authority starts after the whole run of `/` and `\` that follows the scheme, however long, none included. A connector string holding a userinfo there, and a datasource URL holding a userinfo password there, are refused (`422 INVALID_METADATA` at the save door, located at the string, naming the parts, never the value); a bare username in a datasource URL stays accepted. -- **One grammar at both doors.** The connector entry and the datasource doors read the one userinfo grammar, and both read redactors rewrite exactly what the write refuses, at the position the write judged, serving every other character as stored. -- **Unchanged.** Every other scheme keeps `scheme://` or a scheme-relative `//` (`postgres:`, `libsql:`, `mongodb:` and the rest carry no authority without `//`); `file:` carries no userinfo; a URL spelled `scheme://` reads exactly as before. A stored row is never re-judged: a save of its served form keeps the stored value, and an edit of the served value stores exactly the edit. - -## FROM → TO - -```ts -// before — parsed green; now refused at providerConfig.baseUrl -{ name: 'billing_api', label: 'Billing API', type: 'api', provider: 'rest', - providerConfig: { baseUrl: 'https:///billing:…@api.billing.example.com/v1' }, - auth: { type: 'none' } }, - -// after — the URL holds no credential, and the password is a reference -{ name: 'billing_api', label: 'Billing API', type: 'api', provider: 'rest', - providerConfig: { baseUrl: 'https://api.billing.example.com/v1' }, - auth: { type: 'basic', username: 'billing', credentialRef: 'BILLING_API_PASSWORD' } }, -``` - -The same holds for `https:billing:…@host`, `https:/billing:…@host` and `https:\billing:…@host`, and for a datasource URL key: spell it `scheme://`, keep it credential-free (a bare username stays accepted) and bind the password through the Setup → Datasources connection form's secret field, or reference it with `external.credentialsRef`. There is deliberately no automatic rewrite: the value in the string may be the only copy of the secret. The upgrade guide entry is `url-credential-special-scheme-read-however-many-slashes`. diff --git a/.changeset/visitor-verification-resend.md b/.changeset/visitor-verification-resend.md deleted file mode 100644 index aa528a88e24..00000000000 --- a/.changeset/visitor-verification-resend.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix(plugin-auth): a verification-email resend from a visitor who is not signed in answers the same whatever the send does - -Clause-②: no -Metadata-protocol: none - -- **The rule.** `POST /api/v1/auth/send-verification-email` from a visitor who is not signed in answers the same whatever the send does. While the email transport is live it answers `{ "status": true }`: the verification mail is built and sent after the response, as the sign-up's verification mail is, and a send that fails is logged on the server at error (`verification email was not delivered`). -- **While the email transport is not live** (no email service, or one whose `isConfigured()` does not answer `true`, such as the `log` transport), every such resend is refused with `400 EMAIL_SERVICE_REQUIRED`, the code `/change-email` and the identity import already answer for a deployment with no email delivery. Its message names the fix (Settings → Mail), and the verify screens show it. -- **Unchanged.** A signed-in caller's resend for their own address, by session cookie or by bearer token, still awaits its send, still reports one that failed, and is not refused while the transport is not live. A server-side `auth.api.sendVerificationEmail` call, which carries no request, still awaits its send. diff --git a/content/docs/deployment/self-hosting.mdx b/content/docs/deployment/self-hosting.mdx index 6fc7ddaf7a7..a0a6d328b34 100644 --- a/content/docs/deployment/self-hosting.mdx +++ b/content/docs/deployment/self-hosting.mdx @@ -75,7 +75,7 @@ docker run -p 8080:8080 \ -e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \ -e OS_AUTH_SECRET \ -e OS_SECRET_KEY \ - ghcr.io/objectstack-ai/objectstack:18.3.0 + ghcr.io/objectstack-ai/objectstack:18.4.0 ``` (`OS_ARTIFACT_PATH` also accepts an `https://` URL, so the artifact can come @@ -93,7 +93,7 @@ docker run -p 8080:8080 \ -e OS_ARTIFACT_URL="https://releases.example.com/hotcrm-2.2.2.json#sha256=<64 hex chars>" \ -e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \ -e OS_AUTH_SECRET -e OS_SECRET_KEY \ - ghcr.io/objectstack-ai/objectstack:18.3.0 + ghcr.io/objectstack-ai/objectstack:18.4.0 ``` Both schemes work: `https://…` is fetched at boot, `file:///…` is read directly @@ -150,7 +150,7 @@ COPY . . RUN npx os build # → dist/objectstack.json # ── Runtime: the official ObjectStack runtime image ────────────────── -FROM ghcr.io/objectstack-ai/objectstack:18.3.0 +FROM ghcr.io/objectstack-ai/objectstack:18.4.0 COPY --from=build --chown=node:node /app/dist/objectstack.json /srv/app/objectstack.json ``` @@ -173,7 +173,7 @@ FROM node:22-slim RUN npm install --prefix /tmp/os-installer-npm --no-save --no-audit --no-fund "npm@11.21.0" \ && export PATH="/tmp/os-installer-npm/node_modules/.bin:$PATH" \ && test "$(npm --version)" = "11.21.0" \ - && npm install -g @objectstack/cli@18.3.0 "pg@^8.0.0" "mysql2@^3.0.0" \ + && npm install -g @objectstack/cli@18.4.0 "pg@^8.0.0" "mysql2@^3.0.0" \ && npm cache clean --force \ && rm -rf /tmp/os-installer-npm diff --git a/content/docs/releases/index.mdx b/content/docs/releases/index.mdx index 89b1f826929..4d0d6cb5c49 100644 --- a/content/docs/releases/index.mdx +++ b/content/docs/releases/index.mdx @@ -18,7 +18,7 @@ migration steps, then covers new capabilities and notable fixes. ## Versions -- [v18](/docs/releases/v18) — The line that carries ADR-0131, total organization ownership with no NULL `organization_id`; its breaking changes landed in stages through the `18.0.0-next.N` prereleases on npm's `next` tag, each with its own changeset and migration, and its GA release, 18.0.0, publishes to npm's `latest` tag and moves it from 17.x to 18 (current series: 18.3.0, released 2026-11-03). +- [v18](/docs/releases/v18) — The line that carries ADR-0131, total organization ownership with no NULL `organization_id`; its breaking changes landed in stages through the `18.0.0-next.N` prereleases on npm's `next` tag, each with its own changeset and migration, and its GA release, 18.0.0, publishes to npm's `latest` tag and moves it from 17.x to 18 (current series: 18.4.0, released 2026-11-06). - [v17.0.0](/docs/releases/v17) — Files become owned `sys_file` records with server-enforced `accept`/`maxSize` and a governed download path, bulk export becomes its own opt-in privilege, the SDK is reconciled against the routes the server actually mounts (21 dead methods out, 40+ real ones in), approval nodes route approvers dynamically via CEL expressions and decision outputs, a datasource that cannot connect fails the boot, and Node 22 becomes the supported floor; 17.1 adds partial field masking, record-view auditing on `sys_audit_log`, and a per-object read-only approval visibility tier — and makes a deactivated permission set or position actually stop granting access, withdraws the bulk-export wildcard from the shipped admin sets, and gives all three flow doors one honest HTTP status table; 17.2 tightens by-id `update`/`delete` against a silently-dropped `where` predicate or a mismatched id, retires `sys_position.permissions` and other dead ADR-0049 surfaces, and stops analytics from answering the wrong number on a cross-object filter (final release: 17.7.0). - [v16.0.0](/docs/releases/v16) — One org identifier (`organizationId`) across hooks and actions, quorum + per-group sign-off (会签) approvals with metadata-declared decision actions, time-relative automations, filtered roll-ups, strict dashboard widgets, an identity-scoped MCP stdio transport, and a platform-wide enforce-or-remove sweep that makes dead metadata loud; 16.1 adds a `requires` capability-provider preflight, two more dashboard build gates, and `runAs:'user'` automations that run with the triggering user's real grants (final release: 16.1.0). - [v15.0.0](/docs/releases/v15) — Explain record access layer by layer, a docked AI workspace in the Console, project-ready Gantt charts, and phone sign-in; 15.1 adds permission-following attachments, no-code third-party connectors, dashboard-wide filters, pinyin search, and whole-record inline editing — with materially safer multi-tenant and write-path defaults (final release: 15.1.1). diff --git a/content/docs/upgrading.mdx b/content/docs/upgrading.mdx index 29277a34273..d563adce0e7 100644 --- a/content/docs/upgrading.mdx +++ b/content/docs/upgrading.mdx @@ -52,7 +52,7 @@ The official image is `ghcr.io/objectstack-ai/objectstack`, and its tags mirror ```bash # docker-compose.yml, or your orchestrator's manifest -image: ghcr.io/objectstack-ai/objectstack:18.3.0 +image: ghcr.io/objectstack-ai/objectstack:18.4.0 ``` On a host running the artifact directly under systemd, the same move is a file diff --git a/docker/README.md b/docker/README.md index 657d155e0b2..82191670b77 100644 --- a/docker/README.md +++ b/docker/README.md @@ -29,7 +29,7 @@ Multi-arch: `linux/amd64` + `linux/arm64`. [Self-Hosted Deployment](https://objectstack.ai/docs/deployment/self-hosting)): ```dockerfile -FROM ghcr.io/objectstack-ai/objectstack:18.3.0 +FROM ghcr.io/objectstack-ai/objectstack:18.4.0 COPY --chown=node:node dist/objectstack.json /srv/app/objectstack.json ``` @@ -40,7 +40,7 @@ docker run -p 8080:8080 \ -v "$PWD/dist/objectstack.json:/srv/app/objectstack.json:ro" \ -e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \ -e OS_AUTH_SECRET -e OS_SECRET_KEY \ - ghcr.io/objectstack-ai/objectstack:18.3.0 + ghcr.io/objectstack-ai/objectstack:18.4.0 ``` `OS_ARTIFACT_PATH` also accepts an `https://` URL, so the artifact can come @@ -72,7 +72,7 @@ for a `file:…` path — one box only, wrong for multi-node) and MongoDB (`libsql://…` / Turso). Add one by extending the image: ```dockerfile -FROM ghcr.io/objectstack-ai/objectstack:18.3.0 +FROM ghcr.io/objectstack-ai/objectstack:18.4.0 USER root RUN npm install -g tedious USER node @@ -100,5 +100,5 @@ reverse-proxy / multi-node guidance: ## Local build of this image ```bash -docker build -t objectstack:dev --build-arg OS_CLI_VERSION=18.3.0 docker/ +docker build -t objectstack:dev --build-arg OS_CLI_VERSION=18.4.0 docker/ ``` diff --git a/examples/app-crm/CHANGELOG.md b/examples/app-crm/CHANGELOG.md index e8a917c6c1c..f6cf085ba38 100644 --- a/examples/app-crm/CHANGELOG.md +++ b/examples/app-crm/CHANGELOG.md @@ -1,5 +1,81 @@ # @objectstack/example-crm +## 4.0.104 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [665db00] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [59bcd22] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [a545a04] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [8589756] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [8cc709d] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [7b80724] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [30dcd8a] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/runtime@18.4.0 + - @objectstack/service-i18n@18.4.0 + ## 4.0.103 ### Patch Changes diff --git a/examples/app-crm/package.json b/examples/app-crm/package.json index b31cd95a287..104fb33156b 100644 --- a/examples/app-crm/package.json +++ b/examples/app-crm/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-crm", - "version": "4.0.103", + "version": "4.0.104", "description": "Minimal CRM example \u2014 a smoke-test workspace that exercises the metadata loading pipeline (objects \u2192 views \u2192 app \u2192 dashboard \u2192 hook \u2192 flow \u2192 seed). For a full-featured enterprise CRM see https://github.com/objectstack-ai/hotcrm.", "license": "Apache-2.0", "private": true, diff --git a/examples/app-multi-package/CHANGELOG.md b/examples/app-multi-package/CHANGELOG.md index cda9231041a..4b17102718a 100644 --- a/examples/app-multi-package/CHANGELOG.md +++ b/examples/app-multi-package/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/example-multi-package +## 0.0.11 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 0.0.10 ### Patch Changes diff --git a/examples/app-multi-package/package.json b/examples/app-multi-package/package.json index 66f414035f5..e1ed51b3c46 100644 --- a/examples/app-multi-package/package.json +++ b/examples/app-multi-package/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-multi-package", - "version": "0.0.10", + "version": "0.0.11", "description": "One release artifact carrying TWO packages that share a namespace (ADR-0130 D4) — the producer-side fixture for `packages[]`", "license": "Apache-2.0", "private": true, diff --git a/examples/app-showcase/CHANGELOG.md b/examples/app-showcase/CHANGELOG.md index fa80fedd71a..e3ebfb08ef8 100644 --- a/examples/app-showcase/CHANGELOG.md +++ b/examples/app-showcase/CHANGELOG.md @@ -1,5 +1,108 @@ # @objectstack/example-showcase +## 0.3.26 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [5cdaddb] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [665db00] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [bcdf8d3] +- Updated dependencies [59bcd22] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [afc761c] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [bc254be] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [a545a04] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [8589756] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [8cc709d] +- Updated dependencies [6e8ec73] +- Updated dependencies [60fe44f] +- Updated dependencies [22f0d65] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [7b80724] +- Updated dependencies [4dde0ae] +- Updated dependencies [94cef75] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [ae5bd5b] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [7d11067] +- Updated dependencies [135c2fb] +- Updated dependencies [898145b] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [63de248] +- Updated dependencies [8d7f906] +- Updated dependencies [c35fed1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [a91ad42] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [1030a80] +- Updated dependencies [b3d2f4b] +- Updated dependencies [dcb5fb3] +- Updated dependencies [eca7cd2] +- Updated dependencies [30dcd8a] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/runtime@18.4.0 + - @objectstack/service-datasource@18.4.0 + - @objectstack/cloud-connection@18.4.0 + - @objectstack/organizations@18.4.0 + - @objectstack/driver-sql@18.4.0 + - @objectstack/connector-mcp@18.4.0 + - @objectstack/connector-openapi@18.4.0 + - @objectstack/connector-rest@18.4.0 + - @objectstack/connector-slack@18.4.0 + - @objectstack/service-i18n@18.4.0 + - @objectstack/service-print@18.4.0 + ## 0.3.25 ### Patch Changes diff --git a/examples/app-showcase/package.json b/examples/app-showcase/package.json index 8b4f832edb1..46f7f9079f3 100644 --- a/examples/app-showcase/package.json +++ b/examples/app-showcase/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-showcase", - "version": "0.3.25", + "version": "0.3.26", "description": "Kitchen-sink showcase workspace — exercises every metadata type, every view type, every chart type, and the major end-to-end capability chains (security, automation, analytics). Built for demonstration, debugging, and coverage-driven verification.", "license": "Apache-2.0", "private": true, diff --git a/examples/app-todo/CHANGELOG.md b/examples/app-todo/CHANGELOG.md index 47eb04b53a8..c0f68e5a340 100644 --- a/examples/app-todo/CHANGELOG.md +++ b/examples/app-todo/CHANGELOG.md @@ -1,5 +1,108 @@ # @objectstack/example-todo +## 4.0.104 + +### Patch Changes + +- Updated dependencies [31fe289] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [9037b13] +- Updated dependencies [665db00] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [cd5f554] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [cfb602c] +- Updated dependencies [343ddb2] +- Updated dependencies [0c666c3] +- Updated dependencies [f672e0d] +- Updated dependencies [38717d3] +- Updated dependencies [bfbdeec] +- Updated dependencies [4adc890] +- Updated dependencies [bcdf8d3] +- Updated dependencies [59bcd22] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [06cccd3] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [a26abbc] +- Updated dependencies [d9d42d4] +- Updated dependencies [67d5d53] +- Updated dependencies [47226e6] +- Updated dependencies [a545a04] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [8589756] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [8cc709d] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [7b80724] +- Updated dependencies [fb1ab89] +- Updated dependencies [6c4c43a] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [63de248] +- Updated dependencies [a2437b5] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [7193160] +- Updated dependencies [30dcd8a] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/objectql@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/runtime@18.4.0 + - @objectstack/mcp@18.4.0 + - @objectstack/client@18.4.0 + - @objectstack/service-knowledge@18.4.0 + - @objectstack/driver-sqlite-wasm@18.4.0 + - @objectstack/metadata@18.4.0 + - @objectstack/knowledge-memory@18.4.0 + - @objectstack/service-i18n@18.4.0 + ## 4.0.103 ### Patch Changes diff --git a/examples/app-todo/package.json b/examples/app-todo/package.json index 94ed2b757cc..16873716ca1 100644 --- a/examples/app-todo/package.json +++ b/examples/app-todo/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-todo", - "version": "4.0.103", + "version": "4.0.104", "description": "Example Todo App using ObjectStack Protocol", "license": "Apache-2.0", "private": true, diff --git a/examples/embed-objectql/CHANGELOG.md b/examples/embed-objectql/CHANGELOG.md index 06b68663fc1..f4c7657021e 100644 --- a/examples/embed-objectql/CHANGELOG.md +++ b/examples/embed-objectql/CHANGELOG.md @@ -1,5 +1,90 @@ # @objectstack/example-embed-objectql +## 0.0.44 + +### Patch Changes + +- Updated dependencies [31fe289] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [cd5f554] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [343ddb2] +- Updated dependencies [0c666c3] +- Updated dependencies [f672e0d] +- Updated dependencies [38717d3] +- Updated dependencies [bfbdeec] +- Updated dependencies [4adc890] +- Updated dependencies [bcdf8d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [a26abbc] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [8cc709d] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [7b80724] +- Updated dependencies [fb1ab89] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [63de248] +- Updated dependencies [a2437b5] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [7193160] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/objectql@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/driver-memory@18.4.0 + ## 0.0.43 ### Patch Changes diff --git a/examples/embed-objectql/package.json b/examples/embed-objectql/package.json index fc04acb5cad..1621b7cf54e 100644 --- a/examples/embed-objectql/package.json +++ b/examples/embed-objectql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-embed-objectql", - "version": "0.0.43", + "version": "0.0.44", "private": true, "description": "Embed the ObjectQL engine as a plain library via @objectstack/objectql/core — no kernel, no plugins, no metadata protocol (ADR-0076).", "type": "module", diff --git a/packages/adapters/hono/CHANGELOG.md b/packages/adapters/hono/CHANGELOG.md index f2008aedd91..04e8385a25b 100644 --- a/packages/adapters/hono/CHANGELOG.md +++ b/packages/adapters/hono/CHANGELOG.md @@ -1,5 +1,27 @@ # @objectstack/hono +## 18.4.0 + +### Patch Changes + +- Updated dependencies [b90a6f7] +- Updated dependencies [3b8c724] +- Updated dependencies [665db00] +- Updated dependencies [59bcd22] +- Updated dependencies [fc869c2] +- Updated dependencies [a545a04] +- Updated dependencies [8589756] +- Updated dependencies [8cc709d] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [7b80724] +- Updated dependencies [38d416f] +- Updated dependencies [b3d2f4b] +- Updated dependencies [30dcd8a] + - @objectstack/runtime@18.4.0 + - @objectstack/plugin-hono-server@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/adapters/hono/package.json b/packages/adapters/hono/package.json index e12fd130c18..d8bdc53a72b 100644 --- a/packages/adapters/hono/package.json +++ b/packages/adapters/hono/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/hono", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "type": "commonjs", "main": "dist/index.js", diff --git a/packages/apps/account/CHANGELOG.md b/packages/apps/account/CHANGELOG.md index fecfa5bff7b..62329920e6f 100644 --- a/packages/apps/account/CHANGELOG.md +++ b/packages/apps/account/CHANGELOG.md @@ -1,5 +1,83 @@ # @objectstack/account +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/platform-objects@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/apps/account/package.json b/packages/apps/account/package.json index 3a363b5def7..e48c8da9558 100644 --- a/packages/apps/account/package.json +++ b/packages/apps/account/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/account", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "ObjectStack Account — the end-user account/self-service console app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "type": "commonjs", diff --git a/packages/apps/connector-docusign/CHANGELOG.md b/packages/apps/connector-docusign/CHANGELOG.md index 9163876d971..9a3826c5e0e 100644 --- a/packages/apps/connector-docusign/CHANGELOG.md +++ b/packages/apps/connector-docusign/CHANGELOG.md @@ -1,5 +1,71 @@ # @objectstack/connector-app-docusign +## 0.1.2 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/connector-openapi@18.4.0 + ## 0.1.1 ### Patch Changes diff --git a/packages/apps/connector-docusign/package.json b/packages/apps/connector-docusign/package.json index 626277d28dd..1a429ff5b5c 100644 --- a/packages/apps/connector-docusign/package.json +++ b/packages/apps/connector-docusign/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-app-docusign", - "version": "0.1.1", + "version": "0.1.2", "description": "DocuSign eSignature connector app (open edition, sandbox first): DocuSign's eSignature OpenAPI document, trimmed offline to three envelope operations, as a declarative `openapi` connector instance authorized by OAuth 2.0 (`authorization_code`) with an OAuth client each deployment's administrator registers and enters on the connection.", "license": "Apache-2.0", "private": true, diff --git a/packages/apps/connector-github/CHANGELOG.md b/packages/apps/connector-github/CHANGELOG.md index 87af2898cc7..60a8cb912d2 100644 --- a/packages/apps/connector-github/CHANGELOG.md +++ b/packages/apps/connector-github/CHANGELOG.md @@ -1,5 +1,71 @@ # @objectstack/connector-app-github +## 0.1.2 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/connector-mcp@18.4.0 + ## 0.1.1 ### Patch Changes diff --git a/packages/apps/connector-github/package.json b/packages/apps/connector-github/package.json index 8c9508df3c6..632f2e8673c 100644 --- a/packages/apps/connector-github/package.json +++ b/packages/apps/connector-github/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-app-github", - "version": "0.1.1", + "version": "0.1.2", "description": "GitHub connector app (open edition): GitHub's remote MCP server as a declarative `mcp` connector instance with a read-tool allowlist, authenticated by a personal access token the administrator supplies through the credential store.", "license": "Apache-2.0", "private": true, diff --git a/packages/apps/connector-stripe/CHANGELOG.md b/packages/apps/connector-stripe/CHANGELOG.md index bba3e4beae9..9ae32576a53 100644 --- a/packages/apps/connector-stripe/CHANGELOG.md +++ b/packages/apps/connector-stripe/CHANGELOG.md @@ -1,5 +1,71 @@ # @objectstack/connector-app-stripe +## 0.1.2 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/connector-openapi@18.4.0 + ## 0.1.1 ### Patch Changes diff --git a/packages/apps/connector-stripe/package.json b/packages/apps/connector-stripe/package.json index c90c88d1a02..9cc354d80e4 100644 --- a/packages/apps/connector-stripe/package.json +++ b/packages/apps/connector-stripe/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-app-stripe", - "version": "0.1.1", + "version": "0.1.2", "description": "Stripe connector app (open edition, read only): Stripe's OpenAPI document, trimmed offline to eight read operations, as a declarative `openapi` connector instance authenticated by a restricted secret key the administrator supplies through the credential store.", "license": "Apache-2.0", "private": true, diff --git a/packages/apps/setup/CHANGELOG.md b/packages/apps/setup/CHANGELOG.md index 437bb6f55f0..c4225889f00 100644 --- a/packages/apps/setup/CHANGELOG.md +++ b/packages/apps/setup/CHANGELOG.md @@ -1,5 +1,83 @@ # @objectstack/setup +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/platform-objects@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/apps/setup/package.json b/packages/apps/setup/package.json index 6e364861bf3..2d85d635397 100644 --- a/packages/apps/setup/package.json +++ b/packages/apps/setup/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/setup", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "ObjectStack Setup — the platform administration app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "type": "commonjs", diff --git a/packages/apps/studio/CHANGELOG.md b/packages/apps/studio/CHANGELOG.md index 396f4a53e16..1ba4bf37476 100644 --- a/packages/apps/studio/CHANGELOG.md +++ b/packages/apps/studio/CHANGELOG.md @@ -1,5 +1,83 @@ # @objectstack/studio +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/platform-objects@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/apps/studio/package.json b/packages/apps/studio/package.json index 619206e40c7..8f4d7fc1f39 100644 --- a/packages/apps/studio/package.json +++ b/packages/apps/studio/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/studio", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "ObjectStack Studio — the metadata builder app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "type": "commonjs", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 4371e3f5e2d..19205e60d60 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,530 @@ # @objectstack/cli +## 18.4.0 + +### Minor Changes + +- 9ebca94: feat(push): `requires: ['push']` and the stack's `push` key turn mobile push on under `objectstack serve`, and a relay configuration is refused at boot + + Clause-②: yes (widening) + Metadata-protocol: added + Skills-docs: follow-up owed — #29969 (skills/objectstack-platform/evals/config-plugins-ops.json: no eval asks for `requires: ['push']` or the `push` key); this diff teaches both in `skills/objectstack-platform/SKILL.md` (the top-level key enumeration and the `requires` table) and in the new `content/docs/kernel/runtime-services/push-service.mdx` + + A deployment that runs `objectstack serve` from a stack file can now turn on mobile push without host code. Before this, the only way to register a push service was a host that composed `PushServicePlugin` in code, so a `notify` node naming `push` had nothing to deliver through on a served stack. + + - **`@objectstack/spec`: the `push` capability token and the `push` stack key.** `push` joins `PLATFORM_CAPABILITY_TOKENS`, provided by `@objectstack/service-push` in the open edition. It is requested, never on the always-on slate. The stack definition gains a top-level `push` key beside `email` and `sms`: it is `PushServiceConfigSchema`, composed as a single value (`COMPOSE_KEY_DISPOSITIONS`). Authoring accepts a direct block, a relay block and absence; it refuses an inline secret, by key name (`key`, `privateKey`, `serviceAccount`, …) with `credentialRef` as the remedy, or pasted into a `credentialRef` (ADR-0143 D3). `PushServiceConfigSchema`'s docblock now names the stack key and `objectstack serve` as its reader; `PushRelayTransportConfigSchema` keeps its `[PLANNED — not yet served]` marker. + - **`@objectstack/core`: the `push` provider row and its argument.** `CAPABILITY_PROVIDERS.push` names `PushServicePlugin`, and `resolveCapabilityArgument('push', …)` builds its argument from the stack's `push` key through `resolvePushCapabilityArg`, read off the provider module as the `email` and `sms` readers are. Both `objectstack serve` and `@objectstack/verify`'s `bootStack` read this table, so an app's tests get the push service its server gets. + - **`@objectstack/service-push`: `resolvePushCapabilityArg`.** It parses the `push` key with `PushServiceConfigSchema` and refuses, before the kernel boots, two configurations: the relay transport (an absent key and `push: {}` read as it), whose sender has not shipped, with a message that says so and prescribes `transport: 'direct'` with an `apns` and/or `fcm` block; and anything the schema refuses, an inline secret first among them, naming each path and the schema's guidance and never the value. On a stack that declares `requires: ['push']` either refusal fails the boot as `Capability "push" (@objectstack/service-push) failed to start: …`. + - **`@objectstack/cli` and `@objectstack/verify`** depend on `@objectstack/service-push`, so the provider the token names resolves out of the box, as the open edition promises. + + To turn push on, add `requires: ['push']` and a direct block naming each vendor's secret by reference: + + ```ts + push: { + transport: 'direct', + apns: { teamId: 'ABCDE12345', keyId: 'KEY1234567', topic: 'com.example.app', environment: 'production', credentialRef: 'APNS_SIGNING_KEY' }, + fcm: { projectId: 'example-app', credentialRef: 'FCM_SERVICE_ACCOUNT' }, + }, + ``` + + In the open tier each `credentialRef` is the name of an environment variable holding the secret. A `push` key on a stack whose `requires` does not name `push` is not read, and `objectstack serve` logs one boot warning saying so. The runtime-services chapter has a new `services.push` page. +- 699ebd8: fix(metadata-protocol)!: the metadata save and publish doors refuse a select field bound to a picklist no loaded package declares, instead of storing it and then refusing every write to the field (#29898) + + Clause-②: no (narrowing) — the metadata save door (draft and live), the per-item and package publish, and the restore doors refuse an `object` or `object_extension` whose field names a picklist no loaded package declares, a body they accepted before. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: no move — `skills/**` teaches no picklist reference verdict and `content/docs/**` carries no severity table for it; the prescription ships in the refusal text, the FROM → TO below and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing at the metadata save and publish doors: `PUT /api/v1/meta/object/NAME` and `PUT /api/v1/meta/object_extension/NAME` (draft and live), `POST /api/v1/packages/PKG/publish-drafts`, the per-item publish, and the rollback / revert restore doors. + + - **Before.** A select (or multiselect) field with `picklist: 'nope_list'`, a name no loaded package declares, saved `200` with its draft reading `_diagnostics.valid: true`, and `publish-drafts` answered `outcome: published`. The field was then served with no options, and every create or update that set it answered `400 VALIDATION_FAILED` ("takes its values from picklist \"nope_list\", which no loaded package declares"). The boot and a late install already refuse the same shape for a package's own objects, and `os validate` / `os build` refuse it for a stack's own fields. + - **Now.** The doors refuse it `422 INVALID_METADATA`, one finding per unknown binding at `fields.FIELD.picklist`, naming the field and the list. A list is declared when the engine's registry resolves it, the same read that serves the field's options: the package's own, a dependency's, the base package's of a `.custom` pair, or any other package installed here. + - **Unchanged.** A binding the object already holds is not judged again on a re-save, so an object published before this release keeps saving; the stored-metadata healing pass (`os migrate meta --stored`) is not judged. A draft stored before this release is judged when it is published. + - **`@objectstack/spec`.** The upgrade guide entry `picklist-binding-unknown-refused` (a semantic migration: no automatic rewrite, because whether the author meant another list, one still to be declared or inline options is a judgement about the field). + - **`@objectstack/cli`.** A source comment in the `os validate` picklist judge no longer says the runtime write path is unjudged. + + **FROM → TO.** `fields.t: { type: 'select', picklist: 'nope_list' }` → `picklist: 'NAME'` of a list a package installed here declares, or declare `nope_list` in a code package (a `*.picklist.ts` file, or `defineStack({ picklists })`), or `options: [...]` in place of `picklist`. +- e3b56b2: feat(spec): a lookup or master-detail field's related-list title is translatable + + Clause-②: yes + Metadata-protocol: added + + A related list the record page derives from a lookup or master-detail field is titled by the field's `relatedListTitle`, or by the child object's label when it has none. That title had no translation slot, so a zh-CN reader saw it in the authoring language, and `os i18n check` could not see the gap. A lookup from an object back to itself was the plainest case: with no title of its own, a contract's record page listed its renewals under 「合同」, the same word as the contract beside it. + + - `FieldTranslationSchema` declares `relatedListTitle`: `objects..fields..relatedListTitle`, on the child object whose field declares the title. `translateObject` overlays it on the served child object (`GET /meta/object`, and the app bundle's object members), which is what the console derives the related list's title from, on the side card and on the body table alike. + - It follows the packaged-base rule (ADR-0029 D9.2a): a title an org re-authored on top of the packaged declaration keeps its text. An authored `relatedListTitle` with no entry keeps its authored text; the child object's label is never substituted for it. + - `os i18n extract` scaffolds the key and `os i18n check` counts it under the field bucket. A field without a `relatedListTitle` is recorded without being demanded. + - The object save door puts a served (translated) `relatedListTitle` back to its source text, as it does for the field's other translated slots. +- bcdf8d3: feat(metadata-protocol,objectql): a package's action can be customized by a declared override in its paired `.custom` package (ADR-0146 D9 `action`), and an override that widens who may run the action is refused + + Clause-②: yes (widening) + Metadata-protocol: added + + - **A declared action override is admitted.** An `action` saved, published or installed in a base's paired `.custom` package with `overrides: { package: '', basedOn: '' }` was refused `403 NOT_OVERRIDABLE` ("action customization is not yet available"). It is now admitted by the save, publish and install doors, `os validate` and `os build`, like a flow override. It replaces the base's action whole, and every read of the base's action serves it under the base's name: the by-name and execution reads, the layered read's `effective` (with `code` still the base's), both lists, the object read's `actions` (what the list toolbar, row menu and record header are drawn from) and the engine's schema, which the REST `/actions` door and the MCP `run_action` bridge read the action from. Deleting the override serves the base's action again everywhere. The action activation switch keyed by the action's name turns the override off while it is armed, and the switch stays where it was when the base is restored. Only the pair declares: a dependent's declaration and the pair's undeclared same-named save stay refused, and a dependent's row is never served. + - **A script action's body is rebound.** The override's `body` runs in place of the base's handler after one resync (`kernel:ready`, `metadata:reloaded`, or the publish itself). Removing the override binds the base's handler again on the next resync, without re-registering the base. `@objectstack/objectql` adds `ObjectQL#registerActionOverride(objectName, actionName, handler, packageName)`, which binds a handler over the one registered under the key and keeps it beneath. `removeActionsByPackage(packageName)` now removes the package's entries from each key's stack, so the handler beneath is bound again. `registerAction` of another package's handler under a key an override holds takes the place beneath the override instead of replacing it: a marketplace-installed base binds its handlers in the install plugin's `kernel:ready` rehydrate, after the boot's resync bound the override, and after a restart the override still runs (and a restore runs the base's body). + - **An override may add a gate and never remove one (ADR-0066 D4).** The write gate refuses, with the new rule `declared_action_widens_access` (`403 NOT_OVERRIDABLE`), an action override that drops a capability the base's `requiredPermissions` names, sets `ai.exposed: true` on an action the base keeps off the agent surface, or drops the base's `ai.requiresConfirmation: true`. The refusal names each widened gate. Adding a capability or turning a gate on is admitted. Everything else (`label`, `type`, `target`, `body`, `params`, `visible` and the rest) is replaced whole, so an override can still change what runs, including a body that runs system-elevated, but only for people who could already run the action. `@objectstack/metadata-protocol` exports `actionOverrideWidening(baseItem, overrideItem)`, which returns the widened gates as clauses, and `OverrideWriteClosure.baseItemOf`, through which a door hands the judge the base's own item. `@objectstack/cli` (`os validate`) and `@objectstack/cloud-connection` (the managed install) pass it. + - The action switch route in the packaged-base refusal sentences is now spelled `POST /api/v1/actions/_activation/:object_or_global/:action`, which keeps the declared-save sentence within the REST door's 500-character message bound for long action names. + + Skills-docs: follow-up #30505 +- 8cc709d: fix(objectql)!: a disabled package's `objectExtensions` fields are withheld from the object they extend, and come back with their values on enable + + Clause-②: yes (narrowing) — while a package is disabled, the served object it extends (`GET /api/v1/meta/object/:name`, the record page, the `/data` field map) no longer carries the fields, validation rules and indexes the package's `objectExtensions` add, so a `/data` write, filter, sort or `fields` projection naming one of those fields is refused as a field the object does not have (`400 INVALID_FIELD` / `INVALID_SORT`), and `GET /api/v1/packages/:id/customizations` no longer lists them among the base's `contributions`. + Metadata-protocol: semantic + + + + **BREAKING** served-object narrowing under package disable (`@objectstack/objectql`, `@objectstack/runtime`, `@objectstack/cli`). + + - **Before.** Disabling a package withheld its objects (`404 OBJECT_PACKAGE_DISABLED`), flows, navigation items, permissions and actions, but the fields its `objectExtensions` add to another package's object stayed on that object: in `/meta/object`, in every `/data` read and on the record page. + - **Now.** The served fold of an object leaves out every `extend` contribution of a disabled package: its fields, its validation rules, its indexes and its field patches. A base layer that already carries them (a tenant overlay saved from a served read, a metadata-service copy of a resolved object) has those fields and rules taken off too. A field the owner itself declares, or another enabled extension also adds, stays. A write naming a withheld field answers exactly what a write naming an unknown field answers; a write through the object's other fields lands and leaves the withheld values alone. + - **Kept.** Schema sync, the federated binding and the lifecycle sweep read the object with every extension folded in (`SchemaRegistry.getStorageObject` / `getAllStorageObjects`), so a disable never drops or skips a column, and a boot that starts with the package disabled still creates them. Enabling the package serves the fields again with the stored values. With `OS_SKIP_SCHEMA_SYNC=1`, `os migrate`'s drift plan no longer reports a disabled module's columns as orphan columns to drop. + - **Who moves.** A client that reads a disabled module's extension fields gets the object without them. A formula on the object that reads a withheld field answers `null`. A validation rule of another, enabled package that reads a withheld field cannot be evaluated, so writes to the object are refused (`400 VALIDATION_FAILED`, naming the rule) until the field's package is enabled again or the rule's package declares and keeps that package. +- a91ad42: fix(metadata-protocol,cli,cloud-connection)!: an object extension that re-declares an owner's field in `fields` beyond its display properties is refused at the save and publish doors, at `os validate` and at the install door, with a prescription naming `fieldPatches` (ADR-0146 D10) (#30610) + + Clause-②: no (narrowing) — the `object_extension` save, publish and restore doors, `os validate` and `POST /api/v1/marketplace/install-local` refuse an `objectExtensions[].fields` entry naming a field the extended object's owner declares when it changes anything outside the display-property whitelist, a body they accepted before. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: no move — `skills/**` and `content/docs/**` already teach `fieldPatches` as the way to change a base field's display, and no page teaches taking an owner field over through `fields`; the prescription ships in the refusal text. + + + + **BREAKING** accept-set narrowing at the metadata save and publish doors (`PUT /api/v1/meta/object_extension/NAME`, draft and live, the per-item and package publish, and the restore doors), at `os validate`, and at the marketplace install door. + + - **Before.** An extension written into `fields` with the name of a field the extended object's owner declares replaced the owner's field whole when the registry folded the object: a module package could turn the owner's `amount: number` into `text`, drop its `required`, or make it `readonly`, and every reader judged the field on the extension's definition. Only the registry's warning named it. + - **Now.** Such an entry is refused when it changes anything outside the display-property whitelist (`label`, `description`, `inlineHelpText`, `placeholder`, an option's `label`): `400 VALIDATION_ERROR` at the save and publish doors (located in `issues[]` at `fields.FIELD`), `422 VALIDATION_ERROR` at the install door (every entry in `details.refusals`), and exit 1 with the entry on `errors` at `os validate`. Each refusal names the keys the entry changes and prescribes `fieldPatches`. Both bodies are read through the field schema first, so a default the parse stamps is no change; options are compared by stored value, so a relabel or reorder is no change. + - **Unchanged.** A re-declaration that changes only display properties is admitted. An extension of an object its own package owns is not judged, nor is an added field. `os validate` judges an owner declared by another package of the same artifact; a target outside the artifact is judged at install, against the installed owner. The registry's warning on a re-declared field stays. `os build` does not yet run the check. + + **The remedy.** Move a display change of an owner's field into the extension's `fieldPatches` (the field name as its key, the whitelisted properties as its body), and add a behaviour of your own as a new field under a new name. + +### Patch Changes + +- 31fe289: feat(objectql,cli): `os migrate recompute-home-amounts` — recompute stored home-currency amounts at the rates in force now, report first, rows without a rate listed and skipped + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/objectql`: `recomputeHomeAmounts(engine, logger, { object, field?, since?, apply?, maxRecords?, maxListed?, source? })`**, with `assertHomeAmountRecomputeSince` (the pure `since` check a door can run before it opens a database), `formatHomeAmountRecomputeReport`, `homeAmountRecomputeComplete` and `HOME_AMOUNT_SKIP_REASONS`. It walks every stored row of an object whose `record`-mode amounts declare `currencyConfig.homeAmount` and projects each through the write-time projection itself (the same rate row, division, single rounding, same-currency rule and organization-timezone day), in the row's own organization, so each organization converts into its own home currency at its own rates and is counted apart. A dry run (the default) reports what would change and writes nothing; `apply` writes only the rows that differ, through `engine.update` with the home field in the payload, so the value stored is the one the write itself computes, and only the home and rate fields move (`updated_at` is kept, so a consumer syncing by `updated_at` does not see the recompute). Being a write, it moves what reads the home field as a fresh write would: stored formulas, parent summaries, and the per-row audit rows where audit capture is installed. Rows that cannot be converted are listed by reason and never written: `rate_missing` (with the currency and date), `code_blank` (an amount with no currency code; the remedy is `os migrate record-currency-codes`), `document_date_blank`, `home_currency_unset` and `unconvertible`. `since` filters on the document date (`homeAmount.rateDate`). One run-level `sys_audit_log` entry records each applied run (counts per organization and reason, and the skipped rows), also when an unclassified error stops the run partway (marked `aborted`, then the error is thrown on). A walk cut short by `maxRecords` is `truncated` and not complete. Refused before any row is read: an unknown object (`OBJECT_NOT_FOUND`), an object or `field` declaring no home amount (`INVALID_FIELD`), a `since` that is not `YYYY-MM-DD` (`VALIDATION_FAILED`). + - **`@objectstack/objectql`: `ObjectQL.homeAmountReadsFor(context?)`**, the reads the write-time projection makes (its serving, and the organization a write converts a row under), for a caller that projects a stored row as a write would. + - **`@objectstack/cli`: `os migrate recompute-home-amounts --object [--field ] [--since YYYY-MM-DD] [--apply] [--yes] [--json] [--force] [--max-records] [--database-url]`**, the shell of `os migrate record-currency-codes`, booting the settings service (each organization's `localization.currency` and `.timezone`) and the audit ledger. It exits 1 while any row is listed and skipped, on a dry run as on an apply, because a total over the home field silently leaves such a row out, and when `--max-records` cut the walk short. +- b13fc74: feat(cli): `os migrate blank-multi-values` rewrites the `[]` and `''` still stored on optional multi-value columns to `null` + + Clause-②: no + Metadata-protocol: none + + Since #29343 the write door stores a cleared optional multi-value value (`[]` or `''` on a `multiselect`, `checkboxes` or `tags` column, or on a `multiple: true` `select`, `radio`, `lookup`, `user`, `file` or `image` column) as `null` (ruling A on #29343). Rows cleared before that still hold the JSON text `'[]'` or `'""'`, so `= null`, grouping and the read shape split them from rows cleared since. This new command is the one-time clean that ruling's execution step 3 binds, an `os migrate` data step of its own beside `blank-single-values`, not part of `os migrate value-shapes`, which rewrites nothing. + + - **Columns.** Read from `blankAsNullColumns` in `@objectstack/core`, the rule the write door and the seed replay read: its optional columns on the `$empty` table's `multi_value` row. A `required` column, a `readonly` column (a platform-stamped `[]`, such as `sys_activity.changed_fields`) and a single-value column are never touched, nor is a non-empty list. + - **Dry run by default.** It counts the stored `[]` and `''` per object and column and writes no row. `--apply` rewrites each affected row's blank columns to `null` through the engine. `--object` narrows the walk and refuses a name the deployment does not declare. `--apply` refuses a SQLite file another process holds unless `--force`. + - **No flag.** Nothing is gated on it. A re-run that reports zero is the verification. +- fbbbaee: fix(cli): `os package install` prints the connector maps the package export withheld (#29873) + + Clause-②: no + Metadata-protocol: none + + An exported package names each connector header or environment map the export withheld whole in its `withheldMaps`, and both install modes of the runtime's install-local endpoint name each map in their answer. The command dropped that list, so an operator who installed an export the documented way (`os package install DIR/NN-ID/objectstack.json`) was never told that the connector sends none of those headers or environment variables. + + The command now prints one warning, one line per map (`connector 'NAME' PATH`, for example `connector 'erp' providerConfig.headers`), and the remedy: re-enter each map where the connector is authored. It does this for a managed install (hot-loaded or not) and for an unmanaged one, from a catalog id or an artifact file. An answer without the list prints nothing new, and the exit code is unchanged. The command has no `--json` output. +- 8d5aab3: fix(cli): `os dev` and `os start` share one data-key file, so a secret saved under `npm run dev` reads back under `os start` + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/cli`.** Both commands open the same project database, but `os start` used to mint its own data key under its own home (`PROJECT/.objectstack/dev-crypto-key` in project mode) while `os dev` kept the key at `$HOME/.objectstack/dev-crypto-key` (or `$OS_HOME`'s). Every secret written during development then read back as unset under `os start`. Both commands now resolve the key file the way `os dev` always has (`os start --home` counts as `OS_HOME`) and hand it to their `serve` child on the internal `OS_INTERNAL_CRYPTO_KEY_FILE` channel. The project-local file an earlier `os start` minted is not orphaned: when it alone holds a key it is still read, and the boot says which file; when it and the shared file hold different keys, the shared one is used and a warning names both paths. An explicit `OS_SECRET_KEY` / `OS_DEV_CRYPTO_KEY` still wins, unchanged. + - **`@objectstack/service-settings`.** New exports `resolvePersistedKeyFile` (the one resolution both commands take, beside the provider's own key-file path) and `KEY_FILE_CHANNEL_ENV`; the local crypto provider's file tier reads the file that channel names, ahead of `OS_HOME`. +- 665db00: fix(cli): `os dev` on an `os init -t app` scaffold no longer composes a second `MetadataPlugin` and warns "Plugin superseded: 'com.objectstack.metadata'" + + Clause-②: no — boot diagnostics only; nothing accepted or published changes + Metadata-protocol: none + + The dev artifact door in `os serve` / `os dev` added its own `MetadataPlugin` unless the stack already held one, but it recognised that plugin only by its class name. The published `@objectstack/metadata` bundle names the class `_MetadataPlugin`, so on an installed CLI the check missed the `MetadataPlugin` the standalone stack had already composed, added a second one, and every dev boot of an untouched scaffold printed a "Plugin superseded" diagnostic blaming a composition the user never wrote. The dev door now asks the same question the host-config door asks: a plugin named `com.objectstack.metadata` (or, as before, a class named `MetadataPlugin`) means the stack has one. A host config with no `MetadataPlugin` of its own still gets the dev door, as before. +- cd5f554: fix(objectql,cli): `os migrate stored-expressions` fills a stored computed field on rows stored before it was declared, and the boot names the command when a sample of rows needs it + + Clause-②: yes (widening) + Metadata-protocol: none + + A stored computed field (a non-`formula` field declaring `expression`) is computed by the engine on every insert and update and on nothing else, so declaring one on an object that already holds rows left the new column NULL on every existing row until each was next written; a filter, sort, roll-up or analytics anchor over it read NULL where the expression gives a value, with no error. A fresh database was right and an upgraded one was not. + + - **`@objectstack/cli`: `os migrate stored-expressions [--object ]… [--field ] [--changed] [--apply] [--yes] [--json] [--force] [--max-records] [--database-url]`.** A dry run (the default) reports, per field, the rows holding a NULL the expression gives a value; `--apply` writes them. Each value is the write's own computation over the stored row (option labels, the one reference hop, the `can()` map), and the apply writes through `engine.update` with `updated_at` kept, so hooks, roll-ups over the column and audit capture see an ordinary write. A NULL the expression itself answers (an empty reference, a null input) is left alone. A value that differs from the expression — the expression changed after the row was written — is counted on every run and rewritten only with `--changed`; an expression reading the clock always differs there. A row on which an expression does not evaluate while it holds a value is listed and not written, because the write would store NULL over it. The command exits 1 while a row is skipped or could not be written, or when `--max-records` cut the walk short. Idempotent: a re-run finds nothing to fill. + - **Boot advisory.** At `kernel:bootstrapped` the engine plugin reads the first 100 rows of each object declaring a stored computed field (nothing for an object declaring none) and, when that sample holds such a NULL, warns once naming each `object.field` and `os migrate stored-expressions`. It writes nothing and never fails a boot. + - **`@objectstack/objectql`:** `backfillStoredExpressions`, `formatStoredExpressionBackfillReport`, `storedExpressionBackfillComplete`, `announceStoredExpressionHoles`, `sampleStoredExpressionHoles`, and `ObjectQL.computeStoredExpressionsOver(object, rows, context?, onFault?)` — a stored computed field's values over rows already stored, computed as a write computes them. +- d9895a1: fix(cli): `os serve` reads `OS_TRUSTED_APP_SCHEMES` into AuthPlugin's `trustedAppSchemes`, so a deployment can register its mobile app for the sign-in hand-off + + Clause-②: no + Metadata-protocol: none + + - **Before.** `serve` constructed `AuthPlugin` with no `trustedAppSchemes`, and no setting a deployment could make reached the key. The `one-time-token` plugin never mounted and `GET /api/v1/auth/one-time-token/handoff` answered `400 … is not a registered app scheme` for every app, whatever the operator set. + - **Now.** `OS_TRUSTED_APP_SCHEMES` is read the way `OS_TRUSTED_ORIGINS` is: comma-separated, each entry trimmed, empty entries dropped, repeats folded. The list is passed as `trustedAppSchemes`. `os dev` and `os start` run `serve` as a child with the inherited environment, so they read it through the same line. + - **Unset stays unset.** No default scheme is registered: unset, empty or only separators, the plugin stays unmounted and the hand-off refuses every target, as before. + - **A bad entry still fails the boot.** Entries are not validated, lower-cased or dropped by the reader; plugin-auth's `assertTrustedAppSchemes` refuses a bad one at construction, naming it. + - Environment variable only: no stack key is added. The hosted runtime's own `AuthPlugin` is not changed. Listed in `content/docs/deployment/environment-variables.mdx` beside `OS_TRUSTED_ORIGINS`. +- f6f663a: fix(cli): when two data-key files hold different keys, `os start` and `os dev` read with the one that opens the project's stored secrets, or a production boot refuses + + Clause-②: yes (narrowing) — a production `os start` / `os serve` boot whose two candidate key files (the shared `$HOME/.objectstack/dev-crypto-key` or `$OS_HOME`'s, and the project's `.objectstack/dev-crypto-key`) both fail to open the database's newest stored secret now refuses to start, naming both files; it used to start and serve every stored secret as unset. The widening half: `@objectstack/service-settings` gains new exports (`chooseKeyFileByTrialOpen`, `settleKeyFileChannel`, `storedSecretReaderOverEngine`, `deferredLocalCryptoProvider`, `DataKeyOpensNothingError`, `KEY_FILE_CANDIDATE_CHANNEL_ENV` and their types). + Metadata-protocol: none + + + + **BREAKING** boot narrowing (`@objectstack/cli`, `@objectstack/service-settings`): a production boot whose candidate data keys open none of the stored secrets refuses to start. + + - **`@objectstack/cli`.** When the shared key file and the project's own key file (the one an earlier `os start` minted) hold different keys, the CLI no longer picks one. It hands the `serve` child both, and the child decides once the database is open: it opens the newest stored `sys_secret` value under each key and reads with the key that opens it, and the boot says which file it took. A project that only ever ran `os start`, on a machine whose home holds a key from `os dev` in another project, reads its secrets back again. With no stored secret yet, the shared file is used, as before. When neither key opens the stored secrets, a production boot (`os start`, `os serve`) refuses to start and names both files; a development boot (`os dev`) starts and prints a boot error naming both files. The one-shot commands (`os migrate`, `os secret rewrap`, `os secret orphans`, `os storage orphans`) now read the same file by the same rules: the project's own file when it alone holds a key, and the key that opens a stored secret when both do. + - **`@objectstack/service-settings`.** The settings service makes that choice in its `start()`, before any reader decrypts, when the CLI names a second candidate on the internal `OS_INTERNAL_CRYPTO_KEY_FILE_CANDIDATE` channel. A leading `~` in `OS_HOME` (or `OBJECTSTACK_HOME`) is now the home directory when the provider finds its key file, as `os start` already read it; it used to name a directory called `~` under the working directory. + + **The remedy.** If a production boot refuses with "neither data key opens this database's stored secrets", find the key those secrets were sealed under (a backup of either file, or the `OS_SECRET_KEY` the deployment once ran with), keep a copy of both files, and put that key at the shared file the message names, or set `OS_SECRET_KEY` to it. + + Supersedes, in the same release: the note for #29934 says that when the shared file and the project's file hold different keys, the shared one is used and a warning names both paths. After this change the boot uses whichever of the two opens the stored secrets, and refuses in production when neither does. +- 159930a: fix(service-settings): when two data-key files hold different keys, the boot judges the newest stored secrets as a page, not only the newest one, and reports a database split between the two keys + + Clause-②: no — an internal boot heuristic and its notice text. The shapes it changes (`StoredSecretReader` now answers the page, `KeyFileChoice` gains the `mixed` reason and a `tally`, `DataKeyOpensNothingError` gains `judged`) were added by #30042, whose note is still pending, so they first ship together in this release; no released export changes. + Metadata-protocol: none + + - **`@objectstack/service-settings`.** When the shared key file and the project's own key file hold different keys, the boot now trial-opens each of the newest stored `sys_secret` rows (up to ten) under both keys and reads with the key that opens more of them, on a tie the one that opens the newest. A row that opens under neither key (a corrupt or foreign row) no longer decides the boot: if an older row opens under one key, that key is used, and only a page where no row opens under either key keeps the production refusal. When each key opens some of the rows, the database holds secrets under both keys: the boot uses the key that opens more of them and prints an error naming both files, how many rows each key opened, and the fix. The boot's notices now say what was measured ("opens the 3 newest stored secrets", "opens 2 of the 3 newest stored secrets") instead of claiming the chosen file opens all of the database's stored secrets. + - **`@objectstack/cli`.** The one-shot commands (`os migrate`, `os secret rewrap`, `os secret orphans`, `os storage orphans`) share the same trial, so they make the same choice. + + **The remedy for a split database.** Keep a copy of both files, then save again each secret that reads as unset: it is sealed under the key the boot uses. Once nothing reads as unset, put that key at the shared file (or set `OS_SECRET_KEY` to it). + + Supersedes, in the same release: the note for #30042 says the boot opens the newest stored secret under each key and quotes the refusal as "neither data key opens this database's stored secrets". After this change the boot judges the newest rows as a page, and the refusal reads "neither data key opens any of the N newest stored secrets". +- afc761c: fix(cloud-connection,cli): an exported `.custom` installed on another environment lands in that environment's writable pair, and its overrides are served (ADR-0146 D2 / D3) + + Clause-②: no + Metadata-protocol: none + + - **Before.** `os package install /objectstack.json` of a base's paired customization package answered success and printed "Package installed into the running kernel", but the install registered the pair as code. None of its overrides was served, `GET /packages//customizations` listed none of them, and the pair turned read-only, so `ensure-customization` answered `writable: false` and every further save into it was refused `WRITABLE_PACKAGE_REQUIRED`. + - **Now.** `POST /api/v1/marketplace/install-local` installs a pair as what it is. The target's own pair is answered by its ensure door, which creates it when absent. Every item of the body is then written into it through the save door a Studio edit takes, with `mode: 'publish'` and the installer as actor, in load order. The overrides are served at once, the customization read lists them, and the pair stays editable. Nothing is registered as code or written to the cached-manifest directory. The answer carries `customizationPair` (`basePackageId`, `packageId`, `created`, `writable`), `writtenCount` and `written`, and the CLI prints them. + - **Refusals, nothing written.** A body key with no item form (code, a `translations` bundle, a key the install-key table does not classify), a nameless item, or two items claiming one name: `422`, every problem listed. A pair manifest the ensure door would not write: `400 PLUGIN_MANIFEST_INVALID`. A pair inside a multi-package artifact: `422`. A pair already read-only on the target: `409 RESOURCE_CONFLICT`, asked before the ensure door. If an earlier install registered it as code, the message names the uninstall and the restart that clear it. The ensure door's own refusal is relayed with its status and code. + - **A save refused part-way.** The answer is a failure with the save's code and status, naming the item and where it sits in the body. A pair this install created is deleted again. A pair that existed before is kept, and the answer lists the items written and the ones not written. + - **The CLI.** A runtime that predates this answers a pair as a managed install. The CLI now prints that as an error with the way back (uninstall, upgrade and restart, install again) and exits 1, instead of printing success. +- 1c53783: feat(cli,create-objectstack): `os build` says when the local dev database holds Studio customizations the artifact leaves behind; the starter's `AGENTS.md` and README say a Studio change lives in the database and name `os package export` + + Clause-②: yes (widening) — the exported CompileCommand gains a protected reportsLocalCustomizations flag (default false); nothing removed or narrowed + Metadata-protocol: none + + - **`@objectstack/cli`: `os build`** ends, on its text face, with one notice when the SQLite database `os dev` opens for the project (the same resolution, over the project's development `.env*` files) holds items of a `.custom` package: how many, in which pair, the database file, and that `os package export` takes them out while `os dev` runs. A change saved in Studio is such an item (ADR-0146 D2), never part of `src/`, so neither the artifact nor the starter's `Dockerfile` carries it. The read is local and read-only; a database that is not a local SQLite file is not probed and prints nothing, and a file that cannot be read is said to be unchecked, never empty. `os compile`, which `os dev` runs on every boot and rebuild, and `os build --json` print nothing new. + - **`create-objectstack`: the generated `AGENTS.md` and the blank starter's README** say that Studio saves into the environment's database as an item of `.custom`, that `build` and the `Dockerfile` leave it behind, that app changes are made in `src/`, and how a customization moves as a package (`os login`, `os package export`, then `os package install` on the deployment, in the numbered order). +- 959c26b: feat(cli,create-objectstack): a new project has a wired place for permission sets and datasets, `os g permission` / `os g dataset` write them, and the generated AGENTS.md names the package manager the project was created with + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`create-objectstack`: the blank starter ships `src/security/index.ts` and `src/datasets/index.ts`**, empty barrels already handed to the stack's `permissions` and `datasets` keys in `objectstack.config.ts`, like the eight directories it wired before. A permission set or a dataset exported from them is loaded and counted by `validate` with no edit to the config. The generated `AGENTS.md` lists both directories. + - **`@objectstack/cli`: `os g permission NAME [--object OBJECT]`** writes `src/security/NAME.permission.ts`, a `definePermissionSet` granting read, create and edit (no delete) on the object `--object` names, or the stack's only object, with `readScope: 'own'` declared. **`os g dataset NAME [--object OBJECT]`** writes `src/datasets/NAME.dataset.ts`, a `defineDataset` over that object with a `record_count` measure, grouped by the record's title field when it has one. `defineDataset` types the literal, so it needs no `as const`. Both pass `os validate`, `os build` and `os lint` with no finding on a fresh project. `os init` wires both directories too. + - **`create-objectstack`: the generated `AGENTS.md` (and `.github/copilot-instructions.md`) says `pnpm run validate` in a project created with pnpm**, and `npm run …` in one created with npm, the same package manager the install line and "Next steps" name. It used to say `npm run` in every project. +- bc254be: fix(cloud-connection,cli): the install-local ledger follows `OS_HOME`, so `os dev --fresh` and a second instance under another `OS_HOME` start without an earlier run's installs + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Now.** With no `storageDir`, the install-local ledger (`LocalManifestSource`, and so `MarketplaceInstallLocalPlugin`) lives at `$OS_HOME/installed-packages` when `OS_HOME` is set, and at `CWD/.objectstack/installed-packages` when it is not. Before, it was always the working directory, so `os dev --fresh` (a fresh `OS_HOME` and database) rehydrated a package an earlier run installed, and so did a second instance under another `OS_HOME`. + - **Unchanged.** `os dev` without `OS_HOME` keeps the ledger in the project, beside its database. `os start` in project mode sets `OS_HOME` to `CWD/.objectstack`, which is the same directory as before. An explicit `storageDir` is used as given. + - **Existing installs.** When the ledger moved (an `OS_HOME` other than `CWD/.objectstack`) and its new directory does not exist yet, the boot copies the `.json` files of the working-directory ledger into it once, and logs a warning naming both directories. The old files are left in place and are not read again; remove them. A copy that fails is logged as an error, and the next boot tries again. Under `os dev --fresh` nothing is copied. + - **`os doctor`** reads the same directory the runtime does, `OS_HOME` included. + - **The widening.** `@objectstack/cloud-connection` exports `resolveInstalledPackagesDir({ env?, cwd? })` (returns `{ dir, legacyDir }`), `INSTALLED_PACKAGES_HOME_SUBDIR`, and `LocalManifestSource.legacyDir` / `adoptLegacyEntries()`. `os doctor` takes the ledger directory from `resolveInstalledPackagesDir`; a `@objectstack/cloud-connection` without it is reported as a reader that does not declare the ledger directory. Align the two packages' versions. +- 8fad7b1: fix(metadata-protocol,cli): `os migrate packageless-overlays` lists a package-less view container `cannot_declare` instead of `movable`, and `--apply` no longer attempts (and fails) it on every run + + Clause-②: yes (widening) + Metadata-protocol: semantic + + - **`@objectstack/metadata-protocol`: the census gains the disposition `cannot_declare`** (`PackagelessOverlayDisposition`, reachable through the exported `PackagelessOverlayCensus`). A row lands there when the schema the save door judges its body by refuses an `overrides` key on it, so the declared save the move makes could never land. Of the five overlay types the ceremony moves, that is a view container (`list` / `listViews` / `form` / `formViews`): only a single view declares an override. Before, such a row was listed `movable` and every `--apply` reported it `failed` with `Unrecognized key(s) on this view container: overrides`, leaving the row where it was. Now `migratePackagelessOverlays` does not attempt it: no outcome, no failure, no pair ensured for it. + - **A view container's write onto a packaged name is accepted again.** `PUT /api/v1/meta/view/` with a container body (`list` / `listViews` / `form` / `formViews`) over a name one installed read-only package ships, naming no package or naming that base with `?package=`, and the matching `DELETE`, answered `403 NOT_OVERRIDABLE` with a fix (a declared save into the pair) that the container schema refuses. They now succeed, as they did before the ADR-0146 D11 closes; every other shape of the five overlay types is closed exactly as before. + - **The two closes read the same answer.** The package-less write close (`packagelessCloseSubject`) closes only what the ceremony moves, so a container's package-less save and delete stay open, as they were before the close: they are the one door such a row keeps. The close of a write naming the read-only base (`overlayPairFixAdmits`, the "the fix can be taken" predicate both closes ask) no longer names the pair as the fix for a container body, because that fix (a declared save into the pair) cannot be taken; that write keeps the answer it had before the close. + - **`@objectstack/cli`: `os migrate packageless-overlays`** prints the way out for a `cannot_declare` row under "left as they are": save each of the container's views into the pair as a single view named `.` (one the base ships declaring `overrides: { package, basedOn }`, a new one declaring nothing), then delete the container row with a delete naming no package (naming the base, for a row bound to it). In `--json`, the row carries `disposition: "cannot_declare"` and `outcome: "left"`, and `--apply` exits 0 for it. +- 8293c74: refactor(lint): the i18n translatable-key walk moves from the CLI into `@objectstack/lint` + + Clause-②: no + Metadata-protocol: none + + `collectExpectedEntries` — the one definition of which translation keys a stack can carry, each with the source text it translates — now lives in `@objectstack/lint` (`src/i18n-walk.ts`) and is exported from the package root with `authorWarnedTranslationGroups`, `humanizeFieldPath` and the `ExpectedEntry` / `ExpectedEntryOptions` types. It moved so a browser can run the same walk the CLI runs: Studio's translation editor builds its source-to-translation table from it. `os i18n extract` and `os lint`'s translation coverage keep reading it unchanged, through the CLI's extractor module, which keeps bundle assembly and serialization and re-exports the walk. + + - No key, no skeleton and no coverage verdict changes: the walk's code moved as is. + - In a browser the walk cannot read the liveness ledger from disk, so its default leaves no translation group out. That is the same set the CLI leaves out today, because the ledger warns on no translation group; a browser caller that must honour a warned group passes `warnedGroups`. + - `@objectstack/spec`: the `action` and `view` liveness ledgers cite the walk at its new path. +- 6ef7f58: feat(scenarios): the deterministic scenario scorer lives in `@objectstack/scenarios` and judges capability tags, agent-action exposure, write outcomes and per-persona answers against a loaded card + + Clause-②: no + Metadata-protocol: none + + - **`@objectstack/scenarios/score`** (private, unpublished): the entry the scenario runner calls. `scoreCardArtifact(card, artifact)` scores a built `dist/objectstack.json`; `gradeCardOperation(card, operationId, observation)` judges an observed write against its declared outcome; `gradeCardAnswer(card, { question, persona?, turn })` grades an agent's answer against the loader's computed truth for that persona. Each takes a card the loader returned, and the graders read the card schema's own types. + - **The scorer moved here from `@objectstack/cli`.** That covers the structural shape score, the artifact-to-graph reader and the Run grader, which were never released from the CLI. It now also carries: + - **capability-tag checks**: a closed table of nine tags, listed in the package README. A tag outside it, or a row with nothing in the card to judge, is reported `unscored` and counted nowhere; + - **agent-action exposure**: each declared action exists on its object, carries `ai.exposed: true` and, where the card says it asks first, `ai.requiresConfirmation: true`. A missing action fails; + - **a write-operation grader**: `applied`, `refused` or `pending_confirmation`. An unobserved fact fails; + - **per-persona Run grading**: a `withheld` truth is must-not-reveal, and an all-rows figure outside a scoped persona's rows is a leak. + - **`@objectstack/cli`**: `MetadataEvalCase` is now declared in `corpus.ts`, beside the golden corpus it types, and `metadata-eval.ts` re-exports it under the same name. No behaviour change, no new flag and no new package-entry export. The CLI carries no scenario code. + + No model call anywhere. +- 1030a80: fix(metadata-protocol, cli, cloud-connection): a declared action override moved to another object is told which object the base ships the action on + + Clause-②: no + Metadata-protocol: none + + The `declared_package_ships_no_such_item` refusal (`403 NOT_OVERRIDABLE`) for a `.custom` action override whose `objectName` differs from the base action's used to say the base "ships no action named 'X'" and to "correct the name", though the name was right. It now names both objects and tells the author to keep the base action's `objectName`, at all three doors (the save and publish door, `os validate`, the managed install), from one sentence builder in `judgeOverrideWrite`. A name the base does not ship at all reads as before. The rule, code and status are unchanged. +- 89dbc74: fix(cli): `os migrate` value walks name the columns they skip because the package that adds them is disabled + + Clause-②: no + Metadata-protocol: none + + `os migrate blank-single-values`, `blank-multi-values`, `option-codes`, `text-scalars` and `value-shapes` read each object as it is served. While a module package is disabled, the fields its `objectExtensions` add are withheld from that object (their columns and values are kept), so these runs could not read those columns and their reports read as if they had. Each report now lists every such column as `object.field (package ID)`, and the `--json` document carries the same list as `withheldColumns`. Enable the package and re-run to cover them. Nothing else changes: the runs still read and rewrite only what the served object declares. +- fec1f58: fix(cli): a connection label printed by the CLI removes a password or a credential query pair wherever the URL rules read one + + Clause-②: no + Metadata-protocol: none + + - **One credential grammar.** The database label that `os dev`, `os start` and `os serve` print, and the migrate and resync confirms, now remove URL credentials through the same shared function the datasource read door applies (`redactUrlCredentials` from `@objectstack/spec/data`): the userinfo password, and every credential query pair judged by the shared query-pair judge and its one list of names. Wherever those rules read a credential, on every path the label takes, the label drops it. When the shared rules read more, the label follows with no edit of its own. + - **Unchanged.** Every value the shared rules read no credential in prints exactly as before: `postgres://`, `libsql://` and `file:` values, values with no userinfo, a username-only userinfo, and everything the URL parser already cleaned. +- Updated dependencies [31fe289] +- Updated dependencies [a35fc66] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [13c0c5b] +- Updated dependencies [fe835b4] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [5cdaddb] +- Updated dependencies [75bbebb] +- Updated dependencies [509c40a] +- Updated dependencies [88cd0ab] +- Updated dependencies [c23501b] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [46d2b65] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [8d5aab3] +- Updated dependencies [665db00] +- Updated dependencies [e3b56b2] +- Updated dependencies [18112bf] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [cd5f554] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [07685ae] +- Updated dependencies [eee4149] +- Updated dependencies [85a8fa0] +- Updated dependencies [1756666] +- Updated dependencies [f9e5bef] +- Updated dependencies [63b86b3] +- Updated dependencies [0f3ee58] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [cfb602c] +- Updated dependencies [171b6dd] +- Updated dependencies [f6f663a] +- Updated dependencies [343ddb2] +- Updated dependencies [0c666c3] +- Updated dependencies [f672e0d] +- Updated dependencies [38717d3] +- Updated dependencies [bfbdeec] +- Updated dependencies [43faa22] +- Updated dependencies [4adc890] +- Updated dependencies [fdad178] +- Updated dependencies [15f808a] +- Updated dependencies [bcdf8d3] +- Updated dependencies [59bcd22] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [159930a] +- Updated dependencies [c045c34] +- Updated dependencies [dc51811] +- Updated dependencies [c133cfe] +- Updated dependencies [b77c160] +- Updated dependencies [afc761c] +- Updated dependencies [09720e2] +- Updated dependencies [a545a04] +- Updated dependencies [1c53783] +- Updated dependencies [04f36b6] +- Updated dependencies [959c26b] +- Updated dependencies [25fea77] +- Updated dependencies [a98e5cd] +- Updated dependencies [06cccd3] +- Updated dependencies [86ae2ca] +- Updated dependencies [0837fdc] +- Updated dependencies [4be9734] +- Updated dependencies [4be9734] +- Updated dependencies [1a3ea0c] +- Updated dependencies [bb86ff9] +- Updated dependencies [8f05e61] +- Updated dependencies [9afcd6a] +- Updated dependencies [870ebce] +- Updated dependencies [d1d90b7] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [db04e5c] +- Updated dependencies [a26abbc] +- Updated dependencies [bc254be] +- Updated dependencies [8eca18f] +- Updated dependencies [8b117c0] +- Updated dependencies [d9d42d4] +- Updated dependencies [67d5d53] +- Updated dependencies [47226e6] +- Updated dependencies [a545a04] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [8fad7b1] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [8589756] +- Updated dependencies [77c203a] +- Updated dependencies [54b491a] +- Updated dependencies [8293c74] +- Updated dependencies [bee9836] +- Updated dependencies [a93ef3e] +- Updated dependencies [0e4dfbe] +- Updated dependencies [ad0fbfd] +- Updated dependencies [8cc709d] +- Updated dependencies [b5765be] +- Updated dependencies [8fa738f] +- Updated dependencies [5a878a3] +- Updated dependencies [6e8ec73] +- Updated dependencies [60fe44f] +- Updated dependencies [22f0d65] +- Updated dependencies [fabd259] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [26c2efd] +- Updated dependencies [f7afab8] +- Updated dependencies [7b80724] +- Updated dependencies [e760204] +- Updated dependencies [fb1ab89] +- Updated dependencies [6c4c43a] +- Updated dependencies [455b4f0] +- Updated dependencies [4dde0ae] +- Updated dependencies [94cef75] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [ae5bd5b] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [7d11067] +- Updated dependencies [135c2fb] +- Updated dependencies [898145b] +- Updated dependencies [482e089] +- Updated dependencies [0e4747a] +- Updated dependencies [4bf6e8f] +- Updated dependencies [a0c68ae] +- Updated dependencies [ede9e6b] +- Updated dependencies [c5101a0] +- Updated dependencies [08e0247] +- Updated dependencies [6701c08] +- Updated dependencies [ca37605] +- Updated dependencies [cabdfe8] +- Updated dependencies [ab159a9] +- Updated dependencies [66a8822] +- Updated dependencies [739c972] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [63de248] +- Updated dependencies [8d7f906] +- Updated dependencies [c35fed1] +- Updated dependencies [a2437b5] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [e9731fc] +- Updated dependencies [a91ad42] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [1030a80] +- Updated dependencies [55a00ba] +- Updated dependencies [badf24d] +- Updated dependencies [474c030] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [70981e6] +- Updated dependencies [85c7374] +- Updated dependencies [dcb5fb3] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [42e1211] +- Updated dependencies [7193160] +- Updated dependencies [72d7db0] +- Updated dependencies [d0da880] +- Updated dependencies [7b2bb66] +- Updated dependencies [30dcd8a] +- Updated dependencies [c92b887] +- Updated dependencies [713beba] +- Updated dependencies [21510bb] +- Updated dependencies [d7c0389] +- Updated dependencies [6dd2ca5] +- Updated dependencies [917f024] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [a7293f7] +- Updated dependencies [59e6463] +- Updated dependencies [63a470a] +- Updated dependencies [4851668] + - @objectstack/objectql@18.4.0 + - @objectstack/service-analytics@18.4.0 + - @objectstack/service-push@18.4.0 + - @objectstack/service-messaging@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/runtime@18.4.0 + - @objectstack/plugin-approvals@18.4.0 + - @objectstack/plugin-sharing@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/verify@18.4.0 + - @objectstack/metadata-protocol@18.4.0 + - @objectstack/service-datasource@18.4.0 + - @objectstack/rest@18.4.0 + - @objectstack/plugin-email@18.4.0 + - @objectstack/plugin-auth@18.4.0 + - @objectstack/mcp@18.4.0 + - @objectstack/lint@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/service-queue@18.4.0 + - @objectstack/service-settings@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/trigger-api@18.4.0 + - @objectstack/plugin-audit@18.4.0 + - @objectstack/cloud-connection@18.4.0 + - @objectstack/plugin-security@18.4.0 + - create-objectstack@18.4.0 + - @objectstack/service-automation@18.4.0 + - @objectstack/client@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/service-storage@18.4.0 + - @objectstack/plugin-hono-server@18.4.0 + - @objectstack/plugin-webhooks@18.4.0 + - @objectstack/service-mailbox@18.4.0 + - @objectstack/service-realtime@18.4.0 + - @objectstack/driver-sql@18.4.0 + - @objectstack/driver-turso@18.4.0 + - @objectstack/driver-mongodb@18.4.0 + - @objectstack/plugin-pinyin-search@18.4.0 + - @objectstack/service-package@18.4.0 + - @objectstack/trigger-record-change@18.4.0 + - @objectstack/account@18.4.0 + - @objectstack/setup@18.4.0 + - @objectstack/driver-memory@18.4.0 + - @objectstack/driver-sqlite-wasm@18.4.0 + - @objectstack/metadata@18.4.0 + - @objectstack/observability@18.4.0 + - @objectstack/service-cache@18.4.0 + - @objectstack/service-job@18.4.0 + - @objectstack/service-sms@18.4.0 + - @objectstack/trigger-schedule@18.4.0 + - @objectstack/types@18.4.0 + - @objectstack/console@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index c7672bdcd98..c861b27a40a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cli", - "version": "18.3.0", + "version": "18.4.0", "description": "Command Line Interface for ObjectStack Protocol", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/client-react/CHANGELOG.md b/packages/client-react/CHANGELOG.md index 7a6a02e1881..f8a71cb3cee 100644 --- a/packages/client-react/CHANGELOG.md +++ b/packages/client-react/CHANGELOG.md @@ -1,5 +1,80 @@ # @objectstack/client-react +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/client@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/client-react/package.json b/packages/client-react/package.json index 6c0ab23e330..2173fb15a58 100644 --- a/packages/client-react/package.json +++ b/packages/client-react/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client-react", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "React hooks for ObjectStack Client SDK", "type": "commonjs", diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index bc1d9e013fc..a60724b117b 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -1,5 +1,97 @@ # @objectstack/client +## 18.4.0 + +### Patch Changes + +- 2046d21: feat(objectql,rest)!: a new assignment to a banned user is refused, and a banned user's records can be counted and handed over in bulk (#30205) + + Clause-②: yes (narrowing) — the published `FieldErrorCode` enum gains the member `reference_user_banned`. A non-system create or update that sets a user field (the owner field, a `user` field, or a `lookup` to `sys_user`) to a user whose ban is in force is refused `400 VALIDATION_FAILED` with that field code, a write that answered `201` / `200` before. A value the row already stores is not judged, so a record that already holds the banned user stays editable. The two new routes and SDK methods are additions. + Metadata-protocol: none + Skills-docs: no move in `skills/**`, which teaches no user-assignment verdict; `content/docs/permissions/administrator-guide.mdx` gains "Handing over a banned user's work" and its offboarding quick path points at it. + + + + **BREAKING** accept-set narrowing at every data write door (`POST` / `PATCH /api/v1/data/OBJECT`, batch, import, and a flow's elevated record write, which keeps the reference check): banning a user signs them out, but the records they own and are assigned stay where they were, and new work could still be handed to them. + + - **Before.** The Console's person picker hid a banned user, but `POST /api/v1/data/OBJECT` with `assignee` set to that user answered `201`, so new work landed on someone who cannot sign in. + - **Now.** The write-time reference check also asks whether a referenced user's ban is in force (`banned` is true and `ban_expires` is empty or still ahead) and refuses such a value with `reference_user_banned`, beside `reference_not_found`. It is read the way the existence probe reads, so a caller who cannot read the user object is refused the same way. A ban whose expiry has passed is not in force. + - **Unchanged.** A value every row of the write already stores (an edit form carries the whole record back), a system write the reference check already exempts (seed, install, restore), and a missing user (still `reference_not_found`). + - **New: the count.** `GET /api/v1/security/users/ID/assigned-work` (`client.security.assignedWork.count`) answers, per object and user field, how many records hold the user, counted under the caller's own context. Platform tables (`sys_*`), `managedBy` objects and `readonly` / `system` fields such as `created_by` are not counted; objects the caller cannot read are listed in `unreadable`; at most 200 fields are counted (`truncated`). + - **New: the hand-over.** `POST /api/v1/security/users/ID/assigned-work/transfer` (`client.security.assignedWork.transfer`) with `{ toUserId, objects? }` moves up to 500 records per call to an active user, each as an ordinary update under the caller's context, and answers per field what moved and what failed, plus how many remain. A banned, unknown or identical `toUserId` is refused before any write. Multi-valued user fields are counted but not transferred. Neither route touches the ban. + - **`@objectstack/spec`.** `FieldErrorCode` gains `reference_user_banned`, with its message in `en`, `zh-CN`, `ja-JP` and `es-ES`. + + **For a caller.** A write that names a banned user in a user field names an active one instead; the refusal names the field and the user. No stored metadata or row changes, so there is nothing for `objectstack migrate meta` to rewrite. An integration that reassigns work after a ban can call the transfer route and repeat it while `remaining` is above 0. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/client/package.json b/packages/client/package.json index f94205c79af..c20032a23bb 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Official Client SDK for ObjectStack Protocol", "type": "commonjs", diff --git a/packages/cloud-connection/CHANGELOG.md b/packages/cloud-connection/CHANGELOG.md index 4dbf34b4d91..4ca9ef3ff1e 100644 --- a/packages/cloud-connection/CHANGELOG.md +++ b/packages/cloud-connection/CHANGELOG.md @@ -1,5 +1,261 @@ # @objectstack/cloud-connection +## 18.4.0 + +### Minor Changes + +- bcdf8d3: feat(metadata-protocol,objectql): a package's action can be customized by a declared override in its paired `.custom` package (ADR-0146 D9 `action`), and an override that widens who may run the action is refused + + Clause-②: yes (widening) + Metadata-protocol: added + + - **A declared action override is admitted.** An `action` saved, published or installed in a base's paired `.custom` package with `overrides: { package: '', basedOn: '' }` was refused `403 NOT_OVERRIDABLE` ("action customization is not yet available"). It is now admitted by the save, publish and install doors, `os validate` and `os build`, like a flow override. It replaces the base's action whole, and every read of the base's action serves it under the base's name: the by-name and execution reads, the layered read's `effective` (with `code` still the base's), both lists, the object read's `actions` (what the list toolbar, row menu and record header are drawn from) and the engine's schema, which the REST `/actions` door and the MCP `run_action` bridge read the action from. Deleting the override serves the base's action again everywhere. The action activation switch keyed by the action's name turns the override off while it is armed, and the switch stays where it was when the base is restored. Only the pair declares: a dependent's declaration and the pair's undeclared same-named save stay refused, and a dependent's row is never served. + - **A script action's body is rebound.** The override's `body` runs in place of the base's handler after one resync (`kernel:ready`, `metadata:reloaded`, or the publish itself). Removing the override binds the base's handler again on the next resync, without re-registering the base. `@objectstack/objectql` adds `ObjectQL#registerActionOverride(objectName, actionName, handler, packageName)`, which binds a handler over the one registered under the key and keeps it beneath. `removeActionsByPackage(packageName)` now removes the package's entries from each key's stack, so the handler beneath is bound again. `registerAction` of another package's handler under a key an override holds takes the place beneath the override instead of replacing it: a marketplace-installed base binds its handlers in the install plugin's `kernel:ready` rehydrate, after the boot's resync bound the override, and after a restart the override still runs (and a restore runs the base's body). + - **An override may add a gate and never remove one (ADR-0066 D4).** The write gate refuses, with the new rule `declared_action_widens_access` (`403 NOT_OVERRIDABLE`), an action override that drops a capability the base's `requiredPermissions` names, sets `ai.exposed: true` on an action the base keeps off the agent surface, or drops the base's `ai.requiresConfirmation: true`. The refusal names each widened gate. Adding a capability or turning a gate on is admitted. Everything else (`label`, `type`, `target`, `body`, `params`, `visible` and the rest) is replaced whole, so an override can still change what runs, including a body that runs system-elevated, but only for people who could already run the action. `@objectstack/metadata-protocol` exports `actionOverrideWidening(baseItem, overrideItem)`, which returns the widened gates as clauses, and `OverrideWriteClosure.baseItemOf`, through which a door hands the judge the base's own item. `@objectstack/cli` (`os validate`) and `@objectstack/cloud-connection` (the managed install) pass it. + - The action switch route in the packaged-base refusal sentences is now spelled `POST /api/v1/actions/_activation/:object_or_global/:action`, which keeps the declared-save sentence within the REST door's 500-character message bound for long action names. + + Skills-docs: follow-up #30505 +- fc869c2: feat(spec)!: a seed is create-only by default — an omitted `mode` is `ignore`, so a restart, an install or an upgrade never writes a seeded value back over a customer's edit (#30081) + + Clause-②: yes (narrowing) — the declared default of `Seed.mode` (and of `SeedLoaderConfig.defaultMode`, now the same field) moves from `upsert` to `ignore`. The accept set is unchanged and every authored `mode` parses exactly as before; what narrows is what a replay does for a dataset that declares no mode: it no longer updates an existing row. + Metadata-protocol: semantic + + + + **BREAKING** for a seed dataset that declares no `mode` and relies on a replay to write its values back onto existing rows. + + - **Before.** A seed with no `mode` replayed as `upsert`. A seed loads again on every single-tenant boot (production included, under `os start` and `os serve`), on every package install or upgrade and on every publish, so each replay wrote the shipped value back over a customer's edit to a seeded row, with no warning. Measured on a production boot before the change (card comment 6633189577): an API edit to a seeded row went back to the shipped value on the next restart, after a boot-artifact upgrade, and after an `os package install` upgrade. + - **Now.** `SeedSchema.mode` defaults to `ignore`: a replay inserts a record whose row is missing and leaves an existing row exactly as it is. A row nobody edited is not rewritten either, so its `updated_at` does not move, and a value changed in a later seed version reaches only a row the load creates. Maintainer ruling 6631016988 (option A). + - **One default, stated once.** `SeedLoaderConfig.defaultMode` is `SeedSchema`'s own `mode` field, default included. The six replay sites (`@objectstack/runtime`: the inline boot seed, the per-organization replayer, the dev hot reload and the published-seed apply; `@objectstack/metadata-protocol`: the publish batch; `@objectstack/cloud-connection`: install, reseed and the rehydrate healer) stop restating `defaultMode: 'upsert'`, and the save door reads the schema's default for a seed item that declares no mode. + - **Compiled artifacts.** `os build` stores the parsed seed, so a package built before this change carries a written `mode: "upsert"` on every dataset. The loader cannot tell that value from an authored one, and nothing converts it: such a package keeps today's replay until it is rebuilt. + - **Installed packages.** A package installed with `os package install` (`@objectstack/cloud-connection`'s install-local) keeps the body it was installed with in its install ledger, and the sample-data reseed (`POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data`) replays that body's datasets in their stored mode. So an entry installed from a pre-change artifact keeps writing the shipped values back over edited rows on every reseed until the **rebuilt artifact is re-installed**; rebuilding alone changes nothing for it. An entry installed from a rebuilt artifact reseeds create-only: it puts back the rows that are missing and leaves an edited row as it is. The ledger's other replays never overwrite: the boot rehydrate seeds nothing, the heal seeds only when every seeded object is empty, and the per-organization replay runs only for a new organization. + - **Stored `seed` items.** A `seed` metadata item stored with no `mode` is not rewritten either. Its next publish parses it to `ignore`, so that publish inserts only the rows it is missing where it used to write its values back over existing rows, with no signal. + - **Unchanged.** An explicit `mode: 'upsert'` still writes every declared column back on each replay, and `columns: { FIELD: { applyOn: 'create' } }` still exempts the columns a customer or a job owns. `insert`, `update` and `replace` behave as before. + + **The remedy.** Rebuild every package so its compiled artifact carries the mode its source declares, and re-install the rebuilt artifact wherever the package was installed with `os package install`, so its install ledger carries that mode too. To put an installed package's sample data back to the shipped values, purge it (`…/purge-sample-data`) and then reseed. For a dataset that is reference data the package must keep current, declare `mode: 'upsert'`, and mark the columns the customer or a job owns `applyOn: 'create'`. Leave `mode` unset for business and starter data the customer owns once it is loaded. +- 8589756: feat(runtime,cloud-connection): the marketplace install, the sample-data reseed, the restart heal and the per-organization seed replay pick `Seed.locale` datasets by the organization's language + + Clause-②: yes (widening) + Metadata-protocol: semantic + + An app that ships one demo dataset per language market (`locale: ['zh-CN']` beside `locale: ['en-US']`) now gets the right one on every path that seeds an organization, not only on the boot path. Before, the install paths passed no locale, so a Chinese organization received the English rows and a page of refused-insert warnings for the Chinese twins (or both sets, where no unique key stopped the second), and one server's per-organization replay gave every new organization the boot app's `i18n.defaultLocale`. + + The locale is resolved for the organization the rows land in. The first answer wins: + + 1. the organization's `localization.locale` setting, through the settings service's own cascade (an env override, the organization's row, the deployment-wide `global` row). Only a value somebody set counts: the manifest's built-in `en-US` default (`source: 'default'`) is not the organization's choice; + 2. `sys_user.locale` of the person installing, reseeding or founding the organization; + 3. the app's declared `i18n.defaultLocale`. + + Nothing answers ⇒ no locale is sent, and every dataset loads as before (the loader still warns, naming each locale-scoped dataset it let through). A dataset without `locale` loads whatever the answer is. When no dataset in a load declares `locale`, neither the setting nor the user is read. + + - **The per-organization replay** resolves the locale per organization, at replay time, instead of once at boot. At founding the organization has no setting of its own yet, so its founder's language decides; an organization inserted with no person behind it takes the app's default. The replayer (`seed-replayer` service) now takes an optional second argument, `(organizationId, { userId })`; a caller passing only the organization id works as before. + - **`@objectstack/organizations`**: the org-scoping middleware hands the replayer the insert's attributed user (better-auth's create runs as system with the signed-in creator attributed) as the founder. + - **`@objectstack/cloud-connection`**: the install and the reseed pass the installing user and the package's own `i18n.defaultLocale`; the restart heal passes the package's default only (no person starts it); the replayer this plugin registers when the host has none passes the founder and no app default, because it replays every installed package at once. + - **`@objectstack/runtime`** exports the shared producer: `resolveSeedLocaleForLoad(host, { seeds, organizationId, userId, appDefaultLocale })`, `appDefaultSeedLocale(body)`, `seedsDeclareLocale(seeds)`, `describeSeedLocale(resolved)` and the `ResolvedSeedLocale` / `SeedLocaleQuestion` / `SeedLocaleSource` types. + - A settings or user read that throws is not skipped over: the seed reports the failure (the install's seed summary, the reseed's `RESEED_SKIPPED`, the replay's warning) and writes nothing, rather than seeding another language over a choice that could not be read. A host with no `settings` service skips step 1. + + Unchanged: the boot seed and the dev hot-reload seeder still filter on the app's `i18n.defaultLocale`; package apply and draft publish still pass no locale. +- a91ad42: fix(metadata-protocol,cli,cloud-connection)!: an object extension that re-declares an owner's field in `fields` beyond its display properties is refused at the save and publish doors, at `os validate` and at the install door, with a prescription naming `fieldPatches` (ADR-0146 D10) (#30610) + + Clause-②: no (narrowing) — the `object_extension` save, publish and restore doors, `os validate` and `POST /api/v1/marketplace/install-local` refuse an `objectExtensions[].fields` entry naming a field the extended object's owner declares when it changes anything outside the display-property whitelist, a body they accepted before. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: no move — `skills/**` and `content/docs/**` already teach `fieldPatches` as the way to change a base field's display, and no page teaches taking an owner field over through `fields`; the prescription ships in the refusal text. + + + + **BREAKING** accept-set narrowing at the metadata save and publish doors (`PUT /api/v1/meta/object_extension/NAME`, draft and live, the per-item and package publish, and the restore doors), at `os validate`, and at the marketplace install door. + + - **Before.** An extension written into `fields` with the name of a field the extended object's owner declares replaced the owner's field whole when the registry folded the object: a module package could turn the owner's `amount: number` into `text`, drop its `required`, or make it `readonly`, and every reader judged the field on the extension's definition. Only the registry's warning named it. + - **Now.** Such an entry is refused when it changes anything outside the display-property whitelist (`label`, `description`, `inlineHelpText`, `placeholder`, an option's `label`): `400 VALIDATION_ERROR` at the save and publish doors (located in `issues[]` at `fields.FIELD`), `422 VALIDATION_ERROR` at the install door (every entry in `details.refusals`), and exit 1 with the entry on `errors` at `os validate`. Each refusal names the keys the entry changes and prescribes `fieldPatches`. Both bodies are read through the field schema first, so a default the parse stamps is no change; options are compared by stored value, so a relabel or reorder is no change. + - **Unchanged.** A re-declaration that changes only display properties is admitted. An extension of an object its own package owns is not judged, nor is an added field. `os validate` judges an owner declared by another package of the same artifact; a target outside the artifact is judged at install, against the installed owner. The registry's warning on a re-declared field stays. `os build` does not yet run the check. + + **The remedy.** Move a display change of an owner's field into the extension's `fieldPatches` (the field name as its key, the whitelisted properties as its body), and add a behaviour of your own as a new field under a new name. + +### Patch Changes + +- afc761c: fix(cloud-connection,cli): an exported `.custom` installed on another environment lands in that environment's writable pair, and its overrides are served (ADR-0146 D2 / D3) + + Clause-②: no + Metadata-protocol: none + + - **Before.** `os package install /objectstack.json` of a base's paired customization package answered success and printed "Package installed into the running kernel", but the install registered the pair as code. None of its overrides was served, `GET /packages//customizations` listed none of them, and the pair turned read-only, so `ensure-customization` answered `writable: false` and every further save into it was refused `WRITABLE_PACKAGE_REQUIRED`. + - **Now.** `POST /api/v1/marketplace/install-local` installs a pair as what it is. The target's own pair is answered by its ensure door, which creates it when absent. Every item of the body is then written into it through the save door a Studio edit takes, with `mode: 'publish'` and the installer as actor, in load order. The overrides are served at once, the customization read lists them, and the pair stays editable. Nothing is registered as code or written to the cached-manifest directory. The answer carries `customizationPair` (`basePackageId`, `packageId`, `created`, `writable`), `writtenCount` and `written`, and the CLI prints them. + - **Refusals, nothing written.** A body key with no item form (code, a `translations` bundle, a key the install-key table does not classify), a nameless item, or two items claiming one name: `422`, every problem listed. A pair manifest the ensure door would not write: `400 PLUGIN_MANIFEST_INVALID`. A pair inside a multi-package artifact: `422`. A pair already read-only on the target: `409 RESOURCE_CONFLICT`, asked before the ensure door. If an earlier install registered it as code, the message names the uninstall and the restart that clear it. The ensure door's own refusal is relayed with its status and code. + - **A save refused part-way.** The answer is a failure with the save's code and status, naming the item and where it sits in the body. A pair this install created is deleted again. A pair that existed before is kept, and the answer lists the items written and the ones not written. + - **The CLI.** A runtime that predates this answers a pair as a managed install. The CLI now prints that as an error with the way back (uninstall, upgrade and restart, install again) and exits 1, instead of printing success. +- bc254be: fix(cloud-connection,cli): the install-local ledger follows `OS_HOME`, so `os dev --fresh` and a second instance under another `OS_HOME` start without an earlier run's installs + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Now.** With no `storageDir`, the install-local ledger (`LocalManifestSource`, and so `MarketplaceInstallLocalPlugin`) lives at `$OS_HOME/installed-packages` when `OS_HOME` is set, and at `CWD/.objectstack/installed-packages` when it is not. Before, it was always the working directory, so `os dev --fresh` (a fresh `OS_HOME` and database) rehydrated a package an earlier run installed, and so did a second instance under another `OS_HOME`. + - **Unchanged.** `os dev` without `OS_HOME` keeps the ledger in the project, beside its database. `os start` in project mode sets `OS_HOME` to `CWD/.objectstack`, which is the same directory as before. An explicit `storageDir` is used as given. + - **Existing installs.** When the ledger moved (an `OS_HOME` other than `CWD/.objectstack`) and its new directory does not exist yet, the boot copies the `.json` files of the working-directory ledger into it once, and logs a warning naming both directories. The old files are left in place and are not read again; remove them. A copy that fails is logged as an error, and the next boot tries again. Under `os dev --fresh` nothing is copied. + - **`os doctor`** reads the same directory the runtime does, `OS_HOME` included. + - **The widening.** `@objectstack/cloud-connection` exports `resolveInstalledPackagesDir({ env?, cwd? })` (returns `{ dir, legacyDir }`), `INSTALLED_PACKAGES_HOME_SUBDIR`, and `LocalManifestSource.legacyDir` / `adoptLegacyEntries()`. `os doctor` takes the ledger directory from `resolveInstalledPackagesDir`; a `@objectstack/cloud-connection` without it is reported as a reader that does not declare the ledger directory. Align the two packages' versions. +- 6e8ec73: feat(cloud-connection): `GET /api/v1/marketplace/install-local/:manifestId/sample-data` answers how many of an installed app's sample records a purge would delete now + + Clause-②: yes (widening) + Metadata-protocol: none + + - **New read-only route.** `GET /api/v1/marketplace/install-local/:manifestId/sample-data` answers `{ success: true, data: { manifestId, count, unidentified } }`. `count` is the number of rows `POST …/purge-sample-data` would delete if it ran now, found by the purge's own matcher with nothing deleted. A record a user added is not counted, and neither is a sample record whose key field (its dataset's `externalId`) was edited, because the purge deletes neither. A sample record edited only in other fields is still a sample row: the purge deletes it and the count includes it. `unidentified` counts the sample records the purge would report in `errors` and leave in place (no usable key value, or a key two rows carry). Before this, the count was known only from the install answer's `data.seeded`, so after a reload no API could say how much sample data was left; the installed-apps listing's `withSampleData` says only whether any is left. + - **Same rules as the purge.** It requires `manage_metadata`, answers `404` for a package that is not installed and `422 OS_PROTOCOL_INCOMPATIBLE` for an entry this runtime refused to load, and counts in the purge's scope (the caller's active organization under an organization wall, every row without one). A walled session with no active organization gets `count: 0`, the same answer as the listing's `withSampleData: false` for that session. A package with no seed datasets gets `count: 0`. With no data engine or metadata service it answers `500 DRIVER_UNAVAILABLE`, never `0`. + - **It cannot delete.** It is a `GET` and accepts no query parameter: any parameter (`dryRun`, `confirm`, …) is refused with `400 VALIDATION_ERROR` before any row is read. + - **Unchanged.** What the purge deletes, its permission and its answer; the listing's `withSampleData`. + - `@objectstack/cloud-connection` now depends on `@objectstack/rest`, for the shared closed-query-set refusal message. +- 60fe44f: feat(cloud-connection,metadata-core): an install the protocol handshake admitted without checking says so in its answer, not only in the server log + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/cloud-connection`: `POST /api/v1/marketplace/install-local` answers `data.protocolCheckSkipped`** when the ADR-0087 handshake admitted the package without checking it: `{ reason, packageId, protocolVersion, message }`, plus `requiredRange` and `rangeSource` for `reason: 'unparsed-range'`. `reason` is the handshake's own verdict: `no-range` (the package declares no `engines.protocol`) or `unparsed-range` (it declares a range the handshake cannot read). `protocolVersion` is the protocol the package was installed under unchecked, and `message` says so in plain words for the person who pressed Install. It is on all three success answers (managed, `unmanaged`, and a paired customization package), and omitted, not nulled, when the range was checked, so that answer's key set is unchanged. The `warn` log line stays as it was. + - **`@objectstack/metadata-core`: `assertProtocolCompat` returns the verdict it admitted the package under** (new exported type `ProtocolCompatAdmitted`; it returned `void`), and the `no-range` and `unparsed-range` verdicts of `checkProtocolCompat` carry `protocolVersion`, the version they were judged against. A door that answers a caller reads the skipped check from the handshake itself instead of judging the range a second time. +- 7d11067: Clause-②: no + Metadata-protocol: none + + The one-time copy of the working-directory install ledger into its `OS_HOME` location (#30217) now creates its staging directory with an exclusive `mkdirSync` of a randomly named sibling instead of `mkdtempSync`. Behaviour is unchanged: the copy still lands in a unique sibling of the ledger directory, is renamed into place, and is removed if the copy fails. This repairs the red `main` build (#30467), where the repository's in-tree scratch scan could not read the `mkdtempSync` base. +- 63de248: fix(objectql, metadata-protocol, cloud-connection)!: a declared `.custom` action override is keyed and judged the same way at every door, and only its declared base's handler slots beneath it + + Clause-②: yes (narrowing) — the protocol door (save and publish) now refuses a declared action override whose `objectName` is not the base action's, with `403 NOT_OVERRIDABLE` and rule `declared_package_ships_no_such_item`, the refusal `os validate` and the managed install already gave it; the save door used to admit it. The managed install now admits a re-install of the pair whose override relaxes an earlier tightening back to the base's own gates, which it used to refuse with `declared_action_widens_access` while the save door admitted it. Every other input answers as before. + Metadata-protocol: none + Skills-docs: follow-up #30505 — the declared action override's docs card; it owes one sentence that an override keeps the base action's `objectName`. + + + + **BREAKING** accept-set narrowing at the protocol door (`@objectstack/metadata-protocol`, ADR-0146 D2/D9): a declared action override names its base's action by `:`, the identity `os validate` and the managed install key it on. + + - **Before.** The save and publish doors matched a declared action override's base by name alone, so an override in the base's `.custom` pair could move the base's action to another object. Its handler was not rebound there, and `os validate` and the install door refused the same body. + - **Now.** The save door refuses it as the other two doors do: the base ships no action of that name on that object. Keep the base action's `objectName` in the override, or remove `overrides` and add the action under the pair's own name. + + `@objectstack/cloud-connection`: the managed install judges a declared action override against the installed base's code artifact (`SchemaRegistry#getArtifactItem`), as the save door does. The protocol's per-package listing it read before serves an armed override under the base's envelope, so a re-install of the pair was judged against its own previous version. + + `@objectstack/objectql`: `ObjectQL#registerActionOverride` takes the package the override declares (`overrides.package`) as an optional fifth argument, and the authored-action resync passes it. `registerAction` under a key such an override holds slots the handler beneath it only when the registry's code artifact of the action belongs to that package, on that object; any other registration, one with no package included, replaces the override and is warned like every other handler collision. Before, any other package's registration slotted beneath it, so a row of another base's pair bound before the real base registered kept running after the real base's handler arrived, while every read served the real base's action; and a registration with no package slotted beneath it at `debug`, where the method's docblock promises a warning. +- 1030a80: fix(metadata-protocol, cli, cloud-connection): a declared action override moved to another object is told which object the base ships the action on + + Clause-②: no + Metadata-protocol: none + + The `declared_package_ships_no_such_item` refusal (`403 NOT_OVERRIDABLE`) for a `.custom` action override whose `objectName` differs from the base action's used to say the base "ships no action named 'X'" and to "correct the name", though the name was right. It now names both objects and tells the author to keep the base action's `objectName`, at all three doors (the save and publish door, `os validate`, the managed install), from one sentence builder in `judgeOverrideWrite`. A name the base does not ship at all reads as before. The rule, code and status are unchanged. +- Updated dependencies [31fe289] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [75bbebb] +- Updated dependencies [88cd0ab] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [665db00] +- Updated dependencies [e3b56b2] +- Updated dependencies [18112bf] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [cd5f554] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [07685ae] +- Updated dependencies [eee4149] +- Updated dependencies [63b86b3] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [343ddb2] +- Updated dependencies [0c666c3] +- Updated dependencies [f672e0d] +- Updated dependencies [38717d3] +- Updated dependencies [bfbdeec] +- Updated dependencies [43faa22] +- Updated dependencies [4adc890] +- Updated dependencies [fdad178] +- Updated dependencies [15f808a] +- Updated dependencies [bcdf8d3] +- Updated dependencies [59bcd22] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [0837fdc] +- Updated dependencies [4be9734] +- Updated dependencies [1a3ea0c] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [a26abbc] +- Updated dependencies [8eca18f] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [a545a04] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [8fad7b1] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [8589756] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [8cc709d] +- Updated dependencies [8fa738f] +- Updated dependencies [60fe44f] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [7b80724] +- Updated dependencies [fb1ab89] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [4bf6e8f] +- Updated dependencies [739c972] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [63de248] +- Updated dependencies [a2437b5] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [a91ad42] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [1030a80] +- Updated dependencies [55a00ba] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7193160] +- Updated dependencies [30dcd8a] +- Updated dependencies [713beba] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/objectql@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/runtime@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/metadata-protocol@18.4.0 + - @objectstack/rest@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/observability@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/cloud-connection/package.json b/packages/cloud-connection/package.json index 017084a8205..897ea372220 100644 --- a/packages/cloud-connection/package.json +++ b/packages/cloud-connection/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cloud-connection", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Runtime-side client for an ObjectStack cloud control plane — marketplace browse proxy, install-local, device-code binding, org catalog and installed views, and the /api/v1/runtime/config discovery endpoint. Open mechanism (cloud ADR-0008): the hub service, plan policy, and entitlements stay server-side.", "type": "module", diff --git a/packages/connectors/connector-imap/CHANGELOG.md b/packages/connectors/connector-imap/CHANGELOG.md index f9e76e626ec..e6549a12663 100644 --- a/packages/connectors/connector-imap/CHANGELOG.md +++ b/packages/connectors/connector-imap/CHANGELOG.md @@ -1,5 +1,9 @@ # @objectstack/connector-imap +## 18.4.0 + +No changes in this release. + ## 18.3.0 No changes in this release. diff --git a/packages/connectors/connector-imap/package.json b/packages/connectors/connector-imap/package.json index c196533b55e..097e28997a6 100644 --- a/packages/connectors/connector-imap/package.json +++ b/packages/connectors/connector-imap/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-imap", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "IMAP connector for ObjectStack — a read-only IMAP4rev1 session (EXAMINE, UID FETCH with BODY.PEEK) and the per-user-shaped mail account and credential abstraction the mailbox sync connects through.", "type": "commonjs", diff --git a/packages/connectors/connector-mcp/CHANGELOG.md b/packages/connectors/connector-mcp/CHANGELOG.md index 04a8600ccf1..f4f1f81f718 100644 --- a/packages/connectors/connector-mcp/CHANGELOG.md +++ b/packages/connectors/connector-mcp/CHANGELOG.md @@ -1,5 +1,80 @@ # @objectstack/connector-mcp +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/connectors/connector-mcp/package.json b/packages/connectors/connector-mcp/package.json index db7e951a8ed..21424f75b53 100644 --- a/packages/connectors/connector-mcp/package.json +++ b/packages/connectors/connector-mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-mcp", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Model Context Protocol (MCP) connector for ObjectStack — a generic adapter that turns any MCP server's tools into a connector's actions on the automation engine's connector registry (ADR-0024).", "type": "commonjs", diff --git a/packages/connectors/connector-openapi/CHANGELOG.md b/packages/connectors/connector-openapi/CHANGELOG.md index 24d2bcf0081..58f72d827f5 100644 --- a/packages/connectors/connector-openapi/CHANGELOG.md +++ b/packages/connectors/connector-openapi/CHANGELOG.md @@ -1,5 +1,80 @@ # @objectstack/connector-openapi +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/connectors/connector-openapi/package.json b/packages/connectors/connector-openapi/package.json index 220c0cd0f1b..a6a18800861 100644 --- a/packages/connectors/connector-openapi/package.json +++ b/packages/connectors/connector-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-openapi", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "OpenAPI 3.x connector generator for ObjectStack — turns a declarative OpenAPI document into connector actions on the automation engine's registry, with a self-contained static-auth HTTP transport (ADR-0023).", "type": "commonjs", diff --git a/packages/connectors/connector-rest/CHANGELOG.md b/packages/connectors/connector-rest/CHANGELOG.md index c254670a065..c48445cef85 100644 --- a/packages/connectors/connector-rest/CHANGELOG.md +++ b/packages/connectors/connector-rest/CHANGELOG.md @@ -1,5 +1,80 @@ # @objectstack/connector-rest +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/connectors/connector-rest/package.json b/packages/connectors/connector-rest/package.json index 3a248167c7f..a2276af6474 100644 --- a/packages/connectors/connector-rest/package.json +++ b/packages/connectors/connector-rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-rest", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Generic REST connector for ObjectStack — the reference concrete connector that registers a `request` action on the automation engine's connector registry (ADR-0018 §Addendum).", "type": "commonjs", diff --git a/packages/connectors/connector-slack/CHANGELOG.md b/packages/connectors/connector-slack/CHANGELOG.md index f815b45d8f3..ce3c66457a7 100644 --- a/packages/connectors/connector-slack/CHANGELOG.md +++ b/packages/connectors/connector-slack/CHANGELOG.md @@ -1,5 +1,79 @@ # @objectstack/connector-slack +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/connectors/connector-slack/package.json b/packages/connectors/connector-slack/package.json index 446b73d7d93..53dbc9d932c 100644 --- a/packages/connectors/connector-slack/package.json +++ b/packages/connectors/connector-slack/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-slack", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Slack Web API connector for ObjectStack — registers `chat.postMessage` / `chat.update` / `call` actions on the automation engine's connector registry (ADR-0018 §Addendum, ADR-0022).", "type": "commonjs", diff --git a/packages/console/CHANGELOG.md b/packages/console/CHANGELOG.md index 98b05c92792..c79644c8142 100644 --- a/packages/console/CHANGELOG.md +++ b/packages/console/CHANGELOG.md @@ -1,5 +1,9 @@ # @objectstack/console +## 18.4.0 + +No changes in this release. + ## 18.3.0 No changes in this release. diff --git a/packages/console/package.json b/packages/console/package.json index 531e38480e1..df9738adc5a 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/console", - "version": "18.3.0", + "version": "18.4.0", "description": "Prebuilt Console SPA built from this workspace's ui/apps/console (@object-ui/console) at the same version, installed as a dependency of @objectstack/cli.", "license": "Apache-2.0", "homepage": "https://github.com/objectstack-ai/objectstack/tree/main/packages/console", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index aec2cd718c7..acfa48c60bc 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,237 @@ # @objectstack/core +## 18.4.0 + +### Minor Changes + +- 9ebca94: feat(push): `requires: ['push']` and the stack's `push` key turn mobile push on under `objectstack serve`, and a relay configuration is refused at boot + + Clause-②: yes (widening) + Metadata-protocol: added + Skills-docs: follow-up owed — #29969 (skills/objectstack-platform/evals/config-plugins-ops.json: no eval asks for `requires: ['push']` or the `push` key); this diff teaches both in `skills/objectstack-platform/SKILL.md` (the top-level key enumeration and the `requires` table) and in the new `content/docs/kernel/runtime-services/push-service.mdx` + + A deployment that runs `objectstack serve` from a stack file can now turn on mobile push without host code. Before this, the only way to register a push service was a host that composed `PushServicePlugin` in code, so a `notify` node naming `push` had nothing to deliver through on a served stack. + + - **`@objectstack/spec`: the `push` capability token and the `push` stack key.** `push` joins `PLATFORM_CAPABILITY_TOKENS`, provided by `@objectstack/service-push` in the open edition. It is requested, never on the always-on slate. The stack definition gains a top-level `push` key beside `email` and `sms`: it is `PushServiceConfigSchema`, composed as a single value (`COMPOSE_KEY_DISPOSITIONS`). Authoring accepts a direct block, a relay block and absence; it refuses an inline secret, by key name (`key`, `privateKey`, `serviceAccount`, …) with `credentialRef` as the remedy, or pasted into a `credentialRef` (ADR-0143 D3). `PushServiceConfigSchema`'s docblock now names the stack key and `objectstack serve` as its reader; `PushRelayTransportConfigSchema` keeps its `[PLANNED — not yet served]` marker. + - **`@objectstack/core`: the `push` provider row and its argument.** `CAPABILITY_PROVIDERS.push` names `PushServicePlugin`, and `resolveCapabilityArgument('push', …)` builds its argument from the stack's `push` key through `resolvePushCapabilityArg`, read off the provider module as the `email` and `sms` readers are. Both `objectstack serve` and `@objectstack/verify`'s `bootStack` read this table, so an app's tests get the push service its server gets. + - **`@objectstack/service-push`: `resolvePushCapabilityArg`.** It parses the `push` key with `PushServiceConfigSchema` and refuses, before the kernel boots, two configurations: the relay transport (an absent key and `push: {}` read as it), whose sender has not shipped, with a message that says so and prescribes `transport: 'direct'` with an `apns` and/or `fcm` block; and anything the schema refuses, an inline secret first among them, naming each path and the schema's guidance and never the value. On a stack that declares `requires: ['push']` either refusal fails the boot as `Capability "push" (@objectstack/service-push) failed to start: …`. + - **`@objectstack/cli` and `@objectstack/verify`** depend on `@objectstack/service-push`, so the provider the token names resolves out of the box, as the open edition promises. + + To turn push on, add `requires: ['push']` and a direct block naming each vendor's secret by reference: + + ```ts + push: { + transport: 'direct', + apns: { teamId: 'ABCDE12345', keyId: 'KEY1234567', topic: 'com.example.app', environment: 'production', credentialRef: 'APNS_SIGNING_KEY' }, + fcm: { projectId: 'example-app', credentialRef: 'FCM_SERVICE_ACCOUNT' }, + }, + ``` + + In the open tier each `credentialRef` is the name of an environment variable holding the secret. A `push` key on a stack whose `requires` does not name `push` is not read, and `objectstack serve` logs one boot warning saying so. The runtime-services chapter has a new `services.push` page. +- aa57c76: feat(spec): three forward date-range presets: `next_month`, `next_quarter` and `next_30_days` + + Clause-②: yes (widening) + Metadata-protocol: added + + The relative dates a dashboard date filter and the filter builder offer looked only backward. A forecast asks forward: what closes next month, next quarter, in the next 30 days. Those questions had to use a custom range, whose fixed dates go stale. + + `DATE_RANGE_PRESETS` now also declares `next_month` (the whole of next month), `next_quarter` (the whole of next quarter) and `next_30_days` (today and the 30 days after it, the mirror of `last_30_days`). Every place that accepts a preset accepts them: `dashboard.dateRange.defaultRange`, a date global filter's `defaultValue`, the filter builder's `between` row and `AnalyticsQuery.timeDimensions[].dateRange`. They sit beside the family they extend in the filter bar: `next_month` after `last_month`, `next_quarter` after `last_quarter`, `next_30_days` after `last_90_days`. Their `DATE_RANGE_PRESET_MACRO_WINDOWS` rows follow the inclusive last-day convention: `['{next_month_start}', '{next_month_end}']`, `['{next_quarter_start}', '{next_quarter_end}']` and `['{today}', '{30_days_from_now}']`. A preset name written as a bare filter comparand is refused with that window as the prescription, as for the other presets. + + `@objectstack/core`'s `resolveAnalyticsDateRangePreset` resolves the three in its half-open form. `next_30_days` stops before `{31_days_from_now}`. `next_month` and `next_quarter` stop before the month and the quarter after next, which no macro token names: the resolver reads `next_month_start` (or `next_quarter_start`) again from the start of the day it named, so the end is still a day the one token resolver computed. No macro token is added. + + In the console, the three presets have labels in all ten locale packs, and the client date-macro resolver now answers `{next_month_end}` and `{next_quarter_end}`, two spec tokens it did not resolve before. `@objectstack/verify`'s dashboard-window proof also writes fixture rows on a forward preset's future edges, so it judges the three new windows; the days it writes for the other presets are unchanged. `@objectstack/lint` changes documentation only. +- af8aab1: fix(spec,core): the server locale resolvers read the platform locale table, so a `zh-TW` / `zh-HK` / `zh-MO` reader gets a registered `zh-Hant` bundle instead of Simplified Chinese + + Clause-②: yes (widening) + Metadata-protocol: semantic + + `resolveBundleLocale` (`@objectstack/spec/system`) and `resolveLocale` (`@objectstack/core`) now run one ladder, the new `resolveLocaleAmong(requested, available)` export of `@objectstack/spec/system`: exact, case-insensitive, then **the same platform locale table language**, then base language, then the first registered variant. The new step maps the requested code and each available key through `platformLocaleTagOf` (the table's read aliases) and picks the key spelled as the table tag, else the first key the table maps to that tag. + + What changes for a reader: a request for `zh-TW`, `zh-HK` or `zh-MO` reaches a registered `zh-Hant` key, and `zh`, `zh-CN`, `zh-SG` or `zh-Hans` reaches `zh-CN` (or `zh-Hans`), whatever order the bundle registered them in. Before, the first registered `zh-*` key answered both, so a Traditional reader got the Simplified bundle (and with `zh-Hant` registered first, a Simplified reader got the Traditional one). A bare `zh` key now counts as Simplified for a `zh-TW` request, which the base-language step used to hand it. The step also applies to `ja` → `ja-JP`, `es` → `es-ES`, `en` → `en-US` and `pt` → `pt-BR`, which answer as before against today's alias-spelled bundles. + + `resolveOfferedLocale` (`@objectstack/core`, the matcher Setup's default-language options and the IdP locale claim use) now answers through the same ladder, so a Traditional request reaches an offered `zh-Hant`. Where a list offers two codes of one table language, the code spelled as the table tag wins over the first one offered: `zh` over `['zh-CN', 'zh-Hans']` answers `zh-Hans`, and `zh-HK` over `['zh-TW', 'zh-Hant']` answers `zh-Hant`. + + Unchanged: a code the table does not know (`es-MX`, `en-GB`, `pt-PT`) falls back through base language and variant expansion exactly as before, and a `zh-TW` request still reaches `zh-CN` when no Traditional key is registered. No call that matched before stops matching. Every caller of these functions moves with them, among them the i18n REST door, built-in validation and operation messages, email template locale folding, hook, flow and approval copy, and plugin-auth's refusal copy. +- 21510bb: feat(plugin-security)!: under a walled tenancy posture every external object states its organization scope + + Clause-②: no (narrowing) — under the `isolated` and `group` postures an external object (an `external` block) that states no organization scope is refused by name when the boot completes, and one registered after the boot answers no row and accepts no write; nothing is widened, and the `single` posture is unchanged. + Metadata-protocol: narrowed + + + + **BREAKING** for a deployment running the `isolated` or `group` tenancy posture (`OS_TENANCY_POSTURE`) with an external object that states nothing about its organization scope. + + - **The rule (ADR-0015 §10 as amended, ADR-0095 D1).** The platform provisions no storage for an external object, so the `organization_id` it injects into the registered schema is a column the remote may not carry. Whose rows an external object serves — one organization's or every organization's — is a fact only its author knows, and under a walled posture the object states it, in one of three ways: its own `organization_id` field (the remote rows carry the organization column and the driver returns it; a remote column of another name is mapped onto it with `external.columnMap: { : 'organization_id' }`); `tenancy: { enabled: false }` (shared by every organization); or `systemFields: { tenant: false }` (the same opt-out on the system fields). These are the wall's own two opt-out clauses and the one column it reads; the whole-block `systemFields: false` states nothing. + - **What the rule does.** `SecurityPlugin` refuses the boot from its `kernel:ready` handler, beside the ADR-0131 D5 refusal, naming every external object that states nothing and the three ways out (`ExternalObjectOrganizationScopeUndeclaredError`). An external object registered after the boot — a Studio import, a runtime datasource — that states nothing answers no row and accepts no write: the Layer 0 verdict is `deny` (`computeTenantLayer0Verdict`), the write wall refuses every insert, update and delete with a sentence naming the undeclared scope and the three ways out, and the plugin says so once in a warning. An object declaring its own `organization_id` is walled: a member of organization A reads only A's rows, through a driver that honours `where` and through one that ignores it (the engine's floor re-applies the predicate), and a row missing the column reads to no one. An object declaring `tenancy: { enabled: false }` or `systemFields: { tenant: false }` is shared. + - **`tenancy.tenantField` on an external object is closed the same way.** The organization wall keys on `organization_id` alone, and a declared tenant field is read only by storage the platform provisions, which an external object has none of; so on an external object a `tenantField` naming another column is refused at boot and denied after it, with the remedy spelled out (`external.columnMap` onto `organization_id`, or `tenancy: { enabled: false }`). Local objects are unchanged. + - **Who moves.** Every walled deployment with an external object stating nothing: declare one of the three and restart. The showcase's two federated analytics objects now declare `tenancy: { enabled: false }`. A `single`-posture deployment owes no declaration and behaves exactly as before. + - **`@objectstack/core`.** `TenantLayer0Input` gains the optional `externalOrganizationScopeUndeclared`; set, the verdict is `deny` under a walled posture, ahead of the platform-admin crossing. Omitted, every existing caller reads as before. + - **`@objectstack/spec`.** The ADR-0087 D3 entry `external-object-organization-scope-declared` tells an upgrading operator what to declare. + + Skills-docs: follow-up #29866 — `content/docs/data-modeling/virtual-objects.mdx` (a multi-organization section), `skills/objectstack-data/rules/datasources.md` (the three declarations), `docs/adr/0015-external-datasource-federation.md` (the §10 and D7 amendment); the sibling docs PR on `claude/sec-hardening-g-docs`, governed (Tier H), landed by the maintainer's hand. + +### Patch Changes + +- 771b2db: fix(core,service-messaging,service-queue): an idle dispatch loop wakes at the next due time, and a sole writer's loop no longer re-runs the idle ramp after every boot + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/core`: `DispatchLoop` wakes at the next due time.** A new optional `DispatchLoopOptions.nextDueAt()` answers the earliest instant work becomes claimable (a deferred row, a claim's expiry). The loop asks it once per idle streak, after the first empty tick, and never sleeps past the answer, so a deferred row goes out within one tick of coming due whatever `maxIdleIntervalMs` is. The answer is remembered rather than re-read on every ceiling tick, so an idle loop's steady-state cost is unchanged; the new `DispatchLoop.wakeAt(dueAtMs)` lets a producer in the same process announce a deferred row at no read. An answer at or before the tick that found nothing is not chased, and a rejected tick or read still counts as idle, so neither can make the loop spin. + - **`@objectstack/core`: `DispatchLoopOptions.soleWriter`** (default `false`). On a host where no other process writes the store, an empty tick sleeps straight to the next due time or the ceiling instead of re-running the ramp from `intervalMs`, which only ever bought latency on rows another process wrote. A fresh sole-writer loop over an empty store now ticks once in its first minute; before, every boot re-ran the ramp (six ticks in the first minute at the dispatchers' 500 ms base, five at the queue's 1 s). + - **`@objectstack/service-messaging`:** `INotificationOutbox.nextDueAt` / `IHttpOutbox.nextDueAt` (optional; both built-in stores implement it: two reads, the earliest future `next_attempt_at` / `next_retry_at` and the oldest `in_flight` claim plus the claim TTL), wired into `NotificationDispatcher` and `HttpDispatcher`; `soleWriter` on both dispatchers' options, and `dispatchSoleWriter` on `MessagingServicePlugin`. Retries, quiet hours, digest windows and a crashed node's claims are taken at their due time instead of up to `dispatchMaxIdleIntervalMs` late. + - **`@objectstack/service-queue`:** `DbQueueAdapter` reads the earliest future `scheduled_for` of its subscribed queues (one read per idle streak), a deferred `publish()` announces its due time to the loop through `wakeAt` instead of waiting out the backoff, and `DbQueueAdapterOptions.soleWriter` (reachable as `QueueServicePlugin` `db.soleWriter`) skips the ramp. + + Nothing changes for a host that sets neither `soleWriter` flag except the due-time wake: the ramp keeps its shape, cut short only when a due row is earlier, and an idle stretch costs one extra read at its start. +- f19fbb8: fix(core): a hook that aborts with free text answers `HOOK_ABORTED` on the import row too, not `IMPORT_ROW_FAILED` + + Clause-②: no + Metadata-protocol: none + + - **Before.** A sandboxed hook body that aborts with free text (`throw new Error('…')`, no `code`) answers `/api/v1/data/:object`, `/createMany`, `/api/v1/batch` and every `POST /api/v1/data/:object/batch` row with `HOOK_ABORTED` and the hook's sentence. `POST /api/v1/data/:object/import` and the async import job answered the same abort as a row `{ ok: false, code: 'IMPORT_ROW_FAILED', error: }`, so an import client branching on the row's `code` could not tell a business rule's refusal from any other failed row. + - **Now.** The import row answers `HOOK_ABORTED` (the `UNDECLARED_HOOK_REFUSAL_CODE` the other doors read), judged by `resolveThrownHttpError`, the resolver that declares that code. The value moves, from `IMPORT_ROW_FAILED` to `HOOK_ABORTED`, on every row the core import runner builds from a write failure: the synchronous import, the async import job's stored results, the create and update halves of an upsert, and every write path (single-row, batched, partial-success). + - **Unchanged.** A code the hook declared still wins, registered or not. The row's `error` is still the hook's sentence. A body that crashed, a refusal that declared a 5xx status, a driver fault and every other row failure keep the code they had. The row's `code` is an open string in `ImportRowResultSchema`, so no schema moves, and no error code is added: `HOOK_ABORTED` is the one the ledger already registers. +- 2046d21: feat(objectql,rest)!: a new assignment to a banned user is refused, and a banned user's records can be counted and handed over in bulk (#30205) + + Clause-②: yes (narrowing) — the published `FieldErrorCode` enum gains the member `reference_user_banned`. A non-system create or update that sets a user field (the owner field, a `user` field, or a `lookup` to `sys_user`) to a user whose ban is in force is refused `400 VALIDATION_FAILED` with that field code, a write that answered `201` / `200` before. A value the row already stores is not judged, so a record that already holds the banned user stays editable. The two new routes and SDK methods are additions. + Metadata-protocol: none + Skills-docs: no move in `skills/**`, which teaches no user-assignment verdict; `content/docs/permissions/administrator-guide.mdx` gains "Handing over a banned user's work" and its offboarding quick path points at it. + + + + **BREAKING** accept-set narrowing at every data write door (`POST` / `PATCH /api/v1/data/OBJECT`, batch, import, and a flow's elevated record write, which keeps the reference check): banning a user signs them out, but the records they own and are assigned stay where they were, and new work could still be handed to them. + + - **Before.** The Console's person picker hid a banned user, but `POST /api/v1/data/OBJECT` with `assignee` set to that user answered `201`, so new work landed on someone who cannot sign in. + - **Now.** The write-time reference check also asks whether a referenced user's ban is in force (`banned` is true and `ban_expires` is empty or still ahead) and refuses such a value with `reference_user_banned`, beside `reference_not_found`. It is read the way the existence probe reads, so a caller who cannot read the user object is refused the same way. A ban whose expiry has passed is not in force. + - **Unchanged.** A value every row of the write already stores (an edit form carries the whole record back), a system write the reference check already exempts (seed, install, restore), and a missing user (still `reference_not_found`). + - **New: the count.** `GET /api/v1/security/users/ID/assigned-work` (`client.security.assignedWork.count`) answers, per object and user field, how many records hold the user, counted under the caller's own context. Platform tables (`sys_*`), `managedBy` objects and `readonly` / `system` fields such as `created_by` are not counted; objects the caller cannot read are listed in `unreadable`; at most 200 fields are counted (`truncated`). + - **New: the hand-over.** `POST /api/v1/security/users/ID/assigned-work/transfer` (`client.security.assignedWork.transfer`) with `{ toUserId, objects? }` moves up to 500 records per call to an active user, each as an ordinary update under the caller's context, and answers per field what moved and what failed, plus how many remain. A banned, unknown or identical `toUserId` is refused before any write. Multi-valued user fields are counted but not transferred. Neither route touches the ban. + - **`@objectstack/spec`.** `FieldErrorCode` gains `reference_user_banned`, with its message in `en`, `zh-CN`, `ja-JP` and `es-ES`. + + **For a caller.** A write that names a banned user in a user field names an active one instead; the refusal names the field and the user. No stored metadata or row changes, so there is nothing for `objectstack migrate meta` to rewrite. An integration that reassigns work after a ban can call the transfer route and repeat it while `remaining` is above 0. +- f7afab8: fix(core,objectql): effective object permissions read the external write gate, so a read-only external object is no longer offered New, Import, Edit or Delete + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Behaviour.** On an object whose external write gate is closed (ADR-0015 §5.3 Gate 3: a federated datasource, and not both `datasource.external.allowWrites` and `object.external.writable`), every insert, update and delete is refused `403 EXTERNAL_WRITE_FORBIDDEN` for every caller. The effective object-permission map now says so: the entry grants no `allowCreate`, `allowEdit`, `allowDelete` or `allowTransfer`, a `modifyAllRecords` on it is lowered to the `viewAllRecords` read it implies, and `apiOperations` carries no write operation (`create`, `update`, `delete`, `upsert`, `bulk`, `import`, `restore`, `purge`). Read and export are unchanged. Both consumers move together: `/auth/me/permissions` (the console hides those doors with no client change) and `ISecurityService.getEffectiveObjectPermissions`, so `current_user.can(object, 'create' | 'edit' | 'delete' | 'transfer')` now answers `false` on such an object, as the write itself always did. + - **`@objectstack/objectql`: `ObjectQL.isExternalWriteForbidden(objectName)`**, the gate's verdict as a read-only question. The write gate (`assertWriteAllowed`) reads the same verdict, so the two cannot disagree. + - **`@objectstack/core`: `engineExternalWriteForbidden(engine)`** (the accessor over an engine, asked when the member is present) and the optional `isExternalWriteForbidden` accessor on `EffectiveObjectPermissionsSchemaSource`; `annotateEffectiveApiOperations` takes the same predicate as an optional third argument. A caller that hands over neither gets the map it got before. +- d5f3bc5: fix(core): effective object permissions on a guarded managed object grant no edit, delete or transfer the write guard refuses, `modifyAllRecords` included + + Clause-②: yes (widening) + Metadata-protocol: none + + The managed write clamp behind `/auth/me/permissions` and `ISecurityService.getEffectiveObjectPermissions` cleared `allowEdit`, `allowCreate` and `allowDelete` on an object in a guarded `managedBy` bucket (`better-auth`, `engine-owned`, `append-only`) that does not opt the verb in through `userActions`, but left two grants the write guard refuses: + + - **`modifyAllRecords`**, which grants edit, delete and transfer on its own. A permission set naming such an object with `{ modifyAllRecords: true }` made `current_user.can(object, 'edit' | 'delete' | 'transfer')` answer `true` while every one of those writes was refused. Where the guard refuses `edit` or `delete`, it is now lowered to `viewAllRecords` (the read it implies, so read is unchanged), and each verb the object still opts in is kept as its plain bit: an object opting `edit` in (such as `sys_user`) keeps edit and transfer and loses delete; an object opting nothing in keeps read only. An object opting both `edit` and `delete` in keeps `modifyAllRecords`. The external write clamp lowers it through the same helper. + - **`allowTransfer`**, which now follows `edit`: an owner change is an update, refused unless `edit` is opted in. This also corrects a super-user wildcard admin, who was answered `can(object, 'transfer')` `true` on every guarded object that does not opt `edit` in. + + No write changes: the guard refused each of these before and still does. Only the served capability answer, and the console buttons that read it, now match it. +- e3948ae: fix(platform-objects): an organization membership, a two-factor enrollment and the two SCIM join rows are named by the people and groups they point at, not raw ids + + Clause-②: no + Metadata-protocol: none + + - **The record titles.** Four identity join objects built their title (`display_title`) by splicing stored lookup ids, so a record page's heading, a list's Title column and every reference to the row read ids. Each now reads names one hop through its lookups (ADR-0079 addendum, 2026-10-18), as `sys_business_unit_member` and `sys_team_member` do. A person is their `name`, or their `email` where the name is blank: + - `sys_member`: `Ada Lovelace (admin)`, or `Ada Lovelace` with no role; + - `sys_two_factor`: `Two-factor for Ada Lovelace`; + - `sys_scim_projection_grant`: `billing_admin → Ada Lovelace`; + - `sys_scim_group_member`: the provisioned user's `user_name` and the group's `display_name`, each its object's own title: `ada.lovelace@idp.example in Operations`. + + Every hop is read as the reader: a person, provisioned user or group the reader may not read, or one that no longer exists, leaves the title empty rather than showing an id. No column is stored, so a rename shows on the next read. + - **The authorization prelude reads no title.** `@objectstack/core`'s grant resolver reads `sys_member` three times (the caller's memberships, the active organization's first page of members, and the position-holder count). Each read now names the columns it decides on, as the `sys_user_permission_set` grant read already did, so the membership's title, which now reads a `sys_user` row, is never computed there. An authenticated request still pays the same reads and round trips as before. +- 1c5cee6: fix(service-settings): an `OS_LOCALIZATION_LOCALE` naming a language the boot is still loading is no longer dropped, and Setup's default language accepts by the same locale matcher as the IdP locale claim (#30456) + + Clause-②: yes (widening) — `@objectstack/core` exports `resolveOfferedLocale(requested, offered)`; `SettingsService` gains `holdOptionSourceVerdicts()` and `sealOptionSources()`. The save of `localization.locale` (`PUT /api/settings/localization`, `SettingsService.setMany`) and an `OS_LOCALIZATION_LOCALE` override now also accept a region or base of an offered language (`fr-FR` where `fr` is offered, `fr` where `fr-FR` is) and another case of an offered code (`zh-cn` for `zh-CN`); nothing accepted before is refused. The IdP locale claim (`resolveIdpLocale`) lands a script or region spelling on the offered code the platform locale table says it means (`zh-TW` on `zh-Hant`, not on a `zh-Hans` listed first). + Metadata-protocol: none + + - **The boot window.** The deployment's locale list fills during the boot: the app's authored translations arrive at `kernel:ready`. A `kernel:ready` reader of `localization.locale` (auth's mail and SMS default language) could ask before the language an `OS_LOCALIZATION_LOCALE` override names was in the list; the override was then reported `IGNORED` and the reader kept the next layer's value, though the boot went on to offer that language. `SettingsServicePlugin` now holds the option verdict on a sourced key until `kernel:bootstrapped`: while held, an override the list does not admit yet is in force and not reported; at `kernel:bootstrapped` it is judged again, and one the list still does not admit is reported once, as before, and a settings change is emitted for `localization.locale` so a reader that applied it reads the key again. The other value checks (pattern, domain, bounds, format) are judged at once, as ever. A `SettingsService` constructed without the plugin judges at once, as it always has. + - **One matcher.** Setup's default language and the IdP locale claim now both ask `resolveOfferedLocale` (`@objectstack/core`): `resolveLocale`'s exact and case-insensitive steps, then the platform locale table's language (`zh-TW` is `zh-Hant`, `en-US` is `en`), then `resolveLocale`'s base-language and variant steps. A language one door takes, the other takes too. + - **Comment.** `AUTH_EMAIL_TEMPLATE_LOCALES` (plugin-auth) no longer claims to be the setting's options: since #30286 the setting offers the deployment's locale list, and the two ends agree through `normalizeAuthEmailLocale`'s pass-through and the email service's `en-US` fallback. +- badf24d: fix(core): a banned user's assigned-work count and hand-over now include the platform's injected `owner_id` + + Clause-②: no + Metadata-protocol: none + + The field picker behind the assigned-work count and the bulk hand-over skipped every `system: true` user reference. The `owner_id` the platform injects on every business object is `system: true` (it is a platform column) but not `readonly` (ownership is transferable), so it was skipped: on any object whose author had not declared `owner_id` themselves, the records a banned user owned were counted as zero and never handed over. The picker now leaves out `readonly` user references only, so `created_by` / `updated_by` stay out and the injected `owner_id` is counted and handed over. Platform tables (`sys_*`), `managedBy` objects and injected columns with no storage stay excluded as before. +- f4cd4ec: fix(core)!: a banned user's credentials are refused while the ban holds, at every door that accepts an API key + + Clause-②: yes (narrowing) — while a user's ban holds, a request authenticated by an API key that user owns is answered as a key that does not exist (the generic `401 UNAUTHENTICATED`), at every door that accepts a key: the REST data door, the MCP door, and every other door that resolves a key through the shared key-admission seam. The widening half: `@objectstack/core` exports the one ban predicate, `isUserBanInForce` (with `USER_BAN_FIELDS` and the `UserBanFields` type), which the key-admission seam and plugin-auth's session-shaped refusals now share. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at every key-admitting door (`@objectstack/core`): a banned user's API keys are refused while the ban holds. + + - **The rule.** A ban holds every credential of the user. Banning a user already ends their sessions and refuses their sign-in; their API keys now answer as a key that does not exist for as long as the ban holds. A ban with an expiry stops binding once the expiry has passed, exactly as sign-in does — no sign-in is needed to lapse it. + - **Not a revocation.** No key row is revoked or rewritten. Lifting the ban (or letting it expire) admits the same keys again, with nothing reissued, each still judged by its own lifecycle: a key left unused past the inactivity window during the ban answers as inactive (`API_KEY_INACTIVE`), and its remedy is a new key. + - **One anonymous entry.** A key refused for its owner (a ban) or for the tenancy posture (no organization under `isolated`) resolves to the same entry as a key that does not exist, on every composition: the guest's grants on a face that serves the guest, nothing elsewhere. It never falls through to a session. + - **Who moves.** An integration running on a key whose owner is banned gets `401` until the ban is lifted or expires. + + **The remedy.** Lift the ban, or mint a replacement key under a user who is not banned. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index 91b70fd4346..bdd248c36b1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/core", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Microkernel Core for ObjectStack", "type": "module", diff --git a/packages/create-objectstack/CHANGELOG.md b/packages/create-objectstack/CHANGELOG.md index c2c91f2f077..1cb03b6b843 100644 --- a/packages/create-objectstack/CHANGELOG.md +++ b/packages/create-objectstack/CHANGELOG.md @@ -1,5 +1,29 @@ # create-objectstack +## 18.4.0 + +### Patch Changes + +- 1c53783: feat(cli,create-objectstack): `os build` says when the local dev database holds Studio customizations the artifact leaves behind; the starter's `AGENTS.md` and README say a Studio change lives in the database and name `os package export` + + Clause-②: yes (widening) — the exported CompileCommand gains a protected reportsLocalCustomizations flag (default false); nothing removed or narrowed + Metadata-protocol: none + + - **`@objectstack/cli`: `os build`** ends, on its text face, with one notice when the SQLite database `os dev` opens for the project (the same resolution, over the project's development `.env*` files) holds items of a `.custom` package: how many, in which pair, the database file, and that `os package export` takes them out while `os dev` runs. A change saved in Studio is such an item (ADR-0146 D2), never part of `src/`, so neither the artifact nor the starter's `Dockerfile` carries it. The read is local and read-only; a database that is not a local SQLite file is not probed and prints nothing, and a file that cannot be read is said to be unchecked, never empty. `os compile`, which `os dev` runs on every boot and rebuild, and `os build --json` print nothing new. + - **`create-objectstack`: the generated `AGENTS.md` and the blank starter's README** say that Studio saves into the environment's database as an item of `.custom`, that `build` and the `Dockerfile` leave it behind, that app changes are made in `src/`, and how a customization moves as a package (`os login`, `os package export`, then `os package install` on the deployment, in the numbered order). +- 959c26b: feat(cli,create-objectstack): a new project has a wired place for permission sets and datasets, `os g permission` / `os g dataset` write them, and the generated AGENTS.md names the package manager the project was created with + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`create-objectstack`: the blank starter ships `src/security/index.ts` and `src/datasets/index.ts`**, empty barrels already handed to the stack's `permissions` and `datasets` keys in `objectstack.config.ts`, like the eight directories it wired before. A permission set or a dataset exported from them is loaded and counted by `validate` with no edit to the config. The generated `AGENTS.md` lists both directories. + - **`@objectstack/cli`: `os g permission NAME [--object OBJECT]`** writes `src/security/NAME.permission.ts`, a `definePermissionSet` granting read, create and edit (no delete) on the object `--object` names, or the stack's only object, with `readScope: 'own'` declared. **`os g dataset NAME [--object OBJECT]`** writes `src/datasets/NAME.dataset.ts`, a `defineDataset` over that object with a `record_count` measure, grouped by the record's title field when it has one. `defineDataset` types the literal, so it needs no `as const`. Both pass `os validate`, `os build` and `os lint` with no finding on a fresh project. `os init` wires both directories too. + - **`create-objectstack`: the generated `AGENTS.md` (and `.github/copilot-instructions.md`) says `pnpm run validate` in a project created with pnpm**, and `npm run …` in one created with npm, the same package manager the install line and "Next steps" name. It used to say `npm run` in every project. +- a98e5cd: Clause-②: no + Metadata-protocol: none + + The blank template now says its `note` object is a sample: the generated README gains a "The sample `note` object" section with the steps to delete or replace it, and `src/objects/note.object.ts` opens with a comment saying the same. A developer no longer finds a `note` object in their app, over REST or in an MCP client's object list, without anything telling them it came from the scaffold. + ## 18.3.0 ### Patch Changes diff --git a/packages/create-objectstack/package.json b/packages/create-objectstack/package.json index 33302a659e5..b431b6d6aca 100644 --- a/packages/create-objectstack/package.json +++ b/packages/create-objectstack/package.json @@ -1,6 +1,6 @@ { "name": "create-objectstack", - "version": "18.3.0", + "version": "18.4.0", "description": "Create a new ObjectStack project — npx create-objectstack", "bin": { "create-objectstack": "./bin/create-objectstack.js" diff --git a/packages/create-objectstack/src/templates/blank/package.json b/packages/create-objectstack/src/templates/blank/package.json index e11f0a65c04..125b3997454 100644 --- a/packages/create-objectstack/src/templates/blank/package.json +++ b/packages/create-objectstack/src/templates/blank/package.json @@ -16,17 +16,17 @@ "test": "vitest run" }, "dependencies": { - "@objectstack/spec": "18.3.0", - "@objectstack/runtime": "18.3.0", - "@objectstack/plugin-hono-server": "18.3.0", - "@objectstack/connector-rest": "18.3.0", - "@objectstack/connector-openapi": "18.3.0", - "@objectstack/connector-mcp": "18.3.0" + "@objectstack/spec": "18.4.0", + "@objectstack/runtime": "18.4.0", + "@objectstack/plugin-hono-server": "18.4.0", + "@objectstack/connector-rest": "18.4.0", + "@objectstack/connector-openapi": "18.4.0", + "@objectstack/connector-mcp": "18.4.0" }, "devDependencies": { - "@objectstack/cli": "18.3.0", - "@objectstack/skills": "18.3.0", - "@objectstack/verify": "18.3.0", + "@objectstack/cli": "18.4.0", + "@objectstack/skills": "18.4.0", + "@objectstack/verify": "18.4.0", "typescript": "^5.3.0", "vitest": "~4.1.12" }, diff --git a/packages/drivers/driver-memory/CHANGELOG.md b/packages/drivers/driver-memory/CHANGELOG.md index 8216c873124..abc9e71677d 100644 --- a/packages/drivers/driver-memory/CHANGELOG.md +++ b/packages/drivers/driver-memory/CHANGELOG.md @@ -1,5 +1,80 @@ # @objectstack/driver-memory +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/drivers/driver-memory/package.json b/packages/drivers/driver-memory/package.json index a4b28cc9bbe..fd900841a09 100644 --- a/packages/drivers/driver-memory/package.json +++ b/packages/drivers/driver-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-memory", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "In-Memory Driver for ObjectStack (Reference Implementation)", "type": "commonjs", diff --git a/packages/drivers/driver-mongodb/CHANGELOG.md b/packages/drivers/driver-mongodb/CHANGELOG.md index 504b08b4e0f..d7b1219dac5 100644 --- a/packages/drivers/driver-mongodb/CHANGELOG.md +++ b/packages/drivers/driver-mongodb/CHANGELOG.md @@ -1,5 +1,80 @@ # @objectstack/driver-mongodb +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/drivers/driver-mongodb/package.json b/packages/drivers/driver-mongodb/package.json index 942d4ec0518..715c45efac5 100644 --- a/packages/drivers/driver-mongodb/package.json +++ b/packages/drivers/driver-mongodb/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-mongodb", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "MongoDB Driver for ObjectStack - Native document database driver via official mongodb client", "type": "commonjs", diff --git a/packages/drivers/driver-odata/CHANGELOG.md b/packages/drivers/driver-odata/CHANGELOG.md index cb0d48131f6..5a907bef0c6 100644 --- a/packages/drivers/driver-odata/CHANGELOG.md +++ b/packages/drivers/driver-odata/CHANGELOG.md @@ -1,5 +1,80 @@ # @objectstack/driver-odata +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/drivers/driver-odata/package.json b/packages/drivers/driver-odata/package.json index 1aa5b49b17f..aedbb5fb9a0 100644 --- a/packages/drivers/driver-odata/package.json +++ b/packages/drivers/driver-odata/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-odata", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "OData v4 and v2 datasource driver for ObjectStack — serve an OData service's entity sets as external objects through a plugin driver", "keywords": [ diff --git a/packages/drivers/driver-sql/CHANGELOG.md b/packages/drivers/driver-sql/CHANGELOG.md index 90c6b0e841e..b07c75de594 100644 --- a/packages/drivers/driver-sql/CHANGELOG.md +++ b/packages/drivers/driver-sql/CHANGELOG.md @@ -1,5 +1,105 @@ # @objectstack/driver-sql +## 18.4.0 + +### Patch Changes + +- 8d7f906: fix(driver-sql): schema sync never drops an existing table that holds rows; a `_id`-keyed table with rows is refused, naming the table and the column + + Clause-②: yes (narrowing) — `SqlDriver.initObjects` / `syncSchema` refuse one more case that used to succeed: an existing table that has a `_id` column, no `id` column and at least one row. It used to be dropped and recreated empty; it is now refused with an `ExistingTableAdoptionRefusal` naming the table, the column and the row count, and nothing is changed. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing in schema sync (`@objectstack/driver-sql`): a table the sync would have rebuilt is refused while it holds rows. + + - **Before.** Schema sync adopts an existing table by adding the columns the object declares and the table lacks, rows kept. A table keyed by `_id` with no `id` column (the storage key of the ObjectQL driver this one was ported from) was instead dropped and recreated as the object declares, whatever it held. Storing an object on a managed datasource over an existing database reached it: an object named after such a table deleted its rows, and the save answered success. + - **Now.** That rebuild runs only on an empty table, as before. A table that holds rows is refused before anything is written: no column is added, no index is built, no row is touched. The refusal is thrown from `initObjects` and reaches the caller of the sync. The metadata save, the boot sync and a datasource connect report a refused sync in the server log at `error`, followed by this message, as they do for any refused sync. + - **Who moves.** Only a deployment storing an object on a managed SQL datasource whose table of the same name is keyed by `_id` and holds rows. No platform table has that shape. + + **The remedy.** To keep the rows and use the table, rename its `_id` column to `id`, then save the object again (or restart). To read the table as it is, bind the object through a datasource whose `schemaMode` is `external`. If the rows are not needed, empty the table and save again; the sync then rebuilds it. +- dcb5fb3: fix(driver-turso): the remote face joins a large `$and` / `$or` as balanced halves, so comment and attachment reads fit a hosted libSQL server's depth limit + + Clause-②: no + Metadata-protocol: none + + On a remote Turso / libSQL database (`TursoDriver` with a `libsql://`, `https://` or `wss://` url and no `syncUrl`, as every hosted environment runs), every read of `sys_comment` and `sys_attachment` answered `500 DATABASE_ERROR` once the deployment registered roughly a hundred objects, as an installed app does. The driver's log named the cause: `Expression tree is too large (maximum depth 100)`. The comment and attachment read gates OR one `ReadableIds` branch per registered parent object, and the remote face's compiler joined a group's members into one flat chain, one expression level per member, while the hosted server refuses a tree deeper than 100. + + The local face (`SqlDriver`) has emitted such groups as balanced halves since #29888. The remote face compiles its SQL text itself, so that change never reached it. It now regroups by the same rule, shared from `@objectstack/driver-sql` as `joinBalancedGroup` (with `FLAT_GROUP_MAX_MEMBERS`): a group of at most 8 members compiles byte-identical to before, and a larger one as two parenthesised halves, recursively, so the depth it adds grows with the logarithm of its size. Measured on the gate's own shape with 134 branches and a three-level master chain: 162 levels before, 38 after. + + The regrouped expression selects exactly the rows the flat one did, on every dialect: `AND` and `OR` are associative over SQL's three-valued logic, the members keep their order, and so do their bind values. No gate, scope or tenant wall changes. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/observability@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/drivers/driver-sql/package.json b/packages/drivers/driver-sql/package.json index a1571708b2b..ef5893b06dd 100644 --- a/packages/drivers/driver-sql/package.json +++ b/packages/drivers/driver-sql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-sql", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "SQL Driver for ObjectStack - Supports PostgreSQL, MySQL, SQLite via Knex", "type": "commonjs", diff --git a/packages/drivers/driver-sqlite-wasm/CHANGELOG.md b/packages/drivers/driver-sqlite-wasm/CHANGELOG.md index 17930be629d..40dde4b03b3 100644 --- a/packages/drivers/driver-sqlite-wasm/CHANGELOG.md +++ b/packages/drivers/driver-sqlite-wasm/CHANGELOG.md @@ -1,5 +1,82 @@ # @objectstack/driver-sqlite-wasm +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [8d7f906] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [dcb5fb3] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/driver-sql@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/drivers/driver-sqlite-wasm/package.json b/packages/drivers/driver-sqlite-wasm/package.json index bc330f1a79c..f9687de23d6 100644 --- a/packages/drivers/driver-sqlite-wasm/package.json +++ b/packages/drivers/driver-sqlite-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-sqlite-wasm", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "WASM SQLite Driver for ObjectStack — runs in browser/WebContainer (StackBlitz) without native bindings", "keywords": [ diff --git a/packages/drivers/driver-turso/CHANGELOG.md b/packages/drivers/driver-turso/CHANGELOG.md index 1287bb124f7..36dbad94750 100644 --- a/packages/drivers/driver-turso/CHANGELOG.md +++ b/packages/drivers/driver-turso/CHANGELOG.md @@ -1,5 +1,92 @@ # @objectstack/driver-turso +## 18.4.0 + +### Patch Changes + +- dcb5fb3: fix(driver-turso): the remote face joins a large `$and` / `$or` as balanced halves, so comment and attachment reads fit a hosted libSQL server's depth limit + + Clause-②: no + Metadata-protocol: none + + On a remote Turso / libSQL database (`TursoDriver` with a `libsql://`, `https://` or `wss://` url and no `syncUrl`, as every hosted environment runs), every read of `sys_comment` and `sys_attachment` answered `500 DATABASE_ERROR` once the deployment registered roughly a hundred objects, as an installed app does. The driver's log named the cause: `Expression tree is too large (maximum depth 100)`. The comment and attachment read gates OR one `ReadableIds` branch per registered parent object, and the remote face's compiler joined a group's members into one flat chain, one expression level per member, while the hosted server refuses a tree deeper than 100. + + The local face (`SqlDriver`) has emitted such groups as balanced halves since #29888. The remote face compiles its SQL text itself, so that change never reached it. It now regroups by the same rule, shared from `@objectstack/driver-sql` as `joinBalancedGroup` (with `FLAT_GROUP_MAX_MEMBERS`): a group of at most 8 members compiles byte-identical to before, and a larger one as two parenthesised halves, recursively, so the depth it adds grows with the logarithm of its size. Measured on the gate's own shape with 134 branches and a three-level master chain: 162 levels before, 38 after. + + The regrouped expression selects exactly the rows the flat one did, on every dialect: `AND` and `OR` are associative over SQL's three-valued logic, the members keep their order, and so do their bind values. No gate, scope or tenant wall changes. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [8d7f906] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [dcb5fb3] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/driver-sql@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/drivers/driver-turso/package.json b/packages/drivers/driver-turso/package.json index 8d9e91bc103..396a8e2e58f 100644 --- a/packages/drivers/driver-turso/package.json +++ b/packages/drivers/driver-turso/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-turso", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Turso/libSQL Driver for ObjectStack — Edge-first SQLite with embedded replicas", "keywords": [ diff --git a/packages/formula/CHANGELOG.md b/packages/formula/CHANGELOG.md index 6e7fba5f3e9..d5fae42b709 100644 --- a/packages/formula/CHANGELOG.md +++ b/packages/formula/CHANGELOG.md @@ -1,5 +1,166 @@ # @objectstack/formula +## 18.4.0 + +### Minor Changes + +- 4c2cd29: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(lint)!: publish refuses a validation rule (or another fail-closed predicate) that orders a text or boolean field against a number, instead of warning that it "evaluates to null" and then refusing every write (#29904) + + Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the object save door and the package publish) refuses a `type-mismatch` it used to pass with a warning, at the predicate sites that fail closed. Nothing is widened. + Metadata-protocol: narrowed + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate`, the object save door and `POST /api/v1/packages/:id/publish-drafts`. + + - **Before.** `record.name > 5` on a text field, written as a validation rule's `condition`, published with one `expression-invalid` advisory at `severity: warning` saying the expression "silently evaluates to null". A validation rule fails closed, so from then on every create and update whose `name` was not numeric text was refused: `400 VALIDATION_FAILED` "Validation rule 'name_gt' could not be evaluated (runtime: no such overload: dyn > int) — write rejected". + - **Now.** The same rule is refused at publish as `expression-invalid` at `severity: error`, at the rule's `condition` path, with a message that says the write would be refused and how to compare like with like. This is the severity the nullable-comparison verdict (`record.qty > 1000` on a nullable number) already had at the same slot. + - **Which sites.** Those whose runtime refuses the write (or aborts the operation) when the expression faults: a validation rule's `condition` and `when`, nested `then` / `otherwise` rules included; a field's `requiredWhen` and `readonlyWhen`; an option's `visibleWhen` (a write that picks the option); a hook's `condition`. The `validate_expression` MCP tool answers the same for `site: 'validation'`. + - **Unchanged.** A formula field's `expression` keeps the warning: there the fault reads null, which is what the warning says. So do flow conditions (the run fails, the write that fired it lands), a field's display `visibleWhen` / `settledWhen`, field group and action predicates, and `conditionalFormatting`. Sharing-rule conditions compile to a query filter and are not judged by this verdict. + - **`@objectstack/spec`.** The upgrade guide entry `validation-type-mismatch-refused` (a semantic migration: no automatic rewrite, because whether the author meant the length, a number stored as text or another field is a judgement about the rule). + - **`@objectstack/formula`.** `ExprSchemaHint` gains `faultOutcome?: 'fail-closed'`, which the caller sets for a fail-closed site; the `type-mismatch` refusal's `params` carry `outcome: 'fail-closed'` when it is graded an error there. + + **FROM → TO.** `condition: 'record.name > 5'` on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text (it faults, and refuses the write, on any other). A boolean: `record.active > 0` → `record.active == true`. +- 9037b13: fix(lint)!: a text-vs-number comparison in a record-triggered flow condition is refused with what really happens (the run fails, the write lands), and in an advisory validation rule the refusal no longer says the write is refused (#29926) + + Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses a `type-mismatch` in a record-triggered flow's start or edge condition that it used to pass with a warning; and the fail-closed arm of the verdict is served under its own code `type-mismatch-fail-closed` instead of `type-mismatch` with `params.outcome`. Added beside them: the codes `type-mismatch-run-fails` and `type-mismatch-fail-open`, and the exported type `ExprFaultOutcome`. + Metadata-protocol: narrowed + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names each outcome) and the MCP skill text in `packages/mcp/src/skill-md.ts` (`flow_condition` now errors too); `skills/**` teaches no text-vs-number comparison in a flow condition or an advisory rule, so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Record-triggered flow conditions — before.** `name > 5` on a text field, as the start node's `condition` or an edge `condition` of a flow whose start `triggerType` is `record-*`, published with an `expression-invalid` warning saying the expression "silently evaluates to null". It does not: the engine's `evaluateCondition` throws on the fault, so the run fails at that condition, and the record-change trigger isolates the write that fired it, so the write lands and the flow's work is not done. + - **Now.** The same condition is refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with a message saying the run fails at the condition and the triggering write still lands. This is the severity and outcome the nullable-comparison verdict already has on those conditions (#29026). A flow that is not record-triggered, and a node `condition` other than the start node's (inert), keep the warning. The `validate_expression` MCP tool answers the same for `site: 'flow_condition'`. + - **Advisory validation rules** (`severity: 'warning'` / `'info'`). Still refused at publish, as the nullable twin refuses them, but under `type-mismatch-fail-open` with a message that says what happens: the rule is skipped, the write proceeds, and the rule never gives its advisory. The nullable twin's message on the same rule now says the same instead of "the write is rejected fail-closed". A `severity: 'error'` rule keeps both refusals and their wording. + - **Refusal codes (`@objectstack/formula`).** A code names one message template, so each outcome has its own: `type-mismatch` (warning, reads null), `type-mismatch-fail-closed`, `type-mismatch-run-fails`, `type-mismatch-fail-open` (all errors). `ExprSchemaHint.faultOutcome` takes `'fail-closed' | 'run-fails' | 'fail-open'` (exported as `ExprFaultOutcome`); the `outcome` param on `type-mismatch` is gone. + - **`@objectstack/spec`.** Upgrade guide entry `flow-condition-type-mismatch-refused` (semantic: no mechanical rewrite). The `validation-type-mismatch-refused` entry's acceptance criteria now say a create answers 201, the data door's status. + + **FROM → TO.** Flow condition `name > 5` on a text field → `size(name) > 5` for its length, `score > 5` on a number field for a value, or `double(name) > 5` only when every value is numeric text. A boolean: `active > 0` → `active == true`. A consumer keyed to the code: `type-mismatch` with `params.outcome === 'fail-closed'` → `type-mismatch-fail-closed`. + + **The renamed code shipped in 18.3.0.** `@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` already serve #29904's fail-closed refusal as `type-mismatch` with `params.outcome: 'fail-closed'` (the `outcome` param is in the 18.3.0 `ExpressionRefusalParams` type), although 18.3.0's changelog has no line for it: its changeset (`29904-type-mismatch-fail-closed.md`) was not consumed by that release. This release renames that arm `type-mismatch-fail-closed` and removes the `outcome` param, so a consumer keyed to 18.3.0's `type-mismatch` + `outcome` moves to the new code (FROM → TO above). The same changeset also says flow conditions keep the warning; a record-triggered flow's start and edge conditions are now refused, as above. +- e8afd1e: fix(lint)!: every flow condition's text-vs-number comparison gets a verdict that says what the run does — a record-triggered flow's `record.` read and `decision` branch are refused like its bare name, a time-relative sweep of the flow's object is refused like a record-triggered flow, a bare name a flow variable holds is no longer typed as the field, and any other flow's warning says the run fails instead of "evaluates to null" (#29959) + + Clause-②: yes (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses, on a record-triggered flow, a `type-mismatch` in a `decision` branch and in a `record.` / `previous.` read of a start or edge condition, which it passed with no verdict before; refuses the same on a time-relative flow sweeping the object its conditions are typed against, where 18.3.0 served a warning; and a flow condition's `type-mismatch` warning is served under its own code `type-mismatch-run-may-fail` instead of `type-mismatch`. Added beside them (the widening half): the optional `severity` and `triggerKind` inputs of the `validate_expression` MCP tool, the code `type-mismatch-run-may-fail` in `@objectstack/formula`'s `ExpressionRefusalCode`, the optional `flowVariables` key of `@objectstack/formula`'s `ExprSchemaHint`, and a bare flow-condition name that a declared flow variable or node id holds is no longer typed as the field it shadows (no verdict where 18.3.0 served a `type-mismatch` warning). + Metadata-protocol: narrowed + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names the decision branch, the `record.` read and the warning on other flows) and the MCP skill text in `packages/mcp/src/skill-md.ts` (the two new `validate_expression` inputs); `skills/**` teaches no text-vs-number comparison in a flow condition (grep for `type mismatch`, `evaluates to null`, `validate_expression` and the decision examples), so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Record-triggered flows — before.** The start and edge conditions refused `name > 5` on a text field (`type-mismatch-run-fails`), but `record.name > 5` in the same slot, and `name > 5` in a `decision` node's `config.conditions[].expression`, published with no verdict at all. Both fail the run the same way: the engine's `evaluateCondition` throws on the fault, the run binds the trigger record both as `record` and flattened to bare names, and a decision branch is evaluated over the same variables as an edge. + - **Now.** Both are refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with the message the bare name already gets: the run fails at the condition and the triggering write still lands. `previous.` is judged the same way. The date verdicts stay on the bare name only, as before. + - **Time-relative flows.** A sweep launches each run with the swept row as its record, so on a flow whose `timeRelative.object` is the start node's `objectName` (or is omitted) the start, edge and `decision` conditions get the record-triggered verdict above, bare name included: `type-mismatch-run-fails`, where 18.3.0 served a `type-mismatch` warning. A sweep of another object keeps the warning below. + - **Any other flow** (no `triggerType`, a `schedule` flow, an `api` flow) and an inert node `condition` keep a warning. Its text said the expression "silently evaluates to null", which is false on every flow: a condition that faults fails the run. It is now `type-mismatch-run-may-fail`, worded for the failed run, and a warning because such a flow is handed whatever its entrance carries (an API request body, an action's partial record, a parent flow's record, or nothing), so the field's type is not proven to be what the name holds. A `record.` read and a `decision` branch there get the same warning. The flow registration log line reads the same text. + - **A bare name a flow variable holds.** The run seeds a flow's declared variables (and each node's outputs, under its id) before it flattens the record, and the flattening skips a name already bound. So in a flow that declares `variables: [{ name: 'title', type: 'number' }]` over a text field `title`, a bare `title > 5` reads the number and runs; it was refused as `type-mismatch-run-fails` on the head that added that error, and warned on 18.3.0. It now gets no type verdict (the `flow-variable-shadows-field` warning still names the shadowing). `record.title > 5` reads the field and stays refused. + - **`validate_expression` (MCP).** Two optional inputs, so the tool answers what the gate will: `severity` (`error` | `warning` | `info`, site `validation`; default `error`) gives an advisory rule the gate's `type-mismatch-fail-open` instead of the fail-closed verdict, and `triggerKind` (`record_change` | `time_relative` | `schedule` | `api` | `none`, site `flow_condition`; default `record_change`) gives a `schedule`, `api` or `none` flow the gate's warning instead of the run-fails error (`time_relative` answers run-fails, its record being a row of `objectName`). Either input on a site it does not describe is refused as a tool error. A call that passes neither answers as before. + - **`@objectstack/spec`.** Upgrade guide entry `flow-record-read-and-decision-type-mismatch-refused` (semantic: no mechanical rewrite). + + **FROM → TO.** In a record-triggered flow, or a time-relative flow sweeping its own object: `record.name > 5` (or a decision branch `name > 5`) on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text. A boolean: `record.active > 0` → `record.active == true`. A consumer keyed to the code of a flow condition's warning: `type-mismatch` → `type-mismatch-run-may-fail` (`@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` serve `type-mismatch` there; the record-scoped `type-mismatch`, on a formula field or a display predicate, is unchanged). + +### Patch Changes + +- 0f3ee58: fix(formula,lint): a date-arithmetic verdict in a flow condition says the run fails, not that the value reads null + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/formula`: four codes join `ExpressionRefusalCode`**: `date-arithmetic-run-fails`, `date-arithmetic-run-may-fail`, `date-text-arithmetic-run-fails` and `date-text-arithmetic-run-may-fail`, with the same params as `date-arithmetic` / `date-text-arithmetic`. A flow condition (a `'flattened'` site) that does arithmetic on a date field used to be told the field "silently evaluates to null", but the engine's `evaluateCondition` throws on the fault on every flow, so the run fails at the condition. Where the caller declares the run proven to hold the field (`faultOutcome: 'run-fails'`, with `bareNamesUnproven` unset) the verdict is served under `-run-fails` and says the run fails; anywhere else in a flow it is served under `-run-may-fail` and says the run fails wherever the name holds the field's date when the condition runs (a flow started by an API call, an action or a parent flow binds what its entrance hands over, which can be a number). The grade is unchanged: an error at every site. A `record`-scoped site (a formula field, a validation rule, a display predicate) keeps `date-arithmetic` / `date-text-arithmetic` and their sentence. A consumer keyed to the old codes on a flow condition re-keys to the new ones. + - **`@objectstack/lint`**: the fault-outcome ledger's docblock names the date verdicts' flow codes; the routing is the existing `faultOutcome` / `bareNamesUnproven` hint, unchanged. +- 29cbfcc: fix(lint): a flow condition's bare-name type verdict no longer refuses a name a `screen` field holds, and stands down to the warning after a node that can bind any name (#30025) + + Clause-②: yes (widening) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) no longer refuses, on a record-triggered flow (or a time-relative sweep of its own object), a `type-mismatch-run-fails` on a BARE condition name that a `screen` node's declared field holds (no verdict now), nor on a bare name in a flow with a node that can bind a name no reader sees (`script`, `connector_action`, `wait`, `subflow`, `map`, a screen with no input contract), which is now the `type-mismatch-run-may-fail` warning. A `record.` / `previous.` read keeps its refusal, and nothing new is refused. The null-guard verdict's bare names also leave a screen's field names out. Added beside them: the optional `bareNamesUnproven` key of `@objectstack/formula`'s `ExprSchemaHint`, and `collectScreenFieldNames` in `@objectstack/lint`'s flow-variable-scope module. + Metadata-protocol: none + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names the screen field and the open binder); `skills/**` and the MCP skill text teach no bare flow-condition name a screen or an open binder holds, so no skill or eval moves. + + - **Before.** A record-triggered flow whose `screen` node collects `title` (a number) and whose edge after it reads `title > 5`, with `title` a text field on the trigger object, was refused `type-mismatch-run-fails`: the bare name was typed as the field. The run binds the collected number there (a resume lands a screen's values under their plain names), so the condition runs. The same refusal hit a bare name after a `script`, `connector_action`, `wait`, `subflow` or `map` node, or a screen with no input contract, any of which can bind the name to a value nothing can see at build. + - **Now.** A screen's declared field names join the names a bare read holds instead of the field, so they are not typed as it (the null-guard verdict leaves them out too). In a flow with a node that can bind any name, a bare-name mismatch is the `type-mismatch-run-may-fail` warning, as the null-guard verdict already stands its bare names down there; `record.` is still refused. The stand-down reads the flow's nodes only: an entrance (and the publish door's per-write snapshot, which sees none) changes which record both the bare name and `record.` read, not whether a node rebinds the bare name, so a bare-name refusal on a record-triggered flow with no such node is unchanged at every door. +- 67d5d53: fix(formula,lint,mcp): a fail-closed date verdict says the write is refused, and a time-relative sweep's run-fails verdict names no firing write + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/formula`: three codes join `ExpressionRefusalCode`** (`EXPRESSION_REFUSAL_CODES`), with the params of the code they split from: + - `date-arithmetic-fail-closed` and `date-text-arithmetic-fail-closed`. On a site that fails closed (`faultOutcome: 'fail-closed'`: an `error` validation rule, a field `requiredWhen` / `readonlyWhen`, an option `visibleWhen`, a hook `condition`), date arithmetic used to be told "this faults at runtime, so the field silently evaluates to null". There the fault refuses the write instead, and the verdict now says so. A formula field and an advisory (`warning` / `info`) validation rule keep `date-arithmetic` / `date-text-arithmetic` and their sentence. A consumer keyed to the old codes on a fail-closed site re-keys to the new ones. + - `type-mismatch-sweep-run-fails`. A time-relative sweep of the typed object is graded `run-fails` like a record-triggered flow, but no write fires its run: the sweep starts one run per row it finds. Its condition's text-vs-number verdict used to say "the write that fired it still lands". Served where the new optional `ExprSchemaHint.timeRelativeSweep` is set beside `faultOutcome: 'run-fails'`, it now says the run for that row fails and nothing else happens. A record-triggered flow keeps `type-mismatch-run-fails` and its sentence. + - The grade is unchanged at every site. What is refused, warned or passed is unchanged. + - **`@objectstack/lint`**: the flow walk passes `timeRelativeSweep` on a `time_relative` flow. The nullable-comparison verdict, which also says "still lands", runs on record-triggered flows only, so it is unchanged. + - **`@objectstack/mcp`**: `validate_expression` with `site: 'flow_condition'` and `triggerKind: 'time_relative'` passes the same key, so its answer stays the gate's. +- 0e4747a: fix(formula): an advisory validation rule's date verdict says the rule is skipped and the write proceeds + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/formula`: two codes join `ExpressionRefusalCode`** (`EXPRESSION_REFUSAL_CODES`), with the params of the code each splits from: `date-arithmetic-fail-open` and `date-text-arithmetic-fail-open`. + - On an advisory (`severity: 'warning'` / `'info'`) validation rule (`faultOutcome: 'fail-open'`), date arithmetic used to be told "this faults at runtime, so the field silently evaluates to null". There is no field there, and nothing reads null: the rule is skipped and logged, and the write proceeds. The verdict now closes on the sentence `type-mismatch-fail-open` already uses for that outcome. This replaces the earlier note that an advisory rule keeps `date-arithmetic` / `date-text-arithmetic`. + - A formula field keeps `date-arithmetic` / `date-text-arithmetic` and the null sentence. A consumer keyed to the old codes on an advisory rule re-keys to the new ones. + - The grade is unchanged: the verdict is an error, as before. What is refused, warned or passed is unchanged. + - `@objectstack/lint` and `@objectstack/mcp` `validate_expression` already pass `faultOutcome: 'fail-open'` for an advisory rule, so both serve the new sentence with no change of their own. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/formula/package.json b/packages/formula/package.json index 6f68ab97eaa..fb019306696 100644 --- a/packages/formula/package.json +++ b/packages/formula/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/formula", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "ObjectStack canonical expression engine — CEL (cel-js) + ObjectStack stdlib + dialect registry", "type": "commonjs", diff --git a/packages/lint/CHANGELOG.md b/packages/lint/CHANGELOG.md index d52564a713a..87d4793f403 100644 --- a/packages/lint/CHANGELOG.md +++ b/packages/lint/CHANGELOG.md @@ -1,5 +1,390 @@ # @objectstack/lint +## 18.4.0 + +### Minor Changes + +- 46d2b65: fix(lint)!: a report whose `runtimeFilter` names a field its dataset's object lacks is refused at publish (`report-filter-field-unknown`), as the dashboard twin already is (#29900) + + Clause-②: yes (narrowing) — the report publish door, `os validate` and `os build` refuse a report `runtimeFilter` key they accepted before; `@objectstack/lint` also exports `validateReportRuntimeFilters`, `REPORT_FILTER_FIELD_UNKNOWN` and the `ReportRuntimeFilterFinding` type. + Metadata-protocol: narrowed + Skills-docs: none — `skills/**` and `content/docs/**` teach `runtimeFilter` keys that are fields of the dataset's object, which this rule accepts; no page teaches a key the object lacks. The reasoning ships as the `os explain report-filter-field-unknown` entry. + + + + **BREAKING** accept-set narrowing at the report publish door (`PUT /api/v1/meta/report/NAME`, `POST /api/v1/meta/report/NAME/publish`, package draft publish; Studio, REST and MCP authors) and in `os validate` / `os build`. + + - **Before.** A report with `runtimeFilter: { no_such_field: 'x' }` over a dataset whose object has no `no_such_field` published `200`, and every reader of the report saw a "Dataset query failed: 400 Bad Request — Filter member no_such_field …" panel instead of numbers. The same key on a dashboard widget's `filter` was already refused `422` (`widget-filter-field-unknown`). + - **Now.** It is refused `422 INVALID_METADATA` with `report-filter-field-unknown` at `reports[0].runtimeFilter.no_such_field`, and the verdict names the object and the nearest field ("Did you mean …"). A `joined` report's block filter is judged against the block's dataset, and the report-level `runtimeFilter` (merged into every block's query) against each block's dataset. + - **Accepted, as on a widget.** A field of the dataset's object, a registry-injected system column (`created_at`, `owner_id` where injected), a dotted relationship path, and every `$` operator and combinator. One more key is accepted on a report: a bare key naming a dimension of the bound dataset, which the dataset query binds through that dimension. + - **Not judged.** A `dataset` that resolves to nothing (`chart-dataset-unknown` reports it once), an object the stack (or the publish snapshot) does not carry, an object with no readable field map, and a dotted key whose prefix the dataset's `include` does not declare. + - **`os explain report-filter-field-unknown`** prints the reasoning; `RULE_EXPLANATIONS` gains the entry. + + **What an author does.** Nothing for a report whose filter keys name real fields or dimensions. A refused key never served the report (every reader saw the error panel), so there is no rewrite that keeps what the author meant: name a field of the dataset's object, a dimension of the dataset, or a `relationship.field` path whose prefix the dataset's `include` declares. The refusal names the key and the nearest field. +- 4c2cd29: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(lint)!: publish refuses a validation rule (or another fail-closed predicate) that orders a text or boolean field against a number, instead of warning that it "evaluates to null" and then refusing every write (#29904) + + Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the object save door and the package publish) refuses a `type-mismatch` it used to pass with a warning, at the predicate sites that fail closed. Nothing is widened. + Metadata-protocol: narrowed + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate`, the object save door and `POST /api/v1/packages/:id/publish-drafts`. + + - **Before.** `record.name > 5` on a text field, written as a validation rule's `condition`, published with one `expression-invalid` advisory at `severity: warning` saying the expression "silently evaluates to null". A validation rule fails closed, so from then on every create and update whose `name` was not numeric text was refused: `400 VALIDATION_FAILED` "Validation rule 'name_gt' could not be evaluated (runtime: no such overload: dyn > int) — write rejected". + - **Now.** The same rule is refused at publish as `expression-invalid` at `severity: error`, at the rule's `condition` path, with a message that says the write would be refused and how to compare like with like. This is the severity the nullable-comparison verdict (`record.qty > 1000` on a nullable number) already had at the same slot. + - **Which sites.** Those whose runtime refuses the write (or aborts the operation) when the expression faults: a validation rule's `condition` and `when`, nested `then` / `otherwise` rules included; a field's `requiredWhen` and `readonlyWhen`; an option's `visibleWhen` (a write that picks the option); a hook's `condition`. The `validate_expression` MCP tool answers the same for `site: 'validation'`. + - **Unchanged.** A formula field's `expression` keeps the warning: there the fault reads null, which is what the warning says. So do flow conditions (the run fails, the write that fired it lands), a field's display `visibleWhen` / `settledWhen`, field group and action predicates, and `conditionalFormatting`. Sharing-rule conditions compile to a query filter and are not judged by this verdict. + - **`@objectstack/spec`.** The upgrade guide entry `validation-type-mismatch-refused` (a semantic migration: no automatic rewrite, because whether the author meant the length, a number stored as text or another field is a judgement about the rule). + - **`@objectstack/formula`.** `ExprSchemaHint` gains `faultOutcome?: 'fail-closed'`, which the caller sets for a fail-closed site; the `type-mismatch` refusal's `params` carry `outcome: 'fail-closed'` when it is graded an error there. + + **FROM → TO.** `condition: 'record.name > 5'` on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text (it faults, and refuses the write, on any other). A boolean: `record.active > 0` → `record.active == true`. +- 9037b13: fix(lint)!: a text-vs-number comparison in a record-triggered flow condition is refused with what really happens (the run fails, the write lands), and in an advisory validation rule the refusal no longer says the write is refused (#29926) + + Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses a `type-mismatch` in a record-triggered flow's start or edge condition that it used to pass with a warning; and the fail-closed arm of the verdict is served under its own code `type-mismatch-fail-closed` instead of `type-mismatch` with `params.outcome`. Added beside them: the codes `type-mismatch-run-fails` and `type-mismatch-fail-open`, and the exported type `ExprFaultOutcome`. + Metadata-protocol: narrowed + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names each outcome) and the MCP skill text in `packages/mcp/src/skill-md.ts` (`flow_condition` now errors too); `skills/**` teaches no text-vs-number comparison in a flow condition or an advisory rule, so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Record-triggered flow conditions — before.** `name > 5` on a text field, as the start node's `condition` or an edge `condition` of a flow whose start `triggerType` is `record-*`, published with an `expression-invalid` warning saying the expression "silently evaluates to null". It does not: the engine's `evaluateCondition` throws on the fault, so the run fails at that condition, and the record-change trigger isolates the write that fired it, so the write lands and the flow's work is not done. + - **Now.** The same condition is refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with a message saying the run fails at the condition and the triggering write still lands. This is the severity and outcome the nullable-comparison verdict already has on those conditions (#29026). A flow that is not record-triggered, and a node `condition` other than the start node's (inert), keep the warning. The `validate_expression` MCP tool answers the same for `site: 'flow_condition'`. + - **Advisory validation rules** (`severity: 'warning'` / `'info'`). Still refused at publish, as the nullable twin refuses them, but under `type-mismatch-fail-open` with a message that says what happens: the rule is skipped, the write proceeds, and the rule never gives its advisory. The nullable twin's message on the same rule now says the same instead of "the write is rejected fail-closed". A `severity: 'error'` rule keeps both refusals and their wording. + - **Refusal codes (`@objectstack/formula`).** A code names one message template, so each outcome has its own: `type-mismatch` (warning, reads null), `type-mismatch-fail-closed`, `type-mismatch-run-fails`, `type-mismatch-fail-open` (all errors). `ExprSchemaHint.faultOutcome` takes `'fail-closed' | 'run-fails' | 'fail-open'` (exported as `ExprFaultOutcome`); the `outcome` param on `type-mismatch` is gone. + - **`@objectstack/spec`.** Upgrade guide entry `flow-condition-type-mismatch-refused` (semantic: no mechanical rewrite). The `validation-type-mismatch-refused` entry's acceptance criteria now say a create answers 201, the data door's status. + + **FROM → TO.** Flow condition `name > 5` on a text field → `size(name) > 5` for its length, `score > 5` on a number field for a value, or `double(name) > 5` only when every value is numeric text. A boolean: `active > 0` → `active == true`. A consumer keyed to the code: `type-mismatch` with `params.outcome === 'fail-closed'` → `type-mismatch-fail-closed`. + + **The renamed code shipped in 18.3.0.** `@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` already serve #29904's fail-closed refusal as `type-mismatch` with `params.outcome: 'fail-closed'` (the `outcome` param is in the 18.3.0 `ExpressionRefusalParams` type), although 18.3.0's changelog has no line for it: its changeset (`29904-type-mismatch-fail-closed.md`) was not consumed by that release. This release renames that arm `type-mismatch-fail-closed` and removes the `outcome` param, so a consumer keyed to 18.3.0's `type-mismatch` + `outcome` moves to the new code (FROM → TO above). The same changeset also says flow conditions keep the warning; a record-triggered flow's start and edge conditions are now refused, as above. +- e8afd1e: fix(lint)!: every flow condition's text-vs-number comparison gets a verdict that says what the run does — a record-triggered flow's `record.` read and `decision` branch are refused like its bare name, a time-relative sweep of the flow's object is refused like a record-triggered flow, a bare name a flow variable holds is no longer typed as the field, and any other flow's warning says the run fails instead of "evaluates to null" (#29959) + + Clause-②: yes (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses, on a record-triggered flow, a `type-mismatch` in a `decision` branch and in a `record.` / `previous.` read of a start or edge condition, which it passed with no verdict before; refuses the same on a time-relative flow sweeping the object its conditions are typed against, where 18.3.0 served a warning; and a flow condition's `type-mismatch` warning is served under its own code `type-mismatch-run-may-fail` instead of `type-mismatch`. Added beside them (the widening half): the optional `severity` and `triggerKind` inputs of the `validate_expression` MCP tool, the code `type-mismatch-run-may-fail` in `@objectstack/formula`'s `ExpressionRefusalCode`, the optional `flowVariables` key of `@objectstack/formula`'s `ExprSchemaHint`, and a bare flow-condition name that a declared flow variable or node id holds is no longer typed as the field it shadows (no verdict where 18.3.0 served a `type-mismatch` warning). + Metadata-protocol: narrowed + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names the decision branch, the `record.` read and the warning on other flows) and the MCP skill text in `packages/mcp/src/skill-md.ts` (the two new `validate_expression` inputs); `skills/**` teaches no text-vs-number comparison in a flow condition (grep for `type mismatch`, `evaluates to null`, `validate_expression` and the decision examples), so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Record-triggered flows — before.** The start and edge conditions refused `name > 5` on a text field (`type-mismatch-run-fails`), but `record.name > 5` in the same slot, and `name > 5` in a `decision` node's `config.conditions[].expression`, published with no verdict at all. Both fail the run the same way: the engine's `evaluateCondition` throws on the fault, the run binds the trigger record both as `record` and flattened to bare names, and a decision branch is evaluated over the same variables as an edge. + - **Now.** Both are refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with the message the bare name already gets: the run fails at the condition and the triggering write still lands. `previous.` is judged the same way. The date verdicts stay on the bare name only, as before. + - **Time-relative flows.** A sweep launches each run with the swept row as its record, so on a flow whose `timeRelative.object` is the start node's `objectName` (or is omitted) the start, edge and `decision` conditions get the record-triggered verdict above, bare name included: `type-mismatch-run-fails`, where 18.3.0 served a `type-mismatch` warning. A sweep of another object keeps the warning below. + - **Any other flow** (no `triggerType`, a `schedule` flow, an `api` flow) and an inert node `condition` keep a warning. Its text said the expression "silently evaluates to null", which is false on every flow: a condition that faults fails the run. It is now `type-mismatch-run-may-fail`, worded for the failed run, and a warning because such a flow is handed whatever its entrance carries (an API request body, an action's partial record, a parent flow's record, or nothing), so the field's type is not proven to be what the name holds. A `record.` read and a `decision` branch there get the same warning. The flow registration log line reads the same text. + - **A bare name a flow variable holds.** The run seeds a flow's declared variables (and each node's outputs, under its id) before it flattens the record, and the flattening skips a name already bound. So in a flow that declares `variables: [{ name: 'title', type: 'number' }]` over a text field `title`, a bare `title > 5` reads the number and runs; it was refused as `type-mismatch-run-fails` on the head that added that error, and warned on 18.3.0. It now gets no type verdict (the `flow-variable-shadows-field` warning still names the shadowing). `record.title > 5` reads the field and stays refused. + - **`validate_expression` (MCP).** Two optional inputs, so the tool answers what the gate will: `severity` (`error` | `warning` | `info`, site `validation`; default `error`) gives an advisory rule the gate's `type-mismatch-fail-open` instead of the fail-closed verdict, and `triggerKind` (`record_change` | `time_relative` | `schedule` | `api` | `none`, site `flow_condition`; default `record_change`) gives a `schedule`, `api` or `none` flow the gate's warning instead of the run-fails error (`time_relative` answers run-fails, its record being a row of `objectName`). Either input on a site it does not describe is refused as a tool error. A call that passes neither answers as before. + - **`@objectstack/spec`.** Upgrade guide entry `flow-record-read-and-decision-type-mismatch-refused` (semantic: no mechanical rewrite). + + **FROM → TO.** In a record-triggered flow, or a time-relative flow sweeping its own object: `record.name > 5` (or a decision branch `name > 5`) on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text. A boolean: `record.active > 0` → `record.active == true`. A consumer keyed to the code of a flow condition's warning: `type-mismatch` → `type-mismatch-run-may-fail` (`@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` serve `type-mismatch` there; the record-scoped `type-mismatch`, on a formula field or a display predicate, is unchanged). +- 85a8fa0: fix(lint)!: under a package closure, a dashboard widget `filter` or report `runtimeFilter` on another package's dataset is judged against that dataset's object, so a key the object lacks is refused at publish (#30005) + + Clause-②: no (narrowing) — the runtime publish door, for a write that names its package, refuses a dashboard widget `filter` key or a report `runtimeFilter` key on a dataset outside the package's closure when the dataset's object has no such field, a body it accepted before. Nothing is widened: `narrowObjectsToPackageClosure` keeps its signature and its four-limb answer, and no export is added to any package entry. + Metadata-protocol: narrowed + Skills-docs: updated here — `content/docs/deployment/validating-metadata.mdx` (the fourth-door paragraph) now names the fifth closure limb for dashboard and report writes; `skills/**` teaches neither the rules nor the closure. + + + + **BREAKING** accept-set narrowing at the runtime publish door for `dashboard` and `report` writes (the per-item publish and the package draft publish; Studio, REST and MCP authors), for a write whose package is known. `os validate`, `os build` and `os lint` are unchanged: they never narrow to a closure. + + - **Before.** The publish gate narrows the live `objects` to the written package's closure (the package, its declared dependencies, platform objects, rows with no package) but carries every dataset. A runtime package's dashboard on `showcase_task_metrics`, a dataset the showcase package ships over `showcase_task`, therefore found the dataset but not its object, and `widget-filter-field-unknown` and `report-filter-field-unknown` skipped the filter as "object not in this snapshot". `filter: { no_such_field: 'x' }` published `200`, and every reader saw an empty tile or an error panel. + - **Now.** A dashboard or report write also carries the base object of each dataset it binds (`widgets[].dataset`; a report's `dataset` and each joined block's `dataset`), which is the object the dataset query resolves the filter against at run time. The same key is refused `422 INVALID_METADATA` at `dashboards[0].widgets[0].filter.no_such_field` (or `reports[0].runtimeFilter.no_such_field`), the verdict the same key already got on a dataset inside the closure or with no closure in force. + - **Which rules now judge such a dataset.** Every rule that resolves a dashboard's or report's filter against the dataset's object, all six already judging datasets inside the closure: `widget-filter-field-unknown`, `widget-filter-field-not-included`, `dashboard-filter-field-unknown`, `dashboard-filter-field-not-included`, `dashboard-filter-field-unprovisioned` and `report-filter-field-unknown`. + - **A dimension name as a widget filter key.** On a dataset outside the closure, a widget `filter` key that names a dimension of the dataset but no field of its object is now refused with `widget-filter-field-unknown`, as it already was on a dataset inside the closure. The dataset query binds such a key; the correction of that verdict, for both cases, is tracked in #30006. + - **Accepted, as before.** Every key the object has, a registry-injected system column, a dotted relationship path, every `$` operator. A dotted key whose hop lands on an object the snapshot still does not carry is not judged. + - **Not changed.** Only dashboard and report writes carry the extra objects. A flow trigger, a list view or a dataset base object outside the closure is still refused as before (`flow-trigger-unknown-object`, `list-view-object-outside-closure`, `object-reference-unknown`), and a list view's `chart.dataset` brings no object in. + + **What an author does.** Nothing for a board or report whose filter keys name real fields. A refused key never filtered anything, so there is no rewrite that keeps what the author meant: name a field of the dataset's object. The refusal names the key and the nearest field. +- 3a27208: fix(lint)!: a stored sort naming a field that declares `sortable: false` is refused by `os validate` and at publish, judged by the same sortability projection the Data API list door refuses it by — on a list view and on every stored page sort (#30023) + + Clause-②: yes (narrowing) — `sort-field-unsortable` (error) now also fires on a sort naming a field whose author declared `sortable: false`, on the list-view rungs it already walked and on every stored page sort it did not walk before (a block's own `sort`, every element `dataSource.sort`, a related list's `parentDataSource.sort`, a list page's `interfaceConfig.sort`, an app page's `record.sort`, where a `formula` sort is now refused too), so `os validate` / `os build` / `os lint` and the `page` and `view` publish doors refuse metadata they used to accept. The widening half: `@objectstack/lint` exports `indexObjectSortability` and the `SortabilityFields` type, and `checkSortDeclaration` takes an optional trailing `sortability` index. + Metadata-protocol: narrowed + Skills-docs: none — `skills/**` names no `sortable` field key and no page or view `sort` rule; `content/docs/**` already teaches `sortable: false` as "do not allow sorting by this field". The prescription ships in the finding's fix line, `os explain sort-field-unsortable` and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing at authoring (`@objectstack/lint`): a stored sort the Data API list door refuses is refused before it is stored. + + - **Before.** A list view whose `sort` named a field marked `sortable: false` passed `os validate` and the `PUT /api/v1/meta/view` door, then answered `400 INVALID_SORT` on its first fetch and on every fetch after it. No page sort was judged for sortability at all: a `record:related_list`, `record:line_items`, `list-view` or `object-*` block sorting by such a field (or by a `formula` field), an element `dataSource.sort`, a related list's `parentDataSource.sort`, a list page's `interfaceConfig.sort` and an app page's `record.sort` all saved green and failed at read. + - **Now.** `validateSortableFields` judges each of those positions by `resolveObjectSortability`, the projection the list door refuses by, and reports `sort-field-unsortable` at `error` with the door's own clause ("a field on object X that declares `sortable: false`") at the slot's path. The reference-integrity suite now runs on a `page` write for this one member, so the Studio, REST `/meta` and MCP page doors give the same refusal (422). Only the literal `false` counts: a field with no `sortable` key, or `sortable: true`, sorts as before. A `formula` field also marked `sortable: false` keeps its virtual-field verdict. Existence on page sorts stays `page-field-unknown`. + - **Not changed.** A stored view or page already carrying such a sort keeps loading and serving; only its next save is judged, and its first fetch still answers 400 as it did since the list door started refusing it. + + **The remedy.** Sort by another field, or remove `sortable: false` from the field to make it sortable. + + **Supersedes, in the same release.** The `@objectstack/spec` / `@objectstack/metadata-protocol` / `@objectstack/mcp` note for #29896 says "Lint does not yet judge these at authoring." From this release it does: `os validate` and the publish doors refuse every stored sort that note lists, and the stored page sorts it did not list. +- cfb602c: fix(lint)!: a time-relative flow whose start node names no `objectName` has its conditions typed against the object it sweeps (`timeRelative.object`), so a text-vs-number comparison, a read of a field the swept object does not declare and date arithmetic there are refused like on a sweep of the start node's object; a sweep of another object is told why it only warns; and `validate_expression` takes the swept object (#30026) + + Clause-②: yes (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses, in the start, edge and `decision` conditions of a time-relative flow whose start node names no `objectName`, a `type-mismatch` (`type-mismatch-run-fails`), a `record.FIELD` read of a field the swept object does not declare (`unknown field`), and date arithmetic (`date-arithmetic-run-fails` / `date-text-arithmetic-run-fails`), which it passed with no verdict at all before. Added beside it (the widening half): the optional `sweepObject` input of the `validate_expression` MCP tool. + Metadata-protocol: narrowed + Skills-docs: updated here — the MCP skill text in `packages/mcp/src/skill-md.ts` (the new `sweepObject` input); `skills/**` teaches no time-relative flow condition with a text-vs-number comparison, so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Before.** A time-relative flow was typed only against its start node's `objectName`. A start that named none, with the swept object in `timeRelative.object` (the key the descriptor requires), got no type verdict on its conditions at all: `title > 5` on a text field published clean, and every run the sweep launched failed at that condition. + - **Now.** The conditions of such a flow are typed against `timeRelative.object`: each run's record is a row of that object, so its start, edge and `decision` conditions get every verdict a sweep of the start node's object already gets. Three of them are new refusals there: a text-vs-number comparison (`type-mismatch-run-fails`), a `record.FIELD` read of a field the swept object does not declare (`unknown field FIELD on OBJECT`; the run fails on the missing key), and arithmetic on a date field (`date-arithmetic-run-fails`, or `date-text-arithmetic-run-fails` against text). The shadowing and near-miss warnings and the root check are judged against the same object. + - **A sweep of another object** (the start node's `objectName` is not `timeRelative.object`) keeps the `type-mismatch-run-may-fail` warning, and the flow now gets one more warning, at its start node's `config.objectName`, saying why: the conditions are checked against that object's fields while each run's record is a row of the swept one. It is served only on a flow that got a warning it explains. + - **`validate_expression` (MCP).** New optional input `sweepObject` (site `flow_condition` with `triggerKind: 'time_relative'` only; a tool error anywhere else): the flow's `timeRelative.object`. When it is another object than `objectName`, a type mismatch is the gate's warning instead of the run-fails error. A call that does not pass it answers as before. + - **Correction to the 18.x changeset of the flow-condition type check.** It described a time-relative flow whose `timeRelative.object` "is omitted" as sweeping the start node's object. That shape never binds: `TimeRelativeTriggerSchema` requires `object`, and the gate refuses its absence (`flow-time-relative-descriptor-invalid`). Such a flow now gets the warning, not the run-fails verdict, on its conditions, since no run of it binds a row. + + **FROM → TO.** In a time-relative flow whose start names no `objectName`, sweeping `crm_opportunity` with a text field `title` and a date field `close_date`: `title > 5` (or `record.title > 5`, or a decision branch `title * 2 > 10`) → `size(title) > 5` for its length, a number field for a value, or `double(title) > 5` only when every value is numeric text. `record.nope > 1`, where `crm_opportunity` declares no `nope` → the field's declared name, or the object that declares it as `timeRelative.object`. `close_date + 1 > …` → `addDays(close_date, 1) > …` to shift a date (`addMonths(d, n)` and `daysFromNow(n)` likewise), or `daysBetween(a, b)` for a span in whole days. A flow whose start names one object and sweeps another: set the start node's `objectName` to the swept object, or remove it. +- c045c34: fix(lint)!: a stored page sort naming a field its object lacks is refused by `os validate` and at the page publish door (`page-field-unknown`, error), on every page sort slot (#30093) + + Clause-②: no (narrowing) — an unknown sort field on a page slot moves from silent or warning to error: `os validate` / `os build` / `os lint` and the `page` publish door refuse a page sort they accepted before. + Metadata-protocol: narrowed + Skills-docs: updated here — content/docs/deployment/validating-metadata.mdx + + + + **BREAKING** accept-set narrowing at the page publish door (`PUT /api/v1/meta/page/NAME`; Studio, REST and MCP authors) and in `os validate` / `os build` / `os lint`. + + - **Before.** A page sort naming a field the object does not have saved green, and its first read, and every read after it, answered `400 INVALID_SORT` at the list door. A list page's `interfaceConfig.sort` and a `record:related_list` block's `sort` drew a `page-field-unknown` warning on the CLI alone; an element `dataSource.sort`, a related list's `parentDataSource.sort`, an app page's `record.sort` and the own `sort` of a `record:line_items`, `list-view` or `object-*` block were read by nothing. + - **Now.** Every one of those slots is judged by `validateSortableFields`, the reference-integrity member the page publish door already runs, and an unknown name is `page-field-unknown` at `error`, at the slot's path, bound to the object the slot's renderer reads (`dataSource.object`, else the block's object prop; `interfaceConfig.source`, else the page's `object`; `parentDataSource.object`; `record.object`). The page door refuses it `422 INVALID_METADATA`. A registry-injected column on an `external` object is `page-field-unprovisioned` at `warning`, as before. `validatePageFieldBindings` no longer reads a page sort, so each slot has one reader. + - **Not judged.** A dotted relationship path (the list door refuses it as its own verdict), a system column such as `created_at`, an object the stack (or the publish snapshot) does not carry, and an object with no field map. + - **Not changed.** A stored page already carrying such a sort keeps loading; only its next save is judged, and its read answers 400 as it did before. + + **What an author does.** Name a field the object declares, or add the field to the object. A refused name never sorted the page, so there is nothing to keep. +- 54b491a: fix(lint)!: a time-relative flow whose start node names, in `config.objectName`, another object than the one it sweeps (`config.timeRelative.object`) is refused with `flow-time-relative-object-mismatch`: each run binds a row of the swept object, while the start's `objectName` is what its conditions are checked against (#30283) + + Clause-②: yes (narrowing) — a new published readiness rule id, `flow-time-relative-object-mismatch`, graded `error`: the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses a time-relative start whose `config.objectName` is set and is not `config.timeRelative.object`, which it passed with no readiness verdict before. + Metadata-protocol: narrowed + Skills-docs: none — `skills/**` and `content/docs/**` teach the time-relative sweep with `timeRelative.object` alone and no `objectName` on its start node, which this rule accepts; the MCP skill text keeps its `sweepObject` advice for `validate_expression`, whose expression verdict this change does not move. + + + + **BREAKING** accept-set narrowing at the author-time flow readiness gate, served by `os build`, `os validate`, `POST /api/v1/packages/:id/publish-drafts` and the flow publish door. + + - **Before.** A time-relative start could name `config.objectName: 'A'` and sweep `config.timeRelative.object: 'B'`. The run binds only `B`: the engine's binding resolver reads `timeRelative.object` first, and the trigger launches each run with a `B` row as `record` and `B` as the run's object. The expression checks typed the flow's conditions against `A` all the same. Measured both ways: `record.amount > 10`, where only `A` declares `amount`, passed every check and failed every run (`No such key: amount`); `record.priority > 1`, a correct read of a `B` field, was refused as `unknown field priority on A`. + - **Now.** That start is refused at `config.objectName`, naming both objects and the fix. A start whose `objectName` equals `timeRelative.object`, or that names none, is not reported; neither is a start whose `triggerType` routes the flow to the record-change trigger (a `record-` token, or an array holding one), which binds `objectName` and never sweeps. The runtime publish gate judges only the flow being written, so a stored flow that names two objects keeps being served; its own next publish is refused. + - **The `config.objectName` warning on a sweep of another object** (served where a condition's type verdict was softened to a warning) now names `flow-time-relative-object-mismatch` as the verdict that asks for the same fix. + + **What an author does.** Nothing for a time-relative start that names no `objectName`, or names the object it sweeps. A start that names two objects says which one the author meant, and nothing mechanical can: for the swept one, remove `config.objectName` (the sweep never reads it, so the run is unchanged) or set it to the value of `config.timeRelative.object`, then correct any condition the expression checks now refuse against that object; for the named one, point `config.timeRelative.object` (and its `dateField`) at it instead. The refusal names both objects. +- 482e089: fix(lint)!: the field-name indexes judge an object as the registry serves it, so a read, write or sort naming a field an `objectExtensions` entry adds is no longer refused as unknown, and the readonly, sortability and translation rules judge that field by its served definition (#30471) + + Clause-②: yes (narrowing) — `os validate` / `os build` / `os lint` refuse three shapes they accepted before: a hook body's literal `ctx.api.object(NAME).update(…)` / `.updateById(…)` / `.insert(…)` writing a field an `objectExtensions` entry adds and declares `readonly: true` moves from a `hook-body-write-unknown-field` warning to a `hook-api-update-readonly-field` error; and a translation key naming a field or validation rule that only a replaced earlier entry of the same package adds, or only a top-level `objectExtensions` beside `packages[]` adds, is now `translation-target-unknown` at `error`, where it resolved, and with that top-level-beside shape no longer read, a translation subtree `objects.X` keyed to an object the stack does not define and only such an entry extends loses the extended-object skip (rung 2b) and is `translation-target-unknown` at `error` too; and a field an `objectExtensions` entry RE-DECLARES on its owner's object is judged on the extension's definition, which replaces the owner's whole in the fold as in the registry's `mergeObjectDefinitions` (`{ ...base.fields, ...extension.fields }`), so an owner `amount: number` re-declared as `formula`, `readonly: true` or `text` now gets that definition's `sort-field-unsortable`, readonly-write or expression type-mismatch verdict, each at `error` (ADR-0146 D10 already refuses that shape on the extension; no CLI door does). The widening half: a formula, validation, flow condition, hook condition, sharing-rule condition, flow-node, action-body or hook-body write, react page field prop, translation key, page sort or list-view sort naming a field an `objectExtensions` entry of the same artifact adds is no longer refused as an unknown field. + Metadata-protocol: narrowed + Skills-docs: updated here — content/docs/automation/hook-bodies.mdx + + + + **BREAKING** accept-set narrowing in `os validate` / `os build` / `os lint` (`@objectstack/lint`), for multi-package artifacts whose bodies add fields to another package's object through `objectExtensions`. + + - **Before.** The field-name indexes of the expression passes (validation rules, formulas, flow and hook conditions, sharing-rule conditions), the three write rules, a react page's field props, the three readonly write rules and the sort rule read an object's declared `fields` only, so a name an `objectExtensions` entry adds was refused as unknown although the runtime serves the field: `flow-node-write-unknown-field` and `sort-field-unknown` / `page-field-unknown` (page sorts) at `error`, `action-body-write-unknown-field`, `hook-body-write-unknown-field` and `page-field-unknown` (react props) at `warning`, the expression passes' unknown-field error. The translation rule folded extensions through a reader of its own that kept every entry and also read a top-level `objectExtensions` beside `packages[]`. + - **Now.** Every one of them reads the one shared fold in `object-graph.ts` (since #30383 the field-path rules' fold): the declared fields plus each field an `objectExtensions` entry adds, one entry per package per object, `packages[]` alone when the artifact carries it. A name no body declares or adds keeps the same refusal, rule id, severity and message; a misspelling within edit distance 3 of an extension-added name now gets a did-you-mean naming it. A name the fold adds is judged by its served definition, so these findings move (union artifact and per-package leg alike): + - a hook body's `ctx.api` update / updateById / insert of a `readonly: true` field: `hook-body-write-unknown-field` `warning` → `hook-api-update-readonly-field` `error`, the severity a declared readonly field already gets (a non-system hook's `ctx.api` write is stripped at run time and the call returns success); + - a translation key naming a field or rule only a replaced earlier entry of the same package, or only a top-level `objectExtensions` beside `packages[]`, adds: resolved → `translation-target-unknown` `error`, as the registry serves the object; + - a translation subtree `objects.X` keyed to an object the stack does not define and only a top-level `objectExtensions` beside `packages[]` extends: skipped as an extended object (rung 2b) → `translation-target-unknown` `error`; + - an owner's field an `objectExtensions` entry re-declares is judged on the extension's definition, which replaces the owner's whole as the registry's `mergeObjectDefinitions` does (`{ ...base.fields, ...extension.fields }`): an owner `amount: number` re-declared as `formula` and sorted (page or list view) → `sort-field-unsortable` `error`; re-declared `readonly: true` and written by a flow `update_record` or a hook's `ctx.api` update → `flow-update-readonly-field` / `hook-api-update-readonly-field` `error`; re-declared `text` and multiplied in a validation rule → the expression type mismatch at `error`. Each passed before, the owner's definition being the one judged. ADR-0146 D10 refuses this shape on the extension at registration; no CLI door does; + - a flow `update_record` / `create_record` of a `readonly: true` field: `flow-node-write-unknown-field` `error` → `flow-update-readonly-field` `error`; + - a page sort (every slot) or list-view sort by a `formula` field or one declaring `sortable: false`: `page-field-unknown` / `sort-field-unknown` `error` → `sort-field-unsortable` `error`; + - a `readonlyWhen` field written by a flow `update_record` (`error` → `flow-update-readonly-when-field` `warning`), a hook's `ctx.api` update (`warning` → `hook-api-update-readonly-when-field` `warning`) or an action's `ctx.api` update (`warning` → `action-api-update-readonly-when-field` `warning`); + - an expression reading the field: the unknown-field error becomes the served type's verdict (a type mismatch or a missing null guard, at `error` in validation rules and hook conditions, at `warning` in formulas and sharing-rule conditions) or nothing. + Every other read, write or sort naming such a field passes: a hook's `ctx.input` stamp, an action's `ctx.api` write of a `readonly: true` field (an action runs elevated) and a `create_record` of a `readonlyWhen` field land at run time. + - **Not judged.** A field two packages write at the same deciding priority is a served name, but no rule judges its type, nullability, readonly or sortability: which definition wins is registration order. An object that declares no field of its own stays unjudged by the write, readonly and sort rules, extended or not (an introspected schema). A react page's `searchableFields` still reads declared fields only. + - **Not changed.** Which extension entries the fold keeps, the runtime, and `defineStack` (its related-list check stays additive). A stack that names no extension field reads as before. + + **What an author does.** For a hook writing a readonly extension field, declare the hook `runAs: 'system'`, stamp the value in the record's own `beforeInsert` / `beforeUpdate` hook, or drop the field from the payload. For a translation key the fold drops, key it to a field or rule of the entry the registry keeps (the package's last entry for that object, in a `packages[]` body), or delete it. +- e9731fc: fix(lint)!: the remaining field indexes (form layout, notify email-field recipient, public form file field, flow templates, page visualization bindings, searchable fields, autonumber formats) judge an object as the registry serves it, and an object that declares no field keeps the list-view rule's skip once an extension adds to it (#30608) + + Clause-②: yes (narrowing) — `os validate` / `os build` / `os lint` refuse shapes they accepted before, each a field an `objectExtensions` entry of the same artifact adds or re-declares, now judged on its served definition: a notify node's `recipients.emailField` naming an owner `email` field an extension re-declares as `text` is `notify-email-field-not-email` at `error`; a form open to anonymous intake whose section names an `image` / `file` field an extension adds, or an owner field an extension re-declares as one, is `public-form-file-field` at `error`; a `list` page whose `allowedVisualizations` leads with `gallery` over an owner `image` field an extension re-declares as `text` is `page/visualization-without-binding` at `error`; a `searchableFields` entry naming an owner text field an extension re-declares as `formula` is `searchable-field-unsearchable` at `error`; an autonumber field an extension adds whose format names a field the object lacks is `autonumber-references-unknown-field` at `error`, and an autonumber format naming an owner field declared `required: true` that an extension re-declares without it is `autonumber-references-optional-field` at `warning`; a flow template `{{ record.F.sub }}` through an owner text field an extension re-declares as a lookup is `flow-template-lookup-traversal` at `warning`. The widening half: a form section, a notify email-field recipient, a flow template hole, a gallery or calendar binding, a `searchableFields` entry (an object's own set, a list view's narrowing, a react page's ``) or an autonumber format naming a field an `objectExtensions` entry adds is no longer refused as unknown, and a list view over an object that declares no field is no longer refused once an extension adds to it. + Metadata-protocol: narrowed + Skills-docs: none — measured: no sentence in `skills/` or `content/docs/` states a declared-only reading of the moved rule ids + + + + **BREAKING** accept-set narrowing in `os validate` / `os build` / `os lint` (`@objectstack/lint`), for multi-package artifacts whose bodies add fields to another package's object through `objectExtensions`. + + - **Before.** Seven field indexes read an object's declared `fields` only: the form-layout rule (`form-field-unknown`), the notify email-field recipient rule, the public form file-field rule, the flow template rule, the page visualization binding rule, the searchable-fields index (`indexObjectSearchTargets`, behind both an object's or list view's `searchableFields` and a react page's ``) and the autonumber format lint. A field an `objectExtensions` entry adds was unknown to them although the runtime serves it, and a field an extension re-declares was judged on the owner's definition. The list-view rule, since it reads the served graph, judged an object that declares no field as having only the extension's fields. + - **Now.** Each reads the shared fold in `object-graph.ts` (`objectsAsServed`): the declared fields plus each field an `objectExtensions` entry adds, an extension's re-declared field replacing the owner's definition whole, as the registry's `mergeObjectDefinitions` does. A name no body declares or adds keeps the same refusal, rule id, severity and message. These findings move (union artifact and per-package leg alike): + - an extension-added field named by a form section, a notify email-field recipient (typed `email`), a flow template hole, a `searchableFields` entry or an autonumber format (declared `required: true`): its unknown-field verdict (`form-field-unknown` `warning`, `notify-email-field-unknown` `error`, `flow-template-unknown-field` `warning`, `searchable-field-unknown` `error`, `autonumber-references-unknown-field` `error`) → nothing; a `gallery` / `calendar` binding derives from it, so `page/visualization-without-binding` → nothing; + - the same field typed otherwise: a notify recipient typed `text` → `notify-email-field-not-email` `error` (was `notify-email-field-unknown` `error`); an autonumber format naming it not `required` → `autonumber-references-optional-field` `warning` (was `autonumber-references-unknown-field` `error`); on a public form, an `image` / `file` / `avatar` / `video` / `audio` field → `public-form-file-field` `error` (was unjudged); + - an owner field an extension re-declares: judged on the extension's type, `required`, or autonumber format (the rows the Clause-② line names), and the inverse re-declarations pass (an owner `file` field re-declared `text` on a public form, an owner lookup re-declared `text` in a template, an owner autonumber re-declared `text`); + - an autonumber field an extension adds is checked as an owner's is; + - a list view over an object that declares no field: `list-view-field-unknown` `error` → nothing, extended or not. + - a flow variable bound to a record whose name is a field an `objectExtensions` entry adds to the trigger object: the record-triggered run flattens that field to top level too, so the variable is an ambiguous root and its holes are no longer judged (`flow-template-unknown-field` `warning` → nothing), as for a variable named after a declared trigger field; + - a form section on an extended object whose owner declares `fields` in the legacy array shape: the owner's array fields are now known names, so `form-field-unknown` `warning` on them → nothing. + - **Not judged.** A field two packages write at the same deciding priority is a served name, but no rule judges its type, `required` or autonumber format: which definition wins is registration order (a gallery page over an object carrying one is not reported). An object that declares no field of its own stays unjudged by the notify, public form, page visualization, searchable and autonumber rules and by the list-view rule, extended or not (an introspected schema). The form-layout and flow template rules judged such an object against the empty set before and still do, an extension's own names now known. + - **Not changed.** Which extension entries the fold keeps, the runtime, the object-level reads (`external`, the injected anchors, an object's own list views and `searchableFields`), and the finding paths. A stack that names no extension field reads as before. A flow variable whose name no body declares or adds on the trigger object is judged as before. A form section on an object nothing extends reads only map-shaped `fields`, as before: its legacy array fields are still reported `form-field-unknown`. + + **What an author does.** Fix the extension's field definition (type an email recipient `email`, keep a file field off a public form, give a gallery an image field, keep a search column stored, mark an autonumber scope field `required: true`) or the reference that names it. + +### Patch Changes + +- 1756666: fix(lint): a widget filter keyed on a dataset dimension publishes, and a filter key naming a dataset measure is refused with its own message + + Clause-②: yes (widening) + Metadata-protocol: none + Skills-docs: updated here — content/docs/deployment/validating-metadata.mdx + + `widget-filter-field-unknown` and `report-filter-field-unknown` now judge a bare filter key against one accept set, held in `judgeDatasetFilterKeys`, so the two rules cannot drift: a field of the dataset's object (authored or registry-injected), then a dimension of the dataset. + + - **Widened:** a dashboard widget's own `filter` keyed on a dataset dimension's name (`{ state: 'open' }` for a dimension `state` over the field `status`) was refused at publish as an unknown field, although the dataset query binds it through the dimension's field. It now publishes, as the same key on a report's `runtimeFilter` already did. + - **Refused with its own message:** a bare key naming a dataset measure was already refused by both rules as an unknown field. It is now refused as a measure: a filter compares each row before aggregation, so the dataset query would compare the measure's raw column (`{ amount_sum: { $gt: 100 } }` compiles to `amount > 100`, not a filter on the total), or no column at all for a count, derived or duration measure. The fix line names the measure's column when it reads exactly one. A measure that shares a field's name is still the field, and accepted. + + No key that published before is refused now. +- f9e5bef: feat(trigger-api): an `api` hook verifies the vendor signature its start node declares (`config.signature`) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/trigger-api`.** `start()` reads the hook's one verifier with `apiTriggerVerifierOf(binding.config)` (`@objectstack/spec/automation`). A start node that declares its sender's scheme (`config.signature`: DocuSign Connect's base64 `X-DocuSign-Signature-1`, a Stripe-style `t=…,v1=…`, a GitHub-style `sha256=`) now admits a correctly signed post that was answered `401` before. That scheme REPLACES the `x-objectstack-signature` check for its hook: the declared header is read on both faces of the door (the self-hosted mount and `handleInboundHook`) with `Headers.get`, so a repeated field is its one joined value; the HMAC-SHA256 is computed under the key `config.secretRef` names (read at every post through the engine's reader, `503` while it has no value), over the raw body or `.`; each digest is decoded strictly to 32 bytes and compared with `timingSafeEqual`, any one of several `v1` elements matching (a key roll); and a timestamped scheme is held to `HTTP_SIGNATURE_TOLERANCE_SECONDS` from `@objectstack/core`. A body-only scheme has no replay window, as the contract states. A valid `x-objectstack-signature` admits nothing on such a hook, and a post without the declared header is refused. A declaration the spec answers `unusable` arms a hook that refuses every post before any secret is read and never falls back to `x-objectstack-signature`; a declared scheme bound without the engine's `config.secretRef` reader is refused at `start()`. Every failure is the same `401 INVALID_SIGNATURE` with its own cause in the server log (`declared-header-absent`, `declared-header-malformed`, `declared-prefix-missing`, `declared-no-digest`, `declared-mismatch`, `declared-outside-window`, `declared-unusable`), never the key, the header value or the body. A hook that declares no scheme is verified exactly as before. + - **`@objectstack/lint`.** `validateFlowApiTriggerSecret` grades an `api` hook by that same reader: a usable declaration is ready on its `config.secretRef` alone, and an unusable one is the new error `flow-api-trigger-signature-unusable` (with its `os explain` entry), located at the key the spec refuses. It fires only on a declaration the flow parse already refuses, so no stack that passed before is refused now. + - **`@objectstack/skills`.** The automation and api skills teach `config.signature` where they said a vendor-signed callback cannot be admitted. +- 0f3ee58: fix(formula,lint): a date-arithmetic verdict in a flow condition says the run fails, not that the value reads null + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/formula`: four codes join `ExpressionRefusalCode`**: `date-arithmetic-run-fails`, `date-arithmetic-run-may-fail`, `date-text-arithmetic-run-fails` and `date-text-arithmetic-run-may-fail`, with the same params as `date-arithmetic` / `date-text-arithmetic`. A flow condition (a `'flattened'` site) that does arithmetic on a date field used to be told the field "silently evaluates to null", but the engine's `evaluateCondition` throws on the fault on every flow, so the run fails at the condition. Where the caller declares the run proven to hold the field (`faultOutcome: 'run-fails'`, with `bareNamesUnproven` unset) the verdict is served under `-run-fails` and says the run fails; anywhere else in a flow it is served under `-run-may-fail` and says the run fails wherever the name holds the field's date when the condition runs (a flow started by an API call, an action or a parent flow binds what its entrance hands over, which can be a number). The grade is unchanged: an error at every site. A `record`-scoped site (a formula field, a validation rule, a display predicate) keeps `date-arithmetic` / `date-text-arithmetic` and their sentence. A consumer keyed to the old codes on a flow condition re-keys to the new ones. + - **`@objectstack/lint`**: the fault-outcome ledger's docblock names the date verdicts' flow codes; the routing is the existing `faultOutcome` / `bareNamesUnproven` hint, unchanged. +- 29cbfcc: fix(lint): a flow condition's bare-name type verdict no longer refuses a name a `screen` field holds, and stands down to the warning after a node that can bind any name (#30025) + + Clause-②: yes (widening) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) no longer refuses, on a record-triggered flow (or a time-relative sweep of its own object), a `type-mismatch-run-fails` on a BARE condition name that a `screen` node's declared field holds (no verdict now), nor on a bare name in a flow with a node that can bind a name no reader sees (`script`, `connector_action`, `wait`, `subflow`, `map`, a screen with no input contract), which is now the `type-mismatch-run-may-fail` warning. A `record.` / `previous.` read keeps its refusal, and nothing new is refused. The null-guard verdict's bare names also leave a screen's field names out. Added beside them: the optional `bareNamesUnproven` key of `@objectstack/formula`'s `ExprSchemaHint`, and `collectScreenFieldNames` in `@objectstack/lint`'s flow-variable-scope module. + Metadata-protocol: none + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names the screen field and the open binder); `skills/**` and the MCP skill text teach no bare flow-condition name a screen or an open binder holds, so no skill or eval moves. + + - **Before.** A record-triggered flow whose `screen` node collects `title` (a number) and whose edge after it reads `title > 5`, with `title` a text field on the trigger object, was refused `type-mismatch-run-fails`: the bare name was typed as the field. The run binds the collected number there (a resume lands a screen's values under their plain names), so the condition runs. The same refusal hit a bare name after a `script`, `connector_action`, `wait`, `subflow` or `map` node, or a screen with no input contract, any of which can bind the name to a value nothing can see at build. + - **Now.** A screen's declared field names join the names a bare read holds instead of the field, so they are not typed as it (the null-guard verdict leaves them out too). In a flow with a node that can bind any name, a bare-name mismatch is the `type-mismatch-run-may-fail` warning, as the null-guard verdict already stands its bare names down there; `record.` is still refused. The stand-down reads the flow's nodes only: an entrance (and the publish door's per-write snapshot, which sees none) changes which record both the bare name and `record.` read, not whether a node rebinds the bare name, so a bare-name refusal on a record-triggered flow with no such node is unchanged at every door. +- aa57c76: feat(spec): three forward date-range presets: `next_month`, `next_quarter` and `next_30_days` + + Clause-②: yes (widening) + Metadata-protocol: added + + The relative dates a dashboard date filter and the filter builder offer looked only backward. A forecast asks forward: what closes next month, next quarter, in the next 30 days. Those questions had to use a custom range, whose fixed dates go stale. + + `DATE_RANGE_PRESETS` now also declares `next_month` (the whole of next month), `next_quarter` (the whole of next quarter) and `next_30_days` (today and the 30 days after it, the mirror of `last_30_days`). Every place that accepts a preset accepts them: `dashboard.dateRange.defaultRange`, a date global filter's `defaultValue`, the filter builder's `between` row and `AnalyticsQuery.timeDimensions[].dateRange`. They sit beside the family they extend in the filter bar: `next_month` after `last_month`, `next_quarter` after `last_quarter`, `next_30_days` after `last_90_days`. Their `DATE_RANGE_PRESET_MACRO_WINDOWS` rows follow the inclusive last-day convention: `['{next_month_start}', '{next_month_end}']`, `['{next_quarter_start}', '{next_quarter_end}']` and `['{today}', '{30_days_from_now}']`. A preset name written as a bare filter comparand is refused with that window as the prescription, as for the other presets. + + `@objectstack/core`'s `resolveAnalyticsDateRangePreset` resolves the three in its half-open form. `next_30_days` stops before `{31_days_from_now}`. `next_month` and `next_quarter` stop before the month and the quarter after next, which no macro token names: the resolver reads `next_month_start` (or `next_quarter_start`) again from the start of the day it named, so the end is still a day the one token resolver computed. No macro token is added. + + In the console, the three presets have labels in all ten locale packs, and the client date-macro resolver now answers `{next_month_end}` and `{next_quarter_end}`, two spec tokens it did not resolve before. `@objectstack/verify`'s dashboard-window proof also writes fixture rows on a forward preset's future edges, so it judges the three new windows; the days it writes for the other presets are unchanged. `@objectstack/lint` changes documentation only. +- c133cfe: fix(lint)!: the object publish door refuses an object whose own list view sorts by a field the list door refuses (`sortable: false`, a `formula`, or a name the object lacks) (#30095) + + Clause-②: no (narrowing) — an object write the door accepted before is refused: an object whose built-in `listViews.*.sort` names a field the list door answers `400 INVALID_SORT` now saves `422 INVALID_METADATA`. + Metadata-protocol: none + Skills-docs: none — `skills/**` names no `sortable` field key, and `content/docs/deployment/validating-metadata.mdx` lists the doors by rule family, not by reference-integrity member, so it names no object crossing to correct; the prescription ships in the refusal text. + + + + **BREAKING** accept-set narrowing at the object publish door (`PUT /api/v1/meta/object/NAME`; Studio, REST and MCP authors). + + - **Before.** `validateSortableFields` judged a list view's `sort` on the CLI, at the `view` door and at the `page` door, but not on an `object` write. An object whose built-in list view sorted by a field declaring `sortable: false`, by a virtual `formula`, or by a name the object does not have saved green, and that list view answered `400 INVALID_SORT` on its first load and every load after it. + - **Now.** The member declares `object`, so the object door refuses that save `422 INVALID_METADATA` with `sort-field-unsortable` or `sort-field-unknown` at `error`, at the sort key's path (`objects.NAME.listViews.KEY.sort[i]`). Nothing is stored. + - **Not changed.** A stored object already carrying such a sort keeps loading; only its next save is judged, and a stored sibling in violation is never charged to another object's write. A `?mode=draft` save is not judged, as before. + + **What an author does.** Sort the list view by a sortable field, or remove `sortable: false` from the field it names. A refused sort never ordered the list, so there is nothing to keep. +- 778aedf: fix: the Business Unit page and the `security-private-no-readscope` hint say what needs the enterprise edition + + Clause-②: no + Metadata-protocol: none + + - **Business Unit page (`sys_business_unit` description, en and zh-CN).** It used to say units are "used to scope data and reports". It now says what a unit does in the open edition: sharing rules can share records with a unit, or a unit and the units under it, and members get that access and lose it when they leave. It also says that letting a permission set read every record of a unit (read scope `unit`) needs the enterprise edition. That matches the compile refusal, which is unchanged. + - **Business unit Timezone help (en and zh-CN).** It used to say date boundaries are computed in this zone. Nothing reads the column, so the help now says it changes no date calculation and that Settings → Localization → Default timezone is the one in effect, as the organization's Timezone help already does. + - **`security-private-no-readscope` hint (`os validate`, `os lint`).** The hint now offers only the scopes the stack can compile. When `requires` lists `hierarchy-security`, nothing changes: it offers `own_and_reports`, `unit`, `unit_and_below` and `org`. Without it, the hint offers `org` (or widening the object's `sharingModel`), and says that the three hierarchy scopes need `requires: ['hierarchy-security']`, which `@objectstack/security-enterprise` provides. The rule still fires on the same grants at the same severity. +- 67d5d53: fix(formula,lint,mcp): a fail-closed date verdict says the write is refused, and a time-relative sweep's run-fails verdict names no firing write + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/formula`: three codes join `ExpressionRefusalCode`** (`EXPRESSION_REFUSAL_CODES`), with the params of the code they split from: + - `date-arithmetic-fail-closed` and `date-text-arithmetic-fail-closed`. On a site that fails closed (`faultOutcome: 'fail-closed'`: an `error` validation rule, a field `requiredWhen` / `readonlyWhen`, an option `visibleWhen`, a hook `condition`), date arithmetic used to be told "this faults at runtime, so the field silently evaluates to null". There the fault refuses the write instead, and the verdict now says so. A formula field and an advisory (`warning` / `info`) validation rule keep `date-arithmetic` / `date-text-arithmetic` and their sentence. A consumer keyed to the old codes on a fail-closed site re-keys to the new ones. + - `type-mismatch-sweep-run-fails`. A time-relative sweep of the typed object is graded `run-fails` like a record-triggered flow, but no write fires its run: the sweep starts one run per row it finds. Its condition's text-vs-number verdict used to say "the write that fired it still lands". Served where the new optional `ExprSchemaHint.timeRelativeSweep` is set beside `faultOutcome: 'run-fails'`, it now says the run for that row fails and nothing else happens. A record-triggered flow keeps `type-mismatch-run-fails` and its sentence. + - The grade is unchanged at every site. What is refused, warned or passed is unchanged. + - **`@objectstack/lint`**: the flow walk passes `timeRelativeSweep` on a `time_relative` flow. The nullable-comparison verdict, which also says "still lands", runs on record-triggered flows only, so it is unchanged. + - **`@objectstack/mcp`**: `validate_expression` with `site: 'flow_condition'` and `triggerKind: 'time_relative'` passes the same key, so its answer stays the gate's. +- 8293c74: refactor(lint): the i18n translatable-key walk moves from the CLI into `@objectstack/lint` + + Clause-②: no + Metadata-protocol: none + + `collectExpectedEntries` — the one definition of which translation keys a stack can carry, each with the source text it translates — now lives in `@objectstack/lint` (`src/i18n-walk.ts`) and is exported from the package root with `authorWarnedTranslationGroups`, `humanizeFieldPath` and the `ExpectedEntry` / `ExpectedEntryOptions` types. It moved so a browser can run the same walk the CLI runs: Studio's translation editor builds its source-to-translation table from it. `os i18n extract` and `os lint`'s translation coverage keep reading it unchanged, through the CLI's extractor module, which keeps bundle assembly and serialization and re-exports the walk. + + - No key, no skeleton and no coverage verdict changes: the walk's code moved as is. + - In a browser the walk cannot read the liveness ledger from disk, so its default leaves no translation group out. That is the same set the CLI leaves out today, because the ledger warns on no translation group; a browser caller that must honour a warned group passes `warnedGroups`. + - `@objectstack/spec`: the `action` and `view` liveness ledgers cite the walk at its new path. +- ad0fbfd: fix(lint): a permission set's field key on a field that an `objectExtensions` entry adds, a sibling package's in the same artifact included, is no longer refused as `security-fls-unknown-field` (#30303) + + Clause-②: yes (widening: the FLS-key check accepts extension-added fields) + Metadata-protocol: none + + - **Before.** The FLS-key check resolved `object.field` against the object's declared `fields` only. On a multi-package artifact, an app package's permission set naming `crm_contract.o2c_probe_note`, a field its module package adds to `crm_contract` through `objectExtensions`, failed `os build`, `os validate` and `os lint` with "crm_contract declares no field o2c_probe_note", although the runtime serves the field and the key masks it. A single-package stack's own extension fields were refused the same way. + - **Now.** The key resolves against the object as the registry serves it: its declared fields plus every field an `objectExtensions` entry of any body of the artifact adds to it, folded as `@objectstack/objectql` folds them (fields by name; one entry per package per object, a later entry replacing an earlier one; `extend` naming the object verbatim). A field that no body declares or adds keeps the same refusal, with the same rule id, severity and message, and so does a field an entry only patches through `fieldPatches`, which adds no field. The hint's nearest name reads the served fields too. + - **One fold.** The controlled-by-parent merge (#23210), the membership-key column check and the FLS-key check now read extensions through one shared fold in `object-graph.ts`. +- e760204: fix(lint): the field-path rules judge an object as the registry serves it, so a view, report, dataset, page, widget, mapping, related list or row policy naming a field that an `objectExtensions` entry adds is no longer refused (#30383) + + Clause-②: yes (widening: field-path rules accept fields a sibling package adds through `objectExtensions`, which the runtime already serves; any rule that deliberately stays on declared fields says why in its header) + Metadata-protocol: none (lint judgements, no authorable key) + + - **Before.** Twelve rules resolved a field path against the objects' declared `fields` only: dataset fields and filters, report runtime filters, dashboard widget filters, list view columns and keys, an object's own field lists, page bindings, filter preset comparands, RLS predicates, sharing-rule criteria, thank-you panels, measure aggregates and the field-consumer credit for dataset paths. On a multi-package artifact, where a module package adds fields to an app package's object through `objectExtensions`, a path naming one of those fields read as unknown (for example `dataset-field-unknown`, `list-view-field-unknown`, `rls-predicate-unknown-field`) although the runtime serves the field. Two rules folded extensions through readers of their own: `related-list-ref-unknown` read only the stack's top-level `objectExtensions`, so an inbound lookup a sibling package adds was not seen, and `mapping-target-field-unknown` kept every entry, where the registry keeps one per package per object. + - **Now.** Every one of them reads the one shared fold in `object-graph.ts`: the object's declared fields plus every field an `objectExtensions` entry of any body of the artifact adds, one entry per package per object, a later entry replacing an earlier one. A field no body declares or adds keeps the same refusal, with the same rule id, severity and message. The rules that judge a field's type (measure aggregates, preset comparands, sharing-rule criteria, thank-you panels) judge an extension-added field on its served definition, as they judge a declared one. A field two packages write at the same deciding priority is still a served name, but which definition wins is registration order, so the rules that judge its type leave it unjudged, and a thank-you panel admits it only when every such definition is engine-generated. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [67d5d53] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [0e4747a] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/sdui-parser@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/lint/package.json b/packages/lint/package.json index 050ed2726ee..fdbb9fc245e 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/lint", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Static, build-time validation for an ObjectStack metadata graph — dashboard widget bindings, CEL/predicate expressions, and more. Pure (stack) => Issue[] functions shared by the CLI's `os validate` and any other consumer (e.g. AI authoring). Depends on @objectstack/spec; never on a runtime.", "type": "module", diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index ccee68b11c9..e12d6aca7dc 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,217 @@ # @objectstack/plugin-mcp-server +## 18.4.0 + +### Minor Changes + +- 140d8ce: fix(spec,metadata-protocol,mcp)!: a field marked `sortable: false` is no longer offered or performed as a sort — the sortability projection reports it unsortable, and the Data API and the stdio MCP `query_records` refuse a sort on it with `400 INVALID_SORT` (#29896) + + Clause-②: yes (narrowing) — `GET /api/v1/data/:object` (and every request that reaches the same list door: `POST /api/v1/data/:object/query`, the export route, the RPC dispatcher, the record-page `related[].sort`) and the stdio MCP `query_records` refuse a sort on a field whose author declared `sortable: false`, which they used to answer sorted by the column; a stored list view `sort` or `record:related_list` `sort` naming such a field is now refused when it is read. The widening half: `@objectstack/spec/api` exports `FIELD_UNSORTABLE_AUTHOR_DECLARED`, and `FieldSortabilitySchema.reason` accepts `'author-declared'` beside `'virtual-type'`. + Metadata-protocol: narrowed + Skills-docs: none — `skills/**` names no `sortable` field key, and `content/docs/**` already teaches `sortable: false` as "do not allow sorting by this field" and `INVALID_SORT` as "the field must exist and have `sortable: true`", which is what the platform now does. The prescription ships in the refusal text and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing at the Data API list door (`@objectstack/metadata-protocol`) and the stdio MCP `query_records` (`@objectstack/mcp`): a sort on a field marked `sortable: false` is refused. + + - **Before.** An author who marked a field `sortable: false` (a long text, an unindexed column on a large table) got nothing for it. The sortability projection served on `GET /api/v1/meta/object/NAME` reported the field `{ sortable: true }`, so the list header and the Sort picker offered it, `GET /api/v1/data/NAME?sort=FIELD` answered 200 sorted by it, and so did the stdio MCP `query_records` with that `orderBy`. + - **Now.** The projection reports `{ sortable: false, reason: 'author-declared' }` for that field, so the grid header and the Sort picker, which read the projection, stop offering it. The Data API judges the sort by the same projection and refuses it with `400 INVALID_SORT` before any read, the answer a sort on a `formula` field already gets; the stdio MCP `query_records` refuses it the same way before the engine runs. Only the literal `false` counts: a field with no `sortable` key, or with `sortable: true`, sorts as before. A `formula` field also marked `sortable: false` keeps its `reason: 'virtual-type'` answer. + - **Stored sorts.** A list view whose `sort` names such a field, or a page `record:related_list` block whose `sort` does, used to load sorted and is now refused when it is read. Lint does not yet judge these at authoring. The upgrade-guide entry `field-sortable-false-sort-refused` lists them. + - **Not changed.** In-process callers that reach `ObjectQL.find` directly (hooks, flows) keep sorting by the stored column: the engine door judges only whether a column is stored, and `sortable: false` governs what the platform offers callers, not what its storage can do. + + **The remedy.** Drop the sort on that field or sort by another one, in requests and in stored view and related-list sorts. An author who wants the field sortable removes `sortable: false` from it. +- 4c2cd29: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(lint)!: publish refuses a validation rule (or another fail-closed predicate) that orders a text or boolean field against a number, instead of warning that it "evaluates to null" and then refusing every write (#29904) + + Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the object save door and the package publish) refuses a `type-mismatch` it used to pass with a warning, at the predicate sites that fail closed. Nothing is widened. + Metadata-protocol: narrowed + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate`, the object save door and `POST /api/v1/packages/:id/publish-drafts`. + + - **Before.** `record.name > 5` on a text field, written as a validation rule's `condition`, published with one `expression-invalid` advisory at `severity: warning` saying the expression "silently evaluates to null". A validation rule fails closed, so from then on every create and update whose `name` was not numeric text was refused: `400 VALIDATION_FAILED` "Validation rule 'name_gt' could not be evaluated (runtime: no such overload: dyn > int) — write rejected". + - **Now.** The same rule is refused at publish as `expression-invalid` at `severity: error`, at the rule's `condition` path, with a message that says the write would be refused and how to compare like with like. This is the severity the nullable-comparison verdict (`record.qty > 1000` on a nullable number) already had at the same slot. + - **Which sites.** Those whose runtime refuses the write (or aborts the operation) when the expression faults: a validation rule's `condition` and `when`, nested `then` / `otherwise` rules included; a field's `requiredWhen` and `readonlyWhen`; an option's `visibleWhen` (a write that picks the option); a hook's `condition`. The `validate_expression` MCP tool answers the same for `site: 'validation'`. + - **Unchanged.** A formula field's `expression` keeps the warning: there the fault reads null, which is what the warning says. So do flow conditions (the run fails, the write that fired it lands), a field's display `visibleWhen` / `settledWhen`, field group and action predicates, and `conditionalFormatting`. Sharing-rule conditions compile to a query filter and are not judged by this verdict. + - **`@objectstack/spec`.** The upgrade guide entry `validation-type-mismatch-refused` (a semantic migration: no automatic rewrite, because whether the author meant the length, a number stored as text or another field is a judgement about the rule). + - **`@objectstack/formula`.** `ExprSchemaHint` gains `faultOutcome?: 'fail-closed'`, which the caller sets for a fail-closed site; the `type-mismatch` refusal's `params` carry `outcome: 'fail-closed'` when it is graded an error there. + + **FROM → TO.** `condition: 'record.name > 5'` on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text (it faults, and refuses the write, on any other). A boolean: `record.active > 0` → `record.active == true`. +- 9037b13: fix(lint)!: a text-vs-number comparison in a record-triggered flow condition is refused with what really happens (the run fails, the write lands), and in an advisory validation rule the refusal no longer says the write is refused (#29926) + + Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses a `type-mismatch` in a record-triggered flow's start or edge condition that it used to pass with a warning; and the fail-closed arm of the verdict is served under its own code `type-mismatch-fail-closed` instead of `type-mismatch` with `params.outcome`. Added beside them: the codes `type-mismatch-run-fails` and `type-mismatch-fail-open`, and the exported type `ExprFaultOutcome`. + Metadata-protocol: narrowed + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names each outcome) and the MCP skill text in `packages/mcp/src/skill-md.ts` (`flow_condition` now errors too); `skills/**` teaches no text-vs-number comparison in a flow condition or an advisory rule, so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Record-triggered flow conditions — before.** `name > 5` on a text field, as the start node's `condition` or an edge `condition` of a flow whose start `triggerType` is `record-*`, published with an `expression-invalid` warning saying the expression "silently evaluates to null". It does not: the engine's `evaluateCondition` throws on the fault, so the run fails at that condition, and the record-change trigger isolates the write that fired it, so the write lands and the flow's work is not done. + - **Now.** The same condition is refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with a message saying the run fails at the condition and the triggering write still lands. This is the severity and outcome the nullable-comparison verdict already has on those conditions (#29026). A flow that is not record-triggered, and a node `condition` other than the start node's (inert), keep the warning. The `validate_expression` MCP tool answers the same for `site: 'flow_condition'`. + - **Advisory validation rules** (`severity: 'warning'` / `'info'`). Still refused at publish, as the nullable twin refuses them, but under `type-mismatch-fail-open` with a message that says what happens: the rule is skipped, the write proceeds, and the rule never gives its advisory. The nullable twin's message on the same rule now says the same instead of "the write is rejected fail-closed". A `severity: 'error'` rule keeps both refusals and their wording. + - **Refusal codes (`@objectstack/formula`).** A code names one message template, so each outcome has its own: `type-mismatch` (warning, reads null), `type-mismatch-fail-closed`, `type-mismatch-run-fails`, `type-mismatch-fail-open` (all errors). `ExprSchemaHint.faultOutcome` takes `'fail-closed' | 'run-fails' | 'fail-open'` (exported as `ExprFaultOutcome`); the `outcome` param on `type-mismatch` is gone. + - **`@objectstack/spec`.** Upgrade guide entry `flow-condition-type-mismatch-refused` (semantic: no mechanical rewrite). The `validation-type-mismatch-refused` entry's acceptance criteria now say a create answers 201, the data door's status. + + **FROM → TO.** Flow condition `name > 5` on a text field → `size(name) > 5` for its length, `score > 5` on a number field for a value, or `double(name) > 5` only when every value is numeric text. A boolean: `active > 0` → `active == true`. A consumer keyed to the code: `type-mismatch` with `params.outcome === 'fail-closed'` → `type-mismatch-fail-closed`. + + **The renamed code shipped in 18.3.0.** `@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` already serve #29904's fail-closed refusal as `type-mismatch` with `params.outcome: 'fail-closed'` (the `outcome` param is in the 18.3.0 `ExpressionRefusalParams` type), although 18.3.0's changelog has no line for it: its changeset (`29904-type-mismatch-fail-closed.md`) was not consumed by that release. This release renames that arm `type-mismatch-fail-closed` and removes the `outcome` param, so a consumer keyed to 18.3.0's `type-mismatch` + `outcome` moves to the new code (FROM → TO above). The same changeset also says flow conditions keep the warning; a record-triggered flow's start and edge conditions are now refused, as above. +- e8afd1e: fix(lint)!: every flow condition's text-vs-number comparison gets a verdict that says what the run does — a record-triggered flow's `record.` read and `decision` branch are refused like its bare name, a time-relative sweep of the flow's object is refused like a record-triggered flow, a bare name a flow variable holds is no longer typed as the field, and any other flow's warning says the run fails instead of "evaluates to null" (#29959) + + Clause-②: yes (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses, on a record-triggered flow, a `type-mismatch` in a `decision` branch and in a `record.` / `previous.` read of a start or edge condition, which it passed with no verdict before; refuses the same on a time-relative flow sweeping the object its conditions are typed against, where 18.3.0 served a warning; and a flow condition's `type-mismatch` warning is served under its own code `type-mismatch-run-may-fail` instead of `type-mismatch`. Added beside them (the widening half): the optional `severity` and `triggerKind` inputs of the `validate_expression` MCP tool, the code `type-mismatch-run-may-fail` in `@objectstack/formula`'s `ExpressionRefusalCode`, the optional `flowVariables` key of `@objectstack/formula`'s `ExprSchemaHint`, and a bare flow-condition name that a declared flow variable or node id holds is no longer typed as the field it shadows (no verdict where 18.3.0 served a `type-mismatch` warning). + Metadata-protocol: narrowed + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names the decision branch, the `record.` read and the warning on other flows) and the MCP skill text in `packages/mcp/src/skill-md.ts` (the two new `validate_expression` inputs); `skills/**` teaches no text-vs-number comparison in a flow condition (grep for `type mismatch`, `evaluates to null`, `validate_expression` and the decision examples), so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Record-triggered flows — before.** The start and edge conditions refused `name > 5` on a text field (`type-mismatch-run-fails`), but `record.name > 5` in the same slot, and `name > 5` in a `decision` node's `config.conditions[].expression`, published with no verdict at all. Both fail the run the same way: the engine's `evaluateCondition` throws on the fault, the run binds the trigger record both as `record` and flattened to bare names, and a decision branch is evaluated over the same variables as an edge. + - **Now.** Both are refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with the message the bare name already gets: the run fails at the condition and the triggering write still lands. `previous.` is judged the same way. The date verdicts stay on the bare name only, as before. + - **Time-relative flows.** A sweep launches each run with the swept row as its record, so on a flow whose `timeRelative.object` is the start node's `objectName` (or is omitted) the start, edge and `decision` conditions get the record-triggered verdict above, bare name included: `type-mismatch-run-fails`, where 18.3.0 served a `type-mismatch` warning. A sweep of another object keeps the warning below. + - **Any other flow** (no `triggerType`, a `schedule` flow, an `api` flow) and an inert node `condition` keep a warning. Its text said the expression "silently evaluates to null", which is false on every flow: a condition that faults fails the run. It is now `type-mismatch-run-may-fail`, worded for the failed run, and a warning because such a flow is handed whatever its entrance carries (an API request body, an action's partial record, a parent flow's record, or nothing), so the field's type is not proven to be what the name holds. A `record.` read and a `decision` branch there get the same warning. The flow registration log line reads the same text. + - **A bare name a flow variable holds.** The run seeds a flow's declared variables (and each node's outputs, under its id) before it flattens the record, and the flattening skips a name already bound. So in a flow that declares `variables: [{ name: 'title', type: 'number' }]` over a text field `title`, a bare `title > 5` reads the number and runs; it was refused as `type-mismatch-run-fails` on the head that added that error, and warned on 18.3.0. It now gets no type verdict (the `flow-variable-shadows-field` warning still names the shadowing). `record.title > 5` reads the field and stays refused. + - **`validate_expression` (MCP).** Two optional inputs, so the tool answers what the gate will: `severity` (`error` | `warning` | `info`, site `validation`; default `error`) gives an advisory rule the gate's `type-mismatch-fail-open` instead of the fail-closed verdict, and `triggerKind` (`record_change` | `time_relative` | `schedule` | `api` | `none`, site `flow_condition`; default `record_change`) gives a `schedule`, `api` or `none` flow the gate's warning instead of the run-fails error (`time_relative` answers run-fails, its record being a row of `objectName`). Either input on a site it does not describe is refused as a tool error. A call that passes neither answers as before. + - **`@objectstack/spec`.** Upgrade guide entry `flow-record-read-and-decision-type-mismatch-refused` (semantic: no mechanical rewrite). + + **FROM → TO.** In a record-triggered flow, or a time-relative flow sweeping its own object: `record.name > 5` (or a decision branch `name > 5`) on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text. A boolean: `record.active > 0` → `record.active == true`. A consumer keyed to the code of a flow condition's warning: `type-mismatch` → `type-mismatch-run-may-fail` (`@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` serve `type-mismatch` there; the record-scoped `type-mismatch`, on a formula field or a display predicate, is unchanged). +- cfb602c: fix(lint)!: a time-relative flow whose start node names no `objectName` has its conditions typed against the object it sweeps (`timeRelative.object`), so a text-vs-number comparison, a read of a field the swept object does not declare and date arithmetic there are refused like on a sweep of the start node's object; a sweep of another object is told why it only warns; and `validate_expression` takes the swept object (#30026) + + Clause-②: yes (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses, in the start, edge and `decision` conditions of a time-relative flow whose start node names no `objectName`, a `type-mismatch` (`type-mismatch-run-fails`), a `record.FIELD` read of a field the swept object does not declare (`unknown field`), and date arithmetic (`date-arithmetic-run-fails` / `date-text-arithmetic-run-fails`), which it passed with no verdict at all before. Added beside it (the widening half): the optional `sweepObject` input of the `validate_expression` MCP tool. + Metadata-protocol: narrowed + Skills-docs: updated here — the MCP skill text in `packages/mcp/src/skill-md.ts` (the new `sweepObject` input); `skills/**` teaches no time-relative flow condition with a text-vs-number comparison, so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Before.** A time-relative flow was typed only against its start node's `objectName`. A start that named none, with the swept object in `timeRelative.object` (the key the descriptor requires), got no type verdict on its conditions at all: `title > 5` on a text field published clean, and every run the sweep launched failed at that condition. + - **Now.** The conditions of such a flow are typed against `timeRelative.object`: each run's record is a row of that object, so its start, edge and `decision` conditions get every verdict a sweep of the start node's object already gets. Three of them are new refusals there: a text-vs-number comparison (`type-mismatch-run-fails`), a `record.FIELD` read of a field the swept object does not declare (`unknown field FIELD on OBJECT`; the run fails on the missing key), and arithmetic on a date field (`date-arithmetic-run-fails`, or `date-text-arithmetic-run-fails` against text). The shadowing and near-miss warnings and the root check are judged against the same object. + - **A sweep of another object** (the start node's `objectName` is not `timeRelative.object`) keeps the `type-mismatch-run-may-fail` warning, and the flow now gets one more warning, at its start node's `config.objectName`, saying why: the conditions are checked against that object's fields while each run's record is a row of the swept one. It is served only on a flow that got a warning it explains. + - **`validate_expression` (MCP).** New optional input `sweepObject` (site `flow_condition` with `triggerKind: 'time_relative'` only; a tool error anywhere else): the flow's `timeRelative.object`. When it is another object than `objectName`, a type mismatch is the gate's warning instead of the run-fails error. A call that does not pass it answers as before. + - **Correction to the 18.x changeset of the flow-condition type check.** It described a time-relative flow whose `timeRelative.object` "is omitted" as sweeping the start node's object. That shape never binds: `TimeRelativeTriggerSchema` requires `object`, and the gate refuses its absence (`flow-time-relative-descriptor-invalid`). Such a flow now gets the warning, not the run-fails verdict, on its conditions, since no run of it binds a row. + + **FROM → TO.** In a time-relative flow whose start names no `objectName`, sweeping `crm_opportunity` with a text field `title` and a date field `close_date`: `title > 5` (or `record.title > 5`, or a decision branch `title * 2 > 10`) → `size(title) > 5` for its length, a number field for a value, or `double(title) > 5` only when every value is numeric text. `record.nope > 1`, where `crm_opportunity` declares no `nope` → the field's declared name, or the object that declares it as `timeRelative.object`. `close_date + 1 > …` → `addDays(close_date, 1) > …` to shift a date (`addMonths(d, n)` and `daysFromNow(n)` likewise), or `daysBetween(a, b)` for a span in whole days. A flow whose start names one object and sweeps another: set the start node's `objectName` to the swept object, or remove it. + +### Patch Changes + +- 06cccd3: fix: the inbox's own notification list no longer shows the raw topic, and an MCP argument refusal ends at the fix + + Clause-②: no + Metadata-protocol: none + + - **`@objectstack/service-messaging`.** The `mine` list view of `sys_inbox_message` (a person's own notifications) showed a Topic column reading the producer's machine id, such as `notify` for a flow's notify node or `collab.mention` for a mention. Nothing labels those ids, so the column is gone from that view: it now shows the title, the severity and the time. The `topic` field itself is unchanged and still searchable, so a filter or another view can use it. + - **`@objectstack/mcp`.** When a tool call carries an argument the tool does not declare, the refusal names the argument and the spelling to send, and now stops there. It no longer ends with the sentence "This argument used to be dropped silently and the call still succeeded, …", which explained the history of the check to internal developers and gave the caller nothing to act on. Every tool still refuses an undeclared argument exactly as before; only the closing sentence is gone. +- 67d5d53: fix(formula,lint,mcp): a fail-closed date verdict says the write is refused, and a time-relative sweep's run-fails verdict names no firing write + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/formula`: three codes join `ExpressionRefusalCode`** (`EXPRESSION_REFUSAL_CODES`), with the params of the code they split from: + - `date-arithmetic-fail-closed` and `date-text-arithmetic-fail-closed`. On a site that fails closed (`faultOutcome: 'fail-closed'`: an `error` validation rule, a field `requiredWhen` / `readonlyWhen`, an option `visibleWhen`, a hook `condition`), date arithmetic used to be told "this faults at runtime, so the field silently evaluates to null". There the fault refuses the write instead, and the verdict now says so. A formula field and an advisory (`warning` / `info`) validation rule keep `date-arithmetic` / `date-text-arithmetic` and their sentence. A consumer keyed to the old codes on a fail-closed site re-keys to the new ones. + - `type-mismatch-sweep-run-fails`. A time-relative sweep of the typed object is graded `run-fails` like a record-triggered flow, but no write fires its run: the sweep starts one run per row it finds. Its condition's text-vs-number verdict used to say "the write that fired it still lands". Served where the new optional `ExprSchemaHint.timeRelativeSweep` is set beside `faultOutcome: 'run-fails'`, it now says the run for that row fails and nothing else happens. A record-triggered flow keeps `type-mismatch-run-fails` and its sentence. + - The grade is unchanged at every site. What is refused, warned or passed is unchanged. + - **`@objectstack/lint`**: the flow walk passes `timeRelativeSweep` on a `time_relative` flow. The nullable-comparison verdict, which also says "still lands", runs on record-triggered flows only, so it is unchanged. + - **`@objectstack/mcp`**: `validate_expression` with `site: 'flow_condition'` and `triggerKind: 'time_relative'` passes the same key, so its answer stays the gate's. +- 6c4c43a: fix(service-automation,mcp): a time-relative flow's conditions are typed against the object its sweep scans, at registration and in the `validate_expression` advice + + Clause-②: no + Metadata-protocol: none + + - **`@objectstack/service-automation`**: the registration pass's advisory schema check (the logged `unknown field` / typo / type-mismatch warnings) typed a time-relative flow's conditions against the start node's `config.objectName`, while the run binds a row of `config.timeRelative.object`. A flow stored with a start naming another object kept a warning on a field that is right for the scanned object and none on a field that fails at run time; a start naming no `objectName` got no schema check at all. The pass now reads the object off the flow's trigger binding, the same answer the run uses: `timeRelative.object` when it is a string, `config.objectName` only otherwise. Every other flow is typed as before. The warnings stay logged, never thrown. + - **`@objectstack/mcp`**: `validate_expression`'s description, its `sweepObject` input text and the SKILL.md line no longer advise passing a `sweepObject` other than `objectName` as a supported shape. They say to pass the scanned object as `objectName`, and that a start whose `objectName` names another object is refused at publish (`flow-time-relative-object-mismatch`). The tool's inputs and verdicts are unchanged. +- 614e831: feat(app-shell): Connect an Agent adds connect cards for WorkBuddy, Doubao Work and QwenWork, and drops the VS Code card + + Clause-②: no + Metadata-protocol: none + + The Connect an Agent page (Account → Developer, and Setup) now carries three more client cards, each filled in with the deployment's MCP URL, listed after the Claude, Codex and Cursor cards: + + - **WorkBuddy** — the `mcpServers` entry for Connectors → Custom connector → Configure MCP, with the `streamableHttp` transport spelling WorkBuddy reads, and the reminder that the connector stays inactive until it is trusted. + - **Doubao Work** — the name, transport and server URL to type into Skills · Connectors → New → New custom connector. + - **QwenWork** — the `mcpServers` JSON for Extensions → Connectors → Paste JSON config, with the `streamable-http` spelling QwenWork reads. + + Each of the three names the fallback for a client version without the browser sign-in: the request header `Authorization: Bearer `. A minted key is now revealed in that form as well as the existing `x-api-key` line. + + The claude.ai / Claude Desktop and Claude Code cards merge into one full-width Claude card with a half for each. The Codex CLI card becomes a Codex card for the Codex app: the name, transport and URL to enter under Settings → MCP servers → Add server, and where to sign in (new string `connectAgent.codex.hint`); it no longer shows a `config.toml` snippet. The VS Code / Copilot card is gone, with its string `connectAgent.vscode.body`; the generic `mcpServers` snippet on the Cursor card and the MCP URL at the top still serve any MCP client. The client cards and the API-key card form one list, the API-key card last. Long one-line snippets wrap instead of being clipped. The Doubao Work and QwenWork card titles are strings too (`connectAgent.doubao.title`, `connectAgent.qwenwork.title`), so the zh pack shows the products' own names, 豆包工作 and 千问办公. New strings live under `connectAgent.fields`, `connectAgent.workbuddy`, `connectAgent.doubao` and `connectAgent.qwenwork`, translated in all ten packs. Nothing on the server changes; `@objectstack/mcp` updates only its description of the page. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [67d5d53] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [0e4747a] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/mcp/package.json b/packages/mcp/package.json index f97b769afb1..ccc5e2c654b 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/mcp", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "ObjectStack as an MCP server — exposes your app's objects (and AI tools) over the Model Context Protocol (stdio + Streamable HTTP)", "type": "module", diff --git a/packages/metadata-core/CHANGELOG.md b/packages/metadata-core/CHANGELOG.md index 1ea659eb4d4..fe7b036cd2d 100644 --- a/packages/metadata-core/CHANGELOG.md +++ b/packages/metadata-core/CHANGELOG.md @@ -1,5 +1,77 @@ # @objectstack/metadata-core +## 18.4.0 + +### Patch Changes + +- 60fe44f: feat(cloud-connection,metadata-core): an install the protocol handshake admitted without checking says so in its answer, not only in the server log + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/cloud-connection`: `POST /api/v1/marketplace/install-local` answers `data.protocolCheckSkipped`** when the ADR-0087 handshake admitted the package without checking it: `{ reason, packageId, protocolVersion, message }`, plus `requiredRange` and `rangeSource` for `reason: 'unparsed-range'`. `reason` is the handshake's own verdict: `no-range` (the package declares no `engines.protocol`) or `unparsed-range` (it declares a range the handshake cannot read). `protocolVersion` is the protocol the package was installed under unchecked, and `message` says so in plain words for the person who pressed Install. It is on all three success answers (managed, `unmanaged`, and a paired customization package), and omitted, not nulled, when the range was checked, so that answer's key set is unchanged. The `warn` log line stays as it was. + - **`@objectstack/metadata-core`: `assertProtocolCompat` returns the verdict it admitted the package under** (new exported type `ProtocolCompatAdmitted`; it returned `void`), and the `no-range` and `unparsed-range` verdicts of `checkProtocolCompat` carry `protocolVersion`, the version they were judged against. A door that answers a caller reads the skipped check from the handshake itself instead of judging the range a second time. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/metadata-core/package.json b/packages/metadata-core/package.json index 8fa054750a1..56ab5aa5358 100644 --- a/packages/metadata-core/package.json +++ b/packages/metadata-core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-core", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Metadata Repository contracts: types, canonicalization, errors, interface (ADR-0008).", "type": "module", diff --git a/packages/metadata-fs/CHANGELOG.md b/packages/metadata-fs/CHANGELOG.md index d80fb2f7263..90047bb48f4 100644 --- a/packages/metadata-fs/CHANGELOG.md +++ b/packages/metadata-fs/CHANGELOG.md @@ -1,5 +1,12 @@ # @objectstack/metadata-fs +## 18.4.0 + +### Patch Changes + +- Updated dependencies [60fe44f] + - @objectstack/metadata-core@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/metadata-fs/package.json b/packages/metadata-fs/package.json index e75514451b0..b49e4f6df78 100644 --- a/packages/metadata-fs/package.json +++ b/packages/metadata-fs/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-fs", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "FileSystemRepository: Node-only Repository implementation backed by JSON files and a JSONL change log (ADR-0008).", "type": "module", diff --git a/packages/metadata-protocol/CHANGELOG.md b/packages/metadata-protocol/CHANGELOG.md index a97b45d9c45..9fb437730ec 100644 --- a/packages/metadata-protocol/CHANGELOG.md +++ b/packages/metadata-protocol/CHANGELOG.md @@ -1,5 +1,372 @@ # @objectstack/metadata-protocol +## 18.4.0 + +### Minor Changes + +- 140d8ce: fix(spec,metadata-protocol,mcp)!: a field marked `sortable: false` is no longer offered or performed as a sort — the sortability projection reports it unsortable, and the Data API and the stdio MCP `query_records` refuse a sort on it with `400 INVALID_SORT` (#29896) + + Clause-②: yes (narrowing) — `GET /api/v1/data/:object` (and every request that reaches the same list door: `POST /api/v1/data/:object/query`, the export route, the RPC dispatcher, the record-page `related[].sort`) and the stdio MCP `query_records` refuse a sort on a field whose author declared `sortable: false`, which they used to answer sorted by the column; a stored list view `sort` or `record:related_list` `sort` naming such a field is now refused when it is read. The widening half: `@objectstack/spec/api` exports `FIELD_UNSORTABLE_AUTHOR_DECLARED`, and `FieldSortabilitySchema.reason` accepts `'author-declared'` beside `'virtual-type'`. + Metadata-protocol: narrowed + Skills-docs: none — `skills/**` names no `sortable` field key, and `content/docs/**` already teaches `sortable: false` as "do not allow sorting by this field" and `INVALID_SORT` as "the field must exist and have `sortable: true`", which is what the platform now does. The prescription ships in the refusal text and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing at the Data API list door (`@objectstack/metadata-protocol`) and the stdio MCP `query_records` (`@objectstack/mcp`): a sort on a field marked `sortable: false` is refused. + + - **Before.** An author who marked a field `sortable: false` (a long text, an unindexed column on a large table) got nothing for it. The sortability projection served on `GET /api/v1/meta/object/NAME` reported the field `{ sortable: true }`, so the list header and the Sort picker offered it, `GET /api/v1/data/NAME?sort=FIELD` answered 200 sorted by it, and so did the stdio MCP `query_records` with that `orderBy`. + - **Now.** The projection reports `{ sortable: false, reason: 'author-declared' }` for that field, so the grid header and the Sort picker, which read the projection, stop offering it. The Data API judges the sort by the same projection and refuses it with `400 INVALID_SORT` before any read, the answer a sort on a `formula` field already gets; the stdio MCP `query_records` refuses it the same way before the engine runs. Only the literal `false` counts: a field with no `sortable` key, or with `sortable: true`, sorts as before. A `formula` field also marked `sortable: false` keeps its `reason: 'virtual-type'` answer. + - **Stored sorts.** A list view whose `sort` names such a field, or a page `record:related_list` block whose `sort` does, used to load sorted and is now refused when it is read. Lint does not yet judge these at authoring. The upgrade-guide entry `field-sortable-false-sort-refused` lists them. + - **Not changed.** In-process callers that reach `ObjectQL.find` directly (hooks, flows) keep sorting by the stored column: the engine door judges only whether a column is stored, and `sortable: false` governs what the platform offers callers, not what its storage can do. + + **The remedy.** Drop the sort on that field or sort by another one, in requests and in stored view and related-list sorts. An author who wants the field sortable removes `sortable: false` from it. +- 699ebd8: fix(metadata-protocol)!: the metadata save and publish doors refuse a select field bound to a picklist no loaded package declares, instead of storing it and then refusing every write to the field (#29898) + + Clause-②: no (narrowing) — the metadata save door (draft and live), the per-item and package publish, and the restore doors refuse an `object` or `object_extension` whose field names a picklist no loaded package declares, a body they accepted before. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: no move — `skills/**` teaches no picklist reference verdict and `content/docs/**` carries no severity table for it; the prescription ships in the refusal text, the FROM → TO below and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing at the metadata save and publish doors: `PUT /api/v1/meta/object/NAME` and `PUT /api/v1/meta/object_extension/NAME` (draft and live), `POST /api/v1/packages/PKG/publish-drafts`, the per-item publish, and the rollback / revert restore doors. + + - **Before.** A select (or multiselect) field with `picklist: 'nope_list'`, a name no loaded package declares, saved `200` with its draft reading `_diagnostics.valid: true`, and `publish-drafts` answered `outcome: published`. The field was then served with no options, and every create or update that set it answered `400 VALIDATION_FAILED` ("takes its values from picklist \"nope_list\", which no loaded package declares"). The boot and a late install already refuse the same shape for a package's own objects, and `os validate` / `os build` refuse it for a stack's own fields. + - **Now.** The doors refuse it `422 INVALID_METADATA`, one finding per unknown binding at `fields.FIELD.picklist`, naming the field and the list. A list is declared when the engine's registry resolves it, the same read that serves the field's options: the package's own, a dependency's, the base package's of a `.custom` pair, or any other package installed here. + - **Unchanged.** A binding the object already holds is not judged again on a re-save, so an object published before this release keeps saving; the stored-metadata healing pass (`os migrate meta --stored`) is not judged. A draft stored before this release is judged when it is published. + - **`@objectstack/spec`.** The upgrade guide entry `picklist-binding-unknown-refused` (a semantic migration: no automatic rewrite, because whether the author meant another list, one still to be declared or inline options is a judgement about the field). + - **`@objectstack/cli`.** A source comment in the `os validate` picklist judge no longer says the runtime write path is unjudged. + + **FROM → TO.** `fields.t: { type: 'select', picklist: 'nope_list' }` → `picklist: 'NAME'` of a list a package installed here declares, or declare `nope_list` in a code package (a `*.picklist.ts` file, or `defineStack({ picklists })`), or `options: [...]` in place of `picklist`. +- e3b56b2: feat(spec): a lookup or master-detail field's related-list title is translatable + + Clause-②: yes + Metadata-protocol: added + + A related list the record page derives from a lookup or master-detail field is titled by the field's `relatedListTitle`, or by the child object's label when it has none. That title had no translation slot, so a zh-CN reader saw it in the authoring language, and `os i18n check` could not see the gap. A lookup from an object back to itself was the plainest case: with no title of its own, a contract's record page listed its renewals under 「合同」, the same word as the contract beside it. + + - `FieldTranslationSchema` declares `relatedListTitle`: `objects..fields..relatedListTitle`, on the child object whose field declares the title. `translateObject` overlays it on the served child object (`GET /meta/object`, and the app bundle's object members), which is what the console derives the related list's title from, on the side card and on the body table alike. + - It follows the packaged-base rule (ADR-0029 D9.2a): a title an org re-authored on top of the packaged declaration keeps its text. An authored `relatedListTitle` with no entry keeps its authored text; the child object's label is never substituted for it. + - `os i18n extract` scaffolds the key and `os i18n check` counts it under the field bucket. A field without a `relatedListTitle` is recorded without being demanded. + - The object save door puts a served (translated) `relatedListTitle` back to its source text, as it does for the field's other translated slots. +- fdad178: fix(metadata-protocol,rest): a `.custom` pair's declared override of a page, dataset, app, book, dashboard, view or report is labelled in the ruled order on every `/meta` read — the pair's own translation, then the override's own label, then the base's translation (#30062) + + Clause-②: yes (widening) — `ObjectStackProtocolImplementation` gains six public methods: `getPackagedPageBase`, `getPackagedDatasetBase`, `getPackagedAppBase`, `getPackagedBookBase`, `servedOverrideBaseOf` and `getPairTranslationBundle` (measured on `packages/metadata-protocol/dist/index.d.ts`); `@objectstack/rest`'s exported `MetaBookTreeSources` accepts one more optional key, `resolveProtocol` (measured on `packages/rest/dist/index.d.ts`). No accepted key, value or answer is taken away. + Metadata-protocol: semantic + + - **Now.** A changed label on a declared override reads the pair's own translation of that key when the pair ships one, else the override's own text, in every locale, on `GET /meta/:type/:name`, `GET /meta/:type` and `GET /meta/book/:name/tree`. An unchanged label still reads the base's translation. Before, a page, dataset, app or book override read the base's translation over its own changed label, and no type read the pair's translation first. + - **The pair's translations.** They are the pair's own active `translation` rows (`package_id` = the pair). A `.custom` package has no code bundle, so it has no code translation source. A package-less authored row is in the deployment catalog but is not the pair's, so it does not rank first. + - **Object.** An object is extended, never replaced (ADR-0146 D10), so it has no declared override and no pair bundle: seven types take one. + - **The book tree.** `GET /meta/book/:name/tree` now judges a book against its packaged base as the list does, so the docs sidebar and the book list serve the same label (#26690). +- 15f808a: fix(metadata-protocol): a mapping override is served, a pair's removal of its own override of any overridable type brings the base's item back, and an excluded type's `overrides` refusal names a remedy that works for it (#30062) + + Clause-②: no — ADR-0146 D2 says an item in the customization package replaces the base's item and removing it brings the base's back, and ADR-0146's revert ("Restore package original") is deleting the overriding item from `.custom` so the base's document is served again. The 403 on removing a pair's own doc or mapping override, and the unserved mapping override, refused what that text already admits. A removal that names the base still keeps today's answer. + Metadata-protocol: semantic + + - **A mapping override in `.custom` is served.** A mapping item that declares `overrides: { package: '', … }` in the base's customization package now replaces the base's mapping for the by-name read (no `?package`, `?package=`), both lists and the layered read, as page, app, dataset, doc and book already did. The import door (`@objectstack/rest`'s `import-mapping.ts`) resolves a named mapping through that package-less by-name read, so it now gets the pair's mapping. + - **Removing a pair's own override works for every overridable type.** `DELETE` of a declared override the customization package holds (named with `?package=.custom`) was admitted only for a flow; a doc or mapping override answered `403 NOT_OVERRIDABLE` on the environment kernel. The removal now admits the pair's row for every type `isOverridableMetadataType` names. The row is deleted and the base's item is served again under every address. A removal that names the base (`?package=`) is unchanged: the pair's row stays and its override stays served. + - **An excluded type's refusal names a remedy that works for it.** A save of `overrides` on a type that takes no override now cites that type's own record (ADR-0146 D6 for the security catalog and code-only types, D1 for a datasource, D9 for a behavioural switch, D10 for a field, ADR-0126 §3 for one-shot types, D2 for the rest) and prescribes a door that exists for that type: a new item under a new name where the type may be created at runtime, the documented switch route for a sharing rule, Setup › Queues for a queue, Setup › Datasources for a datasource, an object extension for a field, and a new version of the base for a type defined only in package source. The error code and status are unchanged. + - **`@object-ui/app-shell`: the All metadata tile agrees with the Customize door.** On a read-only package, a type's tile reads 可定制 (Customizable) exactly where Studio offers a Customize door for it (flow and app included, whose overlay flag is off) and 只读 (Read-only) elsewhere (doc and mapping, which have no door yet). It no longer reads the overlay flag. + + The label precedence of a translated override (card item 3) is not in this change. +- bcdf8d3: feat(metadata-protocol,objectql): a package's action can be customized by a declared override in its paired `.custom` package (ADR-0146 D9 `action`), and an override that widens who may run the action is refused + + Clause-②: yes (widening) + Metadata-protocol: added + + - **A declared action override is admitted.** An `action` saved, published or installed in a base's paired `.custom` package with `overrides: { package: '', basedOn: '' }` was refused `403 NOT_OVERRIDABLE` ("action customization is not yet available"). It is now admitted by the save, publish and install doors, `os validate` and `os build`, like a flow override. It replaces the base's action whole, and every read of the base's action serves it under the base's name: the by-name and execution reads, the layered read's `effective` (with `code` still the base's), both lists, the object read's `actions` (what the list toolbar, row menu and record header are drawn from) and the engine's schema, which the REST `/actions` door and the MCP `run_action` bridge read the action from. Deleting the override serves the base's action again everywhere. The action activation switch keyed by the action's name turns the override off while it is armed, and the switch stays where it was when the base is restored. Only the pair declares: a dependent's declaration and the pair's undeclared same-named save stay refused, and a dependent's row is never served. + - **A script action's body is rebound.** The override's `body` runs in place of the base's handler after one resync (`kernel:ready`, `metadata:reloaded`, or the publish itself). Removing the override binds the base's handler again on the next resync, without re-registering the base. `@objectstack/objectql` adds `ObjectQL#registerActionOverride(objectName, actionName, handler, packageName)`, which binds a handler over the one registered under the key and keeps it beneath. `removeActionsByPackage(packageName)` now removes the package's entries from each key's stack, so the handler beneath is bound again. `registerAction` of another package's handler under a key an override holds takes the place beneath the override instead of replacing it: a marketplace-installed base binds its handlers in the install plugin's `kernel:ready` rehydrate, after the boot's resync bound the override, and after a restart the override still runs (and a restore runs the base's body). + - **An override may add a gate and never remove one (ADR-0066 D4).** The write gate refuses, with the new rule `declared_action_widens_access` (`403 NOT_OVERRIDABLE`), an action override that drops a capability the base's `requiredPermissions` names, sets `ai.exposed: true` on an action the base keeps off the agent surface, or drops the base's `ai.requiresConfirmation: true`. The refusal names each widened gate. Adding a capability or turning a gate on is admitted. Everything else (`label`, `type`, `target`, `body`, `params`, `visible` and the rest) is replaced whole, so an override can still change what runs, including a body that runs system-elevated, but only for people who could already run the action. `@objectstack/metadata-protocol` exports `actionOverrideWidening(baseItem, overrideItem)`, which returns the widened gates as clauses, and `OverrideWriteClosure.baseItemOf`, through which a door hands the judge the base's own item. `@objectstack/cli` (`os validate`) and `@objectstack/cloud-connection` (the managed install) pass it. + - The action switch route in the packaged-base refusal sentences is now spelled `POST /api/v1/actions/_activation/:object_or_global/:action`, which keeps the declared-save sentence within the REST door's 500-character message bound for long action names. + + Skills-docs: follow-up #30505 +- 59bcd22: fix(metadata-protocol, runtime)!: a package created at runtime is refused when it carries `navigationContributions`, which its install never registered (#30072) + + Clause-②: yes (narrowing) — `POST /api/v1/packages` (both install limbs, its no-protocol fallback included) and the protocol install primitive `installPackage` (and so the package duplicate and every in-process caller) refuse a manifest whose `navigationContributions` is a non-empty list with `400 VALIDATION_ERROR`, a manifest they stored and answered `201` for before. Widened: `@objectstack/metadata-protocol` publishes one new export, `runtimeNavigationContributionsRefusal`, the predicate both doors ask. + Metadata-protocol: narrowed + + + + **BREAKING** accept-set narrowing at the runtime package install door (Studio, REST, MCP and AI authors creating a package at runtime). `os validate`, `os build` and code packages are unchanged. + + - **Before.** A package created through `POST /api/v1/packages` with `navigationContributions` targeting another app answered `201` and was stored, but no entry ever appeared in the target app, not after a restart either: only a code package's `registerApp` registers contributions, and neither the install nor the `sys_packages` replay at boot does. Nothing said why. + - **Now.** The same install is refused `400 VALIDATION_ERROR` before anything is stored, with a sentence that names the key and both routes that work. The refusal is asked ahead of the duplicate-id `409`, so the answer does not depend on whether the id is installed. A manifest with no `navigationContributions`, or an empty list, installs as before. + - **Not changed.** A code package's contributions (the Setup app's capability plugins, a multi-package artifact's modules, a marketplace package) are registered and served as before. A package already stored with the key keeps loading at boot, and its entries stay unregistered, as they always were. + + **What an author does.** To add an entry to an installed app's menu, save an override of that app in its customization package (the app copied with the entry added; Studio's navigation editor writes it). To contribute menu entries into another package's app, ship them in a code package's manifest, which registers them when it loads. Then install the runtime package without `navigationContributions`. +- fc869c2: feat(spec)!: a seed is create-only by default — an omitted `mode` is `ignore`, so a restart, an install or an upgrade never writes a seeded value back over a customer's edit (#30081) + + Clause-②: yes (narrowing) — the declared default of `Seed.mode` (and of `SeedLoaderConfig.defaultMode`, now the same field) moves from `upsert` to `ignore`. The accept set is unchanged and every authored `mode` parses exactly as before; what narrows is what a replay does for a dataset that declares no mode: it no longer updates an existing row. + Metadata-protocol: semantic + + + + **BREAKING** for a seed dataset that declares no `mode` and relies on a replay to write its values back onto existing rows. + + - **Before.** A seed with no `mode` replayed as `upsert`. A seed loads again on every single-tenant boot (production included, under `os start` and `os serve`), on every package install or upgrade and on every publish, so each replay wrote the shipped value back over a customer's edit to a seeded row, with no warning. Measured on a production boot before the change (card comment 6633189577): an API edit to a seeded row went back to the shipped value on the next restart, after a boot-artifact upgrade, and after an `os package install` upgrade. + - **Now.** `SeedSchema.mode` defaults to `ignore`: a replay inserts a record whose row is missing and leaves an existing row exactly as it is. A row nobody edited is not rewritten either, so its `updated_at` does not move, and a value changed in a later seed version reaches only a row the load creates. Maintainer ruling 6631016988 (option A). + - **One default, stated once.** `SeedLoaderConfig.defaultMode` is `SeedSchema`'s own `mode` field, default included. The six replay sites (`@objectstack/runtime`: the inline boot seed, the per-organization replayer, the dev hot reload and the published-seed apply; `@objectstack/metadata-protocol`: the publish batch; `@objectstack/cloud-connection`: install, reseed and the rehydrate healer) stop restating `defaultMode: 'upsert'`, and the save door reads the schema's default for a seed item that declares no mode. + - **Compiled artifacts.** `os build` stores the parsed seed, so a package built before this change carries a written `mode: "upsert"` on every dataset. The loader cannot tell that value from an authored one, and nothing converts it: such a package keeps today's replay until it is rebuilt. + - **Installed packages.** A package installed with `os package install` (`@objectstack/cloud-connection`'s install-local) keeps the body it was installed with in its install ledger, and the sample-data reseed (`POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data`) replays that body's datasets in their stored mode. So an entry installed from a pre-change artifact keeps writing the shipped values back over edited rows on every reseed until the **rebuilt artifact is re-installed**; rebuilding alone changes nothing for it. An entry installed from a rebuilt artifact reseeds create-only: it puts back the rows that are missing and leaves an edited row as it is. The ledger's other replays never overwrite: the boot rehydrate seeds nothing, the heal seeds only when every seeded object is empty, and the per-organization replay runs only for a new organization. + - **Stored `seed` items.** A `seed` metadata item stored with no `mode` is not rewritten either. Its next publish parses it to `ignore`, so that publish inserts only the rows it is missing where it used to write its values back over existing rows, with no signal. + - **Unchanged.** An explicit `mode: 'upsert'` still writes every declared column back on each replay, and `columns: { FIELD: { applyOn: 'create' } }` still exempts the columns a customer or a job owns. `insert`, `update` and `replace` behave as before. + + **The remedy.** Rebuild every package so its compiled artifact carries the mode its source declares, and re-install the rebuilt artifact wherever the package was installed with `os package install`, so its install ledger carries that mode too. To put an installed package's sample data back to the shipped values, purge it (`…/purge-sample-data`) and then reseed. For a dataset that is reference data the package must keep current, declare `mode: 'upsert'`, and mark the columns the customer or a job owns `applyOn: 'create'`. Leave `mode` unset for business and starter data the customer owns once it is loaded. +- 0837fdc: fix(metadata-protocol): an email template at a locale the base does not ship keeps its package-less write on a fresh install; the named-base refusal names `os migrate packageless-overlays` + + Clause-②: no + Metadata-protocol: semantic + + - **An added locale is writable before the pair exists.** Both overlay-write closes (the package-less write, and the write naming the read-only base with `?package=`) stay open where the fix they would name cannot be taken: an `email_template` member at a locale the base does not ship is refused as a declared override, so its write stays open (ADR-0146 D11). The close asked the override write gate about the pair; on a fresh install the pair is not ensured yet, the gate's closure read answers nothing for a package that does not exist, and the close read that "not concerned" as "admitted". So `PUT /api/v1/meta/email_template/` with a zh-CN body beside a base that ships en-US only answered `403 NOT_OVERRIDABLE`, naming a pair write that was refused too (`declared_package_ships_no_such_item`). The close now judges a pair that is not installed with the closure the ensure door gives it (the base and what the base reaches), so the write is accepted, as it is once the pair exists. A member the base ships is closed exactly as before. + - **The named-base refusal names the ceremony.** `os migrate packageless-overlays` moves a row stored bound to the read-only base into the pair with its declaration. The sentence for a save, publish or rollback naming the base now says such a row "is served until moved by `os migrate packageless-overlays`, overridden by the pair, or deleted naming the base". One spelling, every door; the code and status (`403 NOT_OVERRIDABLE`) are unchanged. +- 8fad7b1: fix(metadata-protocol,cli): `os migrate packageless-overlays` lists a package-less view container `cannot_declare` instead of `movable`, and `--apply` no longer attempts (and fails) it on every run + + Clause-②: yes (widening) + Metadata-protocol: semantic + + - **`@objectstack/metadata-protocol`: the census gains the disposition `cannot_declare`** (`PackagelessOverlayDisposition`, reachable through the exported `PackagelessOverlayCensus`). A row lands there when the schema the save door judges its body by refuses an `overrides` key on it, so the declared save the move makes could never land. Of the five overlay types the ceremony moves, that is a view container (`list` / `listViews` / `form` / `formViews`): only a single view declares an override. Before, such a row was listed `movable` and every `--apply` reported it `failed` with `Unrecognized key(s) on this view container: overrides`, leaving the row where it was. Now `migratePackagelessOverlays` does not attempt it: no outcome, no failure, no pair ensured for it. + - **A view container's write onto a packaged name is accepted again.** `PUT /api/v1/meta/view/` with a container body (`list` / `listViews` / `form` / `formViews`) over a name one installed read-only package ships, naming no package or naming that base with `?package=`, and the matching `DELETE`, answered `403 NOT_OVERRIDABLE` with a fix (a declared save into the pair) that the container schema refuses. They now succeed, as they did before the ADR-0146 D11 closes; every other shape of the five overlay types is closed exactly as before. + - **The two closes read the same answer.** The package-less write close (`packagelessCloseSubject`) closes only what the ceremony moves, so a container's package-less save and delete stay open, as they were before the close: they are the one door such a row keeps. The close of a write naming the read-only base (`overlayPairFixAdmits`, the "the fix can be taken" predicate both closes ask) no longer names the pair as the fix for a container body, because that fix (a declared save into the pair) cannot be taken; that write keeps the answer it had before the close. + - **`@objectstack/cli`: `os migrate packageless-overlays`** prints the way out for a `cannot_declare` row under "left as they are": save each of the container's views into the pair as a single view named `.` (one the base ships declaring `overrides: { package, basedOn }`, a new one declaring nothing), then delete the container row with a delete naming no package (naming the base, for a row bound to it). In `--json`, the row carries `disposition: "cannot_declare"` and `outcome: "left"`, and `--apply` exits 0 for it. +- a7b8876: feat(spec,metadata-protocol): the publish receipt's `seedApplied.landed` names the rows a seed created and the rows it only matched, per object, capped at 200 ids per list + + Clause-②: yes (widening) + Metadata-protocol: added + + - **`@objectstack/spec`: `seedApplied.landed`** on both publish responses (`PublishMetaItemResponseSchema`, `PublishPackageDraftsResponseSchema`), one shared schema `SeedAppliedLandedSchema`: per object, `inserted` (the rows this publish created) and `matched` (pre-existing rows a seed record's key matched: updated, left as they were, or refused an update), each a `SeedAppliedIdListSchema` `{ ids, total, truncated }`. `ids` carries at most `SEED_APPLIED_LANDED_IDS_CAP` (200) ids, in the loader's record order (datasets in load order, records as authored); past it `truncated` is `true` and `total` is the uncut count. Absent `landed` means "not reported" (a load that threw, or a producer that reports no ids), never "nothing landed". Only `inserted` names rows the seed created: a `matched` row may be a customer's own record that shares the seed's natural key, and under `ignore` (the default) such a row is matched without being written. + - **`@objectstack/spec`: `SeedLoadResult.insertedIds`**, the ids among `landedIds` whose row the load created. Per load: a row an earlier load inserted is matched on a replay, so a reader tracking a seed's rows across publishes accumulates `insertedIds` over every load. `landedIds`' TSDoc now says what each seed mode puts in it. + - **`@objectstack/metadata-protocol`**: `applySeedBodies` returns `landed`, so `publishPackageDrafts` and `publishMetaItem` both carry it; the loader records which landed rows it inserted. `seedAppliedLanded(results)` is exported, the one projection both receipt producers fold the loader's ids through. + - **`@objectstack/runtime`**: the package-publish door's fallback seed apply (for protocols that do not self-apply) reports the same `landed`. + - Accepted cost (objectstack-ai/cloud#3598 ruling A): row ids appear on the public publish responses, bounded by the cap. +- 38d416f: fix(metadata-protocol,runtime): a seed load that throws part-way still reports `seedApplied.landed` for the datasets that finished before the throw + + Clause-②: yes (widening: a thrown load's receipt carries `landed` for the datasets that finished, where it carried none) + Metadata-protocol: semantic + + - **`@objectstack/spec`: `seedApplied.landed`** (both publish responses) now says what it covers on a load that threw part-way (`success: false` with `error`): only the datasets that FINISHED before the throw. The dataset that threw is not listed (its rows are in `inserted` / `updated`, its object in `error`). Absent `landed` now means the apply failed before any load ran, or the producer reports no ids; a throw before any dataset finished reports `landed: []`. + - **`@objectstack/metadata-protocol`**: `SeedLoadPartialWrites` gains `finished`, the finished datasets' `object` / `landedIds` / `insertedIds` in load order, and `applySeedBodies`' catch folds it through the same `seedAppliedLanded` projection (same two lists, same 200 cap) the returned path uses. `seedLoadPartialWrites` and `SeedLoadPartialWrites` are exported from the package root. + - **`@objectstack/runtime`**: the package-publish door's fallback seed apply reads the same `seedLoadPartialWrites` off a thrown load and reports the same `landed`, plus the `inserted` / `updated` counts the response schema requires (it reported neither before) and the object whose seed stopped the load in `error`, as `metadata-protocol`'s apply does. + - Why the dataset that threw is left out: its ids are only partly known when it throws. A batched insert whose response was lost may have committed rows whose ids never came back, so a list of the ids the loader did see would read as complete while missing rows. + + Supersedes, in the same release: `.changeset/30348-seed-receipt-landed-ids.md` says absent `landed` means "not reported (a load that threw, or a producer that reports no ids)". After this change a load that threw part-way reports `landed` for its finished datasets; absent now means no load ran, or a producer that reports no ids. +- a91ad42: fix(metadata-protocol,cli,cloud-connection)!: an object extension that re-declares an owner's field in `fields` beyond its display properties is refused at the save and publish doors, at `os validate` and at the install door, with a prescription naming `fieldPatches` (ADR-0146 D10) (#30610) + + Clause-②: no (narrowing) — the `object_extension` save, publish and restore doors, `os validate` and `POST /api/v1/marketplace/install-local` refuse an `objectExtensions[].fields` entry naming a field the extended object's owner declares when it changes anything outside the display-property whitelist, a body they accepted before. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: no move — `skills/**` and `content/docs/**` already teach `fieldPatches` as the way to change a base field's display, and no page teaches taking an owner field over through `fields`; the prescription ships in the refusal text. + + + + **BREAKING** accept-set narrowing at the metadata save and publish doors (`PUT /api/v1/meta/object_extension/NAME`, draft and live, the per-item and package publish, and the restore doors), at `os validate`, and at the marketplace install door. + + - **Before.** An extension written into `fields` with the name of a field the extended object's owner declares replaced the owner's field whole when the registry folded the object: a module package could turn the owner's `amount: number` into `text`, drop its `required`, or make it `readonly`, and every reader judged the field on the extension's definition. Only the registry's warning named it. + - **Now.** Such an entry is refused when it changes anything outside the display-property whitelist (`label`, `description`, `inlineHelpText`, `placeholder`, an option's `label`): `400 VALIDATION_ERROR` at the save and publish doors (located in `issues[]` at `fields.FIELD`), `422 VALIDATION_ERROR` at the install door (every entry in `details.refusals`), and exit 1 with the entry on `errors` at `os validate`. Each refusal names the keys the entry changes and prescribes `fieldPatches`. Both bodies are read through the field schema first, so a default the parse stamps is no change; options are compared by stored value, so a relabel or reorder is no change. + - **Unchanged.** A re-declaration that changes only display properties is admitted. An extension of an object its own package owns is not judged, nor is an added field. `os validate` judges an owner declared by another package of the same artifact; a target outside the artifact is judged at install, against the installed owner. The registry's warning on a re-declared field stays. `os build` does not yet run the check. + + **The remedy.** Move a display change of an owner's field into the extension's `fieldPatches` (the field name as its key, the whitelisted properties as its body), and add a behaviour of your own as a new field under a new name. + +### Patch Changes + +- 3b8c724: fix(objectql, metadata-protocol, runtime): a package's export carries the `objectExtensions` it was installed with, and the extended object's owner's export does not + + Clause-②: yes (widening) + Metadata-protocol: none + + A package installed through `POST /api/v1/marketplace/install-local` (what `os package install` sends, and how every entry of a whole-deployment export is restored) that extends another package's object exported without its `objectExtensions`: `GET /api/v1/packages//export` answered the package's other items and no extension, while the extension's fields were folded into the owner's export instead, as if the owner had shipped them. So a deployment restored from an export lost those fields on its next export, with no error, and a `.custom` pair installed from its export no longer exported its extension. + + - **The extending package's export** (and its entry in `GET /api/v1/packages/export`) now carries every extension it contributes as `objectExtensions`, however it was installed: an extension published through `/api/v1/meta/object_extension` (as before) or a manifest's `objectExtensions` entry (install-local, or a code package). A published extension is exported as it is stored; a manifest entry as it was registered, with `priority` written only when it is not the default `200`. + - **The extended object's owner's export** now carries the object as the owner declares it, without the fields, field patches or labels of ANY other package's extension. Until now only its own `.custom` pair's extension was left out; an extension from any other installed package was folded in. + - **Every served read is unchanged**: `GET /api/v1/meta/object/`, the object list, the console and the data API keep serving the object with every extension folded in. + + `SchemaRegistry` gains `getObjectExtensionsOf(packageId)`, which reads a package's `extend` contributions back as the `objectExtensions` entries they were registered from (the inverse of `registerObjectExtension`). `listItems`, `getAllObjects` and `foldObjectExtendersOnto` take an `onlyExtensionsOf` option beside `withoutExtensionsOf`: each object is resolved with only that package's `extend` contributions folded in. The metadata protocol's per-package layer (the package export) now passes `onlyExtensionsOf`, naming the listed package, and no longer passes `withoutExtensionsOf`, which keeps its meaning for any other caller. + + Supersedes, in the same release: the `.custom` pair note for #29903 says an extension contributed by any other installed package is still folded into the base's object in the base's export. After this change it is not: the base's export leaves out every other package's extension, and that package's own export carries it. +- 88cd0ab: fix(metadata-protocol): a hook that aborts with free text answers `HOOK_ABORTED` on the per-object batch row too, not `VALIDATION_ERROR` + + Clause-②: no + Metadata-protocol: none + + - **Before.** Since #27597 (and #27672) a sandboxed hook body that aborts with free text (`throw new Error('…')`, no `code`) answers `/api/v1/data/:object`, `/createMany` and `/api/v1/batch` with `400 HOOK_ABORTED` and the hook's sentence. `POST /api/v1/data/:object/batch` answered the same abort as a row `{ code: 'VALIDATION_ERROR', message: }`: the row mapper (`toRowApiError`) derived the code from the `400` the doors serve, so a bulk client branching on the code read a business refusal as malformed input. + - **Now.** The row answers `HOOK_ABORTED` (the `UNDECLARED_HOOK_REFUSAL_CODE` the other doors read). The value moves, from `VALIDATION_ERROR` to `HOOK_ABORTED`, on every row that shares the mapper: `POST /api/v1/data/:object/batch` with `operation` `create`, `update`, `upsert` or `delete`, in every mode (plain, `atomic`, `continueOnError`), and the per-row results of `updateManyData` / `deleteManyData`. + - **Unchanged.** A code the hook declared still wins (a registered code is its own row code; an unregistered one still gets the code its status derives). The row's `message` is still the hook's sentence and `httpStatus` is still absent when the body declared no status. A crashed body and a non-sandbox producer answer as before. No error code is added; `HOOK_ABORTED` is the one #27597 registered. +- 5f47410: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(objectql, metadata-protocol): a base package's own export carries its objects without its `.custom` pair's extension + + Clause-②: yes (widening) + Metadata-protocol: none + + After an administrator added a field to a vendor package's object through Studio's Customize (an extension published in `.custom`, for example `region__c`, or a relabelled base field), `GET /api/v1/packages//export` carried that field and label inside the base's object, as if the vendor had shipped them. The pair's export carries the same extension as its `objectExtensions` entry, so installing the two exports elsewhere declared the customer's field twice, once as the vendor's. ADR-0146 D8 item 3 says the base's export carries the base unchanged. + + - **The base's export** (and its entry in `GET /api/v1/packages/export`) now carries each object as the base declares it, with no field, label or other patch from its pair's extension. + - **The pair's export** is unchanged: it carries the extension in `objectExtensions`. + - **Every served read is unchanged**: `GET /api/v1/meta/object/`, the object list, the console and the data API keep serving the object with the pair's field and labels folded in. + + An extension contributed by any other installed package is still folded into the base's object in the base's export, as before. + + `SchemaRegistry.listItems` takes a `withoutExtensionsOf` option beside `includeDisabledPackages`, and `getAllObjects` and `foldObjectExtendersOnto` take the same option: each object is resolved with that package's `extend` contributions left out of the fold. Only the metadata protocol's per-package layer (the package export) passes it. +- 07685ae: fix(metadata-protocol): `os migrate packageless-overlays --apply` heals the registry after it deletes the unbound row, as the delete door does + + Clause-②: no + Metadata-protocol: none + + - **Now.** After the ceremony saves a row into its base's `.custom` pair and reads it back, it deletes the unbound row through the protocol's one repository and then runs the delete door's registry heal for an active row. The unbound row's registration leaves with the row, and the pair's row is registered again, so the process serves what a restart serves. Before, the delete went through a second repository and no heal followed, so the unbound row stayed registered beside the base's entry until the next restart. The five reads of the item already served the pair's row, so no read answered differently. + - **Unchanged.** A draft row's removal heals nothing, as a draft discard does not. Outcomes, receipts and the rows written are the same. +- 43faa22: refactor(metadata-protocol): the eight `ObjectStackProtocolImplementation` doors that kept an inferred return type spell it on the signature + + Clause-②: no + Metadata-protocol: none + + - `getDiscovery`, `findData`, `getData`, `cloneData`, `saveMetaItem`, `publishMetaItem`, `rollbackMetaItem` and `deleteMetaItem` now declare their return type, copied from the published declaration file it was inferred into. The built `index.d.ts` and `index.d.cts` are byte-equal before and after, and no behaviour changes; only the source maps move with the new lines. Nothing to do on upgrade. +- a545a04: fix(runtime, metadata-protocol): the permission-set clone door copies a platform set whose grants name an object this deployment does not install — `member_default` copies into its package's customization pair again on a boot without `@objectstack/plugin-audit` + + Clause-②: yes (widening) — `ObjectStackProtocolImplementation.saveMetaItem`'s request gains one optional key, `inheritedGrantObjects`, reachable from `@objectstack/metadata-protocol`'s `.` entry types + Metadata-protocol: none + + - **What failed:** cloning a set through `POST /api/v1/security/permission-sets/:name/clone` answered `422 INVALID_METADATA` when the source grants on an object no package registers here. The commonest source is the platform baseline `member_default`, which grants `sys_comment` and `sys_activity`. `@objectstack/plugin-audit` registers both objects, and it is an optional package: a deployment that does not install it could not clone `member_default` at all. + - **The rule it now follows:** the save door's unknown-grant refusal (#28990) already leaves alone a grant the set itself already holds, because that grant is not the current write's. A clone's grants are the source's, copied verbatim, so the same holds for a copy. The clone door now tells the save which grant keys the copy inherits (`inheritedGrantObjects`), and the refusal does not judge those. + - **Still refused:** an unknown object that the save does not say it inherits is this write's grant, and is refused exactly as before. That covers a `/meta` save, a re-save that adds a grant, and a draft's publish. No HTTP door forwards the new key. +- 8fa738f: fix(metadata-protocol): duplicating a package copies a permission set that grants on an optional plugin's object not installed here, as the permission-set clone door does + + Clause-②: no + Metadata-protocol: none + + - **Now.** `duplicatePackage` saves a copied permission set with the grant keys of its SOURCE row as stored passed as `inheritedGrantObjects`, so the save door's unknown-object refusal does not judge them (#30255's rule for the clone door). A platform set granting on `sys_comment` / `sys_activity` on a deployment without `@objectstack/plugin-audit` is copied; before, it was left in `failed[]` with the 422 finding and the duplicate answered `success: false`. + - **Still refused.** A grant the copy's rewrite renames onto the copy's own object (`SOURCE_NS_x` → `TARGET_NS_x`) is not the source's key and is judged as before: when that object did not copy, the set lands in `failed[]` naming that grant alone. The keys come from the stored row, never from the request. + - **Unchanged, by ruling.** `importUnmanagedPackage` still refuses such a set `422 INVALID_METADATA` and rolls the import back: the package is the request's body, not a stored row, so its permission sets are new writes and every grant is judged. +- 4bf6e8f: fix(metadata-protocol): contributed navigation entries are translated again, the whole Setup sidebar among them + + Clause-②: no + Metadata-protocol: none + + `getPackagedAppBase(name)` now answers the app's packaged declaration with the code layer's navigation contributions folded in, by the one fold every served app read applies (`SchemaRegistry.applyNavContributions`, ADR-0029 D7). Before, it answered the declaration alone, so `translateApp` read every contributed entry's id as one a `.custom` override authored (ADR-0146 D2), skipped the catalog and served the source text: on a `zh-CN` deployment every Setup leaf entry (Users, Permission Set Assignment, Sharing Rules, API Keys and the rest) read English beside Chinese group headings, although the catalog carries their keys. Contributed entries now read the catalog's translation; the served app's shape is unchanged, only label values move. A navigation node a `.custom` override authors is in no contribution, so it still reads only the pair's translations, else its own text. +- 63de248: fix(objectql, metadata-protocol, cloud-connection)!: a declared `.custom` action override is keyed and judged the same way at every door, and only its declared base's handler slots beneath it + + Clause-②: yes (narrowing) — the protocol door (save and publish) now refuses a declared action override whose `objectName` is not the base action's, with `403 NOT_OVERRIDABLE` and rule `declared_package_ships_no_such_item`, the refusal `os validate` and the managed install already gave it; the save door used to admit it. The managed install now admits a re-install of the pair whose override relaxes an earlier tightening back to the base's own gates, which it used to refuse with `declared_action_widens_access` while the save door admitted it. Every other input answers as before. + Metadata-protocol: none + Skills-docs: follow-up #30505 — the declared action override's docs card; it owes one sentence that an override keeps the base action's `objectName`. + + + + **BREAKING** accept-set narrowing at the protocol door (`@objectstack/metadata-protocol`, ADR-0146 D2/D9): a declared action override names its base's action by `:`, the identity `os validate` and the managed install key it on. + + - **Before.** The save and publish doors matched a declared action override's base by name alone, so an override in the base's `.custom` pair could move the base's action to another object. Its handler was not rebound there, and `os validate` and the install door refused the same body. + - **Now.** The save door refuses it as the other two doors do: the base ships no action of that name on that object. Keep the base action's `objectName` in the override, or remove `overrides` and add the action under the pair's own name. + + `@objectstack/cloud-connection`: the managed install judges a declared action override against the installed base's code artifact (`SchemaRegistry#getArtifactItem`), as the save door does. The protocol's per-package listing it read before serves an armed override under the base's envelope, so a re-install of the pair was judged against its own previous version. + + `@objectstack/objectql`: `ObjectQL#registerActionOverride` takes the package the override declares (`overrides.package`) as an optional fifth argument, and the authored-action resync passes it. `registerAction` under a key such an override holds slots the handler beneath it only when the registry's code artifact of the action belongs to that package, on that object; any other registration, one with no package included, replaces the override and is warned like every other handler collision. Before, any other package's registration slotted beneath it, so a row of another base's pair bound before the real base registered kept running after the real base's handler arrived, while every read served the real base's action; and a registration with no package slotted beneath it at `debug`, where the method's docblock promises a warning. +- 1030a80: fix(metadata-protocol, cli, cloud-connection): a declared action override moved to another object is told which object the base ships the action on + + Clause-②: no + Metadata-protocol: none + + The `declared_package_ships_no_such_item` refusal (`403 NOT_OVERRIDABLE`) for a `.custom` action override whose `objectName` differs from the base action's used to say the base "ships no action named 'X'" and to "correct the name", though the name was right. It now names both objects and tells the author to keep the base action's `objectName`, at all three doors (the save and publish door, `os validate`, the managed install), from one sentence builder in `judgeOverrideWrite`. A name the base does not ship at all reads as before. The rule, code and status are unchanged. +- 55a00ba: fix(metadata-protocol): an object save or publish whose storage sync the datasource refuses now says so on its answer + + Clause-②: no + Metadata-protocol: none + + `PUT /api/v1/meta/object/NAME` (a publish-mode save) and `POST /api/v1/meta/object/NAME/publish` sync the object's table after the item is written. When the datasource refused that sync, both answered `200` success with nothing about storage, and only the server log said the table was not created or altered. Both answers now carry one `warning` on their existing `advisories` key, with the rule id `object-storage-sync-refused`, naming the object and saying its storage did not follow. The datasource's own text is quoted when it declared a client refusal (a 4xx status); otherwise the advisory says the reason is in the server log, which keeps the `error` line it already printed. The status code and every other key are unchanged, the publish is still not failed, and an answer whose sync ran has no new key. + + A client that groups or suppresses advisories by rule id sees one new id. The package-draft batch publish (Studio's "publish whole app") still reports a refused sync in the server log only. +- 713beba: Clause-②: no + Metadata-protocol: none + + fix(metadata-protocol): the per-item rollback (`POST /api/v1/meta/:type/:name/rollback`) restores into the row the by-name read naming no package serves. A `.custom` pair's declared override stored beside a row bound to its read-only base (a row saved before the named-base close and not yet moved) is now rolled back in place, and the base row is left untouched; before, the door read a package-agnostic binding, addressed the base row, and was refused `403 NOT_OVERRIDABLE` naming a row the caller never meant. The door takes no new parameter. A rollback whose served row is the base's own is still refused by the named-base close. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [46d2b65] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [85a8fa0] +- Updated dependencies [1756666] +- Updated dependencies [f9e5bef] +- Updated dependencies [0f3ee58] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [cfb602c] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [c045c34] +- Updated dependencies [dc51811] +- Updated dependencies [c133cfe] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [778aedf] +- Updated dependencies [d9d42d4] +- Updated dependencies [67d5d53] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [54b491a] +- Updated dependencies [8293c74] +- Updated dependencies [ad0fbfd] +- Updated dependencies [60fe44f] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [e760204] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [482e089] +- Updated dependencies [0e4747a] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [e9731fc] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/lint@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/metadata@18.4.0 + - @objectstack/types@18.4.0 + - @objectstack/sdui-parser@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/metadata-protocol/package.json b/packages/metadata-protocol/package.json index 771c5cd2397..7429543234f 100644 --- a/packages/metadata-protocol/package.json +++ b/packages/metadata-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-protocol", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "ObjectStack metadata management protocol: sys_metadata CRUD, draft/publish, locks, package ownership, diagnostics (ADR-0076).", "type": "module", diff --git a/packages/metadata/CHANGELOG.md b/packages/metadata/CHANGELOG.md index 730038672e5..febe4d56791 100644 --- a/packages/metadata/CHANGELOG.md +++ b/packages/metadata/CHANGELOG.md @@ -1,5 +1,83 @@ # @objectstack/metadata +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [60fe44f] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/types@18.4.0 + - @objectstack/metadata-fs@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/metadata/package.json b/packages/metadata/package.json index fc9c5fb3085..550e996186b 100644 --- a/packages/metadata/package.json +++ b/packages/metadata/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Metadata loading, saving, and persistence for ObjectStack", "type": "module", diff --git a/packages/objectql/CHANGELOG.md b/packages/objectql/CHANGELOG.md index fba22b42ffd..8f36816ce37 100644 --- a/packages/objectql/CHANGELOG.md +++ b/packages/objectql/CHANGELOG.md @@ -1,5 +1,321 @@ # @objectstack/objectql +## 18.4.0 + +### Minor Changes + +- bcdf8d3: feat(metadata-protocol,objectql): a package's action can be customized by a declared override in its paired `.custom` package (ADR-0146 D9 `action`), and an override that widens who may run the action is refused + + Clause-②: yes (widening) + Metadata-protocol: added + + - **A declared action override is admitted.** An `action` saved, published or installed in a base's paired `.custom` package with `overrides: { package: '', basedOn: '' }` was refused `403 NOT_OVERRIDABLE` ("action customization is not yet available"). It is now admitted by the save, publish and install doors, `os validate` and `os build`, like a flow override. It replaces the base's action whole, and every read of the base's action serves it under the base's name: the by-name and execution reads, the layered read's `effective` (with `code` still the base's), both lists, the object read's `actions` (what the list toolbar, row menu and record header are drawn from) and the engine's schema, which the REST `/actions` door and the MCP `run_action` bridge read the action from. Deleting the override serves the base's action again everywhere. The action activation switch keyed by the action's name turns the override off while it is armed, and the switch stays where it was when the base is restored. Only the pair declares: a dependent's declaration and the pair's undeclared same-named save stay refused, and a dependent's row is never served. + - **A script action's body is rebound.** The override's `body` runs in place of the base's handler after one resync (`kernel:ready`, `metadata:reloaded`, or the publish itself). Removing the override binds the base's handler again on the next resync, without re-registering the base. `@objectstack/objectql` adds `ObjectQL#registerActionOverride(objectName, actionName, handler, packageName)`, which binds a handler over the one registered under the key and keeps it beneath. `removeActionsByPackage(packageName)` now removes the package's entries from each key's stack, so the handler beneath is bound again. `registerAction` of another package's handler under a key an override holds takes the place beneath the override instead of replacing it: a marketplace-installed base binds its handlers in the install plugin's `kernel:ready` rehydrate, after the boot's resync bound the override, and after a restart the override still runs (and a restore runs the base's body). + - **An override may add a gate and never remove one (ADR-0066 D4).** The write gate refuses, with the new rule `declared_action_widens_access` (`403 NOT_OVERRIDABLE`), an action override that drops a capability the base's `requiredPermissions` names, sets `ai.exposed: true` on an action the base keeps off the agent surface, or drops the base's `ai.requiresConfirmation: true`. The refusal names each widened gate. Adding a capability or turning a gate on is admitted. Everything else (`label`, `type`, `target`, `body`, `params`, `visible` and the rest) is replaced whole, so an override can still change what runs, including a body that runs system-elevated, but only for people who could already run the action. `@objectstack/metadata-protocol` exports `actionOverrideWidening(baseItem, overrideItem)`, which returns the widened gates as clauses, and `OverrideWriteClosure.baseItemOf`, through which a door hands the judge the base's own item. `@objectstack/cli` (`os validate`) and `@objectstack/cloud-connection` (the managed install) pass it. + - The action switch route in the packaged-base refusal sentences is now spelled `POST /api/v1/actions/_activation/:object_or_global/:action`, which keeps the declared-save sentence within the REST door's 500-character message bound for long action names. + + Skills-docs: follow-up #30505 +- 8cc709d: fix(objectql)!: a disabled package's `objectExtensions` fields are withheld from the object they extend, and come back with their values on enable + + Clause-②: yes (narrowing) — while a package is disabled, the served object it extends (`GET /api/v1/meta/object/:name`, the record page, the `/data` field map) no longer carries the fields, validation rules and indexes the package's `objectExtensions` add, so a `/data` write, filter, sort or `fields` projection naming one of those fields is refused as a field the object does not have (`400 INVALID_FIELD` / `INVALID_SORT`), and `GET /api/v1/packages/:id/customizations` no longer lists them among the base's `contributions`. + Metadata-protocol: semantic + + + + **BREAKING** served-object narrowing under package disable (`@objectstack/objectql`, `@objectstack/runtime`, `@objectstack/cli`). + + - **Before.** Disabling a package withheld its objects (`404 OBJECT_PACKAGE_DISABLED`), flows, navigation items, permissions and actions, but the fields its `objectExtensions` add to another package's object stayed on that object: in `/meta/object`, in every `/data` read and on the record page. + - **Now.** The served fold of an object leaves out every `extend` contribution of a disabled package: its fields, its validation rules, its indexes and its field patches. A base layer that already carries them (a tenant overlay saved from a served read, a metadata-service copy of a resolved object) has those fields and rules taken off too. A field the owner itself declares, or another enabled extension also adds, stays. A write naming a withheld field answers exactly what a write naming an unknown field answers; a write through the object's other fields lands and leaves the withheld values alone. + - **Kept.** Schema sync, the federated binding and the lifecycle sweep read the object with every extension folded in (`SchemaRegistry.getStorageObject` / `getAllStorageObjects`), so a disable never drops or skips a column, and a boot that starts with the package disabled still creates them. Enabling the package serves the fields again with the stored values. With `OS_SKIP_SCHEMA_SYNC=1`, `os migrate`'s drift plan no longer reports a disabled module's columns as orphan columns to drop. + - **Who moves.** A client that reads a disabled module's extension fields gets the object without them. A formula on the object that reads a withheld field answers `null`. A validation rule of another, enabled package that reads a withheld field cannot be evaluated, so writes to the object are refused (`400 VALIDATION_FAILED`, naming the rule) until the field's package is enabled again or the rule's package declares and keeps that package. +- fb1ab89: fix(objectql,rest)!: an object extension that renames `label` and sets no `pluralLabel` is served without the base's plural, so list titles stop showing the old name (#30391) + + Clause-②: no (narrowing) — `/meta/object` (item and list reads, both transports) and every registry read of an object (`SchemaRegistry.getObject`, the fold) stop serving `pluralLabel` for an object an extension renames without a plural of its own, where they served the base's plural (and, in a translated locale, the base's translation of it). The shipped showcase's `showcase_account` is one: its extension renames it `Account (Success Overlay)` and declares no plural. Nothing an author writes is refused. + Metadata-protocol: semantic + Skills-docs: follow-up — `skills/objectstack-data/SKILL.md` "Object Extension Model" says an extension "can add but never remove"; a renaming `label` now takes the base's `pluralLabel` off. The sentence wants that exception (outside this change's surface). + + + + **BREAKING** served-value narrowing for an object an extension renames. + + - **Before.** An `objectExtensions` entry or a `.custom` `object_extension` item that set `label` and no `pluralLabel` was folded with the new label and the base's `pluralLabel`. A translated read served the base package's translation of that plural (`objects.NAME.pluralLabel`), so a list title, read plural first, kept the old name in every locale. + - **Now.** `pluralLabel` is the plural of `label`: such an extension is served as an object that declares no `pluralLabel`, in every locale, so a list titles it by its label. Where the owning package's `.custom` pair saves its own translation of `objects.NAME.pluralLabel`, that translation is served (ruling record 6631250894: a translation in the pair, then the override's own text, then the base's translation). + - **Unchanged.** An extension that sets `pluralLabel` is served with it. One that sets no `label`, or repeats the label it extends, keeps the base's plural and its translation. A plural the tenant set stays, and a tenant's own rename yields as before (ADR-0029 D9.2a). An object no extension renames is served as before. The packaged base the translators compare against keeps its plural. + + Nothing has to be rewritten: an extension that wants a plural for its new name declares `pluralLabel` beside `label`, as it always could, and a `.custom` pair can save `objects.NAME.pluralLabel` in its own `translation` item for a translated locale. + +### Patch Changes + +- 31fe289: feat(objectql,cli): `os migrate recompute-home-amounts` — recompute stored home-currency amounts at the rates in force now, report first, rows without a rate listed and skipped + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/objectql`: `recomputeHomeAmounts(engine, logger, { object, field?, since?, apply?, maxRecords?, maxListed?, source? })`**, with `assertHomeAmountRecomputeSince` (the pure `since` check a door can run before it opens a database), `formatHomeAmountRecomputeReport`, `homeAmountRecomputeComplete` and `HOME_AMOUNT_SKIP_REASONS`. It walks every stored row of an object whose `record`-mode amounts declare `currencyConfig.homeAmount` and projects each through the write-time projection itself (the same rate row, division, single rounding, same-currency rule and organization-timezone day), in the row's own organization, so each organization converts into its own home currency at its own rates and is counted apart. A dry run (the default) reports what would change and writes nothing; `apply` writes only the rows that differ, through `engine.update` with the home field in the payload, so the value stored is the one the write itself computes, and only the home and rate fields move (`updated_at` is kept, so a consumer syncing by `updated_at` does not see the recompute). Being a write, it moves what reads the home field as a fresh write would: stored formulas, parent summaries, and the per-row audit rows where audit capture is installed. Rows that cannot be converted are listed by reason and never written: `rate_missing` (with the currency and date), `code_blank` (an amount with no currency code; the remedy is `os migrate record-currency-codes`), `document_date_blank`, `home_currency_unset` and `unconvertible`. `since` filters on the document date (`homeAmount.rateDate`). One run-level `sys_audit_log` entry records each applied run (counts per organization and reason, and the skipped rows), also when an unclassified error stops the run partway (marked `aborted`, then the error is thrown on). A walk cut short by `maxRecords` is `truncated` and not complete. Refused before any row is read: an unknown object (`OBJECT_NOT_FOUND`), an object or `field` declaring no home amount (`INVALID_FIELD`), a `since` that is not `YYYY-MM-DD` (`VALIDATION_FAILED`). + - **`@objectstack/objectql`: `ObjectQL.homeAmountReadsFor(context?)`**, the reads the write-time projection makes (its serving, and the organization a write converts a row under), for a caller that projects a stored row as a write would. + - **`@objectstack/cli`: `os migrate recompute-home-amounts --object [--field ] [--since YYYY-MM-DD] [--apply] [--yes] [--json] [--force] [--max-records] [--database-url]`**, the shell of `os migrate record-currency-codes`, booting the settings service (each organization's `localization.currency` and `.timezone`) and the audit ledger. It exits 1 while any row is listed and skipped, on a dry run as on an apply, because a total over the home field silently leaves such a row out, and when `--max-records` cut the walk short. +- 3b8c724: fix(objectql, metadata-protocol, runtime): a package's export carries the `objectExtensions` it was installed with, and the extended object's owner's export does not + + Clause-②: yes (widening) + Metadata-protocol: none + + A package installed through `POST /api/v1/marketplace/install-local` (what `os package install` sends, and how every entry of a whole-deployment export is restored) that extends another package's object exported without its `objectExtensions`: `GET /api/v1/packages//export` answered the package's other items and no extension, while the extension's fields were folded into the owner's export instead, as if the owner had shipped them. So a deployment restored from an export lost those fields on its next export, with no error, and a `.custom` pair installed from its export no longer exported its extension. + + - **The extending package's export** (and its entry in `GET /api/v1/packages/export`) now carries every extension it contributes as `objectExtensions`, however it was installed: an extension published through `/api/v1/meta/object_extension` (as before) or a manifest's `objectExtensions` entry (install-local, or a code package). A published extension is exported as it is stored; a manifest entry as it was registered, with `priority` written only when it is not the default `200`. + - **The extended object's owner's export** now carries the object as the owner declares it, without the fields, field patches or labels of ANY other package's extension. Until now only its own `.custom` pair's extension was left out; an extension from any other installed package was folded in. + - **Every served read is unchanged**: `GET /api/v1/meta/object/`, the object list, the console and the data API keep serving the object with every extension folded in. + + `SchemaRegistry` gains `getObjectExtensionsOf(packageId)`, which reads a package's `extend` contributions back as the `objectExtensions` entries they were registered from (the inverse of `registerObjectExtension`). `listItems`, `getAllObjects` and `foldObjectExtendersOnto` take an `onlyExtensionsOf` option beside `withoutExtensionsOf`: each object is resolved with only that package's `extend` contributions folded in. The metadata protocol's per-package layer (the package export) now passes `onlyExtensionsOf`, naming the listed package, and no longer passes `withoutExtensionsOf`, which keeps its meaning for any other caller. + + Supersedes, in the same release: the `.custom` pair note for #29903 says an extension contributed by any other installed package is still folded into the base's object in the base's export. After this change it is not: the base's export leaves out every other package's extension, and that package's own export carries it. +- 5f47410: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(objectql, metadata-protocol): a base package's own export carries its objects without its `.custom` pair's extension + + Clause-②: yes (widening) + Metadata-protocol: none + + After an administrator added a field to a vendor package's object through Studio's Customize (an extension published in `.custom`, for example `region__c`, or a relabelled base field), `GET /api/v1/packages//export` carried that field and label inside the base's object, as if the vendor had shipped them. The pair's export carries the same extension as its `objectExtensions` entry, so installing the two exports elsewhere declared the customer's field twice, once as the vendor's. ADR-0146 D8 item 3 says the base's export carries the base unchanged. + + - **The base's export** (and its entry in `GET /api/v1/packages/export`) now carries each object as the base declares it, with no field, label or other patch from its pair's extension. + - **The pair's export** is unchanged: it carries the extension in `objectExtensions`. + - **Every served read is unchanged**: `GET /api/v1/meta/object/`, the object list, the console and the data API keep serving the object with the pair's field and labels folded in. + + An extension contributed by any other installed package is still folded into the base's object in the base's export, as before. + + `SchemaRegistry.listItems` takes a `withoutExtensionsOf` option beside `includeDisabledPackages`, and `getAllObjects` and `foldObjectExtendersOnto` take the same option: each object is resolved with that package's `extend` contributions left out of the fold. Only the metadata protocol's per-package layer (the package export) passes it. +- 314ad95: fix(objectql): the lifecycle sweep no longer runs 60 s after a boot that created its datastore from empty; its first sweep runs one interval later + + Clause-②: yes (widening) — the exported `LifecycleEngineLike` gains the optional member `wasDatastoreCreatedFromEmpty?(): boolean` (an engine without it keeps the old schedule); no accept set changes. + Metadata-protocol: none + + - `LifecycleService.start()` asks the engine, at the initial delay, whether this process created the datastore from empty (`ObjectQL.wasDatastoreCreatedFromEmpty()`, answered from every driver's schema-sync stats without a statement). When it did, the scheduled first sweep is skipped and the sweep runs on the interval, one interval later. Such a store holds no row older than the boot, and no declared window is shorter than an hour, so the skipped sweep had nothing to reap. It still read every declared object, the `lifecycle` settings and every lookup the reference audit walks: 130 statements in the first minute of a hosted tenant kernel's first boot. + - A store that existed before the boot, or an engine that cannot say, sweeps at the initial delay as before. A direct `sweep()` call always sweeps. `OS_LIFECYCLE_DISABLED`, `initialDelayMs` and `sweepIntervalMs` keep their meaning. + - `LifecycleEngineLike` gains the optional member `wasDatastoreCreatedFromEmpty?(): boolean`. An engine double that omits it keeps the old schedule. + - A re-boot on an existing store still sweeps at the initial delay. Persisting a last-swept watermark for that case is separate work. +- cd5f554: fix(objectql,cli): `os migrate stored-expressions` fills a stored computed field on rows stored before it was declared, and the boot names the command when a sample of rows needs it + + Clause-②: yes (widening) + Metadata-protocol: none + + A stored computed field (a non-`formula` field declaring `expression`) is computed by the engine on every insert and update and on nothing else, so declaring one on an object that already holds rows left the new column NULL on every existing row until each was next written; a filter, sort, roll-up or analytics anchor over it read NULL where the expression gives a value, with no error. A fresh database was right and an upgraded one was not. + + - **`@objectstack/cli`: `os migrate stored-expressions [--object ]… [--field ] [--changed] [--apply] [--yes] [--json] [--force] [--max-records] [--database-url]`.** A dry run (the default) reports, per field, the rows holding a NULL the expression gives a value; `--apply` writes them. Each value is the write's own computation over the stored row (option labels, the one reference hop, the `can()` map), and the apply writes through `engine.update` with `updated_at` kept, so hooks, roll-ups over the column and audit capture see an ordinary write. A NULL the expression itself answers (an empty reference, a null input) is left alone. A value that differs from the expression — the expression changed after the row was written — is counted on every run and rewritten only with `--changed`; an expression reading the clock always differs there. A row on which an expression does not evaluate while it holds a value is listed and not written, because the write would store NULL over it. The command exits 1 while a row is skipped or could not be written, or when `--max-records` cut the walk short. Idempotent: a re-run finds nothing to fill. + - **Boot advisory.** At `kernel:bootstrapped` the engine plugin reads the first 100 rows of each object declaring a stored computed field (nothing for an object declaring none) and, when that sample holds such a NULL, warns once naming each `object.field` and `os migrate stored-expressions`. It writes nothing and never fails a boot. + - **`@objectstack/objectql`:** `backfillStoredExpressions`, `formatStoredExpressionBackfillReport`, `storedExpressionBackfillComplete`, `announceStoredExpressionHoles`, `sampleStoredExpressionHoles`, and `ObjectQL.computeStoredExpressionsOver(object, rows, context?, onFault?)` — a stored computed field's values over rows already stored, computed as a write computes them. +- 343ddb2: refactor(objectql): `ObjectRepository` and `ScopedContext` in `engine.ts` are facades over their own facet modules, moved by the `engine.ts` codemod's second pass; no behaviour or published declaration changes + + Clause-②: no + Metadata-protocol: none + + - 20 of the 24 methods of the two classes keep their signatures and public docblocks on the class and forward, in one call, to an unedited copy of their body in `engine-object-repository.ts` or `engine-scoped-context.ts`. Each instance gets one host, built on first use (`engine-scoped-hosts.ts`), so a spy or a subclass override on an instance is still the method a body reaches. + - Four `ScopedContext` methods keep their bodies on the class, each named in the facade gate's registry with the card that will move it: `transaction` and the symbol-keyed `SEED_USER_GRANTS` method hand the instance itself on; `derive` and the symbol-keyed `APP_AUTHORED_HOOK_API` method set a private flag on another instance. + - Nothing to do on upgrade: the exported names, the published `.d.ts` surface and what every method does are the same. +- 0c666c3: refactor(objectql): the private docblocks of the last 28 hand-series stubs in `engine.ts` move into their hand modules, onto the functions that hold their bodies; no behaviour or public declaration changes + + Clause-②: no + Metadata-protocol: none + + - The second and last slice of the `engine.ts` codemod's `--hand-docs` pass: 10 hand modules, 858 docblock lines. With the first slice, every private hand-series stub that had a docblock now keeps it beside its body; public and protected docblocks stay on the class. `engine.ts` goes from 12,618 to 11,752 lines. + - The liveness ledger's `query.groupBy` row now cites `engine-delete-and-aggregate.ts#aggregate`, where the body lives, because `engine.ts` no longer names the key. + - Nothing to do on upgrade: the exported names, the public `.d.ts` surface and what every method does are the same. +- f672e0d: refactor(objectql): the private docblocks of 36 hand-series stubs in `engine.ts` move into their hand modules, onto the functions that hold their bodies; no behaviour or public declaration changes + + Clause-②: no + Metadata-protocol: none + + - A new third pass of the `engine.ts` codemod, `--hand-docs`, moves a private hand-series stub's docblock onto the function its stub calls, as the first two passes already do for the bodies they move. A public or protected method keeps its docblock on the class. + - This is the first of two slices: 8 hand modules here, the other 9 next. `engine.ts` goes from 13,550 to 12,618 lines. + - Nothing to do on upgrade: the exported names, the public `.d.ts` surface and what every method does are the same. +- bfbdeec: refactor(objectql): the eight `ObjectQL` methods that kept an inferred return type spell it, and five of them move into their facets + + Clause-②: no + Metadata-protocol: none + + - `registerApp`, `registerHook`, `registerDriver`, `init`, `triggerHooks`, `destroy`, `setDatasourceMapping` and `getStatus` now declare their return type, copied from the published declaration file it was inferred into. The bodies of `getStatus`, `triggerHooks`, `setDatasourceMapping`, `init` and `destroy` move out of `engine.ts` into their facet modules (the methods stay on the class as one-call stubs). The built declaration files are byte-equal before and after, and no behaviour changes; only the source maps move. Nothing to do on upgrade. +- 4adc890: refactor(objectql): the six `ObjectQL` methods the #4918 query-options erasure pin kept in `engine.ts` are typed and move into their facet modules; no behaviour or public declaration changes + + Clause-②: no + Metadata-protocol: none + + - The six internal reads in `seedAutonumber`, `referenceExists`, `resolveMasterDetailParent`, `resolveMasterDetailParents`, `inspectDanglingReferences` and `probeReferencingRows` no longer erase their query options to `any`. Four are plain literals `tsc` now checks against `EngineQueryOptions`; the keyset walk's `where` and the dangling-reference audit's port options are named as `EngineQueryOptions` slots instead. `engine.ts` leaves the erasure baseline with zero sites. + - A new fourth pass of the `engine.ts` codemod, `--members`, moves the six bodies the way the first pass moves a body, into the facet modules it already wrote, and proves the move by undoing it. `engine.ts` goes from 11,771 to 11,316 lines. + - Nothing to do on upgrade: the exported names, the public `.d.ts` declarations and what every method does are the same. +- 2046d21: feat(objectql,rest)!: a new assignment to a banned user is refused, and a banned user's records can be counted and handed over in bulk (#30205) + + Clause-②: yes (narrowing) — the published `FieldErrorCode` enum gains the member `reference_user_banned`. A non-system create or update that sets a user field (the owner field, a `user` field, or a `lookup` to `sys_user`) to a user whose ban is in force is refused `400 VALIDATION_FAILED` with that field code, a write that answered `201` / `200` before. A value the row already stores is not judged, so a record that already holds the banned user stays editable. The two new routes and SDK methods are additions. + Metadata-protocol: none + Skills-docs: no move in `skills/**`, which teaches no user-assignment verdict; `content/docs/permissions/administrator-guide.mdx` gains "Handing over a banned user's work" and its offboarding quick path points at it. + + + + **BREAKING** accept-set narrowing at every data write door (`POST` / `PATCH /api/v1/data/OBJECT`, batch, import, and a flow's elevated record write, which keeps the reference check): banning a user signs them out, but the records they own and are assigned stay where they were, and new work could still be handed to them. + + - **Before.** The Console's person picker hid a banned user, but `POST /api/v1/data/OBJECT` with `assignee` set to that user answered `201`, so new work landed on someone who cannot sign in. + - **Now.** The write-time reference check also asks whether a referenced user's ban is in force (`banned` is true and `ban_expires` is empty or still ahead) and refuses such a value with `reference_user_banned`, beside `reference_not_found`. It is read the way the existence probe reads, so a caller who cannot read the user object is refused the same way. A ban whose expiry has passed is not in force. + - **Unchanged.** A value every row of the write already stores (an edit form carries the whole record back), a system write the reference check already exempts (seed, install, restore), and a missing user (still `reference_not_found`). + - **New: the count.** `GET /api/v1/security/users/ID/assigned-work` (`client.security.assignedWork.count`) answers, per object and user field, how many records hold the user, counted under the caller's own context. Platform tables (`sys_*`), `managedBy` objects and `readonly` / `system` fields such as `created_by` are not counted; objects the caller cannot read are listed in `unreadable`; at most 200 fields are counted (`truncated`). + - **New: the hand-over.** `POST /api/v1/security/users/ID/assigned-work/transfer` (`client.security.assignedWork.transfer`) with `{ toUserId, objects? }` moves up to 500 records per call to an active user, each as an ordinary update under the caller's context, and answers per field what moved and what failed, plus how many remain. A banned, unknown or identical `toUserId` is refused before any write. Multi-valued user fields are counted but not transferred. Neither route touches the ban. + - **`@objectstack/spec`.** `FieldErrorCode` gains `reference_user_banned`, with its message in `en`, `zh-CN`, `ja-JP` and `es-ES`. + + **For a caller.** A write that names a banned user in a user field names an active one instead; the refusal names the field and the user. No stored metadata or row changes, so there is nothing for `objectstack migrate meta` to rewrite. An integration that reassigns work after a ban can call the transfer route and repeat it while `remaining` is above 0. +- a26abbc: fix(objectql): an uninstalled package no longer reads as read-only until a restart + + Clause-②: yes (widening) — `@objectstack/objectql`'s exported `SchemaRegistry` gains the public method `onPackageUninstalled(listener)`, which returns its unsubscribe. Nothing is refused that was accepted before. + Metadata-protocol: none + + An administrator who uninstalled a package through `DELETE /api/v1/marketplace/install-local/:id` got a 200 "withdrawn from the running kernel", but on the same instance the package id still read as a booted code package until the server restarted. Uninstalling a `.custom` pair that an older runtime had installed as code, as the pair install now prescribes, left `POST /api/v1/packages//ensure-customization` answering `created: true` with `writable: false`, so nothing could be customized into the new pair. The engine's manifest map, which the ADR-0070 writability check reads first, kept the id after the registry had withdrawn the package. + + `SchemaRegistry.uninstallPackage` now announces each completed uninstall to `onPackageUninstalled` listeners, after the package record is gone (never for a refused uninstall), and the engine drops its manifest entry there, so the id is released on whichever door withdrew the package, not per door. A reinstall of the same id registers it again as before. A listener that throws does not turn the completed uninstall into a refusal; it is logged at `error`. +- f7afab8: fix(core,objectql): effective object permissions read the external write gate, so a read-only external object is no longer offered New, Import, Edit or Delete + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Behaviour.** On an object whose external write gate is closed (ADR-0015 §5.3 Gate 3: a federated datasource, and not both `datasource.external.allowWrites` and `object.external.writable`), every insert, update and delete is refused `403 EXTERNAL_WRITE_FORBIDDEN` for every caller. The effective object-permission map now says so: the entry grants no `allowCreate`, `allowEdit`, `allowDelete` or `allowTransfer`, a `modifyAllRecords` on it is lowered to the `viewAllRecords` read it implies, and `apiOperations` carries no write operation (`create`, `update`, `delete`, `upsert`, `bulk`, `import`, `restore`, `purge`). Read and export are unchanged. Both consumers move together: `/auth/me/permissions` (the console hides those doors with no client change) and `ISecurityService.getEffectiveObjectPermissions`, so `current_user.can(object, 'create' | 'edit' | 'delete' | 'transfer')` now answers `false` on such an object, as the write itself always did. + - **`@objectstack/objectql`: `ObjectQL.isExternalWriteForbidden(objectName)`**, the gate's verdict as a read-only question. The write gate (`assertWriteAllowed`) reads the same verdict, so the two cannot disagree. + - **`@objectstack/core`: `engineExternalWriteForbidden(engine)`** (the accessor over an engine, asked when the member is present) and the optional `isExternalWriteForbidden` accessor on `EffectiveObjectPermissionsSchemaSource`; `annotateEffectiveApiOperations` takes the same predicate as an optional third argument. A caller that hands over neither gets the map it got before. +- 7b80724: fix(objectql): a roll-up summary a disabled package adds by extension is recomputed when the package is enabled again + + Clause-②: no — a stored summary column stays current across a disable; nothing a caller sends is refused + Metadata-protocol: none + + - **Before.** While a package was disabled, the `summary` fields its `objectExtensions` add to another package's object were withheld from the roll-up index with the rest of the served fold. When the summary's child belongs to an enabled package, the child writes made during the disable landed and recomputed nothing, so after `PATCH /packages/:id/enable` the parent served the value the summary held at the disable, until the next write of one of its children. + - **Now.** Enabling a disabled package recomputes, on every parent row, the roll-ups it adds by extension, and writes a row only where its stored value differs. It runs on every enable door (`SchemaRegistry.onPackageEnabled`, new), and `PATCH /packages/:id/enable` (and an install's `enableOnInstall: true`) answers after it has settled. A row that cannot be recomputed is logged at `error` and the rest of the walk goes on; `ObjectQL.refreshSummariesEnabledBy(packageId)` (new) re-runs it. +- 63de248: fix(objectql, metadata-protocol, cloud-connection)!: a declared `.custom` action override is keyed and judged the same way at every door, and only its declared base's handler slots beneath it + + Clause-②: yes (narrowing) — the protocol door (save and publish) now refuses a declared action override whose `objectName` is not the base action's, with `403 NOT_OVERRIDABLE` and rule `declared_package_ships_no_such_item`, the refusal `os validate` and the managed install already gave it; the save door used to admit it. The managed install now admits a re-install of the pair whose override relaxes an earlier tightening back to the base's own gates, which it used to refuse with `declared_action_widens_access` while the save door admitted it. Every other input answers as before. + Metadata-protocol: none + Skills-docs: follow-up #30505 — the declared action override's docs card; it owes one sentence that an override keeps the base action's `objectName`. + + + + **BREAKING** accept-set narrowing at the protocol door (`@objectstack/metadata-protocol`, ADR-0146 D2/D9): a declared action override names its base's action by `:`, the identity `os validate` and the managed install key it on. + + - **Before.** The save and publish doors matched a declared action override's base by name alone, so an override in the base's `.custom` pair could move the base's action to another object. Its handler was not rebound there, and `os validate` and the install door refused the same body. + - **Now.** The save door refuses it as the other two doors do: the base ships no action of that name on that object. Keep the base action's `objectName` in the override, or remove `overrides` and add the action under the pair's own name. + + `@objectstack/cloud-connection`: the managed install judges a declared action override against the installed base's code artifact (`SchemaRegistry#getArtifactItem`), as the save door does. The protocol's per-package listing it read before serves an armed override under the base's envelope, so a re-install of the pair was judged against its own previous version. + + `@objectstack/objectql`: `ObjectQL#registerActionOverride` takes the package the override declares (`overrides.package`) as an optional fifth argument, and the authored-action resync passes it. `registerAction` under a key such an override holds slots the handler beneath it only when the registry's code artifact of the action belongs to that package, on that object; any other registration, one with no package included, replaces the override and is warned like every other handler collision. Before, any other package's registration slotted beneath it, so a row of another base's pair bound before the real base registered kept running after the real base's handler arrived, while every read served the real base's action; and a registration with no package slotted beneath it at `debug`, where the method's docblock promises a warning. +- a2437b5: fix(objectql): a package registered again lists its navigation contributions once, not once more per registration + + Clause-②: no — the served app navigation returns to what a fresh boot serves; nothing a caller sends is refused + Metadata-protocol: none + + - **Before.** Registering a package body appended its `navigationContributions` to the target app's list every time. An artifact reload (`metadata:reloaded`, fired when `os dev` recompiles the artifact or on the HMR reload POST) re-registers every package of the artifact, so after one reload the sidebar listed every module's contributed entries twice, three times after the next, until the server restarted. An uninstalled package's contributions also stayed in the app. + - **Now.** A package body's contributions replace that package's previous set (`SchemaRegistry.setPackageNavContributions`, new), in the place its first registration put them, so the navigation after any number of reloads, re-registrations and disable/enable cycles equals a fresh boot's, in the same order. Uninstalling a package drops its contributions. `registerAppNavContribution` keeps its additive meaning. +- 7193160: fix(objectql): a data driver receives the declared driver options and nothing else + + Clause-②: no + Metadata-protocol: none + + The engine → driver hop is a closed contract, as the engine's own option bags are: every driver call the engine makes is handed the keys `DriverOptionsSchema` declares and no other key. + + - **Which calls.** `find`, `findOne`, `count`, `aggregate`, `insert`, `update` and `delete` (by id and multi-row), the reads and writes those verbs make along the way, and the raw `execute` door. + - **What arrives, unchanged.** `transaction`, `tenantId`, `tenantIds`, `timezone`, `preserveAudit` and `bypassTenantAudit` reach the driver exactly as before, and a caller's explicit value still wins. The schema's other declared keys pass when a caller sets them at a door that admits them, which is `insert` and the raw `execute` door; `find`, `findOne`, `update` and `delete` admit only the six keys above at their own door. The retired `timeout` tombstone is not handed on. + - **What stays with the engine.** Undeclared keys in a caller's engine option bag stay with the engine and are no longer copied onto a driver's options. A driver reads the query from its `query` argument, as the driver contract declares. + - **Hooks are unchanged.** An `after*` hook still reads `input.options` as the caller's bag with the driver options merged on. + - **Who moves.** A driver that read a key `DriverOptionsSchema` does not declare off its options argument: read the query from the `query` argument, or have the key declared on `DriverOptionsSchema` first. No driver in this repository reads one. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [88cd0ab] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [07685ae] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [43faa22] +- Updated dependencies [fdad178] +- Updated dependencies [15f808a] +- Updated dependencies [bcdf8d3] +- Updated dependencies [59bcd22] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [0837fdc] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [67d5d53] +- Updated dependencies [47226e6] +- Updated dependencies [a545a04] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [8fad7b1] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [8fa738f] +- Updated dependencies [60fe44f] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [0e4747a] +- Updated dependencies [4bf6e8f] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [63de248] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [a91ad42] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [1030a80] +- Updated dependencies [55a00ba] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [713beba] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/metadata-protocol@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/metadata@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/objectql/package.json b/packages/objectql/package.json index 154150dff40..133b8467bf7 100644 --- a/packages/objectql/package.json +++ b/packages/objectql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/objectql", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Isomorphic ObjectQL Engine for ObjectStack", "type": "commonjs", diff --git a/packages/observability/CHANGELOG.md b/packages/observability/CHANGELOG.md index 274d3bbe42d..91a12f2b81c 100644 --- a/packages/observability/CHANGELOG.md +++ b/packages/observability/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/observability +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/observability/package.json b/packages/observability/package.json index b11247e7937..09a9c86a0a5 100644 --- a/packages/observability/package.json +++ b/packages/observability/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/observability", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Observability contracts and exporters for ObjectStack — MetricsRegistry, ErrorReporter, Logger plus noop/console/OTLP-HTTP exporters. Deployment-target neutral; runtime and services depend on this so the same instrumentation works on Cloudflare Workers, Node, and self-hosted Kubernetes.", "type": "module", diff --git a/packages/platform-objects/CHANGELOG.md b/packages/platform-objects/CHANGELOG.md index 81d65e3afc6..6412db4ff9c 100644 --- a/packages/platform-objects/CHANGELOG.md +++ b/packages/platform-objects/CHANGELOG.md @@ -1,5 +1,227 @@ # @objectstack/platform-objects +## 18.4.0 + +### Minor Changes + +- c7d1367: feat(spec): an action param can name the widget that draws it (`widget`, same key as on a field), so a param can ask for the permission-set picker (ADR-0131 D4) + + Clause-②: yes (widening) + Metadata-protocol: added + + - **`@objectstack/spec`: `ActionParamSchema.widget`**, an optional string with the same key, vocabulary and meaning as `FieldSchema.widget`. It names a registered field component, looked up as `field:` plus the name, to draw the param in the action dialog instead of the `type` default. `type` stays the value's type. The case it exists for is `widget: 'permission-ref'` on a `text` param: it lists the permission sets of the security catalog and submits the chosen machine name, the picker the grant column `sys_user_permission_set.permission_set` already uses. Like the field key it is an open string: an unregistered name is kept, and the dialog draws the `type` renderer instead. A non-string is refused. A param that does not declare the key parses exactly as before. Until now a param naming `widget` was refused as an unknown key. + - **Not read yet.** The console's param resolver does not carry the key to the dialog's widget until objectstack#30260 lands. Until then a param that declares it is drawn by its `type` renderer, and its value is submitted as before. The key's description says so, and the liveness ledger grades it `planned` with an author warning: `os validate` and `os lint` warn an author who writes it. + - **`@objectstack/platform-objects`: the action form's Params repeater offers the key** as a `Widget` column, labelled in en, zh-CN, ja-JP and es-ES. + +### Patch Changes + +- a52761f: fix(console): an action another package places on `sys_user` at `record_header` / `record_more` draws again on the user's record page, in the drawer and on the full page (#29951) + + Clause-②: no + Metadata-protocol: none + + - **The header reads `placedActions: true`.** `page:header`'s `placedActions` (#30018) is now drawn by the console's header renderer (`PageHeaderActions` in `@object-ui/components`): `true` draws every action of the page's object whose `locations` include `record_header` or `record_more`, in declaration order, read from the object's metadata at render time, so an action an installed package adds to the object is drawn without editing the page. Omitted or `false`, an authored `actions` list is still the whole set (#11592). A `true` beside an authored list, which the spec refuses, draws none of the object's actions and is reported once in the console. + - **One placed-header reader.** `placedHeaderActions` (`@object-ui/types`) is the rule both headers read: the synthesized default record page's header in `RecordDetailView` and a `placedActions: true` header. The `page:header` registration publishes the `placedActions` input. + - **The platform record pages opt in.** `sys_user_detail` and `sys_organization_detail` (`@objectstack/platform-objects`) and `sys_approval_request_detail` (`@objectstack/plugin-approvals`) set `placedActions: true` in place of the id lists #27219 froze, which dropped every header action another package places on those objects. The tab strips are unchanged, and every action the lists named is placed at `record_header` / `record_more`, so each still draws. + - `@objectstack/spec`: the `placedActions` describe no longer opens with `[PLANNED — not yet rendered]`, and the `page` liveness record names the reader. +- 86ae2ca: fix(platform-objects): zh-CN metadata forms say 仪表盘 for dashboard (#30164) + + Clause-②: no + Metadata-protocol: none + + The zh-CN dashboard authoring form the platform serves (`MetadataFormsTranslations`, which `/api/v1/meta/types` localizes forms with) still said 仪表板 on six strings: the form title, the basics section description, and the help text of the header, show-title, show-description and widgets fields. The Console and Studio say 仪表盘, so one object carried two names in one flow of work. All six now say 仪表盘. The generated bundle is the extract's merge baseline, so its leaves were corrected in place and the bundle regenerated (`pnpm i18n:extract` reproduces it byte for byte; `pnpm check:i18n` is green). en, ja-JP and es-ES are unchanged. +- 977be38: fix(service-settings)!: Setup no longer offers a Feature Flags page whose four toggles changed nothing (#30204) + + Clause-②: no (narrowing) — removes the four settable keys of the `feature_flags` settings namespace (`ai_enabled`, `kanban_swimlanes`, `realtime_cursors`, `inline_comments`), the namespace itself (left empty by their removal), its Setup entry and the `featureFlagsSettingsManifest` export. Nothing is widened. + Metadata-protocol: none + + + + **BREAKING** for an administrator who saved one of the four Feature Flags toggles, and for a host that imports `featureFlagsSettingsManifest`. + + - **Why.** No code read any of the four keys, so turning one on changed nothing (ADR-0049 enforce-or-remove). A flag comes back with the feature that reads it. + - **Removed keys.** `feature_flags.ai_enabled`, `feature_flags.kanban_swimlanes`, `feature_flags.realtime_cursors` and `feature_flags.inline_comments`: removed; each had no effect. The in-app AI panel is decided elsewhere and is unchanged. + - **The namespace.** `feature_flags` is no longer registered, so `GET` and `PUT /api/settings/feature_flags` answer `404 UNKNOWN_NAMESPACE`, and `SettingsService.getManifest('feature_flags')` throws `UnknownNamespaceError` (`SETTINGS_UNKNOWN_NAMESPACE`). Setup's Configuration group no longer lists Feature Flags (`@objectstack/platform-objects`, entry `nav_settings_feature_flags` and its four locale labels). + - **Stored values.** A `sys_setting` row saved under `feature_flags` and an `OS_FEATURE_FLAGS_*` environment variable are left in place and become inert: nothing registers the namespace, so nothing resolves them. They are not cleaned up; delete them at will. + + The one-line fix for a host: + + - FROM `featureFlagsSettingsManifest` TO nothing: drop it from the host's `SettingsServicePlugin({ manifests })` list; `builtinSettingsManifests` no longer carries it. +- 740edc5: fix(plugin-audit,platform-objects): the audit log's default list shows what people did, not the rows the platform writes beside it; the audit objects read in Chinese + + Clause-②: no + Metadata-protocol: none + + Setup's 审计日志 opens the first `sys_audit_log` list view, `recent`. One sign-in used to put two rows there (the `login` row and an `update` of the person's `sys_user` stamping `last_login_at` / `last_login_ip`), the first sign-in through the console a third (the token-signing key in `sys_jwks`), and one settings change three or four (the `config_change` row, the stored setting, a secret's ciphertext, and the settings trail's own entry). + + - The ledger writer now marks the sign-in stamp row with `metadata: {"event":"auth.last_login_stamp"}`, the way `config_change` rows carry `settings.set`. Only an update of `sys_user` whose changes are the stamp's fields alone is marked; an administrator's edit of a user is not. The row is still written. + - `recent` leaves out the marked row, the rows on `sys_secret`, `sys_setting_audit`, `sys_platform_setting_audit`, `sys_jwks`, `sys_migration` and `sys_migration_journal`, and the rows on `sys_setting` / `sys_platform_setting` other than `config_change`. Nothing is removed from storage, and `all_events` (全部) still lists every row. + - `sys_platform_setting` and `sys_platform_setting_audit` were in no translation bundle, so they read English in every locale; they now ship in `@objectstack/platform-objects`' bundles with zh-CN translations (ja-JP and es-ES carry the English fill). `sys_comment_reaction`'s zh-CN bundle is translated. +- 778aedf: fix: the Business Unit page and the `security-private-no-readscope` hint say what needs the enterprise edition + + Clause-②: no + Metadata-protocol: none + + - **Business Unit page (`sys_business_unit` description, en and zh-CN).** It used to say units are "used to scope data and reports". It now says what a unit does in the open edition: sharing rules can share records with a unit, or a unit and the units under it, and members get that access and lose it when they leave. It also says that letting a permission set read every record of a unit (read scope `unit`) needs the enterprise edition. That matches the compile refusal, which is unchanged. + - **Business unit Timezone help (en and zh-CN).** It used to say date boundaries are computed in this zone. Nothing reads the column, so the help now says it changes no date calculation and that Settings → Localization → Default timezone is the one in effect, as the organization's Timezone help already does. + - **`security-private-no-readscope` hint (`os validate`, `os lint`).** The hint now offers only the scopes the stack can compile. When `requires` lists `hierarchy-security`, nothing changes: it offers `own_and_reports`, `unit`, `unit_and_below` and `org`. Without it, the hint offers `org` (or widening the object's `sharingModel`), and says that the three hierarchy scopes need `requires: ['hierarchy-security']`, which `@objectstack/security-enterprise` provides. The rule still fires on the same grants at the same severity. +- 71318af: fix(platform-objects): Edit organization's Parent Organization and Sort Order help says they matter only where the deployment runs several organizations + + Clause-②: no + Metadata-protocol: none + + On a deployment with one organization, Edit organization still shows 上级组织 / Parent Organization and 排序 / Sort Order, and nothing told the administrator they do nothing there. Their help text, in the object declaration and in the `en` / `zh-CN` packs, now ends with "Meaningful only where the deployment runs several organizations." (「仅在部署运行多个组织时有意义。」). No field, permission or behaviour changes; the fields stay visible. +- 0005fec: fix: on a zh-CN console every row of the view properties panel's collapsed sections (表格选项 / 分页 / 终端用户控制 / 导航与共享) reads in Chinese + + Clause-②: no + Metadata-protocol: none + + - `viewForm` (`@objectstack/spec/ui`) now spells out the children of its `selection`, `pagination`, `userFilters`, `appearance`, `userActions`, `navigation` and `sharing` composites, and gives the rows that had none a hint. Each declared hint is the schema description the panel already showed, so an English console reads the same text; `os i18n extract` now emits a `metadataForms.view.fields['.']` key per row. + - `userFilters.fields` is enumerated as an inline-table repeater, and `UserFilterFieldSchema`'s six properties carry a `.meta({ title })` (Field / Label / Type / Options / Show Count / Default Values) — the English column heads, unchanged. + - The panel no longer offers `navigation.view` and `sharing.type`: both are `retiredKey()` tombstones that the schema-derived child list used to render as editable rows. A stored value is untouched by a panel save (the composite editor spreads the block it was given). + - The platform metadata-forms catalogs carry zh-CN, ja-JP and es-ES text for every new row, and the console's zh-CN overlay copies the zh-CN rows. +- e8e924f: fix(plugin-auth,platform-objects): an organization's require-MFA refusal reads in the caller's language, an accepted save that turns it on names the grace period in force, and the field help says two-factor must be on (#30411) + + Clause-②: no + Metadata-protocol: none + + - **Before.** Saving `sys_organization.require_mfa` on while two-factor enrollment was off was refused with an English sentence, whatever language the administrator's console used. An accepted save that turned it on answered nothing about when members would be blocked, and the configured grace period (`auth.mfa_grace_period_days`) was readable only through the `auth` settings, which need `manage_platform_settings`, a capability an organization administrator does not hold. The field help did not say the requirement needs two-factor on. + - **Now.** The refusal is in the request's language (English, Simplified Chinese, Japanese, Spanish; matched from `Accept-Language` exactly, then by language, else English), with the same shape as before: `400 VALIDATION_FAILED`, one `require_mfa` / `invalid_value` field entry carrying the same sentence as the top-level message. A save that is admitted and turns the requirement on (two-factor on, the stored value off, or an insert) answers a save `notice` on the create or update answer, in the same language: every member without an authenticator app must enroll, within the grace period in force, in days, read per write from the same setting the MFA gate's clock holds to. A save that turns nothing on (clearing it, re-saving a stored `true`, writing other columns) answers no notice. The field help, in all four languages, says the requirement needs two-factor authentication on for the deployment (`OS_AUTH_TWO_FACTOR=true`) and that saving it on is refused while it is off. + - **Unchanged.** Which writes are refused and which are admitted, the refusal's code, status and field entry, the MFA gate and its grace clock, and every `auth` setting and its read permission. No member is added to any answer: `notice` is the create and update answers' existing optional member. +- 35cd64c: fix(platform-objects): a team membership is named by the person and the team, and a team's Members list shows the user and when they joined, not two raw ids + + Clause-②: no + Metadata-protocol: none + + - **The record title.** `sys_team_member`'s title (`display_title`) composed its two stored lookup ids, so a team's Members panel, a membership's own page and every reference to the row read `USER_ID in TEAM_ID`. It now reads the user's `name` (their `email` where the name is blank) and the team's `name`, one hop through each lookup (ADR-0079 addendum, 2026-10-18), as `sys_business_unit_member`'s title does: `Ada Lovelace in Core Platform`. Both hops are read as the reader: a user or team the reader may not read, or one that no longer exists, leaves the title empty rather than showing an id. No column is stored, so a renamed user or team is named anew on the next read. + - **The team page's Members list.** A related list with no authored columns leads with the record title (ADR-0079), so the Members list opened on a Title column beside the User column. The `team_id` lookup now declares `relatedListColumns: ['user_id', 'created_at']`: the list shows the user and when the membership began, drawn as declared, with no title column. + - **A user's Teams list.** The `user_id` lookup declares `relatedListColumns: ['team_id', 'created_at']`, for a user page that derives its related lists. Setup's own `sys_user_detail` page is authored and lists no teams, so it is unchanged. +- e3948ae: fix(platform-objects): an organization membership, a two-factor enrollment and the two SCIM join rows are named by the people and groups they point at, not raw ids + + Clause-②: no + Metadata-protocol: none + + - **The record titles.** Four identity join objects built their title (`display_title`) by splicing stored lookup ids, so a record page's heading, a list's Title column and every reference to the row read ids. Each now reads names one hop through its lookups (ADR-0079 addendum, 2026-10-18), as `sys_business_unit_member` and `sys_team_member` do. A person is their `name`, or their `email` where the name is blank: + - `sys_member`: `Ada Lovelace (admin)`, or `Ada Lovelace` with no role; + - `sys_two_factor`: `Two-factor for Ada Lovelace`; + - `sys_scim_projection_grant`: `billing_admin → Ada Lovelace`; + - `sys_scim_group_member`: the provisioned user's `user_name` and the group's `display_name`, each its object's own title: `ada.lovelace@idp.example in Operations`. + + Every hop is read as the reader: a person, provisioned user or group the reader may not read, or one that no longer exists, leaves the title empty rather than showing an id. No column is stored, so a rename shows on the next read. + - **The authorization prelude reads no title.** `@objectstack/core`'s grant resolver reads `sys_member` three times (the caller's memberships, the active organization's first page of members, and the position-holder count). Each read now names the columns it decides on, as the `sys_user_permission_set` grant read already did, so the membership's title, which now reads a `sys_user` row, is never computed there. An authenticated request still pays the same reads and round trips as before. +- f4cd4ec: fix(core)!: a banned user's credentials are refused while the ban holds, at every door that accepts an API key + + Clause-②: yes (narrowing) — while a user's ban holds, a request authenticated by an API key that user owns is answered as a key that does not exist (the generic `401 UNAUTHENTICATED`), at every door that accepts a key: the REST data door, the MCP door, and every other door that resolves a key through the shared key-admission seam. The widening half: `@objectstack/core` exports the one ban predicate, `isUserBanInForce` (with `USER_BAN_FIELDS` and the `UserBanFields` type), which the key-admission seam and plugin-auth's session-shaped refusals now share. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at every key-admitting door (`@objectstack/core`): a banned user's API keys are refused while the ban holds. + + - **The rule.** A ban holds every credential of the user. Banning a user already ends their sessions and refuses their sign-in; their API keys now answer as a key that does not exist for as long as the ban holds. A ban with an expiry stops binding once the expiry has passed, exactly as sign-in does — no sign-in is needed to lapse it. + - **Not a revocation.** No key row is revoked or rewritten. Lifting the ban (or letting it expire) admits the same keys again, with nothing reissued, each still judged by its own lifecycle: a key left unused past the inactivity window during the ban answers as inactive (`API_KEY_INACTIVE`), and its remedy is a new key. + - **One anonymous entry.** A key refused for its owner (a ban) or for the tenancy posture (no organization under `isolated`) resolves to the same entry as a key that does not exist, on every composition: the guest's grants on a face that serves the guest, nothing elsewhere. It never falls through to a session. + - **Who moves.** An integration running on a key whose owner is banned gets `401` until the ban is lifted or expires. + + **The remedy.** Lift the ban, or mint a replacement key under a user who is not banned. +- b3d2f4b: fix(runtime,plugin-auth)!: a banned user's OAuth access tokens and refresh grants are refused while the ban holds + + Clause-②: yes (narrowing) — while a user's ban holds, an OAuth access token that user consented to is answered at the MCP door as a token that does not verify (the generic `401`, with the same body and `WWW-Authenticate` header), and so is an agent forward credential acting for that user; `/oauth2/userinfo` answers that token as an access token that does not verify (`401 invalid_token`), and `/oauth2/introspect` answers that user's access and refresh tokens as inactive (`{ "active": false }`); the token endpoint mints that user no token: a `refresh_token` grant answers as an invalid refresh token (`400 invalid_grant`), and an `authorization_code` exchange is refused, with the code spent: by the provider's own session answer (`400 invalid_request`) when the ban ended the user's sessions, as the ban routes do, and otherwise as an invalid code (`400 invalid_grant`). Those are the doors this change narrows. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at the MCP door and at the OAuth authorization server's token, userinfo and introspection endpoints: a banned user's OAuth access tokens and refresh grants are refused while the ban holds. + + - **The rule.** Banning a user already ends their sessions and refuses their sign-in and their API keys. While the ban holds, their OAuth access tokens are now also refused at the MCP door and read as not valid at userinfo and introspection, and the token endpoint mints them no new token (no refresh, no code exchange). A ban with an expiry stops binding once the expiry has passed, exactly as sign-in does. + - **Not a revocation.** No token, refresh token or consent row is revoked or rewritten. Lifting the ban (or letting it expire) admits the same access token at the MCP door and lets the same refresh token renew, with nothing reissued. An authorization code presented while the ban held is spent, so the client signs the user in again. + - **Cost.** One primary-key read of the user's ban columns per OAuth credential the MCP door resolves, and one read of the user row per active introspection answer that names a user; the token and userinfo endpoints read nothing extra. A store fault on the MCP door's read answers `503 SERVICE_UNAVAILABLE`, as every other authorization-input read on that door does; a store fault on the introspection read fails the request rather than answering active or inactive. + - **Who moves.** An MCP client or OIDC relying party connected over OAuth on behalf of a banned user gets `401` at the MCP door and at userinfo, `{ "active": false }` from introspection, and `400` from the token endpoint, until the ban is lifted or expires. The Ban User confirmation (`@objectstack/platform-objects`) and the administrator and authentication guides now say so. + + **The remedy.** Lift the ban, or connect the client as a user who is not banned. +- 7b2bb66: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(plugin-auth): a session a password reset ends leaves a revocation trail + + Clause-②: no + Metadata-protocol: none + + - **A session a password reset ends leaves a revocation trail.** When a completed self-service password reset ends the account's sessions — the emailed link (`POST /api/v1/auth/reset-password`), and the phone OTP reset (`POST /api/v1/auth/phone-number/reset-password`); better-auth's email-OTP reset door, which the platform does not mount, is covered on its own path too — each `sys_session` row is now kept and stamped, `revoked_at` with `revoke_reason: password_reset`, the same kind of trail an interactive revoke leaves, instead of being deleted. The session is still refused (`401`). + - **The value says who ended the session.** `password_reset` is distinct from `user_revoked` (the owner revoking their own sessions) and from `admin_password_reset`, which an administrator's forced reset keeps writing. + - **`sys_session.revoke_reason`'s published vocabulary names `password_reset`** (`@objectstack/platform-objects`, the field's description and its translated help). + - **Unchanged.** Which sessions a reset ends; sign-out, which still deletes its row; the account's API keys, which a self-service reset does not revoke; and the audit log, which records no `logout` row for a reset. +- bdbaf80: fix: Setup's form help speaks to an administrator, and the queue pages, the mail status banner and the test-email toast read in Chinese on a zh-CN console + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Field help (English source and zh-CN):** the permission set and position assignment forms, the out-of-office (approval delegation) rule, and the business unit, organization and user forms no longer describe the implementation. Gone: "fail-closed", "half-open [from, until)", "agent grants", "graph semantics", "expanded by graph queries", "Null = root of tenant", "HRIS", "When true / When false" (zh-CN 「为 true 时」 now reads 「开启后」) and an HTTP status code. The help now says when an assignment starts and ends, what turning a switch on does, and what to fill in. Text only: no field name, key, option value or behaviour changes. + - **`@objectstack/plugin-sharing`:** `sys_queue` and `sys_queue_member` join the plugin's translation bundle, with zh-CN text, so the 队列 and 队列成员 pages, their columns, help and the 新建 button read in Chinese. ja-JP and es-ES carry English fills for now. + - **`@objectstack/plugin-email`:** the live-status statement at the top of Setup → Email and the "Send test email" answer (sent, failed, logged only) are written in the caller's display language, en and zh-CN today; another language gets English. + - **`@objectstack/service-settings`:** a settings action handler (`SettingsActionHandler`) receives an optional `locale`, the caller's display language read from the request the same way the live-status statement's is (`Accept-Language`, else `?locale`); `runAction` takes it as a fifth `display` argument. Existing handlers compile and behave unchanged. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [60fe44f] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/metadata-core@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/platform-objects/package.json b/packages/platform-objects/package.json index 101768f3fa3..7316e73a7f9 100644 --- a/packages/platform-objects/package.json +++ b/packages/platform-objects/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/platform-objects", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Core platform object schemas for ObjectStack — identity, security, audit, tenant, and metadata objects", "type": "commonjs", diff --git a/packages/plugins/embedder-openai/CHANGELOG.md b/packages/plugins/embedder-openai/CHANGELOG.md index 06d5b12bb0e..3e3f3519e72 100644 --- a/packages/plugins/embedder-openai/CHANGELOG.md +++ b/packages/plugins/embedder-openai/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/embedder-openai +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/plugins/embedder-openai/package.json b/packages/plugins/embedder-openai/package.json index 16c65810ac7..3cc7c3fb43c 100644 --- a/packages/plugins/embedder-openai/package.json +++ b/packages/plugins/embedder-openai/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/embedder-openai", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "OpenAI-compatible embedder for ObjectStack — works against OpenAI, 阿里通义 DashScope, 智谱 BigModel, 硅基流动 SiliconFlow, 火山引擎 Doubao, MiniMax, Ollama, and any drop-in OpenAI-shape endpoint.", "type": "commonjs", diff --git a/packages/plugins/knowledge-memory/CHANGELOG.md b/packages/plugins/knowledge-memory/CHANGELOG.md index 6dea9381ce8..c407ec03d0a 100644 --- a/packages/plugins/knowledge-memory/CHANGELOG.md +++ b/packages/plugins/knowledge-memory/CHANGELOG.md @@ -1,5 +1,80 @@ # @objectstack/knowledge-memory +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/service-knowledge@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/plugins/knowledge-memory/package.json b/packages/plugins/knowledge-memory/package.json index 108a8303b59..5d347c779e0 100644 --- a/packages/plugins/knowledge-memory/package.json +++ b/packages/plugins/knowledge-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-memory", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "In-memory knowledge adapter for ObjectStack (dev / test reference implementation).", "type": "commonjs", diff --git a/packages/plugins/knowledge-ragflow/CHANGELOG.md b/packages/plugins/knowledge-ragflow/CHANGELOG.md index f661e129303..b19dbfe5944 100644 --- a/packages/plugins/knowledge-ragflow/CHANGELOG.md +++ b/packages/plugins/knowledge-ragflow/CHANGELOG.md @@ -1,5 +1,80 @@ # @objectstack/knowledge-ragflow +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/service-knowledge@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/plugins/knowledge-ragflow/package.json b/packages/plugins/knowledge-ragflow/package.json index c83e63ce7bd..6a53086eff7 100644 --- a/packages/plugins/knowledge-ragflow/package.json +++ b/packages/plugins/knowledge-ragflow/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-ragflow", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "RAGFlow knowledge adapter for ObjectStack — production-grade RAG via the Apache 2.0 RAGFlow REST API.", "type": "commonjs", diff --git a/packages/plugins/organizations/CHANGELOG.md b/packages/plugins/organizations/CHANGELOG.md index 656ca44dffa..a94faf0b0ca 100644 --- a/packages/plugins/organizations/CHANGELOG.md +++ b/packages/plugins/organizations/CHANGELOG.md @@ -1,5 +1,126 @@ # @objectstack/organizations +## 18.4.0 + +### Patch Changes + +- 8589756: feat(runtime,cloud-connection): the marketplace install, the sample-data reseed, the restart heal and the per-organization seed replay pick `Seed.locale` datasets by the organization's language + + Clause-②: yes (widening) + Metadata-protocol: semantic + + An app that ships one demo dataset per language market (`locale: ['zh-CN']` beside `locale: ['en-US']`) now gets the right one on every path that seeds an organization, not only on the boot path. Before, the install paths passed no locale, so a Chinese organization received the English rows and a page of refused-insert warnings for the Chinese twins (or both sets, where no unique key stopped the second), and one server's per-organization replay gave every new organization the boot app's `i18n.defaultLocale`. + + The locale is resolved for the organization the rows land in. The first answer wins: + + 1. the organization's `localization.locale` setting, through the settings service's own cascade (an env override, the organization's row, the deployment-wide `global` row). Only a value somebody set counts: the manifest's built-in `en-US` default (`source: 'default'`) is not the organization's choice; + 2. `sys_user.locale` of the person installing, reseeding or founding the organization; + 3. the app's declared `i18n.defaultLocale`. + + Nothing answers ⇒ no locale is sent, and every dataset loads as before (the loader still warns, naming each locale-scoped dataset it let through). A dataset without `locale` loads whatever the answer is. When no dataset in a load declares `locale`, neither the setting nor the user is read. + + - **The per-organization replay** resolves the locale per organization, at replay time, instead of once at boot. At founding the organization has no setting of its own yet, so its founder's language decides; an organization inserted with no person behind it takes the app's default. The replayer (`seed-replayer` service) now takes an optional second argument, `(organizationId, { userId })`; a caller passing only the organization id works as before. + - **`@objectstack/organizations`**: the org-scoping middleware hands the replayer the insert's attributed user (better-auth's create runs as system with the signed-in creator attributed) as the founder. + - **`@objectstack/cloud-connection`**: the install and the reseed pass the installing user and the package's own `i18n.defaultLocale`; the restart heal passes the package's default only (no person starts it); the replayer this plugin registers when the host has none passes the founder and no app default, because it replays every installed package at once. + - **`@objectstack/runtime`** exports the shared producer: `resolveSeedLocaleForLoad(host, { seeds, organizationId, userId, appDefaultLocale })`, `appDefaultSeedLocale(body)`, `seedsDeclareLocale(seeds)`, `describeSeedLocale(resolved)` and the `ResolvedSeedLocale` / `SeedLocaleQuestion` / `SeedLocaleSource` types. + - A settings or user read that throws is not skipped over: the seed reports the failure (the install's seed summary, the reseed's `RESEED_SKIPPED`, the replay's warning) and writes nothing, rather than seeding another language over a choice that could not be read. A host with no `settings` service skips step 1. + + Unchanged: the boot seed and the dev hot-reload seeder still filter on the app's `i18n.defaultLocale`; package apply and draft publish still pass no locale. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [c23501b] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [9afcd6a] +- Updated dependencies [870ebce] +- Updated dependencies [d1d90b7] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [67d5d53] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [a93ef3e] +- Updated dependencies [0e4dfbe] +- Updated dependencies [b5765be] +- Updated dependencies [5a878a3] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [26c2efd] +- Updated dependencies [f7afab8] +- Updated dependencies [455b4f0] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [0e4747a] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [85c7374] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [c92b887] +- Updated dependencies [21510bb] +- Updated dependencies [6dd2ca5] +- Updated dependencies [0fd363d] +- Updated dependencies [a7293f7] +- Updated dependencies [63a470a] +- Updated dependencies [4851668] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/plugin-auth@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/plugins/organizations/package.json b/packages/plugins/organizations/package.json index aaa86be049d..607831ba78f 100644 --- a/packages/plugins/organizations/package.json +++ b/packages/plugins/organizations/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/organizations", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Multi-organization runtime for ObjectStack — registers the `org-scoping` service that turns single-database row-level Organization isolation on: `organization_id` auto-stamp on insert, per-org seed replay, default-organization bootstrap, and the walled-posture membership-policy gate.", "type": "commonjs", diff --git a/packages/plugins/plugin-approvals/CHANGELOG.md b/packages/plugins/plugin-approvals/CHANGELOG.md index 1270e070fc3..3778568b067 100644 --- a/packages/plugins/plugin-approvals/CHANGELOG.md +++ b/packages/plugins/plugin-approvals/CHANGELOG.md @@ -1,5 +1,154 @@ # @objectstack/plugin-approvals +## 18.4.0 + +### Patch Changes + +- 13c0c5b: fix(plugin-approvals)!: an approval node refuses to open when a team or position approver's directory read fails, or a queue approver's expansion rejects, and the ownership-queue reads page to completion + + Clause-②: yes (narrowing) — an approval node no longer opens when a `team` or `position` approver (static, or an `expression` approver with `resolveAs` `team` / `position`) cannot be expanded because a directory read or the cross-organization read filter failed, or when a `queue` approver's expansion rejects. Under every `onEmptyApprovers` policy, the `fallbackApprovers` slate included, it now fails the node with a retryable `APPROVER_DIRECTORY_UNREADABLE:` error naming the read, where it used to open on the rest of the slate, on the fallback slate or on the administrators, or auto-approve. The registered ownership-queue service does not reject yet: a failed member read inside it still answers nobody until #29604 part 2 (#30251), so a `queue` approver refuses here only when `expandMembers` itself rejects. Every other input answers as before. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at the approval node (`@objectstack/plugin-approvals`): a failed directory read behind a `team` or `position` approver, or a `queue` approver whose expansion rejects, refuses the node instead of opening it on a slate that is missing people (ADR-0142 D6 as amended, ruling record 6599731698 on #29604). + + - **Before.** A `team` or `position` approver whose `sys_team_member`, `sys_user_position` or `sys_member` read failed kept its `type:value` literal slot, which nobody can act on, and logged the failure. A `queue` approver whose expansion threw answered "expanded to nobody" and added no slot. Either way the node went on as if those people did not exist: beside other approvers it opened on them alone, and on an otherwise empty slate the node's `onEmptyApprovers` policy acted, opening on the `fallbackApprovers` or the organization's administrators, or auto-approving. + - **Now.** The node does not open and writes no request, approver or action row. It fails with an `ApproverDirectoryUnreadableError` whose message starts `APPROVER_DIRECTORY_UNREADABLE:`, names the approver, the organization and the read that failed (a failure of the cross-organization read filter is named as that filter's read, on a static approver and on an `expression` one alike), and is retryable: the flow node's failure is a runtime one, so the run can be resumed once the directory can be read. A `fallbackApprovers` slate whose own `team` or `position` read fails refuses the same way. `department` / `business_unit`, `org_membership_level` and `manager` approvers are not named by the ruling and answer exactly as before. + - **Unchanged.** A decision on a request opened before the approver snapshot existed still re-reads its slate on the old path, which keeps the literal slot on a failed read, because the decision has already been recorded when that read happens. + - **Not yet.** The ownership-queue service shipped in `@objectstack/plugin-sharing` answers a failed `sys_queue` / `sys_queue_member`, team or position read with nobody (logged once) rather than rejecting, so a `queue` approver backed by it still adds no slot on such a failure. Its refusal arrives with #29604 part 2, once the contract can carry it (#30251). + + `@objectstack/plugin-sharing`: the ownership-queue service reads its `sys_queue` and `sys_queue_member` rows by id, page after page, to the end, where it read one page of 10,000 rows and answered on it. A queue's 10,001st member, an organization's 10,001st queue and the queues naming a recipient past that bound are now found. A read that fails partway answers as a failed read did before (that read contributes nobody, logged once, with the read's reason and without the walk's own "refuses instead" clause), never with the pages it had read. +- fe835b4: fix(plugin-sharing)!: the ownership-queue service refuses a failed member read when the caller asks, and the approver slate and the sharing-recipient grant recompute ask + + Clause-②: yes (narrowing) — `expandMembers` and `isMember` of the registered ownership-queue service, called with `onFailedRead: 'refuse'`, now reject when a read behind the queue fails, where they answered nobody. The approver slate asks for that answer, so an approval node whose `queue` approver cannot be read refuses to open under every `onEmptyApprovers` policy, where it opened on the rest of the slate, on the fallback slate or on the administrators, or auto-approved. The sharing-rule recompute asks for it too, so a whole-rule pass (`evaluateRule`, the REST evaluate endpoint, the boot backfill), the recipient-axis revoke and the per-user pass on a `queue`-recipient rule reject on such a failure, where they completed by revoking the grants of the members who could not be read. Every call without the option, and every read that succeeds, answers as before. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at the ownership-queue service's `'refuse'` answer and at its two recompute consumers (`@objectstack/plugin-sharing`, `@objectstack/plugin-approvals`): an empty expansion is a fact, a failed read is a fault (ADR-0142 D2 as amended), and the approver slate (D6) and the sharing-recipient grant recompute (D7) now get the fault instead of "nobody". + + - **The service.** `OwnershipQueueService.expandMembers(name, org, { onFailedRead: 'refuse' })` and `isMember(user, name, org, { onFailedRead: 'refuse' })` reject when the queue's `sys_queue` row, its `sys_queue_member` rows, or a member table one of its specifications reads (`sys_team_member`, `sys_user_position`, `sys_member`, `sys_business_unit_member`, `sys_business_unit`) cannot be read, or a paged read cannot vouch for completion. The rejection's message starts `[queue] queue '' in organization '' does not answer its members, because a read behind it failed:` and names that read; the read's own error rides along as `cause`. It never answers `[]`, a partial set or `false` instead. `isMember` still answers `true` when a read that succeeded reaches the user, and answers `false` only when every read behind it succeeded. An empty queue is still `[]`, and a name that resolves nowhere is still `[]` / `false`. + - **Unchanged: the default.** Without the option (`onFailedRead: 'nobody'`), a failed read still contributes nobody, logged once. The sitting-record access path (`queuesOf`), the claim admission and the assignment pool take that answer, so they deny on a failed read exactly as before. A failed read of the activation ledger (`sys_metadata_activation`) is not a failed read in this sense under either answer: it still reads "switched on" for that call, logged, the posture the sharing-rule service takes for its own position recipients. + - **The approver slate (D6).** A `queue` approver asks with `onFailedRead: 'refuse'` beside the addressing purpose. When the registered service rejects, the node fails with the retryable `APPROVER_DIRECTORY_UNREADABLE:` error that a failed team or position read already raises, naming the queue's failed read, and writes no request, approver or action row. Before, the registered service answered nobody, so the queue added no slot and the node's `onEmptyApprovers` policy acted on a slate missing those people. + - **The sharing-recipient grant recompute (D7).** A `queue`-recipient rule's expansion and its per-user membership check ask with `onFailedRead: 'refuse'`. A failed member read now rejects before any grant is diffed, so the rule's existing grants stay as they were until a pass can read the queue, and each recompute's caller logs it: the record pass and the object pass per rule (`record reconcile failed for rule`, `object reconcile failed for rule`), the queue-membership and business-unit write recomputes (`queue-membership revocation failed`, `queue-membership re-grant failed`, `business-unit revocation failed`, `business-unit re-grant failed`), the membership-row recompute (`recipient membership re-evaluation failed`) and the boot backfill (`boot rule backfill failed for rule`). A direct call to `evaluateRule`, `revokeRuleGrantsForRetiredRecipients` or `reconcileRuleForUser`, the REST evaluate endpoint included, rejects with the service's failure. Before, the failed read answered nobody and the pass revoked the grant of every member behind it. This is the answer a `team` or `position` recipient has given since its own reads began to refuse. + - **Supersedes the approval node's "Not yet".** The approval node's own entry in this release ("an approval node refuses to open when a team or position approver's directory read fails…") says, in its `Clause-②:` line and its **Not yet** bullet, that the registered ownership-queue service answers a failed member read with nobody rather than rejecting, so a `queue` approver backed by it refuses only when `expandMembers` itself rejects. That was true when it was written and is not after this change: the service now rejects on that failure when asked, and the slate asks, so a `queue` approver backed by it refuses on a failed `sys_queue`, `sys_queue_member`, team, position or business-unit read, as a `team` or `position` approver does. Read this bullet in place of those two sentences. +- a52761f: fix(console): an action another package places on `sys_user` at `record_header` / `record_more` draws again on the user's record page, in the drawer and on the full page (#29951) + + Clause-②: no + Metadata-protocol: none + + - **The header reads `placedActions: true`.** `page:header`'s `placedActions` (#30018) is now drawn by the console's header renderer (`PageHeaderActions` in `@object-ui/components`): `true` draws every action of the page's object whose `locations` include `record_header` or `record_more`, in declaration order, read from the object's metadata at render time, so an action an installed package adds to the object is drawn without editing the page. Omitted or `false`, an authored `actions` list is still the whole set (#11592). A `true` beside an authored list, which the spec refuses, draws none of the object's actions and is reported once in the console. + - **One placed-header reader.** `placedHeaderActions` (`@object-ui/types`) is the rule both headers read: the synthesized default record page's header in `RecordDetailView` and a `placedActions: true` header. The `page:header` registration publishes the `placedActions` input. + - **The platform record pages opt in.** `sys_user_detail` and `sys_organization_detail` (`@objectstack/platform-objects`) and `sys_approval_request_detail` (`@objectstack/plugin-approvals`) set `placedActions: true` in place of the id lists #27219 froze, which dropped every header action another package places on those objects. The tab strips are unchanged, and every action the lists named is placed at `record_header` / `record_more`, so each still draws. + - `@objectstack/spec`: the `placedActions` describe no longer opens with `[PLANNED — not yet rendered]`, and the `page` liveness record names the reader. +- a0c68ae: feat(approvals,messaging): approval requests, reminders and outcomes also go by e-mail, linking to the request page; messaging gains a code-declared notification template tier + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/plugin-approvals`: `approval.requested`, `approval.reminder`, `approval.approved` and `approval.rejected` request the `inbox` and `email` channels** in one emit. Before, every approval notice named no channel, so messaging delivered it to the in-app inbox alone and nobody who does not watch the Console bell heard of it. Every other `approval.*` topic stays inbox-only, and delivery preferences filter the e-mail as they filter any channel. The inbox is unchanged. On a deployment with no mail transport the e-mail is whatever the `email` channel answers there. + - **The approval e-mail** is the notice's own title as its subject and its body (in the recipient's language, as the inbox renders it) followed by one line that opens the request, in `en`, `zh-CN`, `ja-JP` and `es-ES`. The link is a new payload member, `approvalUrl`: the request page (`/system/approvals?request=ID`) under the deployment's public origin. `actionUrl` stays relative, as the Console routes on it. It opens the request for a signed-in reader and decides nothing; no ADR-0043 action link or token is in any payload. + - **`publicBaseUrl` now resolves when the option is unset**: `OS_AUTH_URL` (legacy `BETTER_AUTH_URL`), then `OS_BASE_URL`, never a guessed `localhost`. With none of them set, or a value that is not an absolute `http(s)` URL, links stay relative and the plugin warns once at start, naming the variable to set. The same origin also prefixes ADR-0043's `actionLinkUrl`, as its option always documented. To give approval e-mails a working link, set `OS_BASE_URL` (or `OS_AUTH_URL`) to the deployment's address. + - **`@objectstack/service-messaging`: `MessagingService.registerBuiltinTemplates(templates)`** (and the `BuiltinNotificationTemplate` type) declares `(topic, channel, locale)` notification templates in code. The channels' template store reads them at every rung of its locale ladder after the recipient organization's own `sys_notification_template` row: the rung's row, else the rung's built-in, else the next rung, else the generic title and body. Nothing is written to the database (ADR-0131 §1.4), so an organization that authors a row overrides the built-in with nothing to migrate. A malformed template, or a `(topic, channel, locale)` already declared, is refused with a `TypeError`, and nothing from that call is kept. +- bdbaf80: fix: Setup's form help speaks to an administrator, and the queue pages, the mail status banner and the test-email toast read in Chinese on a zh-CN console + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Field help (English source and zh-CN):** the permission set and position assignment forms, the out-of-office (approval delegation) rule, and the business unit, organization and user forms no longer describe the implementation. Gone: "fail-closed", "half-open [from, until)", "agent grants", "graph semantics", "expanded by graph queries", "Null = root of tenant", "HRIS", "When true / When false" (zh-CN 「为 true 时」 now reads 「开启后」) and an HTTP status code. The help now says when an assignment starts and ends, what turning a switch on does, and what to fill in. Text only: no field name, key, option value or behaviour changes. + - **`@objectstack/plugin-sharing`:** `sys_queue` and `sys_queue_member` join the plugin's translation bundle, with zh-CN text, so the 队列 and 队列成员 pages, their columns, help and the 新建 button read in Chinese. ja-JP and es-ES carry English fills for now. + - **`@objectstack/plugin-email`:** the live-status statement at the top of Setup → Email and the "Send test email" answer (sent, failed, logged only) are written in the caller's display language, en and zh-CN today; another language gets English. + - **`@objectstack/service-settings`:** a settings action handler (`SettingsActionHandler`) receives an optional `locale`, the caller's display language read from the request the same way the live-status statement's is (`Accept-Language`, else `?locale`); `runAction` takes it as a fifth `display` argument. Existing handlers compile and behave unchanged. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [d9d42d4] +- Updated dependencies [67d5d53] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [60fe44f] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [0e4747a] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-approvals/package.json b/packages/plugins/plugin-approvals/package.json index 046e0f0166f..026feb3af89 100644 --- a/packages/plugins/plugin-approvals/package.json +++ b/packages/plugins/plugin-approvals/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-approvals", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Multi-step approval engine for ObjectStack — sys_approval_process + sys_approval_request + sys_approval_action + IApprovalService.", "type": "commonjs", diff --git a/packages/plugins/plugin-audit/CHANGELOG.md b/packages/plugins/plugin-audit/CHANGELOG.md index 7462e5107ad..b7cf909c1c8 100644 --- a/packages/plugins/plugin-audit/CHANGELOG.md +++ b/packages/plugins/plugin-audit/CHANGELOG.md @@ -1,5 +1,134 @@ # @objectstack/plugin-audit +## 18.4.0 + +### Patch Changes + +- 171b6dd: fix(plugin-audit): the activity list opens newest first, without the platform's own rows, under a plain name and description + + Clause-②: no + Metadata-protocol: none + + - **`sys_activity` declares two list views.** `recent` comes first, so it is the list Home's "view all activity" and the bell's Activity tab open (both name no view): newest first, and only rows a person wrote (`actor_id` set) about something other than the platform's own objects: the metadata store, the data-migration ledger, and every identity object `@objectstack/platform-objects/identity` declares (users, members, organizations, sessions, keys, SCIM): every sign-in stamps the last-login columns on `sys_user` under the signing-in person's own name, the signing key in `sys_jwks` is minted under the first sign-in's session, and adding a person writes `sys_user` and `sys_member` rows that person can read. Sign-in stamps, migrations, seeds and metadata history no longer sit between the business rows; an administrator's edits to users are in `all`. `all` lists every row the reader may read, platform rows included, newest first. Nothing is removed from storage and nothing changes in what is written; every excluded row still has its `sys_audit_log` entry. + - **Label and description.** The object reads `Activity Feed` (zh-CN `动态`) instead of `Activity`, which collided with an app's own activity object (a CRM's calls and meetings), and its description says what the page is instead of `Recent activity stream entries (lightweight, denormalized)`. +- 740edc5: fix(plugin-audit,platform-objects): the audit log's default list shows what people did, not the rows the platform writes beside it; the audit objects read in Chinese + + Clause-②: no + Metadata-protocol: none + + Setup's 审计日志 opens the first `sys_audit_log` list view, `recent`. One sign-in used to put two rows there (the `login` row and an `update` of the person's `sys_user` stamping `last_login_at` / `last_login_ip`), the first sign-in through the console a third (the token-signing key in `sys_jwks`), and one settings change three or four (the `config_change` row, the stored setting, a secret's ciphertext, and the settings trail's own entry). + + - The ledger writer now marks the sign-in stamp row with `metadata: {"event":"auth.last_login_stamp"}`, the way `config_change` rows carry `settings.set`. Only an update of `sys_user` whose changes are the stamp's fields alone is marked; an administrator's edit of a user is not. The row is still written. + - `recent` leaves out the marked row, the rows on `sys_secret`, `sys_setting_audit`, `sys_platform_setting_audit`, `sys_jwks`, `sys_migration` and `sys_migration_journal`, and the rows on `sys_setting` / `sys_platform_setting` other than `config_change`. Nothing is removed from storage, and `all_events` (全部) still lists every row. + - `sys_platform_setting` and `sys_platform_setting_audit` were in no translation bundle, so they read English in every locale; they now ship in `@objectstack/platform-objects`' bundles with zh-CN translations (ja-JP and es-ES carry the English fill). `sys_comment_reaction`'s zh-CN bundle is translated. +- c5101a0: i18n(plugin-audit): the audit, activity and comment objects and the activity-feed sentences ship in all eleven platform languages + + Clause-②: no + Metadata-protocol: none + + `sys_audit_log`, `sys_activity`, `sys_comment` and `sys_comment_reaction` now carry Traditional Chinese (`zh-Hant`), Arabic (`ar`), French (`fr`), Brazilian Portuguese (`pt-BR`), Russian (`ru`), German (`de`) and Korean (`ko`) beside the existing `en`, `zh-CN`, `ja-JP` and `es-ES`, and so do the activity-feed summaries ("Created … ", "Updated …: fields", "… mentioned you"). A reader in one of those languages sees the audit trail, the activity feed and the mention notice in their own language instead of English. Each new locale is registered under its platform-table tag with its source-hash companion and its messages, and the package's extract command lists the seven tags, so the existing `check:i18n` bundle check covers them. Part of #30512. +- Updated dependencies [31fe289] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [cd5f554] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [343ddb2] +- Updated dependencies [0c666c3] +- Updated dependencies [f672e0d] +- Updated dependencies [38717d3] +- Updated dependencies [bfbdeec] +- Updated dependencies [4adc890] +- Updated dependencies [bcdf8d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [a26abbc] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [8cc709d] +- Updated dependencies [60fe44f] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [7b80724] +- Updated dependencies [fb1ab89] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [63de248] +- Updated dependencies [a2437b5] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7193160] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/objectql@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-audit/package.json b/packages/plugins/plugin-audit/package.json index 14ee5ec6310..509d40497be 100644 --- a/packages/plugins/plugin-audit/package.json +++ b/packages/plugins/plugin-audit/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-audit", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Audit Plugin for ObjectStack — System audit log object and audit trail", "type": "commonjs", diff --git a/packages/plugins/plugin-auth/CHANGELOG.md b/packages/plugins/plugin-auth/CHANGELOG.md index d5853667aef..96ad8a2d8ff 100644 --- a/packages/plugins/plugin-auth/CHANGELOG.md +++ b/packages/plugins/plugin-auth/CHANGELOG.md @@ -1,5 +1,374 @@ # Changelog +## 18.4.0 + +### Minor Changes + +- 4be8a2e: feat(spec)!: retire `emailAndPassword.revokeSessionsOnPasswordReset` — a completed password reset always ends every session the account holds, and there is no setting (#29909) + + Clause-②: no (narrowing) — `EmailAndPasswordConfig.revokeSessionsOnPasswordReset` becomes a `retiredKey()` tombstone, so the schema parse and `tsc` refuse it at any value, and plugin-auth's entry guard refuses it at any value too, `true` included, which it accepted before. Nothing is widened. + Metadata-protocol: narrowed + + + + **BREAKING** for a configuration that still writes `emailAndPassword.revokeSessionsOnPasswordReset`, at any value. + + - **`@objectstack/spec`.** `EmailAndPasswordConfig.revokeSessionsOnPasswordReset` is retired (ADR-0049 enforce-or-remove). A completed self-service password reset already ended every session the account holds, on every reset door, and an explicit `false` was already refused at boot, so the key could hold one value only. It is now a tombstone: a typed configuration that writes it fails `tsc`, and `AuthConfigSchema` / `EmailAndPasswordConfigSchema` refuse it with the prescription. The authorable-surface row reads `[RETIRED]`, `RETIRED_KEYS_BY_MAJOR[18]` lists `system/EmailAndPasswordConfig:revokeSessionsOnPasswordReset`, and the upgrade guide carries the semantic entry `email-and-password-revoke-sessions-on-reset-retired`. The prescription is exported as `REVOKE_SESSIONS_ON_PASSWORD_RESET_RETIRED` (`@objectstack/spec/system`). No ADR-0087 conversion: the key lives in the configuration a host hands the auth plugin, never in a stack or a stored row, so `os migrate meta` has nothing to rewrite. + - **`@objectstack/plugin-auth`.** The auth manager takes its configuration unparsed, so it refuses the retired key itself, where the configuration enters: at construction, which stops the boot, and on a configuration patch, where the standing configuration keeps ruling. It now refuses `true` as well as `false`, with the tombstone's own prescription. `true` asked for what the platform does anyway; the key is to be deleted either way. The auth instance still sets better-auth's own option of that name itself, so a completed reset ends the account's sessions exactly as before. + - **Unchanged.** What a password reset does: every self-service reset door ends every session the account holds, and each ended session is stamped `revoke_reason: password_reset`. An absent key is accepted, as before. + + **The remedy.** Delete `revokeSessionsOnPasswordReset` from the `emailAndPassword` block of the auth configuration, whatever it holds; nothing replaces it. + +### Patch Changes + +- c23501b: fix: a user's language preference is stored in its BCP-47 case, so `zh-cn` and `ZH-CN` are saved as `zh-CN` and the console renders that user's language (#29895) + + Clause-②: no + Metadata-protocol: none + + `sys_user.locale` used to store a tag as written. The column checks only the shape of a tag, so `zh-cn` was accepted, and the console, which picks its UI language by exact match, then rendered English for that user with no error. Every write of the column is now stored in BCP-47 case: language lower case, region upper case, script title case (`ZH-HANS-cn` → `zh-Hans-CN`). This applies to the data API (`PATCH /api/v1/data/sys_user/:id` and its bulk form), the admin user import and the identity-provider locale sync. The fold is the one `LocaleSchema` applies to translation locales. Nothing new is refused: `zh_CN` is still refused with 400, and an empty value still clears the preference. Rows stored mis-cased before this release are not rewritten. Such a row reads back as stored until the column is written again. +- cde492d: feat(plugin-auth): a page navigation to the mobile sign-in hand-off that has to sign in first goes to the sign-in page, so the app opens only the hand-off + + Clause-②: no + Metadata-protocol: none + + `GET /api/v1/auth/one-time-token/handoff` answered a missing or old session with `401` as raw JSON, so the mobile app had to open the Console's `/login` route itself and know how that page treats `redirect`. An installed app stays in use for months, so a Console upgrade could strand it. Now the app opens only the hand-off: + + - A page navigation with no session, or with one older than five minutes, answers `302` to this deployment's Console sign-in page, with `redirect` naming the same request. `Location` is root-relative, so the browser stays on the origin the request arrived on, and `uiBasePath` is honoured. + - Any other request still answers `401`, so the app's anonymous probe keeps reading "this server registered me". A navigation reporting `Sec-Fetch-Site: same-origin` answers `401` too: it is the sign-in page forwarding a session the route refuses. A deployment that does not serve the Console keeps the `401` too, and says so once at `warn`. + - A token is still minted only for a session under five minutes old, and every `400` (a cross-site navigation, an unknown parameter, an unregistered scheme, a bad challenge) is still decided first, with no redirect. + - A device whose clock is behind does not bounce between the route and the sign-in page. The redirect carries `handoff_refused_at`, the server's time of the refusal, and the page holds its form for any session not created after it, whatever the device's clock says. A session created after it forwards, so a provider's sign-in that returns to the same URL still ends at the app. The links between the sign-in and sign-up pages carry the mark beside a hand-off `redirect`, and every other link is unchanged. + + An app can change its entry only once its server runs this release: an older server answers the new entry with JSON. +- 09720e2: `GET /api/v1/auth/config` now says whether email leaves the deployment, in a new optional `features.emailDelivery` flag. It is `true` only when the wired email service reports itself configured, and `false` with no email service or with the stock `log` transport, which writes mail to the server console instead of sending it. The forgot-password page can read it to stop saying a reset link was sent on a deployment that sends no mail. + + The flag describes the deployment, not an address: `POST /api/v1/auth/request-password-reset` still answers a known and an unknown address the same way, with or without mail. + + Like every public auth flag, it is classified in `PUBLIC_AUTH_FEATURES` (login surface, opt-in), so an action may now also name it: `requiresFeature: 'emailDelivery'` is a new accepted value, lowered to `features.emailDelivery == true`. + + Clause-②: yes (widening) + Metadata-protocol: added +- 9afcd6a: fix(plugin-auth)!: an organization cannot require multi-factor authentication while two-factor enrollment is off, and the MFA gate never holds a user who has no way to enroll (#30201) + + Clause-②: no (narrowing) — while the two-factor feature is off, a write that turns `sys_organization.require_mfa` on (an insert with it on, or an update of a row that stores it off) is refused `400 VALIDATION_FAILED`, a value an administrator could save before; re-saving a row that already stores it on passes. No published export, error code or settings key is added; nothing is widened. + Metadata-protocol: none + Skills-docs: follow-up owed — content/docs/permissions/authentication.mdx (the `mfa_required` row says `require_mfa` is settable per organization with no condition; it needs two-factor on) + + + + **BREAKING** accept-set narrowing on `sys_organization.require_mfa` writes, served by every data door (the organization's edit form, REST, MCP), on a deployment where two-factor is off. + + - **Before.** An administrator could tick Require Multi-Factor Auth on an organization while two-factor was off (the default, `features.twoFactor: false`), and it saved. Once the grace period ended, every member, the owner included, was held `403 MFA_REQUIRED` and sent to enroll, and the enrollment endpoint (`POST /api/v1/auth/two-factor/enable`) answered 404: the twoFactor plugin that serves it was never turned on. Nobody could undo it from inside the product, because Setup sits behind the same gate. The global switch never had this hole: the `auth.mfa_required` setting turns the requirement and the plugin on as one piece. + - **Now.** While two-factor is off, a write that turns `require_mfa` on is refused: an insert with it on, or an update of a row whose stored value is off. The refusal carries one `fields[]` entry on `require_mfa` (`invalid_value`) saying why, who would have been locked out, and the ways forward. It applies to every write context, the system's included, on the by-id door, an insert, and a multi-row update, which is judged row by row (one row turning it on refuses the whole write, with nothing written). Re-sending `require_mfa: true` for a row that already stores it (a whole-row update, an import re-run, a save with no change) passes, so other edits of that row are not blocked. Clearing the column and writing other columns pass as before, and every write passes once two-factor is on (read per write, so no restart is needed). + - **The gate.** ADR-0069 D3's MFA gate holds a user only while they can enroll. With two-factor off, an MFA requirement (an organization's `require_mfa`, or `auth.mfa_required` under an `OS_AUTH_TWO_FACTOR=false` pin, or a host that sets `mfaRequired` without `plugins.twoFactor`) holds nobody and starts no grace clock, and the server says so once in a warning with the remedy. An organization row stored with `require_mfa = true` before this release therefore locks no one out; it starts holding unenrolled members once two-factor is turned on. + - **One answer.** Whether two-factor is on is decided in one place, the same `OS_AUTH_TWO_FACTOR` env, then `plugins.twoFactor`, then off, for the plugin list, `features.twoFactor` in `/api/v1/auth/config`, the gate and the write refusal. + - **Unchanged.** An enrolled user is never held. With two-factor on, the requirement and its grace period behave exactly as before. The `auth.mfa_required` setting still turns two-factor on with the requirement. + + **The remedy.** To require multi-factor authentication for one organization, turn two-factor enrollment on for the deployment first, with `OS_AUTH_TWO_FACTOR=true`, and then tick the organization's box. Unless `OS_AUTH_TWO_FACTOR` is set to `false` (the env var outranks the settings), turning on the `auth.mfa_required` setting instead also works: it requires multi-factor authentication for every organization and turns enrollment on with it. +- 870ebce: fix(plugin-auth)!: saving the auth settings refuses to turn off the last way to sign in (#30202) + + Clause-②: yes (narrowing) — a save an administrator could make before is now refused at the settings save door with the existing `SETTINGS_VALIDATION` / `invalid_value` field error: a save after which email and password is off and nothing else signs anyone in, judged on the whole save, including the Google change that removes the last provider while email and password is off. No new error code or settings key. The widening half: `SettingsWritePredicateInput`, exported from `@objectstack/service-settings`, gains an optional `batch` field, the whole write a predicate is asked about. + Metadata-protocol: none + Skills-docs: none — no file under `skills/**` teaches `auth.email_password_enabled` or the settings write predicate; `content/docs/deployment/environment-variables.mdx` already names the `OS_AUTH_EMAIL_PASSWORD_ENABLED` override used for recovery below. + + + + **BREAKING** accept-set narrowing at the auth settings save door (`PUT /api/settings/auth`, `SettingsService.setMany('auth', …)`): a save that would leave no way to sign in is refused, where it was saved before. + + - **Before.** Setup → Settings → Authentication, "Enable email/password login" off, save: "Settings saved", with no social provider and SSO off. After the next sign-out nobody could sign in, the administrator included. + - **Now.** A save after which email and password is off and no other way in is in force answers `400 SETTINGS_VALIDATION`. The field error sits at `email_password_enabled` (`code: invalid_value`, `constraint.lastSignInMethod: email_password`), and its message, in the caller's language (en, zh-CN, ja-JP, es-ES), says to keep or set up another sign-in method before or in that save. Nothing in that batch is written, and email and password stays on. + - **The whole save is judged.** `{ email_password_enabled: false, google_enabled: false }` (or with `google_client_secret` cleared) on a deployment signing in with email and password plus Google is refused, with a field error at each of the two keys. Google filled in (switch, client ID and secret) in the same save that turns email and password off is accepted, and so is the switch alone over a client ID and secret already stored. + - **What counts as another way in.** A social or OIDC provider in force whose client ID and client secret are both set; enterprise SSO switched on with at least one identity provider registered (the switch alone does not count); phone-number sign-in; a host sign-in handoff the host declared. `ssoOnlyMode` alone, magic link, passkeys, and a provider listed without its credentials do not count. + - **The other direction.** While email and password is off, `google_enabled: false`, or clearing `google_client_id` / `google_client_secret` when no environment variable or stack config supplies it, is refused (`constraint.lastSignInMethod: google`) if Google is the last way in. + - **Already locked out.** Set `OS_AUTH_EMAIL_PASSWORD_ENABLED=true` and restart: the environment override wins over the stored `false`, and email and password sign-in comes back. Then set up another method before turning it off again. + - **For a write predicate's author (`@objectstack/service-settings`).** `SettingsWritePredicateInput.batch` holds every key the write sets in the namespace, the asked key among them, with the value it would store (`null` clears). `setMany` always passes it, frozen. A rule over several keys judges the state after the whole write from it instead of one key against the rest as they stand. Existing predicates need no change. +- d1d90b7: fix(plugin-auth)!: clearing the allowed IP ranges applies at once, and a console save that leaves out the saving address is refused + + Clause-②: yes (narrowing) — a settings console save (`PUT /api/settings/auth`) of an `allowed_ip_ranges` list none of whose entries matches the saving request's own address is refused, where it was stored before; and `@objectstack/service-settings` widens its published surface: three new exported types (`SettingsWriteOptions`, `SettingsRequestTransport`, `SettingsWritePredicateOptions`), `setMany`'s fourth parameter accepts `SettingsWriteOptions`, `registerWritePredicate` takes an optional fourth `options`, and `SettingsWritePredicateInput` gains an optional `transport`. + Metadata-protocol: none + Skills-docs: none — `skills/**` teaches no `allowed_ip_ranges`; `content/docs/permissions/authentication.mdx` describes the setting and does not yet state the save refusal or the recovery, which ship here in the refusal text and this entry (the page is outside this change's surface and is named for a follow-up). + + + + **BREAKING** for an administrator who saves, through the settings console, an allowed-IP list (ADR-0069 D5) that does not include the address they are connected from. + + - **Clearing applies at once.** The console sends an emptied field as `null`, which resolves with source `default`, and the settings binding used to skip a default-sourced `allowed_ip_ranges`: the last stored list kept refusing sign-in (`403 IP_NOT_ALLOWED`) until a restart while the field showed empty. A cleared or restored-default value now hands the gate back to the deployment's own declaration in the same pass: no list when it declared none, the `allowedIpRanges` the plugin options or an `auth:configure` handler declared otherwise. + - **The save is refused.** A list no entry of which matches the saving request's address is refused with `400 SETTINGS_VALIDATION`, `invalid_value` at `allowed_ip_ranges`, the address in `constraint.clientAddress`, and a message in the caller's language that names it. The address is the one the gate judges at sign-in: the transport's peer address, or the forwarded one when the stack declares `server.trustProxy`. When the server cannot establish the address, every non-empty list is refused, because the gate would refuse that request too. Clearing the list is never refused, and an in-process write (boot, seed, migration) carries no request and is not judged. + - **Recovery when already locked out.** Start the server with `OS_AUTH_ALLOWED_IP_RANGES` set to an empty value. The settings env override wins over the stored list and opens the gate; remove it once the list is fixed. + - **`@objectstack/service-settings`.** `setMany`'s options (`SettingsWriteOptions`) and a write predicate's input gain an optional `transport` (`SettingsRequestTransport`: the transport's peer address and request headers), which the settings routes set on a save so a write predicate can judge the change against the caller's own connection. A predicate sees only the headers it names at registration (`registerWritePredicate(…, { requestHeaders })`, `SettingsWritePredicateOptions`), so a credential header never reaches one that did not ask for it. The transport identifies and authorizes nothing and is never stored. + - **Who moves.** An administrator adds their own address or range to the list, or saves it from an address inside it. +- a93ef3e: fix(plugin-auth)!: deleting the last identity provider is refused, and a boot that serves no sign-in method says so (#30300) + + Clause-②: yes (narrowing) — a request an administrator could make before is now refused: `POST /api/v1/auth/sso/delete-provider` (Setup's "Delete SSO Provider") on the last identity provider, while email and password is off and nothing else signs anyone in, answers `409 DELETE_RESTRICTED` (an existing standard code, the one an organization delete refused for the same reason answers), or `503 SERVICE_UNAVAILABLE` when the provider list cannot be read to tell. No new error code, setting or field. The widening half: `AuthManager` gains `setSsoProviderDeleteFloor()`, marked `@internal`, through which `AuthPlugin` hands it the judge. The boot report is a new `error` log line only; nothing about the boot's outcome changes. + Metadata-protocol: none + Skills-docs: none — no file under `skills/**` teaches deleting an SSO provider or the sign-in boot reports; `content/docs/deployment/self-hosting.mdx` gains "No sign-in method left" with the three doors and the recovery. + + + + **BREAKING** accept-set narrowing at `POST /api/v1/auth/sso/delete-provider`: deleting the identity provider that is the last way to sign in is refused, where it was deleted before. + + - **Before.** Email and password off, one SSO identity provider registered and nothing else in force. Setup → the provider → "Delete SSO Provider": `200 {"success":true}`, the row gone. From then on nobody could sign in, the administrator included (measured on a real boot). + - **Now.** That delete answers `409` with `code: DELETE_RESTRICTED`, `lastSignInMethod: enterprise_sso`, and a message in the caller's language (en, zh-CN, ja-JP, es-ES) saying to turn email and password back on, or register another identity provider or set up another sign-in method, first. Nothing is deleted. When the provider list cannot be read to tell, it answers `503 SERVICE_UNAVAILABLE` instead of letting the delete through. + - **Still allowed.** Deleting a provider while another one remains, while email and password is on, while enterprise SSO is switched off, or while another way in counts: a social or OIDC provider with a client ID and secret, phone-number sign-in, or a host sign-in handoff. An unknown provider is still the vendor's `404`, and a caller with no session the vendor's `401`; the vendor's own provider-access check still decides who may delete which provider. + - **The redeploy door.** A boot whose configuration no longer declares the last way in (for example a redeploy without the `GOOGLE_CLIENT_*` pair while email and password is off) still boots, as before, and now reports at `error` under the grep token `no_sign_in_method_at_boot`, naming the consequence and the recovery. It is a positive claim only: a provider table that cannot be read says nothing. Refusing such a boot is a later option, not done: a deployment that signs people in through a door the auth plugin cannot see must keep starting. + - **Already locked out.** Set `OS_AUTH_EMAIL_PASSWORD_ENABLED=true` and restart: the environment override wins over a stored `auth.email_password_enabled = false` and over the stack config, and email and password sign-in comes back. +- 0e4dfbe: fix(plugin-auth)!: the global `auth.mfa_required` switch cannot be saved on while `OS_AUTH_TWO_FACTOR=false` keeps two-factor enrollment off (#30302) + + Clause-②: no (narrowing) — while `OS_AUTH_TWO_FACTOR` is set to off, a save that turns the `auth.mfa_required` setting on is refused `400 SETTINGS_VALIDATION` (`invalid_value` at `mfa_required`), a value an administrator could save before; saving it off, or re-saving a value already in force, passes. No published export, error code, settings key or `/auth/config` member is added; nothing is widened. + Metadata-protocol: none + Skills-docs: updated here — `content/docs/permissions/authentication.mdx` (the `mfa_required` row) and `docs/HARDENING.md` (the MFA row); `skills/**` teaches neither `mfa_required` nor `require_mfa`, so no skill or eval moves. + + + + **BREAKING** accept-set narrowing on the `auth.mfa_required` settings save (Setup → Authentication, `PUT /api/settings/auth`, `SettingsService.setMany`), on a deployment that sets `OS_AUTH_TWO_FACTOR` to off. + + - **Before.** Turning on "Require multi-factor authentication" saved and read as on. The settings pass turns the requirement and the two-factor plugin on as one piece, but the `OS_AUTH_TWO_FACTOR` env var outranks the plugin flag, so under `OS_AUTH_TWO_FACTOR=false` nobody could enroll an authenticator app, and the MFA gate, which holds only a user who can enroll, enforced nothing. An organization's own `require_mfa` already refused this state; the global switch did not. + - **Now.** That save is refused before anything is written: the whole save, other keys in it included, with one field error at `mfa_required` naming `OS_AUTH_TWO_FACTOR`, in the caller's language (English, Simplified Chinese, Japanese, Spanish), and the constraint `{ twoFactor: false, overriddenBy: 'OS_AUTH_TWO_FACTOR' }`. + - **Unchanged.** With `OS_AUTH_TWO_FACTOR` unset or on, the switch saves and turns enrollment on with the requirement, as before. Turning it off is never refused. A switch saved on before this release is left as stored and still not enforced; `GET /api/v1/auth/config` answers `features.twoFactor: false` there, the same decision the gate makes, which is the reading a console uses to say the requirement is not enforced. + + **The remedy.** Set `OS_AUTH_TWO_FACTOR=true` on the deployment (or remove it) and restart, then turn on `auth.mfa_required`. +- b5765be: fix(plugin-auth)!: clearing an authentication setting, or restoring the namespace's defaults, takes effect at once (#30312) + + Clause-②: yes (narrowing) — one save an administrator could make before is now refused at the settings save door with the existing `SETTINGS_VALIDATION` / `invalid_value` field error: clearing `auth.email_password_enabled` on a deployment that itself declared email and password off, when nothing else signs anyone in after the save. Before, that clear was saved and changed nothing at runtime; now a clear applies, and that one would lock everyone out. No new error code, settings key or export. + Metadata-protocol: none + Skills-docs: none — no file under `skills/**` teaches the auth settings namespace, its clear or the sign-in floor. + + + + **BREAKING** accept-set narrowing at the auth settings save door (`PUT /api/settings/auth`, `SettingsService.setMany('auth', …)`): one clear that was saved before is refused. + + - **The fix.** A cleared `auth.*` key (the console sends `null`) or a namespace reset resolves `source: 'default'`. Every key except `allowed_ip_ranges` (#30203) then kept its last stored value in force until a restart: lockout threshold, max sessions per user, password policy, session lifetime, MFA, API-key lifetime, rate limits, Google, audience. Now the running value returns in the same pass to what the deployment declared (plugin options, `auth:configure`), or to the built-in default where it declared nothing. A value in a block the auth manager judges (`emailAndPassword`, `plugins`, `audience`) is restored through that same block, so it is judged as a save of it would be. Environment overrides keep ruling. + - **The refused clear.** With email and password declared off by the deployment and turned on in Setup, and no other way in (Google switched off in Setup, no other provider, no SSO, no phone sign-in, no host handoff), clearing "Enable email/password login" now answers `400 SETTINGS_VALIDATION` at `email_password_enabled` (`constraint.lastSignInMethod: email_password`), and nothing is written. Set up another sign-in method first, or save the toggle on explicitly instead of clearing it. + - **Session lifetime.** A stored `session_expiry_days` / `session_refresh_days` now overrides its field of the deployment's declared `session` block instead of replacing the whole block. +- 5a878a3: fix(plugin-auth): an IPv6 CIDR in the allowed IP ranges admits the IPv6 clients inside it, at the sign-in gate and at the settings save check + + Clause-②: no + Metadata-protocol: none + + `auth.allowed_ip_ranges` (ADR-0069 D5) promises CIDR ranges, but its matcher read only IPv4 CIDR and exact addresses: an IPv6 entry such as `2001:db8::/32` was compared as text, so it never matched a real client. The sign-in gate then refused every IPv6 client the list named, and the settings save check refused the save for an administrator connected over IPv6 from inside it. `ipMatchesRange` now reads an IPv6 CIDR as the first `n` of 128 bits, and both doors use it, so they keep judging the same address the same way. + + - An exact IPv6 entry matches the address it denotes however it is spelled (`2001:DB8:0:0::1` matches `2001:db8::1`). + - An IPv4-mapped address, dotted (`::ffff:203.0.113.5`) or hex (`::ffff:cb00:7105`), is still judged as the IPv4 address it maps. A zone identifier on an address (`fe80::1%eth0`) is dropped before matching. + - Families never cross: an IPv4 range never admits a pure IPv6 address, and an IPv6 range never admits an IPv4 one. + - An IPv6 prefix must be a whole number from 0 to 128. An entry with any other prefix matches only its base address, as it did before. IPv4 entries are read exactly as before. +- 26c2efd: fix(plugin-auth): Setup's Register SAML Provider registers a complete form; it answered 400 for every one + + Clause-②: no — a form the bridge always refused registers; nothing a caller sends today is refused + Metadata-protocol: none + + - **Before.** `POST /api/v1/auth/admin/sso/register-saml` (Setup → SSO Providers → Register SAML Provider) answered `400 SAML_REGISTER_FAILED` "[body.samlConfig.idpMetadata] idpMetadata.entityID is required when IdP metadata XML is not provided" for every complete form. The bridge sent `samlConfig` with no IdP descriptor, which `@better-auth/sso` requires when no IdP metadata XML is given, so no SAML provider could be registered from Setup. + - **Now.** The bridge sends `samlConfig.idpMetadata.entityID` from the form's IdP Entity ID field (`issuer`), and a complete form registers the provider (one `sys_sso_provider` row) and answers the SP ACS and metadata URLs as before. The form's fields, the required set and the refusal of an incomplete form (`400 INVALID_REQUEST` naming the missing field) are unchanged. The IdP SSO URL and signing certificate stay where they were (`samlConfig.entryPoint`, `samlConfig.cert`). +- 455b4f0: feat(plugin-auth): a platform admin can read the address the allowed IP ranges save check will judge, before saving, at `GET /api/v1/auth/admin/client-address` (#30398) + + Clause-②: yes (widening) — a new authenticated HTTP read on the auth mount, ledgered `server-only` in the plugin's auth route ledger; no spec schema, SDK method or package export moves. + Metadata-protocol: none + + A save of `auth.allowed_ip_ranges` (ADR-0069 D5) is refused when the list leaves out the address the saving request comes from, and the refusal names that address. Until now that refusal was the only place the address appeared, so an administrator learned which address counts by being refused. The new read answers it beforehand. + + - **The answer.** `200 { success: true, data: { clientAddress, trustProxy } }` with `Cache-Control: no-store`. `clientAddress` is the caller's address exactly as the save check and the sign-in gate judge it, through the same call the save check makes: the forwarded `X-Forwarded-For` / `X-Real-IP` address only when the stack declares `server.trustProxy`, the connection's peer address otherwise, in the same text (an IPv4-mapped peer reads `::ffff:127.0.0.1`, as the refusal names it). It is `null` when the server cannot establish an address; while any list is set, such a connection is refused at sign-in and any non-empty list at the save. `trustProxy` says whether forwarded headers counted. + - **Who may read it.** A platform admin, through the gate every ObjectStack `/admin/*` auth route uses: no session answers `401 UNAUTHENTICATED`, a session held by the authentication policy (an expired password, required two-factor not set up) answers `403` with that posture's code, and any other caller `403 PERMISSION_DENIED`. It is not served to anonymous callers, and `GET /api/v1/auth/config` is unchanged. + - **Closed query set.** The route takes no query parameters; any one is refused with `400 VALIDATION_ERROR`. + - **Behind the allow-list.** Like every auth route except the sign-in page's two render reads, the read is judged by the allow-list in force: a caller already outside it gets `403 IP_NOT_ALLOWED`. + + The save check, the sign-in gate and the address rule are unchanged: the save check now reaches the rule through the helper the read shares, with the same inputs. +- e8e924f: fix(plugin-auth,platform-objects): an organization's require-MFA refusal reads in the caller's language, an accepted save that turns it on names the grace period in force, and the field help says two-factor must be on (#30411) + + Clause-②: no + Metadata-protocol: none + + - **Before.** Saving `sys_organization.require_mfa` on while two-factor enrollment was off was refused with an English sentence, whatever language the administrator's console used. An accepted save that turned it on answered nothing about when members would be blocked, and the configured grace period (`auth.mfa_grace_period_days`) was readable only through the `auth` settings, which need `manage_platform_settings`, a capability an organization administrator does not hold. The field help did not say the requirement needs two-factor on. + - **Now.** The refusal is in the request's language (English, Simplified Chinese, Japanese, Spanish; matched from `Accept-Language` exactly, then by language, else English), with the same shape as before: `400 VALIDATION_FAILED`, one `require_mfa` / `invalid_value` field entry carrying the same sentence as the top-level message. A save that is admitted and turns the requirement on (two-factor on, the stored value off, or an insert) answers a save `notice` on the create or update answer, in the same language: every member without an authenticator app must enroll, within the grace period in force, in days, read per write from the same setting the MFA gate's clock holds to. A save that turns nothing on (clearing it, re-saving a stored `true`, writing other columns) answers no notice. The field help, in all four languages, says the requirement needs two-factor authentication on for the deployment (`OS_AUTH_TWO_FACTOR=true`) and that saving it on is refused while it is off. + - **Unchanged.** Which writes are refused and which are admitted, the refusal's code, status and field entry, the MFA gate and its grace clock, and every `auth` setting and its read permission. No member is added to any answer: `notice` is the create and update answers' existing optional member. +- 1c5cee6: fix(service-settings): an `OS_LOCALIZATION_LOCALE` naming a language the boot is still loading is no longer dropped, and Setup's default language accepts by the same locale matcher as the IdP locale claim (#30456) + + Clause-②: yes (widening) — `@objectstack/core` exports `resolveOfferedLocale(requested, offered)`; `SettingsService` gains `holdOptionSourceVerdicts()` and `sealOptionSources()`. The save of `localization.locale` (`PUT /api/settings/localization`, `SettingsService.setMany`) and an `OS_LOCALIZATION_LOCALE` override now also accept a region or base of an offered language (`fr-FR` where `fr` is offered, `fr` where `fr-FR` is) and another case of an offered code (`zh-cn` for `zh-CN`); nothing accepted before is refused. The IdP locale claim (`resolveIdpLocale`) lands a script or region spelling on the offered code the platform locale table says it means (`zh-TW` on `zh-Hant`, not on a `zh-Hans` listed first). + Metadata-protocol: none + + - **The boot window.** The deployment's locale list fills during the boot: the app's authored translations arrive at `kernel:ready`. A `kernel:ready` reader of `localization.locale` (auth's mail and SMS default language) could ask before the language an `OS_LOCALIZATION_LOCALE` override names was in the list; the override was then reported `IGNORED` and the reader kept the next layer's value, though the boot went on to offer that language. `SettingsServicePlugin` now holds the option verdict on a sourced key until `kernel:bootstrapped`: while held, an override the list does not admit yet is in force and not reported; at `kernel:bootstrapped` it is judged again, and one the list still does not admit is reported once, as before, and a settings change is emitted for `localization.locale` so a reader that applied it reads the key again. The other value checks (pattern, domain, bounds, format) are judged at once, as ever. A `SettingsService` constructed without the plugin judges at once, as it always has. + - **One matcher.** Setup's default language and the IdP locale claim now both ask `resolveOfferedLocale` (`@objectstack/core`): `resolveLocale`'s exact and case-insensitive steps, then the platform locale table's language (`zh-TW` is `zh-Hant`, `en-US` is `en`), then `resolveLocale`'s base-language and variant steps. A language one door takes, the other takes too. + - **Comment.** `AUTH_EMAIL_TEMPLATE_LOCALES` (plugin-auth) no longer claims to be the setting's options: since #30286 the setting offers the deployment's locale list, and the two ends agree through `normalizeAuthEmailLocale`'s pass-through and the email service's `en-US` fallback. +- f4cd4ec: fix(core)!: a banned user's credentials are refused while the ban holds, at every door that accepts an API key + + Clause-②: yes (narrowing) — while a user's ban holds, a request authenticated by an API key that user owns is answered as a key that does not exist (the generic `401 UNAUTHENTICATED`), at every door that accepts a key: the REST data door, the MCP door, and every other door that resolves a key through the shared key-admission seam. The widening half: `@objectstack/core` exports the one ban predicate, `isUserBanInForce` (with `USER_BAN_FIELDS` and the `UserBanFields` type), which the key-admission seam and plugin-auth's session-shaped refusals now share. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at every key-admitting door (`@objectstack/core`): a banned user's API keys are refused while the ban holds. + + - **The rule.** A ban holds every credential of the user. Banning a user already ends their sessions and refuses their sign-in; their API keys now answer as a key that does not exist for as long as the ban holds. A ban with an expiry stops binding once the expiry has passed, exactly as sign-in does — no sign-in is needed to lapse it. + - **Not a revocation.** No key row is revoked or rewritten. Lifting the ban (or letting it expire) admits the same keys again, with nothing reissued, each still judged by its own lifecycle: a key left unused past the inactivity window during the ban answers as inactive (`API_KEY_INACTIVE`), and its remedy is a new key. + - **One anonymous entry.** A key refused for its owner (a ban) or for the tenancy posture (no organization under `isolated`) resolves to the same entry as a key that does not exist, on every composition: the guest's grants on a face that serves the guest, nothing elsewhere. It never falls through to a session. + - **Who moves.** An integration running on a key whose owner is banned gets `401` until the ban is lifted or expires. + + **The remedy.** Lift the ban, or mint a replacement key under a user who is not banned. +- b3d2f4b: fix(runtime,plugin-auth)!: a banned user's OAuth access tokens and refresh grants are refused while the ban holds + + Clause-②: yes (narrowing) — while a user's ban holds, an OAuth access token that user consented to is answered at the MCP door as a token that does not verify (the generic `401`, with the same body and `WWW-Authenticate` header), and so is an agent forward credential acting for that user; `/oauth2/userinfo` answers that token as an access token that does not verify (`401 invalid_token`), and `/oauth2/introspect` answers that user's access and refresh tokens as inactive (`{ "active": false }`); the token endpoint mints that user no token: a `refresh_token` grant answers as an invalid refresh token (`400 invalid_grant`), and an `authorization_code` exchange is refused, with the code spent: by the provider's own session answer (`400 invalid_request`) when the ban ended the user's sessions, as the ban routes do, and otherwise as an invalid code (`400 invalid_grant`). Those are the doors this change narrows. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at the MCP door and at the OAuth authorization server's token, userinfo and introspection endpoints: a banned user's OAuth access tokens and refresh grants are refused while the ban holds. + + - **The rule.** Banning a user already ends their sessions and refuses their sign-in and their API keys. While the ban holds, their OAuth access tokens are now also refused at the MCP door and read as not valid at userinfo and introspection, and the token endpoint mints them no new token (no refresh, no code exchange). A ban with an expiry stops binding once the expiry has passed, exactly as sign-in does. + - **Not a revocation.** No token, refresh token or consent row is revoked or rewritten. Lifting the ban (or letting it expire) admits the same access token at the MCP door and lets the same refresh token renew, with nothing reissued. An authorization code presented while the ban held is spent, so the client signs the user in again. + - **Cost.** One primary-key read of the user's ban columns per OAuth credential the MCP door resolves, and one read of the user row per active introspection answer that names a user; the token and userinfo endpoints read nothing extra. A store fault on the MCP door's read answers `503 SERVICE_UNAVAILABLE`, as every other authorization-input read on that door does; a store fault on the introspection read fails the request rather than answering active or inactive. + - **Who moves.** An MCP client or OIDC relying party connected over OAuth on behalf of a banned user gets `401` at the MCP door and at userinfo, `{ "active": false }` from introspection, and `400` from the token endpoint, until the ban is lifted or expires. The Ban User confirmation (`@objectstack/platform-objects`) and the administrator and authentication guides now say so. + + **The remedy.** Lift the ban, or connect the client as a user who is not banned. +- 85c7374: fix(plugin-auth): a change-email request notifies the current address whatever way the caller is signed in + + Clause-②: no + Metadata-protocol: none + + - **The rule.** Every `POST /api/v1/auth/change-email` request that is accepted mails the account's current address the `auth.email_change_notice`, whatever way the caller is signed in. The notice goes to the account the request changes: its address is read from the session the endpoint itself acted on. + - **Unchanged.** The notice's template and text; the confirmation mail to the requested address; a refused request still mails nothing; the notice still never blocks the change. +- 7b2bb66: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(plugin-auth): a session a password reset ends leaves a revocation trail + + Clause-②: no + Metadata-protocol: none + + - **A session a password reset ends leaves a revocation trail.** When a completed self-service password reset ends the account's sessions — the emailed link (`POST /api/v1/auth/reset-password`), and the phone OTP reset (`POST /api/v1/auth/phone-number/reset-password`); better-auth's email-OTP reset door, which the platform does not mount, is covered on its own path too — each `sys_session` row is now kept and stamped, `revoked_at` with `revoke_reason: password_reset`, the same kind of trail an interactive revoke leaves, instead of being deleted. The session is still refused (`401`). + - **The value says who ended the session.** `password_reset` is distinct from `user_revoked` (the owner revoking their own sessions) and from `admin_password_reset`, which an administrator's forced reset keeps writing. + - **`sys_session.revoke_reason`'s published vocabulary names `password_reset`** (`@objectstack/platform-objects`, the field's description and its translated help). + - **Unchanged.** Which sessions a reset ends; sign-out, which still deletes its row; the account's API keys, which a self-service reset does not revoke; and the audit log, which records no `logout` row for a reset. +- c92b887: fix(plugin-auth): the phone-number password-reset request answers without waiting for the SMS send, as the emailed reset does + + Clause-②: no + Metadata-protocol: none + + - **The rule.** `POST /api/v1/auth/phone-number/request-password-reset` answers at once, the same way for any number: the reset text is built and sent after the response, as the emailed reset (`POST /api/v1/auth/request-password-reset`) already sends its mail. + - **A send that fails is logged on the server**, at error (`phone password-reset text was not delivered`), and never reaches the caller, who is answered `{ status: true }` either way, as before. The one-time code is in the text only, never in that line. + - **Unchanged.** The answer's status and body; the per-number cooldown and hourly cap and the per-IP limit on the route; the text itself; and a server-side `auth.api.requestPasswordResetPhoneNumber` call, which carries no request and still awaits its send. +- 6dd2ca5: fix(plugin-security, plugin-auth)!: a person-to-permission-set assignment refuses an agent ceiling set at every write door, and so does the self-registration permission set + + Clause-②: no (narrowing) — a non-system insert or update of `sys_user_permission_set` that would store an assignment naming one of the three MCP agent ceiling sets (`mcp_agent_data_read`, `mcp_agent_data_write`, `mcp_agent_restricted`) is refused with `400 VALIDATION_FAILED` (`invalid_value` at `permission_set`), for every caller, a platform administrator included; and an audience configuration whose `selfRegistrationPermissionSet` names one is refused (`[audience] invalid audience configuration`), at construction and through the auth settings; nothing is widened. + Metadata-protocol: none + + + + **BREAKING** for a deployment whose administrators assign an MCP agent ceiling set to users through the permission-set assignment table, or whose audience names one as `selfRegistrationPermissionSet`. + + - **The rule (ADR-0090 D10).** The agent ceiling sets are an AI agent's own grants. The door that admits an agent connected over MCP derives its ceiling from the scopes its user consented to and puts it on the agent's request; no assignment row is read for it. A `sys_user_permission_set` row is a person's grant, so it never names one. Every door that writes the table refuses such a row: the data API's create and update, `createMany`, `updateMany`, the per-object and cross-object batch, and both import doors (the row fails with `code: 'invalid_value'` at `permission_set`). An update that keeps an assignment naming one in force (a new holder, a validity window, an echo of the set) is refused too. + - **The self-registration set (`@objectstack/plugin-auth`).** A self-registrant is a person, so the audience configuration that names the set every self-registrant receives refuses an agent ceiling set, beside its existing refusal of `admin_full_access`: `auth.audience.selfRegistrationPermissionSet` in the stack config stops the boot, and `audience_self_registration_permission_set` in the auth settings is refused while the standing audience keeps ruling. Both answer `[audience] invalid audience configuration: selfRegistrationPermissionSet must not be '...'`. + - **Unchanged.** Every other permission set stays assignable, and removing an assignment stays open. A system write is not judged: the platform's fixed-name grant writers name no ceiling, the self-registration grant writes only the validated set, and seed replay and the development-only demo-staff seed write the sets their stack declares. An agent connected over MCP holds its ceiling exactly as before. An organization administrator is still refused the same sets on the organization boundary first (`403 PERMISSION_DENIED`). + - **Who moves, and the fix.** An assignment stored before this release that names an agent ceiling set is left as it is, and can no longer be edited: delete it (`DELETE /api/v1/data/sys_user_permission_set/ID`), or rename it to a permission set meant for people. List them with `GET /api/v1/data/sys_user_permission_set?permission_set=mcp_agent_data_read` and the same query for the other two names. An audience that names an agent ceiling set as `selfRegistrationPermissionSet` declares a permission set meant for people instead (`member_default`, or the deployment's own). +- a7293f7: fix(plugin-auth): a sign-up answers without waiting for any mail send, whether or not the address already has an account + + Clause-②: no + Metadata-protocol: none + + - **The rule.** `POST /api/v1/auth/sign-up/email` answers without waiting for any mail send, whether or not the address already has an account: the verification mail, and the notice to an address that already has an account, are built and sent after the response, as the password-reset mail already is. + - **A send that fails is logged on the server**, at error (`verification email was not delivered`; the notice keeps its own error line), and never reaches the caller, whose answer is the one a sent mail gets, as before. + - **Unchanged.** The answer's status and body; the sign-up rate limits; the mails themselves; a signed-in caller's `/send-verification-email` resend, which still awaits its send and still reports one that failed; and a server-side `auth.api.signUpEmail` call, which carries no request and still awaits its sends. +- 4851668: fix(plugin-auth): a verification-email resend from a visitor who is not signed in answers the same whatever the send does + + Clause-②: no + Metadata-protocol: none + + - **The rule.** `POST /api/v1/auth/send-verification-email` from a visitor who is not signed in answers the same whatever the send does. While the email transport is live it answers `{ "status": true }`: the verification mail is built and sent after the response, as the sign-up's verification mail is, and a send that fails is logged on the server at error (`verification email was not delivered`). + - **While the email transport is not live** (no email service, or one whose `isConfigured()` does not answer `true`, such as the `log` transport), every such resend is refused with `400 EMAIL_SERVICE_REQUIRED`, the code `/change-email` and the identity import already answer for a deployment with no email delivery. Its message names the fix (Settings → Mail), and the verify screens show it. + - **Unchanged.** A signed-in caller's resend for their own address, by session cookie or by bearer token, still awaits its send, still reports one that failed, and is not refused while the transport is not live. A server-side `auth.api.sendVerificationEmail` call, which carries no request, still awaits its send. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [75bbebb] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [18112bf] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [63b86b3] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [fdad178] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [06cccd3] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [1a3ea0c] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [8eca18f] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [fb1ab89] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [a0c68ae] +- Updated dependencies [ede9e6b] +- Updated dependencies [ca37605] +- Updated dependencies [cabdfe8] +- Updated dependencies [739c972] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/service-messaging@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/rest@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index 47f2f79951e..4adbff862b7 100644 --- a/packages/plugins/plugin-auth/package.json +++ b/packages/plugins/plugin-auth/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-auth", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Authentication & Identity Plugin for ObjectStack", "type": "commonjs", diff --git a/packages/plugins/plugin-dev/CHANGELOG.md b/packages/plugins/plugin-dev/CHANGELOG.md index 1fa93857465..22fcb75222a 100644 --- a/packages/plugins/plugin-dev/CHANGELOG.md +++ b/packages/plugins/plugin-dev/CHANGELOG.md @@ -1,5 +1,153 @@ # @objectstack/plugin-dev +## 18.4.0 + +### Patch Changes + +- Updated dependencies [31fe289] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [75bbebb] +- Updated dependencies [c23501b] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [665db00] +- Updated dependencies [e3b56b2] +- Updated dependencies [18112bf] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [cd5f554] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [63b86b3] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [343ddb2] +- Updated dependencies [0c666c3] +- Updated dependencies [f672e0d] +- Updated dependencies [38717d3] +- Updated dependencies [bfbdeec] +- Updated dependencies [4adc890] +- Updated dependencies [fdad178] +- Updated dependencies [bcdf8d3] +- Updated dependencies [59bcd22] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [a545a04] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [1a3ea0c] +- Updated dependencies [bb86ff9] +- Updated dependencies [8f05e61] +- Updated dependencies [9afcd6a] +- Updated dependencies [870ebce] +- Updated dependencies [d1d90b7] +- Updated dependencies [2046d21] +- Updated dependencies [a26abbc] +- Updated dependencies [8eca18f] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [a545a04] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [8589756] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [a93ef3e] +- Updated dependencies [0e4dfbe] +- Updated dependencies [8cc709d] +- Updated dependencies [b5765be] +- Updated dependencies [5a878a3] +- Updated dependencies [fabd259] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [26c2efd] +- Updated dependencies [f7afab8] +- Updated dependencies [7b80724] +- Updated dependencies [fb1ab89] +- Updated dependencies [455b4f0] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [ab159a9] +- Updated dependencies [66a8822] +- Updated dependencies [739c972] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [63de248] +- Updated dependencies [a2437b5] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [474c030] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [70981e6] +- Updated dependencies [85c7374] +- Updated dependencies [eca7cd2] +- Updated dependencies [42e1211] +- Updated dependencies [7193160] +- Updated dependencies [72d7db0] +- Updated dependencies [d0da880] +- Updated dependencies [7b2bb66] +- Updated dependencies [30dcd8a] +- Updated dependencies [c92b887] +- Updated dependencies [21510bb] +- Updated dependencies [d7c0389] +- Updated dependencies [6dd2ca5] +- Updated dependencies [917f024] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [a7293f7] +- Updated dependencies [59e6463] +- Updated dependencies [63a470a] +- Updated dependencies [4851668] + - @objectstack/objectql@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/runtime@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/rest@18.4.0 + - @objectstack/plugin-auth@18.4.0 + - @objectstack/plugin-security@18.4.0 + - @objectstack/service-storage@18.4.0 + - @objectstack/plugin-hono-server@18.4.0 + - @objectstack/service-realtime@18.4.0 + - @objectstack/account@18.4.0 + - @objectstack/setup@18.4.0 + - @objectstack/driver-memory@18.4.0 + - @objectstack/service-i18n@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/plugins/plugin-dev/package.json b/packages/plugins/plugin-dev/package.json index ad877dbc9c2..717632f196c 100644 --- a/packages/plugins/plugin-dev/package.json +++ b/packages/plugins/plugin-dev/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-dev", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Development Assembly Plugin for ObjectStack — wires the real platform stack for zero-config local development", "type": "commonjs", diff --git a/packages/plugins/plugin-email/CHANGELOG.md b/packages/plugins/plugin-email/CHANGELOG.md index b104f6d6a7a..04fd40f4370 100644 --- a/packages/plugins/plugin-email/CHANGELOG.md +++ b/packages/plugins/plugin-email/CHANGELOG.md @@ -1,5 +1,121 @@ # @objectstack/plugin-email +## 18.4.0 + +### Patch Changes + +- 509c40a: fix(plugin-email): a non-string recipient display name answers the send door's 400, not 500 + + Clause-②: no + Metadata-protocol: none + + `POST /api/v1/email/send` with a recipient such as `{ "address": "ok@example.com", "name": 42 }` in `to`, `cc`, `bcc` or `replyTo` (or as one entry of a `to` / `cc` / `bcc` list) now answers `400 VALIDATION_FAILED` with "invalid recipient address in `to`: the display name of ok@example.com must be a string" (`reply-to address` for `replyTo`). It used to answer `500 EMAIL_SEND_FAILED` with `a.name?.trim is not a function`. + + The refusal itself was already right. The 500 came from the code that writes the `sys_email` rejection row after the refusal: it called `.trim()` on the caller's raw display name. That writer now reads the name as unknown input and leaves a non-string name out of the row (the row's `error` column names it), so the caller gets the 400 and the rejection is still recorded. A string name, an absent name and a `null` name behave as before. +- db04e5c: fix(plugin-email,service-settings): mail whose sender has no From name goes out under the product name, not "ObjectStack" + + Clause-②: no + Metadata-protocol: none + + - **Now.** A default sender with no display name of its own goes out under the product name the auth mails' subject and body already use: the Setup → Branding product name when an administrator set one, else the deployment's name (`OS_APP_NAME`, then `config.email.appName`, `config.email.defaultTemplateContext.appName` and the top-level `appName`), else `ObjectStack`. A Branding save renames the sender on the next mail, with no restart. The "Send test email" button sends under the same name. + - **Which senders.** The sender saved in Setup → Mail with the From name left empty, and a sender declared at boot without a name (`OS_EMAIL_FROM=no-reply@acme.com`, or `config.email.defaultFrom` with an `address` alone). Before, the first went out as `ObjectStack <…>` and the second with no display name. + - **Unchanged.** A From name somebody set always wins: one saved in Setup → Mail, or one declared at boot (`OS_EMAIL_FROM="Acme "`, `config.email.defaultFrom.name`). A sender a send names itself, and a template's own sender, are never renamed. Under a walled tenancy posture, where Setup → Branding is not offered, the deployment's name applies. + - **`@objectstack/service-settings`.** Setup → Mail → From name no longer has the default `ObjectStack`: an unsaved field reads empty, and `GET /api/settings/mail` answers `from_name` as `value: null`, `source: 'default'`. The field's help says that an empty value uses the product name. A From name saved earlier is kept and still wins; clear the field to follow the product name. +- bdbaf80: fix: Setup's form help speaks to an administrator, and the queue pages, the mail status banner and the test-email toast read in Chinese on a zh-CN console + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Field help (English source and zh-CN):** the permission set and position assignment forms, the out-of-office (approval delegation) rule, and the business unit, organization and user forms no longer describe the implementation. Gone: "fail-closed", "half-open [from, until)", "agent grants", "graph semantics", "expanded by graph queries", "Null = root of tenant", "HRIS", "When true / When false" (zh-CN 「为 true 时」 now reads 「开启后」) and an HTTP status code. The help now says when an assignment starts and ends, what turning a switch on does, and what to fill in. Text only: no field name, key, option value or behaviour changes. + - **`@objectstack/plugin-sharing`:** `sys_queue` and `sys_queue_member` join the plugin's translation bundle, with zh-CN text, so the 队列 and 队列成员 pages, their columns, help and the 新建 button read in Chinese. ja-JP and es-ES carry English fills for now. + - **`@objectstack/plugin-email`:** the live-status statement at the top of Setup → Email and the "Send test email" answer (sent, failed, logged only) are written in the caller's display language, en and zh-CN today; another language gets English. + - **`@objectstack/service-settings`:** a settings action handler (`SettingsActionHandler`) receives an optional `locale`, the caller's display language read from the request the same way the live-status statement's is (`Accept-Language`, else `?locale`); `runAction` takes it as a fifth `display` argument. Existing handlers compile and behave unchanged. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [d9d42d4] +- Updated dependencies [67d5d53] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [0e4747a] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/platform-objects@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-email/package.json b/packages/plugins/plugin-email/package.json index 4b279612df8..e507f1db782 100644 --- a/packages/plugins/plugin-email/package.json +++ b/packages/plugins/plugin-email/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-email", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Email service plugin for ObjectStack — IEmailService + transport-pluggable outbound delivery with sys_email persistence.", "type": "commonjs", diff --git a/packages/plugins/plugin-hono-server/CHANGELOG.md b/packages/plugins/plugin-hono-server/CHANGELOG.md index 9dc3e3dbbc9..c9d954cfeca 100644 --- a/packages/plugins/plugin-hono-server/CHANGELOG.md +++ b/packages/plugins/plugin-hono-server/CHANGELOG.md @@ -1,5 +1,89 @@ # @objectstack/plugin-hono-server +## 18.4.0 + +### Patch Changes + +- f7afab8: fix(core,objectql): effective object permissions read the external write gate, so a read-only external object is no longer offered New, Import, Edit or Delete + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Behaviour.** On an object whose external write gate is closed (ADR-0015 §5.3 Gate 3: a federated datasource, and not both `datasource.external.allowWrites` and `object.external.writable`), every insert, update and delete is refused `403 EXTERNAL_WRITE_FORBIDDEN` for every caller. The effective object-permission map now says so: the entry grants no `allowCreate`, `allowEdit`, `allowDelete` or `allowTransfer`, a `modifyAllRecords` on it is lowered to the `viewAllRecords` read it implies, and `apiOperations` carries no write operation (`create`, `update`, `delete`, `upsert`, `bulk`, `import`, `restore`, `purge`). Read and export are unchanged. Both consumers move together: `/auth/me/permissions` (the console hides those doors with no client change) and `ISecurityService.getEffectiveObjectPermissions`, so `current_user.can(object, 'create' | 'edit' | 'delete' | 'transfer')` now answers `false` on such an object, as the write itself always did. + - **`@objectstack/objectql`: `ObjectQL.isExternalWriteForbidden(objectName)`**, the gate's verdict as a read-only question. The write gate (`assertWriteAllowed`) reads the same verdict, so the two cannot disagree. + - **`@objectstack/core`: `engineExternalWriteForbidden(engine)`** (the accessor over an engine, asked when the member is present) and the optional `isExternalWriteForbidden` accessor on `EffectiveObjectPermissionsSchemaSource`; `annotateEffectiveApiOperations` takes the same predicate as an optional third argument. A caller that hands over neither gets the map it got before. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/observability@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/plugins/plugin-hono-server/package.json b/packages/plugins/plugin-hono-server/package.json index d184de97e26..d3fd0174423 100644 --- a/packages/plugins/plugin-hono-server/package.json +++ b/packages/plugins/plugin-hono-server/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-hono-server", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Standard Hono Server Adapter for ObjectStack Runtime", "type": "commonjs", diff --git a/packages/plugins/plugin-pinyin-search/CHANGELOG.md b/packages/plugins/plugin-pinyin-search/CHANGELOG.md index f2280f5483b..5159b370ae2 100644 --- a/packages/plugins/plugin-pinyin-search/CHANGELOG.md +++ b/packages/plugins/plugin-pinyin-search/CHANGELOG.md @@ -1,5 +1,98 @@ # @objectstack/plugin-pinyin-search +## 18.4.0 + +### Patch Changes + +- Updated dependencies [31fe289] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [cd5f554] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [343ddb2] +- Updated dependencies [0c666c3] +- Updated dependencies [f672e0d] +- Updated dependencies [38717d3] +- Updated dependencies [bfbdeec] +- Updated dependencies [4adc890] +- Updated dependencies [bcdf8d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [a26abbc] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [8cc709d] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [7b80724] +- Updated dependencies [fb1ab89] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [63de248] +- Updated dependencies [a2437b5] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [7193160] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/objectql@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/plugins/plugin-pinyin-search/package.json b/packages/plugins/plugin-pinyin-search/package.json index 6ffca057a52..1313c691e36 100644 --- a/packages/plugins/plugin-pinyin-search/package.json +++ b/packages/plugins/plugin-pinyin-search/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-pinyin-search", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Pinyin search recall for ObjectStack — populates the hidden `__search` companion column (full pinyin + initials of the display/name field) so `$search` hits CJK names typed as pinyin. Locale-gated via OS_SEARCH_PINYIN_ENABLED (#2486).", "type": "commonjs", diff --git a/packages/plugins/plugin-security/CHANGELOG.md b/packages/plugins/plugin-security/CHANGELOG.md index 4670afa0fbd..a9fe0236b60 100644 --- a/packages/plugins/plugin-security/CHANGELOG.md +++ b/packages/plugins/plugin-security/CHANGELOG.md @@ -1,5 +1,268 @@ # @objectstack/plugin-security +## 18.4.0 + +### Minor Changes + +- 21510bb: feat(plugin-security)!: under a walled tenancy posture every external object states its organization scope + + Clause-②: no (narrowing) — under the `isolated` and `group` postures an external object (an `external` block) that states no organization scope is refused by name when the boot completes, and one registered after the boot answers no row and accepts no write; nothing is widened, and the `single` posture is unchanged. + Metadata-protocol: narrowed + + + + **BREAKING** for a deployment running the `isolated` or `group` tenancy posture (`OS_TENANCY_POSTURE`) with an external object that states nothing about its organization scope. + + - **The rule (ADR-0015 §10 as amended, ADR-0095 D1).** The platform provisions no storage for an external object, so the `organization_id` it injects into the registered schema is a column the remote may not carry. Whose rows an external object serves — one organization's or every organization's — is a fact only its author knows, and under a walled posture the object states it, in one of three ways: its own `organization_id` field (the remote rows carry the organization column and the driver returns it; a remote column of another name is mapped onto it with `external.columnMap: { : 'organization_id' }`); `tenancy: { enabled: false }` (shared by every organization); or `systemFields: { tenant: false }` (the same opt-out on the system fields). These are the wall's own two opt-out clauses and the one column it reads; the whole-block `systemFields: false` states nothing. + - **What the rule does.** `SecurityPlugin` refuses the boot from its `kernel:ready` handler, beside the ADR-0131 D5 refusal, naming every external object that states nothing and the three ways out (`ExternalObjectOrganizationScopeUndeclaredError`). An external object registered after the boot — a Studio import, a runtime datasource — that states nothing answers no row and accepts no write: the Layer 0 verdict is `deny` (`computeTenantLayer0Verdict`), the write wall refuses every insert, update and delete with a sentence naming the undeclared scope and the three ways out, and the plugin says so once in a warning. An object declaring its own `organization_id` is walled: a member of organization A reads only A's rows, through a driver that honours `where` and through one that ignores it (the engine's floor re-applies the predicate), and a row missing the column reads to no one. An object declaring `tenancy: { enabled: false }` or `systemFields: { tenant: false }` is shared. + - **`tenancy.tenantField` on an external object is closed the same way.** The organization wall keys on `organization_id` alone, and a declared tenant field is read only by storage the platform provisions, which an external object has none of; so on an external object a `tenantField` naming another column is refused at boot and denied after it, with the remedy spelled out (`external.columnMap` onto `organization_id`, or `tenancy: { enabled: false }`). Local objects are unchanged. + - **Who moves.** Every walled deployment with an external object stating nothing: declare one of the three and restart. The showcase's two federated analytics objects now declare `tenancy: { enabled: false }`. A `single`-posture deployment owes no declaration and behaves exactly as before. + - **`@objectstack/core`.** `TenantLayer0Input` gains the optional `externalOrganizationScopeUndeclared`; set, the verdict is `deny` under a walled posture, ahead of the platform-admin crossing. Omitted, every existing caller reads as before. + - **`@objectstack/spec`.** The ADR-0087 D3 entry `external-object-organization-scope-declared` tells an upgrading operator what to declare. + + Skills-docs: follow-up #29866 — `content/docs/data-modeling/virtual-objects.mdx` (a multi-organization section), `skills/objectstack-data/rules/datasources.md` (the three declarations), `docs/adr/0015-external-datasource-federation.md` (the §10 and D7 amendment); the sibling docs PR on `claude/sec-hardening-g-docs`, governed (Tier H), landed by the maintainer's hand. +- 0fd363d: fix(spec)!: an object permission that edits or deletes a record also reads it + + Clause-②: no (narrowing) — the authoring object-permission schema refuses an entry granting `allowEdit: true` or `allowDelete: true` without read (`allowRead`, `viewAllRecords` or `modifyAllRecords`), a permission set it accepted before; the permission evaluator grants edit and delete only together with read; and a by-id update or delete from a caller the read door refuses answers that refusal. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: docs moved — `content/docs/permissions/permission-sets.mdx` states the rule under the object permission bits; `skills/**` teaches no entry granting edit or delete without read (its examples grant read beside edit), so no skill moves. + + + + **BREAKING** accept-set narrowing on `ObjectPermissionSchema` (and so `PermissionSetSchema`, `definePermissionSet`, `defineStack`, `os validate` / `os build`, and the metadata save and publish doors for `permission`). + + - **Before.** `objects: { crm_lead: { allowEdit: true } }` parsed, and the evaluator granted edit on `crm_lead` to a holder who could not read it. + - **Now.** The schema refuses it: one `custom` issue at `objects.crm_lead.allowEdit` (or `.allowDelete`), naming both fixes. `objectPermissionGrants` answers edit and delete as `read ∧ grant` (the shape `allowExport` already had), and `PermissionEvaluator.checkObjectPermission` asks it of the caller's merged entry, so the read half may come from another set — the same answer `/me/permissions` gives the client. + - **Stored rows.** A permission set stored before this release keeps loading; its unread edit or delete grant grants nothing. The response-side `EffectiveObjectPermissionSchema` stays tolerant. A built artifact carrying the shape is refused when it is parsed and is rebuilt from a corrected source. + - **By-id writes.** A by-id update or delete from a caller whose read of the object is refused — with these grants that is a capability the object requires for a read and not for a write — answers that read refusal for every id it addresses: a caller who cannot read an object writes none of its records by id. + - **`@object-ui/app-shell`.** The permission set editor ticks Read when Edit or Delete is ticked, and unticking any bit that reads (Read, View All or Modify All) unticks Edit and Delete once no bit that reads remains on the entry. + - Unchanged: `allowCreate` and `allowTransfer` are not part of this rule. + + **FROM → TO.** `objects: { crm_lead: { allowEdit: true } }` → `objects: { crm_lead: { allowRead: true, allowEdit: true } }` if the set should edit the object, or `objects: { crm_lead: {} }` if it should not. The upgrade-guide entry is `permission-write-without-read-refused` (a semantic migration: no automatic rewrite, because only the author knows which of the two the set means). + +### Patch Changes + +- a545a04: fix(plugin-security): every member reads and posts comments, and reads the activity history, of the records they can read — the platform baseline `member_default` now grants `sys_comment` (read, create, edit) and `sys_activity` (read) + + Clause-②: no — runtime permission behaviour (human-floor security/permission class, ruled by the maintainer), not a published accept-set or public-surface widening + Metadata-protocol: none + + Comments (`enable.feeds`) and the activity timeline (`enable.activities`) are on by default on every object, but no shipped permission set named `sys_comment` or `sys_activity`. A member of an app whose permission sets name only the app's own objects — what `create-objectstack` generates — was refused both (`403` on `GET /api/v1/data/sys_comment` and `GET /api/v1/data/sys_activity`), on records they can read: the record page showed "no permission" for comments and activity, and offered no comment box. An app author no longer needs to grant the two objects by hand. + + - **Which rows is unchanged.** The grant opens the two objects; plugin-audit's parent-record gates still decide the rows, so a member reads and posts comments, and reads the history, of exactly the records they can read, and nothing of any other record — a record another user owns under the app's row scope, or a record of an object the member does not read. A member edits the comments they wrote. The grant carries no delete bit (deleting a comment stays the moderation path, through a delete bit an app grants), no export and no view-all or modify-all. + - **`enable.feeds: false` still closes comments** on that object: creating a comment there, or moving one into it, answers `403 FEEDS_DISABLED`. `enable.activities: false` still writes no activity row for the object. + - **External members are not reached.** A holder of the `external` organization role does not hold the `everyone` anchor and resolves `external_default` in place of `member_default`, so a portal user still gets comments and activity only through an explicit grant, under the same two gates. + - **No log noise.** A member's activity read skips the parent objects they cannot read without reading them, so the console header's feed logs no `[Security] Access denied` line per skipped object. + - An app that already grants the two objects to its members keeps working unchanged: permission sets merge most-permissively, so its own lines (a delete bit included) still apply beside the baseline's. + - **Lands with the permission-set clone door fix** (`.changeset/30255-clone-inherits-held-grants.md`). This grant first landed alone and was reverted: on a deployment that does not install `@objectstack/plugin-audit`, neither object is registered, and cloning `member_default` answered `422 INVALID_METADATA`. The clone door now copies such a set, so the grant and the fix ship together. +- 8f05e61: fix(plugin-security): an organization administrator's update by id is refused before the row is looked up, whatever the id names + + Clause-②: no + Metadata-protocol: none + + An organization administrator (`organization_admin` or `organization_admin_no_bypass`, holding no `adminScope`) updates an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. An edit in place is never part of that administrator's right, so the write is refused before the row is looked up: `403 PERMISSION_DENIED`, with the same sentence and details for a row, for a row the administrator cannot read, and for an id that names no row. + + Unchanged: a removal by id of a row the administrator cannot read, or of an id that names no row, answers the read door's `404 RECORD_NOT_FOUND`. An administrator who also holds an `adminScope` is judged by that scope. +- f7afab8: fix(core,objectql): effective object permissions read the external write gate, so a read-only external object is no longer offered New, Import, Edit or Delete + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Behaviour.** On an object whose external write gate is closed (ADR-0015 §5.3 Gate 3: a federated datasource, and not both `datasource.external.allowWrites` and `object.external.writable`), every insert, update and delete is refused `403 EXTERNAL_WRITE_FORBIDDEN` for every caller. The effective object-permission map now says so: the entry grants no `allowCreate`, `allowEdit`, `allowDelete` or `allowTransfer`, a `modifyAllRecords` on it is lowered to the `viewAllRecords` read it implies, and `apiOperations` carries no write operation (`create`, `update`, `delete`, `upsert`, `bulk`, `import`, `restore`, `purge`). Read and export are unchanged. Both consumers move together: `/auth/me/permissions` (the console hides those doors with no client change) and `ISecurityService.getEffectiveObjectPermissions`, so `current_user.can(object, 'create' | 'edit' | 'delete' | 'transfer')` now answers `false` on such an object, as the write itself always did. + - **`@objectstack/objectql`: `ObjectQL.isExternalWriteForbidden(objectName)`**, the gate's verdict as a read-only question. The write gate (`assertWriteAllowed`) reads the same verdict, so the two cannot disagree. + - **`@objectstack/core`: `engineExternalWriteForbidden(engine)`** (the accessor over an engine, asked when the member is present) and the optional `isExternalWriteForbidden` accessor on `EffectiveObjectPermissionsSchemaSource`; `annotateEffectiveApiOperations` takes the same predicate as an optional third argument. A caller that hands over neither gets the map it got before. +- 474c030: fix(plugin-security): an assignment write the administrator cannot address answers not-found, as a read does; a refusal names only values the caller can read + + Clause-②: no + Metadata-protocol: none + + An organization administrator or a delegated administrator (a caller holding an `adminScope`, an organization administrator who holds one included) updates or removes an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. + + - **A row the caller cannot read, and an id that names no row,** now answer `404 RECORD_NOT_FOUND`: the read door's answer for that object and id, which is what a by-id write of any other object already answers. Both used to answer `403 PERMISSION_DENIED` ("the assignment row … could not be read"). An organization administrator's edit in place is asked the same question first, so an edit of a row it reads is still refused as an edit. + - **A caller the read door refuses outright** (no read grant on the table) still gets that read refusal. + - **A refusal names a value only when the caller can read it.** A delegated administrator's `DELEGATION_SCOPE_VIOLATION` names the row's position, business unit, holder or permission set only when the caller's own read returns that column at its stored value, or when the caller wrote the value itself. Otherwise the sentence is the same without the value (for example "the assignment's position distributes a permission set which is not in the scope's allowlist"), and `details` leaves the value out. The verdict does not change: it is still taken on the stored row. + - **Unchanged.** A row the caller reads keeps its verdict and its guidance. Inserts and tenant-level administrators are judged as before, and every write that was admitted is still admitted. + + For clients: a by-id update or delete of an assignment row that is gone now answers `404`, as it does on every other object. +- 70981e6: fix(plugin-security): a by-id write's row re-read propagates a store fault, and judges the platform's own predicates without the caller-predicate guard + + Clause-②: yes (widening) + Metadata-protocol: none + + Before an update or delete by id, the security middleware re-reads the addressed row under the caller's context with the platform's write predicates for that row (the tenancy wall and the row-level write predicates). Two rules hold for that re-read, and the second for the controlled-by-parent master read a detail write asks as well: + + - **A store fault propagates as raised.** A re-read the store cannot answer (an unavailable datasource, a driver failure) is the write's answer, unchanged — for `ERR_DATASOURCE_UNAVAILABLE` the transports answer `503` — and nothing is written. Only a declared refusal (a 4xx) keeps the record-level `403 PERMISSION_DENIED`, one answer whatever the id names. The controlled-by-parent master read keeps its `403` for every failure. + - **The platform's own predicates are not the caller's predicate.** The field-level predicate guard refuses a caller's filter on a field its field rules do not let it read, and judges only the caller's predicate: a predicate subtree the platform composed and marked as its own is not judged. That holds for every such subtree that reaches the guard inside a caller-context read — the by-id write re-read and the controlled-by-parent master read, the analytics read-scope merge handed to an aggregate under the caller's context, and a record-sharing read scope composed before the security middleware. Only platform-composed predicates are exempt; nothing the caller writes is. Under the `group` and `isolated` tenancy postures, a caller whose field rules hide the organization column updates and deletes the records of its organization by id, writes a controlled-by-parent detail under a master of its organization, and aggregates over the records of its organization. A record of another organization and an id that names no record answer that caller exactly as they answer any other caller. + + Unchanged: a caller's own filter on a field it cannot read is refused, on a read, an analytics query and a predicate write; a record the caller reads but may not write keeps its `403`; a system write is not the gate's. +- 42e1211: fix(plugin-security): a delegated administrator's refused assignment write answers the same for a row it cannot read as for a missing one + + Clause-②: no + Metadata-protocol: none + + A delegated administrator — a caller holding an `adminScope`, an organization administrator who holds one included — updates or removes an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. The delegated-administration gate (ADR-0090 D12) now asks first whether the caller itself reads that row, in its own context. + + - **A row the caller cannot read** is answered exactly as an id that names no row is: the read door's `404 RECORD_NOT_FOUND` for that object and id. Both are answered at the gate, before any later step judges the write. + - **A row the caller reads** keeps the verdict and the guidance it had: the gate judges the stored row, as before. + - **A caller the read door refuses outright** (no read grant on the table) is answered as before: the read refusal, for every id the gate refuses, and the writes it was admitted are still admitted. + - **Unchanged.** Inserts, tenant-level administrators, and organization administrators who hold no scope are judged as before. +- 72d7db0: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(plugin-security): an organization administrator's refused assignment write answers the same for a row it cannot read as for a missing one + + Clause-②: no + Metadata-protocol: none + + An organization administrator (`organization_admin` or `organization_admin_no_bypass`) removes an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. The delegated-administration gate (ADR-0090 D12) now asks the administrator's own read, in its own context, whether that row is there to remove. + + - **The removal of a row the administrator cannot read** is answered exactly as the removal of an id that names no row is: the read door's `404 RECORD_NOT_FOUND` for that object and id. + - **A row the administrator can read** is judged on the stored row. A refusal names the organization boundary, and names the row's organization, position or permission set only when the administrator's own read returns that value as stored. + - **Unchanged.** An edit in place of a row it reads is refused as before, every removal that was admitted is still admitted, and a tenant-level administrator is judged as before. +- d0da880: fix(plugin-security): an organization administrator's assignment removal is judged on the stored row, and its refusal names only values the caller can read + + Clause-②: no + Metadata-protocol: none + + An organization administrator (`organization_admin` or `organization_admin_no_bypass`) removes an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. Once its own read returns the row, the delegated-administration gate (ADR-0090 D12) judges the removal on the stored row, as it judges a delegated administrator's write. + + - **The verdict is the stored row's.** A field rule or a masking rule on the row's `position` or `permission_set` does not change it: the removal of a row inside the organization boundary is admitted, and the removal of a row outside it is refused. + - **A refusal names a value only when the caller can read it.** The row's organization, its position and the sets that position carries, or its permission set are named only when the caller's own read returned that column at its stored value. Otherwise the sentence leaves the value out, for example "the assignment's position is not inside the organization boundary", and `details` leaves it out too. + - **Unchanged.** A row the caller cannot read, and an id that names no row, still answer the read door's `404 RECORD_NOT_FOUND`. An edit in place is refused as before. Inserts, tenant-level administrators and delegated administrators are judged as before. +- d7c0389: fix(plugin-security)!: a write from a caller the read door refuses is refused, whatever rows it addresses + + Clause-②: no (narrowing) — a predicate update or delete (`multi: true`, addressed by a `where`) from a caller whose read of the object is refused is refused with that read refusal before the write runs, where it was admitted before; nothing is widened. + Metadata-protocol: none + + + + **BREAKING** for a caller whose read of an object is refused while its write is admitted, and that updates or deletes that object's records by predicate. With the permission model's own grants that caller exists in one shape only: an object whose `requiredPermissions` map gates `read` and not `update` or `delete` (ADR-0066 ⑤), and a caller holding the write grant without the capability the read requires. + + - **The rule.** A caller the read door refuses writes none of the object's records, by id or by predicate. The predicate update and delete such a caller addresses — a flow's `update_record` or `delete_record` node with `multi: true` run as that user, or an engine `update` / `delete` with `multi: true` under that caller's context — now answer the read door's own refusal (the `PERMISSION_DENIED` envelope that caller's read gets), and nothing is written. The by-id update and delete are refused too. + - **Unchanged.** A caller who reads the object keeps its predicate write, scoped to the records it reads. A caller the object's CRUD grants refuse is refused as before. A write the platform issues under the caller's context (a hook's or a cascade's own write, the referential clear of a deleted record's lookups) and a system write keep their answers. A store fault on the read propagates as before. + - **Who moves.** A deployment that relies on such a caller writing by predicate grants it the capability the object's read requires, or gates the write verbs on a capability that caller holds. +- 6dd2ca5: fix(plugin-security, plugin-auth)!: a person-to-permission-set assignment refuses an agent ceiling set at every write door, and so does the self-registration permission set + + Clause-②: no (narrowing) — a non-system insert or update of `sys_user_permission_set` that would store an assignment naming one of the three MCP agent ceiling sets (`mcp_agent_data_read`, `mcp_agent_data_write`, `mcp_agent_restricted`) is refused with `400 VALIDATION_FAILED` (`invalid_value` at `permission_set`), for every caller, a platform administrator included; and an audience configuration whose `selfRegistrationPermissionSet` names one is refused (`[audience] invalid audience configuration`), at construction and through the auth settings; nothing is widened. + Metadata-protocol: none + + + + **BREAKING** for a deployment whose administrators assign an MCP agent ceiling set to users through the permission-set assignment table, or whose audience names one as `selfRegistrationPermissionSet`. + + - **The rule (ADR-0090 D10).** The agent ceiling sets are an AI agent's own grants. The door that admits an agent connected over MCP derives its ceiling from the scopes its user consented to and puts it on the agent's request; no assignment row is read for it. A `sys_user_permission_set` row is a person's grant, so it never names one. Every door that writes the table refuses such a row: the data API's create and update, `createMany`, `updateMany`, the per-object and cross-object batch, and both import doors (the row fails with `code: 'invalid_value'` at `permission_set`). An update that keeps an assignment naming one in force (a new holder, a validity window, an echo of the set) is refused too. + - **The self-registration set (`@objectstack/plugin-auth`).** A self-registrant is a person, so the audience configuration that names the set every self-registrant receives refuses an agent ceiling set, beside its existing refusal of `admin_full_access`: `auth.audience.selfRegistrationPermissionSet` in the stack config stops the boot, and `audience_self_registration_permission_set` in the auth settings is refused while the standing audience keeps ruling. Both answer `[audience] invalid audience configuration: selfRegistrationPermissionSet must not be '...'`. + - **Unchanged.** Every other permission set stays assignable, and removing an assignment stays open. A system write is not judged: the platform's fixed-name grant writers name no ceiling, the self-registration grant writes only the validated set, and seed replay and the development-only demo-staff seed write the sets their stack declares. An agent connected over MCP holds its ceiling exactly as before. An organization administrator is still refused the same sets on the organization boundary first (`403 PERMISSION_DENIED`). + - **Who moves, and the fix.** An assignment stored before this release that names an agent ceiling set is left as it is, and can no longer be edited: delete it (`DELETE /api/v1/data/sys_user_permission_set/ID`), or rename it to a permission set meant for people. List them with `GET /api/v1/data/sys_user_permission_set?permission_set=mcp_agent_data_read` and the same query for the other two names. An audience that names an agent ceiling set as `selfRegistrationPermissionSet` declares a permission set meant for people instead (`member_default`, or the deployment's own). +- 917f024: fix(plugin-security)!: an external object that states no organization scope reads as empty for every caller, the platform owner included + + Clause-②: no (narrowing) — under the `isolated` and `group` postures, an external object registered after the boot that states no organization scope reads as empty for every caller, the declared platform owner included; nothing is widened, and the `single` posture is unchanged. + Metadata-protocol: none + + + + **BREAKING** for a deployment running the `isolated` or `group` tenancy posture (`OS_TENANCY_POSTURE`) that registers an external object after the boot without stating its organization scope. + + - **The rule (ADR-0015 §10 as amended).** Whose rows an external object serves is a fact only its author can state. Under a walled posture an external object that states none is refused by name when the boot completes, and one registered after the boot — a Studio import, a runtime datasource — reads as empty for every caller, the platform owner included, and accepts no write, until it states its scope. That deny judges the object's configuration, not the reader's organization. + - **What is unchanged.** The declared platform owner's reads still cross the organization wall everywhere else: on local objects, on an external object that declares its own `organization_id`, and in a session with no active organization. Writes stay walled for everyone. A member's reads are unchanged. + - **Who moves, and the fix.** Declare one of the three on the external object, as the ADR-0087 entry `external-object-organization-scope-declared` prescribes: an `organization_id` field of its own (the rows are then walled by organization), `tenancy: { enabled: false }` (shared by every organization), or `systemFields: { tenant: false }` (the same opt-out on the system fields). +- bdbaf80: fix: Setup's form help speaks to an administrator, and the queue pages, the mail status banner and the test-email toast read in Chinese on a zh-CN console + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Field help (English source and zh-CN):** the permission set and position assignment forms, the out-of-office (approval delegation) rule, and the business unit, organization and user forms no longer describe the implementation. Gone: "fail-closed", "half-open [from, until)", "agent grants", "graph semantics", "expanded by graph queries", "Null = root of tenant", "HRIS", "When true / When false" (zh-CN 「为 true 时」 now reads 「开启后」) and an HTTP status code. The help now says when an assignment starts and ends, what turning a switch on does, and what to fill in. Text only: no field name, key, option value or behaviour changes. + - **`@objectstack/plugin-sharing`:** `sys_queue` and `sys_queue_member` join the plugin's translation bundle, with zh-CN text, so the 队列 and 队列成员 pages, their columns, help and the 新建 button read in Chinese. ja-JP and es-ES carry English fills for now. + - **`@objectstack/plugin-email`:** the live-status statement at the top of Setup → Email and the "Send test email" answer (sent, failed, logged only) are written in the caller's display language, en and zh-CN today; another language gets English. + - **`@objectstack/service-settings`:** a settings action handler (`SettingsActionHandler`) receives an optional `locale`, the caller's display language read from the request the same way the live-status statement's is (`Accept-Language`, else `?locale`); `runAction` takes it as a fifth `display` argument. Existing handlers compile and behave unchanged. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [d9d42d4] +- Updated dependencies [67d5d53] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [60fe44f] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [0e4747a] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-security/package.json b/packages/plugins/plugin-security/package.json index f13edcb9030..1e027afcede 100644 --- a/packages/plugins/plugin-security/package.json +++ b/packages/plugins/plugin-security/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-security", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Security Plugin for ObjectStack — RBAC, RLS, and Field-Level Security Runtime", "type": "commonjs", diff --git a/packages/plugins/plugin-sharing/CHANGELOG.md b/packages/plugins/plugin-sharing/CHANGELOG.md index f3641f6cc16..3d7474daa02 100644 --- a/packages/plugins/plugin-sharing/CHANGELOG.md +++ b/packages/plugins/plugin-sharing/CHANGELOG.md @@ -1,5 +1,154 @@ # @objectstack/plugin-sharing +## 18.4.0 + +### Patch Changes + +- 13c0c5b: fix(plugin-approvals)!: an approval node refuses to open when a team or position approver's directory read fails, or a queue approver's expansion rejects, and the ownership-queue reads page to completion + + Clause-②: yes (narrowing) — an approval node no longer opens when a `team` or `position` approver (static, or an `expression` approver with `resolveAs` `team` / `position`) cannot be expanded because a directory read or the cross-organization read filter failed, or when a `queue` approver's expansion rejects. Under every `onEmptyApprovers` policy, the `fallbackApprovers` slate included, it now fails the node with a retryable `APPROVER_DIRECTORY_UNREADABLE:` error naming the read, where it used to open on the rest of the slate, on the fallback slate or on the administrators, or auto-approve. The registered ownership-queue service does not reject yet: a failed member read inside it still answers nobody until #29604 part 2 (#30251), so a `queue` approver refuses here only when `expandMembers` itself rejects. Every other input answers as before. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at the approval node (`@objectstack/plugin-approvals`): a failed directory read behind a `team` or `position` approver, or a `queue` approver whose expansion rejects, refuses the node instead of opening it on a slate that is missing people (ADR-0142 D6 as amended, ruling record 6599731698 on #29604). + + - **Before.** A `team` or `position` approver whose `sys_team_member`, `sys_user_position` or `sys_member` read failed kept its `type:value` literal slot, which nobody can act on, and logged the failure. A `queue` approver whose expansion threw answered "expanded to nobody" and added no slot. Either way the node went on as if those people did not exist: beside other approvers it opened on them alone, and on an otherwise empty slate the node's `onEmptyApprovers` policy acted, opening on the `fallbackApprovers` or the organization's administrators, or auto-approving. + - **Now.** The node does not open and writes no request, approver or action row. It fails with an `ApproverDirectoryUnreadableError` whose message starts `APPROVER_DIRECTORY_UNREADABLE:`, names the approver, the organization and the read that failed (a failure of the cross-organization read filter is named as that filter's read, on a static approver and on an `expression` one alike), and is retryable: the flow node's failure is a runtime one, so the run can be resumed once the directory can be read. A `fallbackApprovers` slate whose own `team` or `position` read fails refuses the same way. `department` / `business_unit`, `org_membership_level` and `manager` approvers are not named by the ruling and answer exactly as before. + - **Unchanged.** A decision on a request opened before the approver snapshot existed still re-reads its slate on the old path, which keeps the literal slot on a failed read, because the decision has already been recorded when that read happens. + - **Not yet.** The ownership-queue service shipped in `@objectstack/plugin-sharing` answers a failed `sys_queue` / `sys_queue_member`, team or position read with nobody (logged once) rather than rejecting, so a `queue` approver backed by it still adds no slot on such a failure. Its refusal arrives with #29604 part 2, once the contract can carry it (#30251). + + `@objectstack/plugin-sharing`: the ownership-queue service reads its `sys_queue` and `sys_queue_member` rows by id, page after page, to the end, where it read one page of 10,000 rows and answered on it. A queue's 10,001st member, an organization's 10,001st queue and the queues naming a recipient past that bound are now found. A read that fails partway answers as a failed read did before (that read contributes nobody, logged once, with the read's reason and without the walk's own "refuses instead" clause), never with the pages it had read. +- fe835b4: fix(plugin-sharing)!: the ownership-queue service refuses a failed member read when the caller asks, and the approver slate and the sharing-recipient grant recompute ask + + Clause-②: yes (narrowing) — `expandMembers` and `isMember` of the registered ownership-queue service, called with `onFailedRead: 'refuse'`, now reject when a read behind the queue fails, where they answered nobody. The approver slate asks for that answer, so an approval node whose `queue` approver cannot be read refuses to open under every `onEmptyApprovers` policy, where it opened on the rest of the slate, on the fallback slate or on the administrators, or auto-approved. The sharing-rule recompute asks for it too, so a whole-rule pass (`evaluateRule`, the REST evaluate endpoint, the boot backfill), the recipient-axis revoke and the per-user pass on a `queue`-recipient rule reject on such a failure, where they completed by revoking the grants of the members who could not be read. Every call without the option, and every read that succeeds, answers as before. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at the ownership-queue service's `'refuse'` answer and at its two recompute consumers (`@objectstack/plugin-sharing`, `@objectstack/plugin-approvals`): an empty expansion is a fact, a failed read is a fault (ADR-0142 D2 as amended), and the approver slate (D6) and the sharing-recipient grant recompute (D7) now get the fault instead of "nobody". + + - **The service.** `OwnershipQueueService.expandMembers(name, org, { onFailedRead: 'refuse' })` and `isMember(user, name, org, { onFailedRead: 'refuse' })` reject when the queue's `sys_queue` row, its `sys_queue_member` rows, or a member table one of its specifications reads (`sys_team_member`, `sys_user_position`, `sys_member`, `sys_business_unit_member`, `sys_business_unit`) cannot be read, or a paged read cannot vouch for completion. The rejection's message starts `[queue] queue '' in organization '' does not answer its members, because a read behind it failed:` and names that read; the read's own error rides along as `cause`. It never answers `[]`, a partial set or `false` instead. `isMember` still answers `true` when a read that succeeded reaches the user, and answers `false` only when every read behind it succeeded. An empty queue is still `[]`, and a name that resolves nowhere is still `[]` / `false`. + - **Unchanged: the default.** Without the option (`onFailedRead: 'nobody'`), a failed read still contributes nobody, logged once. The sitting-record access path (`queuesOf`), the claim admission and the assignment pool take that answer, so they deny on a failed read exactly as before. A failed read of the activation ledger (`sys_metadata_activation`) is not a failed read in this sense under either answer: it still reads "switched on" for that call, logged, the posture the sharing-rule service takes for its own position recipients. + - **The approver slate (D6).** A `queue` approver asks with `onFailedRead: 'refuse'` beside the addressing purpose. When the registered service rejects, the node fails with the retryable `APPROVER_DIRECTORY_UNREADABLE:` error that a failed team or position read already raises, naming the queue's failed read, and writes no request, approver or action row. Before, the registered service answered nobody, so the queue added no slot and the node's `onEmptyApprovers` policy acted on a slate missing those people. + - **The sharing-recipient grant recompute (D7).** A `queue`-recipient rule's expansion and its per-user membership check ask with `onFailedRead: 'refuse'`. A failed member read now rejects before any grant is diffed, so the rule's existing grants stay as they were until a pass can read the queue, and each recompute's caller logs it: the record pass and the object pass per rule (`record reconcile failed for rule`, `object reconcile failed for rule`), the queue-membership and business-unit write recomputes (`queue-membership revocation failed`, `queue-membership re-grant failed`, `business-unit revocation failed`, `business-unit re-grant failed`), the membership-row recompute (`recipient membership re-evaluation failed`) and the boot backfill (`boot rule backfill failed for rule`). A direct call to `evaluateRule`, `revokeRuleGrantsForRetiredRecipients` or `reconcileRuleForUser`, the REST evaluate endpoint included, rejects with the service's failure. Before, the failed read answered nobody and the pass revoked the grant of every member behind it. This is the answer a `team` or `position` recipient has given since its own reads began to refuse. + - **Supersedes the approval node's "Not yet".** The approval node's own entry in this release ("an approval node refuses to open when a team or position approver's directory read fails…") says, in its `Clause-②:` line and its **Not yet** bullet, that the registered ownership-queue service answers a failed member read with nobody rather than rejecting, so a `queue` approver backed by it refuses only when `expandMembers` itself rejects. That was true when it was written and is not after this change: the service now rejects on that failure when asked, and the slate asks, so a `queue` approver backed by it refuses on a failed `sys_queue`, `sys_queue_member`, team, position or business-unit read, as a `team` or `position` approver does. Read this bullet in place of those two sentences. +- bdbaf80: fix: Setup's form help speaks to an administrator, and the queue pages, the mail status banner and the test-email toast read in Chinese on a zh-CN console + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Field help (English source and zh-CN):** the permission set and position assignment forms, the out-of-office (approval delegation) rule, and the business unit, organization and user forms no longer describe the implementation. Gone: "fail-closed", "half-open [from, until)", "agent grants", "graph semantics", "expanded by graph queries", "Null = root of tenant", "HRIS", "When true / When false" (zh-CN 「为 true 时」 now reads 「开启后」) and an HTTP status code. The help now says when an assignment starts and ends, what turning a switch on does, and what to fill in. Text only: no field name, key, option value or behaviour changes. + - **`@objectstack/plugin-sharing`:** `sys_queue` and `sys_queue_member` join the plugin's translation bundle, with zh-CN text, so the 队列 and 队列成员 pages, their columns, help and the 新建 button read in Chinese. ja-JP and es-ES carry English fills for now. + - **`@objectstack/plugin-email`:** the live-status statement at the top of Setup → Email and the "Send test email" answer (sent, failed, logged only) are written in the caller's display language, en and zh-CN today; another language gets English. + - **`@objectstack/service-settings`:** a settings action handler (`SettingsActionHandler`) receives an optional `locale`, the caller's display language read from the request the same way the live-status statement's is (`Accept-Language`, else `?locale`); `runAction` takes it as a fifth `display` argument. Existing handlers compile and behave unchanged. +- Updated dependencies [31fe289] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [cd5f554] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [343ddb2] +- Updated dependencies [0c666c3] +- Updated dependencies [f672e0d] +- Updated dependencies [38717d3] +- Updated dependencies [bfbdeec] +- Updated dependencies [4adc890] +- Updated dependencies [bcdf8d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [a26abbc] +- Updated dependencies [d9d42d4] +- Updated dependencies [67d5d53] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [8cc709d] +- Updated dependencies [60fe44f] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [7b80724] +- Updated dependencies [fb1ab89] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [0e4747a] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [63de248] +- Updated dependencies [a2437b5] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7193160] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/objectql@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-sharing/package.json b/packages/plugins/plugin-sharing/package.json index f3168ed6d37..425c4d6af22 100644 --- a/packages/plugins/plugin-sharing/package.json +++ b/packages/plugins/plugin-sharing/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-sharing", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Record-level sharing for ObjectStack — sys_record_share + middleware that enforces sharingModel + ISharingService.", "type": "commonjs", diff --git a/packages/plugins/plugin-webhooks/CHANGELOG.md b/packages/plugins/plugin-webhooks/CHANGELOG.md index e8ce0c50d4a..ab30616f4d1 100644 --- a/packages/plugins/plugin-webhooks/CHANGELOG.md +++ b/packages/plugins/plugin-webhooks/CHANGELOG.md @@ -1,5 +1,103 @@ # @objectstack/plugin-webhooks +## 18.4.0 + +### Patch Changes + +- 08e0247: i18n(plugin-webhooks): the webhook object's labels, help, options and views ship in all eleven platform languages + + Clause-②: no + Metadata-protocol: none + + `sys_webhook` now carries Traditional Chinese (`zh-Hant`), Arabic (`ar`), French (`fr`), Brazilian Portuguese (`pt-BR`), Russian (`ru`), German (`de`) and Korean (`ko`) beside the existing `en`, `zh-CN`, `ja-JP` and `es-ES`, so an administrator reading Setup in one of those languages sees the webhook screens in their own language instead of English. Each new locale is registered under its platform-table tag with its source-hash companion, and the package's extract command lists the seven tags, so the existing `check:i18n` bundle check covers them. Part of #30512. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [06cccd3] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [a0c68ae] +- Updated dependencies [ede9e6b] +- Updated dependencies [ca37605] +- Updated dependencies [cabdfe8] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/service-messaging@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-webhooks/package.json b/packages/plugins/plugin-webhooks/package.json index 15e99c53e9a..7cd7963105e 100644 --- a/packages/plugins/plugin-webhooks/package.json +++ b/packages/plugins/plugin-webhooks/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-webhooks", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Persistent, cluster-aware webhook dispatcher. Durable outbox + per-partition cluster.lock for exactly-once-ish delivery across nodes. See content/docs/concepts/webhook-delivery.mdx.", "type": "module", diff --git a/packages/qa/dogfood/CHANGELOG.md b/packages/qa/dogfood/CHANGELOG.md index 0f77840187c..8b59f759663 100644 --- a/packages/qa/dogfood/CHANGELOG.md +++ b/packages/qa/dogfood/CHANGELOG.md @@ -1,5 +1,191 @@ # @objectstack/dogfood +## 0.0.52 + +### Patch Changes + +- Updated dependencies [31fe289] +- Updated dependencies [a35fc66] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [13c0c5b] +- Updated dependencies [fe835b4] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [5cdaddb] +- Updated dependencies [509c40a] +- Updated dependencies [c23501b] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [665db00] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [cd5f554] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [cfb602c] +- Updated dependencies [171b6dd] +- Updated dependencies [343ddb2] +- Updated dependencies [0c666c3] +- Updated dependencies [f672e0d] +- Updated dependencies [38717d3] +- Updated dependencies [bfbdeec] +- Updated dependencies [4adc890] +- Updated dependencies [bcdf8d3] +- Updated dependencies [59bcd22] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [a545a04] +- Updated dependencies [25fea77] +- Updated dependencies [06cccd3] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [8f05e61] +- Updated dependencies [9afcd6a] +- Updated dependencies [870ebce] +- Updated dependencies [d1d90b7] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [db04e5c] +- Updated dependencies [a26abbc] +- Updated dependencies [d9d42d4] +- Updated dependencies [67d5d53] +- Updated dependencies [47226e6] +- Updated dependencies [a545a04] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [8589756] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [a93ef3e] +- Updated dependencies [0e4dfbe] +- Updated dependencies [8cc709d] +- Updated dependencies [b5765be] +- Updated dependencies [5a878a3] +- Updated dependencies [60fe44f] +- Updated dependencies [22f0d65] +- Updated dependencies [fabd259] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [26c2efd] +- Updated dependencies [f7afab8] +- Updated dependencies [7b80724] +- Updated dependencies [fb1ab89] +- Updated dependencies [6c4c43a] +- Updated dependencies [455b4f0] +- Updated dependencies [4dde0ae] +- Updated dependencies [94cef75] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [ae5bd5b] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [135c2fb] +- Updated dependencies [898145b] +- Updated dependencies [0e4747a] +- Updated dependencies [a0c68ae] +- Updated dependencies [ede9e6b] +- Updated dependencies [c5101a0] +- Updated dependencies [08e0247] +- Updated dependencies [ca37605] +- Updated dependencies [cabdfe8] +- Updated dependencies [66a8822] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [63de248] +- Updated dependencies [c35fed1] +- Updated dependencies [a2437b5] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [474c030] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [70981e6] +- Updated dependencies [85c7374] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [42e1211] +- Updated dependencies [7193160] +- Updated dependencies [72d7db0] +- Updated dependencies [d0da880] +- Updated dependencies [7b2bb66] +- Updated dependencies [30dcd8a] +- Updated dependencies [c92b887] +- Updated dependencies [21510bb] +- Updated dependencies [d7c0389] +- Updated dependencies [6dd2ca5] +- Updated dependencies [917f024] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [a7293f7] +- Updated dependencies [59e6463] +- Updated dependencies [63a470a] +- Updated dependencies [4851668] + - @objectstack/objectql@18.4.0 + - @objectstack/service-analytics@18.4.0 + - @objectstack/service-messaging@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/runtime@18.4.0 + - @objectstack/plugin-approvals@18.4.0 + - @objectstack/plugin-sharing@18.4.0 + - @objectstack/verify@18.4.0 + - @objectstack/service-datasource@18.4.0 + - @objectstack/plugin-email@18.4.0 + - @objectstack/plugin-auth@18.4.0 + - @objectstack/mcp@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/plugin-audit@18.4.0 + - @objectstack/plugin-security@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/service-storage@18.4.0 + - @objectstack/plugin-webhooks@18.4.0 + - @objectstack/example-crm@4.0.104 + - @objectstack/example-showcase@0.3.26 + - @objectstack/connector-mcp@18.4.0 + - @objectstack/plugin-pinyin-search@18.4.0 + - @objectstack/trigger-record-change@18.4.0 + - @objectstack/example-multi-package@0.0.11 + - @objectstack/connector-openapi@18.4.0 + - @objectstack/connector-rest@18.4.0 + - @objectstack/metadata@18.4.0 + - @objectstack/trigger-schedule@18.4.0 + - @objectstack/types@18.4.0 + ## 0.0.51 ### Patch Changes diff --git a/packages/qa/dogfood/package.json b/packages/qa/dogfood/package.json index 8f6f446be0d..b3bad7afc2e 100644 --- a/packages/qa/dogfood/package.json +++ b/packages/qa/dogfood/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/dogfood", - "version": "0.0.51", + "version": "0.0.52", "private": true, "license": "Apache-2.0", "description": "Dogfood regression gate — hand-written golden tests that boot real example apps through @objectstack/verify's in-process HTTP stack, pinning historical runtime regressions (#2018 timezone bucketing, #1994 cross-owner RLS, #2004 field fidelity) that static checks miss.", diff --git a/packages/qa/downstream-contract/CHANGELOG.md b/packages/qa/downstream-contract/CHANGELOG.md index f5a57b24fbc..0054c85201a 100644 --- a/packages/qa/downstream-contract/CHANGELOG.md +++ b/packages/qa/downstream-contract/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/downstream-contract +## 0.0.50 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 0.0.49 ### Patch Changes diff --git a/packages/qa/downstream-contract/package.json b/packages/qa/downstream-contract/package.json index f62ddf6c174..37a69bcbde0 100644 --- a/packages/qa/downstream-contract/package.json +++ b/packages/qa/downstream-contract/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/downstream-contract", - "version": "0.0.49", + "version": "0.0.50", "description": "Frozen third-party consumer fixture — a backward-compatibility gate for @objectstack/spec. Authored the way an external project on a published release authors metadata; if a spec change breaks it, that change is breaking (#2035).", "license": "Apache-2.0", "private": true, diff --git a/packages/qa/http-conformance/CHANGELOG.md b/packages/qa/http-conformance/CHANGELOG.md index f30ae9d33d2..604f2f04857 100644 --- a/packages/qa/http-conformance/CHANGELOG.md +++ b/packages/qa/http-conformance/CHANGELOG.md @@ -1,5 +1,24 @@ # @objectstack/http-conformance +## 0.1.12 + +### Patch Changes + +- Updated dependencies [9ebca94] +- Updated dependencies [771b2db] +- Updated dependencies [f19fbb8] +- Updated dependencies [aa57c76] +- Updated dependencies [2046d21] +- Updated dependencies [f7afab8] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [1c5cee6] +- Updated dependencies [af8aab1] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [21510bb] + - @objectstack/core@18.4.0 + ## 0.1.11 ### Patch Changes diff --git a/packages/qa/http-conformance/package.json b/packages/qa/http-conformance/package.json index 7c5e1c36fb8..df3d20cd4b7 100644 --- a/packages/qa/http-conformance/package.json +++ b/packages/qa/http-conformance/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/http-conformance", - "version": "0.1.11", + "version": "0.1.12", "private": true, "license": "Apache-2.0", "description": "HTTP transport-port conformance gate (ADR-0076 D11/OQ#10, #2462) — a zero-dependency node:http reference implementation of IHttpServer plus a cross-adapter suite that boots the dispatcher bridge and REST generator on it AND on plugin-hono-server, pinning that the port stays free of framework-isms. Not published; validation instrument, not a product server.", diff --git a/packages/qa/scenarios/CHANGELOG.md b/packages/qa/scenarios/CHANGELOG.md new file mode 100644 index 00000000000..c4553681554 --- /dev/null +++ b/packages/qa/scenarios/CHANGELOG.md @@ -0,0 +1,115 @@ +# @objectstack/scenarios + +## 0.1.1 + +### Patch Changes + +- 58085ff: Clause-②: no + Metadata-protocol: none + + New private package `@objectstack/scenarios` (`packages/qa/scenarios`, not published): the zod schema and loader for scenario cards, plus the open library's three control cards under `docs/qa/scenarios/`. A card states an authoring prompt, the structural `expect` the deterministic scorer reads, capability tags (area half closed to the platform-checklist areas) and a closed category, seed rows with data questions whose truth the loader computes from a declarative spec (`count | sum | avg | min | max | topN | unanswerable`, with filters, relative windows against a card-level `asOf`, relation paths through declared lookups, groupBy and order), personas with row scope whose per-persona truths are computed too, write operations that declare their outcome (post-state, refusal code or pending confirmation, with the audit actor), five guardrail checks, optional agent actions, and one iteration request. A malformed card is refused with its file and field path. No `packages/spec` change and no public surface change. +- 6ef7f58: feat(scenarios): the deterministic scenario scorer lives in `@objectstack/scenarios` and judges capability tags, agent-action exposure, write outcomes and per-persona answers against a loaded card + + Clause-②: no + Metadata-protocol: none + + - **`@objectstack/scenarios/score`** (private, unpublished): the entry the scenario runner calls. `scoreCardArtifact(card, artifact)` scores a built `dist/objectstack.json`; `gradeCardOperation(card, operationId, observation)` judges an observed write against its declared outcome; `gradeCardAnswer(card, { question, persona?, turn })` grades an agent's answer against the loader's computed truth for that persona. Each takes a card the loader returned, and the graders read the card schema's own types. + - **The scorer moved here from `@objectstack/cli`.** That covers the structural shape score, the artifact-to-graph reader and the Run grader, which were never released from the CLI. It now also carries: + - **capability-tag checks**: a closed table of nine tags, listed in the package README. A tag outside it, or a row with nothing in the card to judge, is reported `unscored` and counted nowhere; + - **agent-action exposure**: each declared action exists on its object, carries `ai.exposed: true` and, where the card says it asks first, `ai.requiresConfirmation: true`. A missing action fails; + - **a write-operation grader**: `applied`, `refused` or `pending_confirmation`. An unobserved fact fails; + - **per-persona Run grading**: a `withheld` truth is must-not-reveal, and an all-rows figure outside a scoped persona's rows is a leak. + - **`@objectstack/cli`**: `MetadataEvalCase` is now declared in `corpus.ts`, beside the golden corpus it types, and `metadata-eval.ts` re-exports it under the same name. No behaviour change, no new flag and no new package-entry export. The CLI carries no scenario code. + + No model call anywhere. +- Updated dependencies [31fe289] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [13c0c5b] +- Updated dependencies [fe835b4] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [cd5f554] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [343ddb2] +- Updated dependencies [0c666c3] +- Updated dependencies [f672e0d] +- Updated dependencies [38717d3] +- Updated dependencies [bfbdeec] +- Updated dependencies [4adc890] +- Updated dependencies [bcdf8d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [a26abbc] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [8cc709d] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [7b80724] +- Updated dependencies [fb1ab89] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [63de248] +- Updated dependencies [a2437b5] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [7193160] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/objectql@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/plugin-sharing@18.4.0 + - @objectstack/core@18.4.0 diff --git a/packages/qa/scenarios/package.json b/packages/qa/scenarios/package.json index 2fdbbe621d2..548285ea699 100644 --- a/packages/qa/scenarios/package.json +++ b/packages/qa/scenarios/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/scenarios", - "version": "0.1.0", + "version": "0.1.1", "private": true, "license": "Apache-2.0", "description": "Scenario-card schema and loader for the open scenario library under docs/qa/scenarios/. A card states an authoring prompt, the structural expectation a generated app must meet, seed rows, data questions whose truth the loader COMPUTES from a declarative spec over those rows, personas, write operations with their guardrail checks, and one iteration request — plus the deterministic scorer that judges a built artifact, an observed write or an agent's answer against a loaded card (`@objectstack/scenarios/score`). Not published, no build step, no model calls: a devx instrument, not a protocol (packages/spec carries no obligation for this format).", diff --git a/packages/rest/CHANGELOG.md b/packages/rest/CHANGELOG.md index a0c78d34dad..37ad91c60f3 100644 --- a/packages/rest/CHANGELOG.md +++ b/packages/rest/CHANGELOG.md @@ -1,5 +1,207 @@ # @objectstack/rest +## 18.4.0 + +### Minor Changes + +- fdad178: fix(metadata-protocol,rest): a `.custom` pair's declared override of a page, dataset, app, book, dashboard, view or report is labelled in the ruled order on every `/meta` read — the pair's own translation, then the override's own label, then the base's translation (#30062) + + Clause-②: yes (widening) — `ObjectStackProtocolImplementation` gains six public methods: `getPackagedPageBase`, `getPackagedDatasetBase`, `getPackagedAppBase`, `getPackagedBookBase`, `servedOverrideBaseOf` and `getPairTranslationBundle` (measured on `packages/metadata-protocol/dist/index.d.ts`); `@objectstack/rest`'s exported `MetaBookTreeSources` accepts one more optional key, `resolveProtocol` (measured on `packages/rest/dist/index.d.ts`). No accepted key, value or answer is taken away. + Metadata-protocol: semantic + + - **Now.** A changed label on a declared override reads the pair's own translation of that key when the pair ships one, else the override's own text, in every locale, on `GET /meta/:type/:name`, `GET /meta/:type` and `GET /meta/book/:name/tree`. An unchanged label still reads the base's translation. Before, a page, dataset, app or book override read the base's translation over its own changed label, and no type read the pair's translation first. + - **The pair's translations.** They are the pair's own active `translation` rows (`package_id` = the pair). A `.custom` package has no code bundle, so it has no code translation source. A package-less authored row is in the deployment catalog but is not the pair's, so it does not rank first. + - **Object.** An object is extended, never replaced (ADR-0146 D10), so it has no declared override and no pair bundle: seven types take one. + - **The book tree.** `GET /meta/book/:name/tree` now judges a book against its packaged base as the list does, so the docs sidebar and the book list serve the same label (#26690). +- fb1ab89: fix(objectql,rest)!: an object extension that renames `label` and sets no `pluralLabel` is served without the base's plural, so list titles stop showing the old name (#30391) + + Clause-②: no (narrowing) — `/meta/object` (item and list reads, both transports) and every registry read of an object (`SchemaRegistry.getObject`, the fold) stop serving `pluralLabel` for an object an extension renames without a plural of its own, where they served the base's plural (and, in a translated locale, the base's translation of it). The shipped showcase's `showcase_account` is one: its extension renames it `Account (Success Overlay)` and declares no plural. Nothing an author writes is refused. + Metadata-protocol: semantic + Skills-docs: follow-up — `skills/objectstack-data/SKILL.md` "Object Extension Model" says an extension "can add but never remove"; a renaming `label` now takes the base's `pluralLabel` off. The sentence wants that exception (outside this change's surface). + + + + **BREAKING** served-value narrowing for an object an extension renames. + + - **Before.** An `objectExtensions` entry or a `.custom` `object_extension` item that set `label` and no `pluralLabel` was folded with the new label and the base's `pluralLabel`. A translated read served the base package's translation of that plural (`objects.NAME.pluralLabel`), so a list title, read plural first, kept the old name in every locale. + - **Now.** `pluralLabel` is the plural of `label`: such an extension is served as an object that declares no `pluralLabel`, in every locale, so a list titles it by its label. Where the owning package's `.custom` pair saves its own translation of `objects.NAME.pluralLabel`, that translation is served (ruling record 6631250894: a translation in the pair, then the override's own text, then the base's translation). + - **Unchanged.** An extension that sets `pluralLabel` is served with it. One that sets no `label`, or repeats the label it extends, keeps the base's plural and its translation. A plural the tenant set stays, and a tenant's own rename yields as before (ADR-0029 D9.2a). An object no extension renames is served as before. The packaged base the translators compare against keeps its plural. + + Nothing has to be rewritten: an extension that wants a plural for its new name declares `pluralLabel` beside `label`, as it always could, and a `.custom` pair can save `objects.NAME.pluralLabel` in its own `translation` item for a translated locale. + +### Patch Changes + +- 75bbebb: fix(rest)!: the approvals request list refuses a status value no approval request can have, and reads a comma list of statuses as a list + + Clause-②: yes (narrowing) — `GET /api/v1/approvals/requests` (and its environment-scoped twin) refuses a `status` value outside the set `sys_approval_request.status` declares (`pending`, `approved`, `rejected`, `recalled`, `returned`, `cancelled`) with `400 VALIDATION_ERROR`, where it answered `200 {"data":[]}`. An empty value or an empty comma entry is refused too. The widening half: a comma list of declared statuses now filters on all of them, where it matched no row. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at the approvals request list (`@objectstack/rest`): a `status` value that is not a declared approval status is refused instead of answering an empty list. + + - **Before.** `?status=bogus`, `?status=Pending` or `?status=waiting` answered `200 {"data":[]}`, which reads the same as "no request is in that status", so an integration polling the inbox with a misspelt status skipped every pending approval in silence. `?status=pending,returned`, the spelling `@objectstack/client`'s `approvals.listRequests({ status: [...] })` sends for an array, was read as one status and answered the same empty list. `?status=` filtered on nothing and answered every status. + - **Now.** Each comma-separated entry must be a declared status, spelt exactly. Otherwise the request is refused with `400` and the nested `{ "error": { "code": "VALIDATION_ERROR", "message" } }` body the route already answers for an unknown or repeated parameter name; the message names the bad value and the allowed values, and the approval service is not asked. One declared status filters as before. Several filter on any of them (the service's `$in` push-down), and `total` counts on the same filter. A declared status with no rows still answers `200 {"data":[]}`. + - **Who moves.** The console and the client SDK send declared statuses only. A script that sends another spelling, or an empty `status=`, gets the 400. + + **The remedy.** Send one of `pending`, `approved`, `rejected`, `recalled`, `returned`, `cancelled` (lower case), or several separated by commas. To list every status, leave `status` out. +- 18112bf: fix(rest): the one-request record page accepts a related list keyed by a `user` field again + + Clause-②: no + Metadata-protocol: none + + - **Regression in 18.3.0 (#29946, duly#185).** `POST /data/:object/:id/page` (#27713) checked each related list's field against a private `lookup`/`master_detail` list and read `reference` raw, so a related list on `sys_user` keyed by a `user` field (an owner, an assignee) answered `400 VALIDATION_FAILED … is not a lookup or master-detail field that references 'sys_user'`, and the console's full-page record view showed "could not load this record". The related list's own row query (`{ [field]: id }`) follows that field, and 18.2.0 read each related list through that list read, with no type check. + - The check now asks the spec's one arbiter of a field's target, `referenceTargetOf` (`lookup`, `master_detail`, `user`, `tree`; a `user` field's target is fixed to `sys_user` by its type, whether or not its `reference` spells it), the same judgement the engine's `$expand` and delete walk read. A field that does not reference the page's object is still a located `400 invalid_relationship`: a `user` field on any other object's page, a `lookup` to another object, a `tree` on another object, an unreadable `reference` carrier. The message now reads `… is not a lookup, master-detail, user or tree field that references ''.` +- 63b86b3: fix(rest): the approvals, email, sharing, sharing-rule and analytics routes answer from the request's environment kernel when one REST plugin fronts many environments + + Clause-②: no + Metadata-protocol: none + + - **What was wrong.** On a host that mounts one `RestApiPlugin` in front of many environment kernels (the hosted runtime shape), the five provider seams the plugin hands `RestServer` for `approvals`, `email`, `sharing`, `sharingRules` and `analytics` read the HOST kernel's service whatever environment they were asked for, and the unscoped (hostname-routed) mount asked them for no environment at all. The environment's own services were never reached: `GET /api/v1/approvals/requests` answered `{ data: [] }`, `GET /api/v1/approvals/requests/:id` answered 501, and the sharing, sharing-rule and email doors answered 501, on every hosted environment. The record page's approvals part answered no requests. + - **What changes.** Given an environment id and a registered `kernel-manager`, each of the five providers now reads its slot on that environment's kernel (`kernelManager.getOrCreate(id)`), the same acquisition the data routes make. The routes pass the id the shared request→environment resolution answered: the `/environments/:environmentId` param on the scoped mount, else the host's `kernel-resolver` (or the hostname / `X-Environment-Id` chain) on the unscoped one. There is no fallback to the host's occupant: an environment whose kernel carries no such service gets the door's own "not configured" answer. + - **Analytics.** `POST /analytics/dataset/query` already read the environment's service first, and now reads it alone: when the environment's kernel carries no analytics service, or cannot be acquired, the answer is the route's 501 rather than the host's service. + - **Unchanged.** A single-kernel deployment (no `kernel-manager`) reads all five services on the plugin's own kernel exactly as before, for every route and every id. The reserved `platform` id and a request the resolver places in no environment also read the plugin's own kernel. + - **No migration.** No configuration, route, response shape or export changes. +- 1a3ea0c: fix(rest): the `/security` routes, and the auth, settings, i18n and metadata seams, answer from the request's environment kernel when one REST plugin fronts many environments + + Clause-②: no + Metadata-protocol: none + + - **What was wrong.** On a host that mounts one `RestApiPlugin` in front of many environment kernels (the hosted runtime shape), the `security` provider read the HOST kernel's service whatever environment it was asked for, and the unscoped (hostname-routed) suggested-binding mount asked it for no environment at all. The host has no `security` service, so `GET /security/suggested-bindings` and `POST /security/suggested-bindings/:id/confirm` answered 501 on every hosted environment: no owner could accept an installed app's default permission set, and every invited member read 403 on that app's data. `security/explain` and the record, export and meta doors that read the same service fell back to the host's occupant whenever the environment's kernel did not answer. + - **What changes.** Given an environment id and a registered `kernel-manager`, the `security` provider now reads `security` on that environment's kernel, the same environment-scoped seam the approvals, email, sharing, sharing-rule and analytics providers use. Every route that reads it passes the id the shared request→environment resolution answered: the `/environments/:environmentId` param on the scoped mount, else the host's `kernel-resolver` (or the hostname / `X-Environment-Id` chain) on the unscoped one. The host's occupant is no longer a fallback. + - **The audited seams.** The `auth`, `settings`, `i18n` and `metadata` providers are environment-scoped the same way. On a hosted runtime the REST face's execution context now localizes (time zone, locale, currency) from the environment's own settings, as the runtime dispatcher's already did, instead of the host's. An environment whose kernel carries no `auth` service is no longer authenticated by the host's identity store. The `tenancy` provider keeps reading the plugin's own kernel: every consumer reads the bound environment kernel's `tenancy` first. + - **Unchanged.** A single-kernel deployment (no `kernel-manager`) reads every one of these services on the plugin's own kernel exactly as before, for every route and every id. The reserved `platform` id and a request the resolver places in no environment also read the plugin's own kernel. + - **No migration.** No configuration, route, response shape or export changes. +- 2046d21: feat(objectql,rest)!: a new assignment to a banned user is refused, and a banned user's records can be counted and handed over in bulk (#30205) + + Clause-②: yes (narrowing) — the published `FieldErrorCode` enum gains the member `reference_user_banned`. A non-system create or update that sets a user field (the owner field, a `user` field, or a `lookup` to `sys_user`) to a user whose ban is in force is refused `400 VALIDATION_FAILED` with that field code, a write that answered `201` / `200` before. A value the row already stores is not judged, so a record that already holds the banned user stays editable. The two new routes and SDK methods are additions. + Metadata-protocol: none + Skills-docs: no move in `skills/**`, which teaches no user-assignment verdict; `content/docs/permissions/administrator-guide.mdx` gains "Handing over a banned user's work" and its offboarding quick path points at it. + + + + **BREAKING** accept-set narrowing at every data write door (`POST` / `PATCH /api/v1/data/OBJECT`, batch, import, and a flow's elevated record write, which keeps the reference check): banning a user signs them out, but the records they own and are assigned stay where they were, and new work could still be handed to them. + + - **Before.** The Console's person picker hid a banned user, but `POST /api/v1/data/OBJECT` with `assignee` set to that user answered `201`, so new work landed on someone who cannot sign in. + - **Now.** The write-time reference check also asks whether a referenced user's ban is in force (`banned` is true and `ban_expires` is empty or still ahead) and refuses such a value with `reference_user_banned`, beside `reference_not_found`. It is read the way the existence probe reads, so a caller who cannot read the user object is refused the same way. A ban whose expiry has passed is not in force. + - **Unchanged.** A value every row of the write already stores (an edit form carries the whole record back), a system write the reference check already exempts (seed, install, restore), and a missing user (still `reference_not_found`). + - **New: the count.** `GET /api/v1/security/users/ID/assigned-work` (`client.security.assignedWork.count`) answers, per object and user field, how many records hold the user, counted under the caller's own context. Platform tables (`sys_*`), `managedBy` objects and `readonly` / `system` fields such as `created_by` are not counted; objects the caller cannot read are listed in `unreadable`; at most 200 fields are counted (`truncated`). + - **New: the hand-over.** `POST /api/v1/security/users/ID/assigned-work/transfer` (`client.security.assignedWork.transfer`) with `{ toUserId, objects? }` moves up to 500 records per call to an active user, each as an ordinary update under the caller's context, and answers per field what moved and what failed, plus how many remain. A banned, unknown or identical `toUserId` is refused before any write. Multi-valued user fields are counted but not transferred. Neither route touches the ban. + - **`@objectstack/spec`.** `FieldErrorCode` gains `reference_user_banned`, with its message in `en`, `zh-CN`, `ja-JP` and `es-ES`. + + **For a caller.** A write that names a banned user in a user field names an active one instead; the refusal names the field and the user. No stored metadata or row changes, so there is nothing for `objectstack migrate meta` to rewrite. An integration that reassigns work after a ban can call the transfer route and repeat it while `remaining` is above 0. +- 8eca18f: fix(rest): the transactional `POST /batch`, the `/meta` object-state and published reads, and the meta read gate's disabled-package check use the request's environment's data engine when one REST plugin fronts many environments + + Clause-②: no + Metadata-protocol: none + + - **What was wrong.** On a host that mounts one `RestApiPlugin` in front of many environment kernels (the hosted runtime shape), the `objectql` provider read the HOST kernel's engine whatever environment it was asked for, and the unscoped (hostname-routed) mount asked it for no environment at all. `POST /batch` therefore opened its transaction on the host's engine and sent its update and delete ops through it, while its create ops went to the environment's own protocol, joined to the host's transaction. `GET /meta/object/:name/state/:field` judged the object by the host engine's registry, and `GET /meta/:type/:name/published` and the meta read gate asked the host engine's registry whether an environment's package is disabled. The request's execution context already read the environment's engine, so one request used two kernels. + - **What changes.** Given an environment id and a registered `kernel-manager`, the `objectql` provider now reads `objectql` on that environment's kernel, the same environment-scoped seam the approvals, email, sharing, security and other providers use. Every route that reads it passes the id the shared request→environment resolution answered: the `/environments/:environmentId` param on the scoped mount, else the host's `kernel-resolver` (or the hostname / `X-Environment-Id` chain) on the unscoped one. The host's engine is no longer a fallback: an environment whose kernel carries no engine answers `POST /batch` with `501 NOT_IMPLEMENTED`. + - **Unchanged.** A single-kernel deployment (no `kernel-manager`) reads the engine on the plugin's own kernel exactly as before, through the same factory-aware lookup: a factory that fails to build is still a loud `503`, and only an engine that was never registered reads as absent. The reserved `platform` id and a request the resolver places in no environment also read the plugin's own kernel. + - **No migration.** No configuration, route, response shape or export changes. +- 29702d5: fix(spec,rest): the record page's `timeline` count counts one create or delete written to both the activity and the ledger once, as the 动态 stream draws it (#30343) + + Clause-②: yes (narrowing) — `RecordPageTimelineCountSchema.total` falls for the same rows: with a field change counted, `created` (`deleted`) activity rows now step aside as often as the ledger holds `create` (`delete`) writes, beside the `updated` rows that already did; widening in the same line: `@objectstack/spec/api` exports one more constant, `RECORD_TIMELINE_FOLDED_ACTIVITY_TYPES`. No exported member is removed or renamed. + Metadata-protocol: none — a read answer's count, not anything an author writes. + + + + - **Now.** An audit reader on a record with field history sees the 动态 badge count one action once: one create and one edit read 「动态 2」 above the two rows the stream draws. Before, the create counted twice (3). + - **The rule.** Every `updated` row steps aside, as before. A create (a delete) steps aside as often as both sources hold one: the fewer of the record's `created` (`deleted`) activity rows and its `create` (`delete`) ledger writes. A record created before its history was kept has no `create` write, so its `created` row stays in the count, as the stream draws it. + - **Unchanged.** A member without audit read counts the same activity rows as before. A comment adds one. The total stays an upper bound for the `showCompleted` / `types` and no-diff cases; it can fall one short where the stream does not pair a create's (delete's) two rows (different actors, or stamped more than the stream's pairing window apart). +- 739c972: fix(rest): the approvals request list's refusal of a non-string `status` says what the parameter takes, instead of quoting `[object Object]` + + Clause-②: no — the same requests are refused with the same status and envelope; only the message for a non-string `status` changes + Metadata-protocol: none + + `GET /api/v1/approvals/requests?status[$ne]=pending` (a `status` the query parser turns into an object) was refused with `"[object Object]"` quoted as if it were the value sent. It is now refused with `The "status" query parameter must be one status, or several separated by commas, given as plain text. Allowed values: pending, approved, rejected, recalled, returned, cancelled.` Still `400 VALIDATION_ERROR` in the nested envelope, and the approval service is still not asked. The approvals docs page now lists all six request statuses (`cancelled` was missing). +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [60fe44f] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/service-package@18.4.0 + - @objectstack/metadata@18.4.0 + - @objectstack/observability@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/rest/package.json b/packages/rest/package.json index dceb3eba85b..5324095798c 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/rest", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "ObjectStack REST API Server - automatic REST endpoint generation from protocol", "type": "module", diff --git a/packages/runtime/CHANGELOG.md b/packages/runtime/CHANGELOG.md index af14d247ab8..4e95e56bee9 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -1,5 +1,355 @@ # @objectstack/runtime +## 18.4.0 + +### Minor Changes + +- 59bcd22: fix(metadata-protocol, runtime)!: a package created at runtime is refused when it carries `navigationContributions`, which its install never registered (#30072) + + Clause-②: yes (narrowing) — `POST /api/v1/packages` (both install limbs, its no-protocol fallback included) and the protocol install primitive `installPackage` (and so the package duplicate and every in-process caller) refuse a manifest whose `navigationContributions` is a non-empty list with `400 VALIDATION_ERROR`, a manifest they stored and answered `201` for before. Widened: `@objectstack/metadata-protocol` publishes one new export, `runtimeNavigationContributionsRefusal`, the predicate both doors ask. + Metadata-protocol: narrowed + + + + **BREAKING** accept-set narrowing at the runtime package install door (Studio, REST, MCP and AI authors creating a package at runtime). `os validate`, `os build` and code packages are unchanged. + + - **Before.** A package created through `POST /api/v1/packages` with `navigationContributions` targeting another app answered `201` and was stored, but no entry ever appeared in the target app, not after a restart either: only a code package's `registerApp` registers contributions, and neither the install nor the `sys_packages` replay at boot does. Nothing said why. + - **Now.** The same install is refused `400 VALIDATION_ERROR` before anything is stored, with a sentence that names the key and both routes that work. The refusal is asked ahead of the duplicate-id `409`, so the answer does not depend on whether the id is installed. A manifest with no `navigationContributions`, or an empty list, installs as before. + - **Not changed.** A code package's contributions (the Setup app's capability plugins, a multi-package artifact's modules, a marketplace package) are registered and served as before. A package already stored with the key keeps loading at boot, and its entries stay unregistered, as they always were. + + **What an author does.** To add an entry to an installed app's menu, save an override of that app in its customization package (the app copied with the entry added; Studio's navigation editor writes it). To contribute menu entries into another package's app, ship them in a code package's manifest, which registers them when it loads. Then install the runtime package without `navigationContributions`. +- fc869c2: feat(spec)!: a seed is create-only by default — an omitted `mode` is `ignore`, so a restart, an install or an upgrade never writes a seeded value back over a customer's edit (#30081) + + Clause-②: yes (narrowing) — the declared default of `Seed.mode` (and of `SeedLoaderConfig.defaultMode`, now the same field) moves from `upsert` to `ignore`. The accept set is unchanged and every authored `mode` parses exactly as before; what narrows is what a replay does for a dataset that declares no mode: it no longer updates an existing row. + Metadata-protocol: semantic + + + + **BREAKING** for a seed dataset that declares no `mode` and relies on a replay to write its values back onto existing rows. + + - **Before.** A seed with no `mode` replayed as `upsert`. A seed loads again on every single-tenant boot (production included, under `os start` and `os serve`), on every package install or upgrade and on every publish, so each replay wrote the shipped value back over a customer's edit to a seeded row, with no warning. Measured on a production boot before the change (card comment 6633189577): an API edit to a seeded row went back to the shipped value on the next restart, after a boot-artifact upgrade, and after an `os package install` upgrade. + - **Now.** `SeedSchema.mode` defaults to `ignore`: a replay inserts a record whose row is missing and leaves an existing row exactly as it is. A row nobody edited is not rewritten either, so its `updated_at` does not move, and a value changed in a later seed version reaches only a row the load creates. Maintainer ruling 6631016988 (option A). + - **One default, stated once.** `SeedLoaderConfig.defaultMode` is `SeedSchema`'s own `mode` field, default included. The six replay sites (`@objectstack/runtime`: the inline boot seed, the per-organization replayer, the dev hot reload and the published-seed apply; `@objectstack/metadata-protocol`: the publish batch; `@objectstack/cloud-connection`: install, reseed and the rehydrate healer) stop restating `defaultMode: 'upsert'`, and the save door reads the schema's default for a seed item that declares no mode. + - **Compiled artifacts.** `os build` stores the parsed seed, so a package built before this change carries a written `mode: "upsert"` on every dataset. The loader cannot tell that value from an authored one, and nothing converts it: such a package keeps today's replay until it is rebuilt. + - **Installed packages.** A package installed with `os package install` (`@objectstack/cloud-connection`'s install-local) keeps the body it was installed with in its install ledger, and the sample-data reseed (`POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data`) replays that body's datasets in their stored mode. So an entry installed from a pre-change artifact keeps writing the shipped values back over edited rows on every reseed until the **rebuilt artifact is re-installed**; rebuilding alone changes nothing for it. An entry installed from a rebuilt artifact reseeds create-only: it puts back the rows that are missing and leaves an edited row as it is. The ledger's other replays never overwrite: the boot rehydrate seeds nothing, the heal seeds only when every seeded object is empty, and the per-organization replay runs only for a new organization. + - **Stored `seed` items.** A `seed` metadata item stored with no `mode` is not rewritten either. Its next publish parses it to `ignore`, so that publish inserts only the rows it is missing where it used to write its values back over existing rows, with no signal. + - **Unchanged.** An explicit `mode: 'upsert'` still writes every declared column back on each replay, and `columns: { FIELD: { applyOn: 'create' } }` still exempts the columns a customer or a job owns. `insert`, `update` and `replace` behave as before. + + **The remedy.** Rebuild every package so its compiled artifact carries the mode its source declares, and re-install the rebuilt artifact wherever the package was installed with `os package install`, so its install ledger carries that mode too. To put an installed package's sample data back to the shipped values, purge it (`…/purge-sample-data`) and then reseed. For a dataset that is reference data the package must keep current, declare `mode: 'upsert'`, and mark the columns the customer or a job owns `applyOn: 'create'`. Leave `mode` unset for business and starter data the customer owns once it is loaded. +- 8589756: feat(runtime,cloud-connection): the marketplace install, the sample-data reseed, the restart heal and the per-organization seed replay pick `Seed.locale` datasets by the organization's language + + Clause-②: yes (widening) + Metadata-protocol: semantic + + An app that ships one demo dataset per language market (`locale: ['zh-CN']` beside `locale: ['en-US']`) now gets the right one on every path that seeds an organization, not only on the boot path. Before, the install paths passed no locale, so a Chinese organization received the English rows and a page of refused-insert warnings for the Chinese twins (or both sets, where no unique key stopped the second), and one server's per-organization replay gave every new organization the boot app's `i18n.defaultLocale`. + + The locale is resolved for the organization the rows land in. The first answer wins: + + 1. the organization's `localization.locale` setting, through the settings service's own cascade (an env override, the organization's row, the deployment-wide `global` row). Only a value somebody set counts: the manifest's built-in `en-US` default (`source: 'default'`) is not the organization's choice; + 2. `sys_user.locale` of the person installing, reseeding or founding the organization; + 3. the app's declared `i18n.defaultLocale`. + + Nothing answers ⇒ no locale is sent, and every dataset loads as before (the loader still warns, naming each locale-scoped dataset it let through). A dataset without `locale` loads whatever the answer is. When no dataset in a load declares `locale`, neither the setting nor the user is read. + + - **The per-organization replay** resolves the locale per organization, at replay time, instead of once at boot. At founding the organization has no setting of its own yet, so its founder's language decides; an organization inserted with no person behind it takes the app's default. The replayer (`seed-replayer` service) now takes an optional second argument, `(organizationId, { userId })`; a caller passing only the organization id works as before. + - **`@objectstack/organizations`**: the org-scoping middleware hands the replayer the insert's attributed user (better-auth's create runs as system with the signed-in creator attributed) as the founder. + - **`@objectstack/cloud-connection`**: the install and the reseed pass the installing user and the package's own `i18n.defaultLocale`; the restart heal passes the package's default only (no person starts it); the replayer this plugin registers when the host has none passes the founder and no app default, because it replays every installed package at once. + - **`@objectstack/runtime`** exports the shared producer: `resolveSeedLocaleForLoad(host, { seeds, organizationId, userId, appDefaultLocale })`, `appDefaultSeedLocale(body)`, `seedsDeclareLocale(seeds)`, `describeSeedLocale(resolved)` and the `ResolvedSeedLocale` / `SeedLocaleQuestion` / `SeedLocaleSource` types. + - A settings or user read that throws is not skipped over: the seed reports the failure (the install's seed summary, the reseed's `RESEED_SKIPPED`, the replay's warning) and writes nothing, rather than seeding another language over a choice that could not be read. A host with no `settings` service skips step 1. + + Unchanged: the boot seed and the dev hot-reload seeder still filter on the app's `i18n.defaultLocale`; package apply and draft publish still pass no locale. +- 8cc709d: fix(objectql)!: a disabled package's `objectExtensions` fields are withheld from the object they extend, and come back with their values on enable + + Clause-②: yes (narrowing) — while a package is disabled, the served object it extends (`GET /api/v1/meta/object/:name`, the record page, the `/data` field map) no longer carries the fields, validation rules and indexes the package's `objectExtensions` add, so a `/data` write, filter, sort or `fields` projection naming one of those fields is refused as a field the object does not have (`400 INVALID_FIELD` / `INVALID_SORT`), and `GET /api/v1/packages/:id/customizations` no longer lists them among the base's `contributions`. + Metadata-protocol: semantic + + + + **BREAKING** served-object narrowing under package disable (`@objectstack/objectql`, `@objectstack/runtime`, `@objectstack/cli`). + + - **Before.** Disabling a package withheld its objects (`404 OBJECT_PACKAGE_DISABLED`), flows, navigation items, permissions and actions, but the fields its `objectExtensions` add to another package's object stayed on that object: in `/meta/object`, in every `/data` read and on the record page. + - **Now.** The served fold of an object leaves out every `extend` contribution of a disabled package: its fields, its validation rules, its indexes and its field patches. A base layer that already carries them (a tenant overlay saved from a served read, a metadata-service copy of a resolved object) has those fields and rules taken off too. A field the owner itself declares, or another enabled extension also adds, stays. A write naming a withheld field answers exactly what a write naming an unknown field answers; a write through the object's other fields lands and leaves the withheld values alone. + - **Kept.** Schema sync, the federated binding and the lifecycle sweep read the object with every extension folded in (`SchemaRegistry.getStorageObject` / `getAllStorageObjects`), so a disable never drops or skips a column, and a boot that starts with the package disabled still creates them. Enabling the package serves the fields again with the stored values. With `OS_SKIP_SCHEMA_SYNC=1`, `os migrate`'s drift plan no longer reports a disabled module's columns as orphan columns to drop. + - **Who moves.** A client that reads a disabled module's extension fields gets the object without them. A formula on the object that reads a withheld field answers `null`. A validation rule of another, enabled package that reads a withheld field cannot be evaluated, so writes to the object are refused (`400 VALIDATION_FAILED`, naming the rule) until the field's package is enabled again or the rule's package declares and keeps that package. + +### Patch Changes + +- b90a6f7: fix(runtime): a flow start input declared a record id (`objectName`) is loaded in the caller's own scope from the declared object at every door that starts the flow by name, before the run exists + + Clause-②: yes (narrowing) — a start that ran before is now refused when it hands a declared record-id input an id the caller cannot read in the declared object, an id that names no record there, or a value that is not one id. No flow shipped in this repository declares the key, and an input without the declaration answers exactly as before. + Metadata-protocol: none + + + + **The rule.** A flow start input may declare that its value is the id of a record of a named object, with `objectName` on the variable (`{ name: 'accountId', type: 'text', isInput: true, objectName: 'crm_account' }`). Every door that starts the flow by name now loads such an input from the declared object in the caller's own scope before the run exists, whether or not the flow's definition reads the input in plain sight. A record the caller cannot read there and an id that names no record get the same answer, `404 RECORD_NOT_FOUND` (the action door's own refusal), and no run is created. A value that is not one id (a string or a number) is refused `400` naming the input: `VALIDATION_FAILED` from the start check, or, at an action whose param declares a type, `VALIDATION_ERROR` from that param's own contract, which answers first. The system principal is not judged. + + **Which doors.** `POST /api/v1/automation/:name/trigger` and the legacy `POST /api/v1/automation/trigger/:name`; a declared endpoint of `type: 'flow'`; an action of `type: 'flow'` over the REST action route and the MCP `run_action` tool. All of them ask the one check they already shared for inputs a definition reads in plain sight. + + **Beside the definition's own reads, never in place of them.** An input declared a record id of one object and also read by the definition as an id of another (a `get_record` node's `filter.id`, say) is loaded from both, so the caller must be able to read both rows. A parent flow that hands its input on, unchanged, to a `subflow` or `map` callee whose input carries the declaration is judged by that declaration too. A `loop` or `map` that later rebinds the name does not withdraw the declaration: it is the author's word about the value the caller sent. + + **What stays as it was.** An input without the declaration keeps the visible-read rule: it is judged only where the definition reads it as a record id. A start whose caller can read the declared record starts as before. + + **If a start now answers 404 or 400.** Hand the declared input the id of a record the caller can read in the object the input names, or start the flow as the system principal. + + `@objectstack/spec`: the `objectName` describe on a flow variable no longer says that no door applies it, and its liveness row is `live`, so `os validate` no longer warns `liveness-planned-property` on a flow that declares it. +- 3b8c724: fix(objectql, metadata-protocol, runtime): a package's export carries the `objectExtensions` it was installed with, and the extended object's owner's export does not + + Clause-②: yes (widening) + Metadata-protocol: none + + A package installed through `POST /api/v1/marketplace/install-local` (what `os package install` sends, and how every entry of a whole-deployment export is restored) that extends another package's object exported without its `objectExtensions`: `GET /api/v1/packages//export` answered the package's other items and no extension, while the extension's fields were folded into the owner's export instead, as if the owner had shipped them. So a deployment restored from an export lost those fields on its next export, with no error, and a `.custom` pair installed from its export no longer exported its extension. + + - **The extending package's export** (and its entry in `GET /api/v1/packages/export`) now carries every extension it contributes as `objectExtensions`, however it was installed: an extension published through `/api/v1/meta/object_extension` (as before) or a manifest's `objectExtensions` entry (install-local, or a code package). A published extension is exported as it is stored; a manifest entry as it was registered, with `priority` written only when it is not the default `200`. + - **The extended object's owner's export** now carries the object as the owner declares it, without the fields, field patches or labels of ANY other package's extension. Until now only its own `.custom` pair's extension was left out; an extension from any other installed package was folded in. + - **Every served read is unchanged**: `GET /api/v1/meta/object/`, the object list, the console and the data API keep serving the object with every extension folded in. + + `SchemaRegistry` gains `getObjectExtensionsOf(packageId)`, which reads a package's `extend` contributions back as the `objectExtensions` entries they were registered from (the inverse of `registerObjectExtension`). `listItems`, `getAllObjects` and `foldObjectExtendersOnto` take an `onlyExtensionsOf` option beside `withoutExtensionsOf`: each object is resolved with only that package's `extend` contributions folded in. The metadata protocol's per-package layer (the package export) now passes `onlyExtensionsOf`, naming the listed package, and no longer passes `withoutExtensionsOf`, which keeps its meaning for any other caller. + + Supersedes, in the same release: the `.custom` pair note for #29903 says an extension contributed by any other installed package is still folded into the base's object in the base's export. After this change it is not: the base's export leaves out every other package's extension, and that package's own export carries it. +- 665db00: fix(runtime): the empty kernel's own stub declares its protocol range, so `os start` in an empty directory boots with no compatibility warning + + Clause-②: no — boot diagnostics only; nothing accepted or published changes + Metadata-protocol: none + + The empty-boot stub `createDefaultHostConfig({ requireArtifact: false })` writes to `~/.objectstack/dist/objectstack.json` (manifest id `com.objectstack.empty`) now declares `engines: { protocol: '^' }`, the major of the runtime that writes it. Before, it declared no range, so the ADR-0087 handshake warned "package 'com.objectstack.empty' declares no engines.protocol range" on the first boot of the docs' one-line Community quickstart (`npm i -g @objectstack/cli && os start`), about a package the user never wrote. A stub already on disk from an older CLI is refreshed in place on the next `os start`, including when the start supervisor hands it down by path (it resolves `/dist/objectstack.json` as an artifact once that file exists): only `manifest.engines.protocol` is rewritten, and only while the file at the home location is still the stub. The same refresh re-stamps a stub written under another protocol major, which would otherwise be refused as `OS_PROTOCOL_INCOMPATIBLE` once the runtime moves to the next major. An artifact that is not the stub, or a file that is not JSON, is left alone. +- a545a04: fix(runtime, metadata-protocol): the permission-set clone door copies a platform set whose grants name an object this deployment does not install — `member_default` copies into its package's customization pair again on a boot without `@objectstack/plugin-audit` + + Clause-②: yes (widening) — `ObjectStackProtocolImplementation.saveMetaItem`'s request gains one optional key, `inheritedGrantObjects`, reachable from `@objectstack/metadata-protocol`'s `.` entry types + Metadata-protocol: none + + - **What failed:** cloning a set through `POST /api/v1/security/permission-sets/:name/clone` answered `422 INVALID_METADATA` when the source grants on an object no package registers here. The commonest source is the platform baseline `member_default`, which grants `sys_comment` and `sys_activity`. `@objectstack/plugin-audit` registers both objects, and it is an optional package: a deployment that does not install it could not clone `member_default` at all. + - **The rule it now follows:** the save door's unknown-grant refusal (#28990) already leaves alone a grant the set itself already holds, because that grant is not the current write's. A clone's grants are the source's, copied verbatim, so the same holds for a copy. The clone door now tells the save which grant keys the copy inherits (`inheritedGrantObjects`), and the refusal does not judge those. + - **Still refused:** an unknown object that the save does not say it inherits is this write's grant, and is refused exactly as before. That covers a `/meta` save, a re-save that adds a grant, and a draft's publish. No HTTP door forwards the new key. +- a7b8876: feat(spec,metadata-protocol): the publish receipt's `seedApplied.landed` names the rows a seed created and the rows it only matched, per object, capped at 200 ids per list + + Clause-②: yes (widening) + Metadata-protocol: added + + - **`@objectstack/spec`: `seedApplied.landed`** on both publish responses (`PublishMetaItemResponseSchema`, `PublishPackageDraftsResponseSchema`), one shared schema `SeedAppliedLandedSchema`: per object, `inserted` (the rows this publish created) and `matched` (pre-existing rows a seed record's key matched: updated, left as they were, or refused an update), each a `SeedAppliedIdListSchema` `{ ids, total, truncated }`. `ids` carries at most `SEED_APPLIED_LANDED_IDS_CAP` (200) ids, in the loader's record order (datasets in load order, records as authored); past it `truncated` is `true` and `total` is the uncut count. Absent `landed` means "not reported" (a load that threw, or a producer that reports no ids), never "nothing landed". Only `inserted` names rows the seed created: a `matched` row may be a customer's own record that shares the seed's natural key, and under `ignore` (the default) such a row is matched without being written. + - **`@objectstack/spec`: `SeedLoadResult.insertedIds`**, the ids among `landedIds` whose row the load created. Per load: a row an earlier load inserted is matched on a replay, so a reader tracking a seed's rows across publishes accumulates `insertedIds` over every load. `landedIds`' TSDoc now says what each seed mode puts in it. + - **`@objectstack/metadata-protocol`**: `applySeedBodies` returns `landed`, so `publishPackageDrafts` and `publishMetaItem` both carry it; the loader records which landed rows it inserted. `seedAppliedLanded(results)` is exported, the one projection both receipt producers fold the loader's ids through. + - **`@objectstack/runtime`**: the package-publish door's fallback seed apply (for protocols that do not self-apply) reports the same `landed`. + - Accepted cost (objectstack-ai/cloud#3598 ruling A): row ids appear on the public publish responses, bounded by the cap. +- 7b80724: fix(objectql): a roll-up summary a disabled package adds by extension is recomputed when the package is enabled again + + Clause-②: no — a stored summary column stays current across a disable; nothing a caller sends is refused + Metadata-protocol: none + + - **Before.** While a package was disabled, the `summary` fields its `objectExtensions` add to another package's object were withheld from the roll-up index with the rest of the served fold. When the summary's child belongs to an enabled package, the child writes made during the disable landed and recomputed nothing, so after `PATCH /packages/:id/enable` the parent served the value the summary held at the disable, until the next write of one of its children. + - **Now.** Enabling a disabled package recomputes, on every parent row, the roll-ups it adds by extension, and writes a row only where its stored value differs. It runs on every enable door (`SchemaRegistry.onPackageEnabled`, new), and `PATCH /packages/:id/enable` (and an install's `enableOnInstall: true`) answers after it has settled. A row that cannot be recomputed is logged at `error` and the rest of the walk goes on; `ObjectQL.refreshSummariesEnabledBy(packageId)` (new) re-runs it. +- 38d416f: fix(metadata-protocol,runtime): a seed load that throws part-way still reports `seedApplied.landed` for the datasets that finished before the throw + + Clause-②: yes (widening: a thrown load's receipt carries `landed` for the datasets that finished, where it carried none) + Metadata-protocol: semantic + + - **`@objectstack/spec`: `seedApplied.landed`** (both publish responses) now says what it covers on a load that threw part-way (`success: false` with `error`): only the datasets that FINISHED before the throw. The dataset that threw is not listed (its rows are in `inserted` / `updated`, its object in `error`). Absent `landed` now means the apply failed before any load ran, or the producer reports no ids; a throw before any dataset finished reports `landed: []`. + - **`@objectstack/metadata-protocol`**: `SeedLoadPartialWrites` gains `finished`, the finished datasets' `object` / `landedIds` / `insertedIds` in load order, and `applySeedBodies`' catch folds it through the same `seedAppliedLanded` projection (same two lists, same 200 cap) the returned path uses. `seedLoadPartialWrites` and `SeedLoadPartialWrites` are exported from the package root. + - **`@objectstack/runtime`**: the package-publish door's fallback seed apply reads the same `seedLoadPartialWrites` off a thrown load and reports the same `landed`, plus the `inserted` / `updated` counts the response schema requires (it reported neither before) and the object whose seed stopped the load in `error`, as `metadata-protocol`'s apply does. + - Why the dataset that threw is left out: its ids are only partly known when it throws. A batched insert whose response was lost may have committed rows whose ids never came back, so a list of the ids the loader did see would read as complete while missing rows. + + Supersedes, in the same release: `.changeset/30348-seed-receipt-landed-ids.md` says absent `landed` means "not reported (a load that threw, or a producer that reports no ids)". After this change a load that threw part-way reports `landed` for its finished datasets; absent now means no load ran, or a producer that reports no ids. +- b3d2f4b: fix(runtime,plugin-auth)!: a banned user's OAuth access tokens and refresh grants are refused while the ban holds + + Clause-②: yes (narrowing) — while a user's ban holds, an OAuth access token that user consented to is answered at the MCP door as a token that does not verify (the generic `401`, with the same body and `WWW-Authenticate` header), and so is an agent forward credential acting for that user; `/oauth2/userinfo` answers that token as an access token that does not verify (`401 invalid_token`), and `/oauth2/introspect` answers that user's access and refresh tokens as inactive (`{ "active": false }`); the token endpoint mints that user no token: a `refresh_token` grant answers as an invalid refresh token (`400 invalid_grant`), and an `authorization_code` exchange is refused, with the code spent: by the provider's own session answer (`400 invalid_request`) when the ban ended the user's sessions, as the ban routes do, and otherwise as an invalid code (`400 invalid_grant`). Those are the doors this change narrows. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at the MCP door and at the OAuth authorization server's token, userinfo and introspection endpoints: a banned user's OAuth access tokens and refresh grants are refused while the ban holds. + + - **The rule.** Banning a user already ends their sessions and refuses their sign-in and their API keys. While the ban holds, their OAuth access tokens are now also refused at the MCP door and read as not valid at userinfo and introspection, and the token endpoint mints them no new token (no refresh, no code exchange). A ban with an expiry stops binding once the expiry has passed, exactly as sign-in does. + - **Not a revocation.** No token, refresh token or consent row is revoked or rewritten. Lifting the ban (or letting it expire) admits the same access token at the MCP door and lets the same refresh token renew, with nothing reissued. An authorization code presented while the ban held is spent, so the client signs the user in again. + - **Cost.** One primary-key read of the user's ban columns per OAuth credential the MCP door resolves, and one read of the user row per active introspection answer that names a user; the token and userinfo endpoints read nothing extra. A store fault on the MCP door's read answers `503 SERVICE_UNAVAILABLE`, as every other authorization-input read on that door does; a store fault on the introspection read fails the request rather than answering active or inactive. + - **Who moves.** An MCP client or OIDC relying party connected over OAuth on behalf of a banned user gets `401` at the MCP door and at userinfo, `{ "active": false }` from introspection, and `400` from the token endpoint, until the ban is lifted or expires. The Ban User confirmation (`@objectstack/platform-objects`) and the administrator and authentication guides now say so. + + **The remedy.** Lift the ban, or connect the client as a user who is not banned. +- 30dcd8a: fix(runtime): a per-record action called with no recordId is refused 400 naming it, and an unknown id 404s at the door, before its flow runs + + Clause-②: no — every call refused now already failed on `main`: the flow behind the action ran and answered `400 FLOW_FAILED` naming its internal node, with no id or with an id the caller cannot resolve. + Metadata-protocol: none + + An action declared `execution: 'perRecord'` (an api action or a flow action) called through the REST `/actions` door or MCP `run_action` with no `recordId`, or an empty one, is refused `400 VALIDATION_ERROR` naming `recordId`, and nothing is forwarded or started. The same api action called with an id the caller's scope does not resolve answers the door's own `404 RECORD_NOT_FOUND` instead of being forwarded, as a flow or script action already did. Actions declared `aggregate`, or declaring no `execution`, run record-less as before (#29173). Fixes #29881. +- Updated dependencies [31fe289] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [5cdaddb] +- Updated dependencies [75bbebb] +- Updated dependencies [88cd0ab] +- Updated dependencies [c23501b] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [46d2b65] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [18112bf] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [cd5f554] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [07685ae] +- Updated dependencies [eee4149] +- Updated dependencies [85a8fa0] +- Updated dependencies [1756666] +- Updated dependencies [f9e5bef] +- Updated dependencies [63b86b3] +- Updated dependencies [0f3ee58] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [cfb602c] +- Updated dependencies [343ddb2] +- Updated dependencies [0c666c3] +- Updated dependencies [f672e0d] +- Updated dependencies [38717d3] +- Updated dependencies [bfbdeec] +- Updated dependencies [43faa22] +- Updated dependencies [4adc890] +- Updated dependencies [fdad178] +- Updated dependencies [15f808a] +- Updated dependencies [bcdf8d3] +- Updated dependencies [59bcd22] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [c045c34] +- Updated dependencies [dc51811] +- Updated dependencies [c133cfe] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [a545a04] +- Updated dependencies [25fea77] +- Updated dependencies [0837fdc] +- Updated dependencies [4be9734] +- Updated dependencies [1a3ea0c] +- Updated dependencies [bb86ff9] +- Updated dependencies [8f05e61] +- Updated dependencies [9afcd6a] +- Updated dependencies [870ebce] +- Updated dependencies [d1d90b7] +- Updated dependencies [2046d21] +- Updated dependencies [778aedf] +- Updated dependencies [a26abbc] +- Updated dependencies [8eca18f] +- Updated dependencies [d9d42d4] +- Updated dependencies [67d5d53] +- Updated dependencies [47226e6] +- Updated dependencies [a545a04] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [8fad7b1] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [54b491a] +- Updated dependencies [8293c74] +- Updated dependencies [a93ef3e] +- Updated dependencies [0e4dfbe] +- Updated dependencies [ad0fbfd] +- Updated dependencies [8cc709d] +- Updated dependencies [b5765be] +- Updated dependencies [8fa738f] +- Updated dependencies [5a878a3] +- Updated dependencies [60fe44f] +- Updated dependencies [22f0d65] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [26c2efd] +- Updated dependencies [f7afab8] +- Updated dependencies [7b80724] +- Updated dependencies [e760204] +- Updated dependencies [fb1ab89] +- Updated dependencies [455b4f0] +- Updated dependencies [4dde0ae] +- Updated dependencies [94cef75] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [ae5bd5b] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [135c2fb] +- Updated dependencies [898145b] +- Updated dependencies [482e089] +- Updated dependencies [0e4747a] +- Updated dependencies [4bf6e8f] +- Updated dependencies [739c972] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [63de248] +- Updated dependencies [8d7f906] +- Updated dependencies [c35fed1] +- Updated dependencies [a2437b5] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [e9731fc] +- Updated dependencies [a91ad42] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [1030a80] +- Updated dependencies [55a00ba] +- Updated dependencies [badf24d] +- Updated dependencies [474c030] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [70981e6] +- Updated dependencies [85c7374] +- Updated dependencies [dcb5fb3] +- Updated dependencies [eca7cd2] +- Updated dependencies [42e1211] +- Updated dependencies [7193160] +- Updated dependencies [72d7db0] +- Updated dependencies [d0da880] +- Updated dependencies [7b2bb66] +- Updated dependencies [c92b887] +- Updated dependencies [713beba] +- Updated dependencies [21510bb] +- Updated dependencies [d7c0389] +- Updated dependencies [6dd2ca5] +- Updated dependencies [917f024] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [a7293f7] +- Updated dependencies [63a470a] +- Updated dependencies [4851668] + - @objectstack/objectql@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/metadata-protocol@18.4.0 + - @objectstack/service-datasource@18.4.0 + - @objectstack/rest@18.4.0 + - @objectstack/plugin-auth@18.4.0 + - @objectstack/lint@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/plugin-security@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/driver-sql@18.4.0 + - @objectstack/driver-turso@18.4.0 + - @objectstack/driver-memory@18.4.0 + - @objectstack/driver-sqlite-wasm@18.4.0 + - @objectstack/metadata@18.4.0 + - @objectstack/observability@18.4.0 + - @objectstack/service-cluster@18.4.0 + - @objectstack/service-i18n@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/runtime/package.json b/packages/runtime/package.json index cb7b3c9bdb6..2e8ad0f43f0 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/runtime", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "ObjectStack Core Runtime & Query Engine", "type": "module", diff --git a/packages/sdui-parser/CHANGELOG.md b/packages/sdui-parser/CHANGELOG.md index 232c811d197..d8a10c82a6b 100644 --- a/packages/sdui-parser/CHANGELOG.md +++ b/packages/sdui-parser/CHANGELOG.md @@ -1,5 +1,9 @@ # @objectstack/sdui-parser +## 18.4.0 + +No changes in this release. + ## 18.3.0 No changes in this release. diff --git a/packages/sdui-parser/package.json b/packages/sdui-parser/package.json index 9816e75e413..584d16e432c 100644 --- a/packages/sdui-parser/package.json +++ b/packages/sdui-parser/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/sdui-parser", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "ObjectStack constrained JSX-source → SDUI SchemaNode tree compiler (parse, never execute). Isomorphic, zero React. ADR-0080.", "type": "commonjs", diff --git a/packages/services/service-analytics/CHANGELOG.md b/packages/services/service-analytics/CHANGELOG.md index e7b0867e8cc..b46eee7c730 100644 --- a/packages/services/service-analytics/CHANGELOG.md +++ b/packages/services/service-analytics/CHANGELOG.md @@ -1,5 +1,96 @@ # Changelog — @objectstack/service-analytics +## 18.4.0 + +### Patch Changes + +- a35fc66: fix(service-analytics): the refusal of an ungrouped aggregate over a record-currency amount names the home field as the remedy for a consolidated total (ADR-0144 D6) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Now.** A `sum`, `avg`, `min` or `max` over a `currencyMode: 'record'` amount that does not group by its code field is still refused `INVALID_FIELD` / 400 before anything is read, with the same code, status, member, field and object. Its message now names both remedies. For one total in the home currency, point the measure at the amount's home field: by name (`homeAmount.field`) when the host relays it, or else as "the field its `currencyConfig.homeAmount.field` names", with the declaration to add if it has none. For one row per currency, add the dimension over the code field. Before, the message named only the grouping. + - **Unchanged.** A measure over the home field is no record-mode amount, so it is served unchanged: ungrouped, grouped by the code field, and as a `totals` grand total, labelled with the home currency. + - **The widening.** `AnalyticsServiceConfig.sourceFieldMeta` may answer one more optional key, `homeAmountField`: the amount's `currencyConfig.homeAmount.field`, beside a `currencyField` only. `AnalyticsServicePlugin` relays it. A host that does not answer it gets the message without the field's name. +- 55d9dd4: feat(spec,service-analytics,data-objectstack): a dataset answer says, per measure, which records that measure counted (`drillScopes`) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/spec`: `AnalyticsResult.drillScopes?: Record`**, also declared on the `AnalyticsResultResponse` wire schema. Each selected measure name maps to the list filter, on the answer's `object`, that selects the records the measure aggregates: the dataset's own `filter`, AND the measure's own `filter`, AND, for a measure over a field, that field holding a value (both instants for a duration measure), with each `{token}` resolved as the query resolved it. It is `{}` when none of these applies. A measure that no list filter can select is left out: a derived measure, or a scope that names a relationship path (`account.region`, as a key or a `$field` reference). A measure left out offers no drill. A `__compare` column has no entry. + - **`@objectstack/service-analytics`: `queryDataset` sets `drillScopes` on every dataset answer that has rows**, on both query faces, from the same token-resolved filters the numbers were computed under. A drill that lists the clicked bucket (`dimensionFields` / `drillRawRows` / `drillRawTotals`, `drillRanges`) AND the widget's own filter AND this scope lists exactly the records the number counted. Before this, a drill built from the bucket and the widget filter alone listed every record in the bucket, so a filtered measure such as "paid invoices" showed 4 records behind the number 2. + - **`@object-ui/data-objectstack`: `queryDataset` passes `drillScopes` through**, typed as the spec slot. The adapter lists the keys it returns, so until now the sidecar was dropped before it reached a widget. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/services/service-analytics/package.json b/packages/services/service-analytics/package.json index 71e27ba417e..ee161be5b0e 100644 --- a/packages/services/service-analytics/package.json +++ b/packages/services/service-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-analytics", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Analytics Service for ObjectStack — implements IAnalyticsService with multi-driver strategy pattern (NativeSQL, ObjectQL, InMemory)", "type": "module", diff --git a/packages/services/service-automation/CHANGELOG.md b/packages/services/service-automation/CHANGELOG.md index 7965aac4a0e..27cb131d1b1 100644 --- a/packages/services/service-automation/CHANGELOG.md +++ b/packages/services/service-automation/CHANGELOG.md @@ -1,5 +1,166 @@ # @objectstack/service-automation +## 18.4.0 + +### Minor Changes + +- 04f36b6: feat(service-automation): a record-triggered flow's notification opens the record that started the run, without the author writing `sourceObject` and `sourceId` + + Clause-②: no — no accepted key, value or exported type changes; the node's existing `sourceObject` / `sourceId` / `actionUrl` keys keep their meaning when written. + Metadata-protocol: semantic + + - **Before.** A `notify` node built its click-through link only from an explicit `sourceObject` + `sourceId` pair (or an `actionUrl`). The obvious record-triggered flow — `recipients`, `title`, `message` — passed `os validate` and sent a notification whose link was empty, so a click opened the notification list instead of the record. + - **Now.** A `notify` node that writes none of `sourceObject`, `sourceId` and `actionUrl` links to the run's trigger record when the run has one: a record change (create or update), a time-relative sweep's row, or an action started on a record. The notification stores that record as its `source_object` / `source_id`, and the inbox opens `/OBJECT/ID`. + - **Unchanged.** An explicit pair or an explicit `actionUrl` wins, as before. A half-written pair is still dropped rather than defaulted. A run with no trigger record (a schedule, an inbound API post, a manual start with no record) links nowhere, as before. A delete event is not linked: its record is gone once the write lands. + - **To link elsewhere** from a record-triggered flow, write the `actionUrl` or the `sourceObject` + `sourceId` pair the notification should open. + +### Patch Changes + +- 4be9734: fix(automation)!: a `currency` or `percent` screen field refuses a non-number on resume, as a `number` field does, and the console renders `percent` as a numeric input (#30178) + + Clause-②: yes (narrowing) — `POST …/runs/RUN/resume` refuses a string under a `type: 'currency'` or `type: 'percent'` screen field, a bag it accepted before. Nothing is widened. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at the screen-flow resume door. + + - **Before.** A `type: 'percent'` or `type: 'currency'` screen field took any string on resume: `"75"` under a `max` of 60, `"abc"` too. The bound compares numbers, so it never judged a string, and the run consumed its suspension on the value. The console rendered `percent` as a text input and posted a string, so a `percent` field's `min` / `max` held for no console user. + - **Now.** `number`, `currency` and `percent` are one value domain: a present value that is not a finite JSON number is refused with `invalid_type` inside `400 INVALID_SCREEN_INPUT`, the suspension is kept, and a corrected resume still lands. A numeric string is not read as the number it spells. The console renders `percent` as a numeric input carrying `min` / `max`, so it posts a number and the browser stops an over-limit value before any request. + - **Unchanged.** An absent value and a hidden field are not judged; `rating`, `slider`, `progress`, `text` and untyped fields still take a string; a JSON number on `currency` / `percent` is judged against the bound as before. + - **For a caller that skips the console.** The resume bag carries the JSON number (`60`), not a string; the refusal names the field and says it must be a number. No stored metadata changes, so there is nothing for `objectstack migrate meta` to rewrite. +- 6c4c43a: fix(service-automation,mcp): a time-relative flow's conditions are typed against the object its sweep scans, at registration and in the `validate_expression` advice + + Clause-②: no + Metadata-protocol: none + + - **`@objectstack/service-automation`**: the registration pass's advisory schema check (the logged `unknown field` / typo / type-mismatch warnings) typed a time-relative flow's conditions against the start node's `config.objectName`, while the run binds a row of `config.timeRelative.object`. A flow stored with a start naming another object kept a warning on a field that is right for the scanned object and none on a field that fails at run time; a start naming no `objectName` got no schema check at all. The pass now reads the object off the flow's trigger binding, the same answer the run uses: `timeRelative.object` when it is a string, `config.objectName` only otherwise. Every other flow is typed as before. The warnings stay logged, never thrown. + - **`@objectstack/mcp`**: `validate_expression`'s description, its `sweepObject` input text and the SKILL.md line no longer advise passing a `sweepObject` other than `objectName` as a supported shape. They say to pass the scanned object as `objectName`, and that a start whose `objectName` names another object is refused at publish (`flow-time-relative-object-mismatch`). The tool's inputs and verdicts are unchanged. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [88cd0ab] +- Updated dependencies [c23501b] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [07685ae] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [43faa22] +- Updated dependencies [fdad178] +- Updated dependencies [15f808a] +- Updated dependencies [bcdf8d3] +- Updated dependencies [59bcd22] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [86ae2ca] +- Updated dependencies [0837fdc] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [9afcd6a] +- Updated dependencies [870ebce] +- Updated dependencies [d1d90b7] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [d9d42d4] +- Updated dependencies [67d5d53] +- Updated dependencies [47226e6] +- Updated dependencies [a545a04] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [8fad7b1] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [a93ef3e] +- Updated dependencies [0e4dfbe] +- Updated dependencies [b5765be] +- Updated dependencies [8fa738f] +- Updated dependencies [5a878a3] +- Updated dependencies [60fe44f] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [26c2efd] +- Updated dependencies [f7afab8] +- Updated dependencies [455b4f0] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [0e4747a] +- Updated dependencies [4bf6e8f] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [63de248] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [a91ad42] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [1030a80] +- Updated dependencies [55a00ba] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [85c7374] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [c92b887] +- Updated dependencies [713beba] +- Updated dependencies [21510bb] +- Updated dependencies [6dd2ca5] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [a7293f7] +- Updated dependencies [63a470a] +- Updated dependencies [4851668] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/metadata-protocol@18.4.0 + - @objectstack/plugin-auth@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/services/service-automation/package.json b/packages/services/service-automation/package.json index 993c3cbebee..5c768708ad0 100644 --- a/packages/services/service-automation/package.json +++ b/packages/services/service-automation/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-automation", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Automation Service for ObjectStack — implements IAutomationService with plugin-based DAG flow execution engine", "type": "module", diff --git a/packages/services/service-cache/CHANGELOG.md b/packages/services/service-cache/CHANGELOG.md index a7f60c18fc0..f8a79ac9918 100644 --- a/packages/services/service-cache/CHANGELOG.md +++ b/packages/services/service-cache/CHANGELOG.md @@ -1,5 +1,80 @@ # @objectstack/service-cache +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/observability@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-cache/package.json b/packages/services/service-cache/package.json index 09f1f8e34fb..8f02c70e49b 100644 --- a/packages/services/service-cache/package.json +++ b/packages/services/service-cache/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cache", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Cache Service for ObjectStack — implements ICacheService with in-memory and Redis adapters", "type": "module", diff --git a/packages/services/service-cluster-redis/CHANGELOG.md b/packages/services/service-cluster-redis/CHANGELOG.md index aa8e2bcba77..3d78054cd66 100644 --- a/packages/services/service-cluster-redis/CHANGELOG.md +++ b/packages/services/service-cluster-redis/CHANGELOG.md @@ -1,5 +1,71 @@ # @objectstack/service-cluster-redis +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/service-cluster@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-cluster-redis/package.json b/packages/services/service-cluster-redis/package.json index 9efd0a0d264..c72ca0a410e 100644 --- a/packages/services/service-cluster-redis/package.json +++ b/packages/services/service-cluster-redis/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cluster-redis", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Redis cluster driver for ObjectStack — implements IPubSub/ILock/IKV/ICounter against Redis using ioredis.", "type": "module", diff --git a/packages/services/service-cluster/CHANGELOG.md b/packages/services/service-cluster/CHANGELOG.md index d23a744fc4b..9d2aad34475 100644 --- a/packages/services/service-cluster/CHANGELOG.md +++ b/packages/services/service-cluster/CHANGELOG.md @@ -1,5 +1,79 @@ # @objectstack/service-cluster +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-cluster/package.json b/packages/services/service-cluster/package.json index 8652415ff2c..61d682f78e2 100644 --- a/packages/services/service-cluster/package.json +++ b/packages/services/service-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cluster", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Cluster Service for ObjectStack — pluggable PubSub/Lock/KV/Counter primitives. Memory driver included; postgres/redis drivers ship separately.", "type": "module", diff --git a/packages/services/service-datasource/CHANGELOG.md b/packages/services/service-datasource/CHANGELOG.md index b140aebcc7c..2af2af5e857 100644 --- a/packages/services/service-datasource/CHANGELOG.md +++ b/packages/services/service-datasource/CHANGELOG.md @@ -1,5 +1,176 @@ # @objectstack/service-external-datasource +## 18.4.0 + +### Minor Changes + +- 4dde0ae: fix(spec)!: a SQLite datasource's `config.filename` must name a database: `''` and a name that starts with `:` without being exactly `:memory:` are refused by the stack parse and the metadata save door, not only by the datasource admin doors + + Clause-②: no (narrowing) — `SqliteConfigSchema.filename` and `SqliteWasmConfigSchema.filename` refuse `''` and any `:`-prefixed value other than exactly `:memory:`, so `defineStack` / `os validate` / `os build` and `PUT /api/v1/meta/datasource/NAME` refuse a `sqlite` / `sqlite-wasm` datasource they accepted before. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: none — `skills/**` teaches no SQLite `filename` value, and `content/docs/**` teaches only `:memory:` and file paths, which stay accepted. The prescription ships in the refusal text, the FROM → TO below and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing on `SqliteConfigSchema.filename` and `SqliteWasmConfigSchema.filename` (`@objectstack/spec`), and so at every door that judges a datasource against the spec: `defineStack`, `os validate`, `os build` and `PUT /api/v1/meta/datasource/NAME`. + + - **Before.** Both schemas typed `filename` as any placeholder-free string. `:memory:/srv/data/parts.db` (the New datasource dialog's prefilled `:memory:` with a path typed after it) and `''` parsed. Every layer reads a name that starts with `:` as fileless, so no file was ever opened: a `sqlite` datasource opened it as a relative path whose directory does not exist and every statement failed, and a `sqlite-wasm` one opened a throwaway in-memory database whose writes were gone at the next connect. The datasource admin doors (Test connection, create, update) already refused both; a datasource declared in a stack or saved through the metadata door was stored anyway, and the admin door's update would not take it back. + - **Now.** The parse answers one issue at `filename` (`config.filename` on a datasource, `datasources.N.config.filename` in a stack) that names the value, says why and prescribes the fix: `'…' is neither ':memory:' nor a file path. A name that starts with ':' is not read as a file, so this datasource would store nothing: use exactly ':memory:' …`, or `'' is empty and names no database file. …`. `defineStack` raises it as `STACK_SCHEMA_INVALID` (422); the metadata door answers `422 INVALID_METADATA`. + - **Unchanged.** Exactly `:memory:`, an absent `filename` (still defaulting to `:memory:`), and relative, absolute, Windows-drive and `file:` paths parse as before. Whether a path can be opened or created is still judged only at runtime, by the datasource admin doors; the schema cannot touch the file system. + - **`@objectstack/service-datasource`.** The admin doors give the same answers as before, word for word. The two arms the string alone decides now come from the driver-contract parse they already run, so `sqlite-database-filename.ts` keeps only its filesystem arm: one definition, in the spec. + - **`@objectstack/spec`.** The upgrade-guide entry `sqlite-filename-names-no-database-refused` (a semantic migration: no automatic rewrite, because whether the author meant an ephemeral database or the path typed after the prefix is a judgement about the datasource). + + **FROM → TO.** `config: { filename: ':memory:/srv/data/parts.db' }` → `filename: '/srv/data/parts.db'` to keep the data in that file, or `filename: ':memory:'` for an ephemeral database; `filename: ''` → `':memory:'` or a file path. + +### Patch Changes + +- 5cdaddb: fix(service-datasource)!: a datasource update whose new configuration fails to connect stores none of it, and a whitespace-only `secret` is refused like an empty one + + Clause-②: yes (narrowing) — `PATCH /api/v1/datasources/:name` and `POST /api/v1/datasources` refuse one more body that used to answer 200 / 201: a `secret` whose value is whitespace only (`" "`, a tab and a newline), now `400 DATASOURCE_ADMIN_ERROR` with the empty-secret answer. The widening half: the `reregisterPool` member of the exported `DatasourceAdminServiceConfig` may now answer `{ previousPoolKept: boolean }` (still optional; a seam that answers nothing reads as before). + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at the datasource admin doors (`@objectstack/service-datasource`): a whitespace-only `secret` is refused. + + - **Before.** A `PATCH` whose new configuration failed to connect (a wrong password, a blank `secret`, a changed `config` that does not reach the database) answered `200` with `status: "error"` and a reason saying the previous pool was kept and the new configuration was NOT in force. It had already stored the new record, repointed `external.credentialsRef` at a new `sys_secret` row and removed the working one, so the next restart connected the configuration that failed. A whitespace-only `secret` passed the empty-secret guard and reached this path. + - **Now.** The update rebuilds the pool before it stores anything. When the previous pool is kept, nothing of the patch is stored: the record, its `credentialsRef` and every other key of the patch stay as they were, and the `sys_secret` row sealed for the patch is removed again. The answer is unchanged (`200`, `status: "error"`, "the new configuration is NOT in force"), and now true: a restart connects the configuration that was serving. A configuration that connects is stored and in force, and the replaced secret is retired, as before. A whitespace-only `secret` is refused before anything is written, by the same guard and with the same message as the empty string; a value with whitespace around other characters is still sealed as sent. + - **Who moves.** The console sends `secret` only when the field holds a value. A script or SDK call that sends a whitespace-only `secret` gets the 400. A host that wires its own `reregisterPool` seam keeps today's behaviour until it answers `{ previousPoolKept: true }` for a rebuild that kept the previous pool. + + **The remedy.** To keep the stored credential, leave `secret` out of the request (or send `null`). To replace it, send the new credential. +- 22f0d65: fix(service-datasource)!: Test connection and Save refuse a SQLite filename that is neither `:memory:` nor a usable file path, where Test connection used to pass it + + Clause-②: yes (narrowing) — `POST /api/v1/datasources/test`, `POST /api/v1/datasources` and a `PATCH /api/v1/datasources/:name` that touches `config` or `driver` refuse a `sqlite` / `sqlite-wasm` `config.filename` that is empty, starts with `:` without being exactly `:memory:`, or names a path that can be neither opened nor created. The test answered `ok: true` for the first two and the save stored all three; both now answer with the value and the reason (`result.ok: false` from the test, `400 DATASOURCE_ADMIN_ERROR` from the save), the envelope every invalid `config` already gets. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at the datasource admin doors (`@objectstack/service-datasource`): a SQLite filename that names no usable database is refused by Test connection and by Save (#30331). + + - **Before.** The New datasource dialog's `:memory:` with a typed path after it (`:memory:/srv/data/parts.db`) tested green. Every layer reads a name that starts with `:` as fileless, so the probe's missing-file check skipped it. Saved, a `sqlite` datasource then opened it as a relative path under the server's working directory whose directory does not exist, so every query failed while the health check answered `false` (which the probe did not read as a failure); a `sqlite-wasm` one opened an ephemeral in-memory database, so writes succeeded and were gone at the next restart. An empty filename tested green too, and a path under a regular file or in a directory the server may not write could be saved. + - **Now.** Test connection answers `ok: false` and Save answers `400 DATASOURCE_ADMIN_ERROR`, under `Invalid configuration for driver '…'`, with a `config.filename:` line that names the value and why: not `:memory:` and not a file path, empty, or the operating system's reason the file cannot be opened or created. + - **Unchanged.** Exactly `:memory:` tests green and saves. An existing database file tests green and saves. A creatable path that does not exist yet is still refused by the test (which never creates it, #27512) and accepted by Save, which creates it on connect. `turso` names its target as a url and is not judged here. + + **The remedy.** Use exactly `:memory:` for an ephemeral database, or the path of a database file with nothing in front of it. +- 94cef75: fix(service-datasource): Test connection answers `ok: false` when the driver connects but its own health check answers `false` (#30407) + + Clause-②: no — a served answer comes to match the driver's own health answer; no accept set changes. + Metadata-protocol: none + + The datasource probe awaited the handle's `ping` (the driver's `checkHealth()`) and dropped its boolean, so a connect that only warned and then reported itself unhealthy answered Test connection `ok: true`. The probe now reads that answer: a literal `false` answers `ok: false` with a sentence saying the driver connected but its health check reported the database as unhealthy. A connect or ping that throws keeps answering the thrown message, and a handle whose `ping` answers nothing keeps answering `ok: true`. +- ae5bd5b: fix(service-datasource): the refusal to delete a datasource that objects still use names its reason and the objects in `error.details` + + Clause-②: yes (widening) + Metadata-protocol: none + + - `DELETE /api/v1/datasources/:name` on a runtime datasource that objects are still bound to keeps answering `400` `DATASOURCE_ADMIN_ERROR` with the same English message, and now also carries `error.details`: `{ reason: 'objects_bound', count, objects }`. `reason` tells this refusal apart from every other 400 of the datasource admin routes, `count` is the number in the message, and `objects` lists the bound objects' names, sorted, so a client can name them in its own language without reading object metadata. The names come from the same read as the count, so the two cannot disagree. + - `DatasourceAdminServiceConfig.countBoundObjectsDiagnosed` may now return `objects` (the names it counted); the shipped plugin wiring does. A host whose read reports only a number keeps working, and its refusal carries `count` without `objects`. + - Every other 400 of these routes answers exactly as before, with no `details`. The degraded-read refusal (`503` `SERVICE_UNAVAILABLE`) is unchanged. +- 135c2fb: fix(service-datasource)!: an object imported from a package's external datasource is saved into that package, or into its customization package + + Clause-②: yes (narrowing) — `POST /api/v1/datasources/:name/external/tables/:remote/import` (and the MCP `import_object` tool) now saves the object into the package that declares the datasource, or into that package's customization package `.custom` when the package is read-only, and names it in that package's namespace. An explicit `name` in the read-only base's own namespace (`showcase_orders` over the showcase's `showcase_external`) is now refused with ADR-0028's message naming the pair-prefixed name (`showcase_custom_orders`); under a walled tenancy posture (`group`, `isolated`) an import that needs a customization package that does not exist yet is refused, naming the platform operator and the ensure door. `ExternalDatasourceServiceConfig` gains the optional `resolveImportLanding`, `persistObject` an optional third `packageId` argument, and the package exports the `ImportLanding` type. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at the external-datasource import door (`@objectstack/service-datasource`), ruling B on where an imported object lives (ruling record 6660988968). + + - **Where an import lands.** The datasource's owning package decides. A datasource no package declares (added in Setup) keeps today's answer: a package-less row, environment metadata that no package export carries (ADR-0146 D11). A datasource declared by a writable package saves the object into that package. A datasource declared by a read-only installed package (a booted code package or a marketplace install) saves it into that package's customization package `.custom`, which the import obtains from the ensure door first (ADR-0146 D2/D3), so `os package export` and `GET /api/v1/packages/.custom/export` carry it. + - **The name it gets.** The save door judges the name against the package written into, so an import over a read-only package's datasource is named in the pair's namespace: the showcase's `customers` table imports as `showcase_custom_customers`, where it imported as `showcase_customers`. The draft door (`…/tables/:remote/draft`, the `*.object.ts` an author commits into the package's own source) still names it in the datasource package's namespace. + - **Walled postures.** Under `group` and `isolated` the platform operator alone creates a customization package. The import never creates it: it saves into one that already exists, and while none does it is refused (`400 EXTERNAL_IMPORT_ERROR`) with a message naming the platform operator and `POST /api/v1/packages//ensure-customization`. It is never saved package-less instead. + - **Objects imported before this release** stay where they are, package-less and under their old names. Importing the same table again makes a second object under the new name in the customization package; delete the old one once nothing points at it. +- 898145b: fix(service-datasource): an object drafted or imported from an external datasource table gives each field a readable label + + Clause-②: no + Metadata-protocol: none + + Every field of a generated object draft (`generateObjectDraft`, `importObject`, `os datasource introspect`) now carries a `label`, derived from the field's name by the same snake_case to Title Case step that already labels the object: a table with columns `part_no` and `part_name` yields fields labelled `Part No` and `Part Name`, so the record form of a synced object no longer shows raw column names. The label follows the name the field is saved under (after `rename` and the numbering of a taken name), a lookup keeps the label of the column it replaces, and the rendered `*.object.ts` source carries the same label. The object's own label is unchanged. Objects imported before this release keep their stored fields; re-import or edit the labels to change them. +- c35fed1: fix(service-datasource): the datasource driver catalog serves its words in the requester's language, so the New datasource dialog reads in one language + + Clause-②: no + Metadata-protocol: none + + `GET /api/v1/datasources/drivers` now serves each driver's label and description, and each connection field's `title` and `description` (Filename, Auto-migrate schema, Host, Port, …), in the language the request names: the top `Accept-Language` tag, else `?locale=`. The code is read through the platform locale table (`platformLocaleTagOf`, shortened one subtag at a time), so `zh-TW` and `zh-HK` get Traditional Chinese, `zh`, `zh-CN` and `zh-SG` Simplified, `pt` Brazilian Portuguese and `fr-CA` French. Every language on the table is translated for every offered driver: Simplified and Traditional Chinese, Spanish, Arabic, French, Brazilian Portuguese, Russian, German, Japanese and Korean. English requests, codes the table does not name, and product names (SQLite, PostgreSQL, MySQL / MariaDB, MongoDB) are served the English text, never a key. `DRIVER_CATALOG` stays the English catalog, and the new `driverCatalogFor(locale)` in the package source builds the localized one; ids, icons, types and defaults are unchanged in every language. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [60fe44f] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [8d7f906] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [dcb5fb3] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/driver-sql@18.4.0 + - @objectstack/driver-turso@18.4.0 + - @objectstack/driver-mongodb@18.4.0 + - @objectstack/driver-memory@18.4.0 + - @objectstack/driver-sqlite-wasm@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-datasource/package.json b/packages/services/service-datasource/package.json index 54ea590dcd5..cdb9b86ad68 100644 --- a/packages/services/service-datasource/package.json +++ b/packages/services/service-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-datasource", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "The datasource service (ADR-0015): external-table federation (introspect/draft/import/validate) + runtime UI datasource lifecycle (list/test/create/update/remove + REST routes). Open-source mechanism; the tier line falls on which ICryptoProvider / driver factory a host injects.", "type": "module", diff --git a/packages/services/service-i18n/CHANGELOG.md b/packages/services/service-i18n/CHANGELOG.md index 74bb0c50dea..b0566c39a29 100644 --- a/packages/services/service-i18n/CHANGELOG.md +++ b/packages/services/service-i18n/CHANGELOG.md @@ -1,5 +1,80 @@ # @objectstack/service-i18n +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/services/service-i18n/package.json b/packages/services/service-i18n/package.json index a5306930196..311c3a8d634 100644 --- a/packages/services/service-i18n/package.json +++ b/packages/services/service-i18n/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-i18n", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "I18n Service for ObjectStack — implements II18nService with file-based locale loading", "type": "module", diff --git a/packages/services/service-job/CHANGELOG.md b/packages/services/service-job/CHANGELOG.md index 73509f42b52..49b5f8bd54e 100644 --- a/packages/services/service-job/CHANGELOG.md +++ b/packages/services/service-job/CHANGELOG.md @@ -1,5 +1,90 @@ # @objectstack/service-job +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-job/package.json b/packages/services/service-job/package.json index 485db75343c..05e6b6812dc 100644 --- a/packages/services/service-job/package.json +++ b/packages/services/service-job/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-job", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Job Service for ObjectStack — implements IJobService with setInterval and cron scheduling", "type": "module", diff --git a/packages/services/service-knowledge/CHANGELOG.md b/packages/services/service-knowledge/CHANGELOG.md index 5a724e7c792..23397d73822 100644 --- a/packages/services/service-knowledge/CHANGELOG.md +++ b/packages/services/service-knowledge/CHANGELOG.md @@ -1,5 +1,79 @@ # @objectstack/service-knowledge +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-knowledge/package.json b/packages/services/service-knowledge/package.json index 05d2ab6bf5d..88d3240582a 100644 --- a/packages/services/service-knowledge/package.json +++ b/packages/services/service-knowledge/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-knowledge", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Knowledge Service for ObjectStack — orchestrator implementing IKnowledgeService over pluggable IKnowledgeAdapter backends (RAGFlow, LlamaIndex, Dify, in-memory).", "type": "module", diff --git a/packages/services/service-mailbox/CHANGELOG.md b/packages/services/service-mailbox/CHANGELOG.md index fe2e0bb0ffb..48cdad6055c 100644 --- a/packages/services/service-mailbox/CHANGELOG.md +++ b/packages/services/service-mailbox/CHANGELOG.md @@ -1,5 +1,98 @@ # @objectstack/service-mailbox +## 18.4.0 + +### Patch Changes + +- 6701c08: i18n(service-mailbox): the mailbox objects' labels, help, options and views ship in all eleven platform languages + + Clause-②: no + Metadata-protocol: none + + The mailbox objects (`sys_mailbox`, `sys_mail_message` and `sys_mail_association`) now carry Traditional Chinese (`zh-Hant`), Arabic (`ar`), French (`fr`), Brazilian Portuguese (`pt-BR`), Russian (`ru`), German (`de`) and Korean (`ko`) beside the existing `en`, `zh-CN`, `ja-JP` and `es-ES`, so a user reading the app in one of those languages sees the mailbox screens in their own language instead of English. RFC 5322 header names (`Message-ID`, `In-Reply-To`, `Reply-To`, …) and the IMAP `UIDVALIDITY` term stay as written, as they already do in `zh-CN`. Each new locale is registered under its platform-table tag with its source-hash companion, and the package's extract command lists the seven tags, so the existing `check:i18n` bundle check covers them. Part of #30512. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/types@18.4.0 + - @objectstack/connector-imap@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/services/service-mailbox/package.json b/packages/services/service-mailbox/package.json index a7aa782e0d3..4e380582ce2 100644 --- a/packages/services/service-mailbox/package.json +++ b/packages/services/service-mailbox/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-mailbox", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Mailbox Service for ObjectStack \u2014 inbound mail: the mirrored message (sys_mail_message), its record associations and their read gates.", "type": "module", diff --git a/packages/services/service-messaging/CHANGELOG.md b/packages/services/service-messaging/CHANGELOG.md index acc832a5ef1..a7d75446890 100644 --- a/packages/services/service-messaging/CHANGELOG.md +++ b/packages/services/service-messaging/CHANGELOG.md @@ -1,5 +1,154 @@ # @objectstack/service-messaging +## 18.4.0 + +### Patch Changes + +- 8aa2e91: feat(notifications): the `push` channel — a `notify` node naming `push` reaches the recipient's phones through APNs and FCM + + Clause-②: yes (widening) + Metadata-protocol: none + + - **New package `@objectstack/service-push`.** `PushServicePlugin` registers the kernel's `push` service: APNs over HTTP/2 with token-based auth (an ES256 provider token signed with the `.p8` key, reused for fifty minutes) and FCM over the HTTP v1 API (an OAuth access token from the service account). It reads `PushServiceConfigSchema` from its `config` option and refuses an inline secret at boot with the schema's guidance. It serves the direct transport (`transport: 'direct'`, the operator's own APNs key and Firebase project, for a white-label build); the hosted relay, the schema's default and so also an empty config, is refused at init until its sender ships (#29920). Each secret is named by `credentialRef` and resolved when the transport first signs; in the open tier the reference is the name of an environment variable (ADR-0143 D3). + - **`@objectstack/service-messaging`: the `push` channel.** Mounted while a `push` service is registered, as the sms channel is while an `sms` service is. One delivery reaches every enabled device the recipient registered, on each vendor the push service has a transport for. The push carries the notification id and, when it is a same-server path (`PushRouteSchema`), the notification's `actionUrl` as `route`; any other `actionUrl` is left out and the delivery still goes. Each device's push also carries the origin stored on its row (`PushDeliveryDataSchema.origin`) when the row holds one; the registry does not store it yet, so today none does. A token the vendor reports dead (APNs `BadDeviceToken` / `Unregistered`, FCM `UNREGISTERED`) is retired through `invalidateDeviceToken`: the device row is kept, disabled, and the user's other devices still get the push. A recipient with no enabled device, or whose every device was reported dead, is graded `invalid_recipient`; no push service is refused `transport_not_configured`, as the sms and email channels refuse it. + - **`@objectstack/spec`: description and comment text only.** The `[PLANNED]` markers come off `PushServiceConfigSchema`, `PushDirectTransportConfigSchema`, `ApnsTransportConfigSchema`, `FcmTransportConfigSchema` and `PushDeliveryDataSchema`, which now have their reader and producer; `PushRelayTransportConfigSchema` keeps `[PLANNED — not yet served]`, now saying the push service refuses it at boot (#29920). `NotificationChannelSchema`'s JSDoc lists `push` among the registered channels, the docblocks name `@objectstack/service-push` where they named `plugin-notification-push`, and the `NOT_FOUND` synonym waiver's reason names service-messaging's own provenance row. No key, schema shape or export changes. +- bcdb1db: fix(spec,messaging): a push device's `locale` is described as what it is — stored for the device, and no push renders by it + + Clause-②: no + Metadata-protocol: none + + - **`@objectstack/spec`**: `UserDeviceSchema.locale` and `RegisterDeviceRequestSchema.locale` no longer say pushes to the device are rendered in its locale. They now say the value is stored for the device and that a push arrives in the recipient's language: `sys_user.locale`, then the deployment default. Description text only; nothing a caller sends is accepted or refused differently. + - **`@objectstack/service-messaging`**: the `sys_user_device.locale` field description says the same, in all four shipped languages. A test now pins it end to end: with the real push channel over the real device registry, a phone registered with a locale other than its user's receives the push in the user's language, on the inline fan-out and on the durable outbox path alike. +- 771b2db: fix(core,service-messaging,service-queue): an idle dispatch loop wakes at the next due time, and a sole writer's loop no longer re-runs the idle ramp after every boot + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/core`: `DispatchLoop` wakes at the next due time.** A new optional `DispatchLoopOptions.nextDueAt()` answers the earliest instant work becomes claimable (a deferred row, a claim's expiry). The loop asks it once per idle streak, after the first empty tick, and never sleeps past the answer, so a deferred row goes out within one tick of coming due whatever `maxIdleIntervalMs` is. The answer is remembered rather than re-read on every ceiling tick, so an idle loop's steady-state cost is unchanged; the new `DispatchLoop.wakeAt(dueAtMs)` lets a producer in the same process announce a deferred row at no read. An answer at or before the tick that found nothing is not chased, and a rejected tick or read still counts as idle, so neither can make the loop spin. + - **`@objectstack/core`: `DispatchLoopOptions.soleWriter`** (default `false`). On a host where no other process writes the store, an empty tick sleeps straight to the next due time or the ceiling instead of re-running the ramp from `intervalMs`, which only ever bought latency on rows another process wrote. A fresh sole-writer loop over an empty store now ticks once in its first minute; before, every boot re-ran the ramp (six ticks in the first minute at the dispatchers' 500 ms base, five at the queue's 1 s). + - **`@objectstack/service-messaging`:** `INotificationOutbox.nextDueAt` / `IHttpOutbox.nextDueAt` (optional; both built-in stores implement it: two reads, the earliest future `next_attempt_at` / `next_retry_at` and the oldest `in_flight` claim plus the claim TTL), wired into `NotificationDispatcher` and `HttpDispatcher`; `soleWriter` on both dispatchers' options, and `dispatchSoleWriter` on `MessagingServicePlugin`. Retries, quiet hours, digest windows and a crashed node's claims are taken at their due time instead of up to `dispatchMaxIdleIntervalMs` late. + - **`@objectstack/service-queue`:** `DbQueueAdapter` reads the earliest future `scheduled_for` of its subscribed queues (one read per idle streak), a deferred `publish()` announces its due time to the loop through `wakeAt` instead of waiting out the backoff, and `DbQueueAdapterOptions.soleWriter` (reachable as `QueueServicePlugin` `db.soleWriter`) skips the ramp. + + Nothing changes for a host that sets neither `soleWriter` flag except the due-time wake: the ramp keeps its shape, cut short only when a due row is earlier, and an idle stretch costs one extra read at its start. +- 06cccd3: fix: the inbox's own notification list no longer shows the raw topic, and an MCP argument refusal ends at the fix + + Clause-②: no + Metadata-protocol: none + + - **`@objectstack/service-messaging`.** The `mine` list view of `sys_inbox_message` (a person's own notifications) showed a Topic column reading the producer's machine id, such as `notify` for a flow's notify node or `collab.mention` for a mention. Nothing labels those ids, so the column is gone from that view: it now shows the title, the severity and the time. The `topic` field itself is unchanged and still searchable, so a filter or another view can use it. + - **`@objectstack/mcp`.** When a tool call carries an argument the tool does not declare, the refusal names the argument and the spelling to send, and now stops there. It no longer ends with the sentence "This argument used to be dropped silently and the call still succeeded, …", which explained the history of the check to internal developers and gave the caller nothing to act on. Every tool still refuses an undeclared argument exactly as before; only the closing sentence is gone. +- a0c68ae: feat(approvals,messaging): approval requests, reminders and outcomes also go by e-mail, linking to the request page; messaging gains a code-declared notification template tier + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/plugin-approvals`: `approval.requested`, `approval.reminder`, `approval.approved` and `approval.rejected` request the `inbox` and `email` channels** in one emit. Before, every approval notice named no channel, so messaging delivered it to the in-app inbox alone and nobody who does not watch the Console bell heard of it. Every other `approval.*` topic stays inbox-only, and delivery preferences filter the e-mail as they filter any channel. The inbox is unchanged. On a deployment with no mail transport the e-mail is whatever the `email` channel answers there. + - **The approval e-mail** is the notice's own title as its subject and its body (in the recipient's language, as the inbox renders it) followed by one line that opens the request, in `en`, `zh-CN`, `ja-JP` and `es-ES`. The link is a new payload member, `approvalUrl`: the request page (`/system/approvals?request=ID`) under the deployment's public origin. `actionUrl` stays relative, as the Console routes on it. It opens the request for a signed-in reader and decides nothing; no ADR-0043 action link or token is in any payload. + - **`publicBaseUrl` now resolves when the option is unset**: `OS_AUTH_URL` (legacy `BETTER_AUTH_URL`), then `OS_BASE_URL`, never a guessed `localhost`. With none of them set, or a value that is not an absolute `http(s)` URL, links stay relative and the plugin warns once at start, naming the variable to set. The same origin also prefixes ADR-0043's `actionLinkUrl`, as its option always documented. To give approval e-mails a working link, set `OS_BASE_URL` (or `OS_AUTH_URL`) to the deployment's address. + - **`@objectstack/service-messaging`: `MessagingService.registerBuiltinTemplates(templates)`** (and the `BuiltinNotificationTemplate` type) declares `(topic, channel, locale)` notification templates in code. The channels' template store reads them at every rung of its locale ladder after the recipient organization's own `sys_notification_template` row: the rung's row, else the rung's built-in, else the next rung, else the generic title and body. Nothing is written to the database (ADR-0131 §1.4), so an organization that authors a row overrides the built-in with nothing to migrate. A malformed template, or a `(topic, channel, locale)` already declared, is refused with a `TypeError`, and nothing from that call is kept. +- ede9e6b: fix(notifications): the `email` channel answers unavailable while the email service only has the log transport, so a notification is no longer recorded as delivered when no mail left the server + + Clause-②: no + Metadata-protocol: none + + - **What was wrong.** plugin-email registers the `email` service on every boot; with no `mail` settings saved its live transport is the log transport, which writes the message to the server log and to `sys_email` and sends nothing. The `email` channel treated "an email service is registered" as "available", so fan-out wrote a `sys_notification_delivery` row per recipient and the dispatcher marked it `success` while nobody was mailed. + - **What changes.** The channel now asks the service's own deliverability answer, `IEmailService.isConfigured()`, on every call. While it answers `false`, `isAvailable()` answers `{ available: false, reason: 'transport_not_configured' }`: fan-out writes no delivery row and records the suppression on `sys_notification.suppressed_channels`, exactly as it does with no email service at all. `send()` refuses a row that was already written with the same `transport_not_configured` reason, graded permanent, and never hands it to the service. No new reason and no new error code. + - **No restart needed.** The answer is read from the live transport, so saving a working provider under Setup → Email makes the channel available on the next notification, and switching back to the log transport makes it unavailable again. + - **Unchanged.** An email service that does not implement `isConfigured` (it is optional on the contract) keeps today's behaviour: the channel stays available and sends through it. `EmailSenderSurface` gains the optional `isConfigured?(): boolean` member, mirroring the contract. +- ca37605: i18n(service-messaging): the notification objects and the digest heading ship in Traditional Chinese, Arabic, French and Brazilian Portuguese + + Clause-②: no + Metadata-protocol: none + + The messaging objects (`sys_inbox_message`, `sys_notification_receipt`, `sys_notification_delivery`, `sys_notification_preference`, `sys_notification_template`, `sys_http_delivery`, `sys_user_device`) and the digest heading "You have {{count}} notifications" now carry `zh-Hant`, `ar`, `fr` and `pt-BR` beside the existing `en`, `zh-CN`, `ja-JP` and `es-ES`, so a recipient in one of those languages reads the notification screens and the digest heading in their own language instead of English. `t()` selects no plural form, so the Arabic heading is worded as a tally (`عدد الإشعارات لديك: {{count}}`), correct for every count. A recipient whose language is `fr-FR` now reads the `fr` pack. Each new locale is registered under its platform-table tag with its source-hash companion and its messages, and the package's extract command lists the tags, so the existing `check:i18n` bundle check covers them. First half of package 5 of #30512; `ru`, `de` and `ko` follow in the second half. +- cabdfe8: i18n(service-messaging): the notification objects and the digest heading ship in Russian, German and Korean + + Clause-②: no + Metadata-protocol: none + + The messaging objects and the digest heading "You have {{count}} notifications" now carry `ru`, `de` and `ko` as well, so the package ships all eleven platform languages. `t()` selects no plural form, so the Russian heading is worded as a tally (`Уведомлений: {{count}}`), correct for every count. Each new locale is registered under its platform-table tag with its source-hash companion and its messages, and the package's extract command lists the tags, so the existing `check:i18n` bundle check covers them. Second half of package 5 of #30512. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/services/service-messaging/package.json b/packages/services/service-messaging/package.json index bfa39126f83..1ac92f17169 100644 --- a/packages/services/service-messaging/package.json +++ b/packages/services/service-messaging/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-messaging", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Messaging Service for ObjectStack — outbound notification dispatch (ADR-0012). Ships the MessagingChannel registry, emit() fan-out, and the always-on inbox channel; other channels (email/webhook/push/IM) plug in.", "type": "module", diff --git a/packages/services/service-package/CHANGELOG.md b/packages/services/service-package/CHANGELOG.md index 43a675b1f0b..1cf6a886953 100644 --- a/packages/services/service-package/CHANGELOG.md +++ b/packages/services/service-package/CHANGELOG.md @@ -1,5 +1,81 @@ # @objectstack/service-package +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [60fe44f] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/metadata-core@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-package/package.json b/packages/services/service-package/package.json index b6c92f8f928..6c1423bf04d 100644 --- a/packages/services/service-package/package.json +++ b/packages/services/service-package/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-package", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Package management service for ObjectStack — publish, install, and manage packages", "type": "module", diff --git a/packages/services/service-print/CHANGELOG.md b/packages/services/service-print/CHANGELOG.md index e45fabb312c..ffa6fd74e52 100644 --- a/packages/services/service-print/CHANGELOG.md +++ b/packages/services/service-print/CHANGELOG.md @@ -1,5 +1,84 @@ # @objectstack/service-print +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [fabd259] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [66a8822] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [59e6463] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/service-storage@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-print/package.json b/packages/services/service-print/package.json index 26e6ae58b11..e94e97d4852 100644 --- a/packages/services/service-print/package.json +++ b/packages/services/service-print/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-print", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Print Service for ObjectStack — renders a record's print page in headless Chromium under the requesting user's own principal and archives the PDF as a file attached to the record.", "type": "module", diff --git a/packages/services/service-push/CHANGELOG.md b/packages/services/service-push/CHANGELOG.md new file mode 100644 index 00000000000..ef269a3326d --- /dev/null +++ b/packages/services/service-push/CHANGELOG.md @@ -0,0 +1,110 @@ +# @objectstack/service-push + +## 18.4.0 + +### Minor Changes + +- 9ebca94: feat(push): `requires: ['push']` and the stack's `push` key turn mobile push on under `objectstack serve`, and a relay configuration is refused at boot + + Clause-②: yes (widening) + Metadata-protocol: added + Skills-docs: follow-up owed — #29969 (skills/objectstack-platform/evals/config-plugins-ops.json: no eval asks for `requires: ['push']` or the `push` key); this diff teaches both in `skills/objectstack-platform/SKILL.md` (the top-level key enumeration and the `requires` table) and in the new `content/docs/kernel/runtime-services/push-service.mdx` + + A deployment that runs `objectstack serve` from a stack file can now turn on mobile push without host code. Before this, the only way to register a push service was a host that composed `PushServicePlugin` in code, so a `notify` node naming `push` had nothing to deliver through on a served stack. + + - **`@objectstack/spec`: the `push` capability token and the `push` stack key.** `push` joins `PLATFORM_CAPABILITY_TOKENS`, provided by `@objectstack/service-push` in the open edition. It is requested, never on the always-on slate. The stack definition gains a top-level `push` key beside `email` and `sms`: it is `PushServiceConfigSchema`, composed as a single value (`COMPOSE_KEY_DISPOSITIONS`). Authoring accepts a direct block, a relay block and absence; it refuses an inline secret, by key name (`key`, `privateKey`, `serviceAccount`, …) with `credentialRef` as the remedy, or pasted into a `credentialRef` (ADR-0143 D3). `PushServiceConfigSchema`'s docblock now names the stack key and `objectstack serve` as its reader; `PushRelayTransportConfigSchema` keeps its `[PLANNED — not yet served]` marker. + - **`@objectstack/core`: the `push` provider row and its argument.** `CAPABILITY_PROVIDERS.push` names `PushServicePlugin`, and `resolveCapabilityArgument('push', …)` builds its argument from the stack's `push` key through `resolvePushCapabilityArg`, read off the provider module as the `email` and `sms` readers are. Both `objectstack serve` and `@objectstack/verify`'s `bootStack` read this table, so an app's tests get the push service its server gets. + - **`@objectstack/service-push`: `resolvePushCapabilityArg`.** It parses the `push` key with `PushServiceConfigSchema` and refuses, before the kernel boots, two configurations: the relay transport (an absent key and `push: {}` read as it), whose sender has not shipped, with a message that says so and prescribes `transport: 'direct'` with an `apns` and/or `fcm` block; and anything the schema refuses, an inline secret first among them, naming each path and the schema's guidance and never the value. On a stack that declares `requires: ['push']` either refusal fails the boot as `Capability "push" (@objectstack/service-push) failed to start: …`. + - **`@objectstack/cli` and `@objectstack/verify`** depend on `@objectstack/service-push`, so the provider the token names resolves out of the box, as the open edition promises. + + To turn push on, add `requires: ['push']` and a direct block naming each vendor's secret by reference: + + ```ts + push: { + transport: 'direct', + apns: { teamId: 'ABCDE12345', keyId: 'KEY1234567', topic: 'com.example.app', environment: 'production', credentialRef: 'APNS_SIGNING_KEY' }, + fcm: { projectId: 'example-app', credentialRef: 'FCM_SERVICE_ACCOUNT' }, + }, + ``` + + In the open tier each `credentialRef` is the name of an environment variable holding the secret. A `push` key on a stack whose `requires` does not name `push` is not read, and `objectstack serve` logs one boot warning saying so. The runtime-services chapter has a new `services.push` page. + +### Patch Changes + +- 8aa2e91: feat(notifications): the `push` channel — a `notify` node naming `push` reaches the recipient's phones through APNs and FCM + + Clause-②: yes (widening) + Metadata-protocol: none + + - **New package `@objectstack/service-push`.** `PushServicePlugin` registers the kernel's `push` service: APNs over HTTP/2 with token-based auth (an ES256 provider token signed with the `.p8` key, reused for fifty minutes) and FCM over the HTTP v1 API (an OAuth access token from the service account). It reads `PushServiceConfigSchema` from its `config` option and refuses an inline secret at boot with the schema's guidance. It serves the direct transport (`transport: 'direct'`, the operator's own APNs key and Firebase project, for a white-label build); the hosted relay, the schema's default and so also an empty config, is refused at init until its sender ships (#29920). Each secret is named by `credentialRef` and resolved when the transport first signs; in the open tier the reference is the name of an environment variable (ADR-0143 D3). + - **`@objectstack/service-messaging`: the `push` channel.** Mounted while a `push` service is registered, as the sms channel is while an `sms` service is. One delivery reaches every enabled device the recipient registered, on each vendor the push service has a transport for. The push carries the notification id and, when it is a same-server path (`PushRouteSchema`), the notification's `actionUrl` as `route`; any other `actionUrl` is left out and the delivery still goes. Each device's push also carries the origin stored on its row (`PushDeliveryDataSchema.origin`) when the row holds one; the registry does not store it yet, so today none does. A token the vendor reports dead (APNs `BadDeviceToken` / `Unregistered`, FCM `UNREGISTERED`) is retired through `invalidateDeviceToken`: the device row is kept, disabled, and the user's other devices still get the push. A recipient with no enabled device, or whose every device was reported dead, is graded `invalid_recipient`; no push service is refused `transport_not_configured`, as the sms and email channels refuse it. + - **`@objectstack/spec`: description and comment text only.** The `[PLANNED]` markers come off `PushServiceConfigSchema`, `PushDirectTransportConfigSchema`, `ApnsTransportConfigSchema`, `FcmTransportConfigSchema` and `PushDeliveryDataSchema`, which now have their reader and producer; `PushRelayTransportConfigSchema` keeps `[PLANNED — not yet served]`, now saying the push service refuses it at boot (#29920). `NotificationChannelSchema`'s JSDoc lists `push` among the registered channels, the docblocks name `@objectstack/service-push` where they named `plugin-notification-push`, and the `NOT_FOUND` synonym waiver's reason names service-messaging's own provenance row. No key, schema shape or export changes. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 diff --git a/packages/services/service-push/package.json b/packages/services/service-push/package.json index ffeaad0e15c..ec81b8cde8c 100644 --- a/packages/services/service-push/package.json +++ b/packages/services/service-push/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-push", - "version": "18.2.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Push service for ObjectStack — IPushService over the APNs (token auth) and FCM HTTP v1 transports, configured by PushServiceConfigSchema with credentials by reference.", "type": "module", diff --git a/packages/services/service-queue/CHANGELOG.md b/packages/services/service-queue/CHANGELOG.md index f2e56201912..7527c2b9296 100644 --- a/packages/services/service-queue/CHANGELOG.md +++ b/packages/services/service-queue/CHANGELOG.md @@ -1,5 +1,112 @@ # @objectstack/service-queue +## 18.4.0 + +### Patch Changes + +- 771b2db: fix(core,service-messaging,service-queue): an idle dispatch loop wakes at the next due time, and a sole writer's loop no longer re-runs the idle ramp after every boot + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/core`: `DispatchLoop` wakes at the next due time.** A new optional `DispatchLoopOptions.nextDueAt()` answers the earliest instant work becomes claimable (a deferred row, a claim's expiry). The loop asks it once per idle streak, after the first empty tick, and never sleeps past the answer, so a deferred row goes out within one tick of coming due whatever `maxIdleIntervalMs` is. The answer is remembered rather than re-read on every ceiling tick, so an idle loop's steady-state cost is unchanged; the new `DispatchLoop.wakeAt(dueAtMs)` lets a producer in the same process announce a deferred row at no read. An answer at or before the tick that found nothing is not chased, and a rejected tick or read still counts as idle, so neither can make the loop spin. + - **`@objectstack/core`: `DispatchLoopOptions.soleWriter`** (default `false`). On a host where no other process writes the store, an empty tick sleeps straight to the next due time or the ceiling instead of re-running the ramp from `intervalMs`, which only ever bought latency on rows another process wrote. A fresh sole-writer loop over an empty store now ticks once in its first minute; before, every boot re-ran the ramp (six ticks in the first minute at the dispatchers' 500 ms base, five at the queue's 1 s). + - **`@objectstack/service-messaging`:** `INotificationOutbox.nextDueAt` / `IHttpOutbox.nextDueAt` (optional; both built-in stores implement it: two reads, the earliest future `next_attempt_at` / `next_retry_at` and the oldest `in_flight` claim plus the claim TTL), wired into `NotificationDispatcher` and `HttpDispatcher`; `soleWriter` on both dispatchers' options, and `dispatchSoleWriter` on `MessagingServicePlugin`. Retries, quiet hours, digest windows and a crashed node's claims are taken at their due time instead of up to `dispatchMaxIdleIntervalMs` late. + - **`@objectstack/service-queue`:** `DbQueueAdapter` reads the earliest future `scheduled_for` of its subscribed queues (one read per idle streak), a deferred `publish()` announces its due time to the loop through `wakeAt` instead of waiting out the backoff, and `DbQueueAdapterOptions.soleWriter` (reachable as `QueueServicePlugin` `db.soleWriter`) skips the ramp. + + Nothing changes for a host that sets neither `soleWriter` flag except the due-time wake: the ramp keeps its shape, cut short only when a due row is earlier, and an idle stretch costs one extra read at its start. +- 8b117c0: fix(service-queue): a job whose worker died mid-handler is reclaimed once its lease runs out, instead of staying `running` forever + + Clause-②: no — restores the documented `leaseMs` reclaim; no accept set or export widens + Metadata-protocol: none + + `DbQueueAdapterOptions.leaseMs` documented a lease "before another worker may reclaim", but the claim read only `pending` rows, so a worker that crashed, was OOM-killed or was redeployed mid-handler left its `sys_job_queue` row `running` for good: never retried, never dead-lettered. + + - **Reclaim.** A poll tick reads the table's `running` rows (one read for every queue, at most once per lease window) and returns each whose `locked_until` has passed to the retry path. The lost run counts as an attempt: the row goes back to `pending` on its backoff, or to `dlq` once `maxAttempts` is spent, with `last_error` saying the lease expired. The write is a compare-and-set on the expired lease, so a holder that renewed in the meantime keeps its run. + - **Renewal.** While a handler runs, its worker moves its lease a full window ahead every third of `leaseMs`, so a slow but live handler is never reclaimed and run twice. A handler must not block the event loop for most of a lease window. + - **Sole writer.** With `soleWriter` no other process claims rows, so the reclaim reads once per boot (finding what a crashed previous process left behind) and again only when one of this process's own claims was left unsettled by a failed write and its lease runs out. `nextDueAt` folds in those leases and the earliest live lease the last read saw, at no read of its own, so the loop wakes for them instead of at the idle ceiling. + - **Cost.** Without `soleWriter`, an idle queue pays one more indexed read per lease window (default 30 s): an idle hour at the defaults goes from 125 to 246 reads. With `soleWriter`, one more read per boot: a fresh kernel's first minute goes from 2 to 3 reads, and an idle hour carries no other. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-queue/package.json b/packages/services/service-queue/package.json index c544200877d..a9b64f1a942 100644 --- a/packages/services/service-queue/package.json +++ b/packages/services/service-queue/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-queue", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Queue Service for ObjectStack — implements IQueueService with in-memory and durable DB-backed (sys_job_queue) adapters", "type": "module", diff --git a/packages/services/service-realtime/CHANGELOG.md b/packages/services/service-realtime/CHANGELOG.md index b44a141d330..97cb6441225 100644 --- a/packages/services/service-realtime/CHANGELOG.md +++ b/packages/services/service-realtime/CHANGELOG.md @@ -1,5 +1,96 @@ # @objectstack/service-realtime +## 18.4.0 + +### Patch Changes + +- ab159a9: i18n(service-realtime): the presence object's labels, help and option names ship in all eleven platform languages + + Clause-②: no + Metadata-protocol: none + + `sys_presence` now carries Traditional Chinese (`zh-Hant`), Arabic (`ar`), French (`fr`), Brazilian Portuguese (`pt-BR`), Russian (`ru`), German (`de`) and Korean (`ko`) beside the existing `en`, `zh-CN`, `ja-JP` and `es-ES`, so a reader in one of those languages sees the object's labels in their own language instead of English. Each new locale is registered under its platform-table tag and carries its source-hash companion, and the package's extract command lists the seven tags, so the existing `check:i18n` bundle check covers them. Part of #30512. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/services/service-realtime/package.json b/packages/services/service-realtime/package.json index 457e44efaac..8d05b53ab0c 100644 --- a/packages/services/service-realtime/package.json +++ b/packages/services/service-realtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-realtime", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Realtime Service for ObjectStack — implements IRealtimeService with WebSocket and in-memory pub/sub", "type": "module", diff --git a/packages/services/service-settings/CHANGELOG.md b/packages/services/service-settings/CHANGELOG.md index ca474553258..af99f46f3e4 100644 --- a/packages/services/service-settings/CHANGELOG.md +++ b/packages/services/service-settings/CHANGELOG.md @@ -1,5 +1,232 @@ # @objectstack/service-settings +## 18.4.0 + +### Patch Changes + +- 8d5aab3: fix(cli): `os dev` and `os start` share one data-key file, so a secret saved under `npm run dev` reads back under `os start` + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/cli`.** Both commands open the same project database, but `os start` used to mint its own data key under its own home (`PROJECT/.objectstack/dev-crypto-key` in project mode) while `os dev` kept the key at `$HOME/.objectstack/dev-crypto-key` (or `$OS_HOME`'s). Every secret written during development then read back as unset under `os start`. Both commands now resolve the key file the way `os dev` always has (`os start --home` counts as `OS_HOME`) and hand it to their `serve` child on the internal `OS_INTERNAL_CRYPTO_KEY_FILE` channel. The project-local file an earlier `os start` minted is not orphaned: when it alone holds a key it is still read, and the boot says which file; when it and the shared file hold different keys, the shared one is used and a warning names both paths. An explicit `OS_SECRET_KEY` / `OS_DEV_CRYPTO_KEY` still wins, unchanged. + - **`@objectstack/service-settings`.** New exports `resolvePersistedKeyFile` (the one resolution both commands take, beside the provider's own key-file path) and `KEY_FILE_CHANNEL_ENV`; the local crypto provider's file tier reads the file that channel names, ahead of `OS_HOME`. +- f6f663a: fix(cli): when two data-key files hold different keys, `os start` and `os dev` read with the one that opens the project's stored secrets, or a production boot refuses + + Clause-②: yes (narrowing) — a production `os start` / `os serve` boot whose two candidate key files (the shared `$HOME/.objectstack/dev-crypto-key` or `$OS_HOME`'s, and the project's `.objectstack/dev-crypto-key`) both fail to open the database's newest stored secret now refuses to start, naming both files; it used to start and serve every stored secret as unset. The widening half: `@objectstack/service-settings` gains new exports (`chooseKeyFileByTrialOpen`, `settleKeyFileChannel`, `storedSecretReaderOverEngine`, `deferredLocalCryptoProvider`, `DataKeyOpensNothingError`, `KEY_FILE_CANDIDATE_CHANNEL_ENV` and their types). + Metadata-protocol: none + + + + **BREAKING** boot narrowing (`@objectstack/cli`, `@objectstack/service-settings`): a production boot whose candidate data keys open none of the stored secrets refuses to start. + + - **`@objectstack/cli`.** When the shared key file and the project's own key file (the one an earlier `os start` minted) hold different keys, the CLI no longer picks one. It hands the `serve` child both, and the child decides once the database is open: it opens the newest stored `sys_secret` value under each key and reads with the key that opens it, and the boot says which file it took. A project that only ever ran `os start`, on a machine whose home holds a key from `os dev` in another project, reads its secrets back again. With no stored secret yet, the shared file is used, as before. When neither key opens the stored secrets, a production boot (`os start`, `os serve`) refuses to start and names both files; a development boot (`os dev`) starts and prints a boot error naming both files. The one-shot commands (`os migrate`, `os secret rewrap`, `os secret orphans`, `os storage orphans`) now read the same file by the same rules: the project's own file when it alone holds a key, and the key that opens a stored secret when both do. + - **`@objectstack/service-settings`.** The settings service makes that choice in its `start()`, before any reader decrypts, when the CLI names a second candidate on the internal `OS_INTERNAL_CRYPTO_KEY_FILE_CANDIDATE` channel. A leading `~` in `OS_HOME` (or `OBJECTSTACK_HOME`) is now the home directory when the provider finds its key file, as `os start` already read it; it used to name a directory called `~` under the working directory. + + **The remedy.** If a production boot refuses with "neither data key opens this database's stored secrets", find the key those secrets were sealed under (a backup of either file, or the `OS_SECRET_KEY` the deployment once ran with), keep a copy of both files, and put that key at the shared file the message names, or set `OS_SECRET_KEY` to it. + + Supersedes, in the same release: the note for #29934 says that when the shared file and the project's file hold different keys, the shared one is used and a warning names both paths. After this change the boot uses whichever of the two opens the stored secrets, and refuses in production when neither does. +- 159930a: fix(service-settings): when two data-key files hold different keys, the boot judges the newest stored secrets as a page, not only the newest one, and reports a database split between the two keys + + Clause-②: no — an internal boot heuristic and its notice text. The shapes it changes (`StoredSecretReader` now answers the page, `KeyFileChoice` gains the `mixed` reason and a `tally`, `DataKeyOpensNothingError` gains `judged`) were added by #30042, whose note is still pending, so they first ship together in this release; no released export changes. + Metadata-protocol: none + + - **`@objectstack/service-settings`.** When the shared key file and the project's own key file hold different keys, the boot now trial-opens each of the newest stored `sys_secret` rows (up to ten) under both keys and reads with the key that opens more of them, on a tie the one that opens the newest. A row that opens under neither key (a corrupt or foreign row) no longer decides the boot: if an older row opens under one key, that key is used, and only a page where no row opens under either key keeps the production refusal. When each key opens some of the rows, the database holds secrets under both keys: the boot uses the key that opens more of them and prints an error naming both files, how many rows each key opened, and the fix. The boot's notices now say what was measured ("opens the 3 newest stored secrets", "opens 2 of the 3 newest stored secrets") instead of claiming the chosen file opens all of the database's stored secrets. + - **`@objectstack/cli`.** The one-shot commands (`os migrate`, `os secret rewrap`, `os secret orphans`, `os storage orphans`) share the same trial, so they make the same choice. + + **The remedy for a split database.** Keep a copy of both files, then save again each secret that reads as unset: it is sealed under the key the boot uses. Once nothing reads as unset, put that key at the shared file (or set `OS_SECRET_KEY` to it). + + Supersedes, in the same release: the note for #30042 says the boot opens the newest stored secret under each key and quotes the refusal as "neither data key opens this database's stored secrets". After this change the boot judges the newest rows as a page, and the refusal reads "neither data key opens any of the N newest stored secrets". +- 870ebce: fix(plugin-auth)!: saving the auth settings refuses to turn off the last way to sign in (#30202) + + Clause-②: yes (narrowing) — a save an administrator could make before is now refused at the settings save door with the existing `SETTINGS_VALIDATION` / `invalid_value` field error: a save after which email and password is off and nothing else signs anyone in, judged on the whole save, including the Google change that removes the last provider while email and password is off. No new error code or settings key. The widening half: `SettingsWritePredicateInput`, exported from `@objectstack/service-settings`, gains an optional `batch` field, the whole write a predicate is asked about. + Metadata-protocol: none + Skills-docs: none — no file under `skills/**` teaches `auth.email_password_enabled` or the settings write predicate; `content/docs/deployment/environment-variables.mdx` already names the `OS_AUTH_EMAIL_PASSWORD_ENABLED` override used for recovery below. + + + + **BREAKING** accept-set narrowing at the auth settings save door (`PUT /api/settings/auth`, `SettingsService.setMany('auth', …)`): a save that would leave no way to sign in is refused, where it was saved before. + + - **Before.** Setup → Settings → Authentication, "Enable email/password login" off, save: "Settings saved", with no social provider and SSO off. After the next sign-out nobody could sign in, the administrator included. + - **Now.** A save after which email and password is off and no other way in is in force answers `400 SETTINGS_VALIDATION`. The field error sits at `email_password_enabled` (`code: invalid_value`, `constraint.lastSignInMethod: email_password`), and its message, in the caller's language (en, zh-CN, ja-JP, es-ES), says to keep or set up another sign-in method before or in that save. Nothing in that batch is written, and email and password stays on. + - **The whole save is judged.** `{ email_password_enabled: false, google_enabled: false }` (or with `google_client_secret` cleared) on a deployment signing in with email and password plus Google is refused, with a field error at each of the two keys. Google filled in (switch, client ID and secret) in the same save that turns email and password off is accepted, and so is the switch alone over a client ID and secret already stored. + - **What counts as another way in.** A social or OIDC provider in force whose client ID and client secret are both set; enterprise SSO switched on with at least one identity provider registered (the switch alone does not count); phone-number sign-in; a host sign-in handoff the host declared. `ssoOnlyMode` alone, magic link, passkeys, and a provider listed without its credentials do not count. + - **The other direction.** While email and password is off, `google_enabled: false`, or clearing `google_client_id` / `google_client_secret` when no environment variable or stack config supplies it, is refused (`constraint.lastSignInMethod: google`) if Google is the last way in. + - **Already locked out.** Set `OS_AUTH_EMAIL_PASSWORD_ENABLED=true` and restart: the environment override wins over the stored `false`, and email and password sign-in comes back. Then set up another method before turning it off again. + - **For a write predicate's author (`@objectstack/service-settings`).** `SettingsWritePredicateInput.batch` holds every key the write sets in the namespace, the asked key among them, with the value it would store (`null` clears). `setMany` always passes it, frozen. A rule over several keys judges the state after the whole write from it instead of one key against the rest as they stand. Existing predicates need no change. +- d1d90b7: fix(plugin-auth)!: clearing the allowed IP ranges applies at once, and a console save that leaves out the saving address is refused + + Clause-②: yes (narrowing) — a settings console save (`PUT /api/settings/auth`) of an `allowed_ip_ranges` list none of whose entries matches the saving request's own address is refused, where it was stored before; and `@objectstack/service-settings` widens its published surface: three new exported types (`SettingsWriteOptions`, `SettingsRequestTransport`, `SettingsWritePredicateOptions`), `setMany`'s fourth parameter accepts `SettingsWriteOptions`, `registerWritePredicate` takes an optional fourth `options`, and `SettingsWritePredicateInput` gains an optional `transport`. + Metadata-protocol: none + Skills-docs: none — `skills/**` teaches no `allowed_ip_ranges`; `content/docs/permissions/authentication.mdx` describes the setting and does not yet state the save refusal or the recovery, which ship here in the refusal text and this entry (the page is outside this change's surface and is named for a follow-up). + + + + **BREAKING** for an administrator who saves, through the settings console, an allowed-IP list (ADR-0069 D5) that does not include the address they are connected from. + + - **Clearing applies at once.** The console sends an emptied field as `null`, which resolves with source `default`, and the settings binding used to skip a default-sourced `allowed_ip_ranges`: the last stored list kept refusing sign-in (`403 IP_NOT_ALLOWED`) until a restart while the field showed empty. A cleared or restored-default value now hands the gate back to the deployment's own declaration in the same pass: no list when it declared none, the `allowedIpRanges` the plugin options or an `auth:configure` handler declared otherwise. + - **The save is refused.** A list no entry of which matches the saving request's address is refused with `400 SETTINGS_VALIDATION`, `invalid_value` at `allowed_ip_ranges`, the address in `constraint.clientAddress`, and a message in the caller's language that names it. The address is the one the gate judges at sign-in: the transport's peer address, or the forwarded one when the stack declares `server.trustProxy`. When the server cannot establish the address, every non-empty list is refused, because the gate would refuse that request too. Clearing the list is never refused, and an in-process write (boot, seed, migration) carries no request and is not judged. + - **Recovery when already locked out.** Start the server with `OS_AUTH_ALLOWED_IP_RANGES` set to an empty value. The settings env override wins over the stored list and opens the gate; remove it once the list is fixed. + - **`@objectstack/service-settings`.** `setMany`'s options (`SettingsWriteOptions`) and a write predicate's input gain an optional `transport` (`SettingsRequestTransport`: the transport's peer address and request headers), which the settings routes set on a save so a write predicate can judge the change against the caller's own connection. A predicate sees only the headers it names at registration (`registerWritePredicate(…, { requestHeaders })`, `SettingsWritePredicateOptions`), so a credential header never reaches one that did not ask for it. The transport identifies and authorizes nothing and is never stored. + - **Who moves.** An administrator adds their own address or range to the list, or saves it from an address inside it. +- 977be38: fix(service-settings)!: Setup no longer offers a Feature Flags page whose four toggles changed nothing (#30204) + + Clause-②: no (narrowing) — removes the four settable keys of the `feature_flags` settings namespace (`ai_enabled`, `kanban_swimlanes`, `realtime_cursors`, `inline_comments`), the namespace itself (left empty by their removal), its Setup entry and the `featureFlagsSettingsManifest` export. Nothing is widened. + Metadata-protocol: none + + + + **BREAKING** for an administrator who saved one of the four Feature Flags toggles, and for a host that imports `featureFlagsSettingsManifest`. + + - **Why.** No code read any of the four keys, so turning one on changed nothing (ADR-0049 enforce-or-remove). A flag comes back with the feature that reads it. + - **Removed keys.** `feature_flags.ai_enabled`, `feature_flags.kanban_swimlanes`, `feature_flags.realtime_cursors` and `feature_flags.inline_comments`: removed; each had no effect. The in-app AI panel is decided elsewhere and is unchanged. + - **The namespace.** `feature_flags` is no longer registered, so `GET` and `PUT /api/settings/feature_flags` answer `404 UNKNOWN_NAMESPACE`, and `SettingsService.getManifest('feature_flags')` throws `UnknownNamespaceError` (`SETTINGS_UNKNOWN_NAMESPACE`). Setup's Configuration group no longer lists Feature Flags (`@objectstack/platform-objects`, entry `nav_settings_feature_flags` and its four locale labels). + - **Stored values.** A `sys_setting` row saved under `feature_flags` and an `OS_FEATURE_FLAGS_*` environment variable are left in place and become inert: nothing registers the namespace, so nothing resolves them. They are not cleaned up; delete them at will. + + The one-line fix for a host: + + - FROM `featureFlagsSettingsManifest` TO nothing: drop it from the host's `SettingsServicePlugin({ manifests })` list; `builtinSettingsManifests` no longer carries it. +- db04e5c: fix(plugin-email,service-settings): mail whose sender has no From name goes out under the product name, not "ObjectStack" + + Clause-②: no + Metadata-protocol: none + + - **Now.** A default sender with no display name of its own goes out under the product name the auth mails' subject and body already use: the Setup → Branding product name when an administrator set one, else the deployment's name (`OS_APP_NAME`, then `config.email.appName`, `config.email.defaultTemplateContext.appName` and the top-level `appName`), else `ObjectStack`. A Branding save renames the sender on the next mail, with no restart. The "Send test email" button sends under the same name. + - **Which senders.** The sender saved in Setup → Mail with the From name left empty, and a sender declared at boot without a name (`OS_EMAIL_FROM=no-reply@acme.com`, or `config.email.defaultFrom` with an `address` alone). Before, the first went out as `ObjectStack <…>` and the second with no display name. + - **Unchanged.** A From name somebody set always wins: one saved in Setup → Mail, or one declared at boot (`OS_EMAIL_FROM="Acme "`, `config.email.defaultFrom.name`). A sender a send names itself, and a template's own sender, are never renamed. Under a walled tenancy posture, where Setup → Branding is not offered, the deployment's name applies. + - **`@objectstack/service-settings`.** Setup → Mail → From name no longer has the default `ObjectStack`: an unsaved field reads empty, and `GET /api/settings/mail` answers `from_name` as `value: null`, `source: 'default'`. The field's help says that an empty value uses the product name. A From name saved earlier is kept and still wins; clear the field to follow the product name. +- bee9836: fix(service-settings)!: Setup's default language (`localization.locale`) offers and accepts the languages the deployment supports, not four hard-coded ones (#30286) + + Clause-②: yes (narrowing) — `SettingsService` gains `registerOptionSource(namespace, key, source)` and the package exports the `SettingsOptionSource`, `SettingsOptionSourceAnswer` and `SettingsServedOption` types; `getNamespace` takes an optional third `display` argument. The save of `localization.locale` (`PUT /api/settings/localization`, `SettingsService.setMany`) and an `OS_LOCALIZATION_LOCALE` override now accept the i18n service's locale list instead of the declared `en-US`, `zh-CN`, `ja-JP`, `es-ES`: a language the deployment supports is accepted where it was refused (`fr-FR`, `vi-VN`), and one of the four the deployment does not support is refused where it was accepted (`ja-JP` on an app that declares `i18n.supportedLocales: ['en', 'zh-CN']`). + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing on the `localization.locale` settings save (Setup → Localization → Default language, `PUT /api/settings/localization`, `SettingsService.setMany`) and on an `OS_LOCALIZATION_LOCALE` override, on a deployment whose language list leaves out one of `en-US`, `zh-CN`, `ja-JP`, `es-ES`. + + - **Before.** Default language offered English (US), 简体中文, 日本語 and Español (España), and the save accepted exactly those four, whatever the deployment shipped. A French pack, or a Vietnamese translation item the user's language menu already offered, could not be chosen as the organization's default. + - **Now.** The options and the accepted values are the i18n service's locale list, the one `GET /api/v1/i18n/locales` and the user's language menu read (the app's declared `i18n.supportedLocales`, else every locale a bundle or translation item has loaded), asked at each save and page read, so a translation item added at runtime is offered at once. Each option is named in its own language (the platform locale table's name, else `Intl.DisplayNames` in that language, else the code). A stored spelling of an offered language is still accepted and shown chosen (`en-US` where `en` is offered, `zh-CN` where `zh-Hans` is). A value outside the list is refused `400 SETTINGS_VALIDATION` with `invalid_option` at `locale`, listing the offered values. With no i18n service in the composition the declared four stand. + - **Unchanged.** A value stored before this release that the deployment no longer offers stays in effect and readable: the page offers it, named as no longer offered in the reader's language, and a save of other keys passes it by; only a save that writes it again is judged. Every reader of the setting (formatting, auth email and SMS, seeds, activity titles) reads it as before. + + **The remedy.** To keep offering a language the deployment refuses, ship it: add the language to the app's `i18n.supportedLocales` (and its translations), then pick it in Setup. +- 590724f: feat: a settings toggle can declare the question asked before it is turned off (`confirmOffText`), and Setup's email and password sign-in toggle declares one + + Clause-②: yes (widening) + Metadata-protocol: added + + - `SpecifierSchema` (`@objectstack/spec/system`) gains an optional `confirmOffText: I18nLabel`: the confirm dialog text shown before a toggle's value changes to off. Omitted, nothing is asked, so every existing manifest reads as before. The name mirrors the `confirmText` an `action_button` handler already declares. + - Only a `toggle` may declare it. On any other specifier type the parse refuses it at `confirmOffText`, naming the type, because nothing would ever ask the question there. + - The platform translation bundle carries it at the toggle's own address, beside its label: `settings..keys..confirmOffText`. + - `@objectstack/service-settings`: the `auth` manifest's `email_password_enabled` toggle declares "People who sign in with a password will need another way in.", with zh-CN, ja-JP and es-ES text in the shipped bundles. The settings translation coverage test now holds a declared `confirmOffText` to the same per-locale coverage as the toggle's label. + - The console asking the question before save is a separate change; until it lands, the key is declared and parsed but not yet shown. +- b55d6ab: fix(service-settings): the settings manifest list serves a sourced select's options, the table the save judges against, as the namespace read does + + Clause-②: no — the manifest list now states the table the save already enforces; nothing a caller sends is refused differently. + Metadata-protocol: none + + A select key whose options come from the running system (`localization.locale`: the languages the deployment's i18n service reports) was served its live options only by the namespace read (`GET /api/settings/:namespace`). `SettingsService.getManifest()`, `SettingsService.listManifests()` and `GET /api/settings` still carried the declared table (the four codes `en-US`, `zh-CN`, `ja-JP`, `es-ES`), so a reader of the manifest list (the Setup hub, an SDK caller) was told a table that is not the one a save is judged against. + + - **Now.** All three are served through the same resolution as the namespace read: a sourced key carries its source's options; every other key, and a sourced key whose source has no answer (no i18n service in the composition), carries the declared table as before. `getManifest(namespace, display?)` and `listManifests(ctx?, display?)` take an optional display language; `GET /api/settings` passes the request's. The registered manifest is never changed. + - **Spec.** The `valueDomain` description in `settings-manifest.zod.ts` names the runtime option source beside `valueDomain` as the other mover of a select's enforcement boundary: where a source answers, the declared `options` are the fallback. +- 1c5cee6: fix(service-settings): an `OS_LOCALIZATION_LOCALE` naming a language the boot is still loading is no longer dropped, and Setup's default language accepts by the same locale matcher as the IdP locale claim (#30456) + + Clause-②: yes (widening) — `@objectstack/core` exports `resolveOfferedLocale(requested, offered)`; `SettingsService` gains `holdOptionSourceVerdicts()` and `sealOptionSources()`. The save of `localization.locale` (`PUT /api/settings/localization`, `SettingsService.setMany`) and an `OS_LOCALIZATION_LOCALE` override now also accept a region or base of an offered language (`fr-FR` where `fr` is offered, `fr` where `fr-FR` is) and another case of an offered code (`zh-cn` for `zh-CN`); nothing accepted before is refused. The IdP locale claim (`resolveIdpLocale`) lands a script or region spelling on the offered code the platform locale table says it means (`zh-TW` on `zh-Hant`, not on a `zh-Hans` listed first). + Metadata-protocol: none + + - **The boot window.** The deployment's locale list fills during the boot: the app's authored translations arrive at `kernel:ready`. A `kernel:ready` reader of `localization.locale` (auth's mail and SMS default language) could ask before the language an `OS_LOCALIZATION_LOCALE` override names was in the list; the override was then reported `IGNORED` and the reader kept the next layer's value, though the boot went on to offer that language. `SettingsServicePlugin` now holds the option verdict on a sourced key until `kernel:bootstrapped`: while held, an override the list does not admit yet is in force and not reported; at `kernel:bootstrapped` it is judged again, and one the list still does not admit is reported once, as before, and a settings change is emitted for `localization.locale` so a reader that applied it reads the key again. The other value checks (pattern, domain, bounds, format) are judged at once, as ever. A `SettingsService` constructed without the plugin judges at once, as it always has. + - **One matcher.** Setup's default language and the IdP locale claim now both ask `resolveOfferedLocale` (`@objectstack/core`): `resolveLocale`'s exact and case-insensitive steps, then the platform locale table's language (`zh-TW` is `zh-Hant`, `en-US` is `en`), then `resolveLocale`'s base-language and variant steps. A language one door takes, the other takes too. + - **Comment.** `AUTH_EMAIL_TEMPLATE_LOCALES` (plugin-auth) no longer claims to be the setting's options: since #30286 the setting offers the deployment's locale list, and the two ends agree through `normalizeAuthEmailLocale`'s pass-through and the email service's `en-US` fallback. +- bdbaf80: fix: Setup's form help speaks to an administrator, and the queue pages, the mail status banner and the test-email toast read in Chinese on a zh-CN console + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Field help (English source and zh-CN):** the permission set and position assignment forms, the out-of-office (approval delegation) rule, and the business unit, organization and user forms no longer describe the implementation. Gone: "fail-closed", "half-open [from, until)", "agent grants", "graph semantics", "expanded by graph queries", "Null = root of tenant", "HRIS", "When true / When false" (zh-CN 「为 true 时」 now reads 「开启后」) and an HTTP status code. The help now says when an assignment starts and ends, what turning a switch on does, and what to fill in. Text only: no field name, key, option value or behaviour changes. + - **`@objectstack/plugin-sharing`:** `sys_queue` and `sys_queue_member` join the plugin's translation bundle, with zh-CN text, so the 队列 and 队列成员 pages, their columns, help and the 新建 button read in Chinese. ja-JP and es-ES carry English fills for now. + - **`@objectstack/plugin-email`:** the live-status statement at the top of Setup → Email and the "Send test email" answer (sent, failed, logged only) are written in the caller's display language, en and zh-CN today; another language gets English. + - **`@objectstack/service-settings`:** a settings action handler (`SettingsActionHandler`) receives an optional `locale`, the caller's display language read from the request the same way the live-status statement's is (`Accept-Language`, else `?locale`); `runAction` takes it as a fifth `display` argument. Existing handlers compile and behave unchanged. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-settings/package.json b/packages/services/service-settings/package.json index 10a252ba734..bfafc47cde9 100644 --- a/packages/services/service-settings/package.json +++ b/packages/services/service-settings/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-settings", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Settings service for ObjectStack — manifest registry + K/V resolver (OS_* env > Tenant > User > Default) + REST routes. See ADR-0007.", "type": "module", diff --git a/packages/services/service-sms/CHANGELOG.md b/packages/services/service-sms/CHANGELOG.md index 16c9a5cde2c..9263ac231f1 100644 --- a/packages/services/service-sms/CHANGELOG.md +++ b/packages/services/service-sms/CHANGELOG.md @@ -1,5 +1,98 @@ # @objectstack/service-sms +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [c23501b] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [9afcd6a] +- Updated dependencies [870ebce] +- Updated dependencies [d1d90b7] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [a93ef3e] +- Updated dependencies [0e4dfbe] +- Updated dependencies [b5765be] +- Updated dependencies [5a878a3] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [26c2efd] +- Updated dependencies [f7afab8] +- Updated dependencies [455b4f0] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [85c7374] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [c92b887] +- Updated dependencies [21510bb] +- Updated dependencies [6dd2ca5] +- Updated dependencies [0fd363d] +- Updated dependencies [a7293f7] +- Updated dependencies [63a470a] +- Updated dependencies [4851668] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/plugin-auth@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-sms/package.json b/packages/services/service-sms/package.json index f4fce1b63fa..2c97bc484ab 100644 --- a/packages/services/service-sms/package.json +++ b/packages/services/service-sms/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-sms", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "SMS service for ObjectStack — ISmsService + transport-pluggable outbound delivery (Aliyun / Twilio / log).", "type": "commonjs", diff --git a/packages/services/service-storage/CHANGELOG.md b/packages/services/service-storage/CHANGELOG.md index 2526da9231d..d3cb37e246b 100644 --- a/packages/services/service-storage/CHANGELOG.md +++ b/packages/services/service-storage/CHANGELOG.md @@ -1,5 +1,118 @@ # @objectstack/service-storage +## 18.4.0 + +### Patch Changes + +- fabd259: fix(service-storage): Setup → Storage → Test connection answers in the caller's language + + Clause-②: no + Metadata-protocol: none + + The `storage/test` settings action writes its `message` in the language the settings action route hands its handler as `locale` (`Accept-Language`, else `?locale`), the same source the mail test notice reads. It covers the sentences the product writes: the success sentence (which still names the adapter), the refusal when the probe read back is not the one written, and the refusal for S3 settings missing a bucket or a region. An adapter's own error text stays verbatim: alone in English, as before; in another language behind a lead-in saying the connection test failed. Every language on the platform locale table is served (`en`, `zh-CN`, `zh-Hant`, `es-ES`, `ar`, `fr`, `pt-BR`, `ru`, `de`, `ja-JP`, `ko`), the caller's code read through `platformLocaleTagOf` and then shortened one subtag at a time; an absent locale, or one the table does not name, is answered in English exactly as before. `ok`, `severity`, the probe and its cleanup, and the operator log lines are unchanged. +- 66a8822: i18n(service-storage): the file and upload-session objects ship in all eleven platform languages + + Clause-②: no + Metadata-protocol: none + + `sys_file` and `sys_upload_session` now carry Traditional Chinese (`zh-Hant`), Arabic (`ar`), French (`fr`), Brazilian Portuguese (`pt-BR`), Russian (`ru`), German (`de`) and Korean (`ko`) beside the existing `en`, `zh-CN`, `ja-JP` and `es-ES`, so a reader in one of those languages sees the file and upload screens in their own language instead of English. Each new locale is registered under its platform-table tag with its source-hash companion, and the package's extract command lists the seven tags, so the existing `check:i18n` bundle check covers them. Package 10 of #30512. +- 59e6463: fix(service-storage)!: a stored file's uploader is the user who created it, as an attachment's and a comment's author are + + Clause-②: no (narrowing) — a non-system create of `sys_file` records the session's user as `owner_id`, whatever the request carries, and one whose session names no signed-in user is refused. Nothing is widened. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing on `sys_file` creates (`@objectstack/service-storage`), served by every data door (REST, MCP, the console). + + - **Now.** A stored file's uploader is the user who created it, as an attachment's (`sys_attachment.uploaded_by`) and a comment's (`sys_comment.author_id`) author are. A non-system create of `sys_file` records the session's user as `owner_id`, over whatever the request carries. The column was already read-only on update, so the stamp is the row's uploader for life. + - **Refused.** A non-system create of `sys_file` whose session names no signed-in user is refused with 403 `RECORD_NOT_ACCESSIBLE`, the answer an attachment or comment create with no signed-in user already gets: there is no user to record as the uploader. + - **Unchanged.** The upload doors (presigned, chunked, complete) already record the signed-in user under the system context, and keep doing so. System-context writes (seeds, imports, migrations) keep the `owner_id` they supply, as does a programmatic create with no session at all on a bare kernel. An update of `owner_id` is dropped and reported in `droppedFields`, as before. Stored rows keep what they were written with. The console needs no change: it never creates `sys_file` rows through the data API. + + **The remedy.** None for the console or the upload doors. A client that creates a `sys_file` row through the data API gets the row recorded under its own user, whatever `owner_id` it sends; to record a different uploader, write from a system context (a migration or an import). A server-side caller that creates `sys_file` rows with no signed-in user runs as the system principal or names the user it acts for. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/observability@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/services/service-storage/package.json b/packages/services/service-storage/package.json index 684c1b4716a..10af8fb5ad4 100644 --- a/packages/services/service-storage/package.json +++ b/packages/services/service-storage/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-storage", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Storage Service for ObjectStack — implements IStorageService with local filesystem and S3 adapter skeleton", "type": "module", diff --git a/packages/skills/CHANGELOG.md b/packages/skills/CHANGELOG.md index 3c1e21191db..88fec1c46c0 100644 --- a/packages/skills/CHANGELOG.md +++ b/packages/skills/CHANGELOG.md @@ -1,5 +1,134 @@ # @objectstack/skills +## 18.4.0 + +### Minor Changes + +- d59a1eb: feat(spec): a `page:header` declares `placedActions: true`, the explicit opt-in to draw every action of the bound object placed at `record_header` / `record_more`, read live (#30018) + + Clause-②: yes (widening) + Metadata-protocol: added + + - **`placedActions`** on a `page:header` component's `properties` (`PageHeaderProps`, the `ComponentPropsMap['page:header']` row): `true` asks the header to draw every action of the page's object whose `locations` include `record_header` or `record_more`, read live from the object's metadata, in place of an authored `actions` id list. An action another package places on the object is then drawn without editing the page. Optional with no default: an unset key stays unset in the parsed output. + - **The `actions` list keeps its meaning.** An authored list is the whole set the header draws, and a header with neither `actions` nor `placedActions: true` draws none of the object's actions. `placedActions: false` beside a list is accepted and changes nothing. + - **Refused together.** `placedActions: true` beside an authored `actions` list, an empty one included, is refused at `properties.placedActions` by `os validate`, `os build`, `os lint` and the page save door (`component-props-invalid`, an error). The message names both keys and asks the author to keep one: `actions` for a curated list that is the whole set, or `placedActions: true` for every placed action. Nothing that parsed before is refused: until this release `placedActions` was an unknown key on the closed shape. + - **Declared ahead of its reader.** The console's header renderer does not read the key yet, so a header that sets it draws what the same header without it draws (none of the object's actions unless `actions` names them). The key's describe opens with `[PLANNED — not yet rendered]` until the renderer reads it. + - The published JSON Schema `ui/PageHeaderProps` still accepts the pair: the rule depends on the value `true`, which no projected refinement pattern states, so the parse refuses it and `dropped-refinements.baseline.json` records the site. + - `@objectstack/skills`: `objectstack-ui` `rules/pages.md` names the key in the component catalogue, marked not drawn yet, and drops a refused-shape clause from the print-page paragraph that the sentence before it already states. + +### Patch Changes + +- bc9eaf6: docs(skills): the automation skill's OAuth recipe teaches the installer-supplied client + + Clause-②: no + Metadata-protocol: none + + Recipe 6 of `objectstack-automation/references/integration-recipes.md` now shows `clientSource: 'installer'` beside a declared `clientId` + `credentialRef`, with when to use which: a declared client for an app its publisher deploys, the installer form for one installed on deployments the publisher never sees. The administrator registers a client with the vendor against the `redirectUri` the connection's status read answers, connects with `{ clientId, clientSecret }` (no secret for a public client; a reconnect naming no client keeps the stored one), and the connection reads back `client: { source: 'installer', clientId, hasSecret }`, never the secret. +- 0d40aa1: docs(skills): with no `searchableFields` declared, a field marked `searchable: true` joins the default search set + + Clause-②: no + Metadata-protocol: none + + - **Before.** `objectstack-data`, `objectstack-query` and the `objectstack-ui` list-views rule taught the record-search auto-default as type-only: the name field plus the text-like and `select` columns. A field marked `searchable: true` was described as doing nothing, and a `$searchFields` override naming one as refused. + - **Now.** Each says what the platform resolves: with no `searchableFields` declared, the set `search` scans, and the set a `$searchFields` override may name, is the type-based default plus every field marked `searchable: true`, of any type except `secret`, `password`, `vector` or a virtual `formula`. Each also says what does not change: `searchable: false`, the default, never removes a field the type default scans, and declaring `searchableFields` chooses the set. The enum half of the type default is spelled `select` alone, as the resolver has it. + - No schema, key or behaviour changes. +- 89803c5: docs(skills): the platform skill teaches the `contributions` member of the package customizations read + + Clause-②: no + Metadata-protocol: none + + `skills/objectstack-platform/SKILL.md` now names `contributions` beside the overrides, extensions and switched-off items that `GET /packages//customizations` lists: one entry per item another package adds to the base, stamped with its `packageId`; a `field` or `flow` entry names the base's `object` it is on, a `nav` entry names the base's `app`, its `group` when it targets one, and the item id as `name`; `counts.contributions` counts items, not packages. The text is paid for by deleting three restatements in the same file, so the token ceiling is unchanged. +- 322b6bf: docs(skills): objectstack-automation says a flow's record nodes treat a computed filter operand as a value, never as a query operator + + Clause-②: no + Metadata-protocol: none + + - **Now.** § Filter operands states the rule from the service-automation change: a computed operand is a string, number, bigint, boolean or date, or at an `$in` / `$nin` / `$between` comparand a list of them with no empty entry. Anything else fails the node as a guard before the query runs. The fix: make the expression evaluate to the compared value (for a record, its `id`), and write conditions in the filter itself. The table of failures a `fault` edge does not route now names that refusal beside the `null` operand. + - `SKILL.md` stays under its token ceiling (no ceiling moves). No schema, key, command or behaviour changes. +- dd9add6: docs(skills): objectstack-platform says the D11 close refuses a package-less write only where the row moves as a declared override, and an added email-template locale keeps that write + + Clause-②: no + Metadata-protocol: none + + - **Before.** The ADR-0146 customization paragraph said a pre-D11 unbound Setup row's package-less save, reset and rollback are refused once it is movable. That is false for a row of an email-template locale the base does not ship: `os migrate packageless-overlays` moves it as the pair's own member (`addedMember`, no `overrides`), and the close judges only the declared-override move, so that row's package-less write stays open before and after its move. The clause also left out publish, which the close refuses too. + - **Now.** The clause says the close covers a row movable as a declared override, lists save, publish, reset and rollback, and says an email-template row at a locale the base does not ship moves as the pair's own member and keeps that write. The file stays at its token ceiling: a duplicate "CLI Commands" pointer section is cut, its home the Part 3 pointer to `references/operations.md`. No rule is removed; no schema or behaviour changes. +- 987ff33: docs(skills): teach that a save and `os validate` refuse `titleFormat` + + Clause-②: no + Metadata-protocol: none + + - `objectstack-data` (object properties table) and `objectstack-formula` (template carriers) no longer call `titleFormat` deprecated: since #29499 the metadata save gate and `os validate` refuse an object that declares it (`title-format-retired`), and it moves to `nameField`. The formula skill drops the sentence that restated the template grammar cell beside it. + - ADR-0079 D3 gains a dated amendment note citing ruling 6574393684 and #29499; the decision is unchanged. +- cde9a80: docs(skills): the automation skill teaches that an approval's decision outputs are enforced at the decision + + Clause-②: no + Metadata-protocol: none + + - **Before.** `skills/objectstack-automation/references/state-machines-and-approvals.md` said only that a typed `decisionOutputs` entry renders the matching record picker in the decision dialog (`multiple` collects an id array). An agent writing an approval flow from it could read `type` and `multiple` as widget hints and not know that a value the next step cannot use is refused. + - **Now.** The *Recording a decision* section says what the approvals service enforces, on approve and reject alike and before any write: a picker value must resolve through the `expression` approver's resolvers (`user` by any account's record id, `department` by record id in the request's organization, `team` by record id not owned by another organization, `position` by the machine name of a position someone in that organization holds); `multiple: true` takes a list of distinct values for every kind, bare keys included, and without it one value; any other non-blank value is refused with `400 VALIDATION_FAILED` naming `outputs.KEY`. The Console sends a list for a `multiple: true` text output. The existing `lead_review` node is the example. The file stays within its token ceiling (5724 of 5730). +- 3abe011: docs(skills): the automation skill's OAuth recipe names a stdio MCP server's credential route as `transport.credentialEnv` + + Clause-②: no + Metadata-protocol: none + + Recipe 6 of `objectstack-automation/references/integration-recipes.md` said a `stdio` server's credentials ride `transport.env` — the literal-in-metadata route the `connector-credential-literal` advisory warns against. It now says what the `mcp` provider does: the grant-bearing kinds stay `http` only; a `stdio` server takes a static `auth: { type: 'bearer', credentialRef }`, handed to the child process in the variable `transport.credentialEnv` names, never a literal in `transport.env`. Paid inside the file's token ceiling by dropping the opening paragraph's restatement of two facts that keep their homes: the packaged spec (SKILL.md § Integrations are declared, not coded) and where an `api` hook's secret goes (SKILL.md's `secretRef` row and recipe 4). No eval asserted the old route; none changes. +- 6a4737a: docs(skills): objectstack-data teaches `relatedList` placement by each value's role, not by record-page tabs or columns + + Clause-②: no + Metadata-protocol: none + + - **Before.** `rules/relationships.md` said `relatedList: 'primary'` draws "a full-width table in the details column after the first field section (in the Details tab, no tab of its own)" and that `true` / omitted is "a side card (the shared **Related** tab)". The spec's `relatedList` describe no longer names any tab or column, so the skill taught a page layout the record page does not draw. + - **Now.** The three bullets follow the spec describe: `'primary'` is part of the record's main content, a full-width table in the record's detail body (still the default for an inline-edited `master_detail` FK); `false` means the child object is not shown on the parent record page; `true` or omitted is an auxiliary panel on the record page. Layout stays ADR-0085's to define. + - The file gets smaller and stays under its token ceiling; no ceiling moves, no schema, key or behaviour changes. +- 7bafab3: docs(skills): `objectstack-ui` no longer says `page:header`'s `placedActions: true` is not drawn, and an eval exercises it (#29951) + + Clause-②: no + Metadata-protocol: none + + - `rules/pages.md`: the component catalogue's `page:header` row says `placedActions: true` draws every placed action, read live. The console's header renderer reads the key since #29951's ui half, so the "not drawn yet" qualifier #30018 wrote is dropped. + - `evals/record-page.json`: a third case asks for a custom full record page whose header shows every action packages place on the object; the expected answer keeps the page and its tab strip and sets `placedActions: true` on the `page:header` node, with no `actions` id list beside it. +- b83e1d0: test(skills): the platform skill's evals ask for push notifications on an `os serve` deployment + + Clause-②: no + Metadata-protocol: none + + `skills/objectstack-platform/evals/config-plugins-ops.json` gains case 12: the prompt asks to turn on push notifications for an `os serve` deployment, and the expected answer declares `requires: ['push']` with a top-level `push:` key on the stack definition and writes no secret inline (the APNs signing key and the Firebase service-account JSON are named by reference). The assertions stay with what `objectstack-platform/SKILL.md` teaches today, the `push` token and the top-level key; the push block's transport and vendor shape are not asserted until the skill teaches them. +- 0ce44c9: docs(skills): the i18n skill lists a lookup or master-detail field's translatable `relatedListTitle` + + Clause-②: no + Metadata-protocol: none + + - **Before.** The `objectstack-i18n` object-level table listed `label`, `help`, `placeholder`, `options` and `inlineTitle` under `fields.{field_name}`, so an author had no word that the related-list title on a parent's record page is translatable. + - **Now.** The row adds `relatedListTitle`: the title of the related list a `lookup` or `master_detail` field draws on its parent's record page. The key sits on the child object's field, and an untranslated title keeps its authored text, never the child's label. + - The paragraph under *Diff & Coverage Schemas* saying `getCoverage()` / `suggestTranslations()` have no shipped implementation is cut; *Service Contract* in the same file says the same, and the section's opening sentence now names the two schemas as those methods' return shapes. No schema, key or behaviour changes. +- f9e5bef: feat(trigger-api): an `api` hook verifies the vendor signature its start node declares (`config.signature`) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/trigger-api`.** `start()` reads the hook's one verifier with `apiTriggerVerifierOf(binding.config)` (`@objectstack/spec/automation`). A start node that declares its sender's scheme (`config.signature`: DocuSign Connect's base64 `X-DocuSign-Signature-1`, a Stripe-style `t=…,v1=…`, a GitHub-style `sha256=`) now admits a correctly signed post that was answered `401` before. That scheme REPLACES the `x-objectstack-signature` check for its hook: the declared header is read on both faces of the door (the self-hosted mount and `handleInboundHook`) with `Headers.get`, so a repeated field is its one joined value; the HMAC-SHA256 is computed under the key `config.secretRef` names (read at every post through the engine's reader, `503` while it has no value), over the raw body or `.`; each digest is decoded strictly to 32 bytes and compared with `timingSafeEqual`, any one of several `v1` elements matching (a key roll); and a timestamped scheme is held to `HTTP_SIGNATURE_TOLERANCE_SECONDS` from `@objectstack/core`. A body-only scheme has no replay window, as the contract states. A valid `x-objectstack-signature` admits nothing on such a hook, and a post without the declared header is refused. A declaration the spec answers `unusable` arms a hook that refuses every post before any secret is read and never falls back to `x-objectstack-signature`; a declared scheme bound without the engine's `config.secretRef` reader is refused at `start()`. Every failure is the same `401 INVALID_SIGNATURE` with its own cause in the server log (`declared-header-absent`, `declared-header-malformed`, `declared-prefix-missing`, `declared-no-digest`, `declared-mismatch`, `declared-outside-window`, `declared-unusable`), never the key, the header value or the body. A hook that declares no scheme is verified exactly as before. + - **`@objectstack/lint`.** `validateFlowApiTriggerSecret` grades an `api` hook by that same reader: a usable declaration is ready on its `config.secretRef` alone, and an unusable one is the new error `flow-api-trigger-signature-unusable` (with its `os explain` entry), located at the key the spec refuses. It fires only on a declaration the flow parse already refuses, so no stack that passed before is refused now. + - **`@objectstack/skills`.** The automation and api skills teach `config.signature` where they said a vendor-signed callback cannot be admitted. +- 07595c0: docs(skills): the `page:header` title row drops the retired `breadcrumb` and names the live `placedActions: true` + + Clause-②: no + Metadata-protocol: none + + - `skills/objectui/guides/page-builder.md`: the `page:header` sentence named `breadcrumb` among the title row's keys. `PageHeaderProps.breadcrumb` is a `retiredKey` tombstone since @objectstack/spec 17 (ADR-0087 D2), and the props lint refuses an authored one. The row now lists `title`, `subtitle`, `actions` or `placedActions: true`; the last two are refused together. +- 0e42aef: docs(skills): the platform skill names every type a pair's declared override is served for, and that removing the pair's item serves the base's again + + Clause-②: no + Metadata-protocol: none + + `skills/objectstack-platform/SKILL.md`, the "Customizing an installed package (ADR-0146)" paragraph: a `.custom` pair's declared override is served for a view, dashboard, report, email_template, translation, flow, page, app, dataset, doc, book or mapping (the paragraph named six), and removing the pair's item (`DELETE …?package=hotcrm.custom`) serves the base's document again (ADR-0146's revert, which the paragraph did not say). The package's evals gain a dataset-and-page override case and a removal case. +- e2b5773: docs(skills): objectstack-data's datasources rule states the organization scope an external object declares under a walled tenancy posture + + Clause-②: no + Metadata-protocol: none + + - **Before.** The datasources rule taught `external` bindings, `columnMap`, auto-connect and credentials, and said nothing about what an external object's rows are to the organizations of a walled deployment. + - **Now.** One bullet under *Federated (external) objects*: under `OS_TENANCY_POSTURE` `isolated`/`group` every external object states its organization scope — its own `organization_id` field (or `columnMap` onto it), `tenancy: { enabled: false }` (shared), or `systemFields: { tenant: false }`; none refuses the boot by name, and `tenancy.tenantField` is refused there too. Paid within the file's token ceiling (910 → 908 of 911) by trimming the auto-connect footnote, the managed-datasource sentence and the credentials paragraph; no ceiling moves. + ## 18.3.0 ### Minor Changes diff --git a/packages/skills/package.json b/packages/skills/package.json index d49716a1e89..0645d23481e 100644 --- a/packages/skills/package.json +++ b/packages/skills/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/skills", - "version": "18.3.0", + "version": "18.4.0", "description": "The published ObjectStack skills catalog \u2014 the repository's skills/** tree, shipped at the version of the @objectstack/* packages it teaches.", "license": "Apache-2.0", "homepage": "https://github.com/objectstack-ai/objectstack/tree/main/packages/skills", diff --git a/packages/spec/CHANGELOG.md b/packages/spec/CHANGELOG.md index 33c8e59ce24..d520a2adeb1 100644 --- a/packages/spec/CHANGELOG.md +++ b/packages/spec/CHANGELOG.md @@ -1,5 +1,717 @@ # @objectstack/spec +## 18.4.0 + +### Minor Changes + +- 9ebca94: feat(push): `requires: ['push']` and the stack's `push` key turn mobile push on under `objectstack serve`, and a relay configuration is refused at boot + + Clause-②: yes (widening) + Metadata-protocol: added + Skills-docs: follow-up owed — #29969 (skills/objectstack-platform/evals/config-plugins-ops.json: no eval asks for `requires: ['push']` or the `push` key); this diff teaches both in `skills/objectstack-platform/SKILL.md` (the top-level key enumeration and the `requires` table) and in the new `content/docs/kernel/runtime-services/push-service.mdx` + + A deployment that runs `objectstack serve` from a stack file can now turn on mobile push without host code. Before this, the only way to register a push service was a host that composed `PushServicePlugin` in code, so a `notify` node naming `push` had nothing to deliver through on a served stack. + + - **`@objectstack/spec`: the `push` capability token and the `push` stack key.** `push` joins `PLATFORM_CAPABILITY_TOKENS`, provided by `@objectstack/service-push` in the open edition. It is requested, never on the always-on slate. The stack definition gains a top-level `push` key beside `email` and `sms`: it is `PushServiceConfigSchema`, composed as a single value (`COMPOSE_KEY_DISPOSITIONS`). Authoring accepts a direct block, a relay block and absence; it refuses an inline secret, by key name (`key`, `privateKey`, `serviceAccount`, …) with `credentialRef` as the remedy, or pasted into a `credentialRef` (ADR-0143 D3). `PushServiceConfigSchema`'s docblock now names the stack key and `objectstack serve` as its reader; `PushRelayTransportConfigSchema` keeps its `[PLANNED — not yet served]` marker. + - **`@objectstack/core`: the `push` provider row and its argument.** `CAPABILITY_PROVIDERS.push` names `PushServicePlugin`, and `resolveCapabilityArgument('push', …)` builds its argument from the stack's `push` key through `resolvePushCapabilityArg`, read off the provider module as the `email` and `sms` readers are. Both `objectstack serve` and `@objectstack/verify`'s `bootStack` read this table, so an app's tests get the push service its server gets. + - **`@objectstack/service-push`: `resolvePushCapabilityArg`.** It parses the `push` key with `PushServiceConfigSchema` and refuses, before the kernel boots, two configurations: the relay transport (an absent key and `push: {}` read as it), whose sender has not shipped, with a message that says so and prescribes `transport: 'direct'` with an `apns` and/or `fcm` block; and anything the schema refuses, an inline secret first among them, naming each path and the schema's guidance and never the value. On a stack that declares `requires: ['push']` either refusal fails the boot as `Capability "push" (@objectstack/service-push) failed to start: …`. + - **`@objectstack/cli` and `@objectstack/verify`** depend on `@objectstack/service-push`, so the provider the token names resolves out of the box, as the open edition promises. + + To turn push on, add `requires: ['push']` and a direct block naming each vendor's secret by reference: + + ```ts + push: { + transport: 'direct', + apns: { teamId: 'ABCDE12345', keyId: 'KEY1234567', topic: 'com.example.app', environment: 'production', credentialRef: 'APNS_SIGNING_KEY' }, + fcm: { projectId: 'example-app', credentialRef: 'FCM_SERVICE_ACCOUNT' }, + }, + ``` + + In the open tier each `credentialRef` is the name of an environment variable holding the secret. A `push` key on a stack whose `requires` does not name `push` is not read, and `objectstack serve` logs one boot warning saying so. The runtime-services chapter has a new `services.push` page. +- 140d8ce: fix(spec,metadata-protocol,mcp)!: a field marked `sortable: false` is no longer offered or performed as a sort — the sortability projection reports it unsortable, and the Data API and the stdio MCP `query_records` refuse a sort on it with `400 INVALID_SORT` (#29896) + + Clause-②: yes (narrowing) — `GET /api/v1/data/:object` (and every request that reaches the same list door: `POST /api/v1/data/:object/query`, the export route, the RPC dispatcher, the record-page `related[].sort`) and the stdio MCP `query_records` refuse a sort on a field whose author declared `sortable: false`, which they used to answer sorted by the column; a stored list view `sort` or `record:related_list` `sort` naming such a field is now refused when it is read. The widening half: `@objectstack/spec/api` exports `FIELD_UNSORTABLE_AUTHOR_DECLARED`, and `FieldSortabilitySchema.reason` accepts `'author-declared'` beside `'virtual-type'`. + Metadata-protocol: narrowed + Skills-docs: none — `skills/**` names no `sortable` field key, and `content/docs/**` already teaches `sortable: false` as "do not allow sorting by this field" and `INVALID_SORT` as "the field must exist and have `sortable: true`", which is what the platform now does. The prescription ships in the refusal text and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing at the Data API list door (`@objectstack/metadata-protocol`) and the stdio MCP `query_records` (`@objectstack/mcp`): a sort on a field marked `sortable: false` is refused. + + - **Before.** An author who marked a field `sortable: false` (a long text, an unindexed column on a large table) got nothing for it. The sortability projection served on `GET /api/v1/meta/object/NAME` reported the field `{ sortable: true }`, so the list header and the Sort picker offered it, `GET /api/v1/data/NAME?sort=FIELD` answered 200 sorted by it, and so did the stdio MCP `query_records` with that `orderBy`. + - **Now.** The projection reports `{ sortable: false, reason: 'author-declared' }` for that field, so the grid header and the Sort picker, which read the projection, stop offering it. The Data API judges the sort by the same projection and refuses it with `400 INVALID_SORT` before any read, the answer a sort on a `formula` field already gets; the stdio MCP `query_records` refuses it the same way before the engine runs. Only the literal `false` counts: a field with no `sortable` key, or with `sortable: true`, sorts as before. A `formula` field also marked `sortable: false` keeps its `reason: 'virtual-type'` answer. + - **Stored sorts.** A list view whose `sort` names such a field, or a page `record:related_list` block whose `sort` does, used to load sorted and is now refused when it is read. Lint does not yet judge these at authoring. The upgrade-guide entry `field-sortable-false-sort-refused` lists them. + - **Not changed.** In-process callers that reach `ObjectQL.find` directly (hooks, flows) keep sorting by the stored column: the engine door judges only whether a column is stored, and `sortable: false` governs what the platform offers callers, not what its storage can do. + + **The remedy.** Drop the sort on that field or sort by another one, in requests and in stored view and related-list sorts. An author who wants the field sortable removes `sortable: false` from it. +- 699ebd8: fix(metadata-protocol)!: the metadata save and publish doors refuse a select field bound to a picklist no loaded package declares, instead of storing it and then refusing every write to the field (#29898) + + Clause-②: no (narrowing) — the metadata save door (draft and live), the per-item and package publish, and the restore doors refuse an `object` or `object_extension` whose field names a picklist no loaded package declares, a body they accepted before. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: no move — `skills/**` teaches no picklist reference verdict and `content/docs/**` carries no severity table for it; the prescription ships in the refusal text, the FROM → TO below and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing at the metadata save and publish doors: `PUT /api/v1/meta/object/NAME` and `PUT /api/v1/meta/object_extension/NAME` (draft and live), `POST /api/v1/packages/PKG/publish-drafts`, the per-item publish, and the rollback / revert restore doors. + + - **Before.** A select (or multiselect) field with `picklist: 'nope_list'`, a name no loaded package declares, saved `200` with its draft reading `_diagnostics.valid: true`, and `publish-drafts` answered `outcome: published`. The field was then served with no options, and every create or update that set it answered `400 VALIDATION_FAILED` ("takes its values from picklist \"nope_list\", which no loaded package declares"). The boot and a late install already refuse the same shape for a package's own objects, and `os validate` / `os build` refuse it for a stack's own fields. + - **Now.** The doors refuse it `422 INVALID_METADATA`, one finding per unknown binding at `fields.FIELD.picklist`, naming the field and the list. A list is declared when the engine's registry resolves it, the same read that serves the field's options: the package's own, a dependency's, the base package's of a `.custom` pair, or any other package installed here. + - **Unchanged.** A binding the object already holds is not judged again on a re-save, so an object published before this release keeps saving; the stored-metadata healing pass (`os migrate meta --stored`) is not judged. A draft stored before this release is judged when it is published. + - **`@objectstack/spec`.** The upgrade guide entry `picklist-binding-unknown-refused` (a semantic migration: no automatic rewrite, because whether the author meant another list, one still to be declared or inline options is a judgement about the field). + - **`@objectstack/cli`.** A source comment in the `os validate` picklist judge no longer says the runtime write path is unjudged. + + **FROM → TO.** `fields.t: { type: 'select', picklist: 'nope_list' }` → `picklist: 'NAME'` of a list a package installed here declares, or declare `nope_list` in a code package (a `*.picklist.ts` file, or `defineStack({ picklists })`), or `options: [...]` in place of `picklist`. +- 4c2cd29: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(lint)!: publish refuses a validation rule (or another fail-closed predicate) that orders a text or boolean field against a number, instead of warning that it "evaluates to null" and then refusing every write (#29904) + + Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the object save door and the package publish) refuses a `type-mismatch` it used to pass with a warning, at the predicate sites that fail closed. Nothing is widened. + Metadata-protocol: narrowed + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate`, the object save door and `POST /api/v1/packages/:id/publish-drafts`. + + - **Before.** `record.name > 5` on a text field, written as a validation rule's `condition`, published with one `expression-invalid` advisory at `severity: warning` saying the expression "silently evaluates to null". A validation rule fails closed, so from then on every create and update whose `name` was not numeric text was refused: `400 VALIDATION_FAILED` "Validation rule 'name_gt' could not be evaluated (runtime: no such overload: dyn > int) — write rejected". + - **Now.** The same rule is refused at publish as `expression-invalid` at `severity: error`, at the rule's `condition` path, with a message that says the write would be refused and how to compare like with like. This is the severity the nullable-comparison verdict (`record.qty > 1000` on a nullable number) already had at the same slot. + - **Which sites.** Those whose runtime refuses the write (or aborts the operation) when the expression faults: a validation rule's `condition` and `when`, nested `then` / `otherwise` rules included; a field's `requiredWhen` and `readonlyWhen`; an option's `visibleWhen` (a write that picks the option); a hook's `condition`. The `validate_expression` MCP tool answers the same for `site: 'validation'`. + - **Unchanged.** A formula field's `expression` keeps the warning: there the fault reads null, which is what the warning says. So do flow conditions (the run fails, the write that fired it lands), a field's display `visibleWhen` / `settledWhen`, field group and action predicates, and `conditionalFormatting`. Sharing-rule conditions compile to a query filter and are not judged by this verdict. + - **`@objectstack/spec`.** The upgrade guide entry `validation-type-mismatch-refused` (a semantic migration: no automatic rewrite, because whether the author meant the length, a number stored as text or another field is a judgement about the rule). + - **`@objectstack/formula`.** `ExprSchemaHint` gains `faultOutcome?: 'fail-closed'`, which the caller sets for a fail-closed site; the `type-mismatch` refusal's `params` carry `outcome: 'fail-closed'` when it is graded an error there. + + **FROM → TO.** `condition: 'record.name > 5'` on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text (it faults, and refuses the write, on any other). A boolean: `record.active > 0` → `record.active == true`. +- 4be8a2e: feat(spec)!: retire `emailAndPassword.revokeSessionsOnPasswordReset` — a completed password reset always ends every session the account holds, and there is no setting (#29909) + + Clause-②: no (narrowing) — `EmailAndPasswordConfig.revokeSessionsOnPasswordReset` becomes a `retiredKey()` tombstone, so the schema parse and `tsc` refuse it at any value, and plugin-auth's entry guard refuses it at any value too, `true` included, which it accepted before. Nothing is widened. + Metadata-protocol: narrowed + + + + **BREAKING** for a configuration that still writes `emailAndPassword.revokeSessionsOnPasswordReset`, at any value. + + - **`@objectstack/spec`.** `EmailAndPasswordConfig.revokeSessionsOnPasswordReset` is retired (ADR-0049 enforce-or-remove). A completed self-service password reset already ended every session the account holds, on every reset door, and an explicit `false` was already refused at boot, so the key could hold one value only. It is now a tombstone: a typed configuration that writes it fails `tsc`, and `AuthConfigSchema` / `EmailAndPasswordConfigSchema` refuse it with the prescription. The authorable-surface row reads `[RETIRED]`, `RETIRED_KEYS_BY_MAJOR[18]` lists `system/EmailAndPasswordConfig:revokeSessionsOnPasswordReset`, and the upgrade guide carries the semantic entry `email-and-password-revoke-sessions-on-reset-retired`. The prescription is exported as `REVOKE_SESSIONS_ON_PASSWORD_RESET_RETIRED` (`@objectstack/spec/system`). No ADR-0087 conversion: the key lives in the configuration a host hands the auth plugin, never in a stack or a stored row, so `os migrate meta` has nothing to rewrite. + - **`@objectstack/plugin-auth`.** The auth manager takes its configuration unparsed, so it refuses the retired key itself, where the configuration enters: at construction, which stops the boot, and on a configuration patch, where the standing configuration keeps ruling. It now refuses `true` as well as `false`, with the tombstone's own prescription. `true` asked for what the platform does anyway; the key is to be deleted either way. The auth instance still sets better-auth's own option of that name itself, so a completed reset ends the account's sessions exactly as before. + - **Unchanged.** What a password reset does: every self-service reset door ends every session the account holds, and each ended session is stamped `revoke_reason: password_reset`. An absent key is accepted, as before. + + **The remedy.** Delete `revokeSessionsOnPasswordReset` from the `emailAndPassword` block of the auth configuration, whatever it holds; nothing replaces it. +- 9037b13: fix(lint)!: a text-vs-number comparison in a record-triggered flow condition is refused with what really happens (the run fails, the write lands), and in an advisory validation rule the refusal no longer says the write is refused (#29926) + + Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses a `type-mismatch` in a record-triggered flow's start or edge condition that it used to pass with a warning; and the fail-closed arm of the verdict is served under its own code `type-mismatch-fail-closed` instead of `type-mismatch` with `params.outcome`. Added beside them: the codes `type-mismatch-run-fails` and `type-mismatch-fail-open`, and the exported type `ExprFaultOutcome`. + Metadata-protocol: narrowed + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names each outcome) and the MCP skill text in `packages/mcp/src/skill-md.ts` (`flow_condition` now errors too); `skills/**` teaches no text-vs-number comparison in a flow condition or an advisory rule, so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Record-triggered flow conditions — before.** `name > 5` on a text field, as the start node's `condition` or an edge `condition` of a flow whose start `triggerType` is `record-*`, published with an `expression-invalid` warning saying the expression "silently evaluates to null". It does not: the engine's `evaluateCondition` throws on the fault, so the run fails at that condition, and the record-change trigger isolates the write that fired it, so the write lands and the flow's work is not done. + - **Now.** The same condition is refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with a message saying the run fails at the condition and the triggering write still lands. This is the severity and outcome the nullable-comparison verdict already has on those conditions (#29026). A flow that is not record-triggered, and a node `condition` other than the start node's (inert), keep the warning. The `validate_expression` MCP tool answers the same for `site: 'flow_condition'`. + - **Advisory validation rules** (`severity: 'warning'` / `'info'`). Still refused at publish, as the nullable twin refuses them, but under `type-mismatch-fail-open` with a message that says what happens: the rule is skipped, the write proceeds, and the rule never gives its advisory. The nullable twin's message on the same rule now says the same instead of "the write is rejected fail-closed". A `severity: 'error'` rule keeps both refusals and their wording. + - **Refusal codes (`@objectstack/formula`).** A code names one message template, so each outcome has its own: `type-mismatch` (warning, reads null), `type-mismatch-fail-closed`, `type-mismatch-run-fails`, `type-mismatch-fail-open` (all errors). `ExprSchemaHint.faultOutcome` takes `'fail-closed' | 'run-fails' | 'fail-open'` (exported as `ExprFaultOutcome`); the `outcome` param on `type-mismatch` is gone. + - **`@objectstack/spec`.** Upgrade guide entry `flow-condition-type-mismatch-refused` (semantic: no mechanical rewrite). The `validation-type-mismatch-refused` entry's acceptance criteria now say a create answers 201, the data door's status. + + **FROM → TO.** Flow condition `name > 5` on a text field → `size(name) > 5` for its length, `score > 5` on a number field for a value, or `double(name) > 5` only when every value is numeric text. A boolean: `active > 0` → `active == true`. A consumer keyed to the code: `type-mismatch` with `params.outcome === 'fail-closed'` → `type-mismatch-fail-closed`. + + **The renamed code shipped in 18.3.0.** `@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` already serve #29904's fail-closed refusal as `type-mismatch` with `params.outcome: 'fail-closed'` (the `outcome` param is in the 18.3.0 `ExpressionRefusalParams` type), although 18.3.0's changelog has no line for it: its changeset (`29904-type-mismatch-fail-closed.md`) was not consumed by that release. This release renames that arm `type-mismatch-fail-closed` and removes the `outcome` param, so a consumer keyed to 18.3.0's `type-mismatch` + `outcome` moves to the new code (FROM → TO above). The same changeset also says flow conditions keep the warning; a record-triggered flow's start and edge conditions are now refused, as above. +- e3b56b2: feat(spec): a lookup or master-detail field's related-list title is translatable + + Clause-②: yes + Metadata-protocol: added + + A related list the record page derives from a lookup or master-detail field is titled by the field's `relatedListTitle`, or by the child object's label when it has none. That title had no translation slot, so a zh-CN reader saw it in the authoring language, and `os i18n check` could not see the gap. A lookup from an object back to itself was the plainest case: with no title of its own, a contract's record page listed its renewals under 「合同」, the same word as the contract beside it. + + - `FieldTranslationSchema` declares `relatedListTitle`: `objects..fields..relatedListTitle`, on the child object whose field declares the title. `translateObject` overlays it on the served child object (`GET /meta/object`, and the app bundle's object members), which is what the console derives the related list's title from, on the side card and on the body table alike. + - It follows the packaged-base rule (ADR-0029 D9.2a): a title an org re-authored on top of the packaged declaration keeps its text. An authored `relatedListTitle` with no entry keeps its authored text; the child object's label is never substituted for it. + - `os i18n extract` scaffolds the key and `os i18n check` counts it under the field bucket. A field without a `relatedListTitle` is recorded without being demanded. + - The object save door puts a served (translated) `relatedListTitle` back to its source text, as it does for the field's other translated slots. +- e8afd1e: fix(lint)!: every flow condition's text-vs-number comparison gets a verdict that says what the run does — a record-triggered flow's `record.` read and `decision` branch are refused like its bare name, a time-relative sweep of the flow's object is refused like a record-triggered flow, a bare name a flow variable holds is no longer typed as the field, and any other flow's warning says the run fails instead of "evaluates to null" (#29959) + + Clause-②: yes (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses, on a record-triggered flow, a `type-mismatch` in a `decision` branch and in a `record.` / `previous.` read of a start or edge condition, which it passed with no verdict before; refuses the same on a time-relative flow sweeping the object its conditions are typed against, where 18.3.0 served a warning; and a flow condition's `type-mismatch` warning is served under its own code `type-mismatch-run-may-fail` instead of `type-mismatch`. Added beside them (the widening half): the optional `severity` and `triggerKind` inputs of the `validate_expression` MCP tool, the code `type-mismatch-run-may-fail` in `@objectstack/formula`'s `ExpressionRefusalCode`, the optional `flowVariables` key of `@objectstack/formula`'s `ExprSchemaHint`, and a bare flow-condition name that a declared flow variable or node id holds is no longer typed as the field it shadows (no verdict where 18.3.0 served a `type-mismatch` warning). + Metadata-protocol: narrowed + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names the decision branch, the `record.` read and the warning on other flows) and the MCP skill text in `packages/mcp/src/skill-md.ts` (the two new `validate_expression` inputs); `skills/**` teaches no text-vs-number comparison in a flow condition (grep for `type mismatch`, `evaluates to null`, `validate_expression` and the decision examples), so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Record-triggered flows — before.** The start and edge conditions refused `name > 5` on a text field (`type-mismatch-run-fails`), but `record.name > 5` in the same slot, and `name > 5` in a `decision` node's `config.conditions[].expression`, published with no verdict at all. Both fail the run the same way: the engine's `evaluateCondition` throws on the fault, the run binds the trigger record both as `record` and flattened to bare names, and a decision branch is evaluated over the same variables as an edge. + - **Now.** Both are refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with the message the bare name already gets: the run fails at the condition and the triggering write still lands. `previous.` is judged the same way. The date verdicts stay on the bare name only, as before. + - **Time-relative flows.** A sweep launches each run with the swept row as its record, so on a flow whose `timeRelative.object` is the start node's `objectName` (or is omitted) the start, edge and `decision` conditions get the record-triggered verdict above, bare name included: `type-mismatch-run-fails`, where 18.3.0 served a `type-mismatch` warning. A sweep of another object keeps the warning below. + - **Any other flow** (no `triggerType`, a `schedule` flow, an `api` flow) and an inert node `condition` keep a warning. Its text said the expression "silently evaluates to null", which is false on every flow: a condition that faults fails the run. It is now `type-mismatch-run-may-fail`, worded for the failed run, and a warning because such a flow is handed whatever its entrance carries (an API request body, an action's partial record, a parent flow's record, or nothing), so the field's type is not proven to be what the name holds. A `record.` read and a `decision` branch there get the same warning. The flow registration log line reads the same text. + - **A bare name a flow variable holds.** The run seeds a flow's declared variables (and each node's outputs, under its id) before it flattens the record, and the flattening skips a name already bound. So in a flow that declares `variables: [{ name: 'title', type: 'number' }]` over a text field `title`, a bare `title > 5` reads the number and runs; it was refused as `type-mismatch-run-fails` on the head that added that error, and warned on 18.3.0. It now gets no type verdict (the `flow-variable-shadows-field` warning still names the shadowing). `record.title > 5` reads the field and stays refused. + - **`validate_expression` (MCP).** Two optional inputs, so the tool answers what the gate will: `severity` (`error` | `warning` | `info`, site `validation`; default `error`) gives an advisory rule the gate's `type-mismatch-fail-open` instead of the fail-closed verdict, and `triggerKind` (`record_change` | `time_relative` | `schedule` | `api` | `none`, site `flow_condition`; default `record_change`) gives a `schedule`, `api` or `none` flow the gate's warning instead of the run-fails error (`time_relative` answers run-fails, its record being a row of `objectName`). Either input on a site it does not describe is refused as a tool error. A call that passes neither answers as before. + - **`@objectstack/spec`.** Upgrade guide entry `flow-record-read-and-decision-type-mismatch-refused` (semantic: no mechanical rewrite). + + **FROM → TO.** In a record-triggered flow, or a time-relative flow sweeping its own object: `record.name > 5` (or a decision branch `name > 5`) on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text. A boolean: `record.active > 0` → `record.active == true`. A consumer keyed to the code of a flow condition's warning: `type-mismatch` → `type-mismatch-run-may-fail` (`@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` serve `type-mismatch` there; the record-scoped `type-mismatch`, on a formula field or a display predicate, is unchanged). +- 217ba78: feat(spec): an `api` flow's start node declares a third-party sender's signature scheme (`config.signature`), keyed by `config.secretRef` (#29983) + + Clause-②: yes (widening) + Metadata-protocol: added + + + + An inbound `api` hook admitted a post only by ObjectStack's own `x-objectstack-signature`, so a vendor that signs its callbacks its own way (DocuSign Connect, a Stripe-style `t=…,v1=…` header, a GitHub-style `sha256=` header) could not be admitted at all. The start node's `config` now carries `signature`, which declares how such a sender signs: + + - `signs: 'body'`: the raw body alone, the whole header value (after an optional `prefix`) is the digest. DocuSign Connect: `{ signs: 'body', header: 'X-DocuSign-Signature-1', algorithm: 'hmac-sha256', encoding: 'base64' }`. + - `signs: 'timestamp.body'`: `T.RAW_BODY`, the header being `NAME=value` elements whose `timestampElement` carries `T` and whose `digestElement` carries the digest. Stripe-style: `{ signs: 'timestamp.body', header: 'Stripe-Signature', algorithm: 'hmac-sha256', encoding: 'hex', timestampElement: 't', digestElement: 'v1' }`. + + The key is the start node's `config.secretRef`, as for an ObjectStack-signed hook. Declared, the scheme REPLACES the `x-objectstack-signature` check for that hook (one verifier per hook, never optional). The flow parse refuses a `signature` with no `secretRef`, one beside a literal `secret`, an algorithm other than `hmac-sha256`, an encoding other than `hex` or `base64`, a `timestamp.body` scheme with no `timestampElement` (or no `digestElement`, or the same name for both), a body-only scheme that names a timestamp element, a `header` naming `x-objectstack-signature` or another reserved header (`authorization`, `proxy-authorization`, `cookie`, `host`, `content-length`, `transfer-encoding`, `x-idempotency-key`) in any case, and one on a flow that binds no `api` trigger. + + New exports from `@objectstack/spec/automation`: `ApiTriggerSignatureSchema`, `ApiTriggerSignature`, its two arms `ApiTriggerBodySignatureSchema` / `ApiTriggerBodySignature` and `ApiTriggerTimestampedSignatureSchema` / `ApiTriggerTimestampedSignature`, `API_TRIGGER_SIGNATURE_KEY`, `API_TRIGGER_SIGNATURE_RESERVED_HEADERS`, `apiTriggerVerifierOf` (the one reader of which verifier a hook has: `objectstack-signature`, `declared` or `unusable`), `ApiTriggerVerifier`, `apiTriggerSignatureRefusals` and `ApiTriggerSignatureRefusal`. The `ITriggerApiService.handleInboundHook` contract text states what the implementer owes: a constant-time compare of the decoded 32 bytes, the same 300-second window as `x-objectstack-signature` for a scheme that signs a timestamp, no replay window for a body-only scheme, a repeated header read as its one comma-joined value, and the one `401 INVALID_SIGNATURE` for every failure, an unusable declaration included. + + This release declares the key; the `api` trigger does not verify it yet. Until it does, a hook that declares `signature` is still verified by `x-objectstack-signature` alone, so every post its vendor sends is refused `401`, and none is admitted unverified. +- f3a52d3: fix(spec)!: a page `action:icon` (`ActionIconPropsSchema`) and each `action:group` / `action:menu` member refuse `confirmText` beside a non-empty array `params`, as `action:button`, a registered action (`ActionSchema`) and an inline action (`InlineActionSchema`) already do, instead of accepting a pair that opens two dialogs for one decision + + The `action:icon` block forwards its row's `confirmText` and an array `params` (as `actionParams`) to the console's action runner, and both containers do the same for each member. The runner awaits the confirm dialog and only then opens the param dialog, so the user saw a confirm that already read as "the action ran" while nothing had been sent, then a second dialog. The other three action doors refused that pair; these two accepted it. They now refuse it with the same predicate and the same message, at the `confirmText` path: `confirmText` on the icon's properties, `actions.N.confirmText` on a container member. + + Clause-②: no (narrowing) — `ActionIconPropsSchema` and the `action:group` / `action:menu` member rows refuse `confirmText` beside a non-empty array `params`, which they accepted before. Nothing is widened. + + Metadata-protocol: narrowed (a page `action:icon`, and an `action:group` or `action:menu` member, may no longer declare `confirmText` beside a non-empty array `params`) + + + + **BREAKING** accept-set narrowing at `ActionIconPropsSchema` and the `action:group` / `action:menu` member rows (`@objectstack/spec`), served where a page's component `properties` are judged: the component-props gate (`component-props-invalid`, `@objectstack/lint`) at every CLI command (`os validate`, `os compile`, the publish gate) and at the runtime `page` save door. `defineStack` does not parse a component's `properties` bag, so it does not refuse the pair. + + - **Before.** `{ type: 'action:menu', properties: { actions: [{ name: 'reject', label: 'Reject', type: 'api', target: '/api/v1/approvals/reject', confirmText: 'Reject this request?', params: [{ name: 'reason', type: 'textarea' }] }] } }` parsed clean, and the console showed a confirm dialog and then the param dialog. The same held for an `action:icon` with both keys. + - **Now.** The same `properties` are refused at `actions.0.confirmText` (at `confirmText` on an `action:icon`) with the action doors' message: the pair shows TWO dialogs for one decision; carry the question in `description` and drop `confirmText`. + - **Unchanged.** `confirmText` with no `params`, beside `params: []`, or beside an object `params` (an icon's static parameter values; a `type: 'api'` member's request payload — neither opens a dialog) parses as before; so does an array `params` with `description`. + - **Who sees it.** An author whose page declares an `action:icon`, or an `action:group` / `action:menu` member, with both keys. A stored page keeps loading and serving; its next save is refused, with the remedy in the finding. + + **The one-line fix: move the confirm sentence to the action's `description`, which the param dialog draws under its title, and drop `confirmText`.** `os migrate meta --from 17` lists the D3 entry `ui-page-action-row-confirm-text-beside-params-refused`, which names all four page action rows (`action:button`, `action:icon`, and each `action:group` / `action:menu` member) — the `action:button` refusal shipped earlier in 18.x is covered by the same entry. The rewrite is by hand, because whether the sentence reads as the dialog's description line is the author's call. On an `action:icon` with no `label`, `description` is also the tooltip text; give the icon a `label` to keep them apart. +- d59a1eb: feat(spec): a `page:header` declares `placedActions: true`, the explicit opt-in to draw every action of the bound object placed at `record_header` / `record_more`, read live (#30018) + + Clause-②: yes (widening) + Metadata-protocol: added + + - **`placedActions`** on a `page:header` component's `properties` (`PageHeaderProps`, the `ComponentPropsMap['page:header']` row): `true` asks the header to draw every action of the page's object whose `locations` include `record_header` or `record_more`, read live from the object's metadata, in place of an authored `actions` id list. An action another package places on the object is then drawn without editing the page. Optional with no default: an unset key stays unset in the parsed output. + - **The `actions` list keeps its meaning.** An authored list is the whole set the header draws, and a header with neither `actions` nor `placedActions: true` draws none of the object's actions. `placedActions: false` beside a list is accepted and changes nothing. + - **Refused together.** `placedActions: true` beside an authored `actions` list, an empty one included, is refused at `properties.placedActions` by `os validate`, `os build`, `os lint` and the page save door (`component-props-invalid`, an error). The message names both keys and asks the author to keep one: `actions` for a curated list that is the whole set, or `placedActions: true` for every placed action. Nothing that parsed before is refused: until this release `placedActions` was an unknown key on the closed shape. + - **Declared ahead of its reader.** The console's header renderer does not read the key yet, so a header that sets it draws what the same header without it draws (none of the object's actions unless `actions` names them). The key's describe opens with `[PLANNED — not yet rendered]` until the renderer reads it. + - The published JSON Schema `ui/PageHeaderProps` still accepts the pair: the rule depends on the value `true`, which no projected refinement pattern states, so the parse refuses it and `dropped-refinements.baseline.json` records the site. + - `@objectstack/skills`: `objectstack-ui` `rules/pages.md` names the key in the component catalogue, marked not drawn yet, and drops a refused-shape clause from the print-page paragraph that the sentence before it already states. +- 3a27208: fix(lint)!: a stored sort naming a field that declares `sortable: false` is refused by `os validate` and at publish, judged by the same sortability projection the Data API list door refuses it by — on a list view and on every stored page sort (#30023) + + Clause-②: yes (narrowing) — `sort-field-unsortable` (error) now also fires on a sort naming a field whose author declared `sortable: false`, on the list-view rungs it already walked and on every stored page sort it did not walk before (a block's own `sort`, every element `dataSource.sort`, a related list's `parentDataSource.sort`, a list page's `interfaceConfig.sort`, an app page's `record.sort`, where a `formula` sort is now refused too), so `os validate` / `os build` / `os lint` and the `page` and `view` publish doors refuse metadata they used to accept. The widening half: `@objectstack/lint` exports `indexObjectSortability` and the `SortabilityFields` type, and `checkSortDeclaration` takes an optional trailing `sortability` index. + Metadata-protocol: narrowed + Skills-docs: none — `skills/**` names no `sortable` field key and no page or view `sort` rule; `content/docs/**` already teaches `sortable: false` as "do not allow sorting by this field". The prescription ships in the finding's fix line, `os explain sort-field-unsortable` and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing at authoring (`@objectstack/lint`): a stored sort the Data API list door refuses is refused before it is stored. + + - **Before.** A list view whose `sort` named a field marked `sortable: false` passed `os validate` and the `PUT /api/v1/meta/view` door, then answered `400 INVALID_SORT` on its first fetch and on every fetch after it. No page sort was judged for sortability at all: a `record:related_list`, `record:line_items`, `list-view` or `object-*` block sorting by such a field (or by a `formula` field), an element `dataSource.sort`, a related list's `parentDataSource.sort`, a list page's `interfaceConfig.sort` and an app page's `record.sort` all saved green and failed at read. + - **Now.** `validateSortableFields` judges each of those positions by `resolveObjectSortability`, the projection the list door refuses by, and reports `sort-field-unsortable` at `error` with the door's own clause ("a field on object X that declares `sortable: false`") at the slot's path. The reference-integrity suite now runs on a `page` write for this one member, so the Studio, REST `/meta` and MCP page doors give the same refusal (422). Only the literal `false` counts: a field with no `sortable` key, or `sortable: true`, sorts as before. A `formula` field also marked `sortable: false` keeps its virtual-field verdict. Existence on page sorts stays `page-field-unknown`. + - **Not changed.** A stored view or page already carrying such a sort keeps loading and serving; only its next save is judged, and its first fetch still answers 400 as it did since the list door started refusing it. + + **The remedy.** Sort by another field, or remove `sortable: false` from the field to make it sortable. + + **Supersedes, in the same release.** The `@objectstack/spec` / `@objectstack/metadata-protocol` / `@objectstack/mcp` note for #29896 says "Lint does not yet judge these at authoring." From this release it does: `os validate` and the publish doors refuse every stored sort that note lists, and the stored page sorts it did not list. +- aa57c76: feat(spec): three forward date-range presets: `next_month`, `next_quarter` and `next_30_days` + + Clause-②: yes (widening) + Metadata-protocol: added + + The relative dates a dashboard date filter and the filter builder offer looked only backward. A forecast asks forward: what closes next month, next quarter, in the next 30 days. Those questions had to use a custom range, whose fixed dates go stale. + + `DATE_RANGE_PRESETS` now also declares `next_month` (the whole of next month), `next_quarter` (the whole of next quarter) and `next_30_days` (today and the 30 days after it, the mirror of `last_30_days`). Every place that accepts a preset accepts them: `dashboard.dateRange.defaultRange`, a date global filter's `defaultValue`, the filter builder's `between` row and `AnalyticsQuery.timeDimensions[].dateRange`. They sit beside the family they extend in the filter bar: `next_month` after `last_month`, `next_quarter` after `last_quarter`, `next_30_days` after `last_90_days`. Their `DATE_RANGE_PRESET_MACRO_WINDOWS` rows follow the inclusive last-day convention: `['{next_month_start}', '{next_month_end}']`, `['{next_quarter_start}', '{next_quarter_end}']` and `['{today}', '{30_days_from_now}']`. A preset name written as a bare filter comparand is refused with that window as the prescription, as for the other presets. + + `@objectstack/core`'s `resolveAnalyticsDateRangePreset` resolves the three in its half-open form. `next_30_days` stops before `{31_days_from_now}`. `next_month` and `next_quarter` stop before the month and the quarter after next, which no macro token names: the resolver reads `next_month_start` (or `next_quarter_start`) again from the start of the day it named, so the end is still a day the one token resolver computed. No macro token is added. + + In the console, the three presets have labels in all ten locale packs, and the client date-macro resolver now answers `{next_month_end}` and `{next_quarter_end}`, two spec tokens it did not resolve before. `@objectstack/verify`'s dashboard-window proof also writes fixture rows on a forward preset's future edges, so it judges the three new windows; the days it writes for the other presets are unchanged. `@objectstack/lint` changes documentation only. +- fc869c2: feat(spec)!: a seed is create-only by default — an omitted `mode` is `ignore`, so a restart, an install or an upgrade never writes a seeded value back over a customer's edit (#30081) + + Clause-②: yes (narrowing) — the declared default of `Seed.mode` (and of `SeedLoaderConfig.defaultMode`, now the same field) moves from `upsert` to `ignore`. The accept set is unchanged and every authored `mode` parses exactly as before; what narrows is what a replay does for a dataset that declares no mode: it no longer updates an existing row. + Metadata-protocol: semantic + + + + **BREAKING** for a seed dataset that declares no `mode` and relies on a replay to write its values back onto existing rows. + + - **Before.** A seed with no `mode` replayed as `upsert`. A seed loads again on every single-tenant boot (production included, under `os start` and `os serve`), on every package install or upgrade and on every publish, so each replay wrote the shipped value back over a customer's edit to a seeded row, with no warning. Measured on a production boot before the change (card comment 6633189577): an API edit to a seeded row went back to the shipped value on the next restart, after a boot-artifact upgrade, and after an `os package install` upgrade. + - **Now.** `SeedSchema.mode` defaults to `ignore`: a replay inserts a record whose row is missing and leaves an existing row exactly as it is. A row nobody edited is not rewritten either, so its `updated_at` does not move, and a value changed in a later seed version reaches only a row the load creates. Maintainer ruling 6631016988 (option A). + - **One default, stated once.** `SeedLoaderConfig.defaultMode` is `SeedSchema`'s own `mode` field, default included. The six replay sites (`@objectstack/runtime`: the inline boot seed, the per-organization replayer, the dev hot reload and the published-seed apply; `@objectstack/metadata-protocol`: the publish batch; `@objectstack/cloud-connection`: install, reseed and the rehydrate healer) stop restating `defaultMode: 'upsert'`, and the save door reads the schema's default for a seed item that declares no mode. + - **Compiled artifacts.** `os build` stores the parsed seed, so a package built before this change carries a written `mode: "upsert"` on every dataset. The loader cannot tell that value from an authored one, and nothing converts it: such a package keeps today's replay until it is rebuilt. + - **Installed packages.** A package installed with `os package install` (`@objectstack/cloud-connection`'s install-local) keeps the body it was installed with in its install ledger, and the sample-data reseed (`POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data`) replays that body's datasets in their stored mode. So an entry installed from a pre-change artifact keeps writing the shipped values back over edited rows on every reseed until the **rebuilt artifact is re-installed**; rebuilding alone changes nothing for it. An entry installed from a rebuilt artifact reseeds create-only: it puts back the rows that are missing and leaves an edited row as it is. The ledger's other replays never overwrite: the boot rehydrate seeds nothing, the heal seeds only when every seeded object is empty, and the per-organization replay runs only for a new organization. + - **Stored `seed` items.** A `seed` metadata item stored with no `mode` is not rewritten either. Its next publish parses it to `ignore`, so that publish inserts only the rows it is missing where it used to write its values back over existing rows, with no signal. + - **Unchanged.** An explicit `mode: 'upsert'` still writes every declared column back on each replay, and `columns: { FIELD: { applyOn: 'create' } }` still exempts the columns a customer or a job owns. `insert`, `update` and `replace` behave as before. + + **The remedy.** Rebuild every package so its compiled artifact carries the mode its source declares, and re-install the rebuilt artifact wherever the package was installed with `os package install`, so its install ledger carries that mode too. To put an installed package's sample data back to the shipped values, purge it (`…/purge-sample-data`) and then reseed. For a dataset that is reference data the package must keep current, declare `mode: 'upsert'`, and mark the columns the customer or a job owns `applyOn: 'create'`. Leave `mode` unset for business and starter data the customer owns once it is loaded. +- b77c160: feat(spec)!: the object parse refuses a `homeAmount.rateField` whose number field declares a `scale` below 12, the decimals an exchange rate carries; `CURRENCY_FRACTION_DIGITS` is documented as CLDR fraction digits, not ISO 4217 minor units + + Clause-②: no (narrowing) — the object parse (`ObjectSchema`, so `os validate`, `os compile` and the metadata doors that parse an object) refuses an object whose `homeAmount.rateField` names a `number` field declaring `scale` 0 to 11, a declaration it accepted before. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: none — `skills/objectstack-data/rules/field-types.md` teaches `rateField` as a readonly `number` with no `scale`, which this change still accepts; the floor ships in the refusal text, the FROM → TO below and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing in the object parse: `fields.AMOUNT.currencyConfig.homeAmount.rateField`. + + - **Before.** A rate field declared `Field.number({ readonly: true, scale: 4 })` parsed. The engine stored the rate it used with all twelve decimals anyway (the rate row's own value, never rounded), so the declared `scale` only rounded the rate a reader was shown: `0.1389` for a stored `0.138912345678`, a rate that no longer reproduced the stored home amount. + - **Now.** The parse refuses it at `fields.AMOUNT.currencyConfig.homeAmount.rateField`, naming the field, its `scale` and the floor 12, the rate's scale (ruling 6469382727: twelve digits after the decimal point, total precision 24). A rate field with no `scale` is accepted: on a `number` field an absent `scale` declares no fixed width, so the rate shows every digit it was stored with. `scale: 12` or more is accepted. + - **Unchanged.** What the engine stores and computes, and every stored data row (record values). + - **Metadata already on disk.** A built artifact whose object declares such a rate field is refused whole at load (its `ObjectStackDefinitionSchema` parse fails before anything registers): fix the source and rebuild. A stored `sys_metadata` object row is kept as authored and still loads and registers; boot logs `[metadata_spec_invalid]` at `fields.AMOUNT.currencyConfig.homeAmount.rateField`, and its next save is refused `422 INVALID_METADATA` until the rate field's `scale` is raised to 12 or more, or deleted. + - **Docs.** `CURRENCY_FRACTION_DIGITS` and the currency page now say the table is CLDR's fraction digits, and list the sixteen codes where they differ from ISO 4217's minor units: IQD (CLDR 0, ISO 3) and AFN, ALL, COP, HUF, IDR, IRR, KPW, LAK, LBP, MGA, MMK, PKR, SOS, SYP, YER (CLDR 0, ISO 2), measured against SIX's ISO 4217 List One of 2024-06-25. + + **FROM → TO.** `amount_rate: Field.number({ readonly: true, scale: 4 })` → `amount_rate: Field.number({ readonly: true, scale: 12 })`, or `Field.number({ readonly: true })` with no `scale`. +- 09720e2: `GET /api/v1/auth/config` now says whether email leaves the deployment, in a new optional `features.emailDelivery` flag. It is `true` only when the wired email service reports itself configured, and `false` with no email service or with the stock `log` transport, which writes mail to the server console instead of sending it. The forgot-password page can read it to stop saying a reset link was sent on a deployment that sends no mail. + + The flag describes the deployment, not an address: `POST /api/v1/auth/request-password-reset` still answers a known and an unknown address the same way, with or without mail. + + Like every public auth flag, it is classified in `PUBLIC_AUTH_FEATURES` (login surface, opt-in), so an action may now also name it: `requiresFeature: 'emailDelivery'` is a new accepted value, lowered to `features.emailDelivery == true`. + + Clause-②: yes (widening) + Metadata-protocol: added +- d9d42d4: feat(spec): declare the recycle bin's spec surface (ADR-0148): the `sys_recycle_bin` entry contract, the host retention resolver hook, the snapshot shape, the restore refusal codes, the `restored` event name and the two grant keys + + Clause-②: yes (widening) + Metadata-protocol: added + + A protocol addition: new spellings with their readers named, and no tombstoned key returns. Declared, not yet read. Every delete is still a physical delete, and nothing writes or reads the recycle bin until the engine slice lands it (snapshot on delete, restore, purge, empty, the TTL reap, the permission mapping and the REST doors); the Console's admin page and its undo toast follow it. This release gives both something to build on. + + - **`@objectstack/spec` (`@objectstack/spec/data`):** `RECYCLE_BIN_OBJECT` (`'sys_recycle_bin'`) and `RECYCLE_BIN_ENTRY_FIELDS`, the entry's field list the object is built from: `object_name`, `record_id`, `record_count`, `snapshot`, `deleted_by`, `deleted_at`, `retention_days` and `purge_after` (the deadline stamped at delete time), all read-only, with `RecycleBinEntrySchema` as the entry's value shape. The object itself is registered with the engine slice, so no table and no platform-object name exist until a writer does. `RECYCLE_BIN_RETENTION_DAYS_ENV` (`'OS_RECYCLE_BIN_RETENTION_DAYS'`) and `RECYCLE_BIN_DEFAULT_RETENTION_DAYS` (30). `RecycleBinRetentionResolver`, the host hook's contract: `(token: string | undefined) => number | undefined`, called with the same opaque token `resolveFeatures` receives, answering a positive whole number of days or `undefined` to keep the default. `RecycleBinRetentionDaysSchema` and `RecycleBinRetentionDaysEnvSchema` hold the answer and the variable to that rule, and `recycleBinRetentionRefusal(source, value)` is the refusal text: zero or anything that is not a positive whole number is a configuration defect, never "purge now" or "keep forever". The framework knows days, never plans. `RecycleBinSnapshotSchema` (with `RecycleBinSnapshotRowSchema` and `RecycleBinClearedReferenceSchema`): the cascade set an entry holds, the master first and its details in cascade order, secret-channel refs left out, plus the `set_null` references the delete cleared. `RECYCLE_BIN_RESTORE_REFUSAL_CODES` and `RECYCLE_BIN_RESTORE_REFUSAL_STATUS` (409). `RECORD_RESTORED_EVENT` (`'data.record.restored'`): a restore emits it, never a create; `DataEventType` and `RealtimeEventType` list only emitted events, so the name joins them in the change that emits it. `RECYCLE_BIN_GRANT_KEYS`. + - **`@objectstack/spec`, error-code ledger:** `RESTORE_UNIQUE_VALUE_TAKEN`, `RESTORE_PARENT_PURGED` and `RESTORE_PACKAGE_UNINSTALLED`, registered under `@objectstack/objectql`, which answers them. + - **`@objectstack/spec`, object permissions:** two new optional keys, `allowRestoreOwnDeleted` (restore the entries of this object the caller deleted) and `manageRecycleBin` (list every entry of this object, restore any, purge; emptying purges every entry the holder manages). No default, so no parsed entry grows a key. Not read yet: a dispatched `restore` or `purge` stays denied by the permission evaluator's fail-closed backstop whatever they say, and their schema descriptions say they are not read yet. The retired `allowRestore` / `allowPurge` stay tombstoned. +- c7d1367: feat(spec): an action param can name the widget that draws it (`widget`, same key as on a field), so a param can ask for the permission-set picker (ADR-0131 D4) + + Clause-②: yes (widening) + Metadata-protocol: added + + - **`@objectstack/spec`: `ActionParamSchema.widget`**, an optional string with the same key, vocabulary and meaning as `FieldSchema.widget`. It names a registered field component, looked up as `field:` plus the name, to draw the param in the action dialog instead of the `type` default. `type` stays the value's type. The case it exists for is `widget: 'permission-ref'` on a `text` param: it lists the permission sets of the security catalog and submits the chosen machine name, the picker the grant column `sys_user_permission_set.permission_set` already uses. Like the field key it is an open string: an unregistered name is kept, and the dialog draws the `type` renderer instead. A non-string is refused. A param that does not declare the key parses exactly as before. Until now a param naming `widget` was refused as an unknown key. + - **Not read yet.** The console's param resolver does not carry the key to the dialog's widget until objectstack#30260 lands. Until then a param that declares it is drawn by its `type` renderer, and its value is submitted as before. The key's description says so, and the liveness ledger grades it `planned` with an author warning: `os validate` and `os lint` warn an author who writes it. + - **`@objectstack/platform-objects`: the action form's Params repeater offers the key** as a `Widget` column, labelled in en, zh-CN, ja-JP and es-ES. +- f5aaa0b: feat(ui): an action param's `widget` draws the named widget in the action dialog — `widget: 'permission-ref'` on a `text` param is the permission-set picker + + Clause-②: yes (widening) — `@object-ui/core`'s exported `ActionParamDef` gains one optional member, `widget?: string`; `@object-ui/fields`' `RawActionParam` gains the same optional member. No accepted key, value or answer is taken away. + Metadata-protocol: semantic + + `ActionParamSchema.widget` (declared by `@objectstack/spec` with the same key, vocabulary and meaning as `FieldSchema.widget`) was accepted and stored but read by nothing: the action dialog drew every param with its `type` widget. The console now reads it: + + - `resolveActionParams` copies the param's `widget` onto the resolved param. A field-backed param (`{ field }`) that inherits the field's `type` also inherits the field's `widget`; a param that overrides `type` does not, because the field's widget was chosen for the field's value. The param's own `widget` always wins. + - `paramToField` draws the named widget when it is registered, in place of the `type` widget. A name nothing registers keeps the `type` widget (a `textarea` param stays a textarea), and a registered widget on a lookup param without a target is drawn instead of the plain record-id text box. + - So `{ name: 'permissionSet', type: 'text', widget: 'permission-ref' }` opens the permission-set picker: the sets are listed by label beside their machine name, and the machine name is submitted. The server still validates the value by `type`, so the request body is unchanged. + + The spec's liveness ledger marks `params.widget` live, and its `.describe()` no longer says the key is not read yet. +- a7b8876: feat(spec,metadata-protocol): the publish receipt's `seedApplied.landed` names the rows a seed created and the rows it only matched, per object, capped at 200 ids per list + + Clause-②: yes (widening) + Metadata-protocol: added + + - **`@objectstack/spec`: `seedApplied.landed`** on both publish responses (`PublishMetaItemResponseSchema`, `PublishPackageDraftsResponseSchema`), one shared schema `SeedAppliedLandedSchema`: per object, `inserted` (the rows this publish created) and `matched` (pre-existing rows a seed record's key matched: updated, left as they were, or refused an update), each a `SeedAppliedIdListSchema` `{ ids, total, truncated }`. `ids` carries at most `SEED_APPLIED_LANDED_IDS_CAP` (200) ids, in the loader's record order (datasets in load order, records as authored); past it `truncated` is `true` and `total` is the uncut count. Absent `landed` means "not reported" (a load that threw, or a producer that reports no ids), never "nothing landed". Only `inserted` names rows the seed created: a `matched` row may be a customer's own record that shares the seed's natural key, and under `ignore` (the default) such a row is matched without being written. + - **`@objectstack/spec`: `SeedLoadResult.insertedIds`**, the ids among `landedIds` whose row the load created. Per load: a row an earlier load inserted is matched on a replay, so a reader tracking a seed's rows across publishes accumulates `insertedIds` over every load. `landedIds`' TSDoc now says what each seed mode puts in it. + - **`@objectstack/metadata-protocol`**: `applySeedBodies` returns `landed`, so `publishPackageDrafts` and `publishMetaItem` both carry it; the loader records which landed rows it inserted. `seedAppliedLanded(results)` is exported, the one projection both receipt producers fold the loader's ids through. + - **`@objectstack/runtime`**: the package-publish door's fallback seed apply (for protocols that do not self-apply) reports the same `landed`. + - Accepted cost (objectstack-ai/cloud#3598 ruling A): row ids appear on the public publish responses, bounded by the cap. +- 4dde0ae: fix(spec)!: a SQLite datasource's `config.filename` must name a database: `''` and a name that starts with `:` without being exactly `:memory:` are refused by the stack parse and the metadata save door, not only by the datasource admin doors + + Clause-②: no (narrowing) — `SqliteConfigSchema.filename` and `SqliteWasmConfigSchema.filename` refuse `''` and any `:`-prefixed value other than exactly `:memory:`, so `defineStack` / `os validate` / `os build` and `PUT /api/v1/meta/datasource/NAME` refuse a `sqlite` / `sqlite-wasm` datasource they accepted before. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: none — `skills/**` teaches no SQLite `filename` value, and `content/docs/**` teaches only `:memory:` and file paths, which stay accepted. The prescription ships in the refusal text, the FROM → TO below and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing on `SqliteConfigSchema.filename` and `SqliteWasmConfigSchema.filename` (`@objectstack/spec`), and so at every door that judges a datasource against the spec: `defineStack`, `os validate`, `os build` and `PUT /api/v1/meta/datasource/NAME`. + + - **Before.** Both schemas typed `filename` as any placeholder-free string. `:memory:/srv/data/parts.db` (the New datasource dialog's prefilled `:memory:` with a path typed after it) and `''` parsed. Every layer reads a name that starts with `:` as fileless, so no file was ever opened: a `sqlite` datasource opened it as a relative path whose directory does not exist and every statement failed, and a `sqlite-wasm` one opened a throwaway in-memory database whose writes were gone at the next connect. The datasource admin doors (Test connection, create, update) already refused both; a datasource declared in a stack or saved through the metadata door was stored anyway, and the admin door's update would not take it back. + - **Now.** The parse answers one issue at `filename` (`config.filename` on a datasource, `datasources.N.config.filename` in a stack) that names the value, says why and prescribes the fix: `'…' is neither ':memory:' nor a file path. A name that starts with ':' is not read as a file, so this datasource would store nothing: use exactly ':memory:' …`, or `'' is empty and names no database file. …`. `defineStack` raises it as `STACK_SCHEMA_INVALID` (422); the metadata door answers `422 INVALID_METADATA`. + - **Unchanged.** Exactly `:memory:`, an absent `filename` (still defaulting to `:memory:`), and relative, absolute, Windows-drive and `file:` paths parse as before. Whether a path can be opened or created is still judged only at runtime, by the datasource admin doors; the schema cannot touch the file system. + - **`@objectstack/service-datasource`.** The admin doors give the same answers as before, word for word. The two arms the string alone decides now come from the driver-contract parse they already run, so `sqlite-database-filename.ts` keeps only its filesystem arm: one definition, in the spec. + - **`@objectstack/spec`.** The upgrade-guide entry `sqlite-filename-names-no-database-refused` (a semantic migration: no automatic rewrite, because whether the author meant an ephemeral database or the path typed after the prefix is a judgement about the datasource). + + **FROM → TO.** `config: { filename: ':memory:/srv/data/parts.db' }` → `filename: '/srv/data/parts.db'` to keep the data in that file, or `filename: ':memory:'` for an ephemeral database; `filename: ''` → `':memory:'` or a file path. +- 590724f: feat: a settings toggle can declare the question asked before it is turned off (`confirmOffText`), and Setup's email and password sign-in toggle declares one + + Clause-②: yes (widening) + Metadata-protocol: added + + - `SpecifierSchema` (`@objectstack/spec/system`) gains an optional `confirmOffText: I18nLabel`: the confirm dialog text shown before a toggle's value changes to off. Omitted, nothing is asked, so every existing manifest reads as before. The name mirrors the `confirmText` an `action_button` handler already declares. + - Only a `toggle` may declare it. On any other specifier type the parse refuses it at `confirmOffText`, naming the type, because nothing would ever ask the question there. + - The platform translation bundle carries it at the toggle's own address, beside its label: `settings..keys..confirmOffText`. + - `@objectstack/service-settings`: the `auth` manifest's `email_password_enabled` toggle declares "People who sign in with a password will need another way in.", with zh-CN, ja-JP and es-ES text in the shipped bundles. The settings translation coverage test now holds a declared `confirmOffText` to the same per-locale coverage as the toggle's label. + - The console asking the question before save is a separate change; until it lands, the key is declared and parsed but not yet shown. +- 38d416f: fix(metadata-protocol,runtime): a seed load that throws part-way still reports `seedApplied.landed` for the datasets that finished before the throw + + Clause-②: yes (widening: a thrown load's receipt carries `landed` for the datasets that finished, where it carried none) + Metadata-protocol: semantic + + - **`@objectstack/spec`: `seedApplied.landed`** (both publish responses) now says what it covers on a load that threw part-way (`success: false` with `error`): only the datasets that FINISHED before the throw. The dataset that threw is not listed (its rows are in `inserted` / `updated`, its object in `error`). Absent `landed` now means the apply failed before any load ran, or the producer reports no ids; a throw before any dataset finished reports `landed: []`. + - **`@objectstack/metadata-protocol`**: `SeedLoadPartialWrites` gains `finished`, the finished datasets' `object` / `landedIds` / `insertedIds` in load order, and `applySeedBodies`' catch folds it through the same `seedAppliedLanded` projection (same two lists, same 200 cap) the returned path uses. `seedLoadPartialWrites` and `SeedLoadPartialWrites` are exported from the package root. + - **`@objectstack/runtime`**: the package-publish door's fallback seed apply reads the same `seedLoadPartialWrites` off a thrown load and reports the same `landed`, plus the `inserted` / `updated` counts the response schema requires (it reported neither before) and the object whose seed stopped the load in `error`, as `metadata-protocol`'s apply does. + - Why the dataset that threw is left out: its ids are only partly known when it throws. A batched insert whose response was lost may have committed rows whose ids never came back, so a list of the ids the loader did see would read as complete while missing rows. + + Supersedes, in the same release: `.changeset/30348-seed-receipt-landed-ids.md` says absent `landed` means "not reported (a load that threw, or a producer that reports no ids)". After this change a load that threw part-way reports `landed` for its finished datasets; absent now means no load ran, or a producer that reports no ids. +- d2e2f6f: feat(spec): a blueprint dashboard widget says how it aggregates, and a blueprint calendar view says which date field it schedules on + + Clause-②: yes (widening: an optional `aggregate` on the blueprint dashboard widget and an optional `startDateField` on the blueprint view) + Metadata-protocol: added + + - **Widget `aggregate`.** `BlueprintDashboardSchema` widgets and the strict mirror `SolutionBlueprintStrictSchema.dashboards[].widgets[]` gain `aggregate: 'sum' | 'avg' | 'min' | 'max' | 'count'` (optional on the lenient side, required-but-nullable on the mirror). Before this, a widget could only name its `measure` and the reader picked the aggregation from the field type, so a widget meant to average a number field summed it. `sum`, `avg`, `min` and `max` aggregate the `measure` field; `count` counts records and needs no `measure` field (a field named beside it is not read). `measure`'s describe no longer says the aggregation is automatic, and neither side tells the model to infer anything from the widget title. + - **View `startDateField`.** `BlueprintViewSchema` and the strict mirror's `views[]` gain `startDateField`, a snake_case field name, carried into the built view as `calendar.startDateField`. It is read on a `calendar` view and ignored on the other view types, not refused: the strict mirror the design model writes through cannot state a cross-key rule, so a refusal on the lenient side alone would refuse at apply a proposal the model was allowed to generate. + - Both keys are optional on the lenient schema, so every existing blueprint parses unchanged. The strict mirror is only the design model's structured-output contract; a model generating against it now states both keys, `null` when it has nothing to say. +- af8aab1: fix(spec,core): the server locale resolvers read the platform locale table, so a `zh-TW` / `zh-HK` / `zh-MO` reader gets a registered `zh-Hant` bundle instead of Simplified Chinese + + Clause-②: yes (widening) + Metadata-protocol: semantic + + `resolveBundleLocale` (`@objectstack/spec/system`) and `resolveLocale` (`@objectstack/core`) now run one ladder, the new `resolveLocaleAmong(requested, available)` export of `@objectstack/spec/system`: exact, case-insensitive, then **the same platform locale table language**, then base language, then the first registered variant. The new step maps the requested code and each available key through `platformLocaleTagOf` (the table's read aliases) and picks the key spelled as the table tag, else the first key the table maps to that tag. + + What changes for a reader: a request for `zh-TW`, `zh-HK` or `zh-MO` reaches a registered `zh-Hant` key, and `zh`, `zh-CN`, `zh-SG` or `zh-Hans` reaches `zh-CN` (or `zh-Hans`), whatever order the bundle registered them in. Before, the first registered `zh-*` key answered both, so a Traditional reader got the Simplified bundle (and with `zh-Hant` registered first, a Simplified reader got the Traditional one). A bare `zh` key now counts as Simplified for a `zh-TW` request, which the base-language step used to hand it. The step also applies to `ja` → `ja-JP`, `es` → `es-ES`, `en` → `en-US` and `pt` → `pt-BR`, which answer as before against today's alias-spelled bundles. + + `resolveOfferedLocale` (`@objectstack/core`, the matcher Setup's default-language options and the IdP locale claim use) now answers through the same ladder, so a Traditional request reaches an offered `zh-Hant`. Where a list offers two codes of one table language, the code spelled as the table tag wins over the first one offered: `zh` over `['zh-CN', 'zh-Hans']` answers `zh-Hans`, and `zh-HK` over `['zh-TW', 'zh-Hant']` answers `zh-Hant`. + + Unchanged: a code the table does not know (`es-MX`, `en-GB`, `pt-PT`) falls back through base language and variant expansion exactly as before, and a `zh-TW` request still reaches `zh-CN` when no Traditional key is registered. No call that matched before stops matching. Every caller of these functions moves with them, among them the i18n REST door, built-in validation and operation messages, email template locale folding, hook, flow and approval copy, and plugin-auth's refusal copy. +- 4bf9a2d: feat(spec): a blueprint field carries a typed `defaultValue`, so an AI-built boolean flag defaults to false + + Clause-②: yes (widening) + Metadata-protocol: added + Skills-docs: none — no file under `skills/**` restates a blueprint field key (`skills/objectstack-ai/references/_index.md` only points at `solution-blueprint.zod.ts`); the reference page `content/docs/references/ai/solution-blueprint.mdx` is regenerated from the new describe. + + The AI solution blueprint had no slot for a field's default. A blueprint that wrote `{ type: 'boolean', defaultValue: false }` for a 是否完成 / 已付款 / `is_active` flag, as the build guidance teaches, parsed with `success: true` and lost the key, so the built field had no default and every record created without the flag read empty instead of false. A filter on false missed those records. + + - **`defaultValue` on `BlueprintFieldSchema` and on the strict structured-output mirror's fields (`SolutionBlueprintStrictSchema.objects[].fields[]`).** Both sides declare it as one scalar union, `boolean | number | string`. Lenient: optional. Strict: required and nullable (null = no default), as with every other key in that mirror. The describe says the value becomes the built field's `defaultValue`, and states the boolean-flag rule: a yes/no flag is a `boolean` field that is never `required` and carries `defaultValue: false`. + - **The value is checked against the field's `type`, on the lenient side.** The check is the field schema's own literal contract (`checkLiteralDefaultValue`, the one `FieldSchema` runs on a built field's `defaultValue`), so a default the blueprint admits is one the built field admits. These types take a default: `boolean` / `toggle` take true or false; `number` / `currency` / `percent` / `rating` / `slider` / `progress` take a number; `text` / `textarea` / `email` / `url` / `phone` / `markdown` / `html` / `richtext` / `code` / `color` take a string; `time` takes `"HH:MM"`; `select` / `radio` take the `value` of one of the field's own `options`, never a label. + - **Every other type is refused by name, never stripped.** The refusal is a `custom` issue at the field's `defaultValue` that names the field and its type, says why, and lists the types that take one: a `date` / `datetime` literal is frozen at design time; a reference or file type names a record or file that does not exist yet; a `multiselect` / `checkboxes` / `tags` field stores a list; a `formula` / `summary` / `autonumber` field is computed; `password` / `secret`, `signature` / `qrcode` and the structured JSON types have no one value right for every record. A runtime token (`NOW()`, `current_user`) is refused too: a blueprint default is a literal. + - The strict mirror cannot read `type` while it judges this key, so it can generate a value the lenient schema then refuses. `apply_blueprint` parses the lenient schema, and that refusal names the type at the key. + + Additive only: every blueprint that parsed before still parses with the lenient schema. A producer of the STRICT shape must now send `defaultValue` on each field (null when it has none). The cloud build applier is what carries the value into the built field; it moves on its own pin. +- 4564d6f: fix(spec/system): the platform locale table reads the script-subtag Chinese spellings, so a `zh-Hant-TW` / `zh-Hant-HK` reader gets a registered `zh-Hant` bundle instead of Simplified Chinese + + Clause-②: yes (widening) + Metadata-protocol: semantic + + `PLATFORM_LOCALE_READ_ALIASES` (`@objectstack/spec/system`) gains five rows, one for the script-subtag spelling of each regional Chinese alias: `zh-Hans-CN` and `zh-Hans-SG` read as `zh-Hans`; `zh-Hant-TW`, `zh-Hant-HK` and `zh-Hant-MO` read as `zh-Hant`. `platformLocaleTagOf` answers them (case-insensitively, as for every row), and `PlatformLocaleReadAlias` gains the five literal keys. + + What changes for a reader: browsers and operating systems send these spellings. `resolveBundleLocale` and `resolveLocaleAmong` (and so `resolveLocale` and `resolveOfferedLocale` in `@objectstack/core`) now map them through the table before the base-language step, so over a bundle that holds both scripts a `zh-Hant-TW`, `zh-Hant-HK` or `zh-Hant-MO` request reaches the `zh-Hant` key and a `zh-Hans-CN` or `zh-Hans-SG` request reaches the Simplified key (`zh-CN`, `zh-Hans` or a bare `zh`), whatever order the keys were registered in. Before, the first registered `zh-*` key answered them, and over `{ zh, zh-Hant }` a `zh-Hant-TW` request got the bare `zh` (Simplified) key. The table step maps each registered key through the table too, so a bundle key or offered code spelled `zh-Hant-TW` / `zh-Hans-CN` now counts as its script for every request the table reads as that script: `zh-TW` or `zh-Hant` over `[en, zh-CN, zh-Hant-TW]` gets `zh-Hant-TW` (before: `zh-CN`), `zh-CN` over `[en, zh-Hant, zh-Hans-CN]` gets `zh-Hans-CN` (before: `zh-Hant`), and `zh-Hant-TW` over `[en, zh, zh-Hant-HK]` gets `zh-Hant-HK` (before: `zh`). Setup's default-language options (`localization.locale`) name the five codes from the table (`zh-Hant-TW` is 繁體中文, `zh-Hans-CN` is 简体中文) where they used the runtime's display name (中文(繁體,台灣)), and a stored `zh-Hant-TW` default now shows selected on the 繁體中文 option: where the deployment listed `zh-CN` before `zh-Hant`, it used to take the place of the 简体中文 option. The values Setup accepts are unchanged. + + Unchanged: the list stays closed. Only these five spellings are added; a script-subtag spelling without a row (`zh-Hant-US`) and every other code the table does not name (`pt-PT`, `en-GB`, `es-419`) answer `undefined` and fall back through base language and variant expansion exactly as before. With no registered key of the requested script, a script-subtag request still reaches the other script's key, as before. No call that matched before stops matching. +- 21510bb: feat(plugin-security)!: under a walled tenancy posture every external object states its organization scope + + Clause-②: no (narrowing) — under the `isolated` and `group` postures an external object (an `external` block) that states no organization scope is refused by name when the boot completes, and one registered after the boot answers no row and accepts no write; nothing is widened, and the `single` posture is unchanged. + Metadata-protocol: narrowed + + + + **BREAKING** for a deployment running the `isolated` or `group` tenancy posture (`OS_TENANCY_POSTURE`) with an external object that states nothing about its organization scope. + + - **The rule (ADR-0015 §10 as amended, ADR-0095 D1).** The platform provisions no storage for an external object, so the `organization_id` it injects into the registered schema is a column the remote may not carry. Whose rows an external object serves — one organization's or every organization's — is a fact only its author knows, and under a walled posture the object states it, in one of three ways: its own `organization_id` field (the remote rows carry the organization column and the driver returns it; a remote column of another name is mapped onto it with `external.columnMap: { : 'organization_id' }`); `tenancy: { enabled: false }` (shared by every organization); or `systemFields: { tenant: false }` (the same opt-out on the system fields). These are the wall's own two opt-out clauses and the one column it reads; the whole-block `systemFields: false` states nothing. + - **What the rule does.** `SecurityPlugin` refuses the boot from its `kernel:ready` handler, beside the ADR-0131 D5 refusal, naming every external object that states nothing and the three ways out (`ExternalObjectOrganizationScopeUndeclaredError`). An external object registered after the boot — a Studio import, a runtime datasource — that states nothing answers no row and accepts no write: the Layer 0 verdict is `deny` (`computeTenantLayer0Verdict`), the write wall refuses every insert, update and delete with a sentence naming the undeclared scope and the three ways out, and the plugin says so once in a warning. An object declaring its own `organization_id` is walled: a member of organization A reads only A's rows, through a driver that honours `where` and through one that ignores it (the engine's floor re-applies the predicate), and a row missing the column reads to no one. An object declaring `tenancy: { enabled: false }` or `systemFields: { tenant: false }` is shared. + - **`tenancy.tenantField` on an external object is closed the same way.** The organization wall keys on `organization_id` alone, and a declared tenant field is read only by storage the platform provisions, which an external object has none of; so on an external object a `tenantField` naming another column is refused at boot and denied after it, with the remedy spelled out (`external.columnMap` onto `organization_id`, or `tenancy: { enabled: false }`). Local objects are unchanged. + - **Who moves.** Every walled deployment with an external object stating nothing: declare one of the three and restart. The showcase's two federated analytics objects now declare `tenancy: { enabled: false }`. A `single`-posture deployment owes no declaration and behaves exactly as before. + - **`@objectstack/core`.** `TenantLayer0Input` gains the optional `externalOrganizationScopeUndeclared`; set, the verdict is `deny` under a walled posture, ahead of the platform-admin crossing. Omitted, every existing caller reads as before. + - **`@objectstack/spec`.** The ADR-0087 D3 entry `external-object-organization-scope-declared` tells an upgrading operator what to declare. + + Skills-docs: follow-up #29866 — `content/docs/data-modeling/virtual-objects.mdx` (a multi-organization section), `skills/objectstack-data/rules/datasources.md` (the three declarations), `docs/adr/0015-external-datasource-federation.md` (the §10 and D7 amendment); the sibling docs PR on `claude/sec-hardening-g-docs`, governed (Tier H), landed by the maintainer's hand. +- 0fd363d: fix(spec)!: an object permission that edits or deletes a record also reads it + + Clause-②: no (narrowing) — the authoring object-permission schema refuses an entry granting `allowEdit: true` or `allowDelete: true` without read (`allowRead`, `viewAllRecords` or `modifyAllRecords`), a permission set it accepted before; the permission evaluator grants edit and delete only together with read; and a by-id update or delete from a caller the read door refuses answers that refusal. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: docs moved — `content/docs/permissions/permission-sets.mdx` states the rule under the object permission bits; `skills/**` teaches no entry granting edit or delete without read (its examples grant read beside edit), so no skill moves. + + + + **BREAKING** accept-set narrowing on `ObjectPermissionSchema` (and so `PermissionSetSchema`, `definePermissionSet`, `defineStack`, `os validate` / `os build`, and the metadata save and publish doors for `permission`). + + - **Before.** `objects: { crm_lead: { allowEdit: true } }` parsed, and the evaluator granted edit on `crm_lead` to a holder who could not read it. + - **Now.** The schema refuses it: one `custom` issue at `objects.crm_lead.allowEdit` (or `.allowDelete`), naming both fixes. `objectPermissionGrants` answers edit and delete as `read ∧ grant` (the shape `allowExport` already had), and `PermissionEvaluator.checkObjectPermission` asks it of the caller's merged entry, so the read half may come from another set — the same answer `/me/permissions` gives the client. + - **Stored rows.** A permission set stored before this release keeps loading; its unread edit or delete grant grants nothing. The response-side `EffectiveObjectPermissionSchema` stays tolerant. A built artifact carrying the shape is refused when it is parsed and is rebuilt from a corrected source. + - **By-id writes.** A by-id update or delete from a caller whose read of the object is refused — with these grants that is a capability the object requires for a read and not for a write — answers that read refusal for every id it addresses: a caller who cannot read an object writes none of its records by id. + - **`@object-ui/app-shell`.** The permission set editor ticks Read when Edit or Delete is ticked, and unticking any bit that reads (Read, View All or Modify All) unticks Edit and Delete once no bit that reads remains on the entry. + - Unchanged: `allowCreate` and `allowTransfer` are not part of this rule. + + **FROM → TO.** `objects: { crm_lead: { allowEdit: true } }` → `objects: { crm_lead: { allowRead: true, allowEdit: true } }` if the set should edit the object, or `objects: { crm_lead: {} }` if it should not. The upgrade-guide entry is `permission-write-without-read-refused` (a semantic migration: no automatic rewrite, because only the author knows which of the two the set means). +- 63a470a: feat(spec)!: a URL with a special scheme is read for credentials the way a URL parser reads it, however many slashes follow the scheme + + Clause-②: no (narrowing) + Metadata-protocol: narrowed + + + + **BREAKING** accept-set narrowing at the two metadata doors that judge a URL credential: an authored connector instance (`DeclarativeConnectorEntrySchema` — `defineStack({ connectors })`, `os validate` / `os compile`, the artifact load, `PUT /api/v1/meta/connector/:name`) and a datasource driver config's URL keys (`config.url` on postgres, mysql, mongo and turso, turso `config.syncUrl`) at every door that parses the datasource. + + - **A special scheme's authority is read as a URL parser reads it.** For `http`, `https`, `ws`, `wss` and `ftp`, in any letter case, the authority starts after the whole run of `/` and `\` that follows the scheme, however long, none included. A connector string holding a userinfo there, and a datasource URL holding a userinfo password there, are refused (`422 INVALID_METADATA` at the save door, located at the string, naming the parts, never the value); a bare username in a datasource URL stays accepted. + - **One grammar at both doors.** The connector entry and the datasource doors read the one userinfo grammar, and both read redactors rewrite exactly what the write refuses, at the position the write judged, serving every other character as stored. + - **Unchanged.** Every other scheme keeps `scheme://` or a scheme-relative `//` (`postgres:`, `libsql:`, `mongodb:` and the rest carry no authority without `//`); `file:` carries no userinfo; a URL spelled `scheme://` reads exactly as before. A stored row is never re-judged: a save of its served form keeps the stored value, and an edit of the served value stores exactly the edit. + + ## FROM → TO + + ```ts + // before — parsed green; now refused at providerConfig.baseUrl + { name: 'billing_api', label: 'Billing API', type: 'api', provider: 'rest', + providerConfig: { baseUrl: 'https:///billing:…@api.billing.example.com/v1' }, + auth: { type: 'none' } }, + + // after — the URL holds no credential, and the password is a reference + { name: 'billing_api', label: 'Billing API', type: 'api', provider: 'rest', + providerConfig: { baseUrl: 'https://api.billing.example.com/v1' }, + auth: { type: 'basic', username: 'billing', credentialRef: 'BILLING_API_PASSWORD' } }, + ``` + + The same holds for `https:billing:…@host`, `https:/billing:…@host` and `https:\billing:…@host`, and for a datasource URL key: spell it `scheme://`, keep it credential-free (a bare username stays accepted) and bind the password through the Setup → Datasources connection form's secret field, or reference it with `external.credentialsRef`. There is deliberately no automatic rewrite: the value in the string may be the only copy of the secret. The upgrade guide entry is `url-credential-special-scheme-read-however-many-slashes`. + +### Patch Changes + +- 8aa2e91: feat(notifications): the `push` channel — a `notify` node naming `push` reaches the recipient's phones through APNs and FCM + + Clause-②: yes (widening) + Metadata-protocol: none + + - **New package `@objectstack/service-push`.** `PushServicePlugin` registers the kernel's `push` service: APNs over HTTP/2 with token-based auth (an ES256 provider token signed with the `.p8` key, reused for fifty minutes) and FCM over the HTTP v1 API (an OAuth access token from the service account). It reads `PushServiceConfigSchema` from its `config` option and refuses an inline secret at boot with the schema's guidance. It serves the direct transport (`transport: 'direct'`, the operator's own APNs key and Firebase project, for a white-label build); the hosted relay, the schema's default and so also an empty config, is refused at init until its sender ships (#29920). Each secret is named by `credentialRef` and resolved when the transport first signs; in the open tier the reference is the name of an environment variable (ADR-0143 D3). + - **`@objectstack/service-messaging`: the `push` channel.** Mounted while a `push` service is registered, as the sms channel is while an `sms` service is. One delivery reaches every enabled device the recipient registered, on each vendor the push service has a transport for. The push carries the notification id and, when it is a same-server path (`PushRouteSchema`), the notification's `actionUrl` as `route`; any other `actionUrl` is left out and the delivery still goes. Each device's push also carries the origin stored on its row (`PushDeliveryDataSchema.origin`) when the row holds one; the registry does not store it yet, so today none does. A token the vendor reports dead (APNs `BadDeviceToken` / `Unregistered`, FCM `UNREGISTERED`) is retired through `invalidateDeviceToken`: the device row is kept, disabled, and the user's other devices still get the push. A recipient with no enabled device, or whose every device was reported dead, is graded `invalid_recipient`; no push service is refused `transport_not_configured`, as the sms and email channels refuse it. + - **`@objectstack/spec`: description and comment text only.** The `[PLANNED]` markers come off `PushServiceConfigSchema`, `PushDirectTransportConfigSchema`, `ApnsTransportConfigSchema`, `FcmTransportConfigSchema` and `PushDeliveryDataSchema`, which now have their reader and producer; `PushRelayTransportConfigSchema` keeps `[PLANNED — not yet served]`, now saying the push service refuses it at boot (#29920). `NotificationChannelSchema`'s JSDoc lists `push` among the registered channels, the docblocks name `@objectstack/service-push` where they named `plugin-notification-push`, and the `NOT_FOUND` synonym waiver's reason names service-messaging's own provenance row. No key, schema shape or export changes. +- b90a6f7: fix(runtime): a flow start input declared a record id (`objectName`) is loaded in the caller's own scope from the declared object at every door that starts the flow by name, before the run exists + + Clause-②: yes (narrowing) — a start that ran before is now refused when it hands a declared record-id input an id the caller cannot read in the declared object, an id that names no record there, or a value that is not one id. No flow shipped in this repository declares the key, and an input without the declaration answers exactly as before. + Metadata-protocol: none + + + + **The rule.** A flow start input may declare that its value is the id of a record of a named object, with `objectName` on the variable (`{ name: 'accountId', type: 'text', isInput: true, objectName: 'crm_account' }`). Every door that starts the flow by name now loads such an input from the declared object in the caller's own scope before the run exists, whether or not the flow's definition reads the input in plain sight. A record the caller cannot read there and an id that names no record get the same answer, `404 RECORD_NOT_FOUND` (the action door's own refusal), and no run is created. A value that is not one id (a string or a number) is refused `400` naming the input: `VALIDATION_FAILED` from the start check, or, at an action whose param declares a type, `VALIDATION_ERROR` from that param's own contract, which answers first. The system principal is not judged. + + **Which doors.** `POST /api/v1/automation/:name/trigger` and the legacy `POST /api/v1/automation/trigger/:name`; a declared endpoint of `type: 'flow'`; an action of `type: 'flow'` over the REST action route and the MCP `run_action` tool. All of them ask the one check they already shared for inputs a definition reads in plain sight. + + **Beside the definition's own reads, never in place of them.** An input declared a record id of one object and also read by the definition as an id of another (a `get_record` node's `filter.id`, say) is loaded from both, so the caller must be able to read both rows. A parent flow that hands its input on, unchanged, to a `subflow` or `map` callee whose input carries the declaration is judged by that declaration too. A `loop` or `map` that later rebinds the name does not withdraw the declaration: it is the author's word about the value the caller sent. + + **What stays as it was.** An input without the declaration keeps the visible-read rule: it is judged only where the definition reads it as a record id. A start whose caller can read the declared record starts as before. + + **If a start now answers 404 or 400.** Hand the declared input the id of a record the caller can read in the object the input names, or start the flow as the system principal. + + `@objectstack/spec`: the `objectName` describe on a flow variable no longer says that no door applies it, and its liveness row is `live`, so `os validate` no longer warns `liveness-planned-property` on a flow that declares it. +- bcdb1db: fix(spec,messaging): a push device's `locale` is described as what it is — stored for the device, and no push renders by it + + Clause-②: no + Metadata-protocol: none + + - **`@objectstack/spec`**: `UserDeviceSchema.locale` and `RegisterDeviceRequestSchema.locale` no longer say pushes to the device are rendered in its locale. They now say the value is stored for the device and that a push arrives in the recipient's language: `sys_user.locale`, then the deployment default. Description text only; nothing a caller sends is accepted or refused differently. + - **`@objectstack/service-messaging`**: the `sys_user_device.locale` field description says the same, in all four shipped languages. A test now pins it end to end: with the real push channel over the real device registry, a phone registered with a locale other than its user's receives the push in the user's language, on the inline fan-out and on the durable outbox path alike. +- 471add3: docs(approvals): say how a request with no submitter leaves the `returned` state + + Clause-②: no + Metadata-protocol: none + + - **`@objectstack/spec`: comment text only.** The `IApprovalService` docblocks on `recall`, `sendBack` and `resubmit` now state the path for a request with no submitter (its run's trigger resolved no user): nobody can recall it once it is `returned`, a system-context `resubmit` is its way out, and the next round opens `pending` with no submitter, where the system or the override actor may recall it. The `resubmit` docblock now names the system context it already admits. No key, type, schema or behaviour changes. +- a52761f: fix(console): an action another package places on `sys_user` at `record_header` / `record_more` draws again on the user's record page, in the drawer and on the full page (#29951) + + Clause-②: no + Metadata-protocol: none + + - **The header reads `placedActions: true`.** `page:header`'s `placedActions` (#30018) is now drawn by the console's header renderer (`PageHeaderActions` in `@object-ui/components`): `true` draws every action of the page's object whose `locations` include `record_header` or `record_more`, in declaration order, read from the object's metadata at render time, so an action an installed package adds to the object is drawn without editing the page. Omitted or `false`, an authored `actions` list is still the whole set (#11592). A `true` beside an authored list, which the spec refuses, draws none of the object's actions and is reported once in the console. + - **One placed-header reader.** `placedHeaderActions` (`@object-ui/types`) is the rule both headers read: the synthesized default record page's header in `RecordDetailView` and a `placedActions: true` header. The `page:header` registration publishes the `placedActions` input. + - **The platform record pages opt in.** `sys_user_detail` and `sys_organization_detail` (`@objectstack/platform-objects`) and `sys_approval_request_detail` (`@objectstack/plugin-approvals`) set `placedActions: true` in place of the id lists #27219 froze, which dropped every header action another package places on those objects. The tab strips are unchanged, and every action the lists named is placed at `record_header` / `record_more`, so each still draws. + - `@objectstack/spec`: the `placedActions` describe no longer opens with `[PLANNED — not yet rendered]`, and the `page` liveness record names the reader. +- bd65cb6: docs(spec): the push vendor docblocks read ADR-0012 §6.1 as amended + + Clause-②: no + Metadata-protocol: none + + The `PushVendorSchema` docblock and the push transport and stored device docblocks (`system/push-config.zod.ts`, `system/push-device.zod.ts`) no longer say the domestic Android vendors plug in as alternate channel plugins. They now say what ADR-0012 §6.1 decides: `vendor` is the token type the sender routes on (the direct transport today, the hosted relay, the default, once served), core ships `apns` and `fcm`, and phase 2 adds HMS / Honor / Xiaomi / OPPO / vivo behind the relay, with no per-vendor plugin and no per-vendor channel. A new `PushVendorSchema` value is still a spec change made together with the relay's routing for that vendor. Comment text only: no schema, `.describe()`, export or default changes, and `PushVendorSchema` stays `apns | fcm`. +- cde492d: feat(plugin-auth): a page navigation to the mobile sign-in hand-off that has to sign in first goes to the sign-in page, so the app opens only the hand-off + + Clause-②: no + Metadata-protocol: none + + `GET /api/v1/auth/one-time-token/handoff` answered a missing or old session with `401` as raw JSON, so the mobile app had to open the Console's `/login` route itself and know how that page treats `redirect`. An installed app stays in use for months, so a Console upgrade could strand it. Now the app opens only the hand-off: + + - A page navigation with no session, or with one older than five minutes, answers `302` to this deployment's Console sign-in page, with `redirect` naming the same request. `Location` is root-relative, so the browser stays on the origin the request arrived on, and `uiBasePath` is honoured. + - Any other request still answers `401`, so the app's anonymous probe keeps reading "this server registered me". A navigation reporting `Sec-Fetch-Site: same-origin` answers `401` too: it is the sign-in page forwarding a session the route refuses. A deployment that does not serve the Console keeps the `401` too, and says so once at `warn`. + - A token is still minted only for a session under five minutes old, and every `400` (a cross-site navigation, an unknown parameter, an unregistered scheme, a bad challenge) is still decided first, with no redirect. + - A device whose clock is behind does not bounce between the route and the sign-in page. The redirect carries `handoff_refused_at`, the server's time of the refusal, and the page holds its form for any session not created after it, whatever the device's clock says. A session created after it forwards, so a provider's sign-in that returns to the same URL still ends at the app. The links between the sign-in and sign-up pages carry the mark beside a hand-off `redirect`, and every other link is unchanged. + + An app can change its entry only once its server runs this release: an older server answers the new entry with JSON. +- eee4149: feat(spec/system): export the platform locale table `PLATFORM_LOCALES` and its read aliases + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`PLATFORM_LOCALES`** (from `@objectstack/spec/system`): the one list of languages the platform supports, as frozen rows `{ tag, label, rtl }` in menu order: `en`, `zh-Hans`, `zh-Hant`, `es`, `ar` (right to left), `fr`, `pt-BR`, `ru`, `de`, `ja`, `ko`, each labelled with its own native name. `PLATFORM_LOCALE_TAGS` is the tag list and `PlatformLocaleTag` the literal union of the tags. + - **`PLATFORM_LOCALE_READ_ALIASES`** (typed on its ten literal keys, `PlatformLocaleReadAlias`) and **`platformLocaleTagOf(code)`**: the codes stored values already carry, mapped to the table tag they mean (`zh`, `zh-CN`, `zh-SG` to `zh-Hans`; `zh-TW`, `zh-HK`, `zh-MO` to `zh-Hant`; `ja-JP` to `ja`; `es-ES` to `es`; `pt` to `pt-BR`; `en-US` to `en`). A table tag answers itself and the match ignores case (`zh-cn` reads as `zh-Hans`). Any other code answers `undefined`; no region fallback is guessed. + - The table is the target every surface is backfilled to; today the Console ships ten packs without `zh-Hant`, and platform metadata and the built-in emails ship four locales. Nothing reads the table yet and no schema refuses a tag that is not on it: every locale value that parsed before still parses. The protocol page `i18n-standard` gains the table, the read aliases and the rule (same-PR follow; a language is added or removed by changing the table and backfilling once). +- 0c666c3: refactor(objectql): the private docblocks of the last 28 hand-series stubs in `engine.ts` move into their hand modules, onto the functions that hold their bodies; no behaviour or public declaration changes + + Clause-②: no + Metadata-protocol: none + + - The second and last slice of the `engine.ts` codemod's `--hand-docs` pass: 10 hand modules, 858 docblock lines. With the first slice, every private hand-series stub that had a docblock now keeps it beside its body; public and protected docblocks stay on the class. `engine.ts` goes from 12,618 to 11,752 lines. + - The liveness ledger's `query.groupBy` row now cites `engine-delete-and-aggregate.ts#aggregate`, where the body lives, because `engine.ts` no longer names the key. + - Nothing to do on upgrade: the exported names, the public `.d.ts` surface and what every method does are the same. +- 38717d3: fix(spec): the liveness ledger's `query.searchFields` row cites `engine-find.ts#expandSearchOnAst`, where the hand-off lives, so `check:liveness` is green again + + Clause-②: no + Metadata-protocol: none + + - The row cited `engine.ts#expandSearchOnAst`. The body has lived in `engine-find.ts` since the `engine.ts` codemod split, and the last mention of `searchFields` in `engine.ts`, a private docblock, moved out with the first slice of the hand-series docblock move, so the check found no mention of the key in the cited file. + - Nothing to do on upgrade: only the shipped `liveness/query.json` evidence text changes. +- fef212b: docs(spec): the `contributions` array of `GetPackageCustomizationsResponseSchema` says a disabled package's fields are not listed + + Clause-②: no + Metadata-protocol: none + + The `.describe()` of the `contributions` array that `GET /api/v1/packages/:id/customizations` answers already left out a disabled package's flows and navigation items, but its field clause still read as listing the fields of every object extension from any package but the base and its pair. The server has not listed a disabled package's extension fields since the served object's fold began withholding a disabled package's extension; the field clause now says so, in the same parenthetical form as the flow and navigation clauses. Text only: no key, shape, default or export changes, and the reference page `content/docs/references/api/package-lifecycle.mdx` is regenerated from it. +- dc51811: fix(spec): the liveness ledger grades an app page's `record` binding `live`, citing the console reader for each of its keys + + Clause-②: no + Metadata-protocol: none + + - `liveness/page.json` graded `record` and every key under it `planned`, with the note that nothing read a page-level `record` yet. The console now reads it: an app page's binding is read once per page open as the first row under `sort`, and every component's `visibleWhen` sees that row as `record`. `record` and every key under it (`object`, `view`, `filter` with `field` / `operator` / `value`, `sort` with `field` / `order`) are now `live`, each citing the code that reads it. + - Nothing to do on upgrade: only the shipped `liveness/page.json` grades and evidence text change. No key, schema shape or export changes. +- 25fea77: fix(spec): an array in an analytics filter slot is refused once, and the refusal shows the object form + + Clause-②: no + Metadata-protocol: none + + A dashboard widget `filter`, a dataset or measure `filter`, or a report's `runtimeFilter` written as a list view's rule array (`[{ field: 'status', operator: 'in', value: ['new', 'in_progress'] }]`) used to fail `os validate` with two lines that contradicted each other and named no fix: `Expected record but received object.` and `Expected object but received object.`. It now fails with one `invalid_type` issue at the same path, saying that these slots take the condition object a query's `where` takes and showing it: `{ status: { $in: ['new', 'in_progress'] } }`. + + Only the text changes, and the duplicate line goes. Every filter accepted before is accepted, every filter refused before is refused, and the published JSON Schema is unchanged. +- 4be9734: docs(spec)!: the screen-field contract names `number`, `currency` and `percent` as the numeric value domain the resume enforces (#30178) + + Clause-②: yes (narrowing) — the resume door refuses a string under a `type: 'currency'` or `type: 'percent'` screen field (`@objectstack/service-automation`); this package's `ScreenFieldSpec` and `ScreenFieldConfigSchema` text now states that rule. No schema shape changes. + Metadata-protocol: none + + + + **BREAKING** (described here, enforced in `@objectstack/service-automation`): a `currency` or `percent` screen field takes a finite JSON number on resume, as a `number` field does; a numeric string is refused with `invalid_type`, never coerced. `ScreenFieldSpec.type` / `.min` / `.max` and `ScreenFieldConfigSchema.type` / `.min` / `.max` say so, and the `type` describe text names the three kinds. +- bb86ff9: feat(i18n): one label order on an overridden item, for all eight document translators — the pair's translation, then the override's own label, then the base's translation + + Clause-②: yes (widening) + Metadata-protocol: none + + - **New option `TranslateDocumentOptions.pairBundle`.** The translations the served item's paired `.custom` package ships itself (ADR-0146 D2/D3), locale-keyed as `bundle` is. The serving layer's `bundle` is one merged catalog per locale with the package ids dropped, so only this option can tell a translation the pair ships from the base's. + - **The order, per translatable string.** A string the override changed (`valueOverridesPackagedBase` against `packagedBase`, the comparison every translator already makes) reads the pair's translations only, along the locale chain, and otherwise keeps its own text: the base's translation is never read for it. A string the override left alone reads, locale by locale, the pair's translation and then the deployment catalog, so a pair that ships only `en` never displaces the base's `zh-CN` on an untouched label. Nested labels follow the same rule: widget titles, filter and option labels, view bulk-action defs and form sections, report blocks, field labels, help and options, page component copy, tab items and the header, dataset members, app navigation nodes and book groups. + - **`translatePage`, `translateDataset`, `translateApp` and `translateBook` now read `packagedBase`** (their third parameter widens from `ResolveOptions` to `TranslateDocumentOptions`), each string judged against the base's counterpart: the page's components by id and its header, the dataset's members by `name`, the app's navigation nodes by `id`, the book's groups by `key`. A page header title the override left as the base wrote it translates as the base's label. + - **Nothing moves for an existing caller.** With no `pairBundle` (or one that answers none of the chain's locales) every translator gives exactly the answer it gave before, and with no `packagedBase` nothing counts as changed. Passing both for a `.custom` override is the serving layer's half (#30062). + - An object's `validations[].message` and its inline `actions` keep reading the merged catalog: the rule evaluator answers the same message from `i18n.t()` with no pair input, and the refusal text must equal the served text. +- 2046d21: feat(objectql,rest)!: a new assignment to a banned user is refused, and a banned user's records can be counted and handed over in bulk (#30205) + + Clause-②: yes (narrowing) — the published `FieldErrorCode` enum gains the member `reference_user_banned`. A non-system create or update that sets a user field (the owner field, a `user` field, or a `lookup` to `sys_user`) to a user whose ban is in force is refused `400 VALIDATION_FAILED` with that field code, a write that answered `201` / `200` before. A value the row already stores is not judged, so a record that already holds the banned user stays editable. The two new routes and SDK methods are additions. + Metadata-protocol: none + Skills-docs: no move in `skills/**`, which teaches no user-assignment verdict; `content/docs/permissions/administrator-guide.mdx` gains "Handing over a banned user's work" and its offboarding quick path points at it. + + + + **BREAKING** accept-set narrowing at every data write door (`POST` / `PATCH /api/v1/data/OBJECT`, batch, import, and a flow's elevated record write, which keeps the reference check): banning a user signs them out, but the records they own and are assigned stay where they were, and new work could still be handed to them. + + - **Before.** The Console's person picker hid a banned user, but `POST /api/v1/data/OBJECT` with `assignee` set to that user answered `201`, so new work landed on someone who cannot sign in. + - **Now.** The write-time reference check also asks whether a referenced user's ban is in force (`banned` is true and `ban_expires` is empty or still ahead) and refuses such a value with `reference_user_banned`, beside `reference_not_found`. It is read the way the existence probe reads, so a caller who cannot read the user object is refused the same way. A ban whose expiry has passed is not in force. + - **Unchanged.** A value every row of the write already stores (an edit form carries the whole record back), a system write the reference check already exempts (seed, install, restore), and a missing user (still `reference_not_found`). + - **New: the count.** `GET /api/v1/security/users/ID/assigned-work` (`client.security.assignedWork.count`) answers, per object and user field, how many records hold the user, counted under the caller's own context. Platform tables (`sys_*`), `managedBy` objects and `readonly` / `system` fields such as `created_by` are not counted; objects the caller cannot read are listed in `unreadable`; at most 200 fields are counted (`truncated`). + - **New: the hand-over.** `POST /api/v1/security/users/ID/assigned-work/transfer` (`client.security.assignedWork.transfer`) with `{ toUserId, objects? }` moves up to 500 records per call to an active user, each as an ordinary update under the caller's context, and answers per field what moved and what failed, plus how many remain. A banned, unknown or identical `toUserId` is refused before any write. Multi-valued user fields are counted but not transferred. Neither route touches the ban. + - **`@objectstack/spec`.** `FieldErrorCode` gains `reference_user_banned`, with its message in `en`, `zh-CN`, `ja-JP` and `es-ES`. + + **For a caller.** A write that names a banned user in a user field names an active one instead; the refusal names the field and the user. No stored metadata or row changes, so there is nothing for `objectstack migrate meta` to rewrite. An integration that reassigns work after a ban can call the transfer route and repeat it while `remaining` is above 0. +- 47226e6: feat(spec): `IOwnershipQueueService` lets a caller ask for a refusal on a failed member read (`onFailedRead`), the call-time signal ADR-0142 D2 asks each consumer to send + + Clause-②: yes (widening: an optional `onFailedRead` on `expandMembers`' options and an optional options argument on `isMember`; precedent PR #30015) + Metadata-protocol: none + + - **New type `OwnershipQueueFailedReadAnswer`** (`'nobody' | 'refuse'`, `@objectstack/spec/contracts`): what a membership read answers when a read behind the queue fails (`sys_queue`, `sys_queue_member`, `sys_team_member`, `sys_user_position`, `sys_member` through the position transition limb, the business-unit membership and tree). + - **`expandMembers(name, organizationId, { purpose?, onFailedRead? })`** and **`isMember(userId, name, organizationId, { onFailedRead? })`**. `'nobody'` is the default and the answer every caller had before: a failed read is logged at `warn` and contributes nobody. Under `'refuse'` a failed read rejects with a failure that names the read, never `[]`, a partial set or `false`; an empty queue is still `[]`. The docblock names who takes which: the access path (D3), the claim admission (D4) and the assignment pool (D5) take `'nobody'`; the approver slate (D6) and the sharing-recipient grant recompute (D7) take `'refuse'`. The key is independent of `purpose`. + - **Contract only.** Every existing call and implementation compiles unchanged and behaves as today; the producer in `@objectstack/plugin-sharing` and the D6/D7 callers that pass `'refuse'` land with #29604. +- 0005fec: fix: on a zh-CN console every row of the view properties panel's collapsed sections (表格选项 / 分页 / 终端用户控制 / 导航与共享) reads in Chinese + + Clause-②: no + Metadata-protocol: none + + - `viewForm` (`@objectstack/spec/ui`) now spells out the children of its `selection`, `pagination`, `userFilters`, `appearance`, `userActions`, `navigation` and `sharing` composites, and gives the rows that had none a hint. Each declared hint is the schema description the panel already showed, so an English console reads the same text; `os i18n extract` now emits a `metadataForms.view.fields['.']` key per row. + - `userFilters.fields` is enumerated as an inline-table repeater, and `UserFilterFieldSchema`'s six properties carry a `.meta({ title })` (Field / Label / Type / Options / Show Count / Default Values) — the English column heads, unchanged. + - The panel no longer offers `navigation.view` and `sharing.type`: both are `retiredKey()` tombstones that the schema-derived child list used to render as editable rows. A stored value is untouched by a panel save (the composite editor spreads the block it was given). + - The platform metadata-forms catalogs carry zh-CN, ja-JP and es-ES text for every new row, and the console's zh-CN overlay copies the zh-CN rows. +- 2d6402c: fix(spec): the `flow-record-read-and-decision-type-mismatch-refused` upgrade-guide entry names the object a time-relative flow's conditions are typed against: the swept `config.timeRelative.object`, when the start node names no `objectName`, and the start node's `objectName` when that is the swept object (#30279) + + Clause-②: no + Metadata-protocol: none + + Text only: the entry's `surface` and the matching sentence of its `reason`, which `os migrate meta` and the upgrade guide show, said the refusal covers a time-relative flow "sweeping the start node's objectName". The author-time gate also types such a flow against `config.timeRelative.object` when its start names no `objectName`, and refuses the same conditions there; the entry now says so. Its id, kind, replacement and acceptance criteria are unchanged, and no verdict moves. +- 77c203a: docs(spec,i18n): stop naming the removed feature-flags settings page + + Clause-②: no + Metadata-protocol: none + + The `feature_flags` settings namespace and its Setup page were removed from `@objectstack/service-settings` and `@objectstack/platform-objects`. Two shipped texts still pointed at it; both now say what is live. + + - **Settings hub subtitle (`@object-ui/i18n`).** `console.settingsHub.subtitle` no longer promises feature flags, in all ten language packs. English FROM "Configure your system, integrations, and feature flags." TO "Configure your system and integrations."; each other pack drops its own term the same way. No key is added or removed. + - **Spec doc comments (`@objectstack/spec`).** The kernel barrel and `PUBLIC_AUTH_FEATURES` docblocks no longer send readers to a `feature_flags` settings manifest or to `OS_FEATURE_FLAGS_*`. A setting a plugin reads is declared on that plugin's own settings manifest (ADR-0007, env-overridable as `OS_{NAMESPACE}_{KEY}`); capabilities a deployment advertises to clients are the `PUBLIC_AUTH_FEATURES` registry. The `SettingsManifestSchema` docblock (and its generated reference page) gives `mail`, `branding`, `localization` as example namespaces. No schema, type or export changes. +- 8293c74: refactor(lint): the i18n translatable-key walk moves from the CLI into `@objectstack/lint` + + Clause-②: no + Metadata-protocol: none + + `collectExpectedEntries` — the one definition of which translation keys a stack can carry, each with the source text it translates — now lives in `@objectstack/lint` (`src/i18n-walk.ts`) and is exported from the package root with `authorWarnedTranslationGroups`, `humanizeFieldPath` and the `ExpectedEntry` / `ExpectedEntryOptions` types. It moved so a browser can run the same walk the CLI runs: Studio's translation editor builds its source-to-translation table from it. `os i18n extract` and `os lint`'s translation coverage keep reading it unchanged, through the CLI's extractor module, which keeps bundle assembly and serialization and re-exports the walk. + + - No key, no skeleton and no coverage verdict changes: the walk's code moved as is. + - In a browser the walk cannot read the liveness ledger from disk, so its default leaves no translation group out. That is the same set the CLI leaves out today, because the ledger warns on no translation group; a browser caller that must honour a warned group passes `warnedGroups`. + - `@objectstack/spec`: the `action` and `view` liveness ledgers cite the walk at its new path. +- 29702d5: fix(spec,rest): the record page's `timeline` count counts one create or delete written to both the activity and the ledger once, as the 动态 stream draws it (#30343) + + Clause-②: yes (narrowing) — `RecordPageTimelineCountSchema.total` falls for the same rows: with a field change counted, `created` (`deleted`) activity rows now step aside as often as the ledger holds `create` (`delete`) writes, beside the `updated` rows that already did; widening in the same line: `@objectstack/spec/api` exports one more constant, `RECORD_TIMELINE_FOLDED_ACTIVITY_TYPES`. No exported member is removed or renamed. + Metadata-protocol: none — a read answer's count, not anything an author writes. + + + + - **Now.** An audit reader on a record with field history sees the 动态 badge count one action once: one create and one edit read 「动态 2」 above the two rows the stream draws. Before, the create counted twice (3). + - **The rule.** Every `updated` row steps aside, as before. A create (a delete) steps aside as often as both sources hold one: the fewer of the record's `created` (`deleted`) activity rows and its `create` (`delete`) ledger writes. A record created before its history was kept has no `create` write, so its `created` row stays in the count, as the stream draws it. + - **Unchanged.** A member without audit read counts the same activity rows as before. A comment adds one. The total stays an upper bound for the `showCompleted` / `types` and no-diff cases; it can fall one short where the stream does not pair a create's (delete's) two rows (different actors, or stamped more than the stream's pairing window apart). +- a839020: docs(spec): `Seed.locale` says which loads filter on it, and the ownership-queue contract says the registered service honours `onFailedRead: 'refuse'` + + Clause-②: no + Metadata-protocol: none + + Wording only: no schema shape, no type and no behaviour changed. + + - **`Seed.locale`** (docblock and `.describe()`, which the references pages render). It no longer says that installing is locale-neutral. The boot seed and the dev hot-reload seeder filter on the app's `i18n.defaultLocale`. The marketplace install, the sample-data reseed, the restart heal and the per-organization replay filter on the locale `resolveSeedLocaleForLoad` (`@objectstack/runtime`) resolves for the load, first answer wins: the `localization.locale` setting, counted only when somebody set it (not `source: 'default'`); then the `sys_user.locale` of the user who installs, reseeds or founds the organization; then the app's `i18n.defaultLocale` where the load names one. Package apply and draft publish still pass no locale: they load every dataset and warn. + - **`OwnershipQueueFailedReadAnswer` and `IOwnershipQueueService.isMember`** (`@objectstack/spec/contracts`). The registered service, `@objectstack/plugin-sharing`'s, honours `'refuse'`; the "until then a caller passing `'refuse'` still gets `'nobody'`" clause is gone. The activation-ledger read keeps reading "switched on" when it fails, under both answers, and the contract now states that as decided. `isMember` under `'refuse'` reads the queue's own rows (`sys_queue`, `sys_queue_member`) first and rejects at once when one fails; past them it answers `true` as soon as a successful read reaches the user, even beside a failed read, and rejects only when no successful read reaches the user and one failed. +- b55d6ab: fix(service-settings): the settings manifest list serves a sourced select's options, the table the save judges against, as the namespace read does + + Clause-②: no — the manifest list now states the table the save already enforces; nothing a caller sends is refused differently. + Metadata-protocol: none + + A select key whose options come from the running system (`localization.locale`: the languages the deployment's i18n service reports) was served its live options only by the namespace read (`GET /api/settings/:namespace`). `SettingsService.getManifest()`, `SettingsService.listManifests()` and `GET /api/settings` still carried the declared table (the four codes `en-US`, `zh-CN`, `ja-JP`, `es-ES`), so a reader of the manifest list (the Setup hub, an SDK caller) was told a table that is not the one a save is judged against. + + - **Now.** All three are served through the same resolution as the namespace read: a sourced key carries its source's options; every other key, and a sourced key whose source has no answer (no i18n service in the composition), carries the declared table as before. `getManifest(namespace, display?)` and `listManifests(ctx?, display?)` take an optional display language; `GET /api/settings` passes the request's. The registered manifest is never changed. + - **Spec.** The `valueDomain` description in `settings-manifest.zod.ts` names the runtime option source beside `valueDomain` as the other mover of a select's enforcement boundary: where a source answers, the declared `options` are the fallback. +- aad1603: fix(spec): `PackageInstallResponseSchema` declares the installed row that `POST /api/v1/packages` really answers + + Clause-②: yes (narrowing) + Metadata-protocol: none + + **BREAKING** accept-set narrowing at the install answer contract (`@objectstack/spec`): `PackageInstallResponseSchema` refuses the `{ package }` wrapper it accepted, and `PackageInstallResponse['data']` no longer declares `package`, `dependencyResolution`, `namespaceConflicts` or `message`. + + `POST /api/v1/packages` answers `{ success: true, data: }` with status `201`, where the row is the `InstalledPackage` the registry recorded. `PackageInstallResponseSchema` (the output `PackageApiContracts.installPackage` binds) declared `data: { package, dependencyResolution?, namespaceConflicts?, message? }` instead. No surface sent that wrapper, so code written against the contract read `data.package` and got `undefined`. The first-party client already followed the wire: `client.packages.install` resolves to `InstalledPackage`. + + - **FROM** `data: { package: InstalledPackage, dependencyResolution?, namespaceConflicts?, message? }` **TO** `data: InstalledPackage`. The name, the type names (`PackageInstallResponse`, `PackageInstallResponseParsed`) and the binding are unchanged. The server's answer is unchanged. + - **Both directions.** The schema now accepts the bare row the door serves, which it refused, and refuses the `{ package }` wrapper no server sends. The type `PackageInstallResponse['data']` loses `package`, `dependencyResolution`, `namespaceConflicts` and `message`, so TypeScript code that reads them stops compiling. That code read `undefined` at runtime already. + - **Fix for a consumer:** read the row from `data` itself. Replace `response.data.package` with `response.data`. Drop any read of `data.dependencyResolution`, `data.namespaceConflicts` or `data.message` on this route: the door never sent them. + - **Level.** `patch`: this is the shape of an API answer, not anything a metadata author writes. + + +- 55d9dd4: feat(spec,service-analytics,data-objectstack): a dataset answer says, per measure, which records that measure counted (`drillScopes`) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/spec`: `AnalyticsResult.drillScopes?: Record`**, also declared on the `AnalyticsResultResponse` wire schema. Each selected measure name maps to the list filter, on the answer's `object`, that selects the records the measure aggregates: the dataset's own `filter`, AND the measure's own `filter`, AND, for a measure over a field, that field holding a value (both instants for a duration measure), with each `{token}` resolved as the query resolved it. It is `{}` when none of these applies. A measure that no list filter can select is left out: a derived measure, or a scope that names a relationship path (`account.region`, as a key or a `$field` reference). A measure left out offers no drill. A `__compare` column has no entry. + - **`@objectstack/service-analytics`: `queryDataset` sets `drillScopes` on every dataset answer that has rows**, on both query faces, from the same token-resolved filters the numbers were computed under. A drill that lists the clicked bucket (`dimensionFields` / `drillRawRows` / `drillRawTotals`, `drillRanges`) AND the widget's own filter AND this scope lists exactly the records the number counted. Before this, a drill built from the bucket and the widget filter alone listed every record in the bucket, so a filtered measure such as "paid invoices" showed 4 records behind the number 2. + - **`@object-ui/data-objectstack`: `queryDataset` passes `drillScopes` through**, typed as the spec slot. The adapter lists the keys it returns, so until now the sidecar was dropped before it reached a widget. +- 0c9aa82: fix(spec): the liveness ledger's `Seed.locale` row names the six seed-load request builders and where each one's locale comes from, so it no longer says the marketplace install passes none + + Clause-②: no + Metadata-protocol: none + + - `liveness/seed.json`'s `locale` row said three publish/install-time paths pass no locale, the marketplace install among them. The install paths now pick seed datasets by locale: the marketplace install, the sample-data reseed, the restart heal and the per-organization replay each resolve one (the organization's Localization setting, then the user's language, then the app's `i18n.defaultLocale`). The boot seed and the dev hot-reload seeder still use the app's `i18n.defaultLocale`. Package apply and draft publish still pass none. + - The earlier ruling that publish and install are locale-neutral is kept as recorded, followed by a dated line saying its bound now covers package apply and draft publish only. The note also says why the loader's no-locale warning stays. + - Nothing to do on upgrade: only the shipped `liveness/seed.json` note text and its `verifiedAt` date change. No key, schema shape or export changes. +- f2ea193: fix(spec): `resolveBundleLocale` and the bundle reads behind every translator answer a locale only from the bundle's own keys + + Clause-②: no + Metadata-protocol: none + + A requested locale code that names an `Object.prototype` member (`constructor`, `toString`, `__proto__`) is found on every plain-object bundle. Until now `resolveBundleLocale` answered such a code as a registered locale, and the translators' bundle read handed back the inherited function as that locale's data. One answer was visible to readers: an object read (`/meta`) under `?locale=constructor` or `Accept-Language: constructor` served the `Object` constructor as the label of every platform-injected system field still at its English default (so the label went missing from the JSON), and under `__proto__` served `Object.prototype` there. Both reads now check for an own property: the code reads as an unknown locale and takes the same fallback `'xx'` takes, so those fields keep their English label. A bundle that really carries such a key as its own (built by `JSON.parse` or on `Object.create(null)`) still answers it, and an own key whose value is `undefined` still does not answer the exact step. No signature, export or schema changes. +- eca7cd2: feat(spec): `PLATFORM_TOOLS_BY_PACKAGE['service-ai-studio']` lists `create_record`, `update_record` and `remove_sample_data`; `draft_record` is retired + + Clause-②: no + Metadata-protocol: none + + The cloud chat agents now create or change ONE record of the user's data after the user confirms it on a card, and remove the sample rows a seed replay recorded after a card listing them (cloud#3565, the maintainer's ruling of 2026-11-04). The three tools replace `draft_record` (cloud#3383), whose "draft it and the user saves it in a form" ending was never built. The registry lists the three names in their alphabetical order and drops the retired one, so `PLATFORM_PROVIDED_TOOL_NAMES` carries them and a skill whose `tools[]` names them resolves under ADR-0109 reference integrity, while a skill still naming `draft_record` is reported as a fictional tool (`ai-skill-tool-unresolved`). That is the whole change: three accepted `skill.tools[]` values in, one out. No schema shape changes, no export is added or removed. + + **Reach of the retirement.** `draft_record` was a published constant, so removing it breaks any skill whose `tools[]` still names it. Measured before this landed: no skill in this repo names it (`validate-ai-tool-references.test.ts` names it only as the control proving the report fires; the two spec page fixtures called `draft_record` are page names, not tools); the one skill anywhere that names it is the cloud chat's `record_drafting` skill, which the cloud half of cloud#3565 (cloud#3574) moves to `create_record` / `update_record` in the same landing. A third-party skill that still names it reports `ai-skill-tool-unresolved` at validation and is not silently dropped. That is why this is graded `patch` and not left as a silent one. + ## 18.3.0 ### Minor Changes diff --git a/packages/spec/package.json b/packages/spec/package.json index c60250de9cd..d851b73d825 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/spec", - "version": "18.3.0", + "version": "18.4.0", "description": "ObjectStack Protocol & Specification - TypeScript Interfaces, JSON Schemas, and Convention Configurations", "license": "Apache-2.0", "type": "commonjs", diff --git a/packages/triggers/trigger-api/CHANGELOG.md b/packages/triggers/trigger-api/CHANGELOG.md index d65db0e64d8..68e1ed1a607 100644 --- a/packages/triggers/trigger-api/CHANGELOG.md +++ b/packages/triggers/trigger-api/CHANGELOG.md @@ -1,5 +1,87 @@ # @objectstack/trigger-api +## 18.4.0 + +### Patch Changes + +- f9e5bef: feat(trigger-api): an `api` hook verifies the vendor signature its start node declares (`config.signature`) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/trigger-api`.** `start()` reads the hook's one verifier with `apiTriggerVerifierOf(binding.config)` (`@objectstack/spec/automation`). A start node that declares its sender's scheme (`config.signature`: DocuSign Connect's base64 `X-DocuSign-Signature-1`, a Stripe-style `t=…,v1=…`, a GitHub-style `sha256=`) now admits a correctly signed post that was answered `401` before. That scheme REPLACES the `x-objectstack-signature` check for its hook: the declared header is read on both faces of the door (the self-hosted mount and `handleInboundHook`) with `Headers.get`, so a repeated field is its one joined value; the HMAC-SHA256 is computed under the key `config.secretRef` names (read at every post through the engine's reader, `503` while it has no value), over the raw body or `.`; each digest is decoded strictly to 32 bytes and compared with `timingSafeEqual`, any one of several `v1` elements matching (a key roll); and a timestamped scheme is held to `HTTP_SIGNATURE_TOLERANCE_SECONDS` from `@objectstack/core`. A body-only scheme has no replay window, as the contract states. A valid `x-objectstack-signature` admits nothing on such a hook, and a post without the declared header is refused. A declaration the spec answers `unusable` arms a hook that refuses every post before any secret is read and never falls back to `x-objectstack-signature`; a declared scheme bound without the engine's `config.secretRef` reader is refused at `start()`. Every failure is the same `401 INVALID_SIGNATURE` with its own cause in the server log (`declared-header-absent`, `declared-header-malformed`, `declared-prefix-missing`, `declared-no-digest`, `declared-mismatch`, `declared-outside-window`, `declared-unusable`), never the key, the header value or the body. A hook that declares no scheme is verified exactly as before. + - **`@objectstack/lint`.** `validateFlowApiTriggerSecret` grades an `api` hook by that same reader: a usable declaration is ready on its `config.secretRef` alone, and an unusable one is the new error `flow-api-trigger-signature-unusable` (with its `os explain` entry), located at the key the spec refuses. It fires only on a declaration the flow parse already refuses, so no stack that passed before is refused now. + - **`@objectstack/skills`.** The automation and api skills teach `config.signature` where they said a vendor-signed callback cannot be admitted. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/triggers/trigger-api/package.json b/packages/triggers/trigger-api/package.json index 91a9384dfaa..0f2db3aaaa4 100644 --- a/packages/triggers/trigger-api/package.json +++ b/packages/triggers/trigger-api/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-api", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Inbound HTTP/webhook flow trigger for ObjectStack — per-flow HMAC-verified endpoints with queue-backed ingestion (ADR-0041)", "type": "commonjs", diff --git a/packages/triggers/trigger-record-change/CHANGELOG.md b/packages/triggers/trigger-record-change/CHANGELOG.md index 1c489cdb2ff..f7621e7c515 100644 --- a/packages/triggers/trigger-record-change/CHANGELOG.md +++ b/packages/triggers/trigger-record-change/CHANGELOG.md @@ -1,5 +1,79 @@ # @objectstack/plugin-trigger-record-change +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/triggers/trigger-record-change/package.json b/packages/triggers/trigger-record-change/package.json index 6abe554bd01..9db506b7ab6 100644 --- a/packages/triggers/trigger-record-change/package.json +++ b/packages/triggers/trigger-record-change/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-record-change", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Record-change flow trigger for ObjectStack — auto-launches flows on object insert/update/delete via ObjectQL lifecycle hooks (ADR-0018)", "type": "commonjs", diff --git a/packages/triggers/trigger-schedule/CHANGELOG.md b/packages/triggers/trigger-schedule/CHANGELOG.md index f3ea2f678bf..9ed371ad0d9 100644 --- a/packages/triggers/trigger-schedule/CHANGELOG.md +++ b/packages/triggers/trigger-schedule/CHANGELOG.md @@ -1,5 +1,82 @@ # @objectstack/plugin-trigger-schedule +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [60fe44f] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [f7afab8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [f4cd4ec] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/triggers/trigger-schedule/package.json b/packages/triggers/trigger-schedule/package.json index c00a8acedf7..99a5a59af7a 100644 --- a/packages/triggers/trigger-schedule/package.json +++ b/packages/triggers/trigger-schedule/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-schedule", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Schedule flow trigger for ObjectStack \u2014 auto-launches flows on a cron/interval/once schedule via the IJobService (ADR-0018)", "type": "commonjs", diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index 033495aad04..ab32e70f5e4 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/types +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/types/package.json b/packages/types/package.json index 5f02e0dc21e..59a4c23ccc4 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/types", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Shared interfaces describing the ObjectStack Runtime environment", "type": "commonjs", diff --git a/packages/verify/CHANGELOG.md b/packages/verify/CHANGELOG.md index d114ce9f172..b025f8f6594 100644 --- a/packages/verify/CHANGELOG.md +++ b/packages/verify/CHANGELOG.md @@ -1,5 +1,251 @@ # @objectstack/verify +## 18.4.0 + +### Minor Changes + +- 9ebca94: feat(push): `requires: ['push']` and the stack's `push` key turn mobile push on under `objectstack serve`, and a relay configuration is refused at boot + + Clause-②: yes (widening) + Metadata-protocol: added + Skills-docs: follow-up owed — #29969 (skills/objectstack-platform/evals/config-plugins-ops.json: no eval asks for `requires: ['push']` or the `push` key); this diff teaches both in `skills/objectstack-platform/SKILL.md` (the top-level key enumeration and the `requires` table) and in the new `content/docs/kernel/runtime-services/push-service.mdx` + + A deployment that runs `objectstack serve` from a stack file can now turn on mobile push without host code. Before this, the only way to register a push service was a host that composed `PushServicePlugin` in code, so a `notify` node naming `push` had nothing to deliver through on a served stack. + + - **`@objectstack/spec`: the `push` capability token and the `push` stack key.** `push` joins `PLATFORM_CAPABILITY_TOKENS`, provided by `@objectstack/service-push` in the open edition. It is requested, never on the always-on slate. The stack definition gains a top-level `push` key beside `email` and `sms`: it is `PushServiceConfigSchema`, composed as a single value (`COMPOSE_KEY_DISPOSITIONS`). Authoring accepts a direct block, a relay block and absence; it refuses an inline secret, by key name (`key`, `privateKey`, `serviceAccount`, …) with `credentialRef` as the remedy, or pasted into a `credentialRef` (ADR-0143 D3). `PushServiceConfigSchema`'s docblock now names the stack key and `objectstack serve` as its reader; `PushRelayTransportConfigSchema` keeps its `[PLANNED — not yet served]` marker. + - **`@objectstack/core`: the `push` provider row and its argument.** `CAPABILITY_PROVIDERS.push` names `PushServicePlugin`, and `resolveCapabilityArgument('push', …)` builds its argument from the stack's `push` key through `resolvePushCapabilityArg`, read off the provider module as the `email` and `sms` readers are. Both `objectstack serve` and `@objectstack/verify`'s `bootStack` read this table, so an app's tests get the push service its server gets. + - **`@objectstack/service-push`: `resolvePushCapabilityArg`.** It parses the `push` key with `PushServiceConfigSchema` and refuses, before the kernel boots, two configurations: the relay transport (an absent key and `push: {}` read as it), whose sender has not shipped, with a message that says so and prescribes `transport: 'direct'` with an `apns` and/or `fcm` block; and anything the schema refuses, an inline secret first among them, naming each path and the schema's guidance and never the value. On a stack that declares `requires: ['push']` either refusal fails the boot as `Capability "push" (@objectstack/service-push) failed to start: …`. + - **`@objectstack/cli` and `@objectstack/verify`** depend on `@objectstack/service-push`, so the provider the token names resolves out of the box, as the open edition promises. + + To turn push on, add `requires: ['push']` and a direct block naming each vendor's secret by reference: + + ```ts + push: { + transport: 'direct', + apns: { teamId: 'ABCDE12345', keyId: 'KEY1234567', topic: 'com.example.app', environment: 'production', credentialRef: 'APNS_SIGNING_KEY' }, + fcm: { projectId: 'example-app', credentialRef: 'FCM_SERVICE_ACCOUNT' }, + }, + ``` + + In the open tier each `credentialRef` is the name of an environment variable holding the secret. A `push` key on a stack whose `requires` does not name `push` is not read, and `objectstack serve` logs one boot warning saying so. The runtime-services chapter has a new `services.push` page. + +### Patch Changes + +- aa57c76: feat(spec): three forward date-range presets: `next_month`, `next_quarter` and `next_30_days` + + Clause-②: yes (widening) + Metadata-protocol: added + + The relative dates a dashboard date filter and the filter builder offer looked only backward. A forecast asks forward: what closes next month, next quarter, in the next 30 days. Those questions had to use a custom range, whose fixed dates go stale. + + `DATE_RANGE_PRESETS` now also declares `next_month` (the whole of next month), `next_quarter` (the whole of next quarter) and `next_30_days` (today and the 30 days after it, the mirror of `last_30_days`). Every place that accepts a preset accepts them: `dashboard.dateRange.defaultRange`, a date global filter's `defaultValue`, the filter builder's `between` row and `AnalyticsQuery.timeDimensions[].dateRange`. They sit beside the family they extend in the filter bar: `next_month` after `last_month`, `next_quarter` after `last_quarter`, `next_30_days` after `last_90_days`. Their `DATE_RANGE_PRESET_MACRO_WINDOWS` rows follow the inclusive last-day convention: `['{next_month_start}', '{next_month_end}']`, `['{next_quarter_start}', '{next_quarter_end}']` and `['{today}', '{30_days_from_now}']`. A preset name written as a bare filter comparand is refused with that window as the prescription, as for the other presets. + + `@objectstack/core`'s `resolveAnalyticsDateRangePreset` resolves the three in its half-open form. `next_30_days` stops before `{31_days_from_now}`. `next_month` and `next_quarter` stop before the month and the quarter after next, which no macro token names: the resolver reads `next_month_start` (or `next_quarter_start`) again from the start of the day it named, so the end is still a day the one token resolver computed. No macro token is added. + + In the console, the three presets have labels in all ten locale packs, and the client date-macro resolver now answers `{next_month_end}` and `{next_quarter_end}`, two spec tokens it did not resolve before. `@objectstack/verify`'s dashboard-window proof also writes fixture rows on a forward preset's future edges, so it judges the three new windows; the days it writes for the other presets are unchanged. `@objectstack/lint` changes documentation only. +- Updated dependencies [31fe289] +- Updated dependencies [a35fc66] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [13c0c5b] +- Updated dependencies [fe835b4] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [5cdaddb] +- Updated dependencies [75bbebb] +- Updated dependencies [509c40a] +- Updated dependencies [c23501b] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [8d5aab3] +- Updated dependencies [665db00] +- Updated dependencies [e3b56b2] +- Updated dependencies [18112bf] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [cd5f554] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f9e5bef] +- Updated dependencies [63b86b3] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [cfb602c] +- Updated dependencies [171b6dd] +- Updated dependencies [f6f663a] +- Updated dependencies [343ddb2] +- Updated dependencies [0c666c3] +- Updated dependencies [f672e0d] +- Updated dependencies [38717d3] +- Updated dependencies [bfbdeec] +- Updated dependencies [4adc890] +- Updated dependencies [fdad178] +- Updated dependencies [bcdf8d3] +- Updated dependencies [59bcd22] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [159930a] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [a545a04] +- Updated dependencies [04f36b6] +- Updated dependencies [25fea77] +- Updated dependencies [06cccd3] +- Updated dependencies [86ae2ca] +- Updated dependencies [4be9734] +- Updated dependencies [4be9734] +- Updated dependencies [1a3ea0c] +- Updated dependencies [bb86ff9] +- Updated dependencies [8f05e61] +- Updated dependencies [9afcd6a] +- Updated dependencies [870ebce] +- Updated dependencies [d1d90b7] +- Updated dependencies [977be38] +- Updated dependencies [2046d21] +- Updated dependencies [740edc5] +- Updated dependencies [778aedf] +- Updated dependencies [71318af] +- Updated dependencies [db04e5c] +- Updated dependencies [a26abbc] +- Updated dependencies [8eca18f] +- Updated dependencies [8b117c0] +- Updated dependencies [d9d42d4] +- Updated dependencies [67d5d53] +- Updated dependencies [47226e6] +- Updated dependencies [a545a04] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [8589756] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [bee9836] +- Updated dependencies [a93ef3e] +- Updated dependencies [0e4dfbe] +- Updated dependencies [8cc709d] +- Updated dependencies [b5765be] +- Updated dependencies [5a878a3] +- Updated dependencies [22f0d65] +- Updated dependencies [fabd259] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [26c2efd] +- Updated dependencies [f7afab8] +- Updated dependencies [7b80724] +- Updated dependencies [fb1ab89] +- Updated dependencies [6c4c43a] +- Updated dependencies [455b4f0] +- Updated dependencies [4dde0ae] +- Updated dependencies [94cef75] +- Updated dependencies [e8e924f] +- Updated dependencies [590724f] +- Updated dependencies [35cd64c] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [d5f3bc5] +- Updated dependencies [ae5bd5b] +- Updated dependencies [e3948ae] +- Updated dependencies [b55d6ab] +- Updated dependencies [1c5cee6] +- Updated dependencies [aad1603] +- Updated dependencies [135c2fb] +- Updated dependencies [898145b] +- Updated dependencies [a0c68ae] +- Updated dependencies [ede9e6b] +- Updated dependencies [c5101a0] +- Updated dependencies [08e0247] +- Updated dependencies [6701c08] +- Updated dependencies [ca37605] +- Updated dependencies [cabdfe8] +- Updated dependencies [ab159a9] +- Updated dependencies [66a8822] +- Updated dependencies [739c972] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [63de248] +- Updated dependencies [c35fed1] +- Updated dependencies [a2437b5] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [badf24d] +- Updated dependencies [474c030] +- Updated dependencies [f4cd4ec] +- Updated dependencies [b3d2f4b] +- Updated dependencies [70981e6] +- Updated dependencies [85c7374] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [42e1211] +- Updated dependencies [7193160] +- Updated dependencies [72d7db0] +- Updated dependencies [d0da880] +- Updated dependencies [7b2bb66] +- Updated dependencies [30dcd8a] +- Updated dependencies [c92b887] +- Updated dependencies [21510bb] +- Updated dependencies [d7c0389] +- Updated dependencies [6dd2ca5] +- Updated dependencies [917f024] +- Updated dependencies [0fd363d] +- Updated dependencies [bdbaf80] +- Updated dependencies [a7293f7] +- Updated dependencies [59e6463] +- Updated dependencies [63a470a] +- Updated dependencies [4851668] + - @objectstack/objectql@18.4.0 + - @objectstack/service-analytics@18.4.0 + - @objectstack/service-push@18.4.0 + - @objectstack/service-messaging@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/runtime@18.4.0 + - @objectstack/plugin-approvals@18.4.0 + - @objectstack/plugin-sharing@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/service-datasource@18.4.0 + - @objectstack/rest@18.4.0 + - @objectstack/plugin-email@18.4.0 + - @objectstack/plugin-auth@18.4.0 + - @objectstack/mcp@18.4.0 + - @objectstack/service-queue@18.4.0 + - @objectstack/service-settings@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/trigger-api@18.4.0 + - @objectstack/plugin-audit@18.4.0 + - @objectstack/plugin-security@18.4.0 + - @objectstack/service-automation@18.4.0 + - @objectstack/service-storage@18.4.0 + - @objectstack/plugin-hono-server@18.4.0 + - @objectstack/plugin-webhooks@18.4.0 + - @objectstack/service-mailbox@18.4.0 + - @objectstack/service-realtime@18.4.0 + - @objectstack/plugin-pinyin-search@18.4.0 + - @objectstack/service-package@18.4.0 + - @objectstack/trigger-record-change@18.4.0 + - @objectstack/metadata@18.4.0 + - @objectstack/service-cache@18.4.0 + - @objectstack/service-job@18.4.0 + - @objectstack/service-sms@18.4.0 + - @objectstack/trigger-schedule@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/verify/package.json b/packages/verify/package.json index d13ef0b63f8..513e5706aba 100644 --- a/packages/verify/package.json +++ b/packages/verify/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/verify", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Boot any ObjectStack app in-process and verify it through the real HTTP stack — auto-derived CRUD round-trip fidelity plus the cross-owner RLS invariant. Catches runtime regressions that static checks miss.", "type": "module", diff --git a/skills/objectstack-ai/SKILL.md b/skills/objectstack-ai/SKILL.md index bf691e87887..9c9ca6a9216 100644 --- a/skills/objectstack-ai/SKILL.md +++ b/skills/objectstack-ai/SKILL.md @@ -12,7 +12,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: ai tags: agent, tool, skill, knowledge, llm, embedding, mcp --- diff --git a/skills/objectstack-api/SKILL.md b/skills/objectstack-api/SKILL.md index ee551eb0d46..85d21b81fb9 100644 --- a/skills/objectstack-api/SKILL.md +++ b/skills/objectstack-api/SKILL.md @@ -14,7 +14,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: api tags: rest, endpoint, auth, realtime, server --- diff --git a/skills/objectstack-automation/SKILL.md b/skills/objectstack-automation/SKILL.md index b804decc7b6..5f9bfe1b674 100644 --- a/skills/objectstack-automation/SKILL.md +++ b/skills/objectstack-automation/SKILL.md @@ -13,7 +13,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: automation tags: flow, workflow, trigger, approval, state-machine, scheduled, webhook --- diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index 682acf600aa..14540f17a4e 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -15,7 +15,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: data tags: object, field, validation, index, relationship, hook, schema, permission, rls, security, seed, fixture --- diff --git a/skills/objectstack-formula/SKILL.md b/skills/objectstack-formula/SKILL.md index 56bfc9df238..aeff810eafb 100644 --- a/skills/objectstack-formula/SKILL.md +++ b/skills/objectstack-formula/SKILL.md @@ -13,7 +13,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x and @objectstack/formula 18.x (CEL) metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: expression tags: cel, formula, predicate, condition, validation, visibility, seed-dynamic --- diff --git a/skills/objectstack-i18n/SKILL.md b/skills/objectstack-i18n/SKILL.md index d1a68b281bc..3b5ce283b14 100644 --- a/skills/objectstack-i18n/SKILL.md +++ b/skills/objectstack-i18n/SKILL.md @@ -13,7 +13,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: i18n tags: i18n, translation, locale, l10n, bundle, coverage --- diff --git a/skills/objectstack-platform/SKILL.md b/skills/objectstack-platform/SKILL.md index 2025a61a611..6becb8df773 100644 --- a/skills/objectstack-platform/SKILL.md +++ b/skills/objectstack-platform/SKILL.md @@ -17,7 +17,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x and @objectstack/core 18.x (Zod v4 schemas), Node 22+ metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: platform tags: project, defineStack, driver, hono, plugin, kernel, service, requires, capability, DI, lifecycle, cli, deploy, ops --- diff --git a/skills/objectstack-query/SKILL.md b/skills/objectstack-query/SKILL.md index c5e825ddf6b..32f40d80ca5 100644 --- a/skills/objectstack-query/SKILL.md +++ b/skills/objectstack-query/SKILL.md @@ -11,7 +11,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: query tags: query, filter, sort, paginate, aggregate, ObjectQL, full-text --- diff --git a/skills/objectstack-ui/SKILL.md b/skills/objectstack-ui/SKILL.md index eee8e3ad2bf..c5c18e3fc4c 100644 --- a/skills/objectstack-ui/SKILL.md +++ b/skills/objectstack-ui/SKILL.md @@ -18,7 +18,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: ui tags: view, app, page, dashboard, report, chart, action, widget, doc --- diff --git a/skills/objectstack-upgrade/SKILL.md b/skills/objectstack-upgrade/SKILL.md index d8e80d2619b..de8c95c9064 100644 --- a/skills/objectstack-upgrade/SKILL.md +++ b/skills/objectstack-upgrade/SKILL.md @@ -16,7 +16,7 @@ license: Apache-2.0 compatibility: Requires `@objectstack/spec` and `@objectstack/cli` at the TARGET major; the chain replays from the spec's `MIGRATION_SUPPORT_FLOOR`. No network access required. metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: process tags: upgrade, migration, protocol, major, retired-keys, tombstone, conversions, validate, report --- diff --git a/skills/objectui/SKILL.md b/skills/objectui/SKILL.md index ac1d5b3979a..a693a40ebb8 100644 --- a/skills/objectui/SKILL.md +++ b/skills/objectui/SKILL.md @@ -5,7 +5,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x; each @object-ui/* package declares its exact @objectstack/spec range in its own manifest. metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: ui tags: sdui, schema-renderer, json-ui, react, shadcn, tailwind, plugin, expression user-invocable: false diff --git a/ui/apps/console/CHANGELOG.md b/ui/apps/console/CHANGELOG.md index aa211031e6e..c36d2ebe1cf 100644 --- a/ui/apps/console/CHANGELOG.md +++ b/ui/apps/console/CHANGELOG.md @@ -1,5 +1,163 @@ # @object-ui/console +## 18.4.0 + +### Patch Changes + +- a4c3e73: feat(console,mobile,auth): the Console registers the mobile shell's push token under its own session and unregisters it on sign-out and when the shell removes the connection + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@object-ui/console`:** in the ObjectStack mobile shell, a signed-in person who has reached Home or an app is asked for push permission once per page load, after a short delay (never on a sign-in screen): the Console calls `getPushToken()` and registers the token with `POST /api/v1/notifications/devices`. A `null` answer (simulator, permission denied, no token) sends nothing. A token the shell rotates is registered again; an unchanged token for the same person is not sent twice. Another person signing in on the page registers the device for them, and the registry moves the token away from the previous person. An impersonation never registers, so the impersonated person's pushes never reach the administrator's device. The registration sends `deviceId` as the shell's `installId` when it has one, and `origin` only when the page was loaded over `http:` or `https:`. The device is unregistered (`DELETE /api/v1/notifications/devices/:deviceId`) before sign-out ends the session, and when the shell removes this connection; a shell that is neither iOS nor Android registers nothing. A browser is unchanged. + - **`@object-ui/mobile`:** `ObjectStackNative` gains the optional `installId` (`string | null`), and the package exports `NATIVE_PUSH_TOKEN_CHANGED_EVENT` (`objectstack-native:pushTokenChanged`, announced as `has('pushTokenChanged')`) with its event type `NativePushTokenChangedEvent`. Both are feature-detected: a shell that predates them carries neither. + - **`@object-ui/auth`:** `registerBeforeSignOut(step)` registers a step that `signOut()` awaits while the session still exists, and returns the function that removes it. The steps run together and can never hold sign-out back: a rejection is reported and dropped, and sign-out goes ahead after `BEFORE_SIGN_OUT_TIMEOUT_MS` (3 s) whether or not they have settled. +- f35b361: fix(console): the sign-in page finishes the mobile sign-in hand-off instead of landing on a missing Console page + + Clause-②: no + Metadata-protocol: none + + The mobile app opens the Console's sign-in page in the system browser with `?redirect=` naming the hand-off route (`/api/v1/auth/one-time-token/handoff?...`), so that a user whose identity provider refuses embedded WebViews can still sign in. After sign-in the page sent every safe redirect through the Console mount, so the browser landed on `/_console/apps/.../api/v1/auth/one-time-token/handoff?...` and the page said it does not exist. Now: + + - A `redirect` whose path is exactly the hand-off route under the auth base the Console talks to is navigated to unprefixed, by script from the sign-in page. Every other redirect, `/api/v1/data/x` included, stays a Console route as before; a lookalike path, another base path or an absolute URL is never followed out of the mount. + - The hand-off comes before the organization steps: a user with several organizations is no longer sent to the picker first, and one with no active organization does not wait for it to be chosen. + - A session created more than five minutes ago, which the hand-off refuses, keeps the sign-in form on screen instead of forwarding to a JSON refusal. Signing in on that form makes a new session and forwards. + - The sign-up page does the same after a sign-up. +- cde492d: feat(plugin-auth): a page navigation to the mobile sign-in hand-off that has to sign in first goes to the sign-in page, so the app opens only the hand-off + + Clause-②: no + Metadata-protocol: none + + `GET /api/v1/auth/one-time-token/handoff` answered a missing or old session with `401` as raw JSON, so the mobile app had to open the Console's `/login` route itself and know how that page treats `redirect`. An installed app stays in use for months, so a Console upgrade could strand it. Now the app opens only the hand-off: + + - A page navigation with no session, or with one older than five minutes, answers `302` to this deployment's Console sign-in page, with `redirect` naming the same request. `Location` is root-relative, so the browser stays on the origin the request arrived on, and `uiBasePath` is honoured. + - Any other request still answers `401`, so the app's anonymous probe keeps reading "this server registered me". A navigation reporting `Sec-Fetch-Site: same-origin` answers `401` too: it is the sign-in page forwarding a session the route refuses. A deployment that does not serve the Console keeps the `401` too, and says so once at `warn`. + - A token is still minted only for a session under five minutes old, and every `400` (a cross-site navigation, an unknown parameter, an unregistered scheme, a bad challenge) is still decided first, with no redirect. + - A device whose clock is behind does not bounce between the route and the sign-in page. The redirect carries `handoff_refused_at`, the server's time of the refusal, and the page holds its form for any session not created after it, whatever the device's clock says. A session created after it forwards, so a provider's sign-in that returns to the same URL still ends at the app. The links between the sign-in and sign-up pages carry the mark beside a hand-off `redirect`, and every other link is unchanged. + + An app can change its entry only once its server runs this release: an older server answers the new entry with JSON. +- 27b0bc7: feat(console,app-shell): registering from an invitation link joins that invitation's organization and enters the app in one step + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@object-ui/console`: `/register`** opened from an invitation link (`?redirect=/accept-invitation/ID`) now accepts THAT invitation as the account the sign-up just created, switches the session to its organization and enters the console root. A new member used to pass three screens: the sign-up, the accept page ("Accept invitation"), and "Invitation accepted … Continue". The server still decides: an address that is not the invited one is refused, nothing is accepted, and the page goes on to the accept page as before, which shows the refusal. A person who opens the page already signed in accepts nothing there, and a sign-up that waits for email verification is accepted from the accept page the verification mail returns to. + - **`@object-ui/app-shell`: `DefaultRegisterPage`** does the same; it used to go to `/` after the sign-up and leave the invitation behind. New export **`useInvitationSignUpJoin(redirect)`**, the one hook both register pages call, answering `none`, `joining`, `joined` or `refused` (type `InvitationSignUpJoin`). +- c322421: fix(console,auth): after an explicit sign-out, whoever signs in next lands on their own home, not on the page the previous person had open + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@object-ui/auth`: `AuthContextValue.sessionEndedBySignOut`**, `true` from a `signOut()` until a session is established again in the tab, whoever it belongs to; `false` on load, while signed in, and after a session that ended any other way (expired, revoked, signed out in another tab). It is a new required member of the exported `AuthContextValue` type: code that builds its own value of that type (a test double, a custom provider) must add `sessionEndedBySignOut: false` (or its own reading) to type-check. + - **`@object-ui/console`: `LoginRedirect`** sends the visitor to `/login` with no `?redirect=` after an explicit sign-out. A new member signing in after the administrator signed out of a Setup page used to land on that page and read "you do not have access to this app". A session that expires or is revoked under the person still keeps the path (list state stripped as before), so that person signs back in where they were. +- 4128213: fix(console): a nav entry with no icon draws its object's icon, record field labels read in one style, a count chart fits a small count, and a page whose entry failed to load retries (#30126) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Navigation icons.** An app navigation entry of type `object` that names no `icon` now draws the icon its object declares, in the sidebar and in the `nav:menu` page block. Before, every such row drew the same generic page icon, so an app whose navigation names no icons showed one icon down the whole sidebar. The icon is read by object name from the metadata the console already holds, so no request is added and the navigation is not written to. An authored `icon` wins, and an object with no icon, a dashboard, page or report entry, and a host with no resolver keep the generic icon. `@object-ui/layout` exports `resolveNavItemIcon`, the types `NavIconTarget` and `NavTargetIconResolver`, and an optional `resolveTargetIcon` prop on `NavigationRenderer`, alongside the existing `resolveTargetLabel`. + - **Record field labels.** A record page's section field labels no longer carry an uppercase transform. Only Latin text took it, so in a zh console an untranslated label (`REPAIR COST`) read in capitals beside a translated one (`负责人`). Section labels now render as authored, in the style of the highlights strip above them. + - **Small count charts.** A count axis whose largest value is a small whole number (1 to 3) now tops out at that number, so a chart of one record draws its bar to the top of the axis instead of a quarter of the way up a 0–4 axis. An axis with an authored `min`, `max` or `stepSize`, fractional values, or a count of 4 or more draws as before. + - **Boot retry.** When the page's entry script fails to load before anything mounts (the server went away between serving the page and serving its entry, as during an `os dev` restart), the page now reloads itself, backing off from 1 second and doubling, at most five times per tab. Before, the loading splash stayed forever. The count is cleared on the first mount, and where `sessionStorage` cannot be read nothing retries. +- 6c48d48: fix(console): a person who verifies their email during the mobile sign-in hand-off gets a mail link that works, and the hand-off's refusal time survives the verification pages (#30157) + + Clause-②: no + Metadata-protocol: none + + On a server that requires email verification, a person signing up from the mobile app's sign-in (the hand-off `redirect` on `/register`) got a verification mail whose link landed on a Console page that does not exist (`/_console/api/v1/auth/one-time-token/handoff?…`). The mail is usually opened in a browser that is not the app's sign-in window, and often on another device, so the link now returns to `/verify-email?handoff=verified`: the page says the address is confirmed and to go back to the app and sign in there. It offers no Console sign-in and hands out no sign-in token in that browser. The same holds for the "Resend verification email" mail. Every other `redirect` (an invitation, a Console page) keeps the link it had. + + The hand-off's refusal time (`handoff_refused_at`) is now carried through `/verify-email-prompt` on all three hops: `/login`'s "email not verified" branch, `/register`'s "verification required" branch, and "Back to login". Beside any other `redirect`, those links are unchanged. + + New string: `auth.verifyEmail.handoffSuccessDescription` (all ten locales). +- c580081: fix(ui): the console form page Edit, the kanban drop prompt and the Edit wizard judge a field's `requiredWhen` as the engine does, a transition gate against the stored record + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/console` form page (`/forms/:name?recordId=`):** an Edit no longer refuses a record that already sits inside a `requiredWhen` gate without the value (for example a contract activated before its signed-copy rule existed). The field shows no required marker there, and an unrelated edit saves, as REST `PATCH` already accepts. Still refused: the edit that moves a record into the gate without the value, and clearing the value of a record that held it. A create form enforces as before. + - **`@object-ui/plugin-kanban` drop prompt:** dropping a card no longer asks for a field the engine lets rest. A record already inside the gate without the value moves between columns with no dialog; a drop that moves a compliant record into the gate still asks for the value first. + - **`@object-ui/plugin-form` Edit wizard:** the final submit's cross-step check no longer refuses a record that already sits inside a `requiredWhen` gate without the value; it asks `requiredWhenRestsOnStoredRecord` about the record as first read. The record reaches only that stored-record check, so a `visibleWhen` reading `previous` is still unevaluable at that check, as before. Moving a compliant record into the gate is still refused, and a Create wizard enforces as before. + + All three read the stored-record verdict `resolveFieldRuleState` already returns (`requiredWhenAtRest`, or its exported check `requiredWhenRestsOnStoredRecord`); no second rule. +- b6f4c0e: feat(console): the Allowed IP ranges setting shows the address the server judges before you save + + Clause-②: no + Metadata-protocol: none + + A save of `auth.allowed_ip_ranges` whose list leaves out the saving request's own address is refused, and until now the Authentication settings page only told the admin that address in the refusal. The page now reads `GET /api/v1/auth/admin/client-address` (the save check's own reading of the address, `server.trustProxy` included) and draws one line under the field: the address, and whether it came from a trusted proxy's forwarded headers or, with `trustProxy` off, from the connection itself (behind a proxy, the proxy's address). When the server cannot establish an address the line says a non-empty list cannot be saved from here. The line stays beside a refusal. A caller the route refuses (not a platform admin), or a server without the route, sees no line and the page is otherwise unchanged. `@object-ui/i18n` adds `console.settingsView.clientAddressDirect`, `clientAddressViaProxy` and `clientAddressUnknown` in all ten shipped locales. +- ac56acf: fix(console): a value set on a deployment-level settings page reads `Modified`, names the default, and offers Reset to default (#30329) + + Clause-②: yes (widening) — two published language-pack keys in every built-in pack: `console.settingsView.sourceBadgeModified` and `console.settingsView.defaultValueHint` (with `{{value}}`). + Metadata-protocol: none + + A settings page whose manifest declares `scope: 'global'` (Setup's Authentication page is one) writes the deployment's row. A value it had saved was badged `Inherited from Global`, as if it came from somewhere else, and had no Reset to default, so going back meant knowing the default or emptying the box. + + - The page now passes each row the scope its Save writes, resolved the way the server resolves it: the specifier's `scope`, else the manifest's, else the spec default `tenant`. A value whose effective layer is that scope reads `Modified`, names the default layer's value (`Default: 8`; an option's label, Yes or No for a toggle; never a secret's), and offers Reset to default. Reset puts `null` in the draft and Save sends it, like any other change. + - An env-locked value never gets Reset. A value from a layer above the page's scope still reads `Inherited from …`, and the organization and user pages keep their `Overrides …` badges unchanged. + - An unset number box now reads empty after Reset; it kept showing the last number. +- a91409c: fix(core,react,i18n,components,app-shell,console): a write refused because the object's external datasource does not accept writes says the table is read-only, not "you don't have permission" + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@object-ui/core`: `isExternalWriteRefusal(err)`**, re-exported from `@object-ui/react` beside the other write-refusal readers. True when the server refused a write with `EXTERNAL_WRITE_FORBIDDEN`, the ADR-0015 write gate's code for an object bound to an external datasource that does not admit writes (`datasource.external.allowWrites` and `object.external.writable` must both be true). Read from `code`, then `details.code`, like `isPermissionError`. The refusal is a 403, so `isPermissionError` answers true for it too: a caller asks this first. + - **`@object-ui/i18n`: `form.readOnlyExternalSource`**, in all ten packs: "This table is read-only. Its records come from an external data source that does not accept changes." + - **Every surface that met this refusal now says the table is read-only:** the console record form page and the record dialog (`@object-ui/components` form), which said "you don't have permission to save this record"; the record page delete, the list row delete and the record page inline edit, which showed the gate's raw diagnostic; and the calendar's drag-to-reschedule and the record path's stage change. A permission line sent an administrator through permission sets for a grant that cannot exist. +- 4831d50: fix(console): a language picked on the register page is saved as the new person's own language when registering signs them in (#30338) + + Clause-②: no + Metadata-protocol: none + + A language picked in the register page's menu used to apply on that browser only: once the sign-up signed the person in, their saved language stayed empty, so another device, or the language their mail is written in, did not follow the pick. The register page now saves the pick to the person's own language setting before it moves on, the way the sign-in page does, on every way it leaves after a sign-up, joining an organization from an invitation link included. `DefaultRegisterPage` does the same for a pick made earlier on another signed-out page. Nothing is saved when no language was picked, a language the person already has is kept, and a save that fails does not hold up the sign-up (the pick still applies on that browser). A pick followed by a reload of the register page, or by an identity provider's redirect, is still not saved. +- 7b10a30: fix(auth): the forgot-password page no longer says a reset link was sent on a deployment that sends no mail (#30366) + + Clause-②: yes (widening) + Metadata-protocol: none + + `GET /api/v1/auth/config` reports `features.emailDelivery`: `false` when no email service is wired or the stock `log` transport only writes mail to the server console. On such a deployment the forgot-password form used to answer every request with "Check your email — we've sent a password reset link", although no mail leaves. When the server reports `emailDelivery: false`, the success screen now says a reset link can't be emailed here and to ask an administrator, who can set a new password from Setup → Users. + + The text describes the deployment, never the address: it carries no address, and a known and an unknown address read the same, so the page adds no account-existence oracle. Only an explicit `false` changes it; a server that predates the flag (no `emailDelivery` key) or a failed config fetch keeps the "check your email" screen, since those deployments may well send mail. + + `ForgotPasswordFormLabels` gains `noDeliveryTitle` and `noDeliveryDescription`, and `AuthPublicConfig.features` gains `emailDelivery`. New strings `auth.forgotPassword.noDeliveryTitle` and `auth.forgotPassword.noDeliveryDescription` in all ten locales, passed by the Console's forgot-password page and `DefaultForgotPasswordPage`. +- 8b0fbb8: fix(app-shell,fields,plugin-form,plugin-gantt,plugin-grid,components,console): record pages, forms, drawers and preview cards draw a served object label as served + + Clause-②: no + Metadata-protocol: none + + - **Now.** The record page (its title, breadcrumbs, related-list titles, delete and access-denied copy), the record form page and the console's create/edit dialog (title, save toast, way back to the list), the record drawer, the record preview card, the hover card over a lookup, a record-pointer cell, an object-ref cell, the closed-form notice, the gantt name column, the page header's record chrome, a form view's heading, the AI dock's surface chip, the Activity row and the report index name an object by the label the server served, as given. The server has already translated it, by its label order for an overridden item, so a `.custom` object extension's rename is kept over the base package's translation. Before, the served label reached the client's catalog lookup only as its fallback, so the base package's translation of the old name won on these surfaces after the list and header were fixed. + - **Unchanged.** When no label is served, the catalog key is asked first and the machine name (or its humanized form) stays the last resort, as before. An unedited object shows the same text as before, because the server serves the catalog's translation for it. Related lists and reference rails that only know an object's name, and the packaged-actions table (its object list is read without the session's language), still look the name up in the catalog. English is unchanged. +- bbd6072: fix(console): a language picked on the sign-in page is saved as the person's own language when they have none (#30399) + + Clause-②: yes (widening) + Metadata-protocol: none + + A language picked in the sign-in page's menu used to apply on that browser only: after signing in, the person's saved language stayed empty, so another device, or the language their mail is written in, did not follow the pick. The sign-in page now saves the pick to the person's own language setting before it moves on, when they have not chosen one yet. A language they already chose is kept, nothing is saved when no language was picked on the page, and a save that fails does not hold up the sign-in (the pick still applies on that browser). `@object-ui/app-shell` gains `hasSignInLanguagePick`, `saveSignInLanguagePick` and the `SignInLanguagePickWriter` type for the sign-in page. A pick followed by a reload of the sign-in page, or by an identity provider's redirect, is still not saved. +- 35a8f08: fix(console): the sign-in page no longer offers "Sign in as dev admin" when email and password sign-in is off + + Clause-②: no + Metadata-protocol: none + + Under `os dev`, the sign-in page drew the dev-seeded admin row whenever `/api/v1/auth/config` reported `devSeedAdmin`. Its chip signs in with email and password, so with that sign-in turned off the server refused it (`EMAIL_PASSWORD_DISABLED`) while the rest of the page correctly offered only the configured providers. The row is now drawn only when the same config does not report `emailPassword.enabled: false`, the answer the sign-in form already reads. +- c447019: feat(console): a settings toggle that declares `confirmOffText` asks before it is turned off + + Clause-②: no + Metadata-protocol: none + + A settings manifest's `toggle` specifier may declare `confirmOffText` (`SpecifierSchema`: "Confirm dialog text shown before a toggle's value changes to off (omit = no confirm)"), and the authentication manifest declares it on `email_password_enabled`. The Console's settings page did not read the key, so Setup turned email and password sign-in off without asking. Now turning such a toggle off opens a confirmation carrying that text, translated through `settings.NAMESPACE.keys.KEY.confirmOffText` when the locale has it (an inline locale map literal is picked for the page's language otherwise). Cancel leaves the toggle on and puts nothing in the page's unsaved changes, so nothing can be saved; Continue turns it off, and Save sends it like any other edit. Turning a toggle on asks nothing, and a toggle without the key changes at once, exactly as before. +- 062236c: fix(console): a link inside an app's doc keeps the app shell, and the docs chrome reads the session language + + Clause-②: no + Metadata-protocol: none + + A doc read inside an app (`/apps/APP/docs/BOOK/DOC`) rewrote its body cross-references to the portal's `/docs/NAME`, so following one left the app: no top bar, no app navigation. They now resolve under the app's own docs route, as the book sidebar already did, and the header's home link goes to the app's docs index. The header's "Documentation" label, the portal heading and the book sidebar's synthetic "Uncategorized" group title now read the new `help.documentation` and `help.uncategorized` keys, present in all ten locale packs; the group is titled by its key, and authored group labels keep the server's translation. +- a1d0b1e: fix(console): a settings action whose handler declares `confirmText` asks before it runs + + Clause-②: no + Metadata-protocol: none + + The settings manifest's action handler (`SpecifierHandlerSchema`, its `http` and `action` arms) declares `confirmText`: "Confirm dialog text before invoking (omit = no confirm)". The Console's `action_button` row never read it, so an action its manifest marked as needing a confirmation ran on the first click. Now a declared `confirmText` opens a confirmation dialog carrying that text (translated through `settings.NAMESPACE.actions.ID.confirmText` when the locale has it); Cancel runs nothing and Continue runs the action once. A handler without `confirmText`, and a `navigate` handler, runs on click exactly as before. +- @objectstack/sdui-parser@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/apps/console/package.json b/ui/apps/console/package.json index c19548be069..ed78516123c 100644 --- a/ui/apps/console/package.json +++ b/ui/apps/console/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/console", - "version": "18.3.0", + "version": "18.4.0", "description": "ObjectStack Console — opinionated, fork-ready runtime console built on @object-ui/app-shell with the full plugin set wired up. Ships as a Hono UI plugin serving a pre-built SPA.", "license": "MIT", "type": "module", diff --git a/ui/packages/app-shell/CHANGELOG.md b/ui/packages/app-shell/CHANGELOG.md index 26d89123206..3dd47c330b1 100644 --- a/ui/packages/app-shell/CHANGELOG.md +++ b/ui/packages/app-shell/CHANGELOG.md @@ -1,5 +1,823 @@ # @object-ui/app-shell — Changelog +## 18.4.0 + +### Minor Changes + +- 15f808a: fix(metadata-protocol): a mapping override is served, a pair's removal of its own override of any overridable type brings the base's item back, and an excluded type's `overrides` refusal names a remedy that works for it (#30062) + + Clause-②: no — ADR-0146 D2 says an item in the customization package replaces the base's item and removing it brings the base's back, and ADR-0146's revert ("Restore package original") is deleting the overriding item from `.custom` so the base's document is served again. The 403 on removing a pair's own doc or mapping override, and the unserved mapping override, refused what that text already admits. A removal that names the base still keeps today's answer. + Metadata-protocol: semantic + + - **A mapping override in `.custom` is served.** A mapping item that declares `overrides: { package: '', … }` in the base's customization package now replaces the base's mapping for the by-name read (no `?package`, `?package=`), both lists and the layered read, as page, app, dataset, doc and book already did. The import door (`@objectstack/rest`'s `import-mapping.ts`) resolves a named mapping through that package-less by-name read, so it now gets the pair's mapping. + - **Removing a pair's own override works for every overridable type.** `DELETE` of a declared override the customization package holds (named with `?package=.custom`) was admitted only for a flow; a doc or mapping override answered `403 NOT_OVERRIDABLE` on the environment kernel. The removal now admits the pair's row for every type `isOverridableMetadataType` names. The row is deleted and the base's item is served again under every address. A removal that names the base (`?package=`) is unchanged: the pair's row stays and its override stays served. + - **An excluded type's refusal names a remedy that works for it.** A save of `overrides` on a type that takes no override now cites that type's own record (ADR-0146 D6 for the security catalog and code-only types, D1 for a datasource, D9 for a behavioural switch, D10 for a field, ADR-0126 §3 for one-shot types, D2 for the rest) and prescribes a door that exists for that type: a new item under a new name where the type may be created at runtime, the documented switch route for a sharing rule, Setup › Queues for a queue, Setup › Datasources for a datasource, an object extension for a field, and a new version of the base for a type defined only in package source. The error code and status are unchanged. + - **`@object-ui/app-shell`: the All metadata tile agrees with the Customize door.** On a read-only package, a type's tile reads 可定制 (Customizable) exactly where Studio offers a Customize door for it (flow and app included, whose overlay flag is off) and 只读 (Read-only) elsewhere (doc and mapping, which have no door yet). It no longer reads the overlay flag. + + The label precedence of a translated override (card item 3) is not in this change. +- f5aaa0b: feat(ui): an action param's `widget` draws the named widget in the action dialog — `widget: 'permission-ref'` on a `text` param is the permission-set picker + + Clause-②: yes (widening) — `@object-ui/core`'s exported `ActionParamDef` gains one optional member, `widget?: string`; `@object-ui/fields`' `RawActionParam` gains the same optional member. No accepted key, value or answer is taken away. + Metadata-protocol: semantic + + `ActionParamSchema.widget` (declared by `@objectstack/spec` with the same key, vocabulary and meaning as `FieldSchema.widget`) was accepted and stored but read by nothing: the action dialog drew every param with its `type` widget. The console now reads it: + + - `resolveActionParams` copies the param's `widget` onto the resolved param. A field-backed param (`{ field }`) that inherits the field's `type` also inherits the field's `widget`; a param that overrides `type` does not, because the field's widget was chosen for the field's value. The param's own `widget` always wins. + - `paramToField` draws the named widget when it is registered, in place of the `type` widget. A name nothing registers keeps the `type` widget (a `textarea` param stays a textarea), and a registered widget on a lookup param without a target is drawn instead of the plain record-id text box. + - So `{ name: 'permissionSet', type: 'text', widget: 'permission-ref' }` opens the permission-set picker: the sets are listed by label beside their machine name, and the machine name is submitted. The server still validates the value by `type`, so the request body is unchanged. + + The spec's liveness ledger marks `params.widget` live, and its `.describe()` no longer says the key is not read yet. +- 0fd363d: fix(spec)!: an object permission that edits or deletes a record also reads it + + Clause-②: no (narrowing) — the authoring object-permission schema refuses an entry granting `allowEdit: true` or `allowDelete: true` without read (`allowRead`, `viewAllRecords` or `modifyAllRecords`), a permission set it accepted before; the permission evaluator grants edit and delete only together with read; and a by-id update or delete from a caller the read door refuses answers that refusal. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: docs moved — `content/docs/permissions/permission-sets.mdx` states the rule under the object permission bits; `skills/**` teaches no entry granting edit or delete without read (its examples grant read beside edit), so no skill moves. + + + + **BREAKING** accept-set narrowing on `ObjectPermissionSchema` (and so `PermissionSetSchema`, `definePermissionSet`, `defineStack`, `os validate` / `os build`, and the metadata save and publish doors for `permission`). + + - **Before.** `objects: { crm_lead: { allowEdit: true } }` parsed, and the evaluator granted edit on `crm_lead` to a holder who could not read it. + - **Now.** The schema refuses it: one `custom` issue at `objects.crm_lead.allowEdit` (or `.allowDelete`), naming both fixes. `objectPermissionGrants` answers edit and delete as `read ∧ grant` (the shape `allowExport` already had), and `PermissionEvaluator.checkObjectPermission` asks it of the caller's merged entry, so the read half may come from another set — the same answer `/me/permissions` gives the client. + - **Stored rows.** A permission set stored before this release keeps loading; its unread edit or delete grant grants nothing. The response-side `EffectiveObjectPermissionSchema` stays tolerant. A built artifact carrying the shape is refused when it is parsed and is rebuilt from a corrected source. + - **By-id writes.** A by-id update or delete from a caller whose read of the object is refused — with these grants that is a capability the object requires for a read and not for a write — answers that read refusal for every id it addresses: a caller who cannot read an object writes none of its records by id. + - **`@object-ui/app-shell`.** The permission set editor ticks Read when Edit or Delete is ticked, and unticking any bit that reads (Read, View All or Modify All) unticks Edit and Delete once no bit that reads remains on the entry. + - Unchanged: `allowCreate` and `allowTransfer` are not part of this rule. + + **FROM → TO.** `objects: { crm_lead: { allowEdit: true } }` → `objects: { crm_lead: { allowRead: true, allowEdit: true } }` if the set should edit the object, or `objects: { crm_lead: {} }` if it should not. The upgrade-guide entry is `permission-write-without-read-refused` (a semantic migration: no automatic rewrite, because only the author knows which of the two the set means). + +### Patch Changes + +- 1e6f542: feat(studio): a contributed nav entry shows the label the live app shows, and a package that only contributes navigation says where it goes + + Clause-②: yes (widening) + Metadata-protocol: none + + - **The label.** A navigation entry another package contributes to a base's app now shows the label the live app shows, with its id as a muted second line under it. Before, the row showed only the id. The label is looked up by `(app, id)` in the app row the Interfaces pillar already reads (`GET /meta/app?package=`), which serves the folded navigation: the served entries the app's published layer does not hold. No request is added and nothing is read from the contributor's manifest. An id the base also holds, or one two contributions share, keeps the bare id, so no label is guessed. A label-less entry reads what it inherits, through the rail's own resolver. A never-published app has no served row, so its contributed rows keep the id. + - **The contributor's empty state.** A package with no app of its own whose listed manifest declares `navigationContributions` now says what it adds: "This package has no app of its own. It adds 2 navigation entries to HotCRM's app.", with a button that opens that package. The declared app is an address only. Its owner comes from the served app read (`GET /meta/app/:app`), and the count comes from the owner's served customization read (`GET /packages/:owner/customizations`), counting the `nav` contributions stamped with this package for that app. That is at most two reads per declared target app, sent only on that empty state. When the package is disabled, the target app is missing, either read fails, or the count is 0, the text stays "This package has no app yet." + - **New strings.** `@object-ui/i18n`'s studio namespace gains five keys in `en` and `zh` (`engine.studio.if.noOwnApp`, `.noOwnAppTitle`, `.contributesOne`, `.contributesOther`, `.openOwner`). +- 04cbdf7: fix(app-shell): on a read-only item, the Properties panel shown with nothing selected says why its controls are disabled + + Clause-②: no + Metadata-protocol: none + + On the metadata editor page (Studio's All metadata item page, Setup's metadata pages), a locked item, a read-only package, a caller without `manage_metadata`, a code-shipped artifact or a walled catalog type disables every control in the Properties panel. The scoped inspector (a selected flow node or edge) already said why; the panel shown with nothing selected (an object's or view's default inspector, or the generic form a flow shows) greyed its controls silently, and Tab never reached them. The page now wraps that panel, and the generic form on a page without a canvas, in the same reason wrapper the scoped inspectors use: hovering a disabled control or tabbing to the panel opens a tooltip with the reason, and the panel's accessible description is that reason. A writable item renders the panel exactly as before. No registered default inspector changes: the page draws the reason around it. +- 40fa433: fix(app-shell): Studio's publish review sheet no longer lists an `Options` change on a picklist-bound field nobody edited (#29899) + + Clause-②: no + Metadata-protocol: none + + On an object with a field bound to a package picklist (`{ type: 'select', picklist: 'NAME' }`), the publish review sheet listed `~ FIELD · Options` on every publish, beside the change the author actually made. The draft is stored without the field's `options`, while the live read serves the options the runtime resolved from the picklist, and the sheet compared the two as read. + + The sheet now compares both versions of an object in the form the publish door stores: a field that names a picklist is read without `options` on both sides. A field with its own inline `options` is untouched, so a real options change there is still listed, and a field moved between a picklist and inline options still lists both keys. +- b379262: fix(app-shell): the metadata editor's save-status chip says "Not saved" while a save is held, instead of promising an auto-save that never comes (#29901) + + Clause-②: no + Metadata-protocol: none + + On the metadata editor page (Studio's All metadata item page, Setup's metadata pages) with auto-save on, a draft the server refused with `422 INVALID_METADATA` stays dirty, and the chip beside Save kept reading "Auto-saving…" although the auto-save is held until the refused part changes and no further save is sent. The same happened while the inspector holds a fault it reports (a CEL predicate that does not parse), which holds the auto-save the same way. + + The chip now has a held state: while either hold stands on a dirty draft it reads "Not saved — fix the errors to save" in the destructive tone, with the Save button's own explanation as its tooltip. It returns to the usual states as soon as the hold lifts: editing the part the server named, or fixing the inspector fault, lets the auto-save run and the chip reads "Auto-saving…", then "Saved". Nothing about when a save is sent or held changes. New string: `engine.edit.notSavedHeld` (en, zh). +- 7945a3b: fix(app-shell): the Studio publish sheet names a batch of hooks or translations in words ("3 hooks", zh 「钩子 3」) instead of the raw type code ("3 hook") (#29902) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@object-ui/i18n`**: two new count families in every pack, `preview.changes.typeCount.hook` and `preview.changes.typeCount.translation`, with each CLDR slot the pack's language selects. + - **`@object-ui/app-shell`**: the Pending changes / publish sheet's summary line and group headings read those families for `hook` (authored in an object's Hooks tab) and `translation` (authored in Studio's "All metadata" form editor). A type with no family still shows its raw name. +- a52761f: fix(console): an action another package places on `sys_user` at `record_header` / `record_more` draws again on the user's record page, in the drawer and on the full page (#29951) + + Clause-②: no + Metadata-protocol: none + + - **The header reads `placedActions: true`.** `page:header`'s `placedActions` (#30018) is now drawn by the console's header renderer (`PageHeaderActions` in `@object-ui/components`): `true` draws every action of the page's object whose `locations` include `record_header` or `record_more`, in declaration order, read from the object's metadata at render time, so an action an installed package adds to the object is drawn without editing the page. Omitted or `false`, an authored `actions` list is still the whole set (#11592). A `true` beside an authored list, which the spec refuses, draws none of the object's actions and is reported once in the console. + - **One placed-header reader.** `placedHeaderActions` (`@object-ui/types`) is the rule both headers read: the synthesized default record page's header in `RecordDetailView` and a `placedActions: true` header. The `page:header` registration publishes the `placedActions` input. + - **The platform record pages opt in.** `sys_user_detail` and `sys_organization_detail` (`@objectstack/platform-objects`) and `sys_approval_request_detail` (`@objectstack/plugin-approvals`) set `placedActions: true` in place of the id lists #27219 froze, which dropped every header action another package places on those objects. The tab strips are unchanged, and every action the lists named is placed at `record_header` / `record_more`, so each still draws. + - `@objectstack/spec`: the `placedActions` describe no longer opens with `[PLANNED — not yet rendered]`, and the `page` liveness record names the reader. +- 6db9198: fix(ui): a greyed action's `disabledReason` tooltip wraps inside a phone-width viewport instead of running off-screen (#29954) + + Clause-②: no + Metadata-protocol: none + + The record action bar, the `record:quick_actions` button and the schema-driven `action:*` controls (button, icon, group, menu, through the shared disabled-reason trigger) rendered the authored `disabledReason` in a tooltip with no width bound, so a long reason sat on one line and was cut at the right edge of a 390px viewport. All three tooltips now carry the same `max-w-xs` bound the home page's disabled-reason tooltip uses, so the reason wraps. +- 9d1a465: internal: move-only — the Studio Interfaces pillar's navigation rail moves out of `StudioDesignSurface.tsx` into its own module (#29988, slice 3) + + Clause-②: no + Metadata-protocol: none + + No behaviour change. The rail Studio's Interfaces pillar draws for an app's navigation (the nav tree and its groups, the rows a customization package contributes, the off-nav sections for pages, dashboards, reports and views, the two rail contexts, the kind names and kind icons) now lives in `views/studio-design/InterfacesNavRail.tsx`. The moved code is byte-identical apart from its imports and eleven `export`s; `StudioDesignSurface.tsx` imports it from there. The package's public exports are unchanged. +- f7aab89: internal: move-only — Studio's nav item inspector moves out of `StudioDesignSurface.tsx` into its own module (#29988, slice 1) + + Clause-②: no + Metadata-protocol: none + + No behaviour change. The menu-entry inspector Studio shows while you edit an app's navigation, with the nav target pickers and the select it draws with, now lives in `views/studio-design/StudioNavItemInspector.tsx`. The moved code is byte-identical apart from its imports and one `export`; `StudioDesignSurface.tsx` imports it from there and re-exports the inspector, so every import of it keeps working. The package's public exports are unchanged. +- 73a3ef9: internal: move-only — Studio's top-bar package switcher moves out of `StudioDesignSurface.tsx` into its own module (#29988, slice 2) + + Clause-②: no + Metadata-protocol: none + + No behaviour change. The package switcher in Studio's top bar (the package list, New package, the package detail sheet and the deleted-package eviction), with the package-list toast id and the Studio landing path it shares with the surface, now lives in `views/studio-design/PackageSwitcher.tsx`. The moved code is byte-identical apart from its imports and three `export`s; `StudioDesignSurface.tsx` imports it from there. The package's public exports are unchanged. +- c618e89: internal: move-only — Studio's flow rail helpers, pillar kit and Data pillar helpers move out of `StudioDesignSurface.tsx` into three modules, taking it under 10,000 lines (#29988, slice 4) + + Clause-②: no + Metadata-protocol: none + + No behaviour change. Three groups of helpers now live in their own modules under `views/studio-design/`. `AutomationsFlowRail.tsx` holds the Automations rail's flow status dot, its glyph tables, the flow reads and the new-flow trigger helpers. `StudioPillarKit.tsx` holds what every pillar shares: the held-edit notice, the unsaved-work and taken-identifier checks, the "Show me" focus helpers, the draft-saved announcement and the read-only link. `DataPillarHelpers.tsx` holds the Data pillar's field-name, CEL-stamp and form-preview helpers. The moved code is byte-identical apart from its imports and `export`s. `StudioDesignSurface.tsx` imports it from there and re-exports the five names it exported before, so every import of them keeps working. The package's public exports are unchanged. +- 61b487b: fix(app-shell): a personal view opened by its address no longer carries the default view's filter and sort in the address bar + + Clause-②: no + Metadata-protocol: none + + - While the list waits for the reader's own views to resolve a requested or pinned view it does not hold yet, the list's URL state is neither seeded nor written. The default view the page falls back to meanwhile is not the view the address names, so its stored filter, sort and search no longer reach the address bar. Before, when the list preferences landed first, that default seed was written to the URL, and a personal views read landing before the write committed left it there: a reload or a shared link then opened the personal view with conditions nobody chose for it and marked it personalized. + - When the view resolves, the seed is taken for it alone: from its own stored state, or from the `uf_*` params a link carried, as before. +- 8fa370c: fix(app-shell): a record's 动态 stream is read again after a save, so its count and rows hold the change without a reload + + Clause-②: no + Metadata-protocol: none + + - **The record feed follows the record's invalidation.** A save through the edit dialog (or an action, an undo, a ⟳) put the record on the data-invalidation bus, and the page read the record again, but the 动态 feed kept the rows and the count the page payload answered when it opened. The feed now reads again on the same change, on the record page and in the drawer. + - **The re-read asks what the open asked.** Once the record changed after the open, the feed asks the record page route for the streams the open asked, and only those (the discussion, the activity rows, the stream's count), so the new activity row and the server's count land in place. The activity row a save writes exists before the save answers, so one ask after the save sees it; nothing polls. + - **The field changes follow the same change**, so on an object with `enable.trackHistory` the stream's field-change entries are read again too. +- f0ad1f7: fix(app-shell): a flow bound to an object its package may not use is explained on the Problems list in the Console's language + + Clause-②: no + Metadata-protocol: none + + When a package's flow triggers on, scans, or declares a start input of an object outside what the package may use (its own objects, the objects of the packages it declares as dependencies, and system objects), the publish refuses it, and the flow's Problems list showed the server's English sentence in packaging terms to a reader in any language. The Problems list now words that refusal from the served entry's rule id (`flow-trigger-unknown-object`, `flow-input-unknown-object`) and its path: it names the object the flow holds there, says which objects a package can use, and points at the two fixes, picking an object from the list or declaring a dependency on the package that defines it. The copy lives in the Studio string namespace in English and Chinese. Any other server entry, and any of these at a position the Console does not word, still shows the server's sentence as sent. The served shape does not change: the editor pages now keep each served error's `code` on the entries they hand the canvas, where it used to be dropped. +- 03be565: fix(app-shell): New automation no longer saves a typed prefix as the trigger object (#30038) + + Clause-②: no + Metadata-protocol: none + + In Studio's New automation dialog, typing part of an object name and pressing Enter committed the typed text itself, so the flow was created bound to an object that does not exist. The trigger-object picker there now takes objects only: Enter or leaving the field commits the object the text names exactly, else the only match left in the list, else no object, and the picker says no object is chosen. A record preset then keeps the dialog open until an object is picked. + + Every other object picker keeps its free text, because a flow reference may hold an expression. +- 7e9fa4b: fix(app-shell): Studio on a zh-CN console reads in Chinese in the view, page and report panels, the publish sheet, the flow Problems tab and the save-notice / notify nodes, and calls a dashboard 仪表盘 everywhere + + Clause-②: no + Metadata-protocol: none + + The view, page and report property panels render the spec's authoring forms from the console's own copy of the spec, and only the dashboard form had a zh-CN overlay, so section headings such as Basics, Columns & filters and Navigation & sharing, their descriptions and schema-titled rows such as Searchable Fields read in English. The overlay now carries every `METADATA_FORM_REGISTRY` type, worded as the platform's own zh-CN metadata-forms bundle words them: view, page and report in full, the other types by field label, which is what the Pending changes sheet names a changed property by (a flow's `~ Status` now reads `~ 状态`). The sheet's summary line counts datasets as 数据集 instead of the raw type name. + + A flow whose trigger is not armed on this deployment showed the engine's developer sentence under 未在此运行, above 没有问题 —— 该流程结构有效. The note now says, from the engine's `unboundCause` code, what this deployment does not do and who can change it, keeps the engine's sentence as the hover detail, and the line below it says the structure checks found nothing to fix instead of "no problems". A cause the console has no row for still shows the engine's sentence. + + The palette names the save-notice node 保存提示, and the notify node's Template and Template data fields and the save-notice Message field read in Chinese. English consoles read the same English as before. zh-CN Console and Studio text uses 仪表盘 for dashboard throughout. +- 5db77f7: fix(app-shell): on a zh-CN console the view properties panel's empty filter button reads 「+ 添加筛选…」 and the description row's hint reads in Chinese + + Clause-②: no + Metadata-protocol: none + + - The `filter-builder` widget's empty trigger was an English literal (`+ Add filter…`); it now reads the Studio string the dataset scope filter already uses, so it follows the designer locale. + - The zh-CN view form overlay gains the `description` row's help text, which showed the schema's English description. +- f4db5f3: fix(app-shell): the console AI dock tells the agent which app the user has selected and which page or record is in view, instead of the constant `activeApp: 'AI'` and no page at all (#30043) + + Clause-②: no + Metadata-protocol: none + + Every chat request the console sends to `POST /api/v1/ai/agents/:agent/chat` used to carry `context.activeApp: 'AI'`, whatever app the user was in, and the floating dock sent no `context.surface`. So the assistant asked "which app?" and "which record?" while the screen showed both. + + Now: + + - `context.activeApp` is the package of the app the user has selected: the console app the dock sits in (whether the dock runs `ask` or `build`), the app picked for editing on the full-page `/ai/build?package=…`, the package the Studio copilot designs. With no app selected the key is left out; nothing sends a placeholder. + - The dock, on the desktop rail and the phone sheet alike, sends `context.surface` in cloud#1610's existing shape for the page in view: an object list (`artifact: { type: 'object', name }`, with `selection: { kind: 'view', id }` on a saved list view), a record page (the object plus `selection: { kind: 'record', id }`), a dashboard, report or page (`artifact: { type, name }`, `mode: 'design'` in the dashboard designer). A page that shows no one artifact (the app landing, search, a component page) sends no `surface`. + - Both are read on every send, so navigating with the dock open moves the next turn's context. + + The dock's "discussing" chip now names the object by its label on object pages, as the Studio copilot's chip already did for its artifacts. +- bc8945f: feat(app-shell): Studio Interfaces opens a list view from 不在导航中 on its object's page; the 视图 group lists only what no header dropdown reaches, and names a form view and its own type + + Clause-②: yes (widening) + Metadata-protocol: none + + - **A list view opens on its object's page from every door.** A list view picked under 不在导航中 → 视图, and a link to the view itself (`?surface=view:OBJECT.KEY`, the All metadata door), now land on the object's page with its header dropdown (object / view ▾), on that view: the object's nav entry when it has one, else the object itself, with no entry (`?surface=object:OBJECT`, which a reload or a shared link opens again, `?view=` included). The panel there now also carries what only the view's own page offered: its conditional formatting (条件格式化), whose CEL verdicts hold the view's autosave as they held the view page's Save, and the sections of the spec's authoring form for a view, without the keys the panel already edits. A form view, and a view a read-only base's pair adds, still open as themselves. + - **The 视图 group lists what the dropdowns do not reach.** A list view whose object has a nav entry is no longer listed there, a view only drafted in the package included; the list views of an object with no entry stay listed. + - **Form views read as forms.** A form view is listed by its label, else as the object's name and the word for a form ("Lead form", 「线索表单」), never its item name (`crm_lead.form`); the group's heading says what it holds (表单视图, 视图, or 表单视图与列表视图). In the view inspector, a form view's 视图类型 offers the form layouts of the spec's `FormViewSchema.type` (simple, tabbed, wizard) and reads its own, where it read 「simple(未找到)」; a list view's picker is unchanged. + - **`@object-ui/i18n`: three Studio keys,** `engine.studio.if.offNav.viewForms`, `engine.studio.if.offNav.viewMixed` and `engine.studio.if.offNav.formName`, in en and zh. +- 42afac7: feat(app-shell): Studio Interfaces picks an object's list view from the header title, object / view ▾, the way the running list does, and the open view stays in the address + + Clause-②: no + Metadata-protocol: none + + - **The view switcher is in the header's title.** An `object` entry's header reads object / view ▾. The switcher is the running list's own dropdown (`ObjectViewTitleSwitcher`): the object's list views by label, a view only drafted in this package included, a search box, and New view, which opens the Create View dialog. Studio does not offer the reader's own features there: no ☆, no 📌, no recent views, no Save as my view. It shows when the object has one list view too, so New view is always one click away. A read-only package switches views and offers no New view. + - **The Properties panel no longer holds a view picker, Add view or "Back to …".** It keeps the open view's settings, and its title is the view's name as the switcher gives it (the label the object's served `listViews` carry). A view with no label is named "Default view" or by its key; the header names the object before it. + - **The open view is in the address.** Switching writes `?view=.` beside `?surface=` and `?nav=`; the entry's own view writes none. A reload or a shared link opens that view again, a view only drafted in this package included. A link naming a view of another object is not taken. A pending edit is still sent before a switch, and an edit that cannot be sent still asks first. + - **`ObjectViewTitleSwitcher`: the pin is optional.** `onPinnedViewChange`, `pinnedViewId` and `recentViewIds` may be left out; without `onPinnedViewChange` no row draws 📌 and no Pinned section is listed. The running list passes all three, so it is unchanged. + - **`@object-ui/i18n` (studio namespace):** adds `engine.studio.inspector.listView.defaultView`; removes `defaultTitle`, `namedTitle`, `addView`, `backToEntry`, `switchView` and `switchViewDraft`, which only the removed panel controls read. +- d3edfb5: fix(app-shell): the console picks a signed-in user's UI language in the server's `resolveLocale` order + + Clause-②: no + Metadata-protocol: none + + - **Before.** `pickRenderableLocale` matched `sys_user.locale` against the offered locales exactly, then by base language only. A row stored mis-cased (`zh-cn`, written before the server door folded case and served as stored), or a tag the deployment offers only as a variant (`zh` or `zh-Hans-CN` against `zh-CN`), matched nothing, so the console rendered English. + - **Now.** It uses the order the server's i18n routes resolve with: exact, then case-insensitive, then base language, then the first offered variant of that base language. `zh-cn`, `zh` and `zh-Hans-CN` render `zh-CN`; `en-GB` renders `en`. A tag no offered locale shares a language with still matches nothing, and the device language keeps deciding, as before. + - No schema, key or server behaviour changes. +- a9fb14b: fix(app-shell): the Studio form designer lays out the form the app opens, so an object with a default form view no longer edits a layout users never see + + Clause-②: no + Metadata-protocol: none + + - **Which layout.** Studio → Data → an object → Form asks the app's own resolver which form view New and Edit open (`selectRecordFormView`, over the shell's merged objects). With none, the designer edits the object's `fieldGroups`, as before. With one (an `isDefault` form view, or an `OBJECT.create` view for New), the designer shows that view's sections and every layout edit (moving fields, adding, renaming, moving and deleting sections) is saved to the view's draft, which the package publish publishes. A chip in the toolbar names the layout being edited ("Form view: OBJECT.form" or "Field groups"). + - **New and Edit.** Where New and Edit open different form views, a New / Edit switch in the toolbar picks which one the designer and the Preview show. A pending edit of the shown view is sent before the other one loads. + - **Fields not on the form.** On a form view, a field the view lists in no section is not on the form; the designer keeps such fields in a "Not on this form" bucket, and dragging one into a section adds it. A field entry's own keys (`required`, `readonly`, …), a section's other keys, and entries the canvas does not show are written back as they were. A section that references a field group (`group`) keeps the reference until its members are changed. + - **Read-only base.** On a read-only base the view is customized, not edited: the edit is saved into the base's paired customization package as a declared override of the base's view (ADR-0146 D2), the same door list views use. + - **Preview.** The Form tab's Preview now lays the form out by the same form view (its sections, and the tabbed layout), as the dialog it previews does; before, it drew the object's field groups whatever form view the app opened. + - **`@object-ui/i18n`.** English and Chinese strings for the chip, the New / Edit switch and the "Not on this form" bucket. +- 6cc0b81: feat(app-shell): Studio customizes an installed app's navigation into the base's customization package (#30059) + + Clause-②: no + Metadata-protocol: none + + On a read-only (installed or code-shipped) package that has a paired customization package (`.custom`), the Interfaces rail now edits the app's navigation. Before, the rail's Edit button showed only on a writable package and its "+" offered only new pages, dashboards and reports, so an object made in the pair could be reached only by a typed URL. + + - Edit and the "+" open the same navigation editor a writable package uses (add, rename, reorder, group and remove entries, including the ones the base shipped). Nothing is written until an edit is saved. The first saved edit copies the whole app into the pair, declaring `overrides: { package: , basedOn: }`, the same route a view's Customize takes. Later edits update that copy. The base package is never written. + - The entry pickers list the pair's objects, pages, dashboards, reports and views after the base's, so a new object in the pair can be put on the menu. An entry bound to an object only the pair holds carries `requiresObject: `, so the runtime hides it if the object is later removed instead of linking to a missing object. + - The navigation change is a draft in the pair's pending changes and publishes in the same batch (one commit) as the object it opens. + - The rail marks a customized navigation with the same compact "customized" mark its rows carry, "only customizations" counts it and keeps the navigation listed, and the mark's popover offers "Restore package original", which deletes the pair's draft and copy of the app after a confirmation. + + A customized app is a whole copy: a navigation entry the vendor adds in a later version does not appear until the customization is restored and redone. A writable package's navigation editing is unchanged. No spec, server or metadata-type change. +- 797f925: feat(app-shell): Studio customizes an installed package's page into the base's customization package (#30063) + + Clause-②: no + Metadata-protocol: none + + On a read-only (installed or code-shipped) package, a page opened in the Interfaces pillar now offers Customize, the same button a dashboard, report or view shows. Before, Studio showed the page as read-only with no way to change it, although the server already serves a page override from the paired customization package (`.custom`). + + - Customize unlocks the page designer. Nothing is written until an edit is saved. The first saved edit copies the whole page into the pair, declaring `overrides: { package: , basedOn: }`. Later edits update that copy. The base package is never written. + - The edit is a draft in the pair's pending changes; once published, the running app serves the customized page at its usual address. + - The page's header carries the override mark, whose popover offers "Restore package original": it deletes the pair's draft and copy of the page after a confirmation, and the shipped page reads back. + - The Interfaces rail marks a customized page, "only customizations" counts and lists it, and the All metadata directory's page tile counts it. + + A customized page is a whole copy: a change the vendor ships to that page in a later version does not appear until the customization is restored and redone. A writable package's page editing is unchanged. No spec, server or metadata-type change. +- 6f095b9: feat(studio): customize a read-only package's dataset in the All metadata area, see what uses it before publishing, and read a refused publish at the dataset + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Customize a dataset.** On a read-only base package, Studio's All metadata area opens a dataset in its form editor with the dataset designer and live preview, and offers Customize. The first saved edit copies the whole dataset into the base's paired `.custom` package, declaring `overrides: { package, basedOn }`; the header's Publish applies it, and "Restore package original" on the override mark deletes the copy. On a writable package a dataset still opens the generic item page. + - **What uses it.** While a dataset is customized, the editor lists the dashboards, reports and other items that name it, read from the References read, so the person sees what a published change reaches. A failed read says it failed and offers to read again; it never reads as "nothing uses it". + - **A refused publish at the item.** A publish the server refuses for a dataset is placed on its measure or dimension and the designer's input (`Not published. Check Aggregate of the measure "Total Amount"`), in the Console's language, and shown at the dataset's editor as well as on its Pending changes entry. The server's own text stays under Details. + - **References read fixed.** `MetadataClient.references()` now reads the route's declared answer (`{ references: [{ type, name, label, path, kind }] }`) and maps each element to `MetadataReference` (`fromType`, `fromName`, new optional `fromLabel` and `kind`). It used to read a key the route never serves, so Setup's References tab and Studio's item tools showed "Nothing in the metadata graph points at this item. Safe to delete." for every item. An answer the declaration refuses is now a failed read. + - **Published surface.** `MetadataReference`, exported from `@object-ui/data-objectstack`, gains two optional fields, `fromLabel` (the referring item's label, when the server serves one) and `kind` (the served reference kind); existing fields are unchanged. `MetadataClient.references()` now accepts less: a `200` answer that `FindReferencesToMetaResponseSchema` refuses throws instead of resolving `[]`, so a caller must handle the rejection as it already does for a non-2xx answer; in this repository every caller reads it through `useReferencesScan`, which catches it and shows the failed-check state. +- 0c7e95a: fix(app-shell): Studio's rails mark a customized row with the word 定制 / Custom and an added row with 新增 / Added, no fill, instead of a filled square with a glyph that read as an add button (#30070) + + Clause-②: yes (widening) — new studio i18n key `engine.customizationMark.customizedShort` (en, zh) + Metadata-protocol: none + + The compact customization mark the pillar rails draw (and the Interfaces rail's navigation-override mark, which wraps it) now prints one short coloured word with no background. Its tooltip and accessible name are unchanged: the full text ("已定制(HotCRM)") followed by the sentence naming the customization package. It is not a control: a click on it is a click on its row. +- eb6199b: feat(app-shell,plugin-list): a personal view's own conditions are Filter panel rows, and 保存 writes the panel back into that view + + Clause-②: no + Metadata-protocol: none + + - **Opening one of the reader's own views (My views) shows its conditions as ordinary rows in the Filter panel**, with the active count on the Filter control. They are the list's filter while they are there: not applied under the list a second time, and not a personalization. A stored filter the panel cannot hold (an any-of group beside other conditions), or one naming a field the object no longer declares or the reader cannot read, stays applied under the list, as for any other view. + - **Editing those rows marks the list 已个性化, and the marker offers 保存 to that view** (for a reader allowed to update their views). 保存 replaces the row's `config` through the data API with the body Save as my view builds, so the stored `ListView` shape is unchanged and no key is added; the reader's temporary list state for the view is then cleared and the list opens on the saved view. Edits not saved stay the reader's temporary list state and are never written to the view. A panel the fold cannot write losslessly keeps the refusal Save as my view gives, and nothing is written. + - **Save as my view now clears the view it came from**: that view's stored list state (sort, filters, column layout, density, hidden fields) and cached search term go, and the address being left loses its `uf_*` params, so the source no longer reads filtered or 已个性化 after the save. + - Both saves read the Filter panel as the list reports it, not the address's copy, so an emptied panel saves as no filter. + - `@object-ui/plugin-list`: `ListView`'s host prop `viewDefaults` accepts `filters`, the view's own conditions for a host that shows them as panel rows; the `filters` personalization part is then judged against them. +- dfafae8: feat(plugin-list): a reader can add any field they may read to their own list from the Hide Fields popover (#30077) + + Clause-②: no + Metadata-protocol: none + + The Hide Fields popover on a Console object list (the toolbar's, and the Hide Fields section of the Display panel) listed only the columns the view already has, so a reader who wanted another field on their list had to ask an administrator to change the view for everyone. After the view's columns it now lists every other field of the object the reader may read, unticked: business fields first, then system fields such as Owner. A field the reader may not read is not offered, and neither is a field the object definition marks `hidden`. Ticking one adds it as a column of the reader's own list; unticking it takes it off again. + + The addition is the reader's personalization, stored beside their hidden fields in their `ui.list.` preference row (`perView..addedFields`), never written into the view: another user sees the view's columns, and "Reset to view" drops it with the rest of the reader's list. While it is stored the list's Personalized marker names "Column layout". An added column is fetched and drawn like a declared one, and an added lookup field is expanded in the list read as a declared lookup column is. + + A column the view declares with no label of its own (a bare `{ field }` or field name) is now listed in the Hide Fields popover (and in the Group and Row color pickers, which share its list) under the object definition's field label, the label the grid's header shows, instead of under its machine name (`expected_revenue` now reads Expected Revenue); a column's own label and a translation still come first. + + It is offered wherever a Hide Fields choice is stored: a view that declares `columns`, and a saved view. On a system view or fallback tab that declares no columns the popover lists the view's columns alone, as before. + + For a `ListView` host: two new host props, `addedFields` (the names among `schema.columns` the reader added) and `onAddedFieldsChange` (the door that stores them). Without `onAddedFieldsChange` the popover is exactly what it was. No metadata key and no spec change. +- c8e6d38: feat(app-shell): Studio adds an action to an installed app's object, saved in the base's customization package (#30088) + + Clause-②: no + Metadata-protocol: none + + On a read-only (installed or code-shipped) package that has, or can get, its paired customization package (`.custom`), the Data pillar's Actions view of the base's object now offers New. Before, it listed the vendor's actions with no New, so an operations person could not add a button such as "Mark qualified" to an installed app's leads, although the platform accepts the result. + + - New offers the same presets and the same action editor as on a writable package. The action is saved as its own `action` item in the pair, bound to the object by `objectName`, as a draft (the first write asks the ensure door for the pair). The base's object is never written. + - The vendor's actions stay read-only in the list. The added action is listed after them with the "added" customization mark, its edits autosave into its draft in the pair, and Delete removes its draft and its published item from the pair. An added action keeps the name it was created with: a rename is held with a line saying how to save. + - The action publishes with the pair's other drafts from Pending changes, and the Actions list's "only customizations" counts it and narrows the list to the pair's actions. + + A writable package's action editing is unchanged: its actions are still written into the object draft. No spec, server or metadata-type change. +- d99cd3b: feat(app-shell): the Studio list-view panel configures the selection bar's actions (#30089) + + Clause-②: no + Metadata-protocol: none + + The list-view panel of an object's grid view gains a "Selection actions" section. It edits two keys the list view already declares, and no other: + + - `bulkActions`: pick the object's actions by label, plus the built-in Delete (`delete`). Each runs once per selected record, promoted with the action's own label and params. + - `bulkActionDefs`: add, edit or remove a field update (`operation: 'update'`): a button label, the fields the dialog asks for (one required param per field, carrying a select's options and a lookup's object) and an optional confirmation. A def is written only once it has a label and a field and `BulkActionDefSchema` accepts it. + + The section says what happens to the built-in Delete: shown while the view declares neither key, and kept once either is declared only when `bulkActions` names `delete`. A def of another kind (`delete`, an aggregate `custom` dispatch) is listed and can be removed; it is edited in metadata. An emptied key is removed, so a view that declares neither key saves as it was read. On an installed app's read-only view, the first edit goes through the same customization copy as every other setting of the panel. Strings are added in English and Chinese to the `studio` namespace of `@object-ui/i18n`. +- b6caa14: fix(app-shell): Studio's publish review sheet says a customization lives in this environment's database and moves only by `os package export` (#30119) + + Clause-②: yes (widening) — new `@object-ui/i18n` key `preview.changes.customizationEnvironment` in every pack; `preview/savedToCustomization` gains `customizationEnvironmentText` and `PACKAGE_EXPORT_COMMAND` + Metadata-protocol: none + + Reviewing a read-only package's customizations, the sheet adds one line under "Saved to BASE's customizations": the changes are kept in this environment's database as items of the `.custom` package it names (the id the server answered for the base), not in the project's source, and they reach another environment only through `os package export`. +- 27b0bc7: feat(console,app-shell): registering from an invitation link joins that invitation's organization and enters the app in one step + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@object-ui/console`: `/register`** opened from an invitation link (`?redirect=/accept-invitation/ID`) now accepts THAT invitation as the account the sign-up just created, switches the session to its organization and enters the console root. A new member used to pass three screens: the sign-up, the accept page ("Accept invitation"), and "Invitation accepted … Continue". The server still decides: an address that is not the invited one is refused, nothing is accepted, and the page goes on to the accept page as before, which shows the refusal. A person who opens the page already signed in accepts nothing there, and a sign-up that waits for email verification is accepted from the accept page the verification mail returns to. + - **`@object-ui/app-shell`: `DefaultRegisterPage`** does the same; it used to go to `/` after the sign-up and leave the invitation behind. New export **`useInvitationSignUpJoin(redirect)`**, the one hook both register pages call, answering `none`, `joining`, `joined` or `refused` (type `InvitationSignUpJoin`). +- ea5807e: fix(console): four copy and display details from a real-user walkthrough: Latin object names spaced in zh form titles, a title on the lookup's create dialog, a success toast that names the action, and money metrics at the currency's decimals + + Clause-②: yes (widening) + Metadata-protocol: none + + - **zh form titles space a Latin object name.** `新建{{object}}`, `编辑{{object}}` and `查看{{object}}` (and the AI chat's record-write card titles) read `新建 Repair Ticket` when the object has no Chinese label, and still `新建联系人` when it has one. `createI18n` applies the spacing to those keys only, through an i18next post-processor; the packs keep the plain `{{object}}` spelling. + - **The lookup's "create the referenced record" dialog has a title.** A `{ objectName, mode: 'create' }` modal descriptor with no `title` now reads `form.createTitle` with the object's label, the title of the console's own New dialog. An authored title still wins. + - **The default success toast names the action.** An action with a `label` and no `successMessage` (and no matching `outcomeMessages` entry) toasts the new pack key `actions.completedNamed` (`Resolve completed`, zh `“解决”已完成`) instead of the generic `Action completed successfully`. An action with no label keeps the generic text. + - **Money metrics keep the currency's decimals.** A dataset metric over a currency measure with no `format`, and a static metric card with a `currency` and no `format`, show the currency's own minor-unit width (CNY `¥1,293.00`, JPY `¥1,293`), the width the list cell shows. Before, the dataset metric printed the shortest form (`¥1,293`, `¥1,280.5`) and the static card rounded to whole units. An authored `format` pattern still states its width. +- 4128213: fix(console): a nav entry with no icon draws its object's icon, record field labels read in one style, a count chart fits a small count, and a page whose entry failed to load retries (#30126) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Navigation icons.** An app navigation entry of type `object` that names no `icon` now draws the icon its object declares, in the sidebar and in the `nav:menu` page block. Before, every such row drew the same generic page icon, so an app whose navigation names no icons showed one icon down the whole sidebar. The icon is read by object name from the metadata the console already holds, so no request is added and the navigation is not written to. An authored `icon` wins, and an object with no icon, a dashboard, page or report entry, and a host with no resolver keep the generic icon. `@object-ui/layout` exports `resolveNavItemIcon`, the types `NavIconTarget` and `NavTargetIconResolver`, and an optional `resolveTargetIcon` prop on `NavigationRenderer`, alongside the existing `resolveTargetLabel`. + - **Record field labels.** A record page's section field labels no longer carry an uppercase transform. Only Latin text took it, so in a zh console an untranslated label (`REPAIR COST`) read in capitals beside a translated one (`负责人`). Section labels now render as authored, in the style of the highlights strip above them. + - **Small count charts.** A count axis whose largest value is a small whole number (1 to 3) now tops out at that number, so a chart of one record draws its bar to the top of the axis instead of a quarter of the way up a 0–4 axis. An axis with an authored `min`, `max` or `stepSize`, fractional values, or a count of 4 or more draws as before. + - **Boot retry.** When the page's entry script fails to load before anything mounts (the server went away between serving the page and serving its entry, as during an `os dev` restart), the page now reloads itself, backing off from 1 second and doubling, at most five times per tab. Before, the loading splash stayed forever. The count is cleared on the first mount, and where `sessionStorage` cannot be read nothing retries. +- f18adce: feat(app-shell): the first organization's create dialog opens with a default name, so one click continues + + Clause-②: no + Metadata-protocol: none + + For a user with no organization yet — the dialog that opens right after sign-up — the organization name is prefilled from the user's e-mail. A work domain names the company from the domain's registrable label, capitalised (`jane@acme.com` → `Acme`, `jane@acme.co.uk` → `Acme`). A public mailbox domain (a closed list in code: gmail.com, outlook.com, qq.com, 163.com, proton.me and similar, subdomains included) names no company, so it takes the existing default organization name, "My Organization" / 「我的组织」 (`workspace.default`, already in every built-in pack; no translation key is added). An unlisted domain is treated as a work domain. The field stays editable, nothing is created until the user clicks, and creating a further organization still opens with an empty name. The e-mail domain rules live in a product-neutral app-shell util (`utils/emailDomain`), outside the organization pages. +- 1013ef1: fix(app-shell): in Studio Interfaces, a draft-only object's nav entry opens the object's drafted default list view, not a non-existent "Default view" + + Clause-②: no + Metadata-protocol: none + + An object that is still only a draft in the package is absent from the shell's published metadata, so a nav entry for it with no `viewName` used to open `.default`, a view that does not exist. The view switcher listed that "Default view" beside the view the author had drafted as the default, and the first edit in the panel created a second default list view in the package. The entry's default list view is now resolved against the published views and the package's drafted list views together, so it opens the drafted default view, the switcher lists only the views that exist, and an edit writes to that view. Until the package's drafts have been read, a default view that no published view flags is not loaded or listed, so no edit can be made against the wrong view. A published object with no default list view still opens `.default`, created by the first edit, as before. +- 4be9734: fix(automation)!: a `currency` or `percent` screen field refuses a non-number on resume, as a `number` field does, and the console renders `percent` as a numeric input (#30178) + + Clause-②: yes (narrowing) — `POST …/runs/RUN/resume` refuses a string under a `type: 'currency'` or `type: 'percent'` screen field, a bag it accepted before. Nothing is widened. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at the screen-flow resume door. + + - **Before.** A `type: 'percent'` or `type: 'currency'` screen field took any string on resume: `"75"` under a `max` of 60, `"abc"` too. The bound compares numbers, so it never judged a string, and the run consumed its suspension on the value. The console rendered `percent` as a text input and posted a string, so a `percent` field's `min` / `max` held for no console user. + - **Now.** `number`, `currency` and `percent` are one value domain: a present value that is not a finite JSON number is refused with `invalid_type` inside `400 INVALID_SCREEN_INPUT`, the suspension is kept, and a corrected resume still lands. A numeric string is not read as the number it spells. The console renders `percent` as a numeric input carrying `min` / `max`, so it posts a number and the browser stops an over-limit value before any request. + - **Unchanged.** An absent value and a hidden field are not judged; `rating`, `slider`, `progress`, `text` and untyped fields still take a string; a JSON number on `currency` / `percent` is judged against the bound as before. + - **For a caller that skips the console.** The resume bag carries the JSON number (`60`), not a string; the refusal names the field and says it must be a number. No stored metadata changes, so there is nothing for `objectstack migrate meta` to rewrite. +- ef2ad31: fix(app-shell): a dismissed or revoked default permission set in the suggested-bindings panel can be reconsidered (#30199) + + Clause-②: no + Metadata-protocol: none + + The suggested-bindings panel (the install dialog and Marketplace package page, and Studio's Access pillar) drew a dismissed suggestion, or a revoked acceptance, as a state line with no action. An administrator who chose Dismiss could only take it back by calling `POST /api/v1/security/suggested-bindings/:id/reopen` by hand, although the server has offered that door all along. + + Such a row now offers "Reconsider". It calls the re-open door, and the row comes back as a pending suggestion with Accept and Dismiss (a Guest row with Dismiss only, as before). Re-opening grants nothing: the set reaches the Everyone position only when it is accepted again, under the same checks. The button is shown to the same callers who see Accept and Dismiss: a caller without `manage_metadata` is refused the list and sees no panel. A refusal (the row is gone, the set is no longer suggested, or the deployment's security service has no re-open door) is shown with the server's own message, and the panel re-reads the list. + + New client call: `reopenSuggestedBinding(id)`, beside `dismissSuggestedBinding`. New strings: `marketplace.suggestedBindings.reopen`, `.reopening` and `.reopenedToast` in all ten locale packs, and `engine.studio.access.suggestReopen`, `.suggestReopening` and `.suggestReopenedToast` (en, zh). +- fa9c0aa: fix(ui): Setup's everyone note and header tags speak to an administrator + + Clause-②: no + Metadata-protocol: none + + - **Assigned users of a permission set:** a set the everyone position distributes now says 「所有登录成员都持有此权限集。」 / "Every signed-in member holds this set." It no longer names the everyone anchor. + - **Setup list header tags:** three tags now say what they mean for the administrator reading the page, in all ten language packs. Admin config becomes "Configured here" (zh 「在此配置」), Platform schema becomes "Built in · editable" (zh 「平台预置 · 可编辑」), and Identity becomes "Via identity service" (zh 「经身份服务写入」). The tooltip text and the rule for when a tag is drawn do not change. +- f6faf75: fix(console): starting an impersonation lands on home, and a person's permission-set picker no longer offers the MCP agent sets + + Clause-②: no + Metadata-protocol: none + + - **Impersonation landing.** An administrator who starts an impersonation from a Setup page used to stay on that page, which the impersonated user usually cannot open, so the console showed "You don't have access to this app". The impersonation banner now navigates to `/` the moment a session the console held as an ordinary one becomes impersonated, and the console resolves the landing for the user it now holds. A console that boots into an impersonated session (a reload, a new tab, a deep link) stays on the URL it was opened at, and ending an impersonation does not move the page. The navigation goes through the host navigate the console supplies, so a deployment's basename applies. + - **Agent sets in the person picker.** The `permission-ref` field (the permission set on a person's grant, `sys_user_permission_set.permission_set`) no longer offers the three MCP agent ceiling sets (`mcp_agent_data_read`, `mcp_agent_data_write`, `mcp_agent_restricted`, the spec's `MCP_AGENT_PERMISSION_SETS`). Those are an AI assistant's own grants, which the runtime puts on the agent principal directly. A grant that already names one still shows it as the row's value, by its label beside its name. `position-ref` is unchanged. +- fccde75: fix(plugin-chatbot,app-shell): after 确认 or 取消 on the chat's confirm card, the follow-up sent to the model no longer shows as the person's own message + + Clause-②: no + Metadata-protocol: none + + After the operator confirmed or cancelled an action on the chat's confirm card, the chat sent the model a short follow-up («[HITL pa_…] The operator approved the `update_record` call … Acknowledge the outcome …») through the ordinary user-message channel, so the person saw it as their own bubble, after a reload too, and the conversation list showed it as the conversation's preview. The model still receives that follow-up unchanged. What changed is who reads it as the person's words: the chat transcript draws no bubble for it (live, after a reload, and on a shared transcript), the conversation list previews the person's last ask instead, and the conversation-language probe no longer reads it as the person's latest turn, which turned a Chinese conversation's outbound text English when the action's result carried no Chinese. The follow-up is recognised by its leading `[HITL ]` tag, the one part of a stored message that survives a reload; `@object-ui/plugin-chatbot` exports the reader as `isHitlContinuation` (also from `./core`). An ordinary message is sent, drawn and previewed as before. +- 644883e: fix(studio): after Restore package original, the rail's customized mark and the "only customizations" count read the restored item without a reload (#30238) + + Clause-②: no + Metadata-protocol: none + + - **Before.** On an installed (read-only) app, restoring a customized dashboard, report, view or page in Studio brought the shipped item back, but the Interfaces rail kept marking it customized and "only customizations" kept counting it until the page was reloaded. The surface read the base's customizations (`GET /api/v1/packages/:id/customizations`) again only after a publish, and a restore deletes the pair's row without publishing. + - **Now.** The surface reads them again on every metadata-refresh pulse, which a restore already sends (as do a publish and a copilot publish), so every pillar rail's marks and "only customizations" count follow the restore. A publish still reads once. The All metadata area's pages make their own read each time one opens, so they already showed the restored state. +- 8c9f48f: fix(app-shell): the Console header, the app report index and the recent list draw a served report, page or dashboard label as served + + Clause-②: no + Metadata-protocol: none + + - **Now.** A report's crumb and a page's phone title in the header, a report's row in the app's report index, and a report, page or dashboard row in the recent-items list (and an unlabelled sidebar entry that opens a dashboard) draw the label the `/meta` read or the app bundle served as given. The server has already translated it, by its label order for an overridden item: a `.custom` override's or a published edit's own text is kept over the base package's translation. Before, the served label reached the client's catalog lookup only as its fallback, so the base package's translation of the old name won. + - **Unchanged.** An item the read did not serve, or served with no label, is still named by the catalog key under its name (`{ns}.reports..label`, `pages`, `dashboards`), then by its humanized name or machine name as before. An unedited item shows the same text as before, because the server serves the catalog's translation for it. A language switch reads the metadata again, so a served label is never left in the old language. +- 0005fec: fix: on a zh-CN console every row of the view properties panel's collapsed sections (表格选项 / 分页 / 终端用户控制 / 导航与共享) reads in Chinese + + Clause-②: no + Metadata-protocol: none + + - `viewForm` (`@objectstack/spec/ui`) now spells out the children of its `selection`, `pagination`, `userFilters`, `appearance`, `userActions`, `navigation` and `sharing` composites, and gives the rows that had none a hint. Each declared hint is the schema description the panel already showed, so an English console reads the same text; `os i18n extract` now emits a `metadataForms.view.fields['.']` key per row. + - `userFilters.fields` is enumerated as an inline-table repeater, and `UserFilterFieldSchema`'s six properties carry a `.meta({ title })` (Field / Label / Type / Options / Show Count / Default Values) — the English column heads, unchanged. + - The panel no longer offers `navigation.view` and `sharing.type`: both are `retiredKey()` tombstones that the schema-derived child list used to render as editable rows. A stored value is untouched by a panel save (the composite editor spreads the block it was given). + - The platform metadata-forms catalogs carry zh-CN, ja-JP and es-ES text for every new row, and the console's zh-CN overlay copies the zh-CN rows. +- 648089d: feat(app-shell): Studio edits a translation item as a source-to-translation table with its gaps counted + + Clause-②: no + Metadata-protocol: none + + A `translation` item in Studio now opens on a table for its language: a row per string the package declares (objects, fields, options, apps, navigation, views, actions, dashboards and the rest), the source text beside the translation, grouped by item, with a count such as "37 of 120 untranslated" and an "Untranslated only" filter. The rows come from `collectExpectedEntries` in `@objectstack/lint`, the walk `os i18n extract` and `os lint` use, over the package's declared documents. The platform objects the package points at (for example the user object behind an owner lookup) are included, because their labels ship in a few languages only. A row also counts as translated when another bundle already serves the language at its key. + + - Saving writes the same `translation` item to the same place as before (the `.custom` pair on a read-only package): each cell writes the item's JSON at its key. The item's label, language and description stay in the form above the table, and its JSON stays available as an advanced view for what the table does not list (messages, settings UI, metadata forms). + - The "no custom renderer, editing as JSON" notice no longer appears on this page. + - The metadata item list labels an item in a code package's `.custom` pair as customer-written, not as shipped in a code package. + - Labels in English and Chinese. +- 10ecafd: fix(app-shell,auth,i18n): the organization form says requiring MFA needs two-factor on, an accepted save says who must enroll and when, and the enrollment screen names a two-factor-off deployment in the user's language + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Organization form, before saving.** While two-factor is off on the deployment (`features.twoFactor: false` in `/api/v1/auth/config`), the `sys_organization` edit form says, under its title, that Require Multi-Factor Auth needs two-factor, that a save with it ticked is refused, and how to turn two-factor on (Require multi-factor authentication in Setup → Authentication, or `OS_AUTH_TWO_FACTOR=true`). When the save is refused, the server's refusal shows on the field, as before. + - **Organization form, after an accepted save.** A save that turns `require_mfa` on for a row that stored it off adds a line to the saved toast: every member without an authenticator app must set one up, and is blocked from data once the MFA grace period ends (7 days unless changed in Setup → Authentication), counted from their first use after the change. A save of a row that already required it says nothing new. + - **Enrollment screen.** `enrollTotp` and `verifyTotp` of `@object-ui/auth` now reject with the HTTP `status` and the server's `code` on the error (the message is unchanged). The authenticator step reads a `404` (the `/two-factor/*` endpoints are not mounted while two-factor is off) as a translated sentence saying enrollment is not available and who can turn it on, instead of `Two-factor enable failed (404)`, and `INVALID_PASSWORD` as the password screen's sentence. Any other refusal keeps the server's message. + - **The widening.** `@object-ui/i18n` gains `auth.remediation.mfa.unavailable`, `console.organizationMfa.needsTwoFactor` and `console.organizationMfa.nowRequired` in all ten language packs. The two enrollment rejections gain the optional `status` and `code` properties. +- 73b712f: fix(app-shell): every Console surface that names an app draws the served app label as served + + Clause-②: no + Metadata-protocol: none + + - **Now.** The app switcher, the sidebar's app title, the top bar's hidden-app menu, Home's app cards, apps strip, recent rows and setup link, the no-access empty state, the AI chat's app names, the marketplace's "Open app" buttons and the report index's other-app links draw the app `label` (and the app card its `description`) the `/meta/app` read served, as given. The server has already translated it, by its label order for an overridden app: a `.custom` pair's own text is kept over the base package's translation. Before, the served label reached the client's `apps..label` catalog lookup only as its fallback, so the base package's translation of the old name won. + - **Unchanged.** A pending draft under `?preview=draft`, a keyed label and an app with no label or description are still named by the catalog key, then by the machine name as before. An unedited app shows the same text as before, because the server serves the catalog's translation for it. +- efcc1a7: fix(app-shell): the Console sidebar, recent list and object list header draw a served object or list view label as served + + Clause-②: no + Metadata-protocol: none + + - **Now.** An unlabelled sidebar entry that opens an object or a list view, a recent object or list view row (in the sidebar and on Home), the object list page's title, the app header's title on an object route and an object's `/data` page title draw the label the `/meta` read or the app bundle served as given. The server has already translated it, by its label order for an overridden item: a `.custom` object extension's or view override's own text is kept over the base package's translation. Before, the served label reached the client's catalog lookup only as its fallback, so the base package's translation of the old name won. A list is titled by the served `pluralLabel`, else the served `label`. + - **Unchanged.** A list view the client derived (no served read names it), an object the cache does not hold yet, and a pending draft's label are still named by the catalog key, then by the machine name as before. An unedited item shows the same text as before, because the server serves the catalog's translation for it. Record pages, record forms and drawers still look the object's name up in the catalog; they are a follow-up. +- 70c3b1c: fix(studio): on a draft-only object, a new or first-edited list view starts from the object's first five fields + + Clause-②: no + Metadata-protocol: none + + In Studio Interfaces, a list view created on an object that so far exists only as a draft in the package started with no columns. The first edit of a default view that did not exist yet wrote only the column the author touched, and New view with no columns picked wrote none. The list-view panel seeded those columns from the shell's merged objects, which hold published objects only. When the merged definition is missing, the panel now reads the object's drafted definition from this package (`GET /meta/object/:name?preview=draft&package=`, the read the field picker already makes) and seeds from it, so both start from the same first five fields a published object gets. A published object reads nothing new. If the drafted read fails or finds nothing, the seed stays empty as before. +- b4f4781: feat(console): the install note says how many sample records an install loaded, and clears them in place + + Clause-②: no + Metadata-protocol: none + + - **The install note.** After an install into this runtime, the package page's result now says how many sample records the install loaded, read from the install answer's own `data.seeded` report (`inserted + updated + skipped` of an `inline` run), with a "Purge sample data" button right there. An install over an existing one adds that installing again loads the sample data again, including any that was cleared. An install that loaded none (no seed datasets, a copy, a walled session with no active organization) shows nothing new. The success sentence is definite where the runtime answered that it loaded the app: "… is now in the app switcher." + - **The console's own confirm.** Purging sample data (from the install note, or the ⋯ menu, local or cloud) asks in the console's dialog instead of the browser's. When this session installed the package, the dialog says how many sample records will go at most: the number the install loaded. The clearing rule is unchanged: only rows that carry a seed record's key are deleted. + - **A reach from the app's own pages.** When the installer leaves the package page, a console banner (the `banner` notification `ConsoleLayout` draws above every page) says the app holds sample data, with a "Purge sample data" action that brings them back to the package page with the confirm open. Clearing withdraws it. It lasts for the console session; the runtime serves no count of sample rows after the install answer. + - **Types.** `LocalManagedInstallResult` and `LocalUnmanagedInstallResult` gain the optional `seeded` (`LocalInstallSeeded`). + - **Strings.** `@object-ui/i18n` gains `marketplace.detail.sampleDataLoaded`, `sampleDataReloaded`, `purgeConfirmAtMost` (count families), `sampleDataBannerTitle` and `sampleDataBannerMessage` in all 10 locales, and rewords `marketplace.install.localSuccess` in all 10. +- 48231b0: feat(console): the install result says when a package was installed without a protocol compatibility check + + Clause-②: yes (widening) + Metadata-protocol: none + + - **What the administrator now reads.** `POST /api/v1/marketplace/install-local` answers `data.protocolCheckSkipped` when its protocol handshake admitted the package without checking it: the package declares no `engines.protocol` (`no-range`), or a range the runtime cannot read (`unparsed-range`). The three Console surfaces that show an install result now draw it as a caution line under the success sentence: the package page's Install result, the catalog's organization install message, and Setup → Packages → Import. The line names the package, the platform protocol it was installed under and, for `unparsed-range`, the range it declared, in the reader's locale. A `reason` this Console does not know shows the server's own `message`. An install whose range was checked shows nothing new. + - **Types.** `LocalManagedInstallResult` and `LocalUnmanagedInstallResult` gain the optional `protocolCheckSkipped`. + - **New strings.** `@object-ui/i18n` gains `marketplace.install.protocolCheckSkipped.noRange` and `.unparsedRange` in all 10 locales. +- 0d54279: fix(app-shell): a new datasource starts on the External schema mode and says what each mode does; a driver box's default is a placeholder, not a value + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Starts on External.** Setup → Datasources → New datasource now starts on `schemaMode: 'external'` (sent with the `external: {}` block the schema requires) instead of `managed`. The page exists to connect a database that already holds tables and sync them in as objects, and only a federation source offers Sync. A managed datasource is ObjectStack's own storage: once an object is stored in it, the schema sync adds the object's columns to an existing table of the same name. The spec's own default (`managed` when a record declares none) is unchanged; only the dialog's starting choice moved. + - **The modes in business words.** The Schema mode select is ordered External, Managed, Validate only, its two main options are relabelled ("External: read an existing database", "Managed: ObjectStack creates and changes the tables"), and a sentence under it says what the chosen mode does to the database and which one to pick to read an existing database. + - **Why a managed row has no Sync.** A managed datasource's row (not the primary `default`) says it has no Sync objects because ObjectStack creates its tables, and how to read an existing database instead. + - **Box defaults are placeholders.** A driver's text or number box with a default (SQLite `Filename` `:memory:`, PostgreSQL `Host` `localhost` and `Port` `5432`, and so on) starts empty with the default as its placeholder, so a typed path is no longer appended to `:memory:`. A new datasource's Test and Create still send the default for a box left empty, so the request is what it was; an edit shows and sends the stored values. Switches and selects keep their defaults as values. + - **The widening.** Four new `studio` namespace rows in `@object-ui/i18n` (en and zh): `engine.datasource.schemaModeExternalHint`, `engine.datasource.schemaModeManagedHint`, `engine.datasource.schemaModeValidateOnlyHint` and `engine.datasource.noSyncManaged`; the en and zh text of `engine.datasource.schemaModeExternal` and `engine.datasource.schemaModeManaged` changes. +- 8e5fabe: fix(app-shell): after Setup → Datasources syncs objects, the dialog links each new object and names the two steps left: add it to an app, and grant access + + Clause-②: yes (widening) + Metadata-protocol: none + + - **The result.** When a Sync run creates at least one object, the dialog stays open on the result instead of closing behind a toast: the count line, one link per created object (label, with the name beside it when they differ) that opens the object under the app the page is in, and the two steps left before people can use it, adding it to an app and granting access in a permission set. A run that creates nothing closes the dialog as before. Refused tables are still reported in the error toast. + - **The widening.** Four new `studio` namespace rows in `@object-ui/i18n` (en and zh): `engine.datasource.syncNextStepsOne`, `engine.datasource.syncNextSteps`, `engine.datasource.syncNextStepApp` and `engine.datasource.syncNextStepAccess`. + - **Form labels.** A declared field with no `label` is drawn in the form under its name with the first letter capitalised and underscores as spaces (`part_no` → `Part no`), the rule the list already draws it under, so one field reads the same in both. An authored label is unchanged. +- 677ac3b: fix(app-shell): a marketplace package's README fits its detail page; badges render as images and pipe tables as tables + + Clause-②: no + Metadata-protocol: none + + On a package's detail page below the `lg` breakpoint, one long line in a README code block widened the whole README past the page's right edge, where it was cut off with no way to scroll: the page's grid cells took their content's width as their minimum. Each cell now may shrink to the page, so long words wrap and only a code block or a table scrolls, inside itself. + + The README renderer also reads three things it printed as literal markup: + + - Images, `![alt](src)` and linked `[![alt](src)](href)` badges, render from an absolute `https:` URL, without sending the console's address as referrer. Any other source (a repository-relative path such as `LICENSE`, `http:`, `data:`, `javascript:`) is left out, alt text included. A linked image keeps its link only for an absolute `http:` or `https:` destination. + - GFM pipe tables render as tables, with the delimiter row's alignment, each in its own horizontal scroller. + - Thematic breaks (`---`, `***`, `___`) between blocks render as a rule. + + Everything else renders as before; release notes in the Versions panel go through the same renderer. +- a91409c: fix(core,react,i18n,components,app-shell,console): a write refused because the object's external datasource does not accept writes says the table is read-only, not "you don't have permission" + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@object-ui/core`: `isExternalWriteRefusal(err)`**, re-exported from `@object-ui/react` beside the other write-refusal readers. True when the server refused a write with `EXTERNAL_WRITE_FORBIDDEN`, the ADR-0015 write gate's code for an object bound to an external datasource that does not admit writes (`datasource.external.allowWrites` and `object.external.writable` must both be true). Read from `code`, then `details.code`, like `isPermissionError`. The refusal is a 403, so `isPermissionError` answers true for it too: a caller asks this first. + - **`@object-ui/i18n`: `form.readOnlyExternalSource`**, in all ten packs: "This table is read-only. Its records come from an external data source that does not accept changes." + - **Every surface that met this refusal now says the table is read-only:** the console record form page and the record dialog (`@object-ui/components` form), which said "you don't have permission to save this record"; the record page delete, the list row delete and the record page inline edit, which showed the gate's raw diagnostic; and the calendar's drag-to-reschedule and the record path's stage change. A permission line sent an administrator through permission sets for a grant that cannot exist. +- ab12a3b: fix(app-shell): Setup → Datasources names the objects that still use a datasource, in the console's language, before and after its delete + + Clause-②: yes (widening) + Metadata-protocol: none + + - **The confirm.** Opening a datasource's delete reads object metadata and lists every object whose `datasource` names it (label, with the name beside it when they differ). While any is listed, the confirm says the datasource cannot be deleted yet and that each object must be deleted or moved to another datasource first. With none, or when the read fails, the confirm is unchanged. + - **The refusal.** A `400` from `DELETE /api/v1/datasources/:name` re-reads that list. When objects still name the datasource, the toast says, in the console's language, that it was not deleted, which objects still use it, and what to do next. Any other failure, and a `400` with no bound object, keep the server's message in the existing frame. No server text is matched. + - **The widening.** Three new `studio` namespace rows in `@object-ui/i18n` (en and zh): `engine.datasource.deleteBoundBody`, `engine.datasource.deleteBoundRefused` and `engine.datasource.boundObjectSeparator`. +- 2b35374: fix(app-shell): a team made in Setup no longer reads as managed by an outside identity provider, and a header action's write shows on the page's related lists and their counts at once + + Clause-②: no + Metadata-protocol: none + + - The `better-auth` note (the Identity tag's tooltip, and the Setup record header's object-label note) read "Managed by the identity provider" / 「由身份提供方管理」, which told the maker of a hand-made team that the row came from elsewhere and was not theirs to change. It now reads "Written through the built-in identity service" / 「经内置身份服务写入」, and its body says these records are created and changed with the actions on the page or through sign-in, invitation and security flows, all of which go through that service. The values change in all ten language packs; no key is added or removed. + - A record-page action that declares `refreshAfter: true` (an `api` action such as a team's Add Member, a server action, a flow, a declared operation) now refreshes the record AND every related list the page shows: the lists refetch and their tab and card counts re-count. It used to refresh the record alone, so a team's members tab read 0 beside a list of 2 until a later row action on that list. + - Nothing to do on upgrade. +- 6fa842a: fix: a language picked on the sign-in page is kept when the person who signs in is not the browser's last user (#30338) + + Clause-②: no + Metadata-protocol: none + + On a shared browser, signing in as someone other than the last user clears the previous user's saved state, and that sweep also removed the language just picked in the sign-in page's menu, so the person landed in the browser's language. The pick is now remembered for that page-load and put back after the sweep, so the next page opens in the chosen language. What the previous user left is still cleared when nothing was picked. A pick followed by a reload of the sign-in page, or by an identity provider's redirect, is still lost. Saving the pick as the person's own language is a separate change. +- 4831d50: fix(console): a language picked on the register page is saved as the new person's own language when registering signs them in (#30338) + + Clause-②: no + Metadata-protocol: none + + A language picked in the register page's menu used to apply on that browser only: once the sign-up signed the person in, their saved language stayed empty, so another device, or the language their mail is written in, did not follow the pick. The register page now saves the pick to the person's own language setting before it moves on, the way the sign-in page does, on every way it leaves after a sign-up, joining an organization from an invitation link included. `DefaultRegisterPage` does the same for a pick made earlier on another signed-out page. Nothing is saved when no language was picked, a language the person already has is kept, and a save that fails does not hold up the sign-up (the pick still applies on that browser). A pick followed by a reload of the register page, or by an identity provider's redirect, is still not saved. +- cd7f785: fix(app-shell,plugin-detail): the record page offers its 变更 (Field changes) chip, and the activity panel its Field Changes option, only to a reader whose audit-log read answers + + Clause-②: no + Metadata-protocol: none + + On an object with `enable.trackHistory`, the record page's 动态 (Activity) tab offered the chips 全部 / 活动 / 讨论 / 变更 whenever `sys_audit_log` was registered, whatever this reader was allowed to read. A member the server refuses the audit read to (403) got a 变更 chip that always showed nothing, which reads as "nobody changed this record". The page now offers 变更 only once this reader's `sys_audit_log` read has answered. A refused or failed read offers no 变更 chip, exactly as history off does, and the chip joins the others when the read answers. Nothing else waits on the read: the page payload still asks the field changes as before, and the stream's count already counts a refused audit read as no field change. + + The activity panel's filter dropdown follows the same rule: it offers Field Changes (`changes_only`) only when the stream would offer its 变更 chip. A host that says nothing keeps every option, as before. A `changes_only` filter that is no longer offered reads as All Activity. +- 0892a94: fix: on a zh-CN console, the allowed-visualizations chips in a view's settings read the same Chinese words as the view-type select (#30358) + + Clause-②: no + Metadata-protocol: none + + The 允许的可视化 chips (`appearance.allowedVisualizations`) were the only English row left in a list view's end-user section on a zh-CN console: they read Grid, Kanban, Gallery, Calendar, Timeline, Gantt, Map, Chart, Tree. They now read 表格 / 列表, 看板, 画廊, 日历, 时间线, 甘特图, 地图, 图表, 树形, the words the view-type select a few rows up already uses. Both controls take the name from one shared helper, so they cannot drift apart. An en-US console keeps the chip names it had. A multi-select over any other list of values keeps its existing names in every language. +- 7b10a30: fix(auth): the forgot-password page no longer says a reset link was sent on a deployment that sends no mail (#30366) + + Clause-②: yes (widening) + Metadata-protocol: none + + `GET /api/v1/auth/config` reports `features.emailDelivery`: `false` when no email service is wired or the stock `log` transport only writes mail to the server console. On such a deployment the forgot-password form used to answer every request with "Check your email — we've sent a password reset link", although no mail leaves. When the server reports `emailDelivery: false`, the success screen now says a reset link can't be emailed here and to ask an administrator, who can set a new password from Setup → Users. + + The text describes the deployment, never the address: it carries no address, and a known and an unknown address read the same, so the page adds no account-existence oracle. Only an explicit `false` changes it; a server that predates the flag (no `emailDelivery` key) or a failed config fetch keeps the "check your email" screen, since those deployments may well send mail. + + `ForgotPasswordFormLabels` gains `noDeliveryTitle` and `noDeliveryDescription`, and `AuthPublicConfig.features` gains `emailDelivery`. New strings `auth.forgotPassword.noDeliveryTitle` and `auth.forgotPassword.noDeliveryDescription` in all ten locales, passed by the Console's forgot-password page and `DefaultForgotPasswordPage`. +- 8b0fbb8: fix(app-shell,fields,plugin-form,plugin-gantt,plugin-grid,components,console): record pages, forms, drawers and preview cards draw a served object label as served + + Clause-②: no + Metadata-protocol: none + + - **Now.** The record page (its title, breadcrumbs, related-list titles, delete and access-denied copy), the record form page and the console's create/edit dialog (title, save toast, way back to the list), the record drawer, the record preview card, the hover card over a lookup, a record-pointer cell, an object-ref cell, the closed-form notice, the gantt name column, the page header's record chrome, a form view's heading, the AI dock's surface chip, the Activity row and the report index name an object by the label the server served, as given. The server has already translated it, by its label order for an overridden item, so a `.custom` object extension's rename is kept over the base package's translation. Before, the served label reached the client's catalog lookup only as its fallback, so the base package's translation of the old name won on these surfaces after the list and header were fixed. + - **Unchanged.** When no label is served, the catalog key is asked first and the machine name (or its humanized form) stays the last resort, as before. An unedited object shows the same text as before, because the server serves the catalog's translation for it. Related lists and reference rails that only know an object's name, and the packaged-actions table (its object list is read without the session's language), still look the name up in the catalog. English is unchanged. +- bbd6072: fix(console): a language picked on the sign-in page is saved as the person's own language when they have none (#30399) + + Clause-②: yes (widening) + Metadata-protocol: none + + A language picked in the sign-in page's menu used to apply on that browser only: after signing in, the person's saved language stayed empty, so another device, or the language their mail is written in, did not follow the pick. The sign-in page now saves the pick to the person's own language setting before it moves on, when they have not chosen one yet. A language they already chose is kept, nothing is saved when no language was picked on the page, and a save that fails does not hold up the sign-in (the pick still applies on that browser). `@object-ui/app-shell` gains `hasSignInLanguagePick`, `saveSignInLanguagePick` and the `SignInLanguagePickWriter` type for the sign-in page. A pick followed by a reload of the sign-in page, or by an identity provider's redirect, is still not saved. +- c7840ea: fix(app-shell): the Studio item header's "Saved to X's customizations" note says where the customization lives, in the publish sheet's own sentence (#30436) + + Clause-②: no + Metadata-protocol: none + + The note on an item header (Interfaces item, navigation editor, Data object, Automations flow) now goes on with the sentence the pending-changes sheet draws under it (`preview.changes.customizationEnvironment`, from #30119): the changes are kept in this environment's database as items of the real `.custom` pair, and reach another environment only through `os package export`. No new i18n key: the note reuses `customizationEnvironmentText`. Before the pair's first save the note names the `.custom` id that save creates. +- ad2393f: fix(plugin-detail,app-shell): the related list, the reference rail and the packaged-actions table name a renamed object by the name the server serves + + Clause-②: yes (widening) + Metadata-protocol: none + + - **The surfaces.** A `record:related_list` block and a `record:reference_rail` card with no authored `title` are headed with the related object's served plural label, else its served label, as the server translated it for the session (a `.custom` extension's rename included). They used to ask only the client translation bundle, which still held the base package's name. The bundle is asked only when the server serves neither. + - **Packaged actions.** Setup → Packaged Actions reads the object list in the session's language (`Accept-Language`), not the browser's, and its Object column draws that served label as given. + - **The widening.** The served-object-label rule now lives in `@object-ui/core`, which `@object-ui/plugin-detail` can import: new exports `servedObjectLabel`, `servedObjectPluralLabel`, `metadataText` and the types `ObjectLabelLookup` and `ObjectPluralLabelLookup`. app-shell's internal copy is now a re-export of these, so both packages read one rule. +- 5b748df: fix(app-shell): turning on Require MFA on an organization shows the grace period once, with the number Setup set + + Clause-②: no + Metadata-protocol: none + + An accepted save that turns `require_mfa` on for an organization now gets a save notice from the server that names who must enroll and the MFA grace period in force. The console still added its own line under it, which says the same thing with the default "7 days". So the toast told the administrator the grace period twice, and when Setup → Authentication set another number, the two lines disagreed. + + The record form now leaves its own line out when the save answer's notice is the server's MFA notice. It recognizes that notice by its opening sentence, in each of the four languages the server writes it in, and not just by a notice being there: the answer carries one notice, the last one a hook or flow on the write set, so another notice on the same save keeps the console line. A save whose answer has no notice (a server that does not send one) shows the console line as before. +- 47c457b: feat(console): after a reload, the package page still says how many sample records a purge would delete + + Clause-②: no + Metadata-protocol: none + + - **The count on any visit.** The package page of a local install now reads the runtime's count of the sample rows a purge would delete right now (`GET /api/v1/marketplace/install-local/:manifestId/sample-data`), on every visit and again after an install, a reseed or a purge. Before, the purge confirm and the banner that follows the installer to the app knew a count only for an install made in the same browser session; a reload lost it. + - **The confirm.** The local purge confirm draws that count. When it cannot be read (refused, failed), it draws the count this session's install answered, as before. A read that began before an install, reseed or purge of this session answered is older than that answer and is not used. + - **The banner.** A count above zero raises the sample-data banner on leaving the page, as an install of this session does. A count of `0` drops what the session knew: no banner, and no count line in the confirm. + - **Types.** `marketplaceApi` gains `countLocalSampleData` and `LocalSampleDataCount`. + - **Strings.** `@object-ui/i18n` rewords `marketplace.detail.purgeConfirmAtMost` (all plural forms) in all 10 locales: it no longer says the count is "the number this install loaded", which is not what the runtime's count is. It stays an upper bound ("Up to N sample records will be deleted."), true of both counts. No key is added. +- 614e831: feat(app-shell): Connect an Agent adds connect cards for WorkBuddy, Doubao Work and QwenWork, and drops the VS Code card + + Clause-②: no + Metadata-protocol: none + + The Connect an Agent page (Account → Developer, and Setup) now carries three more client cards, each filled in with the deployment's MCP URL, listed after the Claude, Codex and Cursor cards: + + - **WorkBuddy** — the `mcpServers` entry for Connectors → Custom connector → Configure MCP, with the `streamableHttp` transport spelling WorkBuddy reads, and the reminder that the connector stays inactive until it is trusted. + - **Doubao Work** — the name, transport and server URL to type into Skills · Connectors → New → New custom connector. + - **QwenWork** — the `mcpServers` JSON for Extensions → Connectors → Paste JSON config, with the `streamable-http` spelling QwenWork reads. + + Each of the three names the fallback for a client version without the browser sign-in: the request header `Authorization: Bearer `. A minted key is now revealed in that form as well as the existing `x-api-key` line. + + The claude.ai / Claude Desktop and Claude Code cards merge into one full-width Claude card with a half for each. The Codex CLI card becomes a Codex card for the Codex app: the name, transport and URL to enter under Settings → MCP servers → Add server, and where to sign in (new string `connectAgent.codex.hint`); it no longer shows a `config.toml` snippet. The VS Code / Copilot card is gone, with its string `connectAgent.vscode.body`; the generic `mcpServers` snippet on the Cursor card and the MCP URL at the top still serve any MCP client. The client cards and the API-key card form one list, the API-key card last. Long one-line snippets wrap instead of being clipped. The Doubao Work and QwenWork card titles are strings too (`connectAgent.doubao.title`, `connectAgent.qwenwork.title`), so the zh pack shows the products' own names, 豆包工作 and 千问办公. New strings live under `connectAgent.fields`, `connectAgent.workbuddy`, `connectAgent.doubao` and `connectAgent.qwenwork`, translated in all ten packs. Nothing on the server changes; `@objectstack/mcp` updates only its description of the page. +- Updated dependencies [ffeda13] +- Updated dependencies [4261169] +- Updated dependencies [8fefefd] +- Updated dependencies [3b8b23c] +- Updated dependencies [48b3ae6] +- Updated dependencies [ffeda13] +- Updated dependencies [48b3ae6] +- Updated dependencies [7a5f73e] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [a4c3e73] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [1e6f542] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [46d2b65] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [83fc096] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [6e68d8f] +- Updated dependencies [3461420] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [df4c06e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [85a8fa0] +- Updated dependencies [1756666] +- Updated dependencies [f9e5bef] +- Updated dependencies [0f3ee58] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [8c1bf5b] +- Updated dependencies [29cbfcc] +- Updated dependencies [cfb602c] +- Updated dependencies [f68b47c] +- Updated dependencies [75a5c3e] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [eb6199b] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [dfafae8] +- Updated dependencies [14c67d4] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [c045c34] +- Updated dependencies [dc51811] +- Updated dependencies [c133cfe] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [b6caa14] +- Updated dependencies [ff3f83b] +- Updated dependencies [25fea77] +- Updated dependencies [c322421] +- Updated dependencies [ea5807e] +- Updated dependencies [4128213] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [f273c9f] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [2046d21] +- Updated dependencies [fa9c0aa] +- Updated dependencies [5174603] +- Updated dependencies [778aedf] +- Updated dependencies [f6faf75] +- Updated dependencies [c580081] +- Updated dependencies [fccde75] +- Updated dependencies [d9d42d4] +- Updated dependencies [67d5d53] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [54b491a] +- Updated dependencies [8293c74] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [fdd0641] +- Updated dependencies [ad0fbfd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [29702d5] +- Updated dependencies [cd7f785] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [7b10a30] +- Updated dependencies [e760204] +- Updated dependencies [8b0fbb8] +- Updated dependencies [d3b2aaf] +- Updated dependencies [4dde0ae] +- Updated dependencies [4ccd790] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [482e089] +- Updated dependencies [ad2393f] +- Updated dependencies [0e4747a] +- Updated dependencies [55d9dd4] +- Updated dependencies [6c30357] +- Updated dependencies [af8aab1] +- Updated dependencies [062236c] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [e9731fc] +- Updated dependencies [f4d568b] +- Updated dependencies [47c457b] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [c23eafa] +- Updated dependencies [8a0516c] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/auth@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/plugin-chatbot@18.4.0 + - @object-ui/plugin-dashboard@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/plugin-list@18.4.0 + - @object-ui/data-objectstack@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @object-ui/mobile@18.4.0 + - @object-ui/i18n@18.4.0 + - @objectstack/lint@18.4.0 + - @objectstack/formula@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/plugin-grid@18.4.0 + - @object-ui/layout@18.4.0 + - @object-ui/plugin-charts@18.4.0 + - @object-ui/plugin-kanban@18.4.0 + - @object-ui/plugin-form@18.4.0 + - @object-ui/plugin-calendar@18.4.0 + - @object-ui/plugin-report@18.4.0 + - @object-ui/plugin-designer@18.4.0 + - @object-ui/plugin-editor@18.4.0 + - @object-ui/plugin-view@18.4.0 + - @object-ui/collaboration@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/app-shell/package.json b/ui/packages/app-shell/package.json index 790188706ac..0e0bb4abfa2 100644 --- a/ui/packages/app-shell/package.json +++ b/ui/packages/app-shell/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/app-shell", - "version": "18.3.0", + "version": "18.4.0", "private": true, "type": "module", "sideEffects": [ diff --git a/ui/packages/auth/CHANGELOG.md b/ui/packages/auth/CHANGELOG.md index 65e41fdd74c..a733bc64266 100644 --- a/ui/packages/auth/CHANGELOG.md +++ b/ui/packages/auth/CHANGELOG.md @@ -1,5 +1,127 @@ # @object-ui/auth +## 18.4.0 + +### Patch Changes + +- ffeda13: refactor(auth)!: retire twelve zero-consumer exports (the Turnstile script URL, the two phone-identifier helpers, the authenticated-fetch options type, the org-role list, the invitation-status list and guard, and five form primitives) + + Clause-②: no (narrowing) + Metadata-protocol: none + + + + **BREAKING** for a host that imports any of these twelve names from `@object-ui/auth`. None had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected. Every module stays in the package: `TurnstileWidget` keeps loading its script, `LoginForm` keeps reading phone identifiers, `createAuthClient` keeps narrowing invitation statuses, `org-roles.ts` keeps building its pickers from the role list, and `LoginForm` / `RegisterForm` / `ForgotPasswordForm` keep drawing with the form primitives. Only the entry stops publishing them. `TurnstileWidget`, `createAuthenticatedFetch`, `ORG_ROLE_OWNER` / `ORG_ROLE_ADMIN` / `ORG_ROLE_DELEGATED_ADMIN` / `ORG_ROLE_MEMBER`, `ORG_ROLE_LABELS`, `orgRoleGrade`, `invitableOrgRoles`, `assignableOrgRoles`, the `OrgRole` and `AuthInvitationStatus` types, `AUTH_LINK_CLASS`, `AUTH_PRIMARY_BUTTON_CLASS`, `AuthAlertIcon`, `AuthErrorBanner`, `AuthFormHeader` and `AuthSpinner` stay exported unchanged. + + What leaves, and what to write instead: + + - FROM `TURNSTILE_SCRIPT_URL` TO `TurnstileWidget`, which loads the Cloudflare Turnstile script itself. + - FROM `normalizePhoneIdentifier`, `looksLikePhoneIdentifier` TO `LoginForm`, which accepts a phone number as the identifier and normalizes it; keep a local copy if your own sign-in form needs them. + - FROM `AuthenticatedAdapterOptions` TO a local interface: no function in the package takes it (`createAuthenticatedFetch` has its own options). + - FROM `ORG_ROLES` TO `invitableOrgRoles` / `assignableOrgRoles` for the roles a picker offers, or the four `ORG_ROLE_*` constants. + - FROM `AUTH_INVITATION_STATUSES`, `isAuthInvitationStatus` TO the `AuthInvitationStatus` type; `AuthInvitation.status` already arrives narrowed from `createAuthClient`. + - FROM `AUTH_FIELD_LABEL_CLASS`, `AUTH_INPUT_CLASS`, `AuthCheckIcon`, `AuthDivider`, `AuthMailIcon` TO no replacement: the built-in forms apply them themselves; copy the class strings or icons if a custom form needs the same look. +- a4c3e73: feat(console,mobile,auth): the Console registers the mobile shell's push token under its own session and unregisters it on sign-out and when the shell removes the connection + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@object-ui/console`:** in the ObjectStack mobile shell, a signed-in person who has reached Home or an app is asked for push permission once per page load, after a short delay (never on a sign-in screen): the Console calls `getPushToken()` and registers the token with `POST /api/v1/notifications/devices`. A `null` answer (simulator, permission denied, no token) sends nothing. A token the shell rotates is registered again; an unchanged token for the same person is not sent twice. Another person signing in on the page registers the device for them, and the registry moves the token away from the previous person. An impersonation never registers, so the impersonated person's pushes never reach the administrator's device. The registration sends `deviceId` as the shell's `installId` when it has one, and `origin` only when the page was loaded over `http:` or `https:`. The device is unregistered (`DELETE /api/v1/notifications/devices/:deviceId`) before sign-out ends the session, and when the shell removes this connection; a shell that is neither iOS nor Android registers nothing. A browser is unchanged. + - **`@object-ui/mobile`:** `ObjectStackNative` gains the optional `installId` (`string | null`), and the package exports `NATIVE_PUSH_TOKEN_CHANGED_EVENT` (`objectstack-native:pushTokenChanged`, announced as `has('pushTokenChanged')`) with its event type `NativePushTokenChangedEvent`. Both are feature-detected: a shell that predates them carries neither. + - **`@object-ui/auth`:** `registerBeforeSignOut(step)` registers a step that `signOut()` awaits while the session still exists, and returns the function that removes it. The steps run together and can never hold sign-out back: a rejection is reported and dropped, and sign-out goes ahead after `BEFORE_SIGN_OUT_TIMEOUT_MS` (3 s) whether or not they have settled. +- c322421: fix(console,auth): after an explicit sign-out, whoever signs in next lands on their own home, not on the page the previous person had open + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@object-ui/auth`: `AuthContextValue.sessionEndedBySignOut`**, `true` from a `signOut()` until a session is established again in the tab, whoever it belongs to; `false` on load, while signed in, and after a session that ended any other way (expired, revoked, signed out in another tab). It is a new required member of the exported `AuthContextValue` type: code that builds its own value of that type (a test double, a custom provider) must add `sessionEndedBySignOut: false` (or its own reading) to type-check. + - **`@object-ui/console`: `LoginRedirect`** sends the visitor to `/login` with no `?redirect=` after an explicit sign-out. A new member signing in after the administrator signed out of a Setup page used to land on that page and read "you do not have access to this app". A session that expires or is revoked under the person still keeps the path (list state stripped as before), so that person signs back in where they were. +- 10ecafd: fix(app-shell,auth,i18n): the organization form says requiring MFA needs two-factor on, an accepted save says who must enroll and when, and the enrollment screen names a two-factor-off deployment in the user's language + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Organization form, before saving.** While two-factor is off on the deployment (`features.twoFactor: false` in `/api/v1/auth/config`), the `sys_organization` edit form says, under its title, that Require Multi-Factor Auth needs two-factor, that a save with it ticked is refused, and how to turn two-factor on (Require multi-factor authentication in Setup → Authentication, or `OS_AUTH_TWO_FACTOR=true`). When the save is refused, the server's refusal shows on the field, as before. + - **Organization form, after an accepted save.** A save that turns `require_mfa` on for a row that stored it off adds a line to the saved toast: every member without an authenticator app must set one up, and is blocked from data once the MFA grace period ends (7 days unless changed in Setup → Authentication), counted from their first use after the change. A save of a row that already required it says nothing new. + - **Enrollment screen.** `enrollTotp` and `verifyTotp` of `@object-ui/auth` now reject with the HTTP `status` and the server's `code` on the error (the message is unchanged). The authenticator step reads a `404` (the `/two-factor/*` endpoints are not mounted while two-factor is off) as a translated sentence saying enrollment is not available and who can turn it on, instead of `Two-factor enable failed (404)`, and `INVALID_PASSWORD` as the password screen's sentence. Any other refusal keeps the server's message. + - **The widening.** `@object-ui/i18n` gains `auth.remediation.mfa.unavailable`, `console.organizationMfa.needsTwoFactor` and `console.organizationMfa.nowRequired` in all ten language packs. The two enrollment rejections gain the optional `status` and `code` properties. +- fdd0641: fix(auth): the sign-in page reads the sign-in methods the server has now, not the ones it had when the tab was opened (#30299) + + Clause-②: no + Metadata-protocol: none + + The auth client kept the first `/auth/config` answer for the life of the page. An administrator who turned email and password off on the Authentication settings page (with another way in configured) and then signed out was shown the email and password form on the sign-in page, from the copy read while signed in, although the server refuses that form. The client now forgets the cached config whenever the tab is left without a session: on sign-out, and when a session read finds none (an expired session reaches the sign-in page without a sign-out). Readers on one page still share one request, and nothing changes while a session lasts. +- 7b10a30: fix(auth): the forgot-password page no longer says a reset link was sent on a deployment that sends no mail (#30366) + + Clause-②: yes (widening) + Metadata-protocol: none + + `GET /api/v1/auth/config` reports `features.emailDelivery`: `false` when no email service is wired or the stock `log` transport only writes mail to the server console. On such a deployment the forgot-password form used to answer every request with "Check your email — we've sent a password reset link", although no mail leaves. When the server reports `emailDelivery: false`, the success screen now says a reset link can't be emailed here and to ask an administrator, who can set a new password from Setup → Users. + + The text describes the deployment, never the address: it carries no address, and a known and an unknown address read the same, so the page adds no account-existence oracle. Only an explicit `false` changes it; a server that predates the flag (no `emailDelivery` key) or a failed config fetch keeps the "check your email" screen, since those deployments may well send mail. + + `ForgotPasswordFormLabels` gains `noDeliveryTitle` and `noDeliveryDescription`, and `AuthPublicConfig.features` gains `emailDelivery`. New strings `auth.forgotPassword.noDeliveryTitle` and `auth.forgotPassword.noDeliveryDescription` in all ten locales, passed by the Console's forgot-password page and `DefaultForgotPasswordPage`. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/auth/package.json b/ui/packages/auth/package.json index 7402c36354e..e2bcfd81fe6 100644 --- a/ui/packages/auth/package.json +++ b/ui/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/auth", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Authentication system for ObjectUI with AuthProvider, useAuth hook, AuthGuard, and form components.", diff --git a/ui/packages/cli/CHANGELOG.md b/ui/packages/cli/CHANGELOG.md index 4342b81c576..c4ac552ffaa 100644 --- a/ui/packages/cli/CHANGELOG.md +++ b/ui/packages/cli/CHANGELOG.md @@ -1,5 +1,80 @@ # @object-ui/cli +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [a91409c] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [8b0fbb8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @object-ui/components@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/cli/package.json b/ui/packages/cli/package.json index d30498b56e4..32bd537453e 100644 --- a/ui/packages/cli/package.json +++ b/ui/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/cli", - "version": "18.3.0", + "version": "18.4.0", "description": "Standalone CLI for ObjectUI — scaffold, develop, build and validate JSON/YAML schema-driven applications.", "type": "module", "homepage": "https://objectstack.ai/docs/frontend/cli", diff --git a/ui/packages/collaboration/CHANGELOG.md b/ui/packages/collaboration/CHANGELOG.md index 6b285dfcba2..368af4a804f 100644 --- a/ui/packages/collaboration/CHANGELOG.md +++ b/ui/packages/collaboration/CHANGELOG.md @@ -1,5 +1,53 @@ # @object-ui/collaboration +## 18.4.0 + +### Patch Changes + +- Updated dependencies [7a5f73e] +- Updated dependencies [1e6f542] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [aa57c76] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [b6caa14] +- Updated dependencies [ea5807e] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [ef2ad31] +- Updated dependencies [fa9c0aa] +- Updated dependencies [77c203a] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [7b10a30] +- Updated dependencies [062236c] +- Updated dependencies [47c457b] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] + - @object-ui/types@18.4.0 + - @object-ui/i18n@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/collaboration/package.json b/ui/packages/collaboration/package.json index 3a2bf943ca6..75607260f48 100644 --- a/ui/packages/collaboration/package.json +++ b/ui/packages/collaboration/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/collaboration", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Real-time collaboration for ObjectUI with presence tracking, live cursors, conflict resolution, and comment threads.", diff --git a/ui/packages/components/CHANGELOG.md b/ui/packages/components/CHANGELOG.md index ddb5292776d..3860e3afeac 100644 --- a/ui/packages/components/CHANGELOG.md +++ b/ui/packages/components/CHANGELOG.md @@ -1,5 +1,189 @@ # @object-ui/components +## 18.4.0 + +### Minor Changes + +- aa57c76: feat(spec): three forward date-range presets: `next_month`, `next_quarter` and `next_30_days` + + Clause-②: yes (widening) + Metadata-protocol: added + + The relative dates a dashboard date filter and the filter builder offer looked only backward. A forecast asks forward: what closes next month, next quarter, in the next 30 days. Those questions had to use a custom range, whose fixed dates go stale. + + `DATE_RANGE_PRESETS` now also declares `next_month` (the whole of next month), `next_quarter` (the whole of next quarter) and `next_30_days` (today and the 30 days after it, the mirror of `last_30_days`). Every place that accepts a preset accepts them: `dashboard.dateRange.defaultRange`, a date global filter's `defaultValue`, the filter builder's `between` row and `AnalyticsQuery.timeDimensions[].dateRange`. They sit beside the family they extend in the filter bar: `next_month` after `last_month`, `next_quarter` after `last_quarter`, `next_30_days` after `last_90_days`. Their `DATE_RANGE_PRESET_MACRO_WINDOWS` rows follow the inclusive last-day convention: `['{next_month_start}', '{next_month_end}']`, `['{next_quarter_start}', '{next_quarter_end}']` and `['{today}', '{30_days_from_now}']`. A preset name written as a bare filter comparand is refused with that window as the prescription, as for the other presets. + + `@objectstack/core`'s `resolveAnalyticsDateRangePreset` resolves the three in its half-open form. `next_30_days` stops before `{31_days_from_now}`. `next_month` and `next_quarter` stop before the month and the quarter after next, which no macro token names: the resolver reads `next_month_start` (or `next_quarter_start`) again from the start of the day it named, so the end is still a day the one token resolver computed. No macro token is added. + + In the console, the three presets have labels in all ten locale packs, and the client date-macro resolver now answers `{next_month_end}` and `{next_quarter_end}`, two spec tokens it did not resolve before. `@objectstack/verify`'s dashboard-window proof also writes fixture rows on a forward preset's future edges, so it judges the three new windows; the days it writes for the other presets are unchanged. `@objectstack/lint` changes documentation only. + +### Patch Changes + +- 9049be2: fix(plugin-detail): a record page's body table that is wider than the details column says so — its first column stays pinned and ⤢ is offered as 「All columns」 + + Clause-②: no + Metadata-protocol: none + + A body table of the default record page (a related list drawn in the details column) keeps its place in that column, as ADR-0085's role-based layout puts it. When its columns are wider than the column, it now makes the overflow obvious: + + - **The first column stays pinned** while the table scrolls sideways, so a scrolled row is still identifiable (the table's existing `frozenColumns: 1`, set only while the table overflows). + - **⤢ is offered in words.** The enlarge control in the card header, icon-only before, shows a visible 「All columns」 label (`detail.relatedCardAllColumns`, all ten locales) while the table overflows. It opens the same enlarged tab as before, where every column fits. + - **The right-edge shadow** while columns remain to the right is the table's own and unchanged; it clears when the table is scrolled to its end. + + A body table that fits shows none of these: no pinned column and the icon-only ⤢. + + `@object-ui/components`: a `data-table` header label carries its full text as a `title`, so a header the ellipsis cuts can be read on hover, and the table's scroll box carries a `data-table-scroll-box` marker a host can read the overflow from. +- a52761f: fix(console): an action another package places on `sys_user` at `record_header` / `record_more` draws again on the user's record page, in the drawer and on the full page (#29951) + + Clause-②: no + Metadata-protocol: none + + - **The header reads `placedActions: true`.** `page:header`'s `placedActions` (#30018) is now drawn by the console's header renderer (`PageHeaderActions` in `@object-ui/components`): `true` draws every action of the page's object whose `locations` include `record_header` or `record_more`, in declaration order, read from the object's metadata at render time, so an action an installed package adds to the object is drawn without editing the page. Omitted or `false`, an authored `actions` list is still the whole set (#11592). A `true` beside an authored list, which the spec refuses, draws none of the object's actions and is reported once in the console. + - **One placed-header reader.** `placedHeaderActions` (`@object-ui/types`) is the rule both headers read: the synthesized default record page's header in `RecordDetailView` and a `placedActions: true` header. The `page:header` registration publishes the `placedActions` input. + - **The platform record pages opt in.** `sys_user_detail` and `sys_organization_detail` (`@objectstack/platform-objects`) and `sys_approval_request_detail` (`@objectstack/plugin-approvals`) set `placedActions: true` in place of the id lists #27219 froze, which dropped every header action another package places on those objects. The tab strips are unchanged, and every action the lists named is placed at `record_header` / `record_more`, so each still draws. + - `@objectstack/spec`: the `placedActions` describe no longer opens with `[PLANNED — not yet rendered]`, and the `page` liveness record names the reader. +- 6db9198: fix(ui): a greyed action's `disabledReason` tooltip wraps inside a phone-width viewport instead of running off-screen (#29954) + + Clause-②: no + Metadata-protocol: none + + The record action bar, the `record:quick_actions` button and the schema-driven `action:*` controls (button, icon, group, menu, through the shared disabled-reason trigger) rendered the authored `disabledReason` in a tooltip with no width bound, so a long reason sat on one line and was cut at the right edge of a 390px viewport. All three tooltips now carry the same `max-w-xs` bound the home page's disabled-reason tooltip uses, so the reason wraps. +- bff5ba2: fix(ui): the record Edit form judges a field's `requiredWhen` as the engine does, a transition gate against the stored record, so a record already inside the gate without the value saves an unrelated edit + + Clause-②: yes (widening) + Metadata-protocol: none + + This widens the public surface of `@object-ui/core`: the new export `requiredWhenRestsOnStoredRecord` and the new `requiredWhenAtRest` field in the return type of `resolveFieldRuleState`. + + - **`@object-ui/components` form renderer:** on an Edit form (`previousValues` set), a field's `requiredWhen` is required only when the stored record complied with it. A stored record that already violates it (the predicate holds over the stored record and its value is missing, as with a contract activated before its signed-copy rule existed) is not blocked any more: it saves an edit that leaves the field empty, as the engine and REST `PATCH` already accept. This follows `Field.requiredWhen`: the write is refused only when the merged record violates the requirement AND the pre-write record complied. Still refused: the edit that moves a record into the gate without the value, and clearing the value of a record that held it. A Create form enforces as before. The "Required to run" note under an empty field (the update-action heads-up) is no longer shown for such a stored record, because the engine judges the action's write against the same stored record. + - **`@object-ui/core`:** `resolveFieldRuleState` takes `statics.name` (the field's name) and returns `requiredWhenAtRest`. With `previous` and `statics.name` given, a stored record already violating `requiredWhen` resolves `required: false` and `requiredWhenAtRest: true`. A caller that passes no `name` keeps the plain verdict. New export `requiredWhenRestsOnStoredRecord(pred, name, stored, scope?)`, the stored-record check alone. A predicate that cannot be evaluated over the stored record counts it as compliant, as on the server. +- a91409c: fix(core,react,i18n,components,app-shell,console): a write refused because the object's external datasource does not accept writes says the table is read-only, not "you don't have permission" + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@object-ui/core`: `isExternalWriteRefusal(err)`**, re-exported from `@object-ui/react` beside the other write-refusal readers. True when the server refused a write with `EXTERNAL_WRITE_FORBIDDEN`, the ADR-0015 write gate's code for an object bound to an external datasource that does not admit writes (`datasource.external.allowWrites` and `object.external.writable` must both be true). Read from `code`, then `details.code`, like `isPermissionError`. The refusal is a 403, so `isPermissionError` answers true for it too: a caller asks this first. + - **`@object-ui/i18n`: `form.readOnlyExternalSource`**, in all ten packs: "This table is read-only. Its records come from an external data source that does not accept changes." + - **Every surface that met this refusal now says the table is read-only:** the console record form page and the record dialog (`@object-ui/components` form), which said "you don't have permission to save this record"; the record page delete, the list row delete and the record page inline edit, which showed the gate's raw diagnostic; and the calendar's drag-to-reschedule and the record path's stage change. A permission line sent an administrator through permission sets for a grant that cannot exist. +- 8b0fbb8: fix(app-shell,fields,plugin-form,plugin-gantt,plugin-grid,components,console): record pages, forms, drawers and preview cards draw a served object label as served + + Clause-②: no + Metadata-protocol: none + + - **Now.** The record page (its title, breadcrumbs, related-list titles, delete and access-denied copy), the record form page and the console's create/edit dialog (title, save toast, way back to the list), the record drawer, the record preview card, the hover card over a lookup, a record-pointer cell, an object-ref cell, the closed-form notice, the gantt name column, the page header's record chrome, a form view's heading, the AI dock's surface chip, the Activity row and the report index name an object by the label the server served, as given. The server has already translated it, by its label order for an overridden item, so a `.custom` object extension's rename is kept over the base package's translation. Before, the served label reached the client's catalog lookup only as its fallback, so the base package's translation of the old name won on these surfaces after the list and header were fixed. + - **Unchanged.** When no label is served, the catalog key is asked first and the machine name (or its humanized form) stays the last resort, as before. An unedited object shows the same text as before, because the server serves the catalog's translation for it. Related lists and reference rails that only know an object's name, and the packaged-actions table (its object list is read without the session's language), still look the name up in the catalog. English is unchanged. +- Updated dependencies [4261169] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [1e6f542] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [b6caa14] +- Updated dependencies [25fea77] +- Updated dependencies [ea5807e] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [2046d21] +- Updated dependencies [fa9c0aa] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [7b10a30] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [ad2393f] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [062236c] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [47c457b] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/core@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @objectstack/sdui-parser@18.4.0 + - @object-ui/react-runtime@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/components/package.json b/ui/packages/components/package.json index 9ac01f84329..574e26eac59 100644 --- a/ui/packages/components/package.json +++ b/ui/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/components", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Standard UI component library for ObjectUI, built with Shadcn UI + Tailwind CSS", diff --git a/ui/packages/core/CHANGELOG.md b/ui/packages/core/CHANGELOG.md index b23fb93834f..6b9d2d27de3 100644 --- a/ui/packages/core/CHANGELOG.md +++ b/ui/packages/core/CHANGELOG.md @@ -1,5 +1,207 @@ # @object-ui/core +## 18.4.0 + +### Minor Changes + +- aa57c76: feat(spec): three forward date-range presets: `next_month`, `next_quarter` and `next_30_days` + + Clause-②: yes (widening) + Metadata-protocol: added + + The relative dates a dashboard date filter and the filter builder offer looked only backward. A forecast asks forward: what closes next month, next quarter, in the next 30 days. Those questions had to use a custom range, whose fixed dates go stale. + + `DATE_RANGE_PRESETS` now also declares `next_month` (the whole of next month), `next_quarter` (the whole of next quarter) and `next_30_days` (today and the 30 days after it, the mirror of `last_30_days`). Every place that accepts a preset accepts them: `dashboard.dateRange.defaultRange`, a date global filter's `defaultValue`, the filter builder's `between` row and `AnalyticsQuery.timeDimensions[].dateRange`. They sit beside the family they extend in the filter bar: `next_month` after `last_month`, `next_quarter` after `last_quarter`, `next_30_days` after `last_90_days`. Their `DATE_RANGE_PRESET_MACRO_WINDOWS` rows follow the inclusive last-day convention: `['{next_month_start}', '{next_month_end}']`, `['{next_quarter_start}', '{next_quarter_end}']` and `['{today}', '{30_days_from_now}']`. A preset name written as a bare filter comparand is refused with that window as the prescription, as for the other presets. + + `@objectstack/core`'s `resolveAnalyticsDateRangePreset` resolves the three in its half-open form. `next_30_days` stops before `{31_days_from_now}`. `next_month` and `next_quarter` stop before the month and the quarter after next, which no macro token names: the resolver reads `next_month_start` (or `next_quarter_start`) again from the start of the day it named, so the end is still a day the one token resolver computed. No macro token is added. + + In the console, the three presets have labels in all ten locale packs, and the client date-macro resolver now answers `{next_month_end}` and `{next_quarter_end}`, two spec tokens it did not resolve before. `@objectstack/verify`'s dashboard-window proof also writes fixture rows on a forward preset's future edges, so it judges the three new windows; the days it writes for the other presets are unchanged. `@objectstack/lint` changes documentation only. +- f5aaa0b: feat(ui): an action param's `widget` draws the named widget in the action dialog — `widget: 'permission-ref'` on a `text` param is the permission-set picker + + Clause-②: yes (widening) — `@object-ui/core`'s exported `ActionParamDef` gains one optional member, `widget?: string`; `@object-ui/fields`' `RawActionParam` gains the same optional member. No accepted key, value or answer is taken away. + Metadata-protocol: semantic + + `ActionParamSchema.widget` (declared by `@objectstack/spec` with the same key, vocabulary and meaning as `FieldSchema.widget`) was accepted and stored but read by nothing: the action dialog drew every param with its `type` widget. The console now reads it: + + - `resolveActionParams` copies the param's `widget` onto the resolved param. A field-backed param (`{ field }`) that inherits the field's `type` also inherits the field's `widget`; a param that overrides `type` does not, because the field's widget was chosen for the field's value. The param's own `widget` always wins. + - `paramToField` draws the named widget when it is registered, in place of the `type` widget. A name nothing registers keeps the `type` widget (a `textarea` param stays a textarea), and a registered widget on a lookup param without a target is drawn instead of the plain record-id text box. + - So `{ name: 'permissionSet', type: 'text', widget: 'permission-ref' }` opens the permission-set picker: the sets are listed by label beside their machine name, and the machine name is submitted. The server still validates the value by `type`, so the request body is unchanged. + + The spec's liveness ledger marks `params.widget` live, and its `.describe()` no longer says the key is not read yet. + +### Patch Changes + +- 4261169: refactor(core)!: retire forty-two zero-consumer entry exports, and delete the query-AST convenience pair nothing reads + + Clause-②: no (narrowing) + Metadata-protocol: none + + + + **BREAKING** for a host that imports any of these forty-two names from `@object-ui/core`. None had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected. + + Two of them are **gone from the package, not only from the entry**, because nothing inside the package read them either: `buildQueryAST` and the `defaultQueryASTBuilder` instance it wrapped are deleted from `query/query-ast.ts`. + + The other forty stay in their modules for the code and tests that read them and only leave the entry. Every other name the entry exported stays exported unchanged, among them `QueryASTBuilder`, `columnIdentity`, `columnHeader`, `normalizeColumnIdentity`, `toDomProps`, `pickDomProps`, `AFFORDANCE_GRANTS`, `attachObjectSortability`, `readObjectSortability`, `markSwitchedOffActions`, `isActionSwitchedOff`, `attachCascadeDeleteObjects`, `readCascadeDeleteObjects`, `getSortValue`, `compareSortValues`, `bindSubjectPermissions`, `subjectPermissionsOf`, `bindSubjectCapabilities`, `subjectCapabilitiesOf`, `detectNonCanonicalRowSpelling`, `ACTION_DEF_KEYS`, `SPEC_ACTION_KEYS`, `RETIRED_ACTION_KEYS`, `warnOnUnknownActionKeys`, `interpretActionResponse`, `readActionPayload`, `actionCopySource`, `fillTemplateHoles`, `generateThemeVars`, `generateColorVars`, `elementDataSourceBlock`, `ERROR_CODES`, `createError`, `ObjectUIError` with all its subclasses, `debugLog`, `parseDebugFlags`, `CONTEXT_TOKENS`, `resolveFilterPlaceholders`, `buildChartSeries`, `isIntegerAggregate`, `resolveDimensionFieldMeta` and `createFieldColorResolver`. + + What leaves, and what to write instead: + + - FROM `buildQueryAST(query)`, `defaultQueryASTBuilder` TO `new QueryASTBuilder().build(query)`. + - FROM `CANONICAL_COLUMN_IDENTITY_KEY`, `LEGACY_COLUMN_IDENTITY_KEYS`, `CANONICAL_COLUMN_LABEL_KEY` TO `columnIdentity(column)` / `columnHeader(column)`, which read the canonical key and its legacy spellings; FROM `normalizeColumnIdentities(columns)` TO `columns.map(normalizeColumnIdentity)`. + - FROM `isOpenDomAttributeFamily` TO `toDomProps` / `pickDomProps`, which already pass every `data-*` and `aria-*` key. + - FROM `AffordanceGrantRow` TO `AffordanceGrant`, or `(typeof AFFORDANCE_GRANTS)[ConsoleAffordance]` for one row. + - FROM `normalizeObjectSortability`, `OBJECT_SORTABILITY_KEY` TO `attachObjectSortability` / `readObjectSortability`. + - FROM `ACTION_SWITCHED_OFF_KEY` TO `markSwitchedOffActions` / `markSwitchedOffActionsByObject` / `isActionSwitchedOff`. + - FROM `CASCADE_DELETE_OBJECTS_KEY` TO `attachCascadeDeleteObjects` / `readCascadeDeleteObjects`. + - FROM `compareCellValues(a, b, options)` TO `compareSortValues(getSortValue(a, options), getSortValue(b, options))`. + - FROM `SUBJECT_PERMISSIONS`, `SUBJECT_CAPABILITIES` TO `bindSubjectPermissions` / `subjectPermissionsOf` and `bindSubjectCapabilities` / `subjectCapabilitiesOf`. + - FROM `ROW_PREDICATE_CANONICAL_ROOT` TO the literal `'record'`, or `detectNonCanonicalRowSpelling` to check a predicate. + - FROM `NAVIGATION_ALIAS_KEYS`, `HOST_DISPATCH_ACTION_KEYS`, `KNOWN_ACTION_KEYS`, `classifyActionKeys` TO `warnOnUnknownActionKeys`, which classifies an action against the same vocabulary, or `ACTION_DEF_KEYS` / `SPEC_ACTION_KEYS` for the authored key lists. + - FROM `hasDeprecatedObjectParams`, `warnOnDeprecatedObjectParams` TO nothing: `ActionRunner` warns about deprecated object params itself when it runs the action. + - FROM `isLegacyActionEnvelope` TO `interpretActionResponse` / `readActionPayload`, which unwrap the legacy envelope. + - FROM `actionCopyText`, `fillTransitionalCopy` TO `actionCopySource` with `hasTemplateHoles` / `fillTemplateHoles`. + - FROM `toCSSColor`, `generateTypographyVars`, `generateBorderRadiusVars`, `generateShadowVars` TO `generateThemeVars(theme)`, which emits all of those variables, or `generateColorVars` for the colour block alone. + - FROM `isElementDataSourceBlock` TO nothing a host needs: the component registry reads it when `elementDataSourceBlock` marks a renderer. + - FROM `isErrorCode(code)` TO `code in ERROR_CODES`; FROM `formatErrorMessage(error)` TO `error.message` with `ERROR_CODES[error.code]` for the suggestion and doc link. + - FROM `isDebugEnabled` TO `debugLog` / `debugTime`, which are no-ops when debug mode is off. + - FROM `ContextTokenName` TO `(typeof CONTEXT_TOKENS)[number]`. + - FROM `isBuiltinAggregate`, `resolveMeasureLabel` TO `buildChartSeries` with `builtinAggregateLabels`, which labels a built-in aggregate measure the same way; FROM `INTEGER_AGGREGATES` TO `isIntegerAggregate`. + - FROM `resolveDimensionFieldOptions(...)` TO `resolveDimensionFieldMeta(...)`, the same walk, reading each entry's `options`; FROM `resolveRelationshipTarget` TO `resolveDimensionFieldMeta`, which follows the relationship for a dotted path. + - FROM `isColorLiteral`, `buildFieldColorMap`, `FieldColorOption` TO `createFieldColorResolver(fieldDef)`, which builds the colour map and accepts colour literals as-is. +- c2be734: fix(core): a successful `operation: 'update'` action now refreshes the record it wrote, with no `refreshAfter: true` (#29935) + + Clause-②: no + Metadata-protocol: none + + The spec fills in `refreshAfter: false` on an action authored without it, and that parsed shape is what the Console is served. The server-action dispatch (`createServerActionHandler`) refreshed only when `refreshAfter !== false`, so a declared update never refreshed: the route wrote the record and answered 200, the toast reported success, and the record page kept the old values and the action button whose `visible` predicate had turned false. The list the record came from kept it under a filter it no longer matched. + + A declared `operation: 'update'` writes the record it addresses by definition, so its success is now treated as a record change whatever `refreshAfter` says, the same way a client-side `dataSource.update` already reaches every reader. The record-scoped invalidation re-reads the record page (drawer and full page) and refetches every list of that object. `refreshAfter` keeps governing every other server action: a non-update action served with the default `refreshAfter: false` still sends no re-read. The spec default is unchanged. +- ea5807e: fix(console): four copy and display details from a real-user walkthrough: Latin object names spaced in zh form titles, a title on the lookup's create dialog, a success toast that names the action, and money metrics at the currency's decimals + + Clause-②: yes (widening) + Metadata-protocol: none + + - **zh form titles space a Latin object name.** `新建{{object}}`, `编辑{{object}}` and `查看{{object}}` (and the AI chat's record-write card titles) read `新建 Repair Ticket` when the object has no Chinese label, and still `新建联系人` when it has one. `createI18n` applies the spacing to those keys only, through an i18next post-processor; the packs keep the plain `{{object}}` spelling. + - **The lookup's "create the referenced record" dialog has a title.** A `{ objectName, mode: 'create' }` modal descriptor with no `title` now reads `form.createTitle` with the object's label, the title of the console's own New dialog. An authored title still wins. + - **The default success toast names the action.** An action with a `label` and no `successMessage` (and no matching `outcomeMessages` entry) toasts the new pack key `actions.completedNamed` (`Resolve completed`, zh `“解决”已完成`) instead of the generic `Action completed successfully`. An action with no label keeps the generic text. + - **Money metrics keep the currency's decimals.** A dataset metric over a currency measure with no `format`, and a static metric card with a `currency` and no `format`, show the currency's own minor-unit width (CNY `¥1,293.00`, JPY `¥1,293`), the width the list cell shows. Before, the dataset metric printed the shortest form (`¥1,293`, `¥1,280.5`) and the static card rounded to whole units. An authored `format` pattern still states its width. +- d3f23d2: fix(core): the client date-macro resolver answers `{next_week_end}` and `{next_year_end}` + + Clause-②: no + Metadata-protocol: none + + `resolveDateMacros` now resolves `{next_week_end}` (Sunday of next week, Monday-start weeks) and `{next_year_end}` (Dec 31 of next year), the two fixed tokens `@objectstack/spec`'s `DATE_MACRO_TOKENS` declared that the Console passed through as raw text. A test now reads `DATE_MACRO_TOKENS` from the spec and fails on any token the client leaves unresolved, so the two lists cannot drift again. +- ff12aed: fix(core): a compareTo widget on a forward date range (next month, next quarter, next 30 days) compares with the period just before it, not with itself + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/core`: `shiftFilterByCompareTo`** with `kind: 'previousPeriod'` moved only `current_*` / `today` tokens, so a dashboard filtered to `next_month` or `next_quarter` compared the window with itself and the delta always read 0, and `next_30_days` (`{today}` … `{30_days_from_now}`) moved only its start and compared with a window that overlapped it on all but one day. A forward window now steps back one period: `{next_month_*}` becomes this month, `{next_quarter_*}` this quarter, and a `{today}` … `{N_days_from_now}` pair on one field becomes the N + 1 days that end yesterday (`{(N+1)_days_ago}` … `{yesterday}`), the same window the dataset executor computes for that range. Every other token, and the 13 backward presets, compare as before. + - **`@object-ui/core`: `compareToTrendLabelKey`** names that window: `vsThisMonth` for a forward month, `vsThisQuarter` for a forward quarter, and `vsPreviousPeriod` for a forward rolling window (it read `vsLastMonth`, `vsLastQuarter` and `vsYesterday`). + - **`@object-ui/i18n`:** `dashboard.trend.vsThisMonth` and `dashboard.trend.vsThisQuarter` in all ten locale packs (zh: 较本月, 较本季度). + - **`@object-ui/plugin-dashboard`, `@object-ui/plugin-charts`:** English fallbacks for the two keys in the metric tile and in the chart's comparison series label. +- bff5ba2: fix(ui): the record Edit form judges a field's `requiredWhen` as the engine does, a transition gate against the stored record, so a record already inside the gate without the value saves an unrelated edit + + Clause-②: yes (widening) + Metadata-protocol: none + + This widens the public surface of `@object-ui/core`: the new export `requiredWhenRestsOnStoredRecord` and the new `requiredWhenAtRest` field in the return type of `resolveFieldRuleState`. + + - **`@object-ui/components` form renderer:** on an Edit form (`previousValues` set), a field's `requiredWhen` is required only when the stored record complied with it. A stored record that already violates it (the predicate holds over the stored record and its value is missing, as with a contract activated before its signed-copy rule existed) is not blocked any more: it saves an edit that leaves the field empty, as the engine and REST `PATCH` already accept. This follows `Field.requiredWhen`: the write is refused only when the merged record violates the requirement AND the pre-write record complied. Still refused: the edit that moves a record into the gate without the value, and clearing the value of a record that held it. A Create form enforces as before. The "Required to run" note under an empty field (the update-action heads-up) is no longer shown for such a stored record, because the engine judges the action's write against the same stored record. + - **`@object-ui/core`:** `resolveFieldRuleState` takes `statics.name` (the field's name) and returns `requiredWhenAtRest`. With `previous` and `statics.name` given, a stored record already violating `requiredWhen` resolves `required: false` and `requiredWhenAtRest: true`. A caller that passes no `name` keeps the plain verdict. New export `requiredWhenRestsOnStoredRecord(pred, name, stored, scope?)`, the stored-record check alone. A predicate that cannot be evaluated over the stored record counts it as compliant, as on the server. +- 31260ca: fix(core): a compareTo widget on an already-past date range (yesterday, last week / month / quarter / year, last N days) compares with the period before it, and previousYear from 29 February stays in February + + Clause-②: no + Metadata-protocol: none + + - **`shiftFilterByCompareTo`, `kind: 'previousPeriod'`.** `{yesterday}` and the `last_*` tokens were never moved, so a dashboard filtered to `yesterday`, `last_week`, `last_month`, `last_quarter` or `last_year` compared the window with itself and the delta always read 0. They now compare with the period before them: `{yesterday}` with `{2_days_ago}`, and `last_week_*` … `last_year_*` with the week, month, quarter or year before last. The macro vocabulary has no token for "two periods back", so those windows are computed: the `last_*` start is resolved in the display zone, moved back one period, and the end is the day before it. + - **Rolling windows.** `{N_days_ago}` … `{today}` (`last_7_days`, `last_30_days`, `last_90_days`; N + 1 days) moved only its end and compared with the N days ending yesterday, which overlap it. It now compares with the N + 1 days before it, `{(2N+1)_days_ago}` … `{(N+1)_days_ago}`, the same window the dataset executor computes. + - **`kind: 'previousYear'` from 29 February** re-resolved the filter at 1 March of the year before (`new Date(2027, 1, 29)`), so `this_month` compared with March. The day is now clamped to the month's last day, 28 February. + - **`compareToTrendLabelKey`** reads all of these as `vsPreviousPeriod` (they read `vsLastWeek`, `vsLastMonth`, `vsLastQuarter`, `vsLastYear` and `vsYesterday`); no `dashboard.trend.*` key names "the month before last". `today`, the `this_*` and the forward presets compare and label as before. +- a91409c: fix(core,react,i18n,components,app-shell,console): a write refused because the object's external datasource does not accept writes says the table is read-only, not "you don't have permission" + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@object-ui/core`: `isExternalWriteRefusal(err)`**, re-exported from `@object-ui/react` beside the other write-refusal readers. True when the server refused a write with `EXTERNAL_WRITE_FORBIDDEN`, the ADR-0015 write gate's code for an object bound to an external datasource that does not admit writes (`datasource.external.allowWrites` and `object.external.writable` must both be true). Read from `code`, then `details.code`, like `isPermissionError`. The refusal is a 403, so `isPermissionError` answers true for it too: a caller asks this first. + - **`@object-ui/i18n`: `form.readOnlyExternalSource`**, in all ten packs: "This table is read-only. Its records come from an external data source that does not accept changes." + - **Every surface that met this refusal now says the table is read-only:** the console record form page and the record dialog (`@object-ui/components` form), which said "you don't have permission to save this record"; the record page delete, the list row delete and the record page inline edit, which showed the gate's raw diagnostic; and the calendar's drag-to-reschedule and the record path's stage change. A permission line sent an administrator through permission sets for a grant that cannot exist. +- ad2393f: fix(plugin-detail,app-shell): the related list, the reference rail and the packaged-actions table name a renamed object by the name the server serves + + Clause-②: yes (widening) + Metadata-protocol: none + + - **The surfaces.** A `record:related_list` block and a `record:reference_rail` card with no authored `title` are headed with the related object's served plural label, else its served label, as the server translated it for the session (a `.custom` extension's rename included). They used to ask only the client translation bundle, which still held the base package's name. The bundle is asked only when the server serves neither. + - **Packaged actions.** Setup → Packaged Actions reads the object list in the session's language (`Accept-Language`), not the browser's, and its Object column draws that served label as given. + - **The widening.** The served-object-label rule now lives in `@object-ui/core`, which `@object-ui/plugin-detail` can import: new exports `servedObjectLabel`, `servedObjectPluralLabel`, `metadataText` and the types `ObjectLabelLookup` and `ObjectPluralLabelLookup`. app-shell's internal copy is now a re-export of these, so both packages read one rule. +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [67d5d53] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [0e4747a] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/formula@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/core/package.json b/ui/packages/core/package.json index 5e18cb8cf92..f433ed780a3 100644 --- a/ui/packages/core/package.json +++ b/ui/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/core", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "sideEffects": false, "license": "MIT", diff --git a/ui/packages/create-plugin/CHANGELOG.md b/ui/packages/create-plugin/CHANGELOG.md index 5aad5df7eaf..a851ba1d4c7 100644 --- a/ui/packages/create-plugin/CHANGELOG.md +++ b/ui/packages/create-plugin/CHANGELOG.md @@ -1,5 +1,9 @@ # @object-ui/create-plugin +## 18.4.0 + +No changes in this release. + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/create-plugin/package.json b/ui/packages/create-plugin/package.json index da14617d7ee..185d281d65e 100644 --- a/ui/packages/create-plugin/package.json +++ b/ui/packages/create-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/create-plugin", - "version": "18.3.0", + "version": "18.4.0", "description": "CLI tool to scaffold ObjectUI plugins", "type": "module", "license": "MIT", diff --git a/ui/packages/data-objectstack/CHANGELOG.md b/ui/packages/data-objectstack/CHANGELOG.md index 216ea98c567..582bb9f3555 100644 --- a/ui/packages/data-objectstack/CHANGELOG.md +++ b/ui/packages/data-objectstack/CHANGELOG.md @@ -1,5 +1,123 @@ # @object-ui/data-objectstack +## 18.4.0 + +### Patch Changes + +- 7a5f73e: refactor(fields, data-objectstack)!: `UploadProvider` moves into `@object-ui/fields`, the ObjectStack upload adapter into `@object-ui/data-objectstack`, and `@object-ui/providers` is retired + + Clause-②: yes (narrowing) — `@object-ui/fields` gains `UploadProvider`, `useUpload`, `UploadProviderProps` and the three upload contract types, and `@object-ui/data-objectstack` gains `createObjectStackUploadAdapter` and `ObjectStackUploadAdapterOptions`; the whole `@object-ui/providers` package is retired, which takes `createS3Adapter`, `createAzureBlobAdapter`, their option types and the exported `createObjectUrlAdapter` with it. + Metadata-protocol: none + + + + **BREAKING.** `@object-ui/providers` is retired: `18.3.0` is its last published version, and it gets no further release. Its one remaining job was the upload context the file, image and avatar widgets of `@object-ui/fields` upload through, so that context now lives in `@object-ui/fields` itself, and the adapter that talks to an ObjectStack server's storage service lives in `@object-ui/data-objectstack`, beside the other fetch logic against that server. The upload contract the two share is `@object-ui/types`'. + + What leaves, and what to do: + + - FROM `@object-ui/providers` TO `@object-ui/fields` and `@object-ui/data-objectstack`: import `UploadProvider`, `useUpload` and the `UploadProviderProps`, `UploadAdapter`, `UploadOptions` and `UploadResult` types from `@object-ui/fields` (the three contract types also from `@object-ui/types`), and `createObjectStackUploadAdapter` with `ObjectStackUploadAdapterOptions` from `@object-ui/data-objectstack`, then drop the `@object-ui/providers` dependency. `createS3Adapter`, `createAzureBlobAdapter`, `S3AdapterOptions` and `AzureBlobAdapterOptions` had no consumer and leave with no replacement: a host that uploads to its own bucket writes an object with a `name` and an `upload(file, options)` method against the `UploadAdapter` type of `@object-ui/types` and hands it to `UploadProvider`. `createObjectUrlAdapter` is no longer exported: it stays the local `blob:` adapter `useUpload` falls back to when no `UploadProvider` is mounted. + + What does not change: `UploadProvider`, `useUpload` and `createObjectStackUploadAdapter` keep their signatures and behaviour, the presigned upload request is the same, and the widgets read the same context. +- 83fc096: Clause-②: no + Metadata-protocol: none + + A `ConnectionError` for a request nothing answered (an unreachable host, a rejected `fetch`) no longer reads as a server that answered with a 503 (#29916). + + - The `ConnectionError` constructor leaves `statusCode` undefined when the caller passes none. Before, it stamped `503`. `connect()` builds it that way when discovery's `fetch` rejects, every data method opens with `await this.connect()`, and the post-connect `error` connection state builds it the same way, so a read against a down backend was taken for the server's own answer. `answeredLoadFailure` now returns `null` for it and `classifyLoadError` reads `network`, so a failed-read state keeps its "check your connection" sentence. + - A discovery read the server answered with an error status (`!res.ok`) now carries that status (a 401 reads as `unauthorized`), instead of the invented 503. + - An answered 503 or 504 through `createErrorFromResponse` keeps its status, as before. +- 6f095b9: feat(studio): customize a read-only package's dataset in the All metadata area, see what uses it before publishing, and read a refused publish at the dataset + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Customize a dataset.** On a read-only base package, Studio's All metadata area opens a dataset in its form editor with the dataset designer and live preview, and offers Customize. The first saved edit copies the whole dataset into the base's paired `.custom` package, declaring `overrides: { package, basedOn }`; the header's Publish applies it, and "Restore package original" on the override mark deletes the copy. On a writable package a dataset still opens the generic item page. + - **What uses it.** While a dataset is customized, the editor lists the dashboards, reports and other items that name it, read from the References read, so the person sees what a published change reaches. A failed read says it failed and offers to read again; it never reads as "nothing uses it". + - **A refused publish at the item.** A publish the server refuses for a dataset is placed on its measure or dimension and the designer's input (`Not published. Check Aggregate of the measure "Total Amount"`), in the Console's language, and shown at the dataset's editor as well as on its Pending changes entry. The server's own text stays under Details. + - **References read fixed.** `MetadataClient.references()` now reads the route's declared answer (`{ references: [{ type, name, label, path, kind }] }`) and maps each element to `MetadataReference` (`fromType`, `fromName`, new optional `fromLabel` and `kind`). It used to read a key the route never serves, so Setup's References tab and Studio's item tools showed "Nothing in the metadata graph points at this item. Safe to delete." for every item. An answer the declaration refuses is now a failed read. + - **Published surface.** `MetadataReference`, exported from `@object-ui/data-objectstack`, gains two optional fields, `fromLabel` (the referring item's label, when the server serves one) and `kind` (the served reference kind); existing fields are unchanged. `MetadataClient.references()` now accepts less: a `200` answer that `FindReferencesToMetaResponseSchema` refuses throws instead of resolving `[]`, so a caller must handle the rejection as it already does for a non-2xx answer; in this repository every caller reads it through `useReferencesScan`, which catches it and shows the failed-check state. +- 55d9dd4: feat(spec,service-analytics,data-objectstack): a dataset answer says, per measure, which records that measure counted (`drillScopes`) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/spec`: `AnalyticsResult.drillScopes?: Record`**, also declared on the `AnalyticsResultResponse` wire schema. Each selected measure name maps to the list filter, on the answer's `object`, that selects the records the measure aggregates: the dataset's own `filter`, AND the measure's own `filter`, AND, for a measure over a field, that field holding a value (both instants for a duration measure), with each `{token}` resolved as the query resolved it. It is `{}` when none of these applies. A measure that no list filter can select is left out: a derived measure, or a scope that names a relationship path (`account.region`, as a key or a `$field` reference). A measure left out offers no drill. A `__compare` column has no entry. + - **`@objectstack/service-analytics`: `queryDataset` sets `drillScopes` on every dataset answer that has rows**, on both query faces, from the same token-resolved filters the numbers were computed under. A drill that lists the clicked bucket (`dimensionFields` / `drillRawRows` / `drillRawTotals`, `drillRanges`) AND the widget's own filter AND this scope lists exactly the records the number counted. Before this, a drill built from the bucket and the widget filter alone listed every record in the bucket, so a filtered measure such as "paid invoices" showed 4 records behind the number 2. + - **`@object-ui/data-objectstack`: `queryDataset` passes `drillScopes` through**, typed as the spec slot. The adapter lists the keys it returns, so until now the sidecar was dropped before it reached a widget. +- Updated dependencies [4261169] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [ea5807e] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [a91409c] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [ad2393f] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/core@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/client@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/data-objectstack/package.json b/ui/packages/data-objectstack/package.json index 6da7a12cc75..38faa54cbc2 100644 --- a/ui/packages/data-objectstack/package.json +++ b/ui/packages/data-objectstack/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/data-objectstack", - "version": "18.3.0", + "version": "18.4.0", "description": "ObjectStack Data Adapter for ObjectUI", "license": "MIT", "type": "module", diff --git a/ui/packages/fields/CHANGELOG.md b/ui/packages/fields/CHANGELOG.md index 5a434cd690e..9ed2234436e 100644 --- a/ui/packages/fields/CHANGELOG.md +++ b/ui/packages/fields/CHANGELOG.md @@ -1,5 +1,192 @@ # @object-ui/fields +## 18.4.0 + +### Minor Changes + +- f5aaa0b: feat(ui): an action param's `widget` draws the named widget in the action dialog — `widget: 'permission-ref'` on a `text` param is the permission-set picker + + Clause-②: yes (widening) — `@object-ui/core`'s exported `ActionParamDef` gains one optional member, `widget?: string`; `@object-ui/fields`' `RawActionParam` gains the same optional member. No accepted key, value or answer is taken away. + Metadata-protocol: semantic + + `ActionParamSchema.widget` (declared by `@objectstack/spec` with the same key, vocabulary and meaning as `FieldSchema.widget`) was accepted and stored but read by nothing: the action dialog drew every param with its `type` widget. The console now reads it: + + - `resolveActionParams` copies the param's `widget` onto the resolved param. A field-backed param (`{ field }`) that inherits the field's `type` also inherits the field's `widget`; a param that overrides `type` does not, because the field's widget was chosen for the field's value. The param's own `widget` always wins. + - `paramToField` draws the named widget when it is registered, in place of the `type` widget. A name nothing registers keeps the `type` widget (a `textarea` param stays a textarea), and a registered widget on a lookup param without a target is drawn instead of the plain record-id text box. + - So `{ name: 'permissionSet', type: 'text', widget: 'permission-ref' }` opens the permission-set picker: the sets are listed by label beside their machine name, and the machine name is submitted. The server still validates the value by `type`, so the request body is unchanged. + + The spec's liveness ledger marks `params.widget` live, and its `.describe()` no longer says the key is not read yet. + +### Patch Changes + +- 8fefefd: refactor(fields)!: retire six zero-consumer exports (four file-value helpers, the filter-condition extra operators, the grid column sum) + + Clause-②: no (narrowing) + Metadata-protocol: none + + + + **BREAKING** for a host that imports any of these six names from `@object-ui/fields`. None had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected. Every module stays in the package: `FileField`, `ImageField` and `AvatarField` keep reading the file-value helpers, `FilterConditionField` keeps offering its extra operators, `fileUrlFromId` keeps building on the storage base path and `GridField` keeps summing its total column. Only the entry stops publishing them. `fileIdOf`, `fileUrlFromId`, `readFileValue`, `readFileValues`, `fileValueForSubmit`, `isFileIdToken`, `UploadIncompleteError`, the `FileValueView` and `UploadResultLike` types, `condToMongo`, `kvToCondition`, `isMatchAllCriteria`, `FilterConditionField`, `evalArith`, `lookupAutofillPatch`, `computeRow`, `GridField`, `LineItemsField` and the `GridColumn` type stay exported unchanged. + + What leaves, and what to write instead: + + - FROM `isImageValue` TO `readFileValue(value)` and a check on its `mimeType`. + - FROM `uploadResultView`, `withRecentUploads` TO `FileField` / `ImageField` / `AvatarField`, which show a just-uploaded file themselves. + - FROM `FILE_STORAGE_BASE_PATH` TO `fileUrlFromId`, which builds the download URL for a file id on that path. + - FROM `FILTER_CONDITION_EXTRA_OPERATORS` TO `FilterConditionField`, which offers those operators itself. + - FROM `sumColumn` TO `GridField`, which draws the total of its declared total column, or a local reduce over the rows. +- 7a5f73e: refactor(fields, data-objectstack)!: `UploadProvider` moves into `@object-ui/fields`, the ObjectStack upload adapter into `@object-ui/data-objectstack`, and `@object-ui/providers` is retired + + Clause-②: yes (narrowing) — `@object-ui/fields` gains `UploadProvider`, `useUpload`, `UploadProviderProps` and the three upload contract types, and `@object-ui/data-objectstack` gains `createObjectStackUploadAdapter` and `ObjectStackUploadAdapterOptions`; the whole `@object-ui/providers` package is retired, which takes `createS3Adapter`, `createAzureBlobAdapter`, their option types and the exported `createObjectUrlAdapter` with it. + Metadata-protocol: none + + + + **BREAKING.** `@object-ui/providers` is retired: `18.3.0` is its last published version, and it gets no further release. Its one remaining job was the upload context the file, image and avatar widgets of `@object-ui/fields` upload through, so that context now lives in `@object-ui/fields` itself, and the adapter that talks to an ObjectStack server's storage service lives in `@object-ui/data-objectstack`, beside the other fetch logic against that server. The upload contract the two share is `@object-ui/types`'. + + What leaves, and what to do: + + - FROM `@object-ui/providers` TO `@object-ui/fields` and `@object-ui/data-objectstack`: import `UploadProvider`, `useUpload` and the `UploadProviderProps`, `UploadAdapter`, `UploadOptions` and `UploadResult` types from `@object-ui/fields` (the three contract types also from `@object-ui/types`), and `createObjectStackUploadAdapter` with `ObjectStackUploadAdapterOptions` from `@object-ui/data-objectstack`, then drop the `@object-ui/providers` dependency. `createS3Adapter`, `createAzureBlobAdapter`, `S3AdapterOptions` and `AzureBlobAdapterOptions` had no consumer and leave with no replacement: a host that uploads to its own bucket writes an object with a `name` and an `upload(file, options)` method against the `UploadAdapter` type of `@object-ui/types` and hands it to `UploadProvider`. `createObjectUrlAdapter` is no longer exported: it stays the local `blob:` adapter `useUpload` falls back to when no `UploadProvider` is mounted. + + What does not change: `UploadProvider`, `useUpload` and `createObjectStackUploadAdapter` keep their signatures and behaviour, the presigned upload request is the same, and the widgets read the same context. +- 5174603: fix(ui): Setup record pages name a sharing rule's recipient and read its condition in words, and the audit list names a sign-in row's session by its user + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/plugin-detail`.** The record page's highlights strip and details body hand each read face the record as `dependentValues`, as a list grid hands its row. A `recipient-picker` value is an id that means something only beside its `recipient_type`, so a sharing rule's page read the stored user id under 接收方 where its list read the user's name; it now reads the name. + - **`@object-ui/fields`.** `widget: 'filter-condition'` has a read face of its own: a list or detail cell draws the criteria through the widget's read-only display, which says it in the object's field and option labels ("Priority equals Urgent"; an OR group joined by its word, a boolean as a word). A criteria the builder cannot represent still reads as the stored JSON, and an empty one is the empty cell. A read-only widget no longer waits for its `object_name` to say anything: without it, the fields keep their api names. + - **`@object-ui/plugin-grid`.** A pointer cell (`referenceVia`) reads the target record with its name field expanded when that field is a reference the viewer may read, so a record named by a lookup reads as the referenced record's name: the audit list's sign-in rows name the session by its user instead of printing the user id. Any other name field is read as before, with no `$expand`. +- f6faf75: fix(console): starting an impersonation lands on home, and a person's permission-set picker no longer offers the MCP agent sets + + Clause-②: no + Metadata-protocol: none + + - **Impersonation landing.** An administrator who starts an impersonation from a Setup page used to stay on that page, which the impersonated user usually cannot open, so the console showed "You don't have access to this app". The impersonation banner now navigates to `/` the moment a session the console held as an ordinary one becomes impersonated, and the console resolves the landing for the user it now holds. A console that boots into an impersonated session (a reload, a new tab, a deep link) stays on the URL it was opened at, and ending an impersonation does not move the page. The navigation goes through the host navigate the console supplies, so a deployment's basename applies. + - **Agent sets in the person picker.** The `permission-ref` field (the permission set on a person's grant, `sys_user_permission_set.permission_set`) no longer offers the three MCP agent ceiling sets (`mcp_agent_data_read`, `mcp_agent_data_write`, `mcp_agent_restricted`, the spec's `MCP_AGENT_PERMISSION_SETS`). Those are an AI assistant's own grants, which the runtime puts on the agent principal directly. A grant that already names one still shows it as the row's value, by its label beside its name. `position-ref` is unchanged. +- 8b0fbb8: fix(app-shell,fields,plugin-form,plugin-gantt,plugin-grid,components,console): record pages, forms, drawers and preview cards draw a served object label as served + + Clause-②: no + Metadata-protocol: none + + - **Now.** The record page (its title, breadcrumbs, related-list titles, delete and access-denied copy), the record form page and the console's create/edit dialog (title, save toast, way back to the list), the record drawer, the record preview card, the hover card over a lookup, a record-pointer cell, an object-ref cell, the closed-form notice, the gantt name column, the page header's record chrome, a form view's heading, the AI dock's surface chip, the Activity row and the report index name an object by the label the server served, as given. The server has already translated it, by its label order for an overridden item, so a `.custom` object extension's rename is kept over the base package's translation. Before, the served label reached the client's catalog lookup only as its fallback, so the base package's translation of the old name won on these surfaces after the list and header were fixed. + - **Unchanged.** When no label is served, the catalog key is asked first and the machine name (or its humanized form) stays the last resort, as before. An unedited object shows the same text as before, because the server serves the catalog's translation for it. Related lists and reference rails that only know an object's name, and the packaged-actions table (its object list is read without the session's language), still look the name up in the catalog. English is unchanged. +- Updated dependencies [4261169] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [a4c3e73] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [1e6f542] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [b6caa14] +- Updated dependencies [25fea77] +- Updated dependencies [ea5807e] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [2046d21] +- Updated dependencies [fa9c0aa] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [7b10a30] +- Updated dependencies [8b0fbb8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [ad2393f] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [062236c] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [47c457b] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/core@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @object-ui/mobile@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/fields/package.json b/ui/packages/fields/package.json index 2b094ba6942..8b65d1a9406 100644 --- a/ui/packages/fields/package.json +++ b/ui/packages/fields/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/fields", - "version": "18.3.0", + "version": "18.4.0", "description": "Field renderers and registry for ObjectUI", "license": "MIT", "type": "module", diff --git a/ui/packages/i18n/CHANGELOG.md b/ui/packages/i18n/CHANGELOG.md index 57674ebe0bf..2f795db93d6 100644 --- a/ui/packages/i18n/CHANGELOG.md +++ b/ui/packages/i18n/CHANGELOG.md @@ -1,5 +1,377 @@ # @object-ui/i18n +## 18.4.0 + +### Minor Changes + +- aa57c76: feat(spec): three forward date-range presets: `next_month`, `next_quarter` and `next_30_days` + + Clause-②: yes (widening) + Metadata-protocol: added + + The relative dates a dashboard date filter and the filter builder offer looked only backward. A forecast asks forward: what closes next month, next quarter, in the next 30 days. Those questions had to use a custom range, whose fixed dates go stale. + + `DATE_RANGE_PRESETS` now also declares `next_month` (the whole of next month), `next_quarter` (the whole of next quarter) and `next_30_days` (today and the 30 days after it, the mirror of `last_30_days`). Every place that accepts a preset accepts them: `dashboard.dateRange.defaultRange`, a date global filter's `defaultValue`, the filter builder's `between` row and `AnalyticsQuery.timeDimensions[].dateRange`. They sit beside the family they extend in the filter bar: `next_month` after `last_month`, `next_quarter` after `last_quarter`, `next_30_days` after `last_90_days`. Their `DATE_RANGE_PRESET_MACRO_WINDOWS` rows follow the inclusive last-day convention: `['{next_month_start}', '{next_month_end}']`, `['{next_quarter_start}', '{next_quarter_end}']` and `['{today}', '{30_days_from_now}']`. A preset name written as a bare filter comparand is refused with that window as the prescription, as for the other presets. + + `@objectstack/core`'s `resolveAnalyticsDateRangePreset` resolves the three in its half-open form. `next_30_days` stops before `{31_days_from_now}`. `next_month` and `next_quarter` stop before the month and the quarter after next, which no macro token names: the resolver reads `next_month_start` (or `next_quarter_start`) again from the start of the day it named, so the end is still a day the one token resolver computed. No macro token is added. + + In the console, the three presets have labels in all ten locale packs, and the client date-macro resolver now answers `{next_month_end}` and `{next_quarter_end}`, two spec tokens it did not resolve before. `@objectstack/verify`'s dashboard-window proof also writes fixture rows on a forward preset's future edges, so it judges the three new windows; the days it writes for the other presets are unchanged. `@objectstack/lint` changes documentation only. + +### Patch Changes + +- 1e6f542: feat(studio): a contributed nav entry shows the label the live app shows, and a package that only contributes navigation says where it goes + + Clause-②: yes (widening) + Metadata-protocol: none + + - **The label.** A navigation entry another package contributes to a base's app now shows the label the live app shows, with its id as a muted second line under it. Before, the row showed only the id. The label is looked up by `(app, id)` in the app row the Interfaces pillar already reads (`GET /meta/app?package=`), which serves the folded navigation: the served entries the app's published layer does not hold. No request is added and nothing is read from the contributor's manifest. An id the base also holds, or one two contributions share, keeps the bare id, so no label is guessed. A label-less entry reads what it inherits, through the rail's own resolver. A never-published app has no served row, so its contributed rows keep the id. + - **The contributor's empty state.** A package with no app of its own whose listed manifest declares `navigationContributions` now says what it adds: "This package has no app of its own. It adds 2 navigation entries to HotCRM's app.", with a button that opens that package. The declared app is an address only. Its owner comes from the served app read (`GET /meta/app/:app`), and the count comes from the owner's served customization read (`GET /packages/:owner/customizations`), counting the `nav` contributions stamped with this package for that app. That is at most two reads per declared target app, sent only on that empty state. When the package is disabled, the target app is missing, either read fails, or the count is 0, the text stays "This package has no app yet." + - **New strings.** `@object-ui/i18n`'s studio namespace gains five keys in `en` and `zh` (`engine.studio.if.noOwnApp`, `.noOwnAppTitle`, `.contributesOne`, `.contributesOther`, `.openOwner`). +- b379262: fix(app-shell): the metadata editor's save-status chip says "Not saved" while a save is held, instead of promising an auto-save that never comes (#29901) + + Clause-②: no + Metadata-protocol: none + + On the metadata editor page (Studio's All metadata item page, Setup's metadata pages) with auto-save on, a draft the server refused with `422 INVALID_METADATA` stays dirty, and the chip beside Save kept reading "Auto-saving…" although the auto-save is held until the refused part changes and no further save is sent. The same happened while the inspector holds a fault it reports (a CEL predicate that does not parse), which holds the auto-save the same way. + + The chip now has a held state: while either hold stands on a dirty draft it reads "Not saved — fix the errors to save" in the destructive tone, with the Save button's own explanation as its tooltip. It returns to the usual states as soon as the hold lifts: editing the part the server named, or fixing the inspector fault, lets the auto-save run and the chip reads "Auto-saving…", then "Saved". Nothing about when a save is sent or held changes. New string: `engine.edit.notSavedHeld` (en, zh). +- 7945a3b: fix(app-shell): the Studio publish sheet names a batch of hooks or translations in words ("3 hooks", zh 「钩子 3」) instead of the raw type code ("3 hook") (#29902) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@object-ui/i18n`**: two new count families in every pack, `preview.changes.typeCount.hook` and `preview.changes.typeCount.translation`, with each CLDR slot the pack's language selects. + - **`@object-ui/app-shell`**: the Pending changes / publish sheet's summary line and group headings read those families for `hook` (authored in an object's Hooks tab) and `translation` (authored in Studio's "All metadata" form editor). A type with no family still shows its raw name. +- 9049be2: fix(plugin-detail): a record page's body table that is wider than the details column says so — its first column stays pinned and ⤢ is offered as 「All columns」 + + Clause-②: no + Metadata-protocol: none + + A body table of the default record page (a related list drawn in the details column) keeps its place in that column, as ADR-0085's role-based layout puts it. When its columns are wider than the column, it now makes the overflow obvious: + + - **The first column stays pinned** while the table scrolls sideways, so a scrolled row is still identifiable (the table's existing `frozenColumns: 1`, set only while the table overflows). + - **⤢ is offered in words.** The enlarge control in the card header, icon-only before, shows a visible 「All columns」 label (`detail.relatedCardAllColumns`, all ten locales) while the table overflows. It opens the same enlarged tab as before, where every column fits. + - **The right-edge shadow** while columns remain to the right is the table's own and unchanged; it clears when the table is scrolled to its end. + + A body table that fits shows none of these: no pinned column and the icon-only ⤢. + + `@object-ui/components`: a `data-table` header label carries its full text as a `title`, so a header the ellipsis cuts can be read on hover, and the table's scroll box carries a `data-table-scroll-box` marker a host can read the overflow from. +- f0ad1f7: fix(app-shell): a flow bound to an object its package may not use is explained on the Problems list in the Console's language + + Clause-②: no + Metadata-protocol: none + + When a package's flow triggers on, scans, or declares a start input of an object outside what the package may use (its own objects, the objects of the packages it declares as dependencies, and system objects), the publish refuses it, and the flow's Problems list showed the server's English sentence in packaging terms to a reader in any language. The Problems list now words that refusal from the served entry's rule id (`flow-trigger-unknown-object`, `flow-input-unknown-object`) and its path: it names the object the flow holds there, says which objects a package can use, and points at the two fixes, picking an object from the list or declaring a dependency on the package that defines it. The copy lives in the Studio string namespace in English and Chinese. Any other server entry, and any of these at a position the Console does not word, still shows the server's sentence as sent. The served shape does not change: the editor pages now keep each served error's `code` on the entries they hand the canvas, where it used to be dropped. +- 03be565: fix(app-shell): New automation no longer saves a typed prefix as the trigger object (#30038) + + Clause-②: no + Metadata-protocol: none + + In Studio's New automation dialog, typing part of an object name and pressing Enter committed the typed text itself, so the flow was created bound to an object that does not exist. The trigger-object picker there now takes objects only: Enter or leaving the field commits the object the text names exactly, else the only match left in the list, else no object, and the picker says no object is chosen. A record preset then keeps the dialog open until an object is picked. + + Every other object picker keeps its free text, because a flow reference may hold an expression. +- 7e9fa4b: fix(app-shell): Studio on a zh-CN console reads in Chinese in the view, page and report panels, the publish sheet, the flow Problems tab and the save-notice / notify nodes, and calls a dashboard 仪表盘 everywhere + + Clause-②: no + Metadata-protocol: none + + The view, page and report property panels render the spec's authoring forms from the console's own copy of the spec, and only the dashboard form had a zh-CN overlay, so section headings such as Basics, Columns & filters and Navigation & sharing, their descriptions and schema-titled rows such as Searchable Fields read in English. The overlay now carries every `METADATA_FORM_REGISTRY` type, worded as the platform's own zh-CN metadata-forms bundle words them: view, page and report in full, the other types by field label, which is what the Pending changes sheet names a changed property by (a flow's `~ Status` now reads `~ 状态`). The sheet's summary line counts datasets as 数据集 instead of the raw type name. + + A flow whose trigger is not armed on this deployment showed the engine's developer sentence under 未在此运行, above 没有问题 —— 该流程结构有效. The note now says, from the engine's `unboundCause` code, what this deployment does not do and who can change it, keeps the engine's sentence as the hover detail, and the line below it says the structure checks found nothing to fix instead of "no problems". A cause the console has no row for still shows the engine's sentence. + + The palette names the save-notice node 保存提示, and the notify node's Template and Template data fields and the save-notice Message field read in Chinese. English consoles read the same English as before. zh-CN Console and Studio text uses 仪表盘 for dashboard throughout. +- bc8945f: feat(app-shell): Studio Interfaces opens a list view from 不在导航中 on its object's page; the 视图 group lists only what no header dropdown reaches, and names a form view and its own type + + Clause-②: yes (widening) + Metadata-protocol: none + + - **A list view opens on its object's page from every door.** A list view picked under 不在导航中 → 视图, and a link to the view itself (`?surface=view:OBJECT.KEY`, the All metadata door), now land on the object's page with its header dropdown (object / view ▾), on that view: the object's nav entry when it has one, else the object itself, with no entry (`?surface=object:OBJECT`, which a reload or a shared link opens again, `?view=` included). The panel there now also carries what only the view's own page offered: its conditional formatting (条件格式化), whose CEL verdicts hold the view's autosave as they held the view page's Save, and the sections of the spec's authoring form for a view, without the keys the panel already edits. A form view, and a view a read-only base's pair adds, still open as themselves. + - **The 视图 group lists what the dropdowns do not reach.** A list view whose object has a nav entry is no longer listed there, a view only drafted in the package included; the list views of an object with no entry stay listed. + - **Form views read as forms.** A form view is listed by its label, else as the object's name and the word for a form ("Lead form", 「线索表单」), never its item name (`crm_lead.form`); the group's heading says what it holds (表单视图, 视图, or 表单视图与列表视图). In the view inspector, a form view's 视图类型 offers the form layouts of the spec's `FormViewSchema.type` (simple, tabbed, wizard) and reads its own, where it read 「simple(未找到)」; a list view's picker is unchanged. + - **`@object-ui/i18n`: three Studio keys,** `engine.studio.if.offNav.viewForms`, `engine.studio.if.offNav.viewMixed` and `engine.studio.if.offNav.formName`, in en and zh. +- 42afac7: feat(app-shell): Studio Interfaces picks an object's list view from the header title, object / view ▾, the way the running list does, and the open view stays in the address + + Clause-②: no + Metadata-protocol: none + + - **The view switcher is in the header's title.** An `object` entry's header reads object / view ▾. The switcher is the running list's own dropdown (`ObjectViewTitleSwitcher`): the object's list views by label, a view only drafted in this package included, a search box, and New view, which opens the Create View dialog. Studio does not offer the reader's own features there: no ☆, no 📌, no recent views, no Save as my view. It shows when the object has one list view too, so New view is always one click away. A read-only package switches views and offers no New view. + - **The Properties panel no longer holds a view picker, Add view or "Back to …".** It keeps the open view's settings, and its title is the view's name as the switcher gives it (the label the object's served `listViews` carry). A view with no label is named "Default view" or by its key; the header names the object before it. + - **The open view is in the address.** Switching writes `?view=.` beside `?surface=` and `?nav=`; the entry's own view writes none. A reload or a shared link opens that view again, a view only drafted in this package included. A link naming a view of another object is not taken. A pending edit is still sent before a switch, and an edit that cannot be sent still asks first. + - **`ObjectViewTitleSwitcher`: the pin is optional.** `onPinnedViewChange`, `pinnedViewId` and `recentViewIds` may be left out; without `onPinnedViewChange` no row draws 📌 and no Pinned section is listed. The running list passes all three, so it is unchanged. + - **`@object-ui/i18n` (studio namespace):** adds `engine.studio.inspector.listView.defaultView`; removes `defaultTitle`, `namedTitle`, `addView`, `backToEntry`, `switchView` and `switchViewDraft`, which only the removed panel controls read. +- a9fb14b: fix(app-shell): the Studio form designer lays out the form the app opens, so an object with a default form view no longer edits a layout users never see + + Clause-②: no + Metadata-protocol: none + + - **Which layout.** Studio → Data → an object → Form asks the app's own resolver which form view New and Edit open (`selectRecordFormView`, over the shell's merged objects). With none, the designer edits the object's `fieldGroups`, as before. With one (an `isDefault` form view, or an `OBJECT.create` view for New), the designer shows that view's sections and every layout edit (moving fields, adding, renaming, moving and deleting sections) is saved to the view's draft, which the package publish publishes. A chip in the toolbar names the layout being edited ("Form view: OBJECT.form" or "Field groups"). + - **New and Edit.** Where New and Edit open different form views, a New / Edit switch in the toolbar picks which one the designer and the Preview show. A pending edit of the shown view is sent before the other one loads. + - **Fields not on the form.** On a form view, a field the view lists in no section is not on the form; the designer keeps such fields in a "Not on this form" bucket, and dragging one into a section adds it. A field entry's own keys (`required`, `readonly`, …), a section's other keys, and entries the canvas does not show are written back as they were. A section that references a field group (`group`) keeps the reference until its members are changed. + - **Read-only base.** On a read-only base the view is customized, not edited: the edit is saved into the base's paired customization package as a declared override of the base's view (ADR-0146 D2), the same door list views use. + - **Preview.** The Form tab's Preview now lays the form out by the same form view (its sections, and the tabbed layout), as the dialog it previews does; before, it drew the object's field groups whatever form view the app opened. + - **`@object-ui/i18n`.** English and Chinese strings for the chip, the New / Edit switch and the "Not on this form" bucket. +- 6f095b9: feat(studio): customize a read-only package's dataset in the All metadata area, see what uses it before publishing, and read a refused publish at the dataset + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Customize a dataset.** On a read-only base package, Studio's All metadata area opens a dataset in its form editor with the dataset designer and live preview, and offers Customize. The first saved edit copies the whole dataset into the base's paired `.custom` package, declaring `overrides: { package, basedOn }`; the header's Publish applies it, and "Restore package original" on the override mark deletes the copy. On a writable package a dataset still opens the generic item page. + - **What uses it.** While a dataset is customized, the editor lists the dashboards, reports and other items that name it, read from the References read, so the person sees what a published change reaches. A failed read says it failed and offers to read again; it never reads as "nothing uses it". + - **A refused publish at the item.** A publish the server refuses for a dataset is placed on its measure or dimension and the designer's input (`Not published. Check Aggregate of the measure "Total Amount"`), in the Console's language, and shown at the dataset's editor as well as on its Pending changes entry. The server's own text stays under Details. + - **References read fixed.** `MetadataClient.references()` now reads the route's declared answer (`{ references: [{ type, name, label, path, kind }] }`) and maps each element to `MetadataReference` (`fromType`, `fromName`, new optional `fromLabel` and `kind`). It used to read a key the route never serves, so Setup's References tab and Studio's item tools showed "Nothing in the metadata graph points at this item. Safe to delete." for every item. An answer the declaration refuses is now a failed read. + - **Published surface.** `MetadataReference`, exported from `@object-ui/data-objectstack`, gains two optional fields, `fromLabel` (the referring item's label, when the server serves one) and `kind` (the served reference kind); existing fields are unchanged. `MetadataClient.references()` now accepts less: a `200` answer that `FindReferencesToMetaResponseSchema` refuses throws instead of resolving `[]`, so a caller must handle the rejection as it already does for a non-2xx answer; in this repository every caller reads it through `useReferencesScan`, which catches it and shows the failed-check state. +- 5f80129: fix(plugin-detail): a record page's related table offers Export only to a reader the list page offers it to, and an export refusal reads in the Console's language + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/plugin-detail`: the related table asks the list page's question.** The enlarged related list (the record page's active tab) drew Export whenever its data source could export. It now also reads the child object's effective API operation set (`/me/permissions` `apiOperations`) through `usePermissions()`, the source the list page's `exportPermitted` reads, and draws no Export when that set leaves `export` out. A missing set keeps the button, as on the list page. A reader who may export still exports this relation's rows alone. + - **Export refusals read in the Console's language.** When the export route still refuses (a 403, such as `EXPORT_NOT_PERMITTED`), the related table, the list page and the grid show the new `view.exportNotPermitted` sentence in the reader's language instead of the route's English text. Other export failures keep the server's sentence. + - **`@object-ui/i18n`: `view.exportNotPermitted`** in all ten locale packs. +- 0c7e95a: fix(app-shell): Studio's rails mark a customized row with the word 定制 / Custom and an added row with 新增 / Added, no fill, instead of a filled square with a glyph that read as an add button (#30070) + + Clause-②: yes (widening) — new studio i18n key `engine.customizationMark.customizedShort` (en, zh) + Metadata-protocol: none + + The compact customization mark the pillar rails draw (and the Interfaces rail's navigation-override mark, which wraps it) now prints one short coloured word with no background. Its tooltip and accessible name are unchanged: the full text ("已定制(HotCRM)") followed by the sentence naming the customization package. It is not a control: a click on it is a click on its row. +- c8e6d38: feat(app-shell): Studio adds an action to an installed app's object, saved in the base's customization package (#30088) + + Clause-②: no + Metadata-protocol: none + + On a read-only (installed or code-shipped) package that has, or can get, its paired customization package (`.custom`), the Data pillar's Actions view of the base's object now offers New. Before, it listed the vendor's actions with no New, so an operations person could not add a button such as "Mark qualified" to an installed app's leads, although the platform accepts the result. + + - New offers the same presets and the same action editor as on a writable package. The action is saved as its own `action` item in the pair, bound to the object by `objectName`, as a draft (the first write asks the ensure door for the pair). The base's object is never written. + - The vendor's actions stay read-only in the list. The added action is listed after them with the "added" customization mark, its edits autosave into its draft in the pair, and Delete removes its draft and its published item from the pair. An added action keeps the name it was created with: a rename is held with a line saying how to save. + - The action publishes with the pair's other drafts from Pending changes, and the Actions list's "only customizations" counts it and narrows the list to the pair's actions. + + A writable package's action editing is unchanged: its actions are still written into the object draft. No spec, server or metadata-type change. +- d99cd3b: feat(app-shell): the Studio list-view panel configures the selection bar's actions (#30089) + + Clause-②: no + Metadata-protocol: none + + The list-view panel of an object's grid view gains a "Selection actions" section. It edits two keys the list view already declares, and no other: + + - `bulkActions`: pick the object's actions by label, plus the built-in Delete (`delete`). Each runs once per selected record, promoted with the action's own label and params. + - `bulkActionDefs`: add, edit or remove a field update (`operation: 'update'`): a button label, the fields the dialog asks for (one required param per field, carrying a select's options and a lookup's object) and an optional confirmation. A def is written only once it has a label and a field and `BulkActionDefSchema` accepts it. + + The section says what happens to the built-in Delete: shown while the view declares neither key, and kept once either is declared only when `bulkActions` names `delete`. A def of another kind (`delete`, an aggregate `custom` dispatch) is listed and can be removed; it is edited in metadata. An emptied key is removed, so a view that declares neither key saves as it was read. On an installed app's read-only view, the first edit goes through the same customization copy as every other setting of the panel. Strings are added in English and Chinese to the `studio` namespace of `@object-ui/i18n`. +- b6caa14: fix(app-shell): Studio's publish review sheet says a customization lives in this environment's database and moves only by `os package export` (#30119) + + Clause-②: yes (widening) — new `@object-ui/i18n` key `preview.changes.customizationEnvironment` in every pack; `preview/savedToCustomization` gains `customizationEnvironmentText` and `PACKAGE_EXPORT_COMMAND` + Metadata-protocol: none + + Reviewing a read-only package's customizations, the sheet adds one line under "Saved to BASE's customizations": the changes are kept in this environment's database as items of the `.custom` package it names (the id the server answered for the base), not in the project's source, and they reach another environment only through `os package export`. +- ea5807e: fix(console): four copy and display details from a real-user walkthrough: Latin object names spaced in zh form titles, a title on the lookup's create dialog, a success toast that names the action, and money metrics at the currency's decimals + + Clause-②: yes (widening) + Metadata-protocol: none + + - **zh form titles space a Latin object name.** `新建{{object}}`, `编辑{{object}}` and `查看{{object}}` (and the AI chat's record-write card titles) read `新建 Repair Ticket` when the object has no Chinese label, and still `新建联系人` when it has one. `createI18n` applies the spacing to those keys only, through an i18next post-processor; the packs keep the plain `{{object}}` spelling. + - **The lookup's "create the referenced record" dialog has a title.** A `{ objectName, mode: 'create' }` modal descriptor with no `title` now reads `form.createTitle` with the object's label, the title of the console's own New dialog. An authored title still wins. + - **The default success toast names the action.** An action with a `label` and no `successMessage` (and no matching `outcomeMessages` entry) toasts the new pack key `actions.completedNamed` (`Resolve completed`, zh `“解决”已完成`) instead of the generic `Action completed successfully`. An action with no label keeps the generic text. + - **Money metrics keep the currency's decimals.** A dataset metric over a currency measure with no `format`, and a static metric card with a `currency` and no `format`, show the currency's own minor-unit width (CNY `¥1,293.00`, JPY `¥1,293`), the width the list cell shows. Before, the dataset metric printed the shortest form (`¥1,293`, `¥1,280.5`) and the static card rounded to whole units. An authored `format` pattern still states its width. +- ff12aed: fix(core): a compareTo widget on a forward date range (next month, next quarter, next 30 days) compares with the period just before it, not with itself + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/core`: `shiftFilterByCompareTo`** with `kind: 'previousPeriod'` moved only `current_*` / `today` tokens, so a dashboard filtered to `next_month` or `next_quarter` compared the window with itself and the delta always read 0, and `next_30_days` (`{today}` … `{30_days_from_now}`) moved only its start and compared with a window that overlapped it on all but one day. A forward window now steps back one period: `{next_month_*}` becomes this month, `{next_quarter_*}` this quarter, and a `{today}` … `{N_days_from_now}` pair on one field becomes the N + 1 days that end yesterday (`{(N+1)_days_ago}` … `{yesterday}`), the same window the dataset executor computes for that range. Every other token, and the 13 backward presets, compare as before. + - **`@object-ui/core`: `compareToTrendLabelKey`** names that window: `vsThisMonth` for a forward month, `vsThisQuarter` for a forward quarter, and `vsPreviousPeriod` for a forward rolling window (it read `vsLastMonth`, `vsLastQuarter` and `vsYesterday`). + - **`@object-ui/i18n`:** `dashboard.trend.vsThisMonth` and `dashboard.trend.vsThisQuarter` in all ten locale packs (zh: 较本月, 较本季度). + - **`@object-ui/plugin-dashboard`, `@object-ui/plugin-charts`:** English fallbacks for the two keys in the metric tile and in the chart's comparison series label. +- 6c48d48: fix(console): a person who verifies their email during the mobile sign-in hand-off gets a mail link that works, and the hand-off's refusal time survives the verification pages (#30157) + + Clause-②: no + Metadata-protocol: none + + On a server that requires email verification, a person signing up from the mobile app's sign-in (the hand-off `redirect` on `/register`) got a verification mail whose link landed on a Console page that does not exist (`/_console/api/v1/auth/one-time-token/handoff?…`). The mail is usually opened in a browser that is not the app's sign-in window, and often on another device, so the link now returns to `/verify-email?handoff=verified`: the page says the address is confirmed and to go back to the app and sign in there. It offers no Console sign-in and hands out no sign-in token in that browser. The same holds for the "Resend verification email" mail. Every other `redirect` (an invitation, a Console page) keeps the link it had. + + The hand-off's refusal time (`handoff_refused_at`) is now carried through `/verify-email-prompt` on all three hops: `/login`'s "email not verified" branch, `/register`'s "verification required" branch, and "Back to login". Beside any other `redirect`, those links are unchanged. + + New string: `auth.verifyEmail.handoffSuccessDescription` (all ten locales). +- ef2ad31: fix(app-shell): a dismissed or revoked default permission set in the suggested-bindings panel can be reconsidered (#30199) + + Clause-②: no + Metadata-protocol: none + + The suggested-bindings panel (the install dialog and Marketplace package page, and Studio's Access pillar) drew a dismissed suggestion, or a revoked acceptance, as a state line with no action. An administrator who chose Dismiss could only take it back by calling `POST /api/v1/security/suggested-bindings/:id/reopen` by hand, although the server has offered that door all along. + + Such a row now offers "Reconsider". It calls the re-open door, and the row comes back as a pending suggestion with Accept and Dismiss (a Guest row with Dismiss only, as before). Re-opening grants nothing: the set reaches the Everyone position only when it is accepted again, under the same checks. The button is shown to the same callers who see Accept and Dismiss: a caller without `manage_metadata` is refused the list and sees no panel. A refusal (the row is gone, the set is no longer suggested, or the deployment's security service has no re-open door) is shown with the server's own message, and the panel re-reads the list. + + New client call: `reopenSuggestedBinding(id)`, beside `dismissSuggestedBinding`. New strings: `marketplace.suggestedBindings.reopen`, `.reopening` and `.reopenedToast` in all ten locale packs, and `engine.studio.access.suggestReopen`, `.suggestReopening` and `.suggestReopenedToast` (en, zh). +- fa9c0aa: fix(ui): Setup's everyone note and header tags speak to an administrator + + Clause-②: no + Metadata-protocol: none + + - **Assigned users of a permission set:** a set the everyone position distributes now says 「所有登录成员都持有此权限集。」 / "Every signed-in member holds this set." It no longer names the everyone anchor. + - **Setup list header tags:** three tags now say what they mean for the administrator reading the page, in all ten language packs. Admin config becomes "Configured here" (zh 「在此配置」), Platform schema becomes "Built in · editable" (zh 「平台预置 · 可编辑」), and Identity becomes "Via identity service" (zh 「经身份服务写入」). The tooltip text and the rule for when a tag is drawn do not change. +- 77c203a: docs(spec,i18n): stop naming the removed feature-flags settings page + + Clause-②: no + Metadata-protocol: none + + The `feature_flags` settings namespace and its Setup page were removed from `@objectstack/service-settings` and `@objectstack/platform-objects`. Two shipped texts still pointed at it; both now say what is live. + + - **Settings hub subtitle (`@object-ui/i18n`).** `console.settingsHub.subtitle` no longer promises feature flags, in all ten language packs. English FROM "Configure your system, integrations, and feature flags." TO "Configure your system and integrations."; each other pack drops its own term the same way. No key is added or removed. + - **Spec doc comments (`@objectstack/spec`).** The kernel barrel and `PUBLIC_AUTH_FEATURES` docblocks no longer send readers to a `feature_flags` settings manifest or to `OS_FEATURE_FLAGS_*`. A setting a plugin reads is declared on that plugin's own settings manifest (ADR-0007, env-overridable as `OS_{NAMESPACE}_{KEY}`); capabilities a deployment advertises to clients are the `PUBLIC_AUTH_FEATURES` registry. The `SettingsManifestSchema` docblock (and its generated reference page) gives `mail`, `branding`, `localization` as example namespaces. No schema, type or export changes. +- 648089d: feat(app-shell): Studio edits a translation item as a source-to-translation table with its gaps counted + + Clause-②: no + Metadata-protocol: none + + A `translation` item in Studio now opens on a table for its language: a row per string the package declares (objects, fields, options, apps, navigation, views, actions, dashboards and the rest), the source text beside the translation, grouped by item, with a count such as "37 of 120 untranslated" and an "Untranslated only" filter. The rows come from `collectExpectedEntries` in `@objectstack/lint`, the walk `os i18n extract` and `os lint` use, over the package's declared documents. The platform objects the package points at (for example the user object behind an owner lookup) are included, because their labels ship in a few languages only. A row also counts as translated when another bundle already serves the language at its key. + + - Saving writes the same `translation` item to the same place as before (the `.custom` pair on a read-only package): each cell writes the item's JSON at its key. The item's label, language and description stay in the form above the table, and its JSON stays available as an advanced view for what the table does not list (messages, settings UI, metadata forms). + - The "no custom renderer, editing as JSON" notice no longer appears on this page. + - The metadata item list labels an item in a code package's `.custom` pair as customer-written, not as shipped in a code package. + - Labels in English and Chinese. +- 10ecafd: fix(app-shell,auth,i18n): the organization form says requiring MFA needs two-factor on, an accepted save says who must enroll and when, and the enrollment screen names a two-factor-off deployment in the user's language + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Organization form, before saving.** While two-factor is off on the deployment (`features.twoFactor: false` in `/api/v1/auth/config`), the `sys_organization` edit form says, under its title, that Require Multi-Factor Auth needs two-factor, that a save with it ticked is refused, and how to turn two-factor on (Require multi-factor authentication in Setup → Authentication, or `OS_AUTH_TWO_FACTOR=true`). When the save is refused, the server's refusal shows on the field, as before. + - **Organization form, after an accepted save.** A save that turns `require_mfa` on for a row that stored it off adds a line to the saved toast: every member without an authenticator app must set one up, and is blocked from data once the MFA grace period ends (7 days unless changed in Setup → Authentication), counted from their first use after the change. A save of a row that already required it says nothing new. + - **Enrollment screen.** `enrollTotp` and `verifyTotp` of `@object-ui/auth` now reject with the HTTP `status` and the server's `code` on the error (the message is unchanged). The authenticator step reads a `404` (the `/two-factor/*` endpoints are not mounted while two-factor is off) as a translated sentence saying enrollment is not available and who can turn it on, instead of `Two-factor enable failed (404)`, and `INVALID_PASSWORD` as the password screen's sentence. Any other refusal keeps the server's message. + - **The widening.** `@object-ui/i18n` gains `auth.remediation.mfa.unavailable`, `console.organizationMfa.needsTwoFactor` and `console.organizationMfa.nowRequired` in all ten language packs. The two enrollment rejections gain the optional `status` and `code` properties. +- b6f4c0e: feat(console): the Allowed IP ranges setting shows the address the server judges before you save + + Clause-②: no + Metadata-protocol: none + + A save of `auth.allowed_ip_ranges` whose list leaves out the saving request's own address is refused, and until now the Authentication settings page only told the admin that address in the refusal. The page now reads `GET /api/v1/auth/admin/client-address` (the save check's own reading of the address, `server.trustProxy` included) and draws one line under the field: the address, and whether it came from a trusted proxy's forwarded headers or, with `trustProxy` off, from the connection itself (behind a proxy, the proxy's address). When the server cannot establish an address the line says a non-empty list cannot be saved from here. The line stays beside a refusal. A caller the route refuses (not a platform admin), or a server without the route, sees no line and the page is otherwise unchanged. `@object-ui/i18n` adds `console.settingsView.clientAddressDirect`, `clientAddressViaProxy` and `clientAddressUnknown` in all ten shipped locales. +- b4f4781: feat(console): the install note says how many sample records an install loaded, and clears them in place + + Clause-②: no + Metadata-protocol: none + + - **The install note.** After an install into this runtime, the package page's result now says how many sample records the install loaded, read from the install answer's own `data.seeded` report (`inserted + updated + skipped` of an `inline` run), with a "Purge sample data" button right there. An install over an existing one adds that installing again loads the sample data again, including any that was cleared. An install that loaded none (no seed datasets, a copy, a walled session with no active organization) shows nothing new. The success sentence is definite where the runtime answered that it loaded the app: "… is now in the app switcher." + - **The console's own confirm.** Purging sample data (from the install note, or the ⋯ menu, local or cloud) asks in the console's dialog instead of the browser's. When this session installed the package, the dialog says how many sample records will go at most: the number the install loaded. The clearing rule is unchanged: only rows that carry a seed record's key are deleted. + - **A reach from the app's own pages.** When the installer leaves the package page, a console banner (the `banner` notification `ConsoleLayout` draws above every page) says the app holds sample data, with a "Purge sample data" action that brings them back to the package page with the confirm open. Clearing withdraws it. It lasts for the console session; the runtime serves no count of sample rows after the install answer. + - **Types.** `LocalManagedInstallResult` and `LocalUnmanagedInstallResult` gain the optional `seeded` (`LocalInstallSeeded`). + - **Strings.** `@object-ui/i18n` gains `marketplace.detail.sampleDataLoaded`, `sampleDataReloaded`, `purgeConfirmAtMost` (count families), `sampleDataBannerTitle` and `sampleDataBannerMessage` in all 10 locales, and rewords `marketplace.install.localSuccess` in all 10. +- 48231b0: feat(console): the install result says when a package was installed without a protocol compatibility check + + Clause-②: yes (widening) + Metadata-protocol: none + + - **What the administrator now reads.** `POST /api/v1/marketplace/install-local` answers `data.protocolCheckSkipped` when its protocol handshake admitted the package without checking it: the package declares no `engines.protocol` (`no-range`), or a range the runtime cannot read (`unparsed-range`). The three Console surfaces that show an install result now draw it as a caution line under the success sentence: the package page's Install result, the catalog's organization install message, and Setup → Packages → Import. The line names the package, the platform protocol it was installed under and, for `unparsed-range`, the range it declared, in the reader's locale. A `reason` this Console does not know shows the server's own `message`. An install whose range was checked shows nothing new. + - **Types.** `LocalManagedInstallResult` and `LocalUnmanagedInstallResult` gain the optional `protocolCheckSkipped`. + - **New strings.** `@object-ui/i18n` gains `marketplace.install.protocolCheckSkipped.noRange` and `.unparsedRange` in all 10 locales. +- 0d54279: fix(app-shell): a new datasource starts on the External schema mode and says what each mode does; a driver box's default is a placeholder, not a value + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Starts on External.** Setup → Datasources → New datasource now starts on `schemaMode: 'external'` (sent with the `external: {}` block the schema requires) instead of `managed`. The page exists to connect a database that already holds tables and sync them in as objects, and only a federation source offers Sync. A managed datasource is ObjectStack's own storage: once an object is stored in it, the schema sync adds the object's columns to an existing table of the same name. The spec's own default (`managed` when a record declares none) is unchanged; only the dialog's starting choice moved. + - **The modes in business words.** The Schema mode select is ordered External, Managed, Validate only, its two main options are relabelled ("External: read an existing database", "Managed: ObjectStack creates and changes the tables"), and a sentence under it says what the chosen mode does to the database and which one to pick to read an existing database. + - **Why a managed row has no Sync.** A managed datasource's row (not the primary `default`) says it has no Sync objects because ObjectStack creates its tables, and how to read an existing database instead. + - **Box defaults are placeholders.** A driver's text or number box with a default (SQLite `Filename` `:memory:`, PostgreSQL `Host` `localhost` and `Port` `5432`, and so on) starts empty with the default as its placeholder, so a typed path is no longer appended to `:memory:`. A new datasource's Test and Create still send the default for a box left empty, so the request is what it was; an edit shows and sends the stored values. Switches and selects keep their defaults as values. + - **The widening.** Four new `studio` namespace rows in `@object-ui/i18n` (en and zh): `engine.datasource.schemaModeExternalHint`, `engine.datasource.schemaModeManagedHint`, `engine.datasource.schemaModeValidateOnlyHint` and `engine.datasource.noSyncManaged`; the en and zh text of `engine.datasource.schemaModeExternal` and `engine.datasource.schemaModeManaged` changes. +- 8e5fabe: fix(app-shell): after Setup → Datasources syncs objects, the dialog links each new object and names the two steps left: add it to an app, and grant access + + Clause-②: yes (widening) + Metadata-protocol: none + + - **The result.** When a Sync run creates at least one object, the dialog stays open on the result instead of closing behind a toast: the count line, one link per created object (label, with the name beside it when they differ) that opens the object under the app the page is in, and the two steps left before people can use it, adding it to an app and granting access in a permission set. A run that creates nothing closes the dialog as before. Refused tables are still reported in the error toast. + - **The widening.** Four new `studio` namespace rows in `@object-ui/i18n` (en and zh): `engine.datasource.syncNextStepsOne`, `engine.datasource.syncNextSteps`, `engine.datasource.syncNextStepApp` and `engine.datasource.syncNextStepAccess`. + - **Form labels.** A declared field with no `label` is drawn in the form under its name with the first letter capitalised and underscores as spaces (`part_no` → `Part no`), the rule the list already draws it under, so one field reads the same in both. An authored label is unchanged. +- ac56acf: fix(console): a value set on a deployment-level settings page reads `Modified`, names the default, and offers Reset to default (#30329) + + Clause-②: yes (widening) — two published language-pack keys in every built-in pack: `console.settingsView.sourceBadgeModified` and `console.settingsView.defaultValueHint` (with `{{value}}`). + Metadata-protocol: none + + A settings page whose manifest declares `scope: 'global'` (Setup's Authentication page is one) writes the deployment's row. A value it had saved was badged `Inherited from Global`, as if it came from somewhere else, and had no Reset to default, so going back meant knowing the default or emptying the box. + + - The page now passes each row the scope its Save writes, resolved the way the server resolves it: the specifier's `scope`, else the manifest's, else the spec default `tenant`. A value whose effective layer is that scope reads `Modified`, names the default layer's value (`Default: 8`; an option's label, Yes or No for a toggle; never a secret's), and offers Reset to default. Reset puts `null` in the draft and Save sends it, like any other change. + - An env-locked value never gets Reset. A value from a layer above the page's scope still reads `Inherited from …`, and the organization and user pages keep their `Overrides …` badges unchanged. + - An unset number box now reads empty after Reset; it kept showing the last number. +- a91409c: fix(core,react,i18n,components,app-shell,console): a write refused because the object's external datasource does not accept writes says the table is read-only, not "you don't have permission" + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@object-ui/core`: `isExternalWriteRefusal(err)`**, re-exported from `@object-ui/react` beside the other write-refusal readers. True when the server refused a write with `EXTERNAL_WRITE_FORBIDDEN`, the ADR-0015 write gate's code for an object bound to an external datasource that does not admit writes (`datasource.external.allowWrites` and `object.external.writable` must both be true). Read from `code`, then `details.code`, like `isPermissionError`. The refusal is a 403, so `isPermissionError` answers true for it too: a caller asks this first. + - **`@object-ui/i18n`: `form.readOnlyExternalSource`**, in all ten packs: "This table is read-only. Its records come from an external data source that does not accept changes." + - **Every surface that met this refusal now says the table is read-only:** the console record form page and the record dialog (`@object-ui/components` form), which said "you don't have permission to save this record"; the record page delete, the list row delete and the record page inline edit, which showed the gate's raw diagnostic; and the calendar's drag-to-reschedule and the record path's stage change. A permission line sent an administrator through permission sets for a grant that cannot exist. +- ab12a3b: fix(app-shell): Setup → Datasources names the objects that still use a datasource, in the console's language, before and after its delete + + Clause-②: yes (widening) + Metadata-protocol: none + + - **The confirm.** Opening a datasource's delete reads object metadata and lists every object whose `datasource` names it (label, with the name beside it when they differ). While any is listed, the confirm says the datasource cannot be deleted yet and that each object must be deleted or moved to another datasource first. With none, or when the read fails, the confirm is unchanged. + - **The refusal.** A `400` from `DELETE /api/v1/datasources/:name` re-reads that list. When objects still name the datasource, the toast says, in the console's language, that it was not deleted, which objects still use it, and what to do next. Any other failure, and a `400` with no bound object, keep the server's message in the existing frame. No server text is matched. + - **The widening.** Three new `studio` namespace rows in `@object-ui/i18n` (en and zh): `engine.datasource.deleteBoundBody`, `engine.datasource.deleteBoundRefused` and `engine.datasource.boundObjectSeparator`. +- 2b35374: fix(app-shell): a team made in Setup no longer reads as managed by an outside identity provider, and a header action's write shows on the page's related lists and their counts at once + + Clause-②: no + Metadata-protocol: none + + - The `better-auth` note (the Identity tag's tooltip, and the Setup record header's object-label note) read "Managed by the identity provider" / 「由身份提供方管理」, which told the maker of a hand-made team that the row came from elsewhere and was not theirs to change. It now reads "Written through the built-in identity service" / 「经内置身份服务写入」, and its body says these records are created and changed with the actions on the page or through sign-in, invitation and security flows, all of which go through that service. The values change in all ten language packs; no key is added or removed. + - A record-page action that declares `refreshAfter: true` (an `api` action such as a team's Add Member, a server action, a flow, a declared operation) now refreshes the record AND every related list the page shows: the lists refetch and their tab and card counts re-count. It used to refresh the record alone, so a team's members tab read 0 beside a list of 2 until a later row action on that list. + - Nothing to do on upgrade. +- 7b10a30: fix(auth): the forgot-password page no longer says a reset link was sent on a deployment that sends no mail (#30366) + + Clause-②: yes (widening) + Metadata-protocol: none + + `GET /api/v1/auth/config` reports `features.emailDelivery`: `false` when no email service is wired or the stock `log` transport only writes mail to the server console. On such a deployment the forgot-password form used to answer every request with "Check your email — we've sent a password reset link", although no mail leaves. When the server reports `emailDelivery: false`, the success screen now says a reset link can't be emailed here and to ask an administrator, who can set a new password from Setup → Users. + + The text describes the deployment, never the address: it carries no address, and a known and an unknown address read the same, so the page adds no account-existence oracle. Only an explicit `false` changes it; a server that predates the flag (no `emailDelivery` key) or a failed config fetch keeps the "check your email" screen, since those deployments may well send mail. + + `ForgotPasswordFormLabels` gains `noDeliveryTitle` and `noDeliveryDescription`, and `AuthPublicConfig.features` gains `emailDelivery`. New strings `auth.forgotPassword.noDeliveryTitle` and `auth.forgotPassword.noDeliveryDescription` in all ten locales, passed by the Console's forgot-password page and `DefaultForgotPasswordPage`. +- 062236c: fix(console): a link inside an app's doc keeps the app shell, and the docs chrome reads the session language + + Clause-②: no + Metadata-protocol: none + + A doc read inside an app (`/apps/APP/docs/BOOK/DOC`) rewrote its body cross-references to the portal's `/docs/NAME`, so following one left the app: no top bar, no app navigation. They now resolve under the app's own docs route, as the book sidebar already did, and the header's home link goes to the app's docs index. The header's "Documentation" label, the portal heading and the book sidebar's synthetic "Uncategorized" group title now read the new `help.documentation` and `help.uncategorized` keys, present in all ten locale packs; the group is titled by its key, and authored group labels keep the server's translation. +- 47c457b: feat(console): after a reload, the package page still says how many sample records a purge would delete + + Clause-②: no + Metadata-protocol: none + + - **The count on any visit.** The package page of a local install now reads the runtime's count of the sample rows a purge would delete right now (`GET /api/v1/marketplace/install-local/:manifestId/sample-data`), on every visit and again after an install, a reseed or a purge. Before, the purge confirm and the banner that follows the installer to the app knew a count only for an install made in the same browser session; a reload lost it. + - **The confirm.** The local purge confirm draws that count. When it cannot be read (refused, failed), it draws the count this session's install answered, as before. A read that began before an install, reseed or purge of this session answered is older than that answer and is not used. + - **The banner.** A count above zero raises the sample-data banner on leaving the page, as an install of this session does. A count of `0` drops what the session knew: no banner, and no count line in the confirm. + - **Types.** `marketplaceApi` gains `countLocalSampleData` and `LocalSampleDataCount`. + - **Strings.** `@object-ui/i18n` rewords `marketplace.detail.purgeConfirmAtMost` (all plural forms) in all 10 locales: it no longer says the count is "the number this install loaded", which is not what the runtime's count is. It stays an upper bound ("Up to N sample records will be deleted."), true of both counts. No key is added. +- eca7cd2: feat(i18n): `chatbot.tool.create_record` / `update_record` / `remove_sample_data` labels and the record-write card's `console.ai.recordWrite*` labels in all ten packs; `chatbot.tool.draft_record` retired + + Clause-②: no + Metadata-protocol: none + + The spec registry now lists the cloud chat agents' confirmed record write and the sample-data cleanup (cloud#3565) and drops `draft_record`, so the chatbot's tool timeline gets a sentence for each new step in every pack ("Create the record" / "新建记录", "Change the record" / "修改记录", "Remove the sample data" / "清理示例数据") and loses the retired one, keeping `toolLabels-locale-parity-7481` and `toolLabels-sentences-12724` green. The console's record-write card gets its eleven labels under `console.ai.recordWrite*`: the create and update titles (with the object's label), the three buttons 确认 / 打开完整表单 / 取消, the two stored reasons, the "not included" lead-in and the three states (writing, written, cancelled) — plus the 「清理示例数据」 card's four: `console.ai.sampleDataCleanupTitle` (its title over the seeded tables and counts), `console.ai.sampleDataCleanupIrreversible` (「删除后无法恢复」 — the removal cannot be undone, said on every card), and the two count families `console.ai.sampleDataCleanupAbsent` (the recorded rows the card does not list) and `console.ai.sampleDataCleanupEdited` (the rows kept because someone edited them), each with every plural slot its language selects (objectui#11445). +- 614e831: feat(app-shell): Connect an Agent adds connect cards for WorkBuddy, Doubao Work and QwenWork, and drops the VS Code card + + Clause-②: no + Metadata-protocol: none + + The Connect an Agent page (Account → Developer, and Setup) now carries three more client cards, each filled in with the deployment's MCP URL, listed after the Claude, Codex and Cursor cards: + + - **WorkBuddy** — the `mcpServers` entry for Connectors → Custom connector → Configure MCP, with the `streamableHttp` transport spelling WorkBuddy reads, and the reminder that the connector stays inactive until it is trusted. + - **Doubao Work** — the name, transport and server URL to type into Skills · Connectors → New → New custom connector. + - **QwenWork** — the `mcpServers` JSON for Extensions → Connectors → Paste JSON config, with the `streamable-http` spelling QwenWork reads. + + Each of the three names the fallback for a client version without the browser sign-in: the request header `Authorization: Bearer `. A minted key is now revealed in that form as well as the existing `x-api-key` line. + + The claude.ai / Claude Desktop and Claude Code cards merge into one full-width Claude card with a half for each. The Codex CLI card becomes a Codex card for the Codex app: the name, transport and URL to enter under Settings → MCP servers → Add server, and where to sign in (new string `connectAgent.codex.hint`); it no longer shows a `config.toml` snippet. The VS Code / Copilot card is gone, with its string `connectAgent.vscode.body`; the generic `mcpServers` snippet on the Cursor card and the MCP URL at the top still serve any MCP client. The client cards and the API-key card form one list, the API-key card last. Long one-line snippets wrap instead of being clipped. The Doubao Work and QwenWork card titles are strings too (`connectAgent.doubao.title`, `connectAgent.qwenwork.title`), so the zh pack shows the products' own names, 豆包工作 and 千问办公. New strings live under `connectAgent.fields`, `connectAgent.workbuddy`, `connectAgent.doubao` and `connectAgent.qwenwork`, translated in all ten packs. Nothing on the server changes; `@objectstack/mcp` updates only its description of the page. +- Updated dependencies [4261169] +- Updated dependencies [c2be734] +- Updated dependencies [aa57c76] +- Updated dependencies [ea5807e] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [bff5ba2] +- Updated dependencies [31260ca] +- Updated dependencies [f5aaa0b] +- Updated dependencies [a91409c] +- Updated dependencies [ad2393f] + - @object-ui/core@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/i18n/package.json b/ui/packages/i18n/package.json index 641ccc97f2f..29444d0270b 100644 --- a/ui/packages/i18n/package.json +++ b/ui/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/i18n", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "sideEffects": false, "license": "MIT", diff --git a/ui/packages/layout/CHANGELOG.md b/ui/packages/layout/CHANGELOG.md index 0d053f35a5f..6aa1df17ddf 100644 --- a/ui/packages/layout/CHANGELOG.md +++ b/ui/packages/layout/CHANGELOG.md @@ -1,5 +1,97 @@ # @object-ui/layout +## 18.4.0 + +### Patch Changes + +- 4128213: fix(console): a nav entry with no icon draws its object's icon, record field labels read in one style, a count chart fits a small count, and a page whose entry failed to load retries (#30126) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Navigation icons.** An app navigation entry of type `object` that names no `icon` now draws the icon its object declares, in the sidebar and in the `nav:menu` page block. Before, every such row drew the same generic page icon, so an app whose navigation names no icons showed one icon down the whole sidebar. The icon is read by object name from the metadata the console already holds, so no request is added and the navigation is not written to. An authored `icon` wins, and an object with no icon, a dashboard, page or report entry, and a host with no resolver keep the generic icon. `@object-ui/layout` exports `resolveNavItemIcon`, the types `NavIconTarget` and `NavTargetIconResolver`, and an optional `resolveTargetIcon` prop on `NavigationRenderer`, alongside the existing `resolveTargetLabel`. + - **Record field labels.** A record page's section field labels no longer carry an uppercase transform. Only Latin text took it, so in a zh console an untranslated label (`REPAIR COST`) read in capitals beside a translated one (`负责人`). Section labels now render as authored, in the style of the highlights strip above them. + - **Small count charts.** A count axis whose largest value is a small whole number (1 to 3) now tops out at that number, so a chart of one record draws its bar to the top of the axis instead of a quarter of the way up a 0–4 axis. An axis with an authored `min`, `max` or `stepSize`, fractional values, or a count of 4 or more draws as before. + - **Boot retry.** When the page's entry script fails to load before anything mounts (the server went away between serving the page and serving its entry, as during an `os dev` restart), the page now reloads itself, backing off from 1 second and doubling, at most five times per tab. Before, the loading splash stayed forever. The count is cleared on the first mount, and where `sessionStorage` cannot be read nothing retries. +- Updated dependencies [4261169] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [ea5807e] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [a91409c] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [8b0fbb8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [ad2393f] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/core@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @object-ui/components@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/layout/package.json b/ui/packages/layout/package.json index 0a6473711f1..78b895c5bbc 100644 --- a/ui/packages/layout/package.json +++ b/ui/packages/layout/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/layout", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "sideEffects": [ "./dist/index.js", diff --git a/ui/packages/mobile/CHANGELOG.md b/ui/packages/mobile/CHANGELOG.md index e7faae4a991..84b138c9d0d 100644 --- a/ui/packages/mobile/CHANGELOG.md +++ b/ui/packages/mobile/CHANGELOG.md @@ -1,5 +1,21 @@ # @object-ui/mobile +## 18.4.0 + +### Patch Changes + +- a4c3e73: feat(console,mobile,auth): the Console registers the mobile shell's push token under its own session and unregisters it on sign-out and when the shell removes the connection + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@object-ui/console`:** in the ObjectStack mobile shell, a signed-in person who has reached Home or an app is asked for push permission once per page load, after a short delay (never on a sign-in screen): the Console calls `getPushToken()` and registers the token with `POST /api/v1/notifications/devices`. A `null` answer (simulator, permission denied, no token) sends nothing. A token the shell rotates is registered again; an unchanged token for the same person is not sent twice. Another person signing in on the page registers the device for them, and the registry moves the token away from the previous person. An impersonation never registers, so the impersonated person's pushes never reach the administrator's device. The registration sends `deviceId` as the shell's `installId` when it has one, and `origin` only when the page was loaded over `http:` or `https:`. The device is unregistered (`DELETE /api/v1/notifications/devices/:deviceId`) before sign-out ends the session, and when the shell removes this connection; a shell that is neither iOS nor Android registers nothing. A browser is unchanged. + - **`@object-ui/mobile`:** `ObjectStackNative` gains the optional `installId` (`string | null`), and the package exports `NATIVE_PUSH_TOKEN_CHANGED_EVENT` (`objectstack-native:pushTokenChanged`, announced as `has('pushTokenChanged')`) with its event type `NativePushTokenChangedEvent`. Both are feature-detected: a shell that predates them carries neither. + - **`@object-ui/auth`:** `registerBeforeSignOut(step)` registers a step that `signOut()` awaits while the session still exists, and returns the function that removes it. The steps run together and can never hold sign-out back: a rejection is reported and dropped, and sign-out goes ahead after `BEFORE_SIGN_OUT_TIMEOUT_MS` (3 s) whether or not they have settled. +- Updated dependencies [7a5f73e] +- Updated dependencies [a52761f] + - @object-ui/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/mobile/package.json b/ui/packages/mobile/package.json index 1b281add61e..6bb93afa013 100644 --- a/ui/packages/mobile/package.json +++ b/ui/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/mobile", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Mobile optimization for ObjectUI with responsive components, PWA support, and touch gesture handling.", diff --git a/ui/packages/permissions/CHANGELOG.md b/ui/packages/permissions/CHANGELOG.md index 01a19c8735b..a7dbf79a251 100644 --- a/ui/packages/permissions/CHANGELOG.md +++ b/ui/packages/permissions/CHANGELOG.md @@ -1,5 +1,72 @@ # @object-ui/permissions +## 18.4.0 + +### Patch Changes + +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/permissions/package.json b/ui/packages/permissions/package.json index 1d091051172..79c364e1e7d 100644 --- a/ui/packages/permissions/package.json +++ b/ui/packages/permissions/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/permissions", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "RBAC permission system for ObjectUI with object/field/row-level access control, permission guards, and hooks.", diff --git a/ui/packages/plugin-calendar/CHANGELOG.md b/ui/packages/plugin-calendar/CHANGELOG.md index 8a93b2109fe..7c4ae43de71 100644 --- a/ui/packages/plugin-calendar/CHANGELOG.md +++ b/ui/packages/plugin-calendar/CHANGELOG.md @@ -1,5 +1,90 @@ # @object-ui/plugin-calendar +## 18.4.0 + +### Patch Changes + +- a91409c: fix(core,react,i18n,components,app-shell,console): a write refused because the object's external datasource does not accept writes says the table is read-only, not "you don't have permission" + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@object-ui/core`: `isExternalWriteRefusal(err)`**, re-exported from `@object-ui/react` beside the other write-refusal readers. True when the server refused a write with `EXTERNAL_WRITE_FORBIDDEN`, the ADR-0015 write gate's code for an object bound to an external datasource that does not admit writes (`datasource.external.allowWrites` and `object.external.writable` must both be true). Read from `code`, then `details.code`, like `isPermissionError`. The refusal is a 403, so `isPermissionError` answers true for it too: a caller asks this first. + - **`@object-ui/i18n`: `form.readOnlyExternalSource`**, in all ten packs: "This table is read-only. Its records come from an external data source that does not accept changes." + - **Every surface that met this refusal now says the table is read-only:** the console record form page and the record dialog (`@object-ui/components` form), which said "you don't have permission to save this record"; the record page delete, the list row delete and the record page inline edit, which showed the gate's raw diagnostic; and the calendar's drag-to-reschedule and the record path's stage change. A permission line sent an administrator through permission sets for a grant that cannot exist. +- Updated dependencies [4261169] +- Updated dependencies [ffeda13] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [a4c3e73] +- Updated dependencies [1e6f542] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [c2be734] +- Updated dependencies [6e68d8f] +- Updated dependencies [3461420] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [df4c06e] +- Updated dependencies [f68b47c] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [b6caa14] +- Updated dependencies [ff3f83b] +- Updated dependencies [ea5807e] +- Updated dependencies [4128213] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [bff5ba2] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [fa9c0aa] +- Updated dependencies [5174603] +- Updated dependencies [f5aaa0b] +- Updated dependencies [77c203a] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [cd7f785] +- Updated dependencies [7b10a30] +- Updated dependencies [8b0fbb8] +- Updated dependencies [ad2393f] +- Updated dependencies [062236c] +- Updated dependencies [47c457b] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [8a0516c] + - @object-ui/core@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/mobile@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-calendar/package.json b/ui/packages/plugin-calendar/package.json index 684d5e812e5..b568217a38d 100644 --- a/ui/packages/plugin-calendar/package.json +++ b/ui/packages/plugin-calendar/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-calendar", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Calendar view plugins for ObjectUI - includes both ObjectQL-integrated and standalone calendar components", diff --git a/ui/packages/plugin-charts/CHANGELOG.md b/ui/packages/plugin-charts/CHANGELOG.md index df3c0b47b7a..010ca3c562e 100644 --- a/ui/packages/plugin-charts/CHANGELOG.md +++ b/ui/packages/plugin-charts/CHANGELOG.md @@ -1,5 +1,84 @@ # @object-ui/plugin-charts +## 18.4.0 + +### Patch Changes + +- 4128213: fix(console): a nav entry with no icon draws its object's icon, record field labels read in one style, a count chart fits a small count, and a page whose entry failed to load retries (#30126) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Navigation icons.** An app navigation entry of type `object` that names no `icon` now draws the icon its object declares, in the sidebar and in the `nav:menu` page block. Before, every such row drew the same generic page icon, so an app whose navigation names no icons showed one icon down the whole sidebar. The icon is read by object name from the metadata the console already holds, so no request is added and the navigation is not written to. An authored `icon` wins, and an object with no icon, a dashboard, page or report entry, and a host with no resolver keep the generic icon. `@object-ui/layout` exports `resolveNavItemIcon`, the types `NavIconTarget` and `NavTargetIconResolver`, and an optional `resolveTargetIcon` prop on `NavigationRenderer`, alongside the existing `resolveTargetLabel`. + - **Record field labels.** A record page's section field labels no longer carry an uppercase transform. Only Latin text took it, so in a zh console an untranslated label (`REPAIR COST`) read in capitals beside a translated one (`负责人`). Section labels now render as authored, in the style of the highlights strip above them. + - **Small count charts.** A count axis whose largest value is a small whole number (1 to 3) now tops out at that number, so a chart of one record draws its bar to the top of the axis instead of a quarter of the way up a 0–4 axis. An axis with an authored `min`, `max` or `stepSize`, fractional values, or a count of 4 or more draws as before. + - **Boot retry.** When the page's entry script fails to load before anything mounts (the server went away between serving the page and serving its entry, as during an `os dev` restart), the page now reloads itself, backing off from 1 second and doubling, at most five times per tab. Before, the loading splash stayed forever. The count is cleared on the first mount, and where `sessionStorage` cannot be read nothing retries. +- ff12aed: fix(core): a compareTo widget on a forward date range (next month, next quarter, next 30 days) compares with the period just before it, not with itself + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/core`: `shiftFilterByCompareTo`** with `kind: 'previousPeriod'` moved only `current_*` / `today` tokens, so a dashboard filtered to `next_month` or `next_quarter` compared the window with itself and the delta always read 0, and `next_30_days` (`{today}` … `{30_days_from_now}`) moved only its start and compared with a window that overlapped it on all but one day. A forward window now steps back one period: `{next_month_*}` becomes this month, `{next_quarter_*}` this quarter, and a `{today}` … `{N_days_from_now}` pair on one field becomes the N + 1 days that end yesterday (`{(N+1)_days_ago}` … `{yesterday}`), the same window the dataset executor computes for that range. Every other token, and the 13 backward presets, compare as before. + - **`@object-ui/core`: `compareToTrendLabelKey`** names that window: `vsThisMonth` for a forward month, `vsThisQuarter` for a forward quarter, and `vsPreviousPeriod` for a forward rolling window (it read `vsLastMonth`, `vsLastQuarter` and `vsYesterday`). + - **`@object-ui/i18n`:** `dashboard.trend.vsThisMonth` and `dashboard.trend.vsThisQuarter` in all ten locale packs (zh: 较本月, 较本季度). + - **`@object-ui/plugin-dashboard`, `@object-ui/plugin-charts`:** English fallbacks for the two keys in the metric tile and in the chart's comparison series label. +- Updated dependencies [4261169] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [1e6f542] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [c2be734] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [aa57c76] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [b6caa14] +- Updated dependencies [ea5807e] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [bff5ba2] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [fa9c0aa] +- Updated dependencies [f5aaa0b] +- Updated dependencies [77c203a] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [7b10a30] +- Updated dependencies [8b0fbb8] +- Updated dependencies [ad2393f] +- Updated dependencies [062236c] +- Updated dependencies [47c457b] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] + - @object-ui/core@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/components@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/plugin-charts/package.json b/ui/packages/plugin-charts/package.json index 177da6a2d51..dc69e33ec14 100644 --- a/ui/packages/plugin-charts/package.json +++ b/ui/packages/plugin-charts/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-charts", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Chart components plugin for ObjectUI, powered by Recharts", diff --git a/ui/packages/plugin-chatbot/CHANGELOG.md b/ui/packages/plugin-chatbot/CHANGELOG.md index aecb1ed1178..8f5c2242289 100644 --- a/ui/packages/plugin-chatbot/CHANGELOG.md +++ b/ui/packages/plugin-chatbot/CHANGELOG.md @@ -1,5 +1,155 @@ # @object-ui/plugin-chatbot +## 18.4.0 + +### Patch Changes + +- 3b8b23c: refactor(plugin-chatbot)!: retire eight zero-consumer exports (the floating chatbot context hook and its three types, the message item props, one adapter helper and two display helpers) + + Clause-②: no (narrowing) + Metadata-protocol: none + + + + **BREAKING** for a host that imports any of these eight names from `@object-ui/plugin-chatbot`. None had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected. Every module stays in the package: `FloatingChatbotTrigger` and `FloatingChatbotPanel` keep reading the floating state through the hook, `toRuntimeMessages` keeps converting tool invocations, and `ChatbotEnhanced` keeps rendering tool results and error summaries with the two helpers. Only the entry stops publishing them. `FloatingChatbot`, `FloatingChatbotProvider`, `FloatingChatbotTrigger`, `FloatingChatbotPanel` and their `…Props` types, `toRuntimeMessages`, `authoredToRuntimeMessage`, `toRuntimeRole`, `toRuntimeTimestamp`, `toRuntimeToolState`, `humanizeToolName`, `TypingIndicator`, `emitAiUsageRefresh` and `AGENT_ALIAS_GROUPS` stay exported unchanged. + + What leaves, and what to write instead: + + - FROM `useFloatingChatbot`, `FloatingChatbotState`, `FloatingChatbotActions`, `FloatingChatbotContextValue` TO `FloatingChatbotTrigger` and `FloatingChatbotPanel` inside a `FloatingChatbotProvider`, which open, close and toggle the panel themselves; for a control of your own, hold the open state in your own component. + - FROM `ChatMessageProps` TO `ChatbotProps`: `Chatbot` renders the message rows itself. + - FROM `toRuntimeToolInvocation` TO `toRuntimeMessages` or `authoredToRuntimeMessage`, which convert every tool invocation of a message. + - FROM `unwrapToolResult`, `summarizeChatError` TO no replacement: `ChatbotEnhanced` applies both itself; keep a local copy if your own chat surface needs them. +- fccde75: fix(plugin-chatbot,app-shell): after 确认 or 取消 on the chat's confirm card, the follow-up sent to the model no longer shows as the person's own message + + Clause-②: no + Metadata-protocol: none + + After the operator confirmed or cancelled an action on the chat's confirm card, the chat sent the model a short follow-up («[HITL pa_…] The operator approved the `update_record` call … Acknowledge the outcome …») through the ordinary user-message channel, so the person saw it as their own bubble, after a reload too, and the conversation list showed it as the conversation's preview. The model still receives that follow-up unchanged. What changed is who reads it as the person's words: the chat transcript draws no bubble for it (live, after a reload, and on a shared transcript), the conversation list previews the person's last ask instead, and the conversation-language probe no longer reads it as the person's latest turn, which turned a Chinese conversation's outbound text English when the action's result carried no Chinese. The follow-up is recognised by its leading `[HITL ]` tag, the one part of a stored message that survives a reload; `@object-ui/plugin-chatbot` exports the reader as `isHitlContinuation` (also from `./core`). An ordinary message is sent, drawn and previewed as before. +- eca7cd2: feat(plugin-chatbot): the record-write card — ONE record the chat proposes and the user confirms, with 确认 / 打开完整表单 / 取消 (cloud#3565) + + Clause-②: no + Metadata-protocol: none + + The cloud chat's `create_record` / `update_record` tools queue a HITL pending action in the user's name and answer the approval envelope plus the record: `{ status:'pending_approval', pendingActionId, recordWrite:{ operation, objectName, objectLabel, recordId?, label?, reason?, fields:[{ name, label, type, value, current? }], dropped } }`. The chat now draws that as a card instead of the generic Approve / Deny strip and the raw payload: + + - **New export** `detectRecordWrite(result)` (with the `RecordWrite` and `RecordWriteField` types), lifted onto `ChatbotEnhancedToolInvocation.recordWrite` by the live mapper beside `pendingActionId`. It reads the bare object, the persisted `{ type:'text', value }` wrapper and a JSON string; an envelope without the pending id or without a field to confirm is not a card. + - **The card** (`data-testid="record-write"`): the create or update title with the object's label, the record's label and reason, every field as a labelled row — an update shows the stored value struck through, then →, then the proposed one — the fields the server left out and why, and three buttons. 确认 calls `onToolApprove(toolCallId, true)` (the pending action's dispatcher writes as the user); 取消 calls it with `false` and `recordWriteCancelReason`; 打开完整表单 calls it with `false` and `recordWriteOpenFormReason` FIRST, then the new `onOpenRecordForm(write)` prop, so a record saved in the form is never written a second time from the card (disabled where the host passes no handler). After a decision the card says written (with the record's link through `onOpenRecord`, read from the dispatcher's `{ status:'written', objectName, recordId, label }` result), cancelled, or the server's refusal. + - **New props**: `onOpenRecordForm`, `recordWriteCreateTitle(objectLabel)`, `recordWriteUpdateTitle(objectLabel)`, `recordWriteConfirmLabel`, `recordWriteOpenFormLabel`, `recordWriteCancelLabel`, `recordWriteCancelReason`, `recordWriteOpenFormReason`, `recordWriteDroppedLabel`, `recordWriteSubmittingLabel`, `recordWriteWrittenLabel`, `recordWriteCancelledLabel`, all with English defaults. + - **「清理示例数据」** rides the same seam: `detectSampleDataCleanup(result)` (with the `SampleDataCleanup` type) lifts `{ status:'pending_approval', pendingActionId, sampleDataCleanup:{ tables:[{ objectName, label, count }], total, absent, edited } }` onto `sampleDataCleanup`, and the chat draws the tables and counts (`data-testid="sample-data-cleanup"`, title from the new `sampleDataCleanupTitleLabel` prop) above the generic Approve / Deny strip, with the raw payload hidden. The card always says the removal cannot be undone (`sampleDataCleanupIrreversibleLabel`, default "Removed rows cannot be recovered." — there is no recycle bin until objectstack#3146), says how many recorded rows it does not list when `absent` is above 0 (`sampleDataCleanupAbsentLabel(count)`) and how many stay because someone edited them when `edited` is above 0 (`sampleDataCleanupEditedLabel(count)`; an envelope without `edited` reads as 0). + - **`ToolDecisionState`** (widened, additive): every state may carry `decision: 'approved' | 'rejected'`, and an approved `success` may carry `result`, the dispatcher's answer. `useHitlInChat` sets both; the generic strip keeps reading `message` alone. +- Updated dependencies [4261169] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [1e6f542] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [b6caa14] +- Updated dependencies [25fea77] +- Updated dependencies [ea5807e] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [2046d21] +- Updated dependencies [fa9c0aa] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [7b10a30] +- Updated dependencies [8b0fbb8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [ad2393f] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [062236c] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [47c457b] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/core@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/components@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/plugin-chatbot/package.json b/ui/packages/plugin-chatbot/package.json index 571deb0945c..1e0108b830b 100644 --- a/ui/packages/plugin-chatbot/package.json +++ b/ui/packages/plugin-chatbot/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-chatbot", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Chatbot interface plugin for ObjectUI", diff --git a/ui/packages/plugin-dashboard/CHANGELOG.md b/ui/packages/plugin-dashboard/CHANGELOG.md index 64e4e952f20..42a59533421 100644 --- a/ui/packages/plugin-dashboard/CHANGELOG.md +++ b/ui/packages/plugin-dashboard/CHANGELOG.md @@ -1,5 +1,177 @@ # @object-ui/plugin-dashboard +## 18.4.0 + +### Patch Changes + +- 48b3ae6: refactor(plugin-dashboard)!: retire five zero-consumer exports (the config panel's defaults table and hook, the retired-node tombstone and its report and reset helpers) + + Clause-②: no (narrowing) + Metadata-protocol: none + + + + **BREAKING** for a host that imports any of these five names from `@object-ui/plugin-dashboard`. None had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected. Every module stays in the package: the config panels keep using the hook and its table, and the package still registers the tombstone for every retired dashboard node type, so an authored `view:dashboard` is refused by name exactly as before. Only the entry stops publishing them. `RETIRED_DASHBOARD_NODE_TYPES`, `ConfigPanelTranslate`, `DashboardMetricNodeSchema`, the three `WidgetDataset*` types and every component stay exported unchanged. + + What leaves, and what to write instead (none has a replacement export): + + - FROM `CONFIG_PANEL_DEFAULT_TRANSLATIONS`, `useConfigPanelTranslation` TO no replacement: `DashboardConfigPanel` and `WidgetConfigPanel` translate their own labels; mount an `I18nProvider` to change them. + - FROM `RetiredDashboardNodeTombstone` TO no replacement: the package registers it under every key of `RETIRED_DASHBOARD_NODE_TYPES` itself, so rendering a retired node through `SchemaRenderer` still shows the refusal. + - FROM `reportRetiredDashboardNodeType`, `resetRetiredDashboardNodeTypeReports` TO no replacement: the tombstone reports on its own, once per retired type; read the migration text from `RETIRED_DASHBOARD_NODE_TYPES`. +- 14c67d4: feat(plugin-dashboard): a dataset-bound KPI tile opens the records behind its number + + Clause-②: no + Metadata-protocol: none + + A dashboard metric tile bound to a dataset now opens the same drill drawer its charts and tables already open, by click or by Enter/Space. The list is the widget's filter (the dashboard's filters included) AND the answer's date window AND the measure's `drillScopes` entry, which carries the dataset's own filter and the measure's own filter, so the records listed are the records the number counts. A tile whose measure has no `drillScopes` entry (a derived measure, or a scope that names a relationship path) offers no click, as does a tile whose filter the list cannot express; its markup is unchanged. Nothing to author: the click follows from the dataset answer. +- ea5807e: fix(console): four copy and display details from a real-user walkthrough: Latin object names spaced in zh form titles, a title on the lookup's create dialog, a success toast that names the action, and money metrics at the currency's decimals + + Clause-②: yes (widening) + Metadata-protocol: none + + - **zh form titles space a Latin object name.** `新建{{object}}`, `编辑{{object}}` and `查看{{object}}` (and the AI chat's record-write card titles) read `新建 Repair Ticket` when the object has no Chinese label, and still `新建联系人` when it has one. `createI18n` applies the spacing to those keys only, through an i18next post-processor; the packs keep the plain `{{object}}` spelling. + - **The lookup's "create the referenced record" dialog has a title.** A `{ objectName, mode: 'create' }` modal descriptor with no `title` now reads `form.createTitle` with the object's label, the title of the console's own New dialog. An authored title still wins. + - **The default success toast names the action.** An action with a `label` and no `successMessage` (and no matching `outcomeMessages` entry) toasts the new pack key `actions.completedNamed` (`Resolve completed`, zh `“解决”已完成`) instead of the generic `Action completed successfully`. An action with no label keeps the generic text. + - **Money metrics keep the currency's decimals.** A dataset metric over a currency measure with no `format`, and a static metric card with a `currency` and no `format`, show the currency's own minor-unit width (CNY `¥1,293.00`, JPY `¥1,293`), the width the list cell shows. Before, the dataset metric printed the shortest form (`¥1,293`, `¥1,280.5`) and the static card rounded to whole units. An authored `format` pattern still states its width. +- ff12aed: fix(core): a compareTo widget on a forward date range (next month, next quarter, next 30 days) compares with the period just before it, not with itself + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/core`: `shiftFilterByCompareTo`** with `kind: 'previousPeriod'` moved only `current_*` / `today` tokens, so a dashboard filtered to `next_month` or `next_quarter` compared the window with itself and the delta always read 0, and `next_30_days` (`{today}` … `{30_days_from_now}`) moved only its start and compared with a window that overlapped it on all but one day. A forward window now steps back one period: `{next_month_*}` becomes this month, `{next_quarter_*}` this quarter, and a `{today}` … `{N_days_from_now}` pair on one field becomes the N + 1 days that end yesterday (`{(N+1)_days_ago}` … `{yesterday}`), the same window the dataset executor computes for that range. Every other token, and the 13 backward presets, compare as before. + - **`@object-ui/core`: `compareToTrendLabelKey`** names that window: `vsThisMonth` for a forward month, `vsThisQuarter` for a forward quarter, and `vsPreviousPeriod` for a forward rolling window (it read `vsLastMonth`, `vsLastQuarter` and `vsYesterday`). + - **`@object-ui/i18n`:** `dashboard.trend.vsThisMonth` and `dashboard.trend.vsThisQuarter` in all ten locale packs (zh: 较本月, 较本季度). + - **`@object-ui/plugin-dashboard`, `@object-ui/plugin-charts`:** English fallbacks for the two keys in the metric tile and in the chart's comparison series label. +- f4d568b: fix(plugin-report,plugin-dashboard): a report, chart, table or cross-tab drill on a filtered measure lists the records the number counted + + Clause-②: no + Metadata-protocol: none + + A drill used to list the clicked bucket AND the report or widget filter only. The number also carries the dataset's own `filter` and the measure's own `filter`, so a drill on a filtered measure listed a superset: on the app-showcase Revenue Pulse dashboard, the Paid Invoices cell of the Sent row reads 0 and its drill listed all 4 sent invoices. The drill now also ANDs in the answer's `drillScopes` entry for the measure it stands for (the server's per-measure list filter; `{}` adds nothing, so a drill on an unfiltered measure is unchanged). + + - **What a click stands for.** A chart mark stands for its series' measure (a pivoted chart for its one measure), a cross-tab or matrix cell for its column's measure, and a table row for every measure it holds. + - **A table row whose measures select different records drills cell by cell.** When the row's measures all share one scope (one measure, or several plain ones), the row drills as before. Otherwise each measure cell with a scope is its own drill target (test id `dataset-drill-cell`) and the row is not. + - **No list that does not match the number.** A measure with no `drillScopes` entry (a derived measure, or a scope naming a relationship path) offers no drill, and neither does any measure of an answer that carries no `drillScopes`. A comparison (`__compare`) series opens nothing. A chart none of whose drawn measures has an entry gets no click handler. + - **Unchanged:** a report answer with no `object` (a server older than ADR-0021 D2) still hands the host its group-key fallback drill. +- Updated dependencies [4261169] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [1e6f542] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [b6caa14] +- Updated dependencies [25fea77] +- Updated dependencies [ea5807e] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [2046d21] +- Updated dependencies [fa9c0aa] +- Updated dependencies [5174603] +- Updated dependencies [f6faf75] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [7b10a30] +- Updated dependencies [8b0fbb8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [ad2393f] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [062236c] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [47c457b] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/core@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-dashboard/package.json b/ui/packages/plugin-dashboard/package.json index 987d5e309f4..d4dac9b2a2e 100644 --- a/ui/packages/plugin-dashboard/package.json +++ b/ui/packages/plugin-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-dashboard", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Dashboard plugin for ObjectUI", diff --git a/ui/packages/plugin-designer/CHANGELOG.md b/ui/packages/plugin-designer/CHANGELOG.md index da0c596c465..1dc49e19d5f 100644 --- a/ui/packages/plugin-designer/CHANGELOG.md +++ b/ui/packages/plugin-designer/CHANGELOG.md @@ -1,5 +1,135 @@ # @object-ui/plugin-designer +## 18.4.0 + +### Patch Changes + +- Updated dependencies [4261169] +- Updated dependencies [7a5f73e] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [1e6f542] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [83fc096] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [8c1bf5b] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [b6caa14] +- Updated dependencies [25fea77] +- Updated dependencies [ea5807e] +- Updated dependencies [4128213] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [f273c9f] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [2046d21] +- Updated dependencies [fa9c0aa] +- Updated dependencies [5174603] +- Updated dependencies [c580081] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [7b10a30] +- Updated dependencies [8b0fbb8] +- Updated dependencies [d3b2aaf] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [ad2393f] +- Updated dependencies [55d9dd4] +- Updated dependencies [6c30357] +- Updated dependencies [af8aab1] +- Updated dependencies [062236c] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [47c457b] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/core@18.4.0 + - @object-ui/data-objectstack@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/plugin-grid@18.4.0 + - @object-ui/layout@18.4.0 + - @object-ui/plugin-form@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/plugin-designer/package.json b/ui/packages/plugin-designer/package.json index 66406e4be04..f20be20e68f 100644 --- a/ui/packages/plugin-designer/package.json +++ b/ui/packages/plugin-designer/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-designer", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Visual designer plugin for ObjectUI with page, data model, process, and report designers.", diff --git a/ui/packages/plugin-detail/CHANGELOG.md b/ui/packages/plugin-detail/CHANGELOG.md index 7cd97ad8ea7..cb541e1a8c3 100644 --- a/ui/packages/plugin-detail/CHANGELOG.md +++ b/ui/packages/plugin-detail/CHANGELOG.md @@ -1,5 +1,292 @@ # @object-ui/plugin-detail +## 18.4.0 + +### Patch Changes + +- ffeda13: refactor(plugin-detail)!: retire twenty-one zero-consumer exports (record-page synthesis, layout, related-card and timeline helpers, one hook and three types) + + Clause-②: no (narrowing) + Metadata-protocol: none + + + + **BREAKING** for a host that imports any of these twenty-one names from `@object-ui/plugin-detail`. None had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected. Every module stays in the package and keeps its in-package readers (the record page synthesizer, `DetailView`, `RecordPageColumns`, the related cards and the timeline stream); only the entry stops publishing them. Everything else on the entry, including `buildDefaultPageSchema`, `buildDefaultTabs`, `isWideFieldType`, `InlineFieldInput`, `recordSitsInQueue`, `DefaultRecordPageContext`, `prefetchRelatedCounts`, `relatedCountTargetsIn`, `mergeTimelineStream`, `resolveRelatedArrangement` and the `RelatedArrangement` / `RecordPageTier` / `DefaultPageColumns` types, stays exported unchanged. + + What leaves, and what to write instead: + + - FROM `buildDefaultDiscussion`, `buildDefaultTimeline`, `deriveStages` TO `buildDefaultPageSchema`, which builds the whole default record page including its discussion, timeline and stage path. + - FROM `RECORD_PAGE_WIDE_MIN_WIDTH`, `recordPageColumnOfTab`, `RecordPageColumn` TO `recordPageTierFor` and `buildDefaultPageColumns`, which place the page's tabs into columns for a width. + - FROM `inferDetailColumns`, `applyAutoSpan`, `applyDetailAutoLayout` TO `DetailView`, which lays out its sections itself; `isWideFieldType` stays. + - FROM `extractLookupId`, `TEXTUAL_REF_FALLBACK_TYPES` TO `InlineFieldInput`, which reads lookup values itself. + - FROM `queueClaimAdmitted` TO `useQueueClaimOffered`, which answers whether the Claim button shows. + - FROM `useDefaultRecordPage` TO `DefaultRecordPageContext` with `useContext`. + - FROM `relatedCountTargetOf` TO `relatedCountTargetsIn`. + - FROM `filterTimelineStream` TO `mergeTimelineStream`; the timeline panel applies its filter chips itself. + - FROM `RELATED_CARD_NEW_BADGE_MS`, `editRelatedArrangement`, `relatedCardId`, `relatedDefaultOrderHash`, `RelatedArrangementEdit` TO `resolveRelatedArrangement` with `relatedArrangementFromRecord` / `relatedArrangementToRecord`; the related cards edit and badge the arrangement themselves. + - FROM `DetailTabsProps` TO `DetailViewProps`: `DetailTabs` is not on the entry, `DetailView` renders it. +- 6e68d8f: fix(plugin-detail): a record page's body tables total only the columns declared additive — a version number or an instalment number is no longer summed + + Clause-②: no + Metadata-protocol: none + + **Visible behaviour change on a released page.** The 「Totals (this page)」 line under a body table of the default record page (a related list drawn full width in the details column) used to add up every `currency`, `number`, `integer` and `decimal` column. Ordinals were summed with the quantities: a contract's versions v1–v4 read 「版本号 10」, five instalments 「期次 15」. The line now totals: + + - a `currency` column, when every counted row is in one known currency, formatted in it. Each row's currency is the one its cell displays (`resolveFieldCurrency`): the field's code under `currencyMode: 'fixed'`, the tenant default under `dynamic`, the row's own code field under `record`. Rows in different currencies, or a row whose currency is unknown (a `record`-mode row with a blank code, a `dynamic` amount with no tenant default), give that column no total rather than a bare or wrong-currency sum. A `currencyConfig` without `currencyMode: 'fixed'` no longer formats in its `defaultCurrency`, which the spec does not read outside `fixed`; it shows the tenant default, as the column's cells do; + - a `number`, `integer` or `decimal` column only when the record's own object declares a `summary` field whose `summaryOperations` sums that child field (`function: 'sum'`, `object` naming the child). The roll-up is the author's statement that the column adds up. Without one, as for an amount stored as `number` with its currency on the parent, the column gets no total; + - nothing else. When no column qualifies the line is not drawn. +- 3461420: fix(plugin-detail): an empty body table of the record page keeps its header (title, 0, New) with the empty sentence under it, instead of folding into one line that read the empty sentence before the table's title + + Clause-②: no + Metadata-protocol: none + + **Visible behaviour change on a released page.** The default record page draws a body table (a related list drawn full width in the details column, or the Details tab) beside its sibling sections. Because the tables share that container, the related-list empty fold took them for a Related tab stack: once one table had rows, an empty one left its place and reappeared as a single line after the last table, reading 「暂无相关记录 · 偏离 · + 新建」 (no related records · Deviations · New). A body table now never folds. An empty one keeps the header row its non-empty siblings draw (title, count 0, ⤢, New), and the empty sentence moves out of that row to the table's place under it. + + The fold of a record's Related tab, and the empty-panels line of the related column, are unchanged: only a list whose relationship is one of the page's body tables opts out. +- 9049be2: fix(plugin-detail): a record page's body table that is wider than the details column says so — its first column stays pinned and ⤢ is offered as 「All columns」 + + Clause-②: no + Metadata-protocol: none + + A body table of the default record page (a related list drawn in the details column) keeps its place in that column, as ADR-0085's role-based layout puts it. When its columns are wider than the column, it now makes the overflow obvious: + + - **The first column stays pinned** while the table scrolls sideways, so a scrolled row is still identifiable (the table's existing `frozenColumns: 1`, set only while the table overflows). + - **⤢ is offered in words.** The enlarge control in the card header, icon-only before, shows a visible 「All columns」 label (`detail.relatedCardAllColumns`, all ten locales) while the table overflows. It opens the same enlarged tab as before, where every column fits. + - **The right-edge shadow** while columns remain to the right is the table's own and unchanged; it clears when the table is scrolled to its end. + + A body table that fits shows none of these: no pinned column and the icon-only ⤢. + + `@object-ui/components`: a `data-table` header label carries its full text as a `title`, so a header the ellipsis cuts can be read on hover, and the table's scroll box carries a `data-table-scroll-box` marker a host can read the overflow from. +- 6db9198: fix(ui): a greyed action's `disabledReason` tooltip wraps inside a phone-width viewport instead of running off-screen (#29954) + + Clause-②: no + Metadata-protocol: none + + The record action bar, the `record:quick_actions` button and the schema-driven `action:*` controls (button, icon, group, menu, through the shared disabled-reason trigger) rendered the authored `disabledReason` in a tooltip with no width bound, so a long reason sat on one line and was cut at the right edge of a 390px viewport. All three tooltips now carry the same `max-w-xs` bound the home page's disabled-reason tooltip uses, so the reason wraps. +- df4c06e: fix(plugin-detail): the record page's activity feed names the record as the reader reads it, as the record's heading does, when the object is named by a formula + + Clause-②: no + Metadata-protocol: none + + An activity summary quotes the record's name as its writer read it: `sys_activity.record_label`, and the same text inside `summary`, are stored when the write happens. The server composes the rest of the summary in the reader's language on read, but not the name. For an object whose name field is a formula over `label(record.FIELD)`, that name is in the writer's language, so an en-US reader of a review a zh-CN user created read `Created Review "法务 · Legal Counsel 1"` while the heading above it read `Legal · Legal Counsel 1`. + + Each activity row of the page's own record now names the record by the value of the object's name field on the record the page read, which is the heading's name. The change is made where the feed is drawn (`RecordActivityTimeline`), so the activity panel, the 动态 stream and the 动态 panel all read it. Only the name the summary quotes (`"…"`, or `「…」` in Japanese) is replaced. + + What stays as stored: + + - an object whose name field is a stored field (text and the like): the name a row stored is the name the record had then, and a rename does not rewrite older rows; + - a row about another record, a row that carries no `record_label`, and a summary that does not quote the stored name; + - a page with no record around the feed, or whose record has no name to show. +- f68b47c: fix(plugin-detail): the related table leads with the record's name when the relation declares no columns + + Clause-②: no + Metadata-protocol: none + + A related list with no columns declared took its columns from the child object's `highlightFields`, which by design never list the record's title, so the related tab on a record page showed amounts and dates with no deal or contact name, and the only link in a row was its first value. The record's name (`nameField`, else the field the platform derives, ADR-0079) now leads that set, listed once even when a highlight names it, and the grid links that first column to the record. The field walk used when the child declares no `highlightFields` leads with the declared name the same way. The enlarged tab and the table in the record body read the same set. Columns an author declares on the relation are drawn exactly as declared. A `titleFormat`-only object leads with the field the platform derives, not the formatted title, which has no column behind it. +- 5f80129: fix(plugin-detail): a record page's related table offers Export only to a reader the list page offers it to, and an export refusal reads in the Console's language + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/plugin-detail`: the related table asks the list page's question.** The enlarged related list (the record page's active tab) drew Export whenever its data source could export. It now also reads the child object's effective API operation set (`/me/permissions` `apiOperations`) through `usePermissions()`, the source the list page's `exportPermitted` reads, and draws no Export when that set leaves `export` out. A missing set keeps the button, as on the list page. A reader who may export still exports this relation's rows alone. + - **Export refusals read in the Console's language.** When the export route still refuses (a 403, such as `EXPORT_NOT_PERMITTED`), the related table, the list page and the grid show the new `view.exportNotPermitted` sentence in the reader's language instead of the route's English text. Other export failures keep the server's sentence. + - **`@object-ui/i18n`: `view.exportNotPermitted`** in all ten locale packs. +- c1240a3: feat(record page): the timeline's Changes chip holds the audit's field changes only, and is offered only while the record shows its field history + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/plugin-detail`: what Changes holds.** The timeline stream's Changes chip, and the activity panel's `changes_only` filter, hold the field changes read from the audit log (each with its before and after values) and nothing else. The activity rows that share their look (created, updated, deleted, assigned, shared) are events, so they sit under Activity whether or not the record shows its field history; before, they sat under Changes and, on the panel, under `changes_only`. + - **`@object-ui/plugin-detail`: `RecordFieldHistoryContext`.** A host tells the record page's timeline stream whether it shows the record's field history; while it says `false`, the stream draws the All / Activity / Discussion chips and no Changes chip, and a stream left on Changes goes back to All. With no provider every chip draws, as before. `RecordActivityTimeline` takes the chips to offer as `streamFilters` (all four when absent). + - **The Console's record page** hands over its `enable.trackHistory` gate, so an object that does not opt in no longer offers a Changes chip on its timeline tab. + - **The CRM example** opens `enable.trackHistory` on `crm_lead`, `crm_account`, `crm_contact` and `crm_opportunity`, so their record pages show each field change with its before and after values. The flag stays opt-in for every other object. +- 5fa9375: feat(plugin-detail): the default record page lets an editor move a record to another stage by pressing it on the stage path + + Clause-②: no + Metadata-protocol: none + + **Visible behaviour change on a released page.** The record page the console synthesizes for an object with a stage field (no authored page) used to draw its stage path as a read-only progress rail, so moving a deal to its next stage took 编辑 → change the stage → 更新. That page's `record:path` now carries `allowStageChange: true`, the existing opt-in (no new metadata key): + + - **Who can press.** Unchanged from the opt-in: a reader the record page lets edit this record, on a stage field that is not read-only or computed, whose field edit grant holds for the stored row. Everyone else keeps the read-only path, and so does an editor while the record's inline edit holds unsaved changes. + - **Every press asks for confirmation**, then saves the stage through the record's ordinary update with the record's version, so the object's validation rules and `state_machine` decide. A refused move is shown under the path, naming any other field the move needs (for example a field made required by the new stage); nothing is written. + - **Only reachable stages are offered.** On every page with the opt-in, a stage the stage field's blocking `state_machine` cannot reach from the stored value is drawn as a readout, not a button, the same moves the record form, the grid's inline editor and the board offer. A field with no such rule offers every stage, and the server still judges each move. + - **Authored pages keep their own setting.** A page, or an authored `highlights` slot, that declares its own `record:path` keeps whatever it declares: `true`, `false`, or the key left out (a read-only rail). + + To keep a default record page's path read-only, assign the object an authored page (or a `highlights` slot) whose `record:path` leaves `allowStageChange` out or sets it `false`. +- ff3f83b: fix(record page): one action reads as one timeline row, and every row of the timeline panel names who acted + + Clause-②: no + Metadata-protocol: none + + - **One row per action.** When one action wrote both an activity row and an audit-log entry (same record, matching kind: create and created, update and updated, delete and deleted; same actor; stamped within `TIMELINE_PAIR_WINDOW_MS`, 5 seconds), the record page's timeline draws it once: the activity row keeps its actor and summary and shows the field changes, before and after, on the same row. A reader who cannot read the audit log sees the same single row without them. The Changes chip, and the panel's `changes_only`, still show each audit entry on its own. Rows that pair with nothing draw as before. + - **The 动态 panel's peek rows name the actor**, beside the time. + - The record page's 动态 count still comes from the server's `timeline` count, which the spec defines as an upper bound: for a record created with field history on it can still read one more than the rows drawn. +- 4128213: fix(console): a nav entry with no icon draws its object's icon, record field labels read in one style, a count chart fits a small count, and a page whose entry failed to load retries (#30126) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Navigation icons.** An app navigation entry of type `object` that names no `icon` now draws the icon its object declares, in the sidebar and in the `nav:menu` page block. Before, every such row drew the same generic page icon, so an app whose navigation names no icons showed one icon down the whole sidebar. The icon is read by object name from the metadata the console already holds, so no request is added and the navigation is not written to. An authored `icon` wins, and an object with no icon, a dashboard, page or report entry, and a host with no resolver keep the generic icon. `@object-ui/layout` exports `resolveNavItemIcon`, the types `NavIconTarget` and `NavTargetIconResolver`, and an optional `resolveTargetIcon` prop on `NavigationRenderer`, alongside the existing `resolveTargetLabel`. + - **Record field labels.** A record page's section field labels no longer carry an uppercase transform. Only Latin text took it, so in a zh console an untranslated label (`REPAIR COST`) read in capitals beside a translated one (`负责人`). Section labels now render as authored, in the style of the highlights strip above them. + - **Small count charts.** A count axis whose largest value is a small whole number (1 to 3) now tops out at that number, so a chart of one record draws its bar to the top of the axis instead of a quarter of the way up a 0–4 axis. An axis with an authored `min`, `max` or `stepSize`, fractional values, or a count of 4 or more draws as before. + - **Boot retry.** When the page's entry script fails to load before anything mounts (the server went away between serving the page and serving its entry, as during an `os dev` restart), the page now reloads itself, backing off from 1 second and doubling, at most five times per tab. Before, the loading splash stayed forever. The count is cleared on the first mount, and where `sessionStorage` cannot be read nothing retries. +- 5174603: fix(ui): Setup record pages name a sharing rule's recipient and read its condition in words, and the audit list names a sign-in row's session by its user + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/plugin-detail`.** The record page's highlights strip and details body hand each read face the record as `dependentValues`, as a list grid hands its row. A `recipient-picker` value is an id that means something only beside its `recipient_type`, so a sharing rule's page read the stored user id under 接收方 where its list read the user's name; it now reads the name. + - **`@object-ui/fields`.** `widget: 'filter-condition'` has a read face of its own: a list or detail cell draws the criteria through the widget's read-only display, which says it in the object's field and option labels ("Priority equals Urgent"; an OR group joined by its word, a boolean as a word). A criteria the builder cannot represent still reads as the stored JSON, and an empty one is the empty cell. A read-only widget no longer waits for its `object_name` to say anything: without it, the fields keep their api names. + - **`@object-ui/plugin-grid`.** A pointer cell (`referenceVia`) reads the target record with its name field expanded when that field is a reference the viewer may read, so a record named by a lookup reads as the referenced record's name: the audit list's sign-in rows name the session by its user instead of printing the user id. Any other name field is read as before, with no `$expand`. +- a91409c: fix(core,react,i18n,components,app-shell,console): a write refused because the object's external datasource does not accept writes says the table is read-only, not "you don't have permission" + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@object-ui/core`: `isExternalWriteRefusal(err)`**, re-exported from `@object-ui/react` beside the other write-refusal readers. True when the server refused a write with `EXTERNAL_WRITE_FORBIDDEN`, the ADR-0015 write gate's code for an object bound to an external datasource that does not admit writes (`datasource.external.allowWrites` and `object.external.writable` must both be true). Read from `code`, then `details.code`, like `isPermissionError`. The refusal is a 403, so `isPermissionError` answers true for it too: a caller asks this first. + - **`@object-ui/i18n`: `form.readOnlyExternalSource`**, in all ten packs: "This table is read-only. Its records come from an external data source that does not accept changes." + - **Every surface that met this refusal now says the table is read-only:** the console record form page and the record dialog (`@object-ui/components` form), which said "you don't have permission to save this record"; the record page delete, the list row delete and the record page inline edit, which showed the gate's raw diagnostic; and the calendar's drag-to-reschedule and the record path's stage change. A permission line sent an administrator through permission sets for a grant that cannot exist. +- cd7f785: fix(app-shell,plugin-detail): the record page offers its 变更 (Field changes) chip, and the activity panel its Field Changes option, only to a reader whose audit-log read answers + + Clause-②: no + Metadata-protocol: none + + On an object with `enable.trackHistory`, the record page's 动态 (Activity) tab offered the chips 全部 / 活动 / 讨论 / 变更 whenever `sys_audit_log` was registered, whatever this reader was allowed to read. A member the server refuses the audit read to (403) got a 变更 chip that always showed nothing, which reads as "nobody changed this record". The page now offers 变更 only once this reader's `sys_audit_log` read has answered. A refused or failed read offers no 变更 chip, exactly as history off does, and the chip joins the others when the read answers. Nothing else waits on the read: the page payload still asks the field changes as before, and the stream's count already counts a refused audit read as no field change. + + The activity panel's filter dropdown follows the same rule: it offers Field Changes (`changes_only`) only when the stream would offer its 变更 chip. A host that says nothing keeps every option, as before. A `changes_only` filter that is no longer offered reads as All Activity. +- ad2393f: fix(plugin-detail,app-shell): the related list, the reference rail and the packaged-actions table name a renamed object by the name the server serves + + Clause-②: yes (widening) + Metadata-protocol: none + + - **The surfaces.** A `record:related_list` block and a `record:reference_rail` card with no authored `title` are headed with the related object's served plural label, else its served label, as the server translated it for the session (a `.custom` extension's rename included). They used to ask only the client translation bundle, which still held the base package's name. The bundle is asked only when the server serves neither. + - **Packaged actions.** Setup → Packaged Actions reads the object list in the session's language (`Accept-Language`), not the browser's, and its Object column draws that served label as given. + - **The widening.** The served-object-label rule now lives in `@object-ui/core`, which `@object-ui/plugin-detail` can import: new exports `servedObjectLabel`, `servedObjectPluralLabel`, `metadataText` and the types `ObjectLabelLookup` and `ObjectPluralLabelLookup`. app-shell's internal copy is now a re-export of these, so both packages read one rule. +- 8a0516c: refactor(plugin-detail): the record timeline stream pairs audit entries with activity rows by the spec's `RECORD_TIMELINE_FOLDED_ACTIVITY_TYPES` + + Clause-②: no + Metadata-protocol: none + + The 动态 stream used to keep its own copy of the audit action to activity type pairs (`create`/`created`, `update`/`updated`, `delete`/`deleted`). It now reads `RECORD_TIMELINE_FOLDED_ACTIVITY_TYPES` from `@objectstack/spec/api`, the same set the server's `timeline` count steps aside, so the badge and the drawn rows cannot fold different types. No behaviour change: the same three pairs fold, and any other audit action (`restore`, an unknown action) pairs with no activity row. +- Updated dependencies [4261169] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [1e6f542] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [b6caa14] +- Updated dependencies [25fea77] +- Updated dependencies [ea5807e] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [2046d21] +- Updated dependencies [fa9c0aa] +- Updated dependencies [5174603] +- Updated dependencies [f6faf75] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [7b10a30] +- Updated dependencies [8b0fbb8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [ad2393f] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [062236c] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [47c457b] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/core@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-detail/package.json b/ui/packages/plugin-detail/package.json index 4f3abb16a13..82d4cadbdea 100644 --- a/ui/packages/plugin-detail/package.json +++ b/ui/packages/plugin-detail/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-detail", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "DetailView plugin for ObjectUI - comprehensive detail page with sections, tabs, and related lists", diff --git a/ui/packages/plugin-editor/CHANGELOG.md b/ui/packages/plugin-editor/CHANGELOG.md index 3676a47c9ff..629192f18e0 100644 --- a/ui/packages/plugin-editor/CHANGELOG.md +++ b/ui/packages/plugin-editor/CHANGELOG.md @@ -1,5 +1,29 @@ # @object-ui/plugin-editor +## 18.4.0 + +### Patch Changes + +- Updated dependencies [4261169] +- Updated dependencies [7a5f73e] +- Updated dependencies [c2be734] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [aa57c76] +- Updated dependencies [ea5807e] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [bff5ba2] +- Updated dependencies [31260ca] +- Updated dependencies [f5aaa0b] +- Updated dependencies [a91409c] +- Updated dependencies [8b0fbb8] +- Updated dependencies [ad2393f] + - @object-ui/core@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/components@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/plugin-editor/package.json b/ui/packages/plugin-editor/package.json index c722cf80a47..7bfdd759d0e 100644 --- a/ui/packages/plugin-editor/package.json +++ b/ui/packages/plugin-editor/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-editor", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Rich text editor plugin for ObjectUI, powered by Monaco Editor", diff --git a/ui/packages/plugin-form/CHANGELOG.md b/ui/packages/plugin-form/CHANGELOG.md index 1efaaa3f686..881a08acdb1 100644 --- a/ui/packages/plugin-form/CHANGELOG.md +++ b/ui/packages/plugin-form/CHANGELOG.md @@ -1,5 +1,153 @@ # @object-ui/plugin-form +## 18.4.0 + +### Patch Changes + +- c580081: fix(ui): the console form page Edit, the kanban drop prompt and the Edit wizard judge a field's `requiredWhen` as the engine does, a transition gate against the stored record + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/console` form page (`/forms/:name?recordId=`):** an Edit no longer refuses a record that already sits inside a `requiredWhen` gate without the value (for example a contract activated before its signed-copy rule existed). The field shows no required marker there, and an unrelated edit saves, as REST `PATCH` already accepts. Still refused: the edit that moves a record into the gate without the value, and clearing the value of a record that held it. A create form enforces as before. + - **`@object-ui/plugin-kanban` drop prompt:** dropping a card no longer asks for a field the engine lets rest. A record already inside the gate without the value moves between columns with no dialog; a drop that moves a compliant record into the gate still asks for the value first. + - **`@object-ui/plugin-form` Edit wizard:** the final submit's cross-step check no longer refuses a record that already sits inside a `requiredWhen` gate without the value; it asks `requiredWhenRestsOnStoredRecord` about the record as first read. The record reaches only that stored-record check, so a `visibleWhen` reading `previous` is still unevaluable at that check, as before. Moving a compliant record into the gate is still refused, and a Create wizard enforces as before. + + All three read the stored-record verdict `resolveFieldRuleState` already returns (`requiredWhenAtRest`, or its exported check `requiredWhenRestsOnStoredRecord`); no second rule. +- 8e5fabe: fix(app-shell): after Setup → Datasources syncs objects, the dialog links each new object and names the two steps left: add it to an app, and grant access + + Clause-②: yes (widening) + Metadata-protocol: none + + - **The result.** When a Sync run creates at least one object, the dialog stays open on the result instead of closing behind a toast: the count line, one link per created object (label, with the name beside it when they differ) that opens the object under the app the page is in, and the two steps left before people can use it, adding it to an app and granting access in a permission set. A run that creates nothing closes the dialog as before. Refused tables are still reported in the error toast. + - **The widening.** Four new `studio` namespace rows in `@object-ui/i18n` (en and zh): `engine.datasource.syncNextStepsOne`, `engine.datasource.syncNextSteps`, `engine.datasource.syncNextStepApp` and `engine.datasource.syncNextStepAccess`. + - **Form labels.** A declared field with no `label` is drawn in the form under its name with the first letter capitalised and underscores as spaces (`part_no` → `Part no`), the rule the list already draws it under, so one field reads the same in both. An authored label is unchanged. +- 8b0fbb8: fix(app-shell,fields,plugin-form,plugin-gantt,plugin-grid,components,console): record pages, forms, drawers and preview cards draw a served object label as served + + Clause-②: no + Metadata-protocol: none + + - **Now.** The record page (its title, breadcrumbs, related-list titles, delete and access-denied copy), the record form page and the console's create/edit dialog (title, save toast, way back to the list), the record drawer, the record preview card, the hover card over a lookup, a record-pointer cell, an object-ref cell, the closed-form notice, the gantt name column, the page header's record chrome, a form view's heading, the AI dock's surface chip, the Activity row and the report index name an object by the label the server served, as given. The server has already translated it, by its label order for an overridden item, so a `.custom` object extension's rename is kept over the base package's translation. Before, the served label reached the client's catalog lookup only as its fallback, so the base package's translation of the old name won on these surfaces after the list and header were fixed. + - **Unchanged.** When no label is served, the catalog key is asked first and the machine name (or its humanized form) stays the last resort, as before. An unedited object shows the same text as before, because the server serves the catalog's translation for it. Related lists and reference rails that only know an object's name, and the packaged-actions table (its object list is read without the session's language), still look the name up in the catalog. English is unchanged. +- Updated dependencies [4261169] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [1e6f542] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [b6caa14] +- Updated dependencies [25fea77] +- Updated dependencies [ea5807e] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [2046d21] +- Updated dependencies [fa9c0aa] +- Updated dependencies [5174603] +- Updated dependencies [f6faf75] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [7b10a30] +- Updated dependencies [8b0fbb8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [ad2393f] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [062236c] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [47c457b] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/core@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-form/package.json b/ui/packages/plugin-form/package.json index 84c462058cd..6681d3b29ec 100644 --- a/ui/packages/plugin-form/package.json +++ b/ui/packages/plugin-form/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-form", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Form plugin for ObjectUI", diff --git a/ui/packages/plugin-gantt/CHANGELOG.md b/ui/packages/plugin-gantt/CHANGELOG.md index 58bff763b13..a3e2be76555 100644 --- a/ui/packages/plugin-gantt/CHANGELOG.md +++ b/ui/packages/plugin-gantt/CHANGELOG.md @@ -1,5 +1,147 @@ # @object-ui/plugin-gantt +## 18.4.0 + +### Patch Changes + +- 8b0fbb8: fix(app-shell,fields,plugin-form,plugin-gantt,plugin-grid,components,console): record pages, forms, drawers and preview cards draw a served object label as served + + Clause-②: no + Metadata-protocol: none + + - **Now.** The record page (its title, breadcrumbs, related-list titles, delete and access-denied copy), the record form page and the console's create/edit dialog (title, save toast, way back to the list), the record drawer, the record preview card, the hover card over a lookup, a record-pointer cell, an object-ref cell, the closed-form notice, the gantt name column, the page header's record chrome, a form view's heading, the AI dock's surface chip, the Activity row and the report index name an object by the label the server served, as given. The server has already translated it, by its label order for an overridden item, so a `.custom` object extension's rename is kept over the base package's translation. Before, the served label reached the client's catalog lookup only as its fallback, so the base package's translation of the old name won on these surfaces after the list and header were fixed. + - **Unchanged.** When no label is served, the catalog key is asked first and the machine name (or its humanized form) stays the last resort, as before. An unedited object shows the same text as before, because the server serves the catalog's translation for it. Related lists and reference rails that only know an object's name, and the packaged-actions table (its object list is read without the session's language), still look the name up in the catalog. English is unchanged. +- Updated dependencies [4261169] +- Updated dependencies [8fefefd] +- Updated dependencies [ffeda13] +- Updated dependencies [7a5f73e] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [1e6f542] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [6e68d8f] +- Updated dependencies [3461420] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [df4c06e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [f68b47c] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [b6caa14] +- Updated dependencies [ff3f83b] +- Updated dependencies [25fea77] +- Updated dependencies [ea5807e] +- Updated dependencies [4128213] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [2046d21] +- Updated dependencies [fa9c0aa] +- Updated dependencies [5174603] +- Updated dependencies [f6faf75] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [29702d5] +- Updated dependencies [cd7f785] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [7b10a30] +- Updated dependencies [8b0fbb8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [ad2393f] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [062236c] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [47c457b] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [8a0516c] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/core@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-gantt/package.json b/ui/packages/plugin-gantt/package.json index 05ed88afdfa..e4e78b2171f 100644 --- a/ui/packages/plugin-gantt/package.json +++ b/ui/packages/plugin-gantt/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-gantt", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Gantt chart plugin for ObjectUI", diff --git a/ui/packages/plugin-grid/CHANGELOG.md b/ui/packages/plugin-grid/CHANGELOG.md index 779c445196e..3a0ccbc0348 100644 --- a/ui/packages/plugin-grid/CHANGELOG.md +++ b/ui/packages/plugin-grid/CHANGELOG.md @@ -1,5 +1,211 @@ # @object-ui/plugin-grid +## 18.4.0 + +### Minor Changes + +- f273c9f: fix(plugin-grid)!: a list's selection bar and row menu draw only actions the object declares; an undeclared bare `bulkActions` / `rowActions` name draws nothing and warns once in development (#30169) + + Clause-②: no (narrowing) — the grid stops drawing a bare `bulkActions` or legacy `rowActions` name that matches no action the object declares, which it drew before by humanized name. `packages/spec/src/ui/bulk-action.zod.ts` already says the member "names an action the object declares", and lint `action-name-undefined` (ADR-0049) refuses any other at authoring. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: no move — `skills/**` teaches no handler-only bulk or row action name (searched for `bulkActions` / `rowActions` examples and runner-handler text); `content/docs/ui/**` already names only declared actions in its `bulkActions` / `rowActions` examples. + + + + **BREAKING** for a host that registered a runner handler under a bare name the object does not declare (`ActionProvider handlers` / `runner.registerHandler('NAME', …)`) and listed that name in a view's `bulkActions` or `rowActions`. + + - **Before.** `resolveBulkActions` and `resolveLegacyRowActions` kept a name that matched no declared action, and `ObjectGrid` drew it by humanized name and dispatched it to the runner by name. A name nothing handled toasted "not executable" on click. This is how a list whose app had a package disabled kept a ghost button: the server withholds that package's actions from the served object, so their names matched nothing. + - **Now.** Such a name gets no selection-bar button and no row-menu entry, and in development the console logs one warning per list naming the key, the block and each name. The resolvers return those names as `undeclared` (renamed from `unresolved`), used only for that warning. The canonical `'delete'` (and the row menu's `'edit'` / `'delete'`) keep their own paths, unchanged. + - **Unchanged.** A name that matches a declared action is promoted to that action's def exactly as before, under the action's own label, icon, params, `visible` and `requiredPermissions`. + + **Remedy.** No rewrite is derived, because which fix an undeclared name needs is a judgement about the view: declare the named action on the object, or remove the name from `bulkActions` / `rowActions`. The development warning names each one. An action withheld by a disabled package needs nothing: it draws again when the package is enabled. +- d3b2aaf: fix(plugin-list)!: a list's selection bar over a non-grid view (kanban, calendar, gallery, …) draws only actions the object declares, by the grid's own rule; an undeclared bare `bulkActions` name draws nothing and warns once in development (#30394) + + Clause-②: yes (narrowing) — `@object-ui/plugin-grid` exports `resolveBulkActions` and `useUndeclaredActionNamesWarning` (with the `ActionLabelResolver`, `NamedActionDef`, `UndeclaredActionNameKey`, `UndeclaredActionNamesAddress` and `UndeclaredActionNamesBasis` types) so `@object-ui/plugin-list` reuses the grid's fold and warning instead of a second copy; the warning's address gains an optional `component`, defaulting to `ObjectGrid`, so the grid's message is byte-identical. + Metadata-protocol: narrowed + Skills-docs: no move — `skills/**` teaches no handler-only bulk action name (searched for `bulkActions` examples); `content/docs/ui/**` already names only declared actions. + + + + - **Before.** The bar ListView draws itself for every view but the grid mapped each `bulkActions` member straight to a button, labelled by the humanized name, whether or not the object declared it. A name the object does not declare (a misspelling, or an action a disabled package withholds from the served object) showed a button that toasted "not executable" on click. The grid stopped drawing such names in #30169. + - **Now.** The bar folds the names through the grid's `resolveBulkActions`: a name the object declares draws under that action's label (through the app's i18n bundle), the built-in `delete` keeps its own path and gate, and any other name draws nothing. In development the console logs one warning per list naming the key, the view and each name (`[ObjectUI] ListView bulkActions: kanban (objectName: …) — …`), the grid's own message. A bar with nothing left to draw is not shown. + - **Remedy.** Declare the named action on the object, or remove the name from `bulkActions`. An action withheld by a disabled package needs nothing: it draws again when the package is enabled. + +### Patch Changes + +- 8c1bf5b: fix(plugin-list): a stored view sort the platform refuses is no longer sent, so the list loads instead of failing; a column on a `sortable: false` field offers no sort when the grid sorts in the browser + + Clause-②: no + Metadata-protocol: none + + **Visible behaviour change on a released page.** A list view whose stored `sort` (or a restored user sort) names a field the platform will not order by used to send that sort with its data request. The Data API answers such a sort `400 INVALID_SORT`, so the list showed its load error instead of its rows. Fields the platform refuses are the ones its sortability projection, served with the object's metadata, marks unsortable: a `formula` field today, and a field marked `sortable: false` once the platform honours that flag. + + - **`@object-ui/plugin-list`:** the list's data request, its server export and the kanban, calendar, timeline and gantt views (which fetch their own rows) now send the sort without the refused entries. The entries left keep their original order, and a sort with nothing refused in it is sent unchanged. The refused entry is still listed in the sort picker as removable only, as before, and the next sort the list saves leaves it out. With no projection served (an older platform, inline rows), the sort is sent as before. + - **`@object-ui/plugin-grid`:** a column listed in a view's `columns` now reads its field's `sortable: false`, as columns derived from the object's fields already did. Where the grid sorts the rows it holds (inline rows), such a column no longer offers a sort click. A `formula` column keeps its sort click there, since the browser orders by the value the cell shows; where the sort is sent to the server, the platform's projection still decides, as before. +- 5f80129: fix(plugin-detail): a record page's related table offers Export only to a reader the list page offers it to, and an export refusal reads in the Console's language + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/plugin-detail`: the related table asks the list page's question.** The enlarged related list (the record page's active tab) drew Export whenever its data source could export. It now also reads the child object's effective API operation set (`/me/permissions` `apiOperations`) through `usePermissions()`, the source the list page's `exportPermitted` reads, and draws no Export when that set leaves `export` out. A missing set keeps the button, as on the list page. A reader who may export still exports this relation's rows alone. + - **Export refusals read in the Console's language.** When the export route still refuses (a 403, such as `EXPORT_NOT_PERMITTED`), the related table, the list page and the grid show the new `view.exportNotPermitted` sentence in the reader's language instead of the route's English text. Other export failures keep the server's sentence. + - **`@object-ui/i18n`: `view.exportNotPermitted`** in all ten locale packs. +- 5174603: fix(ui): Setup record pages name a sharing rule's recipient and read its condition in words, and the audit list names a sign-in row's session by its user + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/plugin-detail`.** The record page's highlights strip and details body hand each read face the record as `dependentValues`, as a list grid hands its row. A `recipient-picker` value is an id that means something only beside its `recipient_type`, so a sharing rule's page read the stored user id under 接收方 where its list read the user's name; it now reads the name. + - **`@object-ui/fields`.** `widget: 'filter-condition'` has a read face of its own: a list or detail cell draws the criteria through the widget's read-only display, which says it in the object's field and option labels ("Priority equals Urgent"; an OR group joined by its word, a boolean as a word). A criteria the builder cannot represent still reads as the stored JSON, and an empty one is the empty cell. A read-only widget no longer waits for its `object_name` to say anything: without it, the fields keep their api names. + - **`@object-ui/plugin-grid`.** A pointer cell (`referenceVia`) reads the target record with its name field expanded when that field is a reference the viewer may read, so a record named by a lookup reads as the referenced record's name: the audit list's sign-in rows name the session by its user instead of printing the user id. Any other name field is read as before, with no `$expand`. +- 8b0fbb8: fix(app-shell,fields,plugin-form,plugin-gantt,plugin-grid,components,console): record pages, forms, drawers and preview cards draw a served object label as served + + Clause-②: no + Metadata-protocol: none + + - **Now.** The record page (its title, breadcrumbs, related-list titles, delete and access-denied copy), the record form page and the console's create/edit dialog (title, save toast, way back to the list), the record drawer, the record preview card, the hover card over a lookup, a record-pointer cell, an object-ref cell, the closed-form notice, the gantt name column, the page header's record chrome, a form view's heading, the AI dock's surface chip, the Activity row and the report index name an object by the label the server served, as given. The server has already translated it, by its label order for an overridden item, so a `.custom` object extension's rename is kept over the base package's translation. Before, the served label reached the client's catalog lookup only as its fallback, so the base package's translation of the old name won on these surfaces after the list and header were fixed. + - **Unchanged.** When no label is served, the catalog key is asked first and the machine name (or its humanized form) stays the last resort, as before. An unedited object shows the same text as before, because the server serves the catalog's translation for it. Related lists and reference rails that only know an object's name, and the packaged-actions table (its object list is read without the session's language), still look the name up in the catalog. English is unchanged. +- 6c30357: fix(plugin-list): a list's selection bar over a non-grid view (kanban, calendar, gallery, …) draws a declared bulk action only for a user who holds its `requiredPermissions` and only when its `visible` admits a selected record, through the grid bar's own gate (#30535) + + Clause-②: yes (widening) — `@object-ui/plugin-grid` exports `useBulkActionItems` (with the `BulkActionItems` and `LegacyBulkActionItem` types), the grid selection bar's gate, so `@object-ui/plugin-list` reuses it instead of a second copy. + Metadata-protocol: none + + - **Before.** The bar ListView draws itself for every view but the grid folded `bulkActions` through the grid's `resolveBulkActions` and kept only each action's label. An action declaring `requiredPermissions` drew for every user, and one declaring `visible` drew for every selection, while the grid's bar hid both. A name repeated in `bulkActions` drew two buttons under one React key. + - **Now.** The bar passes the resolved actions through `useBulkActionItems`, the hook the grid's bar and its right-click menu read: an action whose `requiredPermissions` the user does not hold is not drawn (unknown capabilities fail open, as on the grid), an action switched off is not drawn, and `visible` is evaluated once per selected record, drawing the action when at least one selected record passes. A repeated name draws one button. The development warning for an object whose schema could not be read now names the component it came from (`ListView` or `ObjectGrid`) instead of "the grid". + - **Remedy.** Nothing to change in metadata. A user who should see a gated action needs the capability it declares. +- Updated dependencies [4261169] +- Updated dependencies [8fefefd] +- Updated dependencies [ffeda13] +- Updated dependencies [7a5f73e] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [a4c3e73] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [1e6f542] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [6e68d8f] +- Updated dependencies [3461420] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [df4c06e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [f68b47c] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [b6caa14] +- Updated dependencies [ff3f83b] +- Updated dependencies [25fea77] +- Updated dependencies [ea5807e] +- Updated dependencies [4128213] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [2046d21] +- Updated dependencies [fa9c0aa] +- Updated dependencies [5174603] +- Updated dependencies [f6faf75] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [29702d5] +- Updated dependencies [cd7f785] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [7b10a30] +- Updated dependencies [8b0fbb8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [ad2393f] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [062236c] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [47c457b] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [8a0516c] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/core@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @object-ui/mobile@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-grid/package.json b/ui/packages/plugin-grid/package.json index 9daa9379e40..9befa2d576f 100644 --- a/ui/packages/plugin-grid/package.json +++ b/ui/packages/plugin-grid/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-grid", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Grid plugin for ObjectUI", diff --git a/ui/packages/plugin-kanban/CHANGELOG.md b/ui/packages/plugin-kanban/CHANGELOG.md index f2f92f119f2..49d562542b6 100644 --- a/ui/packages/plugin-kanban/CHANGELOG.md +++ b/ui/packages/plugin-kanban/CHANGELOG.md @@ -1,5 +1,98 @@ # @object-ui/plugin-kanban +## 18.4.0 + +### Patch Changes + +- c580081: fix(ui): the console form page Edit, the kanban drop prompt and the Edit wizard judge a field's `requiredWhen` as the engine does, a transition gate against the stored record + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/console` form page (`/forms/:name?recordId=`):** an Edit no longer refuses a record that already sits inside a `requiredWhen` gate without the value (for example a contract activated before its signed-copy rule existed). The field shows no required marker there, and an unrelated edit saves, as REST `PATCH` already accepts. Still refused: the edit that moves a record into the gate without the value, and clearing the value of a record that held it. A create form enforces as before. + - **`@object-ui/plugin-kanban` drop prompt:** dropping a card no longer asks for a field the engine lets rest. A record already inside the gate without the value moves between columns with no dialog; a drop that moves a compliant record into the gate still asks for the value first. + - **`@object-ui/plugin-form` Edit wizard:** the final submit's cross-step check no longer refuses a record that already sits inside a `requiredWhen` gate without the value; it asks `requiredWhenRestsOnStoredRecord` about the record as first read. The record reaches only that stored-record check, so a `visibleWhen` reading `previous` is still unevaluable at that check, as before. Moving a compliant record into the gate is still refused, and a Create wizard enforces as before. + + All three read the stored-record verdict `resolveFieldRuleState` already returns (`requiredWhenAtRest`, or its exported check `requiredWhenRestsOnStoredRecord`); no second rule. +- 4ccd790: Clause-②: no + Metadata-protocol: none + + A kanban card move refused because the object's external datasource does not admit writes (`403 EXTERNAL_WRITE_FORBIDDEN`, ADR-0015) now says the table is read-only (`form.readOnlyExternalSource`) instead of "You are not authorized to perform this action.", which sent administrators looking through permission sets that can never unlock the table. The read-only check runs after an author-marked `userMessage` and before the permission check, the order the other write doors use; every other 403 keeps today's text. +- Updated dependencies [4261169] +- Updated dependencies [8fefefd] +- Updated dependencies [ffeda13] +- Updated dependencies [7a5f73e] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [1e6f542] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [c2be734] +- Updated dependencies [6e68d8f] +- Updated dependencies [3461420] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [df4c06e] +- Updated dependencies [f68b47c] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [b6caa14] +- Updated dependencies [ff3f83b] +- Updated dependencies [ea5807e] +- Updated dependencies [4128213] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [bff5ba2] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [fa9c0aa] +- Updated dependencies [5174603] +- Updated dependencies [f6faf75] +- Updated dependencies [f5aaa0b] +- Updated dependencies [77c203a] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [cd7f785] +- Updated dependencies [7b10a30] +- Updated dependencies [8b0fbb8] +- Updated dependencies [ad2393f] +- Updated dependencies [062236c] +- Updated dependencies [47c457b] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [8a0516c] + - @object-ui/core@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-kanban/package.json b/ui/packages/plugin-kanban/package.json index 8eed708a77b..8977523110e 100644 --- a/ui/packages/plugin-kanban/package.json +++ b/ui/packages/plugin-kanban/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-kanban", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Kanban board plugin for ObjectUI, powered by dnd-kit", diff --git a/ui/packages/plugin-list/CHANGELOG.md b/ui/packages/plugin-list/CHANGELOG.md index 1e2fe510dc4..e6050ff2a4d 100644 --- a/ui/packages/plugin-list/CHANGELOG.md +++ b/ui/packages/plugin-list/CHANGELOG.md @@ -1,5 +1,240 @@ # @object-ui/plugin-list +## 18.4.0 + +### Minor Changes + +- d3b2aaf: fix(plugin-list)!: a list's selection bar over a non-grid view (kanban, calendar, gallery, …) draws only actions the object declares, by the grid's own rule; an undeclared bare `bulkActions` name draws nothing and warns once in development (#30394) + + Clause-②: yes (narrowing) — `@object-ui/plugin-grid` exports `resolveBulkActions` and `useUndeclaredActionNamesWarning` (with the `ActionLabelResolver`, `NamedActionDef`, `UndeclaredActionNameKey`, `UndeclaredActionNamesAddress` and `UndeclaredActionNamesBasis` types) so `@object-ui/plugin-list` reuses the grid's fold and warning instead of a second copy; the warning's address gains an optional `component`, defaulting to `ObjectGrid`, so the grid's message is byte-identical. + Metadata-protocol: narrowed + Skills-docs: no move — `skills/**` teaches no handler-only bulk action name (searched for `bulkActions` examples); `content/docs/ui/**` already names only declared actions. + + + + - **Before.** The bar ListView draws itself for every view but the grid mapped each `bulkActions` member straight to a button, labelled by the humanized name, whether or not the object declared it. A name the object does not declare (a misspelling, or an action a disabled package withholds from the served object) showed a button that toasted "not executable" on click. The grid stopped drawing such names in #30169. + - **Now.** The bar folds the names through the grid's `resolveBulkActions`: a name the object declares draws under that action's label (through the app's i18n bundle), the built-in `delete` keeps its own path and gate, and any other name draws nothing. In development the console logs one warning per list naming the key, the view and each name (`[ObjectUI] ListView bulkActions: kanban (objectName: …) — …`), the grid's own message. A bar with nothing left to draw is not shown. + - **Remedy.** Declare the named action on the object, or remove the name from `bulkActions`. An action withheld by a disabled package needs nothing: it draws again when the package is enabled. + +### Patch Changes + +- 48b3ae6: refactor(plugin-list)!: retire three zero-consumer exports (the view switcher dropdown, the tab bar select and the conditional formatting wrapper) + + Clause-②: no (narrowing) + Metadata-protocol: none + + + + **BREAKING** for a host that imports any of these three names from `@object-ui/plugin-list`. None had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected. Every module stays in the package (`ListView` keeps rendering `ViewSwitcherDropdown`); only the entry stops publishing them. `ListView`, `ViewSwitcher`, `TabBar`, `UserFilters`, `normalizeFilterCondition`, `normalizeFilters`, `LIST_DEFAULT_TRANSLATIONS` and every exported type, `TabBarProps` included, stay exported unchanged. + + What leaves, and what to write instead: + + - FROM `ViewSwitcherDropdown` TO `ViewSwitcher`, or render `ListView`, which draws the dropdown itself. + - FROM `TabBarSelect` TO `TabBar`, which takes the same `TabBarProps`. + - FROM `evaluateConditionalFormatting` TO `resolveConditionalFormatting` from `@object-ui/core`, the function it wrapped: same arguments, result typed as `React.CSSProperties`. +- 8c1bf5b: fix(plugin-list): a stored view sort the platform refuses is no longer sent, so the list loads instead of failing; a column on a `sortable: false` field offers no sort when the grid sorts in the browser + + Clause-②: no + Metadata-protocol: none + + **Visible behaviour change on a released page.** A list view whose stored `sort` (or a restored user sort) names a field the platform will not order by used to send that sort with its data request. The Data API answers such a sort `400 INVALID_SORT`, so the list showed its load error instead of its rows. Fields the platform refuses are the ones its sortability projection, served with the object's metadata, marks unsortable: a `formula` field today, and a field marked `sortable: false` once the platform honours that flag. + + - **`@object-ui/plugin-list`:** the list's data request, its server export and the kanban, calendar, timeline and gantt views (which fetch their own rows) now send the sort without the refused entries. The entries left keep their original order, and a sort with nothing refused in it is sent unchanged. The refused entry is still listed in the sort picker as removable only, as before, and the next sort the list saves leaves it out. With no projection served (an older platform, inline rows), the sort is sent as before. + - **`@object-ui/plugin-grid`:** a column listed in a view's `columns` now reads its field's `sortable: false`, as columns derived from the object's fields already did. Where the grid sorts the rows it holds (inline rows), such a column no longer offers a sort click. A `formula` column keeps its sort click there, since the browser orders by the value the cell shows; where the sort is sent to the server, the platform's projection still decides, as before. +- 75a5c3e: fix(plugin-list): the list's sort picker no longer offers a field the object marks hidden, such as the queue column or the search index + + Clause-②: no + Metadata-protocol: none + + **Visible behaviour change on a released page.** A list's 排序 / Sort picker offered every field of the object, including the injected system columns the platform marks `hidden: true` (the queue column, the `__search` companion shown as an untranslated "Search Index", `organization_id`). The Filter panel's field list already left those out; the sort picker now follows the same rule. + + - A field the object definition marks `hidden: true` is no longer listed in the sort picker, and "Add sort" never starts on one. + - A view or saved sort that already orders by such a field keeps working: the field stays listed in that row as removable only (disabled), as the picker already does for an unreadable or refused field, so the row is not blank and can be removed. + - A hidden lookup (such as `organization_id`) no longer raises the "relational fields cannot be sorted" hint under the rows, since the picker never offered it. +- 5f80129: fix(plugin-detail): a record page's related table offers Export only to a reader the list page offers it to, and an export refusal reads in the Console's language + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/plugin-detail`: the related table asks the list page's question.** The enlarged related list (the record page's active tab) drew Export whenever its data source could export. It now also reads the child object's effective API operation set (`/me/permissions` `apiOperations`) through `usePermissions()`, the source the list page's `exportPermitted` reads, and draws no Export when that set leaves `export` out. A missing set keeps the button, as on the list page. A reader who may export still exports this relation's rows alone. + - **Export refusals read in the Console's language.** When the export route still refuses (a 403, such as `EXPORT_NOT_PERMITTED`), the related table, the list page and the grid show the new `view.exportNotPermitted` sentence in the reader's language instead of the route's English text. Other export failures keep the server's sentence. + - **`@object-ui/i18n`: `view.exportNotPermitted`** in all ten locale packs. +- eb6199b: feat(app-shell,plugin-list): a personal view's own conditions are Filter panel rows, and 保存 writes the panel back into that view + + Clause-②: no + Metadata-protocol: none + + - **Opening one of the reader's own views (My views) shows its conditions as ordinary rows in the Filter panel**, with the active count on the Filter control. They are the list's filter while they are there: not applied under the list a second time, and not a personalization. A stored filter the panel cannot hold (an any-of group beside other conditions), or one naming a field the object no longer declares or the reader cannot read, stays applied under the list, as for any other view. + - **Editing those rows marks the list 已个性化, and the marker offers 保存 to that view** (for a reader allowed to update their views). 保存 replaces the row's `config` through the data API with the body Save as my view builds, so the stored `ListView` shape is unchanged and no key is added; the reader's temporary list state for the view is then cleared and the list opens on the saved view. Edits not saved stay the reader's temporary list state and are never written to the view. A panel the fold cannot write losslessly keeps the refusal Save as my view gives, and nothing is written. + - **Save as my view now clears the view it came from**: that view's stored list state (sort, filters, column layout, density, hidden fields) and cached search term go, and the address being left loses its `uf_*` params, so the source no longer reads filtered or 已个性化 after the save. + - Both saves read the Filter panel as the list reports it, not the address's copy, so an emptied panel saves as no filter. + - `@object-ui/plugin-list`: `ListView`'s host prop `viewDefaults` accepts `filters`, the view's own conditions for a host that shows them as panel rows; the `filters` personalization part is then judged against them. +- dfafae8: feat(plugin-list): a reader can add any field they may read to their own list from the Hide Fields popover (#30077) + + Clause-②: no + Metadata-protocol: none + + The Hide Fields popover on a Console object list (the toolbar's, and the Hide Fields section of the Display panel) listed only the columns the view already has, so a reader who wanted another field on their list had to ask an administrator to change the view for everyone. After the view's columns it now lists every other field of the object the reader may read, unticked: business fields first, then system fields such as Owner. A field the reader may not read is not offered, and neither is a field the object definition marks `hidden`. Ticking one adds it as a column of the reader's own list; unticking it takes it off again. + + The addition is the reader's personalization, stored beside their hidden fields in their `ui.list.` preference row (`perView..addedFields`), never written into the view: another user sees the view's columns, and "Reset to view" drops it with the rest of the reader's list. While it is stored the list's Personalized marker names "Column layout". An added column is fetched and drawn like a declared one, and an added lookup field is expanded in the list read as a declared lookup column is. + + A column the view declares with no label of its own (a bare `{ field }` or field name) is now listed in the Hide Fields popover (and in the Group and Row color pickers, which share its list) under the object definition's field label, the label the grid's header shows, instead of under its machine name (`expected_revenue` now reads Expected Revenue); a column's own label and a translation still come first. + + It is offered wherever a Hide Fields choice is stored: a view that declares `columns`, and a saved view. On a system view or fallback tab that declares no columns the popover lists the view's columns alone, as before. + + For a `ListView` host: two new host props, `addedFields` (the names among `schema.columns` the reader added) and `onAddedFieldsChange` (the door that stores them). Without `onAddedFieldsChange` the popover is exactly what it was. No metadata key and no spec change. +- 6c30357: fix(plugin-list): a list's selection bar over a non-grid view (kanban, calendar, gallery, …) draws a declared bulk action only for a user who holds its `requiredPermissions` and only when its `visible` admits a selected record, through the grid bar's own gate (#30535) + + Clause-②: yes (widening) — `@object-ui/plugin-grid` exports `useBulkActionItems` (with the `BulkActionItems` and `LegacyBulkActionItem` types), the grid selection bar's gate, so `@object-ui/plugin-list` reuses it instead of a second copy. + Metadata-protocol: none + + - **Before.** The bar ListView draws itself for every view but the grid folded `bulkActions` through the grid's `resolveBulkActions` and kept only each action's label. An action declaring `requiredPermissions` drew for every user, and one declaring `visible` drew for every selection, while the grid's bar hid both. A name repeated in `bulkActions` drew two buttons under one React key. + - **Now.** The bar passes the resolved actions through `useBulkActionItems`, the hook the grid's bar and its right-click menu read: an action whose `requiredPermissions` the user does not hold is not drawn (unknown capabilities fail open, as on the grid), an action switched off is not drawn, and `visible` is evaluated once per selected record, drawing the action when at least one selected record passes. A repeated name draws one button. The development warning for an object whose schema could not be read now names the component it came from (`ListView` or `ObjectGrid`) instead of "the grid". + - **Remedy.** Nothing to change in metadata. A user who should see a gated action needs the capability it declares. +- c23eafa: fix(plugin-list): the list's 在队列中 ("In a queue") filter is no longer offered on an external (federated) object + + Clause-②: no + Metadata-protocol: none + + An ADR-0015 `external` object is served with the platform's injected `queue` column like any `ownership: 'user'` object, but the remote database owns its schema, so the column has no storage behind it and no row of that table can ever sit in a queue. `objectHasOwnershipQueue` now also requires `platformProvisionsStorage(objectDef)` from `@objectstack/spec/data`, so the toggle appears only on objects whose storage the platform provisions. Ordinary `ownership: 'user'` objects are unchanged. +- Updated dependencies [4261169] +- Updated dependencies [8fefefd] +- Updated dependencies [ffeda13] +- Updated dependencies [7a5f73e] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [a4c3e73] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [1e6f542] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [6e68d8f] +- Updated dependencies [3461420] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [df4c06e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [8c1bf5b] +- Updated dependencies [f68b47c] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [b6caa14] +- Updated dependencies [ff3f83b] +- Updated dependencies [25fea77] +- Updated dependencies [ea5807e] +- Updated dependencies [4128213] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [f273c9f] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [2046d21] +- Updated dependencies [fa9c0aa] +- Updated dependencies [5174603] +- Updated dependencies [f6faf75] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [29702d5] +- Updated dependencies [cd7f785] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [7b10a30] +- Updated dependencies [8b0fbb8] +- Updated dependencies [d3b2aaf] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [ad2393f] +- Updated dependencies [55d9dd4] +- Updated dependencies [6c30357] +- Updated dependencies [af8aab1] +- Updated dependencies [062236c] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [47c457b] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [8a0516c] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/core@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @object-ui/mobile@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/plugin-grid@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-list/package.json b/ui/packages/plugin-list/package.json index 9be39467bcf..a6f463eebf8 100644 --- a/ui/packages/plugin-list/package.json +++ b/ui/packages/plugin-list/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-list", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "ListView plugin for ObjectUI - unified view component with view type switching", diff --git a/ui/packages/plugin-map/CHANGELOG.md b/ui/packages/plugin-map/CHANGELOG.md index 06d0a7be4be..5fb206f69a6 100644 --- a/ui/packages/plugin-map/CHANGELOG.md +++ b/ui/packages/plugin-map/CHANGELOG.md @@ -1,5 +1,46 @@ # @object-ui/plugin-map +## 18.4.0 + +### Patch Changes + +- Updated dependencies [4261169] +- Updated dependencies [ffeda13] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [c2be734] +- Updated dependencies [6e68d8f] +- Updated dependencies [3461420] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [df4c06e] +- Updated dependencies [f68b47c] +- Updated dependencies [5f80129] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [ff3f83b] +- Updated dependencies [ea5807e] +- Updated dependencies [4128213] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [bff5ba2] +- Updated dependencies [31260ca] +- Updated dependencies [5174603] +- Updated dependencies [f5aaa0b] +- Updated dependencies [a91409c] +- Updated dependencies [cd7f785] +- Updated dependencies [8b0fbb8] +- Updated dependencies [ad2393f] +- Updated dependencies [8a0516c] + - @object-ui/core@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-map/package.json b/ui/packages/plugin-map/package.json index 386e9798f8d..2f9137efaf6 100644 --- a/ui/packages/plugin-map/package.json +++ b/ui/packages/plugin-map/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-map", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Map visualization plugin for ObjectUI", diff --git a/ui/packages/plugin-markdown/CHANGELOG.md b/ui/packages/plugin-markdown/CHANGELOG.md index c0a167c7e69..40a936dcfc1 100644 --- a/ui/packages/plugin-markdown/CHANGELOG.md +++ b/ui/packages/plugin-markdown/CHANGELOG.md @@ -1,5 +1,29 @@ # @object-ui/plugin-markdown +## 18.4.0 + +### Patch Changes + +- Updated dependencies [4261169] +- Updated dependencies [7a5f73e] +- Updated dependencies [c2be734] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [aa57c76] +- Updated dependencies [ea5807e] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [bff5ba2] +- Updated dependencies [31260ca] +- Updated dependencies [f5aaa0b] +- Updated dependencies [a91409c] +- Updated dependencies [8b0fbb8] +- Updated dependencies [ad2393f] + - @object-ui/core@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/components@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/plugin-markdown/package.json b/ui/packages/plugin-markdown/package.json index 2ce1104486c..d49e0a4157e 100644 --- a/ui/packages/plugin-markdown/package.json +++ b/ui/packages/plugin-markdown/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-markdown", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Markdown rendering plugin for ObjectUI, powered by react-markdown", diff --git a/ui/packages/plugin-report/CHANGELOG.md b/ui/packages/plugin-report/CHANGELOG.md index 33151467155..4fdd24b3c83 100644 --- a/ui/packages/plugin-report/CHANGELOG.md +++ b/ui/packages/plugin-report/CHANGELOG.md @@ -1,5 +1,83 @@ # @object-ui/plugin-report +## 18.4.0 + +### Patch Changes + +- f4d568b: fix(plugin-report,plugin-dashboard): a report, chart, table or cross-tab drill on a filtered measure lists the records the number counted + + Clause-②: no + Metadata-protocol: none + + A drill used to list the clicked bucket AND the report or widget filter only. The number also carries the dataset's own `filter` and the measure's own `filter`, so a drill on a filtered measure listed a superset: on the app-showcase Revenue Pulse dashboard, the Paid Invoices cell of the Sent row reads 0 and its drill listed all 4 sent invoices. The drill now also ANDs in the answer's `drillScopes` entry for the measure it stands for (the server's per-measure list filter; `{}` adds nothing, so a drill on an unfiltered measure is unchanged). + + - **What a click stands for.** A chart mark stands for its series' measure (a pivoted chart for its one measure), a cross-tab or matrix cell for its column's measure, and a table row for every measure it holds. + - **A table row whose measures select different records drills cell by cell.** When the row's measures all share one scope (one measure, or several plain ones), the row drills as before. Otherwise each measure cell with a scope is its own drill target (test id `dataset-drill-cell`) and the row is not. + - **No list that does not match the number.** A measure with no `drillScopes` entry (a derived measure, or a scope naming a relationship path) offers no drill, and neither does any measure of an answer that carries no `drillScopes`. A comparison (`__compare`) series opens nothing. A chart none of whose drawn measures has an entry gets no click handler. + - **Unchanged:** a report answer with no `object` (a server older than ADR-0021 D2) still hands the host its group-key fallback drill. +- Updated dependencies [4261169] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [1e6f542] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [c2be734] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [aa57c76] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [b6caa14] +- Updated dependencies [ea5807e] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [bff5ba2] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [fa9c0aa] +- Updated dependencies [5174603] +- Updated dependencies [f6faf75] +- Updated dependencies [f5aaa0b] +- Updated dependencies [77c203a] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [7b10a30] +- Updated dependencies [8b0fbb8] +- Updated dependencies [ad2393f] +- Updated dependencies [062236c] +- Updated dependencies [47c457b] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] + - @object-ui/core@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/plugin-report/package.json b/ui/packages/plugin-report/package.json index 540a14f1eb6..455393501fb 100644 --- a/ui/packages/plugin-report/package.json +++ b/ui/packages/plugin-report/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-report", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "main": "dist/index.umd.cjs", "module": "dist/index.js", diff --git a/ui/packages/plugin-timeline/CHANGELOG.md b/ui/packages/plugin-timeline/CHANGELOG.md index 31bf822037f..9c0fb2b17b0 100644 --- a/ui/packages/plugin-timeline/CHANGELOG.md +++ b/ui/packages/plugin-timeline/CHANGELOG.md @@ -1,5 +1,138 @@ # @object-ui/plugin-timeline +## 18.4.0 + +### Patch Changes + +- Updated dependencies [4261169] +- Updated dependencies [ffeda13] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [a4c3e73] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [1e6f542] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [6e68d8f] +- Updated dependencies [3461420] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [df4c06e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [f68b47c] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [b6caa14] +- Updated dependencies [ff3f83b] +- Updated dependencies [25fea77] +- Updated dependencies [ea5807e] +- Updated dependencies [4128213] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [2046d21] +- Updated dependencies [fa9c0aa] +- Updated dependencies [5174603] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [29702d5] +- Updated dependencies [cd7f785] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [7b10a30] +- Updated dependencies [8b0fbb8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [ad2393f] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [062236c] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [47c457b] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [8a0516c] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/core@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @object-ui/mobile@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-timeline/package.json b/ui/packages/plugin-timeline/package.json index 0122a2aa2bd..a501238be19 100644 --- a/ui/packages/plugin-timeline/package.json +++ b/ui/packages/plugin-timeline/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-timeline", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Timeline component plugin for ObjectUI", diff --git a/ui/packages/plugin-tree/CHANGELOG.md b/ui/packages/plugin-tree/CHANGELOG.md index 3833621e780..a9396e42c65 100644 --- a/ui/packages/plugin-tree/CHANGELOG.md +++ b/ui/packages/plugin-tree/CHANGELOG.md @@ -1,5 +1,140 @@ # @object-ui/plugin-tree +## 18.4.0 + +### Patch Changes + +- Updated dependencies [4261169] +- Updated dependencies [8fefefd] +- Updated dependencies [ffeda13] +- Updated dependencies [7a5f73e] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [1e6f542] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [6e68d8f] +- Updated dependencies [3461420] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [df4c06e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [f68b47c] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [b6caa14] +- Updated dependencies [ff3f83b] +- Updated dependencies [25fea77] +- Updated dependencies [ea5807e] +- Updated dependencies [4128213] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [2046d21] +- Updated dependencies [fa9c0aa] +- Updated dependencies [5174603] +- Updated dependencies [f6faf75] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [29702d5] +- Updated dependencies [cd7f785] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [7b10a30] +- Updated dependencies [8b0fbb8] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [ad2393f] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [062236c] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [47c457b] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [8a0516c] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/core@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-tree/package.json b/ui/packages/plugin-tree/package.json index f37cfa20f80..58cbb0dd685 100644 --- a/ui/packages/plugin-tree/package.json +++ b/ui/packages/plugin-tree/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-tree", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Tree / tree-grid visualization plugin for ObjectUI", diff --git a/ui/packages/plugin-view/CHANGELOG.md b/ui/packages/plugin-view/CHANGELOG.md index ae8b254f55c..2b0cb9e55dd 100644 --- a/ui/packages/plugin-view/CHANGELOG.md +++ b/ui/packages/plugin-view/CHANGELOG.md @@ -1,5 +1,131 @@ # @object-ui/plugin-view +## 18.4.0 + +### Patch Changes + +- Updated dependencies [4261169] +- Updated dependencies [8fefefd] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [1e6f542] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [8c1bf5b] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [b6caa14] +- Updated dependencies [25fea77] +- Updated dependencies [ea5807e] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [f273c9f] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [2046d21] +- Updated dependencies [fa9c0aa] +- Updated dependencies [5174603] +- Updated dependencies [c580081] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [7b10a30] +- Updated dependencies [8b0fbb8] +- Updated dependencies [d3b2aaf] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [ad2393f] +- Updated dependencies [55d9dd4] +- Updated dependencies [6c30357] +- Updated dependencies [af8aab1] +- Updated dependencies [062236c] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [47c457b] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/core@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/plugin-grid@18.4.0 + - @object-ui/plugin-form@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-view/package.json b/ui/packages/plugin-view/package.json index ccbac4797c8..92406b62977 100644 --- a/ui/packages/plugin-view/package.json +++ b/ui/packages/plugin-view/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-view", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Object View plugin for ObjectUI", diff --git a/ui/packages/react-runtime/CHANGELOG.md b/ui/packages/react-runtime/CHANGELOG.md index c7d5fc9280e..928ad31f09c 100644 --- a/ui/packages/react-runtime/CHANGELOG.md +++ b/ui/packages/react-runtime/CHANGELOG.md @@ -1,5 +1,9 @@ # @object-ui/react-runtime +## 18.4.0 + +No changes in this release. + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/react-runtime/package.json b/ui/packages/react-runtime/package.json index bdc4428e119..06e3988cc88 100644 --- a/ui/packages/react-runtime/package.json +++ b/ui/packages/react-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/react-runtime", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "sideEffects": false, "license": "MIT", diff --git a/ui/packages/react/CHANGELOG.md b/ui/packages/react/CHANGELOG.md index 94e0a67b812..a277c1329cb 100644 --- a/ui/packages/react/CHANGELOG.md +++ b/ui/packages/react/CHANGELOG.md @@ -1,5 +1,153 @@ # @object-ui/react +## 18.4.0 + +### Patch Changes + +- 8fefefd: refactor(react)!: retire twenty zero-consumer exports, and delete the two motion hooks, two helpers and one type nothing reads + + Clause-②: no (narrowing) + Metadata-protocol: none + + + + **BREAKING** for a host that imports any of these twenty names from `@object-ui/react`. None had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected. + + Seven of them are **gone from the package, not only from the entry**, because nothing inside the package read them either: the `hooks/useReducedMotion.ts` module (`useReducedMotion`) and the `hooks/useViewTransition.ts` module (`useViewTransition`, `ViewTransitionConfig`, `ViewTransitionResult`) are deleted, and the functions `useHasPageVariables` and `getShortcutDescriptions` and the type `AnimationTriggerType` are deleted from their modules. + + The other thirteen stay in their modules for the code that reads them and only leave the entry: `formatUnresolvableVisibilityMessage` (the visibility reporter still formats with it), `evaluateConfigBagInScope` (`useConfigBagEvaluator` still calls it), `SUPPORTED_TRANSITION_PRESETS` / `SUPPORTED_EASING_FUNCTIONS` and the three `SUPPORTED_NOTIFICATION_*` sets (the spec-parity tests still hold them against the spec vocabularies, and `resolveNotificationPosition` still checks positions against its set), `useOptionalTheme`, `resolveNotificationPresentation` (`NotificationProvider` still calls it), the raw `HostMapStyleContext` / `RecordPagePreferencesContext` / `PrintPageContext` objects, and `NoDataSourcePanel` (`ElementDataSourceGate` still renders it). Every other name the entry exported stays exported unchanged, among them `reportUnresolvableVisibilityPredicate`, `UNRESOLVABLE_VISIBILITY_PREFIX`, `UNRESOLVABLE_ENABLEMENT_PREFIX`, `useAnimation`, `useTheme`, `useNotifications`, `NotificationProvider`, the `HostMapStyleProvider` / `RecordPagePreferencesProvider` / `PrintPageProvider` providers with their `useHostMapStyle` / `useRecordPagePreferences` / `usePrintPage` hooks, `usePageVariables`, `useKeyboardShortcuts` and `ElementDataSourceGate`. + + What leaves, and what to write instead: + + - FROM `useReducedMotion` TO `window.matchMedia('(prefers-reduced-motion: reduce)')` in your own hook; the package no longer carries one. + - FROM `useViewTransition`, `ViewTransitionConfig`, `ViewTransitionResult` TO `document.startViewTransition` called directly; the package no longer carries a wrapper. + - FROM `useHasPageVariables` TO `usePageVariables()`, which answers a frozen empty value outside a `PageVariablesProvider`. + - FROM `getShortcutDescriptions` TO a local map over your `KeyboardShortcutDefinition[]` (filter on `description` and `enabled !== false`). + - FROM `AnimationTriggerType` TO a local string union: no function in the package takes it. + - FROM `formatUnresolvableVisibilityMessage` TO `reportUnresolvableVisibilityPredicate`, which formats and reports in one call; filter its lines by `UNRESOLVABLE_VISIBILITY_PREFIX` / `UNRESOLVABLE_ENABLEMENT_PREFIX`. + - FROM `evaluateConfigBagInScope` TO `SchemaRenderer`, which evaluates a node's config bag itself. + - FROM `SUPPORTED_TRANSITION_PRESETS`, `SUPPORTED_EASING_FUNCTIONS` TO the `TransitionPresetType` / `EasingPreset` types, or the spec's `TransitionPresetSchema` / `EasingFunctionSchema` for a runtime list. + - FROM `useOptionalTheme` TO `useTheme` inside a `ThemeProvider`. + - FROM `resolveNotificationPresentation` TO `useNotificationsByPresentation(presentation)`, which returns the notifications a surface of that presentation renders. + - FROM `SUPPORTED_NOTIFICATION_DISPLAY_TYPES`, `SUPPORTED_NOTIFICATION_POSITIONS`, `SUPPORTED_NOTIFICATION_ACTION_VARIANTS` TO the `NotificationDisplayType` / `NotificationPositionValue` / `NotificationActionVariant` types, or `resolveNotificationPosition` to normalize a position. + - FROM `HostMapStyleContext`, `RecordPagePreferencesContext`, `PrintPageContext` TO their providers (`HostMapStyleProvider`, `RecordPagePreferencesProvider`, `PrintPageProvider`) and hooks (`useHostMapStyle`, `useRecordPagePreferences`, `usePrintPage`). + - FROM `NoDataSourcePanel` TO `ElementDataSourceGate`, which renders it when no data source resolves, or `noDataSourceMessage` for the text. +- a91409c: fix(core,react,i18n,components,app-shell,console): a write refused because the object's external datasource does not accept writes says the table is read-only, not "you don't have permission" + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@object-ui/core`: `isExternalWriteRefusal(err)`**, re-exported from `@object-ui/react` beside the other write-refusal readers. True when the server refused a write with `EXTERNAL_WRITE_FORBIDDEN`, the ADR-0015 write gate's code for an object bound to an external datasource that does not admit writes (`datasource.external.allowWrites` and `object.external.writable` must both be true). Read from `code`, then `details.code`, like `isPermissionError`. The refusal is a 403, so `isPermissionError` answers true for it too: a caller asks this first. + - **`@object-ui/i18n`: `form.readOnlyExternalSource`**, in all ten packs: "This table is read-only. Its records come from an external data source that does not accept changes." + - **Every surface that met this refusal now says the table is read-only:** the console record form page and the record dialog (`@object-ui/components` form), which said "you don't have permission to save this record"; the record page delete, the list row delete and the record page inline edit, which showed the gate's raw diagnostic; and the calendar's drag-to-reschedule and the record path's stage change. A permission line sent an administrator through permission sets for a grant that cannot exist. +- Updated dependencies [4261169] +- Updated dependencies [7a5f73e] +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [1e6f542] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [7945a3b] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [f0ad1f7] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [bc8945f] +- Updated dependencies [42afac7] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [0c7e95a] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [c8e6d38] +- Updated dependencies [d99cd3b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [b6caa14] +- Updated dependencies [25fea77] +- Updated dependencies [ea5807e] +- Updated dependencies [d3f23d2] +- Updated dependencies [ff12aed] +- Updated dependencies [6c48d48] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [2046d21] +- Updated dependencies [fa9c0aa] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [648089d] +- Updated dependencies [10ecafd] +- Updated dependencies [b6f4c0e] +- Updated dependencies [b4f4781] +- Updated dependencies [48231b0] +- Updated dependencies [0d54279] +- Updated dependencies [8e5fabe] +- Updated dependencies [ac56acf] +- Updated dependencies [a91409c] +- Updated dependencies [ab12a3b] +- Updated dependencies [2b35374] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [7b10a30] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [ad2393f] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [062236c] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [47c457b] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @object-ui/core@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/react/package.json b/ui/packages/react/package.json index 89e90a6ead9..49805483fdc 100644 --- a/ui/packages/react/package.json +++ b/ui/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/react", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "React bindings and SchemaRenderer component for ObjectUI", diff --git a/ui/packages/types/CHANGELOG.md b/ui/packages/types/CHANGELOG.md index 7c28540feb5..32ddfd4255d 100644 --- a/ui/packages/types/CHANGELOG.md +++ b/ui/packages/types/CHANGELOG.md @@ -1,5 +1,85 @@ # @object-ui/types +## 18.4.0 + +### Patch Changes + +- 7a5f73e: feat(types): `@object-ui/types` publishes the upload adapter contract, `UploadAdapter`, `UploadOptions` and `UploadResult` + + Clause-②: yes (widening) + Metadata-protocol: none + + The three interfaces the file, image and avatar widgets upload through, unchanged in shape from the `@object-ui/providers` package they leave. They live here, as `DataSource` does, because `@object-ui/fields` (whose `UploadProvider` reads an adapter) and `@object-ui/data-objectstack` (whose `createObjectStackUploadAdapter` builds one) do not depend on each other. A host that writes its own adapter types it from here. Nothing existing changes. +- a52761f: fix(console): an action another package places on `sys_user` at `record_header` / `record_more` draws again on the user's record page, in the drawer and on the full page (#29951) + + Clause-②: no + Metadata-protocol: none + + - **The header reads `placedActions: true`.** `page:header`'s `placedActions` (#30018) is now drawn by the console's header renderer (`PageHeaderActions` in `@object-ui/components`): `true` draws every action of the page's object whose `locations` include `record_header` or `record_more`, in declaration order, read from the object's metadata at render time, so an action an installed package adds to the object is drawn without editing the page. Omitted or `false`, an authored `actions` list is still the whole set (#11592). A `true` beside an authored list, which the spec refuses, draws none of the object's actions and is reported once in the console. + - **One placed-header reader.** `placedHeaderActions` (`@object-ui/types`) is the rule both headers read: the synthesized default record page's header in `RecordDetailView` and a `placedActions: true` header. The `page:header` registration publishes the `placedActions` input. + - **The platform record pages opt in.** `sys_user_detail` and `sys_organization_detail` (`@objectstack/platform-objects`) and `sys_approval_request_detail` (`@objectstack/plugin-approvals`) set `placedActions: true` in place of the id lists #27219 froze, which dropped every header action another package places on those objects. The tab strips are unchanged, and every action the lists named is placed at `record_header` / `record_more`, so each still draws. + - `@objectstack/spec`: the `placedActions` describe no longer opens with `[PLANNED — not yet rendered]`, and the `page` liveness record names the reader. +- Updated dependencies [8aa2e91] +- Updated dependencies [b90a6f7] +- Updated dependencies [bcdb1db] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [bd65cb6] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f3a52d3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [0c666c3] +- Updated dependencies [38717d3] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [fef212b] +- Updated dependencies [dc51811] +- Updated dependencies [b77c160] +- Updated dependencies [09720e2] +- Updated dependencies [25fea77] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [2046d21] +- Updated dependencies [d9d42d4] +- Updated dependencies [47226e6] +- Updated dependencies [c7d1367] +- Updated dependencies [f5aaa0b] +- Updated dependencies [0005fec] +- Updated dependencies [2d6402c] +- Updated dependencies [77c203a] +- Updated dependencies [8293c74] +- Updated dependencies [29702d5] +- Updated dependencies [a839020] +- Updated dependencies [a7b8876] +- Updated dependencies [4dde0ae] +- Updated dependencies [590724f] +- Updated dependencies [38d416f] +- Updated dependencies [d2e2f6f] +- Updated dependencies [b55d6ab] +- Updated dependencies [aad1603] +- Updated dependencies [55d9dd4] +- Updated dependencies [af8aab1] +- Updated dependencies [4bf9a2d] +- Updated dependencies [0c9aa82] +- Updated dependencies [4564d6f] +- Updated dependencies [f2ea193] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/types/package.json b/ui/packages/types/package.json index c6e9ef1abec..367107e95aa 100644 --- a/ui/packages/types/package.json +++ b/ui/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/types", - "version": "18.3.0", + "version": "18.4.0", "description": "Pure TypeScript type definitions for ObjectUI - The Protocol Layer", "type": "module", "sideEffects": false,